@geekmidas/audit 0.0.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.
- package/TECHNICAL.md +937 -0
- package/dist/Auditor-CZ8lkASv.d.cts +260 -0
- package/dist/Auditor-ii62d_pF.d.mts +260 -0
- package/dist/Auditor.cjs +0 -0
- package/dist/Auditor.d.cts +2 -0
- package/dist/Auditor.d.mts +2 -0
- package/dist/Auditor.mjs +0 -0
- package/dist/DefaultAuditor-1HDUGMub.cjs +124 -0
- package/dist/DefaultAuditor-1HDUGMub.cjs.map +1 -0
- package/dist/DefaultAuditor-B9Unin1g.d.mts +59 -0
- package/dist/DefaultAuditor-BAVnNmRh.mjs +96 -0
- package/dist/DefaultAuditor-BAVnNmRh.mjs.map +1 -0
- package/dist/DefaultAuditor-Dqc4UZA1.d.cts +59 -0
- package/dist/DefaultAuditor.cjs +3 -0
- package/dist/DefaultAuditor.d.cts +4 -0
- package/dist/DefaultAuditor.d.mts +4 -0
- package/dist/DefaultAuditor.mjs +3 -0
- package/dist/index.cjs +3 -0
- package/dist/index.d.cts +4 -0
- package/dist/index.d.mts +4 -0
- package/dist/index.mjs +3 -0
- package/dist/kysely.cjs +193 -0
- package/dist/kysely.cjs.map +1 -0
- package/dist/kysely.d.cts +165 -0
- package/dist/kysely.d.mts +165 -0
- package/dist/kysely.mjs +191 -0
- package/dist/kysely.mjs.map +1 -0
- package/dist/storage-BtFY7Rha.d.cts +102 -0
- package/dist/storage-DnAMfOA7.d.mts +102 -0
- package/dist/storage.cjs +0 -0
- package/dist/storage.d.cts +3 -0
- package/dist/storage.d.mts +3 -0
- package/dist/storage.mjs +0 -0
- package/dist/types.cjs +0 -0
- package/dist/types.d.cts +2 -0
- package/dist/types.d.mts +2 -0
- package/dist/types.mjs +0 -0
- package/package.json +34 -0
- package/src/Auditor.ts +157 -0
- package/src/DefaultAuditor.ts +141 -0
- package/src/__tests__/DefaultAuditor.spec.ts +420 -0
- package/src/__tests__/KyselyAuditStorage.integration.spec.ts +517 -0
- package/src/__tests__/KyselyAuditStorage.spec.ts +359 -0
- package/src/index.ts +23 -0
- package/src/kysely.ts +391 -0
- package/src/storage.ts +101 -0
- package/src/types.ts +147 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { AuditActor, AuditMetadata, AuditOptions, AuditRecord, AuditableAction, Auditor, ExtractAuditPayload, ExtractAuditType } from "./Auditor-ii62d_pF.mjs";
|
|
2
|
+
import { AuditStorage } from "./storage-DnAMfOA7.mjs";
|
|
3
|
+
|
|
4
|
+
//#region src/DefaultAuditor.d.ts
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Configuration for DefaultAuditor.
|
|
8
|
+
*/
|
|
9
|
+
interface DefaultAuditorConfig {
|
|
10
|
+
/** The actor performing audits (set at construction, immutable) */
|
|
11
|
+
actor: AuditActor;
|
|
12
|
+
/** Storage backend for persisting audits */
|
|
13
|
+
storage: AuditStorage;
|
|
14
|
+
/** Optional metadata to attach to all audits */
|
|
15
|
+
metadata?: AuditMetadata;
|
|
16
|
+
/** Optional custom ID generator (defaults to nanoid) */
|
|
17
|
+
generateId?: () => string;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Default implementation of the Auditor interface.
|
|
21
|
+
* Collects audit records in memory and flushes to storage.
|
|
22
|
+
*
|
|
23
|
+
* @template TAuditAction - Union of all allowed audit action types
|
|
24
|
+
* @template TTransaction - Transaction type (e.g., Kysely Transaction)
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```typescript
|
|
28
|
+
* const auditor = new DefaultAuditor<AppAuditAction>({
|
|
29
|
+
* actor: { id: 'user-123', type: 'user' },
|
|
30
|
+
* storage: auditStorage,
|
|
31
|
+
* metadata: { requestId: 'req-456', endpoint: '/users' },
|
|
32
|
+
* });
|
|
33
|
+
*
|
|
34
|
+
* auditor.audit('user.created', { userId: '789', email: 'test@example.com' });
|
|
35
|
+
*
|
|
36
|
+
* // Flush inside transaction
|
|
37
|
+
* await auditor.flush(trx);
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
declare class DefaultAuditor<TAuditAction extends AuditableAction<string, unknown> = AuditableAction<string, unknown>, TTransaction = unknown> implements Auditor<TAuditAction, TTransaction> {
|
|
41
|
+
readonly actor: AuditActor;
|
|
42
|
+
private readonly storage;
|
|
43
|
+
private metadata?;
|
|
44
|
+
private readonly generateId;
|
|
45
|
+
private records;
|
|
46
|
+
private transaction?;
|
|
47
|
+
constructor(config: DefaultAuditorConfig);
|
|
48
|
+
audit<TType extends ExtractAuditType<TAuditAction>>(type: TType, payload: ExtractAuditPayload<TAuditAction, TType>, options?: AuditOptions): void;
|
|
49
|
+
record(record: Omit<AuditRecord, 'id' | 'timestamp' | 'actor'>): void;
|
|
50
|
+
getRecords(): AuditRecord[];
|
|
51
|
+
flush(trx?: TTransaction): Promise<void>;
|
|
52
|
+
setTransaction(trx: TTransaction): void;
|
|
53
|
+
getTransaction(): TTransaction | undefined;
|
|
54
|
+
clear(): void;
|
|
55
|
+
addMetadata(metadata: AuditMetadata): void;
|
|
56
|
+
}
|
|
57
|
+
//#endregion
|
|
58
|
+
export { DefaultAuditor, DefaultAuditorConfig };
|
|
59
|
+
//# sourceMappingURL=DefaultAuditor-B9Unin1g.d.mts.map
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { nanoid } from "nanoid";
|
|
2
|
+
|
|
3
|
+
//#region src/DefaultAuditor.ts
|
|
4
|
+
/**
|
|
5
|
+
* Default implementation of the Auditor interface.
|
|
6
|
+
* Collects audit records in memory and flushes to storage.
|
|
7
|
+
*
|
|
8
|
+
* @template TAuditAction - Union of all allowed audit action types
|
|
9
|
+
* @template TTransaction - Transaction type (e.g., Kysely Transaction)
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```typescript
|
|
13
|
+
* const auditor = new DefaultAuditor<AppAuditAction>({
|
|
14
|
+
* actor: { id: 'user-123', type: 'user' },
|
|
15
|
+
* storage: auditStorage,
|
|
16
|
+
* metadata: { requestId: 'req-456', endpoint: '/users' },
|
|
17
|
+
* });
|
|
18
|
+
*
|
|
19
|
+
* auditor.audit('user.created', { userId: '789', email: 'test@example.com' });
|
|
20
|
+
*
|
|
21
|
+
* // Flush inside transaction
|
|
22
|
+
* await auditor.flush(trx);
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
var DefaultAuditor = class {
|
|
26
|
+
actor;
|
|
27
|
+
storage;
|
|
28
|
+
metadata;
|
|
29
|
+
generateId;
|
|
30
|
+
records = [];
|
|
31
|
+
transaction;
|
|
32
|
+
constructor(config) {
|
|
33
|
+
this.actor = config.actor;
|
|
34
|
+
this.storage = config.storage;
|
|
35
|
+
this.metadata = config.metadata;
|
|
36
|
+
this.generateId = config.generateId ?? (() => nanoid());
|
|
37
|
+
}
|
|
38
|
+
audit(type, payload, options) {
|
|
39
|
+
const record = {
|
|
40
|
+
id: this.generateId(),
|
|
41
|
+
type,
|
|
42
|
+
operation: options?.operation ?? "CUSTOM",
|
|
43
|
+
table: options?.table,
|
|
44
|
+
entityId: options?.entityId,
|
|
45
|
+
oldValues: options?.oldValues,
|
|
46
|
+
newValues: options?.newValues,
|
|
47
|
+
payload,
|
|
48
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
49
|
+
actor: this.actor,
|
|
50
|
+
metadata: this.metadata
|
|
51
|
+
};
|
|
52
|
+
this.records.push(record);
|
|
53
|
+
}
|
|
54
|
+
record(record) {
|
|
55
|
+
const fullRecord = {
|
|
56
|
+
...record,
|
|
57
|
+
id: this.generateId(),
|
|
58
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
59
|
+
actor: this.actor,
|
|
60
|
+
metadata: this.metadata ? {
|
|
61
|
+
...this.metadata,
|
|
62
|
+
...record.metadata
|
|
63
|
+
} : record.metadata
|
|
64
|
+
};
|
|
65
|
+
this.records.push(fullRecord);
|
|
66
|
+
}
|
|
67
|
+
getRecords() {
|
|
68
|
+
return [...this.records];
|
|
69
|
+
}
|
|
70
|
+
async flush(trx) {
|
|
71
|
+
if (this.records.length === 0) return;
|
|
72
|
+
const recordsToFlush = [...this.records];
|
|
73
|
+
this.records = [];
|
|
74
|
+
const transactionToUse = trx ?? this.transaction;
|
|
75
|
+
await this.storage.write(recordsToFlush, transactionToUse);
|
|
76
|
+
}
|
|
77
|
+
setTransaction(trx) {
|
|
78
|
+
this.transaction = trx;
|
|
79
|
+
}
|
|
80
|
+
getTransaction() {
|
|
81
|
+
return this.transaction;
|
|
82
|
+
}
|
|
83
|
+
clear() {
|
|
84
|
+
this.records = [];
|
|
85
|
+
}
|
|
86
|
+
addMetadata(metadata) {
|
|
87
|
+
this.metadata = this.metadata ? {
|
|
88
|
+
...this.metadata,
|
|
89
|
+
...metadata
|
|
90
|
+
} : metadata;
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
//#endregion
|
|
95
|
+
export { DefaultAuditor };
|
|
96
|
+
//# sourceMappingURL=DefaultAuditor-BAVnNmRh.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"DefaultAuditor-BAVnNmRh.mjs","names":["config: DefaultAuditorConfig","type: TType","payload: ExtractAuditPayload<TAuditAction, TType>","options?: AuditOptions","record: AuditRecord","record: Omit<AuditRecord, 'id' | 'timestamp' | 'actor'>","fullRecord: AuditRecord","trx?: TTransaction","trx: TTransaction","metadata: AuditMetadata"],"sources":["../src/DefaultAuditor.ts"],"sourcesContent":["import { nanoid } from 'nanoid';\nimport type { Auditor } from './Auditor';\nimport type { AuditStorage } from './storage';\nimport type {\n AuditableAction,\n AuditActor,\n AuditMetadata,\n AuditOptions,\n AuditRecord,\n ExtractAuditPayload,\n ExtractAuditType,\n} from './types';\n\n/**\n * Configuration for DefaultAuditor.\n */\nexport interface DefaultAuditorConfig {\n /** The actor performing audits (set at construction, immutable) */\n actor: AuditActor;\n /** Storage backend for persisting audits */\n storage: AuditStorage;\n /** Optional metadata to attach to all audits */\n metadata?: AuditMetadata;\n /** Optional custom ID generator (defaults to nanoid) */\n generateId?: () => string;\n}\n\n/**\n * Default implementation of the Auditor interface.\n * Collects audit records in memory and flushes to storage.\n *\n * @template TAuditAction - Union of all allowed audit action types\n * @template TTransaction - Transaction type (e.g., Kysely Transaction)\n *\n * @example\n * ```typescript\n * const auditor = new DefaultAuditor<AppAuditAction>({\n * actor: { id: 'user-123', type: 'user' },\n * storage: auditStorage,\n * metadata: { requestId: 'req-456', endpoint: '/users' },\n * });\n *\n * auditor.audit('user.created', { userId: '789', email: 'test@example.com' });\n *\n * // Flush inside transaction\n * await auditor.flush(trx);\n * ```\n */\nexport class DefaultAuditor<\n TAuditAction extends AuditableAction<string, unknown> = AuditableAction<\n string,\n unknown\n >,\n TTransaction = unknown,\n> implements Auditor<TAuditAction, TTransaction>\n{\n readonly actor: AuditActor;\n private readonly storage: AuditStorage;\n private metadata?: AuditMetadata;\n private readonly generateId: () => string;\n private records: AuditRecord[] = [];\n private transaction?: TTransaction;\n\n constructor(config: DefaultAuditorConfig) {\n this.actor = config.actor;\n this.storage = config.storage;\n this.metadata = config.metadata;\n this.generateId = config.generateId ?? (() => nanoid());\n }\n\n audit<TType extends ExtractAuditType<TAuditAction>>(\n type: TType,\n payload: ExtractAuditPayload<TAuditAction, TType>,\n options?: AuditOptions,\n ): void {\n const record: AuditRecord = {\n id: this.generateId(),\n type,\n operation: options?.operation ?? 'CUSTOM',\n table: options?.table,\n entityId: options?.entityId,\n oldValues: options?.oldValues,\n newValues: options?.newValues,\n payload,\n timestamp: new Date(),\n actor: this.actor,\n metadata: this.metadata,\n };\n\n this.records.push(record);\n }\n\n record(record: Omit<AuditRecord, 'id' | 'timestamp' | 'actor'>): void {\n const fullRecord: AuditRecord = {\n ...record,\n id: this.generateId(),\n timestamp: new Date(),\n actor: this.actor,\n metadata: this.metadata\n ? { ...this.metadata, ...record.metadata }\n : record.metadata,\n };\n\n this.records.push(fullRecord);\n }\n\n getRecords(): AuditRecord[] {\n return [...this.records];\n }\n\n async flush(trx?: TTransaction): Promise<void> {\n if (this.records.length === 0) {\n return;\n }\n\n const recordsToFlush = [...this.records];\n this.records = [];\n\n // Use explicitly passed transaction, or fall back to stored transaction\n const transactionToUse = trx ?? this.transaction;\n await this.storage.write(recordsToFlush, transactionToUse);\n }\n\n setTransaction(trx: TTransaction): void {\n this.transaction = trx;\n }\n\n getTransaction(): TTransaction | undefined {\n return this.transaction;\n }\n\n clear(): void {\n this.records = [];\n }\n\n addMetadata(metadata: AuditMetadata): void {\n this.metadata = this.metadata\n ? { ...this.metadata, ...metadata }\n : metadata;\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AAgDA,IAAa,iBAAb,MAOA;CACE,AAAS;CACT,AAAiB;CACjB,AAAQ;CACR,AAAiB;CACjB,AAAQ,UAAyB,CAAE;CACnC,AAAQ;CAER,YAAYA,QAA8B;AACxC,OAAK,QAAQ,OAAO;AACpB,OAAK,UAAU,OAAO;AACtB,OAAK,WAAW,OAAO;AACvB,OAAK,aAAa,OAAO,eAAe,MAAM,QAAQ;CACvD;CAED,MACEC,MACAC,SACAC,SACM;EACN,MAAMC,SAAsB;GAC1B,IAAI,KAAK,YAAY;GACrB;GACA,WAAW,SAAS,aAAa;GACjC,OAAO,SAAS;GAChB,UAAU,SAAS;GACnB,WAAW,SAAS;GACpB,WAAW,SAAS;GACpB;GACA,2BAAW,IAAI;GACf,OAAO,KAAK;GACZ,UAAU,KAAK;EAChB;AAED,OAAK,QAAQ,KAAK,OAAO;CAC1B;CAED,OAAOC,QAA+D;EACpE,MAAMC,aAA0B;GAC9B,GAAG;GACH,IAAI,KAAK,YAAY;GACrB,2BAAW,IAAI;GACf,OAAO,KAAK;GACZ,UAAU,KAAK,WACX;IAAE,GAAG,KAAK;IAAU,GAAG,OAAO;GAAU,IACxC,OAAO;EACZ;AAED,OAAK,QAAQ,KAAK,WAAW;CAC9B;CAED,aAA4B;AAC1B,SAAO,CAAC,GAAG,KAAK,OAAQ;CACzB;CAED,MAAM,MAAMC,KAAmC;AAC7C,MAAI,KAAK,QAAQ,WAAW,EAC1B;EAGF,MAAM,iBAAiB,CAAC,GAAG,KAAK,OAAQ;AACxC,OAAK,UAAU,CAAE;EAGjB,MAAM,mBAAmB,OAAO,KAAK;AACrC,QAAM,KAAK,QAAQ,MAAM,gBAAgB,iBAAiB;CAC3D;CAED,eAAeC,KAAyB;AACtC,OAAK,cAAc;CACpB;CAED,iBAA2C;AACzC,SAAO,KAAK;CACb;CAED,QAAc;AACZ,OAAK,UAAU,CAAE;CAClB;CAED,YAAYC,UAA+B;AACzC,OAAK,WAAW,KAAK,WACjB;GAAE,GAAG,KAAK;GAAU,GAAG;EAAU,IACjC;CACL;AACF"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { AuditActor, AuditMetadata, AuditOptions, AuditRecord, AuditableAction, Auditor, ExtractAuditPayload, ExtractAuditType } from "./Auditor-CZ8lkASv.cjs";
|
|
2
|
+
import { AuditStorage } from "./storage-BtFY7Rha.cjs";
|
|
3
|
+
|
|
4
|
+
//#region src/DefaultAuditor.d.ts
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Configuration for DefaultAuditor.
|
|
8
|
+
*/
|
|
9
|
+
interface DefaultAuditorConfig {
|
|
10
|
+
/** The actor performing audits (set at construction, immutable) */
|
|
11
|
+
actor: AuditActor;
|
|
12
|
+
/** Storage backend for persisting audits */
|
|
13
|
+
storage: AuditStorage;
|
|
14
|
+
/** Optional metadata to attach to all audits */
|
|
15
|
+
metadata?: AuditMetadata;
|
|
16
|
+
/** Optional custom ID generator (defaults to nanoid) */
|
|
17
|
+
generateId?: () => string;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Default implementation of the Auditor interface.
|
|
21
|
+
* Collects audit records in memory and flushes to storage.
|
|
22
|
+
*
|
|
23
|
+
* @template TAuditAction - Union of all allowed audit action types
|
|
24
|
+
* @template TTransaction - Transaction type (e.g., Kysely Transaction)
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```typescript
|
|
28
|
+
* const auditor = new DefaultAuditor<AppAuditAction>({
|
|
29
|
+
* actor: { id: 'user-123', type: 'user' },
|
|
30
|
+
* storage: auditStorage,
|
|
31
|
+
* metadata: { requestId: 'req-456', endpoint: '/users' },
|
|
32
|
+
* });
|
|
33
|
+
*
|
|
34
|
+
* auditor.audit('user.created', { userId: '789', email: 'test@example.com' });
|
|
35
|
+
*
|
|
36
|
+
* // Flush inside transaction
|
|
37
|
+
* await auditor.flush(trx);
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
declare class DefaultAuditor<TAuditAction extends AuditableAction<string, unknown> = AuditableAction<string, unknown>, TTransaction = unknown> implements Auditor<TAuditAction, TTransaction> {
|
|
41
|
+
readonly actor: AuditActor;
|
|
42
|
+
private readonly storage;
|
|
43
|
+
private metadata?;
|
|
44
|
+
private readonly generateId;
|
|
45
|
+
private records;
|
|
46
|
+
private transaction?;
|
|
47
|
+
constructor(config: DefaultAuditorConfig);
|
|
48
|
+
audit<TType extends ExtractAuditType<TAuditAction>>(type: TType, payload: ExtractAuditPayload<TAuditAction, TType>, options?: AuditOptions): void;
|
|
49
|
+
record(record: Omit<AuditRecord, 'id' | 'timestamp' | 'actor'>): void;
|
|
50
|
+
getRecords(): AuditRecord[];
|
|
51
|
+
flush(trx?: TTransaction): Promise<void>;
|
|
52
|
+
setTransaction(trx: TTransaction): void;
|
|
53
|
+
getTransaction(): TTransaction | undefined;
|
|
54
|
+
clear(): void;
|
|
55
|
+
addMetadata(metadata: AuditMetadata): void;
|
|
56
|
+
}
|
|
57
|
+
//#endregion
|
|
58
|
+
export { DefaultAuditor, DefaultAuditorConfig };
|
|
59
|
+
//# sourceMappingURL=DefaultAuditor-Dqc4UZA1.d.cts.map
|
package/dist/index.cjs
ADDED
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { AuditActor, AuditMetadata, AuditOperation, AuditOptions, AuditRecord, AuditableAction, Auditor, ExtractAuditPayload, ExtractAuditType, ExtractAuditorAction, MappedAudit } from "./Auditor-CZ8lkASv.cjs";
|
|
2
|
+
import { AuditQueryOptions, AuditStorage } from "./storage-BtFY7Rha.cjs";
|
|
3
|
+
import { DefaultAuditor, DefaultAuditorConfig } from "./DefaultAuditor-Dqc4UZA1.cjs";
|
|
4
|
+
export { AuditActor, AuditMetadata, AuditOperation, AuditOptions, AuditQueryOptions, AuditRecord, AuditStorage, AuditableAction, Auditor, DefaultAuditor, DefaultAuditorConfig, ExtractAuditPayload, ExtractAuditType, ExtractAuditorAction, MappedAudit };
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { AuditActor, AuditMetadata, AuditOperation, AuditOptions, AuditRecord, AuditableAction, Auditor, ExtractAuditPayload, ExtractAuditType, ExtractAuditorAction, MappedAudit } from "./Auditor-ii62d_pF.mjs";
|
|
2
|
+
import { AuditQueryOptions, AuditStorage } from "./storage-DnAMfOA7.mjs";
|
|
3
|
+
import { DefaultAuditor, DefaultAuditorConfig } from "./DefaultAuditor-B9Unin1g.mjs";
|
|
4
|
+
export { AuditActor, AuditMetadata, AuditOperation, AuditOptions, AuditQueryOptions, AuditRecord, AuditStorage, AuditableAction, Auditor, DefaultAuditor, DefaultAuditorConfig, ExtractAuditPayload, ExtractAuditType, ExtractAuditorAction, MappedAudit };
|
package/dist/index.mjs
ADDED
package/dist/kysely.cjs
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
|
|
2
|
+
//#region src/kysely.ts
|
|
3
|
+
/**
|
|
4
|
+
* Execute a callback within a database transaction with automatic audit handling.
|
|
5
|
+
*
|
|
6
|
+
* This wrapper ensures that:
|
|
7
|
+
* 1. The transaction is automatically registered with the auditor
|
|
8
|
+
* 2. Manual audits (via `auditor.audit()`) are flushed BEFORE the transaction commits
|
|
9
|
+
* 3. If audit flush fails, the entire transaction rolls back
|
|
10
|
+
* 4. If the callback fails, audits are NOT written (atomic consistency)
|
|
11
|
+
*
|
|
12
|
+
* **Note:** Declarative audits (defined via `.audit([...])` on the endpoint builder)
|
|
13
|
+
* are processed AFTER the handler returns, so they run outside this transaction.
|
|
14
|
+
* If you need all audits to be atomic with your database operations, use manual
|
|
15
|
+
* audits via `auditor.audit()` inside this wrapper.
|
|
16
|
+
*
|
|
17
|
+
* @param db - Database connection (Kysely, Transaction, or ControlledTransaction)
|
|
18
|
+
* @param auditor - Auditor instance that will receive the transaction
|
|
19
|
+
* @param cb - Callback to execute within the transaction
|
|
20
|
+
* @param settings - Optional transaction settings (isolation level)
|
|
21
|
+
* @returns The result of the callback
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```typescript
|
|
25
|
+
* import { withAuditableTransaction } from '@geekmidas/audit/kysely';
|
|
26
|
+
*
|
|
27
|
+
* const result = await withAuditableTransaction(
|
|
28
|
+
* services.database,
|
|
29
|
+
* auditor,
|
|
30
|
+
* async (trx) => {
|
|
31
|
+
* const user = await trx
|
|
32
|
+
* .insertInto('users')
|
|
33
|
+
* .values(data)
|
|
34
|
+
* .returningAll()
|
|
35
|
+
* .executeTakeFirstOrThrow();
|
|
36
|
+
*
|
|
37
|
+
* // Manual audits are atomic with the transaction
|
|
38
|
+
* auditor.audit('user.created', { userId: user.id, email: user.email });
|
|
39
|
+
*
|
|
40
|
+
* return user;
|
|
41
|
+
* },
|
|
42
|
+
* );
|
|
43
|
+
* // Audits are automatically flushed inside the transaction before commit
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
async function withAuditableTransaction(db, auditor, cb, settings) {
|
|
47
|
+
const execute = async (trx) => {
|
|
48
|
+
auditor.setTransaction(trx);
|
|
49
|
+
const result = await cb(trx);
|
|
50
|
+
await auditor.flush(trx);
|
|
51
|
+
return result;
|
|
52
|
+
};
|
|
53
|
+
if (db.isTransaction) return execute(db);
|
|
54
|
+
const builder = db.transaction();
|
|
55
|
+
if (settings?.isolationLevel) return builder.setIsolationLevel(settings.isolationLevel).execute(execute);
|
|
56
|
+
return builder.execute(execute);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Kysely-based audit storage implementation.
|
|
60
|
+
* Stores audit records in a database table using Kysely.
|
|
61
|
+
*
|
|
62
|
+
* @template DB - Your Kysely database schema
|
|
63
|
+
*
|
|
64
|
+
* @example
|
|
65
|
+
* ```typescript
|
|
66
|
+
* interface Database {
|
|
67
|
+
* audit_logs: AuditLogTable;
|
|
68
|
+
* }
|
|
69
|
+
*
|
|
70
|
+
* const storage = new KyselyAuditStorage({
|
|
71
|
+
* db: kyselyDb,
|
|
72
|
+
* tableName: 'audit_logs',
|
|
73
|
+
* });
|
|
74
|
+
*
|
|
75
|
+
* const auditor = new DefaultAuditor({
|
|
76
|
+
* actor: { id: 'user-123', type: 'user' },
|
|
77
|
+
* storage,
|
|
78
|
+
* });
|
|
79
|
+
* ```
|
|
80
|
+
*/
|
|
81
|
+
var KyselyAuditStorage = class {
|
|
82
|
+
db;
|
|
83
|
+
tableName;
|
|
84
|
+
constructor(config) {
|
|
85
|
+
this.db = config.db;
|
|
86
|
+
this.tableName = config.tableName;
|
|
87
|
+
}
|
|
88
|
+
async write(records, trx) {
|
|
89
|
+
if (records.length === 0) return;
|
|
90
|
+
const db = trx ?? this.db;
|
|
91
|
+
const rows = records.map((record) => this.toRow(record));
|
|
92
|
+
await db.insertInto(this.tableName).values(rows).execute();
|
|
93
|
+
}
|
|
94
|
+
async query(options) {
|
|
95
|
+
let query = this.db.selectFrom(this.tableName).selectAll();
|
|
96
|
+
query = this.applyFilters(query, options);
|
|
97
|
+
const orderBy = options.orderBy ?? "timestamp";
|
|
98
|
+
const orderDirection = options.orderDirection ?? "desc";
|
|
99
|
+
query = query.orderBy(orderBy === "timestamp" ? "timestamp" : "type", orderDirection);
|
|
100
|
+
if (options.limit !== void 0) query = query.limit(options.limit);
|
|
101
|
+
if (options.offset !== void 0) query = query.offset(options.offset);
|
|
102
|
+
const rows = await query.execute();
|
|
103
|
+
return rows.map((row) => this.fromRow(row));
|
|
104
|
+
}
|
|
105
|
+
async count(options) {
|
|
106
|
+
let query = this.db.selectFrom(this.tableName).select((eb) => eb.fn.count("id").as("count"));
|
|
107
|
+
query = this.applyFilters(query, options);
|
|
108
|
+
const result = await query.executeTakeFirst();
|
|
109
|
+
return Number(result?.count ?? 0);
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Get the Kysely database instance for transactional operations.
|
|
113
|
+
* Used by endpoint adaptors to automatically wrap handlers in transactions.
|
|
114
|
+
*/
|
|
115
|
+
getDatabase() {
|
|
116
|
+
return this.db;
|
|
117
|
+
}
|
|
118
|
+
applyFilters(query, options) {
|
|
119
|
+
if (options.type !== void 0) if (Array.isArray(options.type)) query = query.where("type", "in", options.type);
|
|
120
|
+
else query = query.where("type", "=", options.type);
|
|
121
|
+
if (options.entityId !== void 0) {
|
|
122
|
+
const entityId = typeof options.entityId === "string" ? options.entityId : JSON.stringify(options.entityId);
|
|
123
|
+
query = query.where("entityId", "=", entityId);
|
|
124
|
+
}
|
|
125
|
+
if (options.table !== void 0) query = query.where("table", "=", options.table);
|
|
126
|
+
if (options.actorId !== void 0) query = query.where("actorId", "=", options.actorId);
|
|
127
|
+
if (options.from !== void 0) query = query.where("timestamp", ">=", options.from);
|
|
128
|
+
if (options.to !== void 0) query = query.where("timestamp", "<=", options.to);
|
|
129
|
+
return query;
|
|
130
|
+
}
|
|
131
|
+
toRow(record) {
|
|
132
|
+
return {
|
|
133
|
+
id: record.id,
|
|
134
|
+
type: record.type,
|
|
135
|
+
operation: record.operation,
|
|
136
|
+
table: record.table ?? null,
|
|
137
|
+
entityId: record.entityId === void 0 ? null : typeof record.entityId === "string" ? record.entityId : JSON.stringify(record.entityId),
|
|
138
|
+
oldValues: record.oldValues !== void 0 ? JSON.stringify(record.oldValues) : null,
|
|
139
|
+
newValues: record.newValues !== void 0 ? JSON.stringify(record.newValues) : null,
|
|
140
|
+
payload: record.payload !== void 0 ? JSON.stringify(record.payload) : null,
|
|
141
|
+
timestamp: record.timestamp,
|
|
142
|
+
actorId: record.actor?.id ?? null,
|
|
143
|
+
actorType: record.actor?.type ?? null,
|
|
144
|
+
actorData: record.actor !== void 0 ? JSON.stringify(this.getActorData(record.actor)) : null,
|
|
145
|
+
metadata: record.metadata !== void 0 ? JSON.stringify(record.metadata) : null
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
fromRow(row) {
|
|
149
|
+
const actor = row.actorId !== null || row.actorType !== null ? {
|
|
150
|
+
id: row.actorId ?? void 0,
|
|
151
|
+
type: row.actorType ?? void 0,
|
|
152
|
+
...row.actorData ? this.parseJson(row.actorData) : {}
|
|
153
|
+
} : void 0;
|
|
154
|
+
return {
|
|
155
|
+
id: row.id,
|
|
156
|
+
type: row.type,
|
|
157
|
+
operation: row.operation,
|
|
158
|
+
table: row.table ?? void 0,
|
|
159
|
+
entityId: row.entityId ? this.parseEntityId(row.entityId) : void 0,
|
|
160
|
+
oldValues: row.oldValues ? this.parseJson(row.oldValues) : void 0,
|
|
161
|
+
newValues: row.newValues ? this.parseJson(row.newValues) : void 0,
|
|
162
|
+
payload: row.payload ? this.parseJson(row.payload) : void 0,
|
|
163
|
+
timestamp: row.timestamp,
|
|
164
|
+
actor,
|
|
165
|
+
metadata: row.metadata ? this.parseJson(row.metadata) : void 0
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Parse a JSON value that may already be parsed (e.g., from jsonb columns).
|
|
170
|
+
*/
|
|
171
|
+
parseJson(value) {
|
|
172
|
+
if (typeof value === "object") return value;
|
|
173
|
+
return JSON.parse(value);
|
|
174
|
+
}
|
|
175
|
+
getActorData(actor) {
|
|
176
|
+
const { id, type,...rest } = actor;
|
|
177
|
+
return rest;
|
|
178
|
+
}
|
|
179
|
+
parseEntityId(entityId) {
|
|
180
|
+
try {
|
|
181
|
+
const parsed = JSON.parse(entityId);
|
|
182
|
+
if (typeof parsed === "object" && parsed !== null) return parsed;
|
|
183
|
+
return entityId;
|
|
184
|
+
} catch {
|
|
185
|
+
return entityId;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
//#endregion
|
|
191
|
+
exports.KyselyAuditStorage = KyselyAuditStorage;
|
|
192
|
+
exports.withAuditableTransaction = withAuditableTransaction;
|
|
193
|
+
//# sourceMappingURL=kysely.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"kysely.cjs","names":["db: DatabaseConnection<DB>","auditor: TransactionAwareAuditor<Transaction<DB>>","cb: (trx: Transaction<DB>) => Promise<T>","settings?: TransactionSettings","trx: Transaction<DB>","config: KyselyAuditStorageConfig<DB>","records: AuditRecord[]","trx?: unknown","options: AuditQueryOptions","row: AuditLogTable","options: Omit<AuditQueryOptions, 'limit' | 'offset'>","eb: any","query: any","record: AuditRecord","value: string | object","actor: NonNullable<AuditRecord['actor']>","entityId: string"],"sources":["../src/kysely.ts"],"sourcesContent":["import type {\n ControlledTransaction,\n IsolationLevel,\n Kysely,\n Transaction,\n} from 'kysely';\nimport type { AuditQueryOptions, AuditStorage } from './storage';\nimport type { AuditRecord } from './types';\n\n/**\n * Minimal interface for transaction-aware audit flushing.\n * Use this when you need to flush audits within a database transaction.\n *\n * @template TTransaction - Transaction type (e.g., Kysely Transaction)\n *\n * @example\n * ```typescript\n * import { withAuditableTransaction } from '@geekmidas/audit/kysely';\n * import type { TransactionAwareAuditor } from '@geekmidas/audit/kysely';\n *\n * const result = await withAuditableTransaction(\n * db,\n * auditor as TransactionAwareAuditor<Transaction<DB>>,\n * async (trx) => {\n * // Your transactional operations\n * return result;\n * },\n * );\n * ```\n */\nexport interface TransactionAwareAuditor<TTransaction = unknown> {\n /** Register the transaction with the auditor for use during flush */\n setTransaction(trx: TTransaction): void;\n /** Flush all pending audits, optionally within a transaction */\n flush(trx?: TTransaction): Promise<void>;\n}\n\nexport interface TransactionSettings {\n isolationLevel?: IsolationLevel;\n}\n\nexport type DatabaseConnection<T> =\n | ControlledTransaction<T>\n | Kysely<T>\n | Transaction<T>;\n\n/**\n * Execute a callback within a database transaction with automatic audit handling.\n *\n * This wrapper ensures that:\n * 1. The transaction is automatically registered with the auditor\n * 2. Manual audits (via `auditor.audit()`) are flushed BEFORE the transaction commits\n * 3. If audit flush fails, the entire transaction rolls back\n * 4. If the callback fails, audits are NOT written (atomic consistency)\n *\n * **Note:** Declarative audits (defined via `.audit([...])` on the endpoint builder)\n * are processed AFTER the handler returns, so they run outside this transaction.\n * If you need all audits to be atomic with your database operations, use manual\n * audits via `auditor.audit()` inside this wrapper.\n *\n * @param db - Database connection (Kysely, Transaction, or ControlledTransaction)\n * @param auditor - Auditor instance that will receive the transaction\n * @param cb - Callback to execute within the transaction\n * @param settings - Optional transaction settings (isolation level)\n * @returns The result of the callback\n *\n * @example\n * ```typescript\n * import { withAuditableTransaction } from '@geekmidas/audit/kysely';\n *\n * const result = await withAuditableTransaction(\n * services.database,\n * auditor,\n * async (trx) => {\n * const user = await trx\n * .insertInto('users')\n * .values(data)\n * .returningAll()\n * .executeTakeFirstOrThrow();\n *\n * // Manual audits are atomic with the transaction\n * auditor.audit('user.created', { userId: user.id, email: user.email });\n *\n * return user;\n * },\n * );\n * // Audits are automatically flushed inside the transaction before commit\n * ```\n */\nexport async function withAuditableTransaction<DB, T>(\n db: DatabaseConnection<DB>,\n auditor: TransactionAwareAuditor<Transaction<DB>>,\n cb: (trx: Transaction<DB>) => Promise<T>,\n settings?: TransactionSettings,\n): Promise<T> {\n const execute = async (trx: Transaction<DB>): Promise<T> => {\n // Register transaction with auditor\n auditor.setTransaction(trx);\n\n // Execute the callback\n const result = await cb(trx);\n\n // Flush audits BEFORE transaction commits\n // If this fails, the transaction will roll back\n await auditor.flush(trx);\n\n return result;\n };\n\n // If already in a transaction, just run with it\n if (db.isTransaction) {\n return execute(db as Transaction<DB>);\n }\n\n const builder = db.transaction();\n\n if (settings?.isolationLevel) {\n return builder.setIsolationLevel(settings.isolationLevel).execute(execute);\n }\n\n return builder.execute(execute);\n}\n\n/**\n * Database table interface for audit records.\n * Use this to define your audit_logs table in your Kysely database schema.\n *\n * @example\n * ```typescript\n * interface Database {\n * audit_logs: AuditLogTable;\n * // ... other tables\n * }\n * ```\n */\nexport interface AuditLogTable {\n id: string;\n type: string;\n operation: string;\n table: string | null;\n entityId: string | null;\n oldValues: string | null;\n newValues: string | null;\n payload: string | null;\n timestamp: Date;\n actorId: string | null;\n actorType: string | null;\n actorData: string | null;\n metadata: string | null;\n}\n\n/**\n * Configuration for KyselyAuditStorage.\n */\nexport interface KyselyAuditStorageConfig<DB> {\n /** Kysely database instance */\n db: Kysely<DB>;\n /** Table name for audit logs (must be a key in DB that extends AuditLogTable) */\n tableName: keyof DB & string;\n}\n\n/**\n * Kysely-based audit storage implementation.\n * Stores audit records in a database table using Kysely.\n *\n * @template DB - Your Kysely database schema\n *\n * @example\n * ```typescript\n * interface Database {\n * audit_logs: AuditLogTable;\n * }\n *\n * const storage = new KyselyAuditStorage({\n * db: kyselyDb,\n * tableName: 'audit_logs',\n * });\n *\n * const auditor = new DefaultAuditor({\n * actor: { id: 'user-123', type: 'user' },\n * storage,\n * });\n * ```\n */\nexport class KyselyAuditStorage<DB> implements AuditStorage {\n private readonly db: Kysely<DB>;\n private readonly tableName: keyof DB & string;\n\n constructor(config: KyselyAuditStorageConfig<DB>) {\n this.db = config.db;\n this.tableName = config.tableName;\n }\n\n async write(records: AuditRecord[], trx?: unknown): Promise<void> {\n if (records.length === 0) {\n return;\n }\n\n const db = (trx as Transaction<DB>) ?? this.db;\n const rows = records.map((record) => this.toRow(record));\n\n await (db as any)\n .insertInto(this.tableName)\n .values(rows)\n .execute();\n }\n\n async query(options: AuditQueryOptions): Promise<AuditRecord[]> {\n let query = (this.db as any)\n .selectFrom(this.tableName)\n .selectAll();\n\n query = this.applyFilters(query, options);\n\n // Ordering\n const orderBy = options.orderBy ?? 'timestamp';\n const orderDirection = options.orderDirection ?? 'desc';\n query = query.orderBy(\n orderBy === 'timestamp' ? 'timestamp' : 'type',\n orderDirection,\n );\n\n // Pagination\n if (options.limit !== undefined) {\n query = query.limit(options.limit);\n }\n if (options.offset !== undefined) {\n query = query.offset(options.offset);\n }\n\n const rows = await query.execute();\n return rows.map((row: AuditLogTable) => this.fromRow(row));\n }\n\n async count(\n options: Omit<AuditQueryOptions, 'limit' | 'offset'>,\n ): Promise<number> {\n let query = (this.db as any)\n .selectFrom(this.tableName)\n .select((eb: any) => eb.fn.count('id').as('count'));\n\n query = this.applyFilters(query, options);\n\n const result = await query.executeTakeFirst();\n return Number(result?.count ?? 0);\n }\n\n /**\n * Get the Kysely database instance for transactional operations.\n * Used by endpoint adaptors to automatically wrap handlers in transactions.\n */\n getDatabase(): Kysely<DB> {\n return this.db;\n }\n\n private applyFilters(query: any, options: AuditQueryOptions): any {\n // Type filter\n if (options.type !== undefined) {\n if (Array.isArray(options.type)) {\n query = query.where('type', 'in', options.type);\n } else {\n query = query.where('type', '=', options.type);\n }\n }\n\n // Entity ID filter\n if (options.entityId !== undefined) {\n const entityId =\n typeof options.entityId === 'string'\n ? options.entityId\n : JSON.stringify(options.entityId);\n query = query.where('entityId', '=', entityId);\n }\n\n // Table filter\n if (options.table !== undefined) {\n query = query.where('table', '=', options.table);\n }\n\n // Actor ID filter\n if (options.actorId !== undefined) {\n query = query.where('actorId', '=', options.actorId);\n }\n\n // Date range filters\n if (options.from !== undefined) {\n query = query.where('timestamp', '>=', options.from);\n }\n if (options.to !== undefined) {\n query = query.where('timestamp', '<=', options.to);\n }\n\n return query;\n }\n\n private toRow(record: AuditRecord): AuditLogTable {\n return {\n id: record.id,\n type: record.type,\n operation: record.operation,\n table: record.table ?? null,\n entityId:\n record.entityId === undefined\n ? null\n : typeof record.entityId === 'string'\n ? record.entityId\n : JSON.stringify(record.entityId),\n oldValues:\n record.oldValues !== undefined\n ? JSON.stringify(record.oldValues)\n : null,\n newValues:\n record.newValues !== undefined\n ? JSON.stringify(record.newValues)\n : null,\n payload:\n record.payload !== undefined ? JSON.stringify(record.payload) : null,\n timestamp: record.timestamp,\n actorId: record.actor?.id ?? null,\n actorType: record.actor?.type ?? null,\n actorData:\n record.actor !== undefined\n ? JSON.stringify(this.getActorData(record.actor))\n : null,\n metadata:\n record.metadata !== undefined ? JSON.stringify(record.metadata) : null,\n };\n }\n\n private fromRow(row: AuditLogTable): AuditRecord {\n const actor =\n row.actorId !== null || row.actorType !== null\n ? {\n id: row.actorId ?? undefined,\n type: row.actorType ?? undefined,\n ...(row.actorData ? this.parseJson(row.actorData) : {}),\n }\n : undefined;\n\n return {\n id: row.id,\n type: row.type,\n operation: row.operation as AuditRecord['operation'],\n table: row.table ?? undefined,\n entityId: row.entityId\n ? this.parseEntityId(row.entityId)\n : undefined,\n oldValues: row.oldValues\n ? this.parseJson(row.oldValues)\n : undefined,\n newValues: row.newValues\n ? this.parseJson(row.newValues)\n : undefined,\n payload: row.payload ? this.parseJson(row.payload) : undefined,\n timestamp: row.timestamp,\n actor,\n metadata: row.metadata ? this.parseJson(row.metadata) : undefined,\n };\n }\n\n /**\n * Parse a JSON value that may already be parsed (e.g., from jsonb columns).\n */\n private parseJson(value: string | object): Record<string, unknown> {\n if (typeof value === 'object') {\n return value as Record<string, unknown>;\n }\n return JSON.parse(value);\n }\n\n private getActorData(\n actor: NonNullable<AuditRecord['actor']>,\n ): Record<string, unknown> {\n const { id, type, ...rest } = actor;\n return rest;\n }\n\n private parseEntityId(\n entityId: string,\n ): string | Record<string, unknown> {\n try {\n const parsed = JSON.parse(entityId);\n if (typeof parsed === 'object' && parsed !== null) {\n return parsed;\n }\n return entityId;\n } catch {\n return entityId;\n }\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAyFA,eAAsB,yBACpBA,IACAC,SACAC,IACAC,UACY;CACZ,MAAM,UAAU,OAAOC,QAAqC;AAE1D,UAAQ,eAAe,IAAI;EAG3B,MAAM,SAAS,MAAM,GAAG,IAAI;AAI5B,QAAM,QAAQ,MAAM,IAAI;AAExB,SAAO;CACR;AAGD,KAAI,GAAG,cACL,QAAO,QAAQ,GAAsB;CAGvC,MAAM,UAAU,GAAG,aAAa;AAEhC,KAAI,UAAU,eACZ,QAAO,QAAQ,kBAAkB,SAAS,eAAe,CAAC,QAAQ,QAAQ;AAG5E,QAAO,QAAQ,QAAQ,QAAQ;AAChC;;;;;;;;;;;;;;;;;;;;;;;;AA+DD,IAAa,qBAAb,MAA4D;CAC1D,AAAiB;CACjB,AAAiB;CAEjB,YAAYC,QAAsC;AAChD,OAAK,KAAK,OAAO;AACjB,OAAK,YAAY,OAAO;CACzB;CAED,MAAM,MAAMC,SAAwBC,KAA8B;AAChE,MAAI,QAAQ,WAAW,EACrB;EAGF,MAAM,KAAM,OAA2B,KAAK;EAC5C,MAAM,OAAO,QAAQ,IAAI,CAAC,WAAW,KAAK,MAAM,OAAO,CAAC;AAExD,QAAM,AAAC,GACJ,WAAW,KAAK,UAAU,CAC1B,OAAO,KAAK,CACZ,SAAS;CACb;CAED,MAAM,MAAMC,SAAoD;EAC9D,IAAI,QAAQ,AAAC,KAAK,GACf,WAAW,KAAK,UAAU,CAC1B,WAAW;AAEd,UAAQ,KAAK,aAAa,OAAO,QAAQ;EAGzC,MAAM,UAAU,QAAQ,WAAW;EACnC,MAAM,iBAAiB,QAAQ,kBAAkB;AACjD,UAAQ,MAAM,QACZ,YAAY,cAAc,cAAc,QACxC,eACD;AAGD,MAAI,QAAQ,iBACV,SAAQ,MAAM,MAAM,QAAQ,MAAM;AAEpC,MAAI,QAAQ,kBACV,SAAQ,MAAM,OAAO,QAAQ,OAAO;EAGtC,MAAM,OAAO,MAAM,MAAM,SAAS;AAClC,SAAO,KAAK,IAAI,CAACC,QAAuB,KAAK,QAAQ,IAAI,CAAC;CAC3D;CAED,MAAM,MACJC,SACiB;EACjB,IAAI,QAAQ,AAAC,KAAK,GACf,WAAW,KAAK,UAAU,CAC1B,OAAO,CAACC,OAAY,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,QAAQ,CAAC;AAErD,UAAQ,KAAK,aAAa,OAAO,QAAQ;EAEzC,MAAM,SAAS,MAAM,MAAM,kBAAkB;AAC7C,SAAO,OAAO,QAAQ,SAAS,EAAE;CAClC;;;;;CAMD,cAA0B;AACxB,SAAO,KAAK;CACb;CAED,AAAQ,aAAaC,OAAYJ,SAAiC;AAEhE,MAAI,QAAQ,gBACV,KAAI,MAAM,QAAQ,QAAQ,KAAK,CAC7B,SAAQ,MAAM,MAAM,QAAQ,MAAM,QAAQ,KAAK;MAE/C,SAAQ,MAAM,MAAM,QAAQ,KAAK,QAAQ,KAAK;AAKlD,MAAI,QAAQ,qBAAwB;GAClC,MAAM,kBACG,QAAQ,aAAa,WACxB,QAAQ,WACR,KAAK,UAAU,QAAQ,SAAS;AACtC,WAAQ,MAAM,MAAM,YAAY,KAAK,SAAS;EAC/C;AAGD,MAAI,QAAQ,iBACV,SAAQ,MAAM,MAAM,SAAS,KAAK,QAAQ,MAAM;AAIlD,MAAI,QAAQ,mBACV,SAAQ,MAAM,MAAM,WAAW,KAAK,QAAQ,QAAQ;AAItD,MAAI,QAAQ,gBACV,SAAQ,MAAM,MAAM,aAAa,MAAM,QAAQ,KAAK;AAEtD,MAAI,QAAQ,cACV,SAAQ,MAAM,MAAM,aAAa,MAAM,QAAQ,GAAG;AAGpD,SAAO;CACR;CAED,AAAQ,MAAMK,QAAoC;AAChD,SAAO;GACL,IAAI,OAAO;GACX,MAAM,OAAO;GACb,WAAW,OAAO;GAClB,OAAO,OAAO,SAAS;GACvB,UACE,OAAO,sBACH,cACO,OAAO,aAAa,WACzB,OAAO,WACP,KAAK,UAAU,OAAO,SAAS;GACvC,WACE,OAAO,uBACH,KAAK,UAAU,OAAO,UAAU,GAChC;GACN,WACE,OAAO,uBACH,KAAK,UAAU,OAAO,UAAU,GAChC;GACN,SACE,OAAO,qBAAwB,KAAK,UAAU,OAAO,QAAQ,GAAG;GAClE,WAAW,OAAO;GAClB,SAAS,OAAO,OAAO,MAAM;GAC7B,WAAW,OAAO,OAAO,QAAQ;GACjC,WACE,OAAO,mBACH,KAAK,UAAU,KAAK,aAAa,OAAO,MAAM,CAAC,GAC/C;GACN,UACE,OAAO,sBAAyB,KAAK,UAAU,OAAO,SAAS,GAAG;EACrE;CACF;CAED,AAAQ,QAAQJ,KAAiC;EAC/C,MAAM,QACJ,IAAI,YAAY,QAAQ,IAAI,cAAc,OACtC;GACE,IAAI,IAAI;GACR,MAAM,IAAI;GACV,GAAI,IAAI,YAAY,KAAK,UAAU,IAAI,UAAU,GAAG,CAAE;EACvD;AAGP,SAAO;GACL,IAAI,IAAI;GACR,MAAM,IAAI;GACV,WAAW,IAAI;GACf,OAAO,IAAI;GACX,UAAU,IAAI,WACV,KAAK,cAAc,IAAI,SAAS;GAEpC,WAAW,IAAI,YACX,KAAK,UAAU,IAAI,UAAU;GAEjC,WAAW,IAAI,YACX,KAAK,UAAU,IAAI,UAAU;GAEjC,SAAS,IAAI,UAAU,KAAK,UAAU,IAAI,QAAQ;GAClD,WAAW,IAAI;GACf;GACA,UAAU,IAAI,WAAW,KAAK,UAAU,IAAI,SAAS;EACtD;CACF;;;;CAKD,AAAQ,UAAUK,OAAiD;AACjE,aAAW,UAAU,SACnB,QAAO;AAET,SAAO,KAAK,MAAM,MAAM;CACzB;CAED,AAAQ,aACNC,OACyB;EACzB,MAAM,EAAE,IAAI,KAAM,GAAG,MAAM,GAAG;AAC9B,SAAO;CACR;CAED,AAAQ,cACNC,UACkC;AAClC,MAAI;GACF,MAAM,SAAS,KAAK,MAAM,SAAS;AACnC,cAAW,WAAW,YAAY,WAAW,KAC3C,QAAO;AAET,UAAO;EACR,QAAO;AACN,UAAO;EACR;CACF;AACF"}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { AuditRecord } from "./Auditor-CZ8lkASv.cjs";
|
|
2
|
+
import { AuditQueryOptions, AuditStorage } from "./storage-BtFY7Rha.cjs";
|
|
3
|
+
import { ControlledTransaction, IsolationLevel, Kysely, Transaction } from "kysely";
|
|
4
|
+
|
|
5
|
+
//#region src/kysely.d.ts
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Minimal interface for transaction-aware audit flushing.
|
|
9
|
+
* Use this when you need to flush audits within a database transaction.
|
|
10
|
+
*
|
|
11
|
+
* @template TTransaction - Transaction type (e.g., Kysely Transaction)
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```typescript
|
|
15
|
+
* import { withAuditableTransaction } from '@geekmidas/audit/kysely';
|
|
16
|
+
* import type { TransactionAwareAuditor } from '@geekmidas/audit/kysely';
|
|
17
|
+
*
|
|
18
|
+
* const result = await withAuditableTransaction(
|
|
19
|
+
* db,
|
|
20
|
+
* auditor as TransactionAwareAuditor<Transaction<DB>>,
|
|
21
|
+
* async (trx) => {
|
|
22
|
+
* // Your transactional operations
|
|
23
|
+
* return result;
|
|
24
|
+
* },
|
|
25
|
+
* );
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
interface TransactionAwareAuditor<TTransaction = unknown> {
|
|
29
|
+
/** Register the transaction with the auditor for use during flush */
|
|
30
|
+
setTransaction(trx: TTransaction): void;
|
|
31
|
+
/** Flush all pending audits, optionally within a transaction */
|
|
32
|
+
flush(trx?: TTransaction): Promise<void>;
|
|
33
|
+
}
|
|
34
|
+
interface TransactionSettings {
|
|
35
|
+
isolationLevel?: IsolationLevel;
|
|
36
|
+
}
|
|
37
|
+
type DatabaseConnection<T> = ControlledTransaction<T> | Kysely<T> | Transaction<T>;
|
|
38
|
+
/**
|
|
39
|
+
* Execute a callback within a database transaction with automatic audit handling.
|
|
40
|
+
*
|
|
41
|
+
* This wrapper ensures that:
|
|
42
|
+
* 1. The transaction is automatically registered with the auditor
|
|
43
|
+
* 2. Manual audits (via `auditor.audit()`) are flushed BEFORE the transaction commits
|
|
44
|
+
* 3. If audit flush fails, the entire transaction rolls back
|
|
45
|
+
* 4. If the callback fails, audits are NOT written (atomic consistency)
|
|
46
|
+
*
|
|
47
|
+
* **Note:** Declarative audits (defined via `.audit([...])` on the endpoint builder)
|
|
48
|
+
* are processed AFTER the handler returns, so they run outside this transaction.
|
|
49
|
+
* If you need all audits to be atomic with your database operations, use manual
|
|
50
|
+
* audits via `auditor.audit()` inside this wrapper.
|
|
51
|
+
*
|
|
52
|
+
* @param db - Database connection (Kysely, Transaction, or ControlledTransaction)
|
|
53
|
+
* @param auditor - Auditor instance that will receive the transaction
|
|
54
|
+
* @param cb - Callback to execute within the transaction
|
|
55
|
+
* @param settings - Optional transaction settings (isolation level)
|
|
56
|
+
* @returns The result of the callback
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* ```typescript
|
|
60
|
+
* import { withAuditableTransaction } from '@geekmidas/audit/kysely';
|
|
61
|
+
*
|
|
62
|
+
* const result = await withAuditableTransaction(
|
|
63
|
+
* services.database,
|
|
64
|
+
* auditor,
|
|
65
|
+
* async (trx) => {
|
|
66
|
+
* const user = await trx
|
|
67
|
+
* .insertInto('users')
|
|
68
|
+
* .values(data)
|
|
69
|
+
* .returningAll()
|
|
70
|
+
* .executeTakeFirstOrThrow();
|
|
71
|
+
*
|
|
72
|
+
* // Manual audits are atomic with the transaction
|
|
73
|
+
* auditor.audit('user.created', { userId: user.id, email: user.email });
|
|
74
|
+
*
|
|
75
|
+
* return user;
|
|
76
|
+
* },
|
|
77
|
+
* );
|
|
78
|
+
* // Audits are automatically flushed inside the transaction before commit
|
|
79
|
+
* ```
|
|
80
|
+
*/
|
|
81
|
+
declare function withAuditableTransaction<DB, T>(db: DatabaseConnection<DB>, auditor: TransactionAwareAuditor<Transaction<DB>>, cb: (trx: Transaction<DB>) => Promise<T>, settings?: TransactionSettings): Promise<T>;
|
|
82
|
+
/**
|
|
83
|
+
* Database table interface for audit records.
|
|
84
|
+
* Use this to define your audit_logs table in your Kysely database schema.
|
|
85
|
+
*
|
|
86
|
+
* @example
|
|
87
|
+
* ```typescript
|
|
88
|
+
* interface Database {
|
|
89
|
+
* audit_logs: AuditLogTable;
|
|
90
|
+
* // ... other tables
|
|
91
|
+
* }
|
|
92
|
+
* ```
|
|
93
|
+
*/
|
|
94
|
+
interface AuditLogTable {
|
|
95
|
+
id: string;
|
|
96
|
+
type: string;
|
|
97
|
+
operation: string;
|
|
98
|
+
table: string | null;
|
|
99
|
+
entityId: string | null;
|
|
100
|
+
oldValues: string | null;
|
|
101
|
+
newValues: string | null;
|
|
102
|
+
payload: string | null;
|
|
103
|
+
timestamp: Date;
|
|
104
|
+
actorId: string | null;
|
|
105
|
+
actorType: string | null;
|
|
106
|
+
actorData: string | null;
|
|
107
|
+
metadata: string | null;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Configuration for KyselyAuditStorage.
|
|
111
|
+
*/
|
|
112
|
+
interface KyselyAuditStorageConfig<DB> {
|
|
113
|
+
/** Kysely database instance */
|
|
114
|
+
db: Kysely<DB>;
|
|
115
|
+
/** Table name for audit logs (must be a key in DB that extends AuditLogTable) */
|
|
116
|
+
tableName: keyof DB & string;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Kysely-based audit storage implementation.
|
|
120
|
+
* Stores audit records in a database table using Kysely.
|
|
121
|
+
*
|
|
122
|
+
* @template DB - Your Kysely database schema
|
|
123
|
+
*
|
|
124
|
+
* @example
|
|
125
|
+
* ```typescript
|
|
126
|
+
* interface Database {
|
|
127
|
+
* audit_logs: AuditLogTable;
|
|
128
|
+
* }
|
|
129
|
+
*
|
|
130
|
+
* const storage = new KyselyAuditStorage({
|
|
131
|
+
* db: kyselyDb,
|
|
132
|
+
* tableName: 'audit_logs',
|
|
133
|
+
* });
|
|
134
|
+
*
|
|
135
|
+
* const auditor = new DefaultAuditor({
|
|
136
|
+
* actor: { id: 'user-123', type: 'user' },
|
|
137
|
+
* storage,
|
|
138
|
+
* });
|
|
139
|
+
* ```
|
|
140
|
+
*/
|
|
141
|
+
declare class KyselyAuditStorage<DB> implements AuditStorage {
|
|
142
|
+
private readonly db;
|
|
143
|
+
private readonly tableName;
|
|
144
|
+
constructor(config: KyselyAuditStorageConfig<DB>);
|
|
145
|
+
write(records: AuditRecord[], trx?: unknown): Promise<void>;
|
|
146
|
+
query(options: AuditQueryOptions): Promise<AuditRecord[]>;
|
|
147
|
+
count(options: Omit<AuditQueryOptions, 'limit' | 'offset'>): Promise<number>;
|
|
148
|
+
/**
|
|
149
|
+
* Get the Kysely database instance for transactional operations.
|
|
150
|
+
* Used by endpoint adaptors to automatically wrap handlers in transactions.
|
|
151
|
+
*/
|
|
152
|
+
getDatabase(): Kysely<DB>;
|
|
153
|
+
private applyFilters;
|
|
154
|
+
private toRow;
|
|
155
|
+
private fromRow;
|
|
156
|
+
/**
|
|
157
|
+
* Parse a JSON value that may already be parsed (e.g., from jsonb columns).
|
|
158
|
+
*/
|
|
159
|
+
private parseJson;
|
|
160
|
+
private getActorData;
|
|
161
|
+
private parseEntityId;
|
|
162
|
+
}
|
|
163
|
+
//#endregion
|
|
164
|
+
export { AuditLogTable, DatabaseConnection, KyselyAuditStorage, KyselyAuditStorageConfig, TransactionAwareAuditor, TransactionSettings, withAuditableTransaction };
|
|
165
|
+
//# sourceMappingURL=kysely.d.cts.map
|