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.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +120 -0
- data/CHANGELOG.md +52 -0
- data/Goal.md +294 -0
- data/LICENSE +21 -0
- data/README.md +294 -0
- data/RELEASE.md +270 -0
- data/Rakefile +16 -0
- data/app/assets/javascripts/pg_sql_triggers/application.js +5 -0
- data/app/assets/stylesheets/pg_sql_triggers/application.css +179 -0
- data/app/controllers/pg_sql_triggers/application_controller.rb +35 -0
- data/app/controllers/pg_sql_triggers/dashboard_controller.rb +42 -0
- data/app/controllers/pg_sql_triggers/generator_controller.rb +145 -0
- data/app/controllers/pg_sql_triggers/migrations_controller.rb +84 -0
- data/app/controllers/pg_sql_triggers/tables_controller.rb +44 -0
- data/app/models/pg_sql_triggers/application_record.rb +7 -0
- data/app/models/pg_sql_triggers/trigger_registry.rb +93 -0
- data/app/views/layouts/pg_sql_triggers/application.html.erb +72 -0
- data/app/views/pg_sql_triggers/dashboard/index.html.erb +225 -0
- data/app/views/pg_sql_triggers/generator/new.html.erb +370 -0
- data/app/views/pg_sql_triggers/generator/preview.html.erb +77 -0
- data/app/views/pg_sql_triggers/tables/index.html.erb +105 -0
- data/app/views/pg_sql_triggers/tables/show.html.erb +126 -0
- data/config/routes.rb +35 -0
- data/db/migrate/20251222000001_create_pg_sql_triggers_tables.rb +29 -0
- data/lib/generators/pg_sql_triggers/install_generator.rb +36 -0
- data/lib/generators/pg_sql_triggers/templates/README +36 -0
- data/lib/generators/pg_sql_triggers/templates/create_pg_sql_triggers_tables.rb +36 -0
- data/lib/generators/pg_sql_triggers/templates/initializer.rb +27 -0
- data/lib/generators/pg_sql_triggers/templates/trigger_migration.rb.erb +32 -0
- data/lib/generators/pg_sql_triggers/trigger_migration_generator.rb +60 -0
- data/lib/generators/trigger/migration_generator.rb +60 -0
- data/lib/pg_sql_triggers/database_introspection.rb +251 -0
- data/lib/pg_sql_triggers/drift.rb +24 -0
- data/lib/pg_sql_triggers/dsl/trigger_definition.rb +67 -0
- data/lib/pg_sql_triggers/dsl.rb +15 -0
- data/lib/pg_sql_triggers/engine.rb +29 -0
- data/lib/pg_sql_triggers/generator/form.rb +78 -0
- data/lib/pg_sql_triggers/generator/service.rb +251 -0
- data/lib/pg_sql_triggers/generator.rb +8 -0
- data/lib/pg_sql_triggers/migration.rb +15 -0
- data/lib/pg_sql_triggers/migrator.rb +237 -0
- data/lib/pg_sql_triggers/permissions/checker.rb +33 -0
- data/lib/pg_sql_triggers/permissions.rb +35 -0
- data/lib/pg_sql_triggers/registry/manager.rb +47 -0
- data/lib/pg_sql_triggers/registry/validator.rb +15 -0
- data/lib/pg_sql_triggers/registry.rb +36 -0
- data/lib/pg_sql_triggers/sql.rb +21 -0
- data/lib/pg_sql_triggers/testing/dry_run.rb +74 -0
- data/lib/pg_sql_triggers/testing/function_tester.rb +118 -0
- data/lib/pg_sql_triggers/testing/safe_executor.rb +66 -0
- data/lib/pg_sql_triggers/testing/syntax_validator.rb +124 -0
- data/lib/pg_sql_triggers/testing.rb +10 -0
- data/lib/pg_sql_triggers/version.rb +15 -0
- data/lib/pg_sql_triggers.rb +41 -0
- data/lib/tasks/trigger_migrations.rake +254 -0
- data/sig/pg_sql_triggers.rbs +4 -0
- metadata +260 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
<h2>Preview Generated Trigger</h2>
|
|
2
|
+
|
|
3
|
+
<div style="background: #fff3cd; border-left: 4px solid #ffc107; padding: 1rem; margin-bottom: 2rem;">
|
|
4
|
+
<strong>Review before generating:</strong>
|
|
5
|
+
<ul style="margin: 0.5rem 0 0 1rem; padding: 0;">
|
|
6
|
+
<li>Migration file will be created at: <code><%= @file_paths[:migration] %></code></li>
|
|
7
|
+
<li>DSL file will be created at: <code><%= @file_paths[:dsl] %></code></li>
|
|
8
|
+
<li>Trigger will be registered with source: <strong>dsl</strong></li>
|
|
9
|
+
</ul>
|
|
10
|
+
</div>
|
|
11
|
+
|
|
12
|
+
<!-- DSL Preview -->
|
|
13
|
+
<div style="margin-bottom: 2rem;">
|
|
14
|
+
<h3 style="display: flex; align-items: center; gap: 0.5rem;">
|
|
15
|
+
<span>DSL Definition</span>
|
|
16
|
+
<span class="badge badge-info">Ruby</span>
|
|
17
|
+
</h3>
|
|
18
|
+
<pre style="background: #f8f9fa; padding: 1rem; border-radius: 4px; overflow-x: auto; border: 1px solid #dee2e6;"><code><%= @dsl_content %></code></pre>
|
|
19
|
+
</div>
|
|
20
|
+
|
|
21
|
+
<!-- Actions -->
|
|
22
|
+
<%= form_with url: generator_index_path, method: :post, scope: :pg_sql_triggers_generator_form do |f| %>
|
|
23
|
+
<!-- SQL Validation Result -->
|
|
24
|
+
<% if @sql_validation %>
|
|
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;' %>">
|
|
26
|
+
<strong style="<%= @sql_validation[:valid] ? 'color: #155724;' : 'color: #721c24;' %>">
|
|
27
|
+
<%= @sql_validation[:valid] ? '✓ SQL Validation Passed' : '✗ SQL Validation Failed' %>
|
|
28
|
+
</strong>
|
|
29
|
+
<% if @sql_validation[:valid] %>
|
|
30
|
+
<p style="color: #155724; margin: 0.5rem 0 0 0; font-size: 0.875rem;">
|
|
31
|
+
<%= @sql_validation[:message] || 'Function syntax is valid' %>
|
|
32
|
+
</p>
|
|
33
|
+
<% else %>
|
|
34
|
+
<p style="color: #721c24; margin: 0.5rem 0 0 0; font-size: 0.875rem;">
|
|
35
|
+
<%= @sql_validation[:error] || 'Function syntax is invalid' %>
|
|
36
|
+
</p>
|
|
37
|
+
<% end %>
|
|
38
|
+
</div>
|
|
39
|
+
<% end %>
|
|
40
|
+
|
|
41
|
+
<!-- Function Preview -->
|
|
42
|
+
<div style="margin-bottom: 2rem;">
|
|
43
|
+
<h3 style="display: flex; align-items: center; gap: 0.5rem;">
|
|
44
|
+
<span>PL/pgSQL Function</span>
|
|
45
|
+
<span class="badge badge-info">SQL</span>
|
|
46
|
+
<small style="color: #6c757d; font-weight: normal;">(Editable)</small>
|
|
47
|
+
</h3>
|
|
48
|
+
<p style="color: #6c757d; margin-bottom: 0.5rem; font-size: 0.875rem;">
|
|
49
|
+
You can edit the function body below before generating the trigger files.
|
|
50
|
+
</p>
|
|
51
|
+
<%= f.text_area :function_body,
|
|
52
|
+
value: @function_content,
|
|
53
|
+
rows: 20,
|
|
54
|
+
required: true,
|
|
55
|
+
style: "width: 100%; padding: 1rem; border: 1px solid #ced4da; border-radius: 4px; font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'source-code-pro', monospace; font-size: 0.875rem; line-height: 1.5; background: #f8f9fa; resize: vertical;" %>
|
|
56
|
+
</div>
|
|
57
|
+
<%= f.hidden_field :trigger_name, value: @form.trigger_name %>
|
|
58
|
+
<%= f.hidden_field :table_name, value: @form.table_name %>
|
|
59
|
+
<%= f.hidden_field :function_name, value: @form.function_name %>
|
|
60
|
+
<%= f.hidden_field :version, value: @form.version %>
|
|
61
|
+
<%= f.hidden_field :enabled, value: @form.enabled %>
|
|
62
|
+
<%= f.hidden_field :condition, value: @form.condition %>
|
|
63
|
+
<%= f.hidden_field :generate_function_stub, value: @form.generate_function_stub %>
|
|
64
|
+
<% Array(@form.events).reject(&:blank?).each do |event| %>
|
|
65
|
+
<%= hidden_field_tag "pg_sql_triggers_generator_form[events][]", event %>
|
|
66
|
+
<% end %>
|
|
67
|
+
<% Array(@form.environments).reject(&:blank?).each do |env| %>
|
|
68
|
+
<%= hidden_field_tag "pg_sql_triggers_generator_form[environments][]", env %>
|
|
69
|
+
<% end %>
|
|
70
|
+
|
|
71
|
+
<div style="display: flex; gap: 1rem;">
|
|
72
|
+
<%= f.submit "Generate Files", class: "btn btn-success",
|
|
73
|
+
data: { confirm: "This will create files on disk and register the trigger. Continue?" } %>
|
|
74
|
+
<%= link_to "Back to Edit", new_generator_path, class: "btn", style: "background: #6c757d; color: white; text-decoration: none;" %>
|
|
75
|
+
<%= link_to "Cancel", root_path, class: "btn", style: "background: #6c757d; color: white; text-decoration: none;" %>
|
|
76
|
+
</div>
|
|
77
|
+
<% end %>
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 2rem;">
|
|
2
|
+
<h2 style="margin: 0;">Database Tables & Triggers</h2>
|
|
3
|
+
<%= link_to "Generate New Trigger", new_generator_path, class: "btn btn-success" %>
|
|
4
|
+
</div>
|
|
5
|
+
|
|
6
|
+
<!-- Statistics -->
|
|
7
|
+
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem; margin-bottom: 2rem;">
|
|
8
|
+
<div style="background: white; padding: 1.5rem; border-radius: 4px; box-shadow: 0 1px 3px rgba(0,0,0,0.1);">
|
|
9
|
+
<div style="font-size: 2rem; font-weight: 600; color: #28a745;"><%= @tables_with_trigger_count %></div>
|
|
10
|
+
<div style="color: #6c757d;">Tables with Triggers</div>
|
|
11
|
+
</div>
|
|
12
|
+
</div>
|
|
13
|
+
|
|
14
|
+
<!-- Tables List -->
|
|
15
|
+
<% if @tables_with_triggers.any? %>
|
|
16
|
+
<div style="background: white; border-radius: 4px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); overflow: hidden;">
|
|
17
|
+
<table style="margin: 0;">
|
|
18
|
+
<thead>
|
|
19
|
+
<tr>
|
|
20
|
+
<th style="width: 25%;">Table Name</th>
|
|
21
|
+
<th style="width: 10%;">Triggers</th>
|
|
22
|
+
<th style="width: 40%;">Trigger Names & Functions</th>
|
|
23
|
+
<th style="width: 15%;">Status</th>
|
|
24
|
+
<th style="width: 10%;">Actions</th>
|
|
25
|
+
</tr>
|
|
26
|
+
</thead>
|
|
27
|
+
<tbody>
|
|
28
|
+
<% @tables_with_triggers.each do |table| %>
|
|
29
|
+
<tr class="table-row">
|
|
30
|
+
<td>
|
|
31
|
+
<strong style="font-size: 1.1rem;"><%= table[:table_name] %></strong>
|
|
32
|
+
</td>
|
|
33
|
+
<td>
|
|
34
|
+
<span class="badge <%= table[:trigger_count] > 0 ? 'badge-success' : 'badge-danger' %>">
|
|
35
|
+
<%= table[:trigger_count] %>
|
|
36
|
+
</span>
|
|
37
|
+
</td>
|
|
38
|
+
<td>
|
|
39
|
+
<% if table[:trigger_count] > 0 %>
|
|
40
|
+
<div style="display: flex; flex-direction: column; gap: 0.5rem;">
|
|
41
|
+
<% table[:registry_triggers].each do |trigger| %>
|
|
42
|
+
<div style="padding: 0.5rem; background: #f8f9fa; border-radius: 4px; border-left: 3px solid #007bff;">
|
|
43
|
+
<div style="font-weight: 600; color: #007bff;">
|
|
44
|
+
<%= trigger[:trigger_name] %>
|
|
45
|
+
<% if trigger[:enabled] %>
|
|
46
|
+
<span class="badge badge-success" style="font-size: 0.75rem; margin-left: 0.5rem;">Enabled</span>
|
|
47
|
+
<% else %>
|
|
48
|
+
<span class="badge badge-danger" style="font-size: 0.75rem; margin-left: 0.5rem;">Disabled</span>
|
|
49
|
+
<% end %>
|
|
50
|
+
</div>
|
|
51
|
+
<div style="font-size: 0.875rem; color: #6c757d; margin-top: 0.25rem;">
|
|
52
|
+
Function: <code style="background: #e9ecef; padding: 0.125rem 0.25rem; border-radius: 2px;"><%= trigger[:function_name] || 'N/A' %></code>
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
<% end %>
|
|
56
|
+
<% table[:database_triggers].each do |trigger| %>
|
|
57
|
+
<div style="padding: 0.5rem; background: #fff3cd; border-radius: 4px; border-left: 3px solid #ffc107;">
|
|
58
|
+
<div style="font-weight: 600; color: #856404;">
|
|
59
|
+
<%= trigger[:trigger_name] %>
|
|
60
|
+
<span class="badge badge-warning" style="font-size: 0.75rem; margin-left: 0.5rem;">DB Only</span>
|
|
61
|
+
</div>
|
|
62
|
+
<div style="font-size: 0.875rem; color: #6c757d; margin-top: 0.25rem;">
|
|
63
|
+
Function: <code style="background: #e9ecef; padding: 0.125rem 0.25rem; border-radius: 2px;"><%= trigger[:function_name] %></code>
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
<% end %>
|
|
67
|
+
</div>
|
|
68
|
+
<% else %>
|
|
69
|
+
<span style="color: #6c757d; font-style: italic;">No triggers</span>
|
|
70
|
+
<% end %>
|
|
71
|
+
</td>
|
|
72
|
+
<td>
|
|
73
|
+
<% if table[:trigger_count] > 0 %>
|
|
74
|
+
<% enabled_count = table[:registry_triggers].count { |t| t[:enabled] } %>
|
|
75
|
+
<% disabled_count = table[:registry_triggers].count - enabled_count %>
|
|
76
|
+
<div style="display: flex; flex-direction: column; gap: 0.25rem;">
|
|
77
|
+
<% if enabled_count > 0 %>
|
|
78
|
+
<span class="badge badge-success" style="font-size: 0.75rem;"><%= enabled_count %> Enabled</span>
|
|
79
|
+
<% end %>
|
|
80
|
+
<% if disabled_count > 0 %>
|
|
81
|
+
<span class="badge badge-danger" style="font-size: 0.75rem;"><%= disabled_count %> Disabled</span>
|
|
82
|
+
<% end %>
|
|
83
|
+
</div>
|
|
84
|
+
<% else %>
|
|
85
|
+
<span style="color: #6c757d;">—</span>
|
|
86
|
+
<% end %>
|
|
87
|
+
</td>
|
|
88
|
+
<td>
|
|
89
|
+
<%= 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
|
+
</td>
|
|
92
|
+
</tr>
|
|
93
|
+
<% end %>
|
|
94
|
+
</tbody>
|
|
95
|
+
</table>
|
|
96
|
+
</div>
|
|
97
|
+
<% else %>
|
|
98
|
+
<div style="background: #e7f3ff; border-left: 4px solid #007bff; padding: 1.5rem; border-radius: 4px;">
|
|
99
|
+
<h3 style="margin-top: 0;">No tables with triggers found</h3>
|
|
100
|
+
<p style="margin-bottom: 1rem;">No tables with triggers were found in the database. Create your first trigger to get started.</p>
|
|
101
|
+
<%= link_to "Generate New Trigger", new_generator_path, class: "btn btn-primary" %>
|
|
102
|
+
</div>
|
|
103
|
+
<% end %>
|
|
104
|
+
|
|
105
|
+
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
<div style="margin-bottom: 2rem;">
|
|
2
|
+
<h2>Table: <%= @table_info[:table_name] %></h2>
|
|
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
|
+
</div>
|
|
6
|
+
|
|
7
|
+
<!-- Table Information -->
|
|
8
|
+
<div style="background: white; padding: 1.5rem; border-radius: 4px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); margin-bottom: 2rem;">
|
|
9
|
+
<h3 style="margin-top: 0;">Table Columns</h3>
|
|
10
|
+
<% if @columns.any? %>
|
|
11
|
+
<table>
|
|
12
|
+
<thead>
|
|
13
|
+
<tr>
|
|
14
|
+
<th>Column Name</th>
|
|
15
|
+
<th>Data Type</th>
|
|
16
|
+
<th>Nullable</th>
|
|
17
|
+
</tr>
|
|
18
|
+
</thead>
|
|
19
|
+
<tbody>
|
|
20
|
+
<% @columns.each do |column| %>
|
|
21
|
+
<tr>
|
|
22
|
+
<td><strong><%= column[:name] %></strong></td>
|
|
23
|
+
<td><code><%= column[:type] %></code></td>
|
|
24
|
+
<td>
|
|
25
|
+
<% if column[:nullable] %>
|
|
26
|
+
<span class="badge badge-warning">Yes</span>
|
|
27
|
+
<% else %>
|
|
28
|
+
<span class="badge badge-success">No</span>
|
|
29
|
+
<% end %>
|
|
30
|
+
</td>
|
|
31
|
+
</tr>
|
|
32
|
+
<% end %>
|
|
33
|
+
</tbody>
|
|
34
|
+
</table>
|
|
35
|
+
<% else %>
|
|
36
|
+
<p style="color: #6c757d;">No columns found.</p>
|
|
37
|
+
<% end %>
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
<!-- Registry Triggers -->
|
|
41
|
+
<div style="background: white; padding: 1.5rem; border-radius: 4px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); margin-bottom: 2rem;">
|
|
42
|
+
<h3 style="margin-top: 0;">Registered Triggers</h3>
|
|
43
|
+
<% if @table_info[:registry_triggers].any? %>
|
|
44
|
+
<div style="display: flex; flex-direction: column; gap: 1rem;">
|
|
45
|
+
<% @table_info[:registry_triggers].each do |trigger| %>
|
|
46
|
+
<div style="border: 1px solid #dee2e6; border-radius: 4px; padding: 1rem; background: #f8f9fa;">
|
|
47
|
+
<div style="display: flex; justify-content: space-between; align-items: start; margin-bottom: 1rem;">
|
|
48
|
+
<div>
|
|
49
|
+
<h4 style="margin: 0; color: #007bff;">
|
|
50
|
+
<%= trigger.trigger_name %>
|
|
51
|
+
<% if trigger.enabled %>
|
|
52
|
+
<span class="badge badge-success">Enabled</span>
|
|
53
|
+
<% else %>
|
|
54
|
+
<span class="badge badge-danger">Disabled</span>
|
|
55
|
+
<% end %>
|
|
56
|
+
</h4>
|
|
57
|
+
<p style="color: #6c757d; margin: 0.5rem 0 0 0;">
|
|
58
|
+
Version: <strong><%= trigger.version %></strong> |
|
|
59
|
+
Source: <span class="badge badge-info"><%= trigger.source %></span>
|
|
60
|
+
<% if trigger.environment.present? %>
|
|
61
|
+
| Environment: <strong><%= trigger.environment %></strong>
|
|
62
|
+
<% end %>
|
|
63
|
+
</p>
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
|
|
67
|
+
<% if trigger.definition.present? %>
|
|
68
|
+
<% definition = JSON.parse(trigger.definition) rescue {} %>
|
|
69
|
+
<div style="margin-bottom: 1rem;">
|
|
70
|
+
<strong>Function:</strong> <code style="background: #e9ecef; padding: 0.25rem 0.5rem; border-radius: 2px;"><%= definition["function_name"] || "N/A" %></code>
|
|
71
|
+
<% if definition["events"].present? %>
|
|
72
|
+
<span style="margin-left: 1rem;">
|
|
73
|
+
<strong>Events:</strong>
|
|
74
|
+
<% definition["events"].each do |event| %>
|
|
75
|
+
<span class="badge badge-info" style="margin-left: 0.25rem;"><%= event %></span>
|
|
76
|
+
<% end %>
|
|
77
|
+
</span>
|
|
78
|
+
<% end %>
|
|
79
|
+
</div>
|
|
80
|
+
<% end %>
|
|
81
|
+
|
|
82
|
+
<% if trigger.function_body.present? %>
|
|
83
|
+
<details style="margin-top: 1rem;">
|
|
84
|
+
<summary style="cursor: pointer; font-weight: 600; color: #495057;">View Function Body</summary>
|
|
85
|
+
<pre style="background: #f8f9fa; padding: 1rem; border-radius: 4px; margin-top: 0.5rem; overflow-x: auto;"><code><%= trigger.function_body %></code></pre>
|
|
86
|
+
</details>
|
|
87
|
+
<% end %>
|
|
88
|
+
</div>
|
|
89
|
+
<% end %>
|
|
90
|
+
</div>
|
|
91
|
+
<% else %>
|
|
92
|
+
<div style="background: #e7f3ff; border-left: 4px solid #007bff; padding: 1rem; border-radius: 4px;">
|
|
93
|
+
<p style="margin: 0;">No registered triggers for this table.</p>
|
|
94
|
+
<%= 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;" %>
|
|
95
|
+
</div>
|
|
96
|
+
<% end %>
|
|
97
|
+
</div>
|
|
98
|
+
|
|
99
|
+
<!-- Database Triggers (not in registry) -->
|
|
100
|
+
<% if @table_info[:database_triggers].any? %>
|
|
101
|
+
<div style="background: #fff3cd; padding: 1.5rem; border-radius: 4px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); border-left: 4px solid #ffc107;">
|
|
102
|
+
<h3 style="margin-top: 0; color: #856404;">Database Triggers (Not in Registry)</h3>
|
|
103
|
+
<p style="color: #856404; margin-bottom: 1rem;">
|
|
104
|
+
These triggers exist in the database but are not registered in the trigger registry.
|
|
105
|
+
Consider registering them or reviewing their status.
|
|
106
|
+
</p>
|
|
107
|
+
<div style="display: flex; flex-direction: column; gap: 1rem;">
|
|
108
|
+
<% @table_info[:database_triggers].each do |trigger| %>
|
|
109
|
+
<div style="border: 1px solid #ffc107; border-radius: 4px; padding: 1rem; background: white;">
|
|
110
|
+
<h4 style="margin: 0; color: #856404;">
|
|
111
|
+
<%= trigger[:trigger_name] %>
|
|
112
|
+
<span class="badge badge-warning">DB Only</span>
|
|
113
|
+
</h4>
|
|
114
|
+
<p style="color: #6c757d; margin: 0.5rem 0 0 0;">
|
|
115
|
+
Function: <code style="background: #e9ecef; padding: 0.125rem 0.25rem; border-radius: 2px;"><%= trigger[:function_name] %></code>
|
|
116
|
+
</p>
|
|
117
|
+
<details style="margin-top: 0.5rem;">
|
|
118
|
+
<summary style="cursor: pointer; font-weight: 600; color: #495057; font-size: 0.875rem;">View Definition</summary>
|
|
119
|
+
<pre style="background: #f8f9fa; padding: 1rem; border-radius: 4px; margin-top: 0.5rem; overflow-x: auto; font-size: 0.875rem;"><code><%= trigger[:definition] %></code></pre>
|
|
120
|
+
</details>
|
|
121
|
+
</div>
|
|
122
|
+
<% end %>
|
|
123
|
+
</div>
|
|
124
|
+
</div>
|
|
125
|
+
<% end %>
|
|
126
|
+
|
data/config/routes.rb
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
begin
|
|
4
|
+
PgSqlTriggers::Engine.routes.draw do
|
|
5
|
+
root to: "dashboard#index"
|
|
6
|
+
get "dashboard", to: "dashboard#index", as: "dashboard"
|
|
7
|
+
|
|
8
|
+
resources :tables, only: %i[index show]
|
|
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
|
+
resources :migrations, only: [] do
|
|
25
|
+
collection do
|
|
26
|
+
post :up
|
|
27
|
+
post :down
|
|
28
|
+
post :redo
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
rescue ArgumentError => e
|
|
33
|
+
# Ignore duplicate route errors (routes may already be drawn in tests)
|
|
34
|
+
raise unless e.message.include?("already in use")
|
|
35
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class CreatePgSqlTriggersTables < ActiveRecord::Migration[6.0]
|
|
4
|
+
def change
|
|
5
|
+
# Registry table - source of truth for all triggers
|
|
6
|
+
create_table :pg_sql_triggers_registry do |t|
|
|
7
|
+
t.string :trigger_name, null: false
|
|
8
|
+
t.string :table_name, null: false
|
|
9
|
+
t.integer :version, null: false, default: 1
|
|
10
|
+
t.boolean :enabled, null: false, default: false
|
|
11
|
+
t.string :checksum, null: false
|
|
12
|
+
t.string :source, null: false # dsl, generated, manual_sql
|
|
13
|
+
t.string :environment
|
|
14
|
+
t.text :definition # Stored DSL or SQL definition
|
|
15
|
+
t.text :function_body # The actual function body
|
|
16
|
+
t.text :condition # Optional WHEN clause condition
|
|
17
|
+
t.datetime :installed_at
|
|
18
|
+
t.datetime :last_verified_at
|
|
19
|
+
|
|
20
|
+
t.timestamps
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
add_index :pg_sql_triggers_registry, :trigger_name, unique: true
|
|
24
|
+
add_index :pg_sql_triggers_registry, :table_name
|
|
25
|
+
add_index :pg_sql_triggers_registry, :enabled
|
|
26
|
+
add_index :pg_sql_triggers_registry, :source
|
|
27
|
+
add_index :pg_sql_triggers_registry, :environment
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators"
|
|
4
|
+
require "rails/generators/migration"
|
|
5
|
+
|
|
6
|
+
module PgSqlTriggers
|
|
7
|
+
module Generators
|
|
8
|
+
class InstallGenerator < Rails::Generators::Base
|
|
9
|
+
include Rails::Generators::Migration
|
|
10
|
+
|
|
11
|
+
source_root File.expand_path("templates", __dir__)
|
|
12
|
+
|
|
13
|
+
def self.next_migration_number(dirname)
|
|
14
|
+
next_migration_number = current_migration_number(dirname) + 1
|
|
15
|
+
ActiveRecord::Migration.next_migration_number(next_migration_number)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def copy_initializer
|
|
19
|
+
template "initializer.rb", "config/initializers/pg_sql_triggers.rb"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def copy_migrations
|
|
23
|
+
migration_template "create_pg_sql_triggers_tables.rb",
|
|
24
|
+
"db/migrate/create_pg_sql_triggers_tables.rb"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def mount_engine
|
|
28
|
+
route 'mount PgSqlTriggers::Engine => "/pg_sql_triggers"'
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def show_readme
|
|
32
|
+
readme "README" if behavior == :invoke
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
===============================================================================
|
|
2
|
+
|
|
3
|
+
PgSqlTriggers has been installed!
|
|
4
|
+
|
|
5
|
+
Next steps:
|
|
6
|
+
|
|
7
|
+
1. Run migrations:
|
|
8
|
+
$ rails db:migrate
|
|
9
|
+
|
|
10
|
+
2. Mount the engine in your routes.rb (already done):
|
|
11
|
+
mount PgSqlTriggers::Engine => "/pg_sql_triggers"
|
|
12
|
+
|
|
13
|
+
3. Configure the gem in config/initializers/pg_sql_triggers.rb
|
|
14
|
+
|
|
15
|
+
4. Create your first trigger:
|
|
16
|
+
|
|
17
|
+
Option A: Using DSL (app/triggers/example_trigger.rb):
|
|
18
|
+
PgSqlTriggers::DSL.pg_sql_trigger "example_guard" do
|
|
19
|
+
table :users
|
|
20
|
+
on :insert, :update
|
|
21
|
+
function :validate_user_rules
|
|
22
|
+
version 1
|
|
23
|
+
enabled false
|
|
24
|
+
when_env :production
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
Option B: Using trigger migrations:
|
|
28
|
+
rails generate trigger:migration add_example_trigger
|
|
29
|
+
# Edit db/triggers/YYYYMMDDHHMMSS_add_example_trigger.rb
|
|
30
|
+
rake trigger:migrate
|
|
31
|
+
|
|
32
|
+
5. Visit http://localhost:3000/pg_sql_triggers to access the UI
|
|
33
|
+
|
|
34
|
+
For more information, see: https://github.com/samaswin87/pg_sql_triggers
|
|
35
|
+
|
|
36
|
+
===============================================================================
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class CreatePgSqlTriggersTables < ActiveRecord::Migration[6.0]
|
|
4
|
+
def change
|
|
5
|
+
# Registry table - source of truth for all triggers
|
|
6
|
+
create_table :pg_sql_triggers_registry do |t|
|
|
7
|
+
t.string :trigger_name, null: false
|
|
8
|
+
t.string :table_name, null: false
|
|
9
|
+
t.integer :version, null: false, default: 1
|
|
10
|
+
t.boolean :enabled, null: false, default: false
|
|
11
|
+
t.string :checksum, null: false
|
|
12
|
+
t.string :source, null: false # dsl, generated, manual_sql
|
|
13
|
+
t.string :environment
|
|
14
|
+
t.text :definition # Stored DSL or SQL definition
|
|
15
|
+
t.text :function_body # The actual function body
|
|
16
|
+
t.text :condition # Optional WHEN clause condition
|
|
17
|
+
t.datetime :installed_at
|
|
18
|
+
t.datetime :last_verified_at
|
|
19
|
+
|
|
20
|
+
t.timestamps
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
add_index :pg_sql_triggers_registry, :trigger_name, unique: true
|
|
24
|
+
add_index :pg_sql_triggers_registry, :table_name
|
|
25
|
+
add_index :pg_sql_triggers_registry, :enabled
|
|
26
|
+
add_index :pg_sql_triggers_registry, :source
|
|
27
|
+
add_index :pg_sql_triggers_registry, :environment
|
|
28
|
+
|
|
29
|
+
# Trigger migrations table - tracks which trigger migrations have been run
|
|
30
|
+
create_table :trigger_migrations do |t|
|
|
31
|
+
t.string :version, null: false
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
add_index :trigger_migrations, :version, unique: true
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
PgSqlTriggers.configure do |config|
|
|
4
|
+
# Enable or disable the production kill switch
|
|
5
|
+
# When enabled, all destructive operations in production require explicit confirmation
|
|
6
|
+
config.kill_switch_enabled = true
|
|
7
|
+
|
|
8
|
+
# Set the default environment detection
|
|
9
|
+
# By default, uses Rails.env
|
|
10
|
+
config.default_environment = -> { Rails.env }
|
|
11
|
+
|
|
12
|
+
# Set a custom permission checker
|
|
13
|
+
# This should return true/false based on the actor, action, and environment
|
|
14
|
+
# Example:
|
|
15
|
+
# config.permission_checker = ->(actor, action, environment) {
|
|
16
|
+
# # Your custom permission logic here
|
|
17
|
+
# # e.g., check if actor has required role for the action
|
|
18
|
+
# true
|
|
19
|
+
# }
|
|
20
|
+
config.permission_checker = nil
|
|
21
|
+
|
|
22
|
+
# Tables to exclude from listing in the UI
|
|
23
|
+
# Default excluded tables: ar_internal_metadata, schema_migrations, pg_sql_triggers_registry, trigger_migrations
|
|
24
|
+
# Add additional tables you want to exclude:
|
|
25
|
+
# config.excluded_tables = %w[audit_logs temporary_data]
|
|
26
|
+
config.excluded_tables = []
|
|
27
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class <%= class_name %> < PgSqlTriggers::Migration
|
|
4
|
+
def up
|
|
5
|
+
# Add your trigger migration code here
|
|
6
|
+
# Example:
|
|
7
|
+
# execute <<-SQL
|
|
8
|
+
# CREATE OR REPLACE FUNCTION my_function()
|
|
9
|
+
# RETURNS TRIGGER AS $$
|
|
10
|
+
# BEGIN
|
|
11
|
+
# -- Your trigger logic here
|
|
12
|
+
# RETURN NEW;
|
|
13
|
+
# END;
|
|
14
|
+
# $$ LANGUAGE plpgsql;
|
|
15
|
+
#
|
|
16
|
+
# CREATE TRIGGER my_trigger
|
|
17
|
+
# BEFORE INSERT ON my_table
|
|
18
|
+
# FOR EACH ROW
|
|
19
|
+
# EXECUTE FUNCTION my_function();
|
|
20
|
+
# SQL
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def down
|
|
24
|
+
# Add your rollback code here
|
|
25
|
+
# Example:
|
|
26
|
+
# execute <<-SQL
|
|
27
|
+
# DROP TRIGGER IF EXISTS my_trigger ON my_table;
|
|
28
|
+
# DROP FUNCTION IF EXISTS my_function();
|
|
29
|
+
# SQL
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators"
|
|
4
|
+
require "rails/generators/migration"
|
|
5
|
+
require "active_support/core_ext/string/inflections"
|
|
6
|
+
|
|
7
|
+
module PgSqlTriggers
|
|
8
|
+
module Generators
|
|
9
|
+
class TriggerMigrationGenerator < Rails::Generators::Base
|
|
10
|
+
include Rails::Generators::Migration
|
|
11
|
+
|
|
12
|
+
source_root File.expand_path("templates", __dir__)
|
|
13
|
+
|
|
14
|
+
argument :name, type: :string, desc: "Name of the trigger migration"
|
|
15
|
+
|
|
16
|
+
def self.next_migration_number(_dirname)
|
|
17
|
+
# Get the highest migration number from existing migrations
|
|
18
|
+
existing = if Rails.root.join("db/triggers").exist?
|
|
19
|
+
Rails.root.glob("db/triggers/*.rb")
|
|
20
|
+
.map { |f| File.basename(f, ".rb").split("_").first.to_i }
|
|
21
|
+
.reject(&:zero?)
|
|
22
|
+
.max || 0
|
|
23
|
+
else
|
|
24
|
+
0
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Generate next timestamp-based version
|
|
28
|
+
# Format: YYYYMMDDHHMMSS
|
|
29
|
+
now = Time.now.utc
|
|
30
|
+
base = now.strftime("%Y%m%d%H%M%S").to_i
|
|
31
|
+
|
|
32
|
+
# If we have existing migrations, ensure we're incrementing
|
|
33
|
+
base = existing + 1 if existing.positive? && base <= existing
|
|
34
|
+
|
|
35
|
+
base
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def create_trigger_migration
|
|
39
|
+
migration_template(
|
|
40
|
+
"trigger_migration.rb.erb",
|
|
41
|
+
"db/triggers/#{file_name}.rb"
|
|
42
|
+
)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def file_name
|
|
48
|
+
"#{migration_number}_#{name.underscore}"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def migration_number
|
|
52
|
+
self.class.next_migration_number(nil)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def class_name
|
|
56
|
+
name.camelize
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators"
|
|
4
|
+
require "rails/generators/migration"
|
|
5
|
+
require "active_support/core_ext/string/inflections"
|
|
6
|
+
|
|
7
|
+
module Trigger
|
|
8
|
+
module Generators
|
|
9
|
+
class MigrationGenerator < Rails::Generators::Base
|
|
10
|
+
include Rails::Generators::Migration
|
|
11
|
+
|
|
12
|
+
source_root File.expand_path("../../pg_sql_triggers/templates", __dir__)
|
|
13
|
+
|
|
14
|
+
argument :name, type: :string, desc: "Name of the trigger migration"
|
|
15
|
+
|
|
16
|
+
def self.next_migration_number(_dirname)
|
|
17
|
+
# Get the highest migration number from existing migrations
|
|
18
|
+
existing = if Rails.root.join("db/triggers").exist?
|
|
19
|
+
Rails.root.glob("db/triggers/*.rb")
|
|
20
|
+
.map { |f| File.basename(f, ".rb").split("_").first.to_i }
|
|
21
|
+
.reject(&:zero?)
|
|
22
|
+
.max || 0
|
|
23
|
+
else
|
|
24
|
+
0
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Generate next timestamp-based version
|
|
28
|
+
# Format: YYYYMMDDHHMMSS
|
|
29
|
+
now = Time.now.utc
|
|
30
|
+
base = now.strftime("%Y%m%d%H%M%S").to_i
|
|
31
|
+
|
|
32
|
+
# If we have existing migrations, ensure we're incrementing
|
|
33
|
+
base = existing + 1 if existing.positive? && base <= existing
|
|
34
|
+
|
|
35
|
+
base
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def create_trigger_migration
|
|
39
|
+
migration_template(
|
|
40
|
+
"trigger_migration.rb.erb",
|
|
41
|
+
"db/triggers/#{file_name}.rb"
|
|
42
|
+
)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def file_name
|
|
48
|
+
"#{migration_number}_#{name.underscore}"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def class_name
|
|
52
|
+
name.camelize
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def migration_number
|
|
56
|
+
self.class.next_migration_number(nil)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|