data-primals-engine 1.6.2 → 1.6.5

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.
Files changed (39) hide show
  1. package/README.md +56 -911
  2. package/client/package-lock.json +64 -50
  3. package/client/package.json +6 -0
  4. package/client/src/App.scss +29 -0
  5. package/client/src/Dashboard.jsx +1 -1
  6. package/client/src/WorkflowEditor.jsx +101 -0
  7. package/client/src/WorkflowEditor.scss +29 -4
  8. package/client/src/_variables.scss +3 -0
  9. package/doc/automation-workflows.md +102 -0
  10. package/doc/core-concepts.md +33 -0
  11. package/doc/custom-api-endpoints.md +40 -0
  12. package/doc/dashboards-kpis-charts.md +49 -0
  13. package/doc/data-management.md +120 -0
  14. package/doc/data-models.md +75 -0
  15. package/doc/index.md +14 -0
  16. package/doc/roles-permissions.md +43 -0
  17. package/doc/users.md +30 -0
  18. package/package.json +7 -5
  19. package/src/client.js +6 -4
  20. package/src/core.js +31 -12
  21. package/src/defaultModels +1628 -0
  22. package/src/defaultModels.js +14 -0
  23. package/src/filter.js +110 -42
  24. package/src/modules/data/data.history.js +5 -1
  25. package/src/modules/data/data.js +2 -1
  26. package/src/modules/data/data.operations.js +116 -72
  27. package/src/modules/data/data.relations.js +1 -1
  28. package/src/modules/mongodb.js +2 -1
  29. package/src/modules/swagger.js +25 -5
  30. package/src/modules/user.js +138 -76
  31. package/src/modules/workflow.js +187 -177
  32. package/src/providers.js +1 -1
  33. package/swagger-en.yml +2308 -472
  34. package/swagger-fr.yml +487 -3
  35. package/test/core.test.js +341 -0
  36. package/test/data.history.integration.test.js +140 -16
  37. package/test/data.integration.test.js +137 -2
  38. package/test/user.test.js +201 -280
  39. package/test/workflow.integration.test.js +8 -0
@@ -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,147 @@ 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 {safeAssignObject} from "../core.js";
22
+
23
+ /**
24
+ * Récupère une valeur imbriquée dans un objet (ex: 'user.address.city').
25
+ */
26
+ const getNestedValue = (obj, path) => {
27
+ if (!path || !obj) return undefined;
28
+ return path.split('.').reduce((acc, part) => acc && acc[part] !== undefined ? acc[part] : undefined, obj);
29
+ };
30
+
31
+
32
+ /**
33
+ * Remplace les placeholders dans un template (string, object, array) par des valeurs du contextData.
34
+ * Version améliorée avec support des chemins complexes via resolvePathValue.
35
+ */
36
+ export async function substituteVariables(template, contextData, user) {
37
+ // 1. Retourner les types non substituables tels quels
38
+ if (template === null || (typeof template !== 'string' && typeof template !== 'object')) {
39
+ return template;
40
+ }
41
+
42
+ // 2. Gérer les tableaux de manière récursive
43
+ if (Array.isArray(template)) {
44
+ return Promise.all(template.map(item => substituteVariables(item, contextData, user)));
45
+ }
46
+
47
+ // 3. Gérer les objets de manière récursive
48
+ if (typeof template === 'object') {
49
+ const newObj = {};
50
+ for (const key in template) {
51
+ if (Object.prototype.hasOwnProperty.call(template, key)) {
52
+ const val = await substituteVariables(template[key], contextData, user);
53
+ safeAssignObject(newObj, key, val);
54
+ }
55
+ }
56
+ return newObj;
57
+ }
58
+
59
+ // --- À partir d'ici, nous savons que `template` est une chaîne de caractères ---
60
+
61
+ // 4. Construire le contexte complet pour la substitution
62
+ const dbCollection = await getCollectionForUser(user);
63
+ const userEnvVars = await dbCollection.find({ _model: 'env', _user: user.username }).toArray();
64
+ const userEnv = userEnvVars.reduce((acc, v) => ({ ...acc, [v.name]: v.value }), {});
65
+
66
+ // `contextToSearch` contient toutes les données disponibles à sa racine
67
+ const contextToSearch = { ...contextData, env: userEnv };
68
+
69
+ // 5. Logique de résolution de valeur améliorée avec resolvePathValue
70
+ const findValue = async (key) => {
71
+ let path = key.trim();
72
+ if (path.startsWith('context.')) {
73
+ path = path.substring('context.'.length);
74
+ }
75
+ if (path.endsWith('._id')) {
76
+ const basePath = path.slice(0, -4);
77
+ const value = await findValue(basePath);
78
+ return value?._id?.toString(); // Convertit l'ObjectId en string
79
+ }
80
+
81
+ // Gérer les valeurs dynamiques spéciales
82
+ if (path === 'now') {
83
+ return new Date().toISOString();
84
+ } else if (path === 'randomUUID') {
85
+ return crypto.randomUUID();
86
+ } else if( path === "baseUrl" ){
87
+ return process.env.NODE_ENV === 'production' ? 'https://'+getHost()+'/' : 'http://localhost:/'+port;
88
+ }
89
+
90
+ // Détecter si le chemin est complexe (contient plus d'un point)
91
+ if (path.split('.').length > 1) {
92
+ try {
93
+ // Essayer de résoudre le chemin avec resolvePathValue
94
+ const [root, ...rest] = path.split('.');
95
+ // On vérifie si la racine du chemin (ex: 'triggerData') existe dans notre contexte
96
+ if (contextToSearch[root]) {
97
+ const resolvedValue = await resolvePathValue(
98
+ rest.join('.'),
99
+ contextToSearch[root], // On passe le bon objet de départ (ex: l'objet triggerData)
100
+ user
101
+ );
102
+ if (resolvedValue !== undefined) {
103
+ return resolvedValue;
104
+ }
105
+ }
106
+ } catch (error) {
107
+ console.warn(`Erreur lors de la résolution du chemin "${path}":`, error.message);
108
+ // On continue avec la méthode normale si la résolution échoue
109
+ }
110
+ }
111
+
112
+ // Fallback: chercher le chemin dans l'objet de contexte normal
113
+ return getNestedValue(contextToSearch, path);
114
+ };
115
+
116
+ // CAS A : La chaîne est un unique placeholder (ex: "{context.triggerData.product.price}")
117
+ const singlePlaceholderMatch = template.match(/^\{([^}]+)\}$/);
118
+ if (singlePlaceholderMatch) {
119
+ const key = singlePlaceholderMatch[1];
120
+ const value = await findValue(key);
121
+
122
+ if (value === undefined) {
123
+ return template; // Placeholder not found, return as is.
124
+ }
125
+
126
+ // If the resolved value is a string, it might contain more placeholders.
127
+ // We recursively call substituteVariables on it, but only if it's different
128
+ // from the original template to prevent infinite loops.
129
+ if (typeof value === 'string' && value !== template) {
130
+ return substituteVariables(value, contextData, user);
131
+ }
132
+
133
+ // For non-string values or if value is same as template, return the value.
134
+ return value;
135
+ }
136
+
137
+ // CAS B : La chaîne contient plusieurs placeholders ou mix texte/variables
138
+ const placeholderRegex = /\{([^}]+)\}/g;
139
+ const placeholders = [...template.matchAll(placeholderRegex)];
140
+
141
+ // Si aucun placeholder trouvé, retourner la chaîne telle quelle
142
+ if (placeholders.length === 0) {
143
+ return template;
144
+ }
145
+
146
+ // Remplacer chaque placeholder de manière asynchrone
147
+ let result = template;
148
+ for (const [match, key] of placeholders) {
149
+ const value = await findValue(key);
150
+ const replacement = value !== undefined
151
+ ? (value === null ? 'null' : typeof value === 'object' ? JSON.stringify(value) : String(value))
152
+ : match;
153
+ result = result.replace(match, replacement);
154
+ }
155
+
156
+ return result;
157
+ }
158
+
24
159
 
25
160
  let logger = null;
26
161
  export async function onInit(defaultEngine) {
@@ -29,7 +164,6 @@ export async function onInit(defaultEngine) {
29
164
  await scheduleWorkflowTriggers();
30
165
  }
31
166
 
32
-
33
167
  /**
34
168
  * Déclenche un workflow par son nom et lui passe des données de contexte.
35
169
  * C'est la fonction clé à exposer aux endpoints pour lancer des processus métier.
@@ -969,42 +1103,6 @@ export async function executeStepAction(actionDef, contextData, user, dbCollecti
969
1103
  return { success: false, message: error.message || 'Action execution failed' };
970
1104
  }
971
1105
  }
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
1106
 
1009
1107
  /**
1010
1108
  * Résout un chemin de variable complexe (ex: "triggerData.order.customer.contact.email")
@@ -1120,133 +1218,6 @@ async function resolvePathValue(pathString, initialContext, user) {
1120
1218
  return finalValue;
1121
1219
  }
1122
1220
 
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
1221
  /**
1251
1222
  * Triggers the instantiation of a workflowRun if conditions are met.
1252
1223
  * Checks the event type and trigger's data filter.
@@ -1401,6 +1372,7 @@ export async function processWorkflowRun(workflowRunId, user) {
1401
1372
  let currentRunState;
1402
1373
  let contextData = {};
1403
1374
  let stepExecutionsCount = {};
1375
+ let history = [];
1404
1376
 
1405
1377
  try {
1406
1378
  currentRunState = await dbCollection.findOne({ _id: runId, _model: 'workflowRun' });
@@ -1411,6 +1383,7 @@ export async function processWorkflowRun(workflowRunId, user) {
1411
1383
  }
1412
1384
 
1413
1385
  stepExecutionsCount = currentRunState.stepExecutionsCount || {};
1386
+ history = currentRunState.history || [];
1414
1387
  if (['completed', 'failed', 'cancelled'].includes(currentRunState.status)) {
1415
1388
  logger.info(`[processWorkflowRun] WorkflowRun ID: ${runId} is already in a terminal state (${currentRunState.status}). Skipping.`);
1416
1389
  return;
@@ -1420,7 +1393,7 @@ export async function processWorkflowRun(workflowRunId, user) {
1420
1393
  logger.error(error);
1421
1394
  await dbCollection.updateOne(
1422
1395
  { _id: runId },
1423
- { $set: { status: 'failed', error, completedAt: new Date(), stepExecutionsCount } }
1396
+ { $set: { status: 'failed', error, completedAt: new Date(), stepExecutionsCount, history } }
1424
1397
  );
1425
1398
  };
1426
1399
 
@@ -1437,7 +1410,7 @@ export async function processWorkflowRun(workflowRunId, user) {
1437
1410
  const errorMessage = workflowDefinition.startStep ? 'No valid starting step defined in workflow or run state.' : null;
1438
1411
  await dbCollection.updateOne(
1439
1412
  { _id: runId },
1440
- { $set: { status: finalStatus, error: errorMessage, completedAt: new Date(), currentStep: null, stepExecutionsCount } }
1413
+ { $set: { status: finalStatus, error: errorMessage, completedAt: new Date(), currentStep: null, stepExecutionsCount, history } }
1441
1414
  );
1442
1415
  return;
1443
1416
  }
@@ -1462,10 +1435,13 @@ export async function processWorkflowRun(workflowRunId, user) {
1462
1435
  return await logError(`Step definition ID: ${currentStepId} not found.`);
1463
1436
  }
1464
1437
 
1465
- await dbCollection.updateOne(
1466
- { _id: runId },
1467
- { $set: { status: 'running', currentStep: currentStepId, contextData, stepExecutionsCount } }
1468
- );
1438
+ const stepHistoryEntry = {
1439
+ stepId: currentStepId.toString(),
1440
+ stepName: currentStepDef.name,
1441
+ executedAt: new Date(),
1442
+ status: 'pending',
1443
+ actions: []
1444
+ };
1469
1445
 
1470
1446
  let stepSucceeded = true;
1471
1447
  let logInfo = null;
@@ -1502,6 +1478,14 @@ export async function processWorkflowRun(workflowRunId, user) {
1502
1478
  for (const actionId of currentStepDef.actions) {
1503
1479
  if (!isObjectId(actionId)) continue;
1504
1480
  const actionDef = await dbCollection.findOne({ _id: new ObjectId(actionId), _model: 'workflowAction' });
1481
+ const actionHistoryEntry = {
1482
+ actionId: actionId.toString(),
1483
+ actionName: actionDef.name,
1484
+ actionType: actionDef.type,
1485
+ executedAt: new Date(),
1486
+ status: 'pending'
1487
+ };
1488
+
1505
1489
  if (!actionDef) return await logError(`Action definition ${actionId} not found.`);
1506
1490
  const actionResult = await workflowModule.executeStepAction(actionDef, contextData, user, dbCollection);
1507
1491
 
@@ -1540,9 +1524,15 @@ export async function processWorkflowRun(workflowRunId, user) {
1540
1524
  if (!actionResult.success) {
1541
1525
  stepSucceeded = false;
1542
1526
  logInfo = actionResult.message || `Action ${actionDef.name || actionId} failed.`;
1527
+ actionHistoryEntry.status = 'failed';
1528
+ actionHistoryEntry.result = logInfo;
1529
+ stepHistoryEntry.actions.push(actionHistoryEntry);
1543
1530
  break;
1544
1531
  }else{
1545
1532
  logInfo = `Action ${actionDef.name || actionId} : ${actionResult.message}`;
1533
+ actionHistoryEntry.status = 'success';
1534
+ actionHistoryEntry.result = actionResult.message;
1535
+ stepHistoryEntry.actions.push(actionHistoryEntry);
1546
1536
  }
1547
1537
  if (actionResult.updatedContext) {
1548
1538
  contextData = { ...contextData, ...actionResult.updatedContext };
@@ -1551,6 +1541,10 @@ export async function processWorkflowRun(workflowRunId, user) {
1551
1541
  logger.info(`[processWorkflowRun] Run ID: ${runId}, Step ID: ${currentStepId}, Action ID: ${actionId}: Executed successfully.`);
1552
1542
  }
1553
1543
  }
1544
+ if (stepSucceeded) {
1545
+ stepHistoryEntry.status = 'success';
1546
+ stepHistoryEntry.result = 'Step actions completed successfully.';
1547
+ }
1554
1548
  } else {
1555
1549
  logger.info(`[processWorkflowRun] Run ID: ${runId}, Step ID: ${currentStepId}: Conditions not met. Skipping actions.`);
1556
1550
  }
@@ -1558,6 +1552,8 @@ export async function processWorkflowRun(workflowRunId, user) {
1558
1552
  logger.error(`[processWorkflowRun] Run ID: ${runId}, Step ID: ${currentStepId}: Error during condition/action execution: ${error.message}`);
1559
1553
  stepSucceeded = false;
1560
1554
  logInfo = error.message;
1555
+ stepHistoryEntry.status = 'failed';
1556
+ stepHistoryEntry.result = logInfo;
1561
1557
  }
1562
1558
 
1563
1559
  // --- 9. Détermination de la prochaine étape ---
@@ -1587,7 +1583,7 @@ export async function processWorkflowRun(workflowRunId, user) {
1587
1583
 
1588
1584
  // --- 10. Mise à jour de l'état de l'exécution ---
1589
1585
  currentStepId = nextStepId;
1590
- const updatePayload = { contextData };
1586
+ const updatePayload = { contextData, stepExecutionsCount }; // <-- CORRECTION: Always include stepExecutionsCount
1591
1587
 
1592
1588
  if (finalStatusForRun) {
1593
1589
  updatePayload.status = finalStatusForRun;
@@ -1597,7 +1593,21 @@ export async function processWorkflowRun(workflowRunId, user) {
1597
1593
  } else {
1598
1594
  updatePayload.currentStep = currentStepId;
1599
1595
  }
1600
- await dbCollection.updateOne({ _id: runId }, { $set: updatePayload });
1596
+
1597
+ // Update the last history entry with the final status and actions
1598
+ // We use a pipeline to reliably update the last element of the history array.
1599
+ // --- FIX ---
1600
+ // The previous logic using $slice failed when the history array had only one element.
1601
+ // This new logic uses a direct update on the last element of the array, which is more robust.
1602
+ await dbCollection.updateOne(
1603
+ { _id: runId }, {
1604
+ $set: updatePayload,
1605
+ $push: {
1606
+ history: stepHistoryEntry
1607
+ }
1608
+ }
1609
+ );
1610
+
1601
1611
 
1602
1612
  if(finalStatusForRun) {
1603
1613
  logger.info(`[processWorkflowRun] Finished processing for workflowRun ID: ${runId}. Final Status: ${finalStatusForRun}`);
@@ -1607,7 +1617,7 @@ export async function processWorkflowRun(workflowRunId, user) {
1607
1617
  logger.error(`[processWorkflowRun] Critical error during processing of workflowRun ID: ${runId}. Error: ${error.message}`, error.stack);
1608
1618
  await dbCollection.updateOne(
1609
1619
  { _id: runId, status: { $nin: ['completed', 'failed', 'cancelled'] } },
1610
- { $set: { status: 'failed', log: `Critical error: ${error.message}`, completedAt: new Date(), stepExecutionsCount } }
1620
+ { $set: { status: 'failed', log: `Critical error: ${error.message}`, completedAt: new Date(), stepExecutionsCount, history } }
1611
1621
  );
1612
1622
  }
1613
1623
  }
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.