data-primals-engine 1.2.2 → 1.2.4

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 (54) hide show
  1. package/CONTRIBUTING.md +91 -0
  2. package/README.md +33 -17
  3. package/client/src/App.jsx +0 -5
  4. package/client/src/ConditionBuilder.scss +34 -1
  5. package/client/src/ConditionBuilder2.jsx +179 -53
  6. package/client/src/ContentView.jsx +0 -3
  7. package/client/src/CronBuilder.jsx +0 -1
  8. package/client/src/CronPartBuilder.jsx +0 -2
  9. package/client/src/DashboardView.jsx +0 -5
  10. package/client/src/DataEditor.jsx +8 -211
  11. package/client/src/DataLayout.jsx +0 -1
  12. package/client/src/DataTable.jsx +1 -3
  13. package/client/src/Field.jsx +0 -5
  14. package/client/src/FlexBuilder.jsx +1 -1
  15. package/client/src/ModelCreatorField.jsx +1 -5
  16. package/client/src/RTE.jsx +1 -6
  17. package/client/src/RTETrans.jsx +0 -2
  18. package/client/src/RelationField.jsx +1 -1
  19. package/client/src/RelationValue.jsx +1 -2
  20. package/client/src/TourSpotlight.jsx +0 -2
  21. package/client/src/constants.js +1 -1
  22. package/client/src/filter.js +87 -0
  23. package/client/src/hooks/data.js +1 -3
  24. package/client/src/hooks/useTutorials.jsx +0 -1
  25. package/package.json +3 -3
  26. package/server.js +2 -2
  27. package/src/constants.js +2 -2
  28. package/src/defaultModels.js +1 -14
  29. package/src/email.js +9 -7
  30. package/src/engine.js +60 -20
  31. package/src/events.js +1 -1
  32. package/src/filter.js +221 -0
  33. package/src/index.js +1 -1
  34. package/src/middlewares/middleware-mongodb.js +0 -1
  35. package/src/modules/assistant.js +1 -3
  36. package/src/modules/bucket.js +3 -4
  37. package/src/modules/{data.js → data/data.js} +42 -59
  38. package/src/modules/data/index.js +1 -0
  39. package/src/modules/file.js +1 -1
  40. package/src/modules/mongodb.js +0 -1
  41. package/src/modules/user.js +1 -1
  42. package/src/modules/workflow.js +299 -133
  43. package/src/packs.js +249 -8
  44. package/test/data.backup.integration.test.js +7 -5
  45. package/test/data.integration.test.js +8 -6
  46. package/test/events.test.js +1 -1
  47. package/test/file.test.js +11 -17
  48. package/test/import_export.integration.test.js +38 -27
  49. package/test/model.integration.test.js +20 -21
  50. package/test/user.test.js +32 -25
  51. package/test/vm.test.js +51 -0
  52. package/test/workflow.integration.test.js +22 -14
  53. package/test/workflow.robustness.test.js +19 -9
  54. package/src/modules/test +0 -147
@@ -1,4 +1,3 @@
1
- // Exemple conceptuel dans la fonction de planification
2
1
  import {getCollection, getCollectionForUser, isObjectId} from "./mongodb.js";
3
2
  import schedule from "node-schedule";
4
3
  import {ObjectId} from "mongodb";
@@ -7,15 +6,17 @@ import crypto from "node:crypto";
7
6
  import ivm from 'isolated-vm';
8
7
 
9
8
  import {Logger} from "../gameObject.js";
10
- import {deleteData, insertData, patchData, searchData} from "./data.js";
11
- import {maxExecutionsByStep, maxWorkflowSteps, timeoutVM} from "../constants.js";
9
+ import {deleteData, insertData, patchData, searchData} from "./data/index.js";
10
+ import {emailDefaultConfig, maxExecutionsByStep, maxWorkflowSteps} from "../constants.js";
12
11
  import {ChatOpenAI} from "@langchain/openai";
13
12
  import {ChatGoogleGenerativeAI} from "@langchain/google-genai";
14
13
  import {ChatPromptTemplate} from "@langchain/core/prompts";
14
+ import { ChatDeepSeek } from "@langchain/deepseek";
15
15
  import i18n from "../../src/i18n.js";
16
16
  import {sendEmail} from "../email.js";
17
17
 
18
18
  import * as workflowModule from './workflow.js';
19
+ import util from "node:util";
19
20
 
20
21
  let logger = null;
21
22
  export async function onInit(defaultEngine) {
@@ -180,6 +181,22 @@ export async function scheduleWorkflowTriggers() {
180
181
  }
181
182
  }
182
183
 
184
+ async function handleWaitAction(actionDef, contextData, user) {
185
+ const { duration, durationUnit } = actionDef;
186
+ if (!duration || !durationUnit) {
187
+ return { success: false, message: "Wait action requires 'duration' and 'durationUnit'." };
188
+ }
189
+
190
+ // Retourne un statut spécial que le moteur de workflow comprendra
191
+ return {
192
+ success: true,
193
+ status: 'paused', // Statut spécial
194
+ duration,
195
+ durationUnit,
196
+ message: `Workflow will be paused for ${duration} ${durationUnit}.`
197
+ };
198
+ }
199
+
183
200
 
184
201
  export async function executeSafeJavascript(actionDef, context, user) {
185
202
  const code = actionDef.script;
@@ -190,73 +207,102 @@ export async function executeSafeJavascript(actionDef, context, user) {
190
207
  const vmContext = await isolate.createContext();
191
208
  const jail = vmContext.global;
192
209
 
193
- // Helper to create a secure reference for our API functions
194
- const createJailFunction = (fn) => new ivm.Reference(fn);
210
+ const find = async (modelName, filter) => {
211
+ const result = await searchData({ model: modelName, filter: JSON.parse(filter) }, user);
212
+ return new ivm.ExternalCopy(result).copyInto();
213
+ };
214
+ const findOne = async (modelName, filter) => {
215
+ const result = await searchData({ model: modelName, filter: JSON.parse(filter), limit: 1 }, user);
216
+ return new ivm.ExternalCopy(result.data?.[0] || null).copyInto();
217
+ };
195
218
 
196
219
  // 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({ model: modelName, filter }, user);
201
- return new ivm.ExternalCopy(result.data).copyInto();
202
- },
203
- findOne: async (modelName, filter) => {
204
- const result = await searchData({ model: modelName, filter, limit: 1 }, user);
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, filter, user)
209
- }));
220
+ await jail.set('_db_create', new ivm.Reference((modelName, dataObject) => insertData(modelName, JSON.parse(dataObject), {}, user, false)));
221
+ await jail.set('_db_find', new ivm.Reference(find));
222
+ await jail.set('_db_findOne', new ivm.Reference(findOne));
223
+
224
+ await jail.set('_db_update', new ivm.Reference((modelName, filter, updateObject) => patchData(modelName, JSON.parse(filter), JSON.parse(updateObject), {}, user, false)));
225
+ await jail.set('_db_delete', new ivm.Reference((modelName, filter) => deleteData(modelName, JSON.parse(filter), user, false)));
210
226
 
211
- const createLoggerFunction = (level) => {
227
+ const createLoggerMethod = (level) => {
212
228
  return (...args) => {
213
- const finalMessage = logger.trace(level, '[VM Script]', ...args);
229
+ const message = args.join(' ');
214
230
  collectedLogs.push({
215
- level: level,
216
- message: finalMessage,
231
+ level,
232
+ message,
217
233
  timestamp: new Date().toISOString()
218
234
  });
235
+ logger.trace(level, '[VM Script]', message);
219
236
  };
220
237
  };
221
238
 
222
- await jail.set('logger', createJailFunction({
223
- info: createLoggerFunction('info'),
224
- warn: createLoggerFunction('warn'),
225
- error: createLoggerFunction('error')
239
+ await jail.set('_log_info', createLoggerMethod('info'));
240
+ await jail.set('_log_warn', createLoggerMethod('warn'));
241
+ await jail.set('_log_error', createLoggerMethod('error'));
242
+ await jail.set('_env_get', new ivm.Reference(async (variableName) => {
243
+ if (!variableName) return null;
244
+ const result = await searchData({ model: 'env', filter: { name: variableName }, limit: 1 }, user);
245
+ return new ivm.ExternalCopy(result.data?.[0]?.value || null).copyInto();
226
246
  }));
227
-
228
- await jail.set('env', createJailFunction({
229
- get: async (variableName) => {
230
- if (!variableName) return null;
231
- const result = await searchData({ model: 'env', filter: { name: variableName }, limit: 1 }, user);
232
- return new ivm.ExternalCopy(result.data?.[0]?.value || null).copyInto();
233
- },
234
- getAll: async () => {
235
- const result = await searchData({ model: 'env' }, user);
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
- }
247
+ await jail.set('_env_get_all', new ivm.Reference(async () => {
248
+ const result = await searchData({ model: 'env' }, user);
249
+ const envObject = result.data.reduce((acc, v) => {
250
+ acc[v.name] = v.value;
251
+ return acc;
252
+ }, {});
253
+ return new ivm.ExternalCopy(envObject).copyInto();
242
254
  }));
243
255
 
244
- // Inject the context data securely
245
- await jail.set('context', new ivm.ExternalCopy(context).copyInto());
256
+ // Contexte sécurisé
257
+ const safeContext = JSON.parse(JSON.stringify(context));
258
+ await jail.set('context', new ivm.ExternalCopy(safeContext).copyInto());
246
259
 
247
- // 2. Prepare and run the script
248
- // The script is wrapped in an async IIFE to handle promises correctly
260
+ // Exécution
249
261
  const fullScript = `
250
- (async () => {
262
+ const normalizeArgs = args => args.map(arg => {
263
+ if (typeof arg === 'object' && arg !== null) {
264
+ return JSON.stringify(arg); // Convert objects to strings
265
+ }
266
+ return arg;
267
+ });
268
+ const db = {
269
+ create: (...args) => _db_create.applySyncPromise(null, normalizeArgs(args)),
270
+ find: (...args) => _db_find.applySyncPromise(null, normalizeArgs(args)),
271
+ findOne: (...args) => _db_findOne.applySyncPromise(null, normalizeArgs(args)),
272
+ update: (...args) => _db_update.applySyncPromise(null, normalizeArgs(args)),
273
+ delete: (...args) => _db_delete.applySyncPromise(null, normalizeArgs(args))
274
+ };
275
+
276
+ const logger = {
277
+ info: _log_info,
278
+ warn: _log_warn,
279
+ error: _log_error
280
+ };
281
+
282
+ const env = {
283
+ get: _env_get,
284
+ getAll: _env_get_all
285
+ };
286
+
287
+ (async function() {
251
288
  ${code}
252
289
  })();
253
290
  `;
254
291
 
255
- const script = await isolate.compileScript(fullScript);
256
- const result = await script.run(vmContext, { timeout: timeoutVM, promise: true });
292
+ const TIMEOUT = 5000;
293
+ const script = await isolate.compileScript(fullScript, { timeout: TIMEOUT });
294
+ const result = await script.run(vmContext, {
295
+ timeout: TIMEOUT,
296
+ promise: true,
297
+ copy: true // Copie automatique du résultat
298
+ });
257
299
 
258
- // The result is returned as an ExternalCopy, we need to copy it out
259
- return { success: true, data: result, logs: collectedLogs };
300
+ return {
301
+ success: true,
302
+ data: result,
303
+ logs: collectedLogs,
304
+ updatedContext: { result }
305
+ };
260
306
 
261
307
  } catch (error) {
262
308
  const errorMessage = `Script execution failed: ${error.message}`;
@@ -485,7 +531,7 @@ async function handleCreateDataAction(actionDef, contextData, user, dbCollection
485
531
  logger.debug('Final data object after substitution:', dataObject);
486
532
 
487
533
  // 3. Appeler insertData avec l'objet correctement substitué
488
- const result = await insertData(targetModel, dataObject, [], user, true, true); // On attend la fin du workflow déclenché par cette création
534
+ const result = await insertData(targetModel, dataObject, [], user, false, true); // On attend la fin du workflow déclenché par cette création
489
535
 
490
536
  if (result.success) {
491
537
  return { success: true, insertedIds: result.insertedIds };
@@ -607,16 +653,7 @@ async function handleUpdateDataAction(actionDef, contextData, user) {
607
653
  selectorObject,
608
654
  updatesObject,
609
655
  {},
610
- user
611
- );
612
-
613
-
614
- console.log(
615
- targetModel,
616
- selectorObject,
617
- updatesObject,
618
- {},
619
- user
656
+ user, false
620
657
  );
621
658
 
622
659
  // 6. Return result
@@ -626,7 +663,10 @@ async function handleUpdateDataAction(actionDef, contextData, user) {
626
663
  success: true,
627
664
  modifiedCount: updateResult.modifiedCount,
628
665
  matchedCount: updateResult.matchedCount,
629
- message: updateResult.message
666
+ message: updateResult.message,
667
+ updatedContext: {
668
+ triggerData: {...contextData.triggerData || {}, ...updatesObject}
669
+ }
630
670
  };
631
671
  } else {
632
672
  // updateData now throws errors, so this 'else' might not be reached often,
@@ -765,6 +805,9 @@ export async function executeStepAction(actionDef, contextData, user, dbCollecti
765
805
  case 'SendEmail':
766
806
  result = await handleSendEmailAction(actionDef, contextData, user);
767
807
  break;
808
+ case 'Wait':
809
+ result = await handleWaitAction(actionDef, contextData, user);
810
+ break;
768
811
  case 'ExecuteScript':
769
812
  result = await executeSafeJavascript(actionDef, contextData, user);
770
813
  break;
@@ -968,6 +1011,9 @@ export async function substituteVariables(template, contextData, user) {
968
1011
  // 5. Logique de résolution de valeur améliorée avec resolvePathValue
969
1012
  const findValue = async (key) => {
970
1013
  let path = key.trim();
1014
+ if (path.startsWith('context.')) {
1015
+ path = path.substring('context.'.length);
1016
+ }
971
1017
  if (path.endsWith('._id')) {
972
1018
  const basePath = path.slice(0, -4);
973
1019
  const value = await findValue(basePath);
@@ -1012,7 +1058,20 @@ export async function substituteVariables(template, contextData, user) {
1012
1058
  if (singlePlaceholderMatch) {
1013
1059
  const key = singlePlaceholderMatch[1];
1014
1060
  const value = await findValue(key);
1015
- return value !== undefined ? value : template;
1061
+
1062
+ if (value === undefined) {
1063
+ return template; // Placeholder not found, return as is.
1064
+ }
1065
+
1066
+ // If the resolved value is a string, it might contain more placeholders.
1067
+ // We recursively call substituteVariables on it, but only if it's different
1068
+ // from the original template to prevent infinite loops.
1069
+ if (typeof value === 'string' && value !== template) {
1070
+ return substituteVariables(value, contextData, user);
1071
+ }
1072
+
1073
+ // For non-string values or if value is same as template, return the value.
1074
+ return value;
1016
1075
  }
1017
1076
 
1018
1077
  // CAS B : La chaîne contient plusieurs placeholders ou mix texte/variables
@@ -1107,17 +1166,15 @@ export async function triggerWorkflows(triggerData, user, eventType) {
1107
1166
  try {
1108
1167
  const finalFilter = {
1109
1168
  '$and': [
1169
+ {"_id": { "$toObjectId": triggerData._id.toString() }},
1110
1170
  dataFilterCondition // Applique la condition du trigger
1111
1171
  ]
1112
1172
  };
1113
1173
 
1114
- console.debug(`[Workflow Trigger] Vérification dataFilter pour trigger ${trigger._id} avec filtre combiné:`, JSON.stringify(finalFilter));
1115
1174
 
1116
- // Exécuter la vérification dans la base de données
1117
- // Utilisation de countDocuments pour une vérification rapide
1118
- const matchCount = await searchData({ model: targetModelName, filter: finalFilter, limit: 1 }, user);
1119
-
1120
- if (!matchCount.count) {
1175
+ console.debug(`[Workflow Trigger] Vérification dataFilter pour trigger ${trigger._id} avec filtre combiné:`, JSON.stringify(finalFilter));
1176
+ const match = await searchData({ model: triggerData._model, filter: finalFilter, limit: 1 }, user);
1177
+ if (!match.count) {
1121
1178
  console.debug(`[Workflow Trigger] Trigger ${trigger._id}: dataFilter non satisfait par la donnée ${dataId}. WorkflowRun non créé.`);
1122
1179
  continue; // Passer au trigger suivant
1123
1180
  } else {
@@ -1277,9 +1334,16 @@ export async function processWorkflowRun(workflowRunId, user) {
1277
1334
  let conditionsMet = true;
1278
1335
 
1279
1336
  try {
1337
+ // Add logging to see the actual pipeline being executed
1338
+ logger.debug('Executing pipeline:', JSON.stringify(await substituteVariables(currentStepDef.conditions, contextData, user), null, 2));
1339
+
1340
+ // And log the context data to verify processedChunk exists
1341
+ logger.debug('Context data:', JSON.stringify(contextData, null, 2));
1342
+
1280
1343
  // --- 7. Évaluation des conditions de l'étape ---
1281
1344
  if (currentStepDef.conditions && Object.keys(currentStepDef.conditions).length > 0) {
1282
- const searchResult = await searchData({ model: contextData.triggerDataModel, filter: currentStepDef.conditions, limit: 1}, user);
1345
+ const substitutedConditions = await substituteVariables(currentStepDef.conditions, contextData, user);
1346
+ const searchResult = await searchData({ model: contextData.triggerDataModel, filter: substitutedConditions, limit: 1}, user);
1283
1347
  conditionsMet = searchResult && searchResult.count > 0;
1284
1348
  logger.info(`[processWorkflowRun] Run ID: ${runId}, Step ID: ${currentStepId}: Conditions evaluated. Found ${searchResult ? searchResult.count : 0} match(es). Result: ${conditionsMet}`);
1285
1349
  }
@@ -1293,6 +1357,39 @@ export async function processWorkflowRun(workflowRunId, user) {
1293
1357
  const actionDef = await dbCollection.findOne({ _id: new ObjectId(actionId), _model: 'workflowAction' });
1294
1358
  if (!actionDef) return await logError(`Action definition ${actionId} not found.`);
1295
1359
  const actionResult = await workflowModule.executeStepAction(actionDef, contextData, user, dbCollection);
1360
+
1361
+ if (actionResult.status === 'paused') {
1362
+ // L'action demande une pause !
1363
+ const { duration, durationUnit } = actionResult;
1364
+ const now = new Date();
1365
+ let resumeAt = new Date(now);
1366
+
1367
+ // Calculer la date de reprise
1368
+ const ms = { seconds: 1000, minutes: 60000, hours: 3600000, days: 86400000 };
1369
+ resumeAt.setTime(now.getTime() + (duration * ms[durationUnit]));
1370
+
1371
+ logger.info(`[processWorkflowRun] Run ID: ${runId} is pausing. Will resume at: ${resumeAt.toISOString()}`);
1372
+
1373
+ // Mettre à jour le workflowRun avec le statut 'paused' et la date de reprise
1374
+ await dbCollection.updateOne({ _id: runId }, {
1375
+ $set: {
1376
+ status: 'paused',
1377
+ currentStep: currentStepDef.onSuccessStep, // On prépare la prochaine étape
1378
+ contextData,
1379
+ log: actionResult.message
1380
+ }
1381
+ });
1382
+
1383
+ // Planifier le réveil du workflow
1384
+ schedule.scheduleJob(resumeAt, async () => {
1385
+ logger.info(`[Scheduler] Waking up paused workflowRun ID: ${runId}`);
1386
+ // On relance le traitement pour ce workflow spécifique
1387
+ await workflowModule.processWorkflowRun(runId, user);
1388
+ });
1389
+
1390
+ // Arrêter le traitement actuel de cette exécution
1391
+ return; // Très important de stopper la boucle ici
1392
+ }
1296
1393
  if (!actionResult.success) {
1297
1394
  stepSucceeded = false;
1298
1395
  logInfo = actionResult.message || `Action ${actionDef.name || actionId} failed.`;
@@ -1303,6 +1400,7 @@ export async function processWorkflowRun(workflowRunId, user) {
1303
1400
  if (actionResult.updatedContext) {
1304
1401
  contextData = { ...contextData, ...actionResult.updatedContext };
1305
1402
  }
1403
+ //console.log("action", util.inspect(actionResult, false, 8, true));
1306
1404
  logger.info(`[processWorkflowRun] Run ID: ${runId}, Step ID: ${currentStepId}, Action ID: ${actionId}: Executed successfully.`);
1307
1405
  }
1308
1406
  }
@@ -1362,161 +1460,229 @@ export async function processWorkflowRun(workflowRunId, user) {
1362
1460
  logger.error(`[processWorkflowRun] Critical error during processing of workflowRun ID: ${runId}. Error: ${error.message}`, error.stack);
1363
1461
  await dbCollection.updateOne(
1364
1462
  { _id: runId, status: { $nin: ['completed', 'failed', 'cancelled'] } },
1365
- { $set: { status: 'failed', error: `Critical error: ${error.message}`, completedAt: new Date(), stepExecutionsCount } }
1463
+ { $set: { status: 'failed', log: `Critical error: ${error.message}`, completedAt: new Date(), stepExecutionsCount } }
1366
1464
  );
1367
1465
  }
1368
1466
  }
1369
1467
  /**
1370
- * Exécute une action de génération de contenu par IA ('GenerateAIContent').
1371
- * Récupère la clé API (priorité à l'environnement de l'utilisateur), initialise un client LangChain,
1372
- * formate un prompt avec les données du contexte, appelle le LLM, et retourne le résultat
1373
- * pour l'ajouter au contexte du workflow.
1468
+ * Executes an AI content generation action ('GenerateAIContent').
1469
+ * Retrieves the API key (prioritizing the user's environment), initializes a LangChain client,
1470
+ * formats a prompt with context data, calls the LLM, and returns the result
1471
+ * to be added to the workflow context.
1374
1472
  *
1375
- * @param {object} action - La définition de l'action depuis le workflow.
1376
- * @param {object} context - Le contexte d'exécution actuel du workflow.
1377
- * @param {object} user - L'utilisateur qui exécute le workflow.
1473
+ * @param {object} action - The action definition from the workflow.
1474
+ * @param {object} context - The current workflow execution context.
1475
+ * @param {object} user - The user executing the workflow.
1378
1476
  * @returns {Promise<{success: boolean, updatedContext?: object, message?: string}>}
1379
1477
  */
1380
1478
  async function executeGenerateAIContentAction(action, context, user) {
1381
1479
  const { aiProvider, aiModel, prompt } = action;
1382
1480
 
1383
- // 1. Récupérer la clé API (Environnement de l'utilisateur > Environnement de la machine)
1481
+ // 1. Retrieve the API key (User Environment > Machine Environment)
1384
1482
  let apiKey;
1385
1483
 
1386
1484
  const providers = {
1387
1485
  "OpenAI" : "OPENAI_API_KEY",
1388
- "Google": "GOOGLE_API_KEY"
1486
+ "Google": "GOOGLE_API_KEY",
1487
+ "DeepSeek": "DEEPSEEK_API_KEY"
1389
1488
  }
1390
1489
  const envKeyName = providers[aiProvider];
1391
1490
  if( !envKeyName ) {
1392
- return {success: false, message: i18n.t('aiContent.env', `Clé API pour ${aiProvider} (${envKeyName}) non trouvée dans l'environnement de l'utilisateur.`)};
1491
+ return {success: false, message: i18n.t('aiContent.env', `API key for provider ${aiProvider} (${envKeyName}) not found in user environment.`)};
1393
1492
  }
1394
1493
 
1395
- // Cherche d'abord dans les variables d'environnement de l'utilisateur
1494
+ // First look in the user's environment variables
1396
1495
  const envCollection = await getCollectionForUser(user);
1397
1496
  const userEnvVar = await envCollection.findOne({ _model: 'env', name: envKeyName, _user: user.username });
1398
1497
 
1399
1498
  if (userEnvVar && userEnvVar.value) {
1400
1499
  apiKey = userEnvVar.value;
1401
- logger.debug(`[AI Action] Utilisation de la clé API de l'environnement de l'utilisateur pour ${aiProvider}.`);
1500
+ logger.debug(`[AI Action] Using user environment API key for ${aiProvider}.`);
1402
1501
  } else {
1403
1502
  apiKey = process.env[envKeyName];
1404
- logger.debug(`[AI Action] Utilisation de la clé API de l'environnement de la machine pour ${aiProvider}.`);
1503
+ logger.debug(`[AI Action] Using machine environment API key for ${aiProvider}.`);
1405
1504
  }
1406
1505
 
1407
1506
  if (!apiKey) {
1408
- const message = `Clé API pour ${aiProvider} (${envKeyName}) non trouvée dans l'environnement de l'utilisateur ou de la machine.`;
1507
+ const message = `API key for ${aiProvider} (${envKeyName}) not found in user or machine environment.`;
1409
1508
  logger.error(`[AI Action] ${message}`);
1410
1509
  return { success: false, message };
1411
1510
  }
1412
1511
 
1413
- // 2. Initialiser le client LLM avec LangChain
1512
+ // 2. Initialize the LLM client with LangChain
1414
1513
  let llm;
1415
1514
  try {
1416
1515
  switch (aiProvider) {
1417
1516
  case 'OpenAI':
1418
- llm = new ChatOpenAI({ apiKey, modelName: aiModel, temperature: 0.7 });
1517
+ llm = new ChatOpenAI({ apiKey, model: aiModel, temperature: 0.7 });
1518
+ break;
1519
+ case 'Google':
1520
+ llm = new ChatGoogleGenerativeAI({ apiKey, model: aiModel, temperature: 0.7 });
1419
1521
  break;
1420
- case 'GoogleGemini':
1421
- llm = new ChatGoogleGenerativeAI({ apiKey, modelName: aiModel, temperature: 0.7 });
1522
+ case 'DeepSeek':
1523
+ llm = new ChatDeepSeek({ apiKey, model: aiModel, temperature: 0.7 });
1422
1524
  break;
1423
1525
  default:
1424
- throw new Error(`Fournisseur IA non supporté : ${aiProvider}`);
1526
+ throw new Error(`Unsupported AI provider: ${aiProvider}`);
1425
1527
  }
1426
1528
  } catch (initError) {
1427
- const message = `Échec de l'initialisation du client IA pour ${aiProvider}: ${initError.message}`;
1529
+ const message = `Failed to initialize AI client for ${aiProvider}: ${initError.message}`;
1428
1530
  logger.error(`[AI Action] ${message}`);
1429
1531
  return { success: false, message };
1430
1532
  }
1431
1533
 
1432
1534
  try {
1433
1535
  const substitutedPrompt = await substituteVariables(prompt, context, user);
1434
- // 3. Créer le "Prompt Template"
1435
- // LangChain gère la substitution des variables comme {triggerData.name}
1536
+ // 3. Create the "Prompt Template"
1537
+ // LangChain handles variable substitution like {triggerData.name}
1436
1538
  const realPrompt = ChatPromptTemplate.fromTemplate(substitutedPrompt);
1437
1539
 
1438
- // 4. Créer la chaîne de traitement (Prompt + Modèle)
1540
+ // 4. Create the processing chain (Prompt + Model)
1439
1541
  const chain = realPrompt.pipe(llm);
1440
1542
 
1441
- // 5. Invoquer la chaîne avec le contexte complet
1442
- // LangChain remplacera automatiquement les placeholders dans le prompt.
1443
- logger.debug(`[AI Action] Invocation de l'IA avec le modèle ${aiModel}.`);
1543
+ // 5. Invoke the chain with the complete context
1544
+ // LangChain will automatically replace placeholders in the prompt.
1545
+ logger.debug(`[AI Action] Invoking AI with model ${aiModel}.`);
1444
1546
  const response = await chain.invoke(context);
1445
1547
 
1446
- // 6. Préparer le résultat pour le fusionner dans le contexte du workflow
1548
+ // 6. Prepare the result to be merged into the workflow context
1447
1549
  const llmOutput = response.content;
1448
1550
  const outputVariable = 'aiContent';
1449
1551
  const updatedContext = {
1450
1552
  [outputVariable]: llmOutput
1451
1553
  };
1452
1554
 
1453
- logger.info(`[AI Action] Contenu généré avec succès et stocké dans la variable de contexte '${outputVariable}'.`);
1555
+ logger.info(`[AI Action] Content generated successfully and stored in context variable '${outputVariable}'.`);
1454
1556
 
1455
1557
  return {
1456
1558
  success: true,
1457
- updatedContext // Cet objet sera fusionné au contexte principal par le moteur de workflow
1559
+ updatedContext // This object will be merged into the main context by the workflow engine
1458
1560
  };
1459
1561
 
1460
1562
  } catch (llmError) {
1461
- const message = `Erreur durant la génération de contenu IA avec ${aiProvider}: ${llmError.message}`;
1563
+ const message = `Error during AI content generation with ${aiProvider}: ${llmError.message}`;
1462
1564
  logger.error(`[AI Action] ${message}`, llmError.stack);
1463
1565
  return { success: false, message };
1464
1566
  }
1465
1567
  }
1466
1568
 
1467
-
1468
1569
  /**
1469
- * Gère l'action d'envoi d'email d'un workflow.
1470
- * @param {object} action - L'objet workflowAction.
1471
- * @param {object} triggerData - Les données qui ont déclenché le workflow.
1570
+ * Gère l'action d'envoi d'e-mail d'un workflow.
1571
+ * Cette version améliorée peut traiter une liste de destinataires, en envoyant un e-mail
1572
+ * individuel et personnalisé à chacun. Elle gère les placeholders dans le sujet et le corps
1573
+ * de l'e-mail en se basant sur le contexte de chaque destinataire.
1574
+ *
1575
+ * @param {object} action - La définition de l'action 'SendEmail'.
1576
+ * @param {object} contextData - Le contexte d'exécution actuel du workflow.
1472
1577
  * @param {object} user - L'utilisateur propriétaire du workflow.
1578
+ * @returns {Promise<{success: boolean, message: string, data?: {sent: string[], failed: any[]}}>}
1473
1579
  */
1474
- async function handleSendEmailAction(action, triggerData, user) {
1475
-
1476
- logger.info(`[Workflow] Exécution de l'action sendEmail pour l'utilisateur ${user.username}.`);
1580
+ async function handleSendEmailAction(action, contextData, user) {
1581
+ logger.info(`[handleSendEmailAction] Executing for user ${user.username}.`);
1477
1582
 
1478
1583
  // 1. Récupérer la configuration SMTP depuis le modèle 'env' de l'utilisateur
1479
1584
  const envVars = await searchData({
1480
- model: 'env', limit: 100 } // Limite raisonnable pour les variables d'env
1481
- , user);
1482
-
1483
- if (!envVars.data || envVars.data.length === 0) {
1484
- throw new Error("Aucune variable d'environnement (modèle 'env') trouvée pour la configuration SMTP.");
1485
- }
1585
+ model: 'env',
1586
+ filter: { $in: ['$name', ['SMTP_HOST', 'SMTP_PORT', 'SMTP_USER', 'SMTP_PASS', 'SMTP_FROM']] }
1587
+ }, user);
1486
1588
 
1487
1589
  const smtpConfig = envVars.data.reduce((acc, variable) => {
1488
- if (['SMTP_HOST', 'SMTP_PORT', 'SMTP_USER', 'SMTP_PASS', 'SMTP_FROM'].includes(variable.name)) {
1489
- acc[variable.name.replace('SMTP_', '').toLowerCase()] = variable.value;
1490
- }
1590
+ acc[variable.name.replace('SMTP_', '').toLowerCase()] = variable.value;
1491
1591
  return acc;
1492
1592
  }, {});
1593
+ if( !smtpConfig.port )
1594
+ smtpConfig.port = emailDefaultConfig.port;
1493
1595
 
1494
- // 2. Extraire la configuration de l'action et résoudre les placeholders
1596
+ // 2. Valider la configuration de l'action
1495
1597
  const { emailRecipients, emailSubject, emailContent } = action;
1496
1598
  if (!emailRecipients || !emailSubject || !emailContent) {
1497
- throw new Error("SendEmail incomplete (emailRecipients, emailSubject, emailContent are needed).");
1599
+ const msg = "SendEmail action is incomplete. 'emailRecipients', 'emailSubject', and 'emailContent' are required.";
1600
+ logger.error(`[handleSendEmailAction] ${msg}`);
1601
+ return { success: false, message: msg };
1498
1602
  }
1499
1603
 
1500
1604
  try {
1501
- const context = { data: triggerData, user }; // Contexte pour la résolution des placeholders
1605
+ // 3. Résoudre la liste des destinataires. Peut être un placeholder qui retourne un tableau.
1606
+ let resolvedRecipients = await substituteVariables(emailRecipients, contextData, user);
1502
1607
 
1503
- const rto = (await Promise.all(emailRecipients.map(r => substituteVariables(r, context, user))));
1504
- const rsubject = await substituteVariables(emailSubject, context, user);
1505
- const rbody = await substituteVariables(emailContent, context, user);
1608
+ // S'assurer que nous avons toujours un tableau à parcourir
1609
+ if (!Array.isArray(resolvedRecipients)) {
1610
+ resolvedRecipients = [resolvedRecipients];
1611
+ }
1506
1612
 
1507
- // 3. Préparer les données pour sendEmail
1508
- const emailData = {
1509
- title: rsubject,
1510
- content: rbody
1511
- };
1613
+ resolvedRecipients = resolvedRecipients.flat();
1614
+
1615
+ if (resolvedRecipients.length === 0) {
1616
+ return { success: true, message: "No recipients found after substitution. Nothing to send." };
1617
+ }
1512
1618
 
1513
- await sendEmail(rto, emailData, smtpConfig, user.lang);
1514
- logger.info(`[Workflow] Action sendEmail terminée avec succès pour le destinataire: ${rto}`);
1619
+ logger.info(`[handleSendEmailAction] Preparing to send emails to ${resolvedRecipients.length} recipient(s).`);
1515
1620
 
1516
- return { success: true, message: `Email sent to ${rto.join(', ')}` };
1621
+ const allPromises = [];
1622
+ const sentTo = [];
1623
+ const failedFor = [];
1624
+
1625
+ // 4. Itérer sur chaque destinataire pour envoyer un e-mail personnalisé
1626
+ for (const recipient of resolvedRecipients) {
1627
+ // Le destinataire peut être une simple chaîne (email) ou un objet { email: '...', nom: '...' }
1628
+ const recipientEmail = typeof recipient === 'object' && recipient !== null ? recipient.email : recipient;
1629
+
1630
+ if (!recipientEmail || typeof recipientEmail !== 'string') {
1631
+ logger.warn(`[handleSendEmailAction] Skipping an invalid recipient entry:`, recipient);
1632
+ failedFor.push(recipient); // Garder une trace de l'entrée invalide
1633
+ continue;
1634
+ }
1635
+
1636
+
1637
+ // 5. Créer un contexte personnalisé pour ce destinataire spécifique
1638
+ // Cela permet d'utiliser des placeholders comme {recipient.name}
1639
+ const personalizedContext = { ...contextData, recipient };
1640
+
1641
+ // 6. Substituer les variables dans le sujet et le contenu pour ce destinataire
1642
+ const personalizedSubject = await substituteVariables(emailSubject, personalizedContext, user);
1643
+ const personalizedBody = await substituteVariables(emailContent, personalizedContext, user);
1644
+
1645
+ const emailData = { title: personalizedSubject, content: personalizedBody };
1646
+
1647
+ // 7. Envoyer l'e-mail et suivre son résultat
1648
+ const sendPromise = sendEmail([recipientEmail], emailData, smtpConfig, user.lang)
1649
+ .then(() => {
1650
+ sentTo.push(recipient);
1651
+ })
1652
+ .catch(err => {
1653
+ logger.error(`[handleSendEmailAction] Failed to send email to ${recipientEmail}: ${err.message}`);
1654
+ failedFor.push({ recipient: recipientEmail, error: err.message });
1655
+ });
1656
+
1657
+ allPromises.push(sendPromise);
1658
+ }
1659
+
1660
+ // Attendre que toutes les tentatives d'envoi soient terminées
1661
+ await Promise.all(allPromises);
1662
+
1663
+ const summaryMessage = `Email process completed. Sent: ${sentTo.length}. Failed: ${failedFor.length}.`;
1664
+ logger.info(`[handleSendEmailAction] ${summaryMessage}`);
1665
+
1666
+ // L'action elle-même a réussi, même si certains e-mails ont échoué.
1667
+ // Le message de retour et les données fournissent les détails.
1668
+ return {
1669
+ success: true,
1670
+ message: summaryMessage,
1671
+ data: {
1672
+ sent: sentTo,
1673
+ failed: failedFor
1674
+ },
1675
+ updatedContext: {
1676
+ emailResult: {
1677
+ sent: sentTo,
1678
+ failed: failedFor
1679
+ }
1680
+ }
1681
+ };
1517
1682
 
1518
1683
  } catch (error) {
1519
- logger.error(`[handleSendEmailAction] Erreur lors de l'envoi de l'email : ${error.message}`, error.stack);
1520
- return { success: false, message: error.message };
1684
+ const msg = `[handleSendEmailAction] Unexpected error during email processing: ${error.message}`;
1685
+ logger.error(msg, error.stack);
1686
+ return { success: false, message: msg };
1521
1687
  }
1522
1688
  }