@geekmidas/audit 0.0.8 → 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 +24 -0
- 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 +24 -0
- 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
|
@@ -20,9 +20,13 @@ import { DefaultAuditor } from '../DefaultAuditor';
|
|
|
20
20
|
import { type AuditLogTable, KyselyAuditStorage } from '../kysely';
|
|
21
21
|
import type { AuditableAction } from '../types';
|
|
22
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
|
+
|
|
23
27
|
interface TestDatabase {
|
|
24
|
-
|
|
25
|
-
|
|
28
|
+
[AUDIT_TABLE]: AuditLogTable;
|
|
29
|
+
[USERS_TABLE]: {
|
|
26
30
|
id: Generated<number>;
|
|
27
31
|
name: string;
|
|
28
32
|
email: string;
|
|
@@ -52,7 +56,7 @@ describe('KyselyAuditStorage Integration Tests', () => {
|
|
|
52
56
|
|
|
53
57
|
// Create audit_logs table
|
|
54
58
|
await db.schema
|
|
55
|
-
.createTable(
|
|
59
|
+
.createTable(AUDIT_TABLE)
|
|
56
60
|
.ifNotExists()
|
|
57
61
|
.addColumn('id', 'varchar(32)', (col) => col.primaryKey())
|
|
58
62
|
.addColumn('type', 'varchar', (col) => col.notNull())
|
|
@@ -73,7 +77,7 @@ describe('KyselyAuditStorage Integration Tests', () => {
|
|
|
73
77
|
|
|
74
78
|
// Create users table for testing audit integration
|
|
75
79
|
await db.schema
|
|
76
|
-
.createTable(
|
|
80
|
+
.createTable(USERS_TABLE)
|
|
77
81
|
.ifNotExists()
|
|
78
82
|
.addColumn('id', 'serial', (col) => col.primaryKey())
|
|
79
83
|
.addColumn('name', 'varchar', (col) => col.notNull())
|
|
@@ -82,20 +86,20 @@ describe('KyselyAuditStorage Integration Tests', () => {
|
|
|
82
86
|
|
|
83
87
|
storage = new KyselyAuditStorage({
|
|
84
88
|
db,
|
|
85
|
-
tableName:
|
|
89
|
+
tableName: AUDIT_TABLE,
|
|
86
90
|
});
|
|
87
91
|
});
|
|
88
92
|
|
|
89
93
|
afterEach(async () => {
|
|
90
94
|
// Clean up data after each test
|
|
91
|
-
await db.deleteFrom(
|
|
92
|
-
await db.deleteFrom(
|
|
95
|
+
await db.deleteFrom(AUDIT_TABLE).execute();
|
|
96
|
+
await db.deleteFrom(USERS_TABLE).execute();
|
|
93
97
|
});
|
|
94
98
|
|
|
95
99
|
afterAll(async () => {
|
|
96
100
|
// Drop tables and close connection
|
|
97
|
-
await db.schema.dropTable(
|
|
98
|
-
await db.schema.dropTable(
|
|
101
|
+
await db.schema.dropTable(AUDIT_TABLE).ifExists().execute();
|
|
102
|
+
await db.schema.dropTable(USERS_TABLE).ifExists().execute();
|
|
99
103
|
await db.destroy();
|
|
100
104
|
});
|
|
101
105
|
|
|
@@ -112,7 +116,7 @@ describe('KyselyAuditStorage Integration Tests', () => {
|
|
|
112
116
|
await auditor.flush();
|
|
113
117
|
|
|
114
118
|
// Verify record was written
|
|
115
|
-
const records = await db.selectFrom(
|
|
119
|
+
const records = await db.selectFrom(AUDIT_TABLE).selectAll().execute();
|
|
116
120
|
|
|
117
121
|
expect(records).toHaveLength(1);
|
|
118
122
|
expect(records[0].type).toBe('user.created');
|
|
@@ -140,7 +144,7 @@ describe('KyselyAuditStorage Integration Tests', () => {
|
|
|
140
144
|
await auditor.flush();
|
|
141
145
|
|
|
142
146
|
const records = await db
|
|
143
|
-
.selectFrom(
|
|
147
|
+
.selectFrom(AUDIT_TABLE)
|
|
144
148
|
.selectAll()
|
|
145
149
|
.orderBy('timestamp', 'asc')
|
|
146
150
|
.execute();
|
|
@@ -162,7 +166,7 @@ describe('KyselyAuditStorage Integration Tests', () => {
|
|
|
162
166
|
await db.transaction().execute(async (trx) => {
|
|
163
167
|
// Insert user
|
|
164
168
|
const user = await trx
|
|
165
|
-
.insertInto(
|
|
169
|
+
.insertInto(USERS_TABLE)
|
|
166
170
|
.values({ name: 'Test User', email: 'test@example.com' })
|
|
167
171
|
.returningAll()
|
|
168
172
|
.executeTakeFirstOrThrow();
|
|
@@ -179,8 +183,8 @@ describe('KyselyAuditStorage Integration Tests', () => {
|
|
|
179
183
|
});
|
|
180
184
|
|
|
181
185
|
// Verify both user and audit record exist
|
|
182
|
-
const users = await db.selectFrom(
|
|
183
|
-
const audits = await db.selectFrom(
|
|
186
|
+
const users = await db.selectFrom(USERS_TABLE).selectAll().execute();
|
|
187
|
+
const audits = await db.selectFrom(AUDIT_TABLE).selectAll().execute();
|
|
184
188
|
|
|
185
189
|
expect(users).toHaveLength(1);
|
|
186
190
|
expect(audits).toHaveLength(1);
|
|
@@ -198,7 +202,7 @@ describe('KyselyAuditStorage Integration Tests', () => {
|
|
|
198
202
|
const transactionPromise = db.transaction().execute(async (trx) => {
|
|
199
203
|
// Insert user
|
|
200
204
|
const user = await trx
|
|
201
|
-
.insertInto(
|
|
205
|
+
.insertInto(USERS_TABLE)
|
|
202
206
|
.values({ name: 'Rollback User', email: 'rollback@example.com' })
|
|
203
207
|
.returningAll()
|
|
204
208
|
.executeTakeFirstOrThrow();
|
|
@@ -218,8 +222,8 @@ describe('KyselyAuditStorage Integration Tests', () => {
|
|
|
218
222
|
);
|
|
219
223
|
|
|
220
224
|
// Verify both user and audit record were rolled back
|
|
221
|
-
const users = await db.selectFrom(
|
|
222
|
-
const audits = await db.selectFrom(
|
|
225
|
+
const users = await db.selectFrom(USERS_TABLE).selectAll().execute();
|
|
226
|
+
const audits = await db.selectFrom(AUDIT_TABLE).selectAll().execute();
|
|
223
227
|
|
|
224
228
|
expect(users).toHaveLength(0);
|
|
225
229
|
expect(audits).toHaveLength(0);
|
|
@@ -374,7 +378,7 @@ describe('KyselyAuditStorage Integration Tests', () => {
|
|
|
374
378
|
});
|
|
375
379
|
|
|
376
380
|
const user = await db
|
|
377
|
-
.insertInto(
|
|
381
|
+
.insertInto(USERS_TABLE)
|
|
378
382
|
.values({ name: 'John Doe', email: 'john@example.com' })
|
|
379
383
|
.returningAll()
|
|
380
384
|
.executeTakeFirstOrThrow();
|
|
@@ -399,7 +403,7 @@ describe('KyselyAuditStorage Integration Tests', () => {
|
|
|
399
403
|
});
|
|
400
404
|
|
|
401
405
|
await db
|
|
402
|
-
.updateTable(
|
|
406
|
+
.updateTable(USERS_TABLE)
|
|
403
407
|
.set({ name: 'John Smith' })
|
|
404
408
|
.where('id', '=', user.id)
|
|
405
409
|
.execute();
|
|
@@ -424,7 +428,7 @@ describe('KyselyAuditStorage Integration Tests', () => {
|
|
|
424
428
|
metadata: { endpoint: '/users/1', method: 'DELETE' },
|
|
425
429
|
});
|
|
426
430
|
|
|
427
|
-
await db.deleteFrom(
|
|
431
|
+
await db.deleteFrom(USERS_TABLE).where('id', '=', user.id).execute();
|
|
428
432
|
|
|
429
433
|
deleteAuditor.audit(
|
|
430
434
|
'user.deleted',
|
package/src/cache.ts
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import type { Cache } from '@geekmidas/cache';
|
|
2
|
+
import type { AuditQueryOptions, AuditStorage } from './storage';
|
|
3
|
+
import type { AuditRecord, AuditableAction } from './types';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Configuration for CacheAuditStorage.
|
|
7
|
+
*/
|
|
8
|
+
export interface CacheAuditStorageConfig {
|
|
9
|
+
/** Cache instance to use for storage */
|
|
10
|
+
cache: Cache;
|
|
11
|
+
/**
|
|
12
|
+
* Key prefix for audit records.
|
|
13
|
+
* Records are stored as `${prefix}:${id}`.
|
|
14
|
+
* @default 'audit'
|
|
15
|
+
*/
|
|
16
|
+
prefix?: string;
|
|
17
|
+
/**
|
|
18
|
+
* TTL (time-to-live) in seconds for audit records.
|
|
19
|
+
* If not set, records use the cache's default TTL.
|
|
20
|
+
*/
|
|
21
|
+
ttl?: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Cache-based audit storage implementation.
|
|
26
|
+
* Uses any @geekmidas/cache implementation for storage.
|
|
27
|
+
*
|
|
28
|
+
* Best suited for:
|
|
29
|
+
* - Development and testing
|
|
30
|
+
* - Temporary audit logs that don't need persistence
|
|
31
|
+
* - Applications with existing cache infrastructure
|
|
32
|
+
* - Distributed systems needing shared audit state (with Redis/Upstash)
|
|
33
|
+
*
|
|
34
|
+
* Note: Query performance may degrade with large numbers of records
|
|
35
|
+
* since filtering happens in memory after fetching all records.
|
|
36
|
+
*
|
|
37
|
+
* @template TAuditAction - Optional type parameter for type-safe audit actions.
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* ```typescript
|
|
41
|
+
* import { CacheAuditStorage } from '@geekmidas/audit/cache';
|
|
42
|
+
* import { InMemoryCache } from '@geekmidas/cache/memory';
|
|
43
|
+
* import { UpstashCache } from '@geekmidas/cache/upstash';
|
|
44
|
+
*
|
|
45
|
+
* // With in-memory cache (development/testing)
|
|
46
|
+
* const storage = new CacheAuditStorage({
|
|
47
|
+
* cache: new InMemoryCache(),
|
|
48
|
+
* ttl: 86400, // 24 hours
|
|
49
|
+
* });
|
|
50
|
+
*
|
|
51
|
+
* // With Upstash Redis (production)
|
|
52
|
+
* const storage = new CacheAuditStorage({
|
|
53
|
+
* cache: new UpstashCache({
|
|
54
|
+
* url: process.env.UPSTASH_REDIS_URL,
|
|
55
|
+
* token: process.env.UPSTASH_REDIS_TOKEN,
|
|
56
|
+
* }),
|
|
57
|
+
* ttl: 604800, // 7 days
|
|
58
|
+
* });
|
|
59
|
+
* ```
|
|
60
|
+
*/
|
|
61
|
+
export class CacheAuditStorage<
|
|
62
|
+
TAuditAction extends AuditableAction<string, unknown> = AuditableAction<
|
|
63
|
+
string,
|
|
64
|
+
unknown
|
|
65
|
+
>,
|
|
66
|
+
> implements AuditStorage<TAuditAction>
|
|
67
|
+
{
|
|
68
|
+
private readonly cache: Cache;
|
|
69
|
+
private readonly prefix: string;
|
|
70
|
+
private readonly ttl?: number;
|
|
71
|
+
private readonly indexKey: string;
|
|
72
|
+
|
|
73
|
+
constructor(config: CacheAuditStorageConfig) {
|
|
74
|
+
this.cache = config.cache;
|
|
75
|
+
this.prefix = config.prefix ?? 'audit';
|
|
76
|
+
this.ttl = config.ttl;
|
|
77
|
+
this.indexKey = `${this.prefix}:__index__`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Write audit records to cache.
|
|
82
|
+
*/
|
|
83
|
+
async write(records: AuditRecord[]): Promise<void> {
|
|
84
|
+
if (records.length === 0) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Get existing index
|
|
89
|
+
const existingIds = (await this.cache.get<string[]>(this.indexKey)) ?? [];
|
|
90
|
+
const newIds: string[] = [];
|
|
91
|
+
|
|
92
|
+
// Write each record
|
|
93
|
+
for (const record of records) {
|
|
94
|
+
const key = this.getRecordKey(record.id);
|
|
95
|
+
// Serialize Date to ISO string for cache storage
|
|
96
|
+
const serialized = this.serializeRecord(record);
|
|
97
|
+
await this.cache.set(key, serialized, this.ttl);
|
|
98
|
+
newIds.push(record.id);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Update index with new IDs
|
|
102
|
+
const updatedIds = [...existingIds, ...newIds];
|
|
103
|
+
await this.cache.set(this.indexKey, updatedIds, this.ttl);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Query audit records from cache.
|
|
108
|
+
*/
|
|
109
|
+
async query(options: AuditQueryOptions): Promise<AuditRecord[]> {
|
|
110
|
+
const allRecords = await this.getAllRecords();
|
|
111
|
+
let results = this.applyFilters(allRecords, options);
|
|
112
|
+
|
|
113
|
+
// Ordering
|
|
114
|
+
const orderBy = options.orderBy ?? 'timestamp';
|
|
115
|
+
const orderDirection = options.orderDirection ?? 'desc';
|
|
116
|
+
results.sort((a, b) => {
|
|
117
|
+
const aValue = orderBy === 'timestamp' ? a.timestamp.getTime() : a.type;
|
|
118
|
+
const bValue = orderBy === 'timestamp' ? b.timestamp.getTime() : b.type;
|
|
119
|
+
if (aValue < bValue) return orderDirection === 'asc' ? -1 : 1;
|
|
120
|
+
if (aValue > bValue) return orderDirection === 'asc' ? 1 : -1;
|
|
121
|
+
return 0;
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// Pagination
|
|
125
|
+
if (options.offset !== undefined) {
|
|
126
|
+
results = results.slice(options.offset);
|
|
127
|
+
}
|
|
128
|
+
if (options.limit !== undefined) {
|
|
129
|
+
results = results.slice(0, options.limit);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return results;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Count audit records matching filters.
|
|
137
|
+
*/
|
|
138
|
+
async count(
|
|
139
|
+
options: Omit<AuditQueryOptions, 'limit' | 'offset'>,
|
|
140
|
+
): Promise<number> {
|
|
141
|
+
const allRecords = await this.getAllRecords();
|
|
142
|
+
return this.applyFilters(allRecords, options).length;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Get all stored records (for testing/debugging).
|
|
147
|
+
*/
|
|
148
|
+
async getRecords(): Promise<AuditRecord[]> {
|
|
149
|
+
return this.getAllRecords();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Clear all stored records.
|
|
154
|
+
*/
|
|
155
|
+
async clear(): Promise<void> {
|
|
156
|
+
const ids = (await this.cache.get<string[]>(this.indexKey)) ?? [];
|
|
157
|
+
|
|
158
|
+
// Delete all records
|
|
159
|
+
for (const id of ids) {
|
|
160
|
+
await this.cache.delete(this.getRecordKey(id));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Delete index
|
|
164
|
+
await this.cache.delete(this.indexKey);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
private getRecordKey(id: string): string {
|
|
168
|
+
return `${this.prefix}:${id}`;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
private async getAllRecords(): Promise<AuditRecord[]> {
|
|
172
|
+
const ids = (await this.cache.get<string[]>(this.indexKey)) ?? [];
|
|
173
|
+
const records: AuditRecord[] = [];
|
|
174
|
+
const validIds: string[] = [];
|
|
175
|
+
|
|
176
|
+
for (const id of ids) {
|
|
177
|
+
const key = this.getRecordKey(id);
|
|
178
|
+
const serialized = await this.cache.get<SerializedAuditRecord>(key);
|
|
179
|
+
if (serialized) {
|
|
180
|
+
records.push(this.deserializeRecord(serialized));
|
|
181
|
+
validIds.push(id);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Clean up index if some records expired
|
|
186
|
+
if (validIds.length !== ids.length) {
|
|
187
|
+
await this.cache.set(this.indexKey, validIds, this.ttl);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return records;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
private serializeRecord(record: AuditRecord): SerializedAuditRecord {
|
|
194
|
+
return {
|
|
195
|
+
...record,
|
|
196
|
+
timestamp: record.timestamp.toISOString(),
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
private deserializeRecord(serialized: SerializedAuditRecord): AuditRecord {
|
|
201
|
+
return {
|
|
202
|
+
...serialized,
|
|
203
|
+
timestamp: new Date(serialized.timestamp),
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
private applyFilters(
|
|
208
|
+
records: AuditRecord[],
|
|
209
|
+
options: AuditQueryOptions,
|
|
210
|
+
): AuditRecord[] {
|
|
211
|
+
return records.filter((record) => {
|
|
212
|
+
// Type filter
|
|
213
|
+
if (options.type !== undefined) {
|
|
214
|
+
if (Array.isArray(options.type)) {
|
|
215
|
+
if (!options.type.includes(record.type)) return false;
|
|
216
|
+
} else {
|
|
217
|
+
if (record.type !== options.type) return false;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Entity ID filter
|
|
222
|
+
if (options.entityId !== undefined) {
|
|
223
|
+
const entityId =
|
|
224
|
+
typeof options.entityId === 'string'
|
|
225
|
+
? options.entityId
|
|
226
|
+
: JSON.stringify(options.entityId);
|
|
227
|
+
const recordEntityId =
|
|
228
|
+
typeof record.entityId === 'string'
|
|
229
|
+
? record.entityId
|
|
230
|
+
: JSON.stringify(record.entityId);
|
|
231
|
+
if (recordEntityId !== entityId) return false;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Table filter
|
|
235
|
+
if (options.table !== undefined) {
|
|
236
|
+
if (record.table !== options.table) return false;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Actor ID filter
|
|
240
|
+
if (options.actorId !== undefined) {
|
|
241
|
+
if (record.actor?.id !== options.actorId) return false;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Date range filters
|
|
245
|
+
if (options.from !== undefined) {
|
|
246
|
+
if (record.timestamp < options.from) return false;
|
|
247
|
+
}
|
|
248
|
+
if (options.to !== undefined) {
|
|
249
|
+
if (record.timestamp > options.to) return false;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return true;
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Serialized version of AuditRecord for cache storage.
|
|
259
|
+
* Dates are stored as ISO strings.
|
|
260
|
+
*/
|
|
261
|
+
type SerializedAuditRecord = Omit<AuditRecord, 'timestamp'> & {
|
|
262
|
+
timestamp: string;
|
|
263
|
+
};
|
package/src/kysely.ts
CHANGED
|
@@ -286,6 +286,39 @@ export class KyselyAuditStorage<DB> implements AuditStorage {
|
|
|
286
286
|
return this.db;
|
|
287
287
|
}
|
|
288
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
|
+
|
|
289
322
|
private applyFilters(query: any, options: AuditQueryOptions): any {
|
|
290
323
|
// Type filter
|
|
291
324
|
if (options.type !== undefined) {
|
package/src/memory.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { InMemoryCache } from '@geekmidas/cache/memory';
|
|
2
|
+
import { CacheAuditStorage } from './cache';
|
|
3
|
+
import type { AuditableAction } from './types';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* In-memory audit storage implementation.
|
|
7
|
+
* Convenience wrapper around CacheAuditStorage with InMemoryCache.
|
|
8
|
+
*
|
|
9
|
+
* Useful for testing, development, and applications that don't need persistent audit logs.
|
|
10
|
+
*
|
|
11
|
+
* @template TAuditAction - Optional type parameter for type-safe audit actions.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```typescript
|
|
15
|
+
* import { InMemoryAuditStorage } from '@geekmidas/audit/memory';
|
|
16
|
+
* import { DefaultAuditor } from '@geekmidas/audit';
|
|
17
|
+
*
|
|
18
|
+
* const storage = new InMemoryAuditStorage();
|
|
19
|
+
* const auditor = new DefaultAuditor({
|
|
20
|
+
* actor: { id: 'user-123', type: 'user' },
|
|
21
|
+
* storage,
|
|
22
|
+
* });
|
|
23
|
+
*
|
|
24
|
+
* auditor.audit('user.created', { userId: '789', email: 'test@example.com' });
|
|
25
|
+
* await auditor.flush();
|
|
26
|
+
*
|
|
27
|
+
* // Query stored records
|
|
28
|
+
* const records = await storage.query({ type: 'user.created' });
|
|
29
|
+
*
|
|
30
|
+
* // Get all records (for testing)
|
|
31
|
+
* const all = await storage.getRecords();
|
|
32
|
+
*
|
|
33
|
+
* // Clear for next test
|
|
34
|
+
* await storage.clear();
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
export class InMemoryAuditStorage<
|
|
38
|
+
TAuditAction extends AuditableAction<string, unknown> = AuditableAction<
|
|
39
|
+
string,
|
|
40
|
+
unknown
|
|
41
|
+
>,
|
|
42
|
+
> extends CacheAuditStorage<TAuditAction> {
|
|
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
|
+
}
|
package/src/storage.ts
CHANGED
|
@@ -125,4 +125,51 @@ export interface AuditStorage<
|
|
|
125
125
|
* ```
|
|
126
126
|
*/
|
|
127
127
|
databaseServiceName?: string;
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Optional: Execute a callback within a database transaction.
|
|
131
|
+
* The auditor is registered with the transaction and audits are flushed
|
|
132
|
+
* before the transaction commits.
|
|
133
|
+
*
|
|
134
|
+
* This is database-agnostic - each storage implementation provides its own
|
|
135
|
+
* transaction handling based on the underlying database.
|
|
136
|
+
*
|
|
137
|
+
* If a database connection is provided, it should be used instead of the
|
|
138
|
+
* storage's internal connection. If the connection is already a transaction,
|
|
139
|
+
* it should be reused instead of creating a nested transaction.
|
|
140
|
+
*
|
|
141
|
+
* @param auditor - The auditor to register with the transaction
|
|
142
|
+
* @param callback - The callback to execute within the transaction
|
|
143
|
+
* @param db - Optional database connection (may already be a transaction)
|
|
144
|
+
* @returns The result of the callback
|
|
145
|
+
*
|
|
146
|
+
* @example
|
|
147
|
+
* ```typescript
|
|
148
|
+
* // KyselyAuditStorage implementation
|
|
149
|
+
* async withTransaction<T>(auditor, callback, db) {
|
|
150
|
+
* const connection = db ?? this.db;
|
|
151
|
+
* if (connection.isTransaction) {
|
|
152
|
+
* // Reuse existing transaction
|
|
153
|
+
* auditor.setTransaction(connection);
|
|
154
|
+
* const result = await callback();
|
|
155
|
+
* await auditor.flush(connection);
|
|
156
|
+
* return result;
|
|
157
|
+
* }
|
|
158
|
+
* return connection.transaction().execute(async (trx) => {
|
|
159
|
+
* auditor.setTransaction(trx);
|
|
160
|
+
* const result = await callback();
|
|
161
|
+
* await auditor.flush(trx);
|
|
162
|
+
* return result;
|
|
163
|
+
* });
|
|
164
|
+
* }
|
|
165
|
+
* ```
|
|
166
|
+
*/
|
|
167
|
+
withTransaction?<T>(
|
|
168
|
+
auditor: {
|
|
169
|
+
setTransaction(trx: unknown): void;
|
|
170
|
+
flush(trx?: unknown): Promise<void>;
|
|
171
|
+
},
|
|
172
|
+
callback: () => Promise<T>,
|
|
173
|
+
db?: unknown,
|
|
174
|
+
): Promise<T>;
|
|
128
175
|
}
|