@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 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
- - ✅ Support for single and batch operations
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 | 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`) |
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
 
@@ -11,6 +11,6 @@ async function handleCreate(opts) {
11
11
  recordId: result.id,
12
12
  newData: result,
13
13
  };
14
- await (0, process_js_1.processAuditLog)(prisma, auditLog, options);
14
+ await (0, process_js_1.saveAuditLogs)(prisma, [auditLog], options);
15
15
  return result;
16
16
  }
@@ -16,7 +16,7 @@ async function handleDelete(opts) {
16
16
  recordId: current.id,
17
17
  oldData: current,
18
18
  };
19
- await (0, process_js_1.processAuditLog)(prisma, auditLog, options);
19
+ await (0, process_js_1.saveAuditLogs)(prisma, [auditLog], options);
20
20
  }
21
21
  return result;
22
22
  }
@@ -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>;
@@ -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
- if (options.batchInsert && ((_a = prisma.auditLog) === null || _a === void 0 ? void 0 : _a.createMany)) {
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
- if (options.logger) {
36
- for (const entry of finals) {
37
- await options.logger(entry);
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
- return;
24
+ if (options.logger && finals.length) {
25
+ await options.logger(finals);
26
+ }
42
27
  }
43
- for (const log of logs) {
44
- await processAuditLog(prisma, log, options);
28
+ catch (error) {
29
+ console.error("Failed to save audit log to database:", error);
45
30
  }
46
31
  }
@@ -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",
@@ -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.processAuditLog)(prisma, auditLog, options);
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
- // Never audit the audit log model itself
27
- if (["auditLog", "AuditLog"].includes(modelName !== null && modelName !== void 0 ? modelName : "")) {
28
- return query(args);
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@explita/prisma-audit-log",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "A Prisma extension for automatic audit logging",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",