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.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +253 -1
  3. data/GEM_ANALYSIS.md +368 -0
  4. data/README.md +20 -23
  5. data/app/models/pg_sql_triggers/trigger_registry.rb +42 -6
  6. data/app/views/layouts/pg_sql_triggers/application.html.erb +0 -1
  7. data/app/views/pg_sql_triggers/dashboard/index.html.erb +1 -4
  8. data/app/views/pg_sql_triggers/tables/index.html.erb +1 -4
  9. data/app/views/pg_sql_triggers/tables/show.html.erb +0 -2
  10. data/config/routes.rb +0 -14
  11. data/db/migrate/20260228000001_add_for_each_to_pg_sql_triggers_registry.rb +8 -0
  12. data/docs/api-reference.md +44 -153
  13. data/docs/configuration.md +24 -3
  14. data/docs/getting-started.md +17 -16
  15. data/docs/usage-guide.md +38 -67
  16. data/docs/web-ui.md +3 -103
  17. data/lib/generators/pg_sql_triggers/templates/trigger_dsl.rb.tt +11 -0
  18. data/lib/generators/pg_sql_triggers/templates/trigger_migration_full.rb.tt +29 -0
  19. data/lib/generators/pg_sql_triggers/trigger_generator.rb +83 -0
  20. data/lib/pg_sql_triggers/drift/db_queries.rb +12 -8
  21. data/lib/pg_sql_triggers/drift/detector.rb +51 -38
  22. data/lib/pg_sql_triggers/dsl/trigger_definition.rb +17 -23
  23. data/lib/pg_sql_triggers/engine.rb +14 -0
  24. data/lib/pg_sql_triggers/migrator/pre_apply_comparator.rb +8 -9
  25. data/lib/pg_sql_triggers/migrator/safety_validator.rb +32 -12
  26. data/lib/pg_sql_triggers/migrator.rb +53 -6
  27. data/lib/pg_sql_triggers/registry/manager.rb +36 -11
  28. data/lib/pg_sql_triggers/registry/validator.rb +62 -5
  29. data/lib/pg_sql_triggers/sql/kill_switch.rb +153 -275
  30. data/lib/pg_sql_triggers/sql.rb +0 -6
  31. data/lib/pg_sql_triggers/version.rb +1 -1
  32. data/lib/pg_sql_triggers.rb +4 -1
  33. data/pg_sql_triggers.gemspec +53 -0
  34. metadata +7 -13
  35. data/app/controllers/pg_sql_triggers/generator_controller.rb +0 -213
  36. data/app/controllers/pg_sql_triggers/sql_capsules_controller.rb +0 -161
  37. data/app/views/pg_sql_triggers/generator/new.html.erb +0 -388
  38. data/app/views/pg_sql_triggers/generator/preview.html.erb +0 -305
  39. data/app/views/pg_sql_triggers/sql_capsules/new.html.erb +0 -81
  40. data/app/views/pg_sql_triggers/sql_capsules/show.html.erb +0 -85
  41. data/lib/generators/trigger/migration_generator.rb +0 -60
  42. data/lib/pg_sql_triggers/generator/form.rb +0 -80
  43. data/lib/pg_sql_triggers/generator/service.rb +0 -339
  44. data/lib/pg_sql_triggers/generator.rb +0 -8
  45. data/lib/pg_sql_triggers/sql/capsule.rb +0 -79
  46. 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