mongoid_archival 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 07ba0abce107f9b1480d234623377106c955e53f34666b2958ffdd2b76c829f0
4
+ data.tar.gz: 06f63e64420baa03f3bddeca4f971c0a506edfb98c6447140e2db82e4d48bf8f
5
+ SHA512:
6
+ metadata.gz: 0e1727027aea380a8654b8f566acdb8dfec1e8c71bba2bdafb59251b47d2d412eda3eb15cd2de235430a95b6e7f9db76e20d383b49c924642cf06826c9fe2f8e
7
+ data.tar.gz: ea9db2aae467c6a0169e40869702fa5fff7fe62660c7d6f9f016037482d458b945d7fc89553c23b02752ff8f9ebdf4801e658da0b45f89ff77b0365c3511cdb3
data/LICENSE ADDED
@@ -0,0 +1,24 @@
1
+ Copyright (c) 2009 Durran Jordan
2
+ Copyright (c) 2013 Josef Šimánek
3
+ Copyright (c) 2021 TableCheck Inc.
4
+
5
+ MIT License
6
+
7
+ Permission is hereby granted, free of charge, to any person obtaining
8
+ a copy of this software and associated documentation files (the
9
+ "Software"), to deal in the Software without restriction, including
10
+ without limitation the rights to use, copy, modify, merge, publish,
11
+ distribute, sublicense, and/or sell copies of the Software, and to
12
+ permit persons to whom the Software is furnished to do so, subject to
13
+ the following conditions:
14
+
15
+ The above copyright notice and this permission notice shall be
16
+ included in all copies or substantial portions of the Software.
17
+
18
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
19
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
20
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
21
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
22
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
23
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
24
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,245 @@
1
+ # Mongoid::Archivable (mongoid_archival)
2
+
3
+ [![Build Status](https://github.com/tablecheck/mongoid_archival/actions/workflows/test.yml/badge.svg?query=branch%3Amaster)](https://github.com/tablecheck/mongoid_archival/actions/workflows/test.yml?query=branch%3Amaster)
4
+ [![Gem Version](https://img.shields.io/gem/v/mongoid_archival.svg)](https://rubygems.org/gems/mongoid_archival)
5
+
6
+ `Mongoid::Archivable` enables archiving (soft delete) of Mongoid documents.
7
+ Instead of being removed from the database, archived docs are flagged with an `archived_at` timestamp.
8
+ This allows you to maintain references to archived documents, and restore if necessary.
9
+
10
+ #### Instability Warning
11
+
12
+ Versions prior to 1.0.0 are in **alpha** state. Behaviors, APIs, method names, etc.
13
+ may change anytime without warning. Please lock your gem version, be careful when upgrading,
14
+ and write tests in your own project.
15
+
16
+ #### Disambiguation
17
+
18
+ * This gem is different than [mongoid-archivable](https://github.com/Sign2Pay/mongoid-archivable),
19
+ which moves documents to a separate "archive" database/collection. Cannot be used concurrently with
20
+ this gem as both use the `Mongoid::Archivable` namespace.
21
+ * This gem is forked from [mongoid_paranoia](https://github.com/simi/mongoid_paranoia).
22
+ Can be used concurrently with this gem. See section below for key differences.
23
+
24
+ #### TODO
25
+
26
+ * [ ] Support embedded documents.
27
+ * [ ] Support model-level configuration.
28
+ * [ ] Allow rename archive field alias.
29
+ * [ ] Consider adding #archive(callbacks: false)
30
+ * [ ] Consider adding .archive_all query action
31
+
32
+ ## Usage
33
+
34
+ #### Installation
35
+
36
+ In your application's Gemfile:
37
+
38
+ ```ruby
39
+ gem 'mongoid_archival'
40
+ ```
41
+
42
+ #### Adding to Model Class
43
+
44
+ ```ruby
45
+ class Person
46
+ include Mongoid::Document
47
+ include Mongoid::Archivable
48
+
49
+ # TODO: archivable macro
50
+ end
51
+ ```
52
+
53
+ #### Archiving with Documents
54
+
55
+ ```ruby
56
+ # Set the archived_at field to the current time, firing callbacks
57
+ # and archiving any dependent documents. Analogous to Mongoid #destroy method.
58
+ person.archive
59
+
60
+ # Sets the archived_at field to the current time, ignoring callbacks
61
+ # and dependency rules. Analogous to Mongoid #delete method.
62
+ person.archive_without_callbacks
63
+ # TODO person.archive(callbacks: false)
64
+
65
+ # Un-archive an archive document back.
66
+ person.restore
67
+
68
+ # Un-archive an archive document back, including any dependent documents.
69
+ person.restore(recursive: true)
70
+ ```
71
+
72
+ #### Querying
73
+
74
+ ```ruby
75
+ # Return all documents, both archived and non-archived.
76
+ Person.all
77
+
78
+ # Return only documents that are not flagged as archived.
79
+ Person.current
80
+
81
+ # Return only documents that are flagged as archived.
82
+ Person.archived
83
+ ```
84
+
85
+ #### Global Configuration
86
+
87
+ You may globally configure field and method names in an initializer.
88
+
89
+ ```ruby
90
+ # config/initializers/mongoid_archivable.rb
91
+
92
+ Mongoid::Archivable.configure do |c|
93
+ c.archived_field = :archived_at
94
+ c.archived_scope = :archived
95
+ c.nonarchived_scope = :current
96
+ end
97
+ ```
98
+
99
+ #### Callbacks
100
+
101
+ Archivable documents have the following new callbacks.
102
+ Note that these callbacks are **not** fired on `#destroy`.
103
+
104
+ * `before_archive`
105
+ * `after_archive`
106
+ * `around_archive`
107
+ * `before_restore`
108
+ * `after_restore`
109
+ * `around_restore`
110
+
111
+ ```ruby
112
+ class User
113
+ include Mongoid::Document
114
+ include Mongoid::Archivable
115
+
116
+ before_archive :before_archive_action
117
+ after_archive :after_archive_action
118
+ around_archive :around_archive_action
119
+
120
+ before_restore :before_restore_action
121
+ after_restore :after_restore_action
122
+ around_restore :around_restore_action
123
+
124
+ # You may `throw(:abort)` within a callback to prevent
125
+ # the action from proceeding.
126
+ def before_archive_action
127
+ throw(:abort) if name == 'Pete'
128
+ end
129
+ end
130
+ ```
131
+
132
+ #### Relation Dependencies
133
+
134
+ This gem adds two new relation dependency handling strategies:
135
+
136
+ * `:archive` - Invokes `#archive` and callbacks on each dependency, recursively
137
+ including dependencies of dependencies. Analogous to `:destroy`.
138
+ * `:archive_all` - Calls `.set(archived_at: Time.now)` on the
139
+ dependency scope in a single query. Much faster but does not support callbacks
140
+ or dependency recursion. Analogous to `:delete_all`.
141
+
142
+ If the dependent model is not archivable, these strategies will be ignored without any effect.
143
+
144
+ ```ruby
145
+ class User
146
+ include Mongoid::Document
147
+ include Mongoid::Archivable
148
+
149
+ has_many :pokemons, dependent: :archive
150
+ belongs_to :gym, dependent: :archive_all
151
+ end
152
+ ```
153
+
154
+ In addition, dependency strategies `:nullify`, `:restrict_with_exception`,
155
+ and `:restrict_with_error` will be applied when archiving documents.
156
+ `:destroy` and `:delete_all` are intentionally ignored.
157
+
158
+ #### Protecting Against Deletion
159
+
160
+ Add the `Mongoid::Archivable::Protected` mixin to cause
161
+ `#delete` and `#destroy` methods to raise an error.
162
+ The bang methods `#delete!` and `#destroy!` can be used instead.
163
+ This is useful when migrating a legacy codebase.
164
+
165
+ ```ruby
166
+ class Pokemon
167
+ include Mongoid::Document
168
+ include Mongoid::Archivable
169
+ include Mongoid::Archivable::Protected
170
+ end
171
+
172
+ venusaur = Pokemon.create
173
+ venusaur.delete # raises RuntimeError
174
+ venusaur.destroy # raises RuntimeError
175
+ venusaur.delete! # deletes the document without callbacks
176
+ venusaur.destroy! # deletes the document with callbacks
177
+ ```
178
+
179
+ ## Gotchas
180
+
181
+ The following require additional manual changes when using this gem.
182
+
183
+ #### Uniqueness Validation
184
+
185
+ You must set `scope: :archived_at` in your uniqueness validations to prevent
186
+ validating against archived documents.
187
+
188
+ ```ruby
189
+ validates_uniqueness_of :title, scope: :archived_at
190
+ ```
191
+
192
+ #### Indexes
193
+
194
+ You should add `archived_at` to your query indexes. As a rule-of-thumb, we recommend
195
+ to add `archived_at` as the final key; this will create a compound index that will be
196
+ selected with or without `archived_at` in the query.
197
+
198
+ ```ruby
199
+ index category: 1, title: 1, archived_at: 1
200
+ ```
201
+
202
+ Note that this may not give the best performance in all cases, for example
203
+ when performing a time-range query on the value of `archived_at`. Please refer to the
204
+ [MongoDB Indexes documentation](https://docs.mongodb.com/manual/indexes/)
205
+ to learn more about index design.
206
+
207
+ ## Comparison with Mongoid::Paranoia
208
+
209
+ We used [Mongoid::Paranoia](https://github.com/simi/mongoid_paranoia) at [TableCheck](https://www.tablecheck.com/en/join/)
210
+ for many years. While many of design assumptions of Mongoid::Paranoia
211
+ lead to initial productivity, we found them ultimately limiting and unintuitive
212
+ as we grew both our team and our codebase.
213
+
214
+ #### Key Differences
215
+
216
+ * The flag named is `archived_at` rather than `deleted_at`.
217
+ The name `deleted_at` was confusing with respect to hard deletion.
218
+ * Mongoid::Paranoia overrides the `#delete` and `#destroy` methods; this gem does not.
219
+ Monkey patches and hackery are removed; behavior is less surprising.
220
+ * This gem does **not** set a default scope on root (non-embedded) docs.
221
+ Use the `.current` (non-archived) and `.archived` query scopes as needed.
222
+ Mongoid::Paranoia relies on `.unscoped`
223
+
224
+ #### Migration Checklist
225
+
226
+ * [ ] Add `mongoid_archival` to your gemspec **after** `mongoid_paranoia`.
227
+ You may use the two gems together in your project,
228
+ but should include only one of `Mongoid::Archivable` or `Mongoid::Paranoia` into each model class.
229
+ In this manner, you can migrate each model one-by-one.
230
+ * [ ] To avoid accidentally calling `#delete` and `#destroy`, add the
231
+ `Mongoid::Archivable::Protected` mixin to cause those methods to raise an error.
232
+ * [ ] Configure your `archived_field_name = :deleted_at` for backwards compatibility.
233
+ * [ ] Add `.current` to your queries as necessary. You can remove usages of `.unscoped`.
234
+ * [ ] In your relations, replace `dependent: :destroy` with `dependent: :archive` as necessary.
235
+
236
+ ## About Us
237
+
238
+ Mongoid::Archivable is made with ❤ by [TableCheck](https://www.tablecheck.com/en/join/),
239
+ the leading restaurant reservation and guest management app maker.
240
+ If **you** are a ninja-level 🥷 coder (Javascript/Ruby/Elixir/Python/Go),
241
+ designer, product manager, data scientist, QA, etc. and are ready to join us in Tokyo, Japan
242
+ or work remotely, please get in touch at [careers@tablecheck.com](mailto:careers@tablecheck.com).
243
+
244
+ Shout out to Durran Jordan and Josef Šimánek for their original work on
245
+ [Mongoid::Paranoia](https://github.com/simi/mongoid_paranoia).
@@ -0,0 +1,158 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support'
4
+ require 'mongoid/archivable/version'
5
+ require 'mongoid/archivable/configuration'
6
+ require 'mongoid/archivable/depending'
7
+ require 'mongoid/archivable/protected'
8
+
9
+ module Mongoid
10
+
11
+ # Include this module to get archivable root level documents.
12
+ # This will add a archived_at field to the +Document+, managed automatically.
13
+ # Potentially incompatible with unique indices. (if collisions with archived items)
14
+ #
15
+ # @example Make a document archivable.
16
+ # class Person
17
+ # include Mongoid::Document
18
+ # include Mongoid::Archivable
19
+ # end
20
+ module Archivable
21
+ extend ActiveSupport::Concern
22
+
23
+ class << self
24
+ def configuration
25
+ @configuration ||= Configuration.new
26
+ end
27
+
28
+ def reset
29
+ @configuration = Configuration.new
30
+ end
31
+
32
+ # Set an alternate field name for archived_at.
33
+ #
34
+ # @example
35
+ # Mongoid::Archivable.configure do |c|
36
+ # c.archived_field = :my_field_name
37
+ # end
38
+ def configure
39
+ yield(configuration)
40
+ end
41
+ end
42
+
43
+ # class_methods do
44
+ # def archivable(options = {})
45
+ # Mongoid::Archivable::Installer.new(self, options).setup
46
+ # end
47
+ # end
48
+
49
+ included do
50
+ class_attribute :archivable
51
+ self.archivable = true
52
+
53
+ config = Archivable.configuration
54
+
55
+ field config.archived_field, as: :archived_at, type: Time
56
+
57
+ scope config.nonarchived_scope, -> { where(archived_at: nil) }
58
+ scope config.archived_scope, -> { ne(archived_at: nil) }
59
+
60
+ define_model_callbacks :archive
61
+ define_model_callbacks :restore
62
+
63
+ def archive(options = {})
64
+ return if archived?
65
+ raise Errors::ReadonlyDocument.new(self.class) if readonly?
66
+ run_callbacks(:archive) do
67
+ if catch(:abort) { apply_archive_dependencies! }
68
+ archive_without_callbacks(options || {})
69
+ else
70
+ false
71
+ end
72
+ end
73
+ end
74
+
75
+ def archive_without_callbacks(_options = {})
76
+ return if archived?
77
+ raise Errors::ReadonlyDocument.new(self.class) if readonly?
78
+ now = Time.now
79
+ self.archived_at = now
80
+ _archivable_update('$set' => { archived_field => now })
81
+ true
82
+ end
83
+
84
+ # Determines if this document is archived.
85
+ #
86
+ # @example Is the document destroyed?
87
+ # person.destroyed?
88
+ #
89
+ # @return [ true, false ] If the document is destroyed.
90
+ def archived?
91
+ !!archived_at
92
+ end
93
+
94
+ # Restores a previously archived document. Handles this by removing the
95
+ # archived_at flag.
96
+ #
97
+ # @example Restore the document from archived state.
98
+ # document.restore
99
+ #
100
+ # For restoring associated documents use :recursive => true
101
+ # @example Restore the associated documents from archived state.
102
+ # document.restore(recursive: true)
103
+ def restore(options = {})
104
+ return unless archived?
105
+ run_callbacks(:restore) { restore_without_callbacks(options) }
106
+ end
107
+
108
+ def restore_without_callbacks(options = {})
109
+ return unless archived?
110
+ _archivable_update('$unset' => { archived_field => true })
111
+ attributes.delete('archived_at') # TODO: does this need database field name
112
+ restore_relations if options[:recursive]
113
+ true
114
+ end
115
+
116
+ def restore_relations
117
+ relations.each_pair do |name, association|
118
+ next unless association.dependent.in?(%i[archive archive_all])
119
+ next unless _association_archivable?(association)
120
+ relation = send(name)
121
+ next unless relation
122
+ Array.wrap(relation).each do |doc|
123
+ doc.restore(recursive: true)
124
+ end
125
+ end
126
+ end
127
+
128
+ private
129
+
130
+ # Get the collection to be used for archivable operations.
131
+ #
132
+ # @example Get the archivable collection.
133
+ # document.archivable_collection
134
+ #
135
+ # @return [ Collection ] The root collection.
136
+ def archivable_collection
137
+ embedded? ? _root.collection : collection
138
+ end
139
+
140
+ # Get the field to be used for archivable operations.
141
+ #
142
+ # @example Get the archivable field.
143
+ # document.archived_field
144
+ #
145
+ # @return [ String ] The archived at field.
146
+ def archived_field
147
+ field = Archivable.configuration.archived_field
148
+ embedded? ? "#{atomic_position}.#{field}" : field
149
+ end
150
+
151
+ # @return [ Object ] Update result.
152
+ #
153
+ def _archivable_update(value)
154
+ archivable_collection.find(atomic_selector).update_one(value)
155
+ end
156
+ end
157
+ end
158
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mongoid
4
+ module Archivable
5
+ class Configuration
6
+ attr_accessor :archived_field,
7
+ :archived_scope,
8
+ :nonarchived_scope
9
+
10
+ def initialize
11
+ @archived_field = :archived_at
12
+ @archived_scope = :archived
13
+ @nonarchived_scope = :current
14
+ end
15
+ end
16
+ end
17
+ end