@geekmidas/audit 0.0.1 → 0.0.2
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 +2 -0
- package/dist/kysely.cjs.map +1 -1
- package/dist/kysely.d.cts +8 -1
- package/dist/kysely.d.mts +8 -1
- package/dist/kysely.mjs +2 -0
- 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 +5 -2
- package/src/kysely.ts +14 -19
- 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;
|
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: 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 * 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:\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 ? 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: 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(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
|
|
@@ -114,6 +114,12 @@ interface KyselyAuditStorageConfig<DB> {
|
|
|
114
114
|
db: Kysely<DB>;
|
|
115
115
|
/** Table name for audit logs (must be a key in DB that extends AuditLogTable) */
|
|
116
116
|
tableName: keyof DB & string;
|
|
117
|
+
/**
|
|
118
|
+
* Service name of the database service.
|
|
119
|
+
* When set, endpoint adaptors will automatically use the audit transaction as `db`
|
|
120
|
+
* in the handler context if the endpoint's database service has the same name.
|
|
121
|
+
*/
|
|
122
|
+
databaseServiceName?: string;
|
|
117
123
|
}
|
|
118
124
|
/**
|
|
119
125
|
* Kysely-based audit storage implementation.
|
|
@@ -141,6 +147,7 @@ interface KyselyAuditStorageConfig<DB> {
|
|
|
141
147
|
declare class KyselyAuditStorage<DB> implements AuditStorage {
|
|
142
148
|
private readonly db;
|
|
143
149
|
private readonly tableName;
|
|
150
|
+
readonly databaseServiceName?: string;
|
|
144
151
|
constructor(config: KyselyAuditStorageConfig<DB>);
|
|
145
152
|
write(records: AuditRecord[], trx?: unknown): Promise<void>;
|
|
146
153
|
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
|
|
@@ -114,6 +114,12 @@ interface KyselyAuditStorageConfig<DB> {
|
|
|
114
114
|
db: Kysely<DB>;
|
|
115
115
|
/** Table name for audit logs (must be a key in DB that extends AuditLogTable) */
|
|
116
116
|
tableName: keyof DB & string;
|
|
117
|
+
/**
|
|
118
|
+
* Service name of the database service.
|
|
119
|
+
* When set, endpoint adaptors will automatically use the audit transaction as `db`
|
|
120
|
+
* in the handler context if the endpoint's database service has the same name.
|
|
121
|
+
*/
|
|
122
|
+
databaseServiceName?: string;
|
|
117
123
|
}
|
|
118
124
|
/**
|
|
119
125
|
* Kysely-based audit storage implementation.
|
|
@@ -141,6 +147,7 @@ interface KyselyAuditStorageConfig<DB> {
|
|
|
141
147
|
declare class KyselyAuditStorage<DB> implements AuditStorage {
|
|
142
148
|
private readonly db;
|
|
143
149
|
private readonly tableName;
|
|
150
|
+
readonly databaseServiceName?: string;
|
|
144
151
|
constructor(config: KyselyAuditStorageConfig<DB>);
|
|
145
152
|
write(records: AuditRecord[], trx?: unknown): Promise<void>;
|
|
146
153
|
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;
|
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: 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 * 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:\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 ? 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: 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(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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;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;;;;;;;;;;;;;;;;;;;;;;;;AAqED,IAAa,qBAAb,MAA4D;CAC1D,AAAiB;CACjB,AAAiB;CACjB,AAAS;CAET,YAAYC,QAAsC;AAChD,OAAK,KAAK,OAAO;AACjB,OAAK,YAAY,OAAO;AACxB,OAAK,sBAAsB,OAAO;CACnC;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,GAAW,WAAW,KAAK,UAAU,CAAC,OAAO,KAAK,CAAC,SAAS;CACpE;CAED,MAAM,MAAMC,SAAoD;EAC9D,IAAI,QAAQ,AAAC,KAAK,GAAW,WAAW,KAAK,UAAU,CAAC,WAAW;AAEnE,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,WAAW,KAAK,cAAc,IAAI,SAAS;GACzD,WAAW,IAAI,YAAY,KAAK,UAAU,IAAI,UAAU;GACxD,WAAW,IAAI,YAAY,KAAK,UAAU,IAAI,UAAU;GACxD,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,cAAcC,UAAoD;AACxE,MAAI;GACF,MAAM,SAAS,KAAK,MAAM,SAAS;AACnC,cAAW,WAAW,YAAY,WAAW,KAC3C,QAAO;AAET,UAAO;EACR,QAAO;AACN,UAAO;EACR;CACF;AACF"}
|
|
@@ -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
|
|
@@ -155,7 +155,10 @@ describe('KyselyAuditStorage', () => {
|
|
|
155
155
|
expect.objectContaining({
|
|
156
156
|
actorId: 'user-1',
|
|
157
157
|
actorType: 'user',
|
|
158
|
-
actorData: JSON.stringify({
|
|
158
|
+
actorData: JSON.stringify({
|
|
159
|
+
email: 'user@example.com',
|
|
160
|
+
role: 'admin',
|
|
161
|
+
}),
|
|
159
162
|
}),
|
|
160
163
|
]);
|
|
161
164
|
});
|
package/src/kysely.ts
CHANGED
|
@@ -157,6 +157,12 @@ export interface KyselyAuditStorageConfig<DB> {
|
|
|
157
157
|
db: Kysely<DB>;
|
|
158
158
|
/** Table name for audit logs (must be a key in DB that extends AuditLogTable) */
|
|
159
159
|
tableName: keyof DB & string;
|
|
160
|
+
/**
|
|
161
|
+
* Service name of the database service.
|
|
162
|
+
* When set, endpoint adaptors will automatically use the audit transaction as `db`
|
|
163
|
+
* in the handler context if the endpoint's database service has the same name.
|
|
164
|
+
*/
|
|
165
|
+
databaseServiceName?: string;
|
|
160
166
|
}
|
|
161
167
|
|
|
162
168
|
/**
|
|
@@ -185,10 +191,12 @@ export interface KyselyAuditStorageConfig<DB> {
|
|
|
185
191
|
export class KyselyAuditStorage<DB> implements AuditStorage {
|
|
186
192
|
private readonly db: Kysely<DB>;
|
|
187
193
|
private readonly tableName: keyof DB & string;
|
|
194
|
+
readonly databaseServiceName?: string;
|
|
188
195
|
|
|
189
196
|
constructor(config: KyselyAuditStorageConfig<DB>) {
|
|
190
197
|
this.db = config.db;
|
|
191
198
|
this.tableName = config.tableName;
|
|
199
|
+
this.databaseServiceName = config.databaseServiceName;
|
|
192
200
|
}
|
|
193
201
|
|
|
194
202
|
async write(records: AuditRecord[], trx?: unknown): Promise<void> {
|
|
@@ -199,16 +207,11 @@ export class KyselyAuditStorage<DB> implements AuditStorage {
|
|
|
199
207
|
const db = (trx as Transaction<DB>) ?? this.db;
|
|
200
208
|
const rows = records.map((record) => this.toRow(record));
|
|
201
209
|
|
|
202
|
-
await (db as any)
|
|
203
|
-
.insertInto(this.tableName)
|
|
204
|
-
.values(rows)
|
|
205
|
-
.execute();
|
|
210
|
+
await (db as any).insertInto(this.tableName).values(rows).execute();
|
|
206
211
|
}
|
|
207
212
|
|
|
208
213
|
async query(options: AuditQueryOptions): Promise<AuditRecord[]> {
|
|
209
|
-
let query = (this.db as any)
|
|
210
|
-
.selectFrom(this.tableName)
|
|
211
|
-
.selectAll();
|
|
214
|
+
let query = (this.db as any).selectFrom(this.tableName).selectAll();
|
|
212
215
|
|
|
213
216
|
query = this.applyFilters(query, options);
|
|
214
217
|
|
|
@@ -342,15 +345,9 @@ export class KyselyAuditStorage<DB> implements AuditStorage {
|
|
|
342
345
|
type: row.type,
|
|
343
346
|
operation: row.operation as AuditRecord['operation'],
|
|
344
347
|
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,
|
|
348
|
+
entityId: row.entityId ? this.parseEntityId(row.entityId) : undefined,
|
|
349
|
+
oldValues: row.oldValues ? this.parseJson(row.oldValues) : undefined,
|
|
350
|
+
newValues: row.newValues ? this.parseJson(row.newValues) : undefined,
|
|
354
351
|
payload: row.payload ? this.parseJson(row.payload) : undefined,
|
|
355
352
|
timestamp: row.timestamp,
|
|
356
353
|
actor,
|
|
@@ -375,9 +372,7 @@ export class KyselyAuditStorage<DB> implements AuditStorage {
|
|
|
375
372
|
return rest;
|
|
376
373
|
}
|
|
377
374
|
|
|
378
|
-
private parseEntityId(
|
|
379
|
-
entityId: string,
|
|
380
|
-
): string | Record<string, unknown> {
|
|
375
|
+
private parseEntityId(entityId: string): string | Record<string, unknown> {
|
|
381
376
|
try {
|
|
382
377
|
const parsed = JSON.parse(entityId);
|
|
383
378
|
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
|
}
|