@usebetterdev/audit-drizzle 0.4.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs ADDED
@@ -0,0 +1,858 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc2) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc2 = __getOwnPropDesc(from, key)) || desc2.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ auditLogs: () => auditLogs,
24
+ buildCursorCondition: () => buildCursorCondition,
25
+ buildOrderBy: () => buildOrderBy,
26
+ buildWhereConditions: () => buildWhereConditions,
27
+ decodeCursor: () => decodeCursor,
28
+ drizzleAuditAdapter: () => drizzleAuditAdapter,
29
+ encodeCursor: () => encodeCursor,
30
+ withAuditProxy: () => withAuditProxy
31
+ });
32
+ module.exports = __toCommonJS(index_exports);
33
+
34
+ // src/adapter.ts
35
+ var import_drizzle_orm2 = require("drizzle-orm");
36
+
37
+ // src/schema.ts
38
+ var import_pg_core = require("drizzle-orm/pg-core");
39
+ var auditLogs = (0, import_pg_core.pgTable)(
40
+ "audit_logs",
41
+ {
42
+ id: (0, import_pg_core.uuid)().primaryKey().defaultRandom(),
43
+ timestamp: (0, import_pg_core.timestamp)({ withTimezone: true }).notNull().defaultNow(),
44
+ tableName: (0, import_pg_core.text)("table_name").notNull(),
45
+ operation: (0, import_pg_core.text)().notNull(),
46
+ recordId: (0, import_pg_core.text)("record_id").notNull(),
47
+ actorId: (0, import_pg_core.text)("actor_id"),
48
+ beforeData: (0, import_pg_core.jsonb)("before_data"),
49
+ afterData: (0, import_pg_core.jsonb)("after_data"),
50
+ diff: (0, import_pg_core.jsonb)(),
51
+ label: (0, import_pg_core.text)(),
52
+ description: (0, import_pg_core.text)(),
53
+ severity: (0, import_pg_core.text)(),
54
+ compliance: (0, import_pg_core.jsonb)(),
55
+ notify: (0, import_pg_core.boolean)(),
56
+ reason: (0, import_pg_core.text)(),
57
+ metadata: (0, import_pg_core.jsonb)(),
58
+ redactedFields: (0, import_pg_core.jsonb)("redacted_fields")
59
+ },
60
+ (table) => [
61
+ (0, import_pg_core.index)("audit_logs_table_name_timestamp_idx").on(
62
+ table.tableName,
63
+ table.timestamp
64
+ ),
65
+ (0, import_pg_core.index)("audit_logs_actor_id_idx").on(table.actorId),
66
+ (0, import_pg_core.index)("audit_logs_record_id_idx").on(table.recordId),
67
+ (0, import_pg_core.index)("audit_logs_table_name_record_id_idx").on(
68
+ table.tableName,
69
+ table.recordId
70
+ ),
71
+ (0, import_pg_core.index)("audit_logs_operation_idx").on(table.operation),
72
+ (0, import_pg_core.index)("audit_logs_timestamp_idx").on(table.timestamp),
73
+ (0, import_pg_core.index)("audit_logs_timestamp_id_idx").on(table.timestamp, table.id)
74
+ ]
75
+ );
76
+
77
+ // src/column-map.ts
78
+ var VALID_OPERATIONS = /* @__PURE__ */ new Set([
79
+ "INSERT",
80
+ "UPDATE",
81
+ "DELETE"
82
+ ]);
83
+ var VALID_SEVERITIES = /* @__PURE__ */ new Set([
84
+ "low",
85
+ "medium",
86
+ "high",
87
+ "critical"
88
+ ]);
89
+ function isAuditOperation(value) {
90
+ return VALID_OPERATIONS.has(value);
91
+ }
92
+ function isAuditSeverity(value) {
93
+ return VALID_SEVERITIES.has(value);
94
+ }
95
+ function auditLogToRow(log) {
96
+ const row = {
97
+ id: log.id,
98
+ timestamp: log.timestamp,
99
+ tableName: log.tableName,
100
+ operation: log.operation,
101
+ recordId: log.recordId
102
+ };
103
+ if (log.actorId !== void 0) {
104
+ row.actorId = log.actorId;
105
+ }
106
+ if (log.beforeData !== void 0) {
107
+ row.beforeData = log.beforeData;
108
+ }
109
+ if (log.afterData !== void 0) {
110
+ row.afterData = log.afterData;
111
+ }
112
+ if (log.diff !== void 0) {
113
+ row.diff = log.diff;
114
+ }
115
+ if (log.label !== void 0) {
116
+ row.label = log.label;
117
+ }
118
+ if (log.description !== void 0) {
119
+ row.description = log.description;
120
+ }
121
+ if (log.severity !== void 0) {
122
+ row.severity = log.severity;
123
+ }
124
+ if (log.compliance !== void 0) {
125
+ row.compliance = log.compliance;
126
+ }
127
+ if (log.notify !== void 0) {
128
+ row.notify = log.notify;
129
+ }
130
+ if (log.reason !== void 0) {
131
+ row.reason = log.reason;
132
+ }
133
+ if (log.metadata !== void 0) {
134
+ row.metadata = log.metadata;
135
+ }
136
+ if (log.redactedFields !== void 0) {
137
+ row.redactedFields = log.redactedFields;
138
+ }
139
+ return row;
140
+ }
141
+ function rowToAuditLog(row) {
142
+ if (!isAuditOperation(row.operation)) {
143
+ throw new Error(
144
+ `Invalid audit operation: "${row.operation}". Expected one of: INSERT, UPDATE, DELETE`
145
+ );
146
+ }
147
+ const log = {
148
+ id: row.id,
149
+ timestamp: row.timestamp,
150
+ tableName: row.tableName,
151
+ operation: row.operation,
152
+ recordId: row.recordId
153
+ };
154
+ if (row.actorId !== null) {
155
+ log.actorId = row.actorId;
156
+ }
157
+ if (row.beforeData !== null) {
158
+ log.beforeData = row.beforeData;
159
+ }
160
+ if (row.afterData !== null) {
161
+ log.afterData = row.afterData;
162
+ }
163
+ if (row.diff !== null) {
164
+ log.diff = row.diff;
165
+ }
166
+ if (row.label !== null) {
167
+ log.label = row.label;
168
+ }
169
+ if (row.description !== null) {
170
+ log.description = row.description;
171
+ }
172
+ if (row.severity !== null && row.severity !== void 0) {
173
+ if (!isAuditSeverity(row.severity)) {
174
+ throw new Error(
175
+ `Invalid audit severity: "${row.severity}". Expected one of: low, medium, high, critical`
176
+ );
177
+ }
178
+ log.severity = row.severity;
179
+ }
180
+ if (row.compliance !== null) {
181
+ log.compliance = row.compliance;
182
+ }
183
+ if (row.notify !== null) {
184
+ log.notify = row.notify;
185
+ }
186
+ if (row.reason !== null) {
187
+ log.reason = row.reason;
188
+ }
189
+ if (row.metadata !== null) {
190
+ log.metadata = row.metadata;
191
+ }
192
+ if (row.redactedFields !== null) {
193
+ log.redactedFields = row.redactedFields;
194
+ }
195
+ return log;
196
+ }
197
+
198
+ // src/query.ts
199
+ var import_audit_core = require("@usebetterdev/audit-core");
200
+ var import_drizzle_orm = require("drizzle-orm");
201
+ function escapeLikePattern(input) {
202
+ return input.replace(/[%_\\]/g, "\\$&");
203
+ }
204
+ function resolveTimeFilter(filter) {
205
+ if ("date" in filter && filter.date !== void 0) {
206
+ return filter.date;
207
+ }
208
+ if ("duration" in filter && filter.duration !== void 0) {
209
+ return (0, import_audit_core.parseDuration)(filter.duration);
210
+ }
211
+ throw new Error("TimeFilter must have either date or duration");
212
+ }
213
+ function buildWhereConditions(filters) {
214
+ const conditions = [];
215
+ if (filters.resource !== void 0) {
216
+ conditions.push((0, import_drizzle_orm.eq)(auditLogs.tableName, filters.resource.tableName));
217
+ if (filters.resource.recordId !== void 0) {
218
+ conditions.push((0, import_drizzle_orm.eq)(auditLogs.recordId, filters.resource.recordId));
219
+ }
220
+ }
221
+ if (filters.actorIds !== void 0 && filters.actorIds.length > 0) {
222
+ if (filters.actorIds.length === 1) {
223
+ conditions.push((0, import_drizzle_orm.eq)(auditLogs.actorId, filters.actorIds[0]));
224
+ } else {
225
+ conditions.push((0, import_drizzle_orm.inArray)(auditLogs.actorId, filters.actorIds));
226
+ }
227
+ }
228
+ if (filters.severities !== void 0 && filters.severities.length > 0) {
229
+ if (filters.severities.length === 1) {
230
+ conditions.push((0, import_drizzle_orm.eq)(auditLogs.severity, filters.severities[0]));
231
+ } else {
232
+ conditions.push((0, import_drizzle_orm.inArray)(auditLogs.severity, filters.severities));
233
+ }
234
+ }
235
+ if (filters.operations !== void 0 && filters.operations.length > 0) {
236
+ if (filters.operations.length === 1) {
237
+ conditions.push((0, import_drizzle_orm.eq)(auditLogs.operation, filters.operations[0]));
238
+ } else {
239
+ conditions.push((0, import_drizzle_orm.inArray)(auditLogs.operation, filters.operations));
240
+ }
241
+ }
242
+ if (filters.since !== void 0) {
243
+ conditions.push((0, import_drizzle_orm.gte)(auditLogs.timestamp, resolveTimeFilter(filters.since)));
244
+ }
245
+ if (filters.until !== void 0) {
246
+ conditions.push((0, import_drizzle_orm.lte)(auditLogs.timestamp, resolveTimeFilter(filters.until)));
247
+ }
248
+ if (filters.searchText !== void 0 && filters.searchText.length > 0) {
249
+ const escaped = escapeLikePattern(filters.searchText);
250
+ const pattern = `%${escaped}%`;
251
+ const searchCondition = (0, import_drizzle_orm.or)(
252
+ (0, import_drizzle_orm.ilike)(auditLogs.label, pattern),
253
+ (0, import_drizzle_orm.ilike)(auditLogs.description, pattern)
254
+ );
255
+ if (searchCondition !== void 0) {
256
+ conditions.push(searchCondition);
257
+ }
258
+ }
259
+ if (filters.compliance !== void 0 && filters.compliance.length > 0) {
260
+ conditions.push(
261
+ import_drizzle_orm.sql`${auditLogs.compliance} @> ${JSON.stringify(filters.compliance)}::jsonb`
262
+ );
263
+ }
264
+ if (conditions.length === 0) {
265
+ return void 0;
266
+ }
267
+ return (0, import_drizzle_orm.and)(...conditions);
268
+ }
269
+ function buildCursorCondition(cursor, sortOrder) {
270
+ const decoded = decodeCursor(cursor);
271
+ const tsCompare = sortOrder === "asc" ? import_drizzle_orm.gt : import_drizzle_orm.lt;
272
+ const idCompare = sortOrder === "asc" ? import_drizzle_orm.gt : import_drizzle_orm.lt;
273
+ return (0, import_drizzle_orm.or)(
274
+ tsCompare(auditLogs.timestamp, decoded.timestamp),
275
+ (0, import_drizzle_orm.and)(
276
+ (0, import_drizzle_orm.eq)(auditLogs.timestamp, decoded.timestamp),
277
+ idCompare(auditLogs.id, decoded.id)
278
+ )
279
+ );
280
+ }
281
+ function buildOrderBy(sortOrder) {
282
+ if (sortOrder === "asc") {
283
+ return [(0, import_drizzle_orm.asc)(auditLogs.timestamp), (0, import_drizzle_orm.asc)(auditLogs.id)];
284
+ }
285
+ return [(0, import_drizzle_orm.desc)(auditLogs.timestamp), (0, import_drizzle_orm.desc)(auditLogs.id)];
286
+ }
287
+ function encodeCursor(timestamp2, id) {
288
+ const payload = JSON.stringify({ t: timestamp2.toISOString(), i: id });
289
+ return btoa(payload);
290
+ }
291
+ var UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
292
+ function decodeCursor(cursor) {
293
+ let parsed;
294
+ try {
295
+ parsed = JSON.parse(atob(cursor));
296
+ } catch {
297
+ throw new Error("Invalid cursor: failed to decode");
298
+ }
299
+ if (typeof parsed !== "object" || parsed === null || !("t" in parsed) || !("i" in parsed)) {
300
+ throw new Error("Invalid cursor: missing required fields");
301
+ }
302
+ const { t, i } = parsed;
303
+ if (typeof t !== "string" || typeof i !== "string") {
304
+ throw new Error("Invalid cursor: fields must be strings");
305
+ }
306
+ const timestamp2 = new Date(t);
307
+ if (isNaN(timestamp2.getTime())) {
308
+ throw new Error("Invalid cursor: invalid timestamp");
309
+ }
310
+ if (!UUID_PATTERN.test(i)) {
311
+ throw new Error("Invalid cursor: id must be a valid UUID");
312
+ }
313
+ return { timestamp: timestamp2, id: i };
314
+ }
315
+
316
+ // src/adapter.ts
317
+ var DEFAULT_LIMIT = 50;
318
+ var MAX_LIMIT = 250;
319
+ function drizzleAuditAdapter(db) {
320
+ return {
321
+ async writeLog(log) {
322
+ const row = auditLogToRow(log);
323
+ await db.insert(auditLogs).values(row).execute();
324
+ },
325
+ async queryLogs(spec) {
326
+ const sortOrder = spec.sortOrder ?? "desc";
327
+ const limit = Math.min(spec.limit ?? DEFAULT_LIMIT, MAX_LIMIT);
328
+ const whereCondition = buildWhereConditions(spec.filters);
329
+ const cursorCondition = spec.cursor !== void 0 ? buildCursorCondition(spec.cursor, sortOrder) : void 0;
330
+ const combined = (0, import_drizzle_orm2.and)(whereCondition, cursorCondition);
331
+ const fetchLimit = limit + 1;
332
+ const orderColumns = buildOrderBy(sortOrder);
333
+ const query = db.select().from(auditLogs).where(combined).orderBy(...orderColumns).limit(fetchLimit);
334
+ const rows = await query;
335
+ const hasNextPage = rows.length > limit;
336
+ const resultRows = hasNextPage ? rows.slice(0, -1) : rows;
337
+ const entries = resultRows.map(rowToAuditLog);
338
+ const lastRow = resultRows[resultRows.length - 1];
339
+ if (hasNextPage && lastRow !== void 0) {
340
+ return { entries, nextCursor: encodeCursor(lastRow.timestamp, lastRow.id) };
341
+ }
342
+ return { entries };
343
+ },
344
+ async getLogById(id) {
345
+ const query = db.select().from(auditLogs).where((0, import_drizzle_orm2.eq)(auditLogs.id, id)).limit(1);
346
+ const rows = await query;
347
+ const row = rows[0];
348
+ if (row === void 0) {
349
+ return null;
350
+ }
351
+ return rowToAuditLog(row);
352
+ }
353
+ };
354
+ }
355
+
356
+ // src/proxy.ts
357
+ var import_drizzle_orm3 = require("drizzle-orm");
358
+
359
+ // src/operation-map.ts
360
+ var OPERATION_MAP = {
361
+ insert: "INSERT",
362
+ update: "UPDATE",
363
+ delete: "DELETE"
364
+ };
365
+
366
+ // src/proxy.ts
367
+ function withAuditProxy(db, captureLog, options) {
368
+ const primaryKey = options?.primaryKey ?? "id";
369
+ const missingRecordIdPolicy = options?.onMissingRecordId ?? "warn";
370
+ const skipBeforeState = new Set(options?.skipBeforeState ?? []);
371
+ const maxBeforeStateRows = options?.maxBeforeStateRows ?? 1e3;
372
+ const handleError = (error, table, op) => {
373
+ try {
374
+ if (options?.onError !== void 0) {
375
+ options.onError(error);
376
+ } else {
377
+ const msg = error instanceof Error ? error.message : String(error);
378
+ console.error(
379
+ `audit-drizzle: capture failed for ${op} on ${table} \u2014 ${msg}`
380
+ );
381
+ }
382
+ } catch {
383
+ }
384
+ };
385
+ return new Proxy(db, {
386
+ get(target, prop, receiver) {
387
+ if (typeof prop === "string" && (prop === "insert" || prop === "update" || prop === "delete")) {
388
+ const method = prop;
389
+ const originalMethod = Reflect.get(target, prop, receiver);
390
+ return (table) => {
391
+ const tableName = (0, import_drizzle_orm3.getTableName)(table);
392
+ const detectedPk = getPrimaryKeyColumnName(table);
393
+ const effectivePk = detectedPk ?? primaryKey;
394
+ const originalBuilder = originalMethod.call(target, table);
395
+ const ctx = {
396
+ tableName,
397
+ operation: OPERATION_MAP[method],
398
+ captureLog,
399
+ primaryKey: effectivePk,
400
+ handleError,
401
+ onMissingRecordId: missingRecordIdPolicy,
402
+ auditedSet: /* @__PURE__ */ new WeakSet(),
403
+ dbTarget: target,
404
+ table,
405
+ skipBeforeState,
406
+ maxBeforeStateRows
407
+ };
408
+ return wrapBuilder(originalBuilder, ctx);
409
+ };
410
+ }
411
+ if (prop === "transaction") {
412
+ const originalTransaction = Reflect.get(
413
+ target,
414
+ prop,
415
+ receiver
416
+ );
417
+ return (...args) => {
418
+ const callback = args[0];
419
+ const rest = args.slice(1);
420
+ const wrappedCallback = (tx) => {
421
+ const proxiedTx = withAuditProxy(
422
+ tx,
423
+ captureLog,
424
+ options
425
+ );
426
+ return callback(proxiedTx);
427
+ };
428
+ return originalTransaction.call(target, wrappedCallback, ...rest);
429
+ };
430
+ }
431
+ return Reflect.get(target, prop, receiver);
432
+ }
433
+ });
434
+ }
435
+ function wrapBuilder(builder, ctx, state = {}) {
436
+ return new Proxy(builder, {
437
+ get(target, prop, receiver) {
438
+ if (prop === "values") {
439
+ return (...args) => {
440
+ const data = args[0];
441
+ const newTrackedValues = Array.isArray(data) ? data : [data];
442
+ const result = target.values(...args);
443
+ return wrapBuilder(result, ctx, {
444
+ ...state,
445
+ trackedValues: newTrackedValues
446
+ });
447
+ };
448
+ }
449
+ if (prop === "set") {
450
+ return (...args) => {
451
+ const newTrackedSet = args[0];
452
+ const result = target.set(...args);
453
+ return wrapBuilder(result, ctx, {
454
+ ...state,
455
+ trackedSet: newTrackedSet
456
+ });
457
+ };
458
+ }
459
+ if (prop === "where") {
460
+ return (...args) => {
461
+ const condition = args[0];
462
+ const result = target.where(...args);
463
+ return wrapBuilder(result, ctx, {
464
+ ...state,
465
+ trackedWhere: condition
466
+ });
467
+ };
468
+ }
469
+ if (prop === "returning") {
470
+ return (...args) => {
471
+ const result = target.returning(...args);
472
+ return wrapBuilder(result, ctx, {
473
+ ...state,
474
+ hasReturning: true
475
+ });
476
+ };
477
+ }
478
+ if (prop === "then") {
479
+ return (onFulfilled, onRejected) => {
480
+ const thenFn = Reflect.get(target, "then", receiver);
481
+ const needsBeforeState = (ctx.operation === "UPDATE" || ctx.operation === "DELETE") && state.trackedWhere !== void 0 && !ctx.skipBeforeState.has(ctx.tableName) && // For DELETE with .returning(), returned rows ARE the before-state
482
+ !(ctx.operation === "DELETE" && state.hasReturning === true);
483
+ if (needsBeforeState) {
484
+ return executeWithBeforeState(
485
+ target,
486
+ thenFn,
487
+ ctx,
488
+ state,
489
+ onFulfilled,
490
+ onRejected
491
+ );
492
+ }
493
+ return thenFn.call(
494
+ target,
495
+ async (result) => {
496
+ if (ctx.auditedSet.has(target)) {
497
+ return onFulfilled?.(result);
498
+ }
499
+ ctx.auditedSet.add(target);
500
+ try {
501
+ await fireCaptureLog(result, ctx, state);
502
+ } catch (error) {
503
+ ctx.handleError(error, ctx.tableName, ctx.operation);
504
+ }
505
+ return onFulfilled?.(result);
506
+ },
507
+ onRejected
508
+ );
509
+ };
510
+ }
511
+ const value = Reflect.get(target, prop, receiver);
512
+ if (typeof value === "function") {
513
+ return (...args) => {
514
+ const result = value.apply(
515
+ target,
516
+ args
517
+ );
518
+ if (result !== null && typeof result === "object") {
519
+ return wrapBuilder(
520
+ result,
521
+ ctx,
522
+ state
523
+ );
524
+ }
525
+ return result;
526
+ };
527
+ }
528
+ return value;
529
+ }
530
+ });
531
+ }
532
+ function executeWithBeforeState(target, thenFn, ctx, state, onFulfilled, onRejected) {
533
+ const beforePromise = fetchBeforeState(ctx, state);
534
+ return beforePromise.then(
535
+ (beforeRows) => {
536
+ return thenFn.call(
537
+ target,
538
+ async (result) => {
539
+ if (ctx.auditedSet.has(target)) {
540
+ return onFulfilled?.(result);
541
+ }
542
+ ctx.auditedSet.add(target);
543
+ try {
544
+ if (beforeRows !== void 0) {
545
+ await fireCaptureLogWithBeforeState(
546
+ result,
547
+ beforeRows,
548
+ ctx,
549
+ state
550
+ );
551
+ } else {
552
+ await fireCaptureLog(result, ctx, state);
553
+ }
554
+ } catch (error) {
555
+ ctx.handleError(error, ctx.tableName, ctx.operation);
556
+ }
557
+ return onFulfilled?.(result);
558
+ },
559
+ onRejected
560
+ );
561
+ },
562
+ (error) => {
563
+ ctx.handleError(error, ctx.tableName, ctx.operation);
564
+ return thenFn.call(
565
+ target,
566
+ async (result) => {
567
+ if (ctx.auditedSet.has(target)) {
568
+ return onFulfilled?.(result);
569
+ }
570
+ ctx.auditedSet.add(target);
571
+ try {
572
+ await fireCaptureLog(result, ctx, state);
573
+ } catch (captureError) {
574
+ ctx.handleError(captureError, ctx.tableName, ctx.operation);
575
+ }
576
+ return onFulfilled?.(result);
577
+ },
578
+ onRejected
579
+ );
580
+ }
581
+ );
582
+ }
583
+ async function fetchBeforeState(ctx, state) {
584
+ const selectFn = ctx.dbTarget.select;
585
+ if (selectFn === void 0) {
586
+ return void 0;
587
+ }
588
+ const selectBuilder = selectFn.call(ctx.dbTarget);
589
+ const fromFn = selectBuilder.from;
590
+ if (fromFn === void 0) {
591
+ return void 0;
592
+ }
593
+ const fromBuilder = fromFn.call(selectBuilder, ctx.table);
594
+ const whereFn = fromBuilder.where;
595
+ if (whereFn === void 0) {
596
+ return void 0;
597
+ }
598
+ const whereBuilder = whereFn.call(fromBuilder, state.trackedWhere);
599
+ const limitFn = whereBuilder.limit;
600
+ const fetchLimit = ctx.maxBeforeStateRows + 1;
601
+ const queryBuilder = limitFn !== void 0 ? limitFn.call(whereBuilder, fetchLimit) : whereBuilder;
602
+ const rows = await queryBuilder;
603
+ if (rows.length > ctx.maxBeforeStateRows) {
604
+ ctx.handleError(
605
+ new Error(
606
+ `audit-drizzle: before-state SELECT returned more than ${ctx.maxBeforeStateRows} rows, skipping before-state capture`
607
+ ),
608
+ ctx.tableName,
609
+ ctx.operation
610
+ );
611
+ return void 0;
612
+ }
613
+ return rows;
614
+ }
615
+ function getPrimaryKeyColumnName(table) {
616
+ for (const [key, value] of Object.entries(table)) {
617
+ if (value !== null && typeof value === "object" && "primary" in value && value.primary === true) {
618
+ return key;
619
+ }
620
+ }
621
+ return void 0;
622
+ }
623
+ function extractRecordId(row, primaryKey) {
624
+ const value = row[primaryKey];
625
+ if (value !== void 0 && value !== null) {
626
+ return String(value);
627
+ }
628
+ return "";
629
+ }
630
+ function applyMissingRecordIdPolicy(ctx, detail) {
631
+ const policy = ctx.onMissingRecordId;
632
+ if (policy === "skip") {
633
+ return false;
634
+ }
635
+ if (policy === "throw") {
636
+ throw new Error(
637
+ `audit-drizzle: missing recordId for ${ctx.operation} on ${ctx.tableName} \u2014 ${detail}`
638
+ );
639
+ }
640
+ ctx.handleError(
641
+ new Error(
642
+ `audit-drizzle: missing recordId for ${ctx.operation} on ${ctx.tableName} \u2014 ${detail}`
643
+ ),
644
+ ctx.tableName,
645
+ ctx.operation
646
+ );
647
+ return true;
648
+ }
649
+ async function fireCaptureLogWithBeforeState(result, beforeRows, ctx, state) {
650
+ const returnedRows = Array.isArray(result) ? result : [];
651
+ if (ctx.operation === "UPDATE") {
652
+ if (beforeRows.length === 0) {
653
+ ctx.handleError(
654
+ new Error(
655
+ "audit-drizzle: before-state SELECT returned 0 rows but UPDATE succeeded \u2014 possible race condition"
656
+ ),
657
+ ctx.tableName,
658
+ ctx.operation
659
+ );
660
+ await fireCaptureLog(result, ctx, state);
661
+ return;
662
+ }
663
+ const beforeMap = /* @__PURE__ */ new Map();
664
+ for (const row of beforeRows) {
665
+ const pk = extractRecordId(row, ctx.primaryKey);
666
+ if (pk !== "") {
667
+ beforeMap.set(pk, row);
668
+ }
669
+ }
670
+ if (beforeMap.size === 0) {
671
+ ctx.handleError(
672
+ new Error(
673
+ `audit-drizzle: before-state rows exist but none have primary key "${ctx.primaryKey}" \u2014 check primaryKey option`
674
+ ),
675
+ ctx.tableName,
676
+ ctx.operation
677
+ );
678
+ await fireCaptureLog(result, ctx, state);
679
+ return;
680
+ }
681
+ if (state.hasReturning === true && returnedRows.length > 0) {
682
+ for (const returnedRow of returnedRows) {
683
+ const row = returnedRow;
684
+ const recordId = extractRecordId(row, ctx.primaryKey);
685
+ if (recordId === "") {
686
+ continue;
687
+ }
688
+ const beforeRow = beforeMap.get(recordId);
689
+ await ctx.captureLog({
690
+ tableName: ctx.tableName,
691
+ operation: ctx.operation,
692
+ recordId,
693
+ ...beforeRow !== void 0 && { before: beforeRow },
694
+ after: row
695
+ });
696
+ }
697
+ } else {
698
+ for (const [pk, beforeRow] of beforeMap) {
699
+ const afterRow = state.trackedSet !== void 0 ? { ...beforeRow, ...state.trackedSet } : beforeRow;
700
+ await ctx.captureLog({
701
+ tableName: ctx.tableName,
702
+ operation: ctx.operation,
703
+ recordId: pk,
704
+ before: beforeRow,
705
+ after: afterRow
706
+ });
707
+ }
708
+ }
709
+ } else if (ctx.operation === "DELETE") {
710
+ if (beforeRows.length === 0) {
711
+ ctx.handleError(
712
+ new Error(
713
+ "audit-drizzle: before-state SELECT returned 0 rows but DELETE succeeded \u2014 possible race condition"
714
+ ),
715
+ ctx.tableName,
716
+ ctx.operation
717
+ );
718
+ await fireCaptureLog(result, ctx, state);
719
+ return;
720
+ }
721
+ for (const beforeRow of beforeRows) {
722
+ const recordId = extractRecordId(beforeRow, ctx.primaryKey);
723
+ if (recordId === "") {
724
+ const shouldProceed = applyMissingRecordIdPolicy(
725
+ ctx,
726
+ "before-state row missing primary key"
727
+ );
728
+ if (!shouldProceed) {
729
+ continue;
730
+ }
731
+ await ctx.captureLog({
732
+ tableName: ctx.tableName,
733
+ operation: ctx.operation,
734
+ recordId: "unknown",
735
+ before: beforeRow
736
+ });
737
+ continue;
738
+ }
739
+ await ctx.captureLog({
740
+ tableName: ctx.tableName,
741
+ operation: ctx.operation,
742
+ recordId,
743
+ before: beforeRow
744
+ });
745
+ }
746
+ }
747
+ }
748
+ async function fireCaptureLog(result, ctx, state) {
749
+ const { trackedValues, trackedSet } = state;
750
+ const returnedRows = Array.isArray(result) ? result : [];
751
+ if (ctx.operation === "INSERT") {
752
+ const values = trackedValues ?? [];
753
+ for (let i = 0; i < values.length; i++) {
754
+ const row = values[i];
755
+ if (row === void 0) {
756
+ continue;
757
+ }
758
+ const returnedRow = returnedRows.length > i ? returnedRows[i] : void 0;
759
+ const recordId = returnedRow !== void 0 ? extractRecordId(returnedRow, ctx.primaryKey) : extractRecordId(row, ctx.primaryKey);
760
+ if (recordId === "") {
761
+ const shouldProceed = applyMissingRecordIdPolicy(
762
+ ctx,
763
+ "use .returning() or include the primary key in .values()"
764
+ );
765
+ if (!shouldProceed) {
766
+ continue;
767
+ }
768
+ await ctx.captureLog({
769
+ tableName: ctx.tableName,
770
+ operation: ctx.operation,
771
+ recordId: "unknown",
772
+ after: row
773
+ });
774
+ continue;
775
+ }
776
+ await ctx.captureLog({
777
+ tableName: ctx.tableName,
778
+ operation: ctx.operation,
779
+ recordId,
780
+ after: row
781
+ });
782
+ }
783
+ } else if (ctx.operation === "UPDATE") {
784
+ if (returnedRows.length > 0) {
785
+ for (const returnedRow of returnedRows) {
786
+ const row = returnedRow;
787
+ const recordId = extractRecordId(row, ctx.primaryKey);
788
+ if (recordId === "") {
789
+ continue;
790
+ }
791
+ await ctx.captureLog({
792
+ tableName: ctx.tableName,
793
+ operation: ctx.operation,
794
+ recordId,
795
+ ...trackedSet !== void 0 && { after: trackedSet }
796
+ });
797
+ }
798
+ } else if (trackedSet !== void 0) {
799
+ const recordId = extractRecordId(trackedSet, ctx.primaryKey);
800
+ if (recordId === "") {
801
+ const shouldProceed = applyMissingRecordIdPolicy(
802
+ ctx,
803
+ "use .returning() to get the record id"
804
+ );
805
+ if (!shouldProceed) {
806
+ return;
807
+ }
808
+ }
809
+ await ctx.captureLog({
810
+ tableName: ctx.tableName,
811
+ operation: ctx.operation,
812
+ recordId: recordId || "unknown",
813
+ after: trackedSet
814
+ });
815
+ }
816
+ } else if (ctx.operation === "DELETE") {
817
+ if (returnedRows.length > 0) {
818
+ for (const returnedRow of returnedRows) {
819
+ const row = returnedRow;
820
+ const recordId = extractRecordId(row, ctx.primaryKey);
821
+ if (recordId === "") {
822
+ continue;
823
+ }
824
+ await ctx.captureLog({
825
+ tableName: ctx.tableName,
826
+ operation: ctx.operation,
827
+ recordId,
828
+ before: row
829
+ });
830
+ }
831
+ } else {
832
+ const shouldProceed = applyMissingRecordIdPolicy(
833
+ ctx,
834
+ "use .returning() to get the record id"
835
+ );
836
+ if (!shouldProceed) {
837
+ return;
838
+ }
839
+ await ctx.captureLog({
840
+ tableName: ctx.tableName,
841
+ operation: ctx.operation,
842
+ recordId: "unknown"
843
+ });
844
+ }
845
+ }
846
+ }
847
+ // Annotate the CommonJS export names for ESM import in node:
848
+ 0 && (module.exports = {
849
+ auditLogs,
850
+ buildCursorCondition,
851
+ buildOrderBy,
852
+ buildWhereConditions,
853
+ decodeCursor,
854
+ drizzleAuditAdapter,
855
+ encodeCursor,
856
+ withAuditProxy
857
+ });
858
+ //# sourceMappingURL=index.cjs.map