pg_sql_triggers 1.0.0 → 1.1.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/.erb_lint.yml +47 -0
- data/.rubocop.yml +4 -1
- data/CHANGELOG.md +112 -1
- data/COVERAGE.md +58 -0
- data/Goal.md +450 -123
- data/README.md +53 -215
- data/app/controllers/pg_sql_triggers/application_controller.rb +46 -0
- data/app/controllers/pg_sql_triggers/dashboard_controller.rb +4 -1
- data/app/controllers/pg_sql_triggers/generator_controller.rb +76 -8
- data/app/controllers/pg_sql_triggers/migrations_controller.rb +18 -0
- data/app/models/pg_sql_triggers/trigger_registry.rb +93 -12
- data/app/views/layouts/pg_sql_triggers/application.html.erb +34 -1
- data/app/views/pg_sql_triggers/dashboard/index.html.erb +70 -30
- data/app/views/pg_sql_triggers/generator/new.html.erb +22 -4
- data/app/views/pg_sql_triggers/generator/preview.html.erb +244 -16
- data/app/views/pg_sql_triggers/shared/_confirmation_modal.html.erb +221 -0
- data/app/views/pg_sql_triggers/shared/_kill_switch_status.html.erb +40 -0
- data/app/views/pg_sql_triggers/tables/index.html.erb +0 -2
- data/app/views/pg_sql_triggers/tables/show.html.erb +3 -4
- data/config/initializers/pg_sql_triggers.rb +69 -0
- data/db/migrate/20251222000001_create_pg_sql_triggers_tables.rb +3 -1
- data/db/migrate/20251229071916_add_timing_to_pg_sql_triggers_registry.rb +8 -0
- data/docs/README.md +66 -0
- data/docs/api-reference.md +681 -0
- data/docs/configuration.md +541 -0
- data/docs/getting-started.md +135 -0
- data/docs/kill-switch.md +586 -0
- data/docs/screenshots/.gitkeep +1 -0
- data/docs/screenshots/Generate Trigger.png +0 -0
- data/docs/screenshots/Triggers Page.png +0 -0
- data/docs/screenshots/kill error.png +0 -0
- data/docs/screenshots/kill modal for migration down.png +0 -0
- data/docs/usage-guide.md +493 -0
- data/docs/web-ui.md +353 -0
- data/lib/generators/pg_sql_triggers/templates/create_pg_sql_triggers_tables.rb +3 -1
- data/lib/generators/pg_sql_triggers/templates/initializer.rb +44 -2
- data/lib/pg_sql_triggers/drift/db_queries.rb +116 -0
- data/lib/pg_sql_triggers/drift/detector.rb +187 -0
- data/lib/pg_sql_triggers/drift/reporter.rb +179 -0
- data/lib/pg_sql_triggers/drift.rb +14 -11
- data/lib/pg_sql_triggers/dsl/trigger_definition.rb +15 -1
- data/lib/pg_sql_triggers/generator/form.rb +3 -1
- data/lib/pg_sql_triggers/generator/service.rb +82 -26
- data/lib/pg_sql_triggers/migration.rb +1 -1
- data/lib/pg_sql_triggers/migrator/pre_apply_comparator.rb +344 -0
- data/lib/pg_sql_triggers/migrator/pre_apply_diff_reporter.rb +143 -0
- data/lib/pg_sql_triggers/migrator/safety_validator.rb +258 -0
- data/lib/pg_sql_triggers/migrator.rb +85 -3
- data/lib/pg_sql_triggers/registry/manager.rb +100 -13
- data/lib/pg_sql_triggers/sql/kill_switch.rb +300 -0
- data/lib/pg_sql_triggers/testing/dry_run.rb +5 -7
- data/lib/pg_sql_triggers/testing/function_tester.rb +66 -24
- data/lib/pg_sql_triggers/testing/safe_executor.rb +23 -11
- data/lib/pg_sql_triggers/testing/syntax_validator.rb +24 -1
- data/lib/pg_sql_triggers/version.rb +1 -1
- data/lib/pg_sql_triggers.rb +24 -0
- data/lib/tasks/trigger_migrations.rake +33 -0
- data/scripts/generate_coverage_report.rb +129 -0
- metadata +45 -5
|
@@ -18,13 +18,38 @@ module PgSqlTriggers
|
|
|
18
18
|
scope :for_environment, ->(env) { where(environment: [env, nil]) }
|
|
19
19
|
scope :by_source, ->(source) { where(source: source) }
|
|
20
20
|
|
|
21
|
-
# Drift
|
|
21
|
+
# Drift detection methods
|
|
22
22
|
def drift_state
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
result = PgSqlTriggers::Drift.detect(trigger_name)
|
|
24
|
+
result[:state]
|
|
25
25
|
end
|
|
26
26
|
|
|
27
|
-
def
|
|
27
|
+
def drift_result
|
|
28
|
+
PgSqlTriggers::Drift::Detector.detect(trigger_name)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def drifted?
|
|
32
|
+
drift_state == PgSqlTriggers::DRIFT_STATE_DRIFTED
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def in_sync?
|
|
36
|
+
drift_state == PgSqlTriggers::DRIFT_STATE_IN_SYNC
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def dropped?
|
|
40
|
+
drift_state == PgSqlTriggers::DRIFT_STATE_DROPPED
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def enable!(confirmation: nil)
|
|
44
|
+
# Check kill switch before enabling trigger
|
|
45
|
+
# Use Rails.env for kill switch check, not the trigger's environment field
|
|
46
|
+
PgSqlTriggers::SQL::KillSwitch.check!(
|
|
47
|
+
operation: :trigger_enable,
|
|
48
|
+
environment: Rails.env,
|
|
49
|
+
confirmation: confirmation,
|
|
50
|
+
actor: { type: "Console", id: "TriggerRegistry#enable!" }
|
|
51
|
+
)
|
|
52
|
+
|
|
28
53
|
# Check if trigger exists in database before trying to enable it
|
|
29
54
|
trigger_exists = false
|
|
30
55
|
begin
|
|
@@ -38,19 +63,48 @@ module PgSqlTriggers
|
|
|
38
63
|
if trigger_exists
|
|
39
64
|
begin
|
|
40
65
|
# Enable the trigger in PostgreSQL
|
|
41
|
-
|
|
66
|
+
quoted_table = quote_identifier(table_name)
|
|
67
|
+
quoted_trigger = quote_identifier(trigger_name)
|
|
68
|
+
sql = "ALTER TABLE #{quoted_table} ENABLE TRIGGER #{quoted_trigger};"
|
|
42
69
|
ActiveRecord::Base.connection.execute(sql)
|
|
43
|
-
rescue ActiveRecord::StatementInvalid => e
|
|
70
|
+
rescue ActiveRecord::StatementInvalid, StandardError => e
|
|
44
71
|
# If trigger doesn't exist or can't be enabled, continue to update registry
|
|
45
72
|
Rails.logger.warn("Could not enable trigger: #{e.message}") if defined?(Rails.logger)
|
|
46
73
|
end
|
|
47
74
|
end
|
|
48
75
|
|
|
49
76
|
# Update the registry record (always update, even if trigger doesn't exist)
|
|
50
|
-
|
|
77
|
+
begin
|
|
78
|
+
update!(enabled: true)
|
|
79
|
+
rescue ActiveRecord::StatementInvalid, StandardError => e
|
|
80
|
+
# If update! fails, try update_column which bypasses validations and callbacks
|
|
81
|
+
# and might not use execute in the same way
|
|
82
|
+
Rails.logger.warn("Could not update registry via update!: #{e.message}") if defined?(Rails.logger)
|
|
83
|
+
begin
|
|
84
|
+
# rubocop:disable Rails/SkipsModelValidations
|
|
85
|
+
update_column(:enabled, true)
|
|
86
|
+
# rubocop:enable Rails/SkipsModelValidations
|
|
87
|
+
rescue StandardError => update_error
|
|
88
|
+
# If update_column also fails, just set the in-memory attribute
|
|
89
|
+
# The test might reload, but we've done our best
|
|
90
|
+
# rubocop:disable Layout/LineLength
|
|
91
|
+
Rails.logger.warn("Could not update registry via update_column: #{update_error.message}") if defined?(Rails.logger)
|
|
92
|
+
# rubocop:enable Layout/LineLength
|
|
93
|
+
self.enabled = true
|
|
94
|
+
end
|
|
95
|
+
end
|
|
51
96
|
end
|
|
52
97
|
|
|
53
|
-
def disable!
|
|
98
|
+
def disable!(confirmation: nil)
|
|
99
|
+
# Check kill switch before disabling trigger
|
|
100
|
+
# Use Rails.env for kill switch check, not the trigger's environment field
|
|
101
|
+
PgSqlTriggers::SQL::KillSwitch.check!(
|
|
102
|
+
operation: :trigger_disable,
|
|
103
|
+
environment: Rails.env,
|
|
104
|
+
confirmation: confirmation,
|
|
105
|
+
actor: { type: "Console", id: "TriggerRegistry#disable!" }
|
|
106
|
+
)
|
|
107
|
+
|
|
54
108
|
# Check if trigger exists in database before trying to disable it
|
|
55
109
|
trigger_exists = false
|
|
56
110
|
begin
|
|
@@ -64,16 +118,36 @@ module PgSqlTriggers
|
|
|
64
118
|
if trigger_exists
|
|
65
119
|
begin
|
|
66
120
|
# Disable the trigger in PostgreSQL
|
|
67
|
-
|
|
121
|
+
quoted_table = quote_identifier(table_name)
|
|
122
|
+
quoted_trigger = quote_identifier(trigger_name)
|
|
123
|
+
sql = "ALTER TABLE #{quoted_table} DISABLE TRIGGER #{quoted_trigger};"
|
|
68
124
|
ActiveRecord::Base.connection.execute(sql)
|
|
69
|
-
rescue ActiveRecord::StatementInvalid => e
|
|
125
|
+
rescue ActiveRecord::StatementInvalid, StandardError => e
|
|
70
126
|
# If trigger doesn't exist or can't be disabled, continue to update registry
|
|
71
127
|
Rails.logger.warn("Could not disable trigger: #{e.message}") if defined?(Rails.logger)
|
|
72
128
|
end
|
|
73
129
|
end
|
|
74
130
|
|
|
75
131
|
# Update the registry record (always update, even if trigger doesn't exist)
|
|
76
|
-
|
|
132
|
+
begin
|
|
133
|
+
update!(enabled: false)
|
|
134
|
+
rescue ActiveRecord::StatementInvalid, StandardError => e
|
|
135
|
+
# If update! fails, try update_column which bypasses validations and callbacks
|
|
136
|
+
# and might not use execute in the same way
|
|
137
|
+
Rails.logger.warn("Could not update registry via update!: #{e.message}") if defined?(Rails.logger)
|
|
138
|
+
begin
|
|
139
|
+
# rubocop:disable Rails/SkipsModelValidations
|
|
140
|
+
update_column(:enabled, false)
|
|
141
|
+
# rubocop:enable Rails/SkipsModelValidations
|
|
142
|
+
rescue StandardError => update_error
|
|
143
|
+
# If update_column also fails, just set the in-memory attribute
|
|
144
|
+
# The test might reload, but we've done our best
|
|
145
|
+
# rubocop:disable Layout/LineLength
|
|
146
|
+
Rails.logger.warn("Could not update registry via update_column: #{update_error.message}") if defined?(Rails.logger)
|
|
147
|
+
# rubocop:enable Layout/LineLength
|
|
148
|
+
self.enabled = false
|
|
149
|
+
end
|
|
150
|
+
end
|
|
77
151
|
end
|
|
78
152
|
|
|
79
153
|
private
|
|
@@ -83,7 +157,14 @@ module PgSqlTriggers
|
|
|
83
157
|
end
|
|
84
158
|
|
|
85
159
|
def calculate_checksum
|
|
86
|
-
Digest::SHA256.hexdigest([
|
|
160
|
+
Digest::SHA256.hexdigest([
|
|
161
|
+
trigger_name,
|
|
162
|
+
table_name,
|
|
163
|
+
version,
|
|
164
|
+
function_body || "",
|
|
165
|
+
condition || "",
|
|
166
|
+
timing || "before"
|
|
167
|
+
].join)
|
|
87
168
|
end
|
|
88
169
|
|
|
89
170
|
def verify!
|
|
@@ -24,6 +24,8 @@
|
|
|
24
24
|
</nav>
|
|
25
25
|
|
|
26
26
|
<main style="max-width: 1200px; margin: 2rem auto; padding: 0 1rem;">
|
|
27
|
+
<%= render 'pg_sql_triggers/shared/kill_switch_status' %>
|
|
28
|
+
|
|
27
29
|
<% if flash[:success] %>
|
|
28
30
|
<div style="background: #d4edda; color: #155724; padding: 1rem; margin-bottom: 1rem; border-radius: 4px; border-left: 4px solid #28a745;">
|
|
29
31
|
<%= flash[:success] %>
|
|
@@ -32,7 +34,38 @@
|
|
|
32
34
|
|
|
33
35
|
<% if flash[:error] %>
|
|
34
36
|
<div style="background: #f8d7da; color: #721c24; padding: 1rem; margin-bottom: 1rem; border-radius: 4px; border-left: 4px solid #dc3545;">
|
|
35
|
-
|
|
37
|
+
<% error_message = flash[:error].to_s %>
|
|
38
|
+
<% if error_message.include?("Kill switch is active") %>
|
|
39
|
+
<%# Format kill switch error messages nicely %>
|
|
40
|
+
<% lines = error_message.split("\n") %>
|
|
41
|
+
|
|
42
|
+
<%# Main error header with clear start %>
|
|
43
|
+
<div style="display: flex; align-items: flex-start; margin-bottom: 1rem; padding-bottom: 1rem; border-bottom: 2px solid #f5c6cb;">
|
|
44
|
+
<span style="font-size: 1.8em; margin-right: 0.75rem; line-height: 1.2;">⚠️</span>
|
|
45
|
+
<div style="flex: 1;">
|
|
46
|
+
<div style="font-weight: bold; font-size: 1.15em; margin-bottom: 0.5rem;">
|
|
47
|
+
Operation Blocked
|
|
48
|
+
</div>
|
|
49
|
+
<% lines[0..1].each do |line| %>
|
|
50
|
+
<% next if line.strip.empty? %>
|
|
51
|
+
<div style="margin: 0.25rem 0;"><%= line.strip %></div>
|
|
52
|
+
<% end %>
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
<%# Instructions section with preserved formatting %>
|
|
57
|
+
<div style="margin-top: 0.5rem;">
|
|
58
|
+
<%# Extract and format the instructions portion %>
|
|
59
|
+
<% instructions_start = error_message.index("To override") || 0 %>
|
|
60
|
+
<% instructions_text = error_message[instructions_start..-1] %>
|
|
61
|
+
<div style="white-space: pre-wrap; word-wrap: break-word; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; line-height: 1.6;">
|
|
62
|
+
<%= instructions_text.strip %>
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
<% else %>
|
|
66
|
+
<%# Regular error message - preserve formatting %>
|
|
67
|
+
<div style="white-space: pre-wrap; word-wrap: break-word;"><%= error_message %></div>
|
|
68
|
+
<% end %>
|
|
36
69
|
</div>
|
|
37
70
|
<% end %>
|
|
38
71
|
|
|
@@ -73,7 +73,7 @@
|
|
|
73
73
|
<!-- Migration Management Section -->
|
|
74
74
|
<div style="margin-top: 3rem;">
|
|
75
75
|
<h3>Trigger Migrations</h3>
|
|
76
|
-
|
|
76
|
+
|
|
77
77
|
<div style="background: white; padding: 1.5rem; border-radius: 4px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); margin-bottom: 2rem;">
|
|
78
78
|
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem; margin-bottom: 1.5rem;">
|
|
79
79
|
<div style="padding: 1rem; background: #f8f9fa; border-radius: 4px;">
|
|
@@ -97,25 +97,43 @@
|
|
|
97
97
|
<!-- Migration Action Buttons -->
|
|
98
98
|
<div style="display: flex; gap: 1rem; margin-bottom: 1.5rem; flex-wrap: wrap;">
|
|
99
99
|
<% if @pending_migrations.any? %>
|
|
100
|
-
<%= form_with url: up_migrations_path, method: :post, local: true, style: "margin: 0;" do |f| %>
|
|
101
|
-
|
|
102
|
-
style
|
|
103
|
-
|
|
100
|
+
<%= form_with url: up_migrations_path, method: :post, local: true, id: "migration-up-all-form", style: "margin: 0;" do |f| %>
|
|
101
|
+
<button type="button" onclick="showKillSwitchModal('migration-up-all-form')"
|
|
102
|
+
style="padding: 0.75rem 1.5rem; background: #28a745; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 0.875rem; font-weight: 600;">
|
|
103
|
+
Apply All Pending Migrations
|
|
104
|
+
</button>
|
|
104
105
|
<% end %>
|
|
106
|
+
<%= render 'pg_sql_triggers/shared/confirmation_modal',
|
|
107
|
+
operation: :ui_migration_up,
|
|
108
|
+
form_id: 'migration-up-all-form',
|
|
109
|
+
title: 'Apply All Pending Migrations',
|
|
110
|
+
message: "Are you sure you want to apply #{@pending_migrations.count} pending migration(s)?" %>
|
|
105
111
|
<% end %>
|
|
106
|
-
|
|
112
|
+
|
|
107
113
|
<% if @current_migration_version > 0 %>
|
|
108
|
-
<%= form_with url: down_migrations_path, method: :post, local: true, style: "margin: 0;" do |f| %>
|
|
109
|
-
|
|
110
|
-
style
|
|
111
|
-
|
|
114
|
+
<%= form_with url: down_migrations_path, method: :post, local: true, id: "migration-down-form", style: "margin: 0;" do |f| %>
|
|
115
|
+
<button type="button" onclick="showKillSwitchModal('migration-down-form')"
|
|
116
|
+
style="padding: 0.75rem 1.5rem; background: #dc3545; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 0.875rem; font-weight: 600;">
|
|
117
|
+
Rollback Last Migration
|
|
118
|
+
</button>
|
|
112
119
|
<% end %>
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
120
|
+
<%= render 'pg_sql_triggers/shared/confirmation_modal',
|
|
121
|
+
operation: :ui_migration_down,
|
|
122
|
+
form_id: 'migration-down-form',
|
|
123
|
+
title: 'Rollback Last Migration',
|
|
124
|
+
message: 'Are you sure you want to rollback the last migration?' %>
|
|
125
|
+
|
|
126
|
+
<%= form_with url: redo_migrations_path, method: :post, local: true, id: "migration-redo-form", style: "margin: 0;" do |f| %>
|
|
127
|
+
<button type="button" onclick="showKillSwitchModal('migration-redo-form')"
|
|
128
|
+
style="padding: 0.75rem 1.5rem; background: #ffc107; color: #212529; border: none; border-radius: 4px; cursor: pointer; font-size: 0.875rem; font-weight: 600;">
|
|
129
|
+
Redo Last Migration
|
|
130
|
+
</button>
|
|
118
131
|
<% end %>
|
|
132
|
+
<%= render 'pg_sql_triggers/shared/confirmation_modal',
|
|
133
|
+
operation: :ui_migration_redo,
|
|
134
|
+
form_id: 'migration-redo-form',
|
|
135
|
+
title: 'Redo Last Migration',
|
|
136
|
+
message: 'Are you sure you want to redo the last migration?' %>
|
|
119
137
|
<% end %>
|
|
120
138
|
</div>
|
|
121
139
|
|
|
@@ -161,25 +179,47 @@
|
|
|
161
179
|
<td>
|
|
162
180
|
<div style="display: flex; gap: 0.5rem; flex-wrap: wrap;">
|
|
163
181
|
<% if migration[:status] == "down" %>
|
|
164
|
-
|
|
182
|
+
<% form_id = "migration-up-#{migration[:version]}-form" %>
|
|
183
|
+
<%= form_with url: up_migrations_path, method: :post, local: true, id: form_id, style: "margin: 0;" do |f| %>
|
|
165
184
|
<%= f.hidden_field :version, value: migration[:version] %>
|
|
166
|
-
<%=
|
|
167
|
-
style
|
|
168
|
-
|
|
185
|
+
<button type="button" onclick="showKillSwitchModal('<%= form_id %>')"
|
|
186
|
+
style="padding: 0.25rem 0.75rem; background: #28a745; color: white; border: none; border-radius: 3px; cursor: pointer; font-size: 0.75rem;">
|
|
187
|
+
Up
|
|
188
|
+
</button>
|
|
169
189
|
<% end %>
|
|
190
|
+
<%= render 'pg_sql_triggers/shared/confirmation_modal',
|
|
191
|
+
operation: :ui_migration_up,
|
|
192
|
+
form_id: form_id,
|
|
193
|
+
title: 'Apply Migration',
|
|
194
|
+
message: "Are you sure you want to apply migration #{migration[:version]}?" %>
|
|
170
195
|
<% else %>
|
|
171
|
-
|
|
196
|
+
<% form_id_down = "migration-down-#{migration[:version]}-form" %>
|
|
197
|
+
<%= form_with url: down_migrations_path, method: :post, local: true, id: form_id_down, style: "margin: 0;" do |f| %>
|
|
172
198
|
<%= f.hidden_field :version, value: migration[:version] %>
|
|
173
|
-
<%=
|
|
174
|
-
style
|
|
175
|
-
|
|
199
|
+
<button type="button" onclick="showKillSwitchModal('<%= form_id_down %>')"
|
|
200
|
+
style="padding: 0.25rem 0.75rem; background: #dc3545; color: white; border: none; border-radius: 3px; cursor: pointer; font-size: 0.75rem;">
|
|
201
|
+
Down
|
|
202
|
+
</button>
|
|
176
203
|
<% end %>
|
|
177
|
-
<%=
|
|
204
|
+
<%= render 'pg_sql_triggers/shared/confirmation_modal',
|
|
205
|
+
operation: :ui_migration_down,
|
|
206
|
+
form_id: form_id_down,
|
|
207
|
+
title: 'Rollback Migration',
|
|
208
|
+
message: "Are you sure you want to rollback to version #{migration[:version]}?" %>
|
|
209
|
+
|
|
210
|
+
<% form_id_redo = "migration-redo-#{migration[:version]}-form" %>
|
|
211
|
+
<%= form_with url: redo_migrations_path, method: :post, local: true, id: form_id_redo, style: "margin: 0;" do |f| %>
|
|
178
212
|
<%= f.hidden_field :version, value: migration[:version] %>
|
|
179
|
-
<%=
|
|
180
|
-
style
|
|
181
|
-
|
|
213
|
+
<button type="button" onclick="showKillSwitchModal('<%= form_id_redo %>')"
|
|
214
|
+
style="padding: 0.25rem 0.75rem; background: #ffc107; color: #212529; border: none; border-radius: 3px; cursor: pointer; font-size: 0.75rem;">
|
|
215
|
+
Redo
|
|
216
|
+
</button>
|
|
182
217
|
<% end %>
|
|
218
|
+
<%= render 'pg_sql_triggers/shared/confirmation_modal',
|
|
219
|
+
operation: :ui_migration_redo,
|
|
220
|
+
form_id: form_id_redo,
|
|
221
|
+
title: 'Redo Migration',
|
|
222
|
+
message: "Are you sure you want to redo migration #{migration[:version]}?" %>
|
|
183
223
|
<% end %>
|
|
184
224
|
</div>
|
|
185
225
|
</td>
|
|
@@ -187,13 +227,13 @@
|
|
|
187
227
|
<% end %>
|
|
188
228
|
</tbody>
|
|
189
229
|
</table>
|
|
190
|
-
|
|
230
|
+
|
|
191
231
|
<!-- Pagination Controls -->
|
|
192
232
|
<% if @total_pages > 1 %>
|
|
193
233
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-top: 1rem; padding-top: 1rem; border-top: 1px solid #dee2e6;">
|
|
194
234
|
<div>
|
|
195
235
|
<% if @page > 1 %>
|
|
196
|
-
<%= link_to "← Previous", dashboard_path(page: @page - 1, per_page: @per_page),
|
|
236
|
+
<%= link_to "← Previous", dashboard_path(page: @page - 1, per_page: @per_page),
|
|
197
237
|
style: "padding: 0.5rem 1rem; background: #007bff; color: white; text-decoration: none; border-radius: 4px; margin-right: 0.5rem;" %>
|
|
198
238
|
<% end %>
|
|
199
239
|
<% if @page < @total_pages %>
|
|
@@ -206,7 +246,7 @@
|
|
|
206
246
|
</div>
|
|
207
247
|
<div style="display: flex; align-items: center; gap: 0.5rem;">
|
|
208
248
|
<label style="color: #6c757d; font-size: 0.875rem;">Per page:</label>
|
|
209
|
-
<select onchange="window.location.href='<%= dashboard_path %>?page=1&per_page=' + this.value"
|
|
249
|
+
<select onchange="window.location.href='<%= dashboard_path %>?page=1&per_page=' + this.value"
|
|
210
250
|
style="padding: 0.25rem 0.5rem; border: 1px solid #ced4da; border-radius: 4px;">
|
|
211
251
|
<option value="10" <%= 'selected' if @per_page == 10 %>>10</option>
|
|
212
252
|
<option value="20" <%= 'selected' if @per_page == 20 %>>20</option>
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
<%= form_with model: @form, url: preview_generator_index_path, method: :post,
|
|
9
9
|
scope: :pg_sql_triggers_generator_form,
|
|
10
10
|
id: "trigger-generator-form",
|
|
11
|
+
local: true,
|
|
11
12
|
html: { style: "background: white; padding: 2rem; border-radius: 4px; box-shadow: 0 1px 3px rgba(0,0,0,0.1);", onsubmit: "return validateForm();" } do |f| %>
|
|
12
13
|
|
|
13
14
|
<!-- Section 1: Basic Information -->
|
|
@@ -64,7 +65,7 @@
|
|
|
64
65
|
|
|
65
66
|
<div style="margin-bottom: 1rem;">
|
|
66
67
|
<%= f.label :function_body, "Function Body *", style: "display: block; font-weight: 500; margin-bottom: 0.25rem;" %>
|
|
67
|
-
<%
|
|
68
|
+
<%
|
|
68
69
|
default_function_body = @form.function_body.presence || @form.default_function_body
|
|
69
70
|
%>
|
|
70
71
|
<%= f.text_area :function_body,
|
|
@@ -87,6 +88,23 @@
|
|
|
87
88
|
<fieldset style="border: 1px solid #dee2e6; padding: 1rem; margin-bottom: 2rem; border-radius: 4px;">
|
|
88
89
|
<legend style="font-weight: 600; color: #495057; padding: 0 0.5rem;">Trigger Events *</legend>
|
|
89
90
|
|
|
91
|
+
<div style="margin-bottom: 1rem;">
|
|
92
|
+
<%= f.label :timing, "Trigger Timing *", style: "display: block; font-weight: 500; margin-bottom: 0.25rem;" %>
|
|
93
|
+
<%= f.select :timing,
|
|
94
|
+
options_for_select([["Before", "before"], ["After", "after"]], @form.timing || "before"),
|
|
95
|
+
{},
|
|
96
|
+
{
|
|
97
|
+
required: true,
|
|
98
|
+
style: "width: 200px; padding: 0.5rem; border: 1px solid #ced4da; border-radius: 4px;"
|
|
99
|
+
} %>
|
|
100
|
+
<small style="color: #6c757d; display: block; margin-top: 0.25rem;">
|
|
101
|
+
When the trigger should fire: BEFORE (before constraint checks) or AFTER (after constraint checks)
|
|
102
|
+
</small>
|
|
103
|
+
<% if @form.errors[:timing].any? %>
|
|
104
|
+
<div style="color: #dc3545; margin-top: 0.25rem;"><%= @form.errors[:timing].first %></div>
|
|
105
|
+
<% end %>
|
|
106
|
+
</div>
|
|
107
|
+
|
|
90
108
|
<div style="display: flex; gap: 1rem; flex-wrap: wrap;">
|
|
91
109
|
<% %w[insert update delete truncate].each do |event| %>
|
|
92
110
|
<label style="display: flex; align-items: center; cursor: pointer;">
|
|
@@ -282,10 +300,10 @@ document.getElementById('function-name-input')?.addEventListener('input', functi
|
|
|
282
300
|
const functionBodyTextarea = document.getElementById('function-body-textarea');
|
|
283
301
|
if (functionBodyTextarea) {
|
|
284
302
|
const newTemplate = generateFunctionBody(functionName);
|
|
285
|
-
|
|
303
|
+
|
|
286
304
|
// Update placeholder
|
|
287
305
|
functionBodyTextarea.placeholder = newTemplate;
|
|
288
|
-
|
|
306
|
+
|
|
289
307
|
// Update value only if textarea is empty or matches template pattern
|
|
290
308
|
if (!functionBodyTextarea.value || isTemplateValue(functionBodyTextarea.value)) {
|
|
291
309
|
functionBodyTextarea.value = newTemplate;
|
|
@@ -320,7 +338,7 @@ document.getElementById('table-name-select')?.addEventListener('change', functio
|
|
|
320
338
|
.then(data => {
|
|
321
339
|
if (data.valid) {
|
|
322
340
|
messageDiv.innerHTML = '<span style="color: #28a745;">✓ Table exists (' + data.column_count + ' columns)</span>';
|
|
323
|
-
|
|
341
|
+
|
|
324
342
|
// Try to fetch existing triggers for this table
|
|
325
343
|
if (infoDiv && triggersList) {
|
|
326
344
|
fetch('<%= tables_path %>/' + encodeURIComponent(tableName) + '.json', {
|