mongo_trails 10.3.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitattributes +2 -0
- data/.gitignore +1 -0
- data/.travis.yml +13 -0
- data/Appraisals +7 -0
- data/Gemfile +7 -0
- data/Gemfile.lock +62 -0
- data/LICENSE +20 -0
- data/README.md +36 -0
- data/Rakefile +13 -0
- data/gemfiles/rails_5.gemfile +9 -0
- data/gemfiles/rails_5.gemfile.lock +63 -0
- data/gemfiles/rails_6.gemfile +9 -0
- data/gemfiles/rails_6.gemfile.lock +63 -0
- data/lib/mongo_trails.rb +154 -0
- data/lib/mongo_trails/attribute_serializers/README.md +10 -0
- data/lib/mongo_trails/attribute_serializers/attribute_serializer_factory.rb +27 -0
- data/lib/mongo_trails/attribute_serializers/cast_attribute_serializer.rb +51 -0
- data/lib/mongo_trails/attribute_serializers/object_attribute.rb +41 -0
- data/lib/mongo_trails/attribute_serializers/object_changes_attribute.rb +44 -0
- data/lib/mongo_trails/cleaner.rb +60 -0
- data/lib/mongo_trails/compatibility.rb +51 -0
- data/lib/mongo_trails/config.rb +41 -0
- data/lib/mongo_trails/events/base.rb +323 -0
- data/lib/mongo_trails/events/create.rb +32 -0
- data/lib/mongo_trails/events/destroy.rb +42 -0
- data/lib/mongo_trails/events/update.rb +60 -0
- data/lib/mongo_trails/frameworks/cucumber.rb +33 -0
- data/lib/mongo_trails/frameworks/rails.rb +4 -0
- data/lib/mongo_trails/frameworks/rails/controller.rb +109 -0
- data/lib/mongo_trails/frameworks/rails/engine.rb +43 -0
- data/lib/mongo_trails/frameworks/rspec.rb +43 -0
- data/lib/mongo_trails/frameworks/rspec/helpers.rb +29 -0
- data/lib/mongo_trails/has_paper_trail.rb +86 -0
- data/lib/mongo_trails/model_config.rb +249 -0
- data/lib/mongo_trails/mongo_support/config.rb +9 -0
- data/lib/mongo_trails/mongo_support/version.rb +56 -0
- data/lib/mongo_trails/queries/versions/where_object.rb +65 -0
- data/lib/mongo_trails/queries/versions/where_object_changes.rb +75 -0
- data/lib/mongo_trails/record_history.rb +51 -0
- data/lib/mongo_trails/record_trail.rb +304 -0
- data/lib/mongo_trails/reifier.rb +130 -0
- data/lib/mongo_trails/request.rb +166 -0
- data/lib/mongo_trails/serializers/json.rb +46 -0
- data/lib/mongo_trails/serializers/yaml.rb +43 -0
- data/lib/mongo_trails/type_serializers/postgres_array_serializer.rb +48 -0
- data/lib/mongo_trails/version_concern.rb +336 -0
- data/lib/mongo_trails/version_number.rb +23 -0
- data/mongo_trails.gemspec +38 -0
- 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,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
|