active_version 1.0.0 → 1.0.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +8 -1
- data/README.md +32 -6
- data/lib/active_version/audits/audit_record.rb +4 -0
- data/lib/active_version/audits/has_audits/audit_combiner.rb +30 -28
- data/lib/active_version/audits/has_audits/audit_writer.rb +2 -2
- data/lib/active_version/audits/has_audits/change_filters.rb +3 -1
- data/lib/active_version/audits/has_audits.rb +94 -50
- data/lib/active_version/audits/sql_builder.rb +3 -1
- data/lib/active_version/revisions/has_revisions.rb +5 -2
- data/lib/active_version/revisions/revision_record.rb +1 -1
- data/lib/active_version/revisions/sql_builder.rb +3 -1
- data/lib/active_version/runtime.rb +4 -3
- data/lib/active_version/translations/translation_record.rb +3 -3
- data/lib/active_version/version.rb +1 -1
- data/lib/active_version.rb +1 -0
- metadata +3 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4636e51affa996a3f7c8cd98f32e05d6d1892ffe2dc3a79b58edb17bde8619f4
|
|
4
|
+
data.tar.gz: 42b4f546aeec443faf25d9d3b039ed515499f6b9284b520792afd4af711a721e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b78b064c4a79a1d1ce1780de50e009116e2e0864c25323745f6596385d197d058cc7c4a08bc58f07a7ff7addc678415482d7011698f1114da920b3f282498cc0
|
|
7
|
+
data.tar.gz: 3b35459213700a318435ea787706e6565dfa6c95e311fa7e0f3ef7ad39206f9330b465791cc074be818f1b045e60c154d6d188d1385a4aa54bb81cd0b9daef3f
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# CHANGELOG
|
|
2
2
|
|
|
3
|
+
## 1.0.1 (2026-03-11)
|
|
4
|
+
|
|
5
|
+
- Fixed Rails 7.2 thread-local audited options handling in `with_audited_options`
|
|
6
|
+
- Added Rails 6 appraisal and CI matrix coverage
|
|
7
|
+
- Improved test DB connection setup to honor `DATABASE_URL` and fail fast when PostgreSQL is explicitly requested
|
|
8
|
+
- Added compatibility fix for ActiveSupport on older Rails with newer Ruby by loading `logger` early
|
|
9
|
+
- Updated generator integration spec behavior on TruffleRuby to avoid known native extension incompatibility
|
|
10
|
+
|
|
3
11
|
## 1.0.0 (2026-03-08)
|
|
4
12
|
|
|
5
13
|
- Initial release of ActiveVersion library
|
|
@@ -33,4 +41,3 @@
|
|
|
33
41
|
- Added instrumentation hooks via ActiveSupport::Notifications
|
|
34
42
|
- Added configurable column naming and per-model and global configuration options
|
|
35
43
|
- Added comprehensive test suite with unit tests, integration tests, and test helpers
|
|
36
|
-
|
data/README.md
CHANGED
|
@@ -59,6 +59,8 @@ For a step-by-step guide (prerequisites, generators, migrations, model setup, an
|
|
|
59
59
|
|
|
60
60
|
Quick checklist: add the gem → `bundle install` → `rails g active_version:install` → run the feature generators for your models (e.g. `rails g active_version:audits Post --storage=json_column`) → `rails db:migrate` → include the concerns and `has_translations` / `has_revisions` / `has_audits` in each model.
|
|
61
61
|
|
|
62
|
+
For production usage, also configure destination models (`PostAudit`, `PostRevision`, `PostTranslation`) with `configure_audit`, `configure_revision`, and `configure_translation`.
|
|
63
|
+
|
|
62
64
|
## Quick Start
|
|
63
65
|
|
|
64
66
|
### Setup
|
|
@@ -221,20 +223,44 @@ end
|
|
|
221
223
|
class PostRevision < ApplicationRecord
|
|
222
224
|
include ActiveVersion::Revisions::RevisionRecord
|
|
223
225
|
|
|
224
|
-
configure_revision
|
|
225
|
-
|
|
226
|
-
|
|
226
|
+
configure_revision do
|
|
227
|
+
version_column :version
|
|
228
|
+
foreign_key :post_id
|
|
229
|
+
end
|
|
227
230
|
end
|
|
228
231
|
|
|
229
232
|
class PostTranslation < ApplicationRecord
|
|
230
233
|
include ActiveVersion::Translations::TranslationRecord
|
|
231
234
|
|
|
232
|
-
configure_translation
|
|
233
|
-
|
|
234
|
-
|
|
235
|
+
configure_translation do
|
|
236
|
+
locale_column :locale
|
|
237
|
+
foreign_key :post_id
|
|
238
|
+
end
|
|
235
239
|
end
|
|
236
240
|
```
|
|
237
241
|
|
|
242
|
+
Keyword-argument style is also supported:
|
|
243
|
+
|
|
244
|
+
```ruby
|
|
245
|
+
configure_revision(version_column: :version, foreign_key: :post_id)
|
|
246
|
+
configure_translation(locale_column: :locale, foreign_key: :post_id)
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
### Schema Evolution and Sync Strategy
|
|
250
|
+
|
|
251
|
+
When using `:mirror_columns` audits, revisions, and translations, destination tables must stay schema-compatible with their source model payload.
|
|
252
|
+
|
|
253
|
+
- Keep source and destination columns in sync for attributes you persist.
|
|
254
|
+
- Avoid destructive one-shot renames/drops across source and destination.
|
|
255
|
+
- Prefer gradual schema rollout:
|
|
256
|
+
1. add new column(s) to source and destination
|
|
257
|
+
2. deploy write path that can populate both shapes
|
|
258
|
+
3. backfill historical rows as needed
|
|
259
|
+
4. migrate readers to the new shape
|
|
260
|
+
5. deprecate and later remove old columns in a separate rollout
|
|
261
|
+
|
|
262
|
+
This phased approach avoids runtime mismatches and preserves audit/revision/translation continuity during deploys.
|
|
263
|
+
|
|
238
264
|
### Per-Model Configuration
|
|
239
265
|
|
|
240
266
|
```ruby
|
|
@@ -151,6 +151,10 @@ module ActiveVersion
|
|
|
151
151
|
end
|
|
152
152
|
rescue NameError
|
|
153
153
|
# Source class not yet defined, will be set up later
|
|
154
|
+
rescue *ActiveVersion::Runtime.active_record_connection_errors => e
|
|
155
|
+
if defined?(Rails) && Rails.respond_to?(:logger)
|
|
156
|
+
Rails.logger&.debug("[ActiveVersion] Deferred audit association setup for #{name}: #{e.class}: #{e.message}")
|
|
157
|
+
end
|
|
154
158
|
end
|
|
155
159
|
end
|
|
156
160
|
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
|
|
1
3
|
module ActiveVersion
|
|
2
4
|
module Audits
|
|
3
5
|
module HasAudits
|
|
@@ -27,28 +29,31 @@ module ActiveVersion
|
|
|
27
29
|
reload
|
|
28
30
|
end
|
|
29
31
|
|
|
30
|
-
# Clear association cache to ensure we get fresh data from database
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
assoc.reset if assoc.respond_to?(:loaded?) && assoc.loaded?
|
|
36
|
-
end
|
|
32
|
+
# Clear association cache to ensure we get fresh data from database.
|
|
33
|
+
# Avoid calling audits reader directly here to prevent AR 6.1
|
|
34
|
+
# delegation edge cases on dynamic models.
|
|
35
|
+
if respond_to?(:association) && association_cached?(:audits)
|
|
36
|
+
association(:audits).reset
|
|
37
37
|
end
|
|
38
38
|
|
|
39
39
|
# Get all audits fresh from database (not from cache)
|
|
40
40
|
# Query directly to ensure we get updated values after SQL updates
|
|
41
41
|
auditable_type = audited_options[:class_name] || self.class.name
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
42
|
+
auditable_column = ActiveVersion.column_mapper.column_for(self.class, :audits, :auditable)
|
|
43
|
+
version_column = ActiveVersion.column_mapper.column_for(self.class, :audits, :version)
|
|
44
|
+
audit_klass =
|
|
45
|
+
if self.class.reflect_on_association(:audits)
|
|
46
|
+
association(:audits).klass
|
|
47
|
+
else
|
|
48
|
+
self.class.audit_class
|
|
49
|
+
end
|
|
50
|
+
if audit_klass.nil? && self.class.respond_to?(:resolve_audit_class_option, true)
|
|
51
|
+
audit_klass = self.class.send(:resolve_audit_class_option, audited_options[:as])
|
|
51
52
|
end
|
|
53
|
+
break unless audit_klass
|
|
54
|
+
all_audits = audit_klass.where({"#{auditable_column}_type" => auditable_type}.merge(active_version_audit_identity_map))
|
|
55
|
+
.order(version_column => :asc)
|
|
56
|
+
.to_a
|
|
52
57
|
|
|
53
58
|
# Filter out combined audits (those with empty changes)
|
|
54
59
|
# Check raw column value first (before JSON parsing) for "{}" string
|
|
@@ -160,9 +165,9 @@ module ActiveVersion
|
|
|
160
165
|
conn = audit_class.connection
|
|
161
166
|
table_name = audit_class.table_name
|
|
162
167
|
updates = []
|
|
163
|
-
updates << "#{conn.quote_column_name(changes_column)} = #{conn.quote(combined_changes
|
|
168
|
+
updates << "#{conn.quote_column_name(changes_column)} = #{conn.quote(JSON.generate(combined_changes))}"
|
|
164
169
|
if combined_context.any?
|
|
165
|
-
updates << "#{conn.quote_column_name(context_column)} = #{conn.quote(combined_context
|
|
170
|
+
updates << "#{conn.quote_column_name(context_column)} = #{conn.quote(JSON.generate(combined_context))}"
|
|
166
171
|
end
|
|
167
172
|
target_id = conn.quote(combine_target.read_attribute(:id))
|
|
168
173
|
sql = "UPDATE #{conn.quote_table_name(table_name)} SET #{updates.join(", ")} WHERE id = #{target_id}"
|
|
@@ -180,7 +185,7 @@ module ActiveVersion
|
|
|
180
185
|
# Use raw SQL with proper escaping to bypass ActiveRecord's readonly checks
|
|
181
186
|
conn = audit_class.connection
|
|
182
187
|
table_name = audit_class.table_name
|
|
183
|
-
empty_json_str =
|
|
188
|
+
empty_json_str = JSON.generate({})
|
|
184
189
|
empty_json = conn.quote(empty_json_str)
|
|
185
190
|
quoted_comment = conn.quote(combined_comment)
|
|
186
191
|
|
|
@@ -194,16 +199,13 @@ module ActiveVersion
|
|
|
194
199
|
end
|
|
195
200
|
|
|
196
201
|
# Clear association cache to ensure fresh data is loaded after updates
|
|
197
|
-
if respond_to?(:audits)
|
|
198
|
-
|
|
202
|
+
if respond_to?(:association) && association_cached?(:audits)
|
|
203
|
+
association(:audits).reset
|
|
204
|
+
end
|
|
205
|
+
begin
|
|
199
206
|
audits.reset
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
assoc = association(:audits)
|
|
203
|
-
if assoc.respond_to?(:loaded?) && assoc.loaded?
|
|
204
|
-
assoc.reset
|
|
205
|
-
end
|
|
206
|
-
end
|
|
207
|
+
rescue NoMethodError
|
|
208
|
+
nil
|
|
207
209
|
end
|
|
208
210
|
end
|
|
209
211
|
end
|
|
@@ -190,7 +190,7 @@ module ActiveVersion
|
|
|
190
190
|
begin
|
|
191
191
|
audit_class.create!(insert_attrs)
|
|
192
192
|
combine_audits_if_needed if attrs[:action] != "create"
|
|
193
|
-
audits.reset
|
|
193
|
+
association(:audits).reset if association_cached?(:audits)
|
|
194
194
|
nil
|
|
195
195
|
rescue ActiveRecord::RecordNotUnique => e
|
|
196
196
|
# Handle unique constraint violation (likely version conflict)
|
|
@@ -208,7 +208,7 @@ module ActiveVersion
|
|
|
208
208
|
begin
|
|
209
209
|
audit_class.create!(insert_attrs)
|
|
210
210
|
combine_audits_if_needed if attrs[:action] != "create"
|
|
211
|
-
audits.reset
|
|
211
|
+
association(:audits).reset if association_cached?(:audits)
|
|
212
212
|
nil
|
|
213
213
|
rescue => retry_error
|
|
214
214
|
handle_audit_errors(retry_error, attrs[:action])
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
|
|
1
3
|
module ActiveVersion
|
|
2
4
|
module Audits
|
|
3
5
|
module HasAudits
|
|
@@ -105,7 +107,7 @@ module ActiveVersion
|
|
|
105
107
|
changes.each_with_object({}) do |(k, v), h|
|
|
106
108
|
h[k] = v.last if v.is_a?(Array)
|
|
107
109
|
h[k] = v unless v.is_a?(Array)
|
|
108
|
-
h[k] = h[k]
|
|
110
|
+
h[k] = JSON.generate(h[k]) if h[k].is_a?(Hash) || h[k].is_a?(Array)
|
|
109
111
|
end
|
|
110
112
|
end
|
|
111
113
|
end
|
|
@@ -24,14 +24,8 @@ module ActiveVersion
|
|
|
24
24
|
|
|
25
25
|
# Get audit class name
|
|
26
26
|
def self.audit_class
|
|
27
|
-
# Check class attribute first (set by set_audit)
|
|
28
|
-
return superclass.audit_class if respond_to?(:superclass) && superclass.respond_to?(:audit_class) && superclass.audit_class
|
|
29
27
|
return @audit_class if @audit_class
|
|
30
28
|
|
|
31
|
-
# Check if class attribute was set via class_attribute
|
|
32
|
-
attr_value = read_inheritable_attribute(:audit_class) if respond_to?(:read_inheritable_attribute)
|
|
33
|
-
return attr_value if attr_value
|
|
34
|
-
|
|
35
29
|
if audited_options && audited_options[:as]
|
|
36
30
|
klass = case audited_options[:as]
|
|
37
31
|
when String, Symbol
|
|
@@ -168,35 +162,7 @@ module ActiveVersion
|
|
|
168
162
|
# This ensures class_audited_options can find it
|
|
169
163
|
@audited_options_base = normalized.dup
|
|
170
164
|
self.audited_options = normalized
|
|
171
|
-
|
|
172
|
-
# Override audited_options to merge thread-local config
|
|
173
|
-
# class_attribute methods can't be easily overridden, so we need to use alias_method
|
|
174
|
-
unless respond_to?(:audited_options_without_thread_local, true)
|
|
175
|
-
alias_method :audited_options_without_thread_local, :audited_options
|
|
176
|
-
define_singleton_method :audited_options do
|
|
177
|
-
# Get base class-level options (without thread-local)
|
|
178
|
-
# Use send to call private method in correct context
|
|
179
|
-
class_level = send(:class_audited_options)
|
|
180
|
-
key = send(:audited_current_options_key)
|
|
181
|
-
thread_local = ActiveVersion.store_get(key)
|
|
182
|
-
|
|
183
|
-
# Start with class-level options (deep copy to avoid reference issues)
|
|
184
|
-
result = if class_level.is_a?(Hash)
|
|
185
|
-
class_level.deep_dup
|
|
186
|
-
else
|
|
187
|
-
{}
|
|
188
|
-
end
|
|
189
|
-
|
|
190
|
-
# Merge thread-local over class-level (thread-local takes precedence)
|
|
191
|
-
if thread_local.is_a?(Hash) && !thread_local.empty?
|
|
192
|
-
thread_local.each do |k, v|
|
|
193
|
-
result[k] = v
|
|
194
|
-
end
|
|
195
|
-
end
|
|
196
|
-
|
|
197
|
-
result
|
|
198
|
-
end
|
|
199
|
-
end
|
|
165
|
+
install_thread_local_audited_options_reader!
|
|
200
166
|
|
|
201
167
|
self.audit_associated_with = audited_options[:associated_with]
|
|
202
168
|
|
|
@@ -261,6 +227,7 @@ module ActiveVersion
|
|
|
261
227
|
@audited_options_base = normalized.dup
|
|
262
228
|
self.audit_class = resolved_audit_class
|
|
263
229
|
@audit_class = resolved_audit_class # Also set instance variable for the custom method
|
|
230
|
+
install_thread_local_audited_options_reader!
|
|
264
231
|
|
|
265
232
|
# Ensure audit class associations are set up
|
|
266
233
|
resolved_audit_class.setup_associations if resolved_audit_class.respond_to?(:setup_associations)
|
|
@@ -324,15 +291,35 @@ module ActiveVersion
|
|
|
324
291
|
# This overrides the class_attribute reader to merge thread-local overrides
|
|
325
292
|
def update_audited_options(new_options)
|
|
326
293
|
normalized = normalize_audited_options(new_options)
|
|
327
|
-
resolved_audit_class = audit_class
|
|
294
|
+
resolved_audit_class = resolve_audit_class_option(normalized[:as]) || audit_class
|
|
295
|
+
if resolved_audit_class
|
|
296
|
+
self.audit_class = resolved_audit_class
|
|
297
|
+
@audit_class = resolved_audit_class
|
|
298
|
+
if resolved_audit_class.name.present?
|
|
299
|
+
has_many :audits,
|
|
300
|
+
as: :auditable,
|
|
301
|
+
class_name: resolved_audit_class.name.to_s,
|
|
302
|
+
inverse_of: false
|
|
303
|
+
end
|
|
304
|
+
end
|
|
328
305
|
register_audit_column_mappings_from_destination(resolved_audit_class) if resolved_audit_class
|
|
329
306
|
normalized = infer_audit_storage_and_columns(resolved_audit_class, normalized) if resolved_audit_class
|
|
330
307
|
self.audited_options = normalized
|
|
331
308
|
# Store base value in instance variable for class_audited_options to access
|
|
332
309
|
@audited_options_base = normalized.dup
|
|
310
|
+
install_thread_local_audited_options_reader!
|
|
333
311
|
self.audit_associated_with = audited_options[:associated_with]
|
|
334
312
|
end
|
|
335
313
|
|
|
314
|
+
def resolve_audit_class_option(value)
|
|
315
|
+
case value
|
|
316
|
+
when String, Symbol
|
|
317
|
+
value.to_s.safe_constantize
|
|
318
|
+
when Class
|
|
319
|
+
value
|
|
320
|
+
end
|
|
321
|
+
end
|
|
322
|
+
|
|
336
323
|
def normalize_audited_options(options)
|
|
337
324
|
{
|
|
338
325
|
on: Array.wrap(options[:on] || [:create, :update, :destroy]),
|
|
@@ -366,6 +353,7 @@ module ActiveVersion
|
|
|
366
353
|
public
|
|
367
354
|
|
|
368
355
|
def with_audited_options(options = {})
|
|
356
|
+
install_thread_local_audited_options_reader!
|
|
369
357
|
thread_key = audited_current_options_key
|
|
370
358
|
current = ActiveVersion.store_get(thread_key)
|
|
371
359
|
# Store only the thread-local overrides (merge with existing if any)
|
|
@@ -375,14 +363,24 @@ module ActiveVersion
|
|
|
375
363
|
normalized = {}
|
|
376
364
|
# Convert options to hash (paper_trail pattern: simple to_h call)
|
|
377
365
|
# Handle both Hash and objects that respond to to_h
|
|
378
|
-
opts_hash = if options.
|
|
379
|
-
options.to_h
|
|
380
|
-
elsif options.is_a?(Hash)
|
|
366
|
+
opts_hash = if options.is_a?(Hash)
|
|
381
367
|
options
|
|
368
|
+
elsif options.respond_to?(:to_h)
|
|
369
|
+
options.to_h
|
|
382
370
|
else
|
|
383
371
|
{}
|
|
384
372
|
end
|
|
385
373
|
|
|
374
|
+
# Some objects (e.g. Struct.new(:to_h).new({...})) stringify into
|
|
375
|
+
# { to_h: {...} } instead of returning the intended options hash.
|
|
376
|
+
if opts_hash.is_a?(Hash)
|
|
377
|
+
if opts_hash.key?(:to_h) && opts_hash[:to_h].is_a?(Hash)
|
|
378
|
+
opts_hash = opts_hash[:to_h]
|
|
379
|
+
elsif opts_hash.key?("to_h") && opts_hash["to_h"].is_a?(Hash)
|
|
380
|
+
opts_hash = opts_hash["to_h"]
|
|
381
|
+
end
|
|
382
|
+
end
|
|
383
|
+
|
|
386
384
|
opts_hash.each do |k, v|
|
|
387
385
|
next if v.nil?
|
|
388
386
|
key = k.is_a?(Symbol) ? k : k.to_sym
|
|
@@ -422,6 +420,46 @@ module ActiveVersion
|
|
|
422
420
|
|
|
423
421
|
private
|
|
424
422
|
|
|
423
|
+
def install_thread_local_audited_options_reader!
|
|
424
|
+
unless singleton_class.instance_methods(false).include?(:audited_options_without_thread_local)
|
|
425
|
+
singleton_class.alias_method :audited_options_without_thread_local, :audited_options
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
define_singleton_method :audited_options do
|
|
429
|
+
class_level = send(:class_audited_options)
|
|
430
|
+
key = send(:audited_current_options_key)
|
|
431
|
+
thread_local = ActiveVersion.store_get(key)
|
|
432
|
+
|
|
433
|
+
result = if class_level.is_a?(Hash)
|
|
434
|
+
class_level.each_with_object({}) do |(k, v), hash|
|
|
435
|
+
hash[k] = safe_dup_audited_option_value(v)
|
|
436
|
+
end
|
|
437
|
+
else
|
|
438
|
+
{}
|
|
439
|
+
end
|
|
440
|
+
|
|
441
|
+
if thread_local.is_a?(Hash) && !thread_local.empty?
|
|
442
|
+
thread_local.each do |k, v|
|
|
443
|
+
result[k] = v
|
|
444
|
+
end
|
|
445
|
+
end
|
|
446
|
+
|
|
447
|
+
result
|
|
448
|
+
end
|
|
449
|
+
@active_version_audited_options_wrapped = true
|
|
450
|
+
end
|
|
451
|
+
|
|
452
|
+
def safe_dup_audited_option_value(value)
|
|
453
|
+
case value
|
|
454
|
+
when Hash
|
|
455
|
+
value.each_with_object({}) { |(k, v), hash| hash[k] = safe_dup_audited_option_value(v) }
|
|
456
|
+
when Array
|
|
457
|
+
value.map { |item| safe_dup_audited_option_value(item) }
|
|
458
|
+
else
|
|
459
|
+
value
|
|
460
|
+
end
|
|
461
|
+
end
|
|
462
|
+
|
|
425
463
|
# Get the base class_attribute value without thread-local merging
|
|
426
464
|
def class_audited_options
|
|
427
465
|
# Try to get from instance variable first (most direct)
|
|
@@ -516,7 +554,7 @@ module ActiveVersion
|
|
|
516
554
|
else
|
|
517
555
|
inferred[:only] ||= []
|
|
518
556
|
end
|
|
519
|
-
rescue
|
|
557
|
+
rescue *ActiveVersion::Runtime.active_record_connection_errors
|
|
520
558
|
inferred[:storage] ||= ActiveVersion.config.audit_storage
|
|
521
559
|
inferred[:only] ||= []
|
|
522
560
|
end
|
|
@@ -763,7 +801,7 @@ module ActiveVersion
|
|
|
763
801
|
end
|
|
764
802
|
|
|
765
803
|
def clear_rolled_back_audits
|
|
766
|
-
audits.reset
|
|
804
|
+
association(:audits).reset if association_cached?(:audits)
|
|
767
805
|
end
|
|
768
806
|
|
|
769
807
|
# Override audits method to handle dynamically created classes
|
|
@@ -777,18 +815,24 @@ module ActiveVersion
|
|
|
777
815
|
raise ConfigurationError, "Cannot determine class name for dynamically created class. Please specify class_name option in has_audits (e.g., has_audits as: PostAudit, class_name: 'Post')"
|
|
778
816
|
end
|
|
779
817
|
|
|
780
|
-
# If class_name is different from actual class name, query directly
|
|
781
818
|
uses_custom_auditable_id = audited_options[:identity_resolver].present? ||
|
|
782
819
|
Array(active_version_audit_identity_columns).length > 1
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
820
|
+
|
|
821
|
+
audit_klass =
|
|
822
|
+
if !uses_custom_auditable_id && self.class.reflect_on_association(:audits)
|
|
823
|
+
association(:audits).klass
|
|
824
|
+
else
|
|
825
|
+
self.class.audit_class
|
|
826
|
+
end
|
|
827
|
+
audit_klass ||= self.class.send(:resolve_audit_class_option, audited_options[:as]) if self.class.respond_to?(:resolve_audit_class_option, true)
|
|
828
|
+
raise ConfigurationError, "No audit class configured for #{self.class.name}" unless audit_klass
|
|
829
|
+
|
|
830
|
+
if !uses_custom_auditable_id && auditable_type == self.class.name && self.class.reflect_on_association(:audits)
|
|
831
|
+
return super
|
|
791
832
|
end
|
|
833
|
+
|
|
834
|
+
auditable_column = ActiveVersion.column_mapper.column_for(self.class, :audits, :auditable)
|
|
835
|
+
audit_klass.where({"#{auditable_column}_type" => auditable_type}.merge(active_version_audit_identity_map))
|
|
792
836
|
end
|
|
793
837
|
|
|
794
838
|
def active_version_auditable_id_value
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
|
|
1
3
|
module ActiveVersion
|
|
2
4
|
module Audits
|
|
3
5
|
# SQL builder for batch audit operations
|
|
@@ -248,7 +250,7 @@ module ActiveVersion
|
|
|
248
250
|
def prepare_sql_value(value)
|
|
249
251
|
case value
|
|
250
252
|
when Hash, Array
|
|
251
|
-
value
|
|
253
|
+
JSON.generate(value)
|
|
252
254
|
when Time, DateTime
|
|
253
255
|
value.utc
|
|
254
256
|
when Date
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
require "active_version/revisions/has_revisions/revision_queries"
|
|
2
2
|
require "active_version/revisions/has_revisions/revision_manipulation"
|
|
3
|
+
require "json"
|
|
3
4
|
|
|
4
5
|
module ActiveVersion
|
|
5
6
|
module Revisions
|
|
@@ -329,7 +330,7 @@ module ActiveVersion
|
|
|
329
330
|
def revision_sql_value(value)
|
|
330
331
|
case value
|
|
331
332
|
when Hash, Array
|
|
332
|
-
value
|
|
333
|
+
JSON.generate(value)
|
|
333
334
|
when Time, DateTime
|
|
334
335
|
value.utc
|
|
335
336
|
when Date
|
|
@@ -394,16 +395,18 @@ module ActiveVersion
|
|
|
394
395
|
|
|
395
396
|
def create_revision_before_update
|
|
396
397
|
pointer = instance_variable_get(:@active_version_pointer)
|
|
398
|
+
truncated_forward_history = false
|
|
397
399
|
if pointer
|
|
398
400
|
version_column = revision_version_column
|
|
399
401
|
# Classic linear undo/redo behavior: editing after undo drops forward history.
|
|
400
402
|
revisions_scope.where("#{version_column} > ?", pointer).delete_all
|
|
401
403
|
revisions.reset
|
|
402
404
|
remove_instance_variable(:@active_version_pointer)
|
|
405
|
+
truncated_forward_history = true
|
|
403
406
|
end
|
|
404
407
|
# Check if we should create revision
|
|
405
408
|
return unless should_create_revision?
|
|
406
|
-
return if latest_revision_matches_current_state?
|
|
409
|
+
return if truncated_forward_history && latest_revision_matches_current_state?
|
|
407
410
|
|
|
408
411
|
result = create_snapshot!(use_old_values: true)
|
|
409
412
|
|
|
@@ -103,7 +103,7 @@ module ActiveVersion
|
|
|
103
103
|
version_column = fallback_column.to_sym if fallback_column
|
|
104
104
|
end
|
|
105
105
|
validates version_column, presence: true, uniqueness: {scope: Array(source_foreign_key)} if version_column
|
|
106
|
-
rescue NameError,
|
|
106
|
+
rescue NameError, *ActiveVersion::Runtime.active_record_connection_errors
|
|
107
107
|
# Source class not yet defined, will be set up later
|
|
108
108
|
end
|
|
109
109
|
end
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
|
|
1
3
|
module ActiveVersion
|
|
2
4
|
module Revisions
|
|
3
5
|
# SQL builder for batch revision operations
|
|
@@ -251,7 +253,7 @@ module ActiveVersion
|
|
|
251
253
|
def prepare_sql_value(value)
|
|
252
254
|
case value
|
|
253
255
|
when Hash, Array
|
|
254
|
-
value
|
|
256
|
+
JSON.generate(value)
|
|
255
257
|
when Time, DateTime
|
|
256
258
|
value.utc
|
|
257
259
|
when Date
|
|
@@ -91,12 +91,13 @@ module ActiveVersion
|
|
|
91
91
|
def active_record_connection_errors
|
|
92
92
|
return [] unless defined?(::ActiveRecord)
|
|
93
93
|
|
|
94
|
-
[
|
|
94
|
+
errors = [
|
|
95
95
|
::ActiveRecord::ConnectionNotEstablished,
|
|
96
96
|
::ActiveRecord::NoDatabaseError,
|
|
97
|
-
::ActiveRecord::StatementInvalid
|
|
98
|
-
::ActiveRecord::ConnectionNotDefined
|
|
97
|
+
::ActiveRecord::StatementInvalid
|
|
99
98
|
]
|
|
99
|
+
errors << ::ActiveRecord::ConnectionNotDefined if defined?(::ActiveRecord::ConnectionNotDefined)
|
|
100
|
+
errors
|
|
100
101
|
end
|
|
101
102
|
|
|
102
103
|
def supports_transactional_context?(connection)
|
|
@@ -76,7 +76,7 @@ module ActiveVersion
|
|
|
76
76
|
return locale_column if column_names.include?(locale_column.to_s)
|
|
77
77
|
|
|
78
78
|
ActiveVersion.config.translation_locale_column
|
|
79
|
-
rescue NameError,
|
|
79
|
+
rescue NameError, *ActiveVersion::Runtime.active_record_connection_errors
|
|
80
80
|
ActiveVersion.config.translation_locale_column
|
|
81
81
|
end
|
|
82
82
|
|
|
@@ -99,7 +99,7 @@ module ActiveVersion
|
|
|
99
99
|
begin
|
|
100
100
|
locale_column = locale_column_name
|
|
101
101
|
validates locale_column, presence: true, uniqueness: {scope: Array(source_foreign_key)}
|
|
102
|
-
rescue NameError,
|
|
102
|
+
rescue NameError, *ActiveVersion::Runtime.active_record_connection_errors
|
|
103
103
|
# Source class not yet defined, will be set up later
|
|
104
104
|
end
|
|
105
105
|
end
|
|
@@ -116,7 +116,7 @@ module ActiveVersion
|
|
|
116
116
|
column = columns_hash[locale_column.to_s]
|
|
117
117
|
return unless column&.type == :integer
|
|
118
118
|
enum locale_column, I18n.available_locales.index_by(&:to_s)
|
|
119
|
-
rescue NameError,
|
|
119
|
+
rescue NameError, *ActiveVersion::Runtime.active_record_connection_errors
|
|
120
120
|
# Source class not yet defined
|
|
121
121
|
end
|
|
122
122
|
end
|
data/lib/active_version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: active_version
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.0.
|
|
4
|
+
version: 1.0.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Andrei Makarov
|
|
@@ -85,14 +85,14 @@ dependencies:
|
|
|
85
85
|
requirements:
|
|
86
86
|
- - ">="
|
|
87
87
|
- !ruby/object:Gem::Version
|
|
88
|
-
version: '
|
|
88
|
+
version: '1.4'
|
|
89
89
|
type: :development
|
|
90
90
|
prerelease: false
|
|
91
91
|
version_requirements: !ruby/object:Gem::Requirement
|
|
92
92
|
requirements:
|
|
93
93
|
- - ">="
|
|
94
94
|
- !ruby/object:Gem::Version
|
|
95
|
-
version: '
|
|
95
|
+
version: '1.4'
|
|
96
96
|
- !ruby/object:Gem::Dependency
|
|
97
97
|
name: pg
|
|
98
98
|
requirement: !ruby/object:Gem::Requirement
|