@geekmidas/audit 0.0.1 → 0.0.3
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/dist/DefaultAuditor-1HDUGMub.cjs.map +1 -1
- package/dist/{DefaultAuditor-B9Unin1g.d.mts → DefaultAuditor-B-YEyT3q.d.mts} +2 -2
- package/dist/DefaultAuditor-BAVnNmRh.mjs.map +1 -1
- package/dist/{DefaultAuditor-Dqc4UZA1.d.cts → DefaultAuditor-RC-F_fTc.d.cts} +2 -2
- package/dist/DefaultAuditor.d.cts +2 -2
- package/dist/DefaultAuditor.d.mts +2 -2
- package/dist/index.d.cts +2 -2
- package/dist/index.d.mts +2 -2
- package/dist/kysely.cjs +10 -7
- package/dist/kysely.cjs.map +1 -1
- package/dist/kysely.d.cts +15 -6
- package/dist/kysely.d.mts +15 -6
- package/dist/kysely.mjs +10 -7
- package/dist/kysely.mjs.map +1 -1
- package/dist/{storage-DnAMfOA7.d.mts → storage-BmA3SESr.d.mts} +16 -1
- package/dist/{storage-BtFY7Rha.d.cts → storage-ndQzIcWK.d.cts} +16 -1
- package/dist/storage.d.cts +1 -1
- package/dist/storage.d.mts +1 -1
- package/package.json +1 -1
- package/src/Auditor.ts +1 -1
- package/src/DefaultAuditor.ts +1 -1
- package/src/__tests__/DefaultAuditor.spec.ts +69 -18
- package/src/__tests__/KyselyAuditStorage.integration.spec.ts +2 -5
- package/src/__tests__/KyselyAuditStorage.spec.ts +7 -4
- package/src/kysely.ts +32 -42
- package/src/storage.ts +16 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DefaultAuditor-1HDUGMub.cjs","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
|
|
1
|
+
{"version":3,"file":"DefaultAuditor-1HDUGMub.cjs","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 AuditActor,\n AuditMetadata,\n AuditOptions,\n AuditRecord,\n AuditableAction,\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,oBAAQ;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"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { AuditActor, AuditMetadata, AuditOptions, AuditRecord, AuditableAction, Auditor, ExtractAuditPayload, ExtractAuditType } from "./Auditor-ii62d_pF.mjs";
|
|
2
|
-
import { AuditStorage } from "./storage-
|
|
2
|
+
import { AuditStorage } from "./storage-BmA3SESr.mjs";
|
|
3
3
|
|
|
4
4
|
//#region src/DefaultAuditor.d.ts
|
|
5
5
|
|
|
@@ -56,4 +56,4 @@ declare class DefaultAuditor<TAuditAction extends AuditableAction<string, unknow
|
|
|
56
56
|
}
|
|
57
57
|
//#endregion
|
|
58
58
|
export { DefaultAuditor, DefaultAuditorConfig };
|
|
59
|
-
//# sourceMappingURL=DefaultAuditor-
|
|
59
|
+
//# sourceMappingURL=DefaultAuditor-B-YEyT3q.d.mts.map
|
|
@@ -1 +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
|
|
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 AuditActor,\n AuditMetadata,\n AuditOptions,\n AuditRecord,\n AuditableAction,\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"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { AuditActor, AuditMetadata, AuditOptions, AuditRecord, AuditableAction, Auditor, ExtractAuditPayload, ExtractAuditType } from "./Auditor-CZ8lkASv.cjs";
|
|
2
|
-
import { AuditStorage } from "./storage-
|
|
2
|
+
import { AuditStorage } from "./storage-ndQzIcWK.cjs";
|
|
3
3
|
|
|
4
4
|
//#region src/DefaultAuditor.d.ts
|
|
5
5
|
|
|
@@ -56,4 +56,4 @@ declare class DefaultAuditor<TAuditAction extends AuditableAction<string, unknow
|
|
|
56
56
|
}
|
|
57
57
|
//#endregion
|
|
58
58
|
export { DefaultAuditor, DefaultAuditorConfig };
|
|
59
|
-
//# sourceMappingURL=DefaultAuditor-
|
|
59
|
+
//# sourceMappingURL=DefaultAuditor-RC-F_fTc.d.cts.map
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import "./Auditor-CZ8lkASv.cjs";
|
|
2
|
-
import "./storage-
|
|
3
|
-
import { DefaultAuditor, DefaultAuditorConfig } from "./DefaultAuditor-
|
|
2
|
+
import "./storage-ndQzIcWK.cjs";
|
|
3
|
+
import { DefaultAuditor, DefaultAuditorConfig } from "./DefaultAuditor-RC-F_fTc.cjs";
|
|
4
4
|
export { DefaultAuditor, DefaultAuditorConfig };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import "./Auditor-ii62d_pF.mjs";
|
|
2
|
-
import "./storage-
|
|
3
|
-
import { DefaultAuditor, DefaultAuditorConfig } from "./DefaultAuditor-
|
|
2
|
+
import "./storage-BmA3SESr.mjs";
|
|
3
|
+
import { DefaultAuditor, DefaultAuditorConfig } from "./DefaultAuditor-B-YEyT3q.mjs";
|
|
4
4
|
export { DefaultAuditor, DefaultAuditorConfig };
|
package/dist/index.d.cts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import { AuditActor, AuditMetadata, AuditOperation, AuditOptions, AuditRecord, AuditableAction, Auditor, ExtractAuditPayload, ExtractAuditType, ExtractAuditorAction, MappedAudit } from "./Auditor-CZ8lkASv.cjs";
|
|
2
|
-
import { AuditQueryOptions, AuditStorage } from "./storage-
|
|
3
|
-
import { DefaultAuditor, DefaultAuditorConfig } from "./DefaultAuditor-
|
|
2
|
+
import { AuditQueryOptions, AuditStorage } from "./storage-ndQzIcWK.cjs";
|
|
3
|
+
import { DefaultAuditor, DefaultAuditorConfig } from "./DefaultAuditor-RC-F_fTc.cjs";
|
|
4
4
|
export { AuditActor, AuditMetadata, AuditOperation, AuditOptions, AuditQueryOptions, AuditRecord, AuditStorage, AuditableAction, Auditor, DefaultAuditor, DefaultAuditorConfig, ExtractAuditPayload, ExtractAuditType, ExtractAuditorAction, MappedAudit };
|
package/dist/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import { AuditActor, AuditMetadata, AuditOperation, AuditOptions, AuditRecord, AuditableAction, Auditor, ExtractAuditPayload, ExtractAuditType, ExtractAuditorAction, MappedAudit } from "./Auditor-ii62d_pF.mjs";
|
|
2
|
-
import { AuditQueryOptions, AuditStorage } from "./storage-
|
|
3
|
-
import { DefaultAuditor, DefaultAuditorConfig } from "./DefaultAuditor-
|
|
2
|
+
import { AuditQueryOptions, AuditStorage } from "./storage-BmA3SESr.mjs";
|
|
3
|
+
import { DefaultAuditor, DefaultAuditorConfig } from "./DefaultAuditor-B-YEyT3q.mjs";
|
|
4
4
|
export { AuditActor, AuditMetadata, AuditOperation, AuditOptions, AuditQueryOptions, AuditRecord, AuditStorage, AuditableAction, Auditor, DefaultAuditor, DefaultAuditorConfig, ExtractAuditPayload, ExtractAuditType, ExtractAuditorAction, MappedAudit };
|
package/dist/kysely.cjs
CHANGED
|
@@ -81,9 +81,11 @@ async function withAuditableTransaction(db, auditor, cb, settings) {
|
|
|
81
81
|
var KyselyAuditStorage = class {
|
|
82
82
|
db;
|
|
83
83
|
tableName;
|
|
84
|
+
databaseServiceName;
|
|
84
85
|
constructor(config) {
|
|
85
86
|
this.db = config.db;
|
|
86
87
|
this.tableName = config.tableName;
|
|
88
|
+
this.databaseServiceName = config.databaseServiceName;
|
|
87
89
|
}
|
|
88
90
|
async write(records, trx) {
|
|
89
91
|
if (records.length === 0) return;
|
|
@@ -135,14 +137,14 @@ var KyselyAuditStorage = class {
|
|
|
135
137
|
operation: record.operation,
|
|
136
138
|
table: record.table ?? null,
|
|
137
139
|
entityId: record.entityId === void 0 ? null : typeof record.entityId === "string" ? record.entityId : JSON.stringify(record.entityId),
|
|
138
|
-
oldValues: record.oldValues
|
|
139
|
-
newValues: record.newValues
|
|
140
|
-
payload: record.payload
|
|
140
|
+
oldValues: record.oldValues ?? null,
|
|
141
|
+
newValues: record.newValues ?? null,
|
|
142
|
+
payload: record.payload ?? null,
|
|
141
143
|
timestamp: record.timestamp,
|
|
142
144
|
actorId: record.actor?.id ?? null,
|
|
143
145
|
actorType: record.actor?.type ?? null,
|
|
144
|
-
actorData: record.actor !== void 0 ?
|
|
145
|
-
metadata: record.metadata
|
|
146
|
+
actorData: record.actor !== void 0 ? this.getActorData(record.actor) : null,
|
|
147
|
+
metadata: record.metadata ?? null
|
|
146
148
|
};
|
|
147
149
|
}
|
|
148
150
|
fromRow(row) {
|
|
@@ -169,8 +171,9 @@ var KyselyAuditStorage = class {
|
|
|
169
171
|
* Parse a JSON value that may already be parsed (e.g., from jsonb columns).
|
|
170
172
|
*/
|
|
171
173
|
parseJson(value) {
|
|
172
|
-
if (typeof value === "object") return value;
|
|
173
|
-
return JSON.parse(value);
|
|
174
|
+
if (typeof value === "object" && value !== null) return value;
|
|
175
|
+
if (typeof value === "string") return JSON.parse(value);
|
|
176
|
+
return {};
|
|
174
177
|
}
|
|
175
178
|
getActorData(actor) {
|
|
176
179
|
const { id, type,...rest } = actor;
|
package/dist/kysely.cjs.map
CHANGED
|
@@ -1 +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":"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"}
|
|
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: unknown","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 * Column names use snake_case to match standard PostgreSQL conventions.\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: unknown | null;\n newValues: unknown | null;\n payload: unknown | null;\n timestamp: Date;\n actorId: string | null;\n actorType: string | null;\n actorData: unknown | null;\n metadata: unknown | 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 * Service name of the database service.\n * When set, endpoint adaptors will automatically use the audit transaction as `db`\n * in the handler context if the endpoint's database service has the same name.\n */\n databaseServiceName?: 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 readonly databaseServiceName?: string;\n\n constructor(config: KyselyAuditStorageConfig<DB>) {\n this.db = config.db;\n this.tableName = config.tableName;\n this.databaseServiceName = config.databaseServiceName;\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).insertInto(this.tableName).values(rows).execute();\n }\n\n async query(options: AuditQueryOptions): Promise<AuditRecord[]> {\n let query = (this.db as any).selectFrom(this.tableName).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: record.oldValues ?? null,\n newValues: record.newValues ?? null,\n payload: 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 ? this.getActorData(record.actor) : null,\n metadata: 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 ? this.parseEntityId(row.entityId) : undefined,\n oldValues: row.oldValues ? this.parseJson(row.oldValues) : undefined,\n newValues: row.newValues ? this.parseJson(row.newValues) : 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: unknown): Record<string, unknown> {\n if (typeof value === 'object' && value !== null) {\n return value as Record<string, unknown>;\n }\n if (typeof value === 'string') {\n return JSON.parse(value);\n }\n return {};\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(entityId: string): 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":"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"}
|
package/dist/kysely.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { AuditRecord } from "./Auditor-CZ8lkASv.cjs";
|
|
2
|
-
import { AuditQueryOptions, AuditStorage } from "./storage-
|
|
2
|
+
import { AuditQueryOptions, AuditStorage } from "./storage-ndQzIcWK.cjs";
|
|
3
3
|
import { ControlledTransaction, IsolationLevel, Kysely, Transaction } from "kysely";
|
|
4
4
|
|
|
5
5
|
//#region src/kysely.d.ts
|
|
@@ -83,6 +83,8 @@ declare function withAuditableTransaction<DB, T>(db: DatabaseConnection<DB>, aud
|
|
|
83
83
|
* Database table interface for audit records.
|
|
84
84
|
* Use this to define your audit_logs table in your Kysely database schema.
|
|
85
85
|
*
|
|
86
|
+
* Column names use snake_case to match standard PostgreSQL conventions.
|
|
87
|
+
*
|
|
86
88
|
* @example
|
|
87
89
|
* ```typescript
|
|
88
90
|
* interface Database {
|
|
@@ -97,14 +99,14 @@ interface AuditLogTable {
|
|
|
97
99
|
operation: string;
|
|
98
100
|
table: string | null;
|
|
99
101
|
entityId: string | null;
|
|
100
|
-
oldValues:
|
|
101
|
-
newValues:
|
|
102
|
-
payload:
|
|
102
|
+
oldValues: unknown | null;
|
|
103
|
+
newValues: unknown | null;
|
|
104
|
+
payload: unknown | null;
|
|
103
105
|
timestamp: Date;
|
|
104
106
|
actorId: string | null;
|
|
105
107
|
actorType: string | null;
|
|
106
|
-
actorData:
|
|
107
|
-
metadata:
|
|
108
|
+
actorData: unknown | null;
|
|
109
|
+
metadata: unknown | null;
|
|
108
110
|
}
|
|
109
111
|
/**
|
|
110
112
|
* Configuration for KyselyAuditStorage.
|
|
@@ -114,6 +116,12 @@ interface KyselyAuditStorageConfig<DB> {
|
|
|
114
116
|
db: Kysely<DB>;
|
|
115
117
|
/** Table name for audit logs (must be a key in DB that extends AuditLogTable) */
|
|
116
118
|
tableName: keyof DB & string;
|
|
119
|
+
/**
|
|
120
|
+
* Service name of the database service.
|
|
121
|
+
* When set, endpoint adaptors will automatically use the audit transaction as `db`
|
|
122
|
+
* in the handler context if the endpoint's database service has the same name.
|
|
123
|
+
*/
|
|
124
|
+
databaseServiceName?: string;
|
|
117
125
|
}
|
|
118
126
|
/**
|
|
119
127
|
* Kysely-based audit storage implementation.
|
|
@@ -141,6 +149,7 @@ interface KyselyAuditStorageConfig<DB> {
|
|
|
141
149
|
declare class KyselyAuditStorage<DB> implements AuditStorage {
|
|
142
150
|
private readonly db;
|
|
143
151
|
private readonly tableName;
|
|
152
|
+
readonly databaseServiceName?: string;
|
|
144
153
|
constructor(config: KyselyAuditStorageConfig<DB>);
|
|
145
154
|
write(records: AuditRecord[], trx?: unknown): Promise<void>;
|
|
146
155
|
query(options: AuditQueryOptions): Promise<AuditRecord[]>;
|
package/dist/kysely.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { AuditRecord } from "./Auditor-ii62d_pF.mjs";
|
|
2
|
-
import { AuditQueryOptions, AuditStorage } from "./storage-
|
|
2
|
+
import { AuditQueryOptions, AuditStorage } from "./storage-BmA3SESr.mjs";
|
|
3
3
|
import { ControlledTransaction, IsolationLevel, Kysely, Transaction } from "kysely";
|
|
4
4
|
|
|
5
5
|
//#region src/kysely.d.ts
|
|
@@ -83,6 +83,8 @@ declare function withAuditableTransaction<DB, T>(db: DatabaseConnection<DB>, aud
|
|
|
83
83
|
* Database table interface for audit records.
|
|
84
84
|
* Use this to define your audit_logs table in your Kysely database schema.
|
|
85
85
|
*
|
|
86
|
+
* Column names use snake_case to match standard PostgreSQL conventions.
|
|
87
|
+
*
|
|
86
88
|
* @example
|
|
87
89
|
* ```typescript
|
|
88
90
|
* interface Database {
|
|
@@ -97,14 +99,14 @@ interface AuditLogTable {
|
|
|
97
99
|
operation: string;
|
|
98
100
|
table: string | null;
|
|
99
101
|
entityId: string | null;
|
|
100
|
-
oldValues:
|
|
101
|
-
newValues:
|
|
102
|
-
payload:
|
|
102
|
+
oldValues: unknown | null;
|
|
103
|
+
newValues: unknown | null;
|
|
104
|
+
payload: unknown | null;
|
|
103
105
|
timestamp: Date;
|
|
104
106
|
actorId: string | null;
|
|
105
107
|
actorType: string | null;
|
|
106
|
-
actorData:
|
|
107
|
-
metadata:
|
|
108
|
+
actorData: unknown | null;
|
|
109
|
+
metadata: unknown | null;
|
|
108
110
|
}
|
|
109
111
|
/**
|
|
110
112
|
* Configuration for KyselyAuditStorage.
|
|
@@ -114,6 +116,12 @@ interface KyselyAuditStorageConfig<DB> {
|
|
|
114
116
|
db: Kysely<DB>;
|
|
115
117
|
/** Table name for audit logs (must be a key in DB that extends AuditLogTable) */
|
|
116
118
|
tableName: keyof DB & string;
|
|
119
|
+
/**
|
|
120
|
+
* Service name of the database service.
|
|
121
|
+
* When set, endpoint adaptors will automatically use the audit transaction as `db`
|
|
122
|
+
* in the handler context if the endpoint's database service has the same name.
|
|
123
|
+
*/
|
|
124
|
+
databaseServiceName?: string;
|
|
117
125
|
}
|
|
118
126
|
/**
|
|
119
127
|
* Kysely-based audit storage implementation.
|
|
@@ -141,6 +149,7 @@ interface KyselyAuditStorageConfig<DB> {
|
|
|
141
149
|
declare class KyselyAuditStorage<DB> implements AuditStorage {
|
|
142
150
|
private readonly db;
|
|
143
151
|
private readonly tableName;
|
|
152
|
+
readonly databaseServiceName?: string;
|
|
144
153
|
constructor(config: KyselyAuditStorageConfig<DB>);
|
|
145
154
|
write(records: AuditRecord[], trx?: unknown): Promise<void>;
|
|
146
155
|
query(options: AuditQueryOptions): Promise<AuditRecord[]>;
|
package/dist/kysely.mjs
CHANGED
|
@@ -80,9 +80,11 @@ async function withAuditableTransaction(db, auditor, cb, settings) {
|
|
|
80
80
|
var KyselyAuditStorage = class {
|
|
81
81
|
db;
|
|
82
82
|
tableName;
|
|
83
|
+
databaseServiceName;
|
|
83
84
|
constructor(config) {
|
|
84
85
|
this.db = config.db;
|
|
85
86
|
this.tableName = config.tableName;
|
|
87
|
+
this.databaseServiceName = config.databaseServiceName;
|
|
86
88
|
}
|
|
87
89
|
async write(records, trx) {
|
|
88
90
|
if (records.length === 0) return;
|
|
@@ -134,14 +136,14 @@ var KyselyAuditStorage = class {
|
|
|
134
136
|
operation: record.operation,
|
|
135
137
|
table: record.table ?? null,
|
|
136
138
|
entityId: record.entityId === void 0 ? null : typeof record.entityId === "string" ? record.entityId : JSON.stringify(record.entityId),
|
|
137
|
-
oldValues: record.oldValues
|
|
138
|
-
newValues: record.newValues
|
|
139
|
-
payload: record.payload
|
|
139
|
+
oldValues: record.oldValues ?? null,
|
|
140
|
+
newValues: record.newValues ?? null,
|
|
141
|
+
payload: record.payload ?? null,
|
|
140
142
|
timestamp: record.timestamp,
|
|
141
143
|
actorId: record.actor?.id ?? null,
|
|
142
144
|
actorType: record.actor?.type ?? null,
|
|
143
|
-
actorData: record.actor !== void 0 ?
|
|
144
|
-
metadata: record.metadata
|
|
145
|
+
actorData: record.actor !== void 0 ? this.getActorData(record.actor) : null,
|
|
146
|
+
metadata: record.metadata ?? null
|
|
145
147
|
};
|
|
146
148
|
}
|
|
147
149
|
fromRow(row) {
|
|
@@ -168,8 +170,9 @@ var KyselyAuditStorage = class {
|
|
|
168
170
|
* Parse a JSON value that may already be parsed (e.g., from jsonb columns).
|
|
169
171
|
*/
|
|
170
172
|
parseJson(value) {
|
|
171
|
-
if (typeof value === "object") return value;
|
|
172
|
-
return JSON.parse(value);
|
|
173
|
+
if (typeof value === "object" && value !== null) return value;
|
|
174
|
+
if (typeof value === "string") return JSON.parse(value);
|
|
175
|
+
return {};
|
|
173
176
|
}
|
|
174
177
|
getActorData(actor) {
|
|
175
178
|
const { id, type,...rest } = actor;
|
package/dist/kysely.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"kysely.mjs","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":"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"}
|
|
1
|
+
{"version":3,"file":"kysely.mjs","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: unknown","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 * Column names use snake_case to match standard PostgreSQL conventions.\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: unknown | null;\n newValues: unknown | null;\n payload: unknown | null;\n timestamp: Date;\n actorId: string | null;\n actorType: string | null;\n actorData: unknown | null;\n metadata: unknown | 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 * Service name of the database service.\n * When set, endpoint adaptors will automatically use the audit transaction as `db`\n * in the handler context if the endpoint's database service has the same name.\n */\n databaseServiceName?: 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 readonly databaseServiceName?: string;\n\n constructor(config: KyselyAuditStorageConfig<DB>) {\n this.db = config.db;\n this.tableName = config.tableName;\n this.databaseServiceName = config.databaseServiceName;\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).insertInto(this.tableName).values(rows).execute();\n }\n\n async query(options: AuditQueryOptions): Promise<AuditRecord[]> {\n let query = (this.db as any).selectFrom(this.tableName).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: record.oldValues ?? null,\n newValues: record.newValues ?? null,\n payload: 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 ? this.getActorData(record.actor) : null,\n metadata: 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 ? this.parseEntityId(row.entityId) : undefined,\n oldValues: row.oldValues ? this.parseJson(row.oldValues) : undefined,\n newValues: row.newValues ? this.parseJson(row.newValues) : 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: unknown): Record<string, unknown> {\n if (typeof value === 'object' && value !== null) {\n return value as Record<string, unknown>;\n }\n if (typeof value === 'string') {\n return JSON.parse(value);\n }\n return {};\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(entityId: string): 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":"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"}
|
|
@@ -96,7 +96,22 @@ interface AuditStorage {
|
|
|
96
96
|
* ```
|
|
97
97
|
*/
|
|
98
98
|
getDatabase?(): unknown;
|
|
99
|
+
/**
|
|
100
|
+
* Optional: The service name of the database service used by this storage.
|
|
101
|
+
* When set, endpoint adaptors will automatically use the audit transaction as `db`
|
|
102
|
+
* in the handler context if the endpoint's database service has the same name.
|
|
103
|
+
*
|
|
104
|
+
* @example
|
|
105
|
+
* ```typescript
|
|
106
|
+
* const storage = new KyselyAuditStorage({
|
|
107
|
+
* db,
|
|
108
|
+
* tableName: 'audit_logs',
|
|
109
|
+
* databaseServiceName: 'database', // Matches databaseService.serviceName
|
|
110
|
+
* });
|
|
111
|
+
* ```
|
|
112
|
+
*/
|
|
113
|
+
databaseServiceName?: string;
|
|
99
114
|
}
|
|
100
115
|
//#endregion
|
|
101
116
|
export { AuditQueryOptions, AuditStorage };
|
|
102
|
-
//# sourceMappingURL=storage-
|
|
117
|
+
//# sourceMappingURL=storage-BmA3SESr.d.mts.map
|
|
@@ -96,7 +96,22 @@ interface AuditStorage {
|
|
|
96
96
|
* ```
|
|
97
97
|
*/
|
|
98
98
|
getDatabase?(): unknown;
|
|
99
|
+
/**
|
|
100
|
+
* Optional: The service name of the database service used by this storage.
|
|
101
|
+
* When set, endpoint adaptors will automatically use the audit transaction as `db`
|
|
102
|
+
* in the handler context if the endpoint's database service has the same name.
|
|
103
|
+
*
|
|
104
|
+
* @example
|
|
105
|
+
* ```typescript
|
|
106
|
+
* const storage = new KyselyAuditStorage({
|
|
107
|
+
* db,
|
|
108
|
+
* tableName: 'audit_logs',
|
|
109
|
+
* databaseServiceName: 'database', // Matches databaseService.serviceName
|
|
110
|
+
* });
|
|
111
|
+
* ```
|
|
112
|
+
*/
|
|
113
|
+
databaseServiceName?: string;
|
|
99
114
|
}
|
|
100
115
|
//#endregion
|
|
101
116
|
export { AuditQueryOptions, AuditStorage };
|
|
102
|
-
//# sourceMappingURL=storage-
|
|
117
|
+
//# sourceMappingURL=storage-ndQzIcWK.d.cts.map
|
package/dist/storage.d.cts
CHANGED
package/dist/storage.d.mts
CHANGED
package/package.json
CHANGED
package/src/Auditor.ts
CHANGED
package/src/DefaultAuditor.ts
CHANGED
|
@@ -2,11 +2,11 @@ import { nanoid } from 'nanoid';
|
|
|
2
2
|
import type { Auditor } from './Auditor';
|
|
3
3
|
import type { AuditStorage } from './storage';
|
|
4
4
|
import type {
|
|
5
|
-
AuditableAction,
|
|
6
5
|
AuditActor,
|
|
7
6
|
AuditMetadata,
|
|
8
7
|
AuditOptions,
|
|
9
8
|
AuditRecord,
|
|
9
|
+
AuditableAction,
|
|
10
10
|
ExtractAuditPayload,
|
|
11
11
|
ExtractAuditType,
|
|
12
12
|
} from './types';
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
2
|
import { DefaultAuditor } from '../DefaultAuditor';
|
|
3
3
|
import type { AuditStorage } from '../storage';
|
|
4
|
-
import type {
|
|
4
|
+
import type { AuditRecord, AuditableAction } from '../types';
|
|
5
5
|
|
|
6
6
|
// Define test audit actions
|
|
7
7
|
type TestAuditAction =
|
|
@@ -40,7 +40,10 @@ describe('DefaultAuditor', () => {
|
|
|
40
40
|
generateId: () => `custom-${++counter}`,
|
|
41
41
|
});
|
|
42
42
|
|
|
43
|
-
auditor.audit('user.created', {
|
|
43
|
+
auditor.audit('user.created', {
|
|
44
|
+
userId: '123',
|
|
45
|
+
email: 'test@example.com',
|
|
46
|
+
});
|
|
44
47
|
auditor.audit('user.updated', { userId: '123', changes: ['name'] });
|
|
45
48
|
|
|
46
49
|
const records = auditor.getRecords();
|
|
@@ -57,7 +60,10 @@ describe('DefaultAuditor', () => {
|
|
|
57
60
|
generateId: () => 'test-id',
|
|
58
61
|
});
|
|
59
62
|
|
|
60
|
-
auditor.audit('user.created', {
|
|
63
|
+
auditor.audit('user.created', {
|
|
64
|
+
userId: '456',
|
|
65
|
+
email: 'test@example.com',
|
|
66
|
+
});
|
|
61
67
|
|
|
62
68
|
const records = auditor.getRecords();
|
|
63
69
|
expect(records).toHaveLength(1);
|
|
@@ -109,7 +115,10 @@ describe('DefaultAuditor', () => {
|
|
|
109
115
|
generateId: () => 'test-id',
|
|
110
116
|
});
|
|
111
117
|
|
|
112
|
-
auditor.audit('user.created', {
|
|
118
|
+
auditor.audit('user.created', {
|
|
119
|
+
userId: '456',
|
|
120
|
+
email: 'test@example.com',
|
|
121
|
+
});
|
|
113
122
|
|
|
114
123
|
const records = auditor.getRecords();
|
|
115
124
|
expect(records[0].metadata).toEqual({
|
|
@@ -191,7 +200,10 @@ describe('DefaultAuditor', () => {
|
|
|
191
200
|
storage,
|
|
192
201
|
});
|
|
193
202
|
|
|
194
|
-
auditor.audit('user.created', {
|
|
203
|
+
auditor.audit('user.created', {
|
|
204
|
+
userId: '456',
|
|
205
|
+
email: 'test@example.com',
|
|
206
|
+
});
|
|
195
207
|
|
|
196
208
|
const records1 = auditor.getRecords();
|
|
197
209
|
const records2 = auditor.getRecords();
|
|
@@ -218,7 +230,10 @@ describe('DefaultAuditor', () => {
|
|
|
218
230
|
generateId: () => 'test-id',
|
|
219
231
|
});
|
|
220
232
|
|
|
221
|
-
auditor.audit('user.created', {
|
|
233
|
+
auditor.audit('user.created', {
|
|
234
|
+
userId: '456',
|
|
235
|
+
email: 'test@example.com',
|
|
236
|
+
});
|
|
222
237
|
auditor.audit('order.placed', { orderId: 'order-789', total: 99.99 });
|
|
223
238
|
|
|
224
239
|
await auditor.flush();
|
|
@@ -235,7 +250,10 @@ describe('DefaultAuditor', () => {
|
|
|
235
250
|
storage,
|
|
236
251
|
});
|
|
237
252
|
|
|
238
|
-
auditor.audit('user.created', {
|
|
253
|
+
auditor.audit('user.created', {
|
|
254
|
+
userId: '456',
|
|
255
|
+
email: 'test@example.com',
|
|
256
|
+
});
|
|
239
257
|
|
|
240
258
|
const mockTrx = { isTrx: true };
|
|
241
259
|
await auditor.flush(mockTrx);
|
|
@@ -249,7 +267,10 @@ describe('DefaultAuditor', () => {
|
|
|
249
267
|
storage,
|
|
250
268
|
});
|
|
251
269
|
|
|
252
|
-
auditor.audit('user.created', {
|
|
270
|
+
auditor.audit('user.created', {
|
|
271
|
+
userId: '456',
|
|
272
|
+
email: 'test@example.com',
|
|
273
|
+
});
|
|
253
274
|
await auditor.flush();
|
|
254
275
|
|
|
255
276
|
expect(auditor.getRecords()).toEqual([]);
|
|
@@ -272,10 +293,16 @@ describe('DefaultAuditor', () => {
|
|
|
272
293
|
storage,
|
|
273
294
|
});
|
|
274
295
|
|
|
275
|
-
auditor.audit('user.created', {
|
|
296
|
+
auditor.audit('user.created', {
|
|
297
|
+
userId: '1',
|
|
298
|
+
email: 'first@example.com',
|
|
299
|
+
});
|
|
276
300
|
await auditor.flush();
|
|
277
301
|
|
|
278
|
-
auditor.audit('user.created', {
|
|
302
|
+
auditor.audit('user.created', {
|
|
303
|
+
userId: '2',
|
|
304
|
+
email: 'second@example.com',
|
|
305
|
+
});
|
|
279
306
|
await auditor.flush();
|
|
280
307
|
|
|
281
308
|
expect(storage.write).toHaveBeenCalledTimes(2);
|
|
@@ -290,7 +317,10 @@ describe('DefaultAuditor', () => {
|
|
|
290
317
|
storage,
|
|
291
318
|
});
|
|
292
319
|
|
|
293
|
-
auditor.audit('user.created', {
|
|
320
|
+
auditor.audit('user.created', {
|
|
321
|
+
userId: '456',
|
|
322
|
+
email: 'test@example.com',
|
|
323
|
+
});
|
|
294
324
|
auditor.audit('order.placed', { orderId: 'order-789', total: 99.99 });
|
|
295
325
|
|
|
296
326
|
auditor.clear();
|
|
@@ -308,7 +338,10 @@ describe('DefaultAuditor', () => {
|
|
|
308
338
|
});
|
|
309
339
|
|
|
310
340
|
// These should compile without errors
|
|
311
|
-
auditor.audit('user.created', {
|
|
341
|
+
auditor.audit('user.created', {
|
|
342
|
+
userId: '123',
|
|
343
|
+
email: 'test@example.com',
|
|
344
|
+
});
|
|
312
345
|
auditor.audit('user.updated', { userId: '123', changes: ['name'] });
|
|
313
346
|
auditor.audit('order.placed', { orderId: 'order-1', total: 50.0 });
|
|
314
347
|
|
|
@@ -329,7 +362,10 @@ describe('DefaultAuditor', () => {
|
|
|
329
362
|
});
|
|
330
363
|
|
|
331
364
|
auditor.addMetadata({ requestId: 'req-123', endpoint: '/users' });
|
|
332
|
-
auditor.audit('user.created', {
|
|
365
|
+
auditor.audit('user.created', {
|
|
366
|
+
userId: '456',
|
|
367
|
+
email: 'test@example.com',
|
|
368
|
+
});
|
|
333
369
|
|
|
334
370
|
const records = auditor.getRecords();
|
|
335
371
|
expect(records[0].metadata).toEqual({
|
|
@@ -347,7 +383,10 @@ describe('DefaultAuditor', () => {
|
|
|
347
383
|
});
|
|
348
384
|
|
|
349
385
|
auditor.addMetadata({ endpoint: '/users', method: 'POST' });
|
|
350
|
-
auditor.audit('user.created', {
|
|
386
|
+
auditor.audit('user.created', {
|
|
387
|
+
userId: '456',
|
|
388
|
+
email: 'test@example.com',
|
|
389
|
+
});
|
|
351
390
|
|
|
352
391
|
const records = auditor.getRecords();
|
|
353
392
|
expect(records[0].metadata).toEqual({
|
|
@@ -366,7 +405,10 @@ describe('DefaultAuditor', () => {
|
|
|
366
405
|
});
|
|
367
406
|
|
|
368
407
|
auditor.addMetadata({ endpoint: '/new', ip: '192.168.1.1' });
|
|
369
|
-
auditor.audit('user.created', {
|
|
408
|
+
auditor.audit('user.created', {
|
|
409
|
+
userId: '456',
|
|
410
|
+
email: 'test@example.com',
|
|
411
|
+
});
|
|
370
412
|
|
|
371
413
|
const records = auditor.getRecords();
|
|
372
414
|
expect(records[0].metadata).toEqual({
|
|
@@ -384,13 +426,19 @@ describe('DefaultAuditor', () => {
|
|
|
384
426
|
});
|
|
385
427
|
|
|
386
428
|
// Audit before adding metadata
|
|
387
|
-
auditor.audit('user.created', {
|
|
429
|
+
auditor.audit('user.created', {
|
|
430
|
+
userId: '1',
|
|
431
|
+
email: 'first@example.com',
|
|
432
|
+
});
|
|
388
433
|
|
|
389
434
|
// Add metadata
|
|
390
435
|
auditor.addMetadata({ requestId: 'req-123' });
|
|
391
436
|
|
|
392
437
|
// Audit after adding metadata
|
|
393
|
-
auditor.audit('user.created', {
|
|
438
|
+
auditor.audit('user.created', {
|
|
439
|
+
userId: '2',
|
|
440
|
+
email: 'second@example.com',
|
|
441
|
+
});
|
|
394
442
|
|
|
395
443
|
const records = auditor.getRecords();
|
|
396
444
|
expect(records[0].metadata).toBeUndefined();
|
|
@@ -407,7 +455,10 @@ describe('DefaultAuditor', () => {
|
|
|
407
455
|
auditor.addMetadata({ requestId: 'req-123' });
|
|
408
456
|
auditor.addMetadata({ endpoint: '/users' });
|
|
409
457
|
auditor.addMetadata({ method: 'POST' });
|
|
410
|
-
auditor.audit('user.created', {
|
|
458
|
+
auditor.audit('user.created', {
|
|
459
|
+
userId: '456',
|
|
460
|
+
email: 'test@example.com',
|
|
461
|
+
});
|
|
411
462
|
|
|
412
463
|
const records = auditor.getRecords();
|
|
413
464
|
expect(records[0].metadata).toEqual({
|
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
} from 'vitest';
|
|
18
18
|
import { TEST_DATABASE_CONFIG } from '../../../testkit/test/globalSetup';
|
|
19
19
|
import { DefaultAuditor } from '../DefaultAuditor';
|
|
20
|
-
import {
|
|
20
|
+
import { type AuditLogTable, KyselyAuditStorage } from '../kysely';
|
|
21
21
|
import type { AuditableAction } from '../types';
|
|
22
22
|
|
|
23
23
|
interface TestDatabase {
|
|
@@ -112,10 +112,7 @@ describe('KyselyAuditStorage Integration Tests', () => {
|
|
|
112
112
|
await auditor.flush();
|
|
113
113
|
|
|
114
114
|
// Verify record was written
|
|
115
|
-
const records = await db
|
|
116
|
-
.selectFrom('auditLogs')
|
|
117
|
-
.selectAll()
|
|
118
|
-
.execute();
|
|
115
|
+
const records = await db.selectFrom('auditLogs').selectAll().execute();
|
|
119
116
|
|
|
120
117
|
expect(records).toHaveLength(1);
|
|
121
118
|
expect(records[0].type).toBe('user.created');
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
-
import {
|
|
2
|
+
import { type AuditLogTable, KyselyAuditStorage } from '../kysely';
|
|
3
3
|
import type { AuditRecord } from '../types';
|
|
4
4
|
|
|
5
5
|
// Mock Kysely database
|
|
@@ -76,10 +76,10 @@ describe('KyselyAuditStorage', () => {
|
|
|
76
76
|
operation: 'INSERT',
|
|
77
77
|
table: 'users',
|
|
78
78
|
entityId: 'user-123',
|
|
79
|
-
payload:
|
|
79
|
+
payload: { email: 'test@example.com' },
|
|
80
80
|
actorId: 'admin-1',
|
|
81
81
|
actorType: 'admin',
|
|
82
|
-
metadata:
|
|
82
|
+
metadata: { requestId: 'req-123' },
|
|
83
83
|
}),
|
|
84
84
|
]);
|
|
85
85
|
});
|
|
@@ -155,7 +155,10 @@ describe('KyselyAuditStorage', () => {
|
|
|
155
155
|
expect.objectContaining({
|
|
156
156
|
actorId: 'user-1',
|
|
157
157
|
actorType: 'user',
|
|
158
|
-
actorData:
|
|
158
|
+
actorData: {
|
|
159
|
+
email: 'user@example.com',
|
|
160
|
+
role: 'admin',
|
|
161
|
+
},
|
|
159
162
|
}),
|
|
160
163
|
]);
|
|
161
164
|
});
|
package/src/kysely.ts
CHANGED
|
@@ -125,6 +125,8 @@ export async function withAuditableTransaction<DB, T>(
|
|
|
125
125
|
* Database table interface for audit records.
|
|
126
126
|
* Use this to define your audit_logs table in your Kysely database schema.
|
|
127
127
|
*
|
|
128
|
+
* Column names use snake_case to match standard PostgreSQL conventions.
|
|
129
|
+
*
|
|
128
130
|
* @example
|
|
129
131
|
* ```typescript
|
|
130
132
|
* interface Database {
|
|
@@ -139,14 +141,14 @@ export interface AuditLogTable {
|
|
|
139
141
|
operation: string;
|
|
140
142
|
table: string | null;
|
|
141
143
|
entityId: string | null;
|
|
142
|
-
oldValues:
|
|
143
|
-
newValues:
|
|
144
|
-
payload:
|
|
144
|
+
oldValues: unknown | null;
|
|
145
|
+
newValues: unknown | null;
|
|
146
|
+
payload: unknown | null;
|
|
145
147
|
timestamp: Date;
|
|
146
148
|
actorId: string | null;
|
|
147
149
|
actorType: string | null;
|
|
148
|
-
actorData:
|
|
149
|
-
metadata:
|
|
150
|
+
actorData: unknown | null;
|
|
151
|
+
metadata: unknown | null;
|
|
150
152
|
}
|
|
151
153
|
|
|
152
154
|
/**
|
|
@@ -157,6 +159,12 @@ export interface KyselyAuditStorageConfig<DB> {
|
|
|
157
159
|
db: Kysely<DB>;
|
|
158
160
|
/** Table name for audit logs (must be a key in DB that extends AuditLogTable) */
|
|
159
161
|
tableName: keyof DB & string;
|
|
162
|
+
/**
|
|
163
|
+
* Service name of the database service.
|
|
164
|
+
* When set, endpoint adaptors will automatically use the audit transaction as `db`
|
|
165
|
+
* in the handler context if the endpoint's database service has the same name.
|
|
166
|
+
*/
|
|
167
|
+
databaseServiceName?: string;
|
|
160
168
|
}
|
|
161
169
|
|
|
162
170
|
/**
|
|
@@ -185,10 +193,12 @@ export interface KyselyAuditStorageConfig<DB> {
|
|
|
185
193
|
export class KyselyAuditStorage<DB> implements AuditStorage {
|
|
186
194
|
private readonly db: Kysely<DB>;
|
|
187
195
|
private readonly tableName: keyof DB & string;
|
|
196
|
+
readonly databaseServiceName?: string;
|
|
188
197
|
|
|
189
198
|
constructor(config: KyselyAuditStorageConfig<DB>) {
|
|
190
199
|
this.db = config.db;
|
|
191
200
|
this.tableName = config.tableName;
|
|
201
|
+
this.databaseServiceName = config.databaseServiceName;
|
|
192
202
|
}
|
|
193
203
|
|
|
194
204
|
async write(records: AuditRecord[], trx?: unknown): Promise<void> {
|
|
@@ -199,16 +209,11 @@ export class KyselyAuditStorage<DB> implements AuditStorage {
|
|
|
199
209
|
const db = (trx as Transaction<DB>) ?? this.db;
|
|
200
210
|
const rows = records.map((record) => this.toRow(record));
|
|
201
211
|
|
|
202
|
-
await (db as any)
|
|
203
|
-
.insertInto(this.tableName)
|
|
204
|
-
.values(rows)
|
|
205
|
-
.execute();
|
|
212
|
+
await (db as any).insertInto(this.tableName).values(rows).execute();
|
|
206
213
|
}
|
|
207
214
|
|
|
208
215
|
async query(options: AuditQueryOptions): Promise<AuditRecord[]> {
|
|
209
|
-
let query = (this.db as any)
|
|
210
|
-
.selectFrom(this.tableName)
|
|
211
|
-
.selectAll();
|
|
216
|
+
let query = (this.db as any).selectFrom(this.tableName).selectAll();
|
|
212
217
|
|
|
213
218
|
query = this.applyFilters(query, options);
|
|
214
219
|
|
|
@@ -305,25 +310,15 @@ export class KyselyAuditStorage<DB> implements AuditStorage {
|
|
|
305
310
|
: typeof record.entityId === 'string'
|
|
306
311
|
? record.entityId
|
|
307
312
|
: JSON.stringify(record.entityId),
|
|
308
|
-
oldValues:
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
: null,
|
|
312
|
-
newValues:
|
|
313
|
-
record.newValues !== undefined
|
|
314
|
-
? JSON.stringify(record.newValues)
|
|
315
|
-
: null,
|
|
316
|
-
payload:
|
|
317
|
-
record.payload !== undefined ? JSON.stringify(record.payload) : null,
|
|
313
|
+
oldValues: record.oldValues ?? null,
|
|
314
|
+
newValues: record.newValues ?? null,
|
|
315
|
+
payload: record.payload ?? null,
|
|
318
316
|
timestamp: record.timestamp,
|
|
319
317
|
actorId: record.actor?.id ?? null,
|
|
320
318
|
actorType: record.actor?.type ?? null,
|
|
321
319
|
actorData:
|
|
322
|
-
record.actor !== undefined
|
|
323
|
-
|
|
324
|
-
: null,
|
|
325
|
-
metadata:
|
|
326
|
-
record.metadata !== undefined ? JSON.stringify(record.metadata) : null,
|
|
320
|
+
record.actor !== undefined ? this.getActorData(record.actor) : null,
|
|
321
|
+
metadata: record.metadata ?? null,
|
|
327
322
|
};
|
|
328
323
|
}
|
|
329
324
|
|
|
@@ -342,15 +337,9 @@ export class KyselyAuditStorage<DB> implements AuditStorage {
|
|
|
342
337
|
type: row.type,
|
|
343
338
|
operation: row.operation as AuditRecord['operation'],
|
|
344
339
|
table: row.table ?? undefined,
|
|
345
|
-
entityId: row.entityId
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
oldValues: row.oldValues
|
|
349
|
-
? this.parseJson(row.oldValues)
|
|
350
|
-
: undefined,
|
|
351
|
-
newValues: row.newValues
|
|
352
|
-
? this.parseJson(row.newValues)
|
|
353
|
-
: undefined,
|
|
340
|
+
entityId: row.entityId ? this.parseEntityId(row.entityId) : undefined,
|
|
341
|
+
oldValues: row.oldValues ? this.parseJson(row.oldValues) : undefined,
|
|
342
|
+
newValues: row.newValues ? this.parseJson(row.newValues) : undefined,
|
|
354
343
|
payload: row.payload ? this.parseJson(row.payload) : undefined,
|
|
355
344
|
timestamp: row.timestamp,
|
|
356
345
|
actor,
|
|
@@ -361,11 +350,14 @@ export class KyselyAuditStorage<DB> implements AuditStorage {
|
|
|
361
350
|
/**
|
|
362
351
|
* Parse a JSON value that may already be parsed (e.g., from jsonb columns).
|
|
363
352
|
*/
|
|
364
|
-
private parseJson(value:
|
|
365
|
-
if (typeof value === 'object') {
|
|
353
|
+
private parseJson(value: unknown): Record<string, unknown> {
|
|
354
|
+
if (typeof value === 'object' && value !== null) {
|
|
366
355
|
return value as Record<string, unknown>;
|
|
367
356
|
}
|
|
368
|
-
|
|
357
|
+
if (typeof value === 'string') {
|
|
358
|
+
return JSON.parse(value);
|
|
359
|
+
}
|
|
360
|
+
return {};
|
|
369
361
|
}
|
|
370
362
|
|
|
371
363
|
private getActorData(
|
|
@@ -375,9 +367,7 @@ export class KyselyAuditStorage<DB> implements AuditStorage {
|
|
|
375
367
|
return rest;
|
|
376
368
|
}
|
|
377
369
|
|
|
378
|
-
private parseEntityId(
|
|
379
|
-
entityId: string,
|
|
380
|
-
): string | Record<string, unknown> {
|
|
370
|
+
private parseEntityId(entityId: string): string | Record<string, unknown> {
|
|
381
371
|
try {
|
|
382
372
|
const parsed = JSON.parse(entityId);
|
|
383
373
|
if (typeof parsed === 'object' && parsed !== null) {
|
package/src/storage.ts
CHANGED
|
@@ -98,4 +98,20 @@ export interface AuditStorage {
|
|
|
98
98
|
* ```
|
|
99
99
|
*/
|
|
100
100
|
getDatabase?(): unknown;
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Optional: The service name of the database service used by this storage.
|
|
104
|
+
* When set, endpoint adaptors will automatically use the audit transaction as `db`
|
|
105
|
+
* in the handler context if the endpoint's database service has the same name.
|
|
106
|
+
*
|
|
107
|
+
* @example
|
|
108
|
+
* ```typescript
|
|
109
|
+
* const storage = new KyselyAuditStorage({
|
|
110
|
+
* db,
|
|
111
|
+
* tableName: 'audit_logs',
|
|
112
|
+
* databaseServiceName: 'database', // Matches databaseService.serviceName
|
|
113
|
+
* });
|
|
114
|
+
* ```
|
|
115
|
+
*/
|
|
116
|
+
databaseServiceName?: string;
|
|
101
117
|
}
|