data-primals-engine 1.6.2 → 1.6.3

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.
@@ -1,13 +1,12 @@
1
1
  import {getCollection, getCollectionForUser, isObjectId} from "./mongodb.js";
2
2
  import schedule from "node-schedule";
3
3
  import {ObjectId} from "mongodb";
4
- import crypto from "node:crypto";
5
4
 
6
5
  import ivm from 'isolated-vm';
7
6
 
8
7
  import {Logger} from "../gameObject.js";
9
8
  import {deleteData, getModel, insertData, patchData, searchData} from "./data/index.js";
10
- import { maxExecutionsByStep, maxWorkflowSteps, port} from "../constants.js";
9
+ import { maxExecutionsByStep, maxWorkflowSteps } from "../constants.js";
11
10
  import {ChatPromptTemplate} from "@langchain/core/prompts";
12
11
  import i18n from "../../src/i18n.js";
13
12
  import {sendEmail} from "../email.js";
@@ -16,11 +15,10 @@ import * as workflowModule from './workflow.js';
16
15
  import {isConditionMet} from "../filter.js";
17
16
  import { services } from '../services/index.js';
18
17
  import {getEnv, getSmtpConfig} from "./user.js";
19
- import {getHost} from "../constants.js";
20
18
  import { providers } from "./assistant/constants.js";
21
19
  import { getAIProvider } from "./assistant/providers.js";
22
- import {parseSafeJSON, safeAssignObject} from "../core.js";
23
20
  import {Config} from "../config.js";
21
+ import {substituteVariables} from "../filter.js";
24
22
 
25
23
  let logger = null;
26
24
  export async function onInit(defaultEngine) {
@@ -29,6 +27,7 @@ export async function onInit(defaultEngine) {
29
27
  await scheduleWorkflowTriggers();
30
28
  }
31
29
 
30
+ export { substituteVariables } from "../filter.js";
32
31
 
33
32
  /**
34
33
  * Déclenche un workflow par son nom et lui passe des données de contexte.
@@ -969,42 +968,6 @@ export async function executeStepAction(actionDef, contextData, user, dbCollecti
969
968
  return { success: false, message: error.message || 'Action execution failed' };
970
969
  }
971
970
  }
972
- /**
973
- * Récupère une valeur imbriquée dans un objet en utilisant une chaîne de chemin.
974
- * Gère les tableaux et les objets. Retourne undefined si le chemin n'est pas trouvé.
975
- * Exemple: getNestedValue({ a: { b: [ { c: 1 } ] } }, 'a.b.0.c') -> 1
976
- *
977
- * @param {object} obj L'objet source.
978
- * @param {string} path La chaîne de chemin (ex: 'user.address.city').
979
- * @returns {*} La valeur trouvée ou undefined.
980
- */
981
- function getNestedValue(obj, path) {
982
- // Vérifie si l'objet ou le chemin est invalide
983
- if (!obj || typeof path !== 'string') {
984
- return undefined;
985
- }
986
- // Sépare le chemin en clés individuelles (ex: 'a.b.0.c' -> ['a', 'b', '0', 'c'])
987
- const keys = path.split('.');
988
- let current = obj; // Commence à la racine de l'objet
989
-
990
- // Parcourt chaque clé dans le chemin
991
- for (const key of keys) {
992
- // Si à un moment donné on atteint null ou undefined, le chemin est invalide
993
- if (current === null || current === undefined) {
994
- return undefined;
995
- }
996
- // Récupère la valeur pour la clé actuelle
997
- const value = current[key];
998
- // Si la valeur est undefined, le chemin est invalide
999
- if (value === undefined) {
1000
- return undefined;
1001
- }
1002
- // Passe au niveau suivant de l'objet/tableau
1003
- current = value;
1004
- }
1005
- // Retourne la valeur finale trouvée
1006
- return current;
1007
- }
1008
971
 
1009
972
  /**
1010
973
  * Résout un chemin de variable complexe (ex: "triggerData.order.customer.contact.email")
@@ -1120,133 +1083,6 @@ async function resolvePathValue(pathString, initialContext, user) {
1120
1083
  return finalValue;
1121
1084
  }
1122
1085
 
1123
- /**
1124
- * Remplace les placeholders dans un template (string, object, array) par des valeurs du contextData.
1125
- * Version améliorée avec support des chemins complexes via resolvePathValue.
1126
- */
1127
- export async function substituteVariables(template, contextData, user) {
1128
- // 1. Retourner les types non substituables tels quels
1129
- if (template === null || (typeof template !== 'string' && typeof template !== 'object')) {
1130
- return template;
1131
- }
1132
-
1133
- // 2. Gérer les tableaux de manière récursive
1134
- if (Array.isArray(template)) {
1135
- return Promise.all(template.map(item => substituteVariables(item, contextData, user)));
1136
- }
1137
-
1138
- // 3. Gérer les objets de manière récursive
1139
- if (typeof template === 'object') {
1140
- const newObj = {};
1141
- for (const key in template) {
1142
- if (Object.prototype.hasOwnProperty.call(template, key)) {
1143
- const val = await substituteVariables(template[key], contextData, user);
1144
- safeAssignObject(newObj, key, val);
1145
- }
1146
- }
1147
- return newObj;
1148
- }
1149
-
1150
- // --- À partir d'ici, nous savons que `template` est une chaîne de caractères ---
1151
-
1152
- // 4. Construire le contexte complet pour la substitution
1153
- const dbCollection = await getCollectionForUser(user);
1154
- const userEnvVars = await dbCollection.find({ _model: 'env', _user: user.username }).toArray();
1155
- const userEnv = userEnvVars.reduce((acc, v) => ({ ...acc, [v.name]: v.value }), {});
1156
-
1157
- // `contextToSearch` contient toutes les données disponibles à sa racine
1158
- const contextToSearch = { ...contextData, env: userEnv };
1159
-
1160
- // 5. Logique de résolution de valeur améliorée avec resolvePathValue
1161
- const findValue = async (key) => {
1162
- let path = key.trim();
1163
- if (path.startsWith('context.')) {
1164
- path = path.substring('context.'.length);
1165
- }
1166
- if (path.endsWith('._id')) {
1167
- const basePath = path.slice(0, -4);
1168
- const value = await findValue(basePath);
1169
- return value?._id?.toString(); // Convertit l'ObjectId en string
1170
- }
1171
-
1172
- // Gérer les valeurs dynamiques spéciales
1173
- if (path === 'now') {
1174
- return new Date().toISOString();
1175
- } else if (path === 'randomUUID') {
1176
- return crypto.randomUUID();
1177
- } else if( path === "baseUrl" ){
1178
- return process.env.NODE_ENV === 'production' ? 'https://'+getHost()+'/' : 'http://localhost:/'+port;
1179
- }
1180
-
1181
- // Détecter si le chemin est complexe (contient plus d'un point)
1182
- if (path.split('.').length > 1) {
1183
- try {
1184
- // Essayer de résoudre le chemin avec resolvePathValue
1185
- const [root, ...rest] = path.split('.');
1186
- // On vérifie si la racine du chemin (ex: 'triggerData') existe dans notre contexte
1187
- if (contextToSearch[root]) {
1188
- const resolvedValue = await resolvePathValue(
1189
- rest.join('.'),
1190
- contextToSearch[root], // On passe le bon objet de départ (ex: l'objet triggerData)
1191
- user
1192
- );
1193
- if (resolvedValue !== undefined) {
1194
- return resolvedValue;
1195
- }
1196
- }
1197
- } catch (error) {
1198
- console.warn(`Erreur lors de la résolution du chemin "${path}":`, error.message);
1199
- // On continue avec la méthode normale si la résolution échoue
1200
- }
1201
- }
1202
-
1203
- // Fallback: chercher le chemin dans l'objet de contexte normal
1204
- return getNestedValue(contextToSearch, path);
1205
- };
1206
-
1207
- // CAS A : La chaîne est un unique placeholder (ex: "{context.triggerData.product.price}")
1208
- const singlePlaceholderMatch = template.match(/^\{([^}]+)\}$/);
1209
- if (singlePlaceholderMatch) {
1210
- const key = singlePlaceholderMatch[1];
1211
- const value = await findValue(key);
1212
-
1213
- if (value === undefined) {
1214
- return template; // Placeholder not found, return as is.
1215
- }
1216
-
1217
- // If the resolved value is a string, it might contain more placeholders.
1218
- // We recursively call substituteVariables on it, but only if it's different
1219
- // from the original template to prevent infinite loops.
1220
- if (typeof value === 'string' && value !== template) {
1221
- return substituteVariables(value, contextData, user);
1222
- }
1223
-
1224
- // For non-string values or if value is same as template, return the value.
1225
- return value;
1226
- }
1227
-
1228
- // CAS B : La chaîne contient plusieurs placeholders ou mix texte/variables
1229
- const placeholderRegex = /\{([^}]+)\}/g;
1230
- const placeholders = [...template.matchAll(placeholderRegex)];
1231
-
1232
- // Si aucun placeholder trouvé, retourner la chaîne telle quelle
1233
- if (placeholders.length === 0) {
1234
- return template;
1235
- }
1236
-
1237
- // Remplacer chaque placeholder de manière asynchrone
1238
- let result = template;
1239
- for (const [match, key] of placeholders) {
1240
- const value = await findValue(key);
1241
- const replacement = value !== undefined
1242
- ? (value === null ? 'null' : typeof value === 'object' ? JSON.stringify(value) : String(value))
1243
- : match;
1244
- result = result.replace(match, replacement);
1245
- }
1246
-
1247
- return result;
1248
- }
1249
-
1250
1086
  /**
1251
1087
  * Triggers the instantiation of a workflowRun if conditions are met.
1252
1088
  * Checks the event type and trigger's data filter.
@@ -1401,6 +1237,7 @@ export async function processWorkflowRun(workflowRunId, user) {
1401
1237
  let currentRunState;
1402
1238
  let contextData = {};
1403
1239
  let stepExecutionsCount = {};
1240
+ let history = [];
1404
1241
 
1405
1242
  try {
1406
1243
  currentRunState = await dbCollection.findOne({ _id: runId, _model: 'workflowRun' });
@@ -1411,6 +1248,7 @@ export async function processWorkflowRun(workflowRunId, user) {
1411
1248
  }
1412
1249
 
1413
1250
  stepExecutionsCount = currentRunState.stepExecutionsCount || {};
1251
+ history = currentRunState.history || [];
1414
1252
  if (['completed', 'failed', 'cancelled'].includes(currentRunState.status)) {
1415
1253
  logger.info(`[processWorkflowRun] WorkflowRun ID: ${runId} is already in a terminal state (${currentRunState.status}). Skipping.`);
1416
1254
  return;
@@ -1420,7 +1258,7 @@ export async function processWorkflowRun(workflowRunId, user) {
1420
1258
  logger.error(error);
1421
1259
  await dbCollection.updateOne(
1422
1260
  { _id: runId },
1423
- { $set: { status: 'failed', error, completedAt: new Date(), stepExecutionsCount } }
1261
+ { $set: { status: 'failed', error, completedAt: new Date(), stepExecutionsCount, history } }
1424
1262
  );
1425
1263
  };
1426
1264
 
@@ -1437,7 +1275,7 @@ export async function processWorkflowRun(workflowRunId, user) {
1437
1275
  const errorMessage = workflowDefinition.startStep ? 'No valid starting step defined in workflow or run state.' : null;
1438
1276
  await dbCollection.updateOne(
1439
1277
  { _id: runId },
1440
- { $set: { status: finalStatus, error: errorMessage, completedAt: new Date(), currentStep: null, stepExecutionsCount } }
1278
+ { $set: { status: finalStatus, error: errorMessage, completedAt: new Date(), currentStep: null, stepExecutionsCount, history } }
1441
1279
  );
1442
1280
  return;
1443
1281
  }
@@ -1462,10 +1300,13 @@ export async function processWorkflowRun(workflowRunId, user) {
1462
1300
  return await logError(`Step definition ID: ${currentStepId} not found.`);
1463
1301
  }
1464
1302
 
1465
- await dbCollection.updateOne(
1466
- { _id: runId },
1467
- { $set: { status: 'running', currentStep: currentStepId, contextData, stepExecutionsCount } }
1468
- );
1303
+ const stepHistoryEntry = {
1304
+ stepId: currentStepId.toString(),
1305
+ stepName: currentStepDef.name,
1306
+ executedAt: new Date(),
1307
+ status: 'pending',
1308
+ actions: []
1309
+ };
1469
1310
 
1470
1311
  let stepSucceeded = true;
1471
1312
  let logInfo = null;
@@ -1502,6 +1343,14 @@ export async function processWorkflowRun(workflowRunId, user) {
1502
1343
  for (const actionId of currentStepDef.actions) {
1503
1344
  if (!isObjectId(actionId)) continue;
1504
1345
  const actionDef = await dbCollection.findOne({ _id: new ObjectId(actionId), _model: 'workflowAction' });
1346
+ const actionHistoryEntry = {
1347
+ actionId: actionId.toString(),
1348
+ actionName: actionDef.name,
1349
+ actionType: actionDef.type,
1350
+ executedAt: new Date(),
1351
+ status: 'pending'
1352
+ };
1353
+
1505
1354
  if (!actionDef) return await logError(`Action definition ${actionId} not found.`);
1506
1355
  const actionResult = await workflowModule.executeStepAction(actionDef, contextData, user, dbCollection);
1507
1356
 
@@ -1540,9 +1389,15 @@ export async function processWorkflowRun(workflowRunId, user) {
1540
1389
  if (!actionResult.success) {
1541
1390
  stepSucceeded = false;
1542
1391
  logInfo = actionResult.message || `Action ${actionDef.name || actionId} failed.`;
1392
+ actionHistoryEntry.status = 'failed';
1393
+ actionHistoryEntry.result = logInfo;
1394
+ stepHistoryEntry.actions.push(actionHistoryEntry);
1543
1395
  break;
1544
1396
  }else{
1545
1397
  logInfo = `Action ${actionDef.name || actionId} : ${actionResult.message}`;
1398
+ actionHistoryEntry.status = 'success';
1399
+ actionHistoryEntry.result = actionResult.message;
1400
+ stepHistoryEntry.actions.push(actionHistoryEntry);
1546
1401
  }
1547
1402
  if (actionResult.updatedContext) {
1548
1403
  contextData = { ...contextData, ...actionResult.updatedContext };
@@ -1551,6 +1406,10 @@ export async function processWorkflowRun(workflowRunId, user) {
1551
1406
  logger.info(`[processWorkflowRun] Run ID: ${runId}, Step ID: ${currentStepId}, Action ID: ${actionId}: Executed successfully.`);
1552
1407
  }
1553
1408
  }
1409
+ if (stepSucceeded) {
1410
+ stepHistoryEntry.status = 'success';
1411
+ stepHistoryEntry.result = 'Step actions completed successfully.';
1412
+ }
1554
1413
  } else {
1555
1414
  logger.info(`[processWorkflowRun] Run ID: ${runId}, Step ID: ${currentStepId}: Conditions not met. Skipping actions.`);
1556
1415
  }
@@ -1558,6 +1417,8 @@ export async function processWorkflowRun(workflowRunId, user) {
1558
1417
  logger.error(`[processWorkflowRun] Run ID: ${runId}, Step ID: ${currentStepId}: Error during condition/action execution: ${error.message}`);
1559
1418
  stepSucceeded = false;
1560
1419
  logInfo = error.message;
1420
+ stepHistoryEntry.status = 'failed';
1421
+ stepHistoryEntry.result = logInfo;
1561
1422
  }
1562
1423
 
1563
1424
  // --- 9. Détermination de la prochaine étape ---
@@ -1587,7 +1448,7 @@ export async function processWorkflowRun(workflowRunId, user) {
1587
1448
 
1588
1449
  // --- 10. Mise à jour de l'état de l'exécution ---
1589
1450
  currentStepId = nextStepId;
1590
- const updatePayload = { contextData };
1451
+ const updatePayload = { contextData, stepExecutionsCount }; // <-- CORRECTION: Always include stepExecutionsCount
1591
1452
 
1592
1453
  if (finalStatusForRun) {
1593
1454
  updatePayload.status = finalStatusForRun;
@@ -1597,7 +1458,21 @@ export async function processWorkflowRun(workflowRunId, user) {
1597
1458
  } else {
1598
1459
  updatePayload.currentStep = currentStepId;
1599
1460
  }
1600
- await dbCollection.updateOne({ _id: runId }, { $set: updatePayload });
1461
+
1462
+ // Update the last history entry with the final status and actions
1463
+ // We use a pipeline to reliably update the last element of the history array.
1464
+ // --- FIX ---
1465
+ // The previous logic using $slice failed when the history array had only one element.
1466
+ // This new logic uses a direct update on the last element of the array, which is more robust.
1467
+ await dbCollection.updateOne(
1468
+ { _id: runId }, {
1469
+ $set: updatePayload,
1470
+ $push: {
1471
+ history: stepHistoryEntry
1472
+ }
1473
+ }
1474
+ );
1475
+
1601
1476
 
1602
1477
  if(finalStatusForRun) {
1603
1478
  logger.info(`[processWorkflowRun] Finished processing for workflowRun ID: ${runId}. Final Status: ${finalStatusForRun}`);
@@ -1607,7 +1482,7 @@ export async function processWorkflowRun(workflowRunId, user) {
1607
1482
  logger.error(`[processWorkflowRun] Critical error during processing of workflowRun ID: ${runId}. Error: ${error.message}`, error.stack);
1608
1483
  await dbCollection.updateOne(
1609
1484
  { _id: runId, status: { $nin: ['completed', 'failed', 'cancelled'] } },
1610
- { $set: { status: 'failed', log: `Critical error: ${error.message}`, completedAt: new Date(), stepExecutionsCount } }
1485
+ { $set: { status: 'failed', log: `Critical error: ${error.message}`, completedAt: new Date(), stepExecutionsCount, history } }
1611
1486
  );
1612
1487
  }
1613
1488
  }
package/src/providers.js CHANGED
@@ -104,7 +104,7 @@ export class UserProvider {
104
104
  }
105
105
 
106
106
  async hasFeature(user, feature) {
107
- return this.plans[user?.userPlan]?.features.some(f => f === feature);
107
+ return this.plans[user?.userPlan ?? 'free']?.features.some(f => f === feature);
108
108
  }
109
109
 
110
110
  // Ajoutez ici d'autres méthodes nécessaires : findUserById, createUser, etc.
@@ -1,10 +1,10 @@
1
1
  // __tests__/data.history.integration.test.js
2
2
  import { ObjectId } from 'mongodb';
3
3
  import { expect, describe, it, beforeAll, afterAll, beforeEach } from 'vitest';
4
- import { handleGetHistoryRequest } from '../src/modules/data/data.history.js';
4
+ import { handleGetHistoryRequest, handleGetRevisionRequest, handleRevertToRevisionRequest } from '../src/modules/data/data.history.js';
5
5
  import { Config } from '../src/config.js';
6
6
  import { sleep } from '../src/core.js';
7
- import { insertData, editData, deleteModels } from '../src/index.js';
7
+ import { insertData, editData, deleteData } from '../src/index.js';
8
8
  import { getCollection, getCollectionForUser } from '../src/modules/mongodb.js';
9
9
  import { generateUniqueName, initEngine } from "../src/setenv.js";
10
10
  import {purgeData} from "../src/modules/data/data.history.js";
@@ -16,6 +16,20 @@ let historyCollection;
16
16
  let datasCollection;
17
17
  let modelsCollection;
18
18
 
19
+ // Mock Express req/res objects for testing API handlers
20
+ const mockReq = (params = {}, query = {}, body = {}, user = testUser) => ({
21
+ params,
22
+ query,
23
+ body,
24
+ me: user
25
+ });
26
+ const mockRes = () => {
27
+ const res = {};
28
+ res.status = (code) => { res.statusCode = code; return res; };
29
+ res.json = (data) => { res.body = data; return res; };
30
+ return res;
31
+ };
32
+
19
33
  describe('Data History Module Integration Tests', () => {
20
34
 
21
35
  beforeAll(async () => {
@@ -192,6 +206,126 @@ describe('Data History Module Integration Tests', () => {
192
206
  expect(historyCount).toBe(1);
193
207
  });
194
208
 
209
+ it('should create a snapshot history record on document deletion', async () => {
210
+ // 1. Setup model
211
+ const modelName = generateUniqueName('productHistoryDelete');
212
+ const productModelDef = {
213
+ name: modelName,
214
+ description:'',
215
+ _user: testUser.username,
216
+ history: { enabled: true },
217
+ fields: [{ name: 'name', type: 'string' }]
218
+ };
219
+ await modelsCollection.insertOne(productModelDef);
220
+
221
+ // 2. Insert a document
222
+ const insertResult = await insertData(modelName, { name: 'Document to be deleted' }, {}, testUser);
223
+ const docId = new ObjectId(insertResult.data._id);
224
+
225
+ // 3. Delete the document
226
+ await deleteData(modelName, [docId.toString()], testUser);
227
+
228
+ // 4. Verify the new history record (v2)
229
+ const historyRecord = await historyCollection.findOne({ documentId: docId, version: 2 });
230
+
231
+ expect(historyRecord).not.toBeNull();
232
+ expect(historyRecord.operation).toBe('delete');
233
+ expect(historyRecord.snapshot).not.toBeNull();
234
+ expect(historyRecord.snapshot.name).toBe('Document to be deleted');
235
+ expect(historyRecord.changes).toBeUndefined();
236
+ });
237
+
238
+ it('should correctly reconstruct a document at a specific version', async () => {
239
+ // 1. Setup model
240
+ const modelName = generateUniqueName('productHistoryReconstruct');
241
+ const productModelDef = {
242
+ name: modelName,
243
+ _user: testUser.username,
244
+ description:'',
245
+ history: { enabled: true },
246
+ fields: [
247
+ { name: 'name', type: 'string' },
248
+ { name: 'status', type: 'string' },
249
+ { name: 'count', type: 'number' }
250
+ ]
251
+ };
252
+ await modelsCollection.insertOne(productModelDef);
253
+
254
+ // 2. Create and update document to generate history
255
+ const insertResult = await insertData(modelName, { name: 'Reconstruct', status: 'initial', count: 0 }, {}, testUser); // v1
256
+ const docId = new ObjectId(insertResult.data._id);
257
+
258
+ await editData(modelName, { _id: docId }, { status: 'updated' }, {}, testUser); // v2
259
+ await editData(modelName, { _id: docId }, { count: 10 }, {}, testUser); // v3
260
+
261
+ // 3. Test reconstruction at each version via the API handler
262
+ // Version 1 (creation)
263
+ let req = mockReq({ modelName, recordId: docId.toString(), version: '1' });
264
+ let res = mockRes();
265
+ await handleGetRevisionRequest(req, res);
266
+ expect(res.body.success).toBe(true);
267
+ expect(res.body.data.name).toBe('Reconstruct');
268
+ expect(res.body.data.status).toBe('initial');
269
+ expect(res.body.data.count).toBe(0);
270
+
271
+ // Version 2 (status updated)
272
+ req = mockReq({ modelName, recordId: docId.toString(), version: '2' });
273
+ res = mockRes();
274
+ await handleGetRevisionRequest(req, res);
275
+ expect(res.body.success).toBe(true);
276
+ expect(res.body.data.status).toBe('updated');
277
+ expect(res.body.data.count).toBe(0); // count is still 0
278
+
279
+ // Version 3 (count updated)
280
+ req = mockReq({ modelName, recordId: docId.toString(), version: '3' });
281
+ res = mockRes();
282
+ await handleGetRevisionRequest(req, res);
283
+ expect(res.body.success).toBe(true);
284
+ expect(res.body.data.status).toBe('updated');
285
+ expect(res.body.data.count).toBe(10);
286
+ });
287
+
288
+ it('should revert a document to a previous version and create a new history entry', async () => {
289
+ // 1. Setup model
290
+ const modelName = generateUniqueName('productHistoryRevert');
291
+ const productModelDef = {
292
+ name: modelName,
293
+ _user: testUser.username,
294
+ description:'',
295
+ history: { enabled: true },
296
+ fields: [{ name: 'name', type: 'string' }, { name: 'status', type: 'string' }]
297
+ };
298
+ await modelsCollection.insertOne(productModelDef);
299
+
300
+ // 2. Create and update document
301
+ const insertResult = await insertData(modelName, { name: 'Revert Test', status: 'v1' }, {}, testUser); // v1
302
+ const docId = new ObjectId(insertResult.data._id);
303
+ await editData(modelName, { _id: docId }, { status: 'v2' }, {}, testUser); // v2
304
+
305
+ // 3. Revert the document to version 1
306
+ const req = mockReq({ modelName, recordId: docId.toString(), version: '1' });
307
+ const res = mockRes();
308
+ await handleRevertToRevisionRequest(req, res);
309
+
310
+ expect(res.body.success).toBe(true);
311
+ expect(res.body.message).toBe("Document successfully reverted.");
312
+
313
+ // 4. Verify the current state of the document
314
+ const revertedDoc = await datasCollection.findOne({ _id: docId });
315
+ expect(revertedDoc.status).toBe('v1');
316
+
317
+ // 5. Verify that a new history entry (v3) was created for the revert action
318
+ const historyRecords = await historyCollection.find({ documentId: docId }).sort({ version: 1 }).toArray();
319
+ expect(historyRecords).toHaveLength(3);
320
+
321
+ const revertHistoryEntry = historyRecords[2]; // v3
322
+ expect(revertHistoryEntry.version).toBe(3);
323
+ expect(revertHistoryEntry.operation).toBe('update'); // A revert is an update
324
+ expect(revertHistoryEntry.changes).not.toBeNull();
325
+ expect(revertHistoryEntry.changes.status.from).toBe('v2');
326
+ expect(revertHistoryEntry.changes.status.to).toBe('v1');
327
+ });
328
+
195
329
  it('should filter history records by date range', async () => {
196
330
  // 1. Setup model
197
331
  const modelName = generateUniqueName('productHistoryDateFilter');
@@ -227,21 +361,11 @@ describe('Data History Module Integration Tests', () => {
227
361
  });
228
362
 
229
363
  // Mock Express req/res objects
230
- const mockReq = (query) => ({
231
- params: { modelName, recordId: docId.toString() },
232
- query,
233
- me: testUser
234
- });
235
- const mockRes = () => {
236
- const res = {};
237
- res.status = (code) => { res.statusCode = code; return res; };
238
- res.json = (data) => { res.body = data; return res; };
239
- return res;
240
- };
364
+ // Using the global mockReq and mockRes helpers now
241
365
 
242
366
  // 4. Test cases
243
367
  // Case A: Filter for February
244
- let req = mockReq({ startDate: '2023-02-01', endDate: '2023-02-28' });
368
+ let req = mockReq({ modelName, recordId: docId.toString() }, { startDate: '2023-02-01', endDate: '2023-02-28' });
245
369
  let res = mockRes();
246
370
  await handleGetHistoryRequest(req, res);
247
371
  expect(res.body.success).toBe(true);
@@ -249,14 +373,14 @@ describe('Data History Module Integration Tests', () => {
249
373
  expect(res.body.data.map(d => d._v).sort()).toEqual([2, 3]);
250
374
 
251
375
  // Case B: Filter starting from Feb 15th
252
- req = mockReq({ startDate: '2023-02-15' });
376
+ req = mockReq({ modelName, recordId: docId.toString() }, { startDate: '2023-02-15' });
253
377
  res = mockRes();
254
378
  await handleGetHistoryRequest(req, res);
255
379
  expect(res.body.success).toBe(true);
256
380
  expect(res.body.count).toBe(3); // v2, v3, v4
257
381
 
258
382
  // Case C: Filter up to Feb 15th (inclusive)
259
- req = mockReq({ endDate: '2023-02-15' });
383
+ req = mockReq({ modelName, recordId: docId.toString() }, { endDate: '2023-02-15' });
260
384
  res = mockRes();
261
385
  await handleGetHistoryRequest(req, res);
262
386
  expect(res.body.success).toBe(true);