@dynamatix/cat-shared 0.0.81 → 0.0.83
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.
|
@@ -6,31 +6,85 @@ import mongoose from 'mongoose';
|
|
|
6
6
|
|
|
7
7
|
let onAuditLogCreated = null;
|
|
8
8
|
|
|
9
|
-
//
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
+
}
|
|
21
30
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
};
|
|
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 '';
|
|
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
|
+
}
|
|
26
63
|
}
|
|
27
64
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const
|
|
33
|
-
|
|
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;
|
|
34
88
|
}
|
|
35
89
|
|
|
36
90
|
function applyAuditMiddleware(schema, collectionName) {
|
|
@@ -43,24 +97,50 @@ function applyAuditMiddleware(schema, collectionName) {
|
|
|
43
97
|
const userId = context?.userId || 'anonymous';
|
|
44
98
|
const contextId = context?.contextId;
|
|
45
99
|
|
|
46
|
-
const
|
|
47
|
-
|
|
100
|
+
const entityDescription = await resolveEntityDescription(doc, auditConfig);
|
|
101
|
+
const logs = [];
|
|
102
|
+
|
|
103
|
+
// Per-field logs
|
|
104
|
+
if (auditConfig.fields?.length) {
|
|
105
|
+
for (const field of auditConfig.fields) {
|
|
106
|
+
const newValue = doc[field];
|
|
107
|
+
const fieldDescription = await resolveDescription(field, doc);
|
|
108
|
+
logs.push({
|
|
109
|
+
name: fieldDescription,
|
|
110
|
+
entity: entityDescription,
|
|
111
|
+
recordId: contextId || doc._id,
|
|
112
|
+
oldValue: null,
|
|
113
|
+
newValue,
|
|
114
|
+
createdBy: userId,
|
|
115
|
+
externalData: {
|
|
116
|
+
description: entityDescription,
|
|
117
|
+
contextId
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Overall document creation log (optional)
|
|
124
|
+
logs.push({
|
|
125
|
+
name: 'created',
|
|
126
|
+
entity: entityDescription,
|
|
48
127
|
recordId: contextId || doc._id,
|
|
49
128
|
oldValue: null,
|
|
50
129
|
newValue: 'Document created',
|
|
51
130
|
createdBy: userId,
|
|
52
|
-
contextId
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
}
|
|
59
|
-
await AuditLog.create(log);
|
|
60
|
-
await updateContextAuditCount(contextId, 1);
|
|
131
|
+
contextId,
|
|
132
|
+
externalData: {
|
|
133
|
+
description: entityDescription,
|
|
134
|
+
contextId
|
|
135
|
+
}
|
|
136
|
+
});
|
|
61
137
|
|
|
62
|
-
if (
|
|
63
|
-
await
|
|
138
|
+
if (logs.length) {
|
|
139
|
+
await AuditLog.insertMany(logs);
|
|
140
|
+
await updateContextAuditCount(contextId, logs.length);
|
|
141
|
+
if (onAuditLogCreated) {
|
|
142
|
+
await onAuditLogCreated(logs, contextId);
|
|
143
|
+
}
|
|
64
144
|
}
|
|
65
145
|
});
|
|
66
146
|
|
|
@@ -83,6 +163,8 @@ function applyAuditMiddleware(schema, collectionName) {
|
|
|
83
163
|
const userId = context?.userId || 'anonymous';
|
|
84
164
|
const contextId = context?.contextId;
|
|
85
165
|
|
|
166
|
+
const entityDescription = await resolveEntityDescription(result, auditConfig);
|
|
167
|
+
|
|
86
168
|
for (const field of auditConfig.fields) {
|
|
87
169
|
const hasChanged =
|
|
88
170
|
update?.$set?.hasOwnProperty(field) || update?.hasOwnProperty(field);
|
|
@@ -92,22 +174,18 @@ function applyAuditMiddleware(schema, collectionName) {
|
|
|
92
174
|
const oldValue = this._originalDoc ? this._originalDoc[field] : '';
|
|
93
175
|
|
|
94
176
|
if (oldValue !== newValue) {
|
|
95
|
-
|
|
96
|
-
const { oldValue: resolvedOld, newValue: resolvedNew } =
|
|
97
|
-
await resolveAuditValues(field, oldValue, newValue);
|
|
98
|
-
|
|
99
|
-
let externalData = {};
|
|
100
|
-
if (auditConfig.descriptionResolutorForExternalData) {
|
|
101
|
-
externalData.description = await resolveExternalData(auditConfig.descriptionResolutorForExternalData, result[auditConfig.descriptionResolutorForExternalData])
|
|
102
|
-
externalData.contextId = contextId || result._id;
|
|
103
|
-
}
|
|
177
|
+
const fieldDescription = await resolveDescription(field, result);
|
|
104
178
|
logs.push({
|
|
105
|
-
name:
|
|
179
|
+
name: fieldDescription,
|
|
180
|
+
entity: entityDescription,
|
|
106
181
|
recordId: contextId || result._id,
|
|
107
|
-
oldValue
|
|
108
|
-
newValue
|
|
182
|
+
oldValue,
|
|
183
|
+
newValue,
|
|
109
184
|
createdBy: userId,
|
|
110
|
-
externalData
|
|
185
|
+
externalData: {
|
|
186
|
+
description: entityDescription,
|
|
187
|
+
contextId: contextId || result._id
|
|
188
|
+
}
|
|
111
189
|
});
|
|
112
190
|
}
|
|
113
191
|
}
|
|
@@ -138,4 +216,4 @@ export function registerAuditHook(callback) {
|
|
|
138
216
|
onAuditLogCreated = callback;
|
|
139
217
|
}
|
|
140
218
|
|
|
141
|
-
export default applyAuditMiddleware;
|
|
219
|
+
export default applyAuditMiddleware;
|
|
@@ -4,6 +4,10 @@ const valueReferenceMapSchema = new mongoose.Schema({
|
|
|
4
4
|
field: String, // e.g. 'documentTypeId', 'createdBy', 'status'
|
|
5
5
|
model: String, // e.g. 'DocumentType', 'User', 'ApplicationStatus'
|
|
6
6
|
displayField: String, // e.g. 'name', 'fullName', 'label'
|
|
7
|
+
descriptionResolverType: { type: String, enum: ['direct', 'lookup', 'composite', 'custom'], default: 'direct' },
|
|
8
|
+
descriptionField: String, // for direct/lookup
|
|
9
|
+
descriptionFields: [String], // for composite
|
|
10
|
+
descriptionFunction: String // for custom
|
|
7
11
|
});
|
|
8
12
|
|
|
9
13
|
valueReferenceMapSchema.index({ field: 1 }, { unique: true });
|