pg_sql_triggers 1.1.0 → 1.2.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/.rubocop.yml +15 -0
- data/CHANGELOG.md +61 -0
- data/COVERAGE.md +32 -28
- data/README.md +31 -2
- data/RELEASE.md +1 -1
- data/app/controllers/pg_sql_triggers/application_controller.rb +1 -1
- data/app/controllers/pg_sql_triggers/dashboard_controller.rb +10 -0
- data/app/controllers/pg_sql_triggers/migrations_controller.rb +62 -10
- data/app/controllers/pg_sql_triggers/sql_capsules_controller.rb +161 -0
- data/app/controllers/pg_sql_triggers/triggers_controller.rb +165 -0
- data/app/models/pg_sql_triggers/trigger_registry.rb +123 -0
- data/app/views/pg_sql_triggers/dashboard/index.html.erb +40 -2
- data/app/views/pg_sql_triggers/sql_capsules/new.html.erb +81 -0
- data/app/views/pg_sql_triggers/sql_capsules/show.html.erb +85 -0
- data/app/views/pg_sql_triggers/tables/show.html.erb +36 -2
- data/app/views/pg_sql_triggers/triggers/_drop_modal.html.erb +129 -0
- data/app/views/pg_sql_triggers/triggers/_re_execute_modal.html.erb +136 -0
- data/app/views/pg_sql_triggers/triggers/show.html.erb +186 -0
- data/config/routes.rb +9 -0
- data/docs/README.md +2 -2
- data/docs/api-reference.md +252 -4
- data/docs/getting-started.md +1 -1
- data/docs/kill-switch.md +3 -3
- data/docs/web-ui.md +82 -17
- data/lib/generators/pg_sql_triggers/templates/README +1 -1
- data/lib/pg_sql_triggers/registry/manager.rb +28 -13
- data/lib/pg_sql_triggers/registry.rb +41 -0
- data/lib/pg_sql_triggers/sql/capsule.rb +79 -0
- data/lib/pg_sql_triggers/sql/executor.rb +200 -0
- data/lib/pg_sql_triggers/version.rb +1 -1
- metadata +18 -12
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PgSqlTriggers
|
|
4
|
+
module SQL
|
|
5
|
+
# Executor handles the execution of SQL capsules with safety checks and logging
|
|
6
|
+
#
|
|
7
|
+
# @example Execute a SQL capsule
|
|
8
|
+
# capsule = PgSqlTriggers::SQL::Capsule.new(name: "fix", ...)
|
|
9
|
+
# result = PgSqlTriggers::SQL::Executor.execute(capsule, actor: current_actor, confirmation: "EXECUTE FIX")
|
|
10
|
+
#
|
|
11
|
+
class Executor
|
|
12
|
+
class << self
|
|
13
|
+
# Executes a SQL capsule with safety checks
|
|
14
|
+
#
|
|
15
|
+
# @param capsule [Capsule] The SQL capsule to execute
|
|
16
|
+
# @param actor [Hash] Information about who is executing the capsule
|
|
17
|
+
# @param confirmation [String, nil] The confirmation text for kill switch
|
|
18
|
+
# @param dry_run [Boolean] If true, only validate without executing
|
|
19
|
+
# @return [Hash] Result of the execution with :success, :message, and :data keys
|
|
20
|
+
def execute(capsule, actor:, confirmation: nil, dry_run: false)
|
|
21
|
+
validate_capsule!(capsule)
|
|
22
|
+
|
|
23
|
+
# Check permissions
|
|
24
|
+
check_permissions!(actor)
|
|
25
|
+
|
|
26
|
+
# Check kill switch
|
|
27
|
+
check_kill_switch!(capsule, actor, confirmation)
|
|
28
|
+
|
|
29
|
+
# Log the execution attempt
|
|
30
|
+
log_execution_attempt(capsule, actor, dry_run)
|
|
31
|
+
|
|
32
|
+
if dry_run
|
|
33
|
+
return {
|
|
34
|
+
success: true,
|
|
35
|
+
message: "Dry run successful. SQL would be executed.",
|
|
36
|
+
data: { checksum: capsule.checksum }
|
|
37
|
+
}
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Execute in transaction
|
|
41
|
+
result = execute_in_transaction(capsule, actor)
|
|
42
|
+
|
|
43
|
+
# Update registry after successful execution
|
|
44
|
+
update_registry(capsule) if result[:success]
|
|
45
|
+
|
|
46
|
+
# Log the result
|
|
47
|
+
log_execution_result(capsule, actor, result)
|
|
48
|
+
|
|
49
|
+
result
|
|
50
|
+
rescue StandardError => e
|
|
51
|
+
log_execution_error(capsule, actor, e)
|
|
52
|
+
{
|
|
53
|
+
success: false,
|
|
54
|
+
message: "Execution failed: #{e.message}",
|
|
55
|
+
error: e
|
|
56
|
+
}
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Executes a SQL capsule by name from the registry
|
|
60
|
+
#
|
|
61
|
+
# @param capsule_name [String] The name of the capsule to execute
|
|
62
|
+
# @param actor [Hash] Information about who is executing the capsule
|
|
63
|
+
# @param confirmation [String, nil] The confirmation text for kill switch
|
|
64
|
+
# @param dry_run [Boolean] If true, only validate without executing
|
|
65
|
+
# @return [Hash] Result of the execution
|
|
66
|
+
def execute_capsule(capsule_name, actor:, confirmation: nil, dry_run: false)
|
|
67
|
+
capsule = load_capsule_from_registry(capsule_name)
|
|
68
|
+
|
|
69
|
+
unless capsule
|
|
70
|
+
return {
|
|
71
|
+
success: false,
|
|
72
|
+
message: "Capsule '#{capsule_name}' not found in registry"
|
|
73
|
+
}
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
execute(capsule, actor: actor, confirmation: confirmation, dry_run: dry_run)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
private
|
|
80
|
+
|
|
81
|
+
def validate_capsule!(capsule)
|
|
82
|
+
raise ArgumentError, "Capsule must be a PgSqlTriggers::SQL::Capsule" unless capsule.is_a?(Capsule)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def check_permissions!(actor)
|
|
86
|
+
PgSqlTriggers::Permissions.check!(actor, :execute_sql)
|
|
87
|
+
rescue PgSqlTriggers::PermissionError => e
|
|
88
|
+
raise PgSqlTriggers::PermissionError,
|
|
89
|
+
"SQL capsule execution requires Admin role: #{e.message}"
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def check_kill_switch!(_capsule, actor, confirmation)
|
|
93
|
+
PgSqlTriggers::SQL::KillSwitch.check!(
|
|
94
|
+
operation: :execute_sql_capsule,
|
|
95
|
+
environment: Rails.env,
|
|
96
|
+
confirmation: confirmation,
|
|
97
|
+
actor: actor
|
|
98
|
+
)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def execute_in_transaction(capsule, _actor)
|
|
102
|
+
ActiveRecord::Base.transaction do
|
|
103
|
+
result = ActiveRecord::Base.connection.execute(capsule.sql)
|
|
104
|
+
|
|
105
|
+
{
|
|
106
|
+
success: true,
|
|
107
|
+
message: "SQL capsule '#{capsule.name}' executed successfully",
|
|
108
|
+
data: {
|
|
109
|
+
checksum: capsule.checksum,
|
|
110
|
+
rows_affected: result.cmd_tuples || 0
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def update_registry(capsule)
|
|
117
|
+
# Check if capsule already exists in registry
|
|
118
|
+
registry_entry = PgSqlTriggers::TriggerRegistry.find_or_initialize_by(
|
|
119
|
+
trigger_name: capsule.registry_trigger_name
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
registry_entry.assign_attributes(
|
|
123
|
+
table_name: "manual_sql_execution",
|
|
124
|
+
version: Time.current.to_i,
|
|
125
|
+
checksum: capsule.checksum,
|
|
126
|
+
source: "manual_sql",
|
|
127
|
+
function_body: capsule.sql,
|
|
128
|
+
condition: capsule.purpose,
|
|
129
|
+
environment: capsule.environment,
|
|
130
|
+
enabled: true,
|
|
131
|
+
last_executed_at: Time.current
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
registry_entry.save!
|
|
135
|
+
rescue StandardError => e
|
|
136
|
+
logger&.error "[SQL_CAPSULE] Failed to update registry: #{e.message}"
|
|
137
|
+
# Don't fail the execution if registry update fails
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def load_capsule_from_registry(capsule_name)
|
|
141
|
+
trigger_name = "sql_capsule_#{capsule_name}"
|
|
142
|
+
registry_entry = PgSqlTriggers::TriggerRegistry.find_by(
|
|
143
|
+
trigger_name: trigger_name,
|
|
144
|
+
source: "manual_sql"
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
return nil unless registry_entry
|
|
148
|
+
|
|
149
|
+
Capsule.new(
|
|
150
|
+
name: capsule_name,
|
|
151
|
+
environment: registry_entry.environment || Rails.env.to_s,
|
|
152
|
+
purpose: registry_entry.condition || "No purpose specified",
|
|
153
|
+
sql: registry_entry.function_body,
|
|
154
|
+
created_at: registry_entry.created_at
|
|
155
|
+
)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Logging methods
|
|
159
|
+
|
|
160
|
+
def log_execution_attempt(capsule, actor, dry_run)
|
|
161
|
+
mode = dry_run ? "DRY_RUN" : "EXECUTE"
|
|
162
|
+
logger&.info "[SQL_CAPSULE] #{mode} ATTEMPT: name=#{capsule.name} " \
|
|
163
|
+
"environment=#{capsule.environment} actor=#{format_actor(actor)}"
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def log_execution_result(capsule, actor, result)
|
|
167
|
+
status = result[:success] ? "SUCCESS" : "FAILED"
|
|
168
|
+
logger&.info "[SQL_CAPSULE] #{status}: name=#{capsule.name} " \
|
|
169
|
+
"environment=#{capsule.environment} actor=#{format_actor(actor)} " \
|
|
170
|
+
"checksum=#{capsule.checksum}"
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def log_execution_error(capsule, actor, error)
|
|
174
|
+
# Handle case where capsule might not be valid
|
|
175
|
+
capsule_name = capsule.respond_to?(:name) ? capsule.name : "invalid_capsule"
|
|
176
|
+
environment = capsule.respond_to?(:environment) ? capsule.environment : "unknown"
|
|
177
|
+
|
|
178
|
+
logger&.error "[SQL_CAPSULE] ERROR: name=#{capsule_name} " \
|
|
179
|
+
"environment=#{environment} actor=#{format_actor(actor)} " \
|
|
180
|
+
"error=#{error.class.name} message=#{error.message}"
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def format_actor(actor)
|
|
184
|
+
return "unknown" if actor.nil?
|
|
185
|
+
return actor.to_s unless actor.is_a?(Hash)
|
|
186
|
+
|
|
187
|
+
"#{actor[:type] || 'unknown'}:#{actor[:id] || 'unknown'}"
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def logger
|
|
191
|
+
if PgSqlTriggers.respond_to?(:logger) && PgSqlTriggers.logger
|
|
192
|
+
PgSqlTriggers.logger
|
|
193
|
+
elsif defined?(Rails) && Rails.respond_to?(:logger)
|
|
194
|
+
Rails.logger
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
end
|
metadata
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
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.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
|
-
-
|
|
8
|
-
autorequire:
|
|
7
|
+
- samaswin
|
|
9
8
|
bindir: exe
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
13
12
|
- !ruby/object:Gem::Dependency
|
|
14
13
|
name: pg
|
|
@@ -202,7 +201,9 @@ files:
|
|
|
202
201
|
- app/controllers/pg_sql_triggers/dashboard_controller.rb
|
|
203
202
|
- app/controllers/pg_sql_triggers/generator_controller.rb
|
|
204
203
|
- app/controllers/pg_sql_triggers/migrations_controller.rb
|
|
204
|
+
- app/controllers/pg_sql_triggers/sql_capsules_controller.rb
|
|
205
205
|
- app/controllers/pg_sql_triggers/tables_controller.rb
|
|
206
|
+
- app/controllers/pg_sql_triggers/triggers_controller.rb
|
|
206
207
|
- app/models/pg_sql_triggers/application_record.rb
|
|
207
208
|
- app/models/pg_sql_triggers/trigger_registry.rb
|
|
208
209
|
- app/views/layouts/pg_sql_triggers/application.html.erb
|
|
@@ -211,8 +212,13 @@ files:
|
|
|
211
212
|
- app/views/pg_sql_triggers/generator/preview.html.erb
|
|
212
213
|
- app/views/pg_sql_triggers/shared/_confirmation_modal.html.erb
|
|
213
214
|
- app/views/pg_sql_triggers/shared/_kill_switch_status.html.erb
|
|
215
|
+
- app/views/pg_sql_triggers/sql_capsules/new.html.erb
|
|
216
|
+
- app/views/pg_sql_triggers/sql_capsules/show.html.erb
|
|
214
217
|
- app/views/pg_sql_triggers/tables/index.html.erb
|
|
215
218
|
- app/views/pg_sql_triggers/tables/show.html.erb
|
|
219
|
+
- app/views/pg_sql_triggers/triggers/_drop_modal.html.erb
|
|
220
|
+
- app/views/pg_sql_triggers/triggers/_re_execute_modal.html.erb
|
|
221
|
+
- app/views/pg_sql_triggers/triggers/show.html.erb
|
|
216
222
|
- config/initializers/pg_sql_triggers.rb
|
|
217
223
|
- config/routes.rb
|
|
218
224
|
- db/migrate/20251222000001_create_pg_sql_triggers_tables.rb
|
|
@@ -259,6 +265,8 @@ files:
|
|
|
259
265
|
- lib/pg_sql_triggers/registry/manager.rb
|
|
260
266
|
- lib/pg_sql_triggers/registry/validator.rb
|
|
261
267
|
- lib/pg_sql_triggers/sql.rb
|
|
268
|
+
- lib/pg_sql_triggers/sql/capsule.rb
|
|
269
|
+
- lib/pg_sql_triggers/sql/executor.rb
|
|
262
270
|
- lib/pg_sql_triggers/sql/kill_switch.rb
|
|
263
271
|
- lib/pg_sql_triggers/testing.rb
|
|
264
272
|
- lib/pg_sql_triggers/testing/dry_run.rb
|
|
@@ -269,16 +277,15 @@ files:
|
|
|
269
277
|
- lib/tasks/trigger_migrations.rake
|
|
270
278
|
- scripts/generate_coverage_report.rb
|
|
271
279
|
- sig/pg_sql_triggers.rbs
|
|
272
|
-
homepage: https://github.com/
|
|
280
|
+
homepage: https://github.com/samaswin/pg_sql_triggers
|
|
273
281
|
licenses:
|
|
274
282
|
- MIT
|
|
275
283
|
metadata:
|
|
276
|
-
homepage_uri: https://github.com/
|
|
277
|
-
source_code_uri: https://github.com/
|
|
278
|
-
changelog_uri: https://github.com/
|
|
279
|
-
github_repo: ssh://github.com/
|
|
284
|
+
homepage_uri: https://github.com/samaswin/pg_sql_triggers
|
|
285
|
+
source_code_uri: https://github.com/samaswin/pg_sql_triggers
|
|
286
|
+
changelog_uri: https://github.com/samaswin/pg_sql_triggers/blob/main/CHANGELOG.md
|
|
287
|
+
github_repo: ssh://github.com/samaswin/pg_sql_triggers
|
|
280
288
|
rubygems_mfa_required: 'true'
|
|
281
|
-
post_install_message:
|
|
282
289
|
rdoc_options: []
|
|
283
290
|
require_paths:
|
|
284
291
|
- lib
|
|
@@ -293,8 +300,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
293
300
|
- !ruby/object:Gem::Version
|
|
294
301
|
version: '0'
|
|
295
302
|
requirements: []
|
|
296
|
-
rubygems_version:
|
|
297
|
-
signing_key:
|
|
303
|
+
rubygems_version: 4.0.3
|
|
298
304
|
specification_version: 4
|
|
299
305
|
summary: A PostgreSQL Trigger Control Plane for Rails
|
|
300
306
|
test_files: []
|