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 +4 -4
- data/.rubocop.yml +15 -0
- data/CHANGELOG.md +56 -0
- data/COVERAGE.md +32 -28
- data/README.md +29 -0
- 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/api-reference.md +252 -4
- data/docs/web-ui.md +82 -17
- 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 +10 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 54f76c538693c37b1572cb914462269eb8c3600473c1a7c26efda0a7e02eefe5
|
|
4
|
+
data.tar.gz: 8574ff3bb8f835a11f8cec850c5adbff7f17be65a1461b8f8e87242ab3e50b62
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
3
|
+
**Total Coverage: 95.33%**
|
|
4
4
|
|
|
5
|
-
Covered:
|
|
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% ✅ |
|
|
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
|
-
| `
|
|
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
|
-
| `
|
|
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
|
-
| `
|
|
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
|
-
| `
|
|
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/
|
|
51
|
-
| `app/
|
|
52
|
-
| `
|
|
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
|
-
|
|
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
|