@explita/prisma-audit-log 0.1.0 → 0.2.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/README.md +51 -12
- package/dist/core/create.js +1 -1
- package/dist/core/delete.js +1 -1
- package/dist/core/process.d.ts +0 -1
- package/dist/core/process.js +17 -32
- package/dist/core/update-many.js +4 -0
- package/dist/core/update.js +5 -1
- package/dist/index.d.ts +1 -1
- package/dist/lib/prisma-extension.js +15 -3
- package/dist/lib/process-log-helpers.d.ts +6 -1
- package/dist/lib/process-log-helpers.js +69 -0
- package/dist/types.d.ts +30 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -5,10 +5,16 @@ A Prisma extension for automatic audit logging of database operations.
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
7
|
- ✅ Automatic logging of create, update, and delete operations
|
|
8
|
-
- ✅
|
|
8
|
+
- ✅ Efficient batch processing of logs with automatic fallback to single inserts
|
|
9
|
+
- ✅ Support for both single and batch operations
|
|
9
10
|
- ✅ Tracks changed fields for updates
|
|
10
11
|
- ✅ Context-aware logging with user and request information
|
|
11
12
|
- ✅ Customizable field masking and data sanitization
|
|
13
|
+
- ✅ Field-level filtering to include/exclude specific fields from logs
|
|
14
|
+
- ✅ Ignores updates where only `updatedAt`/`updated_at` changed (noise-free logs)
|
|
15
|
+
- ✅ Skip logging for specific operations using a callback function
|
|
16
|
+
- ✅ Proper handling of Decimal and other special types
|
|
17
|
+
- ✅ Ignores logging of AuditLog model
|
|
12
18
|
- ✅ TypeScript support
|
|
13
19
|
|
|
14
20
|
## Installation
|
|
@@ -17,6 +23,8 @@ A Prisma extension for automatic audit logging of database operations.
|
|
|
17
23
|
npm install @explita/prisma-audit-log
|
|
18
24
|
# or
|
|
19
25
|
yarn add @explita/prisma-audit-log
|
|
26
|
+
# or
|
|
27
|
+
pnpm add @explita/prisma-audit-log
|
|
20
28
|
```
|
|
21
29
|
|
|
22
30
|
#
|
|
@@ -56,6 +64,18 @@ const prisma = new PrismaClient().$extends(
|
|
|
56
64
|
maskFields: ["password", "token"],
|
|
57
65
|
maskValue: "[REDACTED]", // default value
|
|
58
66
|
|
|
67
|
+
// Optional: Configure field inclusion/exclusion per model
|
|
68
|
+
fieldFilters: {
|
|
69
|
+
// Exclude sensitive fields from User model
|
|
70
|
+
User: {
|
|
71
|
+
exclude: ["password", "tokens", "refreshToken"],
|
|
72
|
+
},
|
|
73
|
+
// Only include specific fields for Payment model
|
|
74
|
+
Payment: {
|
|
75
|
+
include: ["id", "amount", "status", "createdAt"],
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
|
|
59
79
|
// Optional: Truncate long values
|
|
60
80
|
maxStringLength: 1000,
|
|
61
81
|
maxArrayLength: 50,
|
|
@@ -64,7 +84,24 @@ const prisma = new PrismaClient().$extends(
|
|
|
64
84
|
logger: (log) => {
|
|
65
85
|
// Send logs to your logging service
|
|
66
86
|
console.log("AUDIT:", log);
|
|
87
|
+
// log can be an array of AuditLog objects
|
|
67
88
|
},
|
|
89
|
+
|
|
90
|
+
// Skip logging for specific operations
|
|
91
|
+
skip: ({ model, operation, args }) => {
|
|
92
|
+
// Skip logging for specific models or operations
|
|
93
|
+
if (model === "Session" && operation === "create") return true;
|
|
94
|
+
|
|
95
|
+
// Skip logging for specific conditions
|
|
96
|
+
if (model === "User" && args?.data?.isSystemUpdate) return true;
|
|
97
|
+
|
|
98
|
+
// Skip logging for specific operations on specific models
|
|
99
|
+
if (model === "AuditLog" || model === "audit_logs") return true;
|
|
100
|
+
|
|
101
|
+
return false;
|
|
102
|
+
},
|
|
103
|
+
|
|
104
|
+
// Timestamp-only updates are skipped automatically, so no extra config needed
|
|
68
105
|
})
|
|
69
106
|
);
|
|
70
107
|
```
|
|
@@ -118,17 +155,19 @@ route.put("/test/:id", async (request, reply) => {
|
|
|
118
155
|
|
|
119
156
|
## Configuration Options
|
|
120
157
|
|
|
121
|
-
| Option | Type
|
|
122
|
-
| ----------------- |
|
|
123
|
-
| `includeModels` | `string[]`
|
|
124
|
-
| `excludeModels` | `string[]`
|
|
125
|
-
| `getContext` | `() => AuditContext`
|
|
126
|
-
| `maskFields` | `string[]`
|
|
127
|
-
| `maskValue` | `any`
|
|
128
|
-
| `maxStringLength` | `number`
|
|
129
|
-
| `maxArrayLength` | `number`
|
|
130
|
-
| `maxPayloadBytes` | `number`
|
|
131
|
-
| `
|
|
158
|
+
| Option | Type | Description |
|
|
159
|
+
| ----------------- | ------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------- |
|
|
160
|
+
| `includeModels` | `string[]` | Only log operations on these models |
|
|
161
|
+
| `excludeModels` | `string[]` | Exclude these models from logging |
|
|
162
|
+
| `getContext` | `() => AuditContext` | Function to get current context (user, IP, etc.) |
|
|
163
|
+
| `maskFields` | `string[]` | Fields to mask in logs |
|
|
164
|
+
| `maskValue` | `any` | Value to use for masked fields (default: `[REDACTED]`) |
|
|
165
|
+
| `maxStringLength` | `number` | Truncate long strings |
|
|
166
|
+
| `maxArrayLength` | `number` | Truncate large arrays |
|
|
167
|
+
| `maxPayloadBytes` | `number` | Maximum JSON payload size (before truncation) |
|
|
168
|
+
| `fieldFilters` | `{ [model: string]: { include?: string[]; exclude?: string[] } }` | Configure field inclusion/exclusion per model. Use `include` to whitelist fields or `exclude` to blacklist them. |
|
|
169
|
+
| `skip` | `(params: { model: string; operation: string; args: any }) => boolean \| Promise<boolean>` | Skip logging for specific operations |
|
|
170
|
+
| `logger` | `(log: AuditLog \| AuditLog[]) => void` | Custom logger function for audit logs |
|
|
132
171
|
|
|
133
172
|
## License
|
|
134
173
|
|
package/dist/core/create.js
CHANGED
package/dist/core/delete.js
CHANGED
package/dist/core/process.d.ts
CHANGED
|
@@ -1,3 +1,2 @@
|
|
|
1
1
|
import type { AuditLog, AuditLogOptions } from "../types.js";
|
|
2
|
-
export declare function processAuditLog(prisma: any, auditLog: Omit<AuditLog, "id">, options: AuditLogOptions): Promise<void>;
|
|
3
2
|
export declare function saveAuditLogs(prisma: any, logs: Omit<AuditLog, "id">[], options: AuditLogOptions): Promise<void>;
|
package/dist/core/process.js
CHANGED
|
@@ -1,46 +1,31 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.processAuditLog = processAuditLog;
|
|
4
3
|
exports.saveAuditLogs = saveAuditLogs;
|
|
5
4
|
const process_log_helpers_js_1 = require("../lib/process-log-helpers.js");
|
|
6
|
-
async function processAuditLog(prisma, auditLog, options) {
|
|
7
|
-
const finalLog = await (0, process_log_helpers_js_1.buildFinalLog)(auditLog, options);
|
|
8
|
-
// Use custom logger if provided, otherwise log to console
|
|
9
|
-
if (options.logger) {
|
|
10
|
-
await options.logger(finalLog);
|
|
11
|
-
}
|
|
12
|
-
// else {
|
|
13
|
-
// console.log("[AUDIT LOG]", finalLog);
|
|
14
|
-
// }
|
|
15
|
-
// Try to save to database if AuditLog model exists
|
|
16
|
-
try {
|
|
17
|
-
if (prisma.auditLog) {
|
|
18
|
-
await prisma.auditLog.create({
|
|
19
|
-
data: finalLog,
|
|
20
|
-
});
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
catch (error) {
|
|
24
|
-
console.error("Failed to save audit log to database:", error);
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
5
|
async function saveAuditLogs(prisma, logs, options) {
|
|
28
|
-
var _a;
|
|
6
|
+
var _a, _b;
|
|
29
7
|
if (!logs.length)
|
|
30
8
|
return;
|
|
31
|
-
|
|
9
|
+
try {
|
|
32
10
|
const finals = await Promise.all(logs.map((log) => (0, process_log_helpers_js_1.buildFinalLog)(log, options)));
|
|
33
|
-
if (finals.length)
|
|
11
|
+
if (!finals.length)
|
|
12
|
+
return;
|
|
13
|
+
if ((_a = prisma.auditLog) === null || _a === void 0 ? void 0 : _a.createMany) {
|
|
34
14
|
await prisma.auditLog.createMany({ data: finals });
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
15
|
+
}
|
|
16
|
+
else if ((_b = prisma.auditLog) === null || _b === void 0 ? void 0 : _b.create) {
|
|
17
|
+
// Fall back to individual inserts if createMany is not available
|
|
18
|
+
for (const finalLog of finals) {
|
|
19
|
+
await prisma.auditLog.create({
|
|
20
|
+
data: finalLog,
|
|
21
|
+
});
|
|
39
22
|
}
|
|
40
23
|
}
|
|
41
|
-
|
|
24
|
+
if (options.logger && finals.length) {
|
|
25
|
+
await options.logger(finals);
|
|
26
|
+
}
|
|
42
27
|
}
|
|
43
|
-
|
|
44
|
-
|
|
28
|
+
catch (error) {
|
|
29
|
+
console.error("Failed to save audit log to database:", error);
|
|
45
30
|
}
|
|
46
31
|
}
|
package/dist/core/update-many.js
CHANGED
|
@@ -28,6 +28,10 @@ async function handleUpdateMany(opts) {
|
|
|
28
28
|
if (!updated)
|
|
29
29
|
continue;
|
|
30
30
|
const changedFields = (0, utils_js_1.getChangedFields)(current, updated);
|
|
31
|
+
if (changedFields.length === 1 &&
|
|
32
|
+
(changedFields[0] === "updatedAt" || changedFields[0] === "updated_at")) {
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
31
35
|
const { oldData, newData } = (0, utils_js_1.buildSnapshot)(changedFields, current, updated);
|
|
32
36
|
logs.push({
|
|
33
37
|
action: "UPDATE",
|
package/dist/core/update.js
CHANGED
|
@@ -12,6 +12,10 @@ async function handleUpdate(opts) {
|
|
|
12
12
|
const result = await query(args);
|
|
13
13
|
if (current) {
|
|
14
14
|
const changedFields = (0, utils_js_1.getChangedFields)(current, result);
|
|
15
|
+
if (changedFields.length === 1 &&
|
|
16
|
+
(changedFields[0] === "updatedAt" || changedFields[0] === "updated_at")) {
|
|
17
|
+
return result;
|
|
18
|
+
}
|
|
15
19
|
const { oldData, newData } = (0, utils_js_1.buildSnapshot)(changedFields, current, result);
|
|
16
20
|
const auditLog = {
|
|
17
21
|
action: "UPDATE",
|
|
@@ -21,7 +25,7 @@ async function handleUpdate(opts) {
|
|
|
21
25
|
newData,
|
|
22
26
|
changedFields,
|
|
23
27
|
};
|
|
24
|
-
await (0, process_js_1.
|
|
28
|
+
await (0, process_js_1.saveAuditLogs)(prisma, [auditLog], options);
|
|
25
29
|
}
|
|
26
30
|
return result;
|
|
27
31
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export type { AuditLog, AuditLogOptions } from "./types.js";
|
|
1
|
+
export type { AuditContext, AuditLog, AuditLogOptions, ModelFieldFilters, } from "./types.js";
|
|
2
2
|
export { auditLogExtension } from "./lib/prisma-extension.js";
|
|
@@ -15,17 +15,29 @@ const update_js_1 = require("../core/update.js");
|
|
|
15
15
|
function auditLogExtension(options = {}) {
|
|
16
16
|
return extension_1.Prisma.defineExtension((prisma) => {
|
|
17
17
|
return prisma.$extends({
|
|
18
|
+
name: "auditLogExtension",
|
|
18
19
|
query: {
|
|
19
20
|
$allModels: {
|
|
20
21
|
async $allOperations({ model, operation, args, query }) {
|
|
21
22
|
const modelName = model === null || model === void 0 ? void 0 : model.toString();
|
|
23
|
+
// Never audit the audit log model itself
|
|
24
|
+
if (["auditLog", "AuditLog"].includes(modelName !== null && modelName !== void 0 ? modelName : "")) {
|
|
25
|
+
return query(args);
|
|
26
|
+
}
|
|
22
27
|
// Skip if model is excluded or not included
|
|
23
28
|
if ((0, utils_js_1.shouldSkipModel)(modelName, options)) {
|
|
24
29
|
return query(args);
|
|
25
30
|
}
|
|
26
|
-
//
|
|
27
|
-
if (
|
|
28
|
-
|
|
31
|
+
// Check if the operation should be skipped via the skip callback
|
|
32
|
+
if (options.skip) {
|
|
33
|
+
const shouldSkip = await options.skip({
|
|
34
|
+
model: modelName,
|
|
35
|
+
operation,
|
|
36
|
+
args,
|
|
37
|
+
});
|
|
38
|
+
if (shouldSkip) {
|
|
39
|
+
return query(args);
|
|
40
|
+
}
|
|
29
41
|
}
|
|
30
42
|
const queryArgs = {
|
|
31
43
|
args,
|
|
@@ -1,2 +1,7 @@
|
|
|
1
|
-
import type { AuditLog, AuditLogOptions } from "../types.js";
|
|
1
|
+
import type { AuditLog, AuditLogOptions, FilterFieldsResult, ModelFieldFilters } from "../types.js";
|
|
2
2
|
export declare function buildFinalLog(auditLog: Omit<AuditLog, "id">, options: AuditLogOptions): Promise<Omit<AuditLog, "id">>;
|
|
3
|
+
/**
|
|
4
|
+
* Filter object fields based on include/exclude rules
|
|
5
|
+
* @returns An object containing the filtered data and changedFields
|
|
6
|
+
*/
|
|
7
|
+
export declare function filterFields<T extends Record<string, any>>(data: T | undefined, model: string, fieldFilters?: ModelFieldFilters, changedFields?: string[]): FilterFieldsResult<T>;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.buildFinalLog = buildFinalLog;
|
|
4
|
+
exports.filterFields = filterFields;
|
|
4
5
|
function matchesMaskPath(maskPaths, keyPath) {
|
|
5
6
|
if (!maskPaths || keyPath.length === 0)
|
|
6
7
|
return false;
|
|
@@ -38,6 +39,13 @@ function maskAndTruncate(value, opts, keyPath = []) {
|
|
|
38
39
|
const limited = maxArrayLength ? value.slice(0, maxArrayLength) : value;
|
|
39
40
|
return limited.map((v, i) => maskAndTruncate(v, opts, [...keyPath, String(i)]));
|
|
40
41
|
}
|
|
42
|
+
// Handle Decimal type from Prisma
|
|
43
|
+
if (value &&
|
|
44
|
+
typeof value === "object" &&
|
|
45
|
+
"constructor" in value &&
|
|
46
|
+
["Decimal", "Decimal2"].includes(value.constructor.name)) {
|
|
47
|
+
return value.toString();
|
|
48
|
+
}
|
|
41
49
|
if (typeof value === "object") {
|
|
42
50
|
const out = {};
|
|
43
51
|
for (const [k, v] of Object.entries(value)) {
|
|
@@ -72,6 +80,20 @@ async function buildFinalLog(auditLog, options) {
|
|
|
72
80
|
const finalLog = {
|
|
73
81
|
...auditLog,
|
|
74
82
|
};
|
|
83
|
+
// Apply field filtering if configured
|
|
84
|
+
if (options.fieldFilters) {
|
|
85
|
+
if (finalLog.oldData) {
|
|
86
|
+
const { filteredData } = filterFields(finalLog.oldData, auditLog.model, options.fieldFilters);
|
|
87
|
+
finalLog.oldData = filteredData;
|
|
88
|
+
}
|
|
89
|
+
if (finalLog.newData) {
|
|
90
|
+
const { filteredData, filteredChangedFields } = filterFields(finalLog.newData, auditLog.model, options.fieldFilters, finalLog.changedFields);
|
|
91
|
+
finalLog.newData = filteredData;
|
|
92
|
+
if (filteredChangedFields) {
|
|
93
|
+
finalLog.changedFields = filteredChangedFields;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
75
97
|
// Merge extra context
|
|
76
98
|
if (options.getContext) {
|
|
77
99
|
const context = await options.getContext();
|
|
@@ -91,3 +113,50 @@ async function buildFinalLog(auditLog, options) {
|
|
|
91
113
|
}
|
|
92
114
|
return finalLog;
|
|
93
115
|
}
|
|
116
|
+
/**
|
|
117
|
+
* Filter object fields based on include/exclude rules
|
|
118
|
+
* @returns An object containing the filtered data and changedFields
|
|
119
|
+
*/
|
|
120
|
+
function filterFields(data, model, fieldFilters, changedFields) {
|
|
121
|
+
if (!data)
|
|
122
|
+
return { filteredData: data, filteredChangedFields: changedFields };
|
|
123
|
+
const modelFilters = fieldFilters === null || fieldFilters === void 0 ? void 0 : fieldFilters[model];
|
|
124
|
+
if (!modelFilters)
|
|
125
|
+
return { filteredData: data, filteredChangedFields: changedFields };
|
|
126
|
+
// Handle both include and exclude being optional
|
|
127
|
+
const include = "include" in modelFilters ? modelFilters.include : undefined;
|
|
128
|
+
const exclude = "exclude" in modelFilters ? modelFilters.exclude : undefined;
|
|
129
|
+
let filteredChangedFields = changedFields;
|
|
130
|
+
// If include is specified, only include those fields
|
|
131
|
+
if (include === null || include === void 0 ? void 0 : include.length) {
|
|
132
|
+
const includeSet = new Set(include);
|
|
133
|
+
const result = include.reduce((result, field) => {
|
|
134
|
+
if (field in data) {
|
|
135
|
+
result[field] = data[field];
|
|
136
|
+
}
|
|
137
|
+
return result;
|
|
138
|
+
}, {});
|
|
139
|
+
if (changedFields) {
|
|
140
|
+
filteredChangedFields = changedFields.filter((field) => includeSet.has(field));
|
|
141
|
+
}
|
|
142
|
+
return { filteredData: result, filteredChangedFields };
|
|
143
|
+
}
|
|
144
|
+
// If exclude is specified, exclude those fields
|
|
145
|
+
if (exclude === null || exclude === void 0 ? void 0 : exclude.length) {
|
|
146
|
+
const excludeSet = new Set(exclude);
|
|
147
|
+
const result = Object.entries(data).reduce((result, [key, value]) => {
|
|
148
|
+
if (!excludeSet.has(key)) {
|
|
149
|
+
result[key] = value;
|
|
150
|
+
}
|
|
151
|
+
return result;
|
|
152
|
+
}, {});
|
|
153
|
+
if (changedFields) {
|
|
154
|
+
filteredChangedFields = changedFields.filter((field) => !excludeSet.has(field));
|
|
155
|
+
}
|
|
156
|
+
return { filteredData: result, filteredChangedFields };
|
|
157
|
+
}
|
|
158
|
+
return {
|
|
159
|
+
filteredData: data,
|
|
160
|
+
filteredChangedFields: changedFields,
|
|
161
|
+
};
|
|
162
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
import type { JsArgs } from "@prisma/client/runtime/client";
|
|
2
2
|
export interface AuditLogOptions {
|
|
3
|
+
/**
|
|
4
|
+
* Function to determine if audit logging should be skipped for the current operation
|
|
5
|
+
* @returns true to skip logging, false or undefined to continue with logging
|
|
6
|
+
*/
|
|
7
|
+
skip?: (params: {
|
|
8
|
+
model: string;
|
|
9
|
+
operation: string;
|
|
10
|
+
args: any;
|
|
11
|
+
}) => boolean | Promise<boolean>;
|
|
3
12
|
/**
|
|
4
13
|
* Tables to include in audit logging
|
|
5
14
|
* If not provided, all tables will be included
|
|
@@ -10,6 +19,15 @@ export interface AuditLogOptions {
|
|
|
10
19
|
* Takes precedence over includeModels
|
|
11
20
|
*/
|
|
12
21
|
excludeModels?: string[];
|
|
22
|
+
/**
|
|
23
|
+
* Configure field inclusion/exclusion per model
|
|
24
|
+
* @example
|
|
25
|
+
* {
|
|
26
|
+
* User: { exclude: ['password', 'tokens'] },
|
|
27
|
+
* Payment: { include: ['id', 'amount', 'status'] }
|
|
28
|
+
* }
|
|
29
|
+
*/
|
|
30
|
+
fieldFilters?: ModelFieldFilters;
|
|
13
31
|
/**
|
|
14
32
|
* Function to get extra context for the audit trail (e.g., userId, companyId, branchId, metadata, ...)
|
|
15
33
|
* Return any additional fields your AuditLog model supports. They will be merged into the final log.
|
|
@@ -19,7 +37,7 @@ export interface AuditLogOptions {
|
|
|
19
37
|
* Custom logger function
|
|
20
38
|
* If not provided, logs will be written to console
|
|
21
39
|
*/
|
|
22
|
-
logger?: (log: AuditLog) => void | Promise<void>;
|
|
40
|
+
logger?: (log: AuditLog | AuditLog[]) => void | Promise<void>;
|
|
23
41
|
/**
|
|
24
42
|
* Keys to mask in oldData/newData/metadata. Exact match on property name.
|
|
25
43
|
*/
|
|
@@ -36,8 +54,6 @@ export interface AuditLogOptions {
|
|
|
36
54
|
maxArrayLength?: number;
|
|
37
55
|
/** Maximum JSON payload size per field (old/new/metadata). Entries exceeding this will be replaced with a truncated preview */
|
|
38
56
|
maxPayloadBytes?: number;
|
|
39
|
-
/** Whether to use createMany for bulk inserts when available (default: true) */
|
|
40
|
-
batchInsert?: boolean;
|
|
41
57
|
}
|
|
42
58
|
export interface AuditLog {
|
|
43
59
|
id: string;
|
|
@@ -69,3 +85,14 @@ export type HandlerArgs = {
|
|
|
69
85
|
options: AuditLogOptions;
|
|
70
86
|
operation?: string;
|
|
71
87
|
};
|
|
88
|
+
export type FieldFilterConfig = {
|
|
89
|
+
include?: string[];
|
|
90
|
+
exclude?: string[];
|
|
91
|
+
};
|
|
92
|
+
export type ModelFieldFilters = {
|
|
93
|
+
[model: string]: FieldFilterConfig;
|
|
94
|
+
};
|
|
95
|
+
export interface FilterFieldsResult<T> {
|
|
96
|
+
filteredData: T | undefined;
|
|
97
|
+
filteredChangedFields?: string[];
|
|
98
|
+
}
|