pg_sql_triggers 1.0.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 (59) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/.rubocop.yml +120 -0
  4. data/CHANGELOG.md +52 -0
  5. data/Goal.md +294 -0
  6. data/LICENSE +21 -0
  7. data/README.md +294 -0
  8. data/RELEASE.md +270 -0
  9. data/Rakefile +16 -0
  10. data/app/assets/javascripts/pg_sql_triggers/application.js +5 -0
  11. data/app/assets/stylesheets/pg_sql_triggers/application.css +179 -0
  12. data/app/controllers/pg_sql_triggers/application_controller.rb +35 -0
  13. data/app/controllers/pg_sql_triggers/dashboard_controller.rb +42 -0
  14. data/app/controllers/pg_sql_triggers/generator_controller.rb +145 -0
  15. data/app/controllers/pg_sql_triggers/migrations_controller.rb +84 -0
  16. data/app/controllers/pg_sql_triggers/tables_controller.rb +44 -0
  17. data/app/models/pg_sql_triggers/application_record.rb +7 -0
  18. data/app/models/pg_sql_triggers/trigger_registry.rb +93 -0
  19. data/app/views/layouts/pg_sql_triggers/application.html.erb +72 -0
  20. data/app/views/pg_sql_triggers/dashboard/index.html.erb +225 -0
  21. data/app/views/pg_sql_triggers/generator/new.html.erb +370 -0
  22. data/app/views/pg_sql_triggers/generator/preview.html.erb +77 -0
  23. data/app/views/pg_sql_triggers/tables/index.html.erb +105 -0
  24. data/app/views/pg_sql_triggers/tables/show.html.erb +126 -0
  25. data/config/routes.rb +35 -0
  26. data/db/migrate/20251222000001_create_pg_sql_triggers_tables.rb +29 -0
  27. data/lib/generators/pg_sql_triggers/install_generator.rb +36 -0
  28. data/lib/generators/pg_sql_triggers/templates/README +36 -0
  29. data/lib/generators/pg_sql_triggers/templates/create_pg_sql_triggers_tables.rb +36 -0
  30. data/lib/generators/pg_sql_triggers/templates/initializer.rb +27 -0
  31. data/lib/generators/pg_sql_triggers/templates/trigger_migration.rb.erb +32 -0
  32. data/lib/generators/pg_sql_triggers/trigger_migration_generator.rb +60 -0
  33. data/lib/generators/trigger/migration_generator.rb +60 -0
  34. data/lib/pg_sql_triggers/database_introspection.rb +251 -0
  35. data/lib/pg_sql_triggers/drift.rb +24 -0
  36. data/lib/pg_sql_triggers/dsl/trigger_definition.rb +67 -0
  37. data/lib/pg_sql_triggers/dsl.rb +15 -0
  38. data/lib/pg_sql_triggers/engine.rb +29 -0
  39. data/lib/pg_sql_triggers/generator/form.rb +78 -0
  40. data/lib/pg_sql_triggers/generator/service.rb +251 -0
  41. data/lib/pg_sql_triggers/generator.rb +8 -0
  42. data/lib/pg_sql_triggers/migration.rb +15 -0
  43. data/lib/pg_sql_triggers/migrator.rb +237 -0
  44. data/lib/pg_sql_triggers/permissions/checker.rb +33 -0
  45. data/lib/pg_sql_triggers/permissions.rb +35 -0
  46. data/lib/pg_sql_triggers/registry/manager.rb +47 -0
  47. data/lib/pg_sql_triggers/registry/validator.rb +15 -0
  48. data/lib/pg_sql_triggers/registry.rb +36 -0
  49. data/lib/pg_sql_triggers/sql.rb +21 -0
  50. data/lib/pg_sql_triggers/testing/dry_run.rb +74 -0
  51. data/lib/pg_sql_triggers/testing/function_tester.rb +118 -0
  52. data/lib/pg_sql_triggers/testing/safe_executor.rb +66 -0
  53. data/lib/pg_sql_triggers/testing/syntax_validator.rb +124 -0
  54. data/lib/pg_sql_triggers/testing.rb +10 -0
  55. data/lib/pg_sql_triggers/version.rb +15 -0
  56. data/lib/pg_sql_triggers.rb +41 -0
  57. data/lib/tasks/trigger_migrations.rake +254 -0
  58. data/sig/pg_sql_triggers.rbs +4 -0
  59. metadata +260 -0
@@ -0,0 +1,179 @@
1
+ /*
2
+ * This is a manifest file that'll be compiled into application.css, which will include all the files
3
+ * listed below.
4
+ *
5
+ *= require_self
6
+ */
7
+
8
+ body {
9
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
10
+ margin: 0;
11
+ padding: 0;
12
+ background: #f5f7fa;
13
+ }
14
+
15
+ table {
16
+ width: 100%;
17
+ border-collapse: collapse;
18
+ background: white;
19
+ box-shadow: 0 1px 3px rgba(0,0,0,0.1);
20
+ }
21
+
22
+ th, td {
23
+ padding: 0.75rem;
24
+ text-align: left;
25
+ border-bottom: 1px solid #e1e8ed;
26
+ }
27
+
28
+ th {
29
+ background: #f8f9fa;
30
+ font-weight: 600;
31
+ color: #495057;
32
+ }
33
+
34
+ .badge {
35
+ display: inline-block;
36
+ padding: 0.25rem 0.5rem;
37
+ border-radius: 3px;
38
+ font-size: 0.875rem;
39
+ font-weight: 500;
40
+ }
41
+
42
+ .badge-success {
43
+ background: #d4edda;
44
+ color: #155724;
45
+ }
46
+
47
+ .badge-danger {
48
+ background: #f8d7da;
49
+ color: #721c24;
50
+ }
51
+
52
+ .badge-warning {
53
+ background: #fff3cd;
54
+ color: #856404;
55
+ }
56
+
57
+ .badge-info {
58
+ background: #d1ecf1;
59
+ color: #0c5460;
60
+ }
61
+
62
+ .btn {
63
+ display: inline-block;
64
+ padding: 0.5rem 1rem;
65
+ border-radius: 4px;
66
+ text-decoration: none;
67
+ font-weight: 500;
68
+ cursor: pointer;
69
+ border: none;
70
+ transition: all 0.2s;
71
+ }
72
+
73
+ .btn-primary {
74
+ background: #007bff;
75
+ color: white;
76
+ }
77
+
78
+ .btn-primary:hover {
79
+ background: #0056b3;
80
+ }
81
+
82
+ .btn-danger {
83
+ background: #dc3545;
84
+ color: white;
85
+ }
86
+
87
+ .btn-danger:hover {
88
+ background: #c82333;
89
+ }
90
+
91
+ .btn-success {
92
+ background: #28a745;
93
+ color: white;
94
+ }
95
+
96
+ .btn-success:hover {
97
+ background: #218838;
98
+ }
99
+
100
+ /* Form styles */
101
+ fieldset {
102
+ border: 1px solid #dee2e6;
103
+ padding: 1rem;
104
+ margin-bottom: 2rem;
105
+ border-radius: 4px;
106
+ }
107
+
108
+ legend {
109
+ font-weight: 600;
110
+ color: #495057;
111
+ padding: 0 0.5rem;
112
+ width: auto;
113
+ }
114
+
115
+ input[type="text"],
116
+ input[type="number"],
117
+ select,
118
+ textarea {
119
+ width: 100%;
120
+ padding: 0.5rem;
121
+ border: 1px solid #ced4da;
122
+ border-radius: 4px;
123
+ font-family: inherit;
124
+ }
125
+
126
+ input[type="text"]:focus,
127
+ input[type="number"]:focus,
128
+ select:focus,
129
+ textarea:focus {
130
+ outline: none;
131
+ border-color: #007bff;
132
+ box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
133
+ }
134
+
135
+ /* Code blocks */
136
+ pre {
137
+ background: #f8f9fa;
138
+ padding: 1rem;
139
+ border-radius: 4px;
140
+ overflow-x: auto;
141
+ border: 1px solid #dee2e6;
142
+ }
143
+
144
+ code {
145
+ font-family: 'Monaco', 'Menlo', 'Consolas', monospace;
146
+ font-size: 0.9rem;
147
+ }
148
+
149
+ /* Alert boxes */
150
+ .alert {
151
+ padding: 1rem;
152
+ border-radius: 4px;
153
+ margin-bottom: 1rem;
154
+ border-left: 4px solid;
155
+ }
156
+
157
+ .alert-info {
158
+ background: #d1ecf1;
159
+ border-color: #0c5460;
160
+ color: #0c5460;
161
+ }
162
+
163
+ .alert-warning {
164
+ background: #fff3cd;
165
+ border-color: #ffc107;
166
+ color: #856404;
167
+ }
168
+
169
+ .alert-success {
170
+ background: #d4edda;
171
+ border-color: #28a745;
172
+ color: #155724;
173
+ }
174
+
175
+ .alert-danger {
176
+ background: #f8d7da;
177
+ border-color: #dc3545;
178
+ color: #721c24;
179
+ }
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgSqlTriggers
4
+ class ApplicationController < ActionController::Base
5
+ include PgSqlTriggers::Engine.routes.url_helpers
6
+
7
+ protect_from_forgery with: :exception
8
+ layout "pg_sql_triggers/application"
9
+
10
+ before_action :check_permissions?
11
+
12
+ private
13
+
14
+ def check_permissions?
15
+ # Override this method in host application to implement custom permission checks
16
+ true
17
+ end
18
+
19
+ def current_actor
20
+ # Override this in host application to provide actual user
21
+ {
22
+ type: current_user_type,
23
+ id: current_user_id
24
+ }
25
+ end
26
+
27
+ def current_user_type
28
+ "User"
29
+ end
30
+
31
+ def current_user_id
32
+ "unknown"
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgSqlTriggers
4
+ class DashboardController < ApplicationController
5
+ def index
6
+ @triggers = PgSqlTriggers::TriggerRegistry.order(created_at: :desc)
7
+ @stats = {
8
+ total: @triggers.count,
9
+ enabled: @triggers.enabled.count,
10
+ disabled: @triggers.disabled.count,
11
+ drifted: 0 # Will be calculated by Drift::Detector
12
+ }
13
+
14
+ # Migration status with pagination
15
+ begin
16
+ all_migrations = PgSqlTriggers::Migrator.status
17
+ @pending_migrations = PgSqlTriggers::Migrator.pending_migrations
18
+ @current_migration_version = PgSqlTriggers::Migrator.current_version
19
+
20
+ # Pagination
21
+ @per_page = (params[:per_page] || 20).to_i
22
+ @per_page = [@per_page, 100].min # Cap at 100
23
+ @page = (params[:page] || 1).to_i
24
+ @total_migrations = all_migrations.count
25
+ @total_pages = @total_migrations.positive? ? (@total_migrations.to_f / @per_page).ceil : 1
26
+ @page = @page.clamp(1, @total_pages) # Ensure page is within valid range
27
+
28
+ offset = (@page - 1) * @per_page
29
+ @migration_status = all_migrations.slice(offset, @per_page) || []
30
+ rescue StandardError => e
31
+ Rails.logger.error("Failed to fetch migration status: #{e.message}")
32
+ @migration_status = []
33
+ @pending_migrations = []
34
+ @current_migration_version = 0
35
+ @total_migrations = 0
36
+ @total_pages = 1
37
+ @page = 1
38
+ @per_page = 20
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,145 @@
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
+ @form = PgSqlTriggers::Generator::Form.new
12
+ @available_tables = fetch_available_tables
13
+ end
14
+
15
+ # POST /generator/preview
16
+ # Preview generated DSL and function stub (AJAX or regular POST)
17
+ def preview
18
+ @form = PgSqlTriggers::Generator::Form.new(generator_params)
19
+
20
+ if @form.valid?
21
+ # Validate SQL function body (required field)
22
+ @sql_validation = validate_function_sql(@form)
23
+
24
+ @dsl_content = PgSqlTriggers::Generator::Service.generate_dsl(@form)
25
+ # Use function_body (required)
26
+ @function_content = @form.function_body
27
+ @file_paths = PgSqlTriggers::Generator::Service.file_paths(@form)
28
+
29
+ render :preview
30
+ else
31
+ @available_tables = fetch_available_tables
32
+ render :new
33
+ end
34
+ end
35
+
36
+ # POST /generator/create
37
+ # Actually create the files and register in TriggerRegistry
38
+ def create
39
+ @form = PgSqlTriggers::Generator::Form.new(generator_params)
40
+
41
+ if @form.valid?
42
+ # Validate SQL function body (required field)
43
+ sql_validation = validate_function_sql(@form)
44
+ unless sql_validation[:valid]
45
+ flash.now[:alert] = "Cannot create trigger: SQL validation failed - #{sql_validation[:error]}"
46
+ @available_tables = fetch_available_tables
47
+ @dsl_content = PgSqlTriggers::Generator::Service.generate_dsl(@form)
48
+ @function_content = @form.function_body
49
+ @file_paths = PgSqlTriggers::Generator::Service.file_paths(@form)
50
+ @sql_validation = sql_validation
51
+ render :preview
52
+ return
53
+ end
54
+
55
+ result = PgSqlTriggers::Generator::Service.create_trigger(@form, actor: current_actor)
56
+
57
+ if result[:success]
58
+ files_msg = "Migration: #{result[:migration_path]}, DSL: #{result[:dsl_path]}"
59
+ redirect_to root_path,
60
+ notice: "Trigger generated successfully. Files created: #{files_msg}"
61
+ else
62
+ flash[:alert] = "Generation failed: #{result[:error]}"
63
+ @available_tables = fetch_available_tables
64
+ render :new
65
+ end
66
+ else
67
+ @available_tables = fetch_available_tables
68
+ render :new
69
+ end
70
+ end
71
+
72
+ # POST /generator/validate_table (AJAX)
73
+ # Validate that table exists in database
74
+ def validate_table
75
+ # Extract table_name from JSON request body
76
+ # Rails should parse JSON automatically, but handle both cases
77
+ table_name = extract_table_name_from_request
78
+
79
+ if table_name.blank?
80
+ render json: { valid: false, error: "Table name is required" }, status: :bad_request
81
+ return
82
+ end
83
+
84
+ validator = PgSqlTriggers::DatabaseIntrospection.new
85
+ result = validator.validate_table(table_name)
86
+ render json: result
87
+ end
88
+
89
+ # GET /generator/tables (AJAX)
90
+ # Fetch list of tables for dropdown
91
+ def tables
92
+ tables = fetch_available_tables
93
+ render json: { tables: tables }
94
+ end
95
+
96
+ private
97
+
98
+ def generator_params
99
+ params.expect(
100
+ pg_sql_triggers_generator_form: [:trigger_name, :table_name, :function_name, :version,
101
+ :enabled, :condition, :generate_function_stub, :function_body,
102
+ { events: [], environments: [] }]
103
+ )
104
+ end
105
+
106
+ def check_operator_permission
107
+ return if PgSqlTriggers::Permissions.can?(current_actor, :apply_trigger)
108
+
109
+ redirect_to root_path, alert: "Insufficient permissions. Operator role required."
110
+ end
111
+
112
+ def fetch_available_tables
113
+ PgSqlTriggers::DatabaseIntrospection.new.list_tables
114
+ rescue StandardError => e
115
+ Rails.logger.error("Failed to fetch tables: #{e.message}")
116
+ []
117
+ end
118
+
119
+ def extract_table_name_from_request
120
+ # Rails automatically parses JSON request bodies when Content-Type is application/json
121
+ # The parameters are available directly in params
122
+ table_name = params[:table_name]
123
+
124
+ # If not found, try accessing as string key (some Rails versions use string keys for JSON)
125
+ table_name ||= params["table_name"] if params.key?("table_name")
126
+
127
+ table_name
128
+ end
129
+
130
+ def validate_function_sql(form)
131
+ return nil if form.function_body.blank?
132
+
133
+ # Create a temporary trigger registry object for validation
134
+ temp_registry = PgSqlTriggers::TriggerRegistry.new(
135
+ trigger_name: form.trigger_name,
136
+ function_body: form.function_body
137
+ )
138
+
139
+ validator = PgSqlTriggers::Testing::SyntaxValidator.new(temp_registry)
140
+ validator.validate_function_syntax
141
+ rescue StandardError => e
142
+ { valid: false, error: "Validation error: #{e.message}" }
143
+ end
144
+ end
145
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgSqlTriggers
4
+ # Controller for managing trigger migrations via web UI
5
+ # Provides actions to run migrations up, down, and redo
6
+ class MigrationsController < ApplicationController
7
+ def up
8
+ target_version = params[:version]&.to_i
9
+ PgSqlTriggers::Migrator.ensure_migrations_table!
10
+
11
+ if target_version
12
+ PgSqlTriggers::Migrator.run_up(target_version)
13
+ flash[:success] = "Migration #{target_version} applied successfully."
14
+ else
15
+ pending = PgSqlTriggers::Migrator.pending_migrations
16
+ if pending.any?
17
+ PgSqlTriggers::Migrator.run_up
18
+ count = pending.count
19
+ flash[:success] = "Applied #{count} pending migration(s) successfully."
20
+ else
21
+ flash[:info] = "No pending migrations to apply."
22
+ end
23
+ end
24
+ redirect_to root_path
25
+ rescue StandardError => e
26
+ Rails.logger.error("Migration up failed: #{e.message}\n#{e.backtrace.join("\n")}")
27
+ flash[:error] = "Failed to apply migration: #{e.message}"
28
+ redirect_to root_path
29
+ end
30
+
31
+ def down
32
+ target_version = params[:version]&.to_i
33
+ PgSqlTriggers::Migrator.ensure_migrations_table!
34
+
35
+ current_version = PgSqlTriggers::Migrator.current_version
36
+ if current_version.zero?
37
+ flash[:warning] = "No migrations to rollback."
38
+ redirect_to root_path
39
+ return
40
+ end
41
+
42
+ if target_version
43
+ PgSqlTriggers::Migrator.run_down(target_version)
44
+ flash[:success] = "Migration version #{target_version} rolled back successfully."
45
+ else
46
+ # Rollback one migration by default
47
+ PgSqlTriggers::Migrator.run_down
48
+ flash[:success] = "Rolled back last migration successfully."
49
+ end
50
+ redirect_to root_path
51
+ rescue StandardError => e
52
+ Rails.logger.error("Migration down failed: #{e.message}\n#{e.backtrace.join("\n")}")
53
+ flash[:error] = "Failed to rollback migration: #{e.message}"
54
+ redirect_to root_path
55
+ end
56
+
57
+ def redo
58
+ target_version = params[:version]&.to_i
59
+ PgSqlTriggers::Migrator.ensure_migrations_table!
60
+
61
+ if target_version
62
+ PgSqlTriggers::Migrator.run_down(target_version)
63
+ PgSqlTriggers::Migrator.run_up(target_version)
64
+ flash[:success] = "Migration #{target_version} redone successfully."
65
+ else
66
+ current_version = PgSqlTriggers::Migrator.current_version
67
+ if current_version.zero?
68
+ flash[:warning] = "No migrations to redo."
69
+ redirect_to root_path
70
+ return
71
+ end
72
+
73
+ PgSqlTriggers::Migrator.run_down
74
+ PgSqlTriggers::Migrator.run_up
75
+ flash[:success] = "Last migration redone successfully."
76
+ end
77
+ redirect_to root_path
78
+ rescue StandardError => e
79
+ Rails.logger.error("Migration redo failed: #{e.message}\n#{e.backtrace.join("\n")}")
80
+ flash[:error] = "Failed to redo migration: #{e.message}"
81
+ redirect_to root_path
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgSqlTriggers
4
+ class TablesController < ApplicationController
5
+ def index
6
+ all_tables = PgSqlTriggers::DatabaseIntrospection.new.tables_with_triggers
7
+ # Only show tables that have at least one trigger
8
+ @tables_with_triggers = all_tables.select { |t| t[:trigger_count].positive? }
9
+ @total_tables = @tables_with_triggers.count
10
+ @tables_with_trigger_count = @tables_with_triggers.count
11
+ end
12
+
13
+ def show
14
+ @table_info = PgSqlTriggers::DatabaseIntrospection.new.table_triggers(params[:id])
15
+ @columns = PgSqlTriggers::DatabaseIntrospection.new.table_columns(params[:id])
16
+
17
+ respond_to do |format|
18
+ format.html
19
+ format.json do
20
+ render json: {
21
+ table_name: @table_info[:table_name],
22
+ registry_triggers: @table_info[:registry_triggers].map do |t|
23
+ {
24
+ id: t.id,
25
+ trigger_name: t.trigger_name,
26
+ function_name: if t.definition.present?
27
+ begin
28
+ JSON.parse(t.definition)
29
+ rescue StandardError
30
+ {}
31
+ end["function_name"]
32
+ end,
33
+ enabled: t.enabled,
34
+ version: t.version,
35
+ source: t.source
36
+ }
37
+ end,
38
+ database_triggers: @table_info[:database_triggers]
39
+ }
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgSqlTriggers
4
+ class ApplicationRecord < ActiveRecord::Base
5
+ self.abstract_class = true
6
+ end
7
+ end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgSqlTriggers
4
+ class TriggerRegistry < PgSqlTriggers::ApplicationRecord
5
+ self.table_name = "pg_sql_triggers_registry"
6
+
7
+ # Validations
8
+ validates :trigger_name, presence: true, uniqueness: true
9
+ validates :table_name, presence: true
10
+ validates :version, presence: true, numericality: { only_integer: true, greater_than: 0 }
11
+ validates :checksum, presence: true
12
+ validates :source, presence: true, inclusion: { in: %w[dsl generated manual_sql] }
13
+
14
+ # Scopes
15
+ scope :enabled, -> { where(enabled: true) }
16
+ scope :disabled, -> { where(enabled: false) }
17
+ scope :for_table, ->(table_name) { where(table_name: table_name) }
18
+ scope :for_environment, ->(env) { where(environment: [env, nil]) }
19
+ scope :by_source, ->(source) { where(source: source) }
20
+
21
+ # Drift states
22
+ def drift_state
23
+ # This will be implemented by the Drift::Detector
24
+ PgSqlTriggers::Drift.detect(trigger_name)
25
+ end
26
+
27
+ def enable!
28
+ # Check if trigger exists in database before trying to enable it
29
+ trigger_exists = false
30
+ begin
31
+ introspection = PgSqlTriggers::DatabaseIntrospection.new
32
+ trigger_exists = introspection.trigger_exists?(trigger_name)
33
+ rescue StandardError => e
34
+ # If checking fails, assume trigger doesn't exist and continue
35
+ Rails.logger.warn("Could not check if trigger exists: #{e.message}") if defined?(Rails.logger)
36
+ end
37
+
38
+ if trigger_exists
39
+ begin
40
+ # Enable the trigger in PostgreSQL
41
+ sql = "ALTER TABLE #{quote_identifier(table_name)} ENABLE TRIGGER #{quote_identifier(trigger_name)};"
42
+ ActiveRecord::Base.connection.execute(sql)
43
+ rescue ActiveRecord::StatementInvalid => e
44
+ # If trigger doesn't exist or can't be enabled, continue to update registry
45
+ Rails.logger.warn("Could not enable trigger: #{e.message}") if defined?(Rails.logger)
46
+ end
47
+ end
48
+
49
+ # Update the registry record (always update, even if trigger doesn't exist)
50
+ update!(enabled: true)
51
+ end
52
+
53
+ def disable!
54
+ # Check if trigger exists in database before trying to disable it
55
+ trigger_exists = false
56
+ begin
57
+ introspection = PgSqlTriggers::DatabaseIntrospection.new
58
+ trigger_exists = introspection.trigger_exists?(trigger_name)
59
+ rescue StandardError => e
60
+ # If checking fails, assume trigger doesn't exist and continue
61
+ Rails.logger.warn("Could not check if trigger exists: #{e.message}") if defined?(Rails.logger)
62
+ end
63
+
64
+ if trigger_exists
65
+ begin
66
+ # Disable the trigger in PostgreSQL
67
+ sql = "ALTER TABLE #{quote_identifier(table_name)} DISABLE TRIGGER #{quote_identifier(trigger_name)};"
68
+ ActiveRecord::Base.connection.execute(sql)
69
+ rescue ActiveRecord::StatementInvalid => e
70
+ # If trigger doesn't exist or can't be disabled, continue to update registry
71
+ Rails.logger.warn("Could not disable trigger: #{e.message}") if defined?(Rails.logger)
72
+ end
73
+ end
74
+
75
+ # Update the registry record (always update, even if trigger doesn't exist)
76
+ update!(enabled: false)
77
+ end
78
+
79
+ private
80
+
81
+ def quote_identifier(identifier)
82
+ ActiveRecord::Base.connection.quote_table_name(identifier.to_s)
83
+ end
84
+
85
+ def calculate_checksum
86
+ Digest::SHA256.hexdigest([trigger_name, table_name, version, function_body, condition].join)
87
+ end
88
+
89
+ def verify!
90
+ update!(last_verified_at: Time.current)
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,72 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>PgSqlTriggers - PostgreSQL Trigger Control Plane</title>
5
+ <%= csrf_meta_tags %>
6
+ <%= csp_meta_tag %>
7
+
8
+ <%= stylesheet_link_tag "pg_sql_triggers/application", media: "all" %>
9
+ <%= javascript_include_tag "pg_sql_triggers/application" %>
10
+ </head>
11
+
12
+ <body>
13
+ <nav style="background: #2c3e50; padding: 1rem; color: white;">
14
+ <div style="max-width: 1200px; margin: 0 auto; display: flex; justify-content: space-between; align-items: center;">
15
+ <h1 style="margin: 0; font-size: 1.5rem;">
16
+ <%= link_to "PgSqlTriggers", root_path, style: "color: white; text-decoration: none;" %>
17
+ </h1>
18
+ <div>
19
+ <%= link_to "Dashboard", root_path, style: "color: white; margin-right: 1rem;" %>
20
+ <%= link_to "Tables", tables_path, style: "color: white; margin-right: 1rem;" %>
21
+ <%= link_to "Generator", new_generator_path, style: "color: white;" %>
22
+ </div>
23
+ </div>
24
+ </nav>
25
+
26
+ <main style="max-width: 1200px; margin: 2rem auto; padding: 0 1rem;">
27
+ <% if flash[:success] %>
28
+ <div style="background: #d4edda; color: #155724; padding: 1rem; margin-bottom: 1rem; border-radius: 4px; border-left: 4px solid #28a745;">
29
+ <%= flash[:success] %>
30
+ </div>
31
+ <% end %>
32
+
33
+ <% if flash[:error] %>
34
+ <div style="background: #f8d7da; color: #721c24; padding: 1rem; margin-bottom: 1rem; border-radius: 4px; border-left: 4px solid #dc3545;">
35
+ <%= flash[:error] %>
36
+ </div>
37
+ <% end %>
38
+
39
+ <% if flash[:warning] %>
40
+ <div style="background: #fff3cd; color: #856404; padding: 1rem; margin-bottom: 1rem; border-radius: 4px; border-left: 4px solid #ffc107;">
41
+ <%= flash[:warning] %>
42
+ </div>
43
+ <% end %>
44
+
45
+ <% if flash[:info] %>
46
+ <div style="background: #d1ecf1; color: #0c5460; padding: 1rem; margin-bottom: 1rem; border-radius: 4px; border-left: 4px solid #17a2b8;">
47
+ <%= flash[:info] %>
48
+ </div>
49
+ <% end %>
50
+
51
+ <% if flash[:notice] %>
52
+ <div style="background: #d4edda; color: #155724; padding: 1rem; margin-bottom: 1rem; border-radius: 4px; border-left: 4px solid #28a745;">
53
+ <%= flash[:notice] %>
54
+ </div>
55
+ <% end %>
56
+
57
+ <% if flash[:alert] %>
58
+ <div style="background: #f8d7da; color: #721c24; padding: 1rem; margin-bottom: 1rem; border-radius: 4px; border-left: 4px solid #dc3545;">
59
+ <%= flash[:alert] %>
60
+ </div>
61
+ <% end %>
62
+
63
+ <%= yield %>
64
+ </main>
65
+
66
+ <footer style="background: #ecf0f1; padding: 2rem; margin-top: 4rem; text-align: center;">
67
+ <p style="margin: 0; color: #7f8c8d;">
68
+ PgSqlTriggers - A PostgreSQL Trigger Control Plane for Rails
69
+ </p>
70
+ </footer>
71
+ </body>
72
+ </html>