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.
Files changed (78) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +397 -1
  3. data/COVERAGE.md +26 -19
  4. data/GEM_ANALYSIS.md +368 -0
  5. data/Goal.md +276 -155
  6. data/README.md +45 -22
  7. data/app/assets/javascripts/pg_sql_triggers/trigger_actions.js +50 -0
  8. data/app/controllers/concerns/pg_sql_triggers/error_handling.rb +56 -0
  9. data/app/controllers/concerns/pg_sql_triggers/kill_switch_protection.rb +66 -0
  10. data/app/controllers/concerns/pg_sql_triggers/permission_checking.rb +117 -0
  11. data/app/controllers/pg_sql_triggers/application_controller.rb +10 -62
  12. data/app/controllers/pg_sql_triggers/audit_logs_controller.rb +102 -0
  13. data/app/controllers/pg_sql_triggers/dashboard_controller.rb +4 -9
  14. data/app/controllers/pg_sql_triggers/tables_controller.rb +30 -4
  15. data/app/controllers/pg_sql_triggers/triggers_controller.rb +3 -21
  16. data/app/helpers/pg_sql_triggers/permissions_helper.rb +43 -0
  17. data/app/models/pg_sql_triggers/audit_log.rb +106 -0
  18. data/app/models/pg_sql_triggers/trigger_registry.rb +218 -13
  19. data/app/views/layouts/pg_sql_triggers/application.html.erb +25 -6
  20. data/app/views/pg_sql_triggers/audit_logs/index.html.erb +177 -0
  21. data/app/views/pg_sql_triggers/dashboard/index.html.erb +34 -12
  22. data/app/views/pg_sql_triggers/tables/index.html.erb +75 -5
  23. data/app/views/pg_sql_triggers/tables/show.html.erb +17 -6
  24. data/app/views/pg_sql_triggers/triggers/_drop_modal.html.erb +16 -7
  25. data/app/views/pg_sql_triggers/triggers/_re_execute_modal.html.erb +16 -7
  26. data/app/views/pg_sql_triggers/triggers/show.html.erb +26 -6
  27. data/config/routes.rb +2 -14
  28. data/db/migrate/20260103000001_create_pg_sql_triggers_audit_log.rb +28 -0
  29. data/db/migrate/20260228000001_add_for_each_to_pg_sql_triggers_registry.rb +8 -0
  30. data/docs/README.md +15 -5
  31. data/docs/api-reference.md +233 -151
  32. data/docs/audit-trail.md +413 -0
  33. data/docs/configuration.md +28 -7
  34. data/docs/getting-started.md +17 -16
  35. data/docs/permissions.md +369 -0
  36. data/docs/troubleshooting.md +486 -0
  37. data/docs/ui-guide.md +211 -0
  38. data/docs/usage-guide.md +38 -67
  39. data/docs/web-ui.md +251 -128
  40. data/lib/generators/pg_sql_triggers/templates/trigger_dsl.rb.tt +11 -0
  41. data/lib/generators/pg_sql_triggers/templates/trigger_migration_full.rb.tt +29 -0
  42. data/lib/generators/pg_sql_triggers/trigger_generator.rb +83 -0
  43. data/lib/pg_sql_triggers/drift/db_queries.rb +12 -8
  44. data/lib/pg_sql_triggers/drift/detector.rb +51 -38
  45. data/lib/pg_sql_triggers/dsl/trigger_definition.rb +17 -23
  46. data/lib/pg_sql_triggers/engine.rb +14 -0
  47. data/lib/pg_sql_triggers/errors.rb +245 -0
  48. data/lib/pg_sql_triggers/migrator/pre_apply_comparator.rb +8 -9
  49. data/lib/pg_sql_triggers/migrator/safety_validator.rb +32 -12
  50. data/lib/pg_sql_triggers/migrator.rb +53 -6
  51. data/lib/pg_sql_triggers/permissions/checker.rb +9 -2
  52. data/lib/pg_sql_triggers/registry/manager.rb +36 -11
  53. data/lib/pg_sql_triggers/registry/validator.rb +62 -5
  54. data/lib/pg_sql_triggers/registry.rb +141 -8
  55. data/lib/pg_sql_triggers/sql/kill_switch.rb +153 -247
  56. data/lib/pg_sql_triggers/sql.rb +0 -6
  57. data/lib/pg_sql_triggers/testing/function_tester.rb +2 -0
  58. data/lib/pg_sql_triggers/version.rb +1 -1
  59. data/lib/pg_sql_triggers.rb +7 -7
  60. data/pg_sql_triggers.gemspec +53 -0
  61. metadata +35 -18
  62. data/app/controllers/pg_sql_triggers/generator_controller.rb +0 -213
  63. data/app/controllers/pg_sql_triggers/sql_capsules_controller.rb +0 -161
  64. data/app/views/pg_sql_triggers/generator/new.html.erb +0 -388
  65. data/app/views/pg_sql_triggers/generator/preview.html.erb +0 -305
  66. data/app/views/pg_sql_triggers/sql_capsules/new.html.erb +0 -81
  67. data/app/views/pg_sql_triggers/sql_capsules/show.html.erb +0 -85
  68. data/docs/screenshots/.gitkeep +0 -1
  69. data/docs/screenshots/Generate Trigger.png +0 -0
  70. data/docs/screenshots/Triggers Page.png +0 -0
  71. data/docs/screenshots/kill error.png +0 -0
  72. data/docs/screenshots/kill modal for migration down.png +0 -0
  73. data/lib/generators/trigger/migration_generator.rb +0 -60
  74. data/lib/pg_sql_triggers/generator/form.rb +0 -80
  75. data/lib/pg_sql_triggers/generator/service.rb +0 -307
  76. data/lib/pg_sql_triggers/generator.rb +0 -8
  77. data/lib/pg_sql_triggers/sql/capsule.rb +0 -79
  78. data/lib/pg_sql_triggers/sql/executor.rb +0 -200
@@ -0,0 +1,413 @@
1
+ # Audit Trail Guide
2
+
3
+ PgSqlTriggers provides comprehensive audit logging for all trigger operations. This guide explains how to view, filter, and export audit logs.
4
+
5
+ ## Table of Contents
6
+
7
+ - [Overview](#overview)
8
+ - [Accessing Audit Logs](#accessing-audit-logs)
9
+ - [Viewing Audit Logs](#viewing-audit-logs)
10
+ - [Filtering Logs](#filtering-logs)
11
+ - [Exporting Logs](#exporting-logs)
12
+ - [Console API](#console-api)
13
+ - [Logged Operations](#logged-operations)
14
+ - [Audit Log Structure](#audit-log-structure)
15
+
16
+ ## Overview
17
+
18
+ The audit trail captures:
19
+
20
+ - **Who**: Actor information (user type and ID)
21
+ - **What**: Operation performed (enable, disable, drop, re-execute, etc.)
22
+ - **When**: Timestamp of the operation
23
+ - **Where**: Environment where operation occurred
24
+ - **Result**: Success or failure status
25
+ - **Context**: Before/after state, reasons, confirmation text, error messages
26
+
27
+ All operations are automatically logged, providing a complete audit trail for compliance and debugging.
28
+
29
+ ## Accessing Audit Logs
30
+
31
+ ### Via Web UI
32
+
33
+ Navigate to the Audit Log page:
34
+
35
+ 1. Go to the PgSqlTriggers dashboard
36
+ 2. Click "Audit Log" in the navigation menu
37
+ 3. URL: `http://localhost:3000/pg_sql_triggers/audit_logs`
38
+
39
+ ### Via Console API
40
+
41
+ ```ruby
42
+ # Get all audit logs
43
+ PgSqlTriggers::AuditLog.all
44
+
45
+ # Get logs for a specific trigger
46
+ PgSqlTriggers::AuditLog.for_trigger_name("users_email_validation")
47
+
48
+ # Get recent logs
49
+ PgSqlTriggers::AuditLog.recent.limit(100)
50
+ ```
51
+
52
+ ## Viewing Audit Logs
53
+
54
+ ### Audit Log Table
55
+
56
+ The audit log table displays:
57
+
58
+ - **Timestamp**: When the operation occurred
59
+ - **Operation**: Type of operation (enable, disable, drop, etc.)
60
+ - **Trigger Name**: Affected trigger (if applicable)
61
+ - **Actor**: Who performed the operation (type and ID)
62
+ - **Environment**: Environment where operation occurred
63
+ - **Status**: Success or failure
64
+ - **Reason**: Reason provided (for drop/re-execute operations)
65
+
66
+ ### Viewing Details
67
+
68
+ Click on any audit log entry to view detailed information:
69
+
70
+ - **Before State**: State before the operation (enabled/disabled, function body, etc.)
71
+ - **After State**: State after the operation
72
+ - **Diff**: Changes made (for re-execute operations)
73
+ - **Confirmation Text**: Confirmation text used (if applicable)
74
+ - **Error Message**: Error details (for failed operations)
75
+
76
+ ## Filtering Logs
77
+
78
+ ### Filter Options
79
+
80
+ Filter audit logs by:
81
+
82
+ - **Trigger Name**: Filter by specific trigger
83
+ - **Operation**: Filter by operation type (enable, disable, drop, etc.)
84
+ - **Status**: Filter by success or failure
85
+ - **Environment**: Filter by environment (production, staging, etc.)
86
+ - **Sort Order**: Newest first or oldest first
87
+
88
+ ### Using Filters in UI
89
+
90
+ 1. Navigate to Audit Log page
91
+ 2. Use filter dropdowns at the top
92
+ 3. Click "Apply Filters"
93
+ 4. Use "Clear" to reset filters
94
+
95
+ ### Filtering via Console API
96
+
97
+ ```ruby
98
+ # Filter by trigger
99
+ PgSqlTriggers::AuditLog.for_trigger("users_email_validation")
100
+
101
+ # Filter by operation
102
+ PgSqlTriggers::AuditLog.for_operation("trigger_enable")
103
+
104
+ # Filter by environment
105
+ PgSqlTriggers::AuditLog.for_environment("production")
106
+
107
+ # Filter by status
108
+ PgSqlTriggers::AuditLog.successful
109
+ PgSqlTriggers::AuditLog.failed
110
+
111
+ # Combine filters
112
+ PgSqlTriggers::AuditLog
113
+ .for_trigger("users_email_validation")
114
+ .for_operation("trigger_enable")
115
+ .successful
116
+ .recent
117
+ .limit(50)
118
+ ```
119
+
120
+ ### Advanced Filtering
121
+
122
+ ```ruby
123
+ # Get failed operations in production
124
+ PgSqlTriggers::AuditLog
125
+ .for_environment("production")
126
+ .failed
127
+ .recent
128
+
129
+ # Get all drop operations
130
+ PgSqlTriggers::AuditLog
131
+ .for_operation("trigger_drop")
132
+ .includes(:trigger_name)
133
+
134
+ # Get operations by specific actor
135
+ PgSqlTriggers::AuditLog
136
+ .where("actor->>'type' = ? AND actor->>'id' = ?", "User", "123")
137
+ ```
138
+
139
+ ## Exporting Logs
140
+
141
+ ### CSV Export via UI
142
+
143
+ 1. Apply any desired filters
144
+ 2. Click "Export CSV" button
145
+ 3. CSV file downloads with all visible entries
146
+ 4. Filters are preserved in the export
147
+
148
+ ### CSV Export Format
149
+
150
+ The CSV includes all columns:
151
+
152
+ - Timestamp
153
+ - Operation
154
+ - Trigger Name
155
+ - Actor Type
156
+ - Actor ID
157
+ - Environment
158
+ - Status
159
+ - Reason
160
+ - Error Message (for failures)
161
+ - Created At
162
+
163
+ ### Programmatic Export
164
+
165
+ ```ruby
166
+ # Export to CSV
167
+ require 'csv'
168
+
169
+ logs = PgSqlTriggers::AuditLog.recent.limit(1000)
170
+
171
+ CSV.open("audit_logs.csv", "w") do |csv|
172
+ csv << ["Timestamp", "Operation", "Trigger", "Actor", "Environment", "Status", "Reason"]
173
+
174
+ logs.each do |log|
175
+ csv << [
176
+ log.created_at,
177
+ log.operation,
178
+ log.trigger_name,
179
+ "#{log.actor['type']}:#{log.actor['id']}",
180
+ log.environment,
181
+ log.status,
182
+ log.reason
183
+ ]
184
+ end
185
+ end
186
+ ```
187
+
188
+ ## Console API
189
+
190
+ ### Querying Audit Logs
191
+
192
+ ```ruby
193
+ # Get logs for a trigger
194
+ PgSqlTriggers::AuditLog.for_trigger_name("users_email_validation")
195
+
196
+ # Returns: ActiveRecord::Relation ordered by most recent first
197
+ ```
198
+
199
+ ### Logging Operations
200
+
201
+ Operations are automatically logged, but you can also log manually:
202
+
203
+ ```ruby
204
+ # Log a successful operation
205
+ PgSqlTriggers::AuditLog.log_success(
206
+ operation: :trigger_enable,
207
+ trigger_name: "users_email_validation",
208
+ actor: { type: "Console", id: "admin@example.com" },
209
+ environment: "production",
210
+ before_state: { enabled: false },
211
+ after_state: { enabled: true }
212
+ )
213
+
214
+ # Log a failed operation
215
+ PgSqlTriggers::AuditLog.log_failure(
216
+ operation: :trigger_drop,
217
+ trigger_name: "old_trigger",
218
+ actor: { type: "UI", id: "user_123" },
219
+ environment: "production",
220
+ error_message: "Trigger not found",
221
+ reason: "Cleanup"
222
+ )
223
+ ```
224
+
225
+ ## Logged Operations
226
+
227
+ The following operations are automatically logged:
228
+
229
+ ### Trigger Operations
230
+
231
+ - **`trigger_enable`**: Trigger enabled
232
+ - **`trigger_disable`**: Trigger disabled
233
+ - **`trigger_drop`**: Trigger dropped from database
234
+ - **`trigger_re_execute`**: Trigger re-executed (drop and recreate)
235
+
236
+ ### Migration Operations
237
+
238
+ - **`migration_up`**: Migration applied (up)
239
+ - **`migration_down`**: Migration rolled back (down)
240
+
241
+ ### SQL Capsule Operations
242
+
243
+ - **`sql_capsule_execute`**: SQL capsule executed
244
+ - **`sql_capsule_dry_run`**: SQL capsule dry-run performed
245
+
246
+ ### Generator Operations
247
+
248
+ - **`trigger_generate`**: Trigger generated via UI
249
+
250
+ ## Audit Log Structure
251
+
252
+ ### Database Schema
253
+
254
+ The audit log table (`pg_sql_triggers_audit_log`) contains:
255
+
256
+ | Column | Type | Description |
257
+ |--------|------|-------------|
258
+ | `id` | integer | Primary key |
259
+ | `trigger_name` | string | Trigger name (nullable) |
260
+ | `operation` | string | Operation type |
261
+ | `actor` | jsonb | Actor information (type, id) |
262
+ | `environment` | string | Environment name |
263
+ | `status` | string | "success" or "failure" |
264
+ | `reason` | text | Reason for operation (nullable) |
265
+ | `confirmation_text` | text | Confirmation text used (nullable) |
266
+ | `before_state` | jsonb | State before operation (nullable) |
267
+ | `after_state` | jsonb | State after operation (nullable) |
268
+ | `diff` | text | Diff information (nullable) |
269
+ | `error_message` | text | Error message (for failures) |
270
+ | `created_at` | timestamp | When operation occurred |
271
+ | `updated_at` | timestamp | Last update time |
272
+
273
+ ### Actor Format
274
+
275
+ Actor information is stored as JSON:
276
+
277
+ ```json
278
+ {
279
+ "type": "User",
280
+ "id": "123"
281
+ }
282
+ ```
283
+
284
+ Or for console operations:
285
+
286
+ ```json
287
+ {
288
+ "type": "Console",
289
+ "id": "admin@example.com"
290
+ }
291
+ ```
292
+
293
+ ### State Format
294
+
295
+ Before and after states are stored as JSON:
296
+
297
+ ```json
298
+ {
299
+ "enabled": true,
300
+ "function_body": "CREATE FUNCTION...",
301
+ "version": 1,
302
+ "drift_state": "in_sync"
303
+ }
304
+ ```
305
+
306
+ ## Use Cases
307
+
308
+ ### Compliance Auditing
309
+
310
+ Track who performed what operations for compliance:
311
+
312
+ ```ruby
313
+ # Get all admin operations in production
314
+ PgSqlTriggers::AuditLog
315
+ .for_environment("production")
316
+ .where("actor->>'type' = ?", "Admin")
317
+ .recent
318
+ ```
319
+
320
+ ### Debugging Failed Operations
321
+
322
+ Find and analyze failed operations:
323
+
324
+ ```ruby
325
+ # Get recent failures
326
+ failures = PgSqlTriggers::AuditLog.failed.recent.limit(50)
327
+
328
+ failures.each do |log|
329
+ puts "#{log.created_at}: #{log.operation} on #{log.trigger_name}"
330
+ puts "Error: #{log.error_message}"
331
+ puts "Actor: #{log.actor}"
332
+ puts "---"
333
+ end
334
+ ```
335
+
336
+ ### Trigger History
337
+
338
+ View complete history of a trigger:
339
+
340
+ ```ruby
341
+ history = PgSqlTriggers::AuditLog.for_trigger_name("users_email_validation")
342
+
343
+ history.each do |log|
344
+ puts "#{log.created_at}: #{log.operation} - #{log.status}"
345
+ if log.reason
346
+ puts " Reason: #{log.reason}"
347
+ end
348
+ end
349
+ ```
350
+
351
+ ### Operation Analysis
352
+
353
+ Analyze operation patterns:
354
+
355
+ ```ruby
356
+ # Count operations by type
357
+ PgSqlTriggers::AuditLog
358
+ .group(:operation)
359
+ .count
360
+
361
+ # Count failures by operation
362
+ PgSqlTriggers::AuditLog
363
+ .failed
364
+ .group(:operation)
365
+ .count
366
+
367
+ # Operations by environment
368
+ PgSqlTriggers::AuditLog
369
+ .group(:environment)
370
+ .count
371
+ ```
372
+
373
+ ## Best Practices
374
+
375
+ 1. **Regular Reviews**: Review audit logs regularly for anomalies
376
+ 2. **Retention Policy**: Implement a retention policy for old logs
377
+ 3. **Monitoring**: Set up alerts for failed operations
378
+ 4. **Backup**: Include audit logs in your backup strategy
379
+ 5. **Analysis**: Use audit logs to understand usage patterns
380
+
381
+ ## Troubleshooting
382
+
383
+ ### Audit logs not appearing
384
+
385
+ **Problem**: Operations are not being logged.
386
+
387
+ **Solution**:
388
+ - Check that migrations are run (`rails db:migrate`)
389
+ - Verify the audit log table exists
390
+ - Check Rails logs for audit logging errors
391
+
392
+ ### Performance issues with large logs
393
+
394
+ **Problem**: Audit log queries are slow.
395
+
396
+ **Solution**:
397
+ - Add indexes on frequently queried columns
398
+ - Implement pagination
399
+ - Archive old logs regularly
400
+ - Use filtering to reduce result set size
401
+
402
+ ### Missing actor information
403
+
404
+ **Problem**: Actor shows as "unknown".
405
+
406
+ **Solution**: Ensure `current_actor` method is properly implemented in your controllers.
407
+
408
+ ## Related Documentation
409
+
410
+ - [Web UI Guide](web-ui.md#audit-log) - Using the audit log UI
411
+ - [API Reference](api-reference.md#audit-log-api) - Console API methods
412
+ - [Configuration Reference](configuration.md) - Configuration options
413
+
@@ -50,6 +50,25 @@ config.default_environment = -> {
50
50
  config.default_environment = -> { 'production' }
51
51
  ```
52
52
 
53
+ ### `db_schema`
54
+
55
+ The PostgreSQL schema in which triggers are managed. All system catalog queries use this value. Override when your triggers live in a non-`public` schema.
56
+
57
+ - **Type**: String
58
+ - **Default**: `"public"`
59
+
60
+ ```ruby
61
+ config.db_schema = "public" # default
62
+
63
+ # Use a custom schema
64
+ config.db_schema = "app"
65
+ ```
66
+
67
+ You can also set this at runtime:
68
+ ```ruby
69
+ PgSqlTriggers.db_schema = "app"
70
+ ```
71
+
53
72
  ### `mount_path`
54
73
 
55
74
  Customize where the web UI is mounted (configured in routes, not initializer).
@@ -192,6 +211,8 @@ Custom authorization logic for the web UI and API.
192
211
  - **Returns**: Boolean
193
212
  - **Default**: `->(_actor, _action, _environment) { true }`
194
213
 
214
+ > **Important**: If `permission_checker` is `nil` (not configured) and the app boots in **production**, the engine emits a `Rails.logger.warn` at startup. Configure a real checker before deploying to production.
215
+
195
216
  ```ruby
196
217
  # Default: allow all (development only!)
197
218
  config.permission_checker = ->(_actor, _action, _environment) { true }
@@ -208,12 +229,12 @@ config.permission_checker = ->(actor, action, environment) {
208
229
  return false unless user
209
230
 
210
231
  case action
211
- when :view
212
- user.present?
213
- when :operate
214
- user.operator? || user.admin?
215
- when :admin
216
- user.admin?
232
+ when :view_triggers, :view_diffs
233
+ user.present? # Viewer level
234
+ when :enable_trigger, :disable_trigger, :apply_trigger, :test_trigger
235
+ user.operator? || user.admin? # Operator level
236
+ when :drop_trigger, :override_drift
237
+ user.admin? # Admin level
217
238
  else
218
239
  false
219
240
  end
@@ -287,7 +308,7 @@ The permission checker should handle three levels:
287
308
  #### `:admin`
288
309
  - All `:operate` permissions
289
310
  - Drop triggers
290
- - Execute SQL capsules
311
+ - Override drift
291
312
  - Modify registry directly
292
313
 
293
314
  ## Environment Detection
@@ -45,9 +45,19 @@ After installation, you should have:
45
45
 
46
46
  ## Quick Start Example
47
47
 
48
- ### 1. Create Your First Trigger Definition
48
+ ### 1. Scaffold a Trigger (DSL + Migration)
49
49
 
50
- Create a trigger definition file:
50
+ Use the CLI generator to create both files at once:
51
+
52
+ ```bash
53
+ rails generate pg_sql_triggers:trigger users_email_validation users insert update --timing before --function validate_user_email
54
+ ```
55
+
56
+ This creates:
57
+ - `app/triggers/users_email_validation.rb` — DSL stub
58
+ - `db/triggers/TIMESTAMP_users_email_validation.rb` — migration with function + trigger SQL
59
+
60
+ Edit the DSL stub to match your requirements:
51
61
 
52
62
  ```ruby
53
63
  # app/triggers/users_email_validation.rb
@@ -56,21 +66,12 @@ PgSqlTriggers::DSL.pg_sql_trigger "users_email_validation" do
56
66
  on :insert, :update
57
67
  function :validate_user_email
58
68
 
59
- version 1
60
- enabled false
61
-
62
- when_env :production
69
+ self.version = 1
70
+ self.enabled = true
71
+ timing :before
63
72
  end
64
73
  ```
65
74
 
66
- ### 2. Generate a Migration
67
-
68
- Create a trigger migration to implement the function:
69
-
70
- ```bash
71
- rails generate trigger:migration add_email_validation
72
- ```
73
-
74
75
  Edit the generated migration in `db/triggers/`:
75
76
 
76
77
  ```ruby
@@ -104,7 +105,7 @@ class AddEmailValidation < PgSqlTriggers::Migration
104
105
  end
105
106
  ```
106
107
 
107
- ### 3. Run the Migration
108
+ ### 2. Run the Migration
108
109
 
109
110
  Apply the trigger migration:
110
111
 
@@ -112,7 +113,7 @@ Apply the trigger migration:
112
113
  rake trigger:migrate
113
114
  ```
114
115
 
115
- ### 4. Access the Web UI
116
+ ### 3. Access the Web UI
116
117
 
117
118
  Open your browser and navigate to:
118
119