@geekmidas/audit 9.0.2 → 10.0.0-alpha.1

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.
@@ -1,518 +0,0 @@
1
- import {
2
- CamelCasePlugin,
3
- type Generated,
4
- Kysely,
5
- PostgresDialect,
6
- sql,
7
- } from 'kysely';
8
- import pg from 'pg';
9
- import {
10
- afterAll,
11
- afterEach,
12
- beforeAll,
13
- beforeEach,
14
- describe,
15
- expect,
16
- it,
17
- } from 'vitest';
18
- import { TEST_DATABASE_CONFIG } from '../../../testkit/test/globalSetup';
19
- import { DefaultAuditor } from '../DefaultAuditor';
20
- import { type AuditLogTable, KyselyAuditStorage } from '../kysely';
21
- import type { AuditableAction } from '../types';
22
-
23
- // Use unique table names to avoid conflicts with parallel tests
24
- const AUDIT_TABLE = 'audit_int_logs' as const;
25
- const USERS_TABLE = 'audit_int_users' as const;
26
-
27
- interface TestDatabase {
28
- [AUDIT_TABLE]: AuditLogTable;
29
- [USERS_TABLE]: {
30
- id: Generated<number>;
31
- name: string;
32
- email: string;
33
- };
34
- }
35
-
36
- // Define test audit actions
37
- type TestAuditAction =
38
- | AuditableAction<'user.created', { userId: number; email: string }>
39
- | AuditableAction<'user.updated', { userId: number; changes: string[] }>
40
- | AuditableAction<'user.deleted', { userId: number }>;
41
-
42
- describe('KyselyAuditStorage Integration Tests', () => {
43
- let db: Kysely<TestDatabase>;
44
- let storage: KyselyAuditStorage<TestDatabase>;
45
-
46
- beforeAll(async () => {
47
- db = new Kysely<TestDatabase>({
48
- dialect: new PostgresDialect({
49
- pool: new pg.Pool({
50
- ...TEST_DATABASE_CONFIG,
51
- database: 'postgres',
52
- }),
53
- }),
54
- plugins: [new CamelCasePlugin()],
55
- });
56
-
57
- // Create audit_logs table
58
- await db.schema
59
- .createTable(AUDIT_TABLE)
60
- .ifNotExists()
61
- .addColumn('id', 'varchar(32)', (col) => col.primaryKey())
62
- .addColumn('type', 'varchar', (col) => col.notNull())
63
- .addColumn('operation', 'varchar', (col) => col.notNull())
64
- .addColumn('table', 'varchar')
65
- .addColumn('entityId', 'varchar')
66
- .addColumn('oldValues', 'jsonb')
67
- .addColumn('newValues', 'jsonb')
68
- .addColumn('payload', 'jsonb')
69
- .addColumn('timestamp', 'timestamp', (col) =>
70
- col.defaultTo(sql`now()`).notNull(),
71
- )
72
- .addColumn('actorId', 'varchar')
73
- .addColumn('actorType', 'varchar')
74
- .addColumn('actorData', 'jsonb')
75
- .addColumn('metadata', 'jsonb')
76
- .execute();
77
-
78
- // Create users table for testing audit integration
79
- await db.schema
80
- .createTable(USERS_TABLE)
81
- .ifNotExists()
82
- .addColumn('id', 'serial', (col) => col.primaryKey())
83
- .addColumn('name', 'varchar', (col) => col.notNull())
84
- .addColumn('email', 'varchar', (col) => col.notNull().unique())
85
- .execute();
86
-
87
- storage = new KyselyAuditStorage({
88
- db,
89
- tableName: AUDIT_TABLE,
90
- });
91
- });
92
-
93
- afterEach(async () => {
94
- // Clean up data after each test
95
- await db.deleteFrom(AUDIT_TABLE).execute();
96
- await db.deleteFrom(USERS_TABLE).execute();
97
- });
98
-
99
- afterAll(async () => {
100
- // Drop tables and close connection
101
- await db.schema.dropTable(AUDIT_TABLE).ifExists().execute();
102
- await db.schema.dropTable(USERS_TABLE).ifExists().execute();
103
- await db.destroy();
104
- });
105
-
106
- describe('write', () => {
107
- it('should write audit records to database', async () => {
108
- const auditor = new DefaultAuditor<TestAuditAction>({
109
- actor: { id: 'user-123', type: 'user' },
110
- storage,
111
- metadata: { requestId: 'req-456' },
112
- });
113
-
114
- auditor.audit('user.created', { userId: 1, email: 'test@example.com' });
115
-
116
- await auditor.flush();
117
-
118
- // Verify record was written
119
- const records = await db.selectFrom(AUDIT_TABLE).selectAll().execute();
120
-
121
- expect(records).toHaveLength(1);
122
- expect(records[0].type).toBe('user.created');
123
- expect(records[0].actorId).toBe('user-123');
124
- expect(records[0].actorType).toBe('user');
125
- expect(records[0].payload).toEqual({
126
- userId: 1,
127
- email: 'test@example.com',
128
- });
129
- expect(records[0].metadata).toEqual({
130
- requestId: 'req-456',
131
- });
132
- });
133
-
134
- it('should write multiple audit records in batch', async () => {
135
- const auditor = new DefaultAuditor<TestAuditAction>({
136
- actor: { id: 'admin-1', type: 'admin' },
137
- storage,
138
- });
139
-
140
- auditor.audit('user.created', { userId: 1, email: 'user1@example.com' });
141
- auditor.audit('user.created', { userId: 2, email: 'user2@example.com' });
142
- auditor.audit('user.updated', { userId: 1, changes: ['name'] });
143
-
144
- await auditor.flush();
145
-
146
- const records = await db
147
- .selectFrom(AUDIT_TABLE)
148
- .selectAll()
149
- .orderBy('timestamp', 'asc')
150
- .execute();
151
-
152
- expect(records).toHaveLength(3);
153
- expect(records.map((r) => r.type)).toEqual([
154
- 'user.created',
155
- 'user.created',
156
- 'user.updated',
157
- ]);
158
- });
159
-
160
- it('should write audit records within transaction', async () => {
161
- const auditor = new DefaultAuditor<TestAuditAction>({
162
- actor: { id: 'user-123', type: 'user' },
163
- storage,
164
- });
165
-
166
- await db.transaction().execute(async (trx) => {
167
- // Insert user
168
- const user = await trx
169
- .insertInto(USERS_TABLE)
170
- .values({ name: 'Test User', email: 'test@example.com' })
171
- .returningAll()
172
- .executeTakeFirstOrThrow();
173
-
174
- // Audit the creation
175
- auditor.audit(
176
- 'user.created',
177
- { userId: user.id, email: user.email },
178
- { entityId: String(user.id), table: 'users', operation: 'INSERT' },
179
- );
180
-
181
- // Flush within transaction
182
- await auditor.flush(trx);
183
- });
184
-
185
- // Verify both user and audit record exist
186
- const users = await db.selectFrom(USERS_TABLE).selectAll().execute();
187
- const audits = await db.selectFrom(AUDIT_TABLE).selectAll().execute();
188
-
189
- expect(users).toHaveLength(1);
190
- expect(audits).toHaveLength(1);
191
- expect(audits[0].entityId).toBe(String(users[0].id));
192
- expect(audits[0].table).toBe('users');
193
- expect(audits[0].operation).toBe('INSERT');
194
- });
195
-
196
- it('should rollback audit records when transaction fails', async () => {
197
- const auditor = new DefaultAuditor<TestAuditAction>({
198
- actor: { id: 'user-123', type: 'user' },
199
- storage,
200
- });
201
-
202
- const transactionPromise = db.transaction().execute(async (trx) => {
203
- // Insert user
204
- const user = await trx
205
- .insertInto(USERS_TABLE)
206
- .values({ name: 'Rollback User', email: 'rollback@example.com' })
207
- .returningAll()
208
- .executeTakeFirstOrThrow();
209
-
210
- // Audit the creation
211
- auditor.audit('user.created', { userId: user.id, email: user.email });
212
-
213
- // Flush within transaction
214
- await auditor.flush(trx);
215
-
216
- // Throw error to rollback
217
- throw new Error('Transaction should rollback');
218
- });
219
-
220
- await expect(transactionPromise).rejects.toThrow(
221
- 'Transaction should rollback',
222
- );
223
-
224
- // Verify both user and audit record were rolled back
225
- const users = await db.selectFrom(USERS_TABLE).selectAll().execute();
226
- const audits = await db.selectFrom(AUDIT_TABLE).selectAll().execute();
227
-
228
- expect(users).toHaveLength(0);
229
- expect(audits).toHaveLength(0);
230
- });
231
- });
232
-
233
- describe('query', () => {
234
- beforeEach(async () => {
235
- // Insert test audit records
236
- const auditor1 = new DefaultAuditor<TestAuditAction>({
237
- actor: { id: 'user-1', type: 'user' },
238
- storage,
239
- metadata: { endpoint: '/users' },
240
- });
241
-
242
- const auditor2 = new DefaultAuditor<TestAuditAction>({
243
- actor: { id: 'admin-1', type: 'admin' },
244
- storage,
245
- metadata: { endpoint: '/admin/users' },
246
- });
247
-
248
- auditor1.audit(
249
- 'user.created',
250
- { userId: 1, email: 'user1@example.com' },
251
- { entityId: '1', table: 'users' },
252
- );
253
- auditor1.audit(
254
- 'user.updated',
255
- { userId: 1, changes: ['name'] },
256
- { entityId: '1', table: 'users' },
257
- );
258
- auditor2.audit(
259
- 'user.deleted',
260
- { userId: 2 },
261
- { entityId: '2', table: 'users' },
262
- );
263
-
264
- await auditor1.flush();
265
- await auditor2.flush();
266
- });
267
-
268
- it('should query all records', async () => {
269
- const records = await storage.query({});
270
-
271
- expect(records).toHaveLength(3);
272
- });
273
-
274
- it('should filter by type', async () => {
275
- const records = await storage.query({ type: 'user.created' });
276
-
277
- expect(records).toHaveLength(1);
278
- expect(records[0].type).toBe('user.created');
279
- });
280
-
281
- it('should filter by multiple types', async () => {
282
- const records = await storage.query({
283
- type: ['user.created', 'user.updated'],
284
- });
285
-
286
- expect(records).toHaveLength(2);
287
- });
288
-
289
- it('should filter by actorId', async () => {
290
- const records = await storage.query({ actorId: 'admin-1' });
291
-
292
- expect(records).toHaveLength(1);
293
- expect(records[0].type).toBe('user.deleted');
294
- });
295
-
296
- it('should filter by entityId', async () => {
297
- const records = await storage.query({ entityId: '1' });
298
-
299
- expect(records).toHaveLength(2);
300
- });
301
-
302
- it('should filter by table', async () => {
303
- const records = await storage.query({ table: 'users' });
304
-
305
- expect(records).toHaveLength(3);
306
- });
307
-
308
- it('should apply pagination', async () => {
309
- const page1 = await storage.query({ limit: 2, offset: 0 });
310
- const page2 = await storage.query({ limit: 2, offset: 2 });
311
-
312
- expect(page1).toHaveLength(2);
313
- expect(page2).toHaveLength(1);
314
- });
315
-
316
- it('should order by timestamp descending by default', async () => {
317
- const records = await storage.query({});
318
-
319
- // Records should be in descending order (newest first)
320
- for (let i = 1; i < records.length; i++) {
321
- expect(records[i - 1].timestamp.getTime()).toBeGreaterThanOrEqual(
322
- records[i].timestamp.getTime(),
323
- );
324
- }
325
- });
326
-
327
- it('should order by type ascending', async () => {
328
- const records = await storage.query({
329
- orderBy: 'type',
330
- orderDirection: 'asc',
331
- });
332
-
333
- const types = records.map((r) => r.type);
334
- expect(types).toEqual([...types].sort());
335
- });
336
- });
337
-
338
- describe('count', () => {
339
- beforeEach(async () => {
340
- const auditor = new DefaultAuditor<TestAuditAction>({
341
- actor: { id: 'user-1', type: 'user' },
342
- storage,
343
- });
344
-
345
- auditor.audit('user.created', { userId: 1, email: 'user1@example.com' });
346
- auditor.audit('user.created', { userId: 2, email: 'user2@example.com' });
347
- auditor.audit('user.updated', { userId: 1, changes: ['name'] });
348
-
349
- await auditor.flush();
350
- });
351
-
352
- it('should count all records', async () => {
353
- const count = await storage.count({});
354
-
355
- expect(count).toBe(3);
356
- });
357
-
358
- it('should count with type filter', async () => {
359
- const count = await storage.count({ type: 'user.created' });
360
-
361
- expect(count).toBe(2);
362
- });
363
-
364
- it('should count with actorId filter', async () => {
365
- const count = await storage.count({ actorId: 'user-1' });
366
-
367
- expect(count).toBe(3);
368
- });
369
- });
370
-
371
- describe('complex scenarios', () => {
372
- it('should handle audit trail for user lifecycle', async () => {
373
- // Create user
374
- const createAuditor = new DefaultAuditor<TestAuditAction>({
375
- actor: { id: 'admin-1', type: 'admin' },
376
- storage,
377
- metadata: { endpoint: '/users', method: 'POST' },
378
- });
379
-
380
- const user = await db
381
- .insertInto(USERS_TABLE)
382
- .values({ name: 'John Doe', email: 'john@example.com' })
383
- .returningAll()
384
- .executeTakeFirstOrThrow();
385
-
386
- createAuditor.audit(
387
- 'user.created',
388
- { userId: user.id, email: user.email },
389
- {
390
- entityId: String(user.id),
391
- table: 'users',
392
- operation: 'INSERT',
393
- newValues: { name: 'John Doe', email: 'john@example.com' },
394
- },
395
- );
396
- await createAuditor.flush();
397
-
398
- // Update user
399
- const updateAuditor = new DefaultAuditor<TestAuditAction>({
400
- actor: { id: 'user-1', type: 'user' },
401
- storage,
402
- metadata: { endpoint: '/users/1', method: 'PATCH' },
403
- });
404
-
405
- await db
406
- .updateTable(USERS_TABLE)
407
- .set({ name: 'John Smith' })
408
- .where('id', '=', user.id)
409
- .execute();
410
-
411
- updateAuditor.audit(
412
- 'user.updated',
413
- { userId: user.id, changes: ['name'] },
414
- {
415
- entityId: String(user.id),
416
- table: 'users',
417
- operation: 'UPDATE',
418
- oldValues: { name: 'John Doe' },
419
- newValues: { name: 'John Smith' },
420
- },
421
- );
422
- await updateAuditor.flush();
423
-
424
- // Delete user
425
- const deleteAuditor = new DefaultAuditor<TestAuditAction>({
426
- actor: { id: 'admin-1', type: 'admin' },
427
- storage,
428
- metadata: { endpoint: '/users/1', method: 'DELETE' },
429
- });
430
-
431
- await db.deleteFrom(USERS_TABLE).where('id', '=', user.id).execute();
432
-
433
- deleteAuditor.audit(
434
- 'user.deleted',
435
- { userId: user.id },
436
- {
437
- entityId: String(user.id),
438
- table: 'users',
439
- operation: 'DELETE',
440
- oldValues: { name: 'John Smith', email: 'john@example.com' },
441
- },
442
- );
443
- await deleteAuditor.flush();
444
-
445
- // Query audit trail for this user
446
- const auditTrail = await storage.query({
447
- entityId: String(user.id),
448
- orderDirection: 'asc',
449
- });
450
-
451
- expect(auditTrail).toHaveLength(3);
452
- expect(auditTrail.map((r) => r.type)).toEqual([
453
- 'user.created',
454
- 'user.updated',
455
- 'user.deleted',
456
- ]);
457
- expect(auditTrail.map((r) => r.operation)).toEqual([
458
- 'INSERT',
459
- 'UPDATE',
460
- 'DELETE',
461
- ]);
462
-
463
- // Verify old/new values tracking
464
- expect(auditTrail[1].oldValues).toEqual({ name: 'John Doe' });
465
- expect(auditTrail[1].newValues).toEqual({ name: 'John Smith' });
466
- });
467
-
468
- it('should handle complex entity IDs', async () => {
469
- const auditor = new DefaultAuditor<TestAuditAction>({
470
- actor: { id: 'user-1', type: 'user' },
471
- storage,
472
- });
473
-
474
- // Record with composite entity ID
475
- auditor.record({
476
- type: 'relation.created',
477
- operation: 'INSERT',
478
- entityId: { userId: 1, roleId: 'admin' },
479
- table: 'user_roles',
480
- payload: { granted: true },
481
- });
482
-
483
- await auditor.flush();
484
-
485
- // Query by composite entity ID
486
- const records = await storage.query({
487
- entityId: { userId: 1, roleId: 'admin' },
488
- });
489
-
490
- expect(records).toHaveLength(1);
491
- expect(records[0].entityId).toEqual({ userId: 1, roleId: 'admin' });
492
- });
493
-
494
- it('should preserve actor extra properties', async () => {
495
- const auditor = new DefaultAuditor<TestAuditAction>({
496
- actor: {
497
- id: 'user-1',
498
- type: 'user',
499
- email: 'user@example.com',
500
- roles: ['admin', 'editor'],
501
- },
502
- storage,
503
- });
504
-
505
- auditor.audit('user.created', { userId: 1, email: 'new@example.com' });
506
- await auditor.flush();
507
-
508
- const records = await storage.query({});
509
-
510
- expect(records[0].actor).toEqual({
511
- id: 'user-1',
512
- type: 'user',
513
- email: 'user@example.com',
514
- roles: ['admin', 'editor'],
515
- });
516
- });
517
- });
518
- });