pg_sql_triggers 1.1.1 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: dff79b71aeff432cbe9324568f94bd358c76ca9161162f47b8513401eb783dd1
4
- data.tar.gz: 524af402547aa3ef6be32605089e177a2cde1e7cae2548affa657c2af283cbf1
3
+ metadata.gz: 54f76c538693c37b1572cb914462269eb8c3600473c1a7c26efda0a7e02eefe5
4
+ data.tar.gz: 8574ff3bb8f835a11f8cec850c5adbff7f17be65a1461b8f8e87242ab3e50b62
5
5
  SHA512:
6
- metadata.gz: e57441cf88cfd3e6deb7f9523286fbe862add9c0d07eef5ac127857c20f417b34ed4dc1a307a3c5c15012afbeb8524b566a2c5a5d0ad2b9284f60010bd7be99b
7
- data.tar.gz: b216be1abbea025ff653cde512a309258e211443e86b049d74b37417ef1df3633559b274c78870fba8498fe4afbd36db72de11c45c446a4e80e717491fe72765
6
+ metadata.gz: 5e7ccb4983c43cad0aee550a741e341a3feda8a2fd5541be1786a92ffc26894a80eb8081e57493e6eb05697a362cafaa3fee325b288a716f4f1e8c3de989966e
7
+ data.tar.gz: f24548a2b4fa7f0bb274d11d2085b87e716c44e08923782eee93ffb0d86500892ffca3b6d7c69ba53e1f886743646ac0c8695097589ecb7db4280152b4c1dccf
data/.rubocop.yml CHANGED
@@ -118,6 +118,21 @@ RSpec/MultipleExpectations:
118
118
  RSpec/SpecFilePathFormat:
119
119
  Enabled: false
120
120
 
121
+ RSpec/AnyInstance:
122
+ Enabled: false
123
+
124
+ RSpec/LetSetup:
125
+ Enabled: false
126
+
127
+ RSpec/NestedGroups:
128
+ Enabled: false
129
+
130
+ RSpec/VerifiedDoubles:
131
+ Enabled: false
132
+
133
+ RSpec/RepeatedExample:
134
+ Enabled: false
135
+
121
136
  # Capybara
122
137
  Capybara/RSpec/PredicateMatcher:
123
138
  Enabled: false
data/CHANGELOG.md CHANGED
@@ -7,6 +7,62 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.2.0] - 2026-01-02
11
+
12
+ ### Added
13
+ - **SQL Capsules**: Emergency SQL execution feature for critical operations
14
+ - Named SQL capsules with environment declaration and purpose description
15
+ - Capsule class for creating and managing SQL capsules
16
+ - Executor class for safe, transactional SQL execution
17
+ - Permission checks (Admin role required for execution)
18
+ - Kill switch protection for all executions
19
+ - Checksum calculation and storage in registry
20
+ - Comprehensive logging of all operations
21
+ - Web UI for creating, viewing, and executing SQL capsules
22
+ - Console API: `PgSqlTriggers::SQL::Executor.execute(capsule, actor:, confirmation:)`
23
+
24
+ - **Drop & Re-Execute Flow**: Operational controls for trigger lifecycle management
25
+ - `TriggerRegistry#drop!` method for safely dropping triggers
26
+ - Admin permission required
27
+ - Kill switch protection
28
+ - Reason field (required and logged)
29
+ - Typed confirmation required in protected environments
30
+ - Transactional execution
31
+ - Removes trigger from database and registry
32
+ - `TriggerRegistry#re_execute!` method for fixing drifted triggers
33
+ - Admin permission required
34
+ - Kill switch protection
35
+ - Shows drift diff before execution
36
+ - Reason field (required and logged)
37
+ - Typed confirmation required in protected environments
38
+ - Transactional execution
39
+ - Drops and re-creates trigger from registry definition
40
+ - Web UI buttons for drop and re-execute on trigger detail page
41
+ - Controller actions with proper permission checks and error handling
42
+ - Interactive modals with reason input and confirmation fields
43
+ - Drift comparison shown before re-execution
44
+
45
+ - **Enhanced Permissions Enforcement**:
46
+ - Console APIs with permission checks:
47
+ - `PgSqlTriggers::Registry.enable(trigger_name, actor:, confirmation:)`
48
+ - `PgSqlTriggers::Registry.disable(trigger_name, actor:, confirmation:)`
49
+ - `PgSqlTriggers::Registry.drop(trigger_name, actor:, reason:, confirmation:)`
50
+ - `PgSqlTriggers::Registry.re_execute(trigger_name, actor:, reason:, confirmation:)`
51
+ - Permission checks enforced at console API level
52
+ - Rake tasks already protected by kill switch
53
+ - Clear error messages for permission violations
54
+
55
+ ### Fixed
56
+ - Improved error handling for trigger enable/disable operations
57
+ - Better logging for drop and re-execute operations
58
+ - Fixed rubocop linting issues
59
+
60
+ ### Security
61
+ - All destructive operations (drop, re-execute, SQL capsule execution) require Admin permissions
62
+ - Kill switch protection enforced across all new features
63
+ - Typed confirmation required in protected environments
64
+ - Comprehensive audit logging for all operations
65
+
10
66
  ## [1.1.1] - 2025-12-31
11
67
 
12
68
  ### Changed
data/COVERAGE.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # Code Coverage Report
2
2
 
3
- **Total Coverage: 95.4%**
3
+ **Total Coverage: 95.33%**
4
4
 
5
- Covered: 1678 / 1759 lines
5
+ Covered: 2040 / 2140 lines
6
6
 
7
7
  ---
8
8
 
@@ -10,47 +10,51 @@ Covered: 1678 / 1759 lines
10
10
 
11
11
  | File | Coverage | Covered Lines | Missed Lines | Total Lines |
12
12
  |------|----------|---------------|--------------|-------------|
13
- | `lib/pg_sql_triggers/testing/dry_run.rb` | 100.0% ✅ | 24 | 0 | 24 |
14
- | `lib/pg_sql_triggers/testing.rb` | 100.0% ✅ | 6 | 0 | 6 |
15
- | `lib/pg_sql_triggers/registry/validator.rb` | 100.0% ✅ | 5 | 0 | 5 |
16
- | `lib/pg_sql_triggers/registry.rb` | 100.0% ✅ | 18 | 0 | 18 |
17
- | `lib/pg_sql_triggers/permissions/checker.rb` | 100.0% ✅ | 15 | 0 | 15 |
18
- | `lib/pg_sql_triggers/permissions.rb` | 100.0% ✅ | 11 | 0 | 11 |
19
- | `lib/pg_sql_triggers/migrator/safety_validator.rb` | 100.0% ✅ | 110 | 0 | 110 |
20
- | `lib/pg_sql_triggers/migrator/pre_apply_diff_reporter.rb` | 100.0% ✅ | 75 | 0 | 75 |
21
- | `lib/pg_sql_triggers/migrator/pre_apply_comparator.rb` | 100.0% ✅ | 123 | 0 | 123 |
22
- | `lib/pg_sql_triggers/migration.rb` | 100.0% ✅ | 4 | 0 | 4 |
23
- | `lib/generators/trigger/migration_generator.rb` | 100.0% ✅ | 27 | 0 | 27 |
24
- | `lib/generators/pg_sql_triggers/install_generator.rb` | 100.0% ✅ | 18 | 0 | 18 |
25
- | `lib/pg_sql_triggers/generator/service.rb` | 100.0% ✅ | 93 | 0 | 93 |
26
- | `lib/pg_sql_triggers/generator/form.rb` | 100.0% ✅ | 36 | 0 | 36 |
27
- | `lib/pg_sql_triggers/generator.rb` | 100.0% ✅ | 4 | 0 | 4 |
28
13
  | `lib/pg_sql_triggers/dsl/trigger_definition.rb` | 100.0% ✅ | 37 | 0 | 37 |
29
- | `lib/pg_sql_triggers.rb` | 100.0% ✅ | 45 | 0 | 45 |
14
+ | `lib/pg_sql_triggers/generator.rb` | 100.0% ✅ | 4 | 0 | 4 |
15
+ | `lib/pg_sql_triggers/generator/form.rb` | 100.0% ✅ | 36 | 0 | 36 |
16
+ | `lib/pg_sql_triggers/generator/service.rb` | 100.0% ✅ | 101 | 0 | 101 |
17
+ | `lib/generators/pg_sql_triggers/install_generator.rb` | 100.0% ✅ | 18 | 0 | 18 |
18
+ | `lib/generators/trigger/migration_generator.rb` | 100.0% ✅ | 27 | 0 | 27 |
19
+ | `lib/pg_sql_triggers/migration.rb` | 100.0% ✅ | 4 | 0 | 4 |
20
+ | `lib/pg_sql_triggers/migrator/pre_apply_diff_reporter.rb` | 100.0% ✅ | 75 | 0 | 75 |
21
+ | `lib/pg_sql_triggers/migrator/safety_validator.rb` | 100.0% ✅ | 110 | 0 | 110 |
22
+ | `lib/pg_sql_triggers/permissions.rb` | 100.0% ✅ | 11 | 0 | 11 |
23
+ | `lib/pg_sql_triggers/permissions/checker.rb` | 100.0% ✅ | 15 | 0 | 15 |
24
+ | `lib/pg_sql_triggers/registry.rb` | 100.0% ✅ | 41 | 0 | 41 |
25
+ | `lib/pg_sql_triggers/registry/validator.rb` | 100.0% ✅ | 5 | 0 | 5 |
26
+ | `lib/pg_sql_triggers/sql/capsule.rb` | 100.0% ✅ | 28 | 0 | 28 |
27
+ | `lib/pg_sql_triggers/sql/executor.rb` | 100.0% ✅ | 63 | 0 | 63 |
28
+ | `lib/pg_sql_triggers/testing.rb` | 100.0% ✅ | 6 | 0 | 6 |
29
+ | `lib/pg_sql_triggers/testing/syntax_validator.rb` | 100.0% ✅ | 58 | 0 | 58 |
30
+ | `lib/pg_sql_triggers/testing/dry_run.rb` | 100.0% ✅ | 24 | 0 | 24 |
30
31
  | `config/initializers/pg_sql_triggers.rb` | 100.0% ✅ | 10 | 0 | 10 |
31
32
  | `app/models/pg_sql_triggers/application_record.rb` | 100.0% ✅ | 3 | 0 | 3 |
32
33
  | `app/controllers/pg_sql_triggers/application_controller.rb` | 100.0% ✅ | 28 | 0 | 28 |
33
- | `app/controllers/pg_sql_triggers/dashboard_controller.rb` | 100.0% ✅ | 25 | 0 | 25 |
34
- | `app/controllers/pg_sql_triggers/migrations_controller.rb` | 100.0% ✅ | 63 | 0 | 63 |
34
+ | `lib/pg_sql_triggers.rb` | 100.0% ✅ | 45 | 0 | 45 |
35
35
  | `lib/pg_sql_triggers/dsl.rb` | 100.0% ✅ | 9 | 0 | 9 |
36
- | `lib/pg_sql_triggers/drift.rb` | 100.0% ✅ | 13 | 0 | 13 |
37
36
  | `lib/pg_sql_triggers/drift/db_queries.rb` | 100.0% ✅ | 24 | 0 | 24 |
37
+ | `lib/pg_sql_triggers/drift.rb` | 100.0% ✅ | 13 | 0 | 13 |
38
+ | `lib/pg_sql_triggers/migrator/pre_apply_comparator.rb` | 99.19% ✅ | 122 | 1 | 123 |
38
39
  | `lib/pg_sql_triggers/drift/detector.rb` | 98.48% ✅ | 65 | 1 | 66 |
39
- | `lib/pg_sql_triggers/registry/manager.rb` | 96.36% ✅ | 53 | 2 | 55 |
40
+ | `app/controllers/pg_sql_triggers/sql_capsules_controller.rb` | 97.18% ✅ | 69 | 2 | 71 |
41
+ | `app/controllers/pg_sql_triggers/dashboard_controller.rb` | 96.67% ✅ | 29 | 1 | 30 |
42
+ | `app/controllers/pg_sql_triggers/triggers_controller.rb` | 96.43% ✅ | 81 | 3 | 84 |
40
43
  | `lib/generators/pg_sql_triggers/trigger_migration_generator.rb` | 96.3% ✅ | 26 | 1 | 27 |
41
44
  | `lib/pg_sql_triggers/sql/kill_switch.rb` | 96.0% ✅ | 96 | 4 | 100 |
42
45
  | `lib/pg_sql_triggers/migrator.rb` | 95.42% ✅ | 125 | 6 | 131 |
43
- | `app/controllers/pg_sql_triggers/generator_controller.rb` | 94.68% ✅ | 89 | 5 | 94 |
46
+ | `lib/pg_sql_triggers/registry/manager.rb` | 95.08% ✅ | 58 | 3 | 61 |
44
47
  | `app/controllers/pg_sql_triggers/tables_controller.rb` | 94.44% ✅ | 17 | 1 | 18 |
48
+ | `lib/pg_sql_triggers/database_introspection.rb` | 94.29% ✅ | 66 | 4 | 70 |
45
49
  | `lib/pg_sql_triggers/drift/reporter.rb` | 94.12% ✅ | 96 | 6 | 102 |
46
50
  | `lib/pg_sql_triggers/engine.rb` | 92.86% ✅ | 13 | 1 | 14 |
47
- | `lib/pg_sql_triggers/database_introspection.rb` | 92.86% ✅ | 65 | 5 | 70 |
51
+ | `app/models/pg_sql_triggers/trigger_registry.rb` | 92.25% ✅ | 119 | 10 | 129 |
48
52
  | `lib/pg_sql_triggers/testing/safe_executor.rb` | 91.89% ✅ | 34 | 3 | 37 |
53
+ | `app/controllers/pg_sql_triggers/generator_controller.rb` | 91.49% ✅ | 86 | 8 | 94 |
49
54
  | `lib/pg_sql_triggers/sql.rb` | 90.91% ✅ | 10 | 1 | 11 |
50
- | `lib/pg_sql_triggers/testing/syntax_validator.rb` | 89.66% ⚠️ | 52 | 6 | 58 |
51
- | `app/models/pg_sql_triggers/trigger_registry.rb` | 84.62% ⚠️ | 55 | 10 | 65 |
52
- | `lib/pg_sql_triggers/testing/function_tester.rb` | 79.1% ⚠️ | 53 | 14 | 67 |
53
- | `config/routes.rb` | 16.67% ❌ | 3 | 15 | 18 |
55
+ | `lib/pg_sql_triggers/testing/function_tester.rb` | 89.55% ⚠️ | 60 | 7 | 67 |
56
+ | `app/controllers/pg_sql_triggers/migrations_controller.rb` | 81.4% ⚠️ | 70 | 16 | 86 |
57
+ | `config/routes.rb` | 12.5% | 3 | 21 | 24 |
54
58
 
55
59
  ---
56
60
 
data/README.md CHANGED
@@ -97,9 +97,38 @@ Multi-layered safety mechanism preventing accidental destructive operations in p
97
97
  ### Web Dashboard
98
98
  Visual interface for managing triggers, running migrations, and executing SQL capsules.
99
99
 
100
+ ### SQL Capsules
101
+ Emergency SQL execution feature for critical operations with Admin permission requirements, kill switch protection, and comprehensive logging.
102
+
103
+ ### Drop & Re-Execute Flow
104
+ Operational controls for trigger lifecycle management with drop and re-execute capabilities, drift comparison, and required reason logging.
105
+
100
106
  ### Permissions
101
107
  Three-tier permission system (Viewer, Operator, Admin) with customizable authorization.
102
108
 
109
+ ## Console API
110
+
111
+ PgSqlTriggers provides a comprehensive console API for managing triggers programmatically:
112
+
113
+ ```ruby
114
+ # Enable/disable triggers
115
+ PgSqlTriggers::Registry.enable("users_email_validation", actor: current_user, confirmation: "EXECUTE TRIGGER_ENABLE")
116
+ PgSqlTriggers::Registry.disable("users_email_validation", actor: current_user, confirmation: "EXECUTE TRIGGER_DISABLE")
117
+
118
+ # Drop and re-execute triggers
119
+ PgSqlTriggers::Registry.drop("old_trigger", actor: current_user, reason: "No longer needed", confirmation: "EXECUTE TRIGGER_DROP")
120
+ PgSqlTriggers::Registry.re_execute("drifted_trigger", actor: current_user, reason: "Fix drift", confirmation: "EXECUTE TRIGGER_RE_EXECUTE")
121
+
122
+ # Execute SQL capsules
123
+ capsule = PgSqlTriggers::SQL::Capsule.new(
124
+ name: "emergency_fix",
125
+ environment: "production",
126
+ purpose: "Fix critical data issue",
127
+ sql: "UPDATE users SET status = 'active' WHERE id = 123"
128
+ )
129
+ PgSqlTriggers::SQL::Executor.execute(capsule, actor: current_user, confirmation: "EXECUTE SQL")
130
+ ```
131
+
103
132
  ## Examples
104
133
 
105
134
  For working examples and complete demonstrations, check out the [example repository](https://github.com/samaswin/pg_triggers_example).
@@ -10,7 +10,7 @@ module PgSqlTriggers
10
10
  before_action :check_permissions?
11
11
 
12
12
  # Helper methods available in views
13
- helper_method :current_environment, :kill_switch_active?, :expected_confirmation_text
13
+ helper_method :current_environment, :kill_switch_active?, :expected_confirmation_text, :current_actor
14
14
 
15
15
  private
16
16
 
@@ -2,6 +2,8 @@
2
2
 
3
3
  module PgSqlTriggers
4
4
  class DashboardController < ApplicationController
5
+ before_action :check_viewer_permission
6
+
5
7
  def index
6
8
  @triggers = PgSqlTriggers::TriggerRegistry.order(created_at: :desc)
7
9
 
@@ -41,5 +43,13 @@ module PgSqlTriggers
41
43
  @per_page = 20
42
44
  end
43
45
  end
46
+
47
+ private
48
+
49
+ def check_viewer_permission
50
+ return if PgSqlTriggers::Permissions.can?(current_actor, :view_triggers)
51
+
52
+ redirect_to root_path, alert: "Insufficient permissions. Viewer role required."
53
+ end
44
54
  end
45
55
  end
@@ -4,6 +4,8 @@ module PgSqlTriggers
4
4
  # Controller for managing trigger migrations via web UI
5
5
  # Provides actions to run migrations up, down, and redo
6
6
  class MigrationsController < ApplicationController
7
+ before_action :check_operator_permission
8
+
7
9
  def up
8
10
  # Check kill switch before running migration
9
11
  check_kill_switch(operation: :ui_migration_up, confirmation: params[:confirmation_text])
@@ -73,18 +75,16 @@ module PgSqlTriggers
73
75
  target_version = params[:version]&.to_i
74
76
  PgSqlTriggers::Migrator.ensure_migrations_table!
75
77
 
78
+ current_version = PgSqlTriggers::Migrator.current_version
79
+ if current_version.zero?
80
+ flash[:warning] = "No migrations to redo."
81
+ redirect_to root_path
82
+ return
83
+ end
84
+
76
85
  if target_version
77
- PgSqlTriggers::Migrator.run_down(target_version)
78
- PgSqlTriggers::Migrator.run_up(target_version)
79
- flash[:success] = "Migration #{target_version} redone successfully."
86
+ redo_target_migration(target_version, current_version)
80
87
  else
81
- current_version = PgSqlTriggers::Migrator.current_version
82
- if current_version.zero?
83
- flash[:warning] = "No migrations to redo."
84
- redirect_to root_path
85
- return
86
- end
87
-
88
88
  PgSqlTriggers::Migrator.run_down
89
89
  PgSqlTriggers::Migrator.run_up
90
90
  flash[:success] = "Last migration redone successfully."
@@ -98,5 +98,57 @@ module PgSqlTriggers
98
98
  flash[:error] = "Failed to redo migration: #{e.message}"
99
99
  redirect_to root_path
100
100
  end
101
+
102
+ private
103
+
104
+ def check_operator_permission
105
+ return if PgSqlTriggers::Permissions.can?(current_actor, :apply_trigger)
106
+
107
+ redirect_to root_path, alert: "Insufficient permissions. Operator role required."
108
+ end
109
+
110
+ def redo_target_migration(target_version, current_version)
111
+ # For redo, we need to rollback the specific migration and re-apply it
112
+ # If target_version is the current version, rollback the last migration
113
+ # Otherwise, rollback to one version before target, then run up to target
114
+ if current_version == target_version
115
+ # Rollback the last migration (which is the target)
116
+ PgSqlTriggers::Migrator.run_down
117
+ elsif current_version > target_version
118
+ rollback_to_before_target(target_version)
119
+ else
120
+ # Target version is not applied yet, just run it up
121
+ PgSqlTriggers::Migrator.run_up(target_version)
122
+ flash[:success] = "Migration #{target_version} redone successfully."
123
+ redirect_to root_path
124
+ return
125
+ end
126
+
127
+ # Now run up to the target version
128
+ PgSqlTriggers::Migrator.run_up(target_version)
129
+ flash.now[:success] = "Migration #{target_version} redone successfully."
130
+ end
131
+
132
+ def rollback_to_before_target(target_version)
133
+ # Rollback to one version before target (this will rollback target_version too)
134
+ # Find the migration just before target_version
135
+ all_migrations = PgSqlTriggers::Migrator.migrations.sort_by(&:version)
136
+ prev_migration = all_migrations.find { |m| m.version < target_version }
137
+ if prev_migration
138
+ # Rollback to the previous migration (this rolls back target_version)
139
+ PgSqlTriggers::Migrator.run_down(prev_migration.version)
140
+ else
141
+ # No previous migration, target_version is the first migration
142
+ # Rollback all migrations until we're below target_version
143
+ rollback_until_below_target(target_version)
144
+ end
145
+ end
146
+
147
+ def rollback_until_below_target(target_version)
148
+ while PgSqlTriggers::Migrator.current_version >= target_version
149
+ PgSqlTriggers::Migrator.run_down
150
+ break if PgSqlTriggers::Migrator.current_version.zero?
151
+ end
152
+ end
101
153
  end
102
154
  end
@@ -0,0 +1,161 @@
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
@@ -0,0 +1,165 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgSqlTriggers
4
+ # Controller for managing individual triggers via web UI
5
+ # Provides actions to enable and disable triggers
6
+ class TriggersController < ApplicationController
7
+ before_action :set_trigger, only: %i[show enable disable drop re_execute]
8
+ before_action :check_viewer_permission, only: [:show]
9
+ before_action :check_operator_permission, only: %i[enable disable]
10
+ before_action :check_admin_permission, only: %i[drop re_execute]
11
+
12
+ def show
13
+ # Load trigger details and drift information
14
+ @drift_info = calculate_drift_info
15
+ end
16
+
17
+ def enable
18
+ # Check kill switch before enabling trigger
19
+ check_kill_switch(operation: :ui_trigger_enable, confirmation: params[:confirmation_text])
20
+
21
+ @trigger.enable!(confirmation: params[:confirmation_text])
22
+ flash[:success] = "Trigger '#{@trigger.trigger_name}' enabled successfully."
23
+ redirect_to redirect_path
24
+ rescue PgSqlTriggers::KillSwitchError => e
25
+ flash[:error] = e.message
26
+ redirect_to redirect_path
27
+ rescue StandardError => e
28
+ Rails.logger.error("Enable failed: #{e.message}\n#{e.backtrace.join("\n")}")
29
+ flash[:error] = "Failed to enable trigger: #{e.message}"
30
+ redirect_to redirect_path
31
+ end
32
+
33
+ def disable
34
+ # Check kill switch before disabling trigger
35
+ check_kill_switch(operation: :ui_trigger_disable, confirmation: params[:confirmation_text])
36
+
37
+ @trigger.disable!(confirmation: params[:confirmation_text])
38
+ flash[:success] = "Trigger '#{@trigger.trigger_name}' disabled successfully."
39
+ redirect_to redirect_path
40
+ rescue PgSqlTriggers::KillSwitchError => e
41
+ flash[:error] = e.message
42
+ redirect_to redirect_path
43
+ rescue StandardError => e
44
+ Rails.logger.error("Disable failed: #{e.message}\n#{e.backtrace.join("\n")}")
45
+ flash[:error] = "Failed to disable trigger: #{e.message}"
46
+ redirect_to redirect_path
47
+ end
48
+
49
+ def drop
50
+ # Validate required parameters
51
+ if params[:reason].blank?
52
+ flash[:error] = "Reason is required for dropping a trigger."
53
+ redirect_to redirect_path
54
+ return
55
+ end
56
+
57
+ # Check kill switch before dropping trigger
58
+ check_kill_switch(operation: :trigger_drop, confirmation: params[:confirmation_text])
59
+
60
+ # Drop the trigger
61
+ @trigger.drop!(
62
+ reason: params[:reason],
63
+ confirmation: params[:confirmation_text],
64
+ actor: current_actor
65
+ )
66
+
67
+ flash[:success] = "Trigger '#{@trigger.trigger_name}' dropped successfully."
68
+ redirect_to dashboard_path
69
+ rescue PgSqlTriggers::KillSwitchError => e
70
+ flash[:error] = e.message
71
+ redirect_to redirect_path
72
+ rescue ArgumentError => e
73
+ flash[:error] = "Invalid request: #{e.message}"
74
+ redirect_to redirect_path
75
+ rescue StandardError => e
76
+ Rails.logger.error("Drop failed: #{e.message}\n#{e.backtrace.join("\n")}")
77
+ flash[:error] = "Failed to drop trigger: #{e.message}"
78
+ redirect_to redirect_path
79
+ end
80
+
81
+ def re_execute
82
+ # Validate required parameters
83
+ if params[:reason].blank?
84
+ flash[:error] = "Reason is required for re-executing a trigger."
85
+ redirect_to redirect_path
86
+ return
87
+ end
88
+
89
+ # Check kill switch before re-executing trigger
90
+ check_kill_switch(operation: :trigger_re_execute, confirmation: params[:confirmation_text])
91
+
92
+ # Re-execute the trigger
93
+ @trigger.re_execute!(
94
+ reason: params[:reason],
95
+ confirmation: params[:confirmation_text],
96
+ actor: current_actor
97
+ )
98
+
99
+ flash[:success] = "Trigger '#{@trigger.trigger_name}' re-executed successfully."
100
+ redirect_to redirect_path
101
+ rescue PgSqlTriggers::KillSwitchError => e
102
+ flash[:error] = e.message
103
+ redirect_to redirect_path
104
+ rescue ArgumentError => e
105
+ flash[:error] = "Invalid request: #{e.message}"
106
+ redirect_to redirect_path
107
+ rescue StandardError => e
108
+ Rails.logger.error("Re-execute failed: #{e.message}\n#{e.backtrace.join("\n")}")
109
+ flash[:error] = "Failed to re-execute trigger: #{e.message}"
110
+ redirect_to redirect_path
111
+ end
112
+
113
+ private
114
+
115
+ def set_trigger
116
+ @trigger = PgSqlTriggers::TriggerRegistry.find(params[:id])
117
+ rescue ActiveRecord::RecordNotFound
118
+ flash[:error] = "Trigger not found."
119
+ redirect_to root_path
120
+ end
121
+
122
+ def check_viewer_permission
123
+ return if PgSqlTriggers::Permissions.can?(current_actor, :view_triggers)
124
+
125
+ redirect_to root_path, alert: "Insufficient permissions. Viewer role required."
126
+ end
127
+
128
+ def check_operator_permission
129
+ return if PgSqlTriggers::Permissions.can?(current_actor, :enable_trigger)
130
+
131
+ redirect_to root_path, alert: "Insufficient permissions. Operator role required."
132
+ end
133
+
134
+ def check_admin_permission
135
+ return if PgSqlTriggers::Permissions.can?(current_actor, :drop_trigger)
136
+
137
+ redirect_to root_path, alert: "Insufficient permissions. Admin role required."
138
+ end
139
+
140
+ def redirect_path
141
+ # Redirect back to the referring page if possible, otherwise to dashboard
142
+ params[:redirect_to].presence || request.referer || root_path
143
+ end
144
+
145
+ def calculate_drift_info
146
+ # Get drift information for this trigger
147
+ drift_reporter = PgSqlTriggers::Drift::Reporter.new
148
+ drift_summary = drift_reporter.summary
149
+
150
+ # Find this trigger in the drift summary
151
+ drifted_triggers = drift_summary[:triggers] || []
152
+ drift_data = drifted_triggers.find { |t| t[:trigger_name] == @trigger.trigger_name }
153
+
154
+ {
155
+ has_drift: drift_data.present?,
156
+ drift_type: drift_data&.dig(:drift_type),
157
+ expected_sql: drift_data&.dig(:expected_sql),
158
+ actual_sql: drift_data&.dig(:actual_sql)
159
+ }
160
+ rescue StandardError => e
161
+ Rails.logger.error("Failed to calculate drift: #{e.message}")
162
+ { has_drift: false, drift_type: nil, expected_sql: nil, actual_sql: nil }
163
+ end
164
+ end
165
+ end