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 +7 -0
- data/LICENSE +24 -0
- data/README.md +245 -0
- data/lib/mongoid/archivable.rb +158 -0
- data/lib/mongoid/archivable/configuration.rb +17 -0
- data/lib/mongoid/archivable/depending.rb +57 -0
- data/lib/mongoid/archivable/protected.rb +38 -0
- data/lib/mongoid/archivable/version.rb +7 -0
- data/lib/mongoid_archival.rb +3 -0
- data/perf/scope.rb +65 -0
- data/spec/app/models/address.rb +71 -0
- data/spec/app/models/appointment.rb +7 -0
- data/spec/app/models/archivable_phone.rb +25 -0
- data/spec/app/models/archivable_post.rb +66 -0
- data/spec/app/models/author.rb +7 -0
- data/spec/app/models/fish.rb +10 -0
- data/spec/app/models/person.rb +21 -0
- data/spec/app/models/phone.rb +11 -0
- data/spec/app/models/relations.rb +246 -0
- data/spec/app/models/sport.rb +5 -0
- data/spec/app/models/tag.rb +6 -0
- data/spec/app/models/title.rb +4 -0
- data/spec/mongoid/archive_spec.rb +341 -0
- data/spec/mongoid/configuration_spec.rb +59 -0
- data/spec/mongoid/document_spec.rb +21 -0
- data/spec/mongoid/nested_attributes_spec.rb +164 -0
- data/spec/mongoid/protected_spec.rb +44 -0
- data/spec/mongoid/restore_spec.rb +144 -0
- data/spec/mongoid/scopes_spec.rb +117 -0
- data/spec/mongoid/validatable/uniqueness_spec.rb +77 -0
- data/spec/spec_helper.rb +43 -0
- metadata +141 -0
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
|
+
[](https://github.com/tablecheck/mongoid_archival/actions/workflows/test.yml?query=branch%3Amaster)
|
4
|
+
[](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
|