data-primals-engine 1.2.1 → 1.2.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 +69 -9
- package/client/src/DataEditor.jsx +1 -202
- package/client/src/DataLayout.jsx +4 -21
- package/client/src/DataTable.jsx +4 -2
- package/client/src/RestoreConfirmationModal.jsx +0 -137
- package/client/src/constants.js +1 -1
- package/client/src/hooks/useTutorials.jsx +3 -3
- package/package.json +2 -2
- package/src/constants.js +2 -2
- package/src/data.js +4 -4
- package/src/defaultModels.js +1 -14
- package/src/email.js +8 -6
- package/src/engine.js +12 -8
- package/src/events.js +26 -4
- package/src/filter.js +221 -0
- package/src/modules/bucket.js +115 -5
- package/src/modules/data.js +174 -101
- package/src/modules/file.js +98 -57
- package/src/modules/test +147 -0
- package/src/modules/workflow.js +266 -100
- package/src/packs.js +245 -7
- package/test/data.backup.integration.test.js +5 -2
- package/test/data.integration.test.js +10 -4
- package/test/events.test.js +202 -0
- package/test/file.test.js +193 -0
- package/test/import_export.integration.test.js +23 -19
- package/test/model.integration.test.js +20 -19
- package/test/user.test.js +30 -23
- package/test/vm.test.js +51 -0
- package/test/workflow.integration.test.js +6 -1
- package/test/workflow.robustness.test.js +6 -1
package/src/modules/workflow.js
CHANGED
|
@@ -8,7 +8,7 @@ import ivm from 'isolated-vm';
|
|
|
8
8
|
|
|
9
9
|
import {Logger} from "../gameObject.js";
|
|
10
10
|
import {deleteData, insertData, patchData, searchData} from "./data.js";
|
|
11
|
-
import {maxExecutionsByStep, maxWorkflowSteps, timeoutVM} from "../constants.js";
|
|
11
|
+
import {emailDefaultConfig, maxExecutionsByStep, maxWorkflowSteps, timeoutVM} from "../constants.js";
|
|
12
12
|
import {ChatOpenAI} from "@langchain/openai";
|
|
13
13
|
import {ChatGoogleGenerativeAI} from "@langchain/google-genai";
|
|
14
14
|
import {ChatPromptTemplate} from "@langchain/core/prompts";
|
|
@@ -16,6 +16,8 @@ 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 {isConditionMet} from "../filter.js";
|
|
20
|
+
import util from "node:util";
|
|
19
21
|
|
|
20
22
|
let logger = null;
|
|
21
23
|
export async function onInit(defaultEngine) {
|
|
@@ -180,6 +182,22 @@ export async function scheduleWorkflowTriggers() {
|
|
|
180
182
|
}
|
|
181
183
|
}
|
|
182
184
|
|
|
185
|
+
async function handleWaitAction(actionDef, contextData, user) {
|
|
186
|
+
const { duration, durationUnit } = actionDef;
|
|
187
|
+
if (!duration || !durationUnit) {
|
|
188
|
+
return { success: false, message: "Wait action requires 'duration' and 'durationUnit'." };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Retourne un statut spécial que le moteur de workflow comprendra
|
|
192
|
+
return {
|
|
193
|
+
success: true,
|
|
194
|
+
status: 'paused', // Statut spécial
|
|
195
|
+
duration,
|
|
196
|
+
durationUnit,
|
|
197
|
+
message: `Workflow will be paused for ${duration} ${durationUnit}.`
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
183
201
|
|
|
184
202
|
export async function executeSafeJavascript(actionDef, context, user) {
|
|
185
203
|
const code = actionDef.script;
|
|
@@ -190,73 +208,102 @@ export async function executeSafeJavascript(actionDef, context, user) {
|
|
|
190
208
|
const vmContext = await isolate.createContext();
|
|
191
209
|
const jail = vmContext.global;
|
|
192
210
|
|
|
193
|
-
|
|
194
|
-
|
|
211
|
+
const find = async (modelName, filter) => {
|
|
212
|
+
const result = await searchData({ model: modelName, filter: JSON.parse(filter) }, user);
|
|
213
|
+
return new ivm.ExternalCopy(result).copyInto();
|
|
214
|
+
};
|
|
215
|
+
const findOne = async (modelName, filter) => {
|
|
216
|
+
const result = await searchData({ model: modelName, filter: JSON.parse(filter), limit: 1 }, user);
|
|
217
|
+
return new ivm.ExternalCopy(result.data?.[0] || null).copyInto();
|
|
218
|
+
};
|
|
195
219
|
|
|
196
220
|
// 1. Build the sandboxed API
|
|
197
|
-
await jail.set('
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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
|
-
}));
|
|
221
|
+
await jail.set('_db_create', new ivm.Reference((modelName, dataObject) => insertData(modelName, JSON.parse(dataObject), {}, user, false)));
|
|
222
|
+
await jail.set('_db_find', new ivm.Reference(find));
|
|
223
|
+
await jail.set('_db_findOne', new ivm.Reference(findOne));
|
|
224
|
+
|
|
225
|
+
await jail.set('_db_update', new ivm.Reference((modelName, filter, updateObject) => patchData(modelName, JSON.parse(filter), JSON.parse(updateObject), {}, user, false)));
|
|
226
|
+
await jail.set('_db_delete', new ivm.Reference((modelName, filter) => deleteData(modelName, JSON.parse(filter), user, false)));
|
|
210
227
|
|
|
211
|
-
const
|
|
228
|
+
const createLoggerMethod = (level) => {
|
|
212
229
|
return (...args) => {
|
|
213
|
-
const
|
|
230
|
+
const message = args.join(' ');
|
|
214
231
|
collectedLogs.push({
|
|
215
|
-
level
|
|
216
|
-
message
|
|
232
|
+
level,
|
|
233
|
+
message,
|
|
217
234
|
timestamp: new Date().toISOString()
|
|
218
235
|
});
|
|
236
|
+
logger.trace(level, '[VM Script]', message);
|
|
219
237
|
};
|
|
220
238
|
};
|
|
221
239
|
|
|
222
|
-
await jail.set('
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
240
|
+
await jail.set('_log_info', createLoggerMethod('info'));
|
|
241
|
+
await jail.set('_log_warn', createLoggerMethod('warn'));
|
|
242
|
+
await jail.set('_log_error', createLoggerMethod('error'));
|
|
243
|
+
await jail.set('_env_get', new ivm.Reference(async (variableName) => {
|
|
244
|
+
if (!variableName) return null;
|
|
245
|
+
const result = await searchData({ model: 'env', filter: { name: variableName }, limit: 1 }, user);
|
|
246
|
+
return new ivm.ExternalCopy(result.data?.[0]?.value || null).copyInto();
|
|
226
247
|
}));
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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
|
-
}
|
|
248
|
+
await jail.set('_env_get_all', new ivm.Reference(async () => {
|
|
249
|
+
const result = await searchData({ model: 'env' }, user);
|
|
250
|
+
const envObject = result.data.reduce((acc, v) => {
|
|
251
|
+
acc[v.name] = v.value;
|
|
252
|
+
return acc;
|
|
253
|
+
}, {});
|
|
254
|
+
return new ivm.ExternalCopy(envObject).copyInto();
|
|
242
255
|
}));
|
|
243
256
|
|
|
244
|
-
//
|
|
245
|
-
|
|
257
|
+
// Contexte sécurisé
|
|
258
|
+
const safeContext = JSON.parse(JSON.stringify(context));
|
|
259
|
+
await jail.set('context', new ivm.ExternalCopy(safeContext).copyInto());
|
|
246
260
|
|
|
247
|
-
//
|
|
248
|
-
// The script is wrapped in an async IIFE to handle promises correctly
|
|
261
|
+
// Exécution
|
|
249
262
|
const fullScript = `
|
|
250
|
-
|
|
263
|
+
const normalizeArgs = args => args.map(arg => {
|
|
264
|
+
if (typeof arg === 'object' && arg !== null) {
|
|
265
|
+
return JSON.stringify(arg); // Convert objects to strings
|
|
266
|
+
}
|
|
267
|
+
return arg;
|
|
268
|
+
});
|
|
269
|
+
const db = {
|
|
270
|
+
create: (...args) => _db_create.applySyncPromise(null, normalizeArgs(args)),
|
|
271
|
+
find: (...args) => _db_find.applySyncPromise(null, normalizeArgs(args)),
|
|
272
|
+
findOne: (...args) => _db_findOne.applySyncPromise(null, normalizeArgs(args)),
|
|
273
|
+
update: (...args) => _db_update.applySyncPromise(null, normalizeArgs(args)),
|
|
274
|
+
delete: (...args) => _db_delete.applySyncPromise(null, normalizeArgs(args))
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
const logger = {
|
|
278
|
+
info: _log_info,
|
|
279
|
+
warn: _log_warn,
|
|
280
|
+
error: _log_error
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
const env = {
|
|
284
|
+
get: _env_get,
|
|
285
|
+
getAll: _env_get_all
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
(async function() {
|
|
251
289
|
${code}
|
|
252
290
|
})();
|
|
253
291
|
`;
|
|
254
292
|
|
|
255
|
-
const
|
|
256
|
-
const
|
|
293
|
+
const TIMEOUT = 5000;
|
|
294
|
+
const script = await isolate.compileScript(fullScript, { timeout: TIMEOUT });
|
|
295
|
+
const result = await script.run(vmContext, {
|
|
296
|
+
timeout: TIMEOUT,
|
|
297
|
+
promise: true,
|
|
298
|
+
copy: true // Copie automatique du résultat
|
|
299
|
+
});
|
|
257
300
|
|
|
258
|
-
|
|
259
|
-
|
|
301
|
+
return {
|
|
302
|
+
success: true,
|
|
303
|
+
data: result,
|
|
304
|
+
logs: collectedLogs,
|
|
305
|
+
updatedContext: { result }
|
|
306
|
+
};
|
|
260
307
|
|
|
261
308
|
} catch (error) {
|
|
262
309
|
const errorMessage = `Script execution failed: ${error.message}`;
|
|
@@ -485,7 +532,7 @@ async function handleCreateDataAction(actionDef, contextData, user, dbCollection
|
|
|
485
532
|
logger.debug('Final data object after substitution:', dataObject);
|
|
486
533
|
|
|
487
534
|
// 3. Appeler insertData avec l'objet correctement substitué
|
|
488
|
-
const result = await insertData(targetModel, dataObject, [], user,
|
|
535
|
+
const result = await insertData(targetModel, dataObject, [], user, false, true); // On attend la fin du workflow déclenché par cette création
|
|
489
536
|
|
|
490
537
|
if (result.success) {
|
|
491
538
|
return { success: true, insertedIds: result.insertedIds };
|
|
@@ -607,16 +654,7 @@ async function handleUpdateDataAction(actionDef, contextData, user) {
|
|
|
607
654
|
selectorObject,
|
|
608
655
|
updatesObject,
|
|
609
656
|
{},
|
|
610
|
-
user
|
|
611
|
-
);
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
console.log(
|
|
615
|
-
targetModel,
|
|
616
|
-
selectorObject,
|
|
617
|
-
updatesObject,
|
|
618
|
-
{},
|
|
619
|
-
user
|
|
657
|
+
user, false
|
|
620
658
|
);
|
|
621
659
|
|
|
622
660
|
// 6. Return result
|
|
@@ -626,7 +664,10 @@ async function handleUpdateDataAction(actionDef, contextData, user) {
|
|
|
626
664
|
success: true,
|
|
627
665
|
modifiedCount: updateResult.modifiedCount,
|
|
628
666
|
matchedCount: updateResult.matchedCount,
|
|
629
|
-
message: updateResult.message
|
|
667
|
+
message: updateResult.message,
|
|
668
|
+
updatedContext: {
|
|
669
|
+
triggerData: {...contextData.triggerData || {}, ...updatesObject}
|
|
670
|
+
}
|
|
630
671
|
};
|
|
631
672
|
} else {
|
|
632
673
|
// updateData now throws errors, so this 'else' might not be reached often,
|
|
@@ -765,6 +806,9 @@ export async function executeStepAction(actionDef, contextData, user, dbCollecti
|
|
|
765
806
|
case 'SendEmail':
|
|
766
807
|
result = await handleSendEmailAction(actionDef, contextData, user);
|
|
767
808
|
break;
|
|
809
|
+
case 'Wait':
|
|
810
|
+
result = await handleWaitAction(actionDef, contextData, user);
|
|
811
|
+
break;
|
|
768
812
|
case 'ExecuteScript':
|
|
769
813
|
result = await executeSafeJavascript(actionDef, contextData, user);
|
|
770
814
|
break;
|
|
@@ -968,6 +1012,9 @@ export async function substituteVariables(template, contextData, user) {
|
|
|
968
1012
|
// 5. Logique de résolution de valeur améliorée avec resolvePathValue
|
|
969
1013
|
const findValue = async (key) => {
|
|
970
1014
|
let path = key.trim();
|
|
1015
|
+
if (path.startsWith('context.')) {
|
|
1016
|
+
path = path.substring('context.'.length);
|
|
1017
|
+
}
|
|
971
1018
|
if (path.endsWith('._id')) {
|
|
972
1019
|
const basePath = path.slice(0, -4);
|
|
973
1020
|
const value = await findValue(basePath);
|
|
@@ -1012,7 +1059,20 @@ export async function substituteVariables(template, contextData, user) {
|
|
|
1012
1059
|
if (singlePlaceholderMatch) {
|
|
1013
1060
|
const key = singlePlaceholderMatch[1];
|
|
1014
1061
|
const value = await findValue(key);
|
|
1015
|
-
|
|
1062
|
+
|
|
1063
|
+
if (value === undefined) {
|
|
1064
|
+
return template; // Placeholder not found, return as is.
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
// If the resolved value is a string, it might contain more placeholders.
|
|
1068
|
+
// We recursively call substituteVariables on it, but only if it's different
|
|
1069
|
+
// from the original template to prevent infinite loops.
|
|
1070
|
+
if (typeof value === 'string' && value !== template) {
|
|
1071
|
+
return substituteVariables(value, contextData, user);
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
// For non-string values or if value is same as template, return the value.
|
|
1075
|
+
return value;
|
|
1016
1076
|
}
|
|
1017
1077
|
|
|
1018
1078
|
// CAS B : La chaîne contient plusieurs placeholders ou mix texte/variables
|
|
@@ -1107,17 +1167,16 @@ export async function triggerWorkflows(triggerData, user, eventType) {
|
|
|
1107
1167
|
try {
|
|
1108
1168
|
const finalFilter = {
|
|
1109
1169
|
'$and': [
|
|
1170
|
+
{"_id": { "$toObjectId": triggerData._id.toString() }},
|
|
1110
1171
|
dataFilterCondition // Applique la condition du trigger
|
|
1111
1172
|
]
|
|
1112
1173
|
};
|
|
1113
1174
|
|
|
1114
|
-
console.debug(`[Workflow Trigger] Vérification dataFilter pour trigger ${trigger._id} avec filtre combiné:`, JSON.stringify(finalFilter));
|
|
1115
|
-
|
|
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
1175
|
|
|
1120
|
-
|
|
1176
|
+
console.debug(`[Workflow Trigger] Vérification dataFilter pour trigger ${trigger._id} avec filtre combiné:`, JSON.stringify(finalFilter));
|
|
1177
|
+
console.log({triggerData, finalFilter});
|
|
1178
|
+
const match = await searchData({ model: triggerData._model, filter: finalFilter, limit: 1 }, user);
|
|
1179
|
+
if (!match.count) {
|
|
1121
1180
|
console.debug(`[Workflow Trigger] Trigger ${trigger._id}: dataFilter non satisfait par la donnée ${dataId}. WorkflowRun non créé.`);
|
|
1122
1181
|
continue; // Passer au trigger suivant
|
|
1123
1182
|
} else {
|
|
@@ -1277,9 +1336,16 @@ export async function processWorkflowRun(workflowRunId, user) {
|
|
|
1277
1336
|
let conditionsMet = true;
|
|
1278
1337
|
|
|
1279
1338
|
try {
|
|
1339
|
+
// Add logging to see the actual pipeline being executed
|
|
1340
|
+
logger.debug('Executing pipeline:', JSON.stringify(await substituteVariables(currentStepDef.conditions, contextData, user), null, 2));
|
|
1341
|
+
|
|
1342
|
+
// And log the context data to verify processedChunk exists
|
|
1343
|
+
logger.debug('Context data:', JSON.stringify(contextData, null, 2));
|
|
1344
|
+
|
|
1280
1345
|
// --- 7. Évaluation des conditions de l'étape ---
|
|
1281
1346
|
if (currentStepDef.conditions && Object.keys(currentStepDef.conditions).length > 0) {
|
|
1282
|
-
const
|
|
1347
|
+
const substitutedConditions = await substituteVariables(currentStepDef.conditions, contextData, user);
|
|
1348
|
+
const searchResult = await searchData({ model: contextData.triggerDataModel, filter: substitutedConditions, limit: 1}, user);
|
|
1283
1349
|
conditionsMet = searchResult && searchResult.count > 0;
|
|
1284
1350
|
logger.info(`[processWorkflowRun] Run ID: ${runId}, Step ID: ${currentStepId}: Conditions evaluated. Found ${searchResult ? searchResult.count : 0} match(es). Result: ${conditionsMet}`);
|
|
1285
1351
|
}
|
|
@@ -1292,7 +1358,41 @@ export async function processWorkflowRun(workflowRunId, user) {
|
|
|
1292
1358
|
if (!isObjectId(actionId)) continue;
|
|
1293
1359
|
const actionDef = await dbCollection.findOne({ _id: new ObjectId(actionId), _model: 'workflowAction' });
|
|
1294
1360
|
if (!actionDef) return await logError(`Action definition ${actionId} not found.`);
|
|
1361
|
+
console.log({actionDef});
|
|
1295
1362
|
const actionResult = await workflowModule.executeStepAction(actionDef, contextData, user, dbCollection);
|
|
1363
|
+
|
|
1364
|
+
if (actionResult.status === 'paused') {
|
|
1365
|
+
// L'action demande une pause !
|
|
1366
|
+
const { duration, durationUnit } = actionResult;
|
|
1367
|
+
const now = new Date();
|
|
1368
|
+
let resumeAt = new Date(now);
|
|
1369
|
+
|
|
1370
|
+
// Calculer la date de reprise
|
|
1371
|
+
const ms = { seconds: 1000, minutes: 60000, hours: 3600000, days: 86400000 };
|
|
1372
|
+
resumeAt.setTime(now.getTime() + (duration * ms[durationUnit]));
|
|
1373
|
+
|
|
1374
|
+
logger.info(`[processWorkflowRun] Run ID: ${runId} is pausing. Will resume at: ${resumeAt.toISOString()}`);
|
|
1375
|
+
|
|
1376
|
+
// Mettre à jour le workflowRun avec le statut 'paused' et la date de reprise
|
|
1377
|
+
await dbCollection.updateOne({ _id: runId }, {
|
|
1378
|
+
$set: {
|
|
1379
|
+
status: 'paused',
|
|
1380
|
+
currentStep: currentStepDef.onSuccessStep, // On prépare la prochaine étape
|
|
1381
|
+
contextData,
|
|
1382
|
+
log: actionResult.message
|
|
1383
|
+
}
|
|
1384
|
+
});
|
|
1385
|
+
|
|
1386
|
+
// Planifier le réveil du workflow
|
|
1387
|
+
schedule.scheduleJob(resumeAt, async () => {
|
|
1388
|
+
logger.info(`[Scheduler] Waking up paused workflowRun ID: ${runId}`);
|
|
1389
|
+
// On relance le traitement pour ce workflow spécifique
|
|
1390
|
+
await workflowModule.processWorkflowRun(runId, user);
|
|
1391
|
+
});
|
|
1392
|
+
|
|
1393
|
+
// Arrêter le traitement actuel de cette exécution
|
|
1394
|
+
return; // Très important de stopper la boucle ici
|
|
1395
|
+
}
|
|
1296
1396
|
if (!actionResult.success) {
|
|
1297
1397
|
stepSucceeded = false;
|
|
1298
1398
|
logInfo = actionResult.message || `Action ${actionDef.name || actionId} failed.`;
|
|
@@ -1303,6 +1403,7 @@ export async function processWorkflowRun(workflowRunId, user) {
|
|
|
1303
1403
|
if (actionResult.updatedContext) {
|
|
1304
1404
|
contextData = { ...contextData, ...actionResult.updatedContext };
|
|
1305
1405
|
}
|
|
1406
|
+
console.log("action", util.inspect(actionResult, false, 8, true));
|
|
1306
1407
|
logger.info(`[processWorkflowRun] Run ID: ${runId}, Step ID: ${currentStepId}, Action ID: ${actionId}: Executed successfully.`);
|
|
1307
1408
|
}
|
|
1308
1409
|
}
|
|
@@ -1362,7 +1463,7 @@ export async function processWorkflowRun(workflowRunId, user) {
|
|
|
1362
1463
|
logger.error(`[processWorkflowRun] Critical error during processing of workflowRun ID: ${runId}. Error: ${error.message}`, error.stack);
|
|
1363
1464
|
await dbCollection.updateOne(
|
|
1364
1465
|
{ _id: runId, status: { $nin: ['completed', 'failed', 'cancelled'] } },
|
|
1365
|
-
{ $set: { status: 'failed',
|
|
1466
|
+
{ $set: { status: 'failed', log: `Critical error: ${error.message}`, completedAt: new Date(), stepExecutionsCount } }
|
|
1366
1467
|
);
|
|
1367
1468
|
}
|
|
1368
1469
|
}
|
|
@@ -1466,57 +1567,122 @@ async function executeGenerateAIContentAction(action, context, user) {
|
|
|
1466
1567
|
|
|
1467
1568
|
|
|
1468
1569
|
/**
|
|
1469
|
-
* Gère l'action d'envoi d'
|
|
1470
|
-
*
|
|
1471
|
-
*
|
|
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,
|
|
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',
|
|
1481
|
-
|
|
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
|
-
|
|
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.
|
|
1596
|
+
// 2. Valider la configuration de l'action
|
|
1495
1597
|
const { emailRecipients, emailSubject, emailContent } = action;
|
|
1496
1598
|
if (!emailRecipients || !emailSubject || !emailContent) {
|
|
1497
|
-
|
|
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
|
-
|
|
1605
|
+
// 3. Résoudre la liste des destinataires. Peut être un placeholder qui retourne un tableau.
|
|
1606
|
+
let resolvedRecipients = await substituteVariables(emailRecipients, contextData, user);
|
|
1607
|
+
|
|
1608
|
+
// S'assurer que nous avons toujours un tableau à parcourir
|
|
1609
|
+
if (!Array.isArray(resolvedRecipients)) {
|
|
1610
|
+
resolvedRecipients = [resolvedRecipients];
|
|
1611
|
+
}
|
|
1502
1612
|
|
|
1503
|
-
|
|
1504
|
-
const rsubject = await substituteVariables(emailSubject, context, user);
|
|
1505
|
-
const rbody = await substituteVariables(emailContent, context, user);
|
|
1613
|
+
resolvedRecipients = resolvedRecipients.flat();
|
|
1506
1614
|
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
};
|
|
1615
|
+
if (resolvedRecipients.length === 0) {
|
|
1616
|
+
return { success: true, message: "No recipients found after substitution. Nothing to send." };
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
logger.info(`[handleSendEmailAction] Preparing to send emails to ${resolvedRecipients.length} recipient(s).`);
|
|
1620
|
+
|
|
1621
|
+
const allPromises = [];
|
|
1622
|
+
const sentTo = [];
|
|
1623
|
+
const failedFor = [];
|
|
1512
1624
|
|
|
1513
|
-
|
|
1514
|
-
|
|
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;
|
|
1515
1629
|
|
|
1516
|
-
|
|
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
|
-
|
|
1520
|
-
|
|
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
|
}
|