@geekmidas/audit 9.0.2 → 10.0.0-alpha.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/src/Auditor.ts DELETED
@@ -1,157 +0,0 @@
1
- import type {
2
- AuditActor,
3
- AuditableAction,
4
- AuditMetadata,
5
- AuditOptions,
6
- AuditRecord,
7
- ExtractAuditPayload,
8
- ExtractAuditType,
9
- } from './types';
10
-
11
- /**
12
- * Interface for audit collection and flushing.
13
- * Generic over audit action types for full type safety.
14
- *
15
- * @template TAuditAction - Union of all allowed audit action types
16
- * @template TTransaction - Transaction type (e.g., Kysely Transaction)
17
- *
18
- * @example
19
- * ```typescript
20
- * type AppAuditAction =
21
- * | AuditableAction<'user.created', { userId: string; email: string }>
22
- * | AuditableAction<'user.updated', { userId: string; changes: string[] }>;
23
- *
24
- * // In endpoint handler
25
- * const auditor: Auditor<AppAuditAction> = ...;
26
- *
27
- * // Type-safe audit calls
28
- * auditor.audit('user.created', { userId: '123', email: 'test@example.com' }); // ✅
29
- * auditor.audit('user.created', { orderId: '123' }); // ❌ Type error
30
- * auditor.audit('unknown.type', {}); // ❌ Type error
31
- * ```
32
- */
33
- export interface Auditor<
34
- TAuditAction extends AuditableAction<string, unknown> = AuditableAction<
35
- string,
36
- unknown
37
- >,
38
- TTransaction = unknown,
39
- > {
40
- /**
41
- * The actor for all audits in this context.
42
- * Set at construction time, immutable throughout the request.
43
- */
44
- readonly actor: AuditActor;
45
-
46
- /**
47
- * Record a type-safe audit entry.
48
- * The payload type is inferred from the audit type.
49
- *
50
- * @param type - The audit type (must be a valid type from TAuditAction)
51
- * @param payload - The audit payload (shape enforced by type)
52
- * @param options - Optional audit metadata
53
- *
54
- * @example
55
- * ```typescript
56
- * auditor.audit('user.created', {
57
- * userId: '123',
58
- * email: 'test@example.com',
59
- * });
60
- *
61
- * auditor.audit('order.placed', {
62
- * orderId: 'order-456',
63
- * total: 99.99,
64
- * }, {
65
- * entityId: 'order-456',
66
- * table: 'orders',
67
- * });
68
- * ```
69
- */
70
- audit<TType extends ExtractAuditType<TAuditAction>>(
71
- type: TType,
72
- payload: ExtractAuditPayload<TAuditAction, TType>,
73
- options?: AuditOptions,
74
- ): void;
75
-
76
- /**
77
- * Record a raw audit record.
78
- * Use this when you need full control over the audit structure,
79
- * bypassing type safety.
80
- *
81
- * @param record - The audit record (id, timestamp, actor added automatically)
82
- */
83
- record(record: Omit<AuditRecord, 'id' | 'timestamp' | 'actor'>): void;
84
-
85
- /**
86
- * Get all collected audit records.
87
- * Useful for inspection or custom processing.
88
- */
89
- getRecords(): AuditRecord[];
90
-
91
- /**
92
- * Flush all collected audits to storage.
93
- * Called automatically by the endpoint adaptor inside the transaction.
94
- *
95
- * @param trx - Optional transaction context for atomic writes
96
- */
97
- flush(trx?: TTransaction): Promise<void>;
98
-
99
- /**
100
- * Clear all collected audit records without flushing.
101
- * Use with caution - collected audits will be lost.
102
- */
103
- clear(): void;
104
-
105
- /**
106
- * Add metadata to all future audit records.
107
- * Merges with existing metadata (new values override existing).
108
- * Typically called by adaptors to add request context.
109
- *
110
- * @param metadata - Metadata to add (requestId, endpoint, method, ip, etc.)
111
- *
112
- * @example
113
- * ```typescript
114
- * // In endpoint adaptor
115
- * auditor.addMetadata({
116
- * requestId: 'req-123',
117
- * endpoint: '/users',
118
- * method: 'POST',
119
- * ip: '192.168.1.1',
120
- * });
121
- * ```
122
- */
123
- addMetadata(metadata: AuditMetadata): void;
124
-
125
- /**
126
- * Set the transaction context for audit flushing.
127
- * When set, flush() will use this transaction instead of requiring
128
- * it to be passed explicitly. This enables declarative audits to
129
- * participate in the same transaction as the handler's database operations.
130
- *
131
- * @param trx - The transaction context (e.g., Kysely Transaction)
132
- *
133
- * @example
134
- * ```typescript
135
- * // In handler with explicit transaction management
136
- * const result = await withTransaction(services.database.raw, async (trx) => {
137
- * // Register transaction with auditor so declarative audits use it
138
- * auditor.setTransaction(trx);
139
- *
140
- * const user = await trx.insertInto('users').values(data).returningAll().executeTakeFirstOrThrow();
141
- *
142
- * // Manual audits will also use this transaction when flush() is called
143
- * auditor.audit('user.created', { userId: user.id });
144
- *
145
- * return user;
146
- * });
147
- * // After handler, adaptor calls auditor.flush() which uses the stored transaction
148
- * ```
149
- */
150
- setTransaction(trx: TTransaction): void;
151
-
152
- /**
153
- * Get the currently set transaction context.
154
- * Returns undefined if no transaction has been set.
155
- */
156
- getTransaction(): TTransaction | undefined;
157
- }
@@ -1,141 +0,0 @@
1
- import { nanoid } from 'nanoid';
2
- import type { Auditor } from './Auditor';
3
- import type { AuditStorage } from './storage';
4
- import type {
5
- AuditActor,
6
- AuditableAction,
7
- AuditMetadata,
8
- AuditOptions,
9
- AuditRecord,
10
- ExtractAuditPayload,
11
- ExtractAuditType,
12
- } from './types';
13
-
14
- /**
15
- * Configuration for DefaultAuditor.
16
- */
17
- export interface DefaultAuditorConfig {
18
- /** The actor performing audits (set at construction, immutable) */
19
- actor: AuditActor;
20
- /** Storage backend for persisting audits */
21
- storage: AuditStorage;
22
- /** Optional metadata to attach to all audits */
23
- metadata?: AuditMetadata;
24
- /** Optional custom ID generator (defaults to nanoid) */
25
- generateId?: () => string;
26
- }
27
-
28
- /**
29
- * Default implementation of the Auditor interface.
30
- * Collects audit records in memory and flushes to storage.
31
- *
32
- * @template TAuditAction - Union of all allowed audit action types
33
- * @template TTransaction - Transaction type (e.g., Kysely Transaction)
34
- *
35
- * @example
36
- * ```typescript
37
- * const auditor = new DefaultAuditor<AppAuditAction>({
38
- * actor: { id: 'user-123', type: 'user' },
39
- * storage: auditStorage,
40
- * metadata: { requestId: 'req-456', endpoint: '/users' },
41
- * });
42
- *
43
- * auditor.audit('user.created', { userId: '789', email: 'test@example.com' });
44
- *
45
- * // Flush inside transaction
46
- * await auditor.flush(trx);
47
- * ```
48
- */
49
- export class DefaultAuditor<
50
- TAuditAction extends AuditableAction<string, unknown> = AuditableAction<
51
- string,
52
- unknown
53
- >,
54
- TTransaction = unknown,
55
- > implements Auditor<TAuditAction, TTransaction>
56
- {
57
- readonly actor: AuditActor;
58
- private readonly storage: AuditStorage;
59
- private metadata?: AuditMetadata;
60
- private readonly generateId: () => string;
61
- private records: AuditRecord[] = [];
62
- private transaction?: TTransaction;
63
-
64
- constructor(config: DefaultAuditorConfig) {
65
- this.actor = config.actor;
66
- this.storage = config.storage;
67
- this.metadata = config.metadata;
68
- this.generateId = config.generateId ?? (() => nanoid());
69
- }
70
-
71
- audit<TType extends ExtractAuditType<TAuditAction>>(
72
- type: TType,
73
- payload: ExtractAuditPayload<TAuditAction, TType>,
74
- options?: AuditOptions,
75
- ): void {
76
- const record: AuditRecord = {
77
- id: this.generateId(),
78
- type,
79
- operation: options?.operation ?? 'CUSTOM',
80
- table: options?.table,
81
- entityId: options?.entityId,
82
- oldValues: options?.oldValues,
83
- newValues: options?.newValues,
84
- payload,
85
- timestamp: new Date(),
86
- actor: this.actor,
87
- metadata: this.metadata,
88
- };
89
-
90
- this.records.push(record);
91
- }
92
-
93
- record(record: Omit<AuditRecord, 'id' | 'timestamp' | 'actor'>): void {
94
- const fullRecord: AuditRecord = {
95
- ...record,
96
- id: this.generateId(),
97
- timestamp: new Date(),
98
- actor: this.actor,
99
- metadata: this.metadata
100
- ? { ...this.metadata, ...record.metadata }
101
- : record.metadata,
102
- };
103
-
104
- this.records.push(fullRecord);
105
- }
106
-
107
- getRecords(): AuditRecord[] {
108
- return [...this.records];
109
- }
110
-
111
- async flush(trx?: TTransaction): Promise<void> {
112
- if (this.records.length === 0) {
113
- return;
114
- }
115
-
116
- const recordsToFlush = [...this.records];
117
- this.records = [];
118
-
119
- // Use explicitly passed transaction, or fall back to stored transaction
120
- const transactionToUse = trx ?? this.transaction;
121
- await this.storage.write(recordsToFlush, transactionToUse);
122
- }
123
-
124
- setTransaction(trx: TTransaction): void {
125
- this.transaction = trx;
126
- }
127
-
128
- getTransaction(): TTransaction | undefined {
129
- return this.transaction;
130
- }
131
-
132
- clear(): void {
133
- this.records = [];
134
- }
135
-
136
- addMetadata(metadata: AuditMetadata): void {
137
- this.metadata = this.metadata
138
- ? { ...this.metadata, ...metadata }
139
- : metadata;
140
- }
141
- }
@@ -1,382 +0,0 @@
1
- import { InMemoryCache } from '@geekmidas/cache/memory';
2
- import { beforeEach, describe, expect, it } from 'vitest';
3
- import { CacheAuditStorage } from '../cache';
4
- import type { AuditRecord } from '../types';
5
-
6
- describe('CacheAuditStorage', () => {
7
- let cache: InMemoryCache;
8
- let storage: CacheAuditStorage;
9
-
10
- beforeEach(() => {
11
- cache = new InMemoryCache();
12
- storage = new CacheAuditStorage({ cache });
13
- });
14
-
15
- describe('write', () => {
16
- it('should write records to cache', async () => {
17
- const records: AuditRecord[] = [
18
- {
19
- id: 'audit-1',
20
- type: 'user.created',
21
- operation: 'INSERT',
22
- table: 'users',
23
- entityId: 'user-123',
24
- payload: { email: 'test@example.com' },
25
- timestamp: new Date('2024-01-01T00:00:00Z'),
26
- actor: { id: 'admin-1', type: 'admin' },
27
- metadata: { requestId: 'req-123' },
28
- },
29
- ];
30
-
31
- await storage.write(records);
32
-
33
- const stored = await storage.getRecords();
34
- expect(stored).toHaveLength(1);
35
- expect(stored[0]).toMatchObject({
36
- id: 'audit-1',
37
- type: 'user.created',
38
- entityId: 'user-123',
39
- });
40
- });
41
-
42
- it('should append multiple records', async () => {
43
- const record1: AuditRecord = {
44
- id: 'audit-1',
45
- type: 'user.created',
46
- operation: 'INSERT',
47
- timestamp: new Date('2024-01-01T00:00:00Z'),
48
- };
49
- const record2: AuditRecord = {
50
- id: 'audit-2',
51
- type: 'user.updated',
52
- operation: 'UPDATE',
53
- timestamp: new Date('2024-01-02T00:00:00Z'),
54
- };
55
-
56
- await storage.write([record1]);
57
- await storage.write([record2]);
58
-
59
- const stored = await storage.getRecords();
60
- expect(stored).toHaveLength(2);
61
- });
62
-
63
- it('should do nothing for empty records', async () => {
64
- await storage.write([]);
65
-
66
- const records = await storage.getRecords();
67
- expect(records).toHaveLength(0);
68
- });
69
-
70
- it('should serialize and deserialize dates correctly', async () => {
71
- const timestamp = new Date('2024-06-15T12:30:45.123Z');
72
- await storage.write([
73
- {
74
- id: 'audit-1',
75
- type: 'test',
76
- operation: 'CUSTOM',
77
- timestamp,
78
- },
79
- ]);
80
-
81
- const records = await storage.getRecords();
82
- expect(records[0].timestamp).toBeInstanceOf(Date);
83
- expect(records[0].timestamp.toISOString()).toBe(timestamp.toISOString());
84
- });
85
- });
86
-
87
- describe('query', () => {
88
- beforeEach(async () => {
89
- await storage.write([
90
- {
91
- id: 'audit-1',
92
- type: 'user.created',
93
- operation: 'INSERT',
94
- table: 'users',
95
- entityId: 'user-123',
96
- payload: { email: 'test@example.com' },
97
- timestamp: new Date('2024-01-01T00:00:00Z'),
98
- actor: { id: 'admin-1', type: 'admin' },
99
- },
100
- {
101
- id: 'audit-2',
102
- type: 'user.updated',
103
- operation: 'UPDATE',
104
- table: 'users',
105
- entityId: 'user-123',
106
- timestamp: new Date('2024-01-02T00:00:00Z'),
107
- actor: { id: 'admin-1', type: 'admin' },
108
- },
109
- {
110
- id: 'audit-3',
111
- type: 'order.placed',
112
- operation: 'INSERT',
113
- table: 'orders',
114
- entityId: 'order-456',
115
- timestamp: new Date('2024-01-03T00:00:00Z'),
116
- actor: { id: 'user-1', type: 'user' },
117
- },
118
- ]);
119
- });
120
-
121
- it('should return all records when no filters', async () => {
122
- const results = await storage.query({});
123
- expect(results).toHaveLength(3);
124
- });
125
-
126
- it('should filter by type (string)', async () => {
127
- const results = await storage.query({ type: 'user.created' });
128
- expect(results).toHaveLength(1);
129
- expect(results[0].id).toBe('audit-1');
130
- });
131
-
132
- it('should filter by type (array)', async () => {
133
- const results = await storage.query({
134
- type: ['user.created', 'user.updated'],
135
- });
136
- expect(results).toHaveLength(2);
137
- });
138
-
139
- it('should filter by entityId (string)', async () => {
140
- const results = await storage.query({ entityId: 'user-123' });
141
- expect(results).toHaveLength(2);
142
- });
143
-
144
- it('should filter by entityId (object)', async () => {
145
- await storage.write([
146
- {
147
- id: 'audit-4',
148
- type: 'relation.created',
149
- operation: 'INSERT',
150
- entityId: { userId: 'u1', roleId: 'r1' },
151
- timestamp: new Date('2024-01-04T00:00:00Z'),
152
- },
153
- ]);
154
-
155
- const results = await storage.query({
156
- entityId: { userId: 'u1', roleId: 'r1' },
157
- });
158
- expect(results).toHaveLength(1);
159
- expect(results[0].id).toBe('audit-4');
160
- });
161
-
162
- it('should filter by table', async () => {
163
- const results = await storage.query({ table: 'users' });
164
- expect(results).toHaveLength(2);
165
- });
166
-
167
- it('should filter by actorId', async () => {
168
- const results = await storage.query({ actorId: 'user-1' });
169
- expect(results).toHaveLength(1);
170
- expect(results[0].id).toBe('audit-3');
171
- });
172
-
173
- it('should filter by date range (from)', async () => {
174
- const results = await storage.query({
175
- from: new Date('2024-01-02T00:00:00Z'),
176
- });
177
- expect(results).toHaveLength(2);
178
- });
179
-
180
- it('should filter by date range (to)', async () => {
181
- const results = await storage.query({
182
- to: new Date('2024-01-02T00:00:00Z'),
183
- });
184
- expect(results).toHaveLength(2);
185
- });
186
-
187
- it('should filter by date range (from and to)', async () => {
188
- const results = await storage.query({
189
- from: new Date('2024-01-02T00:00:00Z'),
190
- to: new Date('2024-01-02T00:00:00Z'),
191
- });
192
- expect(results).toHaveLength(1);
193
- expect(results[0].id).toBe('audit-2');
194
- });
195
-
196
- it('should apply pagination (limit)', async () => {
197
- const results = await storage.query({ limit: 2 });
198
- expect(results).toHaveLength(2);
199
- });
200
-
201
- it('should apply pagination (offset)', async () => {
202
- const results = await storage.query({ offset: 1 });
203
- expect(results).toHaveLength(2);
204
- });
205
-
206
- it('should apply pagination (limit and offset)', async () => {
207
- const results = await storage.query({ limit: 1, offset: 1 });
208
- expect(results).toHaveLength(1);
209
- });
210
-
211
- it('should order by timestamp desc by default', async () => {
212
- const results = await storage.query({});
213
- expect(results[0].id).toBe('audit-3'); // Most recent first
214
- expect(results[2].id).toBe('audit-1'); // Oldest last
215
- });
216
-
217
- it('should order by timestamp asc', async () => {
218
- const results = await storage.query({
219
- orderBy: 'timestamp',
220
- orderDirection: 'asc',
221
- });
222
- expect(results[0].id).toBe('audit-1'); // Oldest first
223
- expect(results[2].id).toBe('audit-3'); // Most recent last
224
- });
225
-
226
- it('should order by type', async () => {
227
- const results = await storage.query({
228
- orderBy: 'type',
229
- orderDirection: 'asc',
230
- });
231
- expect(results[0].type).toBe('order.placed');
232
- expect(results[2].type).toBe('user.updated');
233
- });
234
-
235
- it('should combine multiple filters', async () => {
236
- const results = await storage.query({
237
- table: 'users',
238
- actorId: 'admin-1',
239
- });
240
- expect(results).toHaveLength(2);
241
- });
242
- });
243
-
244
- describe('count', () => {
245
- beforeEach(async () => {
246
- await storage.write([
247
- {
248
- id: 'audit-1',
249
- type: 'user.created',
250
- operation: 'INSERT',
251
- timestamp: new Date(),
252
- actor: { id: 'admin-1', type: 'admin' },
253
- },
254
- {
255
- id: 'audit-2',
256
- type: 'user.updated',
257
- operation: 'UPDATE',
258
- timestamp: new Date(),
259
- actor: { id: 'admin-1', type: 'admin' },
260
- },
261
- {
262
- id: 'audit-3',
263
- type: 'order.placed',
264
- operation: 'INSERT',
265
- timestamp: new Date(),
266
- actor: { id: 'user-1', type: 'user' },
267
- },
268
- ]);
269
- });
270
-
271
- it('should count all records', async () => {
272
- const count = await storage.count({});
273
- expect(count).toBe(3);
274
- });
275
-
276
- it('should count with type filter', async () => {
277
- const count = await storage.count({ type: 'user.created' });
278
- expect(count).toBe(1);
279
- });
280
-
281
- it('should count with actor filter', async () => {
282
- const count = await storage.count({ actorId: 'admin-1' });
283
- expect(count).toBe(2);
284
- });
285
- });
286
-
287
- describe('clear', () => {
288
- it('should clear all records', async () => {
289
- await storage.write([
290
- {
291
- id: 'audit-1',
292
- type: 'test',
293
- operation: 'CUSTOM',
294
- timestamp: new Date(),
295
- },
296
- {
297
- id: 'audit-2',
298
- type: 'test',
299
- operation: 'CUSTOM',
300
- timestamp: new Date(),
301
- },
302
- ]);
303
-
304
- let records = await storage.getRecords();
305
- expect(records).toHaveLength(2);
306
-
307
- await storage.clear();
308
-
309
- records = await storage.getRecords();
310
- expect(records).toHaveLength(0);
311
- });
312
- });
313
-
314
- describe('configuration', () => {
315
- it('should use custom prefix', async () => {
316
- const customStorage = new CacheAuditStorage({
317
- cache,
318
- prefix: 'my-audit',
319
- });
320
-
321
- await customStorage.write([
322
- {
323
- id: 'audit-1',
324
- type: 'test',
325
- operation: 'CUSTOM',
326
- timestamp: new Date(),
327
- },
328
- ]);
329
-
330
- // Check that it uses the custom prefix
331
- const indexValue = await cache.get<string[]>('my-audit:__index__');
332
- expect(indexValue).toContain('audit-1');
333
-
334
- const recordValue = await cache.get('my-audit:audit-1');
335
- expect(recordValue).toBeDefined();
336
- });
337
-
338
- it('should use custom TTL', async () => {
339
- const ttl = 3600; // 1 hour
340
- const customStorage = new CacheAuditStorage({
341
- cache,
342
- ttl,
343
- });
344
-
345
- await customStorage.write([
346
- {
347
- id: 'audit-1',
348
- type: 'test',
349
- operation: 'CUSTOM',
350
- timestamp: new Date(),
351
- },
352
- ]);
353
-
354
- // Record should be retrievable
355
- const records = await customStorage.getRecords();
356
- expect(records).toHaveLength(1);
357
- });
358
- });
359
-
360
- describe('type safety', () => {
361
- it('should support generic audit action types', async () => {
362
- type AppAuditAction =
363
- | { type: 'user.created'; payload: { userId: string } }
364
- | { type: 'order.placed'; payload: { orderId: string } };
365
-
366
- const typedStorage = new CacheAuditStorage<AppAuditAction>({ cache });
367
-
368
- await typedStorage.write([
369
- {
370
- id: 'audit-1',
371
- type: 'user.created',
372
- operation: 'CUSTOM',
373
- timestamp: new Date(),
374
- payload: { userId: '123' },
375
- },
376
- ]);
377
-
378
- const records = await typedStorage.getRecords();
379
- expect(records).toHaveLength(1);
380
- });
381
- });
382
- });