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