@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 +858 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +404 -0
- package/dist/index.d.ts +404 -0
- package/dist/index.js +845 -0
- package/dist/index.js.map +1 -0
- package/package.json +62 -0
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
|