whodunit-chronicles 0.1.0 → 0.3.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.
@@ -8,11 +8,15 @@ require_relative 'chronicles/version'
8
8
  require_relative 'chronicles/configuration'
9
9
  require_relative 'chronicles/change_event'
10
10
  require_relative 'chronicles/stream_adapter'
11
- require_relative 'chronicles/audit_processor'
11
+ require_relative 'chronicles/connection'
12
+ require_relative 'chronicles/table'
13
+ require_relative 'chronicles/persistence'
14
+ require_relative 'chronicles/processor'
12
15
  require_relative 'chronicles/service'
13
16
 
14
- # Adapters
15
- require_relative 'chronicles/adapters/postgresql'
17
+ require_relative 'chronicles/errors'
18
+ require_relative 'chronicles/adapter_loader'
19
+ require_relative 'chronicles/composite_processor'
16
20
 
17
21
  module Whodunit
18
22
  # Chronicles - The complete historical record of `whodunit did what?` data
@@ -28,17 +32,15 @@ module Whodunit
28
32
  setting :database_url, default: ENV.fetch('DATABASE_URL', nil)
29
33
  setting :audit_database_url, default: ENV.fetch('AUDIT_DATABASE_URL', nil)
30
34
  setting :adapter, default: :postgresql
35
+ # PostgreSQL-specific settings
31
36
  setting :publication_name, default: 'whodunit_audit'
32
37
  setting :replication_slot_name, default: 'whodunit_audit_slot'
38
+ # MySQL-specific settings
39
+ setting :mysql_server_id, default: 1001
33
40
  setting :batch_size, default: 100
34
41
  setting :max_retry_attempts, default: 3
35
42
  setting :retry_delay, default: 5
36
43
 
37
- class Error < StandardError; end
38
- class ConfigurationError < Error; end
39
- class AdapterError < Error; end
40
- class ReplicationError < Error; end
41
-
42
44
  # Configure Chronicles
43
45
  #
44
46
  # @example
@@ -46,6 +48,9 @@ module Whodunit
46
48
  # config.database_url = "postgresql://localhost/myapp"
47
49
  # config.audit_database_url = "postgresql://localhost/myapp_audit"
48
50
  # config.adapter = :postgresql
51
+ # # OR for MySQL:
52
+ # config.adapter = :mysql
53
+ # config.mysql_server_id = 1001
49
54
  # end
50
55
  def self.configure
51
56
  yield(config) if block_given?
@@ -12,9 +12,9 @@ Gem::Specification.new do |spec|
12
12
  spec.description = 'While Whodunit tracks who made changes, Chronicles captures ' \
13
13
  'what changed by streaming database events into comprehensive ' \
14
14
  'audit trails with zero Rails application overhead.'
15
- spec.homepage = 'https://github.com/whodunit-gem/whodunit-chronicles'
15
+ spec.homepage = 'https://github.com/kanutocd/whodunit-chronicles'
16
16
  spec.license = 'MIT'
17
- spec.required_ruby_version = '>= 3.1.0'
17
+ spec.required_ruby_version = '>= 3.2.0'
18
18
 
19
19
  spec.metadata['allowed_push_host'] = 'https://rubygems.org'
20
20
  spec.metadata['homepage_uri'] = spec.homepage
@@ -33,29 +33,47 @@ Gem::Specification.new do |spec|
33
33
  spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
34
34
  spec.require_paths = ['lib']
35
35
 
36
- # Core dependencies
36
+ # ── Core runtime dependencies ───────────────────────────────
37
37
  spec.add_dependency 'concurrent-ruby', '~> 1.2'
38
38
  spec.add_dependency 'dry-configurable', '~> 1.0'
39
39
  spec.add_dependency 'dry-logger', '~> 1.0'
40
40
 
41
- # Database dependencies
42
- spec.add_dependency 'pg', '~> 1.5'
43
- # Driver for MySQL-compatible database
44
- # spec.add_dependency 'trilogy', '~> 2.9'
41
+ # ── Database adapters — OPTIONAL at runtime ───────────────────────────────
42
+ #
43
+ # Chronicles lazy-loads the driver that matches your configured adapter.
44
+ # You only need to install the gem for the database(s) you actually use:
45
+ #
46
+ # PostgreSQL → gem 'pg', '~> 1.5'
47
+ # MySQL/MariaDB → gem 'trilogy', '~> 2.9'
48
+ #
49
+ # Both are listed here so `bundle install` in development installs them,
50
+ # but they are NOT required at gem load time — only when the adapter loads.
51
+ spec.add_development_dependency 'pg', '~> 1.5'
52
+ spec.add_development_dependency 'trilogy', '~> 2.9'
45
53
 
46
- # Development dependencies
54
+ # bigdecimal: required by trilogy on Ruby 3.4+ (removed from stdlib).
55
+ # Declared here so CI on Ruby 3.4+ doesn't break. Trilogy should own this
56
+ # dependency — track https://github.com/trilogy-libraries/trilogy/issues
57
+ # Required for Ruby 3.4+ compatibility (trilogy dependency)
58
+ spec.add_development_dependency 'bigdecimal', '~> 3.1'
59
+
60
+ # ── Development tooling ───────────────────────────────
47
61
  spec.add_development_dependency 'kramdown', '~> 2.5'
48
62
  spec.add_development_dependency 'minitest', '~> 5.20'
49
63
  spec.add_development_dependency 'mocha', '~> 2.1'
50
64
  spec.add_development_dependency 'pry', '~> 0.14'
51
65
  spec.add_development_dependency 'rake', '~> 13.0'
66
+ spec.add_development_dependency 'rspec_junit_formatter', '~> 0.6.0'
52
67
  spec.add_development_dependency 'rubocop', '~> 1.60'
53
68
  spec.add_development_dependency 'rubocop-minitest', '~> 0.34'
54
69
  spec.add_development_dependency 'rubocop-performance', '~> 1.19'
70
+ spec.add_development_dependency 'rubocop-rake', '~> 0.6'
71
+ spec.add_development_dependency 'rubocop-thread_safety', '~> 0.5'
55
72
  spec.add_development_dependency 'simplecov', '~> 0.22'
73
+ spec.add_development_dependency 'simplecov-cobertura', '~> 3.0'
56
74
  spec.add_development_dependency 'yard', '~> 0.9'
57
75
 
58
- # Security scanning dependencies
59
- spec.add_development_dependency 'brakeman', '~> 6.0'
76
+ # ── Security scanning dependencies ──────────────────────────────
77
+ spec.add_development_dependency 'brakeman', '~> 7.1'
60
78
  spec.add_development_dependency 'bundler-audit', '~> 0.9'
61
79
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: whodunit-chronicles
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ken C. Demanawa
@@ -59,13 +59,41 @@ dependencies:
59
59
  - - "~>"
60
60
  - !ruby/object:Gem::Version
61
61
  version: '1.5'
62
- type: :runtime
62
+ type: :development
63
63
  prerelease: false
64
64
  version_requirements: !ruby/object:Gem::Requirement
65
65
  requirements:
66
66
  - - "~>"
67
67
  - !ruby/object:Gem::Version
68
68
  version: '1.5'
69
+ - !ruby/object:Gem::Dependency
70
+ name: trilogy
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '2.9'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '2.9'
83
+ - !ruby/object:Gem::Dependency
84
+ name: bigdecimal
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '3.1'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '3.1'
69
97
  - !ruby/object:Gem::Dependency
70
98
  name: kramdown
71
99
  requirement: !ruby/object:Gem::Requirement
@@ -136,6 +164,20 @@ dependencies:
136
164
  - - "~>"
137
165
  - !ruby/object:Gem::Version
138
166
  version: '13.0'
167
+ - !ruby/object:Gem::Dependency
168
+ name: rspec_junit_formatter
169
+ requirement: !ruby/object:Gem::Requirement
170
+ requirements:
171
+ - - "~>"
172
+ - !ruby/object:Gem::Version
173
+ version: 0.6.0
174
+ type: :development
175
+ prerelease: false
176
+ version_requirements: !ruby/object:Gem::Requirement
177
+ requirements:
178
+ - - "~>"
179
+ - !ruby/object:Gem::Version
180
+ version: 0.6.0
139
181
  - !ruby/object:Gem::Dependency
140
182
  name: rubocop
141
183
  requirement: !ruby/object:Gem::Requirement
@@ -178,6 +220,34 @@ dependencies:
178
220
  - - "~>"
179
221
  - !ruby/object:Gem::Version
180
222
  version: '1.19'
223
+ - !ruby/object:Gem::Dependency
224
+ name: rubocop-rake
225
+ requirement: !ruby/object:Gem::Requirement
226
+ requirements:
227
+ - - "~>"
228
+ - !ruby/object:Gem::Version
229
+ version: '0.6'
230
+ type: :development
231
+ prerelease: false
232
+ version_requirements: !ruby/object:Gem::Requirement
233
+ requirements:
234
+ - - "~>"
235
+ - !ruby/object:Gem::Version
236
+ version: '0.6'
237
+ - !ruby/object:Gem::Dependency
238
+ name: rubocop-thread_safety
239
+ requirement: !ruby/object:Gem::Requirement
240
+ requirements:
241
+ - - "~>"
242
+ - !ruby/object:Gem::Version
243
+ version: '0.5'
244
+ type: :development
245
+ prerelease: false
246
+ version_requirements: !ruby/object:Gem::Requirement
247
+ requirements:
248
+ - - "~>"
249
+ - !ruby/object:Gem::Version
250
+ version: '0.5'
181
251
  - !ruby/object:Gem::Dependency
182
252
  name: simplecov
183
253
  requirement: !ruby/object:Gem::Requirement
@@ -192,6 +262,20 @@ dependencies:
192
262
  - - "~>"
193
263
  - !ruby/object:Gem::Version
194
264
  version: '0.22'
265
+ - !ruby/object:Gem::Dependency
266
+ name: simplecov-cobertura
267
+ requirement: !ruby/object:Gem::Requirement
268
+ requirements:
269
+ - - "~>"
270
+ - !ruby/object:Gem::Version
271
+ version: '3.0'
272
+ type: :development
273
+ prerelease: false
274
+ version_requirements: !ruby/object:Gem::Requirement
275
+ requirements:
276
+ - - "~>"
277
+ - !ruby/object:Gem::Version
278
+ version: '3.0'
195
279
  - !ruby/object:Gem::Dependency
196
280
  name: yard
197
281
  requirement: !ruby/object:Gem::Requirement
@@ -212,14 +296,14 @@ dependencies:
212
296
  requirements:
213
297
  - - "~>"
214
298
  - !ruby/object:Gem::Version
215
- version: '6.0'
299
+ version: '7.1'
216
300
  type: :development
217
301
  prerelease: false
218
302
  version_requirements: !ruby/object:Gem::Requirement
219
303
  requirements:
220
304
  - - "~>"
221
305
  - !ruby/object:Gem::Version
222
- version: '6.0'
306
+ version: '7.1'
223
307
  - !ruby/object:Gem::Dependency
224
308
  name: bundler-audit
225
309
  requirement: !ruby/object:Gem::Requirement
@@ -243,35 +327,47 @@ executables: []
243
327
  extensions: []
244
328
  extra_rdoc_files: []
245
329
  files:
330
+ - ".codeclimate.yml"
246
331
  - ".rubocop.yml"
247
332
  - ".yardopts"
248
333
  - CHANGELOG.md
249
334
  - CODE_OF_CONDUCT.md
335
+ - CONTRIBUTING.md
250
336
  - LICENSE
251
337
  - README.md
252
338
  - Rakefile
339
+ - docker-compose.yml
340
+ - docker/mysql/init.sql
341
+ - docker/postgres/init.sql
253
342
  - examples/images/campaign-performance-analytics.png
254
343
  - examples/images/candidate-journey-analytics.png
255
344
  - examples/images/recruitment-funnel-analytics.png
256
345
  - lib/.gitkeep
257
346
  - lib/whodunit-chronicles.rb
258
347
  - lib/whodunit/chronicles.rb
348
+ - lib/whodunit/chronicles/adapter_loader.rb
349
+ - lib/whodunit/chronicles/adapters/mysql.rb
259
350
  - lib/whodunit/chronicles/adapters/postgresql.rb
260
- - lib/whodunit/chronicles/audit_processor.rb
261
351
  - lib/whodunit/chronicles/change_event.rb
352
+ - lib/whodunit/chronicles/composite_processor.rb
262
353
  - lib/whodunit/chronicles/configuration.rb
354
+ - lib/whodunit/chronicles/connection.rb
355
+ - lib/whodunit/chronicles/errors.rb
356
+ - lib/whodunit/chronicles/persistence.rb
357
+ - lib/whodunit/chronicles/processor.rb
263
358
  - lib/whodunit/chronicles/service.rb
264
359
  - lib/whodunit/chronicles/stream_adapter.rb
360
+ - lib/whodunit/chronicles/table.rb
265
361
  - lib/whodunit/chronicles/version.rb
266
362
  - whodunit-chronicles.gemspec
267
- homepage: https://github.com/whodunit-gem/whodunit-chronicles
363
+ homepage: https://github.com/kanutocd/whodunit-chronicles
268
364
  licenses:
269
365
  - MIT
270
366
  metadata:
271
367
  allowed_push_host: https://rubygems.org
272
- homepage_uri: https://github.com/whodunit-gem/whodunit-chronicles
273
- source_code_uri: https://github.com/whodunit-gem/whodunit-chronicles
274
- changelog_uri: https://github.com/whodunit-gem/whodunit-chronicles/blob/main/CHANGELOG.md
368
+ homepage_uri: https://github.com/kanutocd/whodunit-chronicles
369
+ source_code_uri: https://github.com/kanutocd/whodunit-chronicles
370
+ changelog_uri: https://github.com/kanutocd/whodunit-chronicles/blob/main/CHANGELOG.md
275
371
  rubygems_mfa_required: 'true'
276
372
  rdoc_options: []
277
373
  require_paths:
@@ -280,7 +376,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
280
376
  requirements:
281
377
  - - ">="
282
378
  - !ruby/object:Gem::Version
283
- version: 3.1.0
379
+ version: 3.2.0
284
380
  required_rubygems_version: !ruby/object:Gem::Requirement
285
381
  requirements:
286
382
  - - ">="
@@ -1,270 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Whodunit
4
- module Chronicles
5
- # Processes database change events and creates audit records
6
- #
7
- # Transforms ChangeEvent objects into structured audit records
8
- # with complete object serialization and metadata.
9
- class AuditProcessor
10
- attr_reader :logger, :audit_connection
11
-
12
- def initialize(
13
- audit_database_url: Chronicles.config.audit_database_url,
14
- logger: Chronicles.logger
15
- )
16
- @audit_database_url = audit_database_url
17
- @logger = logger
18
- @audit_connection = nil
19
- end
20
-
21
- # Process a change event and create audit record
22
- #
23
- # @param change_event [ChangeEvent] The database change to audit
24
- # @return [Hash] The created audit record
25
- def process(change_event)
26
- ensure_audit_connection
27
-
28
- audit_record = build_audit_record(change_event)
29
- persist_audit_record(audit_record)
30
-
31
- log(:debug, 'Processed change event',
32
- table: change_event.qualified_table_name,
33
- action: change_event.action,
34
- audit_id: audit_record[:id])
35
-
36
- audit_record
37
- rescue StandardError => e
38
- log(:error, 'Failed to process change event',
39
- error: e.message,
40
- event: change_event.to_s)
41
- raise
42
- end
43
-
44
- # Process multiple change events in a batch
45
- #
46
- # @param change_events [Array<ChangeEvent>] Array of change events
47
- # @return [Array<Hash>] Array of created audit records
48
- def process_batch(change_events)
49
- return [] if change_events.empty?
50
-
51
- ensure_audit_connection
52
-
53
- audit_records = change_events.map { |event| build_audit_record(event) }
54
- persist_audit_records_batch(audit_records)
55
-
56
- log(:info, 'Processed batch of change events', count: change_events.size)
57
-
58
- audit_records
59
- rescue StandardError => e
60
- log(:error, 'Failed to process batch',
61
- error: e.message,
62
- count: change_events.size)
63
- raise
64
- end
65
-
66
- # Close audit database connection
67
- def close
68
- @audit_connection&.close
69
- @audit_connection = nil
70
- end
71
-
72
- private
73
-
74
- def ensure_audit_connection
75
- return if @audit_connection && !@audit_connection.finished?
76
-
77
- @audit_connection = PG.connect(@audit_database_url || Chronicles.config.database_url)
78
- @audit_connection.type_map_for_results = PG::BasicTypeMapForResults.new(@audit_connection)
79
-
80
- ensure_audit_table_exists
81
- end
82
-
83
- def ensure_audit_table_exists
84
- create_sql = <<~SQL
85
- CREATE TABLE IF NOT EXISTS whodunit_chronicles_audits (
86
- id BIGSERIAL PRIMARY KEY,
87
- table_name TEXT NOT NULL,
88
- schema_name TEXT NOT NULL DEFAULT 'public',
89
- record_id JSONB,
90
- action TEXT NOT NULL CHECK (action IN ('INSERT', 'UPDATE', 'DELETE')),
91
- old_data JSONB,
92
- new_data JSONB,
93
- changes JSONB,
94
- user_id BIGINT,
95
- user_type TEXT,
96
- transaction_id TEXT,
97
- sequence_number INTEGER,
98
- occurred_at TIMESTAMP WITH TIME ZONE NOT NULL,
99
- created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
100
- metadata JSONB DEFAULT '{}'::jsonb,
101
- #{' '}
102
- -- Indexes for performance
103
- CONSTRAINT valid_data_for_action CHECK (
104
- (action = 'INSERT' AND old_data IS NULL AND new_data IS NOT NULL) OR
105
- (action = 'UPDATE' AND old_data IS NOT NULL AND new_data IS NOT NULL) OR#{' '}
106
- (action = 'DELETE' AND old_data IS NOT NULL AND new_data IS NULL)
107
- )
108
- );
109
-
110
- -- Performance indexes
111
- CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_chronicles_audits_table_record#{' '}
112
- ON whodunit_chronicles_audits (table_name, (record_id->>'id'));
113
-
114
- CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_chronicles_audits_occurred_at#{' '}
115
- ON whodunit_chronicles_audits (occurred_at DESC);
116
-
117
- CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_chronicles_audits_user#{' '}
118
- ON whodunit_chronicles_audits (user_id, user_type);
119
-
120
- CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_chronicles_audits_action#{' '}
121
- ON whodunit_chronicles_audits (action);
122
-
123
- -- GIN index for JSONB columns
124
- CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_chronicles_audits_record_id_gin#{' '}
125
- ON whodunit_chronicles_audits USING GIN (record_id);
126
-
127
- CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_chronicles_audits_changes_gin#{' '}
128
- ON whodunit_chronicles_audits USING GIN (changes);
129
- SQL
130
-
131
- @audit_connection.exec(create_sql)
132
- rescue PG::Error => e
133
- # Ignore "already exists" errors from CONCURRENTLY
134
- raise unless e.message.include?('already exists')
135
- end
136
-
137
- def build_audit_record(change_event)
138
- user_info = extract_user_info(change_event)
139
-
140
- {
141
- id: nil, # Will be set by database
142
- table_name: change_event.table_name,
143
- schema_name: change_event.schema_name,
144
- record_id: change_event.primary_key,
145
- action: change_event.action,
146
- old_data: change_event.old_data,
147
- new_data: change_event.new_data,
148
- changes: change_event.changes,
149
- user_id: user_info[:user_id],
150
- user_type: user_info[:user_type],
151
- transaction_id: change_event.transaction_id,
152
- sequence_number: change_event.sequence_number,
153
- occurred_at: change_event.timestamp,
154
- created_at: Time.now,
155
- metadata: build_metadata(change_event),
156
- }
157
- end
158
-
159
- def extract_user_info(change_event)
160
- data = change_event.current_data || {}
161
-
162
- # Look for Whodunit user attribution fields
163
- user_id = data['creator_id'] || data['updater_id'] || data['deleter_id']
164
-
165
- {
166
- user_id: user_id,
167
- user_type: user_id ? 'User' : nil,
168
- }
169
- end
170
-
171
- def build_metadata(change_event)
172
- {
173
- table_schema: change_event.schema_name,
174
- qualified_table_name: change_event.qualified_table_name,
175
- changed_columns: change_event.changed_columns,
176
- adapter_metadata: change_event.metadata,
177
- chronicles_version: Chronicles::VERSION,
178
- }
179
- end
180
-
181
- def persist_audit_record(audit_record)
182
- sql = <<~SQL
183
- INSERT INTO whodunit_chronicles_audits (
184
- table_name, schema_name, record_id, action, old_data, new_data, changes,
185
- user_id, user_type, transaction_id, sequence_number, occurred_at, created_at, metadata
186
- ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)
187
- RETURNING id
188
- SQL
189
-
190
- params = [
191
- audit_record[:table_name],
192
- audit_record[:schema_name],
193
- audit_record[:record_id].to_json,
194
- audit_record[:action],
195
- audit_record[:old_data]&.to_json,
196
- audit_record[:new_data]&.to_json,
197
- audit_record[:changes].to_json,
198
- audit_record[:user_id],
199
- audit_record[:user_type],
200
- audit_record[:transaction_id],
201
- audit_record[:sequence_number],
202
- audit_record[:occurred_at],
203
- audit_record[:created_at],
204
- audit_record[:metadata].to_json,
205
- ]
206
-
207
- result = @audit_connection.exec_params(sql, params)
208
- audit_record[:id] = result.first['id'].to_i
209
- result.clear
210
-
211
- audit_record
212
- end
213
-
214
- def persist_audit_records_batch(audit_records)
215
- return audit_records if audit_records.empty?
216
-
217
- # Use multi-row INSERT for better performance
218
- values_clauses = []
219
- all_params = []
220
- param_index = 1
221
-
222
- audit_records.each do |record|
223
- param_positions = (param_index..(param_index + 13)).map { |i| "$#{i}" }.join(', ')
224
- values_clauses << "(#{param_positions})"
225
-
226
- all_params.push(
227
- record[:table_name],
228
- record[:schema_name],
229
- record[:record_id].to_json,
230
- record[:action],
231
- record[:old_data]&.to_json,
232
- record[:new_data]&.to_json,
233
- record[:changes].to_json,
234
- record[:user_id],
235
- record[:user_type],
236
- record[:transaction_id],
237
- record[:sequence_number],
238
- record[:occurred_at],
239
- record[:created_at],
240
- record[:metadata].to_json,
241
- )
242
-
243
- param_index += 14
244
- end
245
-
246
- sql = <<~SQL
247
- INSERT INTO whodunit_chronicles_audits (
248
- table_name, schema_name, record_id, action, old_data, new_data, changes,
249
- user_id, user_type, transaction_id, sequence_number, occurred_at, created_at, metadata
250
- ) VALUES #{values_clauses.join(', ')}
251
- RETURNING id
252
- SQL
253
-
254
- result = @audit_connection.exec_params(sql, all_params)
255
-
256
- # Set IDs on the audit records
257
- result.each_with_index do |row, index|
258
- audit_records[index][:id] = row['id'].to_i
259
- end
260
-
261
- result.clear
262
- audit_records
263
- end
264
-
265
- def log(level, message, context = {})
266
- logger.public_send(level, message, processor: 'AuditProcessor', **context)
267
- end
268
- end
269
- end
270
- end