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
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
require "rails/generators"
|
|
2
|
+
require "rails/generators/active_record"
|
|
3
|
+
|
|
4
|
+
module ActiveVersion
|
|
5
|
+
module Generators
|
|
6
|
+
class AuditsGenerator < Rails::Generators::NamedBase
|
|
7
|
+
include ActiveRecord::Generators::Migration
|
|
8
|
+
|
|
9
|
+
source_root File.expand_path("templates", __dir__)
|
|
10
|
+
|
|
11
|
+
class_option :storage,
|
|
12
|
+
type: :string,
|
|
13
|
+
default: "json_column",
|
|
14
|
+
desc: "Storage type: json_column, yaml_column, or mirror_columns"
|
|
15
|
+
|
|
16
|
+
def create_audit_model
|
|
17
|
+
template "audit_model.rb.erb",
|
|
18
|
+
File.join("app/models", class_path, "#{file_name}_audit.rb")
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def generate_migration_file
|
|
22
|
+
if options[:storage] == "mirror_columns"
|
|
23
|
+
migration_template "migration_table.rb.erb",
|
|
24
|
+
File.join("db/migrate", "create_#{table_name}.rb"),
|
|
25
|
+
migration_version: migration_version
|
|
26
|
+
else
|
|
27
|
+
migration_template "migration_jsonb.rb.erb",
|
|
28
|
+
File.join("db/migrate", "create_#{table_name}.rb"),
|
|
29
|
+
migration_version: migration_version
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def inject_has_audits
|
|
34
|
+
model_path = File.join("app/models", class_path, "#{file_name}.rb")
|
|
35
|
+
return unless File.exist?(model_path)
|
|
36
|
+
|
|
37
|
+
inject_into_class(model_path, class_name) do
|
|
38
|
+
" has_audits\n"
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def migration_version
|
|
45
|
+
"[#{ActiveRecord::Migration.current_version}]"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def table_name
|
|
49
|
+
"#{file_name}_audits"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def audit_class_name
|
|
53
|
+
"#{class_name}Audit"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def foreign_key
|
|
57
|
+
"#{file_name}_id"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def storage_type
|
|
61
|
+
options[:storage] || "json_column"
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
|
|
2
|
+
class <%= audit_class_name %> < ApplicationRecord
|
|
3
|
+
include ActiveVersion::Audits::AuditRecord
|
|
4
|
+
|
|
5
|
+
self.table_name = "<%= table_name %>"
|
|
6
|
+
|
|
7
|
+
configure_audit do
|
|
8
|
+
storage :<%= storage_type %>
|
|
9
|
+
action_column :action
|
|
10
|
+
changes_column :audited_changes
|
|
11
|
+
context_column :audited_context
|
|
12
|
+
comment_column :comment
|
|
13
|
+
version_column :version
|
|
14
|
+
user_column :user_id
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
class Create<%= audit_class_name.pluralize %> < ActiveRecord::Migration<%= migration_version %>
|
|
2
|
+
def change
|
|
3
|
+
create_table :<%= table_name %> do |t|
|
|
4
|
+
t.references :auditable, polymorphic: true, null: false, index: false
|
|
5
|
+
t.string :action, null: false
|
|
6
|
+
t.public_send(payload_column_type, :audited_changes, null: false)
|
|
7
|
+
t.integer :version, null: false
|
|
8
|
+
t.references :user, polymorphic: true
|
|
9
|
+
t.references :associated, polymorphic: true
|
|
10
|
+
t.text :comment
|
|
11
|
+
t.public_send(payload_column_type, :audited_context)
|
|
12
|
+
t.string :remote_address
|
|
13
|
+
t.string :request_uuid
|
|
14
|
+
|
|
15
|
+
t.timestamps
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
add_index :<%= table_name %>, [:auditable_type, :auditable_id, :version], unique: true, name: "index_<%= table_name %>_on_auditable_and_version"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def postgresql?
|
|
24
|
+
connection.adapter_name.to_s.casecmp("postgresql").zero?
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def payload_column_type
|
|
28
|
+
return :jsonb if postgresql?
|
|
29
|
+
return :json if connection.native_database_types.is_a?(Hash) && connection.native_database_types.key?(:json)
|
|
30
|
+
|
|
31
|
+
:text
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
class Create<%= audit_class_name.pluralize %> < ActiveRecord::Migration<%= migration_version %>
|
|
2
|
+
def change
|
|
3
|
+
create_table :<%= table_name %> do |t|
|
|
4
|
+
t.references :auditable, polymorphic: true, null: false, index: false
|
|
5
|
+
t.string :action, null: false
|
|
6
|
+
t.integer :version, null: false
|
|
7
|
+
t.references :user, polymorphic: true
|
|
8
|
+
t.references :associated, polymorphic: true
|
|
9
|
+
t.text :comment
|
|
10
|
+
t.public_send(payload_column_type, :audited_context)
|
|
11
|
+
t.string :remote_address
|
|
12
|
+
t.string :request_uuid
|
|
13
|
+
|
|
14
|
+
# Store changes as structured columns
|
|
15
|
+
# Note: Add columns based on your model's attributes
|
|
16
|
+
# Example:
|
|
17
|
+
# t.string :title
|
|
18
|
+
# t.text :body
|
|
19
|
+
|
|
20
|
+
t.timestamps
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
add_index :<%= table_name %>, [:auditable_type, :auditable_id, :version], unique: true, name: "index_<%= table_name %>_on_auditable_and_version"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def payload_column_type
|
|
29
|
+
return :jsonb if connection.adapter_name.to_s.casecmp("postgresql").zero?
|
|
30
|
+
return :json if connection.native_database_types.is_a?(Hash) && connection.native_database_types.key?(:json)
|
|
31
|
+
|
|
32
|
+
:text
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
require "rails/generators"
|
|
2
|
+
|
|
3
|
+
module ActiveVersion
|
|
4
|
+
module Generators
|
|
5
|
+
class InstallGenerator < Rails::Generators::Base
|
|
6
|
+
source_root File.expand_path("templates", __dir__)
|
|
7
|
+
|
|
8
|
+
desc "Creates ActiveVersion initializer and configuration file"
|
|
9
|
+
|
|
10
|
+
def create_initializer
|
|
11
|
+
template "initializer.rb.erb", "config/initializers/active_version.rb"
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def show_readme
|
|
15
|
+
readme "README" if behavior == :invoke
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
|
|
2
|
+
# ActiveVersion Configuration
|
|
3
|
+
# See https://github.com/amkisko/active_version.rb for documentation
|
|
4
|
+
|
|
5
|
+
ActiveVersion.configure do |config|
|
|
6
|
+
# Global auditing control
|
|
7
|
+
config.auditing_enabled = true
|
|
8
|
+
|
|
9
|
+
# Current user method (for audits)
|
|
10
|
+
config.current_user_method = :current_user
|
|
11
|
+
|
|
12
|
+
# Ignored attributes (won't be tracked in audits/revisions)
|
|
13
|
+
config.ignored_attributes = %w[lock_version created_at updated_at created_on updated_on]
|
|
14
|
+
|
|
15
|
+
# Translation defaults
|
|
16
|
+
config.translation_locale_column = :locale
|
|
17
|
+
config.translation_default_locale = :en
|
|
18
|
+
|
|
19
|
+
# Revision defaults
|
|
20
|
+
config.revision_version_column = :version
|
|
21
|
+
|
|
22
|
+
# Audit defaults
|
|
23
|
+
# Prefer explicit destination model config via:
|
|
24
|
+
# configure_audit { storage :json_column; action_column :action; ... }
|
|
25
|
+
# configure_revision(version_column:, foreign_key:, ...)
|
|
26
|
+
# configure_translation(locale_column:, foreign_key:, ...)
|
|
27
|
+
# These global values are fallbacks for audit models that do not declare schema.
|
|
28
|
+
config.audit_storage = :json_column
|
|
29
|
+
config.audit_auditable_column = :auditable
|
|
30
|
+
config.audit_associated_column = :associated
|
|
31
|
+
config.audit_remote_address_column = :remote_address
|
|
32
|
+
config.audit_request_uuid_column = :request_uuid
|
|
33
|
+
|
|
34
|
+
# Infrastructure note:
|
|
35
|
+
# Connection routing / connection topology / partition lifecycle are application-owned.
|
|
36
|
+
# ActiveVersion reads/writes using the active ActiveRecord connection.
|
|
37
|
+
|
|
38
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
require "rails/generators"
|
|
2
|
+
require "rails/generators/active_record"
|
|
3
|
+
|
|
4
|
+
module ActiveVersion
|
|
5
|
+
module Generators
|
|
6
|
+
class RevisionsGenerator < Rails::Generators::NamedBase
|
|
7
|
+
include ActiveRecord::Generators::Migration
|
|
8
|
+
|
|
9
|
+
source_root File.expand_path("templates", __dir__)
|
|
10
|
+
|
|
11
|
+
class_option :backfill,
|
|
12
|
+
type: :boolean,
|
|
13
|
+
default: false,
|
|
14
|
+
desc: "Create initial snapshots for existing records"
|
|
15
|
+
|
|
16
|
+
def create_revision_model
|
|
17
|
+
template "revision_model.rb.erb",
|
|
18
|
+
File.join("app/models", class_path, "#{file_name}_revision.rb")
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def generate_migration_file
|
|
22
|
+
migration_template "migration.rb.erb",
|
|
23
|
+
File.join("db/migrate", "create_#{table_name}.rb"),
|
|
24
|
+
migration_version: migration_version
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def inject_has_revisions
|
|
28
|
+
model_path = File.join("app/models", class_path, "#{file_name}.rb")
|
|
29
|
+
return unless File.exist?(model_path)
|
|
30
|
+
|
|
31
|
+
inject_into_class(model_path, class_name) do
|
|
32
|
+
" has_revisions\n"
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def create_backfill_migration
|
|
37
|
+
return unless options[:backfill]
|
|
38
|
+
|
|
39
|
+
migration_template "backfill_migration.rb.erb",
|
|
40
|
+
File.join("db/migrate", "backfill_#{table_name}.rb"),
|
|
41
|
+
migration_version: migration_version
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def migration_version
|
|
47
|
+
"[#{ActiveRecord::Migration.current_version}]"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def table_name
|
|
51
|
+
"#{file_name}_revisions"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def revision_class_name
|
|
55
|
+
"#{class_name}Revision"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def foreign_key
|
|
59
|
+
"#{file_name}_id"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def version_column
|
|
63
|
+
ActiveVersion.config.revision_version_column
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def source_table_name
|
|
67
|
+
file_name.pluralize
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
class Backfill<%= revision_class_name.pluralize %> < ActiveRecord::Migration<%= migration_version %>
|
|
2
|
+
def up
|
|
3
|
+
# Create initial snapshots for existing records
|
|
4
|
+
# Note: Adjust column list based on your actual source table structure
|
|
5
|
+
execute <<-SQL
|
|
6
|
+
INSERT INTO <%= table_name %> (<%= foreign_key %>, <%= version_column %>, title, body, created_at, updated_at)
|
|
7
|
+
SELECT id, 1, title, body, created_at, updated_at
|
|
8
|
+
FROM <%= source_table_name %>
|
|
9
|
+
WHERE id NOT IN (SELECT DISTINCT <%= foreign_key %> FROM <%= table_name %>)
|
|
10
|
+
SQL
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def down
|
|
14
|
+
# Remove initial snapshots
|
|
15
|
+
execute <<-SQL
|
|
16
|
+
DELETE FROM <%= table_name %> WHERE <%= version_column %> = 1
|
|
17
|
+
SQL
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
class Create<%= revision_class_name.pluralize %> < ActiveRecord::Migration<%= migration_version %>
|
|
2
|
+
def change
|
|
3
|
+
create_table :<%= table_name %> do |t|
|
|
4
|
+
t.references :<%= file_name %>, null: false, foreign_key: true, index: false
|
|
5
|
+
t.integer :<%= version_column %>, null: false
|
|
6
|
+
|
|
7
|
+
# Copy all columns from source table
|
|
8
|
+
# Note: This is a simplified version - in production, you'd want to
|
|
9
|
+
# inspect the actual source table columns dynamically
|
|
10
|
+
# For now, we'll add common columns that can be customized
|
|
11
|
+
t.text :title
|
|
12
|
+
t.text :body
|
|
13
|
+
# Add other columns as needed
|
|
14
|
+
|
|
15
|
+
t.timestamps
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
add_index :<%= table_name %>, [:<%= foreign_key %>, :<%= version_column %>], unique: true, name: "index_<%= table_name %>_on_<%= foreign_key %>_and_<%= version_column %>"
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
class Create<%= translation_class_name.pluralize %> < ActiveRecord::Migration<%= migration_version %>
|
|
2
|
+
def change
|
|
3
|
+
create_table :<%= table_name %> do |t|
|
|
4
|
+
t.references :<%= file_name %>, null: false, foreign_key: true, index: false
|
|
5
|
+
t.string :<%= locale_column %>, null: false
|
|
6
|
+
|
|
7
|
+
<% migration_attributes.each do |attr| -%>
|
|
8
|
+
<%= attr %>
|
|
9
|
+
<% end -%>
|
|
10
|
+
|
|
11
|
+
t.timestamps
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
add_index :<%= table_name %>, [:<%= foreign_key %>, :<%= locale_column %>], unique: true, name: "index_<%= table_name %>_on_<%= foreign_key %>_and_<%= locale_column %>"
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
|
|
2
|
+
class <%= translation_class_name %> < ApplicationRecord
|
|
3
|
+
include ActiveVersion::Translations::TranslationRecord
|
|
4
|
+
|
|
5
|
+
configure_translation(locale_column: :<%= locale_column %>,
|
|
6
|
+
foreign_key: :<%= foreign_key %>
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
<% if translated_attributes.any? -%>
|
|
10
|
+
# Translated attributes:
|
|
11
|
+
<% translated_attributes.each do |attr| -%>
|
|
12
|
+
# <%= attr.split(':').first %>
|
|
13
|
+
<% end -%>
|
|
14
|
+
<% end -%>
|
|
15
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
require "rails/generators"
|
|
2
|
+
require "rails/generators/active_record"
|
|
3
|
+
|
|
4
|
+
module ActiveVersion
|
|
5
|
+
module Generators
|
|
6
|
+
class TranslationsGenerator < Rails::Generators::NamedBase
|
|
7
|
+
include ActiveRecord::Generators::Migration
|
|
8
|
+
|
|
9
|
+
source_root File.expand_path("templates", __dir__)
|
|
10
|
+
|
|
11
|
+
argument :attributes, type: :array, default: [], banner: "field:type field:type"
|
|
12
|
+
|
|
13
|
+
class_option :translated_attributes,
|
|
14
|
+
type: :array,
|
|
15
|
+
default: [],
|
|
16
|
+
desc: "Attributes to translate (e.g., title:string body:text)"
|
|
17
|
+
|
|
18
|
+
def create_translation_model
|
|
19
|
+
template "translation_model.rb.erb",
|
|
20
|
+
File.join("app/models", class_path, "#{file_name}_translation.rb")
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def generate_migration_file
|
|
24
|
+
migration_template "migration.rb.erb",
|
|
25
|
+
File.join("db/migrate", "create_#{table_name}.rb"),
|
|
26
|
+
migration_version: migration_version
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def inject_has_translations
|
|
30
|
+
model_path = File.join("app/models", class_path, "#{file_name}.rb")
|
|
31
|
+
return unless File.exist?(model_path)
|
|
32
|
+
|
|
33
|
+
inject_into_class(model_path, class_name) do
|
|
34
|
+
" has_translations\n"
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def migration_version
|
|
41
|
+
"[#{ActiveRecord::Migration.current_version}]"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def table_name
|
|
45
|
+
"#{file_name}_translations"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def translation_class_name
|
|
49
|
+
"#{class_name}Translation"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def foreign_key
|
|
53
|
+
"#{file_name}_id"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def locale_column
|
|
57
|
+
ActiveVersion.config.translation_locale_column
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def translated_attributes
|
|
61
|
+
options[:translated_attributes] || []
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def migration_attributes
|
|
65
|
+
translated_attributes.map do |attr|
|
|
66
|
+
name, type = attr.split(":")
|
|
67
|
+
type ||= "string"
|
|
68
|
+
"t.#{type} :#{name}"
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
class Add<%= type.camelize %>TriggerTo<%= class_name.pluralize %> < ActiveRecord::Migration<%= migration_version %>
|
|
2
|
+
def up
|
|
3
|
+
<%- if type == "audit" -%>
|
|
4
|
+
# Create audit trigger function
|
|
5
|
+
execute <<-SQL
|
|
6
|
+
<%= ActiveVersion::Database::Triggers::PostgreSQL.generate_audit_trigger_function(
|
|
7
|
+
table_name,
|
|
8
|
+
audit_table_name,
|
|
9
|
+
auditable_type: auditable_type,
|
|
10
|
+
version_column: ActiveVersion.config.audit_version_column,
|
|
11
|
+
changes_column: ActiveVersion.config.audit_changes_column,
|
|
12
|
+
context_column: ActiveVersion.config.audit_context_column,
|
|
13
|
+
action_column: ActiveVersion.config.audit_action_column
|
|
14
|
+
).indent(4) %>
|
|
15
|
+
SQL
|
|
16
|
+
|
|
17
|
+
# Create audit trigger
|
|
18
|
+
execute <<-SQL
|
|
19
|
+
<%= ActiveVersion::Database::Triggers::PostgreSQL.generate_audit_trigger(
|
|
20
|
+
table_name,
|
|
21
|
+
trigger_name: trigger_name,
|
|
22
|
+
events: events.map(&:to_sym)
|
|
23
|
+
).indent(4) %>
|
|
24
|
+
SQL
|
|
25
|
+
<%- elsif type == "revision" -%>
|
|
26
|
+
# Create revision trigger function
|
|
27
|
+
execute <<-SQL
|
|
28
|
+
<%= ActiveVersion::Database::Triggers::PostgreSQL.generate_revision_trigger_function(
|
|
29
|
+
table_name,
|
|
30
|
+
revision_table_name,
|
|
31
|
+
foreign_key: "#{file_name}_id",
|
|
32
|
+
version_column: ActiveVersion.config.revision_version_column,
|
|
33
|
+
columns: tracked_columns
|
|
34
|
+
).indent(4) %>
|
|
35
|
+
SQL
|
|
36
|
+
|
|
37
|
+
# Create revision trigger
|
|
38
|
+
execute <<-SQL
|
|
39
|
+
<%= ActiveVersion::Database::Triggers::PostgreSQL.generate_revision_trigger(
|
|
40
|
+
table_name,
|
|
41
|
+
trigger_name: trigger_name
|
|
42
|
+
).indent(4) %>
|
|
43
|
+
SQL
|
|
44
|
+
<%- end -%>
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def down
|
|
48
|
+
<%- if type == "audit" -%>
|
|
49
|
+
# Drop audit trigger
|
|
50
|
+
execute <<-SQL
|
|
51
|
+
<%= ActiveVersion::Database::Triggers::PostgreSQL.drop_trigger(trigger_name, table_name).indent(4) %>
|
|
52
|
+
SQL
|
|
53
|
+
|
|
54
|
+
# Drop audit trigger function
|
|
55
|
+
execute <<-SQL
|
|
56
|
+
<%= ActiveVersion::Database::Triggers::PostgreSQL.drop_trigger_function(trigger_function_name).indent(4) %>
|
|
57
|
+
SQL
|
|
58
|
+
<%- elsif type == "revision" -%>
|
|
59
|
+
# Drop revision trigger
|
|
60
|
+
execute <<-SQL
|
|
61
|
+
<%= ActiveVersion::Database::Triggers::PostgreSQL.drop_trigger(trigger_name, table_name).indent(4) %>
|
|
62
|
+
SQL
|
|
63
|
+
|
|
64
|
+
# Drop revision trigger function
|
|
65
|
+
execute <<-SQL
|
|
66
|
+
<%= ActiveVersion::Database::Triggers::PostgreSQL.drop_trigger_function(trigger_function_name).indent(4) %>
|
|
67
|
+
SQL
|
|
68
|
+
<%- end -%>
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
private
|
|
72
|
+
|
|
73
|
+
def table_name
|
|
74
|
+
"<%= table_name %>"
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def audit_table_name
|
|
78
|
+
"<%= audit_table_name %>"
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def revision_table_name
|
|
82
|
+
"<%= revision_table_name %>"
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def auditable_type
|
|
86
|
+
"<%= auditable_type %>"
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def trigger_name
|
|
90
|
+
"<%= trigger_name %>"
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def trigger_function_name
|
|
94
|
+
"<%= trigger_function_name %>"
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def tracked_columns
|
|
98
|
+
<%= tracked_columns.inspect %>
|
|
99
|
+
end
|
|
100
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
require "rails/generators"
|
|
2
|
+
require "rails/generators/active_record"
|
|
3
|
+
|
|
4
|
+
module ActiveVersion
|
|
5
|
+
module Generators
|
|
6
|
+
class TriggersGenerator < Rails::Generators::NamedBase
|
|
7
|
+
include ActiveRecord::Generators::Migration
|
|
8
|
+
|
|
9
|
+
source_root File.expand_path("templates", __dir__)
|
|
10
|
+
|
|
11
|
+
class_option :type,
|
|
12
|
+
type: :string,
|
|
13
|
+
default: "audit",
|
|
14
|
+
desc: "Trigger type: audit or revision"
|
|
15
|
+
|
|
16
|
+
class_option :events,
|
|
17
|
+
type: :array,
|
|
18
|
+
default: ["insert", "update", "delete"],
|
|
19
|
+
desc: "Events to trigger on (for audits)"
|
|
20
|
+
|
|
21
|
+
def generate_trigger_migration_file
|
|
22
|
+
migration_template "migration.rb.erb",
|
|
23
|
+
File.join("db/migrate", "add_#{type}_trigger_to_#{table_name}.rb"),
|
|
24
|
+
migration_version: migration_version
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def migration_version
|
|
30
|
+
"[#{ActiveRecord::Migration.current_version}]"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def table_name
|
|
34
|
+
file_name.pluralize
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def type
|
|
38
|
+
options[:type] || "audit"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def events
|
|
42
|
+
options[:events] || ["insert", "update", "delete"]
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def trigger_function_name
|
|
46
|
+
"active_version_#{type}_#{table_name}"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def trigger_name
|
|
50
|
+
"active_version_#{type}_on_#{table_name}"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def audit_table_name
|
|
54
|
+
"#{table_name}_audits"
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def revision_table_name
|
|
58
|
+
"#{table_name}_revisions"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def auditable_type
|
|
62
|
+
file_name.classify
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def tracked_columns
|
|
66
|
+
return [] unless ActiveRecord::Base.connected?
|
|
67
|
+
|
|
68
|
+
ActiveRecord::Base.connection.columns(table_name).map(&:name)
|
|
69
|
+
rescue ActiveRecord::NoDatabaseError, ActiveRecord::ConnectionNotEstablished, StandardError
|
|
70
|
+
[]
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
module ActiveVersion
|
|
2
|
+
module SchemaGuards
|
|
3
|
+
def self.validate_partitioned_keys!: (Class record_class, Symbol version_type) -> nil
|
|
4
|
+
def self.expected_unique_columns: (Class record_class, Symbol version_type) -> Array[String]
|
|
5
|
+
def self.postgres?: (untyped connection) -> bool
|
|
6
|
+
def self.partitioned_table?: (untyped connection, String table_name) -> bool
|
|
7
|
+
def self.partition_key_columns: (untyped connection, String table_name) -> Array[String]
|
|
8
|
+
def self.partition_primary_key_columns: (untyped connection, String table_name) -> Array[String]
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
module Query
|
|
12
|
+
def self.audits: (untyped record, ?Hash[Symbol, untyped] opts) -> untyped
|
|
13
|
+
def self.translations: (untyped record, ?Hash[Symbol, untyped] opts) -> untyped
|
|
14
|
+
def self.revisions: (untyped record, ?Hash[Symbol, untyped] opts) -> untyped
|
|
15
|
+
def self.current_transaction: (?Hash[Symbol, untyped] opts) -> untyped
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
module Sharding
|
|
19
|
+
class ConnectionRouter
|
|
20
|
+
def self.connection_for: (Class model_class, Symbol version_type) -> Symbol | String | untyped
|
|
21
|
+
def self.adapter_for: (Class model_class, Symbol version_type) -> untyped
|
|
22
|
+
def self.with_connection: (Class model_class, Symbol version_type) { (untyped) -> untyped } -> untyped
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
module Database
|
|
27
|
+
module Triggers
|
|
28
|
+
module PostgreSQL
|
|
29
|
+
def self.generate_audit_trigger_function: (String table_name, String audit_table_name, ?Hash[Symbol, untyped] options) -> String
|
|
30
|
+
def self.generate_audit_trigger: (String table_name, ?Hash[Symbol, untyped] options) -> String
|
|
31
|
+
def self.generate_revision_trigger_function: (String table_name, String revision_table_name, ?Hash[Symbol, untyped] options) -> String
|
|
32
|
+
def self.generate_revision_trigger: (String table_name, ?Hash[Symbol, untyped] options) -> String
|
|
33
|
+
def self.drop_trigger_function: (String function_name) -> String
|
|
34
|
+
def self.drop_trigger: (String trigger_name, String table_name) -> String
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
module Migrators
|
|
40
|
+
class Base
|
|
41
|
+
def self.migrate: (Class model_class, ?Hash[Symbol, untyped] options) -> Integer
|
|
42
|
+
def self.create_audit_table: (String table_name, ?Hash[Symbol, untyped] options) -> untyped
|
|
43
|
+
def self.create_revision_table: (String table_name, ?Hash[Symbol, untyped] options) -> untyped
|
|
44
|
+
def self.create_translation_table: (String table_name, ?Hash[Symbol, untyped] options) -> untyped
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
class Audited < Base
|
|
48
|
+
def self.migrate: (Class model_class, ?Hash[Symbol, untyped] options) -> Integer
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|