@geekmidas/audit 0.0.3 → 0.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/kysely.d.mts CHANGED
@@ -1,5 +1,4 @@
1
- import { AuditRecord } from "./Auditor-ii62d_pF.mjs";
2
- import { AuditQueryOptions, AuditStorage } from "./storage-BmA3SESr.mjs";
1
+ import { AuditQueryOptions, AuditRecord, AuditStorage } from "./Auditor-D3me-qKX.mjs";
3
2
  import { ControlledTransaction, IsolationLevel, Kysely, Transaction } from "kysely";
4
3
 
5
4
  //#region src/kysely.d.ts
@@ -108,6 +107,23 @@ interface AuditLogTable {
108
107
  actorData: unknown | null;
109
108
  metadata: unknown | null;
110
109
  }
110
+ /**
111
+ * Insertable version of AuditLogTable where id is optional.
112
+ * Use this when your database auto-generates IDs or when using autoId option.
113
+ *
114
+ * @example
115
+ * ```typescript
116
+ * interface Database {
117
+ * audit_logs: AuditLogTable;
118
+ * }
119
+ *
120
+ * // For insertions where id is auto-generated
121
+ * type NewAuditLog = InsertableAuditLogTable;
122
+ * ```
123
+ */
124
+ type InsertableAuditLogTable = Omit<AuditLogTable, 'id'> & {
125
+ id?: string;
126
+ };
111
127
  /**
112
128
  * Configuration for KyselyAuditStorage.
113
129
  */
@@ -122,6 +138,13 @@ interface KyselyAuditStorageConfig<DB> {
122
138
  * in the handler context if the endpoint's database service has the same name.
123
139
  */
124
140
  databaseServiceName?: string;
141
+ /**
142
+ * Let the database auto-generate IDs (e.g., via DEFAULT gen_random_uuid()).
143
+ * When true, the ID field is omitted from inserts if not provided.
144
+ * When false (default), IDs are generated using nanoid if not provided.
145
+ * @default false
146
+ */
147
+ autoId?: boolean;
125
148
  }
126
149
  /**
127
150
  * Kysely-based audit storage implementation.
@@ -149,6 +172,7 @@ interface KyselyAuditStorageConfig<DB> {
149
172
  declare class KyselyAuditStorage<DB> implements AuditStorage {
150
173
  private readonly db;
151
174
  private readonly tableName;
175
+ private readonly autoId;
152
176
  readonly databaseServiceName?: string;
153
177
  constructor(config: KyselyAuditStorageConfig<DB>);
154
178
  write(records: AuditRecord[], trx?: unknown): Promise<void>;
@@ -170,5 +194,5 @@ declare class KyselyAuditStorage<DB> implements AuditStorage {
170
194
  private parseEntityId;
171
195
  }
172
196
  //#endregion
173
- export { AuditLogTable, DatabaseConnection, KyselyAuditStorage, KyselyAuditStorageConfig, TransactionAwareAuditor, TransactionSettings, withAuditableTransaction };
197
+ export { AuditLogTable, DatabaseConnection, InsertableAuditLogTable, KyselyAuditStorage, KyselyAuditStorageConfig, TransactionAwareAuditor, TransactionSettings, withAuditableTransaction };
174
198
  //# sourceMappingURL=kysely.d.mts.map
package/dist/kysely.mjs CHANGED
@@ -1,3 +1,5 @@
1
+ import { nanoid } from "nanoid";
2
+
1
3
  //#region src/kysely.ts
2
4
  /**
3
5
  * Execute a callback within a database transaction with automatic audit handling.
@@ -80,11 +82,13 @@ async function withAuditableTransaction(db, auditor, cb, settings) {
80
82
  var KyselyAuditStorage = class {
81
83
  db;
82
84
  tableName;
85
+ autoId;
83
86
  databaseServiceName;
84
87
  constructor(config) {
85
88
  this.db = config.db;
86
89
  this.tableName = config.tableName;
87
90
  this.databaseServiceName = config.databaseServiceName;
91
+ this.autoId = config.autoId ?? false;
88
92
  }
89
93
  async write(records, trx) {
90
94
  if (records.length === 0) return;
@@ -130,8 +134,9 @@ var KyselyAuditStorage = class {
130
134
  return query;
131
135
  }
132
136
  toRow(record) {
137
+ const id = record.id || (this.autoId ? void 0 : nanoid());
133
138
  return {
134
- id: record.id,
139
+ ...id && { id },
135
140
  type: record.type,
136
141
  operation: record.operation,
137
142
  table: record.table ?? null,
@@ -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: unknown","actor: NonNullable<AuditRecord['actor']>","entityId: string"],"sources":["../src/kysely.ts"],"sourcesContent":["import type {\n ControlledTransaction,\n IsolationLevel,\n Kysely,\n Transaction,\n} from 'kysely';\nimport type { AuditQueryOptions, AuditStorage } from './storage';\nimport type { AuditRecord } from './types';\n\n/**\n * Minimal interface for transaction-aware audit flushing.\n * Use this when you need to flush audits within a database transaction.\n *\n * @template TTransaction - Transaction type (e.g., Kysely Transaction)\n *\n * @example\n * ```typescript\n * import { withAuditableTransaction } from '@geekmidas/audit/kysely';\n * import type { TransactionAwareAuditor } from '@geekmidas/audit/kysely';\n *\n * const result = await withAuditableTransaction(\n * db,\n * auditor as TransactionAwareAuditor<Transaction<DB>>,\n * async (trx) => {\n * // Your transactional operations\n * return result;\n * },\n * );\n * ```\n */\nexport interface TransactionAwareAuditor<TTransaction = unknown> {\n /** Register the transaction with the auditor for use during flush */\n setTransaction(trx: TTransaction): void;\n /** Flush all pending audits, optionally within a transaction */\n flush(trx?: TTransaction): Promise<void>;\n}\n\nexport interface TransactionSettings {\n isolationLevel?: IsolationLevel;\n}\n\nexport type DatabaseConnection<T> =\n | ControlledTransaction<T>\n | Kysely<T>\n | Transaction<T>;\n\n/**\n * Execute a callback within a database transaction with automatic audit handling.\n *\n * This wrapper ensures that:\n * 1. The transaction is automatically registered with the auditor\n * 2. Manual audits (via `auditor.audit()`) are flushed BEFORE the transaction commits\n * 3. If audit flush fails, the entire transaction rolls back\n * 4. If the callback fails, audits are NOT written (atomic consistency)\n *\n * **Note:** Declarative audits (defined via `.audit([...])` on the endpoint builder)\n * are processed AFTER the handler returns, so they run outside this transaction.\n * If you need all audits to be atomic with your database operations, use manual\n * audits via `auditor.audit()` inside this wrapper.\n *\n * @param db - Database connection (Kysely, Transaction, or ControlledTransaction)\n * @param auditor - Auditor instance that will receive the transaction\n * @param cb - Callback to execute within the transaction\n * @param settings - Optional transaction settings (isolation level)\n * @returns The result of the callback\n *\n * @example\n * ```typescript\n * import { withAuditableTransaction } from '@geekmidas/audit/kysely';\n *\n * const result = await withAuditableTransaction(\n * services.database,\n * auditor,\n * async (trx) => {\n * const user = await trx\n * .insertInto('users')\n * .values(data)\n * .returningAll()\n * .executeTakeFirstOrThrow();\n *\n * // Manual audits are atomic with the transaction\n * auditor.audit('user.created', { userId: user.id, email: user.email });\n *\n * return user;\n * },\n * );\n * // Audits are automatically flushed inside the transaction before commit\n * ```\n */\nexport async function withAuditableTransaction<DB, T>(\n db: DatabaseConnection<DB>,\n auditor: TransactionAwareAuditor<Transaction<DB>>,\n cb: (trx: Transaction<DB>) => Promise<T>,\n settings?: TransactionSettings,\n): Promise<T> {\n const execute = async (trx: Transaction<DB>): Promise<T> => {\n // Register transaction with auditor\n auditor.setTransaction(trx);\n\n // Execute the callback\n const result = await cb(trx);\n\n // Flush audits BEFORE transaction commits\n // If this fails, the transaction will roll back\n await auditor.flush(trx);\n\n return result;\n };\n\n // If already in a transaction, just run with it\n if (db.isTransaction) {\n return execute(db as Transaction<DB>);\n }\n\n const builder = db.transaction();\n\n if (settings?.isolationLevel) {\n return builder.setIsolationLevel(settings.isolationLevel).execute(execute);\n }\n\n return builder.execute(execute);\n}\n\n/**\n * Database table interface for audit records.\n * Use this to define your audit_logs table in your Kysely database schema.\n *\n * Column names use snake_case to match standard PostgreSQL conventions.\n *\n * @example\n * ```typescript\n * interface Database {\n * audit_logs: AuditLogTable;\n * // ... other tables\n * }\n * ```\n */\nexport interface AuditLogTable {\n id: string;\n type: string;\n operation: string;\n table: string | null;\n entityId: string | null;\n oldValues: unknown | null;\n newValues: unknown | null;\n payload: unknown | null;\n timestamp: Date;\n actorId: string | null;\n actorType: string | null;\n actorData: unknown | null;\n metadata: unknown | null;\n}\n\n/**\n * Configuration for KyselyAuditStorage.\n */\nexport interface KyselyAuditStorageConfig<DB> {\n /** Kysely database instance */\n db: Kysely<DB>;\n /** Table name for audit logs (must be a key in DB that extends AuditLogTable) */\n tableName: keyof DB & string;\n /**\n * Service name of the database service.\n * When set, endpoint adaptors will automatically use the audit transaction as `db`\n * in the handler context if the endpoint's database service has the same name.\n */\n databaseServiceName?: string;\n}\n\n/**\n * Kysely-based audit storage implementation.\n * Stores audit records in a database table using Kysely.\n *\n * @template DB - Your Kysely database schema\n *\n * @example\n * ```typescript\n * interface Database {\n * audit_logs: AuditLogTable;\n * }\n *\n * const storage = new KyselyAuditStorage({\n * db: kyselyDb,\n * tableName: 'audit_logs',\n * });\n *\n * const auditor = new DefaultAuditor({\n * actor: { id: 'user-123', type: 'user' },\n * storage,\n * });\n * ```\n */\nexport class KyselyAuditStorage<DB> implements AuditStorage {\n private readonly db: Kysely<DB>;\n private readonly tableName: keyof DB & string;\n readonly databaseServiceName?: string;\n\n constructor(config: KyselyAuditStorageConfig<DB>) {\n this.db = config.db;\n this.tableName = config.tableName;\n this.databaseServiceName = config.databaseServiceName;\n }\n\n async write(records: AuditRecord[], trx?: unknown): Promise<void> {\n if (records.length === 0) {\n return;\n }\n\n const db = (trx as Transaction<DB>) ?? this.db;\n const rows = records.map((record) => this.toRow(record));\n\n await (db as any).insertInto(this.tableName).values(rows).execute();\n }\n\n async query(options: AuditQueryOptions): Promise<AuditRecord[]> {\n let query = (this.db as any).selectFrom(this.tableName).selectAll();\n\n query = this.applyFilters(query, options);\n\n // Ordering\n const orderBy = options.orderBy ?? 'timestamp';\n const orderDirection = options.orderDirection ?? 'desc';\n query = query.orderBy(\n orderBy === 'timestamp' ? 'timestamp' : 'type',\n orderDirection,\n );\n\n // Pagination\n if (options.limit !== undefined) {\n query = query.limit(options.limit);\n }\n if (options.offset !== undefined) {\n query = query.offset(options.offset);\n }\n\n const rows = await query.execute();\n return rows.map((row: AuditLogTable) => this.fromRow(row));\n }\n\n async count(\n options: Omit<AuditQueryOptions, 'limit' | 'offset'>,\n ): Promise<number> {\n let query = (this.db as any)\n .selectFrom(this.tableName)\n .select((eb: any) => eb.fn.count('id').as('count'));\n\n query = this.applyFilters(query, options);\n\n const result = await query.executeTakeFirst();\n return Number(result?.count ?? 0);\n }\n\n /**\n * Get the Kysely database instance for transactional operations.\n * Used by endpoint adaptors to automatically wrap handlers in transactions.\n */\n getDatabase(): Kysely<DB> {\n return this.db;\n }\n\n private applyFilters(query: any, options: AuditQueryOptions): any {\n // Type filter\n if (options.type !== undefined) {\n if (Array.isArray(options.type)) {\n query = query.where('type', 'in', options.type);\n } else {\n query = query.where('type', '=', options.type);\n }\n }\n\n // Entity ID filter\n if (options.entityId !== undefined) {\n const entityId =\n typeof options.entityId === 'string'\n ? options.entityId\n : JSON.stringify(options.entityId);\n query = query.where('entityId', '=', entityId);\n }\n\n // Table filter\n if (options.table !== undefined) {\n query = query.where('table', '=', options.table);\n }\n\n // Actor ID filter\n if (options.actorId !== undefined) {\n query = query.where('actorId', '=', options.actorId);\n }\n\n // Date range filters\n if (options.from !== undefined) {\n query = query.where('timestamp', '>=', options.from);\n }\n if (options.to !== undefined) {\n query = query.where('timestamp', '<=', options.to);\n }\n\n return query;\n }\n\n private toRow(record: AuditRecord): AuditLogTable {\n return {\n id: record.id,\n type: record.type,\n operation: record.operation,\n table: record.table ?? null,\n entityId:\n record.entityId === undefined\n ? null\n : typeof record.entityId === 'string'\n ? record.entityId\n : JSON.stringify(record.entityId),\n oldValues: record.oldValues ?? null,\n newValues: record.newValues ?? null,\n payload: record.payload ?? null,\n timestamp: record.timestamp,\n actorId: record.actor?.id ?? null,\n actorType: record.actor?.type ?? null,\n actorData:\n record.actor !== undefined ? this.getActorData(record.actor) : null,\n metadata: record.metadata ?? null,\n };\n }\n\n private fromRow(row: AuditLogTable): AuditRecord {\n const actor =\n row.actorId !== null || row.actorType !== null\n ? {\n id: row.actorId ?? undefined,\n type: row.actorType ?? undefined,\n ...(row.actorData ? this.parseJson(row.actorData) : {}),\n }\n : undefined;\n\n return {\n id: row.id,\n type: row.type,\n operation: row.operation as AuditRecord['operation'],\n table: row.table ?? undefined,\n entityId: row.entityId ? this.parseEntityId(row.entityId) : undefined,\n oldValues: row.oldValues ? this.parseJson(row.oldValues) : undefined,\n newValues: row.newValues ? this.parseJson(row.newValues) : undefined,\n payload: row.payload ? this.parseJson(row.payload) : undefined,\n timestamp: row.timestamp,\n actor,\n metadata: row.metadata ? this.parseJson(row.metadata) : undefined,\n };\n }\n\n /**\n * Parse a JSON value that may already be parsed (e.g., from jsonb columns).\n */\n private parseJson(value: unknown): Record<string, unknown> {\n if (typeof value === 'object' && value !== null) {\n return value as Record<string, unknown>;\n }\n if (typeof value === 'string') {\n return JSON.parse(value);\n }\n return {};\n }\n\n private getActorData(\n actor: NonNullable<AuditRecord['actor']>,\n ): Record<string, unknown> {\n const { id, type, ...rest } = actor;\n return rest;\n }\n\n private parseEntityId(entityId: string): string | Record<string, unknown> {\n try {\n const parsed = JSON.parse(entityId);\n if (typeof parsed === 'object' && parsed !== null) {\n return parsed;\n }\n return entityId;\n } catch {\n return entityId;\n }\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"kysely.mjs","names":["db: DatabaseConnection<DB>","auditor: TransactionAwareAuditor<Transaction<DB>>","cb: (trx: Transaction<DB>) => Promise<T>","settings?: TransactionSettings","trx: Transaction<DB>","config: KyselyAuditStorageConfig<DB>","records: AuditRecord[]","trx?: unknown","options: AuditQueryOptions","row: AuditLogTable","options: Omit<AuditQueryOptions, 'limit' | 'offset'>","eb: any","query: any","record: AuditRecord","value: unknown","actor: NonNullable<AuditRecord['actor']>","entityId: string"],"sources":["../src/kysely.ts"],"sourcesContent":["import type {\n ControlledTransaction,\n IsolationLevel,\n Kysely,\n Transaction,\n} from 'kysely';\nimport { nanoid } from 'nanoid';\nimport type { AuditQueryOptions, AuditStorage } from './storage';\nimport type { AuditRecord } from './types';\n\n/**\n * Minimal interface for transaction-aware audit flushing.\n * Use this when you need to flush audits within a database transaction.\n *\n * @template TTransaction - Transaction type (e.g., Kysely Transaction)\n *\n * @example\n * ```typescript\n * import { withAuditableTransaction } from '@geekmidas/audit/kysely';\n * import type { TransactionAwareAuditor } from '@geekmidas/audit/kysely';\n *\n * const result = await withAuditableTransaction(\n * db,\n * auditor as TransactionAwareAuditor<Transaction<DB>>,\n * async (trx) => {\n * // Your transactional operations\n * return result;\n * },\n * );\n * ```\n */\nexport interface TransactionAwareAuditor<TTransaction = unknown> {\n /** Register the transaction with the auditor for use during flush */\n setTransaction(trx: TTransaction): void;\n /** Flush all pending audits, optionally within a transaction */\n flush(trx?: TTransaction): Promise<void>;\n}\n\nexport interface TransactionSettings {\n isolationLevel?: IsolationLevel;\n}\n\nexport type DatabaseConnection<T> =\n | ControlledTransaction<T>\n | Kysely<T>\n | Transaction<T>;\n\n/**\n * Execute a callback within a database transaction with automatic audit handling.\n *\n * This wrapper ensures that:\n * 1. The transaction is automatically registered with the auditor\n * 2. Manual audits (via `auditor.audit()`) are flushed BEFORE the transaction commits\n * 3. If audit flush fails, the entire transaction rolls back\n * 4. If the callback fails, audits are NOT written (atomic consistency)\n *\n * **Note:** Declarative audits (defined via `.audit([...])` on the endpoint builder)\n * are processed AFTER the handler returns, so they run outside this transaction.\n * If you need all audits to be atomic with your database operations, use manual\n * audits via `auditor.audit()` inside this wrapper.\n *\n * @param db - Database connection (Kysely, Transaction, or ControlledTransaction)\n * @param auditor - Auditor instance that will receive the transaction\n * @param cb - Callback to execute within the transaction\n * @param settings - Optional transaction settings (isolation level)\n * @returns The result of the callback\n *\n * @example\n * ```typescript\n * import { withAuditableTransaction } from '@geekmidas/audit/kysely';\n *\n * const result = await withAuditableTransaction(\n * services.database,\n * auditor,\n * async (trx) => {\n * const user = await trx\n * .insertInto('users')\n * .values(data)\n * .returningAll()\n * .executeTakeFirstOrThrow();\n *\n * // Manual audits are atomic with the transaction\n * auditor.audit('user.created', { userId: user.id, email: user.email });\n *\n * return user;\n * },\n * );\n * // Audits are automatically flushed inside the transaction before commit\n * ```\n */\nexport async function withAuditableTransaction<DB, T>(\n db: DatabaseConnection<DB>,\n auditor: TransactionAwareAuditor<Transaction<DB>>,\n cb: (trx: Transaction<DB>) => Promise<T>,\n settings?: TransactionSettings,\n): Promise<T> {\n const execute = async (trx: Transaction<DB>): Promise<T> => {\n // Register transaction with auditor\n auditor.setTransaction(trx);\n\n // Execute the callback\n const result = await cb(trx);\n\n // Flush audits BEFORE transaction commits\n // If this fails, the transaction will roll back\n await auditor.flush(trx);\n\n return result;\n };\n\n // If already in a transaction, just run with it\n if (db.isTransaction) {\n return execute(db as Transaction<DB>);\n }\n\n const builder = db.transaction();\n\n if (settings?.isolationLevel) {\n return builder.setIsolationLevel(settings.isolationLevel).execute(execute);\n }\n\n return builder.execute(execute);\n}\n\n/**\n * Database table interface for audit records.\n * Use this to define your audit_logs table in your Kysely database schema.\n *\n * Column names use snake_case to match standard PostgreSQL conventions.\n *\n * @example\n * ```typescript\n * interface Database {\n * audit_logs: AuditLogTable;\n * // ... other tables\n * }\n * ```\n */\nexport interface AuditLogTable {\n id: string;\n type: string;\n operation: string;\n table: string | null;\n entityId: string | null;\n oldValues: unknown | null;\n newValues: unknown | null;\n payload: unknown | null;\n timestamp: Date;\n actorId: string | null;\n actorType: string | null;\n actorData: unknown | null;\n metadata: unknown | null;\n}\n\n/**\n * Insertable version of AuditLogTable where id is optional.\n * Use this when your database auto-generates IDs or when using autoId option.\n *\n * @example\n * ```typescript\n * interface Database {\n * audit_logs: AuditLogTable;\n * }\n *\n * // For insertions where id is auto-generated\n * type NewAuditLog = InsertableAuditLogTable;\n * ```\n */\nexport type InsertableAuditLogTable = Omit<AuditLogTable, 'id'> & {\n id?: string;\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 * Let the database auto-generate IDs (e.g., via DEFAULT gen_random_uuid()).\n * When true, the ID field is omitted from inserts if not provided.\n * When false (default), IDs are generated using nanoid if not provided.\n * @default false\n */\n autoId?: boolean;\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 private readonly autoId: boolean;\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 this.autoId = config.autoId ?? false;\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 // If autoId is true, let database generate ID (omit if not provided)\n // If autoId is false (default), generate with nanoid if not provided\n const id = record.id || (this.autoId ? undefined : nanoid());\n\n return {\n ...(id && { id }),\n type: record.type,\n operation: record.operation,\n table: record.table ?? null,\n entityId:\n record.entityId === undefined\n ? null\n : typeof record.entityId === 'string'\n ? record.entityId\n : JSON.stringify(record.entityId),\n oldValues: record.oldValues ?? null,\n newValues: record.newValues ?? null,\n payload: record.payload ?? null,\n timestamp: record.timestamp,\n actorId: record.actor?.id ?? null,\n actorType: record.actor?.type ?? null,\n actorData:\n record.actor !== undefined ? this.getActorData(record.actor) : null,\n metadata: record.metadata ?? null,\n } as AuditLogTable;\n }\n\n private fromRow(row: AuditLogTable): AuditRecord {\n const actor =\n row.actorId !== null || row.actorType !== null\n ? {\n id: row.actorId ?? undefined,\n type: row.actorType ?? undefined,\n ...(row.actorData ? this.parseJson(row.actorData) : {}),\n }\n : undefined;\n\n return {\n id: row.id,\n type: row.type,\n operation: row.operation as AuditRecord['operation'],\n table: row.table ?? undefined,\n entityId: row.entityId ? this.parseEntityId(row.entityId) : undefined,\n oldValues: row.oldValues ? this.parseJson(row.oldValues) : undefined,\n newValues: row.newValues ? this.parseJson(row.newValues) : undefined,\n payload: row.payload ? this.parseJson(row.payload) : undefined,\n timestamp: row.timestamp,\n actor,\n metadata: row.metadata ? this.parseJson(row.metadata) : undefined,\n };\n }\n\n /**\n * Parse a JSON value that may already be parsed (e.g., from jsonb columns).\n */\n private parseJson(value: unknown): Record<string, unknown> {\n if (typeof value === 'object' && value !== null) {\n return value as Record<string, unknown>;\n }\n if (typeof value === 'string') {\n return JSON.parse(value);\n }\n return {};\n }\n\n private getActorData(\n actor: NonNullable<AuditRecord['actor']>,\n ): Record<string, unknown> {\n const { id, type, ...rest } = actor;\n return rest;\n }\n\n private parseEntityId(entityId: string): string | Record<string, unknown> {\n try {\n const parsed = JSON.parse(entityId);\n if (typeof parsed === 'object' && parsed !== null) {\n return parsed;\n }\n return entityId;\n } catch {\n return entityId;\n }\n }\n}\n"],"mappings":"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"}
@@ -1,3 +1,2 @@
1
- import "./Auditor-CZ8lkASv.cjs";
2
- import { AuditQueryOptions, AuditStorage } from "./storage-ndQzIcWK.cjs";
1
+ import { AuditQueryOptions, AuditStorage } from "./Auditor-QYUMGJCH.cjs";
3
2
  export { AuditQueryOptions, AuditStorage };
@@ -1,3 +1,2 @@
1
- import "./Auditor-ii62d_pF.mjs";
2
- import { AuditQueryOptions, AuditStorage } from "./storage-BmA3SESr.mjs";
1
+ import { AuditQueryOptions, AuditStorage } from "./Auditor-D3me-qKX.mjs";
3
2
  export { AuditQueryOptions, AuditStorage };
package/dist/types.d.cts CHANGED
@@ -1,2 +1,2 @@
1
- import { AuditActor, AuditMetadata, AuditOperation, AuditOptions, AuditRecord, AuditableAction, ExtractAuditPayload, ExtractAuditType, ExtractAuditorAction, MappedAudit } from "./Auditor-CZ8lkASv.cjs";
2
- export { AuditActor, AuditMetadata, AuditOperation, AuditOptions, AuditRecord, AuditableAction, ExtractAuditPayload, ExtractAuditType, ExtractAuditorAction, MappedAudit };
1
+ import { AuditActor, AuditMetadata, AuditOperation, AuditOptions, AuditRecord, AuditableAction, ExtractAuditPayload, ExtractAuditType, ExtractAuditorAction, ExtractStorageAuditAction, MappedAudit } from "./Auditor-QYUMGJCH.cjs";
2
+ export { AuditActor, AuditMetadata, AuditOperation, AuditOptions, AuditRecord, AuditableAction, ExtractAuditPayload, ExtractAuditType, ExtractAuditorAction, ExtractStorageAuditAction, MappedAudit };
package/dist/types.d.mts CHANGED
@@ -1,2 +1,2 @@
1
- import { AuditActor, AuditMetadata, AuditOperation, AuditOptions, AuditRecord, AuditableAction, ExtractAuditPayload, ExtractAuditType, ExtractAuditorAction, MappedAudit } from "./Auditor-ii62d_pF.mjs";
2
- export { AuditActor, AuditMetadata, AuditOperation, AuditOptions, AuditRecord, AuditableAction, ExtractAuditPayload, ExtractAuditType, ExtractAuditorAction, MappedAudit };
1
+ import { AuditActor, AuditMetadata, AuditOperation, AuditOptions, AuditRecord, AuditableAction, ExtractAuditPayload, ExtractAuditType, ExtractAuditorAction, ExtractStorageAuditAction, MappedAudit } from "./Auditor-D3me-qKX.mjs";
2
+ export { AuditActor, AuditMetadata, AuditOperation, AuditOptions, AuditRecord, AuditableAction, ExtractAuditPayload, ExtractAuditType, ExtractAuditorAction, ExtractStorageAuditAction, MappedAudit };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geekmidas/audit",
3
- "version": "0.0.3",
3
+ "version": "0.0.5",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
@@ -162,6 +162,89 @@ describe('KyselyAuditStorage', () => {
162
162
  }),
163
163
  ]);
164
164
  });
165
+
166
+ it('should generate ID with nanoid when autoId is false (default)', async () => {
167
+ const records: AuditRecord[] = [
168
+ {
169
+ id: '', // Empty ID
170
+ type: 'user.created',
171
+ operation: 'CUSTOM',
172
+ timestamp: new Date(),
173
+ },
174
+ ];
175
+
176
+ await storage.write(records);
177
+
178
+ const calledValues = mockDb.insertBuilder.values.mock.calls[0][0];
179
+ expect(calledValues[0].id).toBeDefined();
180
+ expect(calledValues[0].id.length).toBeGreaterThan(0);
181
+ });
182
+
183
+ it('should use provided ID when autoId is false (default)', async () => {
184
+ const records: AuditRecord[] = [
185
+ {
186
+ id: 'my-custom-id',
187
+ type: 'user.created',
188
+ operation: 'CUSTOM',
189
+ timestamp: new Date(),
190
+ },
191
+ ];
192
+
193
+ await storage.write(records);
194
+
195
+ expect(mockDb.insertBuilder.values).toHaveBeenCalledWith([
196
+ expect.objectContaining({
197
+ id: 'my-custom-id',
198
+ }),
199
+ ]);
200
+ });
201
+
202
+ it('should omit ID when autoId is true and no ID provided', async () => {
203
+ const storageAutoId = new KyselyAuditStorage({
204
+ db: mockDb.db as any,
205
+ tableName: 'audit_logs',
206
+ autoId: true,
207
+ });
208
+
209
+ const records: AuditRecord[] = [
210
+ {
211
+ id: '', // Empty ID - let database generate
212
+ type: 'user.created',
213
+ operation: 'CUSTOM',
214
+ timestamp: new Date(),
215
+ },
216
+ ];
217
+
218
+ await storageAutoId.write(records);
219
+
220
+ const calledValues = mockDb.insertBuilder.values.mock.calls[0][0];
221
+ expect(calledValues[0].id).toBeUndefined();
222
+ });
223
+
224
+ it('should use provided ID when autoId is true', async () => {
225
+ const storageAutoId = new KyselyAuditStorage({
226
+ db: mockDb.db as any,
227
+ tableName: 'audit_logs',
228
+ autoId: true,
229
+ });
230
+
231
+ const records: AuditRecord[] = [
232
+ {
233
+ id: 'explicit-id',
234
+ type: 'user.created',
235
+ operation: 'CUSTOM',
236
+ timestamp: new Date(),
237
+ },
238
+ ];
239
+
240
+ await storageAutoId.write(records);
241
+
242
+ expect(mockDb.insertBuilder.values).toHaveBeenCalledWith([
243
+ expect.objectContaining({
244
+ id: 'explicit-id',
245
+ }),
246
+ ]);
247
+ });
165
248
  });
166
249
 
167
250
  describe('query', () => {
package/src/index.ts CHANGED
@@ -9,6 +9,7 @@ export type {
9
9
  ExtractAuditPayload,
10
10
  ExtractAuditType,
11
11
  ExtractAuditorAction,
12
+ ExtractStorageAuditAction,
12
13
  MappedAudit,
13
14
  } from './types';
14
15
 
package/src/kysely.ts CHANGED
@@ -4,6 +4,7 @@ import type {
4
4
  Kysely,
5
5
  Transaction,
6
6
  } from 'kysely';
7
+ import { nanoid } from 'nanoid';
7
8
  import type { AuditQueryOptions, AuditStorage } from './storage';
8
9
  import type { AuditRecord } from './types';
9
10
 
@@ -151,6 +152,24 @@ export interface AuditLogTable {
151
152
  metadata: unknown | null;
152
153
  }
153
154
 
155
+ /**
156
+ * Insertable version of AuditLogTable where id is optional.
157
+ * Use this when your database auto-generates IDs or when using autoId option.
158
+ *
159
+ * @example
160
+ * ```typescript
161
+ * interface Database {
162
+ * audit_logs: AuditLogTable;
163
+ * }
164
+ *
165
+ * // For insertions where id is auto-generated
166
+ * type NewAuditLog = InsertableAuditLogTable;
167
+ * ```
168
+ */
169
+ export type InsertableAuditLogTable = Omit<AuditLogTable, 'id'> & {
170
+ id?: string;
171
+ };
172
+
154
173
  /**
155
174
  * Configuration for KyselyAuditStorage.
156
175
  */
@@ -165,6 +184,13 @@ export interface KyselyAuditStorageConfig<DB> {
165
184
  * in the handler context if the endpoint's database service has the same name.
166
185
  */
167
186
  databaseServiceName?: string;
187
+ /**
188
+ * Let the database auto-generate IDs (e.g., via DEFAULT gen_random_uuid()).
189
+ * When true, the ID field is omitted from inserts if not provided.
190
+ * When false (default), IDs are generated using nanoid if not provided.
191
+ * @default false
192
+ */
193
+ autoId?: boolean;
168
194
  }
169
195
 
170
196
  /**
@@ -193,12 +219,14 @@ export interface KyselyAuditStorageConfig<DB> {
193
219
  export class KyselyAuditStorage<DB> implements AuditStorage {
194
220
  private readonly db: Kysely<DB>;
195
221
  private readonly tableName: keyof DB & string;
222
+ private readonly autoId: boolean;
196
223
  readonly databaseServiceName?: string;
197
224
 
198
225
  constructor(config: KyselyAuditStorageConfig<DB>) {
199
226
  this.db = config.db;
200
227
  this.tableName = config.tableName;
201
228
  this.databaseServiceName = config.databaseServiceName;
229
+ this.autoId = config.autoId ?? false;
202
230
  }
203
231
 
204
232
  async write(records: AuditRecord[], trx?: unknown): Promise<void> {
@@ -299,8 +327,12 @@ export class KyselyAuditStorage<DB> implements AuditStorage {
299
327
  }
300
328
 
301
329
  private toRow(record: AuditRecord): AuditLogTable {
330
+ // If autoId is true, let database generate ID (omit if not provided)
331
+ // If autoId is false (default), generate with nanoid if not provided
332
+ const id = record.id || (this.autoId ? undefined : nanoid());
333
+
302
334
  return {
303
- id: record.id,
335
+ ...(id && { id }),
304
336
  type: record.type,
305
337
  operation: record.operation,
306
338
  table: record.table ?? null,
@@ -319,7 +351,7 @@ export class KyselyAuditStorage<DB> implements AuditStorage {
319
351
  actorData:
320
352
  record.actor !== undefined ? this.getActorData(record.actor) : null,
321
353
  metadata: record.metadata ?? null,
322
- };
354
+ } as AuditLogTable;
323
355
  }
324
356
 
325
357
  private fromRow(row: AuditLogTable): AuditRecord {
package/src/storage.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { AuditRecord } from './types';
1
+ import type { AuditableAction, AuditRecord } from './types';
2
2
 
3
3
  /**
4
4
  * Options for querying audit records.
@@ -50,8 +50,19 @@ export interface AuditQueryOptions {
50
50
  * },
51
51
  * };
52
52
  * ```
53
+ *
54
+ * @template TAuditAction - Optional type parameter for type-safe audit actions.
55
+ * When provided, this type is preserved in the Service definition and can be
56
+ * extracted by EndpointBuilder to provide type inference for `.audit([...])`.
53
57
  */
54
- export interface AuditStorage {
58
+ export interface AuditStorage<
59
+ TAuditAction extends AuditableAction<string, unknown> = AuditableAction<
60
+ string,
61
+ unknown
62
+ >,
63
+ > {
64
+ /** @internal Type marker for extracting audit action type */
65
+ readonly __auditActionType?: TAuditAction;
55
66
  /**
56
67
  * Write audit records to storage.
57
68
  * Called by Auditor.flush() to persist collected audits.
package/src/types.ts CHANGED
@@ -143,5 +143,13 @@ export interface MappedAudit<
143
143
  */
144
144
  export type ExtractAuditorAction<T> = T extends Auditor<infer A> ? A : never;
145
145
 
146
+ /**
147
+ * Extract the AuditableAction type from an AuditStorage.
148
+ */
149
+ export type ExtractStorageAuditAction<T> = T extends AuditStorage<infer A>
150
+ ? A
151
+ : AuditableAction<string, unknown>;
152
+
146
153
  // Forward declaration for Auditor type extraction
147
154
  import type { Auditor } from './Auditor';
155
+ import type { AuditStorage } from './storage';
@@ -1,117 +0,0 @@
1
- import { AuditRecord } from "./Auditor-ii62d_pF.mjs";
2
-
3
- //#region src/storage.d.ts
4
-
5
- /**
6
- * Options for querying audit records.
7
- */
8
- interface AuditQueryOptions {
9
- /** Filter by audit type */
10
- type?: string | string[];
11
- /** Filter by entity ID */
12
- entityId?: string | Record<string, unknown>;
13
- /** Filter by table name */
14
- table?: string;
15
- /** Filter by actor ID */
16
- actorId?: string;
17
- /** Filter by date range (start) */
18
- from?: Date;
19
- /** Filter by date range (end) */
20
- to?: Date;
21
- /** Limit number of results */
22
- limit?: number;
23
- /** Offset for pagination */
24
- offset?: number;
25
- /** Sort order */
26
- orderBy?: 'timestamp' | 'type';
27
- /** Sort direction */
28
- orderDirection?: 'asc' | 'desc';
29
- }
30
- /**
31
- * Interface for audit storage backends.
32
- * Implement this to store audits in your preferred location.
33
- *
34
- * @example
35
- * ```typescript
36
- * // Same database storage (recommended for transactions)
37
- * const auditStorage: AuditStorage = {
38
- * async write(records, trx) {
39
- * const db = trx ?? getDatabase();
40
- * await db.insertInto('audit_logs').values(records).execute();
41
- * },
42
- * };
43
- *
44
- * // External audit service
45
- * const externalAuditStorage: AuditStorage = {
46
- * async write(records) {
47
- * await fetch('https://audit.example.com/api/audits', {
48
- * method: 'POST',
49
- * body: JSON.stringify({ records }),
50
- * });
51
- * },
52
- * };
53
- * ```
54
- */
55
- interface AuditStorage {
56
- /**
57
- * Write audit records to storage.
58
- * Called by Auditor.flush() to persist collected audits.
59
- *
60
- * @param records - The audit records to write
61
- * @param trx - Optional transaction context (for same-DB storage)
62
- */
63
- write(records: AuditRecord[], trx?: unknown): Promise<void>;
64
- /**
65
- * Optional: Query audit records for retrieval.
66
- * Implement this for audit log viewing/searching.
67
- *
68
- * @param options - Query filters and pagination
69
- * @returns Matching audit records
70
- */
71
- query?(options: AuditQueryOptions): Promise<AuditRecord[]>;
72
- /**
73
- * Optional: Count audit records matching filters.
74
- * Useful for pagination.
75
- *
76
- * @param options - Query filters (limit/offset ignored)
77
- * @returns Count of matching records
78
- */
79
- count?(options: Omit<AuditQueryOptions, 'limit' | 'offset'>): Promise<number>;
80
- /**
81
- * Optional: Get the database connection for transactional audit writes.
82
- * When implemented, the endpoint adaptor can automatically wrap handlers
83
- * in a transaction, ensuring audits are atomic with other database operations.
84
- *
85
- * @returns Database connection (e.g., Kysely instance)
86
- *
87
- * @example
88
- * ```typescript
89
- * class KyselyAuditStorage implements AuditStorage {
90
- * constructor(private db: Kysely<DB>) {}
91
- *
92
- * getDatabase() {
93
- * return this.db;
94
- * }
95
- * }
96
- * ```
97
- */
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;
114
- }
115
- //#endregion
116
- export { AuditQueryOptions, AuditStorage };
117
- //# sourceMappingURL=storage-BmA3SESr.d.mts.map
@@ -1,117 +0,0 @@
1
- import { AuditRecord } from "./Auditor-CZ8lkASv.cjs";
2
-
3
- //#region src/storage.d.ts
4
-
5
- /**
6
- * Options for querying audit records.
7
- */
8
- interface AuditQueryOptions {
9
- /** Filter by audit type */
10
- type?: string | string[];
11
- /** Filter by entity ID */
12
- entityId?: string | Record<string, unknown>;
13
- /** Filter by table name */
14
- table?: string;
15
- /** Filter by actor ID */
16
- actorId?: string;
17
- /** Filter by date range (start) */
18
- from?: Date;
19
- /** Filter by date range (end) */
20
- to?: Date;
21
- /** Limit number of results */
22
- limit?: number;
23
- /** Offset for pagination */
24
- offset?: number;
25
- /** Sort order */
26
- orderBy?: 'timestamp' | 'type';
27
- /** Sort direction */
28
- orderDirection?: 'asc' | 'desc';
29
- }
30
- /**
31
- * Interface for audit storage backends.
32
- * Implement this to store audits in your preferred location.
33
- *
34
- * @example
35
- * ```typescript
36
- * // Same database storage (recommended for transactions)
37
- * const auditStorage: AuditStorage = {
38
- * async write(records, trx) {
39
- * const db = trx ?? getDatabase();
40
- * await db.insertInto('audit_logs').values(records).execute();
41
- * },
42
- * };
43
- *
44
- * // External audit service
45
- * const externalAuditStorage: AuditStorage = {
46
- * async write(records) {
47
- * await fetch('https://audit.example.com/api/audits', {
48
- * method: 'POST',
49
- * body: JSON.stringify({ records }),
50
- * });
51
- * },
52
- * };
53
- * ```
54
- */
55
- interface AuditStorage {
56
- /**
57
- * Write audit records to storage.
58
- * Called by Auditor.flush() to persist collected audits.
59
- *
60
- * @param records - The audit records to write
61
- * @param trx - Optional transaction context (for same-DB storage)
62
- */
63
- write(records: AuditRecord[], trx?: unknown): Promise<void>;
64
- /**
65
- * Optional: Query audit records for retrieval.
66
- * Implement this for audit log viewing/searching.
67
- *
68
- * @param options - Query filters and pagination
69
- * @returns Matching audit records
70
- */
71
- query?(options: AuditQueryOptions): Promise<AuditRecord[]>;
72
- /**
73
- * Optional: Count audit records matching filters.
74
- * Useful for pagination.
75
- *
76
- * @param options - Query filters (limit/offset ignored)
77
- * @returns Count of matching records
78
- */
79
- count?(options: Omit<AuditQueryOptions, 'limit' | 'offset'>): Promise<number>;
80
- /**
81
- * Optional: Get the database connection for transactional audit writes.
82
- * When implemented, the endpoint adaptor can automatically wrap handlers
83
- * in a transaction, ensuring audits are atomic with other database operations.
84
- *
85
- * @returns Database connection (e.g., Kysely instance)
86
- *
87
- * @example
88
- * ```typescript
89
- * class KyselyAuditStorage implements AuditStorage {
90
- * constructor(private db: Kysely<DB>) {}
91
- *
92
- * getDatabase() {
93
- * return this.db;
94
- * }
95
- * }
96
- * ```
97
- */
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;
114
- }
115
- //#endregion
116
- export { AuditQueryOptions, AuditStorage };
117
- //# sourceMappingURL=storage-ndQzIcWK.d.cts.map