pg_sql_triggers 1.0.0 → 1.0.1
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 +29 -1
- data/Goal.md +408 -123
- data/README.md +47 -215
- data/app/controllers/pg_sql_triggers/application_controller.rb +46 -0
- data/app/controllers/pg_sql_triggers/generator_controller.rb +10 -4
- data/app/controllers/pg_sql_triggers/migrations_controller.rb +18 -0
- data/app/models/pg_sql_triggers/trigger_registry.rb +20 -2
- 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 +4 -4
- data/app/views/pg_sql_triggers/generator/preview.html.erb +14 -6
- data/app/views/pg_sql_triggers/shared/_confirmation_modal.html.erb +189 -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/db/migrate/20251222000001_create_pg_sql_triggers_tables.rb +1 -1
- data/docs/README.md +66 -0
- data/docs/api-reference.md +663 -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 +420 -0
- data/docs/web-ui.md +339 -0
- data/lib/generators/pg_sql_triggers/templates/create_pg_sql_triggers_tables.rb +1 -1
- data/lib/generators/pg_sql_triggers/templates/initializer.rb +36 -2
- data/lib/pg_sql_triggers/generator/service.rb +1 -1
- data/lib/pg_sql_triggers/migration.rb +1 -1
- data/lib/pg_sql_triggers/migrator.rb +27 -3
- data/lib/pg_sql_triggers/registry/manager.rb +6 -6
- 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/safe_executor.rb +23 -11
- data/lib/pg_sql_triggers/version.rb +1 -1
- data/lib/pg_sql_triggers.rb +12 -0
- data/lib/tasks/trigger_migrations.rake +33 -0
- metadata +35 -5
|
@@ -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>
|
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
|
|
65
65
|
<div style="margin-bottom: 1rem;">
|
|
66
66
|
<%= f.label :function_body, "Function Body *", style: "display: block; font-weight: 500; margin-bottom: 0.25rem;" %>
|
|
67
|
-
<%
|
|
67
|
+
<%
|
|
68
68
|
default_function_body = @form.function_body.presence || @form.default_function_body
|
|
69
69
|
%>
|
|
70
70
|
<%= f.text_area :function_body,
|
|
@@ -282,10 +282,10 @@ document.getElementById('function-name-input')?.addEventListener('input', functi
|
|
|
282
282
|
const functionBodyTextarea = document.getElementById('function-body-textarea');
|
|
283
283
|
if (functionBodyTextarea) {
|
|
284
284
|
const newTemplate = generateFunctionBody(functionName);
|
|
285
|
-
|
|
285
|
+
|
|
286
286
|
// Update placeholder
|
|
287
287
|
functionBodyTextarea.placeholder = newTemplate;
|
|
288
|
-
|
|
288
|
+
|
|
289
289
|
// Update value only if textarea is empty or matches template pattern
|
|
290
290
|
if (!functionBodyTextarea.value || isTemplateValue(functionBodyTextarea.value)) {
|
|
291
291
|
functionBodyTextarea.value = newTemplate;
|
|
@@ -320,7 +320,7 @@ document.getElementById('table-name-select')?.addEventListener('change', functio
|
|
|
320
320
|
.then(data => {
|
|
321
321
|
if (data.valid) {
|
|
322
322
|
messageDiv.innerHTML = '<span style="color: #28a745;">✓ Table exists (' + data.column_count + ' columns)</span>';
|
|
323
|
-
|
|
323
|
+
|
|
324
324
|
// Try to fetch existing triggers for this table
|
|
325
325
|
if (infoDiv && triggersList) {
|
|
326
326
|
fetch('<%= tables_path %>/' + encodeURIComponent(tableName) + '.json', {
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
</div>
|
|
20
20
|
|
|
21
21
|
<!-- Actions -->
|
|
22
|
-
<%= form_with url: generator_index_path, method: :post, scope: :pg_sql_triggers_generator_form do |f| %>
|
|
22
|
+
<%= form_with url: generator_index_path, method: :post, scope: :pg_sql_triggers_generator_form, id: "generator-create-form" do |f| %>
|
|
23
23
|
<!-- SQL Validation Result -->
|
|
24
24
|
<% if @sql_validation %>
|
|
25
25
|
<div style="margin-bottom: 2rem; padding: 1rem; border-radius: 4px; <%= @sql_validation[:valid] ? 'background: #d4edda; border-left: 4px solid #28a745;' : 'background: #f8d7da; border-left: 4px solid #dc3545;' %>">
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
<p style="color: #6c757d; margin-bottom: 0.5rem; font-size: 0.875rem;">
|
|
49
49
|
You can edit the function body below before generating the trigger files.
|
|
50
50
|
</p>
|
|
51
|
-
<%= f.text_area :function_body,
|
|
51
|
+
<%= f.text_area :function_body,
|
|
52
52
|
value: @function_content,
|
|
53
53
|
rows: 20,
|
|
54
54
|
required: true,
|
|
@@ -69,9 +69,17 @@
|
|
|
69
69
|
<% end %>
|
|
70
70
|
|
|
71
71
|
<div style="display: flex; gap: 1rem;">
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
72
|
+
<button type="button" onclick="showKillSwitchModal('generator-create-form')" class="btn btn-success"
|
|
73
|
+
style="padding: 0.75rem 1.5rem; background: #28a745; color: white; border: none; border-radius: 4px; cursor: pointer; font-weight: 600;">
|
|
74
|
+
Generate Files
|
|
75
|
+
</button>
|
|
76
|
+
<%= link_to "Back to Edit", new_generator_path, class: "btn", style: "background: #6c757d; color: white; text-decoration: none; padding: 0.75rem 1.5rem; border-radius: 4px;" %>
|
|
77
|
+
<%= link_to "Cancel", root_path, class: "btn", style: "background: #6c757d; color: white; text-decoration: none; padding: 0.75rem 1.5rem; border-radius: 4px;" %>
|
|
76
78
|
</div>
|
|
77
79
|
<% end %>
|
|
80
|
+
|
|
81
|
+
<%= render 'pg_sql_triggers/shared/confirmation_modal',
|
|
82
|
+
operation: :ui_trigger_generate,
|
|
83
|
+
form_id: 'generator-create-form',
|
|
84
|
+
title: 'Generate Trigger Files',
|
|
85
|
+
message: 'This will create files on disk and register the trigger. Continue?' %>
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
<%# Confirmation Modal for Dangerous Operations %>
|
|
2
|
+
<%#
|
|
3
|
+
Usage:
|
|
4
|
+
<%= render 'pg_sql_triggers/shared/confirmation_modal',
|
|
5
|
+
operation: :trigger_migrate_up,
|
|
6
|
+
form_id: 'migration-form',
|
|
7
|
+
title: 'Confirm Migration',
|
|
8
|
+
message: 'Are you sure you want to run this migration?'
|
|
9
|
+
%>
|
|
10
|
+
|
|
11
|
+
<%
|
|
12
|
+
operation = local_assigns[:operation] || :unknown_operation
|
|
13
|
+
form_id = local_assigns[:form_id] || 'confirmation-form'
|
|
14
|
+
title = local_assigns[:title] || 'Confirm Action'
|
|
15
|
+
message = local_assigns[:message] || 'This action requires confirmation.'
|
|
16
|
+
|
|
17
|
+
# Generate expected confirmation text
|
|
18
|
+
if PgSqlTriggers.respond_to?(:kill_switch_confirmation_pattern) &&
|
|
19
|
+
PgSqlTriggers.kill_switch_confirmation_pattern.respond_to?(:call)
|
|
20
|
+
expected_confirmation = PgSqlTriggers.kill_switch_confirmation_pattern.call(operation)
|
|
21
|
+
else
|
|
22
|
+
expected_confirmation = "EXECUTE #{operation.to_s.upcase}"
|
|
23
|
+
end
|
|
24
|
+
%>
|
|
25
|
+
|
|
26
|
+
<div id="kill-switch-modal-<%= form_id %>" class="kill-switch-modal" style="display: none; position: fixed; z-index: 1000; left: 0; top: 0; width: 100%; height: 100%; overflow: auto; background-color: rgba(0,0,0,0.4);">
|
|
27
|
+
<div class="modal-content" style="background-color: #fefefe; margin: 10% auto; padding: 20px; border: 1px solid #888; border-radius: 8px; width: 80%; max-width: 600px; box-shadow: 0 4px 6px rgba(0,0,0,0.1); box-sizing: border-box;">
|
|
28
|
+
<div class="modal-header" style="border-bottom: 1px solid #dee2e6; padding-bottom: 12px; margin-bottom: 16px;">
|
|
29
|
+
<h3 style="margin: 0; color: #dc3545; display: flex; align-items: center; gap: 8px;">
|
|
30
|
+
<span style="font-size: 24px;">⚠️</span>
|
|
31
|
+
<%= title %>
|
|
32
|
+
</h3>
|
|
33
|
+
</div>
|
|
34
|
+
|
|
35
|
+
<div class="modal-body" style="margin-bottom: 20px; box-sizing: border-box;">
|
|
36
|
+
<p style="color: #495057; line-height: 1.6;"><%= message %></p>
|
|
37
|
+
|
|
38
|
+
<% if kill_switch_active? %>
|
|
39
|
+
<div style="background-color: #fff3cd; border: 1px solid #ffc107; border-radius: 4px; padding: 12px; margin: 16px 0; box-sizing: border-box;">
|
|
40
|
+
<p style="margin: 0 0 8px 0; font-weight: bold; color: #856404;">
|
|
41
|
+
🛡️ Kill Switch Protection Active
|
|
42
|
+
</p>
|
|
43
|
+
<p style="margin: 0 0 12px 0; color: #856404; font-size: 14px;">
|
|
44
|
+
You must enter the exact confirmation text below to proceed:
|
|
45
|
+
</p>
|
|
46
|
+
<div style="background-color: #f8f9fa; border: 2px solid #ffc107; border-radius: 4px; padding: 8px; font-family: monospace; font-size: 14px; font-weight: bold; text-align: center; color: #856404; word-break: break-word; box-sizing: border-box;">
|
|
47
|
+
<%= expected_confirmation %>
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
|
|
51
|
+
<div style="margin-top: 16px; box-sizing: border-box;">
|
|
52
|
+
<label for="confirmation-text-<%= form_id %>" style="display: block; margin-bottom: 8px; font-weight: bold; color: #495057;">
|
|
53
|
+
Confirmation Text:
|
|
54
|
+
</label>
|
|
55
|
+
<input
|
|
56
|
+
type="text"
|
|
57
|
+
id="confirmation-text-<%= form_id %>"
|
|
58
|
+
name="confirmation_text"
|
|
59
|
+
class="form-control"
|
|
60
|
+
placeholder="Type the confirmation text above"
|
|
61
|
+
style="width: 100%; max-width: 100%; padding: 8px 12px; border: 1px solid #ced4da; border-radius: 4px; font-family: monospace; box-sizing: border-box;"
|
|
62
|
+
autocomplete="off">
|
|
63
|
+
<small style="display: block; margin-top: 4px; color: #6c757d;">
|
|
64
|
+
Case-sensitive. Must match exactly.
|
|
65
|
+
</small>
|
|
66
|
+
</div>
|
|
67
|
+
<% end %>
|
|
68
|
+
</div>
|
|
69
|
+
|
|
70
|
+
<div class="modal-footer" style="border-top: 1px solid #dee2e6; padding-top: 12px; display: flex; gap: 8px; justify-content: flex-end;">
|
|
71
|
+
<button
|
|
72
|
+
type="button"
|
|
73
|
+
class="btn-cancel"
|
|
74
|
+
onclick="closeKillSwitchModal('<%= form_id %>')"
|
|
75
|
+
style="padding: 8px 16px; border: 1px solid #6c757d; background-color: #6c757d; color: white; border-radius: 4px; cursor: pointer;">
|
|
76
|
+
Cancel
|
|
77
|
+
</button>
|
|
78
|
+
|
|
79
|
+
<% if kill_switch_active? %>
|
|
80
|
+
<button
|
|
81
|
+
type="button"
|
|
82
|
+
class="btn-confirm"
|
|
83
|
+
onclick="submitWithConfirmation('<%= form_id %>', '<%= expected_confirmation %>')"
|
|
84
|
+
style="padding: 8px 16px; border: 1px solid #dc3545; background-color: #dc3545; color: white; border-radius: 4px; cursor: pointer;">
|
|
85
|
+
Confirm & Proceed
|
|
86
|
+
</button>
|
|
87
|
+
<% else %>
|
|
88
|
+
<button
|
|
89
|
+
type="button"
|
|
90
|
+
class="btn-confirm"
|
|
91
|
+
onclick="submitWithoutConfirmation('<%= form_id %>')"
|
|
92
|
+
style="padding: 8px 16px; border: 1px solid #007bff; background-color: #007bff; color: white; border-radius: 4px; cursor: pointer;">
|
|
93
|
+
Proceed
|
|
94
|
+
</button>
|
|
95
|
+
<% end %>
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
</div>
|
|
99
|
+
|
|
100
|
+
<script>
|
|
101
|
+
// Only define functions once
|
|
102
|
+
if (typeof window.showKillSwitchModal === 'undefined') {
|
|
103
|
+
window.showKillSwitchModal = function(formId) {
|
|
104
|
+
const modal = document.getElementById('kill-switch-modal-' + formId);
|
|
105
|
+
if (modal) {
|
|
106
|
+
modal.style.display = 'block';
|
|
107
|
+
// Focus on confirmation input if it exists
|
|
108
|
+
const confirmationInput = document.getElementById('confirmation-text-' + formId);
|
|
109
|
+
if (confirmationInput) {
|
|
110
|
+
setTimeout(() => confirmationInput.focus(), 100);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
window.closeKillSwitchModal = function(formId) {
|
|
116
|
+
const modal = document.getElementById('kill-switch-modal-' + formId);
|
|
117
|
+
if (modal) {
|
|
118
|
+
modal.style.display = 'none';
|
|
119
|
+
// Clear confirmation input
|
|
120
|
+
const confirmationInput = document.getElementById('confirmation-text-' + formId);
|
|
121
|
+
if (confirmationInput) {
|
|
122
|
+
confirmationInput.value = '';
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
window.submitWithConfirmation = function(formId, expectedConfirmation) {
|
|
128
|
+
const confirmationInput = document.getElementById('confirmation-text-' + formId);
|
|
129
|
+
const confirmationText = confirmationInput ? confirmationInput.value.trim() : '';
|
|
130
|
+
|
|
131
|
+
if (confirmationText !== expectedConfirmation) {
|
|
132
|
+
alert('Invalid confirmation text. Please type exactly: ' + expectedConfirmation);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Add confirmation text to the form
|
|
137
|
+
const form = document.getElementById(formId);
|
|
138
|
+
if (form) {
|
|
139
|
+
// Remove any existing confirmation_text input
|
|
140
|
+
const existingInput = form.querySelector('input[name="confirmation_text"]');
|
|
141
|
+
if (existingInput && existingInput !== confirmationInput) {
|
|
142
|
+
existingInput.remove();
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Create a hidden input with the confirmation text
|
|
146
|
+
const hiddenInput = document.createElement('input');
|
|
147
|
+
hiddenInput.type = 'hidden';
|
|
148
|
+
hiddenInput.name = 'confirmation_text';
|
|
149
|
+
hiddenInput.value = confirmationText;
|
|
150
|
+
form.appendChild(hiddenInput);
|
|
151
|
+
|
|
152
|
+
form.submit();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
window.closeKillSwitchModal(formId);
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
window.submitWithoutConfirmation = function(formId) {
|
|
159
|
+
const form = document.getElementById(formId);
|
|
160
|
+
if (form) {
|
|
161
|
+
form.submit();
|
|
162
|
+
}
|
|
163
|
+
window.closeKillSwitchModal(formId);
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
// Close modal when clicking outside of it (only add listener once)
|
|
167
|
+
if (!window._killSwitchModalListenersAttached) {
|
|
168
|
+
window.onclick = function(event) {
|
|
169
|
+
if (event.target.className === 'kill-switch-modal') {
|
|
170
|
+
event.target.style.display = 'none';
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
// Close modal on Escape key
|
|
175
|
+
document.addEventListener('keydown', function(event) {
|
|
176
|
+
if (event.key === 'Escape') {
|
|
177
|
+
const modals = document.querySelectorAll('.kill-switch-modal');
|
|
178
|
+
modals.forEach(modal => {
|
|
179
|
+
if (modal.style.display === 'block') {
|
|
180
|
+
modal.style.display = 'none';
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
window._killSwitchModalListenersAttached = true;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
</script>
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
<%# Kill Switch Status Indicator %>
|
|
2
|
+
<%# Displays the current kill switch status and environment warning if applicable %>
|
|
3
|
+
|
|
4
|
+
<%
|
|
5
|
+
# Debug info (can be removed in production)
|
|
6
|
+
env = current_environment
|
|
7
|
+
is_active = kill_switch_active?
|
|
8
|
+
protected_envs = PgSqlTriggers.respond_to?(:kill_switch_environments) ? PgSqlTriggers.kill_switch_environments : %i[production staging]
|
|
9
|
+
|
|
10
|
+
# Log for debugging (remove in production)
|
|
11
|
+
Rails.logger.debug "[KILL_SWITCH_VIEW] is_active=#{is_active} env=#{env} protected_envs=#{Array(protected_envs).map(&:to_s).join(',')}"
|
|
12
|
+
%>
|
|
13
|
+
|
|
14
|
+
<% if is_active %>
|
|
15
|
+
<div class="kill-switch-status active" style="background-color: #fff3cd; border: 1px solid #ffc107; border-radius: 4px; padding: 12px; margin-bottom: 16px;">
|
|
16
|
+
<div style="display: flex; align-items: center; gap: 8px;">
|
|
17
|
+
<span style="font-size: 20px;">🛡️</span>
|
|
18
|
+
<div>
|
|
19
|
+
<strong style="color: #856404;">Kill Switch Active</strong>
|
|
20
|
+
<p style="margin: 4px 0 0 0; font-size: 14px; color: #856404;">
|
|
21
|
+
Dangerous operations in <strong><%= env %></strong> require confirmation.
|
|
22
|
+
All operations are logged and audited.
|
|
23
|
+
</p>
|
|
24
|
+
</div>
|
|
25
|
+
</div>
|
|
26
|
+
</div>
|
|
27
|
+
<% else %>
|
|
28
|
+
<div class="kill-switch-status inactive" style="background-color: #d1ecf1; border: 1px solid #bee5eb; border-radius: 4px; padding: 12px; margin-bottom: 16px;">
|
|
29
|
+
<div style="display: flex; align-items: center; gap: 8px;">
|
|
30
|
+
<span style="font-size: 20px;">ℹ️</span>
|
|
31
|
+
<div>
|
|
32
|
+
<strong style="color: #0c5460;">Kill Switch Inactive</strong>
|
|
33
|
+
<p style="margin: 4px 0 0 0; font-size: 14px; color: #0c5460;">
|
|
34
|
+
Kill switch is disabled in <strong><%= env %></strong>.
|
|
35
|
+
Operations can be performed without confirmation.
|
|
36
|
+
</p>
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
<% end %>
|
|
@@ -63,14 +63,14 @@
|
|
|
63
63
|
</p>
|
|
64
64
|
</div>
|
|
65
65
|
</div>
|
|
66
|
-
|
|
66
|
+
|
|
67
67
|
<% if trigger.definition.present? %>
|
|
68
68
|
<% definition = JSON.parse(trigger.definition) rescue {} %>
|
|
69
69
|
<div style="margin-bottom: 1rem;">
|
|
70
70
|
<strong>Function:</strong> <code style="background: #e9ecef; padding: 0.25rem 0.5rem; border-radius: 2px;"><%= definition["function_name"] || "N/A" %></code>
|
|
71
71
|
<% if definition["events"].present? %>
|
|
72
72
|
<span style="margin-left: 1rem;">
|
|
73
|
-
<strong>Events:</strong>
|
|
73
|
+
<strong>Events:</strong>
|
|
74
74
|
<% definition["events"].each do |event| %>
|
|
75
75
|
<span class="badge badge-info" style="margin-left: 0.25rem;"><%= event %></span>
|
|
76
76
|
<% end %>
|
|
@@ -78,7 +78,7 @@
|
|
|
78
78
|
<% end %>
|
|
79
79
|
</div>
|
|
80
80
|
<% end %>
|
|
81
|
-
|
|
81
|
+
|
|
82
82
|
<% if trigger.function_body.present? %>
|
|
83
83
|
<details style="margin-top: 1rem;">
|
|
84
84
|
<summary style="cursor: pointer; font-weight: 600; color: #495057;">View Function Body</summary>
|
|
@@ -123,4 +123,3 @@
|
|
|
123
123
|
</div>
|
|
124
124
|
</div>
|
|
125
125
|
<% end %>
|
|
126
|
-
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
class CreatePgSqlTriggersTables < ActiveRecord::Migration[6.
|
|
3
|
+
class CreatePgSqlTriggersTables < ActiveRecord::Migration[6.1]
|
|
4
4
|
def change
|
|
5
5
|
# Registry table - source of truth for all triggers
|
|
6
6
|
create_table :pg_sql_triggers_registry do |t|
|
data/docs/README.md
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# PgSqlTriggers Documentation
|
|
2
|
+
|
|
3
|
+
Welcome to the PgSqlTriggers documentation. This directory contains comprehensive guides and references for all features.
|
|
4
|
+
|
|
5
|
+
## Documentation Index
|
|
6
|
+
|
|
7
|
+
### Getting Started
|
|
8
|
+
- **[Getting Started Guide](getting-started.md)** - Installation, setup, and your first trigger
|
|
9
|
+
|
|
10
|
+
### Core Guides
|
|
11
|
+
- **[Usage Guide](usage-guide.md)** - DSL syntax, migrations, and drift detection
|
|
12
|
+
- **[Web UI Guide](web-ui.md)** - Using the web dashboard
|
|
13
|
+
- **[Kill Switch Guide](kill-switch.md)** - Production safety features
|
|
14
|
+
|
|
15
|
+
### Reference
|
|
16
|
+
- **[Configuration Reference](configuration.md)** - Complete configuration options
|
|
17
|
+
- **[API Reference](api-reference.md)** - Console API and programmatic usage
|
|
18
|
+
|
|
19
|
+
## Quick Links
|
|
20
|
+
|
|
21
|
+
### I want to...
|
|
22
|
+
|
|
23
|
+
#### Install PgSqlTriggers
|
|
24
|
+
Start with [Getting Started](getting-started.md)
|
|
25
|
+
|
|
26
|
+
#### Learn the DSL syntax
|
|
27
|
+
See [Usage Guide - Declaring Triggers](usage-guide.md#declaring-triggers)
|
|
28
|
+
|
|
29
|
+
#### Understand migrations
|
|
30
|
+
See [Usage Guide - Trigger Migrations](usage-guide.md#trigger-migrations)
|
|
31
|
+
|
|
32
|
+
#### Use the web dashboard
|
|
33
|
+
See [Web UI Documentation](web-ui.md)
|
|
34
|
+
|
|
35
|
+
#### Protect production
|
|
36
|
+
See [Kill Switch Documentation](kill-switch.md)
|
|
37
|
+
|
|
38
|
+
#### Configure permissions
|
|
39
|
+
See [Configuration - Permission System](configuration.md#permission-system)
|
|
40
|
+
|
|
41
|
+
#### Use the console API
|
|
42
|
+
See [API Reference](api-reference.md)
|
|
43
|
+
|
|
44
|
+
#### Handle drift detection
|
|
45
|
+
See [Usage Guide - Drift Detection](usage-guide.md#drift-detection)
|
|
46
|
+
|
|
47
|
+
## Screenshots
|
|
48
|
+
|
|
49
|
+
Screenshots referenced in the documentation are stored in the [screenshots](screenshots/) directory.
|
|
50
|
+
|
|
51
|
+
## Contributing to Documentation
|
|
52
|
+
|
|
53
|
+
When updating documentation:
|
|
54
|
+
|
|
55
|
+
1. Keep examples realistic and practical
|
|
56
|
+
2. Include both basic and advanced usage
|
|
57
|
+
3. Add cross-references between related topics
|
|
58
|
+
4. Update this index when adding new pages
|
|
59
|
+
5. Place screenshots in the `screenshots/` directory
|
|
60
|
+
6. Use relative links for internal documentation
|
|
61
|
+
|
|
62
|
+
## External Resources
|
|
63
|
+
|
|
64
|
+
- [GitHub Repository](https://github.com/samaswin87/pg_sql_triggers)
|
|
65
|
+
- [Example Application](https://github.com/samaswin87/pg_triggers_example)
|
|
66
|
+
- [RubyGems](https://rubygems.org/gems/pg_sql_triggers)
|