@usebetterdev/audit-prisma 0.5.0-beta.1 → 0.5.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.cjs
CHANGED
|
@@ -22,10 +22,374 @@ var index_exports = {};
|
|
|
22
22
|
__export(index_exports, {
|
|
23
23
|
prismaAuditAdapter: () => prismaAuditAdapter,
|
|
24
24
|
prismaModelMap: () => prismaModelMap,
|
|
25
|
+
runWithTxClient: () => runWithTxClient,
|
|
25
26
|
withAuditExtension: () => withAuditExtension
|
|
26
27
|
});
|
|
27
28
|
module.exports = __toCommonJS(index_exports);
|
|
28
29
|
|
|
30
|
+
// src/query.ts
|
|
31
|
+
var import_audit_core = require("@usebetterdev/audit-core");
|
|
32
|
+
|
|
33
|
+
// src/column-map.ts
|
|
34
|
+
var VALID_OPERATIONS = /* @__PURE__ */ new Set(["INSERT", "UPDATE", "DELETE"]);
|
|
35
|
+
var VALID_SEVERITIES = /* @__PURE__ */ new Set(["low", "medium", "high", "critical"]);
|
|
36
|
+
function isAuditOperation(value) {
|
|
37
|
+
return VALID_OPERATIONS.has(value);
|
|
38
|
+
}
|
|
39
|
+
function isAuditSeverity(value) {
|
|
40
|
+
return VALID_SEVERITIES.has(value);
|
|
41
|
+
}
|
|
42
|
+
function rowToAuditLog(row) {
|
|
43
|
+
const { id, timestamp, table_name, operation, record_id } = row;
|
|
44
|
+
if (typeof id !== "string") {
|
|
45
|
+
throw new Error("rowToAuditLog: id must be a string");
|
|
46
|
+
}
|
|
47
|
+
if (!(timestamp instanceof Date)) {
|
|
48
|
+
throw new Error("rowToAuditLog: timestamp must be a Date");
|
|
49
|
+
}
|
|
50
|
+
if (typeof table_name !== "string") {
|
|
51
|
+
throw new Error("rowToAuditLog: table_name must be a string");
|
|
52
|
+
}
|
|
53
|
+
if (typeof operation !== "string") {
|
|
54
|
+
throw new Error("rowToAuditLog: operation must be a string");
|
|
55
|
+
}
|
|
56
|
+
if (typeof record_id !== "string") {
|
|
57
|
+
throw new Error("rowToAuditLog: record_id must be a string");
|
|
58
|
+
}
|
|
59
|
+
if (!isAuditOperation(operation)) {
|
|
60
|
+
throw new Error(
|
|
61
|
+
`Invalid audit operation: "${operation}". Expected one of: INSERT, UPDATE, DELETE`
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
const log = {
|
|
65
|
+
id,
|
|
66
|
+
timestamp,
|
|
67
|
+
tableName: table_name,
|
|
68
|
+
operation,
|
|
69
|
+
recordId: record_id
|
|
70
|
+
};
|
|
71
|
+
const {
|
|
72
|
+
actor_id,
|
|
73
|
+
before_data,
|
|
74
|
+
after_data,
|
|
75
|
+
diff,
|
|
76
|
+
label,
|
|
77
|
+
description,
|
|
78
|
+
severity,
|
|
79
|
+
compliance,
|
|
80
|
+
notify,
|
|
81
|
+
reason,
|
|
82
|
+
metadata,
|
|
83
|
+
redacted_fields
|
|
84
|
+
} = row;
|
|
85
|
+
if (actor_id !== null && actor_id !== void 0) {
|
|
86
|
+
log.actorId = String(actor_id);
|
|
87
|
+
}
|
|
88
|
+
if (before_data !== null && before_data !== void 0) {
|
|
89
|
+
log.beforeData = before_data;
|
|
90
|
+
}
|
|
91
|
+
if (after_data !== null && after_data !== void 0) {
|
|
92
|
+
log.afterData = after_data;
|
|
93
|
+
}
|
|
94
|
+
if (diff !== null && diff !== void 0) {
|
|
95
|
+
log.diff = diff;
|
|
96
|
+
}
|
|
97
|
+
if (label !== null && label !== void 0) {
|
|
98
|
+
log.label = String(label);
|
|
99
|
+
}
|
|
100
|
+
if (description !== null && description !== void 0) {
|
|
101
|
+
log.description = String(description);
|
|
102
|
+
}
|
|
103
|
+
if (severity !== null && severity !== void 0) {
|
|
104
|
+
const s = String(severity);
|
|
105
|
+
if (!isAuditSeverity(s)) {
|
|
106
|
+
throw new Error(
|
|
107
|
+
`Invalid audit severity: "${s}". Expected one of: low, medium, high, critical`
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
log.severity = s;
|
|
111
|
+
}
|
|
112
|
+
if (compliance !== null && compliance !== void 0) {
|
|
113
|
+
log.compliance = compliance;
|
|
114
|
+
}
|
|
115
|
+
if (notify !== null && notify !== void 0) {
|
|
116
|
+
log.notify = Boolean(notify);
|
|
117
|
+
}
|
|
118
|
+
if (reason !== null && reason !== void 0) {
|
|
119
|
+
log.reason = String(reason);
|
|
120
|
+
}
|
|
121
|
+
if (metadata !== null && metadata !== void 0) {
|
|
122
|
+
log.metadata = metadata;
|
|
123
|
+
}
|
|
124
|
+
if (redacted_fields !== null && redacted_fields !== void 0) {
|
|
125
|
+
log.redactedFields = redacted_fields;
|
|
126
|
+
}
|
|
127
|
+
return log;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// src/query.ts
|
|
131
|
+
var DEFAULT_LIMIT = 50;
|
|
132
|
+
var MAX_LIMIT = 250;
|
|
133
|
+
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";
|
|
134
|
+
var UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
135
|
+
function encodeCursor(timestamp, id) {
|
|
136
|
+
const payload = JSON.stringify({ t: timestamp.toISOString(), i: id });
|
|
137
|
+
return btoa(payload);
|
|
138
|
+
}
|
|
139
|
+
function decodeCursor(cursor) {
|
|
140
|
+
let parsed;
|
|
141
|
+
try {
|
|
142
|
+
parsed = JSON.parse(atob(cursor));
|
|
143
|
+
} catch {
|
|
144
|
+
throw new Error("Invalid cursor: failed to decode");
|
|
145
|
+
}
|
|
146
|
+
if (typeof parsed !== "object" || parsed === null || !("t" in parsed) || !("i" in parsed)) {
|
|
147
|
+
throw new Error("Invalid cursor: missing required fields");
|
|
148
|
+
}
|
|
149
|
+
const { t, i } = parsed;
|
|
150
|
+
if (typeof t !== "string" || typeof i !== "string") {
|
|
151
|
+
throw new Error("Invalid cursor: fields must be strings");
|
|
152
|
+
}
|
|
153
|
+
const timestamp = new Date(t);
|
|
154
|
+
if (isNaN(timestamp.getTime())) {
|
|
155
|
+
throw new Error("Invalid cursor: invalid timestamp");
|
|
156
|
+
}
|
|
157
|
+
if (!UUID_PATTERN.test(i)) {
|
|
158
|
+
throw new Error("Invalid cursor: id must be a valid UUID");
|
|
159
|
+
}
|
|
160
|
+
return { timestamp, id: i };
|
|
161
|
+
}
|
|
162
|
+
function escapeLikePattern(input) {
|
|
163
|
+
return input.replace(/[%_\\]/g, "\\$&");
|
|
164
|
+
}
|
|
165
|
+
function resolveTimeFilter(filter) {
|
|
166
|
+
if ("date" in filter && filter.date !== void 0) {
|
|
167
|
+
return filter.date;
|
|
168
|
+
}
|
|
169
|
+
if ("duration" in filter && filter.duration !== void 0) {
|
|
170
|
+
return (0, import_audit_core.parseDuration)(filter.duration);
|
|
171
|
+
}
|
|
172
|
+
throw new Error("TimeFilter must have either date or duration");
|
|
173
|
+
}
|
|
174
|
+
function toCount(value) {
|
|
175
|
+
if (typeof value === "number") {
|
|
176
|
+
return value;
|
|
177
|
+
}
|
|
178
|
+
if (typeof value === "string") {
|
|
179
|
+
return Number(value);
|
|
180
|
+
}
|
|
181
|
+
if (typeof value === "bigint") {
|
|
182
|
+
return Number(value);
|
|
183
|
+
}
|
|
184
|
+
return 0;
|
|
185
|
+
}
|
|
186
|
+
function addParam(state, value) {
|
|
187
|
+
state.params.push(value);
|
|
188
|
+
return `$${state.params.length}`;
|
|
189
|
+
}
|
|
190
|
+
function buildWhereClause(filters, cursor, sortOrder) {
|
|
191
|
+
const state = { fragments: [], params: [] };
|
|
192
|
+
if (filters.resource !== void 0) {
|
|
193
|
+
const p = addParam(state, filters.resource.tableName);
|
|
194
|
+
state.fragments.push(`table_name = ${p}`);
|
|
195
|
+
if (filters.resource.recordId !== void 0) {
|
|
196
|
+
const r = addParam(state, filters.resource.recordId);
|
|
197
|
+
state.fragments.push(`record_id = ${r}`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
if (filters.actorIds !== void 0 && filters.actorIds.length > 0) {
|
|
201
|
+
if (filters.actorIds.length === 1) {
|
|
202
|
+
const first = filters.actorIds[0];
|
|
203
|
+
if (first !== void 0) {
|
|
204
|
+
const p = addParam(state, first);
|
|
205
|
+
state.fragments.push(`actor_id = ${p}`);
|
|
206
|
+
}
|
|
207
|
+
} else {
|
|
208
|
+
const p = addParam(state, filters.actorIds);
|
|
209
|
+
state.fragments.push(`actor_id = ANY(${p}::text[])`);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
if (filters.severities !== void 0 && filters.severities.length > 0) {
|
|
213
|
+
if (filters.severities.length === 1) {
|
|
214
|
+
const first = filters.severities[0];
|
|
215
|
+
if (first !== void 0) {
|
|
216
|
+
const p = addParam(state, first);
|
|
217
|
+
state.fragments.push(`severity = ${p}`);
|
|
218
|
+
}
|
|
219
|
+
} else {
|
|
220
|
+
const p = addParam(state, filters.severities);
|
|
221
|
+
state.fragments.push(`severity = ANY(${p}::text[])`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
if (filters.operations !== void 0 && filters.operations.length > 0) {
|
|
225
|
+
if (filters.operations.length === 1) {
|
|
226
|
+
const first = filters.operations[0];
|
|
227
|
+
if (first !== void 0) {
|
|
228
|
+
const p = addParam(state, first);
|
|
229
|
+
state.fragments.push(`operation = ${p}`);
|
|
230
|
+
}
|
|
231
|
+
} else {
|
|
232
|
+
const p = addParam(state, filters.operations);
|
|
233
|
+
state.fragments.push(`operation = ANY(${p}::text[])`);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
if (filters.since !== void 0) {
|
|
237
|
+
const date = resolveTimeFilter(filters.since);
|
|
238
|
+
const p = addParam(state, date.toISOString());
|
|
239
|
+
state.fragments.push(`timestamp >= ${p}::timestamptz`);
|
|
240
|
+
}
|
|
241
|
+
if (filters.until !== void 0) {
|
|
242
|
+
const date = resolveTimeFilter(filters.until);
|
|
243
|
+
const p = addParam(state, date.toISOString());
|
|
244
|
+
state.fragments.push(`timestamp <= ${p}::timestamptz`);
|
|
245
|
+
}
|
|
246
|
+
if (filters.searchText !== void 0 && filters.searchText.length > 0) {
|
|
247
|
+
const escaped = escapeLikePattern(filters.searchText);
|
|
248
|
+
const pattern = `%${escaped}%`;
|
|
249
|
+
const p = addParam(state, pattern);
|
|
250
|
+
state.fragments.push(`(label ILIKE ${p} OR description ILIKE ${p})`);
|
|
251
|
+
}
|
|
252
|
+
if (filters.compliance !== void 0 && filters.compliance.length > 0) {
|
|
253
|
+
const p = addParam(state, JSON.stringify(filters.compliance));
|
|
254
|
+
state.fragments.push(`compliance @> ${p}::jsonb`);
|
|
255
|
+
}
|
|
256
|
+
if (cursor !== void 0) {
|
|
257
|
+
const tRef = addParam(state, cursor.timestamp.toISOString());
|
|
258
|
+
const iRef = addParam(state, cursor.id);
|
|
259
|
+
if (sortOrder === "asc") {
|
|
260
|
+
state.fragments.push(
|
|
261
|
+
`(timestamp > ${tRef}::timestamptz OR (timestamp = ${tRef}::timestamptz AND id::text > ${iRef}))`
|
|
262
|
+
);
|
|
263
|
+
} else {
|
|
264
|
+
state.fragments.push(
|
|
265
|
+
`(timestamp < ${tRef}::timestamptz OR (timestamp = ${tRef}::timestamptz AND id::text < ${iRef}))`
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return state;
|
|
270
|
+
}
|
|
271
|
+
async function queryLogs(spec, db) {
|
|
272
|
+
const sortOrder = spec.sortOrder ?? "desc";
|
|
273
|
+
const limit = Math.min(spec.limit ?? DEFAULT_LIMIT, MAX_LIMIT);
|
|
274
|
+
const fetchLimit = limit + 1;
|
|
275
|
+
const cursor = spec.cursor !== void 0 ? decodeCursor(spec.cursor) : void 0;
|
|
276
|
+
const where = buildWhereClause(spec.filters, cursor, sortOrder);
|
|
277
|
+
const queryParams = [...where.params, fetchLimit];
|
|
278
|
+
const limitRef = `$${queryParams.length}`;
|
|
279
|
+
const orderDir = sortOrder === "asc" ? "ASC" : "DESC";
|
|
280
|
+
const whereClause = where.fragments.length > 0 ? `WHERE ${where.fragments.join(" AND ")}` : "";
|
|
281
|
+
const sql = `SELECT ${SELECT_COLUMNS} FROM audit_logs ${whereClause} ORDER BY timestamp ${orderDir}, id ${orderDir} LIMIT ${limitRef}`;
|
|
282
|
+
const raw = await db.$queryRawUnsafe(sql, ...queryParams);
|
|
283
|
+
const hasNextPage = raw.length > limit;
|
|
284
|
+
const resultRows = hasNextPage ? raw.slice(0, -1) : raw;
|
|
285
|
+
const entries = resultRows.map(rowToAuditLog);
|
|
286
|
+
if (hasNextPage) {
|
|
287
|
+
const lastEntry = entries[entries.length - 1];
|
|
288
|
+
if (lastEntry !== void 0) {
|
|
289
|
+
return {
|
|
290
|
+
entries,
|
|
291
|
+
nextCursor: encodeCursor(lastEntry.timestamp, lastEntry.id)
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
return { entries };
|
|
296
|
+
}
|
|
297
|
+
async function getLogById(id, db) {
|
|
298
|
+
const sql = `SELECT ${SELECT_COLUMNS} FROM audit_logs WHERE id = $1::uuid LIMIT 1`;
|
|
299
|
+
const raw = await db.$queryRawUnsafe(sql, id);
|
|
300
|
+
const row = raw[0];
|
|
301
|
+
if (row === void 0) {
|
|
302
|
+
return null;
|
|
303
|
+
}
|
|
304
|
+
return rowToAuditLog(row);
|
|
305
|
+
}
|
|
306
|
+
async function purgeLogs(options, db) {
|
|
307
|
+
if (options.tableName !== void 0 && options.tableName.trim().length === 0) {
|
|
308
|
+
throw new Error(
|
|
309
|
+
"purgeLogs: tableName must be a non-empty string when provided"
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
let sql = "DELETE FROM audit_logs WHERE timestamp < $1::timestamptz";
|
|
313
|
+
const params = [options.before.toISOString()];
|
|
314
|
+
if (options.tableName !== void 0) {
|
|
315
|
+
params.push(options.tableName);
|
|
316
|
+
sql += ` AND table_name = $${params.length}`;
|
|
317
|
+
}
|
|
318
|
+
const deletedCount = await db.$executeRawUnsafe(sql, ...params);
|
|
319
|
+
return { deletedCount };
|
|
320
|
+
}
|
|
321
|
+
function buildSinceFragment(paramCount) {
|
|
322
|
+
const ref = `$${paramCount}::timestamptz`;
|
|
323
|
+
return {
|
|
324
|
+
where: ` WHERE timestamp >= ${ref}`,
|
|
325
|
+
and: ` AND timestamp >= ${ref}`
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
async function getStats(options, db) {
|
|
329
|
+
const params = [];
|
|
330
|
+
if (options.since !== void 0) {
|
|
331
|
+
params.push(options.since.toISOString());
|
|
332
|
+
}
|
|
333
|
+
const since = params.length > 0 ? buildSinceFragment(params.length) : { where: "", and: "" };
|
|
334
|
+
const summaryQuery = db.$queryRawUnsafe(
|
|
335
|
+
`SELECT COUNT(*) AS total_logs, COUNT(DISTINCT table_name) AS tables_audited FROM audit_logs${since.where}`,
|
|
336
|
+
...params
|
|
337
|
+
);
|
|
338
|
+
const eventsPerDayQuery = db.$queryRawUnsafe(
|
|
339
|
+
`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`,
|
|
340
|
+
...params
|
|
341
|
+
);
|
|
342
|
+
const topActorsQuery = db.$queryRawUnsafe(
|
|
343
|
+
`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`,
|
|
344
|
+
...params
|
|
345
|
+
);
|
|
346
|
+
const topTablesQuery = db.$queryRawUnsafe(
|
|
347
|
+
`SELECT table_name, COUNT(*) AS count FROM audit_logs${since.where} GROUP BY table_name ORDER BY COUNT(*) DESC LIMIT 10`,
|
|
348
|
+
...params
|
|
349
|
+
);
|
|
350
|
+
const operationQuery = db.$queryRawUnsafe(
|
|
351
|
+
`SELECT operation, COUNT(*) AS count FROM audit_logs${since.where} GROUP BY operation`,
|
|
352
|
+
...params
|
|
353
|
+
);
|
|
354
|
+
const severityQuery = db.$queryRawUnsafe(
|
|
355
|
+
`SELECT severity, COUNT(*) AS count FROM audit_logs WHERE severity IS NOT NULL${since.and} GROUP BY severity`,
|
|
356
|
+
...params
|
|
357
|
+
);
|
|
358
|
+
const [summaryRows, eventsPerDayRows, topActorsRows, topTablesRows, operationRows, severityRows] = await Promise.all([summaryQuery, eventsPerDayQuery, topActorsQuery, topTablesQuery, operationQuery, severityQuery]);
|
|
359
|
+
const summary = summaryRows[0];
|
|
360
|
+
const totalLogs = summary !== void 0 ? toCount(summary.total_logs) : 0;
|
|
361
|
+
const tablesAudited = summary !== void 0 ? toCount(summary.tables_audited) : 0;
|
|
362
|
+
const eventsPerDay = eventsPerDayRows.map((row) => ({
|
|
363
|
+
date: String(row.date),
|
|
364
|
+
count: toCount(row.count)
|
|
365
|
+
}));
|
|
366
|
+
const topActors = topActorsRows.map((row) => ({
|
|
367
|
+
actorId: String(row.actor_id),
|
|
368
|
+
count: toCount(row.count)
|
|
369
|
+
}));
|
|
370
|
+
const topTables = topTablesRows.map((row) => ({
|
|
371
|
+
tableName: String(row.table_name),
|
|
372
|
+
count: toCount(row.count)
|
|
373
|
+
}));
|
|
374
|
+
const operationBreakdown = {};
|
|
375
|
+
for (const row of operationRows) {
|
|
376
|
+
operationBreakdown[String(row.operation)] = toCount(row.count);
|
|
377
|
+
}
|
|
378
|
+
const severityBreakdown = {};
|
|
379
|
+
for (const row of severityRows) {
|
|
380
|
+
severityBreakdown[String(row.severity)] = toCount(row.count);
|
|
381
|
+
}
|
|
382
|
+
return {
|
|
383
|
+
totalLogs,
|
|
384
|
+
tablesAudited,
|
|
385
|
+
eventsPerDay,
|
|
386
|
+
topActors,
|
|
387
|
+
topTables,
|
|
388
|
+
operationBreakdown,
|
|
389
|
+
severityBreakdown
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
|
|
29
393
|
// src/adapter.ts
|
|
30
394
|
function prismaAuditAdapter(db) {
|
|
31
395
|
return {
|
|
@@ -60,6 +424,18 @@ function prismaAuditAdapter(db) {
|
|
|
60
424
|
jsonOrNull(log.metadata),
|
|
61
425
|
jsonOrNull(log.redactedFields)
|
|
62
426
|
);
|
|
427
|
+
},
|
|
428
|
+
async queryLogs(spec) {
|
|
429
|
+
return queryLogs(spec, db);
|
|
430
|
+
},
|
|
431
|
+
async getLogById(id) {
|
|
432
|
+
return getLogById(id, db);
|
|
433
|
+
},
|
|
434
|
+
async getStats(options) {
|
|
435
|
+
return getStats(options ?? {}, db);
|
|
436
|
+
},
|
|
437
|
+
async purgeLogs(options) {
|
|
438
|
+
return purgeLogs(options, db);
|
|
63
439
|
}
|
|
64
440
|
};
|
|
65
441
|
}
|
|
@@ -71,7 +447,7 @@ function jsonOrNull(value) {
|
|
|
71
447
|
}
|
|
72
448
|
|
|
73
449
|
// src/extension.ts
|
|
74
|
-
var
|
|
450
|
+
var import_audit_core2 = require("@usebetterdev/audit-core");
|
|
75
451
|
|
|
76
452
|
// src/action-map.ts
|
|
77
453
|
var ACTION_MAP = {
|
|
@@ -126,11 +502,23 @@ function buildTableNameResolver(prisma, transform) {
|
|
|
126
502
|
return (modelName) => modelName;
|
|
127
503
|
}
|
|
128
504
|
|
|
505
|
+
// src/tx-store.ts
|
|
506
|
+
var import_node_async_hooks = require("async_hooks");
|
|
507
|
+
var txStorage = new import_node_async_hooks.AsyncLocalStorage();
|
|
508
|
+
function runWithTxClient(tx, fn) {
|
|
509
|
+
return txStorage.run(tx, fn);
|
|
510
|
+
}
|
|
511
|
+
function getTxClient() {
|
|
512
|
+
return txStorage.getStore();
|
|
513
|
+
}
|
|
514
|
+
|
|
129
515
|
// src/extension.ts
|
|
130
516
|
function withAuditExtension(prisma, captureLog, options = {}) {
|
|
131
517
|
const bulkMode = options.bulkMode ?? "per-row";
|
|
132
518
|
const handleError = buildErrorHandler(options.onError);
|
|
133
519
|
const metadata = options.metadata;
|
|
520
|
+
const skipBeforeCapture = options.skipBeforeCapture;
|
|
521
|
+
const maxBeforeStateRows = options.maxBeforeStateRows ?? 100;
|
|
134
522
|
const resolveTableName = buildTableNameResolver(prisma, options.tableNameTransform);
|
|
135
523
|
const extended = prisma.$extends({
|
|
136
524
|
query: {
|
|
@@ -141,12 +529,14 @@ function withAuditExtension(prisma, captureLog, options = {}) {
|
|
|
141
529
|
args,
|
|
142
530
|
query
|
|
143
531
|
}) {
|
|
144
|
-
const result = await query(args);
|
|
145
532
|
const auditOp = getAuditOperation(operation);
|
|
146
533
|
if (auditOp === void 0) {
|
|
147
|
-
return
|
|
534
|
+
return query(args);
|
|
148
535
|
}
|
|
149
536
|
const tableName = resolveTableName(model);
|
|
537
|
+
const shouldSkipBefore = skipBeforeCapture !== void 0 && skipBeforeCapture.includes(tableName);
|
|
538
|
+
const beforeState = shouldSkipBefore ? { type: "none" } : await fetchBeforeState({ prisma, model, operation, args, handleError, maxBeforeStateRows });
|
|
539
|
+
const result = await query(args);
|
|
150
540
|
try {
|
|
151
541
|
await fireCaptureLog({
|
|
152
542
|
tableName,
|
|
@@ -154,9 +544,11 @@ function withAuditExtension(prisma, captureLog, options = {}) {
|
|
|
154
544
|
auditOp,
|
|
155
545
|
args,
|
|
156
546
|
result,
|
|
547
|
+
beforeState,
|
|
157
548
|
captureLog,
|
|
158
549
|
bulkMode,
|
|
159
|
-
metadata
|
|
550
|
+
metadata,
|
|
551
|
+
actorId: (0, import_audit_core2.getAuditContext)()?.actorId
|
|
160
552
|
});
|
|
161
553
|
} catch (err) {
|
|
162
554
|
handleError(err);
|
|
@@ -168,17 +560,62 @@ function withAuditExtension(prisma, captureLog, options = {}) {
|
|
|
168
560
|
});
|
|
169
561
|
return extended;
|
|
170
562
|
}
|
|
563
|
+
async function fetchBeforeState({
|
|
564
|
+
prisma,
|
|
565
|
+
model,
|
|
566
|
+
operation,
|
|
567
|
+
args,
|
|
568
|
+
handleError,
|
|
569
|
+
maxBeforeStateRows
|
|
570
|
+
}) {
|
|
571
|
+
if (operation === "create" || operation === "createMany" || operation === "createManyAndReturn" || operation === "delete") {
|
|
572
|
+
return { type: "none" };
|
|
573
|
+
}
|
|
574
|
+
const delegate = getModelDelegate(getTxClient() ?? prisma, model);
|
|
575
|
+
if (delegate === void 0) {
|
|
576
|
+
return { type: "none" };
|
|
577
|
+
}
|
|
578
|
+
if (operation === "update" || operation === "upsert") {
|
|
579
|
+
const where = getArgsWhere(args);
|
|
580
|
+
if (where === void 0) {
|
|
581
|
+
return { type: "single", row: void 0 };
|
|
582
|
+
}
|
|
583
|
+
try {
|
|
584
|
+
const found = await delegate.findUnique({ where });
|
|
585
|
+
return { type: "single", row: toRecord(found) };
|
|
586
|
+
} catch (err) {
|
|
587
|
+
handleError(err);
|
|
588
|
+
return { type: "single", row: void 0 };
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
if (operation === "updateMany" || operation === "deleteMany") {
|
|
592
|
+
const where = getArgsWhere(args);
|
|
593
|
+
try {
|
|
594
|
+
const rows = await delegate.findMany({ where: where ?? {}, take: maxBeforeStateRows + 1 });
|
|
595
|
+
if (rows.length > maxBeforeStateRows) {
|
|
596
|
+
return { type: "none" };
|
|
597
|
+
}
|
|
598
|
+
const records = rows.map((r) => toRecord(r)).filter((r) => r !== void 0);
|
|
599
|
+
return { type: "many", rows: records };
|
|
600
|
+
} catch (err) {
|
|
601
|
+
handleError(err);
|
|
602
|
+
return { type: "many", rows: [] };
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
return { type: "none" };
|
|
606
|
+
}
|
|
171
607
|
async function fireCaptureLog({
|
|
172
608
|
tableName,
|
|
173
609
|
operation,
|
|
174
610
|
auditOp,
|
|
175
611
|
args,
|
|
176
612
|
result,
|
|
613
|
+
beforeState,
|
|
177
614
|
captureLog,
|
|
178
615
|
bulkMode,
|
|
179
|
-
metadata
|
|
616
|
+
metadata,
|
|
617
|
+
actorId
|
|
180
618
|
}) {
|
|
181
|
-
const actorId = (0, import_audit_core.getAuditContext)()?.actorId;
|
|
182
619
|
if (operation === "createMany") {
|
|
183
620
|
await handleCreateMany({ tableName, auditOp, args, captureLog, bulkMode, metadata, actorId });
|
|
184
621
|
return;
|
|
@@ -187,18 +624,30 @@ async function fireCaptureLog({
|
|
|
187
624
|
await handleCreateManyAndReturn({ tableName, auditOp, result, captureLog, bulkMode, metadata, actorId });
|
|
188
625
|
return;
|
|
189
626
|
}
|
|
190
|
-
if (operation === "updateMany"
|
|
627
|
+
if (operation === "updateMany") {
|
|
628
|
+
await handleUpdateMany({ tableName, auditOp, beforeState, captureLog, bulkMode, metadata, actorId });
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
if (operation === "deleteMany") {
|
|
632
|
+
await handleDeleteMany({ tableName, auditOp, beforeState, captureLog, bulkMode, metadata, actorId });
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
const row = toRecord(result);
|
|
636
|
+
const recordId = (row !== void 0 ? extractId(row) : void 0) ?? "unknown";
|
|
637
|
+
if (operation === "upsert") {
|
|
638
|
+
const before = beforeState.type === "single" ? beforeState.row : void 0;
|
|
639
|
+
const effectiveOp = before !== void 0 ? "UPDATE" : "INSERT";
|
|
191
640
|
await captureLog({
|
|
192
641
|
tableName,
|
|
193
|
-
operation:
|
|
194
|
-
recordId
|
|
642
|
+
operation: effectiveOp,
|
|
643
|
+
recordId,
|
|
644
|
+
...before !== void 0 && { before },
|
|
645
|
+
...row !== void 0 && { after: row },
|
|
195
646
|
...metadata !== void 0 && { metadata },
|
|
196
647
|
...actorId !== void 0 && { actorId }
|
|
197
648
|
});
|
|
198
649
|
return;
|
|
199
650
|
}
|
|
200
|
-
const row = toRecord(result);
|
|
201
|
-
const recordId = (row !== void 0 ? extractId(row) : void 0) ?? "unknown";
|
|
202
651
|
if (auditOp === "INSERT") {
|
|
203
652
|
await captureLog({
|
|
204
653
|
tableName,
|
|
@@ -211,10 +660,12 @@ async function fireCaptureLog({
|
|
|
211
660
|
return;
|
|
212
661
|
}
|
|
213
662
|
if (auditOp === "UPDATE") {
|
|
663
|
+
const before = beforeState.type === "single" ? beforeState.row : void 0;
|
|
214
664
|
await captureLog({
|
|
215
665
|
tableName,
|
|
216
666
|
operation: auditOp,
|
|
217
667
|
recordId,
|
|
668
|
+
...before !== void 0 && { before },
|
|
218
669
|
...row !== void 0 && { after: row },
|
|
219
670
|
...metadata !== void 0 && { metadata },
|
|
220
671
|
...actorId !== void 0 && { actorId }
|
|
@@ -320,6 +771,94 @@ async function handleCreateManyAndReturn({
|
|
|
320
771
|
})
|
|
321
772
|
);
|
|
322
773
|
}
|
|
774
|
+
async function handleUpdateMany({
|
|
775
|
+
tableName,
|
|
776
|
+
auditOp,
|
|
777
|
+
beforeState,
|
|
778
|
+
captureLog,
|
|
779
|
+
bulkMode,
|
|
780
|
+
metadata,
|
|
781
|
+
actorId
|
|
782
|
+
}) {
|
|
783
|
+
if (bulkMode === "bulk") {
|
|
784
|
+
await captureLog({
|
|
785
|
+
tableName,
|
|
786
|
+
operation: auditOp,
|
|
787
|
+
recordId: "unknown",
|
|
788
|
+
...metadata !== void 0 && { metadata },
|
|
789
|
+
...actorId !== void 0 && { actorId }
|
|
790
|
+
});
|
|
791
|
+
return;
|
|
792
|
+
}
|
|
793
|
+
const rows = beforeState.type === "many" ? beforeState.rows : [];
|
|
794
|
+
if (rows.length === 0) {
|
|
795
|
+
await captureLog({
|
|
796
|
+
tableName,
|
|
797
|
+
operation: auditOp,
|
|
798
|
+
recordId: "unknown",
|
|
799
|
+
...metadata !== void 0 && { metadata },
|
|
800
|
+
...actorId !== void 0 && { actorId }
|
|
801
|
+
});
|
|
802
|
+
return;
|
|
803
|
+
}
|
|
804
|
+
await Promise.all(
|
|
805
|
+
rows.map((row) => {
|
|
806
|
+
const recordId = extractId(row) ?? "unknown";
|
|
807
|
+
return captureLog({
|
|
808
|
+
tableName,
|
|
809
|
+
operation: auditOp,
|
|
810
|
+
recordId,
|
|
811
|
+
before: row,
|
|
812
|
+
...metadata !== void 0 && { metadata },
|
|
813
|
+
...actorId !== void 0 && { actorId }
|
|
814
|
+
});
|
|
815
|
+
})
|
|
816
|
+
);
|
|
817
|
+
}
|
|
818
|
+
async function handleDeleteMany({
|
|
819
|
+
tableName,
|
|
820
|
+
auditOp,
|
|
821
|
+
beforeState,
|
|
822
|
+
captureLog,
|
|
823
|
+
bulkMode,
|
|
824
|
+
metadata,
|
|
825
|
+
actorId
|
|
826
|
+
}) {
|
|
827
|
+
if (bulkMode === "bulk") {
|
|
828
|
+
await captureLog({
|
|
829
|
+
tableName,
|
|
830
|
+
operation: auditOp,
|
|
831
|
+
recordId: "unknown",
|
|
832
|
+
...metadata !== void 0 && { metadata },
|
|
833
|
+
...actorId !== void 0 && { actorId }
|
|
834
|
+
});
|
|
835
|
+
return;
|
|
836
|
+
}
|
|
837
|
+
const rows = beforeState.type === "many" ? beforeState.rows : [];
|
|
838
|
+
if (rows.length === 0) {
|
|
839
|
+
await captureLog({
|
|
840
|
+
tableName,
|
|
841
|
+
operation: auditOp,
|
|
842
|
+
recordId: "unknown",
|
|
843
|
+
...metadata !== void 0 && { metadata },
|
|
844
|
+
...actorId !== void 0 && { actorId }
|
|
845
|
+
});
|
|
846
|
+
return;
|
|
847
|
+
}
|
|
848
|
+
await Promise.all(
|
|
849
|
+
rows.map((row) => {
|
|
850
|
+
const recordId = extractId(row) ?? "unknown";
|
|
851
|
+
return captureLog({
|
|
852
|
+
tableName,
|
|
853
|
+
operation: auditOp,
|
|
854
|
+
recordId,
|
|
855
|
+
before: row,
|
|
856
|
+
...metadata !== void 0 && { metadata },
|
|
857
|
+
...actorId !== void 0 && { actorId }
|
|
858
|
+
});
|
|
859
|
+
})
|
|
860
|
+
);
|
|
861
|
+
}
|
|
323
862
|
function buildErrorHandler(onError) {
|
|
324
863
|
return (error) => {
|
|
325
864
|
try {
|
|
@@ -333,6 +872,20 @@ function buildErrorHandler(onError) {
|
|
|
333
872
|
}
|
|
334
873
|
};
|
|
335
874
|
}
|
|
875
|
+
function isModelDelegate(value) {
|
|
876
|
+
if (value === null || value === void 0 || typeof value !== "object") {
|
|
877
|
+
return false;
|
|
878
|
+
}
|
|
879
|
+
return "findUnique" in value && "findMany" in value && typeof Reflect.get(value, "findUnique") === "function" && typeof Reflect.get(value, "findMany") === "function";
|
|
880
|
+
}
|
|
881
|
+
function getModelDelegate(client, model) {
|
|
882
|
+
const lowerModel = model.charAt(0).toLowerCase() + model.slice(1);
|
|
883
|
+
const value = Reflect.get(Object(client), lowerModel);
|
|
884
|
+
if (isModelDelegate(value)) {
|
|
885
|
+
return value;
|
|
886
|
+
}
|
|
887
|
+
return void 0;
|
|
888
|
+
}
|
|
336
889
|
function extractId(record) {
|
|
337
890
|
const id = record["id"];
|
|
338
891
|
if (id !== void 0 && id !== null) {
|
|
@@ -348,17 +901,24 @@ function toRecord(value) {
|
|
|
348
901
|
}
|
|
349
902
|
function getArgsData(args) {
|
|
350
903
|
if (args !== null && typeof args === "object" && "data" in args) {
|
|
351
|
-
const data = args
|
|
904
|
+
const data = Reflect.get(args, "data");
|
|
352
905
|
if (Array.isArray(data)) {
|
|
353
906
|
return data;
|
|
354
907
|
}
|
|
355
908
|
}
|
|
356
909
|
return [];
|
|
357
910
|
}
|
|
911
|
+
function getArgsWhere(args) {
|
|
912
|
+
if (args !== null && typeof args === "object" && "where" in args) {
|
|
913
|
+
return Reflect.get(args, "where");
|
|
914
|
+
}
|
|
915
|
+
return void 0;
|
|
916
|
+
}
|
|
358
917
|
// Annotate the CommonJS export names for ESM import in node:
|
|
359
918
|
0 && (module.exports = {
|
|
360
919
|
prismaAuditAdapter,
|
|
361
920
|
prismaModelMap,
|
|
921
|
+
runWithTxClient,
|
|
362
922
|
withAuditExtension
|
|
363
923
|
});
|
|
364
924
|
//# sourceMappingURL=index.cjs.map
|