Coder Social home page Coder Social logo

discard's Introduction

Discard Test

Soft deletes for ActiveRecord done right.

What does this do?

A simple ActiveRecord mixin to add conventions for flagging records as discarded.

Installation

Add this line to your application's Gemfile:

gem 'discard', '~> 1.2'

And then execute:

$ bundle

Usage

Declare a record as discardable

Declare the record as being discardable

class Post < ActiveRecord::Base
  include Discard::Model
end

You can either generate a migration using:

rails generate migration add_discarded_at_to_posts discarded_at:datetime:index

or create one yourself like the one below:

class AddDiscardToPosts < ActiveRecord::Migration[5.0]
  def change
    add_column :posts, :discarded_at, :datetime
    add_index :posts, :discarded_at
  end
end

Discard a record

Post.all             # => [#<Post id: 1, ...>]
Post.kept            # => [#<Post id: 1, ...>]
Post.discarded       # => []

post = Post.first   # => #<Post id: 1, ...>
post.discard        # => true
post.discard!       # => Discard::RecordNotDiscarded: Failed to discard the record
post.discarded?     # => true
post.undiscarded?   # => false
post.kept?          # => false
post.discarded_at   # => 2017-04-18 18:49:49 -0700

Post.all             # => [#<Post id: 1, ...>]
Post.kept            # => []
Post.discarded       # => [#<Post id: 1, ...>]

From a controller

Controller actions need a small modification to discard records instead of deleting them. Just replace destroy with discard.

def destroy
  @post.discard
  redirect_to users_url, notice: "Post removed"
end

Undiscard a record

post = Post.first   # => #<Post id: 1, ...>
post.undiscard      # => true
post.undiscard!     # => Discard::RecordNotUndiscarded: Failed to undiscard the record
post.discarded_at   # => nil

From a controller

def update
  @post.undiscard
  redirect_to users_url, notice: "Post undiscarded"
end

Working with associations

Under paranoia, soft deleting a record will destroy any dependent: :destroy associations. Probably not what you want! This leads to all dependent records also needing to be acts_as_paranoid, which makes restoring awkward: paranoia handles this by restoring any records which have their deleted_at set to a similar timestamp. Also, it doesn't always make sense to mark these records as deleted, it depends on the application.

A better approach is to simply mark the one record as discarded, and use SQL joins to restrict finding these if that's desired.

For example, in a blog comment system, with Posts and Comments, you might want to discard the records independently. A user's comment history could include comments on deleted posts.

Post.kept # SELECT * FROM posts WHERE discarded_at IS NULL
Comment.kept # SELECT * FROM comments WHERE discarded_at IS NULL

Or you could decide that comments are dependent on their posts not being discarded. Just override the kept scope on the Comment model.

class Comment < ActiveRecord::Base
  belongs_to :post

  include Discard::Model
  scope :kept, -> { undiscarded.joins(:post).merge(Post.kept) }

  def kept?
    undiscarded? && post.kept?
  end
end

Comment.kept
# SELECT * FROM comments
#    INNER JOIN posts ON comments.post_id = posts.id
# WHERE
#    comments.discarded_at IS NULL AND
#       posts.discarded_at IS NULL

SQL databases are very good at this, and performance should not be an issue.

In both of these cases restoring either of these records will do right thing!

Default scope

It's usually undesirable to add a default scope. It will take more effort to work around and will cause more headaches. If you know you need a default scope, it's easy to add yourself ā¤.

class Post < ActiveRecord::Base
  include Discard::Model
  default_scope -> { kept }
end

Post.all                       # Only kept posts
Post.with_discarded            # All Posts
Post.with_discarded.discarded  # Only discarded posts

Custom column

If you're migrating from paranoia, you might want to continue using the same column.

class Post < ActiveRecord::Base
  include Discard::Model
  self.discard_column = :deleted_at
end

Callbacks

Callbacks can be run before, after, or around the discard and undiscard operations. A likely use is discarding or deleting associated records (but see "Working with associations" for an alternative).

class Comment < ActiveRecord::Base
  include Discard::Model
end

class Post < ActiveRecord::Base
  include Discard::Model

  has_many :comments

  after_discard do
    comments.discard_all
  end

  after_undiscard do
    comments.undiscard_all
  end
end

Warning: Please note that callbacks for save and update are run when discarding/undiscarding a record

Performance tuning

discard_all and undiscard_all is intended to behave like destroy_all which has callbacks, validations, and does one query per record. If performance is a big concern, you may consider replacing it with:

scope.update_all(discarded_at: Time.current) or scope.update_all(discarded_at: nil)

Working with Devise

A common use case is to apply discard to a User record. Even though a user has been discarded they can still login and continue their session. If you are using Devise and wish for discarded users to be unable to login and stop their session you can override Devise's method.

class User < ActiveRecord::Base
  def active_for_authentication?
    super && !discarded?
  end
end

Non-features

  • Special handling of AR counter cache columns - The counter cache counts the total number of records, both kept and discarded.
  • Recursive discards (like AR's dependent: destroy) - This can be avoided using queries (See "Working with associations") or emulated using callbacks.
  • Recursive restores - This concept is fundamentally broken, but not necessary if the recursive discards are avoided.

Extensions

Discard provides the smallest subset of soft-deletion features that we think are useful to all users of the gem. We welcome the addition of gems that work with Discard to provide additional features.

Why not paranoia or acts_as_paranoid?

I've worked with and have helped maintain paranoia for a while. I'm convinced it does the wrong thing for most cases.

Paranoia and acts_as_paranoid both attempt to emulate deletes by setting a column and adding a default scope on the model. This requires some ActiveRecord hackery, and leads to some surprising and awkward behaviour.

  • A default scope is added to hide soft-deleted records, which necessitates adding .with_deleted to associations or anywhere soft-deleted records should be found. šŸ˜ž
    • Adding belongs_to :child, -> { with_deleted } helps, but doesn't work for joins and eager-loading before Rails 5.2
  • delete is overridden (really_delete will actually delete the record) šŸ˜’
  • destroy is overridden (really_destroy will actually delete the record) šŸ˜”
  • dependent: :destroy associations are deleted when performing soft-destroys šŸ˜±
    • requiring any dependent records to also be acts_as_paranoid to avoid losing data. šŸ˜¬

There are some use cases where these behaviours make sense: if you really did want to almost delete the record. More often developers are just looking to hide some records, or mark them as inactive.

Discard takes a different approach. It doesn't override any ActiveRecord methods and instead simply provides convenience methods and scopes for discarding (hiding), restoring, and querying records.

You can find more information about the history and purpose of Discard in this blog post.

Development

After checking out the repo, run bin/setup to install dependencies. Then, run rake test to run the tests. You can also run bin/console for an interactive prompt that will allow you to experiment.

Contributing

Please consider filing an issue with the details of any features you'd like to see before implementing them. Discard is feature-complete and we are only interested in adding additional features that won't require substantial maintenance burden and that will benefit all users of the gem. We encourage anyone that needs additional or different behaviour to either create their own gem that builds off of discard or implement a new package with the different behaviour.

Discard is very simple and we like it that way. Creating your own clone or fork with slightly different behaviour may not be that much work!

If you find a bug in discard, please report it! We try to keep up with any issues and keep the gem running smoothly for everyone! You can report issues here.

License

The gem is available as open source under the terms of the MIT License.

Acknowledgments

  • Ben Morgan who has done a great job maintaining paranoia
  • Ryan Bigg, the original author of paranoia (and many things), as a simpler replacement of acts_as_paranoid
  • All paranoia users and contributors

discard's People

Contributors

adamrdavid avatar archonic avatar aried3r avatar benemdon avatar bughuntsman avatar chbonser avatar chiperific avatar dependabot[bot] avatar itsnickbarry avatar jarednorman avatar jhawthorn avatar joeljuca avatar lylo avatar m-nakamura145 avatar maxlap avatar nunosilva800 avatar olleolleolle avatar petergoldstein avatar robotdana avatar w8m8 avatar wacko avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

discard's Issues

Mongoid support

I'm trying to use this gem with https://github.com/mongodb/mongoid and a few small changes are needed:

  • changing the activerecord dependency to activesupport (there doesn't seem to be any ActiveRecord dependency anyway)
  • add Mongoid field field discard_column, type: Time in the included block of Discard::Model - needs to be done in a way that it doesn't break the ActiveRecord version
  • adjust the scopes to work with Mongoid

Possibly should be done as a separate gem in addition to discard?

guard on discard

Why not add a guard on discard method, like in aasm_state the option: guard. Guard option accept a method or a block. If can discard do the discard, unless raise an error. I don't know if there is now a feature like this, if there is please explain me it. Thank you ! :)

Bang in the methods discard and undiscard

Hey. Congratulations for the great job. I've started to use it today and seems to be a useful tool.

But the methods "discard" and "undiscard", following the rails patterns, should not have a bang, since it changes the database?

discard!
undiscard!

`def undiscard` doesn't follow `return false` pattern

Describe the bug
https://github.com/jhawthorn/discard/blob/master/lib/discard/model.rb#L141

def undiscard seems to violate its expected return value constraints.

    # Discard the record in the database
    #
    # @return [Boolean] true if successful, otherwise false
    def discard
      return false if discarded?
      run_callbacks(:discard) do
        update_attribute(self.class.discard_column, Time.current)
      end
    end

    ...

    # Undiscard the record in the database
    #
    # @return [Boolean] true if successful, otherwise false
    def undiscard
      return unless discarded? # <-------- returns nil
      run_callbacks(:undiscard) do
        update_attribute(self.class.discard_column, nil)
      end
    end

Shouldn't this be return false unless discarded??

Current behavior

> record = SomeModel.first
> record.discarded?
=> false
> record.undiscard # should return false
nil

Expected behavior

> record = SomeModel.first
> record.discarded?
=> false
> record.undiscard # should return false
false

Additional context

  • Ruby 3.1.4
  • Rails 7.0.7

This gem doesn't do much

Moving from paranoia, I find this gem difficult to swallow. All it appears to do is provide a naming convention around a discarded_at timestamp and a few small helpers.

The readme doesn't even mention that you have to replace a lot of references throughout your app to get the "soft delete" type behavior you'd expect (migrating from Paranoia or ActsAsParanoid).

For example, even a simple Post.find needs to be replaced with Post.kept.find.

I get that this approach is useful for a few reasons, like providing verbosity and explicitness throughout a Rails application (which is arguably against their values), or not doing anything magical or by mistake
but it doesn't seem like it has anything to do with soft-deletion, besides the naming convention.

Am I using this wrong or misunderstanding something?

Will this work when saving related models?

I'm considering this gem for a project, but have some hard time understanding how it would impact a set-up in which I'm using accept_nested_attributes for on a model. Could I overwrite the default behavior of that to use discard insstead of destroy when updating the join table?

Run callbacks inside of the same transaction

Describe the bug

Currently the callbacks such as after_discard are run outside of the discarded model's transaction. This can lead to unexpected behavior where, for example, the model is discarded but its' associations discarded in the after_discard callback are not.

discarded? value on validations and callbacks

I'm trying to understand an issue we have on Solidus: solidusio/solidus#3202.

These are two specs that describe the behavior we had with discard 1.0.0:

  describe 'discarded? value on callbacks/validations' do
    with_model :Post, scope: :all do
      table do |t|
        t.datetime :discarded_at
        t.timestamps null: false
      end

      model do
        include Discard::Model
        validate :validate_something, if: :discarded?
        before_discard :do_before_discard, if: :discarded?

        def validate_something; end
        def do_before_discard; end
      end
    end

    let!(:post) { Post.create! }

    it "runs validations with if: :discarded?" do
      expect(post).to receive(:validate_something)

      expect(post.discard).to be true
      expect(post).to be_discarded
    end

    it "does not run callbacks with if: :discarded?" do
      expect(post).to_not receive(:do_before_discard)

      expect(post.discard).to be true
      expect(post).to be_discarded
    end
  end

and in 1.1.0 the validation spec fails with:

1) Discard::Model discarded? value on callbacks runs validations with if: :discarded?
     Failure/Error: expect(post).to receive(:validate_something)

       (#<Post id: 1, discarded_at: "2019-05-08 10:52:56", created_at: "2019-05-08 10:52:56", updated_at: "2019-05-08 10:52:56">).validate_something(*(any args))
           expected: 1 time with any arguments
           received: 0 times with any arguments
     # ./spec/discard/model_spec.rb:24:in `block (3 levels) in <top (required)>'

so the validations are no more called if you use if: :discarded?, differently from the previous version.

I think version 1.1.0 is more consistent with what happens for callbacks, like before_save, since in that case they are not called either.

Not sure if there is a specific reason behind that and I'm not sure if this could be considered a regression since this specific scenario is not documented anywhere.

How forget password token should be linked with non-discarded record

There are 2 users present with same email ([email protected]) in users table. First one is discarded while other is Active(Not Discarded). Now when User sends forget password link, it sets reset_password_token to discarded User. as a result of this, User cant set his password

any idea how to tell devise password controller to attach reset_password_token to Active recored rather discard ?

1

2

NoMethodError (undefined method `undiscarded?' ...

Describe the bug
When using the readme's proposed code for handling associations, I get this error when calling .kept on a record:

undefined method `undiscarded?'`

Happens on both Discard 1.0 or 1.1.

(I am 99% sure this is some oversight on my part, but can't find it.)

To Reproduce

  • I have an Encounter model and an Appointment model.
  • Both have include Discard::Model as the first line in the model class code
  • Encounter belongs_to Appointment
class Encounter < ApplicationRecord
  include Discard::Model
  scope :kept, -> { undiscarded.joins(:appointment).merge(Appointment.kept) }

  def kept?
    undiscarded? && appointment.kept?
  end

  belongs_to :appointment, optional: true
  ...
end

Then in the Rails (5.2.3) console:
Encounter.first.kept?

Produces

NoMethodError (undefined method `undiscarded?' for #<Encounter:0x00007fc44fa9cb88>)
Did you mean?  undiscard
               discarded?
               undiscard!

has_many: use soft-delete when clearing associated collection

Given a has_many association:

class Big
  has_many :littles
end

class Little
  belongs_to :big
end

where the littles table has a discarded_at column, I'd like the following to discard the little records rather than nulling out their big_id or deleting them:

big.littles = []  # or equivalently, big.littles.clear

Is there a way to achieve this currently? So far I haven't found one.

after_discard_commit

Do you think a after_discard_commit makes sense, which is triggered just as after_destroy_commit ?
Reason is, that we have some queue triggers that should only be fired when the transactin was comitted not on rollbacks.

thx

`self.class.discard_column` as a boolean `is_discarded` for "if" and use `updated_at` for "when"

I gotta believe you already have heard/thought of this strategy and made a choice, but I couldn't find it, so I'm asking.

We use timestamps on all our models, as Rails convention expects.

Our Data Architect couldn't understand why we wouldn't just use a boolean is_discarded field and updated_at field to achieve the same result. From their perspective, a constrained "true/false" is easier to index and query than a "NULL/NOT NULL" especially when the NOT NULL values are timestamps.

Did Discard ever consider just a boolean is_discarded field paired with using the updated_at column approach? And if so, what took it out of consideration?

has_many only for undiscarded records?

Instead of using a default_scope, for a model record that has Discard enabled what would be the best way to scope a has_many call from an associated model, so that it only returns undiscarded records?

This could be a good example to add to the documentation.

Adding an config parameter to skip update/save hooks/validations

Picking up the issue brought up by @artemf from #4.

Although for most applications, running update hooks with the discard method should not have any catastrophic, unexpected side-effect, I think it is better to have the ability to choose here. Especially since it is not intuitive to expect 'update' hooks to be run for a method that more similar to 'destroy'. One possible solution is to use update_attribute instead of save but there may be more elegant methods.

Discarding on nested attributes doesn't trigger callbacks

Hey!

I read #7 (comment) which is the solution to this particular issue. But one thing that isn't mentioned is that this will not trigger after_discarded callbacks, if any.

I think paper_trail solved this by checking on after_update if any of the tracked attributes were changed, and if so, create a version, run callbacks etc.

Maybe this is a route this gem could also take?

Documentation for un/discard is not completely correct

# Discard the record in the database
#
# @return [Boolean] true if successful, otherwise false
def discard
return if discarded?

I think either these should return false if it's already discarded or the documentation should read "true if successful or a falsey value". Since the guard clause will make it return nil.

I can submit a PR if you want. Just let me know which option you like better. :)

Add section on README about integration with counter_culture gem

counter_culture automatically sets callbacks for paranoia events which are quite similar to discard. See this piece of code for reference. And here's what I'm currently doing:

class ApplicationRecord < ActiveRecord::Base
  include Discard::Model
  self.discard_column = :deleted_at

  def self.discardable
    default_scope -> { kept }
    if self.after_commit_counter_cache.present?
      after_discard :_update_counts_after_destroy
      after_undiscard :_update_counts_after_create
    end
  end
end

I think it's worth adding this to the README file.

declarative discard parents

rather than

scope :kept, -> { undiscarded.joins(:post).merge(Post.kept) }
scope :with_discarded, -> { unscope(where: :discarded_at).left_joins(:post).merge(Post.with_discarded) }
scope :discarded, -> { where.not(discarded: nil).left_joins(:post).or(left_joins(:post).merge(Post.discarded)) }

def transitively_discarded?
  discarded? || post.discarded?
end

etc etc

would you consider a PR that adds a mechanism to define discard parents that modifies the various scopes

discard_parents :post

or

belongs_to :post, discard_parent: true

or something

discarding on nested_attributes

Hello,

i have a little problem with accepts_nested_attributes_for, is there a standard workaround for discarding such "children" instead of destroying them?

Greets
Marcel

discard has_and_belongs_to_many association

What is the best way to soft delete has_and_belongs_to_many associations as there is no rails model exists for has_and_belongs_to_many association?

lets say there are 2 models

  class Participant < ApplicationRecord
    has_and_belongs_to_many :company_employees
   end

  class CompanyEmployee < ApplicationRecord
    has_and_belongs_to_many :participants
  end

This will create company_employees_participants middle/join table in database BUT without any model in Ruby on Rails. Where i can make settings for discard gem ?

any idea how i can solve this requirement where we need to have soft delete in joined tables using has_and_belongs_to_many

Discarding should skip validations (maybe)

A lot of times, validations to the model are added in retrospect. The usual action thereafter would be to discard the instances that are not validated. This doesn't work as expected since the lack of validation would prevent the instance from being shelved.
Undiscard should ideally not work when validations fail, but I am not sure discard should follow the validations rule.

Discard callbacks don't work with observers

Describe the bug
Calling #discard on an ActiveRecord object does not trigger any before_discard or after_discard callbacks defined in Rails observers.

To Reproduce
Steps to reproduce the behavior:

  1. Install the rails-observers gem
  2. Create an observer class for your ActiveRecord object
  3. Define a before_discard or after_discard callback in the observer
  4. Discard your ActiveRecord object
  5. The callback will not fire

Expected behavior
Discard callbacks should be usable in Rails observers. It would be relatively straightforward to hook into ActiveRecord::Observer and add the callbacks when rails-observers is detected. I'd be happy to submit a PR if this is something you would actually consider merging.

Additional context
Rails 5.2
Ruby 2.5.7
rails-observers 0.1.5
discard 1.1.0

Support for Rails 7.0.0

Describe the bug
Rails 7 has recently shipped and it looks like discard cannot be installed currently alongside it.

Bundler could not find compatible versions for gem "activerecord":
  In Gemfile:
    discard (>= 1.1) was resolved to 1.2.0, which depends on
      activerecord (>= 4.2, < 7)

    rails (~> 7.0.0) was resolved to 7.0.0, which depends on
      activerecord (= 7.0.0)

To Reproduce
Create a new rails 7.x app, add discard to the Gemfile and attempt to bundle.

You'll get the same error as above, where discard won't install alongside activerecord 7.

Expected behavior
The gem should be installable and work as intended on Rails 7.

Additional context
Rails 7.0.0, Ruby 3.0.3.

All new records are discarded use-case

I know that gem is feature-complete, but there is one possible use-case in the gem that I ran into -- and wanted to bring it up.

I had a need to have all new recorders created discarded. It's really easy to do actually:

class CreatePosts < ActiveRecord::Migration[5.0]
  def change
    create_table :posts do |t|
      t.datetime :discarded_at, default: -> { 'CURRENT_TIMESTAMP' }
    end
  end 
end

But I didn't find any way to actually overwrite this default.

Post.create(discarded_at: false)
Post.create(discarded_at:nil)

all these methods lead to the object being discarded in the end. It feels like there is some other obvious way that I'm missing here.

Maybe someone has idea?

Add the ability to use `dependent: :discard` in associations

Adds dependent: :discard as an option which allows

  1. destroying the main model but discarding the associated models
  2. assign an array to the association and discard the previously assigned models

Example:

class User < ApplicationRecord
  include Discard::Model
  has_many :posts, -> { kept }, dependent: :discard
  has_one :email, -> { kept }, dependent: :discard
end

user = User.create
post = user.posts.create
user.posts = [Post.new]
post.reload.discarded? # true

I currently have a config/initializers/discard.rb file which patches the associations to allow the dependent: :discard configuration:

module Discard
  module Dependent
    module HasManyAssociation
      extend ActiveSupport::Concern

      included do
        def handle_dependency_with_discard
          if options[:dependent] == :discard
            load_target.discard_all
          else
            handle_dependency_without_discard
          end
        end
        alias_method :handle_dependency_without_discard, :handle_dependency
        alias_method :handle_dependency, :handle_dependency_with_discard

        def delete_count_with_discard(method, scope)
          if method == :discard
            scope.discard_all
            0
          else
            delete_count_without_discard(method, scope)
          end
        end
        alias_method :delete_count_without_discard, :delete_count
        alias_method :delete_count, :delete_count_with_discard
      end
    end

    module HasOneAssociation
      extend ActiveSupport::Concern

      included do
        def handle_dependency_with_discard
          if options[:dependent] == :discard
            load_target.discard
          else
            handle_dependency_without_discard
          end
        end
        alias_method :handle_dependency_without_discard, :handle_dependency
        alias_method :handle_dependency, :handle_dependency_with_discard

        def remove_target_with_discard!(method)
          if method == :discard
            target.discard
          else
            remove_target_without_discard!(method)
          end
        end
        alias_method :remove_target_without_discard!, :remove_target!
        alias_method :remove_target!, :remove_target_with_discard!
      end
    end

    module BelongsToAssociation
      extend ActiveSupport::Concern

      included do
        def handle_dependency_with_discard
          if options[:dependent] == :discard
            target.discard
          else
            handle_dependency_without_discard
          end
        end
        alias_method :handle_dependency_without_discard, :handle_dependency
        alias_method :handle_dependency, :handle_dependency_with_discard
      end
    end

    module ValidDependentOptions
      extend ActiveSupport::Concern

      class_methods do
        def valid_dependent_options_with_discard
          valid_dependent_options_without_discard + [:discard]
        end
      end

      included do |base|
        class << base
          alias_method :valid_dependent_options_without_discard, :valid_dependent_options
          alias_method :valid_dependent_options, :valid_dependent_options_with_discard
        end
      end
    end
  end
end

ActiveRecord::Associations::HasManyAssociation.include Discard::Dependent::HasManyAssociation
ActiveRecord::Associations::HasOneAssociation.include Discard::Dependent::HasOneAssociation
ActiveRecord::Associations::BelongsToAssociation.include Discard::Dependent::BelongsToAssociation
ActiveRecord::Associations::Builder::HasMany.include Discard::Dependent::ValidDependentOptions
ActiveRecord::Associations::Builder::HasOne.include Discard::Dependent::ValidDependentOptions
ActiveRecord::Associations::Builder::BelongsTo.include Discard::Dependent::ValidDependentOptions

Discard record on has_many through

If you have the following relations:

class Vehicle < ApplicationRecordĀ Ā 
  has_many :vehicle_parts
Ā Ā has_many :parts, through: :vehicle_parts
endĀ 

class VehiclePart < ApplicationRecord
  belongs_to :vehicle
Ā Ā belongs_to :part Ā 
end

class Part < ApplicationRecord
  has_many :vehicle_partsĀ Ā 
  has_many :vehicles, through: :vehicle_parts
end

How would discard be configured? Would all the table have a discarded_at column? And how are we expected to discard a part for a given vehicle, not the part itself?

Without any soft delete tool, I would be doing something like this:

vehicle = Vehicle.find(vehicle_id)
part = Part.find(part_id)
vehicle.parts.delete(part) 

N+1 queries

What is the best way to tackle n+1 queries with discard gem?

Adding a default_scope might be an option but since the documentation says that it is undesirable to use it, I was wondering if there is a better way to tackle this problem?

dependent: :discard

Would be nice to be able to use dependent: :discard on associations

For example
has_one :referral_code, ->{ kept }, dependent: :discard

is discard_all! working on version 1.1.0

Describe the bug
When i used the discard_all! on relation i got the following exception

#<NoMethodError: undefined method `discard_all!' for #ConversationMessage::ActiveRecord_Relation:0x00000000046a1748\nDid you mean? discarded>",

To Reproduce
Model:

class ConversationMessage < ApplicationRecord
    include Discard::Model
    belongs_to :conversation
end

the relation that reproduced the bug:
ConversationMessage.where(conversation_id: params[:id]).discard_all!
Expected behavior
to work normally

add #discard!

what are your thoughts on adding a bang discard method discard! which can throw a Discard::Model::RecordNotDiscarded error?

.discarded query

Hello, I'm not sure this is bug or not.
Even a little information would be helpful.

Describe the bug

User.discarded
=> SELECT [users].* FROM [users] WHERE [users].[discarded_at] IS NULL AND [users].[discarded_at] IS NOT 

Actual log is below. unscoped one is fine!

irb(main):001> User.discarded
/usr/local/bundle/gems/activerecord-sqlserver-adapter-7.0.4.0/lib/active_record/connection_adapters/sqlserver_adapter.rb:111: warning: undefining the allocator of T_DATA class TinyTds::Result
  SQL (1.0ms)  USE [webapp_development]
  User Load (2.9ms)  SELECT [users].* FROM [users] WHERE [users].[discarded_at] IS NULL AND [users].[discarded_at] IS NOT NULL

irb(main):002> User.unscoped.discarded
  User Load (19.9ms)  SELECT [users].* FROM [users] WHERE [users].[discarded_at] IS NOT NULL

class User < ApplicationRecord
  include Discard::Model
  default_scope -> { kept }

  def active_for_authentication?
    super && !discarded?
  end

  def inactive_message
    discarded? ? :discarded : super
  end
etc...

Additional context

discard (1.3.0)
ruby 3.2.2 (2023-03-30 revision e51014f9c0) [aarch64-linux]
Rails 7.0.8

Nil when finding has_one association

Describe the bug
Hey there! I was implementing your after_undiscard do when I came up with an unexpected error/bug. I have a has_one association with a model called Address. I used address.discard on the after_discard which worked perfectly. However, when I tried the same for the undiscard the address is returning as nil (it does not find the address even if it is in the discarded list). I am using address.with_discarded.discarded.undiscard_all since I am in using the default scope. But even looking for address inside the callback returns nil so the following scopes are not the issue.

To Reproduce
Steps to reproduce the behavior:

  1. Add Discard to a model with a has_one association
  2. Discard the model alongside the association
  3. Try to undiscard using the association
  4. See error

Expected behavior
It should find the association and undiscard it.

Additional context

gem "rails", "~> 6.1.4"
ruby "3.1.2"
gem "discard", "~> 1.2"

Support for discarded_by?

It would be amazing to have support for a discarded_by association on the models. Does it make sense to add this to the project?

Warden/Devise: Preventing login for discarded users?

Has anyone else worked on how to prevent discarded users from logging in? I'm on a Rails app with Devise, so of course a naive solution would be to override the default controller behavior ā€”Ā but since I imagine this is not an uncommon scenario, I'm curious to know if anyone's worked out a more portable solution.

Devise::SessionsController#create calls warden.authenticate! (source) before anything else, so I thought that might be a good place to start.

What would be the best way to extend Warden's behavior in this case?

#discard_all doing batch updates ?

Having these two models

class Product < ApplicationRecord
  include Discard::Model
  has_many :variations, class_name: "ProductVariation"
  
  after_discard do
    variations.discard_all
  end
end

class ProductVariations < ApplicationRecord
  include Discard::Model
  belongs_to :product
end

Calling @product.discard will trigger N+1 update query. Example:
screen shot 2018-04-09 at 16 12 19

While it 'kinda works' -- best practice in this case would be to do batch update. I understand that batch updates are pain in the ass in AR (I have my own library I copy from project to project to deal with it). But i decided to throw this here for discussion - it seems reasonable to support batch updates :)

Discarding relations does not work like described in README

I'm trying to implement the after_discard callback to discard related objects.

The README says that a post's comments can be discarded like so;

has_many :comments
after_discard do
  comments.discard_all
end

However, this is what I'm seeing

> Post.last.discard
NoMethodError: undefined method `discard_all' for #<ActiveRecord::Associations::CollectionProxy []>

I'm guessing that the discard_all class method added by this library is not understood by CollectionProxy. I'm running ActiveRecord 5.1.4, has anything changed recently?

The immediate workaround I'm seeing is doing comments.each(&:discard) which is perhaps not as elegant but does the job. Any better ideas?

Sequel Support

Hi,

Discard looks great!

I was wondering if there were any plans to add Sequel support.

Based on a super quick look at the codebase - there does not seem to be much ActiveRecord-specific code.

Integration with Discard gem for soft-deletes

It would be great to provide an integration to the discard gem for soft deletes.

I saw related issues like #43 #42. Adding some good documentation (we need a docs folder like AA has) would really help.

What I've done was add it like this:

# admin/posts.rb
Trestle.resource(:posts) do
  controller do
    # TODO: can extract this to generic soft delete helper
    def destroy
      success = instance.discard

      respond_to do |format|
        format.html do
          if success
            flash[:message] = flash_message("destroy.success", title: "Success!", message: "The %{lowercase_model_name} was successfully deleted.")
            redirect_to_return_location(:destroy, instance, default: admin.path(:index))
          else
            flash[:error] = flash_message("destroy.failure", title: "Warning!", message: "Could not delete %{lowercase_model_name}.")

            if self.instance = admin.find_instance(params)
              redirect_to_return_location(:update, instance, default: admin.instance_path(instance))
            else
              redirect_to_return_location(:destroy, instance, default: admin.path(:index))
            end
          end
        end
        format.json { head :no_content }
        format.js
      end
    end
  end

# models/post.rb
class Post < ApplicationRecord
  include Discard::Model
  # using default scope for the sake of trestle, not sure how to integrate properly
  default_scope -> { kept }
end

What I'd like to do is:

  1. Avoid copying destroy/index methods directly from gem source
  2. Avoid using default scopes
  3. Hook into collection and instance methods to customize how resources are fetch while still retaining the nice pagination, sorting and so on features

Forcing explicit scope: kept, discarded, or with_discarded. Never all

I have a feature request, and proof of concept for it. Looking for some feedback:

I'm using Discard::Model in a model of mine, let's call it Comment. I would like to force other consumers of my Comment model to make an explicit choice of what Comments they want to access. kept, discarded, or with_discarded.

I'd like a mode where I can enforce this behavior:

Comment.all # raise an error - I don't think `all` is explicit enough when working with Discard
Comment.includes(:user) # error - an implicit `all` is even more likely to be overlooked
Comment.count # error - same as above
Comment.kept.includes(:user) # behaves as expected
Comment.with_discarded.includes(:user) # behaves as expected

Why? Because I believe that the scope is very easy for a developer to overlook. By requiring an explicit scoping decision, it forces the developer to consider if they should reveal discarded records too.

An example of how one might enable this mode:

class Comment < ApplicationRecord
  include Discard::Model

  require_discard_scope
end

Comment.count # error

Bonus points: provide a way to override the default raise behavior. For example:

class Comment < ApplicationRecord
  include Discard::Model

  require_discard_scope do
    logger.warn 'A Discard model was used without an explicit scope!'
  end
end

I've been experimenting with this locally and I think I have a promising approach on how to achieve this. Does anybody else have a desire for this functionality? That'll help me determine if I should open a PR into the main line, fork the repo, or create a new gem that works with Discard to provide this functionality.

Thanks all!

Unable to declare scope :kept as per the readme.md

The readme recommends overriding scope :kept for dependent relationships.

scope :kept, -> { undiscarded.joins(:post).merge(Post.kept) }

This results in
ArgumentError: You tried to define a scope named "kept" on the model "Comment", but Active Record already defined a class method with the same name.

Unable to login with Active User having a discarded record

Following flow is not allowing User to login, any idea, how i can resolve it

 Create a New User with email like [email protected]
 User login works fine
 Discard this [email protected] user
 Create an other user with [email protected]
 Confirm this user, confirmation works fine
 now logout - login again with [email protected], It will Not work.

Here is User.rb code

 class User < ActiveRecord::Base
  include Discard::Model

   def active_for_authentication?
     super and self.active? and !discarded_at
   end

   def inactive_message
     self.active? ? super : I18n.t("users.account_suspended")
   end
 end

SessionsController: Using for login

class SessionsController < Devise::SessionsController
   before_action :set_encrypt_cookies, only: [:create]
   
   def create
     resource = warden.authenticate!(scope: resource_name)
     set_flash_message :success, :signed_in
     sign_in_and_redirect(resource_name, resource)
  end

discard method is not working inside transactions

discard method is not working inside transactions

ActiveRecord::Base.transaction do
            begin
                @product.assign_attributes(product_params)
                new_produc = @product.dup
                new_produc.save
                @product.discard
                 # redirect to page
            rescue
                # redirect to page
            end
end

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    šŸ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. šŸ“ŠšŸ“ˆšŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ā¤ļø Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.