mongo_trails 10.3.1

Sign up to get free protection for your applications and to get access to all the features.
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