@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.
@@ -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 AuditableAction,\n AuditActor,\n AuditMetadata,\n AuditOptions,\n AuditRecord,\n ExtractAuditPayload,\n ExtractAuditType,\n} from './types';\n\n/**\n * Configuration for DefaultAuditor.\n */\nexport interface DefaultAuditorConfig {\n /** The actor performing audits (set at construction, immutable) */\n actor: AuditActor;\n /** Storage backend for persisting audits */\n storage: AuditStorage;\n /** Optional metadata to attach to all audits */\n metadata?: AuditMetadata;\n /** Optional custom ID generator (defaults to nanoid) */\n generateId?: () => string;\n}\n\n/**\n * Default implementation of the Auditor interface.\n * Collects audit records in memory and flushes to storage.\n *\n * @template TAuditAction - Union of all allowed audit action types\n * @template TTransaction - Transaction type (e.g., Kysely Transaction)\n *\n * @example\n * ```typescript\n * const auditor = new DefaultAuditor<AppAuditAction>({\n * actor: { id: 'user-123', type: 'user' },\n * storage: auditStorage,\n * metadata: { requestId: 'req-456', endpoint: '/users' },\n * });\n *\n * auditor.audit('user.created', { userId: '789', email: 'test@example.com' });\n *\n * // Flush inside transaction\n * await auditor.flush(trx);\n * ```\n */\nexport class DefaultAuditor<\n TAuditAction extends AuditableAction<string, unknown> = AuditableAction<\n string,\n unknown\n >,\n TTransaction = unknown,\n> implements Auditor<TAuditAction, TTransaction>\n{\n readonly actor: AuditActor;\n private readonly storage: AuditStorage;\n private metadata?: AuditMetadata;\n private readonly generateId: () => string;\n private records: AuditRecord[] = [];\n private transaction?: TTransaction;\n\n constructor(config: DefaultAuditorConfig) {\n this.actor = config.actor;\n this.storage = config.storage;\n this.metadata = config.metadata;\n this.generateId = config.generateId ?? (() => nanoid());\n }\n\n audit<TType extends ExtractAuditType<TAuditAction>>(\n type: TType,\n payload: ExtractAuditPayload<TAuditAction, TType>,\n options?: AuditOptions,\n ): void {\n const record: AuditRecord = {\n id: this.generateId(),\n type,\n operation: options?.operation ?? 'CUSTOM',\n table: options?.table,\n entityId: options?.entityId,\n oldValues: options?.oldValues,\n newValues: options?.newValues,\n payload,\n timestamp: new Date(),\n actor: this.actor,\n metadata: this.metadata,\n };\n\n this.records.push(record);\n }\n\n record(record: Omit<AuditRecord, 'id' | 'timestamp' | 'actor'>): void {\n const fullRecord: AuditRecord = {\n ...record,\n id: this.generateId(),\n timestamp: new Date(),\n actor: this.actor,\n metadata: this.metadata\n ? { ...this.metadata, ...record.metadata }\n : record.metadata,\n };\n\n this.records.push(fullRecord);\n }\n\n getRecords(): AuditRecord[] {\n return [...this.records];\n }\n\n async flush(trx?: TTransaction): Promise<void> {\n if (this.records.length === 0) {\n return;\n }\n\n const recordsToFlush = [...this.records];\n this.records = [];\n\n // Use explicitly passed transaction, or fall back to stored transaction\n const transactionToUse = trx ?? this.transaction;\n await this.storage.write(recordsToFlush, transactionToUse);\n }\n\n setTransaction(trx: TTransaction): void {\n this.transaction = trx;\n }\n\n getTransaction(): TTransaction | undefined {\n return this.transaction;\n }\n\n clear(): void {\n this.records = [];\n }\n\n addMetadata(metadata: AuditMetadata): void {\n this.metadata = this.metadata\n ? { ...this.metadata, ...metadata }\n : metadata;\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgDA,IAAa,iBAAb,MAOA;CACE,AAAS;CACT,AAAiB;CACjB,AAAQ;CACR,AAAiB;CACjB,AAAQ,UAAyB,CAAE;CACnC,AAAQ;CAER,YAAYA,QAA8B;AACxC,OAAK,QAAQ,OAAO;AACpB,OAAK,UAAU,OAAO;AACtB,OAAK,WAAW,OAAO;AACvB,OAAK,aAAa,OAAO,eAAe,MAAM,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
+ {"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-DnAMfOA7.mjs";
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-B9Unin1g.d.mts.map
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 AuditableAction,\n AuditActor,\n AuditMetadata,\n AuditOptions,\n AuditRecord,\n ExtractAuditPayload,\n ExtractAuditType,\n} from './types';\n\n/**\n * Configuration for DefaultAuditor.\n */\nexport interface DefaultAuditorConfig {\n /** The actor performing audits (set at construction, immutable) */\n actor: AuditActor;\n /** Storage backend for persisting audits */\n storage: AuditStorage;\n /** Optional metadata to attach to all audits */\n metadata?: AuditMetadata;\n /** Optional custom ID generator (defaults to nanoid) */\n generateId?: () => string;\n}\n\n/**\n * Default implementation of the Auditor interface.\n * Collects audit records in memory and flushes to storage.\n *\n * @template TAuditAction - Union of all allowed audit action types\n * @template TTransaction - Transaction type (e.g., Kysely Transaction)\n *\n * @example\n * ```typescript\n * const auditor = new DefaultAuditor<AppAuditAction>({\n * actor: { id: 'user-123', type: 'user' },\n * storage: auditStorage,\n * metadata: { requestId: 'req-456', endpoint: '/users' },\n * });\n *\n * auditor.audit('user.created', { userId: '789', email: 'test@example.com' });\n *\n * // Flush inside transaction\n * await auditor.flush(trx);\n * ```\n */\nexport class DefaultAuditor<\n TAuditAction extends AuditableAction<string, unknown> = AuditableAction<\n string,\n unknown\n >,\n TTransaction = unknown,\n> implements Auditor<TAuditAction, TTransaction>\n{\n readonly actor: AuditActor;\n private readonly storage: AuditStorage;\n private metadata?: AuditMetadata;\n private readonly generateId: () => string;\n private records: AuditRecord[] = [];\n private transaction?: TTransaction;\n\n constructor(config: DefaultAuditorConfig) {\n this.actor = config.actor;\n this.storage = config.storage;\n this.metadata = config.metadata;\n this.generateId = config.generateId ?? (() => nanoid());\n }\n\n audit<TType extends ExtractAuditType<TAuditAction>>(\n type: TType,\n payload: ExtractAuditPayload<TAuditAction, TType>,\n options?: AuditOptions,\n ): void {\n const record: AuditRecord = {\n id: this.generateId(),\n type,\n operation: options?.operation ?? 'CUSTOM',\n table: options?.table,\n entityId: options?.entityId,\n oldValues: options?.oldValues,\n newValues: options?.newValues,\n payload,\n timestamp: new Date(),\n actor: this.actor,\n metadata: this.metadata,\n };\n\n this.records.push(record);\n }\n\n record(record: Omit<AuditRecord, 'id' | 'timestamp' | 'actor'>): void {\n const fullRecord: AuditRecord = {\n ...record,\n id: this.generateId(),\n timestamp: new Date(),\n actor: this.actor,\n metadata: this.metadata\n ? { ...this.metadata, ...record.metadata }\n : record.metadata,\n };\n\n this.records.push(fullRecord);\n }\n\n getRecords(): AuditRecord[] {\n return [...this.records];\n }\n\n async flush(trx?: TTransaction): Promise<void> {\n if (this.records.length === 0) {\n return;\n }\n\n const recordsToFlush = [...this.records];\n this.records = [];\n\n // Use explicitly passed transaction, or fall back to stored transaction\n const transactionToUse = trx ?? this.transaction;\n await this.storage.write(recordsToFlush, transactionToUse);\n }\n\n setTransaction(trx: TTransaction): void {\n this.transaction = trx;\n }\n\n getTransaction(): TTransaction | undefined {\n return this.transaction;\n }\n\n clear(): void {\n this.records = [];\n }\n\n addMetadata(metadata: AuditMetadata): void {\n this.metadata = this.metadata\n ? { ...this.metadata, ...metadata }\n : metadata;\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AAgDA,IAAa,iBAAb,MAOA;CACE,AAAS;CACT,AAAiB;CACjB,AAAQ;CACR,AAAiB;CACjB,AAAQ,UAAyB,CAAE;CACnC,AAAQ;CAER,YAAYA,QAA8B;AACxC,OAAK,QAAQ,OAAO;AACpB,OAAK,UAAU,OAAO;AACtB,OAAK,WAAW,OAAO;AACvB,OAAK,aAAa,OAAO,eAAe,MAAM,QAAQ;CACvD;CAED,MACEC,MACAC,SACAC,SACM;EACN,MAAMC,SAAsB;GAC1B,IAAI,KAAK,YAAY;GACrB;GACA,WAAW,SAAS,aAAa;GACjC,OAAO,SAAS;GAChB,UAAU,SAAS;GACnB,WAAW,SAAS;GACpB,WAAW,SAAS;GACpB;GACA,2BAAW,IAAI;GACf,OAAO,KAAK;GACZ,UAAU,KAAK;EAChB;AAED,OAAK,QAAQ,KAAK,OAAO;CAC1B;CAED,OAAOC,QAA+D;EACpE,MAAMC,aAA0B;GAC9B,GAAG;GACH,IAAI,KAAK,YAAY;GACrB,2BAAW,IAAI;GACf,OAAO,KAAK;GACZ,UAAU,KAAK,WACX;IAAE,GAAG,KAAK;IAAU,GAAG,OAAO;GAAU,IACxC,OAAO;EACZ;AAED,OAAK,QAAQ,KAAK,WAAW;CAC9B;CAED,aAA4B;AAC1B,SAAO,CAAC,GAAG,KAAK,OAAQ;CACzB;CAED,MAAM,MAAMC,KAAmC;AAC7C,MAAI,KAAK,QAAQ,WAAW,EAC1B;EAGF,MAAM,iBAAiB,CAAC,GAAG,KAAK,OAAQ;AACxC,OAAK,UAAU,CAAE;EAGjB,MAAM,mBAAmB,OAAO,KAAK;AACrC,QAAM,KAAK,QAAQ,MAAM,gBAAgB,iBAAiB;CAC3D;CAED,eAAeC,KAAyB;AACtC,OAAK,cAAc;CACpB;CAED,iBAA2C;AACzC,SAAO,KAAK;CACb;CAED,QAAc;AACZ,OAAK,UAAU,CAAE;CAClB;CAED,YAAYC,UAA+B;AACzC,OAAK,WAAW,KAAK,WACjB;GAAE,GAAG,KAAK;GAAU,GAAG;EAAU,IACjC;CACL;AACF"}
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-BtFY7Rha.cjs";
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-Dqc4UZA1.d.cts.map
59
+ //# sourceMappingURL=DefaultAuditor-RC-F_fTc.d.cts.map
@@ -1,4 +1,4 @@
1
1
  import "./Auditor-CZ8lkASv.cjs";
2
- import "./storage-BtFY7Rha.cjs";
3
- import { DefaultAuditor, DefaultAuditorConfig } from "./DefaultAuditor-Dqc4UZA1.cjs";
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-DnAMfOA7.mjs";
3
- import { DefaultAuditor, DefaultAuditorConfig } from "./DefaultAuditor-B9Unin1g.mjs";
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-BtFY7Rha.cjs";
3
- import { DefaultAuditor, DefaultAuditorConfig } from "./DefaultAuditor-Dqc4UZA1.cjs";
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-DnAMfOA7.mjs";
3
- import { DefaultAuditor, DefaultAuditorConfig } from "./DefaultAuditor-B9Unin1g.mjs";
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;
@@ -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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;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"}
package/dist/kysely.d.cts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { AuditRecord } from "./Auditor-CZ8lkASv.cjs";
2
- import { AuditQueryOptions, AuditStorage } from "./storage-BtFY7Rha.cjs";
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-DnAMfOA7.mjs";
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;
@@ -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":"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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-DnAMfOA7.d.mts.map
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-BtFY7Rha.d.cts.map
117
+ //# sourceMappingURL=storage-ndQzIcWK.d.cts.map
@@ -1,3 +1,3 @@
1
1
  import "./Auditor-CZ8lkASv.cjs";
2
- import { AuditQueryOptions, AuditStorage } from "./storage-BtFY7Rha.cjs";
2
+ import { AuditQueryOptions, AuditStorage } from "./storage-ndQzIcWK.cjs";
3
3
  export { AuditQueryOptions, AuditStorage };
@@ -1,3 +1,3 @@
1
1
  import "./Auditor-ii62d_pF.mjs";
2
- import { AuditQueryOptions, AuditStorage } from "./storage-DnAMfOA7.mjs";
2
+ import { AuditQueryOptions, AuditStorage } from "./storage-BmA3SESr.mjs";
3
3
  export { AuditQueryOptions, AuditStorage };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geekmidas/audit",
3
- "version": "0.0.1",
3
+ "version": "0.0.2",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
package/src/Auditor.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  import type {
2
- AuditableAction,
3
2
  AuditActor,
4
3
  AuditMetadata,
5
4
  AuditOptions,
6
5
  AuditRecord,
6
+ AuditableAction,
7
7
  ExtractAuditPayload,
8
8
  ExtractAuditType,
9
9
  } from './types';
@@ -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 { AuditableAction, AuditRecord } from '../types';
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', { userId: '123', email: 'test@example.com' });
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', { userId: '456', email: 'test@example.com' });
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', { userId: '456', email: 'test@example.com' });
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', { userId: '456', email: 'test@example.com' });
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', { userId: '456', email: 'test@example.com' });
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', { userId: '456', email: 'test@example.com' });
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', { userId: '456', email: 'test@example.com' });
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', { userId: '1', email: 'first@example.com' });
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', { userId: '2', email: 'second@example.com' });
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', { userId: '456', email: 'test@example.com' });
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', { userId: '123', email: 'test@example.com' });
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', { userId: '456', email: 'test@example.com' });
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', { userId: '456', email: 'test@example.com' });
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', { userId: '456', email: 'test@example.com' });
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', { userId: '1', email: 'first@example.com' });
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', { userId: '2', email: 'second@example.com' });
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', { userId: '456', email: 'test@example.com' });
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 { KyselyAuditStorage, type AuditLogTable } from '../kysely';
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 { KyselyAuditStorage, type AuditLogTable } from '../kysely';
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({ email: 'user@example.com', role: 'admin' }),
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
- ? this.parseEntityId(row.entityId)
347
- : undefined,
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
  }