data-primals-engine 1.1.0 → 1.1.2
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 +38 -2
- package/client/README.md +113 -5
- package/client/src/App.jsx +40 -21
- package/client/src/App.scss +21 -0
- package/client/src/Dashboard.scss +5 -1
- package/client/src/DashboardFlexViewItem.jsx +1 -2
- package/client/src/DisplayFlexNodeRenderer.jsx +120 -4
- package/client/src/FlexBuilder.jsx +2 -2
- package/client/src/FlexBuilder.scss +5 -0
- package/client/src/FlexBuilderControls.jsx +201 -22
- package/client/src/FlexBuilderPreview.jsx +43 -1
- package/client/src/FlexNode.jsx +93 -5
- package/client/src/FlexTreeUtils.js +42 -11
- package/client/src/ModelList.jsx +33 -20
- package/client/src/constants.js +4 -4
- package/package.json +6 -3
- package/server.js +1 -7
- package/src/constants.js +165 -16
- package/src/defaultModels.js +49 -0
- package/src/email.js +1 -1
- package/src/engine.js +17 -0
- package/src/modules/data.js +146 -13
- package/src/modules/workflow.js +155 -17
- package/test/data.integration.test.js +2 -2
package/src/modules/workflow.js
CHANGED
|
@@ -4,17 +4,16 @@ import schedule from "node-schedule";
|
|
|
4
4
|
import {ObjectId} from "mongodb";
|
|
5
5
|
import crypto from "node:crypto";
|
|
6
6
|
|
|
7
|
+
import {NodeVM, VM, VMScript} from 'vm2';
|
|
7
8
|
import {Logger} from "../gameObject.js";
|
|
8
|
-
import {deleteData,
|
|
9
|
-
import {maxExecutionsByStep, maxWorkflowSteps} from "../constants.js";
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
9
|
+
import {deleteData, insertData, patchData, searchData} from "./data.js";
|
|
10
|
+
import {maxExecutionsByStep, maxWorkflowSteps, timeoutVM} from "../constants.js";
|
|
11
|
+
import {ChatOpenAI} from "@langchain/openai";
|
|
12
|
+
import {ChatGoogleGenerativeAI} from "@langchain/google-genai";
|
|
13
|
+
import {ChatPromptTemplate} from "@langchain/core/prompts";
|
|
13
14
|
import i18n from "data-primals-engine/i18n";
|
|
14
15
|
import {sendEmail} from "../email.js";
|
|
15
16
|
|
|
16
|
-
// 1. ADD THIS IMPORT AT THE TOP OF THE FILE
|
|
17
|
-
// This allows the module to call its own exported functions.
|
|
18
17
|
import * as workflowModule from './workflow.js';
|
|
19
18
|
|
|
20
19
|
let logger = null;
|
|
@@ -181,6 +180,144 @@ export async function scheduleWorkflowTriggers() {
|
|
|
181
180
|
}
|
|
182
181
|
|
|
183
182
|
|
|
183
|
+
/* Remplacement de la fonction executeSafeJavascript
|
|
184
|
+
async function executeWithIsolatedVm(actionDef, context) {
|
|
185
|
+
const code = actionDef.script;
|
|
186
|
+
const isolate = new ivm.Isolate({ memoryLimit: 128 }); // Limite de 128MB de mémoire
|
|
187
|
+
|
|
188
|
+
try {
|
|
189
|
+
const vmContext = await isolate.createContext();
|
|
190
|
+
const jail = vmContext.global;
|
|
191
|
+
|
|
192
|
+
// On expose une fonction 'log' sécurisée à l'intérieur de la VM.
|
|
193
|
+
// C'est une référence, pas la fonction elle-même.
|
|
194
|
+
await jail.set('log', new ivm.Reference(function(...args) {
|
|
195
|
+
// On peut préfixer les logs pour savoir qu'ils viennent de la VM
|
|
196
|
+
logger.info('[VM Script Log]', ...args);
|
|
197
|
+
}));
|
|
198
|
+
|
|
199
|
+
// On injecte les données du contexte. Elles sont COPIÉES, pas référencées.
|
|
200
|
+
// C'est une caractéristique de sécurité clé.
|
|
201
|
+
await jail.set('contextData', new ivm.ExternalCopy(context).copyInto());
|
|
202
|
+
// On peut aussi exposer des fonctions spécifiques de votre application
|
|
203
|
+
// await jail.set('myApiFunction', new ivm.Reference(async (params) => { ... }));
|
|
204
|
+
|
|
205
|
+
// On compile le script
|
|
206
|
+
const script = await isolate.compileScript(code);
|
|
207
|
+
|
|
208
|
+
// On exécute le script avec un timeout
|
|
209
|
+
const result = await script.run(vmContext, { timeout: 1000 });
|
|
210
|
+
|
|
211
|
+
// Le résultat est une référence, on le copie pour l'utiliser dans notre contexte principal.
|
|
212
|
+
return result;
|
|
213
|
+
|
|
214
|
+
} catch (error) {
|
|
215
|
+
logger.error("Error executing script with isolated-vm:", error.stack);
|
|
216
|
+
// On propage une erreur propre
|
|
217
|
+
throw new Error(`Script execution failed: ${error.message}`);
|
|
218
|
+
} finally {
|
|
219
|
+
// TRÈS IMPORTANT : Toujours libérer l'isolate pour éviter les fuites de mémoire.
|
|
220
|
+
if (isolate && !isolate.isDisposed) {
|
|
221
|
+
isolate.dispose();
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
REQUIRES NODE >=20
|
|
226
|
+
*/
|
|
227
|
+
|
|
228
|
+
export async function executeSafeJavascript(actionDef, context, user) {
|
|
229
|
+
const code = actionDef.script;
|
|
230
|
+
const collectedLogs = []; // Tableau pour capturer les logs de cette exécution
|
|
231
|
+
|
|
232
|
+
// 1. Construire l'API qui sera exposée au script
|
|
233
|
+
const scriptApi = {
|
|
234
|
+
db: {
|
|
235
|
+
create: (modelName, dataObject) => insertData(modelName, dataObject, {}, user),
|
|
236
|
+
find: async (modelName, filter) => {
|
|
237
|
+
const result = await searchData({ user, query: { model: modelName, filter } });
|
|
238
|
+
return result.data;
|
|
239
|
+
},
|
|
240
|
+
findOne: async (modelName, filter) => {
|
|
241
|
+
const result = await searchData({ user, query: { model: modelName, filter, limit: 1 } });
|
|
242
|
+
return result.data?.[0] || null;
|
|
243
|
+
},
|
|
244
|
+
update: (modelName, filter, updateObject) => patchData(modelName, filter, updateObject, {}, user),
|
|
245
|
+
delete: (modelName, filter) => deleteData(modelName, null, filter, user)
|
|
246
|
+
},
|
|
247
|
+
logger: {
|
|
248
|
+
// Chaque fonction de log utilise maintenant logger.trace() pour générer le message final
|
|
249
|
+
info: (...args) => {
|
|
250
|
+
// On génère le message final formaté une seule fois avec trace()
|
|
251
|
+
const finalMessage = logger.trace('info', '[VM Script]', ...args);
|
|
252
|
+
// On l'affiche en couleur dans la console du serveur
|
|
253
|
+
// On stocke ce même message final dans les logs collectés
|
|
254
|
+
collectedLogs.push({
|
|
255
|
+
level: 'info',
|
|
256
|
+
message: finalMessage, // Le message complet est stocké
|
|
257
|
+
timestamp: new Date().toISOString()
|
|
258
|
+
});
|
|
259
|
+
},
|
|
260
|
+
warn: (...args) => {
|
|
261
|
+
const finalMessage = logger.trace('warn', '[VM Script]', ...args);
|
|
262
|
+
collectedLogs.push({
|
|
263
|
+
level: 'warn',
|
|
264
|
+
message: finalMessage,
|
|
265
|
+
timestamp: new Date().toISOString()
|
|
266
|
+
});
|
|
267
|
+
},
|
|
268
|
+
error: (...args) => {
|
|
269
|
+
const finalMessage = logger.trace('error', '[VM Script]', ...args);
|
|
270
|
+
collectedLogs.push({
|
|
271
|
+
level: 'error',
|
|
272
|
+
message: finalMessage,
|
|
273
|
+
timestamp: new Date().toISOString()
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
},
|
|
277
|
+
env: {
|
|
278
|
+
get: async (variableName) => {
|
|
279
|
+
if (!variableName) return null;
|
|
280
|
+
const result = await searchData({ user, query: { model: 'env', filter: { name: variableName }, limit: 1 } });
|
|
281
|
+
return result.data?.[0]?.value || null;
|
|
282
|
+
},
|
|
283
|
+
getAll: async () => {
|
|
284
|
+
const result = await searchData({ user, query: { model: 'env' } });
|
|
285
|
+
// Retourne un objet { NOM: valeur, ... } plus pratique
|
|
286
|
+
return result.data.reduce((acc, v) => {
|
|
287
|
+
acc[v.name] = v.value;
|
|
288
|
+
return acc;
|
|
289
|
+
}, {});
|
|
290
|
+
}
|
|
291
|
+
},
|
|
292
|
+
...context
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
const vm = new NodeVM({
|
|
296
|
+
timeout: timeoutVM,
|
|
297
|
+
sandbox: scriptApi,
|
|
298
|
+
console: 'off',
|
|
299
|
+
require: false,
|
|
300
|
+
wasm: false
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
try {
|
|
304
|
+
const script = new VMScript(`const script = () => {${code}}; module.exports = script();`);
|
|
305
|
+
const result= vm.run(script);
|
|
306
|
+
return { success: true, data: result, logs: collectedLogs };
|
|
307
|
+
} catch (error) {
|
|
308
|
+
const errorMessage = `Script execution failed: ${error.message}`;
|
|
309
|
+
|
|
310
|
+
// On utilise aussi trace() pour l'erreur critique
|
|
311
|
+
const finalErrorMessage = logger.trace('critical', `[VM Script] ${errorMessage}\n${error.stack}`);
|
|
312
|
+
collectedLogs.push({
|
|
313
|
+
level: 'critical',
|
|
314
|
+
message: finalErrorMessage,
|
|
315
|
+
timestamp: new Date().toISOString()
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
return { success: false, message: errorMessage, logs: collectedLogs };
|
|
319
|
+
}
|
|
320
|
+
}
|
|
184
321
|
/**
|
|
185
322
|
* Handles the 'Webhook' workflow action.
|
|
186
323
|
* Sends an HTTP request to a specified URL with substituted data using native fetch.
|
|
@@ -671,8 +808,9 @@ export async function executeStepAction(actionDef, contextData, user, dbCollecti
|
|
|
671
808
|
case 'SendEmail':
|
|
672
809
|
result = await handleSendEmailAction(actionDef, contextData, user);
|
|
673
810
|
break;
|
|
674
|
-
|
|
675
|
-
|
|
811
|
+
case 'ExecuteScript':
|
|
812
|
+
result = await executeSafeJavascript(actionDef, contextData, user);
|
|
813
|
+
break;
|
|
676
814
|
default:
|
|
677
815
|
logger.error(`[executeStepAction] Unknown action type: ${actionDef.type}`);
|
|
678
816
|
return { success: false, message: `Unknown action type: ${actionDef.type}` };
|
|
@@ -1179,7 +1317,7 @@ export async function processWorkflowRun(workflowRunId, user) {
|
|
|
1179
1317
|
);
|
|
1180
1318
|
|
|
1181
1319
|
let stepSucceeded = true;
|
|
1182
|
-
let
|
|
1320
|
+
let logInfo = null;
|
|
1183
1321
|
let conditionsMet = true;
|
|
1184
1322
|
|
|
1185
1323
|
try {
|
|
@@ -1201,8 +1339,10 @@ export async function processWorkflowRun(workflowRunId, user) {
|
|
|
1201
1339
|
const actionResult = await workflowModule.executeStepAction(actionDef, contextData, user, dbCollection);
|
|
1202
1340
|
if (!actionResult.success) {
|
|
1203
1341
|
stepSucceeded = false;
|
|
1204
|
-
|
|
1342
|
+
logInfo = actionResult.message || `Action ${actionDef.name || actionId} failed.`;
|
|
1205
1343
|
break;
|
|
1344
|
+
}else{
|
|
1345
|
+
logInfo = `Action ${actionDef.name || actionId} : ${actionResult.message}`;
|
|
1206
1346
|
}
|
|
1207
1347
|
if (actionResult.updatedContext) {
|
|
1208
1348
|
contextData = { ...contextData, ...actionResult.updatedContext };
|
|
@@ -1216,7 +1356,7 @@ export async function processWorkflowRun(workflowRunId, user) {
|
|
|
1216
1356
|
} catch (error) {
|
|
1217
1357
|
logger.error(`[processWorkflowRun] Run ID: ${runId}, Step ID: ${currentStepId}: Error during condition/action execution: ${error.message}`);
|
|
1218
1358
|
stepSucceeded = false;
|
|
1219
|
-
|
|
1359
|
+
logInfo = error.message;
|
|
1220
1360
|
}
|
|
1221
1361
|
|
|
1222
1362
|
// --- 9. Détermination de la prochaine étape ---
|
|
@@ -1233,13 +1373,13 @@ export async function processWorkflowRun(workflowRunId, user) {
|
|
|
1233
1373
|
}
|
|
1234
1374
|
} else {
|
|
1235
1375
|
// CHEMIN ÉCHEC/BRANCHE : Une action a échoué OU les conditions n'ont pas été remplies.
|
|
1236
|
-
const reason =
|
|
1376
|
+
const reason = logInfo ? `Action failed: ${logInfo}` : 'Step conditions not met.';
|
|
1237
1377
|
logger.warn(`[processWorkflowRun] Run ID: ${runId}, Step ID: ${currentStepId}: Taking failure/branching path. Reason: ${reason}`);
|
|
1238
1378
|
nextStepId = currentStepDef.onFailureStep;
|
|
1239
1379
|
|
|
1240
1380
|
if (!nextStepId || !isObjectId(nextStepId)) {
|
|
1241
1381
|
// Fin du workflow. Le statut est 'failed' seulement si une vraie erreur s'est produite.
|
|
1242
|
-
finalStatusForRun =
|
|
1382
|
+
finalStatusForRun = logInfo ? 'failed' : 'completed';
|
|
1243
1383
|
nextStepId = null;
|
|
1244
1384
|
}
|
|
1245
1385
|
}
|
|
@@ -1252,9 +1392,7 @@ export async function processWorkflowRun(workflowRunId, user) {
|
|
|
1252
1392
|
updatePayload.status = finalStatusForRun;
|
|
1253
1393
|
updatePayload.completedAt = new Date();
|
|
1254
1394
|
updatePayload.currentStep = null;
|
|
1255
|
-
|
|
1256
|
-
updatePayload.error = stepError;
|
|
1257
|
-
}
|
|
1395
|
+
updatePayload.log = logInfo;
|
|
1258
1396
|
} else {
|
|
1259
1397
|
updatePayload.currentStep = currentStepId;
|
|
1260
1398
|
}
|
|
@@ -723,7 +723,7 @@ describe('Intégration des fonctions CRUD de données avec validation complète'
|
|
|
723
723
|
const packId = packInsertResult.insertedId;
|
|
724
724
|
|
|
725
725
|
// 3. Appeler la fonction à tester avec la nouvelle signature
|
|
726
|
-
const result = await installPack(
|
|
726
|
+
const result = await installPack(packId, currentTestUser, 'en');
|
|
727
727
|
|
|
728
728
|
// 4. Assertions sur le résumé de l'installation
|
|
729
729
|
expect(result.success, `L'installation du pack a échoué: ${result.errors?.join('; ')}`).toBe(true);
|
|
@@ -776,7 +776,7 @@ describe('Intégration des fonctions CRUD de données avec validation complète'
|
|
|
776
776
|
const packInsertResult = await testPacksColInstance.insertOne(mockPackWithInvalidModel);
|
|
777
777
|
const packId = packInsertResult.insertedId;
|
|
778
778
|
|
|
779
|
-
const result = await installPack(
|
|
779
|
+
const result = await installPack(packId, currentTestUser, 'en');
|
|
780
780
|
|
|
781
781
|
// Le succès global est faux s'il y a des erreurs
|
|
782
782
|
expect(result.success).toBe(false);
|