pg_sql_triggers 1.3.0 → 1.4.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/CHANGELOG.md +253 -1
- data/GEM_ANALYSIS.md +368 -0
- data/README.md +20 -23
- data/app/models/pg_sql_triggers/trigger_registry.rb +42 -6
- data/app/views/layouts/pg_sql_triggers/application.html.erb +0 -1
- data/app/views/pg_sql_triggers/dashboard/index.html.erb +1 -4
- data/app/views/pg_sql_triggers/tables/index.html.erb +1 -4
- data/app/views/pg_sql_triggers/tables/show.html.erb +0 -2
- data/config/routes.rb +0 -14
- data/db/migrate/20260228000001_add_for_each_to_pg_sql_triggers_registry.rb +8 -0
- data/docs/api-reference.md +44 -153
- data/docs/configuration.md +24 -3
- data/docs/getting-started.md +17 -16
- data/docs/usage-guide.md +38 -67
- data/docs/web-ui.md +3 -103
- data/lib/generators/pg_sql_triggers/templates/trigger_dsl.rb.tt +11 -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/pg_sql_triggers/drift/db_queries.rb +12 -8
- data/lib/pg_sql_triggers/drift/detector.rb +51 -38
- data/lib/pg_sql_triggers/dsl/trigger_definition.rb +17 -23
- data/lib/pg_sql_triggers/engine.rb +14 -0
- data/lib/pg_sql_triggers/migrator/pre_apply_comparator.rb +8 -9
- data/lib/pg_sql_triggers/migrator/safety_validator.rb +32 -12
- data/lib/pg_sql_triggers/migrator.rb +53 -6
- data/lib/pg_sql_triggers/registry/manager.rb +36 -11
- data/lib/pg_sql_triggers/registry/validator.rb +62 -5
- data/lib/pg_sql_triggers/sql/kill_switch.rb +153 -275
- data/lib/pg_sql_triggers/sql.rb +0 -6
- data/lib/pg_sql_triggers/version.rb +1 -1
- data/lib/pg_sql_triggers.rb +4 -1
- data/pg_sql_triggers.gemspec +53 -0
- metadata +7 -13
- 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
|
@@ -2,327 +2,205 @@
|
|
|
2
2
|
|
|
3
3
|
module PgSqlTriggers
|
|
4
4
|
module SQL
|
|
5
|
-
#
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
5
|
+
# Private helpers for KillSwitch – not part of the public API.
|
|
6
|
+
module KillSwitchHelpers
|
|
7
|
+
def validate_confirmation!(confirmation, operation)
|
|
8
|
+
expected = expected_confirmation(operation)
|
|
9
|
+
|
|
10
|
+
if confirmation.nil? || confirmation.strip.empty?
|
|
11
|
+
raise PgSqlTriggers::KillSwitchError.new(
|
|
12
|
+
"Confirmation text required. Expected: '#{expected}'",
|
|
13
|
+
error_code: "KILL_SWITCH_CONFIRMATION_REQUIRED",
|
|
14
|
+
recovery_suggestion: "Provide the confirmation text: #{expected}",
|
|
15
|
+
context: { operation: operation, expected_confirmation: expected }
|
|
16
|
+
)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
return if confirmation.strip == expected
|
|
20
|
+
|
|
21
|
+
raise PgSqlTriggers::KillSwitchError.new(
|
|
22
|
+
"Invalid confirmation text. Expected: '#{expected}', got: '#{confirmation.strip}'",
|
|
23
|
+
error_code: "KILL_SWITCH_CONFIRMATION_INVALID",
|
|
24
|
+
recovery_suggestion: "Use the exact confirmation text: #{expected}",
|
|
25
|
+
context: {
|
|
26
|
+
operation: operation,
|
|
27
|
+
expected_confirmation: expected,
|
|
28
|
+
provided_confirmation: confirmation.strip
|
|
29
|
+
}
|
|
30
|
+
)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def kill_switch_enabled?
|
|
34
|
+
return true unless PgSqlTriggers.respond_to?(:kill_switch_enabled)
|
|
35
|
+
|
|
36
|
+
value = PgSqlTriggers.kill_switch_enabled
|
|
37
|
+
value.nil? || value
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def protected_environment?(env)
|
|
41
|
+
return false if env.nil?
|
|
42
|
+
|
|
43
|
+
configured = PgSqlTriggers.kill_switch_environments if PgSqlTriggers.respond_to?(:kill_switch_environments)
|
|
44
|
+
Array(configured || %i[production staging]).map(&:to_s).include?(env.to_s)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def resolve_environment(environment)
|
|
48
|
+
return environment.to_s if environment.present?
|
|
49
|
+
return Rails.env.to_s if defined?(Rails) && Rails.respond_to?(:env)
|
|
50
|
+
|
|
51
|
+
if PgSqlTriggers.respond_to?(:default_environment) && PgSqlTriggers.default_environment.respond_to?(:call)
|
|
52
|
+
begin
|
|
53
|
+
return PgSqlTriggers.default_environment.call.to_s
|
|
54
|
+
rescue NameError, NoMethodError => e # rubocop:disable Lint/ShadowedException
|
|
55
|
+
logger&.debug "[KILL_SWITCH] Could not resolve default_environment: #{e.message}"
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "development"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def confirmation_required?
|
|
63
|
+
return true unless PgSqlTriggers.respond_to?(:kill_switch_confirmation_required)
|
|
64
|
+
|
|
65
|
+
value = PgSqlTriggers.kill_switch_confirmation_required
|
|
66
|
+
value.nil? || value
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def expected_confirmation(operation)
|
|
70
|
+
if PgSqlTriggers.respond_to?(:kill_switch_confirmation_pattern) &&
|
|
71
|
+
PgSqlTriggers.kill_switch_confirmation_pattern.respond_to?(:call)
|
|
72
|
+
PgSqlTriggers.kill_switch_confirmation_pattern.call(operation)
|
|
73
|
+
else
|
|
74
|
+
"EXECUTE #{operation.to_s.upcase}"
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def logger
|
|
79
|
+
if PgSqlTriggers.respond_to?(:kill_switch_logger)
|
|
80
|
+
PgSqlTriggers.kill_switch_logger
|
|
81
|
+
elsif defined?(Rails) && Rails.respond_to?(:logger)
|
|
82
|
+
Rails.logger
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def log(level, status, **context)
|
|
87
|
+
actor = context[:actor]
|
|
88
|
+
actor_str = if actor.is_a?(Hash)
|
|
89
|
+
"#{actor[:type] || 'unknown'}:#{actor[:id] || 'unknown'}"
|
|
90
|
+
else
|
|
91
|
+
actor&.to_s || "unknown"
|
|
92
|
+
end
|
|
93
|
+
msg = "[KILL_SWITCH] #{status}: operation=#{context[:operation]} " \
|
|
94
|
+
"environment=#{context[:environment]} actor=#{actor_str}"
|
|
95
|
+
msg = "#{msg} #{context[:extra]}" if context[:extra]
|
|
96
|
+
logger&.public_send(level, msg)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
private_constant :KillSwitchHelpers
|
|
100
|
+
|
|
101
|
+
# KillSwitch: three-layer safety gate for dangerous operations.
|
|
25
102
|
#
|
|
26
|
-
#
|
|
27
|
-
#
|
|
103
|
+
# Layer 1 – config: PgSqlTriggers.kill_switch_enabled / kill_switch_environments
|
|
104
|
+
# Layer 2 – ENV: KILL_SWITCH_OVERRIDE=true (+ optional confirmation)
|
|
105
|
+
# Layer 3 – explicit: confirmation text passed directly to check!
|
|
28
106
|
#
|
|
29
|
-
#
|
|
107
|
+
# @example
|
|
108
|
+
# KillSwitch.check!(operation: :migrate_up, environment: Rails.env,
|
|
109
|
+
# confirmation: params[:confirmation_text],
|
|
110
|
+
# actor: { type: "UI", id: current_user.email })
|
|
30
111
|
module KillSwitch
|
|
31
|
-
# Thread-local storage key for override state
|
|
32
112
|
OVERRIDE_KEY = :pg_sql_triggers_kill_switch_override
|
|
33
113
|
|
|
34
114
|
class << self
|
|
35
|
-
|
|
36
|
-
#
|
|
37
|
-
# @param environment [String, Symbol, nil] The environment to check (defaults to current environment)
|
|
38
|
-
# @param operation [String, Symbol, nil] The operation being performed (for logging)
|
|
39
|
-
# @return [Boolean] true if kill switch is active, false otherwise
|
|
40
|
-
def active?(environment: nil, operation: nil)
|
|
41
|
-
# Check if kill switch is globally disabled
|
|
42
|
-
return false unless kill_switch_enabled?
|
|
43
|
-
|
|
44
|
-
# Detect environment
|
|
45
|
-
env = detect_environment(environment)
|
|
115
|
+
include KillSwitchHelpers
|
|
46
116
|
|
|
47
|
-
|
|
48
|
-
|
|
117
|
+
private :kill_switch_enabled?, :protected_environment?,
|
|
118
|
+
:resolve_environment, :confirmation_required?, :expected_confirmation, :logger, :log
|
|
49
119
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
logger.debug "[KILL_SWITCH] Check: operation=#{operation} environment=#{env} active=#{is_active}"
|
|
53
|
-
end
|
|
120
|
+
def active?(environment: nil, operation: nil)
|
|
121
|
+
return false unless kill_switch_enabled?
|
|
54
122
|
|
|
55
|
-
|
|
123
|
+
env = resolve_environment(environment)
|
|
124
|
+
active = protected_environment?(env)
|
|
125
|
+
logger&.debug "[KILL_SWITCH] Check: operation=#{operation} environment=#{env} active=#{active}" if operation
|
|
126
|
+
active
|
|
56
127
|
end
|
|
57
128
|
|
|
58
|
-
# Checks if an operation should be blocked by the kill switch.
|
|
59
|
-
# Raises KillSwitchError if the operation is blocked.
|
|
60
|
-
#
|
|
61
|
-
# @param operation [String, Symbol] The operation being performed
|
|
62
|
-
# @param environment [String, Symbol, nil] The environment (defaults to current)
|
|
63
|
-
# @param confirmation [String, nil] The confirmation text provided by the user
|
|
64
|
-
# @param actor [Hash, nil] Information about who is performing the operation
|
|
65
|
-
# @raise [PgSqlTriggers::KillSwitchError] if the operation is blocked
|
|
66
|
-
# @return [void]
|
|
67
129
|
def check!(operation:, environment: nil, confirmation: nil, actor: nil)
|
|
68
|
-
env =
|
|
130
|
+
env = resolve_environment(environment)
|
|
69
131
|
|
|
70
|
-
# Check if kill switch is active for this environment
|
|
71
132
|
unless active?(environment: env, operation: operation)
|
|
72
|
-
|
|
133
|
+
log(:info, "ALLOWED",
|
|
134
|
+
operation: operation,
|
|
135
|
+
environment: env,
|
|
136
|
+
actor: actor,
|
|
137
|
+
extra: "reason=not_protected_environment")
|
|
73
138
|
return
|
|
74
139
|
end
|
|
75
140
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
141
|
+
if Thread.current[OVERRIDE_KEY]
|
|
142
|
+
log(:warn, "OVERRIDDEN",
|
|
143
|
+
operation: operation,
|
|
144
|
+
environment: env,
|
|
145
|
+
actor: actor,
|
|
146
|
+
extra: "source=thread_local")
|
|
79
147
|
return
|
|
80
148
|
end
|
|
81
149
|
|
|
82
|
-
|
|
83
|
-
if env_override_active?
|
|
84
|
-
# If ENV override is present, check confirmation if required
|
|
150
|
+
if ENV["KILL_SWITCH_OVERRIDE"]&.downcase == "true"
|
|
85
151
|
if confirmation_required?
|
|
86
152
|
validate_confirmation!(confirmation, operation)
|
|
87
|
-
|
|
88
|
-
|
|
153
|
+
log(:warn, "OVERRIDDEN",
|
|
154
|
+
operation: operation,
|
|
155
|
+
environment: env,
|
|
156
|
+
actor: actor,
|
|
157
|
+
extra: "source=env_with_confirmation confirmation=#{confirmation}")
|
|
89
158
|
else
|
|
90
|
-
|
|
159
|
+
log(:warn, "OVERRIDDEN",
|
|
160
|
+
operation: operation,
|
|
161
|
+
environment: env,
|
|
162
|
+
actor: actor,
|
|
163
|
+
extra: "source=env_without_confirmation")
|
|
91
164
|
end
|
|
92
165
|
return
|
|
93
166
|
end
|
|
94
167
|
|
|
95
|
-
# If confirmation is provided, validate it
|
|
96
168
|
unless confirmation.nil?
|
|
97
169
|
validate_confirmation!(confirmation, operation)
|
|
98
|
-
|
|
99
|
-
|
|
170
|
+
log(:warn, "OVERRIDDEN",
|
|
171
|
+
operation: operation,
|
|
172
|
+
environment: env,
|
|
173
|
+
actor: actor,
|
|
174
|
+
extra: "source=explicit_confirmation confirmation=#{confirmation}")
|
|
100
175
|
return
|
|
101
176
|
end
|
|
102
177
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
178
|
+
log(:error, "BLOCKED", operation: operation, environment: env, actor: actor)
|
|
179
|
+
expected = expected_confirmation(operation)
|
|
180
|
+
raise PgSqlTriggers::KillSwitchError.new(
|
|
181
|
+
"Kill switch is active for #{env} environment. Operation '#{operation}' has been blocked.\n\n" \
|
|
182
|
+
"To override: KILL_SWITCH_OVERRIDE=true or provide confirmation text: #{expected}",
|
|
183
|
+
error_code: "KILL_SWITCH_ACTIVE",
|
|
184
|
+
recovery_suggestion: "Provide the confirmation text: #{expected}",
|
|
185
|
+
context: { operation: operation, environment: env, expected_confirmation: expected }
|
|
186
|
+
)
|
|
106
187
|
end
|
|
107
188
|
|
|
108
|
-
# Temporarily overrides the kill switch for the duration of the block.
|
|
109
|
-
# Uses thread-local storage to ensure thread safety.
|
|
110
|
-
#
|
|
111
|
-
# @param confirmation [String, nil] Optional confirmation text
|
|
112
|
-
# @yield The block to execute with kill switch overridden
|
|
113
|
-
# @return The return value of the block
|
|
114
189
|
def override(confirmation: nil)
|
|
115
190
|
raise ArgumentError, "Block required for kill switch override" unless block_given?
|
|
116
191
|
|
|
117
|
-
|
|
118
|
-
if confirmation.present? && confirmation_required?
|
|
119
|
-
# NOTE: We can't validate against a specific operation here since we don't know it
|
|
120
|
-
# The block itself will call check! with the operation, which will see the override
|
|
192
|
+
if confirmation.present?
|
|
121
193
|
logger&.info "[KILL_SWITCH] Override block initiated with confirmation: #{confirmation}"
|
|
122
194
|
end
|
|
123
|
-
|
|
124
|
-
# Set thread-local override
|
|
125
|
-
previous_value = Thread.current[OVERRIDE_KEY]
|
|
195
|
+
previous = Thread.current[OVERRIDE_KEY]
|
|
126
196
|
Thread.current[OVERRIDE_KEY] = true
|
|
127
|
-
|
|
128
197
|
begin
|
|
129
198
|
yield
|
|
130
199
|
ensure
|
|
131
|
-
|
|
132
|
-
Thread.current[OVERRIDE_KEY] = previous_value
|
|
133
|
-
end
|
|
134
|
-
end
|
|
135
|
-
|
|
136
|
-
# Validates the confirmation text against the expected pattern for the operation.
|
|
137
|
-
#
|
|
138
|
-
# @param confirmation [String, nil] The confirmation text to validate
|
|
139
|
-
# @param operation [String, Symbol] The operation being confirmed
|
|
140
|
-
# @raise [PgSqlTriggers::KillSwitchError] if confirmation is invalid
|
|
141
|
-
# @return [void]
|
|
142
|
-
def validate_confirmation!(confirmation, operation)
|
|
143
|
-
expected = expected_confirmation(operation)
|
|
144
|
-
|
|
145
|
-
if confirmation.nil? || confirmation.strip.empty?
|
|
146
|
-
raise PgSqlTriggers::KillSwitchError.new(
|
|
147
|
-
"Confirmation text required. Expected: '#{expected}'",
|
|
148
|
-
error_code: "KILL_SWITCH_CONFIRMATION_REQUIRED",
|
|
149
|
-
recovery_suggestion: "Provide the confirmation text: #{expected}",
|
|
150
|
-
context: { operation: operation, expected_confirmation: expected }
|
|
151
|
-
)
|
|
200
|
+
Thread.current[OVERRIDE_KEY] = previous
|
|
152
201
|
end
|
|
153
|
-
|
|
154
|
-
return if confirmation.strip == expected
|
|
155
|
-
|
|
156
|
-
raise PgSqlTriggers::KillSwitchError.new(
|
|
157
|
-
"Invalid confirmation text. Expected: '#{expected}', got: '#{confirmation.strip}'",
|
|
158
|
-
error_code: "KILL_SWITCH_CONFIRMATION_INVALID",
|
|
159
|
-
recovery_suggestion: "Use the exact confirmation text: #{expected}",
|
|
160
|
-
context: { operation: operation, expected_confirmation: expected,
|
|
161
|
-
provided_confirmation: confirmation.strip }
|
|
162
|
-
)
|
|
163
|
-
end
|
|
164
|
-
|
|
165
|
-
private
|
|
166
|
-
|
|
167
|
-
# Checks if kill switch is globally enabled in configuration
|
|
168
|
-
def kill_switch_enabled?
|
|
169
|
-
return true unless PgSqlTriggers.respond_to?(:kill_switch_enabled)
|
|
170
|
-
|
|
171
|
-
# Default to true (fail-safe) if not configured
|
|
172
|
-
value = PgSqlTriggers.kill_switch_enabled
|
|
173
|
-
value.nil? || value
|
|
174
|
-
end
|
|
175
|
-
|
|
176
|
-
# Checks if the given environment is protected by the kill switch
|
|
177
|
-
def protected_environment?(environment)
|
|
178
|
-
return false if environment.nil?
|
|
179
|
-
|
|
180
|
-
protected_envs = if PgSqlTriggers.respond_to?(:kill_switch_environments)
|
|
181
|
-
value = PgSqlTriggers.kill_switch_environments
|
|
182
|
-
value.nil? ? %i[production staging] : value
|
|
183
|
-
else
|
|
184
|
-
%i[production staging]
|
|
185
|
-
end
|
|
186
|
-
|
|
187
|
-
protected_envs = Array(protected_envs).map(&:to_s)
|
|
188
|
-
protected_envs.include?(environment.to_s)
|
|
189
|
-
end
|
|
190
|
-
|
|
191
|
-
# Detects the current environment
|
|
192
|
-
def detect_environment(environment)
|
|
193
|
-
return environment.to_s if environment.present?
|
|
194
|
-
|
|
195
|
-
# Try Rails environment
|
|
196
|
-
return Rails.env.to_s if defined?(Rails) && Rails.respond_to?(:env)
|
|
197
|
-
|
|
198
|
-
# Try PgSqlTriggers default_environment
|
|
199
|
-
if PgSqlTriggers.respond_to?(:default_environment) && PgSqlTriggers.default_environment.respond_to?(:call)
|
|
200
|
-
begin
|
|
201
|
-
return PgSqlTriggers.default_environment.call.to_s
|
|
202
|
-
rescue NameError, NoMethodError # rubocop:disable Lint/ShadowedException
|
|
203
|
-
# Fall through to next option if default_environment proc references undefined constants
|
|
204
|
-
end
|
|
205
|
-
end
|
|
206
|
-
|
|
207
|
-
# Fall back to RAILS_ENV or RACK_ENV
|
|
208
|
-
ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "development"
|
|
209
|
-
end
|
|
210
|
-
|
|
211
|
-
# Checks if thread-local override is active
|
|
212
|
-
def thread_override_active?
|
|
213
|
-
Thread.current[OVERRIDE_KEY] == true
|
|
214
|
-
end
|
|
215
|
-
|
|
216
|
-
# Checks if ENV override is active
|
|
217
|
-
def env_override_active?
|
|
218
|
-
ENV["KILL_SWITCH_OVERRIDE"]&.downcase == "true"
|
|
219
|
-
end
|
|
220
|
-
|
|
221
|
-
# Checks if confirmation is required for overrides
|
|
222
|
-
def confirmation_required?
|
|
223
|
-
return true unless PgSqlTriggers.respond_to?(:kill_switch_confirmation_required)
|
|
224
|
-
|
|
225
|
-
# Default to true (safer) if not configured
|
|
226
|
-
value = PgSqlTriggers.kill_switch_confirmation_required
|
|
227
|
-
value.nil? || value
|
|
228
|
-
end
|
|
229
|
-
|
|
230
|
-
# Generates the expected confirmation text for an operation
|
|
231
|
-
def expected_confirmation(operation)
|
|
232
|
-
if PgSqlTriggers.respond_to?(:kill_switch_confirmation_pattern) &&
|
|
233
|
-
PgSqlTriggers.kill_switch_confirmation_pattern.respond_to?(:call)
|
|
234
|
-
PgSqlTriggers.kill_switch_confirmation_pattern.call(operation)
|
|
235
|
-
else
|
|
236
|
-
# Default pattern
|
|
237
|
-
"EXECUTE #{operation.to_s.upcase}"
|
|
238
|
-
end
|
|
239
|
-
end
|
|
240
|
-
|
|
241
|
-
# Returns the configured logger
|
|
242
|
-
def logger
|
|
243
|
-
if PgSqlTriggers.respond_to?(:kill_switch_logger)
|
|
244
|
-
PgSqlTriggers.kill_switch_logger
|
|
245
|
-
elsif defined?(Rails) && Rails.respond_to?(:logger)
|
|
246
|
-
Rails.logger
|
|
247
|
-
end
|
|
248
|
-
end
|
|
249
|
-
|
|
250
|
-
# Logs an allowed operation
|
|
251
|
-
def log_allowed(operation:, environment:, actor:, reason:)
|
|
252
|
-
actor_info = format_actor(actor)
|
|
253
|
-
logger&.info "[KILL_SWITCH] ALLOWED: operation=#{operation} environment=#{environment} " \
|
|
254
|
-
"actor=#{actor_info} reason=#{reason}"
|
|
255
|
-
end
|
|
256
|
-
|
|
257
|
-
# Logs an overridden operation
|
|
258
|
-
def log_override(operation:, environment:, actor:, source:, confirmation: nil)
|
|
259
|
-
actor_info = format_actor(actor)
|
|
260
|
-
conf_info = confirmation ? " confirmation=#{confirmation}" : ""
|
|
261
|
-
logger&.warn "[KILL_SWITCH] OVERRIDDEN: operation=#{operation} environment=#{environment} " \
|
|
262
|
-
"actor=#{actor_info} source=#{source}#{conf_info}"
|
|
263
|
-
end
|
|
264
|
-
|
|
265
|
-
# Logs a blocked operation
|
|
266
|
-
def log_blocked(operation:, environment:, actor:)
|
|
267
|
-
actor_info = format_actor(actor)
|
|
268
|
-
logger&.error "[KILL_SWITCH] BLOCKED: operation=#{operation} environment=#{environment} actor=#{actor_info}"
|
|
269
|
-
end
|
|
270
|
-
|
|
271
|
-
# Formats actor information for logging
|
|
272
|
-
def format_actor(actor)
|
|
273
|
-
return "unknown" if actor.nil?
|
|
274
|
-
return actor.to_s unless actor.is_a?(Hash)
|
|
275
|
-
|
|
276
|
-
"#{actor[:type] || 'unknown'}:#{actor[:id] || 'unknown'}"
|
|
277
|
-
end
|
|
278
|
-
|
|
279
|
-
# Raises a kill switch error with helpful message
|
|
280
|
-
def raise_blocked_error(operation:, environment:)
|
|
281
|
-
expected = expected_confirmation(operation)
|
|
282
|
-
|
|
283
|
-
message = <<~ERROR
|
|
284
|
-
Kill switch is active for #{environment} environment.
|
|
285
|
-
Operation '#{operation}' has been blocked for safety.
|
|
286
|
-
|
|
287
|
-
To override this protection, you must provide confirmation.
|
|
288
|
-
|
|
289
|
-
For CLI/rake tasks, use:
|
|
290
|
-
KILL_SWITCH_OVERRIDE=true CONFIRMATION_TEXT="#{expected}" rake your:task
|
|
291
|
-
|
|
292
|
-
For console operations, use:
|
|
293
|
-
PgSqlTriggers::SQL::KillSwitch.override(confirmation: "#{expected}") do
|
|
294
|
-
# your dangerous operation here
|
|
295
|
-
end
|
|
296
|
-
|
|
297
|
-
For UI operations, enter the confirmation text: #{expected}
|
|
298
|
-
|
|
299
|
-
This protection prevents accidental destructive operations in production.
|
|
300
|
-
Make sure you understand the implications before proceeding.
|
|
301
|
-
ERROR
|
|
302
|
-
|
|
303
|
-
recovery = <<~RECOVERY
|
|
304
|
-
To override, provide the confirmation text: #{expected}
|
|
305
|
-
|
|
306
|
-
For CLI/rake tasks:
|
|
307
|
-
KILL_SWITCH_OVERRIDE=true CONFIRMATION_TEXT="#{expected}" rake your:task
|
|
308
|
-
|
|
309
|
-
For console operations:
|
|
310
|
-
PgSqlTriggers::SQL::KillSwitch.override(confirmation: "#{expected}") do
|
|
311
|
-
# your operation here
|
|
312
|
-
end
|
|
313
|
-
|
|
314
|
-
For UI operations, enter the confirmation text in the modal.
|
|
315
|
-
RECOVERY
|
|
316
|
-
|
|
317
|
-
raise PgSqlTriggers::KillSwitchError.new(
|
|
318
|
-
message,
|
|
319
|
-
error_code: "KILL_SWITCH_ACTIVE",
|
|
320
|
-
recovery_suggestion: recovery.strip,
|
|
321
|
-
context: { operation: operation, environment: environment, expected_confirmation: expected }
|
|
322
|
-
)
|
|
323
202
|
end
|
|
324
203
|
end
|
|
325
204
|
end
|
|
326
|
-
# rubocop:enable Metrics/ModuleLength
|
|
327
205
|
end
|
|
328
206
|
end
|
data/lib/pg_sql_triggers/sql.rb
CHANGED
|
@@ -2,14 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
module PgSqlTriggers
|
|
4
4
|
module SQL
|
|
5
|
-
autoload :Capsule, "pg_sql_triggers/sql/capsule"
|
|
6
|
-
autoload :Executor, "pg_sql_triggers/sql/executor"
|
|
7
5
|
autoload :KillSwitch, "pg_sql_triggers/sql/kill_switch"
|
|
8
6
|
|
|
9
|
-
def self.execute_capsule(capsule_name, **options)
|
|
10
|
-
Executor.execute_capsule(capsule_name, **options)
|
|
11
|
-
end
|
|
12
|
-
|
|
13
7
|
def self.kill_switch_active?
|
|
14
8
|
KillSwitch.active?
|
|
15
9
|
end
|
data/lib/pg_sql_triggers.rb
CHANGED
|
@@ -36,6 +36,10 @@ module PgSqlTriggers
|
|
|
36
36
|
mattr_accessor :allow_unsafe_migrations
|
|
37
37
|
self.allow_unsafe_migrations = false
|
|
38
38
|
|
|
39
|
+
# PostgreSQL schema used by DbQueries. Override for non-public schemas.
|
|
40
|
+
mattr_accessor :db_schema
|
|
41
|
+
self.db_schema = "public"
|
|
42
|
+
|
|
39
43
|
# Drift states
|
|
40
44
|
DRIFT_STATE_IN_SYNC = "in_sync"
|
|
41
45
|
DRIFT_STATE_DRIFTED = "drifted"
|
|
@@ -55,7 +59,6 @@ module PgSqlTriggers
|
|
|
55
59
|
autoload :Permissions, "pg_sql_triggers/permissions"
|
|
56
60
|
autoload :SQL, "pg_sql_triggers/sql"
|
|
57
61
|
autoload :DatabaseIntrospection, "pg_sql_triggers/database_introspection"
|
|
58
|
-
autoload :Generator, "pg_sql_triggers/generator"
|
|
59
62
|
autoload :Testing, "pg_sql_triggers/testing"
|
|
60
63
|
autoload :Migration, "pg_sql_triggers/migration"
|
|
61
64
|
autoload :Migrator, "pg_sql_triggers/migrator"
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "lib/pg_sql_triggers/version"
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = "pg_sql_triggers"
|
|
7
|
+
spec.version = PgSqlTriggers::VERSION
|
|
8
|
+
spec.authors = ["samaswin"]
|
|
9
|
+
spec.email = ["samaswin@gmail.com"]
|
|
10
|
+
|
|
11
|
+
spec.summary = "A PostgreSQL Trigger Control Plane for Rails"
|
|
12
|
+
spec.description = "Production-grade PostgreSQL trigger management for Rails with " \
|
|
13
|
+
"lifecycle management, safe deploys, versioning, drift detection, " \
|
|
14
|
+
"and a mountable UI."
|
|
15
|
+
spec.homepage = "https://github.com/samaswin/pg_sql_triggers"
|
|
16
|
+
spec.license = "MIT"
|
|
17
|
+
spec.required_ruby_version = ">= 3.0.0"
|
|
18
|
+
|
|
19
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
|
20
|
+
spec.metadata["source_code_uri"] = "https://github.com/samaswin/pg_sql_triggers"
|
|
21
|
+
spec.metadata["changelog_uri"] = "https://github.com/samaswin/pg_sql_triggers/blob/main/CHANGELOG.md"
|
|
22
|
+
spec.metadata["github_repo"] = "ssh://github.com/samaswin/pg_sql_triggers"
|
|
23
|
+
spec.metadata["rubygems_mfa_required"] = "true"
|
|
24
|
+
|
|
25
|
+
# Specify which files should be added to the gem when it is released.
|
|
26
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
|
27
|
+
spec.files = Dir.chdir(__dir__) do
|
|
28
|
+
`git ls-files -z`.split("\x0").reject do |f|
|
|
29
|
+
(File.expand_path(f) == __FILE__) ||
|
|
30
|
+
f.start_with?(*%w[bin/ test/ spec/ features/ .git appveyor Gemfile docs/screenshots/])
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
spec.bindir = "exe"
|
|
34
|
+
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
|
35
|
+
spec.require_paths = ["lib"]
|
|
36
|
+
|
|
37
|
+
# Runtime dependencies
|
|
38
|
+
spec.add_dependency "csv", ">= 3.0"
|
|
39
|
+
spec.add_dependency "pg", ">= 1.0"
|
|
40
|
+
spec.add_dependency "rails", ">= 6.1"
|
|
41
|
+
|
|
42
|
+
# Development dependencies
|
|
43
|
+
spec.add_development_dependency "database_cleaner-active_record", "~> 2.0"
|
|
44
|
+
spec.add_development_dependency "erb_lint", "~> 0.9"
|
|
45
|
+
spec.add_development_dependency "factory_bot_rails", "~> 6.0"
|
|
46
|
+
spec.add_development_dependency "rails-controller-testing", "~> 1.0"
|
|
47
|
+
spec.add_development_dependency "rspec", "~> 3.0"
|
|
48
|
+
spec.add_development_dependency "rspec-rails", "~> 6.0"
|
|
49
|
+
spec.add_development_dependency "rubocop", "~> 1.50"
|
|
50
|
+
spec.add_development_dependency "rubocop-rails", "~> 2.19"
|
|
51
|
+
spec.add_development_dependency "rubocop-rspec", "~> 2.20"
|
|
52
|
+
spec.add_development_dependency "simplecov", "~> 0.21"
|
|
53
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: pg_sql_triggers
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- samaswin
|
|
@@ -204,6 +204,7 @@ files:
|
|
|
204
204
|
- ".rubocop.yml"
|
|
205
205
|
- CHANGELOG.md
|
|
206
206
|
- COVERAGE.md
|
|
207
|
+
- GEM_ANALYSIS.md
|
|
207
208
|
- Goal.md
|
|
208
209
|
- LICENSE
|
|
209
210
|
- README.md
|
|
@@ -218,9 +219,7 @@ files:
|
|
|
218
219
|
- app/controllers/pg_sql_triggers/application_controller.rb
|
|
219
220
|
- app/controllers/pg_sql_triggers/audit_logs_controller.rb
|
|
220
221
|
- app/controllers/pg_sql_triggers/dashboard_controller.rb
|
|
221
|
-
- app/controllers/pg_sql_triggers/generator_controller.rb
|
|
222
222
|
- app/controllers/pg_sql_triggers/migrations_controller.rb
|
|
223
|
-
- app/controllers/pg_sql_triggers/sql_capsules_controller.rb
|
|
224
223
|
- app/controllers/pg_sql_triggers/tables_controller.rb
|
|
225
224
|
- app/controllers/pg_sql_triggers/triggers_controller.rb
|
|
226
225
|
- app/helpers/pg_sql_triggers/permissions_helper.rb
|
|
@@ -230,12 +229,8 @@ files:
|
|
|
230
229
|
- app/views/layouts/pg_sql_triggers/application.html.erb
|
|
231
230
|
- app/views/pg_sql_triggers/audit_logs/index.html.erb
|
|
232
231
|
- app/views/pg_sql_triggers/dashboard/index.html.erb
|
|
233
|
-
- app/views/pg_sql_triggers/generator/new.html.erb
|
|
234
|
-
- app/views/pg_sql_triggers/generator/preview.html.erb
|
|
235
232
|
- app/views/pg_sql_triggers/shared/_confirmation_modal.html.erb
|
|
236
233
|
- app/views/pg_sql_triggers/shared/_kill_switch_status.html.erb
|
|
237
|
-
- app/views/pg_sql_triggers/sql_capsules/new.html.erb
|
|
238
|
-
- app/views/pg_sql_triggers/sql_capsules/show.html.erb
|
|
239
234
|
- app/views/pg_sql_triggers/tables/index.html.erb
|
|
240
235
|
- app/views/pg_sql_triggers/tables/show.html.erb
|
|
241
236
|
- app/views/pg_sql_triggers/triggers/_drop_modal.html.erb
|
|
@@ -246,6 +241,7 @@ files:
|
|
|
246
241
|
- db/migrate/20251222000001_create_pg_sql_triggers_tables.rb
|
|
247
242
|
- db/migrate/20251229071916_add_timing_to_pg_sql_triggers_registry.rb
|
|
248
243
|
- db/migrate/20260103000001_create_pg_sql_triggers_audit_log.rb
|
|
244
|
+
- db/migrate/20260228000001_add_for_each_to_pg_sql_triggers_registry.rb
|
|
249
245
|
- docs/README.md
|
|
250
246
|
- docs/api-reference.md
|
|
251
247
|
- docs/audit-trail.md
|
|
@@ -261,9 +257,11 @@ files:
|
|
|
261
257
|
- lib/generators/pg_sql_triggers/templates/README
|
|
262
258
|
- lib/generators/pg_sql_triggers/templates/create_pg_sql_triggers_tables.rb
|
|
263
259
|
- lib/generators/pg_sql_triggers/templates/initializer.rb
|
|
260
|
+
- lib/generators/pg_sql_triggers/templates/trigger_dsl.rb.tt
|
|
264
261
|
- lib/generators/pg_sql_triggers/templates/trigger_migration.rb.erb
|
|
262
|
+
- lib/generators/pg_sql_triggers/templates/trigger_migration_full.rb.tt
|
|
263
|
+
- lib/generators/pg_sql_triggers/trigger_generator.rb
|
|
265
264
|
- lib/generators/pg_sql_triggers/trigger_migration_generator.rb
|
|
266
|
-
- lib/generators/trigger/migration_generator.rb
|
|
267
265
|
- lib/pg_sql_triggers.rb
|
|
268
266
|
- lib/pg_sql_triggers/database_introspection.rb
|
|
269
267
|
- lib/pg_sql_triggers/drift.rb
|
|
@@ -274,9 +272,6 @@ files:
|
|
|
274
272
|
- lib/pg_sql_triggers/dsl/trigger_definition.rb
|
|
275
273
|
- lib/pg_sql_triggers/engine.rb
|
|
276
274
|
- lib/pg_sql_triggers/errors.rb
|
|
277
|
-
- lib/pg_sql_triggers/generator.rb
|
|
278
|
-
- lib/pg_sql_triggers/generator/form.rb
|
|
279
|
-
- lib/pg_sql_triggers/generator/service.rb
|
|
280
275
|
- lib/pg_sql_triggers/migration.rb
|
|
281
276
|
- lib/pg_sql_triggers/migrator.rb
|
|
282
277
|
- lib/pg_sql_triggers/migrator/pre_apply_comparator.rb
|
|
@@ -288,8 +283,6 @@ files:
|
|
|
288
283
|
- lib/pg_sql_triggers/registry/manager.rb
|
|
289
284
|
- lib/pg_sql_triggers/registry/validator.rb
|
|
290
285
|
- lib/pg_sql_triggers/sql.rb
|
|
291
|
-
- lib/pg_sql_triggers/sql/capsule.rb
|
|
292
|
-
- lib/pg_sql_triggers/sql/executor.rb
|
|
293
286
|
- lib/pg_sql_triggers/sql/kill_switch.rb
|
|
294
287
|
- lib/pg_sql_triggers/testing.rb
|
|
295
288
|
- lib/pg_sql_triggers/testing/dry_run.rb
|
|
@@ -298,6 +291,7 @@ files:
|
|
|
298
291
|
- lib/pg_sql_triggers/testing/syntax_validator.rb
|
|
299
292
|
- lib/pg_sql_triggers/version.rb
|
|
300
293
|
- lib/tasks/trigger_migrations.rake
|
|
294
|
+
- pg_sql_triggers.gemspec
|
|
301
295
|
- scripts/generate_coverage_report.rb
|
|
302
296
|
- sig/pg_sql_triggers.rbs
|
|
303
297
|
homepage: https://github.com/samaswin/pg_sql_triggers
|