@usebetterdev/audit-prisma 0.5.0-beta.1 → 0.6.0
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 +572 -12
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +63 -5
- package/dist/index.d.ts +63 -5
- package/dist/index.js +570 -11
- package/dist/index.js.map +1 -1
- package/package.json +4 -3
package/dist/index.js
CHANGED
|
@@ -1,3 +1,366 @@
|
|
|
1
|
+
// src/query.ts
|
|
2
|
+
import { parseDuration } from "@usebetterdev/audit-core";
|
|
3
|
+
|
|
4
|
+
// src/column-map.ts
|
|
5
|
+
var VALID_OPERATIONS = /* @__PURE__ */ new Set(["INSERT", "UPDATE", "DELETE"]);
|
|
6
|
+
var VALID_SEVERITIES = /* @__PURE__ */ new Set(["low", "medium", "high", "critical"]);
|
|
7
|
+
function isAuditOperation(value) {
|
|
8
|
+
return VALID_OPERATIONS.has(value);
|
|
9
|
+
}
|
|
10
|
+
function isAuditSeverity(value) {
|
|
11
|
+
return VALID_SEVERITIES.has(value);
|
|
12
|
+
}
|
|
13
|
+
function rowToAuditLog(row) {
|
|
14
|
+
const { id, timestamp, table_name, operation, record_id } = row;
|
|
15
|
+
if (typeof id !== "string") {
|
|
16
|
+
throw new Error("rowToAuditLog: id must be a string");
|
|
17
|
+
}
|
|
18
|
+
if (!(timestamp instanceof Date)) {
|
|
19
|
+
throw new Error("rowToAuditLog: timestamp must be a Date");
|
|
20
|
+
}
|
|
21
|
+
if (typeof table_name !== "string") {
|
|
22
|
+
throw new Error("rowToAuditLog: table_name must be a string");
|
|
23
|
+
}
|
|
24
|
+
if (typeof operation !== "string") {
|
|
25
|
+
throw new Error("rowToAuditLog: operation must be a string");
|
|
26
|
+
}
|
|
27
|
+
if (typeof record_id !== "string") {
|
|
28
|
+
throw new Error("rowToAuditLog: record_id must be a string");
|
|
29
|
+
}
|
|
30
|
+
if (!isAuditOperation(operation)) {
|
|
31
|
+
throw new Error(
|
|
32
|
+
`Invalid audit operation: "${operation}". Expected one of: INSERT, UPDATE, DELETE`
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
const log = {
|
|
36
|
+
id,
|
|
37
|
+
timestamp,
|
|
38
|
+
tableName: table_name,
|
|
39
|
+
operation,
|
|
40
|
+
recordId: record_id
|
|
41
|
+
};
|
|
42
|
+
const {
|
|
43
|
+
actor_id,
|
|
44
|
+
before_data,
|
|
45
|
+
after_data,
|
|
46
|
+
diff,
|
|
47
|
+
label,
|
|
48
|
+
description,
|
|
49
|
+
severity,
|
|
50
|
+
compliance,
|
|
51
|
+
notify,
|
|
52
|
+
reason,
|
|
53
|
+
metadata,
|
|
54
|
+
redacted_fields
|
|
55
|
+
} = row;
|
|
56
|
+
if (actor_id !== null && actor_id !== void 0) {
|
|
57
|
+
log.actorId = String(actor_id);
|
|
58
|
+
}
|
|
59
|
+
if (before_data !== null && before_data !== void 0) {
|
|
60
|
+
log.beforeData = before_data;
|
|
61
|
+
}
|
|
62
|
+
if (after_data !== null && after_data !== void 0) {
|
|
63
|
+
log.afterData = after_data;
|
|
64
|
+
}
|
|
65
|
+
if (diff !== null && diff !== void 0) {
|
|
66
|
+
log.diff = diff;
|
|
67
|
+
}
|
|
68
|
+
if (label !== null && label !== void 0) {
|
|
69
|
+
log.label = String(label);
|
|
70
|
+
}
|
|
71
|
+
if (description !== null && description !== void 0) {
|
|
72
|
+
log.description = String(description);
|
|
73
|
+
}
|
|
74
|
+
if (severity !== null && severity !== void 0) {
|
|
75
|
+
const s = String(severity);
|
|
76
|
+
if (!isAuditSeverity(s)) {
|
|
77
|
+
throw new Error(
|
|
78
|
+
`Invalid audit severity: "${s}". Expected one of: low, medium, high, critical`
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
log.severity = s;
|
|
82
|
+
}
|
|
83
|
+
if (compliance !== null && compliance !== void 0) {
|
|
84
|
+
log.compliance = compliance;
|
|
85
|
+
}
|
|
86
|
+
if (notify !== null && notify !== void 0) {
|
|
87
|
+
log.notify = Boolean(notify);
|
|
88
|
+
}
|
|
89
|
+
if (reason !== null && reason !== void 0) {
|
|
90
|
+
log.reason = String(reason);
|
|
91
|
+
}
|
|
92
|
+
if (metadata !== null && metadata !== void 0) {
|
|
93
|
+
log.metadata = metadata;
|
|
94
|
+
}
|
|
95
|
+
if (redacted_fields !== null && redacted_fields !== void 0) {
|
|
96
|
+
log.redactedFields = redacted_fields;
|
|
97
|
+
}
|
|
98
|
+
return log;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// src/query.ts
|
|
102
|
+
var DEFAULT_LIMIT = 50;
|
|
103
|
+
var MAX_LIMIT = 250;
|
|
104
|
+
var SELECT_COLUMNS = "id, timestamp, table_name, operation, record_id, actor_id, before_data, after_data, diff, label, description, severity, compliance, notify, reason, metadata, redacted_fields";
|
|
105
|
+
var UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
106
|
+
function encodeCursor(timestamp, id) {
|
|
107
|
+
const payload = JSON.stringify({ t: timestamp.toISOString(), i: id });
|
|
108
|
+
return btoa(payload);
|
|
109
|
+
}
|
|
110
|
+
function decodeCursor(cursor) {
|
|
111
|
+
let parsed;
|
|
112
|
+
try {
|
|
113
|
+
parsed = JSON.parse(atob(cursor));
|
|
114
|
+
} catch {
|
|
115
|
+
throw new Error("Invalid cursor: failed to decode");
|
|
116
|
+
}
|
|
117
|
+
if (typeof parsed !== "object" || parsed === null || !("t" in parsed) || !("i" in parsed)) {
|
|
118
|
+
throw new Error("Invalid cursor: missing required fields");
|
|
119
|
+
}
|
|
120
|
+
const { t, i } = parsed;
|
|
121
|
+
if (typeof t !== "string" || typeof i !== "string") {
|
|
122
|
+
throw new Error("Invalid cursor: fields must be strings");
|
|
123
|
+
}
|
|
124
|
+
const timestamp = new Date(t);
|
|
125
|
+
if (isNaN(timestamp.getTime())) {
|
|
126
|
+
throw new Error("Invalid cursor: invalid timestamp");
|
|
127
|
+
}
|
|
128
|
+
if (!UUID_PATTERN.test(i)) {
|
|
129
|
+
throw new Error("Invalid cursor: id must be a valid UUID");
|
|
130
|
+
}
|
|
131
|
+
return { timestamp, id: i };
|
|
132
|
+
}
|
|
133
|
+
function escapeLikePattern(input) {
|
|
134
|
+
return input.replace(/[%_\\]/g, "\\$&");
|
|
135
|
+
}
|
|
136
|
+
function resolveTimeFilter(filter) {
|
|
137
|
+
if ("date" in filter && filter.date !== void 0) {
|
|
138
|
+
return filter.date;
|
|
139
|
+
}
|
|
140
|
+
if ("duration" in filter && filter.duration !== void 0) {
|
|
141
|
+
return parseDuration(filter.duration);
|
|
142
|
+
}
|
|
143
|
+
throw new Error("TimeFilter must have either date or duration");
|
|
144
|
+
}
|
|
145
|
+
function toCount(value) {
|
|
146
|
+
if (typeof value === "number") {
|
|
147
|
+
return value;
|
|
148
|
+
}
|
|
149
|
+
if (typeof value === "string") {
|
|
150
|
+
return Number(value);
|
|
151
|
+
}
|
|
152
|
+
if (typeof value === "bigint") {
|
|
153
|
+
return Number(value);
|
|
154
|
+
}
|
|
155
|
+
return 0;
|
|
156
|
+
}
|
|
157
|
+
function addParam(state, value) {
|
|
158
|
+
state.params.push(value);
|
|
159
|
+
return `$${state.params.length}`;
|
|
160
|
+
}
|
|
161
|
+
function buildWhereClause(filters, cursor, sortOrder) {
|
|
162
|
+
const state = { fragments: [], params: [] };
|
|
163
|
+
if (filters.resource !== void 0) {
|
|
164
|
+
const p = addParam(state, filters.resource.tableName);
|
|
165
|
+
state.fragments.push(`table_name = ${p}`);
|
|
166
|
+
if (filters.resource.recordId !== void 0) {
|
|
167
|
+
const r = addParam(state, filters.resource.recordId);
|
|
168
|
+
state.fragments.push(`record_id = ${r}`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
if (filters.actorIds !== void 0 && filters.actorIds.length > 0) {
|
|
172
|
+
if (filters.actorIds.length === 1) {
|
|
173
|
+
const first = filters.actorIds[0];
|
|
174
|
+
if (first !== void 0) {
|
|
175
|
+
const p = addParam(state, first);
|
|
176
|
+
state.fragments.push(`actor_id = ${p}`);
|
|
177
|
+
}
|
|
178
|
+
} else {
|
|
179
|
+
const p = addParam(state, filters.actorIds);
|
|
180
|
+
state.fragments.push(`actor_id = ANY(${p}::text[])`);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
if (filters.severities !== void 0 && filters.severities.length > 0) {
|
|
184
|
+
if (filters.severities.length === 1) {
|
|
185
|
+
const first = filters.severities[0];
|
|
186
|
+
if (first !== void 0) {
|
|
187
|
+
const p = addParam(state, first);
|
|
188
|
+
state.fragments.push(`severity = ${p}`);
|
|
189
|
+
}
|
|
190
|
+
} else {
|
|
191
|
+
const p = addParam(state, filters.severities);
|
|
192
|
+
state.fragments.push(`severity = ANY(${p}::text[])`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
if (filters.operations !== void 0 && filters.operations.length > 0) {
|
|
196
|
+
if (filters.operations.length === 1) {
|
|
197
|
+
const first = filters.operations[0];
|
|
198
|
+
if (first !== void 0) {
|
|
199
|
+
const p = addParam(state, first);
|
|
200
|
+
state.fragments.push(`operation = ${p}`);
|
|
201
|
+
}
|
|
202
|
+
} else {
|
|
203
|
+
const p = addParam(state, filters.operations);
|
|
204
|
+
state.fragments.push(`operation = ANY(${p}::text[])`);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
if (filters.since !== void 0) {
|
|
208
|
+
const date = resolveTimeFilter(filters.since);
|
|
209
|
+
const p = addParam(state, date.toISOString());
|
|
210
|
+
state.fragments.push(`timestamp >= ${p}::timestamptz`);
|
|
211
|
+
}
|
|
212
|
+
if (filters.until !== void 0) {
|
|
213
|
+
const date = resolveTimeFilter(filters.until);
|
|
214
|
+
const p = addParam(state, date.toISOString());
|
|
215
|
+
state.fragments.push(`timestamp <= ${p}::timestamptz`);
|
|
216
|
+
}
|
|
217
|
+
if (filters.searchText !== void 0 && filters.searchText.length > 0) {
|
|
218
|
+
const escaped = escapeLikePattern(filters.searchText);
|
|
219
|
+
const pattern = `%${escaped}%`;
|
|
220
|
+
const p = addParam(state, pattern);
|
|
221
|
+
state.fragments.push(`(label ILIKE ${p} OR description ILIKE ${p})`);
|
|
222
|
+
}
|
|
223
|
+
if (filters.compliance !== void 0 && filters.compliance.length > 0) {
|
|
224
|
+
const p = addParam(state, JSON.stringify(filters.compliance));
|
|
225
|
+
state.fragments.push(`compliance @> ${p}::jsonb`);
|
|
226
|
+
}
|
|
227
|
+
if (cursor !== void 0) {
|
|
228
|
+
const tRef = addParam(state, cursor.timestamp.toISOString());
|
|
229
|
+
const iRef = addParam(state, cursor.id);
|
|
230
|
+
if (sortOrder === "asc") {
|
|
231
|
+
state.fragments.push(
|
|
232
|
+
`(timestamp > ${tRef}::timestamptz OR (timestamp = ${tRef}::timestamptz AND id::text > ${iRef}))`
|
|
233
|
+
);
|
|
234
|
+
} else {
|
|
235
|
+
state.fragments.push(
|
|
236
|
+
`(timestamp < ${tRef}::timestamptz OR (timestamp = ${tRef}::timestamptz AND id::text < ${iRef}))`
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return state;
|
|
241
|
+
}
|
|
242
|
+
async function queryLogs(spec, db) {
|
|
243
|
+
const sortOrder = spec.sortOrder ?? "desc";
|
|
244
|
+
const limit = Math.min(spec.limit ?? DEFAULT_LIMIT, MAX_LIMIT);
|
|
245
|
+
const fetchLimit = limit + 1;
|
|
246
|
+
const cursor = spec.cursor !== void 0 ? decodeCursor(spec.cursor) : void 0;
|
|
247
|
+
const where = buildWhereClause(spec.filters, cursor, sortOrder);
|
|
248
|
+
const queryParams = [...where.params, fetchLimit];
|
|
249
|
+
const limitRef = `$${queryParams.length}`;
|
|
250
|
+
const orderDir = sortOrder === "asc" ? "ASC" : "DESC";
|
|
251
|
+
const whereClause = where.fragments.length > 0 ? `WHERE ${where.fragments.join(" AND ")}` : "";
|
|
252
|
+
const sql = `SELECT ${SELECT_COLUMNS} FROM audit_logs ${whereClause} ORDER BY timestamp ${orderDir}, id ${orderDir} LIMIT ${limitRef}`;
|
|
253
|
+
const raw = await db.$queryRawUnsafe(sql, ...queryParams);
|
|
254
|
+
const hasNextPage = raw.length > limit;
|
|
255
|
+
const resultRows = hasNextPage ? raw.slice(0, -1) : raw;
|
|
256
|
+
const entries = resultRows.map(rowToAuditLog);
|
|
257
|
+
if (hasNextPage) {
|
|
258
|
+
const lastEntry = entries[entries.length - 1];
|
|
259
|
+
if (lastEntry !== void 0) {
|
|
260
|
+
return {
|
|
261
|
+
entries,
|
|
262
|
+
nextCursor: encodeCursor(lastEntry.timestamp, lastEntry.id)
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
return { entries };
|
|
267
|
+
}
|
|
268
|
+
async function getLogById(id, db) {
|
|
269
|
+
const sql = `SELECT ${SELECT_COLUMNS} FROM audit_logs WHERE id = $1::uuid LIMIT 1`;
|
|
270
|
+
const raw = await db.$queryRawUnsafe(sql, id);
|
|
271
|
+
const row = raw[0];
|
|
272
|
+
if (row === void 0) {
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
return rowToAuditLog(row);
|
|
276
|
+
}
|
|
277
|
+
async function purgeLogs(options, db) {
|
|
278
|
+
if (options.tableName !== void 0 && options.tableName.trim().length === 0) {
|
|
279
|
+
throw new Error(
|
|
280
|
+
"purgeLogs: tableName must be a non-empty string when provided"
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
let sql = "DELETE FROM audit_logs WHERE timestamp < $1::timestamptz";
|
|
284
|
+
const params = [options.before.toISOString()];
|
|
285
|
+
if (options.tableName !== void 0) {
|
|
286
|
+
params.push(options.tableName);
|
|
287
|
+
sql += ` AND table_name = $${params.length}`;
|
|
288
|
+
}
|
|
289
|
+
const deletedCount = await db.$executeRawUnsafe(sql, ...params);
|
|
290
|
+
return { deletedCount };
|
|
291
|
+
}
|
|
292
|
+
function buildSinceFragment(paramCount) {
|
|
293
|
+
const ref = `$${paramCount}::timestamptz`;
|
|
294
|
+
return {
|
|
295
|
+
where: ` WHERE timestamp >= ${ref}`,
|
|
296
|
+
and: ` AND timestamp >= ${ref}`
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
async function getStats(options, db) {
|
|
300
|
+
const params = [];
|
|
301
|
+
if (options.since !== void 0) {
|
|
302
|
+
params.push(options.since.toISOString());
|
|
303
|
+
}
|
|
304
|
+
const since = params.length > 0 ? buildSinceFragment(params.length) : { where: "", and: "" };
|
|
305
|
+
const summaryQuery = db.$queryRawUnsafe(
|
|
306
|
+
`SELECT COUNT(*) AS total_logs, COUNT(DISTINCT table_name) AS tables_audited FROM audit_logs${since.where}`,
|
|
307
|
+
...params
|
|
308
|
+
);
|
|
309
|
+
const eventsPerDayQuery = db.$queryRawUnsafe(
|
|
310
|
+
`SELECT to_char(date_trunc('day', timestamp), 'YYYY-MM-DD') AS date, COUNT(*) AS count FROM audit_logs${since.where} GROUP BY date_trunc('day', timestamp) ORDER BY date_trunc('day', timestamp) LIMIT 365`,
|
|
311
|
+
...params
|
|
312
|
+
);
|
|
313
|
+
const topActorsQuery = db.$queryRawUnsafe(
|
|
314
|
+
`SELECT actor_id, COUNT(*) AS count FROM audit_logs WHERE actor_id IS NOT NULL${since.and} GROUP BY actor_id ORDER BY COUNT(*) DESC LIMIT 10`,
|
|
315
|
+
...params
|
|
316
|
+
);
|
|
317
|
+
const topTablesQuery = db.$queryRawUnsafe(
|
|
318
|
+
`SELECT table_name, COUNT(*) AS count FROM audit_logs${since.where} GROUP BY table_name ORDER BY COUNT(*) DESC LIMIT 10`,
|
|
319
|
+
...params
|
|
320
|
+
);
|
|
321
|
+
const operationQuery = db.$queryRawUnsafe(
|
|
322
|
+
`SELECT operation, COUNT(*) AS count FROM audit_logs${since.where} GROUP BY operation`,
|
|
323
|
+
...params
|
|
324
|
+
);
|
|
325
|
+
const severityQuery = db.$queryRawUnsafe(
|
|
326
|
+
`SELECT severity, COUNT(*) AS count FROM audit_logs WHERE severity IS NOT NULL${since.and} GROUP BY severity`,
|
|
327
|
+
...params
|
|
328
|
+
);
|
|
329
|
+
const [summaryRows, eventsPerDayRows, topActorsRows, topTablesRows, operationRows, severityRows] = await Promise.all([summaryQuery, eventsPerDayQuery, topActorsQuery, topTablesQuery, operationQuery, severityQuery]);
|
|
330
|
+
const summary = summaryRows[0];
|
|
331
|
+
const totalLogs = summary !== void 0 ? toCount(summary.total_logs) : 0;
|
|
332
|
+
const tablesAudited = summary !== void 0 ? toCount(summary.tables_audited) : 0;
|
|
333
|
+
const eventsPerDay = eventsPerDayRows.map((row) => ({
|
|
334
|
+
date: String(row.date),
|
|
335
|
+
count: toCount(row.count)
|
|
336
|
+
}));
|
|
337
|
+
const topActors = topActorsRows.map((row) => ({
|
|
338
|
+
actorId: String(row.actor_id),
|
|
339
|
+
count: toCount(row.count)
|
|
340
|
+
}));
|
|
341
|
+
const topTables = topTablesRows.map((row) => ({
|
|
342
|
+
tableName: String(row.table_name),
|
|
343
|
+
count: toCount(row.count)
|
|
344
|
+
}));
|
|
345
|
+
const operationBreakdown = {};
|
|
346
|
+
for (const row of operationRows) {
|
|
347
|
+
operationBreakdown[String(row.operation)] = toCount(row.count);
|
|
348
|
+
}
|
|
349
|
+
const severityBreakdown = {};
|
|
350
|
+
for (const row of severityRows) {
|
|
351
|
+
severityBreakdown[String(row.severity)] = toCount(row.count);
|
|
352
|
+
}
|
|
353
|
+
return {
|
|
354
|
+
totalLogs,
|
|
355
|
+
tablesAudited,
|
|
356
|
+
eventsPerDay,
|
|
357
|
+
topActors,
|
|
358
|
+
topTables,
|
|
359
|
+
operationBreakdown,
|
|
360
|
+
severityBreakdown
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
|
|
1
364
|
// src/adapter.ts
|
|
2
365
|
function prismaAuditAdapter(db) {
|
|
3
366
|
return {
|
|
@@ -32,6 +395,18 @@ function prismaAuditAdapter(db) {
|
|
|
32
395
|
jsonOrNull(log.metadata),
|
|
33
396
|
jsonOrNull(log.redactedFields)
|
|
34
397
|
);
|
|
398
|
+
},
|
|
399
|
+
async queryLogs(spec) {
|
|
400
|
+
return queryLogs(spec, db);
|
|
401
|
+
},
|
|
402
|
+
async getLogById(id) {
|
|
403
|
+
return getLogById(id, db);
|
|
404
|
+
},
|
|
405
|
+
async getStats(options) {
|
|
406
|
+
return getStats(options ?? {}, db);
|
|
407
|
+
},
|
|
408
|
+
async purgeLogs(options) {
|
|
409
|
+
return purgeLogs(options, db);
|
|
35
410
|
}
|
|
36
411
|
};
|
|
37
412
|
}
|
|
@@ -98,11 +473,23 @@ function buildTableNameResolver(prisma, transform) {
|
|
|
98
473
|
return (modelName) => modelName;
|
|
99
474
|
}
|
|
100
475
|
|
|
476
|
+
// src/tx-store.ts
|
|
477
|
+
import { AsyncLocalStorage } from "async_hooks";
|
|
478
|
+
var txStorage = new AsyncLocalStorage();
|
|
479
|
+
function runWithTxClient(tx, fn) {
|
|
480
|
+
return txStorage.run(tx, fn);
|
|
481
|
+
}
|
|
482
|
+
function getTxClient() {
|
|
483
|
+
return txStorage.getStore();
|
|
484
|
+
}
|
|
485
|
+
|
|
101
486
|
// src/extension.ts
|
|
102
487
|
function withAuditExtension(prisma, captureLog, options = {}) {
|
|
103
488
|
const bulkMode = options.bulkMode ?? "per-row";
|
|
104
489
|
const handleError = buildErrorHandler(options.onError);
|
|
105
490
|
const metadata = options.metadata;
|
|
491
|
+
const skipBeforeCapture = options.skipBeforeCapture;
|
|
492
|
+
const maxBeforeStateRows = options.maxBeforeStateRows ?? 100;
|
|
106
493
|
const resolveTableName = buildTableNameResolver(prisma, options.tableNameTransform);
|
|
107
494
|
const extended = prisma.$extends({
|
|
108
495
|
query: {
|
|
@@ -113,12 +500,14 @@ function withAuditExtension(prisma, captureLog, options = {}) {
|
|
|
113
500
|
args,
|
|
114
501
|
query
|
|
115
502
|
}) {
|
|
116
|
-
const result = await query(args);
|
|
117
503
|
const auditOp = getAuditOperation(operation);
|
|
118
504
|
if (auditOp === void 0) {
|
|
119
|
-
return
|
|
505
|
+
return query(args);
|
|
120
506
|
}
|
|
121
507
|
const tableName = resolveTableName(model);
|
|
508
|
+
const shouldSkipBefore = skipBeforeCapture !== void 0 && skipBeforeCapture.includes(tableName);
|
|
509
|
+
const beforeState = shouldSkipBefore ? { type: "none" } : await fetchBeforeState({ prisma, model, operation, args, handleError, maxBeforeStateRows });
|
|
510
|
+
const result = await query(args);
|
|
122
511
|
try {
|
|
123
512
|
await fireCaptureLog({
|
|
124
513
|
tableName,
|
|
@@ -126,9 +515,11 @@ function withAuditExtension(prisma, captureLog, options = {}) {
|
|
|
126
515
|
auditOp,
|
|
127
516
|
args,
|
|
128
517
|
result,
|
|
518
|
+
beforeState,
|
|
129
519
|
captureLog,
|
|
130
520
|
bulkMode,
|
|
131
|
-
metadata
|
|
521
|
+
metadata,
|
|
522
|
+
actorId: getAuditContext()?.actorId
|
|
132
523
|
});
|
|
133
524
|
} catch (err) {
|
|
134
525
|
handleError(err);
|
|
@@ -140,17 +531,62 @@ function withAuditExtension(prisma, captureLog, options = {}) {
|
|
|
140
531
|
});
|
|
141
532
|
return extended;
|
|
142
533
|
}
|
|
534
|
+
async function fetchBeforeState({
|
|
535
|
+
prisma,
|
|
536
|
+
model,
|
|
537
|
+
operation,
|
|
538
|
+
args,
|
|
539
|
+
handleError,
|
|
540
|
+
maxBeforeStateRows
|
|
541
|
+
}) {
|
|
542
|
+
if (operation === "create" || operation === "createMany" || operation === "createManyAndReturn" || operation === "delete") {
|
|
543
|
+
return { type: "none" };
|
|
544
|
+
}
|
|
545
|
+
const delegate = getModelDelegate(getTxClient() ?? prisma, model);
|
|
546
|
+
if (delegate === void 0) {
|
|
547
|
+
return { type: "none" };
|
|
548
|
+
}
|
|
549
|
+
if (operation === "update" || operation === "upsert") {
|
|
550
|
+
const where = getArgsWhere(args);
|
|
551
|
+
if (where === void 0) {
|
|
552
|
+
return { type: "single", row: void 0 };
|
|
553
|
+
}
|
|
554
|
+
try {
|
|
555
|
+
const found = await delegate.findUnique({ where });
|
|
556
|
+
return { type: "single", row: toRecord(found) };
|
|
557
|
+
} catch (err) {
|
|
558
|
+
handleError(err);
|
|
559
|
+
return { type: "single", row: void 0 };
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
if (operation === "updateMany" || operation === "deleteMany") {
|
|
563
|
+
const where = getArgsWhere(args);
|
|
564
|
+
try {
|
|
565
|
+
const rows = await delegate.findMany({ where: where ?? {}, take: maxBeforeStateRows + 1 });
|
|
566
|
+
if (rows.length > maxBeforeStateRows) {
|
|
567
|
+
return { type: "none" };
|
|
568
|
+
}
|
|
569
|
+
const records = rows.map((r) => toRecord(r)).filter((r) => r !== void 0);
|
|
570
|
+
return { type: "many", rows: records };
|
|
571
|
+
} catch (err) {
|
|
572
|
+
handleError(err);
|
|
573
|
+
return { type: "many", rows: [] };
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
return { type: "none" };
|
|
577
|
+
}
|
|
143
578
|
async function fireCaptureLog({
|
|
144
579
|
tableName,
|
|
145
580
|
operation,
|
|
146
581
|
auditOp,
|
|
147
582
|
args,
|
|
148
583
|
result,
|
|
584
|
+
beforeState,
|
|
149
585
|
captureLog,
|
|
150
586
|
bulkMode,
|
|
151
|
-
metadata
|
|
587
|
+
metadata,
|
|
588
|
+
actorId
|
|
152
589
|
}) {
|
|
153
|
-
const actorId = getAuditContext()?.actorId;
|
|
154
590
|
if (operation === "createMany") {
|
|
155
591
|
await handleCreateMany({ tableName, auditOp, args, captureLog, bulkMode, metadata, actorId });
|
|
156
592
|
return;
|
|
@@ -159,18 +595,30 @@ async function fireCaptureLog({
|
|
|
159
595
|
await handleCreateManyAndReturn({ tableName, auditOp, result, captureLog, bulkMode, metadata, actorId });
|
|
160
596
|
return;
|
|
161
597
|
}
|
|
162
|
-
if (operation === "updateMany"
|
|
598
|
+
if (operation === "updateMany") {
|
|
599
|
+
await handleUpdateMany({ tableName, auditOp, beforeState, captureLog, bulkMode, metadata, actorId });
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
if (operation === "deleteMany") {
|
|
603
|
+
await handleDeleteMany({ tableName, auditOp, beforeState, captureLog, bulkMode, metadata, actorId });
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
const row = toRecord(result);
|
|
607
|
+
const recordId = (row !== void 0 ? extractId(row) : void 0) ?? "unknown";
|
|
608
|
+
if (operation === "upsert") {
|
|
609
|
+
const before = beforeState.type === "single" ? beforeState.row : void 0;
|
|
610
|
+
const effectiveOp = before !== void 0 ? "UPDATE" : "INSERT";
|
|
163
611
|
await captureLog({
|
|
164
612
|
tableName,
|
|
165
|
-
operation:
|
|
166
|
-
recordId
|
|
613
|
+
operation: effectiveOp,
|
|
614
|
+
recordId,
|
|
615
|
+
...before !== void 0 && { before },
|
|
616
|
+
...row !== void 0 && { after: row },
|
|
167
617
|
...metadata !== void 0 && { metadata },
|
|
168
618
|
...actorId !== void 0 && { actorId }
|
|
169
619
|
});
|
|
170
620
|
return;
|
|
171
621
|
}
|
|
172
|
-
const row = toRecord(result);
|
|
173
|
-
const recordId = (row !== void 0 ? extractId(row) : void 0) ?? "unknown";
|
|
174
622
|
if (auditOp === "INSERT") {
|
|
175
623
|
await captureLog({
|
|
176
624
|
tableName,
|
|
@@ -183,10 +631,12 @@ async function fireCaptureLog({
|
|
|
183
631
|
return;
|
|
184
632
|
}
|
|
185
633
|
if (auditOp === "UPDATE") {
|
|
634
|
+
const before = beforeState.type === "single" ? beforeState.row : void 0;
|
|
186
635
|
await captureLog({
|
|
187
636
|
tableName,
|
|
188
637
|
operation: auditOp,
|
|
189
638
|
recordId,
|
|
639
|
+
...before !== void 0 && { before },
|
|
190
640
|
...row !== void 0 && { after: row },
|
|
191
641
|
...metadata !== void 0 && { metadata },
|
|
192
642
|
...actorId !== void 0 && { actorId }
|
|
@@ -292,6 +742,94 @@ async function handleCreateManyAndReturn({
|
|
|
292
742
|
})
|
|
293
743
|
);
|
|
294
744
|
}
|
|
745
|
+
async function handleUpdateMany({
|
|
746
|
+
tableName,
|
|
747
|
+
auditOp,
|
|
748
|
+
beforeState,
|
|
749
|
+
captureLog,
|
|
750
|
+
bulkMode,
|
|
751
|
+
metadata,
|
|
752
|
+
actorId
|
|
753
|
+
}) {
|
|
754
|
+
if (bulkMode === "bulk") {
|
|
755
|
+
await captureLog({
|
|
756
|
+
tableName,
|
|
757
|
+
operation: auditOp,
|
|
758
|
+
recordId: "unknown",
|
|
759
|
+
...metadata !== void 0 && { metadata },
|
|
760
|
+
...actorId !== void 0 && { actorId }
|
|
761
|
+
});
|
|
762
|
+
return;
|
|
763
|
+
}
|
|
764
|
+
const rows = beforeState.type === "many" ? beforeState.rows : [];
|
|
765
|
+
if (rows.length === 0) {
|
|
766
|
+
await captureLog({
|
|
767
|
+
tableName,
|
|
768
|
+
operation: auditOp,
|
|
769
|
+
recordId: "unknown",
|
|
770
|
+
...metadata !== void 0 && { metadata },
|
|
771
|
+
...actorId !== void 0 && { actorId }
|
|
772
|
+
});
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
775
|
+
await Promise.all(
|
|
776
|
+
rows.map((row) => {
|
|
777
|
+
const recordId = extractId(row) ?? "unknown";
|
|
778
|
+
return captureLog({
|
|
779
|
+
tableName,
|
|
780
|
+
operation: auditOp,
|
|
781
|
+
recordId,
|
|
782
|
+
before: row,
|
|
783
|
+
...metadata !== void 0 && { metadata },
|
|
784
|
+
...actorId !== void 0 && { actorId }
|
|
785
|
+
});
|
|
786
|
+
})
|
|
787
|
+
);
|
|
788
|
+
}
|
|
789
|
+
async function handleDeleteMany({
|
|
790
|
+
tableName,
|
|
791
|
+
auditOp,
|
|
792
|
+
beforeState,
|
|
793
|
+
captureLog,
|
|
794
|
+
bulkMode,
|
|
795
|
+
metadata,
|
|
796
|
+
actorId
|
|
797
|
+
}) {
|
|
798
|
+
if (bulkMode === "bulk") {
|
|
799
|
+
await captureLog({
|
|
800
|
+
tableName,
|
|
801
|
+
operation: auditOp,
|
|
802
|
+
recordId: "unknown",
|
|
803
|
+
...metadata !== void 0 && { metadata },
|
|
804
|
+
...actorId !== void 0 && { actorId }
|
|
805
|
+
});
|
|
806
|
+
return;
|
|
807
|
+
}
|
|
808
|
+
const rows = beforeState.type === "many" ? beforeState.rows : [];
|
|
809
|
+
if (rows.length === 0) {
|
|
810
|
+
await captureLog({
|
|
811
|
+
tableName,
|
|
812
|
+
operation: auditOp,
|
|
813
|
+
recordId: "unknown",
|
|
814
|
+
...metadata !== void 0 && { metadata },
|
|
815
|
+
...actorId !== void 0 && { actorId }
|
|
816
|
+
});
|
|
817
|
+
return;
|
|
818
|
+
}
|
|
819
|
+
await Promise.all(
|
|
820
|
+
rows.map((row) => {
|
|
821
|
+
const recordId = extractId(row) ?? "unknown";
|
|
822
|
+
return captureLog({
|
|
823
|
+
tableName,
|
|
824
|
+
operation: auditOp,
|
|
825
|
+
recordId,
|
|
826
|
+
before: row,
|
|
827
|
+
...metadata !== void 0 && { metadata },
|
|
828
|
+
...actorId !== void 0 && { actorId }
|
|
829
|
+
});
|
|
830
|
+
})
|
|
831
|
+
);
|
|
832
|
+
}
|
|
295
833
|
function buildErrorHandler(onError) {
|
|
296
834
|
return (error) => {
|
|
297
835
|
try {
|
|
@@ -305,6 +843,20 @@ function buildErrorHandler(onError) {
|
|
|
305
843
|
}
|
|
306
844
|
};
|
|
307
845
|
}
|
|
846
|
+
function isModelDelegate(value) {
|
|
847
|
+
if (value === null || value === void 0 || typeof value !== "object") {
|
|
848
|
+
return false;
|
|
849
|
+
}
|
|
850
|
+
return "findUnique" in value && "findMany" in value && typeof Reflect.get(value, "findUnique") === "function" && typeof Reflect.get(value, "findMany") === "function";
|
|
851
|
+
}
|
|
852
|
+
function getModelDelegate(client, model) {
|
|
853
|
+
const lowerModel = model.charAt(0).toLowerCase() + model.slice(1);
|
|
854
|
+
const value = Reflect.get(Object(client), lowerModel);
|
|
855
|
+
if (isModelDelegate(value)) {
|
|
856
|
+
return value;
|
|
857
|
+
}
|
|
858
|
+
return void 0;
|
|
859
|
+
}
|
|
308
860
|
function extractId(record) {
|
|
309
861
|
const id = record["id"];
|
|
310
862
|
if (id !== void 0 && id !== null) {
|
|
@@ -320,16 +872,23 @@ function toRecord(value) {
|
|
|
320
872
|
}
|
|
321
873
|
function getArgsData(args) {
|
|
322
874
|
if (args !== null && typeof args === "object" && "data" in args) {
|
|
323
|
-
const data = args
|
|
875
|
+
const data = Reflect.get(args, "data");
|
|
324
876
|
if (Array.isArray(data)) {
|
|
325
877
|
return data;
|
|
326
878
|
}
|
|
327
879
|
}
|
|
328
880
|
return [];
|
|
329
881
|
}
|
|
882
|
+
function getArgsWhere(args) {
|
|
883
|
+
if (args !== null && typeof args === "object" && "where" in args) {
|
|
884
|
+
return Reflect.get(args, "where");
|
|
885
|
+
}
|
|
886
|
+
return void 0;
|
|
887
|
+
}
|
|
330
888
|
export {
|
|
331
889
|
prismaAuditAdapter,
|
|
332
890
|
prismaModelMap,
|
|
891
|
+
runWithTxClient,
|
|
333
892
|
withAuditExtension
|
|
334
893
|
};
|
|
335
894
|
//# sourceMappingURL=index.js.map
|