pg_sql_triggers 1.3.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.erb_lint.yml +0 -0
- data/.rspec +0 -0
- data/.rubocop.yml +6 -16
- data/AGENTS.md +8 -0
- data/CHANGELOG.md +354 -0
- data/COVERAGE.md +39 -41
- data/LICENSE +0 -0
- data/README.md +44 -26
- data/RELEASE.md +0 -0
- data/Rakefile +5 -0
- data/app/assets/javascripts/pg_sql_triggers/application.js +0 -0
- data/app/assets/javascripts/pg_sql_triggers/trigger_actions.js +0 -0
- data/app/assets/stylesheets/pg_sql_triggers/application.css +0 -0
- data/app/controllers/concerns/pg_sql_triggers/error_handling.rb +0 -0
- data/app/controllers/concerns/pg_sql_triggers/kill_switch_protection.rb +0 -0
- data/app/controllers/concerns/pg_sql_triggers/permission_checking.rb +6 -5
- data/app/controllers/pg_sql_triggers/application_controller.rb +0 -0
- data/app/controllers/pg_sql_triggers/audit_logs_controller.rb +81 -64
- data/app/controllers/pg_sql_triggers/dashboard_controller.rb +111 -34
- data/app/controllers/pg_sql_triggers/migrations_controller.rb +13 -14
- data/app/controllers/pg_sql_triggers/tables_controller.rb +8 -0
- data/app/controllers/pg_sql_triggers/triggers_controller.rb +1 -0
- data/app/helpers/pg_sql_triggers/dashboard_helper.rb +19 -0
- data/app/helpers/pg_sql_triggers/permissions_helper.rb +3 -2
- data/app/models/pg_sql_triggers/application_record.rb +0 -0
- data/app/models/pg_sql_triggers/audit_log.rb +29 -47
- data/app/models/pg_sql_triggers/trigger_registry.rb +137 -74
- data/app/views/layouts/pg_sql_triggers/application.html.erb +0 -1
- data/app/views/pg_sql_triggers/audit_logs/index.html.erb +9 -5
- data/app/views/pg_sql_triggers/dashboard/index.html.erb +107 -27
- data/app/views/pg_sql_triggers/shared/_confirmation_modal.html.erb +0 -0
- data/app/views/pg_sql_triggers/shared/_kill_switch_status.html.erb +0 -0
- data/app/views/pg_sql_triggers/tables/index.html.erb +27 -18
- data/app/views/pg_sql_triggers/tables/show.html.erb +0 -2
- data/app/views/pg_sql_triggers/triggers/_drop_modal.html.erb +0 -0
- data/app/views/pg_sql_triggers/triggers/_re_execute_modal.html.erb +0 -0
- data/app/views/pg_sql_triggers/triggers/show.html.erb +33 -0
- data/config/initializers/pg_sql_triggers.rb +0 -0
- data/config/routes.rb +0 -14
- data/db/migrate/{20251222000001_create_pg_sql_triggers_tables.rb → 20251222104327_create_pg_sql_triggers_tables.rb} +0 -0
- data/db/migrate/20251229071916_add_timing_to_pg_sql_triggers_registry.rb +0 -0
- data/db/migrate/{20260103000001_create_pg_sql_triggers_audit_log.rb → 20260103114508_create_pg_sql_triggers_audit_log.rb} +0 -0
- data/db/migrate/20260228162233_add_for_each_to_pg_sql_triggers_registry.rb +8 -0
- data/db/migrate/20260412185841_add_constraint_deferral_to_pg_sql_triggers_registry.rb +9 -0
- data/docs/README.md +3 -0
- data/docs/api-reference.md +176 -152
- data/docs/audit-trail.md +1 -1
- data/docs/configuration.md +196 -3
- data/docs/getting-started.md +31 -16
- data/docs/kill-switch.md +0 -0
- data/docs/permissions.md +6 -9
- data/docs/troubleshooting.md +0 -0
- data/docs/ui-guide.md +0 -0
- data/docs/usage-guide.md +112 -67
- data/docs/web-ui.md +3 -103
- data/lib/generators/pg_sql_triggers/install_generator.rb +0 -0
- data/lib/generators/pg_sql_triggers/templates/README +0 -0
- data/lib/generators/pg_sql_triggers/templates/create_pg_sql_triggers_tables.rb +0 -0
- data/lib/generators/pg_sql_triggers/templates/initializer.rb +14 -0
- data/lib/generators/pg_sql_triggers/templates/trigger_dsl.rb.tt +11 -0
- data/lib/generators/pg_sql_triggers/templates/trigger_migration.rb.erb +0 -0
- data/lib/generators/pg_sql_triggers/templates/trigger_migration_full.rb.tt +29 -0
- data/lib/generators/pg_sql_triggers/trigger_generator.rb +83 -0
- data/lib/generators/pg_sql_triggers/trigger_migration_generator.rb +0 -0
- data/lib/pg_sql_triggers/alerting.rb +77 -0
- data/lib/pg_sql_triggers/database_introspection.rb +0 -0
- data/lib/pg_sql_triggers/deferral_checksum.rb +54 -0
- data/lib/pg_sql_triggers/drift/db_queries.rb +26 -13
- data/lib/pg_sql_triggers/drift/detector.rb +59 -38
- data/lib/pg_sql_triggers/drift/reporter.rb +0 -0
- data/lib/pg_sql_triggers/drift.rb +5 -0
- data/lib/pg_sql_triggers/dsl/trigger_definition.rb +68 -20
- data/lib/pg_sql_triggers/dsl.rb +0 -0
- data/lib/pg_sql_triggers/engine.rb +49 -0
- data/lib/pg_sql_triggers/errors.rb +0 -0
- data/lib/pg_sql_triggers/events_checksum.rb +114 -0
- data/lib/pg_sql_triggers/migration.rb +5 -6
- data/lib/pg_sql_triggers/migrator/pre_apply_comparator.rb +85 -82
- data/lib/pg_sql_triggers/migrator/pre_apply_diff_reporter.rb +0 -0
- data/lib/pg_sql_triggers/migrator/safety_validator.rb +34 -12
- data/lib/pg_sql_triggers/migrator.rb +137 -94
- data/lib/pg_sql_triggers/permissions/checker.rb +12 -15
- data/lib/pg_sql_triggers/permissions.rb +1 -0
- data/lib/pg_sql_triggers/rake_development_boot.rb +65 -0
- data/lib/pg_sql_triggers/registry/manager.rb +60 -21
- data/lib/pg_sql_triggers/registry/validator.rb +287 -6
- data/lib/pg_sql_triggers/registry.rb +0 -0
- data/lib/pg_sql_triggers/schema_dumper_extension.rb +32 -0
- data/lib/pg_sql_triggers/sql/kill_switch.rb +154 -275
- data/lib/pg_sql_triggers/sql.rb +0 -6
- data/lib/pg_sql_triggers/testing/dry_run.rb +0 -0
- data/lib/pg_sql_triggers/testing/function_tester.rb +97 -107
- data/lib/pg_sql_triggers/testing/safe_executor.rb +0 -0
- data/lib/pg_sql_triggers/testing/syntax_validator.rb +0 -0
- data/lib/pg_sql_triggers/testing.rb +0 -0
- data/lib/pg_sql_triggers/trigger_structure_dumper.rb +111 -0
- data/lib/pg_sql_triggers/version.rb +1 -1
- data/lib/pg_sql_triggers.rb +21 -1
- data/lib/tasks/trigger_migrations.rake +235 -152
- data/rakelib/pg_sql_triggers_environment.rake +9 -0
- data/scripts/generate_coverage_report.rb +4 -1
- data/sig/pg_sql_triggers.rbs +0 -0
- metadata +68 -22
- data/Goal.md +0 -742
- data/app/controllers/pg_sql_triggers/generator_controller.rb +0 -213
- data/app/controllers/pg_sql_triggers/sql_capsules_controller.rb +0 -161
- data/app/views/pg_sql_triggers/generator/new.html.erb +0 -388
- data/app/views/pg_sql_triggers/generator/preview.html.erb +0 -305
- data/app/views/pg_sql_triggers/sql_capsules/new.html.erb +0 -81
- data/app/views/pg_sql_triggers/sql_capsules/show.html.erb +0 -85
- data/lib/generators/trigger/migration_generator.rb +0 -60
- data/lib/pg_sql_triggers/generator/form.rb +0 -80
- data/lib/pg_sql_triggers/generator/service.rb +0 -339
- data/lib/pg_sql_triggers/generator.rb +0 -8
- data/lib/pg_sql_triggers/sql/capsule.rb +0 -79
- data/lib/pg_sql_triggers/sql/executor.rb +0 -200
|
@@ -17,70 +17,52 @@ module PgSqlTriggers
|
|
|
17
17
|
validates :operation, presence: true
|
|
18
18
|
validates :status, presence: true, inclusion: { in: %w[success failure] }
|
|
19
19
|
|
|
20
|
+
# Known keyword options accepted by log_success / log_failure, in addition to
|
|
21
|
+
# +operation+ (required for both) and +error_message+ (required for log_failure).
|
|
22
|
+
SUCCESS_ATTRS = %i[trigger_name actor environment reason confirmation_text
|
|
23
|
+
before_state after_state diff].freeze
|
|
24
|
+
FAILURE_ATTRS = %i[trigger_name actor environment reason confirmation_text before_state].freeze
|
|
25
|
+
|
|
20
26
|
# Class methods for logging operations
|
|
21
27
|
class << self
|
|
22
|
-
# Log a successful operation
|
|
28
|
+
# Log a successful operation.
|
|
23
29
|
#
|
|
24
|
-
#
|
|
25
|
-
#
|
|
26
|
-
#
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
# @param confirmation_text [String, nil] Confirmation text used
|
|
30
|
-
# @param before_state [Hash, nil] State before operation
|
|
31
|
-
# @param after_state [Hash, nil] State after operation
|
|
32
|
-
# @param diff [String, nil] Diff information
|
|
33
|
-
# rubocop:disable Metrics/ParameterLists
|
|
34
|
-
def log_success(operation:, trigger_name: nil, actor: nil, environment: nil,
|
|
35
|
-
reason: nil, confirmation_text: nil, before_state: nil,
|
|
36
|
-
after_state: nil, diff: nil)
|
|
30
|
+
# Required: +operation:+ (Symbol/String).
|
|
31
|
+
# Optional (all via keyword args): trigger_name, actor, environment, reason,
|
|
32
|
+
# confirmation_text, before_state, after_state, diff.
|
|
33
|
+
def log_success(operation:, **options)
|
|
34
|
+
attrs = options.slice(*SUCCESS_ATTRS)
|
|
37
35
|
create!(
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
reason: reason,
|
|
44
|
-
confirmation_text: confirmation_text,
|
|
45
|
-
before_state: before_state,
|
|
46
|
-
after_state: after_state,
|
|
47
|
-
diff: diff
|
|
36
|
+
attrs.merge(
|
|
37
|
+
operation: operation.to_s,
|
|
38
|
+
actor: serialize_actor(attrs[:actor]),
|
|
39
|
+
status: "success"
|
|
40
|
+
)
|
|
48
41
|
)
|
|
49
42
|
rescue StandardError => e
|
|
50
43
|
Rails.logger.error("Failed to log audit entry: #{e.message}") if defined?(Rails.logger)
|
|
51
44
|
nil
|
|
52
45
|
end
|
|
53
|
-
# rubocop:enable Metrics/ParameterLists
|
|
54
46
|
|
|
55
|
-
# Log a failed operation
|
|
47
|
+
# Log a failed operation.
|
|
56
48
|
#
|
|
57
|
-
#
|
|
58
|
-
#
|
|
59
|
-
#
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
# @param reason [String, nil] Reason for the operation (if provided before failure)
|
|
63
|
-
# @param confirmation_text [String, nil] Confirmation text used
|
|
64
|
-
# @param before_state [Hash, nil] State before operation
|
|
65
|
-
# rubocop:disable Metrics/ParameterLists
|
|
66
|
-
def log_failure(operation:, error_message:, trigger_name: nil, actor: nil, environment: nil, reason: nil,
|
|
67
|
-
confirmation_text: nil, before_state: nil)
|
|
49
|
+
# Required: +operation:+ (Symbol/String) and +error_message:+ (String).
|
|
50
|
+
# Optional (all via keyword args): trigger_name, actor, environment, reason,
|
|
51
|
+
# confirmation_text, before_state.
|
|
52
|
+
def log_failure(operation:, error_message:, **options)
|
|
53
|
+
attrs = options.slice(*FAILURE_ATTRS)
|
|
68
54
|
create!(
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
reason: reason,
|
|
76
|
-
confirmation_text: confirmation_text,
|
|
77
|
-
before_state: before_state
|
|
55
|
+
attrs.merge(
|
|
56
|
+
operation: operation.to_s,
|
|
57
|
+
actor: serialize_actor(attrs[:actor]),
|
|
58
|
+
status: "failure",
|
|
59
|
+
error_message: error_message
|
|
60
|
+
)
|
|
78
61
|
)
|
|
79
62
|
rescue StandardError => e
|
|
80
63
|
Rails.logger.error("Failed to log audit entry: #{e.message}") if defined?(Rails.logger)
|
|
81
64
|
nil
|
|
82
65
|
end
|
|
83
|
-
# rubocop:enable Metrics/ParameterLists
|
|
84
66
|
|
|
85
67
|
# Get audit log entries for a specific trigger
|
|
86
68
|
#
|
|
@@ -21,7 +21,9 @@ module PgSqlTriggers
|
|
|
21
21
|
# @example Drop and re-execute triggers
|
|
22
22
|
# trigger.drop!(reason: "No longer needed", actor: current_user, confirmation: "EXECUTE TRIGGER_DROP")
|
|
23
23
|
# trigger.re_execute!(reason: "Fix drift", actor: current_user, confirmation: "EXECUTE TRIGGER_RE_EXECUTE")
|
|
24
|
-
# rubocop:disable Metrics/ClassLength
|
|
24
|
+
# rubocop:disable Metrics/ClassLength -- core AR model: groups lifecycle operations
|
|
25
|
+
# (enable!/disable!/drop!/re_execute!), drift helpers, audit hooks, and SQL builders.
|
|
26
|
+
# Splitting further would fragment tightly-coupled state and audit concerns.
|
|
25
27
|
class TriggerRegistry < PgSqlTriggers::ApplicationRecord
|
|
26
28
|
self.table_name = "pg_sql_triggers_registry"
|
|
27
29
|
|
|
@@ -39,6 +41,16 @@ module PgSqlTriggers
|
|
|
39
41
|
scope :for_environment, ->(env) { where(environment: [env, nil]) }
|
|
40
42
|
scope :by_source, ->(source) { where(source: source) }
|
|
41
43
|
|
|
44
|
+
# Case-insensitive search on trigger name and table name (used by web dashboard).
|
|
45
|
+
scope :matching_search, lambda { |raw|
|
|
46
|
+
query = raw.to_s.strip
|
|
47
|
+
next all if query.blank?
|
|
48
|
+
|
|
49
|
+
sanitized = ActiveRecord::Base.sanitize_sql_like(query)
|
|
50
|
+
term = "%#{sanitized}%"
|
|
51
|
+
where("trigger_name ILIKE :term OR table_name ILIKE :term", term: term)
|
|
52
|
+
}
|
|
53
|
+
|
|
42
54
|
# Returns the current drift state of this trigger.
|
|
43
55
|
#
|
|
44
56
|
# @return [String] One of: "in_sync", "drifted", "manual_override", "disabled", "dropped", "unknown"
|
|
@@ -119,32 +131,12 @@ module PgSqlTriggers
|
|
|
119
131
|
end
|
|
120
132
|
end
|
|
121
133
|
|
|
122
|
-
# Update the registry record (always update, even if trigger doesn't exist)
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
# If update! fails, try update_column which bypasses validations and callbacks
|
|
129
|
-
# and might not use execute in the same way
|
|
130
|
-
Rails.logger.warn("Could not update registry via update!: #{e.message}") if defined?(Rails.logger)
|
|
131
|
-
begin
|
|
132
|
-
# rubocop:disable Rails/SkipsModelValidations
|
|
133
|
-
update_column(:enabled, true)
|
|
134
|
-
# rubocop:enable Rails/SkipsModelValidations
|
|
135
|
-
after_state = capture_state
|
|
136
|
-
log_audit_success(:trigger_enable, actor, before_state: before_state, after_state: after_state)
|
|
137
|
-
rescue StandardError => update_error
|
|
138
|
-
# If update_column also fails, just set the in-memory attribute
|
|
139
|
-
# The test might reload, but we've done our best
|
|
140
|
-
# rubocop:disable Layout/LineLength
|
|
141
|
-
Rails.logger.warn("Could not update registry via update_column: #{update_error.message}") if defined?(Rails.logger)
|
|
142
|
-
# rubocop:enable Layout/LineLength
|
|
143
|
-
self.enabled = true
|
|
144
|
-
after_state = capture_state
|
|
145
|
-
log_audit_success(:trigger_enable, actor, before_state: before_state, after_state: after_state)
|
|
146
|
-
end
|
|
147
|
-
end
|
|
134
|
+
# Update the registry record (always update, even if trigger doesn't exist).
|
|
135
|
+
# If persistence fails for any reason, fall back to the in-memory attribute so
|
|
136
|
+
# callers/observers still see a consistent state for this request.
|
|
137
|
+
persist_enabled_state(true)
|
|
138
|
+
after_state = capture_state
|
|
139
|
+
log_audit_success(:trigger_enable, actor, before_state: before_state, after_state: after_state)
|
|
148
140
|
end
|
|
149
141
|
|
|
150
142
|
# Disables this trigger in the database and updates the registry.
|
|
@@ -191,32 +183,12 @@ module PgSqlTriggers
|
|
|
191
183
|
end
|
|
192
184
|
end
|
|
193
185
|
|
|
194
|
-
# Update the registry record (always update, even if trigger doesn't exist)
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
# If update! fails, try update_column which bypasses validations and callbacks
|
|
201
|
-
# and might not use execute in the same way
|
|
202
|
-
Rails.logger.warn("Could not update registry via update!: #{e.message}") if defined?(Rails.logger)
|
|
203
|
-
begin
|
|
204
|
-
# rubocop:disable Rails/SkipsModelValidations
|
|
205
|
-
update_column(:enabled, false)
|
|
206
|
-
# rubocop:enable Rails/SkipsModelValidations
|
|
207
|
-
after_state = capture_state
|
|
208
|
-
log_audit_success(:trigger_disable, actor, before_state: before_state, after_state: after_state)
|
|
209
|
-
rescue StandardError => update_error
|
|
210
|
-
# If update_column also fails, just set the in-memory attribute
|
|
211
|
-
# The test might reload, but we've done our best
|
|
212
|
-
# rubocop:disable Layout/LineLength
|
|
213
|
-
Rails.logger.warn("Could not update registry via update_column: #{update_error.message}") if defined?(Rails.logger)
|
|
214
|
-
# rubocop:enable Layout/LineLength
|
|
215
|
-
self.enabled = false
|
|
216
|
-
after_state = capture_state
|
|
217
|
-
log_audit_success(:trigger_disable, actor, before_state: before_state, after_state: after_state)
|
|
218
|
-
end
|
|
219
|
-
end
|
|
186
|
+
# Update the registry record (always update, even if trigger doesn't exist).
|
|
187
|
+
# If persistence fails for any reason, fall back to the in-memory attribute so
|
|
188
|
+
# callers/observers still see a consistent state for this request.
|
|
189
|
+
persist_enabled_state(false)
|
|
190
|
+
after_state = capture_state
|
|
191
|
+
log_audit_success(:trigger_disable, actor, before_state: before_state, after_state: after_state)
|
|
220
192
|
end
|
|
221
193
|
|
|
222
194
|
# Drops this trigger from the database and removes it from the registry.
|
|
@@ -247,7 +219,6 @@ module PgSqlTriggers
|
|
|
247
219
|
# Execute DROP TRIGGER in transaction
|
|
248
220
|
ActiveRecord::Base.transaction do
|
|
249
221
|
drop_trigger_from_database
|
|
250
|
-
trigger_name
|
|
251
222
|
destroy!
|
|
252
223
|
log_drop_success
|
|
253
224
|
log_audit_success(:trigger_drop, actor, reason: reason, confirmation_text: confirmation,
|
|
@@ -264,8 +235,9 @@ module PgSqlTriggers
|
|
|
264
235
|
# @param reason [String] Required reason for re-executing the trigger
|
|
265
236
|
# @param confirmation [String, nil] Optional confirmation text for kill switch protection
|
|
266
237
|
# @param actor [Hash, nil] Information about who is performing the action (must have :type and :id keys)
|
|
267
|
-
# @raise [ArgumentError] If reason is missing or empty
|
|
238
|
+
# @raise [ArgumentError] If reason is missing or empty
|
|
268
239
|
# @raise [PgSqlTriggers::KillSwitchError] If kill switch blocks the operation
|
|
240
|
+
# @raise [StandardError] If SQL cannot be generated to recreate the trigger
|
|
269
241
|
# @return [PgSqlTriggers::TriggerRegistry] self
|
|
270
242
|
def re_execute!(reason:, confirmation: nil, actor: nil)
|
|
271
243
|
actor ||= { type: "Console", id: "TriggerRegistry#re_execute!" }
|
|
@@ -286,7 +258,6 @@ module PgSqlTriggers
|
|
|
286
258
|
|
|
287
259
|
# Validate reason is provided
|
|
288
260
|
raise ArgumentError, "Reason is required" if reason.nil? || reason.to_s.strip.empty?
|
|
289
|
-
raise StandardError, "Cannot re-execute: missing function_body" if function_body.blank?
|
|
290
261
|
|
|
291
262
|
log_re_execute_attempt(reason)
|
|
292
263
|
|
|
@@ -316,13 +287,22 @@ module PgSqlTriggers
|
|
|
316
287
|
end
|
|
317
288
|
|
|
318
289
|
def calculate_checksum
|
|
290
|
+
deferral = PgSqlTriggers::DeferralChecksum.parts(
|
|
291
|
+
constraint_trigger: constraint_trigger,
|
|
292
|
+
deferrable: deferrable,
|
|
293
|
+
initially: initially
|
|
294
|
+
)
|
|
295
|
+
events_segment = PgSqlTriggers::EventsChecksum.segment_from_definition_json(definition)
|
|
319
296
|
Digest::SHA256.hexdigest([
|
|
320
297
|
trigger_name,
|
|
321
298
|
table_name,
|
|
322
299
|
version,
|
|
323
300
|
function_body || "",
|
|
324
301
|
condition || "",
|
|
325
|
-
timing || "before"
|
|
302
|
+
timing || "before",
|
|
303
|
+
for_each || "row",
|
|
304
|
+
events_segment,
|
|
305
|
+
*deferral
|
|
326
306
|
].join)
|
|
327
307
|
end
|
|
328
308
|
|
|
@@ -394,7 +374,17 @@ module PgSqlTriggers
|
|
|
394
374
|
end
|
|
395
375
|
|
|
396
376
|
def recreate_trigger
|
|
397
|
-
|
|
377
|
+
# DSL triggers are recreated from the stored JSON definition (+build_trigger_sql_from_definition+).
|
|
378
|
+
# Other sources may persist a full trigger SQL payload in +function_body+.
|
|
379
|
+
sql = if source == "dsl"
|
|
380
|
+
build_trigger_sql_from_definition
|
|
381
|
+
else
|
|
382
|
+
function_body.presence || build_trigger_sql_from_definition
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
raise StandardError, "Cannot re-execute: no SQL could be generated" if sql.blank?
|
|
386
|
+
|
|
387
|
+
ActiveRecord::Base.connection.execute(sql)
|
|
398
388
|
if defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger
|
|
399
389
|
Rails.logger.info "[TRIGGER_RE_EXECUTE] Re-created trigger"
|
|
400
390
|
end
|
|
@@ -405,8 +395,71 @@ module PgSqlTriggers
|
|
|
405
395
|
raise
|
|
406
396
|
end
|
|
407
397
|
|
|
398
|
+
# Build a CREATE TRIGGER SQL statement from the stored DSL definition JSON.
|
|
399
|
+
# Used by re_execute! when function_body is absent (the normal case for DSL triggers).
|
|
400
|
+
def build_trigger_sql_from_definition
|
|
401
|
+
return nil if definition.blank?
|
|
402
|
+
|
|
403
|
+
defn = JSON.parse(definition)
|
|
404
|
+
fn_name = defn["function_name"]
|
|
405
|
+
return nil if fn_name.blank?
|
|
406
|
+
|
|
407
|
+
t_name = table_name
|
|
408
|
+
constraint = ActiveModel::Type::Boolean.new.cast(defn["constraint_trigger"])
|
|
409
|
+
timing_kw = if constraint
|
|
410
|
+
"AFTER"
|
|
411
|
+
else
|
|
412
|
+
(defn["timing"] || timing || "before").to_s.upcase
|
|
413
|
+
end
|
|
414
|
+
events_sql = PgSqlTriggers::EventsChecksum.events_sql_fragment(
|
|
415
|
+
defn,
|
|
416
|
+
quote_column: ->(col) { quote_identifier(col) }
|
|
417
|
+
)
|
|
418
|
+
cond = defn["condition"] || condition
|
|
419
|
+
for_each_kw = (defn["for_each"] || for_each || "row").upcase
|
|
420
|
+
|
|
421
|
+
create_kw = constraint ? "CREATE CONSTRAINT TRIGGER" : "CREATE TRIGGER"
|
|
422
|
+
sql = "#{create_kw} #{quote_identifier(trigger_name)} "
|
|
423
|
+
sql += "#{timing_kw} #{events_sql} ON #{quote_identifier(t_name)} "
|
|
424
|
+
deferral = deferral_sql_fragment(defn)
|
|
425
|
+
sql += "#{deferral} " if deferral.present?
|
|
426
|
+
sql += "FOR EACH #{for_each_kw} "
|
|
427
|
+
sql += "WHEN (#{cond}) " if cond.present?
|
|
428
|
+
sql += "EXECUTE FUNCTION #{fn_name}();"
|
|
429
|
+
sql
|
|
430
|
+
rescue JSON::ParserError
|
|
431
|
+
nil
|
|
432
|
+
end
|
|
433
|
+
|
|
434
|
+
def deferral_sql_fragment(defn)
|
|
435
|
+
return "" unless ActiveModel::Type::Boolean.new.cast(defn["constraint_trigger"])
|
|
436
|
+
|
|
437
|
+
case defn["deferrable"].to_s
|
|
438
|
+
when "not_deferrable"
|
|
439
|
+
"NOT DEFERRABLE"
|
|
440
|
+
when "deferrable"
|
|
441
|
+
case defn["initially"].to_s
|
|
442
|
+
when "deferred"
|
|
443
|
+
"DEFERRABLE INITIALLY DEFERRED"
|
|
444
|
+
when "immediate"
|
|
445
|
+
"DEFERRABLE INITIALLY IMMEDIATE"
|
|
446
|
+
else
|
|
447
|
+
"DEFERRABLE"
|
|
448
|
+
end
|
|
449
|
+
else
|
|
450
|
+
""
|
|
451
|
+
end
|
|
452
|
+
end
|
|
453
|
+
|
|
408
454
|
def update_registry_after_re_execute
|
|
409
|
-
update!(
|
|
455
|
+
update!(last_executed_at: Time.current)
|
|
456
|
+
if !enabled && ActiveRecord::Base.connection.table_exists?(table_name)
|
|
457
|
+
quoted_table = quote_identifier(table_name)
|
|
458
|
+
quoted_trigger = quote_identifier(trigger_name)
|
|
459
|
+
ActiveRecord::Base.connection.execute(
|
|
460
|
+
"ALTER TABLE #{quoted_table} DISABLE TRIGGER #{quoted_trigger};"
|
|
461
|
+
)
|
|
462
|
+
end
|
|
410
463
|
Rails.logger.info "[TRIGGER_RE_EXECUTE] Updated registry" if defined?(Rails.logger)
|
|
411
464
|
end
|
|
412
465
|
|
|
@@ -419,13 +472,12 @@ module PgSqlTriggers
|
|
|
419
472
|
table_name: table_name,
|
|
420
473
|
source: source,
|
|
421
474
|
environment: environment,
|
|
422
|
-
installed_at: installed_at&.iso8601
|
|
475
|
+
installed_at: installed_at&.iso8601,
|
|
476
|
+
function_body: function_body
|
|
423
477
|
}
|
|
424
478
|
end
|
|
425
479
|
|
|
426
|
-
|
|
427
|
-
def log_audit_success(operation, actor, reason: nil, confirmation_text: nil,
|
|
428
|
-
before_state: nil, after_state: nil, diff: nil)
|
|
480
|
+
def log_audit_success(operation, actor, **options)
|
|
429
481
|
return unless defined?(PgSqlTriggers::AuditLog)
|
|
430
482
|
|
|
431
483
|
PgSqlTriggers::AuditLog.log_success(
|
|
@@ -433,18 +485,13 @@ module PgSqlTriggers
|
|
|
433
485
|
trigger_name: trigger_name,
|
|
434
486
|
actor: actor,
|
|
435
487
|
environment: Rails.env,
|
|
436
|
-
|
|
437
|
-
confirmation_text: confirmation_text,
|
|
438
|
-
before_state: before_state,
|
|
439
|
-
after_state: after_state,
|
|
440
|
-
diff: diff
|
|
488
|
+
**options
|
|
441
489
|
)
|
|
442
490
|
rescue StandardError => e
|
|
443
491
|
Rails.logger.error("Failed to log audit entry: #{e.message}") if defined?(Rails.logger)
|
|
444
492
|
end
|
|
445
493
|
|
|
446
|
-
def log_audit_failure(operation, actor, error_message,
|
|
447
|
-
confirmation_text: nil, before_state: nil)
|
|
494
|
+
def log_audit_failure(operation, actor, error_message, **options)
|
|
448
495
|
return unless defined?(PgSqlTriggers::AuditLog)
|
|
449
496
|
|
|
450
497
|
PgSqlTriggers::AuditLog.log_failure(
|
|
@@ -453,14 +500,30 @@ module PgSqlTriggers
|
|
|
453
500
|
actor: actor,
|
|
454
501
|
environment: Rails.env,
|
|
455
502
|
error_message: error_message,
|
|
456
|
-
|
|
457
|
-
confirmation_text: confirmation_text,
|
|
458
|
-
before_state: before_state
|
|
503
|
+
**options
|
|
459
504
|
)
|
|
460
505
|
rescue StandardError => e
|
|
461
506
|
Rails.logger.error("Failed to log audit entry: #{e.message}") if defined?(Rails.logger)
|
|
462
507
|
end
|
|
463
|
-
|
|
508
|
+
|
|
509
|
+
# Persist the +enabled+ flag: +update!+, then +update_column+ if needed, then in-memory.
|
|
510
|
+
# Returns true when a DB write succeeded, false when only the in-memory value changed.
|
|
511
|
+
def persist_enabled_state(value)
|
|
512
|
+
update!(enabled: value)
|
|
513
|
+
true
|
|
514
|
+
rescue ActiveRecord::StatementInvalid, StandardError => e
|
|
515
|
+
Rails.logger.warn("Could not update registry via update!: #{e.message}") if defined?(Rails.logger)
|
|
516
|
+
begin
|
|
517
|
+
# rubocop:disable Rails/SkipsModelValidations -- fallback when update! fails (callbacks/validations/locks)
|
|
518
|
+
update_column(:enabled, value)
|
|
519
|
+
# rubocop:enable Rails/SkipsModelValidations
|
|
520
|
+
true
|
|
521
|
+
rescue ActiveRecord::StatementInvalid, StandardError => e2
|
|
522
|
+
Rails.logger.warn("Could not update registry via update_column: #{e2.message}") if defined?(Rails.logger)
|
|
523
|
+
self.enabled = value
|
|
524
|
+
false
|
|
525
|
+
end
|
|
526
|
+
end
|
|
464
527
|
end
|
|
465
528
|
# rubocop:enable Metrics/ClassLength
|
|
466
529
|
end
|
|
@@ -18,7 +18,6 @@
|
|
|
18
18
|
<div>
|
|
19
19
|
<%= link_to "Dashboard", root_path, style: "color: white; margin-right: 1rem;" %>
|
|
20
20
|
<%= link_to "Tables", tables_path, style: "color: white; margin-right: 1rem;" %>
|
|
21
|
-
<%= link_to "Generator", new_generator_path, style: "color: white; margin-right: 1rem;" %>
|
|
22
21
|
<%= link_to "Audit Log", audit_logs_path, style: "color: white;" %>
|
|
23
22
|
</div>
|
|
24
23
|
</div>
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<h1 style="margin: 0;">Audit Log</h1>
|
|
5
5
|
<div style="display: flex; gap: 1rem;">
|
|
6
6
|
<%= link_to "Dashboard", dashboard_path, style: "padding: 0.5rem 1rem; background: #6c757d; color: white; text-decoration: none; border-radius: 4px; display: inline-block;" %>
|
|
7
|
-
<% csv_params = params.permit(:trigger_name, :operation, :status, :environment, :actor_id).to_h %>
|
|
7
|
+
<% csv_params = params.permit(:trigger_name, :operation, :status, :environment, :actor_id, :q).to_h %>
|
|
8
8
|
<%= link_to "Export CSV", audit_logs_path(csv_params.merge(format: :csv)), style: "padding: 0.5rem 1rem; background: #28a745; color: white; text-decoration: none; border-radius: 4px; display: inline-block;" %>
|
|
9
9
|
</div>
|
|
10
10
|
</div>
|
|
@@ -13,6 +13,10 @@
|
|
|
13
13
|
<div style="background: white; padding: 1.5rem; border-radius: 4px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); margin-bottom: 2rem;">
|
|
14
14
|
<h3 style="margin-top: 0; margin-bottom: 1rem;">Filters</h3>
|
|
15
15
|
<%= form_with url: audit_logs_path, method: :get, local: true, style: "display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem;" do |f| %>
|
|
16
|
+
<div style="grid-column: 1 / -1;">
|
|
17
|
+
<%= label_tag :q, "Search", style: "display: block; margin-bottom: 0.5rem; font-weight: 600;" %>
|
|
18
|
+
<%= text_field_tag :q, params[:q], placeholder: "Trigger, operation, reason, error…", style: "width: 100%; padding: 0.5rem; border: 1px solid #ced4da; border-radius: 4px;" %>
|
|
19
|
+
</div>
|
|
16
20
|
<div>
|
|
17
21
|
<%= f.label :trigger_name, "Trigger Name", style: "display: block; margin-bottom: 0.5rem; font-weight: 600;" %>
|
|
18
22
|
<%= f.select :trigger_name, options_for_select([["All", ""]] + @available_trigger_names.map { |n| [n, n] }, params[:trigger_name]), {}, { style: "width: 100%; padding: 0.5rem; border: 1px solid #ced4da; border-radius: 4px;" } %>
|
|
@@ -48,7 +52,7 @@
|
|
|
48
52
|
<!-- Results Summary -->
|
|
49
53
|
<div style="background: #f8f9fa; padding: 1rem; border-radius: 4px; margin-bottom: 1rem;">
|
|
50
54
|
<strong>Total Results:</strong> <%= @total_count %> entries
|
|
51
|
-
<% if params[:trigger_name].present? || params[:operation].present? || params[:status].present? || params[:environment].present? %>
|
|
55
|
+
<% if params[:trigger_name].present? || params[:operation].present? || params[:status].present? || params[:environment].present? || params[:q].present? %>
|
|
52
56
|
<span style="color: #6c757d;">(filtered)</span>
|
|
53
57
|
<% end %>
|
|
54
58
|
</div>
|
|
@@ -144,7 +148,7 @@
|
|
|
144
148
|
<% if @total_pages > 1 %>
|
|
145
149
|
<div style="display: flex; justify-content: center; align-items: center; gap: 1rem; margin-top: 2rem;">
|
|
146
150
|
<% if @page > 1 %>
|
|
147
|
-
<% prev_params = params.except(:page).permit(:trigger_name, :operation, :status, :environment, :sort, :per_page).to_h %>
|
|
151
|
+
<% prev_params = params.except(:page).permit(:trigger_name, :operation, :status, :environment, :sort, :per_page, :q).to_h %>
|
|
148
152
|
<%= link_to "« Previous", audit_logs_path(prev_params.merge(page: @page - 1)), style: "padding: 0.5rem 1rem; background: #007bff; color: white; text-decoration: none; border-radius: 4px;" %>
|
|
149
153
|
<% else %>
|
|
150
154
|
<span style="padding: 0.5rem 1rem; background: #e9ecef; color: #6c757d; border-radius: 4px; cursor: not-allowed;">« Previous</span>
|
|
@@ -155,7 +159,7 @@
|
|
|
155
159
|
</span>
|
|
156
160
|
|
|
157
161
|
<% if @page < @total_pages %>
|
|
158
|
-
<% next_params = params.except(:page).permit(:trigger_name, :operation, :status, :environment, :sort, :per_page).to_h %>
|
|
162
|
+
<% next_params = params.except(:page).permit(:trigger_name, :operation, :status, :environment, :sort, :per_page, :q).to_h %>
|
|
159
163
|
<%= link_to "Next »", audit_logs_path(next_params.merge(page: @page + 1)), style: "padding: 0.5rem 1rem; background: #007bff; color: white; text-decoration: none; border-radius: 4px;" %>
|
|
160
164
|
<% else %>
|
|
161
165
|
<span style="padding: 0.5rem 1rem; background: #e9ecef; color: #6c757d; border-radius: 4px; cursor: not-allowed;">Next »</span>
|
|
@@ -166,7 +170,7 @@
|
|
|
166
170
|
<div style="background: white; padding: 3rem; border-radius: 4px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); text-align: center;">
|
|
167
171
|
<h3 style="margin-top: 0; color: #6c757d;">No audit log entries found</h3>
|
|
168
172
|
<p style="color: #6c757d;">
|
|
169
|
-
<% if params[:trigger_name].present? || params[:operation].present? || params[:status].present? || params[:environment].present? %>
|
|
173
|
+
<% if params[:trigger_name].present? || params[:operation].present? || params[:status].present? || params[:environment].present? || params[:q].present? %>
|
|
170
174
|
Try adjusting your filters or <%= link_to "clear filters", audit_logs_path, style: "color: #007bff;" %>.
|
|
171
175
|
<% else %>
|
|
172
176
|
Audit log entries will appear here as operations are performed.
|