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
|
@@ -1,213 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module PgSqlTriggers
|
|
4
|
-
class GeneratorController < ApplicationController
|
|
5
|
-
# Permissions: Require OPERATOR level for generation
|
|
6
|
-
before_action :check_operator_permission
|
|
7
|
-
|
|
8
|
-
# GET /generator/new
|
|
9
|
-
# Display the multi-step form wizard
|
|
10
|
-
def new
|
|
11
|
-
# Restore form data from session if available (when user clicks "Back to Edit")
|
|
12
|
-
session_data = session[:generator_form_data]
|
|
13
|
-
if session_data
|
|
14
|
-
# Convert to hash and symbolize keys for Form initialization
|
|
15
|
-
form_data = session_data.is_a?(Hash) ? session_data.symbolize_keys : session_data.to_h.symbolize_keys
|
|
16
|
-
@form = PgSqlTriggers::Generator::Form.new(form_data)
|
|
17
|
-
session.delete(:generator_form_data)
|
|
18
|
-
else
|
|
19
|
-
@form = PgSqlTriggers::Generator::Form.new
|
|
20
|
-
end
|
|
21
|
-
@available_tables = fetch_available_tables
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
# POST /generator/preview
|
|
25
|
-
# Preview generated DSL and function stub (AJAX or regular POST)
|
|
26
|
-
def preview
|
|
27
|
-
@form = PgSqlTriggers::Generator::Form.new(generator_params)
|
|
28
|
-
|
|
29
|
-
# If user clicked "Back to Edit", store form data in session and redirect
|
|
30
|
-
if params[:back_to_edit].present?
|
|
31
|
-
# Store form data as a hash (convert ActionController::Parameters to hash)
|
|
32
|
-
session[:generator_form_data] = generator_params.to_h
|
|
33
|
-
redirect_to new_generator_path
|
|
34
|
-
return
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
if @form.valid?
|
|
38
|
-
# Store form data in session so it can be restored if user clicks "Back to Edit"
|
|
39
|
-
# Convert ActionController::Parameters to hash
|
|
40
|
-
session[:generator_form_data] = generator_params.to_h
|
|
41
|
-
|
|
42
|
-
# Validate SQL function body (required field)
|
|
43
|
-
@sql_validation = validate_function_sql(@form)
|
|
44
|
-
|
|
45
|
-
@dsl_content = PgSqlTriggers::Generator::Service.generate_dsl(@form)
|
|
46
|
-
# Use function_body (required)
|
|
47
|
-
@function_content = @form.function_body
|
|
48
|
-
@file_paths = PgSqlTriggers::Generator::Service.file_paths(@form)
|
|
49
|
-
|
|
50
|
-
render :preview
|
|
51
|
-
else
|
|
52
|
-
@available_tables = fetch_available_tables
|
|
53
|
-
render :new
|
|
54
|
-
end
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
# POST /generator/create
|
|
58
|
-
# Actually create the files and register in TriggerRegistry
|
|
59
|
-
def create
|
|
60
|
-
# Check kill switch before generating trigger
|
|
61
|
-
check_kill_switch(operation: :ui_trigger_generate, confirmation: params[:confirmation_text])
|
|
62
|
-
|
|
63
|
-
@form = PgSqlTriggers::Generator::Form.new(generator_params)
|
|
64
|
-
|
|
65
|
-
if @form.valid?
|
|
66
|
-
# Validate SQL function body (required field)
|
|
67
|
-
sql_validation = validate_function_sql(@form)
|
|
68
|
-
unless sql_validation[:valid]
|
|
69
|
-
flash.now[:alert] = "Cannot create trigger: SQL validation failed - #{sql_validation[:error]}"
|
|
70
|
-
@available_tables = fetch_available_tables
|
|
71
|
-
@dsl_content = PgSqlTriggers::Generator::Service.generate_dsl(@form)
|
|
72
|
-
@function_content = @form.function_body
|
|
73
|
-
@file_paths = PgSqlTriggers::Generator::Service.file_paths(@form)
|
|
74
|
-
@sql_validation = sql_validation
|
|
75
|
-
render :preview
|
|
76
|
-
return
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
result = PgSqlTriggers::Generator::Service.create_trigger(@form, actor: current_actor)
|
|
80
|
-
|
|
81
|
-
if result[:success]
|
|
82
|
-
# Clear session data after successful creation
|
|
83
|
-
session.delete(:generator_form_data)
|
|
84
|
-
files_msg = "Migration: #{result[:migration_path]}, DSL: #{result[:dsl_path]}"
|
|
85
|
-
redirect_to root_path,
|
|
86
|
-
notice: "Trigger generated successfully. Files created: #{files_msg}"
|
|
87
|
-
else
|
|
88
|
-
flash[:alert] = "Generation failed: #{result[:error]}"
|
|
89
|
-
@available_tables = fetch_available_tables
|
|
90
|
-
render :new
|
|
91
|
-
end
|
|
92
|
-
else
|
|
93
|
-
@available_tables = fetch_available_tables
|
|
94
|
-
render :new
|
|
95
|
-
end
|
|
96
|
-
rescue PgSqlTriggers::KillSwitchError => e
|
|
97
|
-
flash[:error] = e.message
|
|
98
|
-
redirect_to root_path
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
# POST /generator/validate_table (AJAX)
|
|
102
|
-
# Validate that table exists in database
|
|
103
|
-
def validate_table
|
|
104
|
-
# Extract table_name from JSON request body
|
|
105
|
-
# Rails should parse JSON automatically, but handle both cases
|
|
106
|
-
table_name = extract_table_name_from_request
|
|
107
|
-
|
|
108
|
-
if table_name.blank?
|
|
109
|
-
render json: { valid: false, error: "Table name is required" }, status: :bad_request
|
|
110
|
-
return
|
|
111
|
-
end
|
|
112
|
-
|
|
113
|
-
validator = PgSqlTriggers::DatabaseIntrospection.new
|
|
114
|
-
result = validator.validate_table(table_name)
|
|
115
|
-
render json: result
|
|
116
|
-
end
|
|
117
|
-
|
|
118
|
-
# GET /generator/tables (AJAX)
|
|
119
|
-
# Fetch list of tables for dropdown
|
|
120
|
-
def tables
|
|
121
|
-
tables = fetch_available_tables
|
|
122
|
-
render json: { tables: tables }
|
|
123
|
-
end
|
|
124
|
-
|
|
125
|
-
private
|
|
126
|
-
|
|
127
|
-
def generator_params
|
|
128
|
-
params.require(:pg_sql_triggers_generator_form).permit(
|
|
129
|
-
:trigger_name, :table_name, :function_name, :version,
|
|
130
|
-
:enabled, :condition, :timing, :generate_function_stub, :function_body,
|
|
131
|
-
events: [], environments: []
|
|
132
|
-
)
|
|
133
|
-
end
|
|
134
|
-
|
|
135
|
-
def check_operator_permission
|
|
136
|
-
return if PgSqlTriggers::Permissions.can?(current_actor, :apply_trigger)
|
|
137
|
-
|
|
138
|
-
redirect_to root_path, alert: "Insufficient permissions. Operator role required."
|
|
139
|
-
end
|
|
140
|
-
|
|
141
|
-
def fetch_available_tables
|
|
142
|
-
PgSqlTriggers::DatabaseIntrospection.new.list_tables
|
|
143
|
-
rescue StandardError => e
|
|
144
|
-
Rails.logger.error("Failed to fetch tables: #{e.message}")
|
|
145
|
-
[]
|
|
146
|
-
end
|
|
147
|
-
|
|
148
|
-
def extract_table_name_from_request
|
|
149
|
-
# Rails automatically parses JSON request bodies when Content-Type is application/json
|
|
150
|
-
# The parameters are available directly in params
|
|
151
|
-
table_name = params[:table_name]
|
|
152
|
-
|
|
153
|
-
# If not found, try accessing as string key (some Rails versions use string keys for JSON)
|
|
154
|
-
table_name ||= params["table_name"] if params.key?("table_name")
|
|
155
|
-
|
|
156
|
-
table_name
|
|
157
|
-
end
|
|
158
|
-
|
|
159
|
-
def validate_function_sql(form)
|
|
160
|
-
return nil if form.function_body.blank?
|
|
161
|
-
|
|
162
|
-
# Create a temporary trigger registry object for validation
|
|
163
|
-
# Only include condition if the column exists
|
|
164
|
-
registry_attributes = {
|
|
165
|
-
trigger_name: form.trigger_name,
|
|
166
|
-
table_name: form.table_name,
|
|
167
|
-
function_body: form.function_body
|
|
168
|
-
}
|
|
169
|
-
# Only set condition if the column exists in the database
|
|
170
|
-
if PgSqlTriggers::TriggerRegistry.column_names.include?("condition")
|
|
171
|
-
registry_attributes[:condition] = form.condition
|
|
172
|
-
end
|
|
173
|
-
|
|
174
|
-
temp_registry = PgSqlTriggers::TriggerRegistry.new(registry_attributes)
|
|
175
|
-
|
|
176
|
-
# Build definition JSON for condition validation
|
|
177
|
-
definition = {
|
|
178
|
-
name: form.trigger_name,
|
|
179
|
-
table_name: form.table_name,
|
|
180
|
-
function_name: form.function_name,
|
|
181
|
-
events: form.events.compact_blank,
|
|
182
|
-
version: form.version,
|
|
183
|
-
enabled: form.enabled,
|
|
184
|
-
environments: form.environments.compact_blank,
|
|
185
|
-
condition: form.condition,
|
|
186
|
-
timing: form.timing || "before",
|
|
187
|
-
function_body: form.function_body
|
|
188
|
-
}
|
|
189
|
-
temp_registry.definition = definition.to_json
|
|
190
|
-
|
|
191
|
-
validator = PgSqlTriggers::Testing::SyntaxValidator.new(temp_registry)
|
|
192
|
-
|
|
193
|
-
# Validate function syntax
|
|
194
|
-
function_result = validator.validate_function_syntax
|
|
195
|
-
return function_result unless function_result[:valid]
|
|
196
|
-
|
|
197
|
-
# Validate condition if present
|
|
198
|
-
if form.condition.present?
|
|
199
|
-
condition_result = validator.validate_condition
|
|
200
|
-
unless condition_result[:valid]
|
|
201
|
-
return {
|
|
202
|
-
valid: false,
|
|
203
|
-
error: "WHEN condition validation failed: #{condition_result[:error]}"
|
|
204
|
-
}
|
|
205
|
-
end
|
|
206
|
-
end
|
|
207
|
-
|
|
208
|
-
function_result
|
|
209
|
-
rescue StandardError => e
|
|
210
|
-
{ valid: false, error: "Validation error: #{e.message}" }
|
|
211
|
-
end
|
|
212
|
-
end
|
|
213
|
-
end
|
|
@@ -1,161 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module PgSqlTriggers
|
|
4
|
-
class SqlCapsulesController < ApplicationController
|
|
5
|
-
before_action :check_admin_permission, only: [:execute]
|
|
6
|
-
before_action :check_operator_permission, only: %i[new create show]
|
|
7
|
-
before_action :load_capsule, only: %i[show execute]
|
|
8
|
-
|
|
9
|
-
def show
|
|
10
|
-
unless @capsule
|
|
11
|
-
redirect_to new_sql_capsule_path, alert: "Capsule not found"
|
|
12
|
-
return
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
@checksum = @capsule.checksum
|
|
16
|
-
@can_execute = can_execute_capsule?
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
def new
|
|
20
|
-
@capsule_name = params[:name] || ""
|
|
21
|
-
@environment = params[:environment] || current_environment
|
|
22
|
-
@purpose = params[:purpose] || ""
|
|
23
|
-
@sql = params[:sql] || ""
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
def create
|
|
27
|
-
# Strip whitespace from parameters
|
|
28
|
-
@capsule_name = params[:name].to_s.strip
|
|
29
|
-
@environment = params[:environment].to_s.strip
|
|
30
|
-
@purpose = params[:purpose].to_s.strip
|
|
31
|
-
@sql = params[:sql].to_s.strip
|
|
32
|
-
|
|
33
|
-
capsule = build_capsule_from_params
|
|
34
|
-
|
|
35
|
-
# Save the capsule to registry
|
|
36
|
-
result = save_capsule_to_registry(capsule)
|
|
37
|
-
|
|
38
|
-
if result[:success]
|
|
39
|
-
redirect_to sql_capsule_path(id: @capsule_name),
|
|
40
|
-
notice: "SQL Capsule '#{capsule.name}' created successfully"
|
|
41
|
-
else
|
|
42
|
-
flash.now[:alert] = result[:message]
|
|
43
|
-
render :new
|
|
44
|
-
end
|
|
45
|
-
rescue ArgumentError => e
|
|
46
|
-
flash.now[:alert] = "Invalid capsule: #{e.message}"
|
|
47
|
-
render :new
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
def execute
|
|
51
|
-
unless @capsule
|
|
52
|
-
redirect_to new_sql_capsule_path, alert: "Capsule not found"
|
|
53
|
-
return
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
# Check kill switch with confirmation
|
|
57
|
-
check_kill_switch(
|
|
58
|
-
operation: :execute_sql_capsule,
|
|
59
|
-
confirmation: params[:confirmation]
|
|
60
|
-
)
|
|
61
|
-
|
|
62
|
-
# Execute the capsule
|
|
63
|
-
result = PgSqlTriggers::SQL::Executor.execute(
|
|
64
|
-
@capsule,
|
|
65
|
-
actor: current_actor,
|
|
66
|
-
confirmation: params[:confirmation],
|
|
67
|
-
dry_run: false
|
|
68
|
-
)
|
|
69
|
-
|
|
70
|
-
if result[:success]
|
|
71
|
-
flash[:notice] = result[:message]
|
|
72
|
-
else
|
|
73
|
-
flash[:alert] = result[:message]
|
|
74
|
-
end
|
|
75
|
-
redirect_to sql_capsule_path(id: params[:id])
|
|
76
|
-
rescue PgSqlTriggers::KillSwitchError => e
|
|
77
|
-
flash[:alert] = "Kill switch blocked execution: #{e.message}"
|
|
78
|
-
redirect_to sql_capsule_path(id: params[:id])
|
|
79
|
-
rescue PgSqlTriggers::PermissionError => e
|
|
80
|
-
flash[:alert] = "Permission denied: #{e.message}"
|
|
81
|
-
redirect_to sql_capsule_path(id: params[:id])
|
|
82
|
-
rescue StandardError => e
|
|
83
|
-
Rails.logger.error("SQL Capsule execution failed: #{e.message}\n#{e.backtrace.join("\n")}")
|
|
84
|
-
flash[:alert] = "Execution failed: #{e.message}"
|
|
85
|
-
redirect_to sql_capsule_path(id: params[:id])
|
|
86
|
-
end
|
|
87
|
-
|
|
88
|
-
private
|
|
89
|
-
|
|
90
|
-
def check_admin_permission
|
|
91
|
-
return if PgSqlTriggers::Permissions.can?(current_actor, :execute_sql)
|
|
92
|
-
|
|
93
|
-
redirect_to dashboard_path, alert: "Insufficient permissions. Admin role required."
|
|
94
|
-
end
|
|
95
|
-
|
|
96
|
-
def check_operator_permission
|
|
97
|
-
return if PgSqlTriggers::Permissions.can?(current_actor, :generate_trigger)
|
|
98
|
-
|
|
99
|
-
redirect_to dashboard_path, alert: "Insufficient permissions. Operator role required."
|
|
100
|
-
end
|
|
101
|
-
|
|
102
|
-
def build_capsule_from_params
|
|
103
|
-
PgSqlTriggers::SQL::Capsule.new(
|
|
104
|
-
name: params[:name].to_s.strip,
|
|
105
|
-
environment: params[:environment].to_s.strip,
|
|
106
|
-
purpose: params[:purpose].to_s.strip,
|
|
107
|
-
sql: params[:sql].to_s.strip
|
|
108
|
-
)
|
|
109
|
-
end
|
|
110
|
-
|
|
111
|
-
def save_capsule_to_registry(capsule)
|
|
112
|
-
# Check if capsule already exists
|
|
113
|
-
existing = PgSqlTriggers::TriggerRegistry.find_by(
|
|
114
|
-
trigger_name: capsule.registry_trigger_name,
|
|
115
|
-
source: "manual_sql"
|
|
116
|
-
)
|
|
117
|
-
|
|
118
|
-
if existing
|
|
119
|
-
return {
|
|
120
|
-
success: false,
|
|
121
|
-
message: "A capsule with this name already exists. Please choose a different name."
|
|
122
|
-
}
|
|
123
|
-
end
|
|
124
|
-
|
|
125
|
-
# Create new registry entry
|
|
126
|
-
registry_entry = PgSqlTriggers::TriggerRegistry.new(
|
|
127
|
-
trigger_name: capsule.registry_trigger_name,
|
|
128
|
-
table_name: "manual_sql_execution",
|
|
129
|
-
version: Time.current.to_i,
|
|
130
|
-
checksum: capsule.checksum,
|
|
131
|
-
source: "manual_sql",
|
|
132
|
-
function_body: capsule.sql,
|
|
133
|
-
condition: capsule.purpose,
|
|
134
|
-
environment: capsule.environment,
|
|
135
|
-
enabled: false # Not executed yet
|
|
136
|
-
)
|
|
137
|
-
|
|
138
|
-
if registry_entry.save
|
|
139
|
-
{ success: true, message: "Capsule saved successfully" }
|
|
140
|
-
else
|
|
141
|
-
{ success: false, message: "Failed to save capsule: #{registry_entry.errors.full_messages.join(', ')}" }
|
|
142
|
-
end
|
|
143
|
-
rescue StandardError => e
|
|
144
|
-
Rails.logger.error("Failed to save capsule to registry: #{e.message}")
|
|
145
|
-
{ success: false, message: "Failed to save capsule: #{e.message}" }
|
|
146
|
-
end
|
|
147
|
-
|
|
148
|
-
def load_capsule
|
|
149
|
-
return if params[:id].blank?
|
|
150
|
-
|
|
151
|
-
@capsule = PgSqlTriggers::SQL::Executor.send(
|
|
152
|
-
:load_capsule_from_registry,
|
|
153
|
-
params[:id]
|
|
154
|
-
)
|
|
155
|
-
end
|
|
156
|
-
|
|
157
|
-
def can_execute_capsule?
|
|
158
|
-
PgSqlTriggers::Permissions.can?(current_actor, :execute_sql)
|
|
159
|
-
end
|
|
160
|
-
end
|
|
161
|
-
end
|