@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
- // Generic resolver: fetch display values using ValueReferenceMap
10
- async function resolveAuditValues(field, oldValue, newValue) {
11
- const map = await ValueReferenceMap.findOne({ field });
12
- if (!map || (!oldValue && !newValue)) return { oldValue, newValue };
13
-
14
- const Model = mongoose.models[map.model];
15
- if (!Model) return { oldValue, newValue };
16
-
17
- const [oldDoc, newDoc] = await Promise.all([
18
- oldValue ? Model.findById(oldValue).lean() : null,
19
- newValue ? Model.findById(newValue).lean() : null
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
- return {
23
- oldValue: oldDoc?.[map.displayField] || oldValue,
24
- newValue: newDoc?.[map.displayField] || newValue
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
- async function resolveExternalData(descriptionResolutorForExternalData, recordId) {
29
- const map = await ValueReferenceMap.findOne({ field: descriptionResolutorForExternalData });
30
- const Model = mongoose.models[map.model];
31
- if (!Model) return "";
32
- const doc = await Model.findById(recordId).lean()
33
- return doc[map.displayField];
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 log = {
47
- name: `${collectionName} created`,
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
- log.externalData = {};
55
- if (auditConfig.descriptionResolutorForExternalData) {
56
- log.externalData.description = await resolveExternalData(auditConfig.descriptionResolutorForExternalData, doc[auditConfig.descriptionResolutorForExternalData])
57
- log.externalData.contextId = contextId;
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 (onAuditLogCreated) {
63
- await onAuditLogCreated([log], contextId);
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
- // Resolve human-readable values
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: `${collectionName}.${field}`,
179
+ name: fieldDescription,
180
+ entity: entityDescription,
106
181
  recordId: contextId || result._id,
107
- oldValue: resolvedOld,
108
- newValue: resolvedNew,
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 });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dynamatix/cat-shared",
3
- "version": "0.0.81",
3
+ "version": "0.0.83",
4
4
  "main": "index.js",
5
5
  "scripts": {
6
6
  "test": "echo \"Error: no test specified\" && exit 1"