mongoid_archival 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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