active_version 1.4.0 → 2.0.0
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 +6 -0
- data/README.md +17 -2
- data/SECURITY.md +3 -5
- data/lib/active_version/adapters/active_record/audits.rb +2 -2
- data/lib/active_version/adapters/active_record/revisions.rb +1 -1
- data/lib/active_version/adapters/active_record/translations.rb +1 -1
- data/lib/active_version/audits/has_audits/audit_writer.rb +39 -35
- data/lib/active_version/revisions/has_revisions/revision_manipulation.rb +8 -4
- data/lib/active_version/unique_version_collision.rb +38 -0
- data/lib/active_version/version.rb +1 -1
- data/lib/active_version.rb +1 -0
- metadata +34 -7
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d52287aa8ea68e0c77baff3065a3140d397c2e638460967880b0b0dbc904b42c
|
|
4
|
+
data.tar.gz: aff296230f04d04d90bafe26e12fe0876fc2cd292cd07e6083d71a595a9ea6f6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 35c7b1c96056c61e5c36a72166d2be067504da0fa922fd439ad4a6b1a09a7630ae80734fb2ad8e97dfb81fd3302c75ed168cb92adb22669f222e5cd6aa363a90
|
|
7
|
+
data.tar.gz: db6759859ffd1139a818c45a23ae486e819e035553b32762f586465cbef3a2597507a80ea424036ecc0718fe6a9dbf25cb11fe5c8e4e6c7556d27e124e13704f
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# CHANGELOG
|
|
2
2
|
|
|
3
|
+
## 2.0.0 (2026-08-21)
|
|
4
|
+
|
|
5
|
+
- BREAKING: Require Active Support and Active Record `>= 7.2.3.2` and `< 9.0`.
|
|
6
|
+
- Retry unique version collisions so a revision snapshot or audit still writes after a concurrent insert, including inside an open PostgreSQL transaction.
|
|
7
|
+
- Add `rfcs/` starter pack: process (RFC 0001), positioning (RFC 0002), and Standards Track design RFCs 0003–0006 for translations, revisions, audits, and generators.
|
|
8
|
+
|
|
3
9
|
## 1.4.0 (2026-07-13)
|
|
4
10
|
|
|
5
11
|
- Add `revision_error_behavior` configuration (`:exception`, `:log`, `:silent`) and per-model `has_revisions error_behavior:` override for revision snapshot failures.
|
data/README.md
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
# ActiveVersion
|
|
2
2
|
|
|
3
|
-
[](https://badge.fury.io/rb/active_version) [](https://github.com/amkisko/active_version.rb/actions/workflows/test.yml) [](https://codecov.io/
|
|
3
|
+
[](https://badge.fury.io/rb/active_version) [](https://github.com/amkisko/active_version.rb/actions/workflows/test.yml) [](https://app.codecov.io/github/amkisko/active_version.rb)
|
|
4
4
|
|
|
5
5
|
A unified versioning library for ActiveRecord that handles translations, revisions, and audits in a single, extensible architecture.
|
|
6
6
|
|
|
7
|
+
Design changes go through `rfcs/` (RFC 0001).
|
|
8
|
+
|
|
7
9
|
## Features
|
|
8
10
|
|
|
9
11
|
- Translations: Locale-based versioning with automatic value copying
|
|
@@ -502,7 +504,7 @@ Environment knobs:
|
|
|
502
504
|
## Requirements
|
|
503
505
|
|
|
504
506
|
- Ruby >= 3.0.0
|
|
505
|
-
- ActiveRecord >=
|
|
507
|
+
- ActiveRecord >= 7.2.3.2 and < 9.0 (default runtime)
|
|
506
508
|
- SQLite or MySQL or PostgreSQL (optional, for triggers and JSONB support)
|
|
507
509
|
|
|
508
510
|
## Contributing
|
|
@@ -511,6 +513,19 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/amkisk
|
|
|
511
513
|
|
|
512
514
|
Development dependencies include [polyrun](https://rubygems.org/gems/polyrun) (declared in `active_version.gemspec`) for **parallel RSpec** (`./bin/polyrun`), coverage, and CI report formats—see [spec/README.md](spec/README.md).
|
|
513
515
|
|
|
516
|
+
## Links
|
|
517
|
+
|
|
518
|
+
- [GitHub](https://github.com/amkisko/active_version.rb)
|
|
519
|
+
- [GitLab](https://gitlab.com/amkisko/active_version.rb)
|
|
520
|
+
- [RubyGems](https://rubygems.org/gems/active_version)
|
|
521
|
+
- [Versions Atom](https://rubygems.org/gems/active_version/versions.atom) (feed id `10668858017926`)
|
|
522
|
+
- [libraries.io](https://libraries.io/rubygems/active_version)
|
|
523
|
+
- [Deps.dev](https://deps.dev/rubygems/active_version)
|
|
524
|
+
- [SonarCloud](https://sonarcloud.io/project/overview?id=amkisko_active_version.rb)
|
|
525
|
+
- [Snyk](https://snyk.io/test/github/amkisko/active_version.rb)
|
|
526
|
+
- [Codecov](https://app.codecov.io/github/amkisko/active_version.rb)
|
|
527
|
+
- [OpenSSF Scorecard](https://scorecard.dev/viewer/?uri=github.com/amkisko/active_version.rb)
|
|
528
|
+
|
|
514
529
|
## License
|
|
515
530
|
|
|
516
531
|
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/SECURITY.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
## Reporting a Vulnerability
|
|
4
4
|
|
|
5
|
-
Do
|
|
5
|
+
Do not open a public issue for security vulnerabilities.
|
|
6
6
|
|
|
7
7
|
Email security details to: security@kiskolabs.com
|
|
8
8
|
|
|
@@ -23,7 +23,5 @@ Include: description, steps to reproduce, potential impact, and suggested fix (i
|
|
|
23
23
|
|
|
24
24
|
## Automation Security
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
* Supply Chain: All automated dependencies must be verified.
|
|
29
|
-
|
|
26
|
+
- Context Isolation: It is strictly forbidden to include production credentials, API keys, or Personally Identifiable Information (PII) in prompts sent to third-party LLMs or automation services.
|
|
27
|
+
- Supply Chain: All automated dependencies must be verified.
|
|
@@ -11,7 +11,7 @@ module ActiveVersion
|
|
|
11
11
|
module ClassMethods
|
|
12
12
|
# Declare that a model has audits
|
|
13
13
|
def has_audits(options = {})
|
|
14
|
-
include ActiveVersion::Audits::HasAudits unless
|
|
14
|
+
include ActiveVersion::Audits::HasAudits unless include?(ActiveVersion::Audits::HasAudits)
|
|
15
15
|
|
|
16
16
|
# Call the HasAudits implementation once included
|
|
17
17
|
ActiveVersion::Audits::HasAudits::ClassMethods.instance_method(:has_audits)
|
|
@@ -30,7 +30,7 @@ end
|
|
|
30
30
|
|
|
31
31
|
# If ActiveRecord::Base is already loaded, include immediately
|
|
32
32
|
if defined?(ActiveRecord::Base) && ActiveRecord::Base.respond_to?(:include)
|
|
33
|
-
unless ActiveRecord::Base.
|
|
33
|
+
unless ActiveRecord::Base.include?(ActiveVersion::Adapters::ActiveRecord::Audits)
|
|
34
34
|
ActiveSupport.on_load(:active_record) { include ActiveVersion::Adapters::ActiveRecord::Audits }
|
|
35
35
|
end
|
|
36
36
|
end
|
|
@@ -43,7 +43,7 @@ end
|
|
|
43
43
|
|
|
44
44
|
# If ActiveRecord::Base is already loaded, include immediately
|
|
45
45
|
if defined?(ActiveRecord::Base) && ActiveRecord::Base.respond_to?(:include)
|
|
46
|
-
unless ActiveRecord::Base.
|
|
46
|
+
unless ActiveRecord::Base.include?(ActiveVersion::Adapters::ActiveRecord::Revisions)
|
|
47
47
|
ActiveSupport.on_load(:active_record) { include ActiveVersion::Adapters::ActiveRecord::Revisions }
|
|
48
48
|
end
|
|
49
49
|
end
|
|
@@ -39,7 +39,7 @@ end
|
|
|
39
39
|
|
|
40
40
|
# If ActiveRecord::Base is already loaded, include immediately
|
|
41
41
|
if defined?(ActiveRecord::Base) && ActiveRecord::Base.respond_to?(:include)
|
|
42
|
-
unless ActiveRecord::Base.
|
|
42
|
+
unless ActiveRecord::Base.include?(ActiveVersion::Adapters::ActiveRecord::Translations)
|
|
43
43
|
ActiveSupport.on_load(:active_record) { include ActiveVersion::Adapters::ActiveRecord::Translations }
|
|
44
44
|
end
|
|
45
45
|
end
|
|
@@ -187,42 +187,46 @@ module ActiveVersion
|
|
|
187
187
|
return nil
|
|
188
188
|
end
|
|
189
189
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
190
|
+
create_audit_record_with_version_retry!(
|
|
191
|
+
insert_attrs,
|
|
192
|
+
version_column: version_column,
|
|
193
|
+
auditable_column: auditable_column,
|
|
194
|
+
action: attrs[:action]
|
|
195
|
+
)
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# One retry on unique (version) collision. Each insert uses a savepoint so
|
|
201
|
+
# PostgreSQL can continue the outer update/lock transaction after a unique violation.
|
|
202
|
+
def create_audit_record_with_version_retry!(insert_attrs, version_column:, auditable_column:, action:)
|
|
203
|
+
attempt = 0
|
|
204
|
+
begin
|
|
205
|
+
attempt += 1
|
|
206
|
+
self.class.transaction(requires_new: true) do
|
|
207
|
+
audit_class.create!(insert_attrs)
|
|
208
|
+
end
|
|
209
|
+
combine_audits_if_needed if action != "create"
|
|
210
|
+
association(:audits).reset if association_cached?(:audits)
|
|
211
|
+
nil
|
|
212
|
+
rescue => error
|
|
213
|
+
unless ActiveVersion::UniqueVersionCollision.match?(error, version_column: version_column) &&
|
|
214
|
+
action != "create" && attempt < 2
|
|
215
|
+
handle_audit_errors(error, action)
|
|
216
|
+
return nil
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
auditable_type_for_query = audited_options[:class_name] || self.class.name
|
|
220
|
+
if auditable_type_for_query.nil?
|
|
221
|
+
raise ConfigurationError, "Cannot determine class name for dynamically created class. Please specify class_name option in has_audits"
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
max_version = audit_class.uncached do
|
|
225
|
+
audit_class.where({"#{auditable_column}_type" => auditable_type_for_query}.merge(active_version_audit_identity_map))
|
|
226
|
+
.maximum(version_column) || 0
|
|
225
227
|
end
|
|
228
|
+
insert_attrs[version_column] = max_version + 1
|
|
229
|
+
retry
|
|
226
230
|
end
|
|
227
231
|
end
|
|
228
232
|
|
|
@@ -421,16 +421,20 @@ module ActiveVersion
|
|
|
421
421
|
revisions.reset
|
|
422
422
|
end
|
|
423
423
|
|
|
424
|
-
# One retry on unique (version)
|
|
424
|
+
# One retry on unique (version) collision. Each insert uses a savepoint so
|
|
425
|
+
# PostgreSQL can continue the outer update/lock transaction after a unique violation.
|
|
425
426
|
def create_revision_record_with_version_retry!(revision_attrs, version_column_sym)
|
|
426
427
|
attempt = 0
|
|
427
428
|
begin
|
|
428
429
|
attempt += 1
|
|
429
|
-
|
|
430
|
-
|
|
430
|
+
self.class.transaction(requires_new: true) do
|
|
431
|
+
revisions.create!(revision_attrs)
|
|
432
|
+
end
|
|
433
|
+
rescue => error
|
|
434
|
+
raise error unless ActiveVersion::UniqueVersionCollision.match?(error, version_column: version_column_sym)
|
|
431
435
|
raise error if attempt >= 2
|
|
432
436
|
|
|
433
|
-
max_version = revisions_scope.maximum(version_column_sym) || 0
|
|
437
|
+
max_version = revisions_scope.uncached { revisions_scope.maximum(version_column_sym) } || 0
|
|
434
438
|
revision_attrs[version_column_sym] = max_version + 1
|
|
435
439
|
retry
|
|
436
440
|
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
module ActiveVersion
|
|
2
|
+
module UniqueVersionCollision
|
|
3
|
+
module_function
|
|
4
|
+
|
|
5
|
+
def match?(error, version_column: nil)
|
|
6
|
+
return true if version_uniqueness_invalid?(error, version_column)
|
|
7
|
+
|
|
8
|
+
unique_constraint_names_version?(error, version_column)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def version_uniqueness_invalid?(error, version_column)
|
|
12
|
+
return false unless error.is_a?(ActiveRecord::RecordInvalid)
|
|
13
|
+
return false if version_column.nil?
|
|
14
|
+
|
|
15
|
+
details = error.record&.errors&.details
|
|
16
|
+
return false unless details
|
|
17
|
+
|
|
18
|
+
Array(details[version_column.to_sym]).any? { |detail| detail[:error] == :taken }
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def unique_constraint_names_version?(error, version_column)
|
|
22
|
+
needle = version_column.to_s
|
|
23
|
+
return false if needle.empty?
|
|
24
|
+
return false unless unique_constraint_error?(error)
|
|
25
|
+
|
|
26
|
+
[error.message, error.cause&.message].compact.any? { |text| text.include?(needle) }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def unique_constraint_error?(error)
|
|
30
|
+
return true if error.is_a?(ActiveRecord::RecordNotUnique)
|
|
31
|
+
return false unless defined?(PG)
|
|
32
|
+
|
|
33
|
+
error.is_a?(ActiveRecord::StatementInvalid) && error.cause.is_a?(PG::UniqueViolation)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private_class_method :version_uniqueness_invalid?, :unique_constraint_names_version?, :unique_constraint_error?
|
|
37
|
+
end
|
|
38
|
+
end
|
data/lib/active_version.rb
CHANGED
|
@@ -12,6 +12,7 @@ require "active_version/column_mapper"
|
|
|
12
12
|
require "active_version/version_registry"
|
|
13
13
|
require "active_version/instrumentation"
|
|
14
14
|
require "active_version/runtime"
|
|
15
|
+
require "active_version/unique_version_collision"
|
|
15
16
|
|
|
16
17
|
# Main entry point for ActiveVersion
|
|
17
18
|
module ActiveVersion
|
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:
|
|
4
|
+
version: 2.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Andrei Makarov
|
|
@@ -15,14 +15,20 @@ dependencies:
|
|
|
15
15
|
requirements:
|
|
16
16
|
- - ">="
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
|
-
version:
|
|
18
|
+
version: 7.2.3.2
|
|
19
|
+
- - "<"
|
|
20
|
+
- !ruby/object:Gem::Version
|
|
21
|
+
version: '9.0'
|
|
19
22
|
type: :runtime
|
|
20
23
|
prerelease: false
|
|
21
24
|
version_requirements: !ruby/object:Gem::Requirement
|
|
22
25
|
requirements:
|
|
23
26
|
- - ">="
|
|
24
27
|
- !ruby/object:Gem::Version
|
|
25
|
-
version:
|
|
28
|
+
version: 7.2.3.2
|
|
29
|
+
- - "<"
|
|
30
|
+
- !ruby/object:Gem::Version
|
|
31
|
+
version: '9.0'
|
|
26
32
|
- !ruby/object:Gem::Dependency
|
|
27
33
|
name: rake
|
|
28
34
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -43,14 +49,20 @@ dependencies:
|
|
|
43
49
|
requirements:
|
|
44
50
|
- - ">="
|
|
45
51
|
- !ruby/object:Gem::Version
|
|
46
|
-
version:
|
|
52
|
+
version: 7.2.3.2
|
|
53
|
+
- - "<"
|
|
54
|
+
- !ruby/object:Gem::Version
|
|
55
|
+
version: '9.0'
|
|
47
56
|
type: :development
|
|
48
57
|
prerelease: false
|
|
49
58
|
version_requirements: !ruby/object:Gem::Requirement
|
|
50
59
|
requirements:
|
|
51
60
|
- - ">="
|
|
52
61
|
- !ruby/object:Gem::Version
|
|
53
|
-
version:
|
|
62
|
+
version: 7.2.3.2
|
|
63
|
+
- - "<"
|
|
64
|
+
- !ruby/object:Gem::Version
|
|
65
|
+
version: '9.0'
|
|
54
66
|
- !ruby/object:Gem::Dependency
|
|
55
67
|
name: rspec
|
|
56
68
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -253,14 +265,14 @@ dependencies:
|
|
|
253
265
|
requirements:
|
|
254
266
|
- - "~>"
|
|
255
267
|
- !ruby/object:Gem::Version
|
|
256
|
-
version: '3.
|
|
268
|
+
version: '3.9'
|
|
257
269
|
type: :development
|
|
258
270
|
prerelease: false
|
|
259
271
|
version_requirements: !ruby/object:Gem::Requirement
|
|
260
272
|
requirements:
|
|
261
273
|
- - "~>"
|
|
262
274
|
- !ruby/object:Gem::Version
|
|
263
|
-
version: '3.
|
|
275
|
+
version: '3.9'
|
|
264
276
|
- !ruby/object:Gem::Dependency
|
|
265
277
|
name: rubocop-thread_safety
|
|
266
278
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -317,6 +329,20 @@ dependencies:
|
|
|
317
329
|
- - "~>"
|
|
318
330
|
- !ruby/object:Gem::Version
|
|
319
331
|
version: '3'
|
|
332
|
+
- !ruby/object:Gem::Dependency
|
|
333
|
+
name: parallel
|
|
334
|
+
requirement: !ruby/object:Gem::Requirement
|
|
335
|
+
requirements:
|
|
336
|
+
- - "~>"
|
|
337
|
+
- !ruby/object:Gem::Version
|
|
338
|
+
version: '1.26'
|
|
339
|
+
type: :development
|
|
340
|
+
prerelease: false
|
|
341
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
342
|
+
requirements:
|
|
343
|
+
- - "~>"
|
|
344
|
+
- !ruby/object:Gem::Version
|
|
345
|
+
version: '1.26'
|
|
320
346
|
description: 'ActiveVersion is a language-agnostic versioning library that unifies
|
|
321
347
|
three core use cases: translations (locale-based), revisions/drafts (workflow snapshots),
|
|
322
348
|
and audits (change tracking).'
|
|
@@ -373,6 +399,7 @@ files:
|
|
|
373
399
|
- lib/active_version/translations.rb
|
|
374
400
|
- lib/active_version/translations/has_translations.rb
|
|
375
401
|
- lib/active_version/translations/translation_record.rb
|
|
402
|
+
- lib/active_version/unique_version_collision.rb
|
|
376
403
|
- lib/active_version/version.rb
|
|
377
404
|
- lib/active_version/version_registry.rb
|
|
378
405
|
- lib/generators/active_version/audits/audits_generator.rb
|