@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.
Files changed (55) hide show
  1. package/dist/{Auditor-DrR0ImvJ.d.cts → Auditor-_Dn2dp8d.d.mts} +4 -1
  2. package/dist/Auditor-_Dn2dp8d.d.mts.map +1 -0
  3. package/dist/{Auditor-Xf4Tp63p.d.mts → Auditor-sW7YAJIA.d.cts} +4 -1
  4. package/dist/Auditor-sW7YAJIA.d.cts.map +1 -0
  5. package/dist/Auditor.d.cts +1 -1
  6. package/dist/Auditor.d.mts +1 -1
  7. package/dist/DefaultAuditor-BAVnNmRh.mjs.map +1 -1
  8. package/dist/{DefaultAuditor-3Q1zVd1b.d.mts → DefaultAuditor-BSYMwojG.d.cts} +3 -2
  9. package/dist/DefaultAuditor-BSYMwojG.d.cts.map +1 -0
  10. package/dist/{DefaultAuditor-BaZa4u_m.d.cts → DefaultAuditor-CYE-uAry.d.mts} +3 -2
  11. package/dist/DefaultAuditor-CYE-uAry.d.mts.map +1 -0
  12. package/dist/DefaultAuditor-JbZ_BQfh.cjs.map +1 -1
  13. package/dist/DefaultAuditor.d.cts +2 -2
  14. package/dist/DefaultAuditor.d.mts +2 -2
  15. package/dist/{cache-0AjJ0zis.d.cts → cache-2VI80Vao.d.mts} +3 -2
  16. package/dist/cache-2VI80Vao.d.mts.map +1 -0
  17. package/dist/cache-BbIl31RL.mjs.map +1 -1
  18. package/dist/{cache-DBbGcCEq.d.mts → cache-CDHqXqcl.d.cts} +3 -2
  19. package/dist/cache-CDHqXqcl.d.cts.map +1 -0
  20. package/dist/cache-l7jAptBl.cjs.map +1 -1
  21. package/dist/cache.d.cts +2 -2
  22. package/dist/cache.d.mts +2 -2
  23. package/dist/index.d.cts +2 -2
  24. package/dist/index.d.mts +2 -2
  25. package/dist/kysely.cjs.map +1 -1
  26. package/dist/kysely.d.cts +2 -1
  27. package/dist/kysely.d.cts.map +1 -0
  28. package/dist/kysely.d.mts +2 -1
  29. package/dist/kysely.d.mts.map +1 -0
  30. package/dist/kysely.mjs.map +1 -1
  31. package/dist/memory.cjs.map +1 -1
  32. package/dist/memory.d.cts +3 -2
  33. package/dist/memory.d.cts.map +1 -0
  34. package/dist/memory.d.mts +3 -2
  35. package/dist/memory.d.mts.map +1 -0
  36. package/dist/memory.mjs.map +1 -1
  37. package/dist/storage.d.cts +1 -1
  38. package/dist/storage.d.mts +1 -1
  39. package/dist/types.d.cts +1 -1
  40. package/dist/types.d.mts +1 -1
  41. package/package.json +3 -3
  42. package/src/Auditor.ts +121 -121
  43. package/src/DefaultAuditor.ts +91 -91
  44. package/src/__tests__/CacheAuditStorage.spec.ts +375 -375
  45. package/src/__tests__/DefaultAuditor.spec.ts +520 -462
  46. package/src/__tests__/InMemoryAuditStorage.spec.ts +311 -311
  47. package/src/__tests__/KyselyAuditStorage.integration.spec.ts +496 -496
  48. package/src/__tests__/KyselyAuditStorage.spec.ts +433 -433
  49. package/src/cache.ts +179 -179
  50. package/src/index.ts +14 -15
  51. package/src/kysely.ts +292 -292
  52. package/src/memory.ts +11 -11
  53. package/src/storage.ts +131 -131
  54. package/src/types.ts +73 -74
  55. package/tsconfig.json +9 -0
@@ -4,440 +4,440 @@ import type { AuditRecord } from '../types';
4
4
 
5
5
  // Mock Kysely database
6
6
  function createMockDb() {
7
- const rows: AuditLogTable[] = [];
8
-
9
- const insertBuilder = {
10
- values: vi.fn().mockReturnThis(),
11
- execute: vi.fn(async () => {
12
- return [];
13
- }),
14
- };
15
-
16
- const selectBuilder = {
17
- selectAll: vi.fn().mockReturnThis(),
18
- select: vi.fn().mockReturnThis(),
19
- where: vi.fn().mockReturnThis(),
20
- orderBy: vi.fn().mockReturnThis(),
21
- limit: vi.fn().mockReturnThis(),
22
- offset: vi.fn().mockReturnThis(),
23
- execute: vi.fn(async () => rows),
24
- executeTakeFirst: vi.fn(async () => ({ count: rows.length })),
25
- };
26
-
27
- const db = {
28
- insertInto: vi.fn(() => insertBuilder),
29
- selectFrom: vi.fn(() => selectBuilder),
30
- };
31
-
32
- return {
33
- db,
34
- insertBuilder,
35
- selectBuilder,
36
- rows,
37
- addRow: (row: AuditLogTable) => rows.push(row),
38
- };
7
+ const rows: AuditLogTable[] = [];
8
+
9
+ const insertBuilder = {
10
+ values: vi.fn().mockReturnThis(),
11
+ execute: vi.fn(async () => {
12
+ return [];
13
+ }),
14
+ };
15
+
16
+ const selectBuilder = {
17
+ selectAll: vi.fn().mockReturnThis(),
18
+ select: vi.fn().mockReturnThis(),
19
+ where: vi.fn().mockReturnThis(),
20
+ orderBy: vi.fn().mockReturnThis(),
21
+ limit: vi.fn().mockReturnThis(),
22
+ offset: vi.fn().mockReturnThis(),
23
+ execute: vi.fn(async () => rows),
24
+ executeTakeFirst: vi.fn(async () => ({ count: rows.length })),
25
+ };
26
+
27
+ const db = {
28
+ insertInto: vi.fn(() => insertBuilder),
29
+ selectFrom: vi.fn(() => selectBuilder),
30
+ };
31
+
32
+ return {
33
+ db,
34
+ insertBuilder,
35
+ selectBuilder,
36
+ rows,
37
+ addRow: (row: AuditLogTable) => rows.push(row),
38
+ };
39
39
  }
40
40
 
41
41
  describe('KyselyAuditStorage', () => {
42
- let mockDb: ReturnType<typeof createMockDb>;
43
- let storage: KyselyAuditStorage<{ audit_logs: AuditLogTable }>;
44
-
45
- beforeEach(() => {
46
- mockDb = createMockDb();
47
- storage = new KyselyAuditStorage({
48
- db: mockDb.db as any,
49
- tableName: 'audit_logs',
50
- });
51
- });
52
-
53
- describe('write', () => {
54
- it('should write records to database', async () => {
55
- const records: AuditRecord[] = [
56
- {
57
- id: 'audit-1',
58
- type: 'user.created',
59
- operation: 'INSERT',
60
- table: 'users',
61
- entityId: 'user-123',
62
- payload: { email: 'test@example.com' },
63
- timestamp: new Date('2024-01-01T00:00:00Z'),
64
- actor: { id: 'admin-1', type: 'admin' },
65
- metadata: { requestId: 'req-123' },
66
- },
67
- ];
68
-
69
- await storage.write(records);
70
-
71
- expect(mockDb.db.insertInto).toHaveBeenCalledWith('audit_logs');
72
- expect(mockDb.insertBuilder.values).toHaveBeenCalledWith([
73
- expect.objectContaining({
74
- id: 'audit-1',
75
- type: 'user.created',
76
- operation: 'INSERT',
77
- table: 'users',
78
- entityId: 'user-123',
79
- payload: { email: 'test@example.com' },
80
- actorId: 'admin-1',
81
- actorType: 'admin',
82
- metadata: { requestId: 'req-123' },
83
- }),
84
- ]);
85
- });
86
-
87
- it('should use transaction when provided', async () => {
88
- const mockTrx = {
89
- insertInto: vi.fn(() => ({
90
- values: vi.fn().mockReturnThis(),
91
- execute: vi.fn(),
92
- })),
93
- };
94
-
95
- const records: AuditRecord[] = [
96
- {
97
- id: 'audit-1',
98
- type: 'user.created',
99
- operation: 'CUSTOM',
100
- timestamp: new Date(),
101
- },
102
- ];
103
-
104
- await storage.write(records, mockTrx);
105
-
106
- expect(mockTrx.insertInto).toHaveBeenCalledWith('audit_logs');
107
- expect(mockDb.db.insertInto).not.toHaveBeenCalled();
108
- });
109
-
110
- it('should do nothing for empty records', async () => {
111
- await storage.write([]);
112
-
113
- expect(mockDb.db.insertInto).not.toHaveBeenCalled();
114
- });
115
-
116
- it('should handle complex entity IDs', async () => {
117
- const records: AuditRecord[] = [
118
- {
119
- id: 'audit-1',
120
- type: 'relation.created',
121
- operation: 'INSERT',
122
- entityId: { userId: 'u1', roleId: 'r1' },
123
- timestamp: new Date(),
124
- },
125
- ];
126
-
127
- await storage.write(records);
128
-
129
- expect(mockDb.insertBuilder.values).toHaveBeenCalledWith([
130
- expect.objectContaining({
131
- entityId: JSON.stringify({ userId: 'u1', roleId: 'r1' }),
132
- }),
133
- ]);
134
- });
135
-
136
- it('should store extra actor properties', async () => {
137
- const records: AuditRecord[] = [
138
- {
139
- id: 'audit-1',
140
- type: 'user.created',
141
- operation: 'CUSTOM',
142
- timestamp: new Date(),
143
- actor: {
144
- id: 'user-1',
145
- type: 'user',
146
- email: 'user@example.com',
147
- role: 'admin',
148
- },
149
- },
150
- ];
151
-
152
- await storage.write(records);
153
-
154
- expect(mockDb.insertBuilder.values).toHaveBeenCalledWith([
155
- expect.objectContaining({
156
- actorId: 'user-1',
157
- actorType: 'user',
158
- actorData: {
159
- email: 'user@example.com',
160
- role: 'admin',
161
- },
162
- }),
163
- ]);
164
- });
165
-
166
- it('should generate ID with nanoid when autoId is false (default)', async () => {
167
- const records: AuditRecord[] = [
168
- {
169
- id: '', // Empty ID
170
- type: 'user.created',
171
- operation: 'CUSTOM',
172
- timestamp: new Date(),
173
- },
174
- ];
175
-
176
- await storage.write(records);
177
-
178
- const calledValues = mockDb.insertBuilder.values.mock.calls[0][0];
179
- expect(calledValues[0].id).toBeDefined();
180
- expect(calledValues[0].id.length).toBeGreaterThan(0);
181
- });
182
-
183
- it('should use provided ID when autoId is false (default)', async () => {
184
- const records: AuditRecord[] = [
185
- {
186
- id: 'my-custom-id',
187
- type: 'user.created',
188
- operation: 'CUSTOM',
189
- timestamp: new Date(),
190
- },
191
- ];
192
-
193
- await storage.write(records);
194
-
195
- expect(mockDb.insertBuilder.values).toHaveBeenCalledWith([
196
- expect.objectContaining({
197
- id: 'my-custom-id',
198
- }),
199
- ]);
200
- });
201
-
202
- it('should omit ID when autoId is true and no ID provided', async () => {
203
- const storageAutoId = new KyselyAuditStorage({
204
- db: mockDb.db as any,
205
- tableName: 'audit_logs',
206
- autoId: true,
207
- });
208
-
209
- const records: AuditRecord[] = [
210
- {
211
- id: '', // Empty ID - let database generate
212
- type: 'user.created',
213
- operation: 'CUSTOM',
214
- timestamp: new Date(),
215
- },
216
- ];
217
-
218
- await storageAutoId.write(records);
219
-
220
- const calledValues = mockDb.insertBuilder.values.mock.calls[0][0];
221
- expect(calledValues[0].id).toBeUndefined();
222
- });
223
-
224
- it('should omit ID even if provided when autoId is true', async () => {
225
- const storageAutoId = new KyselyAuditStorage({
226
- db: mockDb.db as any,
227
- tableName: 'audit_logs',
228
- autoId: true,
229
- });
230
-
231
- const records: AuditRecord[] = [
232
- {
233
- id: 'explicit-id', // This should be ignored when autoId is true
234
- type: 'user.created',
235
- operation: 'CUSTOM',
236
- timestamp: new Date(),
237
- },
238
- ];
239
-
240
- await storageAutoId.write(records);
241
-
242
- // When autoId is true, ID should always be omitted (let database generate)
243
- const calledValues = mockDb.insertBuilder.values.mock.calls[0][0];
244
- expect(calledValues[0].id).toBeUndefined();
245
- });
246
- });
247
-
248
- describe('query', () => {
249
- it('should return records from database', async () => {
250
- mockDb.addRow({
251
- id: 'audit-1',
252
- type: 'user.created',
253
- operation: 'INSERT',
254
- table: 'users',
255
- entityId: 'user-123',
256
- oldValues: null,
257
- newValues: JSON.stringify({ name: 'Test' }),
258
- payload: JSON.stringify({ email: 'test@example.com' }),
259
- timestamp: new Date('2024-01-01T00:00:00Z'),
260
- actorId: 'admin-1',
261
- actorType: 'admin',
262
- actorData: null,
263
- metadata: JSON.stringify({ requestId: 'req-123' }),
264
- });
265
-
266
- const results = await storage.query({});
267
-
268
- expect(results).toHaveLength(1);
269
- expect(results[0]).toMatchObject({
270
- id: 'audit-1',
271
- type: 'user.created',
272
- operation: 'INSERT',
273
- table: 'users',
274
- entityId: 'user-123',
275
- newValues: { name: 'Test' },
276
- payload: { email: 'test@example.com' },
277
- actor: { id: 'admin-1', type: 'admin' },
278
- metadata: { requestId: 'req-123' },
279
- });
280
- });
281
-
282
- it('should apply type filter', async () => {
283
- await storage.query({ type: 'user.created' });
284
-
285
- expect(mockDb.selectBuilder.where).toHaveBeenCalledWith(
286
- 'type',
287
- '=',
288
- 'user.created',
289
- );
290
- });
291
-
292
- it('should apply type array filter', async () => {
293
- await storage.query({ type: ['user.created', 'user.updated'] });
294
-
295
- expect(mockDb.selectBuilder.where).toHaveBeenCalledWith('type', 'in', [
296
- 'user.created',
297
- 'user.updated',
298
- ]);
299
- });
300
-
301
- it('should apply entity ID filter', async () => {
302
- await storage.query({ entityId: 'user-123' });
303
-
304
- expect(mockDb.selectBuilder.where).toHaveBeenCalledWith(
305
- 'entityId',
306
- '=',
307
- 'user-123',
308
- );
309
- });
310
-
311
- it('should apply table filter', async () => {
312
- await storage.query({ table: 'users' });
313
-
314
- expect(mockDb.selectBuilder.where).toHaveBeenCalledWith(
315
- 'table',
316
- '=',
317
- 'users',
318
- );
319
- });
320
-
321
- it('should apply actor ID filter', async () => {
322
- await storage.query({ actorId: 'admin-1' });
323
-
324
- expect(mockDb.selectBuilder.where).toHaveBeenCalledWith(
325
- 'actorId',
326
- '=',
327
- 'admin-1',
328
- );
329
- });
330
-
331
- it('should apply date range filters', async () => {
332
- const from = new Date('2024-01-01');
333
- const to = new Date('2024-12-31');
334
-
335
- await storage.query({ from, to });
336
-
337
- expect(mockDb.selectBuilder.where).toHaveBeenCalledWith(
338
- 'timestamp',
339
- '>=',
340
- from,
341
- );
342
- expect(mockDb.selectBuilder.where).toHaveBeenCalledWith(
343
- 'timestamp',
344
- '<=',
345
- to,
346
- );
347
- });
348
-
349
- it('should apply pagination', async () => {
350
- await storage.query({ limit: 10, offset: 20 });
351
-
352
- expect(mockDb.selectBuilder.limit).toHaveBeenCalledWith(10);
353
- expect(mockDb.selectBuilder.offset).toHaveBeenCalledWith(20);
354
- });
355
-
356
- it('should apply ordering', async () => {
357
- await storage.query({ orderBy: 'type', orderDirection: 'asc' });
358
-
359
- expect(mockDb.selectBuilder.orderBy).toHaveBeenCalledWith('type', 'asc');
360
- });
361
-
362
- it('should default to timestamp desc ordering', async () => {
363
- await storage.query({});
364
-
365
- expect(mockDb.selectBuilder.orderBy).toHaveBeenCalledWith(
366
- 'timestamp',
367
- 'desc',
368
- );
369
- });
370
-
371
- it('should parse complex entity IDs', async () => {
372
- mockDb.addRow({
373
- id: 'audit-1',
374
- type: 'relation.created',
375
- operation: 'INSERT',
376
- table: null,
377
- entityId: JSON.stringify({ userId: 'u1', roleId: 'r1' }),
378
- oldValues: null,
379
- newValues: null,
380
- payload: null,
381
- timestamp: new Date(),
382
- actorId: null,
383
- actorType: null,
384
- actorData: null,
385
- metadata: null,
386
- });
387
-
388
- const results = await storage.query({});
389
-
390
- expect(results[0].entityId).toEqual({ userId: 'u1', roleId: 'r1' });
391
- });
392
-
393
- it('should merge actor data', async () => {
394
- mockDb.addRow({
395
- id: 'audit-1',
396
- type: 'user.created',
397
- operation: 'CUSTOM',
398
- table: null,
399
- entityId: null,
400
- oldValues: null,
401
- newValues: null,
402
- payload: null,
403
- timestamp: new Date(),
404
- actorId: 'user-1',
405
- actorType: 'user',
406
- actorData: JSON.stringify({ email: 'user@example.com' }),
407
- metadata: null,
408
- });
409
-
410
- const results = await storage.query({});
411
-
412
- expect(results[0].actor).toEqual({
413
- id: 'user-1',
414
- type: 'user',
415
- email: 'user@example.com',
416
- });
417
- });
418
- });
419
-
420
- describe('count', () => {
421
- it('should return count from database', async () => {
422
- const count = await storage.count({});
423
-
424
- expect(count).toBe(0);
425
- expect(mockDb.selectBuilder.select).toHaveBeenCalled();
426
- });
427
-
428
- it('should apply filters to count', async () => {
429
- await storage.count({ type: 'user.created', actorId: 'admin-1' });
430
-
431
- expect(mockDb.selectBuilder.where).toHaveBeenCalledWith(
432
- 'type',
433
- '=',
434
- 'user.created',
435
- );
436
- expect(mockDb.selectBuilder.where).toHaveBeenCalledWith(
437
- 'actorId',
438
- '=',
439
- 'admin-1',
440
- );
441
- });
442
- });
42
+ let mockDb: ReturnType<typeof createMockDb>;
43
+ let storage: KyselyAuditStorage<{ audit_logs: AuditLogTable }>;
44
+
45
+ beforeEach(() => {
46
+ mockDb = createMockDb();
47
+ storage = new KyselyAuditStorage({
48
+ db: mockDb.db as any,
49
+ tableName: 'audit_logs',
50
+ });
51
+ });
52
+
53
+ describe('write', () => {
54
+ it('should write records to database', async () => {
55
+ const records: AuditRecord[] = [
56
+ {
57
+ id: 'audit-1',
58
+ type: 'user.created',
59
+ operation: 'INSERT',
60
+ table: 'users',
61
+ entityId: 'user-123',
62
+ payload: { email: 'test@example.com' },
63
+ timestamp: new Date('2024-01-01T00:00:00Z'),
64
+ actor: { id: 'admin-1', type: 'admin' },
65
+ metadata: { requestId: 'req-123' },
66
+ },
67
+ ];
68
+
69
+ await storage.write(records);
70
+
71
+ expect(mockDb.db.insertInto).toHaveBeenCalledWith('audit_logs');
72
+ expect(mockDb.insertBuilder.values).toHaveBeenCalledWith([
73
+ expect.objectContaining({
74
+ id: 'audit-1',
75
+ type: 'user.created',
76
+ operation: 'INSERT',
77
+ table: 'users',
78
+ entityId: 'user-123',
79
+ payload: { email: 'test@example.com' },
80
+ actorId: 'admin-1',
81
+ actorType: 'admin',
82
+ metadata: { requestId: 'req-123' },
83
+ }),
84
+ ]);
85
+ });
86
+
87
+ it('should use transaction when provided', async () => {
88
+ const mockTrx = {
89
+ insertInto: vi.fn(() => ({
90
+ values: vi.fn().mockReturnThis(),
91
+ execute: vi.fn(),
92
+ })),
93
+ };
94
+
95
+ const records: AuditRecord[] = [
96
+ {
97
+ id: 'audit-1',
98
+ type: 'user.created',
99
+ operation: 'CUSTOM',
100
+ timestamp: new Date(),
101
+ },
102
+ ];
103
+
104
+ await storage.write(records, mockTrx);
105
+
106
+ expect(mockTrx.insertInto).toHaveBeenCalledWith('audit_logs');
107
+ expect(mockDb.db.insertInto).not.toHaveBeenCalled();
108
+ });
109
+
110
+ it('should do nothing for empty records', async () => {
111
+ await storage.write([]);
112
+
113
+ expect(mockDb.db.insertInto).not.toHaveBeenCalled();
114
+ });
115
+
116
+ it('should handle complex entity IDs', async () => {
117
+ const records: AuditRecord[] = [
118
+ {
119
+ id: 'audit-1',
120
+ type: 'relation.created',
121
+ operation: 'INSERT',
122
+ entityId: { userId: 'u1', roleId: 'r1' },
123
+ timestamp: new Date(),
124
+ },
125
+ ];
126
+
127
+ await storage.write(records);
128
+
129
+ expect(mockDb.insertBuilder.values).toHaveBeenCalledWith([
130
+ expect.objectContaining({
131
+ entityId: JSON.stringify({ userId: 'u1', roleId: 'r1' }),
132
+ }),
133
+ ]);
134
+ });
135
+
136
+ it('should store extra actor properties', async () => {
137
+ const records: AuditRecord[] = [
138
+ {
139
+ id: 'audit-1',
140
+ type: 'user.created',
141
+ operation: 'CUSTOM',
142
+ timestamp: new Date(),
143
+ actor: {
144
+ id: 'user-1',
145
+ type: 'user',
146
+ email: 'user@example.com',
147
+ role: 'admin',
148
+ },
149
+ },
150
+ ];
151
+
152
+ await storage.write(records);
153
+
154
+ expect(mockDb.insertBuilder.values).toHaveBeenCalledWith([
155
+ expect.objectContaining({
156
+ actorId: 'user-1',
157
+ actorType: 'user',
158
+ actorData: {
159
+ email: 'user@example.com',
160
+ role: 'admin',
161
+ },
162
+ }),
163
+ ]);
164
+ });
165
+
166
+ it('should generate ID with nanoid when autoId is false (default)', async () => {
167
+ const records: AuditRecord[] = [
168
+ {
169
+ id: '', // Empty ID
170
+ type: 'user.created',
171
+ operation: 'CUSTOM',
172
+ timestamp: new Date(),
173
+ },
174
+ ];
175
+
176
+ await storage.write(records);
177
+
178
+ const calledValues = mockDb.insertBuilder.values.mock.calls[0][0];
179
+ expect(calledValues[0].id).toBeDefined();
180
+ expect(calledValues[0].id.length).toBeGreaterThan(0);
181
+ });
182
+
183
+ it('should use provided ID when autoId is false (default)', async () => {
184
+ const records: AuditRecord[] = [
185
+ {
186
+ id: 'my-custom-id',
187
+ type: 'user.created',
188
+ operation: 'CUSTOM',
189
+ timestamp: new Date(),
190
+ },
191
+ ];
192
+
193
+ await storage.write(records);
194
+
195
+ expect(mockDb.insertBuilder.values).toHaveBeenCalledWith([
196
+ expect.objectContaining({
197
+ id: 'my-custom-id',
198
+ }),
199
+ ]);
200
+ });
201
+
202
+ it('should omit ID when autoId is true and no ID provided', async () => {
203
+ const storageAutoId = new KyselyAuditStorage({
204
+ db: mockDb.db as any,
205
+ tableName: 'audit_logs',
206
+ autoId: true,
207
+ });
208
+
209
+ const records: AuditRecord[] = [
210
+ {
211
+ id: '', // Empty ID - let database generate
212
+ type: 'user.created',
213
+ operation: 'CUSTOM',
214
+ timestamp: new Date(),
215
+ },
216
+ ];
217
+
218
+ await storageAutoId.write(records);
219
+
220
+ const calledValues = mockDb.insertBuilder.values.mock.calls[0][0];
221
+ expect(calledValues[0].id).toBeUndefined();
222
+ });
223
+
224
+ it('should omit ID even if provided when autoId is true', async () => {
225
+ const storageAutoId = new KyselyAuditStorage({
226
+ db: mockDb.db as any,
227
+ tableName: 'audit_logs',
228
+ autoId: true,
229
+ });
230
+
231
+ const records: AuditRecord[] = [
232
+ {
233
+ id: 'explicit-id', // This should be ignored when autoId is true
234
+ type: 'user.created',
235
+ operation: 'CUSTOM',
236
+ timestamp: new Date(),
237
+ },
238
+ ];
239
+
240
+ await storageAutoId.write(records);
241
+
242
+ // When autoId is true, ID should always be omitted (let database generate)
243
+ const calledValues = mockDb.insertBuilder.values.mock.calls[0][0];
244
+ expect(calledValues[0].id).toBeUndefined();
245
+ });
246
+ });
247
+
248
+ describe('query', () => {
249
+ it('should return records from database', async () => {
250
+ mockDb.addRow({
251
+ id: 'audit-1',
252
+ type: 'user.created',
253
+ operation: 'INSERT',
254
+ table: 'users',
255
+ entityId: 'user-123',
256
+ oldValues: null,
257
+ newValues: JSON.stringify({ name: 'Test' }),
258
+ payload: JSON.stringify({ email: 'test@example.com' }),
259
+ timestamp: new Date('2024-01-01T00:00:00Z'),
260
+ actorId: 'admin-1',
261
+ actorType: 'admin',
262
+ actorData: null,
263
+ metadata: JSON.stringify({ requestId: 'req-123' }),
264
+ });
265
+
266
+ const results = await storage.query({});
267
+
268
+ expect(results).toHaveLength(1);
269
+ expect(results[0]).toMatchObject({
270
+ id: 'audit-1',
271
+ type: 'user.created',
272
+ operation: 'INSERT',
273
+ table: 'users',
274
+ entityId: 'user-123',
275
+ newValues: { name: 'Test' },
276
+ payload: { email: 'test@example.com' },
277
+ actor: { id: 'admin-1', type: 'admin' },
278
+ metadata: { requestId: 'req-123' },
279
+ });
280
+ });
281
+
282
+ it('should apply type filter', async () => {
283
+ await storage.query({ type: 'user.created' });
284
+
285
+ expect(mockDb.selectBuilder.where).toHaveBeenCalledWith(
286
+ 'type',
287
+ '=',
288
+ 'user.created',
289
+ );
290
+ });
291
+
292
+ it('should apply type array filter', async () => {
293
+ await storage.query({ type: ['user.created', 'user.updated'] });
294
+
295
+ expect(mockDb.selectBuilder.where).toHaveBeenCalledWith('type', 'in', [
296
+ 'user.created',
297
+ 'user.updated',
298
+ ]);
299
+ });
300
+
301
+ it('should apply entity ID filter', async () => {
302
+ await storage.query({ entityId: 'user-123' });
303
+
304
+ expect(mockDb.selectBuilder.where).toHaveBeenCalledWith(
305
+ 'entityId',
306
+ '=',
307
+ 'user-123',
308
+ );
309
+ });
310
+
311
+ it('should apply table filter', async () => {
312
+ await storage.query({ table: 'users' });
313
+
314
+ expect(mockDb.selectBuilder.where).toHaveBeenCalledWith(
315
+ 'table',
316
+ '=',
317
+ 'users',
318
+ );
319
+ });
320
+
321
+ it('should apply actor ID filter', async () => {
322
+ await storage.query({ actorId: 'admin-1' });
323
+
324
+ expect(mockDb.selectBuilder.where).toHaveBeenCalledWith(
325
+ 'actorId',
326
+ '=',
327
+ 'admin-1',
328
+ );
329
+ });
330
+
331
+ it('should apply date range filters', async () => {
332
+ const from = new Date('2024-01-01');
333
+ const to = new Date('2024-12-31');
334
+
335
+ await storage.query({ from, to });
336
+
337
+ expect(mockDb.selectBuilder.where).toHaveBeenCalledWith(
338
+ 'timestamp',
339
+ '>=',
340
+ from,
341
+ );
342
+ expect(mockDb.selectBuilder.where).toHaveBeenCalledWith(
343
+ 'timestamp',
344
+ '<=',
345
+ to,
346
+ );
347
+ });
348
+
349
+ it('should apply pagination', async () => {
350
+ await storage.query({ limit: 10, offset: 20 });
351
+
352
+ expect(mockDb.selectBuilder.limit).toHaveBeenCalledWith(10);
353
+ expect(mockDb.selectBuilder.offset).toHaveBeenCalledWith(20);
354
+ });
355
+
356
+ it('should apply ordering', async () => {
357
+ await storage.query({ orderBy: 'type', orderDirection: 'asc' });
358
+
359
+ expect(mockDb.selectBuilder.orderBy).toHaveBeenCalledWith('type', 'asc');
360
+ });
361
+
362
+ it('should default to timestamp desc ordering', async () => {
363
+ await storage.query({});
364
+
365
+ expect(mockDb.selectBuilder.orderBy).toHaveBeenCalledWith(
366
+ 'timestamp',
367
+ 'desc',
368
+ );
369
+ });
370
+
371
+ it('should parse complex entity IDs', async () => {
372
+ mockDb.addRow({
373
+ id: 'audit-1',
374
+ type: 'relation.created',
375
+ operation: 'INSERT',
376
+ table: null,
377
+ entityId: JSON.stringify({ userId: 'u1', roleId: 'r1' }),
378
+ oldValues: null,
379
+ newValues: null,
380
+ payload: null,
381
+ timestamp: new Date(),
382
+ actorId: null,
383
+ actorType: null,
384
+ actorData: null,
385
+ metadata: null,
386
+ });
387
+
388
+ const results = await storage.query({});
389
+
390
+ expect(results[0].entityId).toEqual({ userId: 'u1', roleId: 'r1' });
391
+ });
392
+
393
+ it('should merge actor data', async () => {
394
+ mockDb.addRow({
395
+ id: 'audit-1',
396
+ type: 'user.created',
397
+ operation: 'CUSTOM',
398
+ table: null,
399
+ entityId: null,
400
+ oldValues: null,
401
+ newValues: null,
402
+ payload: null,
403
+ timestamp: new Date(),
404
+ actorId: 'user-1',
405
+ actorType: 'user',
406
+ actorData: JSON.stringify({ email: 'user@example.com' }),
407
+ metadata: null,
408
+ });
409
+
410
+ const results = await storage.query({});
411
+
412
+ expect(results[0].actor).toEqual({
413
+ id: 'user-1',
414
+ type: 'user',
415
+ email: 'user@example.com',
416
+ });
417
+ });
418
+ });
419
+
420
+ describe('count', () => {
421
+ it('should return count from database', async () => {
422
+ const count = await storage.count({});
423
+
424
+ expect(count).toBe(0);
425
+ expect(mockDb.selectBuilder.select).toHaveBeenCalled();
426
+ });
427
+
428
+ it('should apply filters to count', async () => {
429
+ await storage.count({ type: 'user.created', actorId: 'admin-1' });
430
+
431
+ expect(mockDb.selectBuilder.where).toHaveBeenCalledWith(
432
+ 'type',
433
+ '=',
434
+ 'user.created',
435
+ );
436
+ expect(mockDb.selectBuilder.where).toHaveBeenCalledWith(
437
+ 'actorId',
438
+ '=',
439
+ 'admin-1',
440
+ );
441
+ });
442
+ });
443
443
  });