mongo_trails 17.0.1 → 17.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 943d508b66d94b893dc1a553eb9f974b17484294750410f3b0b986073c4de415
4
- data.tar.gz: 784ec37eed06ee8ffc0674d2f82a9a59d07892bd652a4006ac51b702b7b6e02e
3
+ metadata.gz: 56dbb9f210528411b5b75400afafe8cedb67a22c9fe815534cf5702ffde9dfc0
4
+ data.tar.gz: 1b7e64ade5beddbaedb94c28ce4643373314994776e94a67e4af4b2340773b53
5
5
  SHA512:
6
- metadata.gz: d56c0b823e24c0b18da5dedbbffb79fb1ecd9dc86fdc47c5455353f6a9e50ad242e346aa40d1d1c628985c93ed00a4f13e0947720487bfe4dd13a32b56c3d814
7
- data.tar.gz: b6846cf53080d5f5d11c22bc6bfdbf25904a676a019b64f78311c8d44f803d853d7d5d959b7bce9143b9925706d7ed200fb2e9b3112adbfcfa58aed67ad91ea0
6
+ metadata.gz: 87875ed778feadc9e4d0b36716e12beb84fddf893aae69b222140cbaccf2dfb9ad600c2ef12da86d76c5c1340b25308ba1c138e13d07cfa11ca23f2a10175159
7
+ data.tar.gz: f034ef6b1babac2f7952d12865730e9bf7cbc1c5bcd00f8f1e570d5ad7ae2773c83aaec8167e40d3341b2670d709854ae8973191de018e63ee13343f3b5883d8
data/README.md CHANGED
@@ -33,22 +33,6 @@ require 'mongo_trails/mongo_support/config'
33
33
 
34
34
  Done!
35
35
 
36
- ## Restores
37
-
38
- Paper Trail records `create`, `update` and `destroy`, but a soft-deleted record brought back by [Paranoia](https://github.com/rubysherpas/paranoia) is restored with `update_columns`, which Paper Trail never sees. Include `MongoTrails::Restorable` in a Paranoia model to record a version with event `restore` (`MongoTrails::RESTORE_EVENT`) each time it is restored:
39
-
40
- ```ruby
41
- class Post < ActiveRecord::Base
42
- acts_as_paranoid
43
- has_paper_trail
44
- include MongoTrails::Restorable # after acts_as_paranoid, which defines the restore callbacks
45
- end
46
- ```
47
-
48
- The version's changes are the columns Paranoia writes on restore (`paranoia_restore_attributes`), from their deleted values to the restored ones. Associated records Paranoia leaves deleted (outside the recovery window) get no version. Paranoia is not a dependency of this gem.
49
-
50
- To record a restore done some other way, call `record.paper_trail.record_restore(changes)` with each restored column mapped to `[before, after]`, for example `{ 'deleted_at' => [deleted_at, nil] }`. Like `record_update_columns`, it does nothing while Paper Trail is disabled and respects the record size limit.
51
-
52
36
  ## Config Options
53
37
 
54
38
  `PaperTrail.config.mongo_config = { hosts: ['localhost:27017'], database: 'my_test_db' }` - Options for MongoDB connection
@@ -59,6 +43,21 @@ To record a restore done some other way, call `record.paper_trail.record_restore
59
43
  `PaperTrail.config.sidekiq_worker = PaperTrail::WriteVersionWorker` - Worker class
60
44
  `PaperTrail.config.sidekiq_options = { queue: :default }` - Options for Sidekiq
61
45
 
46
+ `PaperTrail.config.enable_integer_id = true` - Give every version a sequential `integer_id` from a per-collection counter, and order previous/next versions by it. With `false` no counter is used, `integer_id` stays empty and versions are ordered by `created_at`. The first write to a collection with no counter scans the whole collection for its highest `integer_id`, which is expensive on large collections (and on DocumentDB, where no index serves it).
47
+
48
+ `PaperTrail.config.on_write_error = nil` - A callable receiving `(error, version)` when a version write fails because MongoDB is unreachable or drops the connection (`MongoTrails::Version::WRITE_ERRORS`). Inside a transaction versions are written from the after-commit callbacks, so a raising write skips every after-commit callback behind it; a handler lets them run. Without a handler the error raises. Other errors always raise.
49
+
50
+ ```ruby
51
+ PaperTrail.config.on_write_error = lambda do |error, version|
52
+ ErrorReporter.report(error, item_type: version.item_type, item_id: version.item_id)
53
+ version.enqueue_write unless PaperTrail.config.enable_sidekiq # retry through Sidekiq
54
+ end
55
+ ```
56
+
57
+ `PaperTrail::WriteVersionWorker` rebuilds the queued version with `MongoTrails::Version.rebuild` (its `_id`, `created_at` and metadata are kept, `#initialize` is not run again), and treats a version whose `_id` is already stored as written, so a retried write is safe.
58
+
59
+ `MongoTrails::Version.bulk_insert(versions)` - Inserts many versions in one `insert_many`, allocating their `integer_id`s in one counter update when `enable_integer_id` is on.
60
+
62
61
  ## Releasing
63
62
 
64
63
  Update `MongoTrails::VERSION` and all appraisal lockfiles, then validate and release:
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: ..
3
3
  specs:
4
- mongo_trails (17.0.1)
4
+ mongo_trails (17.1.0)
5
5
  mongoid (>= 7, < 10)
6
6
  ostruct
7
7
  paper_trail (>= 16, < 18)
@@ -172,7 +172,7 @@ CHECKSUMS
172
172
  mcp (0.8.0) sha256=ae8bd146bb8e168852866fd26f805f52744f6326afb3211e073f78a95e0c34fb
173
173
  minitest (5.27.0) sha256=2d3b17f8a36fe7801c1adcffdbc38233b938eb0b4966e97a6739055a45fa77d5
174
174
  mongo (2.25.0) sha256=b4663884d23523d24f50c579846956444410f939093e8c2a95fa4b76bb929053
175
- mongo_trails (17.0.1)
175
+ mongo_trails (17.1.0)
176
176
  mongoid (9.1.0) sha256=ef7b50f4c84e636850c3431bc3c92f98481f4e1d147ff06a1c21242d0b242a9e
177
177
  ostruct (0.6.3) sha256=95a2ed4a4bd1d190784e666b47b2d3f078e4a9efda2fccf18f84ddc6538ed912
178
178
  paper_trail (17.0.0) sha256=1c2842061d3874ca7015908e821e2aa14f9b982af2acb2a7974713bf79021c85
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: ..
3
3
  specs:
4
- mongo_trails (17.0.1)
4
+ mongo_trails (17.1.0)
5
5
  mongoid (>= 7, < 10)
6
6
  ostruct
7
7
  paper_trail (>= 16, < 18)
@@ -172,7 +172,7 @@ CHECKSUMS
172
172
  mcp (0.8.0) sha256=ae8bd146bb8e168852866fd26f805f52744f6326afb3211e073f78a95e0c34fb
173
173
  minitest (5.27.0) sha256=2d3b17f8a36fe7801c1adcffdbc38233b938eb0b4966e97a6739055a45fa77d5
174
174
  mongo (2.25.0) sha256=b4663884d23523d24f50c579846956444410f939093e8c2a95fa4b76bb929053
175
- mongo_trails (17.0.1)
175
+ mongo_trails (17.1.0)
176
176
  mongoid (9.1.0) sha256=ef7b50f4c84e636850c3431bc3c92f98481f4e1d147ff06a1c21242d0b242a9e
177
177
  ostruct (0.6.3) sha256=95a2ed4a4bd1d190784e666b47b2d3f078e4a9efda2fccf18f84ddc6538ed912
178
178
  paper_trail (17.0.0) sha256=1c2842061d3874ca7015908e821e2aa14f9b982af2acb2a7974713bf79021c85
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: ..
3
3
  specs:
4
- mongo_trails (17.0.1)
4
+ mongo_trails (17.1.0)
5
5
  mongoid (>= 7, < 10)
6
6
  ostruct
7
7
  paper_trail (>= 16, < 18)
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: ..
3
3
  specs:
4
- mongo_trails (17.0.1)
4
+ mongo_trails (17.1.0)
5
5
  mongoid (>= 7, < 10)
6
6
  ostruct
7
7
  paper_trail (>= 16, < 18)
@@ -175,7 +175,7 @@ CHECKSUMS
175
175
  mcp (0.8.0) sha256=ae8bd146bb8e168852866fd26f805f52744f6326afb3211e073f78a95e0c34fb
176
176
  minitest (5.27.0) sha256=2d3b17f8a36fe7801c1adcffdbc38233b938eb0b4966e97a6739055a45fa77d5
177
177
  mongo (2.25.0) sha256=b4663884d23523d24f50c579846956444410f939093e8c2a95fa4b76bb929053
178
- mongo_trails (17.0.1)
178
+ mongo_trails (17.1.0)
179
179
  mongoid (9.1.0) sha256=ef7b50f4c84e636850c3431bc3c92f98481f4e1d147ff06a1c21242d0b242a9e
180
180
  ostruct (0.6.3) sha256=95a2ed4a4bd1d190784e666b47b2d3f078e4a9efda2fccf18f84ddc6538ed912
181
181
  paper_trail (16.0.0) sha256=e9b9f0fb1b8b590c8231cfa931b282ba92f90e066e393930a5e1c61ae4c5019d
@@ -8,7 +8,9 @@ module PaperTrail
8
8
  :enable_sidekiq,
9
9
  :sidekiq_worker,
10
10
  :sidekiq_options,
11
- :mongo_trails_config
11
+ :mongo_trails_config,
12
+ :enable_integer_id,
13
+ :on_write_error
12
14
  )
13
15
 
14
16
  def initialize
@@ -19,6 +21,8 @@ module PaperTrail
19
21
  @has_paper_trail_defaults = {}
20
22
  @enable_sidekiq = false
21
23
  @sidekiq_options = { queue: :default, retry: true, backtrace: false }
24
+ @enable_integer_id = true
25
+ @on_write_error = nil
22
26
  end
23
27
  end
24
28
  end
@@ -24,9 +24,35 @@ PaperTrail.config.has_paper_trail_defaults = { versions: { class_name: 'MongoTra
24
24
 
25
25
  module MongoTrails
26
26
  class Version
27
+ # Errors a version write can raise when MongoDB is unreachable or drops the connection. Only these reach
28
+ # PaperTrail.config.on_write_error; anything else (a validation error, a bug) still raises.
29
+ WRITE_ERRORS = [Mongo::Error, IOError, SystemCallError, Timeout::Error].freeze
30
+ DUPLICATE_KEY_ERROR_CODE = 11_000
31
+
27
32
  class << self
28
33
  def find(id)
29
- find_by(integer_id: id)
34
+ integer_id_enabled? ? find_by(integer_id: id) : super
35
+ end
36
+
37
+ def integer_id_enabled?
38
+ PaperTrail.config.enable_integer_id != false
39
+ end
40
+
41
+ # PaperTrail orders previous/next versions by primary key when it is an integer, which here means integer_id.
42
+ # Without integer_id they are ordered by created_at.
43
+ def primary_key_is_int?
44
+ integer_id_enabled?
45
+ end
46
+
47
+ # Rebuilds a version from the JSON of one that was already built (see #enqueue_write), keeping its _id,
48
+ # created_at and metadata. It does not run #initialize, so build-time defaults are not applied a second time.
49
+ def rebuild(attributes)
50
+ document = attributes.to_h.stringify_keys.to_h { |name, value| [name, from_json_value(name, value)] }
51
+ instantiate(document).tap { |version| version.new_record = true }
52
+ end
53
+
54
+ def duplicate_key_error?(error)
55
+ error.respond_to?(:code) && error.code == DUPLICATE_KEY_ERROR_CODE
30
56
  end
31
57
 
32
58
  def incrementing_fields
@@ -62,6 +88,17 @@ module MongoTrails
62
88
  ((max_integer_id - count + 1)..max_integer_id).to_a
63
89
  end
64
90
 
91
+ def bulk_insert(versions)
92
+ return [] if versions.empty?
93
+
94
+ documents = versions.map { |version| bulk_insert_attributes(version) }
95
+ if integer_id_enabled?
96
+ integer_ids = next_integer_ids(documents.count { |document| document['integer_id'].nil? })
97
+ documents.each { |document| document['integer_id'] ||= integer_ids.shift }
98
+ end
99
+ collection.insert_many(documents)
100
+ end
101
+
65
102
  def bulk_insert_attributes(version)
66
103
  version = version.is_a?(self) ? version : new(version)
67
104
  attrs = version.attributes.dup
@@ -91,6 +128,17 @@ module MongoTrails
91
128
 
92
129
  private
93
130
 
131
+ # as_json turns ObjectIds and times into strings; every other field type survives JSON as is.
132
+ def from_json_value(name, value)
133
+ return value unless value.is_a?(String)
134
+
135
+ type = fields[name]&.type
136
+ return BSON::ObjectId.legal?(value) ? BSON::ObjectId.from_string(value) : value if type == BSON::ObjectId
137
+ return (Time.zone || Time).parse(value) if [Time, DateTime].include?(type)
138
+
139
+ value
140
+ end
141
+
94
142
  def counter_key(scope)
95
143
  "#{name}:#{scope || prefix_map}"
96
144
  end
@@ -152,6 +200,17 @@ module MongoTrails
152
200
  schedule_version_persistence(bang: true)
153
201
  end
154
202
 
203
+ # Queues the write of this version for PaperTrail::WriteVersionWorker (or the configured sidekiq_worker).
204
+ def enqueue_write
205
+ worker = defined?(PaperTrail.config.sidekiq_worker.queue) ? PaperTrail.config.sidekiq_worker : PaperTrail::WriteVersionWorker
206
+ args = attributes.as_json
207
+ if worker == PaperTrail::WriteVersionWorker
208
+ worker.set(PaperTrail.config.sidekiq_options).perform_async(args)
209
+ else
210
+ worker.perform_async(args)
211
+ end
212
+ end
213
+
155
214
  def initialize(data)
156
215
  data = data.to_h.deep_symbolize_keys
157
216
  item = data.delete(:item)
@@ -236,6 +295,8 @@ module MongoTrails
236
295
  end
237
296
 
238
297
  def assign_integer_id
298
+ return unless self.class.integer_id_enabled?
299
+
239
300
  self.integer_id ||= self.class.next_integer_id
240
301
  end
241
302
 
@@ -248,10 +309,19 @@ module MongoTrails
248
309
  current_time
249
310
  end
250
311
 
312
+ # Inside a transaction this runs from VersionCommitWrap#committed!, among the source record's after-commit
313
+ # callbacks, so an error raised here skips every callback queued behind it. PaperTrail.config.on_write_error
314
+ # receives the connection errors instead of letting them raise; without a handler they raise as before.
251
315
  def persist_version(version, bang: false)
252
- return version.send(:async_save!) if sidekiq_enabled?
316
+ return version.enqueue_write if sidekiq_enabled?
253
317
 
254
318
  bang ? version.save! : version.save
319
+ rescue *WRITE_ERRORS => e
320
+ handler = PaperTrail.config.on_write_error
321
+ raise unless handler
322
+
323
+ handler.call(e, version)
324
+ nil
255
325
  end
256
326
 
257
327
  def parse_time(value)
@@ -273,14 +343,7 @@ module MongoTrails
273
343
  @parser ||= URI::Parser.new
274
344
  end
275
345
 
276
- def async_save!
277
- worker = defined?(PaperTrail.config.sidekiq_worker.queue) ? PaperTrail.config.sidekiq_worker : PaperTrail::WriteVersionWorker
278
- args = attributes.as_json
279
- if worker == PaperTrail::WriteVersionWorker
280
- worker.set(PaperTrail.config.sidekiq_options).perform_async(args)
281
- else
282
- worker.perform_async(args)
283
- end
284
- end
346
+ # Kept for callers of the old private name; use #enqueue_write.
347
+ alias async_save! enqueue_write
285
348
  end
286
349
  end
@@ -4,8 +4,12 @@ module PaperTrail
4
4
  class WriteVersionWorker
5
5
  include Sidekiq::Job
6
6
 
7
+ # The job carries a version that was already built, so it is rebuilt as is rather than built again. A retry after a
8
+ # write whose acknowledgement was lost finds its _id already stored: the version is written, so the job is done.
7
9
  def perform(obj)
8
- MongoTrails::Version.new(obj).save!
10
+ MongoTrails::Version.rebuild(obj).save!
11
+ rescue Mongo::Error::OperationFailure => e
12
+ raise unless MongoTrails::Version.duplicate_key_error?(e)
9
13
  end
10
14
  end
11
15
  end
@@ -43,23 +43,8 @@ module PaperTrail
43
43
  def record_update_columns(changes)
44
44
  return unless enabled?
45
45
 
46
- save_update_columns_version(changes)
47
- end
48
-
49
- # Records a 'restore' version for a soft-deleted record brought back outside Paper Trail's callbacks,
50
- # e.g. by Paranoia, which writes with update_columns. `changes` maps each restored column to
51
- # [before, after], such as { 'deleted_at' => [deleted_at, nil] }.
52
- def record_restore(changes)
53
- return unless enabled?
54
-
55
- save_update_columns_version(changes, event: MongoTrails::RESTORE_EVENT)
56
- end
57
-
58
- private
59
-
60
- def save_update_columns_version(changes, event: nil)
61
- data = Events::Update.new(@record, false, false, changes).data.merge(data_for_update_columns)
62
- data[:event] = event if event
46
+ event = Events::Update.new(@record, false, false, changes)
47
+ data = event.data.merge(data_for_update_columns)
63
48
  versions_assoc = @record.send(@record.class.versions_association_name)
64
49
  version = versions_assoc.new(data)
65
50
  return if exceeds_record_size_limit?(version)
@@ -68,6 +53,8 @@ module PaperTrail
68
53
  version.save_version
69
54
  end
70
55
 
56
+ private
57
+
71
58
  def exceeds_record_size_limit?(version)
72
59
  size_limit = PaperTrail.config.mongo_trails_config&.dig(:record_size_limit)
73
60
  size_limit && size_limit.to_i < version.to_json.to_s.bytesize
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MongoTrails
4
- VERSION = '17.0.1'
4
+ VERSION = '17.1.0'
5
5
  end
data/lib/mongo_trails.rb CHANGED
@@ -12,7 +12,6 @@ require 'mongo_trails/events/base'
12
12
  require 'mongo_trails/model_config'
13
13
  require 'mongo_trails/version_concern'
14
14
  require 'mongo_trails/record_trail'
15
- require 'mongo_trails/restorable'
16
15
 
17
16
  ActiveSupport.on_load(:active_record) do
18
17
  require 'mongo_trails/mongo_support/version'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mongo_trails
3
3
  version: !ruby/object:Gem::Version
4
- version: 17.0.1
4
+ version: 17.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alex Tsirel
@@ -123,7 +123,6 @@ files:
123
123
  - lib/mongo_trails/mongo_support/version_commit_wrap.rb
124
124
  - lib/mongo_trails/mongo_support/write_version_worker.rb
125
125
  - lib/mongo_trails/record_trail.rb
126
- - lib/mongo_trails/restorable.rb
127
126
  - lib/mongo_trails/version.rb
128
127
  - lib/mongo_trails/version_concern.rb
129
128
  - mongo_trails.gemspec
@@ -1,46 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module MongoTrails
4
- # Paper Trail event of the version recorded when a soft-deleted record is restored (RecordTrail#record_restore)
5
- RESTORE_EVENT = 'restore'
6
-
7
- # Records a 'restore' version when a Paranoia model is restored. Paranoia restores with update_columns,
8
- # which Paper Trail never sees, so the version history would otherwise jump from 'destroy' to the next update.
9
- #
10
- # class Post < ActiveRecord::Base
11
- # acts_as_paranoid
12
- # has_paper_trail
13
- # include MongoTrails::Restorable
14
- # end
15
- #
16
- # Include it after acts_as_paranoid, which defines the restore callbacks. The version's changes are the columns
17
- # Paranoia writes on restore (paranoia_restore_attributes), from their deleted values to the restored ones.
18
- # Paranoia runs the callbacks for associated records outside the recovery window too, so only a record that
19
- # was actually restored gets a version.
20
- module Restorable
21
- extend ActiveSupport::Concern
22
-
23
- included do
24
- before_restore :remember_mongo_trails_deleted_attributes
25
- after_restore :record_mongo_trails_restore
26
- end
27
-
28
- private
29
-
30
- def remember_mongo_trails_deleted_attributes
31
- @mongo_trails_deleted_attributes = paranoia_destroyed? ? attributes.slice(*mongo_trails_restore_columns) : nil
32
- end
33
-
34
- def record_mongo_trails_restore
35
- deleted_attributes = @mongo_trails_deleted_attributes
36
- @mongo_trails_deleted_attributes = nil
37
- return if deleted_attributes.nil? || paranoia_destroyed?
38
-
39
- paper_trail.record_restore(deleted_attributes.to_h { |column, value| [column, [value, self[column]]] })
40
- end
41
-
42
- def mongo_trails_restore_columns
43
- send(:paranoia_restore_attributes).keys.map(&:to_s)
44
- end
45
- end
46
- end