data-primals-engine 1.1.1 → 1.1.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/.github/workflows/node.js.yml +2 -7
- package/README.md +2 -2
- package/client/src/App.jsx +33 -16
- 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 +3 -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 +86 -56
- package/test/data.integration.test.js +2 -2
package/src/modules/workflow.js
CHANGED
|
@@ -4,10 +4,11 @@ import schedule from "node-schedule";
|
|
|
4
4
|
import {ObjectId} from "mongodb";
|
|
5
5
|
import crypto from "node:crypto";
|
|
6
6
|
|
|
7
|
-
import
|
|
7
|
+
import ivm from 'isolated-vm';
|
|
8
|
+
|
|
8
9
|
import {Logger} from "../gameObject.js";
|
|
9
10
|
import {deleteData, insertData, patchData, searchData} from "./data.js";
|
|
10
|
-
import {maxExecutionsByStep, maxWorkflowSteps} from "../constants.js";
|
|
11
|
+
import {maxExecutionsByStep, maxWorkflowSteps, timeoutVM} from "../constants.js";
|
|
11
12
|
import {ChatOpenAI} from "@langchain/openai";
|
|
12
13
|
import {ChatGoogleGenerativeAI} from "@langchain/google-genai";
|
|
13
14
|
import {ChatPromptTemplate} from "@langchain/core/prompts";
|
|
@@ -180,68 +181,99 @@ export async function scheduleWorkflowTriggers() {
|
|
|
180
181
|
}
|
|
181
182
|
|
|
182
183
|
|
|
183
|
-
|
|
184
|
-
async function executeWithIsolatedVm(actionDef, context) {
|
|
184
|
+
export async function executeSafeJavascript(actionDef, context, user) {
|
|
185
185
|
const code = actionDef.script;
|
|
186
|
-
const
|
|
186
|
+
const collectedLogs = [];
|
|
187
|
+
const isolate = new ivm.Isolate({ memoryLimit: 128 }); // 128MB memory limit
|
|
187
188
|
|
|
188
189
|
try {
|
|
189
190
|
const vmContext = await isolate.createContext();
|
|
190
191
|
const jail = vmContext.global;
|
|
191
192
|
|
|
192
|
-
//
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
193
|
+
// Helper to create a secure reference for our API functions
|
|
194
|
+
const createJailFunction = (fn) => new ivm.Reference(fn);
|
|
195
|
+
|
|
196
|
+
// 1. Build the sandboxed API
|
|
197
|
+
await jail.set('db', createJailFunction({
|
|
198
|
+
create: (modelName, dataObject) => insertData(modelName, dataObject, {}, user),
|
|
199
|
+
find: async (modelName, filter) => {
|
|
200
|
+
const result = await searchData({ user, query: { model: modelName, filter } });
|
|
201
|
+
return new ivm.ExternalCopy(result.data).copyInto();
|
|
202
|
+
},
|
|
203
|
+
findOne: async (modelName, filter) => {
|
|
204
|
+
const result = await searchData({ user, query: { model: modelName, filter, limit: 1 } });
|
|
205
|
+
return new ivm.ExternalCopy(result.data?.[0] || null).copyInto();
|
|
206
|
+
},
|
|
207
|
+
update: (modelName, filter, updateObject) => patchData(modelName, filter, updateObject, {}, user),
|
|
208
|
+
delete: (modelName, filter) => deleteData(modelName, null, filter, user)
|
|
197
209
|
}));
|
|
198
210
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
211
|
+
const createLoggerFunction = (level) => {
|
|
212
|
+
return (...args) => {
|
|
213
|
+
const finalMessage = logger.trace(level, '[VM Script]', ...args);
|
|
214
|
+
collectedLogs.push({
|
|
215
|
+
level: level,
|
|
216
|
+
message: finalMessage,
|
|
217
|
+
timestamp: new Date().toISOString()
|
|
218
|
+
});
|
|
219
|
+
};
|
|
220
|
+
};
|
|
204
221
|
|
|
205
|
-
|
|
206
|
-
|
|
222
|
+
await jail.set('logger', createJailFunction({
|
|
223
|
+
info: createLoggerFunction('info'),
|
|
224
|
+
warn: createLoggerFunction('warn'),
|
|
225
|
+
error: createLoggerFunction('error')
|
|
226
|
+
}));
|
|
227
|
+
|
|
228
|
+
await jail.set('env', createJailFunction({
|
|
229
|
+
get: async (variableName) => {
|
|
230
|
+
if (!variableName) return null;
|
|
231
|
+
const result = await searchData({ user, query: { model: 'env', filter: { name: variableName }, limit: 1 } });
|
|
232
|
+
return new ivm.ExternalCopy(result.data?.[0]?.value || null).copyInto();
|
|
233
|
+
},
|
|
234
|
+
getAll: async () => {
|
|
235
|
+
const result = await searchData({ user, query: { model: 'env' } });
|
|
236
|
+
const envObject = result.data.reduce((acc, v) => {
|
|
237
|
+
acc[v.name] = v.value;
|
|
238
|
+
return acc;
|
|
239
|
+
}, {});
|
|
240
|
+
return new ivm.ExternalCopy(envObject).copyInto();
|
|
241
|
+
}
|
|
242
|
+
}));
|
|
207
243
|
|
|
208
|
-
//
|
|
209
|
-
|
|
244
|
+
// Inject the context data securely
|
|
245
|
+
await jail.set('context', new ivm.ExternalCopy(context).copyInto());
|
|
210
246
|
|
|
211
|
-
//
|
|
212
|
-
|
|
247
|
+
// 2. Prepare and run the script
|
|
248
|
+
// The script is wrapped in an async IIFE to handle promises correctly
|
|
249
|
+
const fullScript = `
|
|
250
|
+
(async () => {
|
|
251
|
+
${code}
|
|
252
|
+
})();
|
|
253
|
+
`;
|
|
254
|
+
|
|
255
|
+
const script = await isolate.compileScript(fullScript);
|
|
256
|
+
const result = await script.run(vmContext, { timeout: timeoutVM, promise: true });
|
|
257
|
+
|
|
258
|
+
// The result is returned as an ExternalCopy, we need to copy it out
|
|
259
|
+
return { success: true, data: result, logs: collectedLogs };
|
|
213
260
|
|
|
214
261
|
} catch (error) {
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
262
|
+
const errorMessage = `Script execution failed: ${error.message}`;
|
|
263
|
+
const finalErrorMessage = logger.trace('critical', `[VM Script] ${errorMessage}\n${error.stack}`);
|
|
264
|
+
collectedLogs.push({
|
|
265
|
+
level: 'critical',
|
|
266
|
+
message: finalErrorMessage,
|
|
267
|
+
timestamp: new Date().toISOString()
|
|
268
|
+
});
|
|
269
|
+
return { success: false, message: errorMessage, logs: collectedLogs };
|
|
218
270
|
} finally {
|
|
219
|
-
//
|
|
271
|
+
// 3. CRUCIAL: Dispose of the isolate to prevent memory leaks
|
|
220
272
|
if (isolate && !isolate.isDisposed) {
|
|
221
273
|
isolate.dispose();
|
|
222
274
|
}
|
|
223
275
|
}
|
|
224
276
|
}
|
|
225
|
-
REQUIRES NODE >=20
|
|
226
|
-
*/
|
|
227
|
-
async function executeSafeJavascript(actionDef, context) {
|
|
228
|
-
const code = actionDef.script;
|
|
229
|
-
const vm = new VM({
|
|
230
|
-
timeout: 1000, // Time out after 1 second
|
|
231
|
-
sandbox: context, // Pass the context object
|
|
232
|
-
console: 'redirect', // Redirect console output
|
|
233
|
-
require: false, // Disable require
|
|
234
|
-
wasm: false // disable WebAssembly
|
|
235
|
-
});
|
|
236
|
-
|
|
237
|
-
try {
|
|
238
|
-
const script = new VMScript(code);
|
|
239
|
-
return vm.run(script);
|
|
240
|
-
} catch (error) {
|
|
241
|
-
console.error("Error executing script:", error);
|
|
242
|
-
throw error; // or return an error object
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
277
|
|
|
246
278
|
/**
|
|
247
279
|
* Handles the 'Webhook' workflow action.
|
|
@@ -734,10 +766,8 @@ export async function executeStepAction(actionDef, contextData, user, dbCollecti
|
|
|
734
766
|
result = await handleSendEmailAction(actionDef, contextData, user);
|
|
735
767
|
break;
|
|
736
768
|
case 'ExecuteScript':
|
|
737
|
-
result = await executeSafeJavascript(actionDef, contextData);
|
|
769
|
+
result = await executeSafeJavascript(actionDef, contextData, user);
|
|
738
770
|
break;
|
|
739
|
-
|
|
740
|
-
// ... autres cases à venir ...
|
|
741
771
|
default:
|
|
742
772
|
logger.error(`[executeStepAction] Unknown action type: ${actionDef.type}`);
|
|
743
773
|
return { success: false, message: `Unknown action type: ${actionDef.type}` };
|
|
@@ -1244,7 +1274,7 @@ export async function processWorkflowRun(workflowRunId, user) {
|
|
|
1244
1274
|
);
|
|
1245
1275
|
|
|
1246
1276
|
let stepSucceeded = true;
|
|
1247
|
-
let
|
|
1277
|
+
let logInfo = null;
|
|
1248
1278
|
let conditionsMet = true;
|
|
1249
1279
|
|
|
1250
1280
|
try {
|
|
@@ -1266,8 +1296,10 @@ export async function processWorkflowRun(workflowRunId, user) {
|
|
|
1266
1296
|
const actionResult = await workflowModule.executeStepAction(actionDef, contextData, user, dbCollection);
|
|
1267
1297
|
if (!actionResult.success) {
|
|
1268
1298
|
stepSucceeded = false;
|
|
1269
|
-
|
|
1299
|
+
logInfo = actionResult.message || `Action ${actionDef.name || actionId} failed.`;
|
|
1270
1300
|
break;
|
|
1301
|
+
}else{
|
|
1302
|
+
logInfo = `Action ${actionDef.name || actionId} : ${actionResult.message}`;
|
|
1271
1303
|
}
|
|
1272
1304
|
if (actionResult.updatedContext) {
|
|
1273
1305
|
contextData = { ...contextData, ...actionResult.updatedContext };
|
|
@@ -1281,7 +1313,7 @@ export async function processWorkflowRun(workflowRunId, user) {
|
|
|
1281
1313
|
} catch (error) {
|
|
1282
1314
|
logger.error(`[processWorkflowRun] Run ID: ${runId}, Step ID: ${currentStepId}: Error during condition/action execution: ${error.message}`);
|
|
1283
1315
|
stepSucceeded = false;
|
|
1284
|
-
|
|
1316
|
+
logInfo = error.message;
|
|
1285
1317
|
}
|
|
1286
1318
|
|
|
1287
1319
|
// --- 9. Détermination de la prochaine étape ---
|
|
@@ -1298,13 +1330,13 @@ export async function processWorkflowRun(workflowRunId, user) {
|
|
|
1298
1330
|
}
|
|
1299
1331
|
} else {
|
|
1300
1332
|
// CHEMIN ÉCHEC/BRANCHE : Une action a échoué OU les conditions n'ont pas été remplies.
|
|
1301
|
-
const reason =
|
|
1333
|
+
const reason = logInfo ? `Action failed: ${logInfo}` : 'Step conditions not met.';
|
|
1302
1334
|
logger.warn(`[processWorkflowRun] Run ID: ${runId}, Step ID: ${currentStepId}: Taking failure/branching path. Reason: ${reason}`);
|
|
1303
1335
|
nextStepId = currentStepDef.onFailureStep;
|
|
1304
1336
|
|
|
1305
1337
|
if (!nextStepId || !isObjectId(nextStepId)) {
|
|
1306
1338
|
// Fin du workflow. Le statut est 'failed' seulement si une vraie erreur s'est produite.
|
|
1307
|
-
finalStatusForRun =
|
|
1339
|
+
finalStatusForRun = logInfo ? 'failed' : 'completed';
|
|
1308
1340
|
nextStepId = null;
|
|
1309
1341
|
}
|
|
1310
1342
|
}
|
|
@@ -1317,9 +1349,7 @@ export async function processWorkflowRun(workflowRunId, user) {
|
|
|
1317
1349
|
updatePayload.status = finalStatusForRun;
|
|
1318
1350
|
updatePayload.completedAt = new Date();
|
|
1319
1351
|
updatePayload.currentStep = null;
|
|
1320
|
-
|
|
1321
|
-
updatePayload.error = stepError;
|
|
1322
|
-
}
|
|
1352
|
+
updatePayload.log = logInfo;
|
|
1323
1353
|
} else {
|
|
1324
1354
|
updatePayload.currentStep = currentStepId;
|
|
1325
1355
|
}
|
|
@@ -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);
|