@geekmidas/audit 0.0.7 → 0.1.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/README.md +397 -0
- package/dist/{Auditor-D3me-qKX.d.mts → Auditor-DrR0ImvJ.d.cts} +43 -1
- package/dist/{Auditor-QYUMGJCH.d.cts → Auditor-Xf4Tp63p.d.mts} +43 -1
- package/dist/Auditor.d.cts +1 -1
- package/dist/Auditor.d.mts +1 -1
- package/dist/{DefaultAuditor-BTuMMiWh.d.cts → DefaultAuditor-3Q1zVd1b.d.mts} +2 -2
- package/dist/{DefaultAuditor-C1FWrJg6.d.mts → DefaultAuditor-BaZa4u_m.d.cts} +2 -2
- package/dist/DefaultAuditor.d.cts +2 -2
- package/dist/DefaultAuditor.d.mts +2 -2
- package/dist/cache-0AjJ0zis.d.cts +95 -0
- package/dist/cache-BbIl31RL.mjs +167 -0
- package/dist/cache-BbIl31RL.mjs.map +1 -0
- package/dist/cache-DBbGcCEq.d.mts +95 -0
- package/dist/cache-l7jAptBl.cjs +173 -0
- package/dist/cache-l7jAptBl.cjs.map +1 -0
- package/dist/cache.cjs +3 -0
- package/dist/cache.d.cts +3 -0
- package/dist/cache.d.mts +3 -0
- package/dist/cache.mjs +3 -0
- package/dist/index.d.cts +2 -2
- package/dist/index.d.mts +2 -2
- package/dist/kysely.cjs +25 -1
- package/dist/kysely.cjs.map +1 -1
- package/dist/kysely.d.cts +10 -1
- package/dist/kysely.d.mts +10 -1
- package/dist/kysely.mjs +25 -1
- package/dist/kysely.mjs.map +1 -1
- package/dist/memory.cjs +49 -0
- package/dist/memory.cjs.map +1 -0
- package/dist/memory.d.cts +43 -0
- package/dist/memory.d.mts +43 -0
- package/dist/memory.mjs +48 -0
- package/dist/memory.mjs.map +1 -0
- 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 +33 -5
- package/src/__tests__/CacheAuditStorage.spec.ts +382 -0
- package/src/__tests__/InMemoryAuditStorage.spec.ts +317 -0
- package/src/__tests__/KyselyAuditStorage.integration.spec.ts +24 -20
- package/src/cache.ts +263 -0
- package/src/kysely.ts +33 -0
- package/src/memory.ts +50 -0
- package/src/storage.ts +47 -0
|
@@ -0,0 +1,382 @@
|
|
|
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
|
+
});
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it } from 'vitest';
|
|
2
|
+
import { InMemoryAuditStorage } from '../memory';
|
|
3
|
+
import type { AuditRecord } from '../types';
|
|
4
|
+
|
|
5
|
+
describe('InMemoryAuditStorage', () => {
|
|
6
|
+
let storage: InMemoryAuditStorage;
|
|
7
|
+
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
storage = new InMemoryAuditStorage();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
describe('write', () => {
|
|
13
|
+
it('should write records to memory', async () => {
|
|
14
|
+
const records: AuditRecord[] = [
|
|
15
|
+
{
|
|
16
|
+
id: 'audit-1',
|
|
17
|
+
type: 'user.created',
|
|
18
|
+
operation: 'INSERT',
|
|
19
|
+
table: 'users',
|
|
20
|
+
entityId: 'user-123',
|
|
21
|
+
payload: { email: 'test@example.com' },
|
|
22
|
+
timestamp: new Date('2024-01-01T00:00:00Z'),
|
|
23
|
+
actor: { id: 'admin-1', type: 'admin' },
|
|
24
|
+
metadata: { requestId: 'req-123' },
|
|
25
|
+
},
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
await storage.write(records);
|
|
29
|
+
|
|
30
|
+
const stored = await storage.getRecords();
|
|
31
|
+
expect(stored).toHaveLength(1);
|
|
32
|
+
expect(stored[0]).toMatchObject({
|
|
33
|
+
id: 'audit-1',
|
|
34
|
+
type: 'user.created',
|
|
35
|
+
entityId: 'user-123',
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('should append multiple records', async () => {
|
|
40
|
+
const record1: AuditRecord = {
|
|
41
|
+
id: 'audit-1',
|
|
42
|
+
type: 'user.created',
|
|
43
|
+
operation: 'INSERT',
|
|
44
|
+
timestamp: new Date('2024-01-01T00:00:00Z'),
|
|
45
|
+
};
|
|
46
|
+
const record2: AuditRecord = {
|
|
47
|
+
id: 'audit-2',
|
|
48
|
+
type: 'user.updated',
|
|
49
|
+
operation: 'UPDATE',
|
|
50
|
+
timestamp: new Date('2024-01-02T00:00:00Z'),
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
await storage.write([record1]);
|
|
54
|
+
await storage.write([record2]);
|
|
55
|
+
|
|
56
|
+
const stored = await storage.getRecords();
|
|
57
|
+
expect(stored).toHaveLength(2);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('should do nothing for empty records', async () => {
|
|
61
|
+
await storage.write([]);
|
|
62
|
+
|
|
63
|
+
const records = await storage.getRecords();
|
|
64
|
+
expect(records).toHaveLength(0);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
describe('query', () => {
|
|
69
|
+
beforeEach(async () => {
|
|
70
|
+
await storage.write([
|
|
71
|
+
{
|
|
72
|
+
id: 'audit-1',
|
|
73
|
+
type: 'user.created',
|
|
74
|
+
operation: 'INSERT',
|
|
75
|
+
table: 'users',
|
|
76
|
+
entityId: 'user-123',
|
|
77
|
+
payload: { email: 'test@example.com' },
|
|
78
|
+
timestamp: new Date('2024-01-01T00:00:00Z'),
|
|
79
|
+
actor: { id: 'admin-1', type: 'admin' },
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
id: 'audit-2',
|
|
83
|
+
type: 'user.updated',
|
|
84
|
+
operation: 'UPDATE',
|
|
85
|
+
table: 'users',
|
|
86
|
+
entityId: 'user-123',
|
|
87
|
+
timestamp: new Date('2024-01-02T00:00:00Z'),
|
|
88
|
+
actor: { id: 'admin-1', type: 'admin' },
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
id: 'audit-3',
|
|
92
|
+
type: 'order.placed',
|
|
93
|
+
operation: 'INSERT',
|
|
94
|
+
table: 'orders',
|
|
95
|
+
entityId: 'order-456',
|
|
96
|
+
timestamp: new Date('2024-01-03T00:00:00Z'),
|
|
97
|
+
actor: { id: 'user-1', type: 'user' },
|
|
98
|
+
},
|
|
99
|
+
]);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('should return all records when no filters', async () => {
|
|
103
|
+
const results = await storage.query({});
|
|
104
|
+
expect(results).toHaveLength(3);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('should filter by type (string)', async () => {
|
|
108
|
+
const results = await storage.query({ type: 'user.created' });
|
|
109
|
+
expect(results).toHaveLength(1);
|
|
110
|
+
expect(results[0].id).toBe('audit-1');
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('should filter by type (array)', async () => {
|
|
114
|
+
const results = await storage.query({
|
|
115
|
+
type: ['user.created', 'user.updated'],
|
|
116
|
+
});
|
|
117
|
+
expect(results).toHaveLength(2);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('should filter by entityId (string)', async () => {
|
|
121
|
+
const results = await storage.query({ entityId: 'user-123' });
|
|
122
|
+
expect(results).toHaveLength(2);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('should filter by entityId (object)', async () => {
|
|
126
|
+
await storage.write([
|
|
127
|
+
{
|
|
128
|
+
id: 'audit-4',
|
|
129
|
+
type: 'relation.created',
|
|
130
|
+
operation: 'INSERT',
|
|
131
|
+
entityId: { userId: 'u1', roleId: 'r1' },
|
|
132
|
+
timestamp: new Date('2024-01-04T00:00:00Z'),
|
|
133
|
+
},
|
|
134
|
+
]);
|
|
135
|
+
|
|
136
|
+
const results = await storage.query({
|
|
137
|
+
entityId: { userId: 'u1', roleId: 'r1' },
|
|
138
|
+
});
|
|
139
|
+
expect(results).toHaveLength(1);
|
|
140
|
+
expect(results[0].id).toBe('audit-4');
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('should filter by table', async () => {
|
|
144
|
+
const results = await storage.query({ table: 'users' });
|
|
145
|
+
expect(results).toHaveLength(2);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('should filter by actorId', async () => {
|
|
149
|
+
const results = await storage.query({ actorId: 'user-1' });
|
|
150
|
+
expect(results).toHaveLength(1);
|
|
151
|
+
expect(results[0].id).toBe('audit-3');
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('should filter by date range (from)', async () => {
|
|
155
|
+
const results = await storage.query({
|
|
156
|
+
from: new Date('2024-01-02T00:00:00Z'),
|
|
157
|
+
});
|
|
158
|
+
expect(results).toHaveLength(2);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('should filter by date range (to)', async () => {
|
|
162
|
+
const results = await storage.query({
|
|
163
|
+
to: new Date('2024-01-02T00:00:00Z'),
|
|
164
|
+
});
|
|
165
|
+
expect(results).toHaveLength(2);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('should filter by date range (from and to)', async () => {
|
|
169
|
+
const results = await storage.query({
|
|
170
|
+
from: new Date('2024-01-02T00:00:00Z'),
|
|
171
|
+
to: new Date('2024-01-02T00:00:00Z'),
|
|
172
|
+
});
|
|
173
|
+
expect(results).toHaveLength(1);
|
|
174
|
+
expect(results[0].id).toBe('audit-2');
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('should apply pagination (limit)', async () => {
|
|
178
|
+
const results = await storage.query({ limit: 2 });
|
|
179
|
+
expect(results).toHaveLength(2);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('should apply pagination (offset)', async () => {
|
|
183
|
+
const results = await storage.query({ offset: 1 });
|
|
184
|
+
expect(results).toHaveLength(2);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('should apply pagination (limit and offset)', async () => {
|
|
188
|
+
const results = await storage.query({ limit: 1, offset: 1 });
|
|
189
|
+
expect(results).toHaveLength(1);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('should order by timestamp desc by default', async () => {
|
|
193
|
+
const results = await storage.query({});
|
|
194
|
+
expect(results[0].id).toBe('audit-3'); // Most recent first
|
|
195
|
+
expect(results[2].id).toBe('audit-1'); // Oldest last
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('should order by timestamp asc', async () => {
|
|
199
|
+
const results = await storage.query({
|
|
200
|
+
orderBy: 'timestamp',
|
|
201
|
+
orderDirection: 'asc',
|
|
202
|
+
});
|
|
203
|
+
expect(results[0].id).toBe('audit-1'); // Oldest first
|
|
204
|
+
expect(results[2].id).toBe('audit-3'); // Most recent last
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it('should order by type', async () => {
|
|
208
|
+
const results = await storage.query({
|
|
209
|
+
orderBy: 'type',
|
|
210
|
+
orderDirection: 'asc',
|
|
211
|
+
});
|
|
212
|
+
expect(results[0].type).toBe('order.placed');
|
|
213
|
+
expect(results[2].type).toBe('user.updated');
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it('should combine multiple filters', async () => {
|
|
217
|
+
const results = await storage.query({
|
|
218
|
+
table: 'users',
|
|
219
|
+
actorId: 'admin-1',
|
|
220
|
+
});
|
|
221
|
+
expect(results).toHaveLength(2);
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
describe('count', () => {
|
|
226
|
+
beforeEach(async () => {
|
|
227
|
+
await storage.write([
|
|
228
|
+
{
|
|
229
|
+
id: 'audit-1',
|
|
230
|
+
type: 'user.created',
|
|
231
|
+
operation: 'INSERT',
|
|
232
|
+
timestamp: new Date(),
|
|
233
|
+
actor: { id: 'admin-1', type: 'admin' },
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
id: 'audit-2',
|
|
237
|
+
type: 'user.updated',
|
|
238
|
+
operation: 'UPDATE',
|
|
239
|
+
timestamp: new Date(),
|
|
240
|
+
actor: { id: 'admin-1', type: 'admin' },
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
id: 'audit-3',
|
|
244
|
+
type: 'order.placed',
|
|
245
|
+
operation: 'INSERT',
|
|
246
|
+
timestamp: new Date(),
|
|
247
|
+
actor: { id: 'user-1', type: 'user' },
|
|
248
|
+
},
|
|
249
|
+
]);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it('should count all records', async () => {
|
|
253
|
+
const count = await storage.count({});
|
|
254
|
+
expect(count).toBe(3);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it('should count with type filter', async () => {
|
|
258
|
+
const count = await storage.count({ type: 'user.created' });
|
|
259
|
+
expect(count).toBe(1);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it('should count with actor filter', async () => {
|
|
263
|
+
const count = await storage.count({ actorId: 'admin-1' });
|
|
264
|
+
expect(count).toBe(2);
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
describe('clear', () => {
|
|
269
|
+
it('should clear all records', async () => {
|
|
270
|
+
await storage.write([
|
|
271
|
+
{
|
|
272
|
+
id: 'audit-1',
|
|
273
|
+
type: 'test',
|
|
274
|
+
operation: 'CUSTOM',
|
|
275
|
+
timestamp: new Date(),
|
|
276
|
+
},
|
|
277
|
+
{
|
|
278
|
+
id: 'audit-2',
|
|
279
|
+
type: 'test',
|
|
280
|
+
operation: 'CUSTOM',
|
|
281
|
+
timestamp: new Date(),
|
|
282
|
+
},
|
|
283
|
+
]);
|
|
284
|
+
|
|
285
|
+
let records = await storage.getRecords();
|
|
286
|
+
expect(records).toHaveLength(2);
|
|
287
|
+
|
|
288
|
+
await storage.clear();
|
|
289
|
+
|
|
290
|
+
records = await storage.getRecords();
|
|
291
|
+
expect(records).toHaveLength(0);
|
|
292
|
+
});
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
describe('type safety', () => {
|
|
296
|
+
it('should support generic audit action types', async () => {
|
|
297
|
+
type AppAuditAction =
|
|
298
|
+
| { type: 'user.created'; payload: { userId: string } }
|
|
299
|
+
| { type: 'order.placed'; payload: { orderId: string } };
|
|
300
|
+
|
|
301
|
+
const typedStorage = new InMemoryAuditStorage<AppAuditAction>();
|
|
302
|
+
|
|
303
|
+
await typedStorage.write([
|
|
304
|
+
{
|
|
305
|
+
id: 'audit-1',
|
|
306
|
+
type: 'user.created',
|
|
307
|
+
operation: 'CUSTOM',
|
|
308
|
+
timestamp: new Date(),
|
|
309
|
+
payload: { userId: '123' },
|
|
310
|
+
},
|
|
311
|
+
]);
|
|
312
|
+
|
|
313
|
+
const records = await typedStorage.getRecords();
|
|
314
|
+
expect(records).toHaveLength(1);
|
|
315
|
+
});
|
|
316
|
+
});
|
|
317
|
+
});
|