@dynamatix/cat-shared 0.0.96 → 0.0.97
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 +6 -6
- package/index.js +2 -2
- package/middlewares/audit.middleware.js +301 -301
- package/middlewares/index.js +2 -2
- package/models/audit-config.model.js +15 -15
- package/models/audit.model.js +16 -16
- package/models/form-configuration.model.js +269 -269
- package/models/index.js +3 -3
- package/models/value-reference-map.model.js +16 -16
- package/package.json +21 -21
- package/seeders/value-reference-map.seeder.js +66 -66
- package/services/audit-log.hook.js +2 -2
- package/services/index.js +1 -1
- package/services/request-context.service.js +8 -8
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
This is the shared library for Catura Services.
|
|
2
|
-
# Purpose
|
|
3
|
-
|
|
4
|
-
The purposes of this repository are to include:
|
|
5
|
-
- All the models of Catura
|
|
6
|
-
- Shared services in Catura
|
|
1
|
+
This is the shared library for Catura Services.
|
|
2
|
+
# Purpose
|
|
3
|
+
|
|
4
|
+
The purposes of this repository are to include:
|
|
5
|
+
- All the models of Catura
|
|
6
|
+
- Shared services in Catura
|
|
7
7
|
- Shared middlewares in Catura
|
package/index.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export * from './models/index.js';
|
|
2
|
-
export * from './middlewares/index.js';
|
|
1
|
+
export * from './models/index.js';
|
|
2
|
+
export * from './middlewares/index.js';
|
|
3
3
|
export * from './services/index.js';
|
|
@@ -1,302 +1,302 @@
|
|
|
1
|
-
import AuditConfigModel from '../models/audit-config.model.js';
|
|
2
|
-
import AuditLog from '../models/audit.model.js';
|
|
3
|
-
import ValueReferenceMap from '../models/value-reference-map.model.js';
|
|
4
|
-
import { getContext } from '../services/request-context.service.js';
|
|
5
|
-
import mongoose from 'mongoose';
|
|
6
|
-
|
|
7
|
-
let onAuditLogCreated = null;
|
|
8
|
-
|
|
9
|
-
// Optionally, define custom resolvers here
|
|
10
|
-
const customResolvers = {
|
|
11
|
-
// Example:
|
|
12
|
-
// expenditureRationale: (doc) => `Rationale for ${doc.type}: ${doc.rationale}`
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
// Expression-based description resolver: resolves ${fieldName} in the expression using doc, and via Lookup if needed
|
|
16
|
-
async function resolveExpressionDescription(doc, expression, resolverType) {
|
|
17
|
-
const matches = [...expression.matchAll(/\$\{([^}]+)\}/g)];
|
|
18
|
-
let result = expression;
|
|
19
|
-
for (const match of matches) {
|
|
20
|
-
const fieldName = match[1];
|
|
21
|
-
let value = doc[fieldName];
|
|
22
|
-
if (resolverType === 'lookup' && value && mongoose.models['Lookup']) {
|
|
23
|
-
const lookupDoc = await mongoose.models['Lookup'].findById(value).lean();
|
|
24
|
-
value = lookupDoc?.name || value;
|
|
25
|
-
}
|
|
26
|
-
result = result.replace(match[0], value ?? '');
|
|
27
|
-
}
|
|
28
|
-
return result;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
// Update resolveDescription to support expression-based descriptionField
|
|
32
|
-
async function resolveDescription(field, doc) {
|
|
33
|
-
const modelName = doc.constructor.modelName;
|
|
34
|
-
const config = await ValueReferenceMap.findOne({ field, model: modelName });
|
|
35
|
-
if (!config) return field;
|
|
36
|
-
|
|
37
|
-
if (config.descriptionField && config.descriptionField.includes('${')) {
|
|
38
|
-
return await resolveExpressionDescription(doc, config.descriptionField, config.descriptionResolverType);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
switch (config.descriptionResolverType) {
|
|
42
|
-
case 'lookup': {
|
|
43
|
-
const lookupId = doc[config.descriptionField];
|
|
44
|
-
if (!lookupId) return '';
|
|
45
|
-
const lookupDoc = await mongoose.models['Lookup'].findById(lookupId).lean();
|
|
46
|
-
return lookupDoc?.name || '';
|
|
47
|
-
}
|
|
48
|
-
case 'direct':
|
|
49
|
-
return doc[config.descriptionField] || '';
|
|
50
|
-
case 'composite':
|
|
51
|
-
return (config.descriptionFields || [])
|
|
52
|
-
.map(f => doc[f])
|
|
53
|
-
.filter(Boolean)
|
|
54
|
-
.join(' ');
|
|
55
|
-
case 'custom':
|
|
56
|
-
if (typeof customResolvers?.[config.descriptionFunction] === 'function') {
|
|
57
|
-
return await customResolvers[config.descriptionFunction](doc);
|
|
58
|
-
}
|
|
59
|
-
return '';
|
|
60
|
-
default:
|
|
61
|
-
return '';
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// Update resolveEntityDescription to support expression-based descriptionField
|
|
66
|
-
async function resolveEntityDescription(doc, auditConfig) {
|
|
67
|
-
if (!auditConfig.descriptionResolutorForExternalData) return '';
|
|
68
|
-
const field = auditConfig.descriptionResolutorForExternalData;
|
|
69
|
-
const value = doc[field];
|
|
70
|
-
// Try to resolve via ValueReferenceMap
|
|
71
|
-
const map = await ValueReferenceMap.findOne({ field });
|
|
72
|
-
if (map && map.descriptionField && map.descriptionField.includes('${')) {
|
|
73
|
-
return await resolveExpressionDescription(doc, map.descriptionField, map.descriptionResolverType);
|
|
74
|
-
}
|
|
75
|
-
if (map && value) {
|
|
76
|
-
const Model = mongoose.models[map.model];
|
|
77
|
-
if (Model) {
|
|
78
|
-
const refDoc = await Model.findById(value).lean();
|
|
79
|
-
return refDoc?.[map.displayField] || value;
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
// Fallback: If it's an ObjectId and Lookup model exists, resolve to name
|
|
83
|
-
if (mongoose.Types.ObjectId.isValid(value) && mongoose.models['Lookup']) {
|
|
84
|
-
const lookupDoc = await mongoose.models['Lookup'].findById(value).lean();
|
|
85
|
-
return lookupDoc?.name || value;
|
|
86
|
-
}
|
|
87
|
-
return value;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// Add new function to resolve deletion description
|
|
91
|
-
async function resolveDeletionDescription(doc, auditConfig) {
|
|
92
|
-
if (!auditConfig.descriptionResolutorForDeletion) return '';
|
|
93
|
-
const expression = auditConfig.descriptionResolutorForDeletion;
|
|
94
|
-
return await resolveExpressionDescription(doc, expression, 'direct');
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
function applyAuditMiddleware(schema, collectionName) {
|
|
98
|
-
// Handle creation audit
|
|
99
|
-
schema.post('save', async function (doc) {
|
|
100
|
-
const auditConfig = await AuditConfigModel.findOne({ collectionName });
|
|
101
|
-
if (!auditConfig?.trackCreation) return;
|
|
102
|
-
|
|
103
|
-
const context = getContext();
|
|
104
|
-
const userId = context?.userId || 'anonymous';
|
|
105
|
-
const contextId = context?.contextId;
|
|
106
|
-
|
|
107
|
-
const entityDescription = await resolveEntityDescription(doc, auditConfig);
|
|
108
|
-
let logs = [];
|
|
109
|
-
|
|
110
|
-
// Per-field logs
|
|
111
|
-
if (auditConfig.fields?.length) {
|
|
112
|
-
for (const field of auditConfig.fields) {
|
|
113
|
-
let lookupName;
|
|
114
|
-
const newValue = doc[field];
|
|
115
|
-
if (field.endsWith('Lid')) {
|
|
116
|
-
const newlookupDoc = await mongoose.models['Lookup'].findById(newValue).lean();
|
|
117
|
-
if (newlookupDoc) {
|
|
118
|
-
lookupName = newlookupDoc.name;
|
|
119
|
-
} else {
|
|
120
|
-
lookupName = '';
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
const fieldDescription = await resolveDescription(field, doc);
|
|
124
|
-
logs.push({
|
|
125
|
-
name: fieldDescription,
|
|
126
|
-
entity: entityDescription,
|
|
127
|
-
recordId: contextId || doc._id,
|
|
128
|
-
oldValue: null,
|
|
129
|
-
newValue: lookupName || newValue,
|
|
130
|
-
createdBy: userId,
|
|
131
|
-
externalData: {
|
|
132
|
-
description: entityDescription,
|
|
133
|
-
contextId
|
|
134
|
-
}
|
|
135
|
-
});
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
logs = logs.filter(log => {
|
|
139
|
-
// Convert null/undefined to empty string for comparison
|
|
140
|
-
const oldVal = log.oldValue ?? '';
|
|
141
|
-
const newVal = log.newValue ?? '';
|
|
142
|
-
return oldVal !== newVal;
|
|
143
|
-
});
|
|
144
|
-
if (logs.length) {
|
|
145
|
-
await AuditLog.insertMany(logs);
|
|
146
|
-
await updateContextAuditCount(contextId, logs.length);
|
|
147
|
-
if (onAuditLogCreated) {
|
|
148
|
-
await onAuditLogCreated(logs, contextId);
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
// Capture the original doc before update
|
|
154
|
-
schema.pre(['findOneAndUpdate', 'findByIdAndUpdate'], async function (next) {
|
|
155
|
-
this._originalDoc = await this.model.findOne(this.getQuery()).lean();
|
|
156
|
-
next();
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
// Handle update audits
|
|
160
|
-
schema.post(['findOneAndUpdate', 'findByIdAndUpdate'], async function (result) {
|
|
161
|
-
if (!result) return;
|
|
162
|
-
|
|
163
|
-
const auditConfig = await AuditConfigModel.findOne({ collectionName });
|
|
164
|
-
if (!auditConfig?.fields?.length) return;
|
|
165
|
-
|
|
166
|
-
const update = this.getUpdate();
|
|
167
|
-
let logs = [];
|
|
168
|
-
const context = getContext();
|
|
169
|
-
const userId = context?.userId || 'anonymous';
|
|
170
|
-
const contextId = context?.contextId;
|
|
171
|
-
|
|
172
|
-
const entityDescription = await resolveEntityDescription(result, auditConfig);
|
|
173
|
-
|
|
174
|
-
for (const field of auditConfig.fields) {
|
|
175
|
-
|
|
176
|
-
const hasChanged =
|
|
177
|
-
update?.$set?.hasOwnProperty(field) || update?.hasOwnProperty(field);
|
|
178
|
-
|
|
179
|
-
if (hasChanged) {
|
|
180
|
-
const newValue = update?.$set?.[field] ?? update?.[field];
|
|
181
|
-
const oldValue = this._originalDoc ? this._originalDoc[field] : '';
|
|
182
|
-
let lookupOldName;
|
|
183
|
-
let lookupNewName;
|
|
184
|
-
if (field.endsWith('Lid')) {
|
|
185
|
-
const newlookupDoc = await mongoose.models['Lookup'].findById(newValue).lean();
|
|
186
|
-
if (newlookupDoc) {
|
|
187
|
-
|
|
188
|
-
} else {
|
|
189
|
-
|
|
190
|
-
}
|
|
191
|
-
const oldlookupDoc = await mongoose.models['Lookup'].findById(oldValue).lean();
|
|
192
|
-
if (oldlookupDoc) {
|
|
193
|
-
|
|
194
|
-
} else {
|
|
195
|
-
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
// Convert null/undefined to empty string for comparison
|
|
200
|
-
const oldVal = lookupOldName ?? oldValue ?? '';
|
|
201
|
-
const newVal = lookupNewName ?? newValue ?? '';
|
|
202
|
-
if (oldVal !== newVal) {
|
|
203
|
-
const fieldDescription = await resolveDescription(field, result);
|
|
204
|
-
logs.push({
|
|
205
|
-
name: fieldDescription,
|
|
206
|
-
entity: entityDescription,
|
|
207
|
-
recordId: contextId || result._id,
|
|
208
|
-
oldValue: oldVal,
|
|
209
|
-
newValue: newVal,
|
|
210
|
-
createdBy: userId,
|
|
211
|
-
externalData: {
|
|
212
|
-
description: entityDescription,
|
|
213
|
-
contextId: contextId || result._id
|
|
214
|
-
}
|
|
215
|
-
});
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
logs = logs.filter(log => {
|
|
220
|
-
// Convert null/undefined to empty string for comparison
|
|
221
|
-
const oldVal = log.oldValue ?? '';
|
|
222
|
-
const newVal = log.newValue ?? '';
|
|
223
|
-
return oldVal !== newVal;
|
|
224
|
-
});
|
|
225
|
-
if (logs.length) {
|
|
226
|
-
await AuditLog.insertMany(logs);
|
|
227
|
-
await updateContextAuditCount(contextId, logs.length);
|
|
228
|
-
if (onAuditLogCreated) {
|
|
229
|
-
await onAuditLogCreated(logs, contextId);
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
});
|
|
233
|
-
|
|
234
|
-
// Handle delete audits
|
|
235
|
-
schema.pre(['findOneAndDelete', 'findByIdAndDelete', 'deleteOne', 'deleteMany'], async function (next) {
|
|
236
|
-
try {
|
|
237
|
-
// Fetch the document before deletion
|
|
238
|
-
const docToDelete = await this.model.findOne(this.getQuery()).lean();
|
|
239
|
-
if (docToDelete) {
|
|
240
|
-
// Store the document for use in post middleware
|
|
241
|
-
this._docToDelete = docToDelete;
|
|
242
|
-
}
|
|
243
|
-
next();
|
|
244
|
-
} catch (error) {
|
|
245
|
-
next(error);
|
|
246
|
-
}
|
|
247
|
-
});
|
|
248
|
-
|
|
249
|
-
schema.post(['findOneAndDelete', 'findByIdAndDelete', 'deleteOne', 'deleteMany'], async function (result) {
|
|
250
|
-
if (!result) return;
|
|
251
|
-
|
|
252
|
-
const auditConfig = await AuditConfigModel.findOne({ collectionName });
|
|
253
|
-
if (!auditConfig?.trackDeletion) return;
|
|
254
|
-
|
|
255
|
-
const context = getContext();
|
|
256
|
-
const userId = context?.userId || 'anonymous';
|
|
257
|
-
const contextId = context?.contextId;
|
|
258
|
-
|
|
259
|
-
const entityDescription = await resolveEntityDescription(this._docToDelete, auditConfig);
|
|
260
|
-
const deletionDescription = await resolveDeletionDescription(this._docToDelete, auditConfig);
|
|
261
|
-
|
|
262
|
-
const log = {
|
|
263
|
-
name: deletionDescription || 'Entity Deletion',
|
|
264
|
-
entity: entityDescription,
|
|
265
|
-
recordId: contextId || deletedDoc._id,
|
|
266
|
-
oldValue: '',
|
|
267
|
-
newValue: 'Deleted',
|
|
268
|
-
createdBy: userId,
|
|
269
|
-
externalData: {
|
|
270
|
-
description: entityDescription,
|
|
271
|
-
contextId: contextId || deletedDoc._id
|
|
272
|
-
}
|
|
273
|
-
};
|
|
274
|
-
const oldVal = log.oldValue ?? '';
|
|
275
|
-
const newVal = log.newValue ?? '';
|
|
276
|
-
if (oldVal === newVal) {
|
|
277
|
-
return;
|
|
278
|
-
}
|
|
279
|
-
await AuditLog.create(log);
|
|
280
|
-
await updateContextAuditCount(contextId, 1);
|
|
281
|
-
if (onAuditLogCreated) {
|
|
282
|
-
await onAuditLogCreated([log], contextId);
|
|
283
|
-
}
|
|
284
|
-
});
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
async function updateContextAuditCount(contextId, count) {
|
|
288
|
-
count = Number(count);
|
|
289
|
-
if (!contextId || isNaN(count)) return;
|
|
290
|
-
const model = mongoose.models['Application'];
|
|
291
|
-
await model.findByIdAndUpdate(
|
|
292
|
-
{ _id: contextId },
|
|
293
|
-
{ $inc: { newAuditRecordsCount: count } },
|
|
294
|
-
{ new: true, upsert: true }
|
|
295
|
-
);
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
export function registerAuditHook(callback) {
|
|
299
|
-
onAuditLogCreated = callback;
|
|
300
|
-
}
|
|
301
|
-
|
|
1
|
+
import AuditConfigModel from '../models/audit-config.model.js';
|
|
2
|
+
import AuditLog from '../models/audit.model.js';
|
|
3
|
+
import ValueReferenceMap from '../models/value-reference-map.model.js';
|
|
4
|
+
import { getContext } from '../services/request-context.service.js';
|
|
5
|
+
import mongoose from 'mongoose';
|
|
6
|
+
|
|
7
|
+
let onAuditLogCreated = null;
|
|
8
|
+
|
|
9
|
+
// Optionally, define custom resolvers here
|
|
10
|
+
const customResolvers = {
|
|
11
|
+
// Example:
|
|
12
|
+
// expenditureRationale: (doc) => `Rationale for ${doc.type}: ${doc.rationale}`
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
// Expression-based description resolver: resolves ${fieldName} in the expression using doc, and via Lookup if needed
|
|
16
|
+
async function resolveExpressionDescription(doc, expression, resolverType) {
|
|
17
|
+
const matches = [...expression.matchAll(/\$\{([^}]+)\}/g)];
|
|
18
|
+
let result = expression;
|
|
19
|
+
for (const match of matches) {
|
|
20
|
+
const fieldName = match[1];
|
|
21
|
+
let value = doc[fieldName];
|
|
22
|
+
if (resolverType === 'lookup' && value && mongoose.models['Lookup']) {
|
|
23
|
+
const lookupDoc = await mongoose.models['Lookup'].findById(value).lean();
|
|
24
|
+
value = lookupDoc?.name || value;
|
|
25
|
+
}
|
|
26
|
+
result = result.replace(match[0], value ?? '');
|
|
27
|
+
}
|
|
28
|
+
return result;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Update resolveDescription to support expression-based descriptionField
|
|
32
|
+
async function resolveDescription(field, doc) {
|
|
33
|
+
const modelName = doc.constructor.modelName;
|
|
34
|
+
const config = await ValueReferenceMap.findOne({ field, model: modelName });
|
|
35
|
+
if (!config) return field;
|
|
36
|
+
|
|
37
|
+
if (config.descriptionField && config.descriptionField.includes('${')) {
|
|
38
|
+
return await resolveExpressionDescription(doc, config.descriptionField, config.descriptionResolverType);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
switch (config.descriptionResolverType) {
|
|
42
|
+
case 'lookup': {
|
|
43
|
+
const lookupId = doc[config.descriptionField];
|
|
44
|
+
if (!lookupId) return '';
|
|
45
|
+
const lookupDoc = await mongoose.models['Lookup'].findById(lookupId).lean();
|
|
46
|
+
return lookupDoc?.name || '';
|
|
47
|
+
}
|
|
48
|
+
case 'direct':
|
|
49
|
+
return doc[config.descriptionField] || '';
|
|
50
|
+
case 'composite':
|
|
51
|
+
return (config.descriptionFields || [])
|
|
52
|
+
.map(f => doc[f])
|
|
53
|
+
.filter(Boolean)
|
|
54
|
+
.join(' ');
|
|
55
|
+
case 'custom':
|
|
56
|
+
if (typeof customResolvers?.[config.descriptionFunction] === 'function') {
|
|
57
|
+
return await customResolvers[config.descriptionFunction](doc);
|
|
58
|
+
}
|
|
59
|
+
return '';
|
|
60
|
+
default:
|
|
61
|
+
return '';
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Update resolveEntityDescription to support expression-based descriptionField
|
|
66
|
+
async function resolveEntityDescription(doc, auditConfig) {
|
|
67
|
+
if (!auditConfig.descriptionResolutorForExternalData) return '';
|
|
68
|
+
const field = auditConfig.descriptionResolutorForExternalData;
|
|
69
|
+
const value = doc[field];
|
|
70
|
+
// Try to resolve via ValueReferenceMap
|
|
71
|
+
const map = await ValueReferenceMap.findOne({ field });
|
|
72
|
+
if (map && map.descriptionField && map.descriptionField.includes('${')) {
|
|
73
|
+
return await resolveExpressionDescription(doc, map.descriptionField, map.descriptionResolverType);
|
|
74
|
+
}
|
|
75
|
+
if (map && value) {
|
|
76
|
+
const Model = mongoose.models[map.model];
|
|
77
|
+
if (Model) {
|
|
78
|
+
const refDoc = await Model.findById(value).lean();
|
|
79
|
+
return refDoc?.[map.displayField] || value;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
// Fallback: If it's an ObjectId and Lookup model exists, resolve to name
|
|
83
|
+
if (mongoose.Types.ObjectId.isValid(value) && mongoose.models['Lookup']) {
|
|
84
|
+
const lookupDoc = await mongoose.models['Lookup'].findById(value).lean();
|
|
85
|
+
return lookupDoc?.name || value;
|
|
86
|
+
}
|
|
87
|
+
return value;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Add new function to resolve deletion description
|
|
91
|
+
async function resolveDeletionDescription(doc, auditConfig) {
|
|
92
|
+
if (!auditConfig.descriptionResolutorForDeletion) return '';
|
|
93
|
+
const expression = auditConfig.descriptionResolutorForDeletion;
|
|
94
|
+
return await resolveExpressionDescription(doc, expression, 'direct');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function applyAuditMiddleware(schema, collectionName) {
|
|
98
|
+
// Handle creation audit
|
|
99
|
+
schema.post('save', async function (doc) {
|
|
100
|
+
const auditConfig = await AuditConfigModel.findOne({ collectionName });
|
|
101
|
+
if (!auditConfig?.trackCreation) return;
|
|
102
|
+
|
|
103
|
+
const context = getContext();
|
|
104
|
+
const userId = context?.userId || 'anonymous';
|
|
105
|
+
const contextId = context?.contextId;
|
|
106
|
+
|
|
107
|
+
const entityDescription = await resolveEntityDescription(doc, auditConfig);
|
|
108
|
+
let logs = [];
|
|
109
|
+
|
|
110
|
+
// Per-field logs
|
|
111
|
+
if (auditConfig.fields?.length) {
|
|
112
|
+
for (const field of auditConfig.fields) {
|
|
113
|
+
let lookupName;
|
|
114
|
+
const newValue = doc[field];
|
|
115
|
+
if (field.endsWith('Lid')) {
|
|
116
|
+
const newlookupDoc = await mongoose.models['Lookup'].findById(newValue).lean();
|
|
117
|
+
if (newlookupDoc) {
|
|
118
|
+
lookupName = newlookupDoc.name;
|
|
119
|
+
} else {
|
|
120
|
+
lookupName = '';
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
const fieldDescription = await resolveDescription(field, doc);
|
|
124
|
+
logs.push({
|
|
125
|
+
name: fieldDescription,
|
|
126
|
+
entity: entityDescription,
|
|
127
|
+
recordId: contextId || doc._id,
|
|
128
|
+
oldValue: null,
|
|
129
|
+
newValue: lookupName || newValue,
|
|
130
|
+
createdBy: userId,
|
|
131
|
+
externalData: {
|
|
132
|
+
description: entityDescription,
|
|
133
|
+
contextId
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
logs = logs.filter(log => {
|
|
139
|
+
// Convert null/undefined to empty string for comparison
|
|
140
|
+
const oldVal = log.oldValue ?? '';
|
|
141
|
+
const newVal = log.newValue ?? '';
|
|
142
|
+
return oldVal !== newVal;
|
|
143
|
+
});
|
|
144
|
+
if (logs.length) {
|
|
145
|
+
await AuditLog.insertMany(logs);
|
|
146
|
+
await updateContextAuditCount(contextId, logs.length);
|
|
147
|
+
if (onAuditLogCreated) {
|
|
148
|
+
await onAuditLogCreated(logs, contextId);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// Capture the original doc before update
|
|
154
|
+
schema.pre(['findOneAndUpdate', 'findByIdAndUpdate'], async function (next) {
|
|
155
|
+
this._originalDoc = await this.model.findOne(this.getQuery()).lean();
|
|
156
|
+
next();
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// Handle update audits
|
|
160
|
+
schema.post(['findOneAndUpdate', 'findByIdAndUpdate'], async function (result) {
|
|
161
|
+
if (!result) return;
|
|
162
|
+
|
|
163
|
+
const auditConfig = await AuditConfigModel.findOne({ collectionName });
|
|
164
|
+
if (!auditConfig?.fields?.length) return;
|
|
165
|
+
|
|
166
|
+
const update = this.getUpdate();
|
|
167
|
+
let logs = [];
|
|
168
|
+
const context = getContext();
|
|
169
|
+
const userId = context?.userId || 'anonymous';
|
|
170
|
+
const contextId = context?.contextId;
|
|
171
|
+
|
|
172
|
+
const entityDescription = await resolveEntityDescription(result, auditConfig);
|
|
173
|
+
|
|
174
|
+
for (const field of auditConfig.fields) {
|
|
175
|
+
|
|
176
|
+
const hasChanged =
|
|
177
|
+
update?.$set?.hasOwnProperty(field) || update?.hasOwnProperty(field);
|
|
178
|
+
|
|
179
|
+
if (hasChanged) {
|
|
180
|
+
const newValue = update?.$set?.[field] ?? update?.[field];
|
|
181
|
+
const oldValue = this._originalDoc ? this._originalDoc[field] : '';
|
|
182
|
+
let lookupOldName;
|
|
183
|
+
let lookupNewName;
|
|
184
|
+
if (field.endsWith('Lid')) {
|
|
185
|
+
const newlookupDoc = await mongoose.models['Lookup'].findById(newValue).lean();
|
|
186
|
+
if (newlookupDoc) {
|
|
187
|
+
lookupNewName = newlookupDoc.name;
|
|
188
|
+
} else {
|
|
189
|
+
lookupNewName = '';
|
|
190
|
+
}
|
|
191
|
+
const oldlookupDoc = await mongoose.models['Lookup'].findById(oldValue).lean();
|
|
192
|
+
if (oldlookupDoc) {
|
|
193
|
+
lookupOldName = oldlookupDoc.name;
|
|
194
|
+
} else {
|
|
195
|
+
lookupOldName = '';
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Convert null/undefined to empty string for comparison
|
|
200
|
+
const oldVal = lookupOldName ?? oldValue ?? '';
|
|
201
|
+
const newVal = lookupNewName ?? newValue ?? '';
|
|
202
|
+
if (oldVal !== newVal) {
|
|
203
|
+
const fieldDescription = await resolveDescription(field, result);
|
|
204
|
+
logs.push({
|
|
205
|
+
name: fieldDescription,
|
|
206
|
+
entity: entityDescription,
|
|
207
|
+
recordId: contextId || result._id,
|
|
208
|
+
oldValue: oldVal,
|
|
209
|
+
newValue: newVal,
|
|
210
|
+
createdBy: userId,
|
|
211
|
+
externalData: {
|
|
212
|
+
description: entityDescription,
|
|
213
|
+
contextId: contextId || result._id
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
logs = logs.filter(log => {
|
|
220
|
+
// Convert null/undefined to empty string for comparison
|
|
221
|
+
const oldVal = log.oldValue ?? '';
|
|
222
|
+
const newVal = log.newValue ?? '';
|
|
223
|
+
return oldVal !== newVal;
|
|
224
|
+
});
|
|
225
|
+
if (logs.length) {
|
|
226
|
+
await AuditLog.insertMany(logs);
|
|
227
|
+
await updateContextAuditCount(contextId, logs.length);
|
|
228
|
+
if (onAuditLogCreated) {
|
|
229
|
+
await onAuditLogCreated(logs, contextId);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
// Handle delete audits
|
|
235
|
+
schema.pre(['findOneAndDelete', 'findByIdAndDelete', 'deleteOne', 'deleteMany'], async function (next) {
|
|
236
|
+
try {
|
|
237
|
+
// Fetch the document before deletion
|
|
238
|
+
const docToDelete = await this.model.findOne(this.getQuery()).lean();
|
|
239
|
+
if (docToDelete) {
|
|
240
|
+
// Store the document for use in post middleware
|
|
241
|
+
this._docToDelete = docToDelete;
|
|
242
|
+
}
|
|
243
|
+
next();
|
|
244
|
+
} catch (error) {
|
|
245
|
+
next(error);
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
schema.post(['findOneAndDelete', 'findByIdAndDelete', 'deleteOne', 'deleteMany'], async function (result) {
|
|
250
|
+
if (!result) return;
|
|
251
|
+
|
|
252
|
+
const auditConfig = await AuditConfigModel.findOne({ collectionName });
|
|
253
|
+
if (!auditConfig?.trackDeletion) return;
|
|
254
|
+
|
|
255
|
+
const context = getContext();
|
|
256
|
+
const userId = context?.userId || 'anonymous';
|
|
257
|
+
const contextId = context?.contextId;
|
|
258
|
+
|
|
259
|
+
const entityDescription = await resolveEntityDescription(this._docToDelete, auditConfig);
|
|
260
|
+
const deletionDescription = await resolveDeletionDescription(this._docToDelete, auditConfig);
|
|
261
|
+
|
|
262
|
+
const log = {
|
|
263
|
+
name: deletionDescription || 'Entity Deletion',
|
|
264
|
+
entity: entityDescription,
|
|
265
|
+
recordId: contextId || deletedDoc._id,
|
|
266
|
+
oldValue: '',
|
|
267
|
+
newValue: 'Deleted',
|
|
268
|
+
createdBy: userId,
|
|
269
|
+
externalData: {
|
|
270
|
+
description: entityDescription,
|
|
271
|
+
contextId: contextId || deletedDoc._id
|
|
272
|
+
}
|
|
273
|
+
};
|
|
274
|
+
const oldVal = log.oldValue ?? '';
|
|
275
|
+
const newVal = log.newValue ?? '';
|
|
276
|
+
if (oldVal === newVal) {
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
await AuditLog.create(log);
|
|
280
|
+
await updateContextAuditCount(contextId, 1);
|
|
281
|
+
if (onAuditLogCreated) {
|
|
282
|
+
await onAuditLogCreated([log], contextId);
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
async function updateContextAuditCount(contextId, count) {
|
|
288
|
+
count = Number(count);
|
|
289
|
+
if (!contextId || isNaN(count)) return;
|
|
290
|
+
const model = mongoose.models['Application'];
|
|
291
|
+
await model.findByIdAndUpdate(
|
|
292
|
+
{ _id: contextId },
|
|
293
|
+
{ $inc: { newAuditRecordsCount: count } },
|
|
294
|
+
{ new: true, upsert: true }
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
export function registerAuditHook(callback) {
|
|
299
|
+
onAuditLogCreated = callback;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
302
|
export default applyAuditMiddleware;
|
package/middlewares/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export {default as applyAuditMiddleware} from './audit.middleware.js';
|
|
2
|
-
export { registerAuditHook } from './audit.middleware.js';
|
|
1
|
+
export {default as applyAuditMiddleware} from './audit.middleware.js';
|
|
2
|
+
export { registerAuditHook } from './audit.middleware.js';
|
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
import mongoose from 'mongoose';
|
|
2
|
-
|
|
3
|
-
const auditConfigSchema = new mongoose.Schema({
|
|
4
|
-
collectionName: String,
|
|
5
|
-
fields: [String],
|
|
6
|
-
trackCreation: { type: Boolean, default: false },
|
|
7
|
-
trackDeletion: { type: Boolean, default: false },
|
|
8
|
-
descriptionResolutorForExternalData: { type: String, default: null },
|
|
9
|
-
descriptionResolutorForDeletion: { type: String, default: null },
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
const AuditConfigModel = mongoose.models.AuditConfig || mongoose.model('AuditConfig', auditConfigSchema);
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
export default AuditConfigModel;
|
|
1
|
+
import mongoose from 'mongoose';
|
|
2
|
+
|
|
3
|
+
const auditConfigSchema = new mongoose.Schema({
|
|
4
|
+
collectionName: String,
|
|
5
|
+
fields: [String],
|
|
6
|
+
trackCreation: { type: Boolean, default: false },
|
|
7
|
+
trackDeletion: { type: Boolean, default: false },
|
|
8
|
+
descriptionResolutorForExternalData: { type: String, default: null },
|
|
9
|
+
descriptionResolutorForDeletion: { type: String, default: null },
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
const AuditConfigModel = mongoose.models.AuditConfig || mongoose.model('AuditConfig', auditConfigSchema);
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
export default AuditConfigModel;
|