@geekmidas/audit 0.1.0 → 0.2.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.
- package/dist/{Auditor-DrR0ImvJ.d.cts → Auditor-_Dn2dp8d.d.mts} +4 -1
- package/dist/Auditor-_Dn2dp8d.d.mts.map +1 -0
- package/dist/{Auditor-Xf4Tp63p.d.mts → Auditor-sW7YAJIA.d.cts} +4 -1
- package/dist/Auditor-sW7YAJIA.d.cts.map +1 -0
- package/dist/Auditor.d.cts +1 -1
- package/dist/Auditor.d.mts +1 -1
- package/dist/DefaultAuditor-BAVnNmRh.mjs.map +1 -1
- package/dist/{DefaultAuditor-3Q1zVd1b.d.mts → DefaultAuditor-BSYMwojG.d.cts} +3 -2
- package/dist/DefaultAuditor-BSYMwojG.d.cts.map +1 -0
- package/dist/{DefaultAuditor-BaZa4u_m.d.cts → DefaultAuditor-CYE-uAry.d.mts} +3 -2
- package/dist/DefaultAuditor-CYE-uAry.d.mts.map +1 -0
- package/dist/DefaultAuditor-JbZ_BQfh.cjs.map +1 -1
- package/dist/DefaultAuditor.d.cts +2 -2
- package/dist/DefaultAuditor.d.mts +2 -2
- package/dist/{cache-0AjJ0zis.d.cts → cache-2VI80Vao.d.mts} +3 -2
- package/dist/cache-2VI80Vao.d.mts.map +1 -0
- package/dist/cache-BbIl31RL.mjs.map +1 -1
- package/dist/{cache-DBbGcCEq.d.mts → cache-CDHqXqcl.d.cts} +3 -2
- package/dist/cache-CDHqXqcl.d.cts.map +1 -0
- package/dist/cache-l7jAptBl.cjs.map +1 -1
- package/dist/cache.d.cts +2 -2
- package/dist/cache.d.mts +2 -2
- package/dist/index.d.cts +2 -2
- package/dist/index.d.mts +2 -2
- package/dist/kysely.cjs.map +1 -1
- package/dist/kysely.d.cts +2 -1
- package/dist/kysely.d.cts.map +1 -0
- package/dist/kysely.d.mts +2 -1
- package/dist/kysely.d.mts.map +1 -0
- package/dist/kysely.mjs.map +1 -1
- package/dist/memory.cjs.map +1 -1
- package/dist/memory.d.cts +3 -2
- package/dist/memory.d.cts.map +1 -0
- package/dist/memory.d.mts +3 -2
- package/dist/memory.d.mts.map +1 -0
- package/dist/memory.mjs.map +1 -1
- package/dist/storage.d.cts +1 -1
- package/dist/storage.d.mts +1 -1
- package/dist/types.d.cts +1 -1
- package/dist/types.d.mts +1 -1
- package/package.json +3 -3
- package/src/Auditor.ts +121 -121
- package/src/DefaultAuditor.ts +91 -91
- package/src/__tests__/CacheAuditStorage.spec.ts +375 -375
- package/src/__tests__/DefaultAuditor.spec.ts +520 -462
- package/src/__tests__/InMemoryAuditStorage.spec.ts +311 -311
- package/src/__tests__/KyselyAuditStorage.integration.spec.ts +496 -496
- package/src/__tests__/KyselyAuditStorage.spec.ts +433 -433
- package/src/cache.ts +179 -179
- package/src/index.ts +14 -15
- package/src/kysely.ts +292 -292
- package/src/memory.ts +11 -11
- package/src/storage.ts +131 -131
- package/src/types.ts +73 -74
- package/tsconfig.json +9 -0
package/src/kysely.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
ControlledTransaction,
|
|
3
|
+
IsolationLevel,
|
|
4
|
+
Kysely,
|
|
5
|
+
Transaction,
|
|
6
6
|
} from 'kysely';
|
|
7
7
|
import { nanoid } from 'nanoid';
|
|
8
8
|
import type { AuditQueryOptions, AuditStorage } from './storage';
|
|
@@ -30,20 +30,20 @@ import type { AuditRecord } from './types';
|
|
|
30
30
|
* ```
|
|
31
31
|
*/
|
|
32
32
|
export interface TransactionAwareAuditor<TTransaction = unknown> {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
33
|
+
/** Register the transaction with the auditor for use during flush */
|
|
34
|
+
setTransaction(trx: TTransaction): void;
|
|
35
|
+
/** Flush all pending audits, optionally within a transaction */
|
|
36
|
+
flush(trx?: TTransaction): Promise<void>;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
export interface TransactionSettings {
|
|
40
|
-
|
|
40
|
+
isolationLevel?: IsolationLevel;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
export type DatabaseConnection<T> =
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
44
|
+
| ControlledTransaction<T>
|
|
45
|
+
| Kysely<T>
|
|
46
|
+
| Transaction<T>;
|
|
47
47
|
|
|
48
48
|
/**
|
|
49
49
|
* Execute a callback within a database transaction with automatic audit handling.
|
|
@@ -89,37 +89,37 @@ export type DatabaseConnection<T> =
|
|
|
89
89
|
* ```
|
|
90
90
|
*/
|
|
91
91
|
export async function withAuditableTransaction<DB, T>(
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
92
|
+
db: DatabaseConnection<DB>,
|
|
93
|
+
auditor: TransactionAwareAuditor<Transaction<DB>>,
|
|
94
|
+
cb: (trx: Transaction<DB>) => Promise<T>,
|
|
95
|
+
settings?: TransactionSettings,
|
|
96
96
|
): Promise<T> {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
97
|
+
const execute = async (trx: Transaction<DB>): Promise<T> => {
|
|
98
|
+
// Register transaction with auditor
|
|
99
|
+
auditor.setTransaction(trx);
|
|
100
100
|
|
|
101
|
-
|
|
102
|
-
|
|
101
|
+
// Execute the callback
|
|
102
|
+
const result = await cb(trx);
|
|
103
103
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
104
|
+
// Flush audits BEFORE transaction commits
|
|
105
|
+
// If this fails, the transaction will roll back
|
|
106
|
+
await auditor.flush(trx);
|
|
107
107
|
|
|
108
|
-
|
|
109
|
-
|
|
108
|
+
return result;
|
|
109
|
+
};
|
|
110
110
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
111
|
+
// If already in a transaction, just run with it
|
|
112
|
+
if (db.isTransaction) {
|
|
113
|
+
return execute(db as Transaction<DB>);
|
|
114
|
+
}
|
|
115
115
|
|
|
116
|
-
|
|
116
|
+
const builder = db.transaction();
|
|
117
117
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
118
|
+
if (settings?.isolationLevel) {
|
|
119
|
+
return builder.setIsolationLevel(settings.isolationLevel).execute(execute);
|
|
120
|
+
}
|
|
121
121
|
|
|
122
|
-
|
|
122
|
+
return builder.execute(execute);
|
|
123
123
|
}
|
|
124
124
|
|
|
125
125
|
/**
|
|
@@ -137,19 +137,19 @@ export async function withAuditableTransaction<DB, T>(
|
|
|
137
137
|
* ```
|
|
138
138
|
*/
|
|
139
139
|
export interface AuditLogTable {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
140
|
+
id: string;
|
|
141
|
+
type: string;
|
|
142
|
+
operation: string;
|
|
143
|
+
table: string | null;
|
|
144
|
+
entityId: string | null;
|
|
145
|
+
oldValues: unknown | null;
|
|
146
|
+
newValues: unknown | null;
|
|
147
|
+
payload: unknown | null;
|
|
148
|
+
timestamp: Date;
|
|
149
|
+
actorId: string | null;
|
|
150
|
+
actorType: string | null;
|
|
151
|
+
actorData: unknown | null;
|
|
152
|
+
metadata: unknown | null;
|
|
153
153
|
}
|
|
154
154
|
|
|
155
155
|
/**
|
|
@@ -167,30 +167,30 @@ export interface AuditLogTable {
|
|
|
167
167
|
* ```
|
|
168
168
|
*/
|
|
169
169
|
export type InsertableAuditLogTable = Omit<AuditLogTable, 'id'> & {
|
|
170
|
-
|
|
170
|
+
id?: string;
|
|
171
171
|
};
|
|
172
172
|
|
|
173
173
|
/**
|
|
174
174
|
* Configuration for KyselyAuditStorage.
|
|
175
175
|
*/
|
|
176
176
|
export interface KyselyAuditStorageConfig<DB> {
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
177
|
+
/** Kysely database instance */
|
|
178
|
+
db: Kysely<DB>;
|
|
179
|
+
/** Table name for audit logs (must be a key in DB that extends AuditLogTable) */
|
|
180
|
+
tableName: keyof DB & string;
|
|
181
|
+
/**
|
|
182
|
+
* Service name of the database service.
|
|
183
|
+
* When set, endpoint adaptors will automatically use the audit transaction as `db`
|
|
184
|
+
* in the handler context if the endpoint's database service has the same name.
|
|
185
|
+
*/
|
|
186
|
+
databaseServiceName?: string;
|
|
187
|
+
/**
|
|
188
|
+
* Let the database auto-generate IDs (e.g., via DEFAULT gen_random_uuid()).
|
|
189
|
+
* When true, the ID field is omitted from inserts if not provided.
|
|
190
|
+
* When false (default), IDs are generated using nanoid if not provided.
|
|
191
|
+
* @default false
|
|
192
|
+
*/
|
|
193
|
+
autoId?: boolean;
|
|
194
194
|
}
|
|
195
195
|
|
|
196
196
|
/**
|
|
@@ -217,230 +217,230 @@ export interface KyselyAuditStorageConfig<DB> {
|
|
|
217
217
|
* ```
|
|
218
218
|
*/
|
|
219
219
|
export class KyselyAuditStorage<DB> implements AuditStorage {
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
220
|
+
private readonly db: Kysely<DB>;
|
|
221
|
+
private readonly tableName: keyof DB & string;
|
|
222
|
+
private readonly autoId: boolean;
|
|
223
|
+
readonly databaseServiceName?: string;
|
|
224
|
+
|
|
225
|
+
constructor(config: KyselyAuditStorageConfig<DB>) {
|
|
226
|
+
this.db = config.db;
|
|
227
|
+
this.tableName = config.tableName;
|
|
228
|
+
this.databaseServiceName = config.databaseServiceName;
|
|
229
|
+
this.autoId = config.autoId ?? false;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async write(records: AuditRecord[], trx?: unknown): Promise<void> {
|
|
233
|
+
if (records.length === 0) {
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const db = (trx as Transaction<DB>) ?? this.db;
|
|
238
|
+
const rows = records.map((record) => this.toRow(record));
|
|
239
|
+
|
|
240
|
+
await (db as any).insertInto(this.tableName).values(rows).execute();
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
async query(options: AuditQueryOptions): Promise<AuditRecord[]> {
|
|
244
|
+
let query = (this.db as any).selectFrom(this.tableName).selectAll();
|
|
245
|
+
|
|
246
|
+
query = this.applyFilters(query, options);
|
|
247
|
+
|
|
248
|
+
// Ordering
|
|
249
|
+
const orderBy = options.orderBy ?? 'timestamp';
|
|
250
|
+
const orderDirection = options.orderDirection ?? 'desc';
|
|
251
|
+
query = query.orderBy(
|
|
252
|
+
orderBy === 'timestamp' ? 'timestamp' : 'type',
|
|
253
|
+
orderDirection,
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
// Pagination
|
|
257
|
+
if (options.limit !== undefined) {
|
|
258
|
+
query = query.limit(options.limit);
|
|
259
|
+
}
|
|
260
|
+
if (options.offset !== undefined) {
|
|
261
|
+
query = query.offset(options.offset);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const rows = await query.execute();
|
|
265
|
+
return rows.map((row: AuditLogTable) => this.fromRow(row));
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
async count(
|
|
269
|
+
options: Omit<AuditQueryOptions, 'limit' | 'offset'>,
|
|
270
|
+
): Promise<number> {
|
|
271
|
+
let query = (this.db as any)
|
|
272
|
+
.selectFrom(this.tableName)
|
|
273
|
+
.select((eb: any) => eb.fn.count('id').as('count'));
|
|
274
|
+
|
|
275
|
+
query = this.applyFilters(query, options);
|
|
276
|
+
|
|
277
|
+
const result = await query.executeTakeFirst();
|
|
278
|
+
return Number(result?.count ?? 0);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Get the Kysely database instance for transactional operations.
|
|
283
|
+
* Used by endpoint adaptors to automatically wrap handlers in transactions.
|
|
284
|
+
*/
|
|
285
|
+
getDatabase(): Kysely<DB> {
|
|
286
|
+
return this.db;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Execute a callback within a Kysely transaction with automatic audit handling.
|
|
291
|
+
* The auditor is registered with the transaction and audits are flushed
|
|
292
|
+
* before the transaction commits.
|
|
293
|
+
*
|
|
294
|
+
* If the provided db connection is already a transaction, it will be reused
|
|
295
|
+
* instead of creating a nested transaction.
|
|
296
|
+
*/
|
|
297
|
+
async withTransaction<T>(
|
|
298
|
+
auditor: TransactionAwareAuditor<Transaction<DB>>,
|
|
299
|
+
callback: () => Promise<T>,
|
|
300
|
+
db?: DatabaseConnection<DB>,
|
|
301
|
+
): Promise<T> {
|
|
302
|
+
const connection = db ?? this.db;
|
|
303
|
+
|
|
304
|
+
// If already in a transaction, reuse it
|
|
305
|
+
if (connection.isTransaction) {
|
|
306
|
+
const trx = connection as Transaction<DB>;
|
|
307
|
+
auditor.setTransaction(trx);
|
|
308
|
+
const result = await callback();
|
|
309
|
+
await auditor.flush(trx);
|
|
310
|
+
return result;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Create new transaction
|
|
314
|
+
return connection.transaction().execute(async (trx) => {
|
|
315
|
+
auditor.setTransaction(trx);
|
|
316
|
+
const result = await callback();
|
|
317
|
+
await auditor.flush(trx);
|
|
318
|
+
return result;
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
private applyFilters(query: any, options: AuditQueryOptions): any {
|
|
323
|
+
// Type filter
|
|
324
|
+
if (options.type !== undefined) {
|
|
325
|
+
if (Array.isArray(options.type)) {
|
|
326
|
+
query = query.where('type', 'in', options.type);
|
|
327
|
+
} else {
|
|
328
|
+
query = query.where('type', '=', options.type);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Entity ID filter
|
|
333
|
+
if (options.entityId !== undefined) {
|
|
334
|
+
const entityId =
|
|
335
|
+
typeof options.entityId === 'string'
|
|
336
|
+
? options.entityId
|
|
337
|
+
: JSON.stringify(options.entityId);
|
|
338
|
+
query = query.where('entityId', '=', entityId);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Table filter
|
|
342
|
+
if (options.table !== undefined) {
|
|
343
|
+
query = query.where('table', '=', options.table);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Actor ID filter
|
|
347
|
+
if (options.actorId !== undefined) {
|
|
348
|
+
query = query.where('actorId', '=', options.actorId);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Date range filters
|
|
352
|
+
if (options.from !== undefined) {
|
|
353
|
+
query = query.where('timestamp', '>=', options.from);
|
|
354
|
+
}
|
|
355
|
+
if (options.to !== undefined) {
|
|
356
|
+
query = query.where('timestamp', '<=', options.to);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
return query;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
private toRow(record: AuditRecord): AuditLogTable {
|
|
363
|
+
// If autoId is true, let database generate ID (ignore record.id)
|
|
364
|
+
// If autoId is false (default), use record.id or generate with nanoid
|
|
365
|
+
const id = this.autoId ? undefined : record.id || nanoid();
|
|
366
|
+
|
|
367
|
+
return {
|
|
368
|
+
...(id && { id }),
|
|
369
|
+
type: record.type,
|
|
370
|
+
operation: record.operation,
|
|
371
|
+
table: record.table ?? null,
|
|
372
|
+
entityId:
|
|
373
|
+
record.entityId === undefined
|
|
374
|
+
? null
|
|
375
|
+
: typeof record.entityId === 'string'
|
|
376
|
+
? record.entityId
|
|
377
|
+
: JSON.stringify(record.entityId),
|
|
378
|
+
oldValues: record.oldValues ?? null,
|
|
379
|
+
newValues: record.newValues ?? null,
|
|
380
|
+
payload: record.payload ?? null,
|
|
381
|
+
timestamp: record.timestamp,
|
|
382
|
+
actorId: record.actor?.id ?? null,
|
|
383
|
+
actorType: record.actor?.type ?? null,
|
|
384
|
+
actorData:
|
|
385
|
+
record.actor !== undefined ? this.getActorData(record.actor) : null,
|
|
386
|
+
metadata: record.metadata ?? null,
|
|
387
|
+
} as AuditLogTable;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
private fromRow(row: AuditLogTable): AuditRecord {
|
|
391
|
+
const actor =
|
|
392
|
+
row.actorId !== null || row.actorType !== null
|
|
393
|
+
? {
|
|
394
|
+
id: row.actorId ?? undefined,
|
|
395
|
+
type: row.actorType ?? undefined,
|
|
396
|
+
...(row.actorData ? this.parseJson(row.actorData) : {}),
|
|
397
|
+
}
|
|
398
|
+
: undefined;
|
|
399
|
+
|
|
400
|
+
return {
|
|
401
|
+
id: row.id,
|
|
402
|
+
type: row.type,
|
|
403
|
+
operation: row.operation as AuditRecord['operation'],
|
|
404
|
+
table: row.table ?? undefined,
|
|
405
|
+
entityId: row.entityId ? this.parseEntityId(row.entityId) : undefined,
|
|
406
|
+
oldValues: row.oldValues ? this.parseJson(row.oldValues) : undefined,
|
|
407
|
+
newValues: row.newValues ? this.parseJson(row.newValues) : undefined,
|
|
408
|
+
payload: row.payload ? this.parseJson(row.payload) : undefined,
|
|
409
|
+
timestamp: row.timestamp,
|
|
410
|
+
actor,
|
|
411
|
+
metadata: row.metadata ? this.parseJson(row.metadata) : undefined,
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Parse a JSON value that may already be parsed (e.g., from jsonb columns).
|
|
417
|
+
*/
|
|
418
|
+
private parseJson(value: unknown): Record<string, unknown> {
|
|
419
|
+
if (typeof value === 'object' && value !== null) {
|
|
420
|
+
return value as Record<string, unknown>;
|
|
421
|
+
}
|
|
422
|
+
if (typeof value === 'string') {
|
|
423
|
+
return JSON.parse(value);
|
|
424
|
+
}
|
|
425
|
+
return {};
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
private getActorData(
|
|
429
|
+
actor: NonNullable<AuditRecord['actor']>,
|
|
430
|
+
): Record<string, unknown> {
|
|
431
|
+
const { id, type, ...rest } = actor;
|
|
432
|
+
return rest;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
private parseEntityId(entityId: string): string | Record<string, unknown> {
|
|
436
|
+
try {
|
|
437
|
+
const parsed = JSON.parse(entityId);
|
|
438
|
+
if (typeof parsed === 'object' && parsed !== null) {
|
|
439
|
+
return parsed;
|
|
440
|
+
}
|
|
441
|
+
return entityId;
|
|
442
|
+
} catch {
|
|
443
|
+
return entityId;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
446
|
}
|
package/src/memory.ts
CHANGED
|
@@ -35,16 +35,16 @@ import type { AuditableAction } from './types';
|
|
|
35
35
|
* ```
|
|
36
36
|
*/
|
|
37
37
|
export class InMemoryAuditStorage<
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
38
|
+
TAuditAction extends AuditableAction<string, unknown> = AuditableAction<
|
|
39
|
+
string,
|
|
40
|
+
unknown
|
|
41
|
+
>,
|
|
42
42
|
> extends CacheAuditStorage<TAuditAction> {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
43
|
+
constructor() {
|
|
44
|
+
super({
|
|
45
|
+
cache: new InMemoryCache(),
|
|
46
|
+
// Use a long TTL since in-memory cache expires items
|
|
47
|
+
ttl: 86400 * 365, // 1 year
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
50
|
}
|