data-primals-engine 1.6.2-rc1 → 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.
- package/README.md +61 -20
- package/client/src/Dashboard.jsx +1 -1
- package/client/src/WorkflowEditor.jsx +101 -0
- package/client/src/WorkflowEditor.scss +29 -4
- package/package.json +145 -142
- package/src/client.js +4 -0
- package/src/defaultModels +1628 -0
- package/src/defaultModels.js +14 -0
- package/src/filter.js +170 -1
- package/src/modules/assistant/assistant.js +38 -56
- package/src/modules/assistant/providers.js +38 -0
- package/src/modules/data/data.history.js +7 -6
- package/src/modules/data/data.operations.js +20 -5
- package/src/modules/user.js +57 -25
- package/src/modules/workflow.js +53 -178
- package/src/providers.js +1 -1
- package/test/assistant.test.js +207 -0
- package/test/config.test.js +41 -0
- package/test/data.history.integration.test.js +144 -20
- package/test/user.test.js +195 -278
- package/test/workflow.integration.test.js +8 -0
package/src/modules/user.js
CHANGED
|
@@ -15,6 +15,7 @@ import rateLimit from "express-rate-limit";
|
|
|
15
15
|
import ivm from "isolated-vm";
|
|
16
16
|
import {emailDefaultConfig} from "../constants.js";
|
|
17
17
|
import {safeAssignObject} from "../core.js";
|
|
18
|
+
import {substituteVariables} from "../filter.js";
|
|
18
19
|
import {Config} from "../config.js";
|
|
19
20
|
|
|
20
21
|
export const userInitiator = async (req, res, next) => {
|
|
@@ -105,35 +106,38 @@ export async function onInit(defaultEngine) {
|
|
|
105
106
|
* 1. Elle récupère toutes les permissions de base issues des rôles de l'utilisateur.
|
|
106
107
|
* 2. Elle applique ensuite les "exceptions" (ajouts ou retraits de permissions) qui sont valides (non expirées).
|
|
107
108
|
* @param {object} user - L'objet utilisateur pour lequel calculer les permissions.
|
|
108
|
-
* @returns {Promise<
|
|
109
|
+
* @returns {Promise<Map<string, object|null>>} Une Map contenant les noms des permissions actives et leur filtre associé.
|
|
109
110
|
* @private
|
|
110
111
|
*/
|
|
111
112
|
export async function getUserActivePermissions(user, env = null) {
|
|
112
113
|
const datasCollection = await getCollectionForUser(user);
|
|
113
114
|
const now = new Date();
|
|
114
|
-
const activePermissions = new
|
|
115
|
+
const activePermissions = new Map();
|
|
115
116
|
|
|
116
117
|
// --- ÉTAPE 1: Récupérer les permissions de base des rôles ---
|
|
117
118
|
if (user.roles && user.roles.length > 0) {
|
|
118
119
|
const roleIds = user.roles.map(id => new ObjectId(id));
|
|
119
|
-
|
|
120
|
+
|
|
120
121
|
const rolePermissions = await datasCollection.aggregate([
|
|
121
|
-
{ $match: { _id: { $in: roleIds }, _model: "role" } },
|
|
122
|
+
{ $match: { _id: { $in: roleIds }, _model: "role", _user: user.username } },
|
|
122
123
|
{ $unwind: "$permissions" },
|
|
123
|
-
{ $addFields: { "permissionId": { "$toObjectId": "$permissions" } } },
|
|
124
124
|
{
|
|
125
125
|
$lookup: {
|
|
126
126
|
from: datasCollection.collectionName, // Utiliser la même collection
|
|
127
|
-
|
|
128
|
-
|
|
127
|
+
let: { permissionIdStr: "$permissions" }, // La permission est un string
|
|
128
|
+
pipeline: [
|
|
129
|
+
{ $match: {
|
|
130
|
+
$expr: { $eq: [ { $toString: "$_id" }, "$$permissionIdStr" ] }
|
|
131
|
+
}}
|
|
132
|
+
],
|
|
129
133
|
as: "permissionDoc"
|
|
130
134
|
}
|
|
131
135
|
},
|
|
132
136
|
{ $unwind: "$permissionDoc" },
|
|
133
|
-
{ $
|
|
137
|
+
{ $project: { name: "$permissionDoc.name", filter: "$permissionDoc.filter" } }
|
|
134
138
|
]).toArray();
|
|
135
139
|
|
|
136
|
-
rolePermissions.forEach(p => p.
|
|
140
|
+
rolePermissions.forEach(p => p.name && activePermissions.set(p.name, p.filter ?? null));
|
|
137
141
|
}
|
|
138
142
|
|
|
139
143
|
// --- ÉTAPE 2: Appliquer les exceptions de permission ---
|
|
@@ -141,7 +145,7 @@ export async function getUserActivePermissions(user, env = null) {
|
|
|
141
145
|
{
|
|
142
146
|
$match: { // Filtre de base pour les exceptions de l'utilisateur
|
|
143
147
|
_model: "userPermission",
|
|
144
|
-
user: user._id,
|
|
148
|
+
user: user._id.toString(),
|
|
145
149
|
$and: [ // Filtre sur l'environnement et l'expiration
|
|
146
150
|
{
|
|
147
151
|
$or: [ // La permission est soit globale, soit spécifique à l'environnement demandé
|
|
@@ -160,20 +164,22 @@ export async function getUserActivePermissions(user, env = null) {
|
|
|
160
164
|
pipeline: [
|
|
161
165
|
{
|
|
162
166
|
$match: {
|
|
163
|
-
$expr: {
|
|
164
|
-
$eq: [
|
|
165
|
-
"$_id",
|
|
166
|
-
{ $toObjectId: "$$permissionId" } // Conversion ici
|
|
167
|
-
]
|
|
168
|
-
}
|
|
167
|
+
$expr: { $eq: [ { $toString: "$_id" }, "$$permissionId" ] }
|
|
169
168
|
}
|
|
170
169
|
},
|
|
171
|
-
{ $project: { name: 1 } }
|
|
170
|
+
{ $project: { name: 1, filter: 1 } }
|
|
172
171
|
],
|
|
173
172
|
as: 'permissionDoc'
|
|
174
173
|
}
|
|
175
174
|
},
|
|
176
|
-
{ $unwind: '$permissionDoc' }
|
|
175
|
+
{ $unwind: '$permissionDoc' },
|
|
176
|
+
{
|
|
177
|
+
$project: { // On sélectionne les champs dont on a besoin
|
|
178
|
+
isGranted: 1,
|
|
179
|
+
filter: 1, // On ajoute le filtre de l'exception
|
|
180
|
+
permissionDoc: 1
|
|
181
|
+
}
|
|
182
|
+
}
|
|
177
183
|
]).toArray();
|
|
178
184
|
|
|
179
185
|
// Appliquer les exceptions
|
|
@@ -182,7 +188,11 @@ export async function getUserActivePermissions(user, env = null) {
|
|
|
182
188
|
if (!permissionName) continue;
|
|
183
189
|
|
|
184
190
|
if (exception.isGranted) {
|
|
185
|
-
|
|
191
|
+
// Priorité 1: Le filtre défini sur l'exception elle-même.
|
|
192
|
+
// Priorité 2: Le filtre défini sur la permission de base.
|
|
193
|
+
// Priorité 3: null s'il n'y a aucun filtre.
|
|
194
|
+
const finalFilter = exception.filter ?? exception.permissionDoc.filter ?? null;
|
|
195
|
+
activePermissions.set(permissionName, finalFilter);
|
|
186
196
|
} else {
|
|
187
197
|
activePermissions.delete(permissionName);
|
|
188
198
|
}
|
|
@@ -195,15 +205,19 @@ export async function getUserActivePermissions(user, env = null) {
|
|
|
195
205
|
* Vérifie si un utilisateur possède au moins une des permissions spécifiées.
|
|
196
206
|
* Cette fonction utilise la nouvelle logique basée sur les rôles et les exceptions de permission.
|
|
197
207
|
* @param {string|string[]} permissionNames - Le nom de la permission ou un tableau de noms.
|
|
198
|
-
* @param {object} user - L'objet utilisateur
|
|
199
|
-
* @
|
|
208
|
+
* @param {object} user - L'objet utilisateur.
|
|
209
|
+
* @param {string|null} env - L'environnement de la permission.
|
|
210
|
+
* @param {object|null} req - L'objet requête Express optionnel.
|
|
211
|
+
* @returns {Promise<boolean|object>} - `false` si aucune permission n'est trouvée. Sinon, retourne le filtre de la première permission trouvée (ou `true` si aucun filtre n'est défini).
|
|
200
212
|
*/
|
|
201
|
-
export async function hasPermission(permissionNames, user, env = null) {
|
|
213
|
+
export async function hasPermission(permissionNames, user, env = null, req = null) {
|
|
202
214
|
// Garde la compatibilité pour les utilisateurs non-locaux (ex: système)
|
|
203
215
|
if (!isLocalUser(user)) {
|
|
204
216
|
const userRoles = new Set(user.roles || []);
|
|
205
217
|
const requiredPermissions = Array.isArray(permissionNames) ? permissionNames : [permissionNames];
|
|
206
|
-
|
|
218
|
+
const hasPerm = requiredPermissions.some(p => userRoles.has(p));
|
|
219
|
+
// Pour les non-locaux, on ne gère pas les filtres complexes, on retourne juste true/false.
|
|
220
|
+
return hasPerm ? true : false;
|
|
207
221
|
}
|
|
208
222
|
|
|
209
223
|
try {
|
|
@@ -216,9 +230,27 @@ export async function hasPermission(permissionNames, user, env = null) {
|
|
|
216
230
|
// 1. Obtenir l'ensemble final et à jour des permissions de l'utilisateur
|
|
217
231
|
const activePermissions = await getUserActivePermissions(user, env);
|
|
218
232
|
|
|
219
|
-
// 2.
|
|
220
|
-
|
|
233
|
+
// 2. Chercher la première permission correspondante
|
|
234
|
+
for (const pName of requiredPermissions) {
|
|
235
|
+
if (activePermissions.has(pName)) {
|
|
236
|
+
const filter = activePermissions.get(pName);
|
|
237
|
+
// Si un filtre existe, on substitue les variables
|
|
238
|
+
if (filter && typeof filter === 'object' && Object.keys(filter).length > 0) {
|
|
239
|
+
// Le contexte de substitution est enrichi avec des données pertinentes
|
|
240
|
+
const context = {
|
|
241
|
+
user,
|
|
242
|
+
permissionName: pName,
|
|
243
|
+
env,
|
|
244
|
+
now: new Date(),
|
|
245
|
+
req: req ? { query: req.query, body: req.body, params: req.params, ip: req.ip } : null
|
|
246
|
+
};
|
|
247
|
+
return await substituteVariables(filter, context, user);
|
|
248
|
+
}
|
|
249
|
+
return true; // Pas de filtre ou filtre vide, on autorise sans condition supplémentaire
|
|
250
|
+
}
|
|
251
|
+
}
|
|
221
252
|
|
|
253
|
+
return false; // Aucune permission trouvée
|
|
222
254
|
} catch (e) {
|
|
223
255
|
logger.error("Erreur lors de la vérification des permissions :", e);
|
|
224
256
|
return false;
|
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,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 {
|
|
20
|
-
import {
|
|
21
|
-
import {getAIProvider} from "./assistant/assistant.js";
|
|
22
|
-
import {parseSafeJSON, safeAssignObject} from "../core.js";
|
|
18
|
+
import { providers } from "./assistant/constants.js";
|
|
19
|
+
import { getAIProvider } from "./assistant/providers.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
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
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
|
-
|
|
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.
|