active_version 1.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 +7 -0
- data/CHANGELOG.md +36 -0
- data/LICENSE.md +21 -0
- data/README.md +492 -0
- data/SECURITY.md +29 -0
- data/lib/active_version/adapters/active_record/audits.rb +36 -0
- data/lib/active_version/adapters/active_record/base.rb +37 -0
- data/lib/active_version/adapters/active_record/revisions.rb +49 -0
- data/lib/active_version/adapters/active_record/translations.rb +45 -0
- data/lib/active_version/adapters/active_record.rb +10 -0
- data/lib/active_version/adapters/sequel/versioning.rb +282 -0
- data/lib/active_version/adapters/sequel.rb +9 -0
- data/lib/active_version/adapters.rb +5 -0
- data/lib/active_version/audits/audit_record/callbacks.rb +180 -0
- data/lib/active_version/audits/audit_record/serializers.rb +49 -0
- data/lib/active_version/audits/audit_record.rb +522 -0
- data/lib/active_version/audits/has_audits/audit_callbacks.rb +46 -0
- data/lib/active_version/audits/has_audits/audit_combiner.rb +212 -0
- data/lib/active_version/audits/has_audits/audit_writer.rb +282 -0
- data/lib/active_version/audits/has_audits/change_filters.rb +114 -0
- data/lib/active_version/audits/has_audits/database_adapter_helper.rb +86 -0
- data/lib/active_version/audits/has_audits.rb +891 -0
- data/lib/active_version/audits/sql_builder.rb +263 -0
- data/lib/active_version/audits.rb +10 -0
- data/lib/active_version/column_mapper.rb +92 -0
- data/lib/active_version/configuration.rb +124 -0
- data/lib/active_version/database/triggers/postgresql.rb +243 -0
- data/lib/active_version/database.rb +7 -0
- data/lib/active_version/instrumentation.rb +226 -0
- data/lib/active_version/migrators/audited.rb +84 -0
- data/lib/active_version/migrators/base.rb +191 -0
- data/lib/active_version/migrators.rb +8 -0
- data/lib/active_version/query.rb +105 -0
- data/lib/active_version/railtie.rb +17 -0
- data/lib/active_version/revisions/has_revisions/revision_manipulation.rb +499 -0
- data/lib/active_version/revisions/has_revisions/revision_queries.rb +182 -0
- data/lib/active_version/revisions/has_revisions.rb +443 -0
- data/lib/active_version/revisions/revision_record.rb +287 -0
- data/lib/active_version/revisions/sql_builder.rb +266 -0
- data/lib/active_version/revisions.rb +10 -0
- data/lib/active_version/runtime.rb +148 -0
- data/lib/active_version/sharding/connection_router.rb +20 -0
- data/lib/active_version/sharding.rb +7 -0
- data/lib/active_version/tasks/active_version.rake +29 -0
- data/lib/active_version/translations/has_translations.rb +350 -0
- data/lib/active_version/translations/translation_record.rb +258 -0
- data/lib/active_version/translations.rb +9 -0
- data/lib/active_version/version.rb +3 -0
- data/lib/active_version/version_registry.rb +87 -0
- data/lib/active_version.rb +329 -0
- data/lib/generators/active_version/audits/audits_generator.rb +65 -0
- data/lib/generators/active_version/audits/templates/audit_model.rb.erb +16 -0
- data/lib/generators/active_version/audits/templates/migration_jsonb.rb.erb +33 -0
- data/lib/generators/active_version/audits/templates/migration_table.rb.erb +34 -0
- data/lib/generators/active_version/install/install_generator.rb +19 -0
- data/lib/generators/active_version/install/templates/initializer.rb.erb +38 -0
- data/lib/generators/active_version/revisions/revisions_generator.rb +71 -0
- data/lib/generators/active_version/revisions/templates/backfill_migration.rb.erb +19 -0
- data/lib/generators/active_version/revisions/templates/migration.rb.erb +20 -0
- data/lib/generators/active_version/revisions/templates/revision_model.rb.erb +8 -0
- data/lib/generators/active_version/translations/templates/migration.rb.erb +16 -0
- data/lib/generators/active_version/translations/templates/translation_model.rb.erb +15 -0
- data/lib/generators/active_version/translations/translations_generator.rb +73 -0
- data/lib/generators/active_version/triggers/templates/migration.rb.erb +100 -0
- data/lib/generators/active_version/triggers/triggers_generator.rb +74 -0
- data/sig/active_version/advanced.rbs +51 -0
- data/sig/active_version/audits.rbs +128 -0
- data/sig/active_version/configuration.rbs +38 -0
- data/sig/active_version/core.rbs +53 -0
- data/sig/active_version/instrumentation.rbs +17 -0
- data/sig/active_version/registry_and_mapping.rbs +18 -0
- data/sig/active_version/revisions.rbs +70 -0
- data/sig/active_version/runtime.rbs +29 -0
- data/sig/active_version/translations.rbs +43 -0
- data/sig/active_version.rbs +3 -0
- metadata +443 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: ec3713fc6fd0f073d9bb77d1e44c674a2023cf15f263fa05180457648052e8a1
|
|
4
|
+
data.tar.gz: 65ba5308255d161420e028564050e75044b975c9f8b520605d1caedab3d7f05b
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: eaf25bb79e12b3a85144c6c191a31ab97dea2a94c91dca682fa3cc9a729e648c29fd2e593605452945bfce0da7e2b391e290998aa9e69113ab00434b7627adca
|
|
7
|
+
data.tar.gz: fba81088323b53f4ad45acf8e04e82c63085854fcc407c7b4321dd687d06a12f336c1e4fe5f9224ce99cb3125f5e4735ac5237622f39008dbb9da74361562682
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# CHANGELOG
|
|
2
|
+
|
|
3
|
+
## 1.0.0 (2026-03-08)
|
|
4
|
+
|
|
5
|
+
- Initial release of ActiveVersion library
|
|
6
|
+
- Added translations module with locale-based versioning and `has_translations` declaration
|
|
7
|
+
- Added `translate(attr, locale:)` and `translation(locale:)` methods for accessing translations
|
|
8
|
+
- Added `translated_scopes` for dynamic scopes and `translated_copies` for value copying
|
|
9
|
+
- Added automatic default translation creation and locale-based queries
|
|
10
|
+
- Added revisions module with schema-aligned snapshots and `has_revisions` declaration
|
|
11
|
+
- Added automatic revision creation on update with version numbering
|
|
12
|
+
- Added `at_version(version)`, `at(time:, version:)`, and `at!(time:, version:)` methods for version access
|
|
13
|
+
- Added `undo!`, `redo!`, and `switch_to!(version)` methods for version control
|
|
14
|
+
- Added `diff_from(time:, version:)` for diff generation and `create_snapshot!` for manual snapshots
|
|
15
|
+
- Added `without_revisions` for temporarily disabling revision tracking
|
|
16
|
+
- Added audits module with JSONB and table storage options and `has_audits` declaration
|
|
17
|
+
- Added automatic audit creation on create/update/destroy operations
|
|
18
|
+
- Added `audit_sql` for single record SQL generation and `batch_insert_sql` for batch operations
|
|
19
|
+
- Added context tracking (global and per-model) for audits
|
|
20
|
+
- Added comment support, user tracking, request UUID and remote address tracking
|
|
21
|
+
- Added revision reconstruction from audits
|
|
22
|
+
- Added conditional auditing with `if:` and `unless:` options
|
|
23
|
+
- Added audit combining for storage limits and redaction support for sensitive data
|
|
24
|
+
- Added encrypted attributes filtering
|
|
25
|
+
- Added PostgreSQL trigger functions for audits and revisions with trigger generators
|
|
26
|
+
- Added ability to disable triggers via session variables and context support via session variables
|
|
27
|
+
- Added sharding support with connection routing per model, global and per-model shard configuration
|
|
28
|
+
- Added `connection_for`, `adapter_for`, and `with_connection` methods for shard management
|
|
29
|
+
- Added query builder with `ActiveVersion::Query.audits(record, opts)`, `ActiveVersion::Query.translations(record, opts)`, and `ActiveVersion::Query.revisions(record, opts)` methods
|
|
30
|
+
- Added shard-aware queries
|
|
31
|
+
- Added migration helpers with `ActiveVersion::Migrators::Base` base class and `ActiveVersion::Migrators::Audited` for audited gem migration
|
|
32
|
+
- Added Rails generators: `rails g active_version:install`, `rails g active_version:translations Model`, `rails g active_version:revisions Model`, `rails g active_version:audits Model --storage=json_column`, `rails g active_version:triggers Model --type=audit`
|
|
33
|
+
- Added instrumentation hooks via ActiveSupport::Notifications
|
|
34
|
+
- Added configurable column naming and per-model and global configuration options
|
|
35
|
+
- Added comprehensive test suite with unit tests, integration tests, and test helpers
|
|
36
|
+
|
data/LICENSE.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023 amkisko
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
# ActiveVersion
|
|
2
|
+
|
|
3
|
+
[](https://badge.fury.io/rb/active_version) [](https://github.com/amkisko/active_version.rb/actions/workflows/test.yml) [](https://codecov.io/gh/amkisko/active_version.rb)
|
|
4
|
+
|
|
5
|
+
A unified versioning library for ActiveRecord that handles translations, revisions, and audits in a single, extensible architecture.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- Translations: Locale-based versioning with automatic value copying
|
|
10
|
+
- Revisions: Schema-aligned snapshots for workflow management
|
|
11
|
+
- Audits: `:json_column`, `:yaml_column`, or `:mirror_columns` change tracking
|
|
12
|
+
- Database Triggers: Optional PostgreSQL triggers for zero-overhead versioning
|
|
13
|
+
- Sharding Support: Route version tables to separate databases
|
|
14
|
+
- SQL Generation: Batch operations via SQL generation
|
|
15
|
+
- Configurable: Flexible column naming and per-model configuration
|
|
16
|
+
|
|
17
|
+
## Deliberate Differences from PaperTrail, Audited, and Similar Gems
|
|
18
|
+
|
|
19
|
+
ActiveVersion intentionally chooses explicitness and operational readability over implicit defaults.
|
|
20
|
+
|
|
21
|
+
- Do not enable ActiveVersion together with `audited` or `paper_trail` on the same model.
|
|
22
|
+
These libraries will start conflicting with each other on method names occupation.
|
|
23
|
+
- Manual provisioning is required per model and feature.
|
|
24
|
+
You explicitly generate and wire audits, revisions, and translations tables/models for each domain model. We do not auto-provision global defaults behind the scenes.
|
|
25
|
+
Example: unlike gems that create and rely on a single default audits table automatically, ActiveVersion expects deliberate setup per model.
|
|
26
|
+
- Audit schema is configured explicitly on destination audit models.
|
|
27
|
+
Storage mode and audit column mapping belong to the audit model/table contract (`configure_audit`), not hidden global magic.
|
|
28
|
+
- Revision/translation schema is also destination-model owned.
|
|
29
|
+
Version/locale and identity key semantics are declared on revision/translation models.
|
|
30
|
+
- No incremental/delta-chain storage for revisions or audits.
|
|
31
|
+
ActiveVersion avoids patch-chain persistence and reconstruction complexity as a core design choice.
|
|
32
|
+
- No diff/patch-based persistence layer.
|
|
33
|
+
We prefer straightforward record payloads and predictable query/debug behavior.
|
|
34
|
+
Rationale: diff/patch persistence is costly to implement and maintain at scale, while storage footprint is usually manageable with retention policies, archival, partitioning, or cold storage.
|
|
35
|
+
|
|
36
|
+
## Installation
|
|
37
|
+
|
|
38
|
+
Add this line to your application's Gemfile:
|
|
39
|
+
|
|
40
|
+
```ruby
|
|
41
|
+
gem 'active_version'
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
And then execute:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
$ bundle install
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Or install it yourself as:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
$ gem install active_version
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Setting up in an existing project
|
|
57
|
+
|
|
58
|
+
For a step-by-step guide (prerequisites, generators, migrations, model setup, and optional triggers), see [docs/SETUP_IN_EXISTING_PROJECT.md](docs/SETUP_IN_EXISTING_PROJECT.md).
|
|
59
|
+
|
|
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
|
+
|
|
62
|
+
## Quick Start
|
|
63
|
+
|
|
64
|
+
### Setup
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
# Generate initializer
|
|
68
|
+
rails g active_version:install
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Translations
|
|
72
|
+
|
|
73
|
+
```ruby
|
|
74
|
+
# Generate translation support
|
|
75
|
+
rails g active_version:translations Post
|
|
76
|
+
|
|
77
|
+
# In your model
|
|
78
|
+
class Post < ApplicationRecord
|
|
79
|
+
has_translations
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Usage
|
|
83
|
+
post = Post.create!(title: "Hello", body: "World")
|
|
84
|
+
post.translations.create!(locale: "fi", title: "Hei", body: "Maailma")
|
|
85
|
+
|
|
86
|
+
post.translate(:title, locale: "fi") # => "Hei"
|
|
87
|
+
post.translation(locale: "fi") # => PostTranslation instance
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Revisions
|
|
91
|
+
|
|
92
|
+
```ruby
|
|
93
|
+
# Generate revision support
|
|
94
|
+
rails g active_version:revisions Post
|
|
95
|
+
|
|
96
|
+
# In your model
|
|
97
|
+
class Post < ApplicationRecord
|
|
98
|
+
has_revisions
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Usage
|
|
102
|
+
post = Post.create!(title: "v1")
|
|
103
|
+
post.update!(title: "v2") # Creates revision automatically
|
|
104
|
+
|
|
105
|
+
post.current_version # => 1
|
|
106
|
+
post.revision(version: 1) # => PostRevision instance
|
|
107
|
+
post.at_version(1) # => Post instance at version 1
|
|
108
|
+
post.undo! # => Revert to previous version
|
|
109
|
+
post.diff_from(version: 1) # => { "changes" => { "title" => {...} } }
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Audits
|
|
113
|
+
|
|
114
|
+
```ruby
|
|
115
|
+
# Generate audit support
|
|
116
|
+
rails g active_version:audits Post --storage=json_column
|
|
117
|
+
|
|
118
|
+
# In your model
|
|
119
|
+
class Post < ApplicationRecord
|
|
120
|
+
has_audits
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Usage
|
|
124
|
+
post = Post.create!(title: "Hello")
|
|
125
|
+
post.update!(title: "World")
|
|
126
|
+
|
|
127
|
+
post.audits.count # => 2
|
|
128
|
+
post.audits.last.audited_changes # => {"title" => ["Hello", "World"]}
|
|
129
|
+
post.revision(version: 1) # => Post instance at version 1
|
|
130
|
+
|
|
131
|
+
# Generate SQL for batch operations
|
|
132
|
+
sql = post.audit_sql
|
|
133
|
+
PostAudit.batch_insert_sql([post1, post2], force: true)
|
|
134
|
+
PostAudit.batch_insert_sql(force: true) { [post1, post2] }
|
|
135
|
+
PostRevision.batch_insert_sql(version: 1) do |batch|
|
|
136
|
+
post1 = Post.create!(title: "A")
|
|
137
|
+
post2 = Post.create!(title: "B")
|
|
138
|
+
post3 = Post.create!(title: "C")
|
|
139
|
+
post1.update!(title: "A1")
|
|
140
|
+
post2.update!(title: "B1")
|
|
141
|
+
post3.destroy!
|
|
142
|
+
end
|
|
143
|
+
PostAudit.batch_insert(force: true) do |batch|
|
|
144
|
+
batch << post1
|
|
145
|
+
batch << post2
|
|
146
|
+
end
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### Batch Semantics and Feature Interaction
|
|
150
|
+
|
|
151
|
+
- `batch_insert` always executes SQL generated by `batch_insert_sql`.
|
|
152
|
+
- By default, clean records are skipped. Use `force: true` or `allow_saved: true` to include them.
|
|
153
|
+
- Block mode supports:
|
|
154
|
+
- returning records array
|
|
155
|
+
- collector style (`|batch| batch << record`)
|
|
156
|
+
- callback-capture mode for side-effect blocks (no explicit returned records)
|
|
157
|
+
- In batch mode, merge-style features are intentionally not applied:
|
|
158
|
+
- audit compaction (`max_audits` / combine flow) is not executed
|
|
159
|
+
- revision debounce merge window is not executed
|
|
160
|
+
- Other behavior still applies from model configuration:
|
|
161
|
+
- identity resolution and foreign key mapping
|
|
162
|
+
- destination schema configuration
|
|
163
|
+
- context merging (`options[:context]` for audit batch helpers)
|
|
164
|
+
|
|
165
|
+
## Configuration
|
|
166
|
+
|
|
167
|
+
### Global Configuration
|
|
168
|
+
|
|
169
|
+
```ruby
|
|
170
|
+
# config/initializers/active_version.rb
|
|
171
|
+
ActiveVersion.configure do |config|
|
|
172
|
+
config.auditing_enabled = true
|
|
173
|
+
config.current_user_method = :current_user
|
|
174
|
+
|
|
175
|
+
# Global fallback naming (prefer destination audit model config instead)
|
|
176
|
+
config.translation_locale_column = :locale
|
|
177
|
+
config.revision_version_column = :version
|
|
178
|
+
end
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### Destination Audit Model Configuration (Preferred)
|
|
182
|
+
|
|
183
|
+
```ruby
|
|
184
|
+
class PostAudit < ApplicationRecord
|
|
185
|
+
include ActiveVersion::Audits::AuditRecord
|
|
186
|
+
|
|
187
|
+
configure_audit do
|
|
188
|
+
storage :json_column # :json_column | :yaml_column | :mirror_columns
|
|
189
|
+
action_column :action
|
|
190
|
+
changes_column :audited_changes
|
|
191
|
+
context_column :audited_context
|
|
192
|
+
comment_column :comment
|
|
193
|
+
version_column :version
|
|
194
|
+
user_column :user_id
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
Custom storage providers can be registered per audit model:
|
|
200
|
+
|
|
201
|
+
```ruby
|
|
202
|
+
class PostAudit < ApplicationRecord
|
|
203
|
+
include ActiveVersion::Audits::AuditRecord
|
|
204
|
+
|
|
205
|
+
register_storage_provider(:msgpack) do |_audit_class, _column_name|
|
|
206
|
+
MyMsgpackCodec.new # must respond to #load and #dump
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
configure_audit do
|
|
210
|
+
storage :msgpack
|
|
211
|
+
action_column :action
|
|
212
|
+
changes_column :audited_changes
|
|
213
|
+
context_column :audited_context
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
### Destination Revision/Translation Configuration (Preferred)
|
|
219
|
+
|
|
220
|
+
```ruby
|
|
221
|
+
class PostRevision < ApplicationRecord
|
|
222
|
+
include ActiveVersion::Revisions::RevisionRecord
|
|
223
|
+
|
|
224
|
+
configure_revision(version_column: :version,
|
|
225
|
+
foreign_key: :post_id
|
|
226
|
+
)
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
class PostTranslation < ApplicationRecord
|
|
230
|
+
include ActiveVersion::Translations::TranslationRecord
|
|
231
|
+
|
|
232
|
+
configure_translation(locale_column: :locale,
|
|
233
|
+
foreign_key: :post_id
|
|
234
|
+
)
|
|
235
|
+
end
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
### Per-Model Configuration
|
|
239
|
+
|
|
240
|
+
```ruby
|
|
241
|
+
class Post < ApplicationRecord
|
|
242
|
+
has_audits(
|
|
243
|
+
table_name: "custom_post_audits",
|
|
244
|
+
identity_resolver: :external_id, # optional: use custom identity value for auditable_id
|
|
245
|
+
only: [:title, :body], # Only track these fields
|
|
246
|
+
except: [:internal_notes], # Don't track these
|
|
247
|
+
max_audits: 100, # Limit storage
|
|
248
|
+
associated_with: :company, # Track associated model
|
|
249
|
+
if: :should_audit?, # Conditional auditing
|
|
250
|
+
comment_required: true # Require comments
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
has_revisions(
|
|
254
|
+
# Revisions are always table-based
|
|
255
|
+
table_name: "custom_post_revisions",
|
|
256
|
+
foreign_key: :record_uuid, # optional: custom FK column in revision table
|
|
257
|
+
identity_resolver: :external_id # optional: source value used for FK writes/queries
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
has_translations(
|
|
261
|
+
# Translations are always table-based
|
|
262
|
+
table_name: "custom_post_translations",
|
|
263
|
+
foreign_key: :record_uuid # optional: custom FK column in translation table
|
|
264
|
+
)
|
|
265
|
+
end
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
## Advanced Features
|
|
269
|
+
|
|
270
|
+
### Database Triggers
|
|
271
|
+
|
|
272
|
+
```bash
|
|
273
|
+
# Generate trigger for audits
|
|
274
|
+
rails g active_version:triggers Post --type=audit
|
|
275
|
+
|
|
276
|
+
# Generate trigger for revisions
|
|
277
|
+
rails g active_version:triggers Post --type=revision
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
### Infrastructure Ownership
|
|
281
|
+
|
|
282
|
+
```ruby
|
|
283
|
+
# Keep infrastructure concerns in application code:
|
|
284
|
+
# - connection topology routing / connected_to blocks
|
|
285
|
+
# - partition management
|
|
286
|
+
# - replication / topology policy
|
|
287
|
+
#
|
|
288
|
+
# ActiveVersion intentionally does not route between connections/topologies.
|
|
289
|
+
# It follows your current ActiveRecord connection and declared model schema.
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
### Runtime Adapter (Advanced)
|
|
293
|
+
|
|
294
|
+
```ruby
|
|
295
|
+
# Default runtime is ActiveRecord-backed.
|
|
296
|
+
ActiveVersion.runtime_adapter
|
|
297
|
+
|
|
298
|
+
# Advanced/non-AR integrations can provide their own adapter object:
|
|
299
|
+
# - base_connection
|
|
300
|
+
# - connection_for(model_class, version_type)
|
|
301
|
+
# Optional capability hooks:
|
|
302
|
+
# - supports_transactional_context?(connection)
|
|
303
|
+
# - supports_current_transaction_id?(connection)
|
|
304
|
+
#
|
|
305
|
+
# Example:
|
|
306
|
+
# ActiveVersion.runtime_adapter = MyCustomAdapter.new
|
|
307
|
+
|
|
308
|
+
# Optional boot-time contract check:
|
|
309
|
+
# ActiveVersion::Runtime.valid_adapter?(ActiveVersion.runtime_adapter) # => true/false
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
Required adapter contract:
|
|
313
|
+
|
|
314
|
+
```ruby
|
|
315
|
+
class MyCustomAdapter
|
|
316
|
+
def base_connection
|
|
317
|
+
# returns a connection-like object for global/runtime operations
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
def connection_for(model_class, version_type)
|
|
321
|
+
# returns the connection-like object used for this source model/type
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
# Optional capability hooks. If omitted, ActiveVersion falls back to
|
|
325
|
+
# adapter_name-based PostgreSQL detection.
|
|
326
|
+
def supports_transactional_context?(connection)
|
|
327
|
+
connection.adapter_name.to_s.casecmp("postgresql").zero?
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
def supports_current_transaction_id?(connection)
|
|
331
|
+
connection.adapter_name.to_s.casecmp("postgresql").zero?
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
def supports_partition_catalog_checks?(connection)
|
|
335
|
+
connection.adapter_name.to_s.casecmp("postgresql").zero?
|
|
336
|
+
end
|
|
337
|
+
end
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
See runnable samples:
|
|
341
|
+
- [`examples/sinatra_demo/runtime_adapter_example.rb`](examples/sinatra_demo/runtime_adapter_example.rb)
|
|
342
|
+
- [`examples/sinatra_demo/sequel_like_runtime_adapter_example.rb`](examples/sinatra_demo/sequel_like_runtime_adapter_example.rb)
|
|
343
|
+
|
|
344
|
+
For partitioning and connection-topology suggestions, see [docs/PARTITIONING_AND_SHARDING.md](docs/PARTITIONING_AND_SHARDING.md).
|
|
345
|
+
For non-ActiveRecord runtime adapter guidance, see [docs/NON_ACTIVE_RECORD.md](docs/NON_ACTIVE_RECORD.md).
|
|
346
|
+
For dependency/license disclosure, see [docs/THIRD_PARTY_NOTICES.md](docs/THIRD_PARTY_NOTICES.md).
|
|
347
|
+
|
|
348
|
+
### Query Builder
|
|
349
|
+
|
|
350
|
+
```ruby
|
|
351
|
+
# Unified query interface
|
|
352
|
+
ActiveVersion::Query.audits(post, preload: :user, order_by: { desc: :created_at })
|
|
353
|
+
ActiveVersion::Query.translations(post, locale: "en")
|
|
354
|
+
ActiveVersion::Query.revisions(post, version: 2)
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
### Context Tracking
|
|
358
|
+
|
|
359
|
+
```ruby
|
|
360
|
+
# Set global context
|
|
361
|
+
ActiveVersion.with_context(ip: request.ip, user_agent: request.user_agent) do
|
|
362
|
+
post.update!(title: "New")
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
# Set per-model context
|
|
366
|
+
post.audit_context = { request_id: "123" }
|
|
367
|
+
post.save!
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
### Disabling Versioning
|
|
371
|
+
|
|
372
|
+
```ruby
|
|
373
|
+
# Global
|
|
374
|
+
ActiveVersion.without_auditing do
|
|
375
|
+
# ...
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
# Per-model
|
|
379
|
+
Post.without_auditing do
|
|
380
|
+
# ...
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
# Per-instance
|
|
384
|
+
post.without_auditing do
|
|
385
|
+
# ...
|
|
386
|
+
end
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
## Migration from Other Gems
|
|
390
|
+
|
|
391
|
+
### From audited
|
|
392
|
+
|
|
393
|
+
```ruby
|
|
394
|
+
# 1. Generate audit tables
|
|
395
|
+
rails g active_version:audits Post --storage=json_column
|
|
396
|
+
|
|
397
|
+
# 2. Migrate data
|
|
398
|
+
ActiveVersion::Migrators::Audited.migrate(Post)
|
|
399
|
+
|
|
400
|
+
# 3. Update code
|
|
401
|
+
# Replace audited with has_audits
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
### From paper_trail
|
|
405
|
+
|
|
406
|
+
```ruby
|
|
407
|
+
# 1. Generate revision tables
|
|
408
|
+
rails g active_version:revisions Post
|
|
409
|
+
|
|
410
|
+
# 2. Migrate data (manual or via migrator)
|
|
411
|
+
# 3. Update code
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
## Benchmarking
|
|
415
|
+
|
|
416
|
+
Benchmarks are available under RSpec with `:benchmark` tag and are excluded from normal runs by default.
|
|
417
|
+
Latest published results and analysis are in [BENCHMARK.md](BENCHMARK.md).
|
|
418
|
+
The report includes per-record overhead vs ActiveRecord baseline (`p5`, `mean`, `p95`).
|
|
419
|
+
The report also separates ActiveRecord and Sequel benchmark groups to avoid cross-ORM baseline mixing.
|
|
420
|
+
|
|
421
|
+
```bash
|
|
422
|
+
# Normal test run (benchmarks excluded)
|
|
423
|
+
bundle exec rspec
|
|
424
|
+
|
|
425
|
+
# Explicit benchmark run
|
|
426
|
+
usr/bin/benchmark.rb
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
`usr/bin/benchmark.rb` runs two sections by default: `sqlite` and `postgresql`.
|
|
430
|
+
|
|
431
|
+
Environment knobs:
|
|
432
|
+
|
|
433
|
+
- `ACTIVE_VERSION_BENCH_ITERATIONS` (default: `5000`)
|
|
434
|
+
- `ACTIVE_VERSION_BENCH_WARMUP` (default: `200`)
|
|
435
|
+
- `ACTIVE_VERSION_BENCH_ROUNDS` (default: `5`)
|
|
436
|
+
- `BENCHMARK=1` (set automatically by `usr/bin/benchmark.rb`)
|
|
437
|
+
|
|
438
|
+
## API Reference
|
|
439
|
+
|
|
440
|
+
### Translations
|
|
441
|
+
|
|
442
|
+
- `has_translations` - Declare model has translations
|
|
443
|
+
- `translate(attr, locale:)` - Get translated attribute
|
|
444
|
+
- `translation(locale:)` - Get translation record
|
|
445
|
+
- `translated_scopes(*attrs)` - Generate scopes for translated attributes
|
|
446
|
+
- `translated_copies(*attrs)` - Generate copy methods
|
|
447
|
+
|
|
448
|
+
### Revisions
|
|
449
|
+
|
|
450
|
+
- `has_revisions` - Declare model has revisions
|
|
451
|
+
- `current_version` - Get current version number
|
|
452
|
+
- `revision(version:)` - Get revision record
|
|
453
|
+
- `at_version(version)` - Get copy at version
|
|
454
|
+
- `at(time:, version:)` - Get copy at time/version
|
|
455
|
+
- `at!(time:, version:)` - Revert to time/version
|
|
456
|
+
- `undo!` - Revert to previous version
|
|
457
|
+
- `redo!` - Restore to future version
|
|
458
|
+
- `switch_to!(version)` - Switch to version
|
|
459
|
+
- `diff_from(time:, version:)` - Get diff from time/version
|
|
460
|
+
- `create_snapshot!` - Manually create snapshot
|
|
461
|
+
|
|
462
|
+
### Audits
|
|
463
|
+
|
|
464
|
+
- `has_audits` - Declare model has audits
|
|
465
|
+
- `audit_sql` - Generate SQL for single audit
|
|
466
|
+
- `batch_insert_sql(records, **options)` - Generate SQL for batch inserts (also supports block)
|
|
467
|
+
- `batch_insert(records, **options)` - Execute batch inserts (also supports block)
|
|
468
|
+
- `revision(version:)` - Reconstruct from audits
|
|
469
|
+
- `revision_at(time:)` - Reconstruct at time
|
|
470
|
+
- `own_and_associated_audits` - Get own and associated audits
|
|
471
|
+
|
|
472
|
+
## Requirements
|
|
473
|
+
|
|
474
|
+
- Ruby >= 3.0.0
|
|
475
|
+
- ActiveRecord >= 6.0.0 (default runtime)
|
|
476
|
+
- SQLite or MySQL or PostgreSQL (optional, for triggers and JSONB support)
|
|
477
|
+
|
|
478
|
+
## Contributing
|
|
479
|
+
|
|
480
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/amkisko/active_version.rb.
|
|
481
|
+
|
|
482
|
+
## License
|
|
483
|
+
|
|
484
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
|
485
|
+
|
|
486
|
+
## Sponsors
|
|
487
|
+
|
|
488
|
+
Sponsored by [Kisko Labs](https://www.kiskolabs.com).
|
|
489
|
+
|
|
490
|
+
<a href="https://www.kiskolabs.com">
|
|
491
|
+
<img src="kisko.svg" width="200" alt="Sponsored by Kisko Labs" />
|
|
492
|
+
</a>
|
data/SECURITY.md
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# SECURITY
|
|
2
|
+
|
|
3
|
+
## Reporting a Vulnerability
|
|
4
|
+
|
|
5
|
+
Do NOT open a public GitHub issue for security vulnerabilities.
|
|
6
|
+
|
|
7
|
+
Email security details to: security@kiskolabs.com
|
|
8
|
+
|
|
9
|
+
Include: description, steps to reproduce, potential impact, and suggested fix (if available).
|
|
10
|
+
|
|
11
|
+
### Response Timeline
|
|
12
|
+
|
|
13
|
+
- We will acknowledge receipt of your report
|
|
14
|
+
- We will provide an initial assessment
|
|
15
|
+
- We will keep you informed of our progress and resolution timeline
|
|
16
|
+
|
|
17
|
+
### Disclosure Policy
|
|
18
|
+
|
|
19
|
+
- We will work with you to understand and resolve the issue
|
|
20
|
+
- We will credit you for the discovery (unless you prefer to remain anonymous)
|
|
21
|
+
- We will publish a security advisory after the vulnerability is patched
|
|
22
|
+
- We will coordinate public disclosure with you
|
|
23
|
+
|
|
24
|
+
## Automation Security
|
|
25
|
+
|
|
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
|
+
|
|
28
|
+
* Supply Chain: All automated dependencies must be verified.
|
|
29
|
+
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
module ActiveVersion
|
|
2
|
+
module Adapters
|
|
3
|
+
module ActiveRecord
|
|
4
|
+
module Audits
|
|
5
|
+
extend ActiveSupport::Concern
|
|
6
|
+
|
|
7
|
+
included do
|
|
8
|
+
# Add has_audits method to ActiveRecord::Base
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
module ClassMethods
|
|
12
|
+
# Declare that a model has audits
|
|
13
|
+
def has_audits(options = {})
|
|
14
|
+
include ActiveVersion::Audits::HasAudits unless included_modules.include?(ActiveVersion::Audits::HasAudits)
|
|
15
|
+
|
|
16
|
+
# Call the HasAudits implementation once included
|
|
17
|
+
ActiveVersion::Audits::HasAudits::ClassMethods.instance_method(:has_audits)
|
|
18
|
+
.bind_call(self, options)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Include audits adapter in ActiveRecord::Base
|
|
27
|
+
ActiveSupport.on_load(:active_record) do
|
|
28
|
+
include ActiveVersion::Adapters::ActiveRecord::Audits
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# If ActiveRecord::Base is already loaded, include immediately
|
|
32
|
+
if defined?(ActiveRecord::Base) && ActiveRecord::Base.respond_to?(:include)
|
|
33
|
+
unless ActiveRecord::Base.included_modules.include?(ActiveVersion::Adapters::ActiveRecord::Audits)
|
|
34
|
+
ActiveSupport.on_load(:active_record) { include ActiveVersion::Adapters::ActiveRecord::Audits }
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
module ActiveVersion
|
|
2
|
+
module Adapters
|
|
3
|
+
module ActiveRecord
|
|
4
|
+
# Base adapter for ActiveRecord integration
|
|
5
|
+
module Base
|
|
6
|
+
extend ActiveSupport::Concern
|
|
7
|
+
|
|
8
|
+
included do
|
|
9
|
+
# This will be extended by specific versioning modules
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# Instance method to get column names (delegates to class method)
|
|
13
|
+
# This provides compatibility with ActiveRecord's class method as an instance method
|
|
14
|
+
def column_names
|
|
15
|
+
self.class.column_names
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
module ClassMethods
|
|
19
|
+
# Check if model has versioning enabled
|
|
20
|
+
def has_versioning?(version_type)
|
|
21
|
+
ActiveVersion.registry.registered?(self, version_type)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Get version class for this model
|
|
25
|
+
def version_class_for(version_type)
|
|
26
|
+
ActiveVersion.registry.version_class_for(self, version_type)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Include base adapter in ActiveRecord::Base
|
|
35
|
+
ActiveSupport.on_load(:active_record) do
|
|
36
|
+
include ActiveVersion::Adapters::ActiveRecord::Base
|
|
37
|
+
end
|