@geekmidas/audit 0.0.8 → 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 (64) hide show
  1. package/README.md +397 -0
  2. package/dist/{Auditor-D3me-qKX.d.mts → Auditor-_Dn2dp8d.d.mts} +46 -1
  3. package/dist/Auditor-_Dn2dp8d.d.mts.map +1 -0
  4. package/dist/{Auditor-QYUMGJCH.d.cts → Auditor-sW7YAJIA.d.cts} +46 -1
  5. package/dist/Auditor-sW7YAJIA.d.cts.map +1 -0
  6. package/dist/Auditor.d.cts +1 -1
  7. package/dist/Auditor.d.mts +1 -1
  8. package/dist/DefaultAuditor-BAVnNmRh.mjs.map +1 -1
  9. package/dist/{DefaultAuditor-BTuMMiWh.d.cts → DefaultAuditor-BSYMwojG.d.cts} +3 -2
  10. package/dist/DefaultAuditor-BSYMwojG.d.cts.map +1 -0
  11. package/dist/{DefaultAuditor-C1FWrJg6.d.mts → DefaultAuditor-CYE-uAry.d.mts} +3 -2
  12. package/dist/DefaultAuditor-CYE-uAry.d.mts.map +1 -0
  13. package/dist/DefaultAuditor-JbZ_BQfh.cjs.map +1 -1
  14. package/dist/DefaultAuditor.d.cts +2 -2
  15. package/dist/DefaultAuditor.d.mts +2 -2
  16. package/dist/cache-2VI80Vao.d.mts +96 -0
  17. package/dist/cache-2VI80Vao.d.mts.map +1 -0
  18. package/dist/cache-BbIl31RL.mjs +167 -0
  19. package/dist/cache-BbIl31RL.mjs.map +1 -0
  20. package/dist/cache-CDHqXqcl.d.cts +96 -0
  21. package/dist/cache-CDHqXqcl.d.cts.map +1 -0
  22. package/dist/cache-l7jAptBl.cjs +173 -0
  23. package/dist/cache-l7jAptBl.cjs.map +1 -0
  24. package/dist/cache.cjs +3 -0
  25. package/dist/cache.d.cts +3 -0
  26. package/dist/cache.d.mts +3 -0
  27. package/dist/cache.mjs +3 -0
  28. package/dist/index.d.cts +2 -2
  29. package/dist/index.d.mts +2 -2
  30. package/dist/kysely.cjs +24 -0
  31. package/dist/kysely.cjs.map +1 -1
  32. package/dist/kysely.d.cts +11 -1
  33. package/dist/kysely.d.cts.map +1 -0
  34. package/dist/kysely.d.mts +11 -1
  35. package/dist/kysely.d.mts.map +1 -0
  36. package/dist/kysely.mjs +24 -0
  37. package/dist/kysely.mjs.map +1 -1
  38. package/dist/memory.cjs +49 -0
  39. package/dist/memory.cjs.map +1 -0
  40. package/dist/memory.d.cts +44 -0
  41. package/dist/memory.d.cts.map +1 -0
  42. package/dist/memory.d.mts +44 -0
  43. package/dist/memory.d.mts.map +1 -0
  44. package/dist/memory.mjs +48 -0
  45. package/dist/memory.mjs.map +1 -0
  46. package/dist/storage.d.cts +1 -1
  47. package/dist/storage.d.mts +1 -1
  48. package/dist/types.d.cts +1 -1
  49. package/dist/types.d.mts +1 -1
  50. package/package.json +33 -5
  51. package/src/Auditor.ts +121 -121
  52. package/src/DefaultAuditor.ts +91 -91
  53. package/src/__tests__/CacheAuditStorage.spec.ts +382 -0
  54. package/src/__tests__/DefaultAuditor.spec.ts +520 -462
  55. package/src/__tests__/InMemoryAuditStorage.spec.ts +317 -0
  56. package/src/__tests__/KyselyAuditStorage.integration.spec.ts +500 -496
  57. package/src/__tests__/KyselyAuditStorage.spec.ts +433 -433
  58. package/src/cache.ts +263 -0
  59. package/src/index.ts +14 -15
  60. package/src/kysely.ts +292 -259
  61. package/src/memory.ts +50 -0
  62. package/src/storage.ts +132 -85
  63. package/src/types.ts +73 -74
  64. package/tsconfig.json +9 -0
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 { AuditableAction, AuditRecord } 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/index.ts CHANGED
@@ -1,24 +1,23 @@
1
1
  // Core types
2
- export type {
3
- AuditableAction,
4
- AuditActor,
5
- AuditMetadata,
6
- AuditOperation,
7
- AuditOptions,
8
- AuditRecord,
9
- ExtractAuditPayload,
10
- ExtractAuditType,
11
- ExtractAuditorAction,
12
- ExtractStorageAuditAction,
13
- MappedAudit,
14
- } from './types';
15
2
 
16
3
  // Auditor interface
17
4
  export type { Auditor } from './Auditor';
5
+ export type { DefaultAuditorConfig } from './DefaultAuditor';
18
6
 
19
7
  // Default implementation
20
8
  export { DefaultAuditor } from './DefaultAuditor';
21
- export type { DefaultAuditorConfig } from './DefaultAuditor';
22
-
23
9
  // Storage interface
24
10
  export type { AuditQueryOptions, AuditStorage } from './storage';
11
+ export type {
12
+ AuditActor,
13
+ AuditableAction,
14
+ AuditMetadata,
15
+ AuditOperation,
16
+ AuditOptions,
17
+ AuditRecord,
18
+ ExtractAuditorAction,
19
+ ExtractAuditPayload,
20
+ ExtractAuditType,
21
+ ExtractStorageAuditAction,
22
+ MappedAudit,
23
+ } from './types';