pg_sql_triggers 1.2.0 → 1.4.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/CHANGELOG.md +397 -1
- data/COVERAGE.md +26 -19
- data/GEM_ANALYSIS.md +368 -0
- data/Goal.md +276 -155
- data/README.md +45 -22
- data/app/assets/javascripts/pg_sql_triggers/trigger_actions.js +50 -0
- data/app/controllers/concerns/pg_sql_triggers/error_handling.rb +56 -0
- data/app/controllers/concerns/pg_sql_triggers/kill_switch_protection.rb +66 -0
- data/app/controllers/concerns/pg_sql_triggers/permission_checking.rb +117 -0
- data/app/controllers/pg_sql_triggers/application_controller.rb +10 -62
- data/app/controllers/pg_sql_triggers/audit_logs_controller.rb +102 -0
- data/app/controllers/pg_sql_triggers/dashboard_controller.rb +4 -9
- data/app/controllers/pg_sql_triggers/tables_controller.rb +30 -4
- data/app/controllers/pg_sql_triggers/triggers_controller.rb +3 -21
- data/app/helpers/pg_sql_triggers/permissions_helper.rb +43 -0
- data/app/models/pg_sql_triggers/audit_log.rb +106 -0
- data/app/models/pg_sql_triggers/trigger_registry.rb +218 -13
- data/app/views/layouts/pg_sql_triggers/application.html.erb +25 -6
- data/app/views/pg_sql_triggers/audit_logs/index.html.erb +177 -0
- data/app/views/pg_sql_triggers/dashboard/index.html.erb +34 -12
- data/app/views/pg_sql_triggers/tables/index.html.erb +75 -5
- data/app/views/pg_sql_triggers/tables/show.html.erb +17 -6
- data/app/views/pg_sql_triggers/triggers/_drop_modal.html.erb +16 -7
- data/app/views/pg_sql_triggers/triggers/_re_execute_modal.html.erb +16 -7
- data/app/views/pg_sql_triggers/triggers/show.html.erb +26 -6
- data/config/routes.rb +2 -14
- data/db/migrate/20260103000001_create_pg_sql_triggers_audit_log.rb +28 -0
- data/db/migrate/20260228000001_add_for_each_to_pg_sql_triggers_registry.rb +8 -0
- data/docs/README.md +15 -5
- data/docs/api-reference.md +233 -151
- data/docs/audit-trail.md +413 -0
- data/docs/configuration.md +28 -7
- data/docs/getting-started.md +17 -16
- data/docs/permissions.md +369 -0
- data/docs/troubleshooting.md +486 -0
- data/docs/ui-guide.md +211 -0
- data/docs/usage-guide.md +38 -67
- data/docs/web-ui.md +251 -128
- data/lib/generators/pg_sql_triggers/templates/trigger_dsl.rb.tt +11 -0
- data/lib/generators/pg_sql_triggers/templates/trigger_migration_full.rb.tt +29 -0
- data/lib/generators/pg_sql_triggers/trigger_generator.rb +83 -0
- data/lib/pg_sql_triggers/drift/db_queries.rb +12 -8
- data/lib/pg_sql_triggers/drift/detector.rb +51 -38
- data/lib/pg_sql_triggers/dsl/trigger_definition.rb +17 -23
- data/lib/pg_sql_triggers/engine.rb +14 -0
- data/lib/pg_sql_triggers/errors.rb +245 -0
- data/lib/pg_sql_triggers/migrator/pre_apply_comparator.rb +8 -9
- data/lib/pg_sql_triggers/migrator/safety_validator.rb +32 -12
- data/lib/pg_sql_triggers/migrator.rb +53 -6
- data/lib/pg_sql_triggers/permissions/checker.rb +9 -2
- data/lib/pg_sql_triggers/registry/manager.rb +36 -11
- data/lib/pg_sql_triggers/registry/validator.rb +62 -5
- data/lib/pg_sql_triggers/registry.rb +141 -8
- data/lib/pg_sql_triggers/sql/kill_switch.rb +153 -247
- data/lib/pg_sql_triggers/sql.rb +0 -6
- data/lib/pg_sql_triggers/testing/function_tester.rb +2 -0
- data/lib/pg_sql_triggers/version.rb +1 -1
- data/lib/pg_sql_triggers.rb +7 -7
- data/pg_sql_triggers.gemspec +53 -0
- metadata +35 -18
- data/app/controllers/pg_sql_triggers/generator_controller.rb +0 -213
- data/app/controllers/pg_sql_triggers/sql_capsules_controller.rb +0 -161
- data/app/views/pg_sql_triggers/generator/new.html.erb +0 -388
- data/app/views/pg_sql_triggers/generator/preview.html.erb +0 -305
- data/app/views/pg_sql_triggers/sql_capsules/new.html.erb +0 -81
- data/app/views/pg_sql_triggers/sql_capsules/show.html.erb +0 -85
- data/docs/screenshots/.gitkeep +0 -1
- 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/lib/generators/trigger/migration_generator.rb +0 -60
- data/lib/pg_sql_triggers/generator/form.rb +0 -80
- data/lib/pg_sql_triggers/generator/service.rb +0 -307
- data/lib/pg_sql_triggers/generator.rb +0 -8
- data/lib/pg_sql_triggers/sql/capsule.rb +0 -79
- data/lib/pg_sql_triggers/sql/executor.rb +0 -200
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
<%# Audit Log Index Page %>
|
|
2
|
+
<div style="max-width: 1400px; margin: 0 auto; padding: 2rem;">
|
|
3
|
+
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 2rem;">
|
|
4
|
+
<h1 style="margin: 0;">Audit Log</h1>
|
|
5
|
+
<div style="display: flex; gap: 1rem;">
|
|
6
|
+
<%= link_to "Dashboard", dashboard_path, style: "padding: 0.5rem 1rem; background: #6c757d; color: white; text-decoration: none; border-radius: 4px; display: inline-block;" %>
|
|
7
|
+
<% csv_params = params.permit(:trigger_name, :operation, :status, :environment, :actor_id).to_h %>
|
|
8
|
+
<%= link_to "Export CSV", audit_logs_path(csv_params.merge(format: :csv)), style: "padding: 0.5rem 1rem; background: #28a745; color: white; text-decoration: none; border-radius: 4px; display: inline-block;" %>
|
|
9
|
+
</div>
|
|
10
|
+
</div>
|
|
11
|
+
|
|
12
|
+
<!-- Filters -->
|
|
13
|
+
<div style="background: white; padding: 1.5rem; border-radius: 4px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); margin-bottom: 2rem;">
|
|
14
|
+
<h3 style="margin-top: 0; margin-bottom: 1rem;">Filters</h3>
|
|
15
|
+
<%= form_with url: audit_logs_path, method: :get, local: true, style: "display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem;" do |f| %>
|
|
16
|
+
<div>
|
|
17
|
+
<%= f.label :trigger_name, "Trigger Name", style: "display: block; margin-bottom: 0.5rem; font-weight: 600;" %>
|
|
18
|
+
<%= f.select :trigger_name, options_for_select([["All", ""]] + @available_trigger_names.map { |n| [n, n] }, params[:trigger_name]), {}, { style: "width: 100%; padding: 0.5rem; border: 1px solid #ced4da; border-radius: 4px;" } %>
|
|
19
|
+
</div>
|
|
20
|
+
|
|
21
|
+
<div>
|
|
22
|
+
<%= f.label :operation, "Operation", style: "display: block; margin-bottom: 0.5rem; font-weight: 600;" %>
|
|
23
|
+
<%= f.select :operation, options_for_select([["All", ""]] + @available_operations.map { |o| [o.humanize, o] }, params[:operation]), {}, { style: "width: 100%; padding: 0.5rem; border: 1px solid #ced4da; border-radius: 4px;" } %>
|
|
24
|
+
</div>
|
|
25
|
+
|
|
26
|
+
<div>
|
|
27
|
+
<%= f.label :status, "Status", style: "display: block; margin-bottom: 0.5rem; font-weight: 600;" %>
|
|
28
|
+
<%= f.select :status, options_for_select([["All", ""], ["Success", "success"], ["Failure", "failure"]], params[:status]), {}, { style: "width: 100%; padding: 0.5rem; border: 1px solid #ced4da; border-radius: 4px;" } %>
|
|
29
|
+
</div>
|
|
30
|
+
|
|
31
|
+
<div>
|
|
32
|
+
<%= f.label :environment, "Environment", style: "display: block; margin-bottom: 0.5rem; font-weight: 600;" %>
|
|
33
|
+
<%= f.select :environment, options_for_select([["All", ""]] + @available_environments.map { |e| [e.humanize, e] }, params[:environment]), {}, { style: "width: 100%; padding: 0.5rem; border: 1px solid #ced4da; border-radius: 4px;" } %>
|
|
34
|
+
</div>
|
|
35
|
+
|
|
36
|
+
<div>
|
|
37
|
+
<%= f.label :sort, "Sort Order", style: "display: block; margin-bottom: 0.5rem; font-weight: 600;" %>
|
|
38
|
+
<%= f.select :sort, options_for_select([["Newest First", "desc"], ["Oldest First", "asc"]], params[:sort] || "desc"), {}, { style: "width: 100%; padding: 0.5rem; border: 1px solid #ced4da; border-radius: 4px;" } %>
|
|
39
|
+
</div>
|
|
40
|
+
|
|
41
|
+
<div style="display: flex; align-items: flex-end; gap: 0.5rem;">
|
|
42
|
+
<%= f.submit "Apply Filters", style: "padding: 0.5rem 1rem; background: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer; font-weight: 600;" %>
|
|
43
|
+
<%= link_to "Clear", audit_logs_path, style: "padding: 0.5rem 1rem; background: #6c757d; color: white; text-decoration: none; border-radius: 4px; display: inline-block;" %>
|
|
44
|
+
</div>
|
|
45
|
+
<% end %>
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
<!-- Results Summary -->
|
|
49
|
+
<div style="background: #f8f9fa; padding: 1rem; border-radius: 4px; margin-bottom: 1rem;">
|
|
50
|
+
<strong>Total Results:</strong> <%= @total_count %> entries
|
|
51
|
+
<% if params[:trigger_name].present? || params[:operation].present? || params[:status].present? || params[:environment].present? %>
|
|
52
|
+
<span style="color: #6c757d;">(filtered)</span>
|
|
53
|
+
<% end %>
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
<!-- Audit Log Table -->
|
|
57
|
+
<% if @audit_logs.any? %>
|
|
58
|
+
<div style="background: white; border-radius: 4px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); overflow-x: auto;">
|
|
59
|
+
<table style="width: 100%; border-collapse: collapse;">
|
|
60
|
+
<thead>
|
|
61
|
+
<tr style="background: #f8f9fa; border-bottom: 2px solid #dee2e6;">
|
|
62
|
+
<th style="padding: 0.75rem; text-align: left; font-weight: 600;">Time</th>
|
|
63
|
+
<th style="padding: 0.75rem; text-align: left; font-weight: 600;">Trigger</th>
|
|
64
|
+
<th style="padding: 0.75rem; text-align: left; font-weight: 600;">Operation</th>
|
|
65
|
+
<th style="padding: 0.75rem; text-align: left; font-weight: 600;">Status</th>
|
|
66
|
+
<th style="padding: 0.75rem; text-align: left; font-weight: 600;">Environment</th>
|
|
67
|
+
<th style="padding: 0.75rem; text-align: left; font-weight: 600;">Actor</th>
|
|
68
|
+
<th style="padding: 0.75rem; text-align: left; font-weight: 600;">Reason</th>
|
|
69
|
+
<th style="padding: 0.75rem; text-align: left; font-weight: 600;">Error</th>
|
|
70
|
+
</tr>
|
|
71
|
+
</thead>
|
|
72
|
+
<tbody>
|
|
73
|
+
<% @audit_logs.each do |log| %>
|
|
74
|
+
<tr style="border-bottom: 1px solid #dee2e6;">
|
|
75
|
+
<td style="padding: 0.75rem;">
|
|
76
|
+
<span title="<%= log.created_at.strftime("%Y-%m-%d %H:%M:%S %Z") %>" style="cursor: help;">
|
|
77
|
+
<%= distance_of_time_in_words_to_now(log.created_at) %> ago
|
|
78
|
+
</span>
|
|
79
|
+
<br>
|
|
80
|
+
<small style="color: #6c757d;"><%= log.created_at.strftime("%Y-%m-%d %H:%M:%S") %></small>
|
|
81
|
+
</td>
|
|
82
|
+
<td style="padding: 0.75rem;">
|
|
83
|
+
<% if log.trigger_name.present? %>
|
|
84
|
+
<%= link_to log.trigger_name, trigger_path(PgSqlTriggers::TriggerRegistry.find_by(trigger_name: log.trigger_name)), style: "color: #007bff; text-decoration: none;" rescue log.trigger_name %>
|
|
85
|
+
<% else %>
|
|
86
|
+
<span style="color: #6c757d;">—</span>
|
|
87
|
+
<% end %>
|
|
88
|
+
</td>
|
|
89
|
+
<td style="padding: 0.75rem;">
|
|
90
|
+
<code style="background: #f8f9fa; padding: 0.25rem 0.5rem; border-radius: 3px; font-size: 0.875rem;"><%= log.operation %></code>
|
|
91
|
+
</td>
|
|
92
|
+
<td style="padding: 0.75rem;">
|
|
93
|
+
<% if log.status == "success" %>
|
|
94
|
+
<span style="background: #d4edda; color: #155724; padding: 0.25rem 0.5rem; border-radius: 3px; font-size: 0.875rem; font-weight: 600;">Success</span>
|
|
95
|
+
<% else %>
|
|
96
|
+
<span style="background: #f8d7da; color: #721c24; padding: 0.25rem 0.5rem; border-radius: 3px; font-size: 0.875rem; font-weight: 600;">Failure</span>
|
|
97
|
+
<% end %>
|
|
98
|
+
</td>
|
|
99
|
+
<td style="padding: 0.75rem;">
|
|
100
|
+
<% if log.environment.present? %>
|
|
101
|
+
<span class="badge badge-info"><%= log.environment %></span>
|
|
102
|
+
<% else %>
|
|
103
|
+
<span style="color: #6c757d;">—</span>
|
|
104
|
+
<% end %>
|
|
105
|
+
</td>
|
|
106
|
+
<td style="padding: 0.75rem;">
|
|
107
|
+
<% if log.actor.present? %>
|
|
108
|
+
<% actor_type = log.actor.is_a?(Hash) ? (log.actor["type"] || log.actor[:type]) : nil %>
|
|
109
|
+
<% actor_id = log.actor.is_a?(Hash) ? (log.actor["id"] || log.actor[:id]) : nil %>
|
|
110
|
+
<% if actor_type && actor_id %>
|
|
111
|
+
<code style="background: #f8f9fa; padding: 0.25rem 0.5rem; border-radius: 3px; font-size: 0.875rem;"><%= "#{actor_type}:#{actor_id}" %></code>
|
|
112
|
+
<% else %>
|
|
113
|
+
<span style="color: #6c757d;">—</span>
|
|
114
|
+
<% end %>
|
|
115
|
+
<% else %>
|
|
116
|
+
<span style="color: #6c757d;">—</span>
|
|
117
|
+
<% end %>
|
|
118
|
+
</td>
|
|
119
|
+
<td style="padding: 0.75rem;">
|
|
120
|
+
<% if log.reason.present? %>
|
|
121
|
+
<span title="<%= log.reason %>" style="cursor: help;">
|
|
122
|
+
<%= truncate(log.reason, length: 50) %>
|
|
123
|
+
</span>
|
|
124
|
+
<% else %>
|
|
125
|
+
<span style="color: #6c757d;">—</span>
|
|
126
|
+
<% end %>
|
|
127
|
+
</td>
|
|
128
|
+
<td style="padding: 0.75rem;">
|
|
129
|
+
<% if log.error_message.present? %>
|
|
130
|
+
<span style="color: #dc3545; cursor: help;" title="<%= log.error_message %>">
|
|
131
|
+
<%= truncate(log.error_message, length: 50) %>
|
|
132
|
+
</span>
|
|
133
|
+
<% else %>
|
|
134
|
+
<span style="color: #6c757d;">—</span>
|
|
135
|
+
<% end %>
|
|
136
|
+
</td>
|
|
137
|
+
</tr>
|
|
138
|
+
<% end %>
|
|
139
|
+
</tbody>
|
|
140
|
+
</table>
|
|
141
|
+
</div>
|
|
142
|
+
|
|
143
|
+
<!-- Pagination -->
|
|
144
|
+
<% if @total_pages > 1 %>
|
|
145
|
+
<div style="display: flex; justify-content: center; align-items: center; gap: 1rem; margin-top: 2rem;">
|
|
146
|
+
<% if @page > 1 %>
|
|
147
|
+
<% prev_params = params.except(:page).permit(:trigger_name, :operation, :status, :environment, :sort, :per_page).to_h %>
|
|
148
|
+
<%= link_to "« Previous", audit_logs_path(prev_params.merge(page: @page - 1)), style: "padding: 0.5rem 1rem; background: #007bff; color: white; text-decoration: none; border-radius: 4px;" %>
|
|
149
|
+
<% else %>
|
|
150
|
+
<span style="padding: 0.5rem 1rem; background: #e9ecef; color: #6c757d; border-radius: 4px; cursor: not-allowed;">« Previous</span>
|
|
151
|
+
<% end %>
|
|
152
|
+
|
|
153
|
+
<span style="color: #6c757d;">
|
|
154
|
+
Page <%= @page %> of <%= @total_pages %>
|
|
155
|
+
</span>
|
|
156
|
+
|
|
157
|
+
<% if @page < @total_pages %>
|
|
158
|
+
<% next_params = params.except(:page).permit(:trigger_name, :operation, :status, :environment, :sort, :per_page).to_h %>
|
|
159
|
+
<%= link_to "Next »", audit_logs_path(next_params.merge(page: @page + 1)), style: "padding: 0.5rem 1rem; background: #007bff; color: white; text-decoration: none; border-radius: 4px;" %>
|
|
160
|
+
<% else %>
|
|
161
|
+
<span style="padding: 0.5rem 1rem; background: #e9ecef; color: #6c757d; border-radius: 4px; cursor: not-allowed;">Next »</span>
|
|
162
|
+
<% end %>
|
|
163
|
+
</div>
|
|
164
|
+
<% end %>
|
|
165
|
+
<% else %>
|
|
166
|
+
<div style="background: white; padding: 3rem; border-radius: 4px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); text-align: center;">
|
|
167
|
+
<h3 style="margin-top: 0; color: #6c757d;">No audit log entries found</h3>
|
|
168
|
+
<p style="color: #6c757d;">
|
|
169
|
+
<% if params[:trigger_name].present? || params[:operation].present? || params[:status].present? || params[:environment].present? %>
|
|
170
|
+
Try adjusting your filters or <%= link_to "clear filters", audit_logs_path, style: "color: #007bff;" %>.
|
|
171
|
+
<% else %>
|
|
172
|
+
Audit log entries will appear here as operations are performed.
|
|
173
|
+
<% end %>
|
|
174
|
+
</p>
|
|
175
|
+
</div>
|
|
176
|
+
<% end %>
|
|
177
|
+
</div>
|
|
@@ -2,9 +2,6 @@
|
|
|
2
2
|
<h2 style="margin: 0;">Trigger Dashboard</h2>
|
|
3
3
|
<div style="display: flex; gap: 1rem;">
|
|
4
4
|
<%= link_to "View Tables", tables_path, class: "btn btn-primary", style: "font-size: 1rem; padding: 0.75rem 1.5rem; text-decoration: none;" %>
|
|
5
|
-
<%= link_to "Generate New Trigger", new_generator_path,
|
|
6
|
-
class: "btn btn-success",
|
|
7
|
-
style: "font-size: 1rem; padding: 0.75rem 1.5rem; text-decoration: none;" %>
|
|
8
5
|
</div>
|
|
9
6
|
</div>
|
|
10
7
|
|
|
@@ -40,6 +37,7 @@
|
|
|
40
37
|
<th>Version</th>
|
|
41
38
|
<th>Status</th>
|
|
42
39
|
<th>Source</th>
|
|
40
|
+
<th>Last Applied</th>
|
|
43
41
|
<th>Actions</th>
|
|
44
42
|
</tr>
|
|
45
43
|
</thead>
|
|
@@ -60,11 +58,20 @@
|
|
|
60
58
|
</td>
|
|
61
59
|
<td><span class="badge badge-info"><%= trigger.source %></span></td>
|
|
62
60
|
<td>
|
|
63
|
-
<% if
|
|
64
|
-
<
|
|
61
|
+
<% if trigger.installed_at.present? %>
|
|
62
|
+
<span title="<%= trigger.installed_at.strftime("%Y-%m-%d %H:%M:%S %Z") %>" style="cursor: help;">
|
|
63
|
+
<%= distance_of_time_in_words_to_now(trigger.installed_at) %> ago
|
|
64
|
+
</span>
|
|
65
|
+
<% else %>
|
|
66
|
+
<span style="color: #6c757d;">Never</span>
|
|
67
|
+
<% end %>
|
|
68
|
+
</td>
|
|
69
|
+
<td>
|
|
70
|
+
<div style="display: flex; gap: 0.5rem; flex-wrap: wrap;">
|
|
71
|
+
<% if PgSqlTriggers::Permissions.can?(current_actor, :enable_trigger) %>
|
|
65
72
|
<% if trigger.enabled %>
|
|
66
73
|
<% form_id = "trigger-disable-#{trigger.id}-form" %>
|
|
67
|
-
<%= form_with url: disable_trigger_path(trigger), method: :post, local:
|
|
74
|
+
<%= form_with url: disable_trigger_path(trigger), method: :post, local: false, id: form_id, style: "margin: 0;" do |f| %>
|
|
68
75
|
<%= f.hidden_field :redirect_to, value: dashboard_path %>
|
|
69
76
|
<button type="button" onclick="showKillSwitchModal('<%= form_id %>')"
|
|
70
77
|
style="padding: 0.25rem 0.75rem; background: #dc3545; color: white; border: none; border-radius: 3px; cursor: pointer; font-size: 0.75rem;">
|
|
@@ -78,7 +85,7 @@
|
|
|
78
85
|
message: "Are you sure you want to disable trigger '#{trigger.trigger_name}'?" %>
|
|
79
86
|
<% else %>
|
|
80
87
|
<% form_id = "trigger-enable-#{trigger.id}-form" %>
|
|
81
|
-
<%= form_with url: enable_trigger_path(trigger), method: :post, local:
|
|
88
|
+
<%= form_with url: enable_trigger_path(trigger), method: :post, local: false, id: form_id, style: "margin: 0;" do |f| %>
|
|
82
89
|
<%= f.hidden_field :redirect_to, value: dashboard_path %>
|
|
83
90
|
<button type="button" onclick="showKillSwitchModal('<%= form_id %>')"
|
|
84
91
|
style="padding: 0.25rem 0.75rem; background: #28a745; color: white; border: none; border-radius: 3px; cursor: pointer; font-size: 0.75rem;">
|
|
@@ -91,10 +98,25 @@
|
|
|
91
98
|
title: 'Enable Trigger',
|
|
92
99
|
message: "Are you sure you want to enable trigger '#{trigger.trigger_name}'?" %>
|
|
93
100
|
<% end %>
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
101
|
+
<% end %>
|
|
102
|
+
|
|
103
|
+
<% if PgSqlTriggers::Permissions.can?(current_actor, :drop_trigger) %>
|
|
104
|
+
<% begin %>
|
|
105
|
+
<% drift_info = trigger.drift_result %>
|
|
106
|
+
<% if drift_info && drift_info[:state] == 'drifted' %>
|
|
107
|
+
<%= render 'pg_sql_triggers/triggers/re_execute_modal', trigger: trigger, drift_info: drift_info, redirect_to: dashboard_path, button_size: :small %>
|
|
108
|
+
<% end %>
|
|
109
|
+
<% rescue StandardError %>
|
|
110
|
+
<%# Skip if drift detection fails %>
|
|
111
|
+
<% end %>
|
|
112
|
+
|
|
113
|
+
<%= render 'pg_sql_triggers/triggers/drop_modal', trigger: trigger, redirect_to: dashboard_path, button_size: :small %>
|
|
114
|
+
<% end %>
|
|
115
|
+
|
|
116
|
+
<% unless PgSqlTriggers::Permissions.can?(current_actor, :enable_trigger) || PgSqlTriggers::Permissions.can?(current_actor, :drop_trigger) %>
|
|
117
|
+
<span style="color: #6c757d;">—</span>
|
|
118
|
+
<% end %>
|
|
119
|
+
</div>
|
|
98
120
|
</td>
|
|
99
121
|
</tr>
|
|
100
122
|
<% end %>
|
|
@@ -104,7 +126,7 @@
|
|
|
104
126
|
<div style="background: #e7f3ff; border-left: 4px solid #007bff; padding: 1.5rem; border-radius: 4px;">
|
|
105
127
|
<h3 style="margin-top: 0;">No triggers yet</h3>
|
|
106
128
|
<p style="margin-bottom: 1rem;">Get started by generating your first trigger using the form-based wizard.</p>
|
|
107
|
-
<%= link_to "
|
|
129
|
+
<%= link_to "View Tables", tables_path, class: "btn btn-primary" %>
|
|
108
130
|
</div>
|
|
109
131
|
<% end %>
|
|
110
132
|
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 2rem;">
|
|
2
2
|
<h2 style="margin: 0;">Database Tables & Triggers</h2>
|
|
3
|
-
<%= link_to "Generate New Trigger", new_generator_path, class: "btn btn-success" %>
|
|
4
3
|
</div>
|
|
5
4
|
|
|
6
5
|
<!-- Statistics -->
|
|
@@ -9,11 +8,37 @@
|
|
|
9
8
|
<div style="font-size: 2rem; font-weight: 600; color: #28a745;"><%= @tables_with_trigger_count %></div>
|
|
10
9
|
<div style="color: #6c757d;">Tables with Triggers</div>
|
|
11
10
|
</div>
|
|
11
|
+
<div style="background: white; padding: 1.5rem; border-radius: 4px; box-shadow: 0 1px 3px rgba(0,0,0,0.1);">
|
|
12
|
+
<div style="font-size: 2rem; font-weight: 600; color: #6c757d;"><%= @tables_without_trigger_count %></div>
|
|
13
|
+
<div style="color: #6c757d;">Tables without Triggers</div>
|
|
14
|
+
</div>
|
|
15
|
+
<div style="background: white; padding: 1.5rem; border-radius: 4px; box-shadow: 0 1px 3px rgba(0,0,0,0.1);">
|
|
16
|
+
<div style="font-size: 2rem; font-weight: 600; color: #007bff;"><%= @total_tables_count %></div>
|
|
17
|
+
<div style="color: #6c757d;">Total Tables</div>
|
|
18
|
+
</div>
|
|
19
|
+
</div>
|
|
20
|
+
|
|
21
|
+
<!-- Filter Controls -->
|
|
22
|
+
<div style="background: white; padding: 1rem 1.5rem; border-radius: 4px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); margin-bottom: 2rem;">
|
|
23
|
+
<div style="display: flex; align-items: center; gap: 1.5rem; flex-wrap: wrap;">
|
|
24
|
+
<label style="color: #495057; font-weight: 600; font-size: 0.875rem;">Filter:</label>
|
|
25
|
+
<div style="display: flex; gap: 1rem; flex-wrap: wrap;">
|
|
26
|
+
<%= link_to "All Tables", tables_path(filter: 'all', page: 1, per_page: @per_page),
|
|
27
|
+
style: "padding: 0.5rem 1rem; background: #{@filter == 'all' ? '#007bff' : '#f8f9fa'}; color: #{@filter == 'all' ? 'white' : '#495057'}; text-decoration: none; border-radius: 4px; border: 1px solid #{@filter == 'all' ? '#007bff' : '#dee2e6'}; font-size: 0.875rem; font-weight: #{@filter == 'all' ? '600' : '400'};" %>
|
|
28
|
+
<%= link_to "With Triggers", tables_path(filter: 'with_triggers', page: 1, per_page: @per_page),
|
|
29
|
+
style: "padding: 0.5rem 1rem; background: #{@filter == 'with_triggers' ? '#28a745' : '#f8f9fa'}; color: #{@filter == 'with_triggers' ? 'white' : '#495057'}; text-decoration: none; border-radius: 4px; border: 1px solid #{@filter == 'with_triggers' ? '#28a745' : '#dee2e6'}; font-size: 0.875rem; font-weight: #{@filter == 'with_triggers' ? '600' : '400'};" %>
|
|
30
|
+
<%= link_to "Without Triggers", tables_path(filter: 'without_triggers', page: 1, per_page: @per_page),
|
|
31
|
+
style: "padding: 0.5rem 1rem; background: #{@filter == 'without_triggers' ? '#6c757d' : '#f8f9fa'}; color: #{@filter == 'without_triggers' ? 'white' : '#495057'}; text-decoration: none; border-radius: 4px; border: 1px solid #{@filter == 'without_triggers' ? '#6c757d' : '#dee2e6'}; font-size: 0.875rem; font-weight: #{@filter == 'without_triggers' ? '600' : '400'};" %>
|
|
32
|
+
</div>
|
|
33
|
+
</div>
|
|
12
34
|
</div>
|
|
13
35
|
|
|
14
36
|
<!-- Tables List -->
|
|
15
37
|
<% if @tables_with_triggers.any? %>
|
|
16
38
|
<div style="background: white; border-radius: 4px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); overflow: hidden;">
|
|
39
|
+
<div style="margin-bottom: 1rem; color: #6c757d; font-size: 0.875rem; padding: 1rem 1rem 0 1rem;">
|
|
40
|
+
Showing <%= (@page - 1) * @per_page + 1 %>-<%= [@page * @per_page, @total_tables].min %> of <%= @total_tables %> tables
|
|
41
|
+
</div>
|
|
17
42
|
<table style="margin: 0;">
|
|
18
43
|
<thead>
|
|
19
44
|
<tr>
|
|
@@ -87,17 +112,62 @@
|
|
|
87
112
|
</td>
|
|
88
113
|
<td>
|
|
89
114
|
<%= link_to "View Details", table_path(table[:table_name]), class: "btn btn-primary", style: "padding: 0.25rem 0.5rem; font-size: 0.875rem;" %>
|
|
90
|
-
<%= link_to "Create Trigger", new_generator_path(pg_sql_triggers_generator_form: { table_name: table[:table_name] }), class: "btn btn-success", style: "padding: 0.25rem 0.5rem; font-size: 0.875rem; margin-top: 0.25rem; display: block;" %>
|
|
91
115
|
</td>
|
|
92
116
|
</tr>
|
|
93
117
|
<% end %>
|
|
94
118
|
</tbody>
|
|
95
119
|
</table>
|
|
120
|
+
|
|
121
|
+
<!-- Pagination Controls -->
|
|
122
|
+
<% if @total_pages > 1 %>
|
|
123
|
+
<div style="display: flex; justify-content: space-between; align-items: center; margin-top: 1rem; padding: 1rem; border-top: 1px solid #dee2e6;">
|
|
124
|
+
<div>
|
|
125
|
+
<% if @page > 1 %>
|
|
126
|
+
<%= link_to "← Previous", tables_path(filter: @filter, page: @page - 1, per_page: @per_page),
|
|
127
|
+
style: "padding: 0.5rem 1rem; background: #007bff; color: white; text-decoration: none; border-radius: 4px; margin-right: 0.5rem;" %>
|
|
128
|
+
<% end %>
|
|
129
|
+
<% if @page < @total_pages %>
|
|
130
|
+
<%= link_to "Next →", tables_path(filter: @filter, page: @page + 1, per_page: @per_page),
|
|
131
|
+
style: "padding: 0.5rem 1rem; background: #007bff; color: white; text-decoration: none; border-radius: 4px;" %>
|
|
132
|
+
<% end %>
|
|
133
|
+
</div>
|
|
134
|
+
<div style="color: #6c757d; font-size: 0.875rem;">
|
|
135
|
+
Page <%= @page %> of <%= @total_pages %>
|
|
136
|
+
</div>
|
|
137
|
+
<div style="display: flex; align-items: center; gap: 0.5rem;">
|
|
138
|
+
<label style="color: #6c757d; font-size: 0.875rem;">Per page:</label>
|
|
139
|
+
<select onchange="window.location.href='<%= tables_path %>?filter=<%= @filter %>&page=1&per_page=' + this.value"
|
|
140
|
+
style="padding: 0.25rem 0.5rem; border: 1px solid #ced4da; border-radius: 4px;">
|
|
141
|
+
<option value="10" <%= 'selected' if @per_page == 10 %>>10</option>
|
|
142
|
+
<option value="20" <%= 'selected' if @per_page == 20 %>>20</option>
|
|
143
|
+
<option value="50" <%= 'selected' if @per_page == 50 %>>50</option>
|
|
144
|
+
<option value="100" <%= 'selected' if @per_page == 100 %>>100</option>
|
|
145
|
+
</select>
|
|
146
|
+
</div>
|
|
147
|
+
</div>
|
|
148
|
+
<% end %>
|
|
96
149
|
</div>
|
|
97
150
|
<% else %>
|
|
98
151
|
<div style="background: #e7f3ff; border-left: 4px solid #007bff; padding: 1.5rem; border-radius: 4px;">
|
|
99
|
-
<h3 style="margin-top: 0;">
|
|
100
|
-
|
|
101
|
-
|
|
152
|
+
<h3 style="margin-top: 0;">
|
|
153
|
+
<% if @filter == 'with_triggers' %>
|
|
154
|
+
No tables with triggers found
|
|
155
|
+
<% elsif @filter == 'without_triggers' %>
|
|
156
|
+
No tables without triggers found
|
|
157
|
+
<% else %>
|
|
158
|
+
No tables found
|
|
159
|
+
<% end %>
|
|
160
|
+
</h3>
|
|
161
|
+
<p style="margin-bottom: 1rem;">
|
|
162
|
+
<% if @filter == 'with_triggers' %>
|
|
163
|
+
No tables with triggers were found in the database. Create your first trigger to get started.
|
|
164
|
+
<% elsif @filter == 'without_triggers' %>
|
|
165
|
+
All tables in the database have triggers.
|
|
166
|
+
<% else %>
|
|
167
|
+
No tables were found in the database.
|
|
168
|
+
<% end %>
|
|
169
|
+
</p>
|
|
170
|
+
<% if @filter == 'with_triggers' || @filter == 'all' %>
|
|
171
|
+
<% end %>
|
|
102
172
|
</div>
|
|
103
173
|
<% end %>
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
<div style="margin-bottom: 2rem;">
|
|
2
2
|
<h2>Table: <%= @table_info[:table_name] %></h2>
|
|
3
3
|
<%= link_to "← Back to Tables", tables_path, class: "btn", style: "background: #6c757d; color: white; text-decoration: none; margin-right: 1rem;" %>
|
|
4
|
-
<%= link_to "Create Trigger for this Table", new_generator_path(pg_sql_triggers_generator_form: { table_name: @table_info[:table_name] }), class: "btn btn-success" %>
|
|
5
4
|
</div>
|
|
6
5
|
|
|
7
6
|
<!-- Table Information -->
|
|
@@ -64,8 +63,8 @@
|
|
|
64
63
|
</div>
|
|
65
64
|
</div>
|
|
66
65
|
|
|
67
|
-
|
|
68
|
-
|
|
66
|
+
<div style="margin-bottom: 1rem; display: flex; gap: 0.5rem; flex-wrap: wrap;">
|
|
67
|
+
<% if PgSqlTriggers::Permissions.can?(current_actor, :enable_trigger) %>
|
|
69
68
|
<% if trigger.enabled %>
|
|
70
69
|
<% form_id = "table-trigger-disable-#{trigger.id}-form" %>
|
|
71
70
|
<%= form_with url: disable_trigger_path(trigger), method: :post, local: true, id: form_id, style: "margin: 0;" do |f| %>
|
|
@@ -95,8 +94,21 @@
|
|
|
95
94
|
title: 'Enable Trigger',
|
|
96
95
|
message: "Are you sure you want to enable trigger '#{trigger.trigger_name}' on table '#{@table_info[:table_name]}'?" %>
|
|
97
96
|
<% end %>
|
|
98
|
-
|
|
99
|
-
|
|
97
|
+
<% end %>
|
|
98
|
+
|
|
99
|
+
<% if PgSqlTriggers::Permissions.can?(current_actor, :drop_trigger) %>
|
|
100
|
+
<% begin %>
|
|
101
|
+
<% drift_info = trigger.drift_result %>
|
|
102
|
+
<% if drift_info && drift_info[:state] == 'drifted' %>
|
|
103
|
+
<%= render 'pg_sql_triggers/triggers/re_execute_modal', trigger: trigger, drift_info: drift_info, redirect_to: table_path(@table_info[:table_name]) %>
|
|
104
|
+
<% end %>
|
|
105
|
+
<% rescue StandardError %>
|
|
106
|
+
<%# Skip if drift detection fails %>
|
|
107
|
+
<% end %>
|
|
108
|
+
|
|
109
|
+
<%= render 'pg_sql_triggers/triggers/drop_modal', trigger: trigger, redirect_to: table_path(@table_info[:table_name]) %>
|
|
110
|
+
<% end %>
|
|
111
|
+
</div>
|
|
100
112
|
|
|
101
113
|
<% if trigger.definition.present? %>
|
|
102
114
|
<% definition = JSON.parse(trigger.definition) rescue {} %>
|
|
@@ -125,7 +137,6 @@
|
|
|
125
137
|
<% else %>
|
|
126
138
|
<div style="background: #e7f3ff; border-left: 4px solid #007bff; padding: 1rem; border-radius: 4px;">
|
|
127
139
|
<p style="margin: 0;">No registered triggers for this table.</p>
|
|
128
|
-
<%= link_to "Create a trigger", new_generator_path(pg_sql_triggers_generator_form: { table_name: @table_info[:table_name] }), class: "btn btn-primary", style: "margin-top: 0.5rem;" %>
|
|
129
140
|
</div>
|
|
130
141
|
<% end %>
|
|
131
142
|
</div>
|
|
@@ -1,12 +1,21 @@
|
|
|
1
|
-
<% form_id = "trigger-drop-#{trigger.id}-form" %>
|
|
1
|
+
<% form_id = local_assigns[:form_id] || "trigger-drop-#{trigger.id}-form" %>
|
|
2
|
+
<% redirect_path = local_assigns[:redirect_to] || params[:redirect_to] || trigger_path(trigger) %>
|
|
3
|
+
<% button_size = local_assigns[:button_size] || :medium %>
|
|
2
4
|
|
|
3
5
|
<%= form_with url: drop_trigger_path(trigger), method: :post, local: true, id: form_id, style: "margin: 0; display: inline-block;" do |f| %>
|
|
4
|
-
<%= f.hidden_field :redirect_to, value:
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
<%= f.hidden_field :redirect_to, value: redirect_path %>
|
|
7
|
+
|
|
8
|
+
<% if button_size == :small %>
|
|
9
|
+
<button type="button" onclick="showDropModal('<%= form_id %>')"
|
|
10
|
+
style="padding: 0.25rem 0.75rem; background: #6c757d; color: white; border: none; border-radius: 3px; cursor: pointer; font-size: 0.75rem;">
|
|
11
|
+
Drop
|
|
12
|
+
</button>
|
|
13
|
+
<% else %>
|
|
14
|
+
<button type="button" onclick="showDropModal('<%= form_id %>')"
|
|
15
|
+
style="padding: 0.75rem 1.5rem; background: #6c757d; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 0.875rem; font-weight: 600;">
|
|
16
|
+
Drop Trigger
|
|
17
|
+
</button>
|
|
18
|
+
<% end %>
|
|
10
19
|
<% end %>
|
|
11
20
|
|
|
12
21
|
<!-- Drop Modal -->
|
|
@@ -1,12 +1,21 @@
|
|
|
1
|
-
<% form_id = "trigger-re-execute-#{trigger.id}-form" %>
|
|
1
|
+
<% form_id = local_assigns[:form_id] || "trigger-re-execute-#{trigger.id}-form" %>
|
|
2
|
+
<% redirect_path = local_assigns[:redirect_to] || params[:redirect_to] || trigger_path(trigger) %>
|
|
3
|
+
<% button_size = local_assigns[:button_size] || :medium %>
|
|
2
4
|
|
|
3
5
|
<%= form_with url: re_execute_trigger_path(trigger), method: :post, local: true, id: form_id, style: "margin: 0; display: inline-block;" do |f| %>
|
|
4
|
-
<%= f.hidden_field :redirect_to, value:
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
<%= f.hidden_field :redirect_to, value: redirect_path %>
|
|
7
|
+
|
|
8
|
+
<% if button_size == :small %>
|
|
9
|
+
<button type="button" onclick="showReExecuteModal('<%= form_id %>')"
|
|
10
|
+
style="padding: 0.25rem 0.75rem; background: #ffc107; color: #000; border: none; border-radius: 3px; cursor: pointer; font-size: 0.75rem;">
|
|
11
|
+
Re-Execute
|
|
12
|
+
</button>
|
|
13
|
+
<% else %>
|
|
14
|
+
<button type="button" onclick="showReExecuteModal('<%= form_id %>')"
|
|
15
|
+
style="padding: 0.75rem 1.5rem; background: #ffc107; color: #000; border: none; border-radius: 4px; cursor: pointer; font-size: 0.875rem; font-weight: 600;">
|
|
16
|
+
Re-Execute Trigger
|
|
17
|
+
</button>
|
|
18
|
+
<% end %>
|
|
10
19
|
<% end %>
|
|
11
20
|
|
|
12
21
|
<!-- Re-Execute Modal -->
|
|
@@ -1,7 +1,17 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
<%= link_to "
|
|
4
|
-
<%= link_to "
|
|
1
|
+
<!-- Breadcrumb Navigation -->
|
|
2
|
+
<div style="margin-bottom: 1rem; color: #6c757d; font-size: 0.875rem;">
|
|
3
|
+
<%= link_to "Dashboard", dashboard_path, style: "color: #007bff; text-decoration: none;" %> /
|
|
4
|
+
<%= link_to "Tables", tables_path, style: "color: #007bff; text-decoration: none;" %> /
|
|
5
|
+
<%= link_to @trigger.table_name, table_path(@trigger.table_name), style: "color: #007bff; text-decoration: none;" %> /
|
|
6
|
+
<span style="color: #495057; font-weight: 600;"><%= @trigger.trigger_name %></span>
|
|
7
|
+
</div>
|
|
8
|
+
|
|
9
|
+
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 2rem;">
|
|
10
|
+
<h2 style="margin: 0;">Trigger Details: <%= @trigger.trigger_name %></h2>
|
|
11
|
+
<div style="display: flex; gap: 0.5rem;">
|
|
12
|
+
<%= link_to "← Back to Dashboard", dashboard_path, class: "btn", style: "background: #6c757d; color: white; text-decoration: none; padding: 0.5rem 1rem;" %>
|
|
13
|
+
<%= link_to "View Table", table_path(@trigger.table_name), class: "btn", style: "background: #007bff; color: white; text-decoration: none; padding: 0.5rem 1rem;" %>
|
|
14
|
+
</div>
|
|
5
15
|
</div>
|
|
6
16
|
|
|
7
17
|
<!-- Drift Warning -->
|
|
@@ -55,8 +65,18 @@
|
|
|
55
65
|
|
|
56
66
|
<% if @trigger.installed_at.present? %>
|
|
57
67
|
<div style="margin-bottom: 1rem;">
|
|
58
|
-
<div style="color: #6c757d; font-size: 0.875rem; margin-bottom: 0.25rem;">
|
|
59
|
-
<div
|
|
68
|
+
<div style="color: #6c757d; font-size: 0.875rem; margin-bottom: 0.25rem;">Last Applied</div>
|
|
69
|
+
<div title="<%= @trigger.installed_at.strftime("%Y-%m-%d %H:%M:%S %Z") %>">
|
|
70
|
+
<%= distance_of_time_in_words_to_now(@trigger.installed_at) %> ago
|
|
71
|
+
<small style="color: #6c757d;">(<%= @trigger.installed_at.strftime("%Y-%m-%d %H:%M:%S") %>)</small>
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
<% end %>
|
|
75
|
+
|
|
76
|
+
<% if @trigger.last_verified_at.present? %>
|
|
77
|
+
<div style="margin-bottom: 1rem;">
|
|
78
|
+
<div style="color: #6c757d; font-size: 0.875rem; margin-bottom: 0.25rem;">Last Verified</div>
|
|
79
|
+
<div><%= @trigger.last_verified_at.strftime("%Y-%m-%d %H:%M:%S %Z") %></div>
|
|
60
80
|
</div>
|
|
61
81
|
<% end %>
|
|
62
82
|
|
data/config/routes.rb
CHANGED
|
@@ -7,20 +7,6 @@ begin
|
|
|
7
7
|
|
|
8
8
|
resources :tables, only: %i[index show]
|
|
9
9
|
|
|
10
|
-
resources :generator, only: %i[new create] do
|
|
11
|
-
collection do
|
|
12
|
-
post :preview
|
|
13
|
-
post :validate_table
|
|
14
|
-
get :tables
|
|
15
|
-
end
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
resources :sql_capsules, only: %i[new create show] do
|
|
19
|
-
member do
|
|
20
|
-
post :execute
|
|
21
|
-
end
|
|
22
|
-
end
|
|
23
|
-
|
|
24
10
|
resources :migrations, only: [] do
|
|
25
11
|
collection do
|
|
26
12
|
post :up
|
|
@@ -37,6 +23,8 @@ begin
|
|
|
37
23
|
post :re_execute
|
|
38
24
|
end
|
|
39
25
|
end
|
|
26
|
+
|
|
27
|
+
resources :audit_logs, only: [:index]
|
|
40
28
|
end
|
|
41
29
|
rescue ArgumentError => e
|
|
42
30
|
# Ignore duplicate route errors (routes may already be drawn in tests)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class CreatePgSqlTriggersAuditLog < ActiveRecord::Migration[6.1]
|
|
4
|
+
def change
|
|
5
|
+
create_table :pg_sql_triggers_audit_log do |t|
|
|
6
|
+
t.string :trigger_name
|
|
7
|
+
t.string :operation, null: false
|
|
8
|
+
t.jsonb :actor # Store actor information (type, id)
|
|
9
|
+
t.string :environment
|
|
10
|
+
t.string :status, null: false # success, failure
|
|
11
|
+
t.text :reason
|
|
12
|
+
t.string :confirmation_text
|
|
13
|
+
t.jsonb :before_state # Store state before operation
|
|
14
|
+
t.jsonb :after_state # Store state after operation
|
|
15
|
+
t.text :diff # Store diff if applicable
|
|
16
|
+
t.text :error_message # Store error message if operation failed
|
|
17
|
+
|
|
18
|
+
t.timestamps
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
add_index :pg_sql_triggers_audit_log, :trigger_name
|
|
22
|
+
add_index :pg_sql_triggers_audit_log, :operation
|
|
23
|
+
add_index :pg_sql_triggers_audit_log, :status
|
|
24
|
+
add_index :pg_sql_triggers_audit_log, :environment
|
|
25
|
+
add_index :pg_sql_triggers_audit_log, :created_at
|
|
26
|
+
add_index :pg_sql_triggers_audit_log, %i[trigger_name created_at]
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class AddForEachToPgSqlTriggersRegistry < ActiveRecord::Migration[6.1]
|
|
4
|
+
def change
|
|
5
|
+
add_column :pg_sql_triggers_registry, :for_each, :string, default: "row", null: false
|
|
6
|
+
add_index :pg_sql_triggers_registry, :for_each
|
|
7
|
+
end
|
|
8
|
+
end
|