@explita/prisma-audit-log 0.1.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 +139 -0
- package/dist/core/create-many.d.ts +7 -0
- package/dist/core/create-many.js +51 -0
- package/dist/core/create.d.ts +2 -0
- package/dist/core/create.js +16 -0
- package/dist/core/delete-many.d.ts +2 -0
- package/dist/core/delete-many.js +25 -0
- package/dist/core/delete.d.ts +2 -0
- package/dist/core/delete.js +22 -0
- package/dist/core/process.d.ts +3 -0
- package/dist/core/process.js +46 -0
- package/dist/core/update-many.d.ts +2 -0
- package/dist/core/update-many.js +43 -0
- package/dist/core/update.d.ts +2 -0
- package/dist/core/update.js +27 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +5 -0
- package/dist/lib/prisma-extension.d.ts +5 -0
- package/dist/lib/prisma-extension.js +62 -0
- package/dist/lib/process-log-helpers.d.ts +2 -0
- package/dist/lib/process-log-helpers.js +93 -0
- package/dist/lib/utils.d.ts +7 -0
- package/dist/lib/utils.js +40 -0
- package/dist/types.d.ts +71 -0
- package/dist/types.js +2 -0
- package/package.json +32 -0
package/README.md
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# Prisma Audit Log
|
|
2
|
+
|
|
3
|
+
A Prisma extension for automatic audit logging of database operations.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- ✅ Automatic logging of create, update, and delete operations
|
|
8
|
+
- ✅ Support for single and batch operations
|
|
9
|
+
- ✅ Tracks changed fields for updates
|
|
10
|
+
- ✅ Context-aware logging with user and request information
|
|
11
|
+
- ✅ Customizable field masking and data sanitization
|
|
12
|
+
- ✅ TypeScript support
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install @explita/prisma-audit-log
|
|
18
|
+
# or
|
|
19
|
+
yarn add @explita/prisma-audit-log
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
#
|
|
23
|
+
|
|
24
|
+
### Basic Setup
|
|
25
|
+
|
|
26
|
+
```typescript
|
|
27
|
+
import { PrismaClient } from "@prisma/client";
|
|
28
|
+
import { auditLogExtension } from "@explita/prisma-audit-log";
|
|
29
|
+
import { getContext } from "..."; // or your auth context
|
|
30
|
+
|
|
31
|
+
const prisma = new PrismaClient().$extends(
|
|
32
|
+
auditLogExtension({
|
|
33
|
+
// Optional: Only include specific models
|
|
34
|
+
// includeModels: ['User', 'Post'],
|
|
35
|
+
|
|
36
|
+
// Optional: Exclude specific models
|
|
37
|
+
// excludeModels: ['SensitiveData'],
|
|
38
|
+
|
|
39
|
+
// Get context for audit logs.
|
|
40
|
+
// The values returned here will be merged with operation context.
|
|
41
|
+
// Make sure whatever returned matches your audit log schema.
|
|
42
|
+
getContext: () => {
|
|
43
|
+
const { auth, req } = getContext();
|
|
44
|
+
return {
|
|
45
|
+
userId: auth.id,
|
|
46
|
+
companyId: auth.companyId,
|
|
47
|
+
ipAddress: req.ip,
|
|
48
|
+
metadata: {
|
|
49
|
+
ip: req.ip,
|
|
50
|
+
browser: req.headers["user-agent"],
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
// Optional: Mask sensitive fields
|
|
56
|
+
maskFields: ["password", "token"],
|
|
57
|
+
maskValue: "[REDACTED]", // default value
|
|
58
|
+
|
|
59
|
+
// Optional: Truncate long values
|
|
60
|
+
maxStringLength: 1000,
|
|
61
|
+
maxArrayLength: 50,
|
|
62
|
+
|
|
63
|
+
// Optional: Custom logger
|
|
64
|
+
logger: (log) => {
|
|
65
|
+
// Send logs to your logging service
|
|
66
|
+
console.log("AUDIT:", log);
|
|
67
|
+
},
|
|
68
|
+
})
|
|
69
|
+
);
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
#
|
|
73
|
+
|
|
74
|
+
### Database Schema
|
|
75
|
+
|
|
76
|
+
Add this to your Prisma schema:
|
|
77
|
+
|
|
78
|
+
```prisma
|
|
79
|
+
model AuditLog {
|
|
80
|
+
id String @id @default(cuid())
|
|
81
|
+
userId String? @map("user_id")
|
|
82
|
+
recordId String @map("record_id")
|
|
83
|
+
action String
|
|
84
|
+
model String
|
|
85
|
+
oldData Json? @map("old_data")
|
|
86
|
+
newData Json? @map("new_data")
|
|
87
|
+
changedFields String[] @map("changed_fields")
|
|
88
|
+
ipAddress String? @map("ip_address")
|
|
89
|
+
userAgent String? @map("user_agent")
|
|
90
|
+
metadata Json?
|
|
91
|
+
createdAt DateTime @default(now()) @map("created_at")
|
|
92
|
+
|
|
93
|
+
@@map("audit_logs")
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
#
|
|
98
|
+
|
|
99
|
+
### Example Usage
|
|
100
|
+
|
|
101
|
+
```typescript
|
|
102
|
+
// Example Fastify route
|
|
103
|
+
route.put("/test/:id", async (request, reply) => {
|
|
104
|
+
// These operations will be automatically logged
|
|
105
|
+
await db.branch.update({
|
|
106
|
+
where: { id: "123" },
|
|
107
|
+
data: { address: "123 Main St" },
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
await db.branch.updateMany({
|
|
111
|
+
where: { phone: "0123456789" },
|
|
112
|
+
data: { phone: "1023456789" },
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
reply.send("Operations completed");
|
|
116
|
+
});
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## Configuration Options
|
|
120
|
+
|
|
121
|
+
| Option | Type | Description |
|
|
122
|
+
| ----------------- | -------------------- | ------------------------------------------------------ |
|
|
123
|
+
| `includeModels` | `string[]` | Only log operations on these models |
|
|
124
|
+
| `excludeModels` | `string[]` | Exclude these models from logging |
|
|
125
|
+
| `getContext` | `() => AuditContext` | Function to get current context (user, IP, etc.) |
|
|
126
|
+
| `maskFields` | `string[]` | Fields to mask in logs |
|
|
127
|
+
| `maskValue` | `any` | Value to use for masked fields (default: `[REDACTED]`) |
|
|
128
|
+
| `maxStringLength` | `number` | Truncate long strings |
|
|
129
|
+
| `maxArrayLength` | `number` | Truncate large arrays |
|
|
130
|
+
| `maxPayloadBytes` | `number` | Maximum JSON payload size |
|
|
131
|
+
| `batchInsert` | `boolean` | Use batch inserts (default: `true`) |
|
|
132
|
+
|
|
133
|
+
## License
|
|
134
|
+
|
|
135
|
+
MIT
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
Built with ❤️ by [Explita](https://explita.ng)
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { HandlerArgs } from "../types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Handle createMany and createManyAndReturn operations
|
|
4
|
+
* - createManyAndReturn: logs each created record using the returned rows
|
|
5
|
+
* - createMany: logs best-effort using the input data (uses provided id if present)
|
|
6
|
+
*/
|
|
7
|
+
export declare function handleCreateMany(opts: HandlerArgs): Promise<any>;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.handleCreateMany = handleCreateMany;
|
|
4
|
+
const process_js_1 = require("./process.js");
|
|
5
|
+
/**
|
|
6
|
+
* Handle createMany and createManyAndReturn operations
|
|
7
|
+
* - createManyAndReturn: logs each created record using the returned rows
|
|
8
|
+
* - createMany: logs best-effort using the input data (uses provided id if present)
|
|
9
|
+
*/
|
|
10
|
+
async function handleCreateMany(opts) {
|
|
11
|
+
const { args, prisma, query, options, modelName, operation } = opts;
|
|
12
|
+
if (operation === "createManyAndReturn") {
|
|
13
|
+
const created = await query(args);
|
|
14
|
+
const rows = Array.isArray(created) ? created : [created];
|
|
15
|
+
// Build logs
|
|
16
|
+
const logs = rows.map((row) => {
|
|
17
|
+
var _a;
|
|
18
|
+
return ({
|
|
19
|
+
action: "CREATE",
|
|
20
|
+
model: modelName,
|
|
21
|
+
recordId: String((_a = row.id) !== null && _a !== void 0 ? _a : "unknown"),
|
|
22
|
+
newData: row,
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
await (0, process_js_1.saveAuditLogs)(prisma, logs, options);
|
|
26
|
+
return created;
|
|
27
|
+
}
|
|
28
|
+
// Fallback for createMany (does not return created records)
|
|
29
|
+
const rawData = args === null || args === void 0 ? void 0 : args.data;
|
|
30
|
+
const dataArray = Array.isArray(rawData)
|
|
31
|
+
? rawData
|
|
32
|
+
: rawData
|
|
33
|
+
? [rawData]
|
|
34
|
+
: [];
|
|
35
|
+
// Guard: skip logging when data is empty or missing
|
|
36
|
+
if (!dataArray.length) {
|
|
37
|
+
return query(args);
|
|
38
|
+
}
|
|
39
|
+
const result = await query(args);
|
|
40
|
+
const logs = dataArray.map((item) => {
|
|
41
|
+
var _a;
|
|
42
|
+
return ({
|
|
43
|
+
action: "CREATE",
|
|
44
|
+
model: modelName,
|
|
45
|
+
recordId: String((_a = item === null || item === void 0 ? void 0 : item.id) !== null && _a !== void 0 ? _a : "unknown"),
|
|
46
|
+
newData: item,
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
await (0, process_js_1.saveAuditLogs)(prisma, logs, options);
|
|
50
|
+
return result;
|
|
51
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.handleCreate = handleCreate;
|
|
4
|
+
const process_js_1 = require("./process.js");
|
|
5
|
+
async function handleCreate(opts) {
|
|
6
|
+
const { args, prisma, query, options, modelName } = opts;
|
|
7
|
+
const result = await query(args);
|
|
8
|
+
const auditLog = {
|
|
9
|
+
action: "CREATE",
|
|
10
|
+
model: modelName,
|
|
11
|
+
recordId: result.id,
|
|
12
|
+
newData: result,
|
|
13
|
+
};
|
|
14
|
+
await (0, process_js_1.processAuditLog)(prisma, auditLog, options);
|
|
15
|
+
return result;
|
|
16
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.handleDeleteMany = handleDeleteMany;
|
|
4
|
+
const process_js_1 = require("./process.js");
|
|
5
|
+
async function handleDeleteMany(opts) {
|
|
6
|
+
const { args, prisma, query, options, modelName } = opts;
|
|
7
|
+
// Get the current state of records that will be deleted
|
|
8
|
+
const currentRecords = await prisma[modelName].findMany({
|
|
9
|
+
where: args["where"],
|
|
10
|
+
});
|
|
11
|
+
// Guard: if no records match, skip logging
|
|
12
|
+
if (!currentRecords.length) {
|
|
13
|
+
return query(args);
|
|
14
|
+
}
|
|
15
|
+
const result = await query(args);
|
|
16
|
+
// Create audit logs for each deleted record
|
|
17
|
+
const logs = currentRecords.map((record) => ({
|
|
18
|
+
action: "DELETE",
|
|
19
|
+
model: modelName,
|
|
20
|
+
recordId: record.id,
|
|
21
|
+
oldData: record,
|
|
22
|
+
}));
|
|
23
|
+
await (0, process_js_1.saveAuditLogs)(prisma, logs, options);
|
|
24
|
+
return result;
|
|
25
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.handleDelete = handleDelete;
|
|
4
|
+
const process_js_1 = require("./process.js");
|
|
5
|
+
async function handleDelete(opts) {
|
|
6
|
+
const { args, prisma, query, options, modelName } = opts;
|
|
7
|
+
// Get the current state before delete
|
|
8
|
+
const current = await prisma[modelName].findUnique({
|
|
9
|
+
where: args["where"],
|
|
10
|
+
});
|
|
11
|
+
const result = await query(args);
|
|
12
|
+
if (current) {
|
|
13
|
+
const auditLog = {
|
|
14
|
+
action: "DELETE",
|
|
15
|
+
model: modelName,
|
|
16
|
+
recordId: current.id,
|
|
17
|
+
oldData: current,
|
|
18
|
+
};
|
|
19
|
+
await (0, process_js_1.processAuditLog)(prisma, auditLog, options);
|
|
20
|
+
}
|
|
21
|
+
return result;
|
|
22
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { AuditLog, AuditLogOptions } from "../types.js";
|
|
2
|
+
export declare function processAuditLog(prisma: any, auditLog: Omit<AuditLog, "id">, options: AuditLogOptions): Promise<void>;
|
|
3
|
+
export declare function saveAuditLogs(prisma: any, logs: Omit<AuditLog, "id">[], options: AuditLogOptions): Promise<void>;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.processAuditLog = processAuditLog;
|
|
4
|
+
exports.saveAuditLogs = saveAuditLogs;
|
|
5
|
+
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
|
+
async function saveAuditLogs(prisma, logs, options) {
|
|
28
|
+
var _a;
|
|
29
|
+
if (!logs.length)
|
|
30
|
+
return;
|
|
31
|
+
if (options.batchInsert && ((_a = prisma.auditLog) === null || _a === void 0 ? void 0 : _a.createMany)) {
|
|
32
|
+
const finals = await Promise.all(logs.map((log) => (0, process_log_helpers_js_1.buildFinalLog)(log, options)));
|
|
33
|
+
if (finals.length) {
|
|
34
|
+
await prisma.auditLog.createMany({ data: finals });
|
|
35
|
+
if (options.logger) {
|
|
36
|
+
for (const entry of finals) {
|
|
37
|
+
await options.logger(entry);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
for (const log of logs) {
|
|
44
|
+
await processAuditLog(prisma, log, options);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.handleUpdateMany = handleUpdateMany;
|
|
4
|
+
const utils_js_1 = require("../lib/utils.js");
|
|
5
|
+
const process_js_1 = require("./process.js");
|
|
6
|
+
async function handleUpdateMany(opts) {
|
|
7
|
+
const { args, prisma, query, options, modelName, operation } = opts;
|
|
8
|
+
// Get the current state of records that will be updated
|
|
9
|
+
const currentRecords = await prisma[modelName].findMany({
|
|
10
|
+
where: args["where"],
|
|
11
|
+
});
|
|
12
|
+
// Guard: if no records will be updated, skip logging
|
|
13
|
+
if (!currentRecords.length) {
|
|
14
|
+
return query(args);
|
|
15
|
+
}
|
|
16
|
+
const ids = currentRecords.map((row) => row.id);
|
|
17
|
+
const result = await query(args);
|
|
18
|
+
//updated records
|
|
19
|
+
const updatedRecords = operation == "createManyAndReturn"
|
|
20
|
+
? result
|
|
21
|
+
: await prisma[modelName].findMany({
|
|
22
|
+
where: { id: { in: ids } },
|
|
23
|
+
});
|
|
24
|
+
const logs = [];
|
|
25
|
+
// Create audit logs for each updated record
|
|
26
|
+
for (const current of currentRecords) {
|
|
27
|
+
const updated = updatedRecords.find((r) => r.id === current.id);
|
|
28
|
+
if (!updated)
|
|
29
|
+
continue;
|
|
30
|
+
const changedFields = (0, utils_js_1.getChangedFields)(current, updated);
|
|
31
|
+
const { oldData, newData } = (0, utils_js_1.buildSnapshot)(changedFields, current, updated);
|
|
32
|
+
logs.push({
|
|
33
|
+
action: "UPDATE",
|
|
34
|
+
model: modelName,
|
|
35
|
+
recordId: current.id,
|
|
36
|
+
oldData,
|
|
37
|
+
newData,
|
|
38
|
+
changedFields,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
await (0, process_js_1.saveAuditLogs)(prisma, logs, options);
|
|
42
|
+
return result;
|
|
43
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.handleUpdate = handleUpdate;
|
|
4
|
+
const utils_js_1 = require("../lib/utils.js");
|
|
5
|
+
const process_js_1 = require("./process.js");
|
|
6
|
+
async function handleUpdate(opts) {
|
|
7
|
+
const { args, prisma, query, options, modelName } = opts;
|
|
8
|
+
// Get the current state before update
|
|
9
|
+
const current = await prisma[modelName].findUnique({
|
|
10
|
+
where: args["where"],
|
|
11
|
+
});
|
|
12
|
+
const result = await query(args);
|
|
13
|
+
if (current) {
|
|
14
|
+
const changedFields = (0, utils_js_1.getChangedFields)(current, result);
|
|
15
|
+
const { oldData, newData } = (0, utils_js_1.buildSnapshot)(changedFields, current, result);
|
|
16
|
+
const auditLog = {
|
|
17
|
+
action: "UPDATE",
|
|
18
|
+
model: modelName,
|
|
19
|
+
recordId: result.id,
|
|
20
|
+
oldData,
|
|
21
|
+
newData,
|
|
22
|
+
changedFields,
|
|
23
|
+
};
|
|
24
|
+
await (0, process_js_1.processAuditLog)(prisma, auditLog, options);
|
|
25
|
+
}
|
|
26
|
+
return result;
|
|
27
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.auditLogExtension = void 0;
|
|
4
|
+
var prisma_extension_js_1 = require("./lib/prisma-extension.js");
|
|
5
|
+
Object.defineProperty(exports, "auditLogExtension", { enumerable: true, get: function () { return prisma_extension_js_1.auditLogExtension; } });
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { AuditLogOptions } from "../types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Creates a Prisma extension that adds audit logging
|
|
4
|
+
*/
|
|
5
|
+
export declare function auditLogExtension(options?: AuditLogOptions): (client: any) => import("@prisma/client/extension").PrismaClientExtends<import("@prisma/client/runtime/client").InternalArgs<{}, {}, {}, {}> & import("@prisma/client/runtime/client").DefaultArgs>;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.auditLogExtension = auditLogExtension;
|
|
4
|
+
const extension_1 = require("@prisma/client/extension");
|
|
5
|
+
const utils_js_1 = require("./utils.js");
|
|
6
|
+
const create_js_1 = require("../core/create.js");
|
|
7
|
+
const create_many_js_1 = require("../core/create-many.js");
|
|
8
|
+
const update_many_js_1 = require("../core/update-many.js");
|
|
9
|
+
const delete_js_1 = require("../core/delete.js");
|
|
10
|
+
const delete_many_js_1 = require("../core/delete-many.js");
|
|
11
|
+
const update_js_1 = require("../core/update.js");
|
|
12
|
+
/**
|
|
13
|
+
* Creates a Prisma extension that adds audit logging
|
|
14
|
+
*/
|
|
15
|
+
function auditLogExtension(options = {}) {
|
|
16
|
+
return extension_1.Prisma.defineExtension((prisma) => {
|
|
17
|
+
return prisma.$extends({
|
|
18
|
+
query: {
|
|
19
|
+
$allModels: {
|
|
20
|
+
async $allOperations({ model, operation, args, query }) {
|
|
21
|
+
const modelName = model === null || model === void 0 ? void 0 : model.toString();
|
|
22
|
+
// Skip if model is excluded or not included
|
|
23
|
+
if ((0, utils_js_1.shouldSkipModel)(modelName, options)) {
|
|
24
|
+
return query(args);
|
|
25
|
+
}
|
|
26
|
+
// Never audit the audit log model itself
|
|
27
|
+
if (["auditLog", "AuditLog"].includes(modelName !== null && modelName !== void 0 ? modelName : "")) {
|
|
28
|
+
return query(args);
|
|
29
|
+
}
|
|
30
|
+
const queryArgs = {
|
|
31
|
+
args,
|
|
32
|
+
prisma,
|
|
33
|
+
query,
|
|
34
|
+
options,
|
|
35
|
+
modelName,
|
|
36
|
+
operation,
|
|
37
|
+
};
|
|
38
|
+
// Handle different operations
|
|
39
|
+
switch (operation) {
|
|
40
|
+
case "create":
|
|
41
|
+
return (0, create_js_1.handleCreate)(queryArgs);
|
|
42
|
+
case "createMany":
|
|
43
|
+
case "createManyAndReturn":
|
|
44
|
+
return (0, create_many_js_1.handleCreateMany)(queryArgs);
|
|
45
|
+
case "delete":
|
|
46
|
+
return (0, delete_js_1.handleDelete)(queryArgs);
|
|
47
|
+
case "deleteMany":
|
|
48
|
+
return (0, delete_many_js_1.handleDeleteMany)(queryArgs);
|
|
49
|
+
case "update":
|
|
50
|
+
return (0, update_js_1.handleUpdate)(queryArgs);
|
|
51
|
+
case "updateMany":
|
|
52
|
+
case "updateManyAndReturn":
|
|
53
|
+
return (0, update_many_js_1.handleUpdateMany)(queryArgs);
|
|
54
|
+
default:
|
|
55
|
+
return query(args);
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.buildFinalLog = buildFinalLog;
|
|
4
|
+
function matchesMaskPath(maskPaths, keyPath) {
|
|
5
|
+
if (!maskPaths || keyPath.length === 0)
|
|
6
|
+
return false;
|
|
7
|
+
return maskPaths.some((path) => {
|
|
8
|
+
const segments = path.split(".");
|
|
9
|
+
if (segments.length !== keyPath.length)
|
|
10
|
+
return false;
|
|
11
|
+
return segments.every((segment, index) => segment === "*" || segment === keyPath[index]);
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
function maskAndTruncate(value, opts, keyPath = []) {
|
|
15
|
+
const { maskFields, maskPaths, maskValue = "[REDACTED]", maxStringLength, maxArrayLength, } = opts;
|
|
16
|
+
const currentKey = keyPath[keyPath.length - 1];
|
|
17
|
+
const shouldMaskField = currentKey && (maskFields === null || maskFields === void 0 ? void 0 : maskFields.includes(currentKey));
|
|
18
|
+
const shouldMaskPath = matchesMaskPath(maskPaths, keyPath);
|
|
19
|
+
if (shouldMaskField || shouldMaskPath) {
|
|
20
|
+
return maskValue;
|
|
21
|
+
}
|
|
22
|
+
if (value == null)
|
|
23
|
+
return value;
|
|
24
|
+
if (value instanceof Date) {
|
|
25
|
+
const iso = value.toISOString();
|
|
26
|
+
if (maxStringLength && iso.length > maxStringLength) {
|
|
27
|
+
return iso.slice(0, maxStringLength) + "…";
|
|
28
|
+
}
|
|
29
|
+
return iso;
|
|
30
|
+
}
|
|
31
|
+
if (typeof value === "string") {
|
|
32
|
+
if (maxStringLength && value.length > maxStringLength) {
|
|
33
|
+
return value.slice(0, maxStringLength) + "…";
|
|
34
|
+
}
|
|
35
|
+
return value;
|
|
36
|
+
}
|
|
37
|
+
if (Array.isArray(value)) {
|
|
38
|
+
const limited = maxArrayLength ? value.slice(0, maxArrayLength) : value;
|
|
39
|
+
return limited.map((v, i) => maskAndTruncate(v, opts, [...keyPath, String(i)]));
|
|
40
|
+
}
|
|
41
|
+
if (typeof value === "object") {
|
|
42
|
+
const out = {};
|
|
43
|
+
for (const [k, v] of Object.entries(value)) {
|
|
44
|
+
out[k] = maskAndTruncate(v, opts, [...keyPath, k]);
|
|
45
|
+
}
|
|
46
|
+
return out;
|
|
47
|
+
}
|
|
48
|
+
return value;
|
|
49
|
+
}
|
|
50
|
+
function enforcePayloadLimit(value, opts) {
|
|
51
|
+
const { maxPayloadBytes } = opts;
|
|
52
|
+
if (!maxPayloadBytes || value == null)
|
|
53
|
+
return value;
|
|
54
|
+
try {
|
|
55
|
+
const json = JSON.stringify(value);
|
|
56
|
+
if (json.length <= maxPayloadBytes)
|
|
57
|
+
return value;
|
|
58
|
+
return {
|
|
59
|
+
truncated: true,
|
|
60
|
+
originalSize: json.length,
|
|
61
|
+
preview: json.slice(0, maxPayloadBytes),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return {
|
|
66
|
+
truncated: true,
|
|
67
|
+
reason: "serialization_failed",
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
async function buildFinalLog(auditLog, options) {
|
|
72
|
+
const finalLog = {
|
|
73
|
+
...auditLog,
|
|
74
|
+
};
|
|
75
|
+
// Merge extra context
|
|
76
|
+
if (options.getContext) {
|
|
77
|
+
const context = await options.getContext();
|
|
78
|
+
if (context && typeof context === "object") {
|
|
79
|
+
Object.assign(finalLog, context);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
// Apply masking/truncation to payload sections
|
|
83
|
+
if (finalLog.oldData) {
|
|
84
|
+
finalLog.oldData = enforcePayloadLimit(maskAndTruncate(finalLog.oldData, options), options);
|
|
85
|
+
}
|
|
86
|
+
if (finalLog.newData) {
|
|
87
|
+
finalLog.newData = enforcePayloadLimit(maskAndTruncate(finalLog.newData, options), options);
|
|
88
|
+
}
|
|
89
|
+
if (finalLog.metadata) {
|
|
90
|
+
finalLog.metadata = enforcePayloadLimit(maskAndTruncate(finalLog.metadata, options), options);
|
|
91
|
+
}
|
|
92
|
+
return finalLog;
|
|
93
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { AuditLogOptions } from "../types.js";
|
|
2
|
+
export declare function shouldSkipModel(modelName: string | undefined, options: AuditLogOptions): boolean;
|
|
3
|
+
export declare function getChangedFields(oldObj: any, newObj: any): string[];
|
|
4
|
+
export declare function buildSnapshot(changedFields: string[], current: any, updated: any): {
|
|
5
|
+
newData: any;
|
|
6
|
+
oldData: any;
|
|
7
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.shouldSkipModel = shouldSkipModel;
|
|
4
|
+
exports.getChangedFields = getChangedFields;
|
|
5
|
+
exports.buildSnapshot = buildSnapshot;
|
|
6
|
+
function shouldSkipModel(modelName, options) {
|
|
7
|
+
var _a;
|
|
8
|
+
if (!modelName)
|
|
9
|
+
return true;
|
|
10
|
+
// Skip if model is in exclude list
|
|
11
|
+
if ((_a = options.excludeModels) === null || _a === void 0 ? void 0 : _a.includes(modelName)) {
|
|
12
|
+
return true;
|
|
13
|
+
}
|
|
14
|
+
// Skip if includeModels is specified and model is not in the list
|
|
15
|
+
if (options.includeModels && !options.includeModels.includes(modelName)) {
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
function getChangedFields(oldObj, newObj) {
|
|
21
|
+
const changedFields = [];
|
|
22
|
+
for (const key in newObj) {
|
|
23
|
+
if (JSON.stringify(oldObj[key]) !== JSON.stringify(newObj[key])) {
|
|
24
|
+
changedFields.push(key);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return changedFields;
|
|
28
|
+
}
|
|
29
|
+
function buildSnapshot(changedFields, current, updated) {
|
|
30
|
+
// Build partial snapshots containing only changed fields
|
|
31
|
+
const oldData = changedFields.reduce((acc, key) => {
|
|
32
|
+
acc[key] = current[key];
|
|
33
|
+
return acc;
|
|
34
|
+
}, {});
|
|
35
|
+
const newData = changedFields.reduce((acc, key) => {
|
|
36
|
+
acc[key] = updated[key];
|
|
37
|
+
return acc;
|
|
38
|
+
}, {});
|
|
39
|
+
return { newData, oldData };
|
|
40
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { JsArgs } from "@prisma/client/runtime/client";
|
|
2
|
+
export interface AuditLogOptions {
|
|
3
|
+
/**
|
|
4
|
+
* Tables to include in audit logging
|
|
5
|
+
* If not provided, all tables will be included
|
|
6
|
+
*/
|
|
7
|
+
includeModels?: string[];
|
|
8
|
+
/**
|
|
9
|
+
* Tables to exclude from audit logging
|
|
10
|
+
* Takes precedence over includeModels
|
|
11
|
+
*/
|
|
12
|
+
excludeModels?: string[];
|
|
13
|
+
/**
|
|
14
|
+
* Function to get extra context for the audit trail (e.g., userId, companyId, branchId, metadata, ...)
|
|
15
|
+
* Return any additional fields your AuditLog model supports. They will be merged into the final log.
|
|
16
|
+
*/
|
|
17
|
+
getContext?: () => AuditContext | Promise<AuditContext | undefined>;
|
|
18
|
+
/**
|
|
19
|
+
* Custom logger function
|
|
20
|
+
* If not provided, logs will be written to console
|
|
21
|
+
*/
|
|
22
|
+
logger?: (log: AuditLog) => void | Promise<void>;
|
|
23
|
+
/**
|
|
24
|
+
* Keys to mask in oldData/newData/metadata. Exact match on property name.
|
|
25
|
+
*/
|
|
26
|
+
maskFields?: string[];
|
|
27
|
+
/**
|
|
28
|
+
* Dot-delimited paths to mask (e.g., "payment.card.number"). Supports '*' wildcard per segment.
|
|
29
|
+
*/
|
|
30
|
+
maskPaths?: string[];
|
|
31
|
+
/** Value used to replace masked fields (default: "[REDACTED]") */
|
|
32
|
+
maskValue?: any;
|
|
33
|
+
/** Truncate long string values to this length (default: no limit) */
|
|
34
|
+
maxStringLength?: number;
|
|
35
|
+
/** Truncate arrays to this length (default: no limit) */
|
|
36
|
+
maxArrayLength?: number;
|
|
37
|
+
/** Maximum JSON payload size per field (old/new/metadata). Entries exceeding this will be replaced with a truncated preview */
|
|
38
|
+
maxPayloadBytes?: number;
|
|
39
|
+
/** Whether to use createMany for bulk inserts when available (default: true) */
|
|
40
|
+
batchInsert?: boolean;
|
|
41
|
+
}
|
|
42
|
+
export interface AuditLog {
|
|
43
|
+
id: string;
|
|
44
|
+
action: "CREATE" | "UPDATE" | "DELETE";
|
|
45
|
+
model: string;
|
|
46
|
+
recordId: string;
|
|
47
|
+
oldData?: Record<string, any>;
|
|
48
|
+
newData?: Record<string, any>;
|
|
49
|
+
changedFields?: string[];
|
|
50
|
+
userId?: string;
|
|
51
|
+
ipAddress?: string;
|
|
52
|
+
userAgent?: string;
|
|
53
|
+
metadata?: Record<string, any>;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Arbitrary context that can be merged into the audit log.
|
|
57
|
+
* Include any additional fields your schema supports (e.g., companyId, branchId, tenantId, etc.)
|
|
58
|
+
*/
|
|
59
|
+
export interface AuditContext {
|
|
60
|
+
userId?: string;
|
|
61
|
+
metadata?: Record<string, any>;
|
|
62
|
+
[key: string]: any;
|
|
63
|
+
}
|
|
64
|
+
export type HandlerArgs = {
|
|
65
|
+
prisma: any;
|
|
66
|
+
modelName: string;
|
|
67
|
+
args: JsArgs;
|
|
68
|
+
query: (args: JsArgs) => Promise<any>;
|
|
69
|
+
options: AuditLogOptions;
|
|
70
|
+
operation?: string;
|
|
71
|
+
};
|
package/dist/types.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@explita/prisma-audit-log",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A Prisma extension for automatic audit logging",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"prisma",
|
|
9
|
+
"audit",
|
|
10
|
+
"logging",
|
|
11
|
+
"extension"
|
|
12
|
+
],
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"author": "Explita",
|
|
15
|
+
"scripts": {
|
|
16
|
+
"clean": "rimraf dist",
|
|
17
|
+
"build": "npm run clean && tsc",
|
|
18
|
+
"prepublishOnly": "npm run build"
|
|
19
|
+
},
|
|
20
|
+
"peerDependencies": {
|
|
21
|
+
"@prisma/client": "^7"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@types/node": "^24",
|
|
25
|
+
"rimraf": "^6.0.1",
|
|
26
|
+
"typescript": "^5"
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"dist",
|
|
30
|
+
"README.md"
|
|
31
|
+
]
|
|
32
|
+
}
|