paranoia-3.0.0/0000755000004100000410000000000014664106564013350 5ustar www-datawww-dataparanoia-3.0.0/.gitignore0000644000004100000410000000005414664106564015337 0ustar www-datawww-datapkg/* *.gem .bundle tmp .rvmrc Gemfile.lock paranoia-3.0.0/CONTRIBUTING.md0000644000004100000410000000236614664106564015610 0ustar www-datawww-dataParanoia is an open source project and we encourage contributions. ## Filing an issue When filing an issue on the Paranoia project, please provide these details: * A comprehensive list of steps to reproduce the issue. * What you're *expecting* to happen compared with what's *actually* happening. * Your application's complete `Gemfile.lock`, and `Gemfile.lock` as text in a [Gist](https://gist.github.com) (*not as an image*) * Any relevant stack traces ("Full trace" preferred) In 99% of cases, this information is enough to determine the cause and solution to the problem that is being described. Please remember to format code using triple backticks (\`) so that it is neatly formatted when the issue is posted. ## Pull requests We gladly accept pull requests to add documentation, fix bugs and, in some circumstances, add new features to Paranoia. Here's a quick guide: 1. Fork the repo. 2. Run the tests. We only take pull requests with passing tests, and it's great to know that you have a clean slate. 3. Create new branch then make changes and add tests for your changes. Only refactoring and documentation changes require no new tests. If you are adding functionality or fixing a bug, we need tests! 4. Push to your fork and submit a pull request. paranoia-3.0.0/.github/0000755000004100000410000000000014664106564014710 5ustar www-datawww-dataparanoia-3.0.0/.github/workflows/0000755000004100000410000000000014664106564016745 5ustar www-datawww-dataparanoia-3.0.0/.github/workflows/build.yml0000644000004100000410000000211314664106564020564 0ustar www-datawww-data# This workflow uses actions that are not certified by GitHub. # They are provided by a third-party and are governed by # separate terms of service, privacy policy, and support # documentation. # This workflow will download a prebuilt Ruby version, install dependencies and run tests with Rake # For more information see: https://github.com/marketplace/actions/setup-ruby-jruby-and-truffleruby name: build on: [push, pull_request] jobs: test: runs-on: ubuntu-20.04 strategy: fail-fast: false matrix: rails: ["edge", "~> 7.2.0", "~> 7.1.0", "~> 7.0.0", "~> 6.1.0"] ruby: ["3.3","3.2", "3.1", "3.0", "2.7"] exclude: - rails: "~> 7.2.0" ruby: "3.0" - rails: "~> 7.2.0" ruby: "2.7" - rails: "edge" ruby: "3.0" - rails: "edge" ruby: "2.7" env: RAILS: ${{ matrix.rails }} steps: - uses: actions/checkout@v4 - uses: ruby/setup-ruby@v1 with: ruby-version: ${{ matrix.ruby }} bundler-cache: true - run: bundle exec rake paranoia-3.0.0/lib/0000755000004100000410000000000014664106564014116 5ustar www-datawww-dataparanoia-3.0.0/lib/paranoia/0000755000004100000410000000000014664106564015710 5ustar www-datawww-dataparanoia-3.0.0/lib/paranoia/rspec.rb0000644000004100000410000000145014664106564017351 0ustar www-datawww-dataif defined?(RSpec) require 'rspec/expectations' # Validate the subject's class did call "acts_as_paranoid" RSpec::Matchers.define :act_as_paranoid do match { |subject| subject.class.ancestors.include?(Paranoia) } failure_message_proc = lambda do "expected #{subject.class} to use `acts_as_paranoid`" end failure_message_when_negated_proc = lambda do "expected #{subject.class} not to use `acts_as_paranoid`" end if respond_to?(:failure_message_when_negated) failure_message(&failure_message_proc) failure_message_when_negated(&failure_message_when_negated_proc) else # RSpec 2 compatibility: failure_message_for_should(&failure_message_proc) failure_message_for_should_not(&failure_message_when_negated_proc) end end end paranoia-3.0.0/lib/paranoia/active_record_5_2.rb0000644000004100000410000000231514664106564021514 0ustar www-datawww-datamodule HandleParanoiaDestroyedInBelongsToAssociation def handle_dependency return unless load_target case options[:dependent] when :destroy target.destroy if target.respond_to?(:paranoia_destroyed?) raise ActiveRecord::Rollback unless target.paranoia_destroyed? else raise ActiveRecord::Rollback unless target.destroyed? end else target.send(options[:dependent]) end end end module HandleParanoiaDestroyedInHasOneAssociation def delete(method = options[:dependent]) if load_target case method when :delete target.delete when :destroy target.destroyed_by_association = reflection target.destroy if target.respond_to?(:paranoia_destroyed?) throw(:abort) unless target.paranoia_destroyed? else throw(:abort) unless target.destroyed? end when :nullify target.update_columns(reflection.foreign_key => nil) if target.persisted? end end end end ActiveRecord::Associations::BelongsToAssociation.prepend HandleParanoiaDestroyedInBelongsToAssociation ActiveRecord::Associations::HasOneAssociation.prepend HandleParanoiaDestroyedInHasOneAssociation paranoia-3.0.0/lib/paranoia/version.rb0000644000004100000410000000005714664106564017724 0ustar www-datawww-datamodule Paranoia VERSION = '3.0.0'.freeze end paranoia-3.0.0/lib/paranoia.rb0000644000004100000410000003034614664106564016243 0ustar www-datawww-datarequire 'active_record' unless defined? ActiveRecord if [ActiveRecord::VERSION::MAJOR, ActiveRecord::VERSION::MINOR] == [5, 2] || ActiveRecord::VERSION::MAJOR > 5 require 'paranoia/active_record_5_2' end module Paranoia @@default_sentinel_value = nil # Change default_sentinel_value in a rails initializer def self.default_sentinel_value=(val) @@default_sentinel_value = val end def self.default_sentinel_value @@default_sentinel_value end def self.included(klazz) klazz.extend Query end module Query def paranoid? ; true ; end # If you want to find all records, even those which are deleted def with_deleted if ActiveRecord::VERSION::STRING >= "4.1" return unscope where: paranoia_column end all.tap { |x| x.default_scoped = false } end # If you want to find only the deleted records def only_deleted if paranoia_sentinel_value.nil? return with_deleted.where.not(paranoia_column => paranoia_sentinel_value) end # if paranoia_sentinel_value is not null, then it is possible that # some deleted rows will hold a null value in the paranoia column # these will not match != sentinel value because "NULL != value" is # NULL under the sql standard # Scoping with the table_name is mandatory to avoid ambiguous errors when joining tables. scoped_quoted_paranoia_column = "#{connection.quote_table_name(self.table_name)}.#{connection.quote_column_name(paranoia_column)}" with_deleted.where("#{scoped_quoted_paranoia_column} IS NULL OR #{scoped_quoted_paranoia_column} != ?", paranoia_sentinel_value) end alias_method :deleted, :only_deleted # If you want to restore a record def restore(id_or_ids, opts = {}) ids = Array(id_or_ids).flatten any_object_instead_of_id = ids.any? { |id| ActiveRecord::Base === id } if any_object_instead_of_id ids.map! { |id| ActiveRecord::Base === id ? id.id : id } ActiveSupport::Deprecation.warn("You are passing an instance of ActiveRecord::Base to `restore`. " \ "Please pass the id of the object by calling `.id`") end ids.map { |id| only_deleted.find(id).restore!(opts) } end end def paranoia_destroy with_transaction_returning_status do result = run_callbacks(:destroy) do @_disable_counter_cache = paranoia_destroyed? result = paranoia_delete next result unless result && ActiveRecord::VERSION::STRING >= '4.2' each_counter_cached_associations do |association| foreign_key = association.reflection.foreign_key.to_sym next if destroyed_by_association && destroyed_by_association.foreign_key.to_sym == foreign_key next unless send(association.reflection.name) association.decrement_counters end @_trigger_destroy_callback = true @_disable_counter_cache = false result end raise ActiveRecord::Rollback, "Not destroyed" unless paranoia_destroyed? result end || false end alias_method :destroy, :paranoia_destroy def paranoia_destroy! paranoia_destroy || raise(ActiveRecord::RecordNotDestroyed.new("Failed to destroy the record", self)) end def trigger_transactional_callbacks? super || @_trigger_destroy_callback && paranoia_destroyed? end def paranoia_delete raise ActiveRecord::ReadOnlyRecord, "#{self.class} is marked as readonly" if readonly? if persisted? # if a transaction exists, add the record so that after_commit # callbacks can be run add_to_transaction update_columns(paranoia_destroy_attributes) elsif !frozen? assign_attributes(paranoia_destroy_attributes) end self end alias_method :delete, :paranoia_delete def restore!(opts = {}) self.class.transaction do run_callbacks(:restore) do recovery_window_range = get_recovery_window_range(opts) # Fixes a bug where the build would error because attributes were frozen. # This only happened on Rails versions earlier than 4.1. noop_if_frozen = ActiveRecord.version < Gem::Version.new("4.1") if within_recovery_window?(recovery_window_range) && ((noop_if_frozen && !@attributes.frozen?) || !noop_if_frozen) @_disable_counter_cache = !paranoia_destroyed? write_attribute paranoia_column, paranoia_sentinel_value update_columns(paranoia_restore_attributes) each_counter_cached_associations do |association| if send(association.reflection.name) association.increment_counters end end @_disable_counter_cache = false end restore_associated_records(recovery_window_range) if opts[:recursive] end end self end alias :restore :restore! def get_recovery_window_range(opts) return opts[:recovery_window_range] if opts[:recovery_window_range] return unless opts[:recovery_window] (deletion_time - opts[:recovery_window]..deletion_time + opts[:recovery_window]) end def within_recovery_window?(recovery_window_range) return true unless recovery_window_range recovery_window_range.cover?(deletion_time) end def paranoia_destroyed? paranoia_column_value != paranoia_sentinel_value end alias :deleted? :paranoia_destroyed? def really_destroy!(update_destroy_attributes: true) with_transaction_returning_status do run_callbacks(:real_destroy) do @_disable_counter_cache = paranoia_destroyed? dependent_reflections = self.class.reflections.select do |name, reflection| reflection.options[:dependent] == :destroy end if dependent_reflections.any? dependent_reflections.each do |name, reflection| association_data = self.send(name) # has_one association can return nil # .paranoid? will work for both instances and classes next unless association_data && association_data.paranoid? if reflection.collection? next association_data.with_deleted.find_each { |record| record.really_destroy!(update_destroy_attributes: update_destroy_attributes) } end association_data.really_destroy!(update_destroy_attributes: update_destroy_attributes) end end update_columns(paranoia_destroy_attributes) if update_destroy_attributes destroy_without_paranoia end end end private def counter_cache_disabled? defined?(@_disable_counter_cache) && @_disable_counter_cache end def counter_cached_association_names return [] if counter_cache_disabled? super end def each_counter_cached_associations return [] if counter_cache_disabled? if defined?(super) super else counter_cached_association_names.each do |name| yield association(name) end end end def paranoia_restore_attributes { paranoia_column => paranoia_sentinel_value }.merge(timestamp_attributes_with_current_time) end def paranoia_destroy_attributes { paranoia_column => current_time_from_proper_timezone }.merge(timestamp_attributes_with_current_time) end def timestamp_attributes_with_current_time timestamp_attributes_for_update_in_model.each_with_object({}) { |attr,hash| hash[attr] = current_time_from_proper_timezone } end def paranoia_find_has_one_target(association) association_foreign_key = association.options[:through].present? ? association.klass.primary_key : association.foreign_key association_find_conditions = { association_foreign_key => self.id } association_find_conditions[association.type] = self.class.name if association.type scope = association.klass.only_deleted.where(association_find_conditions) scope = scope.merge(association.scope) if association.scope scope.first end # restore associated records that have been soft deleted when # we called #destroy def restore_associated_records(recovery_window_range = nil) destroyed_associations = self.class.reflect_on_all_associations.select do |association| association.options[:dependent] == :destroy end destroyed_associations.each do |association| association_data = send(association.name) unless association_data.nil? if association_data.paranoid? if association.collection? association_data.only_deleted.each do |record| record.restore(:recursive => true, :recovery_window_range => recovery_window_range) end else association_data.restore(:recursive => true, :recovery_window_range => recovery_window_range) end end end if association_data.nil? && association.macro.to_s == "has_one" if association.klass.paranoid? paranoia_find_has_one_target(association) .try!(:restore, recursive: true, :recovery_window_range => recovery_window_range) end end end if ActiveRecord.version.to_s > '7' # Method deleted in https://github.com/rails/rails/commit/dd5886d00a2d5f31ccf504c391aad93deb014eb8 @association_cache.clear if persisted? && destroyed_associations.present? else clear_association_cache if destroyed_associations.present? end end end ActiveSupport.on_load(:active_record) do class ActiveRecord::Base def self.acts_as_paranoid(options={}) if included_modules.include?(Paranoia) puts "[WARN] #{self.name} is calling acts_as_paranoid more than once!" return end define_model_callbacks :restore, :real_destroy alias_method :really_destroyed?, :destroyed? alias_method :really_delete, :delete alias_method :destroy_without_paranoia, :destroy include Paranoia class_attribute :paranoia_column, :paranoia_sentinel_value self.paranoia_column = (options[:column] || :deleted_at).to_s self.paranoia_sentinel_value = options.fetch(:sentinel_value) { Paranoia.default_sentinel_value } def self.paranoia_scope where(paranoia_column => paranoia_sentinel_value) end class << self; alias_method :without_deleted, :paranoia_scope end unless options[:without_default_scope] default_scope { paranoia_scope } end before_restore { self.class.notify_observers(:before_restore, self) if self.class.respond_to?(:notify_observers) } after_restore { self.class.notify_observers(:after_restore, self) if self.class.respond_to?(:notify_observers) } end # Please do not use this method in production. # Pretty please. def self.I_AM_THE_DESTROYER! # TODO: actually implement spelling error fixes puts %Q{ Sharon: "There should be a method called I_AM_THE_DESTROYER!" Ryan: "What should this method do?" Sharon: "It should fix all the spelling errors on the page!" } end def self.paranoid? ; false ; end def paranoid? ; self.class.paranoid? ; end private def paranoia_column self.class.paranoia_column end def paranoia_column_value send(paranoia_column) end def paranoia_sentinel_value self.class.paranoia_sentinel_value end def deletion_time paranoia_column_value.acts_like?(:time) ? paranoia_column_value : deleted_at end end end require 'paranoia/rspec' if defined? RSpec module ActiveRecord module Validations module UniquenessParanoiaValidator def build_relation(klass, *args) relation = super return relation unless klass.respond_to?(:paranoia_column) arel_paranoia_scope = klass.arel_table[klass.paranoia_column].eq(klass.paranoia_sentinel_value) if ActiveRecord::VERSION::STRING >= "5.0" relation.where(arel_paranoia_scope) else relation.and(arel_paranoia_scope) end end end class UniquenessValidator < ActiveModel::EachValidator prepend UniquenessParanoiaValidator end class AssociationNotSoftDestroyedValidator < ActiveModel::EachValidator def validate_each(record, attribute, value) # if association is soft destroyed, add an error if value.present? && value.paranoia_destroyed? record.errors.add(attribute, 'has been soft-deleted') end end end end end paranoia-3.0.0/Rakefile0000644000004100000410000000024114664106564015012 0ustar www-datawww-datarequire 'bundler' Bundler::GemHelper.install_tasks task :test do Dir['test/*_test.rb'].each do |testfile| load testfile end end task :default => :test paranoia-3.0.0/paranoia.gemspec0000644000004100000410000000311614664106564016510 0ustar www-datawww-data# -*- encoding: utf-8 -*- require File.expand_path("../lib/paranoia/version", __FILE__) Gem::Specification.new do |s| s.name = "paranoia" s.version = Paranoia::VERSION s.platform = Gem::Platform::RUBY s.authors = %w(radarlistener@gmail.com) s.email = %w(ben@benmorgan.io john.hawthorn@gmail.com) s.homepage = "https://github.com/rubysherpas/paranoia" s.license = 'MIT' s.summary = "Paranoia is a re-implementation of acts_as_paranoid for Rails 3, 4, and 5, using much, much, much less code." s.description = <<-DSC Paranoia is a re-implementation of acts_as_paranoid for Rails 5, 6, and 7, using much, much, much less code. You would use either plugin / gem if you wished that when you called destroy on an Active Record object that it didn't actually destroy it, but just "hid" the record. Paranoia does this by setting a deleted_at field to the current time when you destroy a record, and hides it by scoping all queries on your model to only include records which do not have a deleted_at field. DSC s.required_rubygems_version = ">= 1.3.6" s.required_ruby_version = '>= 2.7' s.add_dependency 'activerecord', '>= 6', '< 8.1' s.add_development_dependency "bundler", ">= 1.0.0" s.add_development_dependency "rake" s.files = Dir.chdir(File.expand_path('..', __FILE__)) do files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)}) } files end s.executables = `git ls-files`.split("\n").map{|f| f =~ /^bin\/(.*)/ ? $1 : nil}.compact s.require_path = 'lib' end paranoia-3.0.0/CODE_OF_CONDUCT.md0000644000004100000410000000623014664106564016150 0ustar www-datawww-data# Contributor Covenant Code of Conduct ## Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards Examples of behavior that contributes to creating a positive environment include: * Using welcoming and inclusive language * Being respectful of differing viewpoints and experiences * Gracefully accepting constructive criticism * Focusing on what is best for the community * Showing empathy towards other community members Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery and unwelcome sexual attention or advances * Trolling, insulting/derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or electronic address, without explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at ben@benmorgan.io. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] [homepage]: http://contributor-covenant.org [version]: http://contributor-covenant.org/version/1/4/ paranoia-3.0.0/Gemfile0000644000004100000410000000107014664106564014641 0ustar www-datawww-datasource 'https://rubygems.org' sqlite = ENV['SQLITE_VERSION'] if sqlite gem 'sqlite3', sqlite, platforms: [:ruby] else gem 'sqlite3', '~> 1.4', platforms: [:ruby] end platforms :jruby do gem 'activerecord-jdbcsqlite3-adapter' end if RUBY_ENGINE == 'rbx' platforms :rbx do gem 'rubinius-developer_tools' gem 'rubysl', '~> 2.0' gem 'rubysl-test-unit' end end rails = ENV['RAILS'] || '~> 6.0.4' if rails == 'edge' gem 'rails', github: 'rails/rails' else gem 'rails', rails end # Specify your gem's dependencies in paranoia.gemspec gemspec paranoia-3.0.0/LICENSE0000644000004100000410000000162514664106564014361 0ustar www-datawww-dataPermission is hereby granted, without written agreement and without license or royalty fees, to use, copy, modify, and distribute this software and its documentation for any purpose, provided that the above copyright notice and the following two paragraphs appear in all copies of this software. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF THE COPYRIGHT HOLDER HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. THE COPYRIGHT HOLDER SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND THE COPYRIGHT HOLDER HAS NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. paranoia-3.0.0/README.md0000644000004100000410000002520114664106564014627 0ustar www-datawww-data[![Gem Version](https://badge.fury.io/rb/paranoia.svg)](https://badge.fury.io/rb/paranoia) [![build](https://github.com/rubysherpas/paranoia/actions/workflows/build.yml/badge.svg)](https://github.com/rubysherpas/paranoia/actions/workflows/build.yml) **Notice:** `paranoia` has some surprising behaviour (like overriding ActiveRecord's `delete` and `destroy`) and is not recommended for new projects. See [`discard`'s README](https://github.com/jhawthorn/discard#why-not-paranoia-or-acts_as_paranoid) for more details. Paranoia will continue to accept bug fixes and support new versions of Rails but isn't accepting new features. # Paranoia Paranoia is a re-implementation of [acts\_as\_paranoid](http://github.com/ActsAsParanoid/acts_as_paranoid) for Rails 3/4/5, using much, much, much less code. When your app is using Paranoia, calling `destroy` on an ActiveRecord object doesn't actually destroy the database record, but just *hides* it. Paranoia does this by setting a `deleted_at` field to the current time when you `destroy` a record, and hides it by scoping all queries on your model to only include records which do not have a `deleted_at` field. If you wish to actually destroy an object you may call `really_destroy!`. **WARNING**: This will also *really destroy* all `dependent: :destroy` records, so please aim this method away from face when using. If a record has `has_many` associations defined AND those associations have `dependent: :destroy` set on them, then they will also be soft-deleted if `acts_as_paranoid` is set, otherwise the normal destroy will be called. ***See [Destroying through association callbacks](#destroying-through-association-callbacks) for clarifying examples.*** ## Getting Started Video Setup and basic usage of the paranoia gem [GoRails #41](https://gorails.com/episodes/soft-delete-with-paranoia) ## Installation & Usage For Rails 3, please use version 1 of Paranoia: ``` ruby gem "paranoia", "~> 1.0" ``` For Rails 4 and 5, please use version 2 of Paranoia (2.2 or greater required for rails 5): ``` ruby gem "paranoia", "~> 2.2" ``` Of course you can install this from GitHub as well from one of these examples: ``` ruby gem "paranoia", github: "rubysherpas/paranoia", branch: "rails3" gem "paranoia", github: "rubysherpas/paranoia", branch: "rails4" gem "paranoia", github: "rubysherpas/paranoia", branch: "rails5" ``` Then run: ``` shell bundle install ``` Updating is as simple as `bundle update paranoia`. #### Run your migrations for the desired models Run: ``` shell bin/rails generate migration AddDeletedAtToClients deleted_at:datetime:index ``` and now you have a migration ``` ruby class AddDeletedAtToClients < ActiveRecord::Migration def change add_column :clients, :deleted_at, :datetime add_index :clients, :deleted_at end end ``` ### Usage #### In your model: ``` ruby class Client < ActiveRecord::Base acts_as_paranoid # ... end ``` Hey presto, it's there! Calling `destroy` will now set the `deleted_at` column: ``` ruby >> client.deleted_at # => nil >> client.destroy # => client >> client.deleted_at # => [current timestamp] ``` If you really want it gone *gone*, call `really_destroy!`: ``` ruby >> client.deleted_at # => nil >> client.really_destroy! # => client ``` If you need skip updating timestamps for deleting records, call `really_destroy!(update_destroy_attributes: false)`. When we call `really_destroy!(update_destroy_attributes: false)` on the parent `client`, then each child `email` will also have `really_destroy!(update_destroy_attributes: false)` called. ``` ruby >> client.really_destroy!(update_destroy_attributes: false) # => client ``` If you want to use a column other than `deleted_at`, you can pass it as an option: ``` ruby class Client < ActiveRecord::Base acts_as_paranoid column: :destroyed_at ... end ``` If you want to skip adding the default scope: ``` ruby class Client < ActiveRecord::Base acts_as_paranoid without_default_scope: true ... end ``` If you want to access soft-deleted associations, override the getter method: ``` ruby def product Product.unscoped { super } end ``` If you want to include associated soft-deleted objects, you can (un)scope the association: ``` ruby class Person < ActiveRecord::Base belongs_to :group, -> { with_deleted } end Person.includes(:group).all ``` If you want to find all records, even those which are deleted: ``` ruby Client.with_deleted ``` If you want to exclude deleted records, when not able to use the default_scope (e.g. when using without_default_scope): ``` ruby Client.without_deleted ``` If you want to find only the deleted records: ``` ruby Client.only_deleted ``` If you want to check if a record is soft-deleted: ``` ruby client.paranoia_destroyed? # or client.deleted? ``` If you want to restore a record: ``` ruby Client.restore(id) # or client.restore ``` If you want to restore a whole bunch of records: ``` ruby Client.restore([id1, id2, ..., idN]) ``` If you want to restore a record and their dependently destroyed associated records: ``` ruby Client.restore(id, :recursive => true) # or client.restore(:recursive => true) ``` If you want to restore a record and only those dependently destroyed associated records that were deleted within 2 minutes of the object upon which they depend: ``` ruby Client.restore(id, :recursive => true, :recovery_window => 2.minutes) # or client.restore(:recursive => true, :recovery_window => 2.minutes) ``` Note that by default paranoia will not prevent that a soft destroyed object can't be associated with another object of a different model. A Rails validator is provided should you require this functionality: ``` ruby validates :some_assocation, association_not_soft_destroyed: true ``` This validator makes sure that `some_assocation` is not soft destroyed. If the object is soft destroyed the main object is rendered invalid and an validation error is added. For more information, please look at the tests. #### About indexes: Beware that you should adapt all your indexes for them to work as fast as previously. For example, ``` ruby add_index :clients, :group_id add_index :clients, [:group_id, :other_id] ``` should be replaced with ``` ruby add_index :clients, :group_id, where: "deleted_at IS NULL" add_index :clients, [:group_id, :other_id], where: "deleted_at IS NULL" ``` Of course, this is not necessary for the indexes you always use in association with `with_deleted` or `only_deleted`. ##### Unique Indexes Because NULL != NULL in standard SQL, we can not simply create a unique index on the deleted_at column and expect it to enforce that there only be one record with a certain combination of values. If your database supports them, good alternatives include partial indexes (above) and indexes on computed columns. E.g. ``` ruby add_index :clients, [:group_id, 'COALESCE(deleted_at, false)'], unique: true ``` If not, an alternative is to create a separate column which is maintained alongside deleted_at for the sake of enforcing uniqueness. To that end, paranoia makes use of two method to make its destroy and restore actions: paranoia_restore_attributes and paranoia_destroy_attributes. ``` ruby add_column :clients, :active, :boolean add_index :clients, [:group_id, :active], unique: true class Client < ActiveRecord::Base # optionally have paranoia make use of your unique column, so that # your lookups will benefit from the unique index acts_as_paranoid column: :active, sentinel_value: true def paranoia_restore_attributes { deleted_at: nil, active: true } end def paranoia_destroy_attributes { deleted_at: current_time_from_proper_timezone, active: nil } end end ``` ##### Destroying through association callbacks When dealing with `dependent: :destroy` associations and `acts_as_paranoid`, it's important to remember that whatever method is called on the parent model will be called on the child model. For example, given both models of an association have `acts_as_paranoid` defined: ``` ruby class Client < ActiveRecord::Base acts_as_paranoid has_many :emails, dependent: :destroy end class Email < ActiveRecord::Base acts_as_paranoid belongs_to :client end ``` When we call `destroy` on the parent `client`, it will call `destroy` on all of its associated children `emails`: ``` ruby >> client.emails.count # => 5 >> client.destroy # => client >> client.deleted_at # => [current timestamp] >> Email.where(client_id: client.id).count # => 0 >> Email.with_deleted.where(client_id: client.id).count # => 5 ``` Similarly, when we call `really_destroy!` on the parent `client`, then each child `email` will also have `really_destroy!` called: ``` ruby >> client.emails.count # => 5 >> client.id # => 12345 >> client.really_destroy! # => client >> Client.find 12345 # => ActiveRecord::RecordNotFound >> Email.with_deleted.where(client_id: client.id).count # => 0 ``` However, if the child model `Email` does not have `acts_as_paranoid` set, then calling `destroy` on the parent `client` will also call `destroy` on each child `email`, thereby actually destroying them: ``` ruby class Client < ActiveRecord::Base acts_as_paranoid has_many :emails, dependent: :destroy end class Email < ActiveRecord::Base belongs_to :client end >> client.emails.count # => 5 >> client.destroy # => client >> Email.where(client_id: client.id).count # => 0 >> Email.with_deleted.where(client_id: client.id).count # => NoMethodError: undefined method `with_deleted' for # ``` ## Acts As Paranoid Migration You can replace the older `acts_as_paranoid` methods as follows: | Old Syntax | New Syntax | |:-------------------------- |:------------------------------ | |`find_with_deleted(:all)` | `Client.with_deleted` | |`find_with_deleted(:first)` | `Client.with_deleted.first` | |`find_with_deleted(id)` | `Client.with_deleted.find(id)` | The `recover` method in `acts_as_paranoid` runs `update` callbacks. Paranoia's `restore` method does not do this. ## Callbacks Paranoia provides several callbacks. It triggers `destroy` callback when the record is marked as deleted and `real_destroy` when the record is completely removed from database. It also calls `restore` callback when the record is restored via paranoia For example if you want to index your records in some search engine you can go like this: ```ruby class Product < ActiveRecord::Base acts_as_paranoid after_destroy :update_document_in_search_engine after_restore :update_document_in_search_engine after_real_destroy :remove_document_from_search_engine end ``` You can use these events just like regular Rails callbacks with before, after and around hooks. ## License This gem is released under the MIT license. paranoia-3.0.0/CHANGELOG.md0000644000004100000410000002202714664106564015164 0ustar www-datawww-data# paranoia Changelog ## 3.0.0 - August 13, 2024 _Tagged as 3.0 as Ruby + Rails version constraints have been modernised._ - [#564](https://github.com/rubysherpas/paranoia/pull/564) Support Rails edge - [#563](https://github.com/rubysherpas/paranoia/pull/563) Support Rails 7.2 ## 2.6.4 - July 20, 2024 * [#554](https://github.com/rubysherpas/paranoia/pull/554) Support prebuilt counter cache association list (#554) [Joé Dupuis](https://github.com/JoeDupuis) * [#551](https://github.com/rubysherpas/paranoia/pull/551) Fix: restore has_one with scope (#551) [Paweł Charyło](https://github.com/zygzagZ) * [#555](https://github.com/rubysherpas/paranoia/pull/555) 📝 Add Yard documentation for Paranoia::Query (#555) [Clément Prod'homme](https://github.com/cprodhomme) ## 2.6.3 - Oct 12, 2023 * [#548](https://github.com/rubysherpas/paranoia/pull/548) Add support for [Rails 7.1](https://github.com/rails/rails/releases/tag/v7.1.0) (#548) [Indyarocks](https://github.com/indyarocks) ## 2.6.2 - Jun 6, 2023 * [#441](https://github.com/rubysherpas/paranoia/pull/441) Recursive restore with has_many/one through assocs (#441) [Emil Ong](https://github.com/emilong) ## 2.6.1 - Nov 16, 2022 * [#535](https://github.com/rubysherpas/paranoia/pull/535) Allow to skip updating paranoia_destroy_attributes for records while really_destroy! [Anton Bogdanov](https://github.com/kortirso) ## 2.6.0 - Mar 23, 2022 * [#512](https://github.com/rubysherpas/paranoia/pull/512) Quote table names; Mysql 8 has keywords that might match table names which cause an exception. * [#476](https://github.com/rubysherpas/paranoia/pull/476) Fix syntax error in documentation. * [#485](https://github.com/rubysherpas/paranoia/pull/485) Rollback transaction if destroy aborted. * [#522](https://github.com/rubysherpas/paranoia/pull/522) Add failing tests for association with abort on destroy. * [#513](https://github.com/rubysherpas/paranoia/pull/513) Fix create callback called on destroy. ## 2.5.3 * [#532](https://github.com/rubysherpas/paranoia/pull/532) Fix: correct bug when sentinel_value is not a timestamp [Hassanin Ahmed](https://github.com/sas1ni69) * [#531](https://github.com/rubysherpas/paranoia/pull/531) Added test case to reproduce bug introduce in v2.5.1 [Sherif Elkassaby](https://github.com/sherif-nedap) * [#529](https://github.com/rubysherpas/paranoia/pull/529) Fix: Do not define a RSpec matcher when RSpec isn't present [Sebastian Welther](https://github.com/swelther) ## 2.5.2 * [#526](https://github.com/rubysherpas/paranoia/pull/526) Do not include tests files in packaged gem [Jason Fleetwood-Boldt](https://github.com/jasonfb) * [#492](https://github.com/rubysherpas/paranoia/pull/492) Warn if acts_as_paranoid is called more than once on the same model [Ignatius Reza](https://github.com/ignatiusreza) ## 2.5.1 * [#481](https://github.com/rubysherpas/paranoia/pull/481) Replaces hard coded `deleted_at` with `paranoia_column`. [Hassanin Ahmed](https://github.com/sas1ni69) ## 2.5.0 * [#516](https://github.com/rubysherpas/paranoia/pull/516) Add support for ActiveRecord 7.0, drop support for EOL Ruby < 2.5 and Rails < 5.1 adding support for Rails 7 [Mathieu Jobin](https://github.com/mathieujobin) * [#515](https://github.com/rubysherpas/paranoia/pull/515) Switch from Travis CI to GitHub Actions [Shinichi Maeshima](https://github.com/willnet) ## 2.4.3 * [#503](https://github.com/rubysherpas/paranoia/pull/503) Bump activerecord dependency for Rails 6.1 [Jörg Schiller](https://github.com/joergschiller) * [#483](https://github.com/rubysherpas/paranoia/pull/483) Update JRuby version to 9.2.8.0 + remove EOL Ruby 2.2 [Uwe Kubosch](https://github.com/donv) * [#482](https://github.com/rubysherpas/paranoia/pull/482) Fix after_commit for Rails 6 [Ashwin Hegde](https://github.com/hashwin) ## 2.4.2 * [#470](https://github.com/rubysherpas/paranoia/pull/470) Add support for ActiveRecord 6.0 [Anton Kolodii](https://github.com/iggant), [Jared Norman](https://github.com/jarednorman) ## 2.4.1 * [#435](https://github.com/rubysherpas/paranoia/pull/435) Monkeypatch activerecord relations to work with rails 5.2.0 [Bartosz Bonisławski (@bbonislawski)](https://github.com/bbonislawski) ## 2.4.0 * [#423](https://github.com/rubysherpas/paranoia/pull/423) Add `paranoia_destroy` and `paranoia_delete` aliases [John Hawthorn (@jhawthorn)](https://github.com/jhawthorn) * [#408](https://github.com/rubysherpas/paranoia/pull/408) Fix instance variable `@_disable_counter_cache` not initialized warning. [Akira Matsuda (@amatsuda)](https://github.com/amatsuda) * [#412](https://github.com/rubysherpas/paranoia/pull/412) Fix `really_destroy!` behavior with `sentinel_value` [Steve Rice (@steverice)](https://github.com/steverice) ## 2.3.1 * [#397](https://github.com/rubysherpas/paranoia/pull/397) Bump active record max version to support 5.1 final ## 2.3.0 (2017-04-14) * [#393](https://github.com/rubysherpas/paranoia/pull/393) Drop support for Rails 4.1 and begin supporting Rails 5.1. [Miklós Fazekas (@mfazekas)](https://github.com/mfazekas) * [#391](https://github.com/rubysherpas/paranoia/pull/391) Use Contributor Covenant Version 1.4 [Ben A. Morgan (@BenMorganIO)](https://github.com/BenMorganIO) * [#390](https://github.com/rubysherpas/paranoia/pull/390) Fix counter cache with double destroy, really_destroy, and restore [Chris Oliver (@excid3)](https://github.com/excid3) * [#389](https://github.com/rubysherpas/paranoia/pull/389) Added association not soft destroyed validator _Fixes [#380](https://github.com/rubysherpas/paranoia/issues/380)_ [Edward Poot (@edwardmp)](https://github.com/edwardmp) * [#383](https://github.com/rubysherpas/paranoia/pull/383) Add recovery window feature _Fixes [#359](https://github.com/rubysherpas/paranoia/issues/359)_ [Andrzej Piątyszek (@konto-andrzeja)](https://github.com/konto-andrzeja) ## 2.2.1 (2017-02-15) * [#371](https://github.com/rubysherpas/paranoia/pull/371) Use ActiveSupport.on_load to correctly re-open ActiveRecord::Base _Fixes [#335](https://github.com/rubysherpas/paranoia/issues/335) and [#381](https://github.com/rubysherpas/paranoia/issues/381)._ [Iaan Krynauw (@iaankrynauw)](https://github.com/iaankrynauw) * [#377](https://github.com/rubysherpas/paranoia/pull/377) Touch record on paranoia-destroy. _Fixes [#296](https://github.com/rubysherpas/paranoia/issues/296)._ [René (@rbr)](https://github.com/rbr) * [#379](https://github.com/rubysherpas/paranoia/pull/379) Fixes a problem of ambiguous table names when using only_deleted method. _Fixes [#26](https://github.com/rubysherpas/paranoia/issues/26) and [#27](https://github.com/rubysherpas/paranoia/pull/27)._ [Thomas Romera (@Erowlin)](https://github.com/Erowlin) ## 2.2.0 (2016-10-21) * Ruby 2.0 or greater is required * Rails 5.0.0.beta1.1 support [@pigeonworks](https://github.com/pigeonworks) [@halostatue](https://github.com/halostatue) and [@gagalago](https://github.com/gagalago) * Previously `#really_destroyed?` may have been defined on non-paranoid models, it is now only available on paranoid models, use regular `#destroyed?` instead. ## 2.1.5 (2016-01-06) * Ruby 2.3 support ## 2.1.4 ## 2.1.3 ## 2.1.2 ## 2.1.1 ## 2.1.0 (2015-01-23) ### Major changes * `#destroyed?` is no longer overridden. Use `#paranoia_destroyed?` for the existing behaviour. [Washington Luiz](https://github.com/huoxito) * `#persisted?` is no longer overridden. * ActiveRecord 4.0 no longer has `#destroy!` as an alias for `#really_destroy!`. * `#destroy` will now raise an exception if called on a readonly record. * `#destroy` on a hard deleted record is now a successful noop. * `#destroy` on a new record will set deleted_at (previously this raised an error) * `#destroy` and `#delete` always return self when successful. ### Bug Fixes * Calling `#destroy` twice will not hard-delete records. Use `#really_destroy!` if this is desired. * Fix errors on non-paranoid has_one dependent associations ## 2.0.5 (2015-01-22) ### Bug fixes * Fix restoring polymorphic has_one relationships [#189](https://github.com/radar/paranoia/pull/189) [#174](https://github.com/radar/paranoia/issues/174) [Patrick Koperwas](https://github.com/PatKoperwas) * Fix errors when restoring a model with a has_one against a non-paranoid model. [#168](https://github.com/radar/paranoia/pull/168) [Shreyas Agarwal](https://github.com/shreyas123) * Fix rspec 2 compatibility [#197](https://github.com/radar/paranoia/pull/197) [Emil Sågfors](https://github.com/lime) * Fix some deprecation warnings on rails 4.2 [Sergey Alekseev](https://github.com/sergey-alekseev) ## 2.0.4 (2014-12-02) ### Features * Add paranoia_scope as named version of default_scope [#184](https://github.com/radar/paranoia/pull/184) [Jozsef Nyitrai](https://github.com/nyjt) ### Bug Fixes * Fix initialization problems when missing table or no database connection [#186](https://github.com/radar/paranoia/issues/186) * Fix broken restore of has_one associations [#185](https://github.com/radar/paranoia/issues/185) [#171](https://github.com/radar/paranoia/pull/171) [Martin Sereinig](https://github.com/srecnig)