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,243 @@
|
|
|
1
|
+
module ActiveVersion
|
|
2
|
+
module Database
|
|
3
|
+
module Triggers
|
|
4
|
+
# PostgreSQL trigger support for ActiveVersion
|
|
5
|
+
module PostgreSQL
|
|
6
|
+
class << self
|
|
7
|
+
# Generate trigger function for audits
|
|
8
|
+
# @param table_name [String] Name of the table to create trigger for
|
|
9
|
+
# @param audit_table_name [String] Name of the audit table
|
|
10
|
+
# @param options [Hash] Trigger options
|
|
11
|
+
# @return [String] SQL for trigger function
|
|
12
|
+
def generate_audit_trigger_function(table_name, audit_table_name, options = {})
|
|
13
|
+
function_name = "active_version_audit_#{table_name}"
|
|
14
|
+
auditable_type = options[:auditable_type] || table_name.classify
|
|
15
|
+
version_column = options[:version_column] || "version"
|
|
16
|
+
changes_column = options[:changes_column] || "audited_changes"
|
|
17
|
+
context_column = options[:context_column] || "audited_context"
|
|
18
|
+
action_column = options[:action_column] || "action"
|
|
19
|
+
|
|
20
|
+
<<~SQL
|
|
21
|
+
CREATE OR REPLACE FUNCTION #{function_name}()
|
|
22
|
+
RETURNS TRIGGER AS $$
|
|
23
|
+
DECLARE
|
|
24
|
+
new_version INTEGER;
|
|
25
|
+
changes JSONB;
|
|
26
|
+
context_data JSONB;
|
|
27
|
+
action_type TEXT;
|
|
28
|
+
BEGIN
|
|
29
|
+
-- Determine action
|
|
30
|
+
IF TG_OP = 'INSERT' THEN
|
|
31
|
+
action_type := 'create';
|
|
32
|
+
changes := to_jsonb(NEW.*);
|
|
33
|
+
ELSIF TG_OP = 'UPDATE' THEN
|
|
34
|
+
action_type := 'update';
|
|
35
|
+
changes := jsonb_build_object(
|
|
36
|
+
'old', to_jsonb(OLD.*),
|
|
37
|
+
'new', to_jsonb(NEW.*)
|
|
38
|
+
);
|
|
39
|
+
ELSIF TG_OP = 'DELETE' THEN
|
|
40
|
+
action_type := 'destroy';
|
|
41
|
+
changes := to_jsonb(OLD.*);
|
|
42
|
+
END IF;
|
|
43
|
+
|
|
44
|
+
-- Get version number
|
|
45
|
+
IF action_type = 'create' THEN
|
|
46
|
+
new_version := 1;
|
|
47
|
+
ELSE
|
|
48
|
+
SELECT COALESCE(MAX(#{version_column}), 0) + 1
|
|
49
|
+
INTO new_version
|
|
50
|
+
FROM #{audit_table_name}
|
|
51
|
+
WHERE #{audit_table_name}.auditable_id = COALESCE(NEW.id, OLD.id)
|
|
52
|
+
AND #{audit_table_name}.auditable_type = '#{auditable_type}';
|
|
53
|
+
END IF;
|
|
54
|
+
|
|
55
|
+
-- Get context from session variables (if set)
|
|
56
|
+
context_data := COALESCE(
|
|
57
|
+
current_setting('active_version.context', true)::jsonb,
|
|
58
|
+
'{}'::jsonb
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
-- Insert audit record
|
|
62
|
+
IF TG_OP = 'DELETE' THEN
|
|
63
|
+
INSERT INTO #{audit_table_name} (
|
|
64
|
+
auditable_id,
|
|
65
|
+
auditable_type,
|
|
66
|
+
#{action_column},
|
|
67
|
+
#{changes_column},
|
|
68
|
+
#{version_column},
|
|
69
|
+
#{context_column},
|
|
70
|
+
created_at,
|
|
71
|
+
updated_at
|
|
72
|
+
) VALUES (
|
|
73
|
+
OLD.id,
|
|
74
|
+
'#{auditable_type}',
|
|
75
|
+
action_type,
|
|
76
|
+
changes,
|
|
77
|
+
new_version,
|
|
78
|
+
context_data,
|
|
79
|
+
NOW(),
|
|
80
|
+
NOW()
|
|
81
|
+
);
|
|
82
|
+
RETURN OLD;
|
|
83
|
+
ELSE
|
|
84
|
+
INSERT INTO #{audit_table_name} (
|
|
85
|
+
auditable_id,
|
|
86
|
+
auditable_type,
|
|
87
|
+
#{action_column},
|
|
88
|
+
#{changes_column},
|
|
89
|
+
#{version_column},
|
|
90
|
+
#{context_column},
|
|
91
|
+
created_at,
|
|
92
|
+
updated_at
|
|
93
|
+
) VALUES (
|
|
94
|
+
NEW.id,
|
|
95
|
+
'#{auditable_type}',
|
|
96
|
+
action_type,
|
|
97
|
+
changes,
|
|
98
|
+
new_version,
|
|
99
|
+
context_data,
|
|
100
|
+
NOW(),
|
|
101
|
+
NOW()
|
|
102
|
+
);
|
|
103
|
+
RETURN NEW;
|
|
104
|
+
END IF;
|
|
105
|
+
END;
|
|
106
|
+
$$ LANGUAGE plpgsql;
|
|
107
|
+
SQL
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Generate trigger for audits
|
|
111
|
+
# @param table_name [String] Name of the table
|
|
112
|
+
# @param options [Hash] Trigger options
|
|
113
|
+
# @return [String] SQL for CREATE TRIGGER statement
|
|
114
|
+
def generate_audit_trigger(table_name, options = {})
|
|
115
|
+
function_name = "active_version_audit_#{table_name}"
|
|
116
|
+
trigger_name = options[:trigger_name] || "active_version_audit_on_#{table_name}"
|
|
117
|
+
events = options[:events] || [:insert, :update, :delete]
|
|
118
|
+
|
|
119
|
+
event_clause = events.map(&:upcase).join(" OR ")
|
|
120
|
+
|
|
121
|
+
<<~SQL
|
|
122
|
+
CREATE TRIGGER #{trigger_name}
|
|
123
|
+
AFTER #{event_clause} ON #{table_name}
|
|
124
|
+
FOR EACH ROW
|
|
125
|
+
WHEN (coalesce(current_setting('active_version.disabled', true), '') <> 'on')
|
|
126
|
+
EXECUTE FUNCTION #{function_name}();
|
|
127
|
+
SQL
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Generate trigger function for revisions
|
|
131
|
+
# @param table_name [String] Name of the table
|
|
132
|
+
# @param revision_table_name [String] Name of the revision table
|
|
133
|
+
# @param options [Hash] Trigger options
|
|
134
|
+
# @return [String] SQL for trigger function
|
|
135
|
+
def generate_revision_trigger_function(table_name, revision_table_name, options = {})
|
|
136
|
+
function_name = "active_version_revision_#{table_name}"
|
|
137
|
+
foreign_key = options[:foreign_key] || "#{table_name.singularize}_id"
|
|
138
|
+
version_column = options[:version_column] || "version"
|
|
139
|
+
|
|
140
|
+
<<~SQL
|
|
141
|
+
CREATE OR REPLACE FUNCTION #{function_name}()
|
|
142
|
+
RETURNS TRIGGER AS $$
|
|
143
|
+
DECLARE
|
|
144
|
+
new_version INTEGER;
|
|
145
|
+
revision_data RECORD;
|
|
146
|
+
BEGIN
|
|
147
|
+
-- Only create revision on UPDATE
|
|
148
|
+
IF TG_OP != 'UPDATE' THEN
|
|
149
|
+
RETURN NEW;
|
|
150
|
+
END IF;
|
|
151
|
+
|
|
152
|
+
-- Skip if nothing changed
|
|
153
|
+
IF NEW.* IS NOT DISTINCT FROM OLD.* THEN
|
|
154
|
+
RETURN NEW;
|
|
155
|
+
END IF;
|
|
156
|
+
|
|
157
|
+
-- Get next version number
|
|
158
|
+
SELECT COALESCE(MAX(#{version_column}), 0) + 1
|
|
159
|
+
INTO new_version
|
|
160
|
+
FROM #{revision_table_name}
|
|
161
|
+
WHERE #{revision_table_name}.#{foreign_key} = NEW.id;
|
|
162
|
+
|
|
163
|
+
-- Create revision with OLD values
|
|
164
|
+
INSERT INTO #{revision_table_name} (
|
|
165
|
+
#{foreign_key},
|
|
166
|
+
#{version_column},
|
|
167
|
+
#{build_revision_columns(table_name, options)},
|
|
168
|
+
created_at,
|
|
169
|
+
updated_at
|
|
170
|
+
)
|
|
171
|
+
SELECT
|
|
172
|
+
NEW.id,
|
|
173
|
+
new_version,
|
|
174
|
+
#{build_revision_values("OLD", table_name, options)},
|
|
175
|
+
NOW(),
|
|
176
|
+
NOW();
|
|
177
|
+
|
|
178
|
+
RETURN NEW;
|
|
179
|
+
END;
|
|
180
|
+
$$ LANGUAGE plpgsql;
|
|
181
|
+
SQL
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# Generate trigger for revisions
|
|
185
|
+
# @param table_name [String] Name of the table
|
|
186
|
+
# @param options [Hash] Trigger options
|
|
187
|
+
# @return [String] SQL for CREATE TRIGGER statement
|
|
188
|
+
def generate_revision_trigger(table_name, options = {})
|
|
189
|
+
function_name = "active_version_revision_#{table_name}"
|
|
190
|
+
trigger_name = options[:trigger_name] || "active_version_revision_on_#{table_name}"
|
|
191
|
+
|
|
192
|
+
<<~SQL
|
|
193
|
+
CREATE TRIGGER #{trigger_name}
|
|
194
|
+
BEFORE UPDATE ON #{table_name}
|
|
195
|
+
FOR EACH ROW
|
|
196
|
+
WHEN (coalesce(current_setting('active_version.disabled', true), '') <> 'on')
|
|
197
|
+
EXECUTE FUNCTION #{function_name}();
|
|
198
|
+
SQL
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# Drop trigger function
|
|
202
|
+
# @param function_name [String] Name of the function
|
|
203
|
+
# @return [String] SQL for DROP FUNCTION statement
|
|
204
|
+
def drop_trigger_function(function_name)
|
|
205
|
+
"DROP FUNCTION IF EXISTS #{function_name}() CASCADE;"
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# Drop trigger
|
|
209
|
+
# @param trigger_name [String] Name of the trigger
|
|
210
|
+
# @param table_name [String] Name of the table
|
|
211
|
+
# @return [String] SQL for DROP TRIGGER statement
|
|
212
|
+
def drop_trigger(trigger_name, table_name)
|
|
213
|
+
"DROP TRIGGER IF EXISTS #{trigger_name} ON #{table_name} CASCADE;"
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
private
|
|
217
|
+
|
|
218
|
+
def build_revision_columns(table_name, options)
|
|
219
|
+
columns = normalize_revision_columns(options)
|
|
220
|
+
columns.join(", ")
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def build_revision_values(record_prefix, table_name, options)
|
|
224
|
+
columns = normalize_revision_columns(options)
|
|
225
|
+
columns.map { |column| "#{record_prefix}.#{column}" }.join(", ")
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def normalize_revision_columns(options)
|
|
229
|
+
raw_columns = Array(options[:columns]).map(&:to_s)
|
|
230
|
+
if raw_columns.empty?
|
|
231
|
+
raise ArgumentError, "revision trigger generation requires :columns option"
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
foreign_key = options[:foreign_key].to_s
|
|
235
|
+
version_column = options[:version_column].to_s
|
|
236
|
+
metadata_columns = [foreign_key, version_column, "id", "created_at", "updated_at"]
|
|
237
|
+
raw_columns.reject { |col| metadata_columns.include?(col) }
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
end
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
module ActiveVersion
|
|
2
|
+
# Instrumentation for ActiveSupport::Notifications
|
|
3
|
+
module Instrumentation
|
|
4
|
+
EVENTS = {
|
|
5
|
+
translation_created: "translation_created.active_version",
|
|
6
|
+
translation_updated: "translation_updated.active_version",
|
|
7
|
+
translation_destroyed: "translation_destroyed.active_version",
|
|
8
|
+
translation_fallback_used: "translation_fallback_used.active_version",
|
|
9
|
+
revision_created: "revision.active_version",
|
|
10
|
+
revision_reverted: "revision_reverted.active_version",
|
|
11
|
+
revision_switch_applied: "revision_switch_applied.active_version",
|
|
12
|
+
revision_write_failed: "revision_write_failed.active_version",
|
|
13
|
+
audit_created: "audit.active_version",
|
|
14
|
+
audit_write_failed: "audit_write_failed.active_version",
|
|
15
|
+
audit_sql_generated: "audit_sql.active_version"
|
|
16
|
+
}.freeze
|
|
17
|
+
|
|
18
|
+
class << self
|
|
19
|
+
# Instrument a translation creation
|
|
20
|
+
def instrument_translation_created(translation, source)
|
|
21
|
+
instrument_event(:translation_created) do
|
|
22
|
+
{
|
|
23
|
+
translation: translation,
|
|
24
|
+
source: source,
|
|
25
|
+
locale: translation.locale
|
|
26
|
+
}
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Instrument a translation update
|
|
31
|
+
def instrument_translation_updated(translation, source)
|
|
32
|
+
instrument_event(:translation_updated) do
|
|
33
|
+
{
|
|
34
|
+
translation: translation,
|
|
35
|
+
source: source,
|
|
36
|
+
locale: translation.locale
|
|
37
|
+
}
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Instrument a translation destroy
|
|
42
|
+
def instrument_translation_destroyed(translation, source)
|
|
43
|
+
instrument_event(:translation_destroyed) do
|
|
44
|
+
{
|
|
45
|
+
translation: translation,
|
|
46
|
+
source: source,
|
|
47
|
+
locale: translation.locale,
|
|
48
|
+
translation_id: translation.id
|
|
49
|
+
}
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Instrument when translation falls back from requested locale
|
|
54
|
+
def instrument_translation_fallback_used(source, attr:, requested_locale:, resolved_locale:)
|
|
55
|
+
instrument_event(:translation_fallback_used) do
|
|
56
|
+
{
|
|
57
|
+
source: source,
|
|
58
|
+
attr: attr,
|
|
59
|
+
requested_locale: requested_locale,
|
|
60
|
+
resolved_locale: resolved_locale
|
|
61
|
+
}
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Instrument a revision creation
|
|
66
|
+
def instrument_revision_created(revision, source)
|
|
67
|
+
source_class = source&.class
|
|
68
|
+
source_class ||= revision.class.source_class if revision.class.respond_to?(:source_class)
|
|
69
|
+
version_value = nil
|
|
70
|
+
|
|
71
|
+
if source_class
|
|
72
|
+
version_column = ActiveVersion.column_mapper.column_for(source_class, :revisions, :version)
|
|
73
|
+
version_value = if revision.respond_to?(version_column)
|
|
74
|
+
revision.public_send(version_column)
|
|
75
|
+
else
|
|
76
|
+
revision[version_column]
|
|
77
|
+
end
|
|
78
|
+
else
|
|
79
|
+
version_value = revision[ActiveVersion.config.revision_version_column] if revision.respond_to?(:[])
|
|
80
|
+
version_value ||= revision.version if revision.respond_to?(:version)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
instrument_event(:revision_created) do
|
|
84
|
+
{
|
|
85
|
+
revision: revision,
|
|
86
|
+
source: source,
|
|
87
|
+
version: version_value
|
|
88
|
+
}
|
|
89
|
+
end
|
|
90
|
+
rescue NameError
|
|
91
|
+
instrument_event(:revision_created) do
|
|
92
|
+
{
|
|
93
|
+
revision: revision,
|
|
94
|
+
source: source,
|
|
95
|
+
version: nil
|
|
96
|
+
}
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Instrument revision revert action
|
|
101
|
+
def instrument_revision_reverted(source, from_version:, to_version:, strategy: :revert_to)
|
|
102
|
+
instrument_event(:revision_reverted) do
|
|
103
|
+
{
|
|
104
|
+
source: source,
|
|
105
|
+
from_version: from_version,
|
|
106
|
+
to_version: to_version,
|
|
107
|
+
strategy: strategy
|
|
108
|
+
}
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Instrument revision pointer switch action
|
|
113
|
+
def instrument_revision_switch_applied(source, from_version:, to_version:, append:)
|
|
114
|
+
instrument_event(:revision_switch_applied) do
|
|
115
|
+
{
|
|
116
|
+
source: source,
|
|
117
|
+
from_version: from_version,
|
|
118
|
+
to_version: to_version,
|
|
119
|
+
append: append
|
|
120
|
+
}
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Instrument revision write failures
|
|
125
|
+
def instrument_revision_write_failed(source, error:)
|
|
126
|
+
instrument_event(:revision_write_failed) do
|
|
127
|
+
{
|
|
128
|
+
source: source,
|
|
129
|
+
error: format_error(error)
|
|
130
|
+
}
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Instrument an audit creation
|
|
135
|
+
def instrument_audit_created(audit, auditable)
|
|
136
|
+
source_class = auditable&.class
|
|
137
|
+
source_class ||= audit.class.source_class if audit.class.respond_to?(:source_class)
|
|
138
|
+
version_value = nil
|
|
139
|
+
|
|
140
|
+
if source_class
|
|
141
|
+
version_column = ActiveVersion.column_mapper.column_for(source_class, :audits, :version)
|
|
142
|
+
version_value = if audit.respond_to?(version_column)
|
|
143
|
+
audit.public_send(version_column)
|
|
144
|
+
else
|
|
145
|
+
audit[version_column]
|
|
146
|
+
end
|
|
147
|
+
else
|
|
148
|
+
version_value = audit[ActiveVersion.config.audit_version_column] if audit.respond_to?(:[])
|
|
149
|
+
version_value ||= audit.version if audit.respond_to?(:version)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
instrument_event(:audit_created) do
|
|
153
|
+
{
|
|
154
|
+
audit: audit,
|
|
155
|
+
auditable: auditable,
|
|
156
|
+
action: audit.action,
|
|
157
|
+
version: version_value
|
|
158
|
+
}
|
|
159
|
+
end
|
|
160
|
+
rescue NameError
|
|
161
|
+
instrument_event(:audit_created) do
|
|
162
|
+
{
|
|
163
|
+
audit: audit,
|
|
164
|
+
auditable: auditable,
|
|
165
|
+
action: audit.action,
|
|
166
|
+
version: nil
|
|
167
|
+
}
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Instrument audit write failures
|
|
172
|
+
def instrument_audit_write_failed(source, error:, action:)
|
|
173
|
+
instrument_event(:audit_write_failed) do
|
|
174
|
+
{
|
|
175
|
+
source: source,
|
|
176
|
+
action: action,
|
|
177
|
+
error: format_error(error)
|
|
178
|
+
}
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# Instrument SQL generation
|
|
183
|
+
def instrument_audit_sql_generated(model, sql)
|
|
184
|
+
instrument_event(:audit_sql_generated) do
|
|
185
|
+
{
|
|
186
|
+
model: model,
|
|
187
|
+
sql: sql
|
|
188
|
+
}
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
private
|
|
193
|
+
|
|
194
|
+
def instrument_event(event_key)
|
|
195
|
+
event_name = EVENTS[event_key]
|
|
196
|
+
return unless event_name
|
|
197
|
+
return unless listening?(event_name)
|
|
198
|
+
|
|
199
|
+
payload = block_given? ? yield : {}
|
|
200
|
+
ActiveSupport::Notifications.instrument(event_name, payload)
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def listening?(event_name)
|
|
204
|
+
ActiveSupport::Notifications.notifier.listening?(event_name)
|
|
205
|
+
rescue => e
|
|
206
|
+
log_debug("listener probe failed for #{event_name}: #{e.class}: #{e.message}; assuming listeners may exist")
|
|
207
|
+
true
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def format_error(error)
|
|
211
|
+
return nil if error.nil?
|
|
212
|
+
|
|
213
|
+
{
|
|
214
|
+
class: error.class.name,
|
|
215
|
+
message: error.message
|
|
216
|
+
}
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def log_debug(message)
|
|
220
|
+
if defined?(Rails) && Rails.respond_to?(:logger)
|
|
221
|
+
Rails.logger&.debug("[ActiveVersion::Instrumentation] #{message}")
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
end
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
require "active_version/migrators/base"
|
|
2
|
+
|
|
3
|
+
module ActiveVersion
|
|
4
|
+
module Migrators
|
|
5
|
+
# Migrator from audited gem
|
|
6
|
+
class Audited < Base
|
|
7
|
+
class << self
|
|
8
|
+
# Migrate audits from audited gem
|
|
9
|
+
# @param model_class [Class] ActiveRecord model class
|
|
10
|
+
# @param options [Hash] Migration options
|
|
11
|
+
# @option options [Boolean] :dry_run (false) Don't actually migrate
|
|
12
|
+
# @return [Integer] Number of records migrated
|
|
13
|
+
def migrate(model_class, options = {})
|
|
14
|
+
return 0 unless model_class.respond_to?(:audited?)
|
|
15
|
+
|
|
16
|
+
audit_class = model_class.audit_class
|
|
17
|
+
return 0 unless audit_class
|
|
18
|
+
|
|
19
|
+
old_audits = source_audits(model_class)
|
|
20
|
+
count = 0
|
|
21
|
+
|
|
22
|
+
if old_audits.respond_to?(:find_each)
|
|
23
|
+
old_audits.find_each do |old_audit|
|
|
24
|
+
count += 1
|
|
25
|
+
next if options[:dry_run]
|
|
26
|
+
|
|
27
|
+
audit_data = convert_audit(old_audit, model_class)
|
|
28
|
+
create_audit(nil, audit_data, audit_class)
|
|
29
|
+
end
|
|
30
|
+
else
|
|
31
|
+
# Handle array case (from mocked source_audits in tests)
|
|
32
|
+
old_audits.each do |old_audit|
|
|
33
|
+
count += 1
|
|
34
|
+
next if options[:dry_run]
|
|
35
|
+
|
|
36
|
+
audit_data = convert_audit(old_audit, model_class)
|
|
37
|
+
create_audit(nil, audit_data, audit_class)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
count
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def source_audits(model_class)
|
|
47
|
+
# Try to find old audit class
|
|
48
|
+
old_audit_class = if defined?(::Audited::Audit)
|
|
49
|
+
::Audited::Audit
|
|
50
|
+
else
|
|
51
|
+
begin
|
|
52
|
+
"#{model_class.name}Audit".constantize
|
|
53
|
+
rescue
|
|
54
|
+
nil
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
return [] unless old_audit_class
|
|
59
|
+
|
|
60
|
+
old_audit_class.where(auditable_type: model_class.name)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def convert_audit(old_audit, model_class)
|
|
64
|
+
auditable_column = ActiveVersion.column_mapper.column_for(model_class, :audits, :auditable).to_s
|
|
65
|
+
version_column = ActiveVersion.column_mapper.column_for(model_class, :audits, :version).to_s
|
|
66
|
+
changes_column = ActiveVersion.column_mapper.column_for(model_class, :audits, :changes).to_s
|
|
67
|
+
context_column = ActiveVersion.column_mapper.column_for(model_class, :audits, :context).to_s
|
|
68
|
+
comment_column = ActiveVersion.column_mapper.column_for(model_class, :audits, :comment).to_s
|
|
69
|
+
|
|
70
|
+
{
|
|
71
|
+
"#{auditable_column}_id" => old_audit.auditable_id,
|
|
72
|
+
"#{auditable_column}_type" => old_audit.auditable_type,
|
|
73
|
+
version_column => old_audit.version,
|
|
74
|
+
changes_column => old_audit.audited_changes,
|
|
75
|
+
context_column => (old_audit.respond_to?(:audited_context) ? old_audit.audited_context : {}),
|
|
76
|
+
comment_column => old_audit.comment,
|
|
77
|
+
:created_at => old_audit.created_at,
|
|
78
|
+
:updated_at => old_audit.updated_at || old_audit.created_at
|
|
79
|
+
}
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|