mongo_trails 10.3.1

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.
Files changed (50) hide show
  1. checksums.yaml +7 -0
  2. data/.gitattributes +2 -0
  3. data/.gitignore +1 -0
  4. data/.travis.yml +13 -0
  5. data/Appraisals +7 -0
  6. data/Gemfile +7 -0
  7. data/Gemfile.lock +62 -0
  8. data/LICENSE +20 -0
  9. data/README.md +36 -0
  10. data/Rakefile +13 -0
  11. data/gemfiles/rails_5.gemfile +9 -0
  12. data/gemfiles/rails_5.gemfile.lock +63 -0
  13. data/gemfiles/rails_6.gemfile +9 -0
  14. data/gemfiles/rails_6.gemfile.lock +63 -0
  15. data/lib/mongo_trails.rb +154 -0
  16. data/lib/mongo_trails/attribute_serializers/README.md +10 -0
  17. data/lib/mongo_trails/attribute_serializers/attribute_serializer_factory.rb +27 -0
  18. data/lib/mongo_trails/attribute_serializers/cast_attribute_serializer.rb +51 -0
  19. data/lib/mongo_trails/attribute_serializers/object_attribute.rb +41 -0
  20. data/lib/mongo_trails/attribute_serializers/object_changes_attribute.rb +44 -0
  21. data/lib/mongo_trails/cleaner.rb +60 -0
  22. data/lib/mongo_trails/compatibility.rb +51 -0
  23. data/lib/mongo_trails/config.rb +41 -0
  24. data/lib/mongo_trails/events/base.rb +323 -0
  25. data/lib/mongo_trails/events/create.rb +32 -0
  26. data/lib/mongo_trails/events/destroy.rb +42 -0
  27. data/lib/mongo_trails/events/update.rb +60 -0
  28. data/lib/mongo_trails/frameworks/cucumber.rb +33 -0
  29. data/lib/mongo_trails/frameworks/rails.rb +4 -0
  30. data/lib/mongo_trails/frameworks/rails/controller.rb +109 -0
  31. data/lib/mongo_trails/frameworks/rails/engine.rb +43 -0
  32. data/lib/mongo_trails/frameworks/rspec.rb +43 -0
  33. data/lib/mongo_trails/frameworks/rspec/helpers.rb +29 -0
  34. data/lib/mongo_trails/has_paper_trail.rb +86 -0
  35. data/lib/mongo_trails/model_config.rb +249 -0
  36. data/lib/mongo_trails/mongo_support/config.rb +9 -0
  37. data/lib/mongo_trails/mongo_support/version.rb +56 -0
  38. data/lib/mongo_trails/queries/versions/where_object.rb +65 -0
  39. data/lib/mongo_trails/queries/versions/where_object_changes.rb +75 -0
  40. data/lib/mongo_trails/record_history.rb +51 -0
  41. data/lib/mongo_trails/record_trail.rb +304 -0
  42. data/lib/mongo_trails/reifier.rb +130 -0
  43. data/lib/mongo_trails/request.rb +166 -0
  44. data/lib/mongo_trails/serializers/json.rb +46 -0
  45. data/lib/mongo_trails/serializers/yaml.rb +43 -0
  46. data/lib/mongo_trails/type_serializers/postgres_array_serializer.rb +48 -0
  47. data/lib/mongo_trails/version_concern.rb +336 -0
  48. data/lib/mongo_trails/version_number.rb +23 -0
  49. data/mongo_trails.gemspec +38 -0
  50. metadata +180 -0
@@ -0,0 +1,249 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PaperTrail
4
+ # Configures an ActiveRecord model, mostly at application boot time, but also
5
+ # sometimes mid-request, with methods like enable/disable.
6
+ class ModelConfig
7
+ E_CANNOT_RECORD_AFTER_DESTROY = <<-STR.strip_heredoc.freeze
8
+ paper_trail.on_destroy(:after) is incompatible with ActiveRecord's
9
+ belongs_to_required_by_default. Use on_destroy(:before)
10
+ or disable belongs_to_required_by_default.
11
+ STR
12
+ E_HPT_ABSTRACT_CLASS = <<~STR.squish.freeze
13
+ An application model (%s) has been configured to use PaperTrail (via
14
+ `has_paper_trail`), but the version model it has been told to use (%s) is
15
+ an `abstract_class`. This could happen when an advanced feature called
16
+ Custom Version Classes (http://bit.ly/2G4ch0G) is misconfigured. When all
17
+ version classes are custom, PaperTrail::Version is configured to be an
18
+ `abstract_class`. This is fine, but all application models must be
19
+ configured to use concrete (not abstract) version models.
20
+ STR
21
+ E_MODEL_LIMIT_REQUIRES_ITEM_SUBTYPE = <<~STR.squish.freeze
22
+ To use PaperTrail's per-model limit in your %s model, you must have an
23
+ item_subtype column in your versions table. See documentation sections
24
+ 2.e.1 Per-model limit, and 4.b.1 The optional item_subtype column.
25
+ STR
26
+ DPR_PASSING_ASSOC_NAME_DIRECTLY_TO_VERSIONS_OPTION = <<~STR.squish
27
+ Passing versions association name as `has_paper_trail versions: %{versions_name}`
28
+ is deprecated. Use `has_paper_trail versions: {name: %{versions_name}}` instead.
29
+ The hash you pass to `versions:` is now passed directly to `has_many`.
30
+ STR
31
+ DPR_CLASS_NAME_OPTION = <<~STR.squish
32
+ Passing Version class name as `has_paper_trail class_name: %{class_name}`
33
+ is deprecated. Use `has_paper_trail versions: {class_name: %{class_name}}`
34
+ instead. The hash you pass to `versions:` is now passed directly to `has_many`.
35
+ STR
36
+
37
+ def initialize(model_class)
38
+ @model_class = model_class
39
+ end
40
+
41
+ # Adds a callback that records a version after a "create" event.
42
+ #
43
+ # @api public
44
+ def on_create
45
+ @model_class.after_create { |r|
46
+ r.paper_trail.record_create if r.paper_trail.save_version?
47
+ }
48
+ return if @model_class.paper_trail_options[:on].include?(:create)
49
+ @model_class.paper_trail_options[:on] << :create
50
+ end
51
+
52
+ # Adds a callback that records a version before or after a "destroy" event.
53
+ #
54
+ # @api public
55
+ def on_destroy(recording_order = "before")
56
+ unless %w[after before].include?(recording_order.to_s)
57
+ raise ArgumentError, 'recording order can only be "after" or "before"'
58
+ end
59
+
60
+ if recording_order.to_s == "after" && cannot_record_after_destroy?
61
+ raise E_CANNOT_RECORD_AFTER_DESTROY
62
+ end
63
+
64
+ @model_class.send(
65
+ "#{recording_order}_destroy",
66
+ lambda do |r|
67
+ return unless r.paper_trail.save_version?
68
+ r.paper_trail.record_destroy(recording_order)
69
+ end
70
+ )
71
+
72
+ return if @model_class.paper_trail_options[:on].include?(:destroy)
73
+ @model_class.paper_trail_options[:on] << :destroy
74
+ end
75
+
76
+ # Adds a callback that records a version after an "update" event.
77
+ #
78
+ # @api public
79
+ def on_update
80
+ @model_class.before_save { |r|
81
+ r.paper_trail.reset_timestamp_attrs_for_update_if_needed
82
+ }
83
+ @model_class.after_update { |r|
84
+ if r.paper_trail.save_version?
85
+ r.paper_trail.record_update(
86
+ force: false,
87
+ in_after_callback: true,
88
+ is_touch: false
89
+ )
90
+ end
91
+ }
92
+ @model_class.after_update { |r|
93
+ r.paper_trail.clear_version_instance
94
+ }
95
+ return if @model_class.paper_trail_options[:on].include?(:update)
96
+ @model_class.paper_trail_options[:on] << :update
97
+ end
98
+
99
+ # Adds a callback that records a version after a "touch" event.
100
+ # @api public
101
+ def on_touch
102
+ @model_class.after_touch { |r|
103
+ r.paper_trail.record_update(
104
+ force: true,
105
+ in_after_callback: true,
106
+ is_touch: true
107
+ )
108
+ }
109
+ end
110
+
111
+ # Set up `@model_class` for PaperTrail. Installs callbacks, associations,
112
+ # "class attributes", instance methods, and more.
113
+ # @api private
114
+ def setup(options = {})
115
+ options[:on] ||= %i[create update destroy touch]
116
+ options[:on] = Array(options[:on]) # Support single symbol
117
+ @model_class.send :include, ::PaperTrail::Model::InstanceMethods
118
+ setup_options(options)
119
+ setup_associations(options)
120
+ check_presence_of_item_subtype_column(options)
121
+ @model_class.after_rollback { paper_trail.clear_rolled_back_versions }
122
+ setup_callbacks_from_options options[:on]
123
+ end
124
+
125
+ def version_class
126
+ @_version_class ||= @model_class.version_class_name.constantize
127
+ end
128
+
129
+ private
130
+
131
+ # Raises an error if the provided class is an `abstract_class`.
132
+ # @api private
133
+ def assert_concrete_activerecord_class(class_name)
134
+ if class_name.constantize.abstract_class?
135
+ raise format(E_HPT_ABSTRACT_CLASS, @model_class, class_name)
136
+ end
137
+ end
138
+
139
+ def cannot_record_after_destroy?
140
+ ::ActiveRecord::Base.belongs_to_required_by_default
141
+ end
142
+
143
+ # Some options require the presence of the `item_subtype` column. Currently
144
+ # only `limit`, but in the future there may be others.
145
+ #
146
+ # @api private
147
+ def check_presence_of_item_subtype_column(options)
148
+ return unless options.key?(:limit)
149
+ return if version_class.item_subtype_column_present?
150
+ raise format(E_MODEL_LIMIT_REQUIRES_ITEM_SUBTYPE, @model_class.name)
151
+ end
152
+
153
+ def check_version_class_name(options)
154
+ # @api private - `version_class_name`
155
+ @model_class.class_attribute :version_class_name
156
+ if options[:class_name]
157
+ ::ActiveSupport::Deprecation.warn(
158
+ format(
159
+ DPR_CLASS_NAME_OPTION,
160
+ class_name: options[:class_name].inspect
161
+ ),
162
+ caller(1)
163
+ )
164
+ options[:versions][:class_name] = options[:class_name]
165
+ end
166
+ @model_class.version_class_name = options[:versions][:class_name] || "PaperTrail::Version"
167
+ end
168
+
169
+ def check_versions_association_name(options)
170
+ # @api private - versions_association_name
171
+ @model_class.class_attribute :versions_association_name
172
+ @model_class.versions_association_name = options[:versions][:name] || :versions
173
+ end
174
+
175
+ def define_has_many_mongo_versions(options)
176
+ options = ensure_versions_option_is_hash(options)
177
+ check_version_class_name(options)
178
+ check_versions_association_name(options)
179
+
180
+ @model_class.class_eval <<-RUBY, __FILE__, __LINE__ + 1
181
+ def #{@model_class.versions_association_name}
182
+ #{@model_class.version_class_name.constantize}
183
+ .where(item_type: #{@model_class}).and(item_id: self.id).order(created_at: :asc)
184
+ end
185
+ RUBY
186
+ end
187
+
188
+ def ensure_versions_option_is_hash(options)
189
+ unless options[:versions].is_a?(Hash)
190
+ if options[:versions]
191
+ ::ActiveSupport::Deprecation.warn(
192
+ format(
193
+ DPR_PASSING_ASSOC_NAME_DIRECTLY_TO_VERSIONS_OPTION,
194
+ versions_name: options[:versions].inspect
195
+ ),
196
+ caller(1)
197
+ )
198
+ end
199
+ options[:versions] = {
200
+ name: options[:versions]
201
+ }
202
+ end
203
+ options
204
+ end
205
+
206
+ def get_versions_scope(options)
207
+ options[:versions][:scope] || -> { order(model.timestamp_sort_order) }
208
+ end
209
+
210
+ def setup_associations(options)
211
+ # @api private - version_association_name
212
+ @model_class.class_attribute :version_association_name
213
+ @model_class.version_association_name = options[:version] || :version
214
+
215
+ # The version this instance was reified from.
216
+ # @api public
217
+ @model_class.send :attr_accessor, @model_class.version_association_name
218
+
219
+ # @api public - paper_trail_event
220
+ @model_class.send :attr_accessor, :paper_trail_event
221
+
222
+ define_has_many_mongo_versions(options)
223
+ end
224
+
225
+ def setup_callbacks_from_options(options_on = [])
226
+ options_on.each do |event|
227
+ public_send("on_#{event}")
228
+ end
229
+ end
230
+
231
+ def setup_options(options)
232
+ # @api public - paper_trail_options - Let's encourage plugins to use eg.
233
+ # `paper_trail_options[:versions][:class_name]` rather than
234
+ # `version_class_name` because the former is documented and the latter is
235
+ # not.
236
+ @model_class.class_attribute :paper_trail_options
237
+ @model_class.paper_trail_options = options.dup
238
+
239
+ %i[ignore skip only].each do |k|
240
+ @model_class.paper_trail_options[k] = [@model_class.paper_trail_options[k]].
241
+ flatten.
242
+ compact.
243
+ map { |attr| attr.is_a?(Hash) ? attr.stringify_keys : attr.to_s }
244
+ end
245
+
246
+ @model_class.paper_trail_options[:meta] ||= {}
247
+ end
248
+ end
249
+ end
@@ -0,0 +1,9 @@
1
+ require 'mongoid'
2
+
3
+ Mongoid.configure do |config|
4
+ config.clients.default = PaperTrail.config.mongo_config
5
+
6
+ config.log_level = :error
7
+ end
8
+
9
+ Mongoid::QueryCache.enabled = false
@@ -0,0 +1,56 @@
1
+ require "mongoid"
2
+ require "autoinc"
3
+
4
+ class AutoIncrementCounters
5
+ include Mongoid::Document
6
+ end
7
+
8
+ module PaperTrail
9
+ class Version
10
+ include PaperTrail::VersionConcern
11
+ include Mongoid::Document
12
+ include Mongoid::Autoinc
13
+
14
+ store_in collection: ->() { "#{PaperTrail::Version.prefix_map}_versions" }
15
+
16
+ field :item_type, type: String
17
+ field :item_id, type: Integer
18
+ field :event, type: String
19
+ field :whodunnit, type: String
20
+ field :object, type: Hash
21
+ field :object_changes, type: Hash
22
+ field :created_at, type: DateTime
23
+ field :integer_id, type: Integer
24
+
25
+ increments :integer_id, scope: -> { PaperTrail::Version.prefix_map }
26
+
27
+ class << self
28
+ def reset
29
+ Mongoid::QueryCache.clear_cache
30
+ end
31
+
32
+ def find(id)
33
+ find_by(integer_id: id)
34
+ end
35
+
36
+ def prefix_map
37
+ (PaperTrail.config.mongo_prefix.is_a?(Proc) ? PaperTrail.config.mongo_prefix.call : 'paper_trail') || 'paper_trail'
38
+ end
39
+ end
40
+
41
+ def initialize(data)
42
+ item = data.delete(:item)
43
+ if item.present?
44
+ data[:item_type] = item.class.name
45
+ data[:item_id] = item.id
46
+ end
47
+ data[:created_at] = Time.zone&.now || Time.now
48
+
49
+ super
50
+ end
51
+
52
+ def item
53
+ item_type.constantize.find(item_id)
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PaperTrail
4
+ module Queries
5
+ module Versions
6
+ # For public API documentation, see `where_object` in
7
+ # `mongo_trails/version_concern.rb`.
8
+ # @api private
9
+ class WhereObject
10
+ # - version_model_class - The class that VersionConcern was mixed into.
11
+ # - attributes - A `Hash` of attributes and values. See the public API
12
+ # documentation for details.
13
+ # @api private
14
+ def initialize(version_model_class, attributes)
15
+ @version_model_class = version_model_class
16
+ @attributes = attributes
17
+ end
18
+
19
+ # @api private
20
+ def execute
21
+ column = @version_model_class.columns_hash["object"]
22
+ raise "where_object can't be called without an object column" unless column
23
+
24
+ case column.type
25
+ when :jsonb
26
+ jsonb
27
+ when :json
28
+ json
29
+ else
30
+ text
31
+ end
32
+ end
33
+
34
+ private
35
+
36
+ # @api private
37
+ def json
38
+ predicates = []
39
+ values = []
40
+ @attributes.each do |field, value|
41
+ predicates.push "object->>? = ?"
42
+ values.concat([field, value.to_s])
43
+ end
44
+ sql = predicates.join(" and ")
45
+ @version_model_class.where(sql, *values)
46
+ end
47
+
48
+ # @api private
49
+ def jsonb
50
+ @version_model_class.where("object @> ?", @attributes.to_json)
51
+ end
52
+
53
+ # @api private
54
+ def text
55
+ arel_field = @version_model_class.arel_table[:object]
56
+ where_conditions = @attributes.map { |field, value|
57
+ ::PaperTrail.serializer.where_object_condition(arel_field, field, value)
58
+ }
59
+ where_conditions = where_conditions.reduce { |a, e| a.and(e) }
60
+ @version_model_class.where(where_conditions)
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PaperTrail
4
+ module Queries
5
+ module Versions
6
+ # For public API documentation, see `where_object_changes` in
7
+ # `mongo_trails/version_concern.rb`.
8
+ # @api private
9
+ class WhereObjectChanges
10
+ # - version_model_class - The class that VersionConcern was mixed into.
11
+ # - attributes - A `Hash` of attributes and values. See the public API
12
+ # documentation for details.
13
+ # @api private
14
+ def initialize(version_model_class, attributes)
15
+ @version_model_class = version_model_class
16
+
17
+ # Currently, this `deep_dup` is necessary because the `jsonb` branch
18
+ # modifies `@attributes`, and that would be a nasty suprise for
19
+ # consumers of this class.
20
+ # TODO: Stop modifying `@attributes`, then remove `deep_dup`.
21
+ @attributes = attributes.deep_dup
22
+ end
23
+
24
+ # @api private
25
+ def execute
26
+ if PaperTrail.config.object_changes_adapter&.respond_to?(:where_object_changes)
27
+ return PaperTrail.config.object_changes_adapter.where_object_changes(
28
+ @version_model_class, @attributes
29
+ )
30
+ end
31
+ case @version_model_class.columns_hash["object_changes"].type
32
+ when :jsonb
33
+ jsonb
34
+ when :json
35
+ json
36
+ else
37
+ text
38
+ end
39
+ end
40
+
41
+ private
42
+
43
+ # @api private
44
+ def json
45
+ predicates = []
46
+ values = []
47
+ @attributes.each do |field, value|
48
+ predicates.push(
49
+ "((object_changes->>? ILIKE ?) OR (object_changes->>? ILIKE ?))"
50
+ )
51
+ values.concat([field, "[#{value.to_json},%", field, "[%,#{value.to_json}]%"])
52
+ end
53
+ sql = predicates.join(" and ")
54
+ @version_model_class.where(sql, *values)
55
+ end
56
+
57
+ # @api private
58
+ def jsonb
59
+ @attributes.each { |field, value| @attributes[field] = [value] }
60
+ @version_model_class.where("object_changes @> ?", @attributes.to_json)
61
+ end
62
+
63
+ # @api private
64
+ def text
65
+ arel_field = @version_model_class.arel_table[:object_changes]
66
+ where_conditions = @attributes.map { |field, value|
67
+ ::PaperTrail.serializer.where_object_changes_condition(arel_field, field, value)
68
+ }
69
+ where_conditions = where_conditions.reduce { |a, e| a.and(e) }
70
+ @version_model_class.where(where_conditions)
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end