snail_trail 0.0.1 → 0.0.2

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 (51) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +1 -1
  3. data/lib/generators/snail_trail/install/USAGE +3 -0
  4. data/lib/generators/snail_trail/install/install_generator.rb +108 -0
  5. data/lib/generators/snail_trail/install/templates/add_object_changes_to_versions.rb.erb +12 -0
  6. data/lib/generators/snail_trail/install/templates/add_transaction_id_column_to_versions.rb.erb +12 -0
  7. data/lib/generators/snail_trail/install/templates/create_versions.rb.erb +41 -0
  8. data/lib/generators/snail_trail/migration_generator.rb +38 -0
  9. data/lib/generators/snail_trail/update_item_subtype/USAGE +4 -0
  10. data/lib/generators/snail_trail/update_item_subtype/templates/update_versions_for_item_subtype.rb.erb +85 -0
  11. data/lib/generators/snail_trail/update_item_subtype/update_item_subtype_generator.rb +19 -0
  12. data/lib/snail_trail/attribute_serializers/README.md +10 -0
  13. data/lib/snail_trail/attribute_serializers/attribute_serializer_factory.rb +41 -0
  14. data/lib/snail_trail/attribute_serializers/cast_attribute_serializer.rb +51 -0
  15. data/lib/snail_trail/attribute_serializers/object_attribute.rb +51 -0
  16. data/lib/snail_trail/attribute_serializers/object_changes_attribute.rb +54 -0
  17. data/lib/snail_trail/cleaner.rb +60 -0
  18. data/lib/snail_trail/compatibility.rb +51 -0
  19. data/lib/snail_trail/config.rb +40 -0
  20. data/lib/snail_trail/errors.rb +33 -0
  21. data/lib/snail_trail/events/base.rb +343 -0
  22. data/lib/snail_trail/events/create.rb +32 -0
  23. data/lib/snail_trail/events/destroy.rb +42 -0
  24. data/lib/snail_trail/events/update.rb +76 -0
  25. data/lib/snail_trail/frameworks/active_record/models/snail_trail/version.rb +16 -0
  26. data/lib/snail_trail/frameworks/active_record.rb +12 -0
  27. data/lib/snail_trail/frameworks/cucumber.rb +33 -0
  28. data/lib/snail_trail/frameworks/rails/controller.rb +103 -0
  29. data/lib/snail_trail/frameworks/rails/railtie.rb +34 -0
  30. data/lib/snail_trail/frameworks/rails.rb +3 -0
  31. data/lib/snail_trail/frameworks/rspec/helpers.rb +29 -0
  32. data/lib/snail_trail/frameworks/rspec.rb +42 -0
  33. data/lib/snail_trail/has_snail_trail.rb +92 -0
  34. data/lib/snail_trail/model_config.rb +265 -0
  35. data/lib/snail_trail/queries/versions/where_attribute_changes.rb +50 -0
  36. data/lib/snail_trail/queries/versions/where_object.rb +65 -0
  37. data/lib/snail_trail/queries/versions/where_object_changes.rb +70 -0
  38. data/lib/snail_trail/queries/versions/where_object_changes_from.rb +57 -0
  39. data/lib/snail_trail/queries/versions/where_object_changes_to.rb +57 -0
  40. data/lib/snail_trail/record_history.rb +51 -0
  41. data/lib/snail_trail/record_trail.rb +375 -0
  42. data/lib/snail_trail/reifier.rb +147 -0
  43. data/lib/snail_trail/request.rb +180 -0
  44. data/lib/snail_trail/serializers/json.rb +36 -0
  45. data/lib/snail_trail/serializers/yaml.rb +68 -0
  46. data/lib/snail_trail/type_serializers/postgres_array_serializer.rb +35 -0
  47. data/lib/snail_trail/version_concern.rb +407 -0
  48. data/lib/snail_trail/version_number.rb +23 -0
  49. data/lib/snail_trail.rb +141 -1
  50. metadata +369 -13
  51. data/lib/snail_trail/version.rb +0 -5
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SnailTrail
4
+ module Rails
5
+ # Extensions to rails controllers. Provides convenient ways to pass certain
6
+ # information to the model layer, with `controller_info` and `whodunnit`.
7
+ # Also includes a convenient on/off switch,
8
+ # `snail_trail_enabled_for_controller`.
9
+ module Controller
10
+ def self.included(controller)
11
+ controller.before_action(
12
+ :set_snail_trail_enabled_for_controller,
13
+ :set_snail_trail_controller_info
14
+ )
15
+ end
16
+
17
+ protected
18
+
19
+ # Returns the user who is responsible for any changes that occur.
20
+ # By default this calls `current_user` and returns the result.
21
+ #
22
+ # Override this method in your controller to call a different
23
+ # method, e.g. `current_person`, or anything you like.
24
+ #
25
+ # @api public
26
+ def user_for_snail_trail
27
+ return unless defined?(current_user)
28
+ current_user.try(:id) || current_user
29
+ end
30
+
31
+ # Returns any information about the controller or request that you
32
+ # want SnailTrail to store alongside any changes that occur. By
33
+ # default this returns an empty hash.
34
+ #
35
+ # Override this method in your controller to return a hash of any
36
+ # information you need. The hash's keys must correspond to columns
37
+ # in your `versions` table, so don't forget to add any new columns
38
+ # you need.
39
+ #
40
+ # For example:
41
+ #
42
+ # {:ip => request.remote_ip, :user_agent => request.user_agent}
43
+ #
44
+ # The columns `ip` and `user_agent` must exist in your `versions` # table.
45
+ #
46
+ # Use the `:meta` option to
47
+ # `SnailTrail::Model::ClassMethods.has_snail_trail` to store any extra
48
+ # model-level data you need.
49
+ #
50
+ # @api public
51
+ def info_for_snail_trail
52
+ {}
53
+ end
54
+
55
+ # Returns `true` (default) or `false` depending on whether SnailTrail
56
+ # should be active for the current request.
57
+ #
58
+ # Override this method in your controller to specify when SnailTrail
59
+ # should be off.
60
+ #
61
+ # ```
62
+ # def snail_trail_enabled_for_controller
63
+ # # Don't omit `super` without a good reason.
64
+ # super && request.user_agent != 'Disable User-Agent'
65
+ # end
66
+ # ```
67
+ #
68
+ # @api public
69
+ def snail_trail_enabled_for_controller
70
+ ::SnailTrail.enabled?
71
+ end
72
+
73
+ private
74
+
75
+ # Tells SnailTrail whether versions should be saved in the current
76
+ # request.
77
+ #
78
+ # @api public
79
+ def set_snail_trail_enabled_for_controller
80
+ ::SnailTrail.request.enabled = snail_trail_enabled_for_controller
81
+ end
82
+
83
+ # Tells SnailTrail who is responsible for any changes that occur.
84
+ #
85
+ # @api public
86
+ def set_snail_trail_whodunnit
87
+ if ::SnailTrail.request.enabled?
88
+ ::SnailTrail.request.whodunnit = user_for_snail_trail
89
+ end
90
+ end
91
+
92
+ # Tells SnailTrail any information from the controller you want to store
93
+ # alongside any changes that occur.
94
+ #
95
+ # @api public
96
+ def set_snail_trail_controller_info
97
+ if ::SnailTrail.request.enabled?
98
+ ::SnailTrail.request.controller_info = info_for_snail_trail
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SnailTrail
4
+ # Represents code to load within Rails framework. See documentation in
5
+ # `railties/lib/rails/railtie.rb`.
6
+ # @api private
7
+ class Railtie < ::Rails::Railtie
8
+ # SnailTrail only has one initializer.
9
+ #
10
+ # We specify `before: "load_config_initializers"` to ensure that the ST
11
+ # initializer happens before "app initializers" (those defined in
12
+ # the app's `config/initalizers`).
13
+ initializer "snail_trail", before: "load_config_initializers" do |app|
14
+ # `on_load` is a "lazy load hook". It "declares a block that will be
15
+ # executed when a Rails component is fully loaded". (See
16
+ # `active_support/lazy_load_hooks.rb`)
17
+ ActiveSupport.on_load(:action_controller) do
18
+ require "snail_trail/frameworks/rails/controller"
19
+
20
+ # Mix our extensions into `ActionController::Base`, which is `self`
21
+ # because of the `class_eval` in `lazy_load_hooks.rb`.
22
+ include SnailTrail::Rails::Controller
23
+ end
24
+
25
+ ActiveSupport.on_load(:active_record) do
26
+ require "snail_trail/frameworks/active_record"
27
+ end
28
+
29
+ if Gem::Version.new(::Rails::VERSION::STRING) >= Gem::Version.new("7.1")
30
+ app.deprecators[:snail_trail] = SnailTrail.deprecator
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "snail_trail/frameworks/rails/railtie"
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SnailTrail
4
+ module RSpec
5
+ module Helpers
6
+ # Included in the RSpec configuration in `frameworks/rspec.rb`
7
+ module InstanceMethods
8
+ # enable versioning for specific blocks (at instance-level)
9
+ def with_versioning
10
+ was_enabled = ::SnailTrail.enabled?
11
+ ::SnailTrail.enabled = true
12
+ yield
13
+ ensure
14
+ ::SnailTrail.enabled = was_enabled
15
+ end
16
+ end
17
+
18
+ # Extended by the RSpec configuration in `frameworks/rspec.rb`
19
+ module ClassMethods
20
+ # enable versioning for specific blocks (at class-level)
21
+ def with_versioning(&block)
22
+ context "with versioning", versioning: true do
23
+ class_exec(&block)
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rspec/core"
4
+ require "rspec/matchers"
5
+ require "snail_trail/frameworks/rspec/helpers"
6
+
7
+ RSpec.configure do |config|
8
+ config.include SnailTrail::RSpec::Helpers::InstanceMethods
9
+ config.extend SnailTrail::RSpec::Helpers::ClassMethods
10
+
11
+ config.before(:each) do
12
+ SnailTrail.enabled = false
13
+ SnailTrail.request.enabled = true
14
+ SnailTrail.request.whodunnit = nil
15
+ SnailTrail.request.controller_info = {} if defined?(Rails) && defined?(RSpec::Rails)
16
+ end
17
+
18
+ config.before(:each, versioning: true) do
19
+ SnailTrail.enabled = true
20
+ end
21
+ end
22
+
23
+ RSpec::Matchers.define :be_versioned do
24
+ # check to see if the model has `has_snail_trail` declared on it
25
+ match { |actual| actual.is_a?(SnailTrail::Model::InstanceMethods) }
26
+ end
27
+
28
+ RSpec::Matchers.define :have_a_version_with do |attributes|
29
+ # check if the model has a version with the specified attributes
30
+ match do |actual|
31
+ versions_association = actual.class.versions_association_name
32
+ actual.send(versions_association).where_object(attributes).any?
33
+ end
34
+ end
35
+
36
+ RSpec::Matchers.define :have_a_version_with_changes do |attributes|
37
+ # check if the model has a version changes with the specified attributes
38
+ match do |actual|
39
+ versions_association = actual.class.versions_association_name
40
+ actual.send(versions_association).where_object_changes(attributes).any?
41
+ end
42
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "snail_trail/attribute_serializers/object_attribute"
4
+ require "snail_trail/attribute_serializers/object_changes_attribute"
5
+ require "snail_trail/model_config"
6
+ require "snail_trail/record_trail"
7
+
8
+ module SnailTrail
9
+ # Extensions to `ActiveRecord::Base`. See `frameworks/active_record.rb`.
10
+ # It is our goal to have the smallest possible footprint here, because
11
+ # `ActiveRecord::Base` is a very crowded namespace! That is why we introduced
12
+ # `.snail_trail` and `#snail_trail`.
13
+ module Model
14
+ def self.included(base)
15
+ base.extend ClassMethods
16
+ end
17
+
18
+ # :nodoc:
19
+ module ClassMethods
20
+ # Declare this in your model to track every create, update, and destroy.
21
+ # Each version of the model is available in the `versions` association.
22
+ #
23
+ # Options:
24
+ #
25
+ # - :on - The events to track (optional; defaults to all of them). Set
26
+ # to an array of `:create`, `:update`, `:destroy` and `:touch` as desired.
27
+ # - :class_name (deprecated) - The name of a custom Version class that
28
+ # includes `SnailTrail::VersionConcern`.
29
+ # - :ignore - An array of attributes for which a new `Version` will not be
30
+ # created if only they change. It can also accept a Hash as an
31
+ # argument where the key is the attribute to ignore (a `String` or
32
+ # `Symbol`), which will only be ignored if the value is a `Proc` which
33
+ # returns truthily.
34
+ # - :if, :unless - Procs that allow to specify conditions when to save
35
+ # versions for an object.
36
+ # - :only - Inverse of `ignore`. A new `Version` will be created only
37
+ # for these attributes if supplied it can also accept a Hash as an
38
+ # argument where the key is the attribute to track (a `String` or
39
+ # `Symbol`), which will only be counted if the value is a `Proc` which
40
+ # returns truthily.
41
+ # - :skip - Fields to ignore completely. As with `ignore`, updates to
42
+ # these fields will not create a new `Version`. In addition, these
43
+ # fields will not be included in the serialized versions of the object
44
+ # whenever a new `Version` is created.
45
+ # - :meta - A hash of extra data to store. You must add a column to the
46
+ # `versions` table for each key. Values are objects or procs (which
47
+ # are called with `self`, i.e. the model with the snail trail). See
48
+ # `SnailTrail::Controller.info_for_snail_trail` for how to store data
49
+ # from the controller.
50
+ # - :versions - Either,
51
+ # - A String (deprecated) - The name to use for the versions
52
+ # association. Default is `:versions`.
53
+ # - A Hash - options passed to `has_many`, plus `name:` and `scope:`.
54
+ # - :version - The name to use for the method which returns the version
55
+ # the instance was reified from. Default is `:version`.
56
+ # - :synchronize_version_creation_timestamp - By default, snail trail
57
+ # sets the `created_at` field for a new Version equal to the `updated_at`
58
+ # column of the model being updated. If you instead want `created_at` to
59
+ # populate with the current timestamp, set this option to `false`.
60
+ #
61
+ # Plugins like the experimental `snail_trail-association_tracking` gem
62
+ # may accept additional options.
63
+ #
64
+ # You can define a default set of options via the configurable
65
+ # `SnailTrail.config.has_snail_trail_defaults` hash in your applications
66
+ # initializer. The hash can contain any of the following options and will
67
+ # provide an overridable default for all models.
68
+ #
69
+ # @api public
70
+ def has_snail_trail(options = {})
71
+ raise Error, "has_snail_trail must be called only once" if self < InstanceMethods
72
+
73
+ defaults = SnailTrail.config.has_snail_trail_defaults
74
+ snail_trail.setup(defaults.merge(options))
75
+ end
76
+
77
+ # @api public
78
+ def snail_trail
79
+ ::SnailTrail::ModelConfig.new(self)
80
+ end
81
+ end
82
+
83
+ # Wrap the following methods in a module so we can include them only in the
84
+ # ActiveRecord models that declare `has_snail_trail`.
85
+ module InstanceMethods
86
+ # @api public
87
+ def snail_trail
88
+ ::SnailTrail::RecordTrail.new(self)
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,265 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SnailTrail
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
8
+ snail_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 SnailTrail (via
14
+ `has_snail_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, SnailTrail::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
+ DPR_PASSING_ASSOC_NAME_DIRECTLY_TO_VERSIONS_OPTION = <<~STR.squish
22
+ Passing versions association name as `has_snail_trail versions: %{versions_name}`
23
+ is deprecated. Use `has_snail_trail versions: {name: %{versions_name}}` instead.
24
+ The hash you pass to `versions:` is now passed directly to `has_many`.
25
+ STR
26
+ DPR_CLASS_NAME_OPTION = <<~STR.squish
27
+ Passing Version class name as `has_snail_trail class_name: %{class_name}`
28
+ is deprecated. Use `has_snail_trail versions: {class_name: %{class_name}}`
29
+ instead. The hash you pass to `versions:` is now passed directly to `has_many`.
30
+ STR
31
+
32
+ def initialize(model_class)
33
+ @model_class = model_class
34
+ end
35
+
36
+ # Adds a callback that records a version after a "create" event.
37
+ #
38
+ # @api public
39
+ def on_create
40
+ @model_class.after_create { |r|
41
+ r.snail_trail.record_create if r.snail_trail.save_version?
42
+ }
43
+ append_option_uniquely(:on, :create)
44
+ end
45
+
46
+ # Adds a callback that records a version before or after a "destroy" event.
47
+ #
48
+ # @api public
49
+ def on_destroy(recording_order = "before")
50
+ assert_valid_recording_order_for_on_destroy(recording_order)
51
+ @model_class.send(
52
+ :"#{recording_order}_destroy",
53
+ lambda do |r|
54
+ return unless r.snail_trail.save_version?
55
+ r.snail_trail.record_destroy(recording_order)
56
+ end
57
+ )
58
+ append_option_uniquely(:on, :destroy)
59
+ end
60
+
61
+ # Adds a callback that records a version after an "update" event.
62
+ #
63
+ # @api public
64
+ def on_update
65
+ @model_class.before_save { |r|
66
+ r.snail_trail.reset_timestamp_attrs_for_update_if_needed
67
+ }
68
+ @model_class.after_update { |r|
69
+ if r.snail_trail.save_version?
70
+ r.snail_trail.record_update(
71
+ force: false,
72
+ in_after_callback: true,
73
+ is_touch: false
74
+ )
75
+ end
76
+ }
77
+ @model_class.after_update { |r|
78
+ r.snail_trail.clear_version_instance
79
+ }
80
+ append_option_uniquely(:on, :update)
81
+ end
82
+
83
+ # Adds a callback that records a version after a "touch" event.
84
+ #
85
+ # Rails < 6.0 (no longer supported by ST) had a bug where dirty-tracking
86
+ # did not occur during a `touch`.
87
+ # (https://github.com/rails/rails/issues/33429) See also:
88
+ # https://github.com/BrandsInsurance/snail_trail/issues/1121
89
+ # https://github.com/BrandsInsurance/snail_trail/issues/1161
90
+ # https://github.com/BrandsInsurance/snail_trail/pull/1285
91
+ #
92
+ # @api public
93
+ def on_touch
94
+ @model_class.after_touch { |r|
95
+ if r.snail_trail.save_version?
96
+ r.snail_trail.record_update(
97
+ force: false,
98
+ in_after_callback: true,
99
+ is_touch: true
100
+ )
101
+ end
102
+ }
103
+ end
104
+
105
+ # Set up `@model_class` for SnailTrail. Installs callbacks, associations,
106
+ # "class attributes", instance methods, and more.
107
+ # @api private
108
+ def setup(options = {})
109
+ options[:on] ||= %i[create update destroy touch]
110
+ options[:on] = Array(options[:on]) # Support single symbol
111
+ @model_class.send :include, ::SnailTrail::Model::InstanceMethods
112
+ setup_options(options)
113
+ setup_associations(options)
114
+ @model_class.after_rollback { snail_trail.clear_rolled_back_versions }
115
+ setup_callbacks_from_options options[:on]
116
+
117
+ setup_transaction_callbacks
118
+ end
119
+
120
+ # @api private
121
+ def version_class
122
+ @version_class ||= @model_class.version_class_name.constantize
123
+ end
124
+
125
+ private
126
+
127
+ # Reset the transaction id when the transaction is closed.
128
+ def setup_transaction_callbacks
129
+ @model_class.after_commit { ::SnailTrail.request.clear_transaction_id }
130
+ @model_class.after_rollback { ::SnailTrail.request.clear_transaction_id }
131
+ end
132
+
133
+ # @api private
134
+ def append_option_uniquely(option, value)
135
+ collection = @model_class.snail_trail_options.fetch(option)
136
+ return if collection.include?(value)
137
+ collection << value
138
+ end
139
+
140
+ # Raises an error if the provided class is an `abstract_class`.
141
+ # @api private
142
+ def assert_concrete_activerecord_class(class_name)
143
+ if class_name.constantize.abstract_class?
144
+ raise Error, format(E_HPT_ABSTRACT_CLASS, @model_class, class_name)
145
+ end
146
+ end
147
+
148
+ # @api private
149
+ def assert_valid_recording_order_for_on_destroy(recording_order)
150
+ unless %w[after before].include?(recording_order.to_s)
151
+ raise ArgumentError, 'recording order can only be "after" or "before"'
152
+ end
153
+
154
+ if recording_order.to_s == "after" && cannot_record_after_destroy?
155
+ raise Error, E_CANNOT_RECORD_AFTER_DESTROY
156
+ end
157
+ end
158
+
159
+ def cannot_record_after_destroy?
160
+ ::ActiveRecord::Base.belongs_to_required_by_default
161
+ end
162
+
163
+ def check_version_class_name(options)
164
+ # @api private - `version_class_name`
165
+ @model_class.class_attribute :version_class_name
166
+ if options[:class_name]
167
+ SnailTrail.deprecator.warn(
168
+ format(
169
+ DPR_CLASS_NAME_OPTION,
170
+ class_name: options[:class_name].inspect
171
+ ),
172
+ caller(1)
173
+ )
174
+ options[:versions][:class_name] = options[:class_name]
175
+ end
176
+ @model_class.version_class_name = options[:versions][:class_name] || "SnailTrail::Version"
177
+ assert_concrete_activerecord_class(@model_class.version_class_name)
178
+ end
179
+
180
+ def check_versions_association_name(options)
181
+ # @api private - versions_association_name
182
+ @model_class.class_attribute :versions_association_name
183
+ @model_class.versions_association_name = options[:versions][:name] || :versions
184
+ end
185
+
186
+ def define_has_many_versions(options)
187
+ options = ensure_versions_option_is_hash(options)
188
+ check_version_class_name(options)
189
+ check_versions_association_name(options)
190
+ scope = get_versions_scope(options)
191
+ @model_class.has_many(
192
+ @model_class.versions_association_name,
193
+ scope,
194
+ class_name: @model_class.version_class_name,
195
+ as: :item,
196
+ **options[:versions].except(:name, :scope)
197
+ )
198
+ end
199
+
200
+ def ensure_versions_option_is_hash(options)
201
+ unless options[:versions].is_a?(Hash)
202
+ if options[:versions]
203
+ SnailTrail.deprecator.warn(
204
+ format(
205
+ DPR_PASSING_ASSOC_NAME_DIRECTLY_TO_VERSIONS_OPTION,
206
+ versions_name: options[:versions].inspect
207
+ ),
208
+ caller(1)
209
+ )
210
+ end
211
+ options[:versions] = {
212
+ name: options[:versions]
213
+ }
214
+ end
215
+ options
216
+ end
217
+
218
+ # Process an `ignore`, `skip`, or `only` option.
219
+ def event_attribute_option(option_name)
220
+ [@model_class.snail_trail_options[option_name]].
221
+ flatten.
222
+ compact.
223
+ map { |attr| attr.is_a?(Hash) ? attr.stringify_keys : attr.to_s }
224
+ end
225
+
226
+ def get_versions_scope(options)
227
+ options[:versions][:scope] || -> { order(model.timestamp_sort_order) }
228
+ end
229
+
230
+ def setup_associations(options)
231
+ # @api private - version_association_name
232
+ @model_class.class_attribute :version_association_name
233
+ @model_class.version_association_name = options[:version] || :version
234
+
235
+ # The version this instance was reified from.
236
+ # @api public
237
+ @model_class.send :attr_accessor, @model_class.version_association_name
238
+
239
+ # @api public - snail_trail_event
240
+ @model_class.send :attr_accessor, :snail_trail_event
241
+
242
+ define_has_many_versions(options)
243
+ end
244
+
245
+ def setup_callbacks_from_options(options_on = [])
246
+ options_on.each do |event|
247
+ public_send(:"on_#{event}")
248
+ end
249
+ end
250
+
251
+ def setup_options(options)
252
+ # @api public - snail_trail_options - Let's encourage plugins to use eg.
253
+ # `snail_trail_options[:versions][:class_name]` rather than
254
+ # `version_class_name` because the former is documented and the latter is
255
+ # not.
256
+ @model_class.class_attribute :snail_trail_options
257
+ @model_class.snail_trail_options = options.dup
258
+
259
+ %i[ignore skip only].each do |k|
260
+ @model_class.snail_trail_options[k] = event_attribute_option(k)
261
+ end
262
+ @model_class.snail_trail_options[:meta] ||= {}
263
+ end
264
+ end
265
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SnailTrail
4
+ module Queries
5
+ module Versions
6
+ # For public API documentation, see `where_attribute_changes` in
7
+ # `snail_trail/version_concern.rb`.
8
+ # @api private
9
+ class WhereAttributeChanges
10
+ # - version_model_class - The class that VersionConcern was mixed into.
11
+ # - attribute - An attribute that changed. See the public API
12
+ # documentation for details.
13
+ # @api private
14
+ def initialize(version_model_class, attribute)
15
+ @version_model_class = version_model_class
16
+ @attribute = attribute
17
+ end
18
+
19
+ # @api private
20
+ def execute
21
+ if SnailTrail.config.object_changes_adapter.respond_to?(:where_attribute_changes)
22
+ return SnailTrail.config.object_changes_adapter.where_attribute_changes(
23
+ @version_model_class, @attribute
24
+ )
25
+ end
26
+ column_type = @version_model_class.columns_hash["object_changes"].type
27
+ case column_type
28
+ when :jsonb, :json
29
+ json
30
+ else
31
+ raise UnsupportedColumnType.new(
32
+ method: "where_attribute_changes",
33
+ expected: "json or jsonb",
34
+ actual: column_type
35
+ )
36
+ end
37
+ end
38
+
39
+ private
40
+
41
+ # @api private
42
+ def json
43
+ sql = "object_changes -> ? IS NOT NULL"
44
+
45
+ @version_model_class.where(sql, @attribute)
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SnailTrail
4
+ module Queries
5
+ module Versions
6
+ # For public API documentation, see `where_object` in
7
+ # `snail_trail/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 Error, "where_object requires 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.push(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
+ ::SnailTrail.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