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.
Files changed (32) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +15 -0
  3. data/CHANGELOG.md +61 -0
  4. data/COVERAGE.md +32 -28
  5. data/README.md +31 -2
  6. data/RELEASE.md +1 -1
  7. data/app/controllers/pg_sql_triggers/application_controller.rb +1 -1
  8. data/app/controllers/pg_sql_triggers/dashboard_controller.rb +10 -0
  9. data/app/controllers/pg_sql_triggers/migrations_controller.rb +62 -10
  10. data/app/controllers/pg_sql_triggers/sql_capsules_controller.rb +161 -0
  11. data/app/controllers/pg_sql_triggers/triggers_controller.rb +165 -0
  12. data/app/models/pg_sql_triggers/trigger_registry.rb +123 -0
  13. data/app/views/pg_sql_triggers/dashboard/index.html.erb +40 -2
  14. data/app/views/pg_sql_triggers/sql_capsules/new.html.erb +81 -0
  15. data/app/views/pg_sql_triggers/sql_capsules/show.html.erb +85 -0
  16. data/app/views/pg_sql_triggers/tables/show.html.erb +36 -2
  17. data/app/views/pg_sql_triggers/triggers/_drop_modal.html.erb +129 -0
  18. data/app/views/pg_sql_triggers/triggers/_re_execute_modal.html.erb +136 -0
  19. data/app/views/pg_sql_triggers/triggers/show.html.erb +186 -0
  20. data/config/routes.rb +9 -0
  21. data/docs/README.md +2 -2
  22. data/docs/api-reference.md +252 -4
  23. data/docs/getting-started.md +1 -1
  24. data/docs/kill-switch.md +3 -3
  25. data/docs/web-ui.md +82 -17
  26. data/lib/generators/pg_sql_triggers/templates/README +1 -1
  27. data/lib/pg_sql_triggers/registry/manager.rb +28 -13
  28. data/lib/pg_sql_triggers/registry.rb +41 -0
  29. data/lib/pg_sql_triggers/sql/capsule.rb +79 -0
  30. data/lib/pg_sql_triggers/sql/executor.rb +200 -0
  31. data/lib/pg_sql_triggers/version.rb +1 -1
  32. metadata +18 -12
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e72011f4407b6d60e5002202eedccf7e34c03075faebdf24301f74521e817904
4
- data.tar.gz: b9df169c3fecf2a3b5bb34bc6a8fd7c3cad9707ded4297276a9d73af3c57871c
3
+ metadata.gz: 54f76c538693c37b1572cb914462269eb8c3600473c1a7c26efda0a7e02eefe5
4
+ data.tar.gz: 8574ff3bb8f835a11f8cec850c5adbff7f17be65a1461b8f8e87242ab3e50b62
5
5
  SHA512:
6
- metadata.gz: 7868c6518becf693b0d34e28c18423220e8fb29ef6a5bf331123e6f0bec08735d1de582f49064b3140106f1906f1d4430fb569dcea1953c2204e5355552b4e89
7
- data.tar.gz: c6d7c92d4588482fe526d7e747d646e5abfcd542d094942348186719bf9b5240d85752e5a3485abe4d72fd21037d013bd155da8e8ddd388d5c36d4dfe88d5fd8
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,67 @@ 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
+
66
+ ## [1.1.1] - 2025-12-31
67
+
68
+ ### Changed
69
+ - Updated git username in repository metadata
70
+
10
71
  ## [1.1.0] - 2025-12-29
11
72
 
12
73
  ### Added
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,12 +97,41 @@ 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
- For working examples and complete demonstrations, check out the [example repository](https://github.com/samaswin87/pg_triggers_example).
134
+ For working examples and complete demonstrations, check out the [example repository](https://github.com/samaswin/pg_triggers_example).
106
135
 
107
136
  ## Core Principles
108
137
 
@@ -125,7 +154,7 @@ See [COVERAGE.md](COVERAGE.md) for detailed coverage information.
125
154
 
126
155
  ## Contributing
127
156
 
128
- Bug reports and pull requests are welcome on GitHub at https://github.com/samaswin87/pg_sql_triggers.
157
+ Bug reports and pull requests are welcome on GitHub at https://github.com/samaswin/pg_sql_triggers.
129
158
 
130
159
  ## License
131
160
 
data/RELEASE.md CHANGED
@@ -152,7 +152,7 @@ gem search pg_sql_triggers
152
152
 
153
153
  ### 11. Create a GitHub Release (Optional but Recommended)
154
154
 
155
- 1. Go to https://github.com/samaswin87/pg_sql_triggers/releases
155
+ 1. Go to https://github.com/samaswin/pg_sql_triggers/releases
156
156
  2. Click "Draft a new release"
157
157
  3. Select the tag you just created (e.g., `v1.0.1`)
158
158
  4. Use the CHANGELOG entry as the release notes
@@ -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