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.
- package/README.md +56 -911
- package/client/package-lock.json +64 -50
- package/client/package.json +6 -0
- package/client/src/App.scss +29 -0
- package/client/src/Dashboard.jsx +1 -1
- package/client/src/WorkflowEditor.jsx +101 -0
- package/client/src/WorkflowEditor.scss +29 -4
- package/client/src/_variables.scss +3 -0
- package/doc/automation-workflows.md +102 -0
- package/doc/core-concepts.md +33 -0
- package/doc/custom-api-endpoints.md +40 -0
- package/doc/dashboards-kpis-charts.md +49 -0
- package/doc/data-management.md +120 -0
- package/doc/data-models.md +75 -0
- package/doc/index.md +14 -0
- package/doc/roles-permissions.md +43 -0
- package/doc/users.md +30 -0
- package/package.json +7 -5
- package/src/client.js +6 -4
- package/src/core.js +31 -12
- package/src/defaultModels +1628 -0
- package/src/defaultModels.js +14 -0
- package/src/filter.js +110 -42
- package/src/modules/data/data.history.js +5 -1
- package/src/modules/data/data.js +2 -1
- package/src/modules/data/data.operations.js +116 -72
- package/src/modules/data/data.relations.js +1 -1
- package/src/modules/mongodb.js +2 -1
- package/src/modules/swagger.js +25 -5
- package/src/modules/user.js +138 -76
- package/src/modules/workflow.js +187 -177
- package/src/providers.js +1 -1
- package/swagger-en.yml +2308 -472
- package/swagger-fr.yml +487 -3
- package/test/core.test.js +341 -0
- package/test/data.history.integration.test.js +140 -16
- package/test/data.integration.test.js +137 -2
- package/test/user.test.js +201 -280
- package/test/workflow.integration.test.js +8 -0
package/src/modules/workflow.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
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
|
-
|
|
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.
|