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,263 @@
|
|
|
1
|
+
module ActiveVersion
|
|
2
|
+
module Audits
|
|
3
|
+
# SQL builder for batch audit operations
|
|
4
|
+
module SQLBuilder
|
|
5
|
+
extend ActiveSupport::Concern
|
|
6
|
+
|
|
7
|
+
module ClassMethods
|
|
8
|
+
BatchCollector = Struct.new(:records) do
|
|
9
|
+
def <<(record)
|
|
10
|
+
records << record
|
|
11
|
+
self
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def add(record)
|
|
15
|
+
self << record
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def concat(list)
|
|
19
|
+
records.concat(Array(list))
|
|
20
|
+
self
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def to_a
|
|
24
|
+
records
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Generate SQL for batch insert of audits
|
|
29
|
+
# @param records [Array] Array of ActiveRecord instances to audit
|
|
30
|
+
# @param options [Hash] Options for batch generation
|
|
31
|
+
# @option options [Boolean] :combine (true) Combine into single INSERT statement
|
|
32
|
+
# @option options [Hash] :context Additional context to merge with each audit
|
|
33
|
+
# @return [String] SQL statement(s) for batch insert
|
|
34
|
+
def batch_insert_sql(records = nil, options = {}, &block)
|
|
35
|
+
records, options = normalize_batch_arguments(records, options)
|
|
36
|
+
captured_values = []
|
|
37
|
+
|
|
38
|
+
if block_given? && block.arity == 0 && Array(records).flatten.compact.empty?
|
|
39
|
+
captured_values = capture_audit_values(options) { yield }
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
if captured_values.any?
|
|
43
|
+
if options[:combine] != false
|
|
44
|
+
return build_combined_insert_sql(self, captured_values)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
return captured_values.map { |values| build_single_insert_sql(self, values) }.join(";\n")
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
records = resolve_batch_records(records, &block)
|
|
51
|
+
return "" if records.empty?
|
|
52
|
+
|
|
53
|
+
# Get audit class from first record
|
|
54
|
+
first_record = records.first
|
|
55
|
+
return "" unless first_record
|
|
56
|
+
|
|
57
|
+
audit_class = first_record.class.audit_class
|
|
58
|
+
return "" unless audit_class
|
|
59
|
+
|
|
60
|
+
version_tracker = {}
|
|
61
|
+
|
|
62
|
+
# Build values for each record
|
|
63
|
+
values_list = records.map do |record|
|
|
64
|
+
build_batch_audit_values(record, audit_class, options, version_tracker)
|
|
65
|
+
end.compact
|
|
66
|
+
|
|
67
|
+
return "" if values_list.empty?
|
|
68
|
+
|
|
69
|
+
# Combine into single INSERT with multiple VALUES
|
|
70
|
+
if options[:combine] != false
|
|
71
|
+
build_combined_insert_sql(audit_class, values_list)
|
|
72
|
+
else
|
|
73
|
+
# Return separate INSERT statements
|
|
74
|
+
values_list.map do |values|
|
|
75
|
+
build_single_insert_sql(audit_class, values)
|
|
76
|
+
end.join(";\n")
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Execute batch insert SQL for audits.
|
|
81
|
+
# Supports the same arguments and block semantics as batch_insert_sql.
|
|
82
|
+
# @return [Integer] 0 when nothing to insert, otherwise adapter execute result
|
|
83
|
+
def batch_insert(records = nil, options = {}, &block)
|
|
84
|
+
sql = batch_insert_sql(records, options, &block)
|
|
85
|
+
return 0 if sql.empty?
|
|
86
|
+
|
|
87
|
+
connection.execute(sql)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
private
|
|
91
|
+
|
|
92
|
+
def normalize_batch_arguments(records, options)
|
|
93
|
+
if records.is_a?(Hash) && options.empty?
|
|
94
|
+
[nil, records]
|
|
95
|
+
else
|
|
96
|
+
[records, options]
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def resolve_batch_records(records, &block)
|
|
101
|
+
return Array(records).flatten.compact unless block_given?
|
|
102
|
+
|
|
103
|
+
collected = []
|
|
104
|
+
if block.arity == 1
|
|
105
|
+
yield(BatchCollector.new(collected))
|
|
106
|
+
else
|
|
107
|
+
yield
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
Array(collected).flatten.compact
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def capture_audit_values(options = {})
|
|
114
|
+
previous_state = ActiveVersion.store_get(:active_version_audit_batch_state)
|
|
115
|
+
state = {
|
|
116
|
+
target_audit_class: self,
|
|
117
|
+
options: options,
|
|
118
|
+
values: [],
|
|
119
|
+
version_tracker: {}
|
|
120
|
+
}
|
|
121
|
+
ActiveVersion.store_set(:active_version_audit_batch_state, state)
|
|
122
|
+
yield
|
|
123
|
+
state[:values]
|
|
124
|
+
ensure
|
|
125
|
+
ActiveVersion.store_set(:active_version_audit_batch_state, previous_state)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def build_batch_audit_values(record, audit_class, options, version_tracker)
|
|
129
|
+
# Allow building values even if record hasn't changed (for testing/documentation)
|
|
130
|
+
return nil unless record.changed? || options[:force] || options[:allow_saved]
|
|
131
|
+
|
|
132
|
+
# Determine action
|
|
133
|
+
action = if record.new_record?
|
|
134
|
+
"create"
|
|
135
|
+
elsif options[:destroy]
|
|
136
|
+
"destroy"
|
|
137
|
+
else
|
|
138
|
+
"update"
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Get audited changes
|
|
142
|
+
changes = record.send(:audited_changes) if record.respond_to?(:audited_changes, true)
|
|
143
|
+
changes ||= record.changes
|
|
144
|
+
|
|
145
|
+
# Build base attributes
|
|
146
|
+
attrs = {
|
|
147
|
+
action: action,
|
|
148
|
+
audited_changes: changes
|
|
149
|
+
}
|
|
150
|
+
attrs[:comment] = record.audit_comment if record.respond_to?(:audit_comment)
|
|
151
|
+
|
|
152
|
+
# Merge context
|
|
153
|
+
global_context = ActiveVersion.context || {}
|
|
154
|
+
instance_context = record.audit_context if record.respond_to?(:audit_context)
|
|
155
|
+
context = global_context.merge(instance_context || {})
|
|
156
|
+
context.merge!(options[:context] || {})
|
|
157
|
+
attrs[:audited_context] = context if context.any?
|
|
158
|
+
|
|
159
|
+
# Set polymorphic association
|
|
160
|
+
auditable_column = ActiveVersion.column_mapper.column_for(record.class, :audits, :auditable)
|
|
161
|
+
identity_map = if record.respond_to?(:active_version_audit_identity_map)
|
|
162
|
+
record.active_version_audit_identity_map
|
|
163
|
+
elsif record.respond_to?(:active_version_auditable_id_value)
|
|
164
|
+
{"#{auditable_column}_id" => record.active_version_auditable_id_value}
|
|
165
|
+
else
|
|
166
|
+
{"#{auditable_column}_id" => record.id}
|
|
167
|
+
end
|
|
168
|
+
return nil if identity_map.values.any?(&:nil?)
|
|
169
|
+
attrs.merge!(identity_map)
|
|
170
|
+
attrs["#{auditable_column}_type"] = record.class.name
|
|
171
|
+
|
|
172
|
+
# Set version
|
|
173
|
+
version_column = ActiveVersion.column_mapper.column_for(record.class, :audits, :version)
|
|
174
|
+
attrs[version_column] = next_batch_version(
|
|
175
|
+
audit_class,
|
|
176
|
+
version_column,
|
|
177
|
+
"#{auditable_column}_type",
|
|
178
|
+
identity_map,
|
|
179
|
+
attrs["#{auditable_column}_type"],
|
|
180
|
+
version_tracker
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
# Set user if available
|
|
184
|
+
user_column = ActiveVersion.column_mapper.column_for(record.class, :audits, :user)
|
|
185
|
+
if user_column && defined?(ActiveVersion::RequestStore) && ActiveVersion::RequestStore.audited_user
|
|
186
|
+
user = ActiveVersion::RequestStore.audited_user
|
|
187
|
+
if user.respond_to?(:id)
|
|
188
|
+
attrs[user_column] = user.id
|
|
189
|
+
if user_column.to_s.end_with?("_id") && user.respond_to?(:class)
|
|
190
|
+
type_column = user_column.to_s.gsub("_id", "_type")
|
|
191
|
+
attrs[type_column] = user.class.name
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
# If user doesn't have id method, skip setting user column
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# Set timestamps
|
|
198
|
+
attrs[:created_at] = Time.current
|
|
199
|
+
attrs[:updated_at] = Time.current
|
|
200
|
+
|
|
201
|
+
attrs
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def next_batch_version(audit_class, version_column, type_column, identity_map, auditable_type, version_tracker)
|
|
205
|
+
normalized_identity_map = identity_map.transform_keys(&:to_s).sort.to_h
|
|
206
|
+
cache_key = [auditable_type, normalized_identity_map]
|
|
207
|
+
previous_version = version_tracker[cache_key]
|
|
208
|
+
if previous_version
|
|
209
|
+
version_tracker[cache_key] = previous_version + 1
|
|
210
|
+
return version_tracker[cache_key]
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
max_version = audit_class
|
|
214
|
+
.where(normalized_identity_map.merge(type_column => auditable_type))
|
|
215
|
+
.maximum(version_column)
|
|
216
|
+
.to_i
|
|
217
|
+
|
|
218
|
+
version_tracker[cache_key] = max_version + 1
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def build_combined_insert_sql(audit_class, values_list)
|
|
222
|
+
return "" if values_list.empty?
|
|
223
|
+
|
|
224
|
+
# Get all columns from all values
|
|
225
|
+
all_columns = values_list.flat_map(&:keys).uniq
|
|
226
|
+
connection = audit_class.connection
|
|
227
|
+
table_name = connection.quote_table_name(audit_class.table_name)
|
|
228
|
+
column_list = all_columns.map { |col| connection.quote_column_name(col) }.join(", ")
|
|
229
|
+
|
|
230
|
+
values_sql = values_list.map do |values|
|
|
231
|
+
row_values = all_columns.map { |col| connection.quote(prepare_sql_value(values[col])) }.join(", ")
|
|
232
|
+
"(#{row_values})"
|
|
233
|
+
end.join(", ")
|
|
234
|
+
|
|
235
|
+
"INSERT INTO #{table_name} (#{column_list}) VALUES #{values_sql}"
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def build_single_insert_sql(audit_class, values)
|
|
239
|
+
stmt = Arel::InsertManager.new
|
|
240
|
+
table = Arel::Table.new(audit_class.table_name)
|
|
241
|
+
stmt.into(table)
|
|
242
|
+
|
|
243
|
+
values.keys.each { |key| stmt.columns << table[key] }
|
|
244
|
+
stmt.values = stmt.create_values(values.values.map { |v| prepare_sql_value(v) })
|
|
245
|
+
stmt.to_sql
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def prepare_sql_value(value)
|
|
249
|
+
case value
|
|
250
|
+
when Hash, Array
|
|
251
|
+
value.to_json
|
|
252
|
+
when Time, DateTime
|
|
253
|
+
value.utc
|
|
254
|
+
when Date
|
|
255
|
+
value.to_time.utc
|
|
256
|
+
else
|
|
257
|
+
value
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
end
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
require "active_version/audits/audit_record"
|
|
2
|
+
require "active_version/audits/has_audits"
|
|
3
|
+
require "active_version/audits/sql_builder"
|
|
4
|
+
|
|
5
|
+
module ActiveVersion
|
|
6
|
+
module Audits
|
|
7
|
+
# Audits module for ActiveVersion
|
|
8
|
+
# Provides change tracking and evidence functionality
|
|
9
|
+
end
|
|
10
|
+
end
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
module ActiveVersion
|
|
2
|
+
# Maps versioning concepts to actual column names
|
|
3
|
+
# Allows developers to configure any column name to any concept
|
|
4
|
+
class ColumnMapper
|
|
5
|
+
def initialize
|
|
6
|
+
@mappings = {}
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
# Register a column mapping for a model and version type
|
|
10
|
+
def register(model_class, version_type, concept, column_name)
|
|
11
|
+
key = mapping_key(model_class, version_type, concept)
|
|
12
|
+
@mappings[key] = column_name.to_sym
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Get column name for a concept
|
|
16
|
+
def column_for(model_class, version_type, concept)
|
|
17
|
+
key = mapping_key(model_class, version_type, concept)
|
|
18
|
+
@mappings[key] || default_column_for(version_type, concept)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Get all mappings for a model and version type
|
|
22
|
+
def mappings_for(model_class, version_type)
|
|
23
|
+
prefix = "#{model_class.name}:#{version_type}:"
|
|
24
|
+
@mappings.select { |k, _v| k.to_s.start_with?(prefix) }
|
|
25
|
+
.transform_keys { |k| k.to_s.sub(prefix, "").to_sym }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def mapping_key(model_class, version_type, concept)
|
|
31
|
+
:"#{model_class.name}:#{version_type}:#{concept}"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def default_column_for(version_type, concept)
|
|
35
|
+
case version_type
|
|
36
|
+
when :translations
|
|
37
|
+
default_translation_column(concept)
|
|
38
|
+
when :revisions
|
|
39
|
+
default_revision_column(concept)
|
|
40
|
+
when :audits
|
|
41
|
+
default_audit_column(concept)
|
|
42
|
+
else
|
|
43
|
+
raise ConfigurationError, "Unknown version type: #{version_type.inspect}"
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def default_translation_column(concept)
|
|
48
|
+
case concept
|
|
49
|
+
when :locale
|
|
50
|
+
ActiveVersion.config.translation_locale_column
|
|
51
|
+
else
|
|
52
|
+
raise ConfigurationError, "Unknown translation concept: #{concept.inspect}"
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def default_revision_column(concept)
|
|
57
|
+
case concept
|
|
58
|
+
when :version
|
|
59
|
+
ActiveVersion.config.revision_version_column
|
|
60
|
+
else
|
|
61
|
+
raise ConfigurationError, "Unknown revision concept: #{concept.inspect}"
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def default_audit_column(concept)
|
|
66
|
+
case concept
|
|
67
|
+
when :action
|
|
68
|
+
ActiveVersion.config.audit_action_column
|
|
69
|
+
when :changes
|
|
70
|
+
ActiveVersion.config.audit_changes_column
|
|
71
|
+
when :context
|
|
72
|
+
ActiveVersion.config.audit_context_column
|
|
73
|
+
when :comment
|
|
74
|
+
ActiveVersion.config.audit_comment_column
|
|
75
|
+
when :version
|
|
76
|
+
ActiveVersion.config.audit_version_column
|
|
77
|
+
when :user
|
|
78
|
+
ActiveVersion.config.audit_user_column
|
|
79
|
+
when :auditable
|
|
80
|
+
ActiveVersion.config.audit_auditable_column
|
|
81
|
+
when :associated
|
|
82
|
+
ActiveVersion.config.audit_associated_column
|
|
83
|
+
when :remote_address
|
|
84
|
+
ActiveVersion.config.audit_remote_address_column
|
|
85
|
+
when :request_uuid
|
|
86
|
+
ActiveVersion.config.audit_request_uuid_column
|
|
87
|
+
else
|
|
88
|
+
raise ConfigurationError, "Unknown audit concept: #{concept.inspect}"
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
module ActiveVersion
|
|
2
|
+
# Global configuration for ActiveVersion
|
|
3
|
+
class Configuration
|
|
4
|
+
attr_accessor :auditing_enabled
|
|
5
|
+
attr_accessor :current_user_method
|
|
6
|
+
attr_accessor :ignored_attributes
|
|
7
|
+
attr_accessor :ignored_default_callbacks
|
|
8
|
+
attr_accessor :store_synthesized_enums
|
|
9
|
+
attr_accessor :execution_scope
|
|
10
|
+
|
|
11
|
+
# Translation defaults
|
|
12
|
+
attr_accessor :translation_locale_column
|
|
13
|
+
attr_accessor :translation_default_locale
|
|
14
|
+
|
|
15
|
+
# Revision defaults
|
|
16
|
+
attr_accessor :revision_version_column
|
|
17
|
+
attr_accessor :revision_foreign_key_suffix
|
|
18
|
+
|
|
19
|
+
# Audit defaults
|
|
20
|
+
attr_accessor :default_audit_class
|
|
21
|
+
attr_accessor :audit_storage
|
|
22
|
+
attr_accessor :audit_action_column
|
|
23
|
+
attr_accessor :audit_changes_column
|
|
24
|
+
attr_accessor :audit_context_column
|
|
25
|
+
attr_accessor :audit_comment_column
|
|
26
|
+
attr_accessor :audit_version_column
|
|
27
|
+
attr_accessor :audit_user_column
|
|
28
|
+
attr_accessor :audit_auditable_column
|
|
29
|
+
attr_accessor :audit_associated_column
|
|
30
|
+
attr_accessor :audit_remote_address_column
|
|
31
|
+
attr_accessor :audit_request_uuid_column
|
|
32
|
+
attr_accessor :max_audits
|
|
33
|
+
attr_accessor :max_revisions
|
|
34
|
+
attr_accessor :return_self_if_no_revisions
|
|
35
|
+
attr_accessor :return_self_if_no_audits
|
|
36
|
+
attr_accessor :audit_error_behavior
|
|
37
|
+
attr_accessor :revision_error_behavior
|
|
38
|
+
attr_accessor :debounce_time
|
|
39
|
+
|
|
40
|
+
def initialize
|
|
41
|
+
# Global settings
|
|
42
|
+
@auditing_enabled = true
|
|
43
|
+
@current_user_method = :current_user
|
|
44
|
+
@ignored_attributes = %w[lock_version created_at updated_at created_on updated_on]
|
|
45
|
+
@ignored_default_callbacks = []
|
|
46
|
+
@store_synthesized_enums = false
|
|
47
|
+
@execution_scope = :fiber
|
|
48
|
+
|
|
49
|
+
# Translation defaults
|
|
50
|
+
@translation_locale_column = :locale
|
|
51
|
+
@translation_default_locale = :en
|
|
52
|
+
|
|
53
|
+
# Revision defaults
|
|
54
|
+
@revision_version_column = :version
|
|
55
|
+
@revision_foreign_key_suffix = "_id"
|
|
56
|
+
|
|
57
|
+
# Audit defaults
|
|
58
|
+
@default_audit_class = nil # When set, has_audits without :as uses this when ModelAudit is not defined
|
|
59
|
+
@audit_storage = :json_column
|
|
60
|
+
@audit_action_column = :action
|
|
61
|
+
@audit_changes_column = :audited_changes
|
|
62
|
+
@audit_context_column = :audited_context
|
|
63
|
+
@audit_comment_column = :comment
|
|
64
|
+
@audit_version_column = :version
|
|
65
|
+
@audit_user_column = :user_id
|
|
66
|
+
@audit_auditable_column = :auditable
|
|
67
|
+
@audit_associated_column = :associated
|
|
68
|
+
@audit_remote_address_column = :remote_address
|
|
69
|
+
@audit_request_uuid_column = :request_uuid
|
|
70
|
+
@max_audits = nil
|
|
71
|
+
@max_revisions = nil
|
|
72
|
+
@return_self_if_no_revisions = false
|
|
73
|
+
@return_self_if_no_audits = false
|
|
74
|
+
@audit_error_behavior = :exception
|
|
75
|
+
@revision_error_behavior = :exception
|
|
76
|
+
@debounce_time = nil # Time in seconds to merge revisions within window
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Validate configuration
|
|
80
|
+
def validate!
|
|
81
|
+
validate_storage_type!
|
|
82
|
+
validate_execution_scope!
|
|
83
|
+
validate_column_names!
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
private
|
|
87
|
+
|
|
88
|
+
def validate_storage_type!
|
|
89
|
+
unless %i[json_column yaml_column mirror_columns].include?(@audit_storage)
|
|
90
|
+
raise ConfigurationError,
|
|
91
|
+
"audit_storage must be :json_column, :yaml_column, or :mirror_columns, got: #{@audit_storage.inspect}"
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def validate_execution_scope!
|
|
96
|
+
return if %i[fiber thread].include?(@execution_scope)
|
|
97
|
+
|
|
98
|
+
raise ConfigurationError,
|
|
99
|
+
"execution_scope must be :fiber or :thread, got: #{@execution_scope.inspect}"
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def validate_column_names!
|
|
103
|
+
# Ensure column names are symbols or strings
|
|
104
|
+
column_attrs = [
|
|
105
|
+
:translation_locale_column,
|
|
106
|
+
:revision_version_column,
|
|
107
|
+
:audit_action_column,
|
|
108
|
+
:audit_changes_column,
|
|
109
|
+
:audit_context_column,
|
|
110
|
+
:audit_comment_column,
|
|
111
|
+
:audit_version_column,
|
|
112
|
+
:audit_user_column
|
|
113
|
+
]
|
|
114
|
+
|
|
115
|
+
column_attrs.each do |attr|
|
|
116
|
+
value = instance_variable_get(:"@#{attr}")
|
|
117
|
+
unless value.is_a?(Symbol) || value.is_a?(String)
|
|
118
|
+
raise ConfigurationError,
|
|
119
|
+
"#{attr} must be a Symbol or String, got: #{value.class}"
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|