data-primals-engine 1.0.0
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/LICENSE.md +7 -0
- package/README.md +169 -0
- package/package.json +72 -0
- package/server.js +85 -0
- package/src/config.js +16 -0
- package/src/constants.js +210 -0
- package/src/core.js +340 -0
- package/src/data.js +495 -0
- package/src/defaultModels.js +1287 -0
- package/src/email.js +349 -0
- package/src/engine.js +168 -0
- package/src/events.js +81 -0
- package/src/gameObject.js +189 -0
- package/src/i18n.js +16855 -0
- package/src/index.js +15 -0
- package/src/middlewares/middleware-mongodb.js +131 -0
- package/src/middlewares/throttle.js +33 -0
- package/src/middlewares/timeout.js +47 -0
- package/src/migrate.js +216 -0
- package/src/migrations/20240522143000-content.js +20 -0
- package/src/modules/assistant.js +296 -0
- package/src/modules/bucket.js +233 -0
- package/src/modules/data.js +5692 -0
- package/src/modules/file.js +192 -0
- package/src/modules/mongodb.js +67 -0
- package/src/modules/swagger.js +18 -0
- package/src/modules/user.js +226 -0
- package/src/modules/workflow.js +1296 -0
- package/src/openai.jobs.js +145 -0
- package/src/packs.js +3799 -0
- package/src/providers.js +61 -0
- package/src/tutorials.js +112 -0
- package/src/user.js +17 -0
- package/src/workers/crypto-worker.js +77 -0
- package/src/workers/import-export-worker.js +36 -0
|
@@ -0,0 +1,1296 @@
|
|
|
1
|
+
// Exemple conceptuel dans la fonction de planification
|
|
2
|
+
import {getCollection, getCollectionForUser, isObjectId} from "./mongodb.js";
|
|
3
|
+
import schedule from "node-schedule";
|
|
4
|
+
import {ObjectId} from "mongodb";
|
|
5
|
+
import crypto from "node:crypto";
|
|
6
|
+
|
|
7
|
+
import {Logger} from "../gameObject.js";
|
|
8
|
+
import {deleteData, editData, insertData, patchData, searchData} from "./data.js";
|
|
9
|
+
import {maxExecutionsByStep, maxWorkflowSteps} from "../constants.js";
|
|
10
|
+
import { ChatOpenAI } from "@langchain/openai";
|
|
11
|
+
import { ChatGoogleGenerativeAI } from "@langchain/google-genai";
|
|
12
|
+
import { ChatPromptTemplate } from "@langchain/core/prompts";
|
|
13
|
+
import i18n from "../../../data-primals-engine/src/i18n.js";
|
|
14
|
+
import {sendEmail} from "../email.js";
|
|
15
|
+
|
|
16
|
+
// 1. ADD THIS IMPORT AT THE TOP OF THE FILE
|
|
17
|
+
// This allows the module to call its own exported functions.
|
|
18
|
+
import * as workflowModule from './workflow.js';
|
|
19
|
+
|
|
20
|
+
let logger = null;
|
|
21
|
+
export async function onInit(defaultEngine) {
|
|
22
|
+
logger = defaultEngine.getComponent(Logger);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Exécute une fonction de manière sécurisée en s'assurant qu'une seule instance
|
|
27
|
+
* s'exécute à la fois, grâce à un système de verrouillage distribué basé sur la base de données.
|
|
28
|
+
* Cette fonction est atomique et conçue pour éviter les conditions de course.
|
|
29
|
+
*
|
|
30
|
+
* @param {string} jobId - Un identifiant unique pour la tâche (ex: 'workflowTrigger_monId').
|
|
31
|
+
* @param {Function} jobFunction - La fonction asynchrone à exécuter si le verrou est acquis.
|
|
32
|
+
* @param {number} [lockDurationMinutes=5] - La durée en minutes pendant laquelle le verrou est considéré comme valide.
|
|
33
|
+
* @returns {Promise<void>}
|
|
34
|
+
*/
|
|
35
|
+
export async function runScheduledJobWithDbLock(jobId, jobFunction, lockDurationMinutes = 5) {
|
|
36
|
+
const jobsCollection = getCollection('job_locks');
|
|
37
|
+
const now = new Date();
|
|
38
|
+
const lockExpiresAt = new Date(now.getTime() + lockDurationMinutes * 60 * 1000);
|
|
39
|
+
let lockAcquired = false; // Drapeau pour savoir si nous devons libérer le verrou
|
|
40
|
+
|
|
41
|
+
// Le bloc try...finally garantit que la libération du verrou est tentée
|
|
42
|
+
// même si la fonction jobFunction lève une exception.
|
|
43
|
+
try {
|
|
44
|
+
// --- PHASE 1: ACQUISITION DU VERROU (de manière atomique) ---
|
|
45
|
+
|
|
46
|
+
// Tentative 1: Mettre à jour un verrou existant qui a expiré.
|
|
47
|
+
// C'est le cas le plus courant après la première exécution.
|
|
48
|
+
// L'opération `updateOne` est atomique.
|
|
49
|
+
const updateResult = await jobsCollection.updateOne(
|
|
50
|
+
{
|
|
51
|
+
jobId: jobId,
|
|
52
|
+
lockedUntil: { $lt: now } // Le verrou est disponible si sa date d'expiration est dans le passé
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
$set: { lockedUntil: lockExpiresAt, lastStarted: now },
|
|
56
|
+
$inc: { runCount: 1 }
|
|
57
|
+
}
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
if (updateResult.modifiedCount === 1) {
|
|
61
|
+
// Succès : nous avons mis à jour le verrou expiré et l'avons acquis.
|
|
62
|
+
lockAcquired = true;
|
|
63
|
+
logger.info(`[Lock] Verrou existant acquis pour la tâche ${jobId}.`);
|
|
64
|
+
} else {
|
|
65
|
+
// Si aucun document n'a été modifié, soit le verrou n'existe pas,
|
|
66
|
+
// soit il est actuellement détenu par un autre processus.
|
|
67
|
+
// Tentative 2: Insérer un nouveau document de verrou.
|
|
68
|
+
// Cette opération échouera avec une erreur de clé dupliquée (code 11000)
|
|
69
|
+
// si un autre processus a réussi à créer le verrou entre-temps.
|
|
70
|
+
try {
|
|
71
|
+
await jobsCollection.insertOne({
|
|
72
|
+
jobId: jobId,
|
|
73
|
+
lockedUntil: lockExpiresAt,
|
|
74
|
+
lastStarted: now,
|
|
75
|
+
runCount: 1
|
|
76
|
+
});
|
|
77
|
+
// Succès : nous avons créé un nouveau verrou et l'avons acquis.
|
|
78
|
+
lockAcquired = true;
|
|
79
|
+
logger.info(`[Lock] Nouveau verrou créé pour la tâche ${jobId}.`);
|
|
80
|
+
} catch (insertError) {
|
|
81
|
+
if (insertError.code === 11000) {
|
|
82
|
+
// Comportement attendu : un autre processus a acquis le verrou.
|
|
83
|
+
// Ce n'est pas une erreur, on saute simplement l'exécution.
|
|
84
|
+
logger.info(`[Lock] Impossible d'acquérir le verrou pour ${jobId}, un autre processus le dent. Exécution ignorée.`);
|
|
85
|
+
} else {
|
|
86
|
+
// Une erreur de base de données inattendue s'est produite.
|
|
87
|
+
throw insertError;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// --- PHASE 2: EXÉCUTION DE LA TÂCHE ---
|
|
93
|
+
if (lockAcquired) {
|
|
94
|
+
logger.info(`[Lock] Exécution de la fonction pour la tâche ${jobId}...`);
|
|
95
|
+
await jobFunction();
|
|
96
|
+
logger.info(`[Lock] La fonction pour la tâche ${jobId} s'est terminée.`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
} catch (error) {
|
|
100
|
+
// Capture les erreurs de la `jobFunction` ou les erreurs inattendues de la base de données.
|
|
101
|
+
logger.error(`Erreur durant l'exécution de la tâche verrouillée ${jobId}:`, error);
|
|
102
|
+
} finally {
|
|
103
|
+
// --- PHASE 3: LIBÉRATION DU VERROU ---
|
|
104
|
+
if (lockAcquired) {
|
|
105
|
+
try {
|
|
106
|
+
// On libère le verrou en mettant sa date d'expiration dans le passé,
|
|
107
|
+
// le rendant immédiatement disponible pour la prochaine exécution.
|
|
108
|
+
await jobsCollection.updateOne(
|
|
109
|
+
{ jobId: jobId },
|
|
110
|
+
{ $set: { lockedUntil: new Date(0) } }
|
|
111
|
+
);
|
|
112
|
+
logger.info(`[Lock] Verrou libéré pour la tâche ${jobId}.`);
|
|
113
|
+
} catch (releaseError) {
|
|
114
|
+
// Il est crucial de logger cette erreur, car un verrou non libéré peut bloquer les futures exécutions.
|
|
115
|
+
logger.error(`CRITIQUE: Échec de la libération du verrou pour la tâche ${jobId}. Une intervention manuelle peut être nécessaire.`, releaseError);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Planifie l'exécution des workflows déclenchés par une cronExpression.
|
|
124
|
+
* Utilise runScheduledJobWithDbLock pour assurer l'exécution unique à travers plusieurs instances.
|
|
125
|
+
*/
|
|
126
|
+
export async function scheduleWorkflowTriggers() {
|
|
127
|
+
logger.info('Starting scheduling of workflow triggers...');
|
|
128
|
+
try {
|
|
129
|
+
const datasCollection = getCollection('datas'); // Ou la collection appropriée pour les workflows
|
|
130
|
+
|
|
131
|
+
// Trouver tous les workflows actifs avec une cronExpression définie
|
|
132
|
+
const workflowsToSchedule = await datasCollection.find({
|
|
133
|
+
_model: 'workflowTrigger',
|
|
134
|
+
cronExpression: { $exists: true, $ne: "" },
|
|
135
|
+
// Ajoutez d'autres conditions si nécessaire (ex: active: true)
|
|
136
|
+
}).toArray();
|
|
137
|
+
|
|
138
|
+
console.log(`Found ${workflowsToSchedule.length} workflow triggers with cron expressions to schedule.`);
|
|
139
|
+
|
|
140
|
+
for (const workflow of workflowsToSchedule) {
|
|
141
|
+
const jobId = `workflowTrigger_${workflow._id}`; // ID unique pour le verrou du job
|
|
142
|
+
const cronExpression = workflow.cronExpression;
|
|
143
|
+
if( !cronExpression )
|
|
144
|
+
continue;
|
|
145
|
+
// Planifier la tâche en utilisant node-schedule
|
|
146
|
+
schedule.scheduleJob(cronExpression, async () => {
|
|
147
|
+
console.log(`Cron triggered for job ${jobId}. Attempting to run with lock...`);
|
|
148
|
+
|
|
149
|
+
// Utiliser runScheduledJobWithDbLock pour exécuter la tâche
|
|
150
|
+
await runScheduledJobWithDbLock(
|
|
151
|
+
jobId,
|
|
152
|
+
async () => {
|
|
153
|
+
// --- Début de la logique spécifique au workflow ---
|
|
154
|
+
// C'est ici que vous mettriez le code qui doit être exécuté
|
|
155
|
+
// lorsque le workflow est déclenché par le cron.
|
|
156
|
+
// Par exemple:
|
|
157
|
+
console.log(`Executing task logic for workflow ${workflow.name} (ID: ${workflow._id})`);
|
|
158
|
+
|
|
159
|
+
// Exemple:
|
|
160
|
+
// const targetModel = workflow.targetModel;
|
|
161
|
+
// const action = workflow.action;
|
|
162
|
+
// await executeWorkflowAction(targetModel, action, workflow.parameters);
|
|
163
|
+
|
|
164
|
+
// Simule une tâche asynchrone
|
|
165
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
166
|
+
|
|
167
|
+
console.log(`Task logic completed for workflow ${workflow.name} (ID: ${workflow._id})`);
|
|
168
|
+
// --- Fin de la logique spécifique au workflow ---
|
|
169
|
+
},
|
|
170
|
+
workflow.lockDurationMinutes || 5 // Utilise la durée du workflow ou une valeur par défaut
|
|
171
|
+
);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
console.log('Finished scheduling workflow triggers.');
|
|
177
|
+
|
|
178
|
+
} catch (error) {
|
|
179
|
+
console.error('Error during scheduling of workflow triggers:', error);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Handles the 'Webhook' workflow action.
|
|
186
|
+
* Sends an HTTP request to a specified URL with substituted data using native fetch.
|
|
187
|
+
*
|
|
188
|
+
* @param {object} actionDef - The definition of the 'Webhook' action.
|
|
189
|
+
* @param {object} contextData - The current workflow run context data.
|
|
190
|
+
* @param {object} user - The user object (peut être utilisé pour l'authentification ou le logging).
|
|
191
|
+
* @param {object} dbCollection - The MongoDB collection (moins pertinent ici, mais gardé pour la cohérence).
|
|
192
|
+
* @returns {Promise<{success: boolean, message?: string, responseStatus?: number, responseBody?: any}>} - Result of the action.
|
|
193
|
+
*/
|
|
194
|
+
async function handleWebhookAction(actionDef, contextData, user, dbCollection) {
|
|
195
|
+
const { name: actionName, _id: actionId, url, method = 'POST', headers: headersTemplate, body: bodyTemplate } = actionDef;
|
|
196
|
+
|
|
197
|
+
// 1. Basic Validation
|
|
198
|
+
if (!url) {
|
|
199
|
+
const msg = `[handleWebhookAction] Action ${actionName} (${actionId}): Missing 'url'.`;
|
|
200
|
+
logger.error(msg);
|
|
201
|
+
return { success: false, message: msg };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
logger.info(`[handleWebhookAction] Action ${actionName} (${actionId}): Executing webhook. Method: ${method}`);
|
|
205
|
+
|
|
206
|
+
try {
|
|
207
|
+
// 2. Substitute Variables
|
|
208
|
+
const substitutedUrl = await substituteVariables(url, contextData, user);
|
|
209
|
+
let substitutedHeadersString;
|
|
210
|
+
let substitutedBodyString;
|
|
211
|
+
let headersObject = {};
|
|
212
|
+
let bodyObject = null;
|
|
213
|
+
|
|
214
|
+
// Substitute Headers (JSON string or object)
|
|
215
|
+
if (headersTemplate) {
|
|
216
|
+
if (typeof headersTemplate === 'string') {
|
|
217
|
+
substitutedHeadersString = await substituteVariables(headersTemplate, contextData, user);
|
|
218
|
+
} else if (typeof headersTemplate === 'object') {
|
|
219
|
+
headersObject = await substituteVariables(headersTemplate, contextData, user);
|
|
220
|
+
} else {
|
|
221
|
+
logger.warn(`[handleWebhookAction] Action ${actionName} (${actionId}): 'headers' has an invalid type (${typeof headersTemplate}). Ignoring.`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Substitute Body (JSON string or object) - only relevant for methods like POST, PUT, PATCH
|
|
226
|
+
if (bodyTemplate && ['POST', 'PUT', 'PATCH'].includes(method.toUpperCase())) {
|
|
227
|
+
if (typeof bodyTemplate === 'string') {
|
|
228
|
+
substitutedBodyString = await substituteVariables(bodyTemplate, contextData, user);
|
|
229
|
+
} else if (typeof bodyTemplate === 'object') {
|
|
230
|
+
bodyObject = await substituteVariables(bodyTemplate, contextData, user);
|
|
231
|
+
} else {
|
|
232
|
+
logger.warn(`[handleWebhookAction] Action ${actionName} (${actionId}): 'body' has an invalid type (${typeof bodyTemplate}). Ignoring.`);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// 3. Parse substituted JSON strings
|
|
237
|
+
if (substitutedHeadersString) {
|
|
238
|
+
try {
|
|
239
|
+
headersObject = JSON.parse(substitutedHeadersString);
|
|
240
|
+
if (typeof headersObject !== 'object' || headersObject === null) {
|
|
241
|
+
throw new Error("Parsed headers is not a valid object.");
|
|
242
|
+
}
|
|
243
|
+
} catch (parseError) {
|
|
244
|
+
logger.error(`[handleWebhookAction] Action ${actionName} (${actionId}): Failed to parse substituted 'headers' JSON. Error: ${parseError.message}. Using default headers. Substituted string: ${substitutedHeadersString}`);
|
|
245
|
+
headersObject = { 'Content-Type': 'application/json' }; // Fallback
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
// Ensure Content-Type if body is present and headers don't specify it
|
|
249
|
+
if (bodyObject !== null || substitutedBodyString) {
|
|
250
|
+
if (!headersObject['Content-Type'] && !headersObject['content-type']) {
|
|
251
|
+
headersObject['Content-Type'] = 'application/json';
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
if (substitutedBodyString) {
|
|
257
|
+
try {
|
|
258
|
+
// Try parsing first, maybe it's valid JSON already
|
|
259
|
+
bodyObject = JSON.parse(substitutedBodyString);
|
|
260
|
+
} catch (parseError) {
|
|
261
|
+
// If parsing fails, treat it as a plain string body
|
|
262
|
+
bodyObject = substitutedBodyString;
|
|
263
|
+
// Adjust Content-Type if it was assumed to be JSON
|
|
264
|
+
if (headersObject['Content-Type'] === 'application/json') {
|
|
265
|
+
headersObject['Content-Type'] = 'text/plain';
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// 4. Prepare Fetch Options
|
|
271
|
+
const fetchOptions = {
|
|
272
|
+
method: method.toUpperCase(),
|
|
273
|
+
headers: headersObject, // Native fetch accepts an object directly
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
if (bodyObject !== null && ['POST', 'PUT', 'PATCH'].includes(fetchOptions.method)) {
|
|
277
|
+
// Stringify if it's an object and content type is JSON, otherwise use as is
|
|
278
|
+
if (typeof bodyObject === 'object' && headersObject['Content-Type'] === 'application/json') {
|
|
279
|
+
fetchOptions.body = JSON.stringify(bodyObject);
|
|
280
|
+
} else {
|
|
281
|
+
fetchOptions.body = bodyObject; // Use string directly
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// 5. Execute Fetch Request using native fetch
|
|
286
|
+
logger.info(`[handleWebhookAction] Action ${actionName} (${actionId}): Calling URL: ${substitutedUrl}`);
|
|
287
|
+
const response = await fetch(substitutedUrl, fetchOptions); // Utilisation de fetch natif
|
|
288
|
+
|
|
289
|
+
// 6. Process Response
|
|
290
|
+
let responseBody;
|
|
291
|
+
const contentType = response.headers.get('content-type');
|
|
292
|
+
try {
|
|
293
|
+
if (contentType && contentType.includes('application/json')) {
|
|
294
|
+
responseBody = await response.json();
|
|
295
|
+
} else {
|
|
296
|
+
responseBody = await response.text();
|
|
297
|
+
}
|
|
298
|
+
} catch (responseParseError) {
|
|
299
|
+
logger.error(`[handleWebhookAction] Action ${actionName} (${actionId}): Failed to parse response body. Error: ${responseParseError.message}`);
|
|
300
|
+
// Try reading as text again in case of error during json parsing
|
|
301
|
+
try {
|
|
302
|
+
responseBody = await response.text();
|
|
303
|
+
} catch (textError) {
|
|
304
|
+
responseBody = "[Could not parse response body]";
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
logger.info(`[handleWebhookAction] Action ${actionName} (${actionId}): Received response. Status: ${response.status}`);
|
|
309
|
+
|
|
310
|
+
// 7. Return Result
|
|
311
|
+
if (response.ok) { // Status code 200-299
|
|
312
|
+
return {
|
|
313
|
+
success: true,
|
|
314
|
+
message: `Webhook executed successfully. Status: ${response.status}`,
|
|
315
|
+
responseStatus: response.status,
|
|
316
|
+
responseBody: responseBody
|
|
317
|
+
// updatedContext: { webhookResponse: responseBody } // Optionnel: Ajouter la réponse au contexte
|
|
318
|
+
};
|
|
319
|
+
} else {
|
|
320
|
+
// Handle non-successful responses (4xx, 5xx)
|
|
321
|
+
const errorMsg = `Webhook execution failed. Status: ${response.status}. Response: ${typeof responseBody === 'string' ? responseBody : JSON.stringify(responseBody)}`;
|
|
322
|
+
logger.error(`[handleWebhookAction] Action ${actionName} (${actionId}): ${errorMsg}`);
|
|
323
|
+
return {
|
|
324
|
+
success: false,
|
|
325
|
+
message: errorMsg,
|
|
326
|
+
responseStatus: response.status,
|
|
327
|
+
responseBody: responseBody
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
} catch (error) {
|
|
332
|
+
// Catch network errors or other unexpected errors during the process
|
|
333
|
+
const msg = `[handleWebhookAction] Action ${actionName} (${actionId}): Unexpected error during webhook execution. Error: ${error.message}`;
|
|
334
|
+
logger.error(msg, error.stack);
|
|
335
|
+
return { success: false, message: msg };
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Handles the 'CreateData' workflow action.
|
|
341
|
+
* Substitutes variables, validates, and inserts a new document.
|
|
342
|
+
*
|
|
343
|
+
* @param {object} actionDef - The definition of the 'CreateData' action.
|
|
344
|
+
* @param {object} contextData - The current workflow run context data.
|
|
345
|
+
* @param {object} user - The user object.
|
|
346
|
+
* @param {object} dbCollection - The MongoDB collection for the user.
|
|
347
|
+
* @returns {Promise<{success: boolean, message?: string, insertedId?: ObjectId}>} - Result of the action.
|
|
348
|
+
*/
|
|
349
|
+
async function handleCreateDataAction(actionDef, contextData, user, dbCollection) {
|
|
350
|
+
const { targetModel, dataToCreate } = actionDef;
|
|
351
|
+
|
|
352
|
+
// 1. Basic Validation
|
|
353
|
+
if (!targetModel || typeof targetModel !== 'string') {
|
|
354
|
+
const msg = `[handleCreateDataAction] Action ${actionDef.name} (${actionDef._id}): Missing or invalid 'targetModel'.`;
|
|
355
|
+
logger.error(msg);
|
|
356
|
+
return { success: false, message: msg };
|
|
357
|
+
}
|
|
358
|
+
if (!dataToCreate) {
|
|
359
|
+
const msg = `[handleCreateDataAction] Action ${actionDef.name} (${actionDef._id}): Missing 'dataToCreate' template.`;
|
|
360
|
+
logger.error(msg);
|
|
361
|
+
return { success: false, message: msg };
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
logger.info(`[handleCreateDataAction] Action ${actionDef.name} (${actionDef._id}): Creating data for model '${targetModel}'.`);
|
|
365
|
+
|
|
366
|
+
try {
|
|
367
|
+
// 2. Substitute Variables in the data template
|
|
368
|
+
let substitutedDataString;
|
|
369
|
+
let dataObject;
|
|
370
|
+
|
|
371
|
+
// dataToCreate might be a string (JSON) or already an object from the model definition
|
|
372
|
+
if (typeof dataToCreate === 'string') {
|
|
373
|
+
substitutedDataString = await substituteVariables(dataToCreate, contextData, user);
|
|
374
|
+
console.log({substitutedDataString});
|
|
375
|
+
} else if (typeof dataToCreate === 'object') {
|
|
376
|
+
// If it's an object, substitute within its values
|
|
377
|
+
const substitutedObject = await substituteVariables(dataToCreate, contextData, user);
|
|
378
|
+
// We still need to stringify and parse if it was originally intended as JSON string
|
|
379
|
+
// Or assume it's meant to be used directly if it came as an object
|
|
380
|
+
// Let's assume direct use for now if it's an object in the definition
|
|
381
|
+
dataObject = substitutedObject;
|
|
382
|
+
|
|
383
|
+
console.log({substitutedObject});
|
|
384
|
+
} else {
|
|
385
|
+
const msg = `[handleCreateDataAction] Action ${actionDef.name} (${actionDef._id}): 'dataToCreate' has an invalid type (${typeof dataToCreate}). Expected string (JSON) or object.`;
|
|
386
|
+
logger.error(msg);
|
|
387
|
+
return { success: false, message: msg };
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
// 3. Parse the substituted data if it was a string
|
|
392
|
+
if (substitutedDataString) {
|
|
393
|
+
try {
|
|
394
|
+
dataObject = JSON.parse(substitutedDataString);
|
|
395
|
+
if (typeof dataObject !== 'object' || dataObject === null) {
|
|
396
|
+
throw new Error("Parsed data is not a valid object.");
|
|
397
|
+
}
|
|
398
|
+
} catch (parseError) {
|
|
399
|
+
const msg = `[handleCreateDataAction] Action ${actionDef.name} (${actionDef._id}): Failed to parse substituted 'dataToCreate' JSON. Error: ${parseError.message}. Substituted string: ${substitutedDataString}`;
|
|
400
|
+
logger.error(msg);
|
|
401
|
+
return { success: false, message: msg };
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const ids = await insertData(targetModel, dataObject, [], user, false);
|
|
406
|
+
return { success: true, insertedIds: ids };
|
|
407
|
+
|
|
408
|
+
} catch (error) {
|
|
409
|
+
const msg = `[handleCreateDataAction] Action ${actionDef.name} (${actionDef._id}): Unexpected error during creation for model '${targetModel}'. Error: ${error.message}`;
|
|
410
|
+
logger.error(msg, error.stack);
|
|
411
|
+
return { success: false, message: msg };
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Handles the 'UpdateData' workflow action.
|
|
418
|
+
* Finds document(s) based on a selector, substitutes variables in updates,
|
|
419
|
+
* validates, and updates the document(s) using the updateData function.
|
|
420
|
+
*
|
|
421
|
+
* @param {object} actionDef - The definition of the 'UpdateData' action.
|
|
422
|
+
* @param {object} contextData - The current workflow run context data.
|
|
423
|
+
* @param {object} user - The user object.
|
|
424
|
+
* @param {object} dbCollection - The MongoDB collection for the user (bien que updateData utilise getCollectionForUser).
|
|
425
|
+
* @returns {Promise<{success: boolean, message?: string, modifiedCount?: number, matchedCount?: number}>} - Result of the action.
|
|
426
|
+
*/
|
|
427
|
+
async function handleUpdateDataAction(actionDef, contextData, user) {
|
|
428
|
+
const { targetModel, targetSelector, fieldsToUpdate, updateMultiple = false } = actionDef; // updateMultiple optionnel, défaut false
|
|
429
|
+
|
|
430
|
+
// 1. Basic Validation
|
|
431
|
+
if (!targetModel || typeof targetModel !== 'string') {
|
|
432
|
+
const msg = `[handleUpdateDataAction] Action ${actionDef.name} (${actionDef._id}): Missing or invalid 'targetModel'.`;
|
|
433
|
+
logger.error(msg);
|
|
434
|
+
return { success: false, message: msg };
|
|
435
|
+
}
|
|
436
|
+
if (!targetSelector) {
|
|
437
|
+
const msg = `[handleUpdateDataAction] Action ${actionDef.name} (${actionDef._id}): Missing 'targetSelector'.`;
|
|
438
|
+
logger.error(msg);
|
|
439
|
+
return { success: false, message: msg };
|
|
440
|
+
}
|
|
441
|
+
if (!fieldsToUpdate) {
|
|
442
|
+
const msg = `[handleUpdateDataAction] Action ${actionDef.name} (${actionDef._id}): Missing 'fieldsToUpdate'.`;
|
|
443
|
+
logger.error(msg);
|
|
444
|
+
return { success: false, message: msg };
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
logger.info(`[handleUpdateDataAction] Action ${actionDef.name} (${actionDef._id}): Updating data for model '${targetModel}'. Multiple: ${updateMultiple}`);
|
|
448
|
+
|
|
449
|
+
try {
|
|
450
|
+
// 2. Substitute Variables in selector and updates
|
|
451
|
+
let substitutedSelectorString;
|
|
452
|
+
let substitutedUpdatesString;
|
|
453
|
+
let selectorObject;
|
|
454
|
+
let updatesObject;
|
|
455
|
+
|
|
456
|
+
// Substitute targetSelector (assuming it's a JSON string or object)
|
|
457
|
+
if (typeof targetSelector === 'string') {
|
|
458
|
+
substitutedSelectorString = await substituteVariables(targetSelector, contextData, user);
|
|
459
|
+
} else if (typeof targetSelector === 'object') {
|
|
460
|
+
selectorObject = await substituteVariables(targetSelector, contextData, user); // Substitute values within the object
|
|
461
|
+
} else {
|
|
462
|
+
const msg = `[handleUpdateDataAction] Action ${actionDef.name} (${actionDef._id}): 'targetSelector' has an invalid type (${typeof targetSelector}). Expected string (JSON) or object.`;
|
|
463
|
+
logger.error(msg);
|
|
464
|
+
return { success: false, message: msg };
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// Substitute fieldsToUpdate (assuming it's a JSON string or object)
|
|
468
|
+
if (typeof fieldsToUpdate === 'string') {
|
|
469
|
+
substitutedUpdatesString = await substituteVariables(fieldsToUpdate, contextData, user);
|
|
470
|
+
} else if (typeof fieldsToUpdate === 'object') {
|
|
471
|
+
updatesObject = await substituteVariables(fieldsToUpdate, contextData, user); // Substitute values within the object
|
|
472
|
+
} else {
|
|
473
|
+
const msg = `[handleUpdateDataAction] Action ${actionDef.name} (${actionDef._id}): 'fieldsToUpdate' has an invalid type (${typeof fieldsToUpdate}). Expected string (JSON) or object.`;
|
|
474
|
+
logger.error(msg);
|
|
475
|
+
return { success: false, message: msg };
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// 3. Parse substituted JSON strings
|
|
479
|
+
if (substitutedSelectorString) {
|
|
480
|
+
try {
|
|
481
|
+
selectorObject = JSON.parse(substitutedSelectorString);
|
|
482
|
+
if (typeof selectorObject !== 'object' || selectorObject === null) {
|
|
483
|
+
throw new Error("Parsed selector is not a valid object.");
|
|
484
|
+
}
|
|
485
|
+
} catch (parseError) {
|
|
486
|
+
const msg = `[handleUpdateDataAction] Action ${actionDef.name} (${actionDef._id}): Failed to parse substituted 'targetSelector' JSON. Error: ${parseError.message}. Substituted string: ${substitutedSelectorString}`;
|
|
487
|
+
logger.error(msg);
|
|
488
|
+
return { success: false, message: msg };
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
if (substitutedUpdatesString) {
|
|
492
|
+
try {
|
|
493
|
+
updatesObject = JSON.parse(substitutedUpdatesString);
|
|
494
|
+
if (typeof updatesObject !== 'object' || updatesObject === null) {
|
|
495
|
+
throw new Error("Parsed updates is not a valid object.");
|
|
496
|
+
}
|
|
497
|
+
} catch (parseError) {
|
|
498
|
+
const msg = `[handleUpdateDataAction] Action ${actionDef.name} (${actionDef._id}): Failed to parse substituted 'fieldsToUpdate' JSON. Error: ${parseError.message}. Substituted string: ${substitutedUpdatesString}`;
|
|
499
|
+
logger.error(msg);
|
|
500
|
+
return { success: false, message: msg };
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// Remove system fields potentially included in updates by mistake
|
|
505
|
+
delete updatesObject._id;
|
|
506
|
+
delete updatesObject._model;
|
|
507
|
+
delete updatesObject._user;
|
|
508
|
+
delete updatesObject._hash;
|
|
509
|
+
|
|
510
|
+
if (Object.keys(updatesObject).length === 0) {
|
|
511
|
+
const msg = `[handleUpdateDataAction] Action ${actionDef.name} (${actionDef._id}): 'fieldsToUpdate' resulted in an empty update object after substitution/parsing. Nothing to update.`;
|
|
512
|
+
logger.warn(msg);
|
|
513
|
+
return { success: true, message: "No fields to update.", modifiedCount: 0, matchedCount: 0 };
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
const updateResult = await patchData(
|
|
517
|
+
targetModel,
|
|
518
|
+
selectorObject,
|
|
519
|
+
updatesObject,
|
|
520
|
+
{},
|
|
521
|
+
user
|
|
522
|
+
);
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
console.log(
|
|
526
|
+
targetModel,
|
|
527
|
+
selectorObject,
|
|
528
|
+
updatesObject,
|
|
529
|
+
{},
|
|
530
|
+
user
|
|
531
|
+
);
|
|
532
|
+
|
|
533
|
+
// 6. Return result
|
|
534
|
+
if (updateResult.success || updateResult.unmodified) {
|
|
535
|
+
logger.info(`[handleUpdateDataAction] Action ${actionDef.name} (${actionDef._id}): Update successful for model '${targetModel}'. Matched: ${updateResult.matchedCount}, Modified: ${updateResult.modifiedCount}`);
|
|
536
|
+
return {
|
|
537
|
+
success: true,
|
|
538
|
+
modifiedCount: updateResult.modifiedCount,
|
|
539
|
+
matchedCount: updateResult.matchedCount,
|
|
540
|
+
message: updateResult.message
|
|
541
|
+
};
|
|
542
|
+
} else {
|
|
543
|
+
// updateData now throws errors, so this 'else' might not be reached often,
|
|
544
|
+
// but kept for safety in case it returns { success: false } in some scenarios.
|
|
545
|
+
const msg = `[handleUpdateDataAction] Action ${actionDef.name} (${actionDef._id}): updateData function reported failure. Message: ${updateResult.error}`;
|
|
546
|
+
logger.error(msg);
|
|
547
|
+
return { success: false, message: msg };
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
} catch (error) {
|
|
551
|
+
// Catch errors thrown by updateData (validation, permissions, DB errors) or other unexpected errors
|
|
552
|
+
const msg = `[handleUpdateDataAction] Action ${actionDef.name} (${actionDef._id}): Unexpected error during update for model '${targetModel}'. Error: ${error.message}`;
|
|
553
|
+
logger.error(msg, error.stack);
|
|
554
|
+
return { success: false, message: msg };
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* Handles the 'DeleteData' workflow action.
|
|
562
|
+
* Finds document(s) based on a selector, substitutes variables,
|
|
563
|
+
* and deletes the document(s) using the deleteData function.
|
|
564
|
+
*
|
|
565
|
+
* @param {object} actionDef - The definition of the 'DeleteData' action.
|
|
566
|
+
* @param {object} contextData - The current workflow run context data.
|
|
567
|
+
* @param {object} user - The user object.
|
|
568
|
+
* @param {object} dbCollection - The MongoDB collection for the user (bien que deleteData utilise getCollectionForUser).
|
|
569
|
+
* @returns {Promise<{success: boolean, message?: string, deletedCount?: number}>} - Result of the action.
|
|
570
|
+
*/
|
|
571
|
+
async function handleDeleteDataAction(actionDef, contextData, user, dbCollection) {
|
|
572
|
+
// deleteMultiple optionnel, défaut false (supprime un seul par défaut)
|
|
573
|
+
const { targetModel, targetSelector, deleteMultiple = false } = actionDef;
|
|
574
|
+
|
|
575
|
+
// 1. Basic Validation
|
|
576
|
+
if (!targetModel || typeof targetModel !== 'string') {
|
|
577
|
+
const msg = `[handleDeleteDataAction] Action ${actionDef.name} (${actionDef._id}): Missing or invalid 'targetModel'.`;
|
|
578
|
+
logger.error(msg);
|
|
579
|
+
return { success: false, message: msg };
|
|
580
|
+
}
|
|
581
|
+
if (!targetSelector) {
|
|
582
|
+
const msg = `[handleDeleteDataAction] Action ${actionDef.name} (${actionDef._id}): Missing 'targetSelector'.`;
|
|
583
|
+
logger.error(msg);
|
|
584
|
+
return { success: false, message: msg };
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
logger.info(`[handleDeleteDataAction] Action ${actionDef.name} (${actionDef._id}): Deleting data for model '${targetModel}'. Multiple: ${deleteMultiple}`);
|
|
588
|
+
|
|
589
|
+
try {
|
|
590
|
+
// 2. Substitute Variables in selector
|
|
591
|
+
let substitutedSelectorString;
|
|
592
|
+
let selectorObject;
|
|
593
|
+
|
|
594
|
+
// Substitute targetSelector (assuming it's a JSON string or object)
|
|
595
|
+
if (typeof targetSelector === 'string') {
|
|
596
|
+
substitutedSelectorString = await substituteVariables(targetSelector, contextData, user);
|
|
597
|
+
} else if (typeof targetSelector === 'object') {
|
|
598
|
+
selectorObject = await substituteVariables(targetSelector, contextData, user); // Substitute values within the object
|
|
599
|
+
} else {
|
|
600
|
+
const msg = `[handleDeleteDataAction] Action ${actionDef.name} (${actionDef._id}): 'targetSelector' has an invalid type (${typeof targetSelector}). Expected string (JSON) or object.`;
|
|
601
|
+
logger.error(msg);
|
|
602
|
+
return { success: false, message: msg };
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// 3. Parse substituted JSON string
|
|
606
|
+
if (substitutedSelectorString) {
|
|
607
|
+
try {
|
|
608
|
+
selectorObject = JSON.parse(substitutedSelectorString);
|
|
609
|
+
if (typeof selectorObject !== 'object' || selectorObject === null) {
|
|
610
|
+
throw new Error("Parsed selector is not a valid object.");
|
|
611
|
+
}
|
|
612
|
+
} catch (parseError) {
|
|
613
|
+
const msg = `[handleDeleteDataAction] Action ${actionDef.name} (${actionDef._id}): Failed to parse substituted 'targetSelector' JSON. Error: ${parseError.message}. Substituted string: ${substitutedSelectorString}`;
|
|
614
|
+
logger.error(msg);
|
|
615
|
+
return { success: false, message: msg };
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// 5. Call the centralized deleteData function (à créer dans data.js)
|
|
620
|
+
// Cette fonction devra gérer la recherche préalable pour les workflows 'DataDeleted' et la suppression des fichiers.
|
|
621
|
+
const deleteResult = await deleteData(
|
|
622
|
+
targetModel, [],
|
|
623
|
+
selectorObject,
|
|
624
|
+
user
|
|
625
|
+
);
|
|
626
|
+
|
|
627
|
+
// 6. Return result
|
|
628
|
+
if (deleteResult.success) {
|
|
629
|
+
logger.info(`[handleDeleteDataAction] Action ${actionDef.name} (${actionDef._id}): Delete successful for model '${targetModel}'. Deleted: ${deleteResult.deletedCount}`);
|
|
630
|
+
return {
|
|
631
|
+
success: true,
|
|
632
|
+
deletedCount: deleteResult.deletedCount,
|
|
633
|
+
message: deleteResult.message // Pass along messages like "not found"
|
|
634
|
+
};
|
|
635
|
+
} else {
|
|
636
|
+
// deleteData devrait lancer des erreurs, mais on garde ce else par sécurité.
|
|
637
|
+
const msg = `[handleDeleteDataAction] Action ${actionDef.name} (${actionDef._id}): deleteData function reported failure. Message: ${deleteResult.message}`;
|
|
638
|
+
logger.error(msg);
|
|
639
|
+
return { success: false, message: msg };
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
} catch (error) {
|
|
643
|
+
// Catch errors thrown by deleteData (permissions, DB errors) or other unexpected errors
|
|
644
|
+
const msg = `[handleDeleteDataAction] Action ${actionDef.name} (${actionDef._id}): Unexpected error during deletion for model '${targetModel}'. Error: ${error.message}`;
|
|
645
|
+
logger.error(msg, error.stack);
|
|
646
|
+
return { success: false, message: msg };
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
// Dans workflow.js
|
|
651
|
+
export async function executeStepAction(actionDef, contextData, user, dbCollection) {
|
|
652
|
+
logger.info(`[executeStepAction] Executing action type ${actionDef.type} for action ${actionDef._id} (${actionDef.name})`);
|
|
653
|
+
|
|
654
|
+
try {
|
|
655
|
+
let result;
|
|
656
|
+
switch (actionDef.type) {
|
|
657
|
+
case 'Log':
|
|
658
|
+
logger.info(`[Workflow Log Action] Action: ${actionDef.name}. Contexte:`, contextData);
|
|
659
|
+
result = { success: true, message: 'Log action executed successfully.' }; // <--- CORRECTION
|
|
660
|
+
break;
|
|
661
|
+
case 'Webhook':
|
|
662
|
+
result = await handleWebhookAction(actionDef, contextData, user, dbCollection);
|
|
663
|
+
break;
|
|
664
|
+
case 'CreateData':
|
|
665
|
+
result = await handleCreateDataAction(actionDef, contextData, user, dbCollection);
|
|
666
|
+
break;
|
|
667
|
+
case 'UpdateData':
|
|
668
|
+
result = await handleUpdateDataAction(actionDef, contextData, user);
|
|
669
|
+
break;
|
|
670
|
+
case 'DeleteData':
|
|
671
|
+
result = await handleDeleteDataAction(actionDef, contextData, user, dbCollection);
|
|
672
|
+
break;
|
|
673
|
+
case 'GenerateAIContent':
|
|
674
|
+
result = await executeGenerateAIContentAction(actionDef, contextData, user);
|
|
675
|
+
break;
|
|
676
|
+
case 'SendEmail':
|
|
677
|
+
result = await handleSendEmailAction(actionDef, contextData, user);
|
|
678
|
+
break;
|
|
679
|
+
// ... autres cases à venir ...
|
|
680
|
+
default:
|
|
681
|
+
logger.error(`[executeStepAction] Unknown action type: ${actionDef.type}`);
|
|
682
|
+
return { success: false, message: `Unknown action type: ${actionDef.type}` };
|
|
683
|
+
}
|
|
684
|
+
return result;
|
|
685
|
+
} catch (error) {
|
|
686
|
+
logger.error(`[executeStepAction] Error executing action ${actionDef.name} (${actionDef._id}): ${error.message}`, error.stack);
|
|
687
|
+
return { success: false, message: error.message || 'Action execution failed' };
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
/**
|
|
691
|
+
* Récupère une valeur imbriquée dans un objet en utilisant une chaîne de chemin.
|
|
692
|
+
* Gère les tableaux et les objets. Retourne undefined si le chemin n'est pas trouvé.
|
|
693
|
+
* Exemple: getNestedValue({ a: { b: [ { c: 1 } ] } }, 'a.b.0.c') -> 1
|
|
694
|
+
*
|
|
695
|
+
* @param {object} obj L'objet source.
|
|
696
|
+
* @param {string} path La chaîne de chemin (ex: 'user.address.city').
|
|
697
|
+
* @returns {*} La valeur trouvée ou undefined.
|
|
698
|
+
*/
|
|
699
|
+
function getNestedValue(obj, path) {
|
|
700
|
+
// Vérifie si l'objet ou le chemin est invalide
|
|
701
|
+
if (!obj || typeof path !== 'string') {
|
|
702
|
+
return undefined;
|
|
703
|
+
}
|
|
704
|
+
// Sépare le chemin en clés individuelles (ex: 'a.b.0.c' -> ['a', 'b', '0', 'c'])
|
|
705
|
+
const keys = path.split('.');
|
|
706
|
+
let current = obj; // Commence à la racine de l'objet
|
|
707
|
+
|
|
708
|
+
// Parcourt chaque clé dans le chemin
|
|
709
|
+
for (const key of keys) {
|
|
710
|
+
// Si à un moment donné on atteint null ou undefined, le chemin est invalide
|
|
711
|
+
if (current === null || current === undefined) {
|
|
712
|
+
return undefined;
|
|
713
|
+
}
|
|
714
|
+
// Récupère la valeur pour la clé actuelle
|
|
715
|
+
const value = current[key];
|
|
716
|
+
// Si la valeur est undefined, le chemin est invalide
|
|
717
|
+
if (value === undefined) {
|
|
718
|
+
return undefined;
|
|
719
|
+
}
|
|
720
|
+
// Passe au niveau suivant de l'objet/tableau
|
|
721
|
+
current = value;
|
|
722
|
+
}
|
|
723
|
+
// Retourne la valeur finale trouvée
|
|
724
|
+
return current;
|
|
725
|
+
}
|
|
726
|
+
/**
|
|
727
|
+
* Remplace les placeholders dans un template (string, object, array) par des valeurs du contextData.
|
|
728
|
+
* Mise à jour pour une meilleure gestion des types et des placeholders introuvables.
|
|
729
|
+
*/
|
|
730
|
+
export async function substituteVariables(template, contextData, user) {
|
|
731
|
+
// 1. Retourner les types non substituables tels quels
|
|
732
|
+
if (template === null || (typeof template !== 'string' && typeof template !== 'object')) {
|
|
733
|
+
return template;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
// 2. Gérer les tableaux de manière récursive
|
|
737
|
+
if (Array.isArray(template)) {
|
|
738
|
+
return Promise.all(template.map(item => substituteVariables(item, contextData, user)));
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
// 3. Gérer les objets de manière récursive
|
|
742
|
+
if (typeof template === 'object') {
|
|
743
|
+
const newObj = {};
|
|
744
|
+
for (const key in template) {
|
|
745
|
+
if (Object.prototype.hasOwnProperty.call(template, key)) {
|
|
746
|
+
newObj[key] = await substituteVariables(template[key], contextData, user);
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
return newObj;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
// --- À partir d'ici, nous savons que `template` est une chaîne de caractères ---
|
|
753
|
+
|
|
754
|
+
// 4. Construire le contexte complet pour la substitution
|
|
755
|
+
const dbCollection = getCollectionForUser(user);
|
|
756
|
+
const userEnvVars = await dbCollection.find({ _model: 'env', _user: user.username }).toArray();
|
|
757
|
+
const userEnv = userEnvVars.reduce((acc, v) => ({ ...acc, [v.name]: v.value }), {});
|
|
758
|
+
|
|
759
|
+
// `contextToSearch` contient toutes les données disponibles à sa racine
|
|
760
|
+
const contextToSearch = { ...contextData, env: userEnv };
|
|
761
|
+
|
|
762
|
+
// 5. Logique de résolution de valeur unifiée
|
|
763
|
+
const findValue = (key) => {
|
|
764
|
+
let value;
|
|
765
|
+
let path = key.trim();
|
|
766
|
+
|
|
767
|
+
// Gérer les raccourcis : {triggerData.field} -> {context.triggerData.field}
|
|
768
|
+
if (path.startsWith('triggerData.')) {
|
|
769
|
+
path = 'context.' + path;
|
|
770
|
+
}
|
|
771
|
+
// Note : Pas de gestion spéciale pour {env.*} car `env` est déjà à la racine de contextToSearch.
|
|
772
|
+
|
|
773
|
+
// Gérer les valeurs dynamiques spéciales
|
|
774
|
+
if (path === 'now') {
|
|
775
|
+
value = new Date().toISOString();
|
|
776
|
+
} else if (path === 'randomUUID') {
|
|
777
|
+
// Suppose que 'crypto' est disponible dans le scope (ex: Node.js >= 19 ou avec import 'node:crypto')
|
|
778
|
+
value = crypto.randomUUID();
|
|
779
|
+
} else {
|
|
780
|
+
// Chercher le chemin dans l'objet de contexte entier
|
|
781
|
+
value = getNestedValue(contextToSearch, path);
|
|
782
|
+
}
|
|
783
|
+
return value;
|
|
784
|
+
};
|
|
785
|
+
|
|
786
|
+
// CAS A : La chaîne est un unique placeholder (ex: "{context.triggerData.product.price}")
|
|
787
|
+
// Ceci préserve le type de donnée original (nombre, booléen, objet, etc.).
|
|
788
|
+
const singlePlaceholderMatch = template.match(/^\{([^}]+)\}$/);
|
|
789
|
+
if (singlePlaceholderMatch) {
|
|
790
|
+
const key = singlePlaceholderMatch[1];
|
|
791
|
+
const value = findValue(key);
|
|
792
|
+
// Si une valeur est trouvée, la retourner directement. Sinon, retourner le template original.
|
|
793
|
+
return value !== undefined ? value : template;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
// CAS B : La chaîne est complexe (ex: "Produit: {context.triggerData.product.name}!")
|
|
797
|
+
// Le résultat sera toujours une chaîne de caractères.
|
|
798
|
+
const placeholderRegex = /\{([^}]+)\}/g;
|
|
799
|
+
return template.replace(placeholderRegex, (match, key) => {
|
|
800
|
+
const value = findValue(key);
|
|
801
|
+
|
|
802
|
+
if (value !== undefined) {
|
|
803
|
+
if (value === null) return 'null'; // Convertir explicitement null en chaîne 'null'
|
|
804
|
+
if (typeof value === 'object') return JSON.stringify(value); // Convertir les objets/tableaux en JSON
|
|
805
|
+
return String(value); // S'assurer que les autres types sont convertis en chaîne
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
return match; // Placeholder non trouvé, le laisser tel quel
|
|
809
|
+
});
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
/**
|
|
813
|
+
* Déclenche l'instanciation d'un workflowRun si les conditions sont remplies.
|
|
814
|
+
* Vérifie le type d'événement et le filtre de données du déclencheur.
|
|
815
|
+
* Crée un document 'workflowRun' pour une exécution asynchrone ultérieure.
|
|
816
|
+
*
|
|
817
|
+
* @param {object} triggerData - La donnée qui a déclenché le(s) workflow(s) (peut être un document de données ou un document de modèle).
|
|
818
|
+
* @param {object} user - L'utilisateur associé.
|
|
819
|
+
* @param {'DataAdded' | 'DataEdited' | 'DataDeleted' | 'ModelAdded' | 'ModelEdited' | 'ModelDeleted'} eventType - Le type d'événement.
|
|
820
|
+
*/
|
|
821
|
+
export async function triggerWorkflows(triggerData, user, eventType) {
|
|
822
|
+
const dataId = eventType.startsWith('Model') ? null : triggerData._id;
|
|
823
|
+
|
|
824
|
+
const trigger = async (triggerData, user, eventType) => {
|
|
825
|
+
|
|
826
|
+
// Basic validation
|
|
827
|
+
if (!triggerData || !user || !eventType) {
|
|
828
|
+
console.warn("triggerWorkflows: Appel invalide - triggerData, user, ou eventType manquant.", { hasTriggerData: !!triggerData, hasUser: !!user, eventType });
|
|
829
|
+
return;
|
|
830
|
+
}
|
|
831
|
+
// Determine the model name and data ID based on the event type
|
|
832
|
+
const targetModelName = eventType.startsWith('Model') ? triggerData.name : triggerData._model;
|
|
833
|
+
const dataId = eventType.startsWith('Model') ? null : triggerData._id; // ID only relevant for data events
|
|
834
|
+
|
|
835
|
+
if (!targetModelName) {
|
|
836
|
+
console.warn(`triggerWorkflows: Impossible de déterminer le nom du modèle pour l'événement ${eventType}.`, triggerData);
|
|
837
|
+
return;
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
console.log(`[Workflow Trigger] Event: ${eventType}, Model: ${targetModelName}${dataId ? `, Data ID: ${dataId}` : ''}, User: ${user.username}`);
|
|
841
|
+
|
|
842
|
+
try {
|
|
843
|
+
const dbCollection = getCollectionForUser(user); // Collection des données utilisateur
|
|
844
|
+
|
|
845
|
+
// 1. Trouver les WorkflowTriggers pertinents
|
|
846
|
+
const workflowTriggers = await dbCollection.find({
|
|
847
|
+
_model: 'workflowTrigger',
|
|
848
|
+
targetModel: targetModelName,
|
|
849
|
+
isActive: true,
|
|
850
|
+
onEvent: eventType, // Assurez-vous que le champ s'appelle bien 'onEvent' dans votre modèle
|
|
851
|
+
$or: [{_user: user._user}, {_user: user.username}]
|
|
852
|
+
}).toArray();
|
|
853
|
+
|
|
854
|
+
if (workflowTriggers.length === 0) {
|
|
855
|
+
console.debug(`[Workflow Trigger] Aucun déclencheur actif trouvé pour ${targetModelName} / ${eventType}.`);
|
|
856
|
+
return;
|
|
857
|
+
}
|
|
858
|
+
console.debug(`[Workflow Trigger] Trouvé ${workflowTriggers.length} déclencheur(s) potentiel(s) pour ${targetModelName} / ${eventType}.`);
|
|
859
|
+
|
|
860
|
+
// 2. Pour chaque déclencheur trouvé, vérifier le filtre de données et créer un workflowRun
|
|
861
|
+
for (const trigger of workflowTriggers) {
|
|
862
|
+
console.debug(`[Workflow Trigger] Vérification du déclencheur ${trigger._id} (${trigger.name || 'Sans nom'})...`);
|
|
863
|
+
|
|
864
|
+
// 3. Vérifier le filtre de données (dataFilter) si applicable
|
|
865
|
+
if (eventType.startsWith('Data') && trigger.dataFilter && dataId) {
|
|
866
|
+
let dataFilterCondition = null;
|
|
867
|
+
try {
|
|
868
|
+
// dataFilter est supposé être stocké comme un objet (ou une string JSON valide)
|
|
869
|
+
if (typeof trigger.dataFilter === 'string') {
|
|
870
|
+
dataFilterCondition = JSON.parse(trigger.dataFilter);
|
|
871
|
+
} else if (typeof trigger.dataFilter === 'object' && trigger.dataFilter !== null) {
|
|
872
|
+
dataFilterCondition = trigger.dataFilter;
|
|
873
|
+
}
|
|
874
|
+
} catch (parseError) {
|
|
875
|
+
console.error(`[Workflow Trigger] Erreur de parsing JSON pour dataFilter du trigger ${trigger._id}:`, parseError);
|
|
876
|
+
continue; // Passer au trigger suivant si le filtre est invalide
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
try {
|
|
880
|
+
const finalFilter = {
|
|
881
|
+
'$and': [
|
|
882
|
+
dataFilterCondition // Applique la condition du trigger
|
|
883
|
+
]
|
|
884
|
+
};
|
|
885
|
+
|
|
886
|
+
console.debug(`[Workflow Trigger] Vérification dataFilter pour trigger ${trigger._id} avec filtre combiné:`, JSON.stringify(finalFilter));
|
|
887
|
+
|
|
888
|
+
// Exécuter la vérification dans la base de données
|
|
889
|
+
// Utilisation de countDocuments pour une vérification rapide
|
|
890
|
+
const matchCount = await searchData({ user, query: { model: targetModelName, filter: finalFilter, limit: 1 } });
|
|
891
|
+
|
|
892
|
+
if (!matchCount.count) {
|
|
893
|
+
console.debug(`[Workflow Trigger] Trigger ${trigger._id}: dataFilter non satisfait par la donnée ${dataId}. WorkflowRun non créé.`);
|
|
894
|
+
continue; // Passer au trigger suivant
|
|
895
|
+
} else {
|
|
896
|
+
console.debug(`[Workflow Trigger] Trigger ${trigger._id}: dataFilter satisfait par la donnée ${dataId}.`);
|
|
897
|
+
}
|
|
898
|
+
} catch (filterError) {
|
|
899
|
+
console.error(`[Workflow Trigger] Erreur lors de la conversion ou de l'exécution du dataFilter pour le trigger ${trigger._id}:`, filterError);
|
|
900
|
+
continue; // Ne pas créer en cas d'erreur de filtre
|
|
901
|
+
}
|
|
902
|
+
} // Fin de la vérification dataFilter
|
|
903
|
+
|
|
904
|
+
// 4. Si les filtres (eventType, dataFilter) sont passés, créer l'instance workflowRun
|
|
905
|
+
if (!trigger.workflow || !isObjectId(trigger.workflow)) {
|
|
906
|
+
console.warn(`[Workflow Trigger] Trigger ${trigger._id} n'a pas de workflow valide associé.`);
|
|
907
|
+
continue;
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
// a. Récupérer la définition du Workflow (juste pour vérifier qu'il existe)
|
|
911
|
+
const workflowDefinition = await dbCollection.findOne({
|
|
912
|
+
_id: new ObjectId(trigger.workflow),
|
|
913
|
+
_model: 'workflow',
|
|
914
|
+
$or: [{_user: user._user}, {_user: user.username}]
|
|
915
|
+
});
|
|
916
|
+
|
|
917
|
+
if (!workflowDefinition) {
|
|
918
|
+
console.warn(`[Workflow Trigger] Workflow ${trigger.workflow} associé au trigger ${trigger._id} non trouvé.`);
|
|
919
|
+
continue;
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
// b. Créer le document workflowRun
|
|
923
|
+
const workflowRunData = {
|
|
924
|
+
_model: 'workflowRun',
|
|
925
|
+
_user: user._user || user.username,
|
|
926
|
+
workflow: workflowDefinition._id, // Référence au workflow parent
|
|
927
|
+
contextData: { // Contexte initial
|
|
928
|
+
triggerDataModel: targetModelName,// Modèle de la donnée déclencheuse
|
|
929
|
+
triggerData: triggerData // Inclure la donnée déclencheuse
|
|
930
|
+
// Vous pourriez ajouter d'autres infos ici si nécessaire
|
|
931
|
+
},
|
|
932
|
+
status: 'pending', // Statut initial
|
|
933
|
+
owner: null,
|
|
934
|
+
startedAt: new Date()
|
|
935
|
+
};
|
|
936
|
+
|
|
937
|
+
try {
|
|
938
|
+
const insertResult = await dbCollection.insertOne(workflowRunData);
|
|
939
|
+
if (insertResult.insertedId) {
|
|
940
|
+
console.info(`[Workflow Trigger] WorkflowRun ${insertResult.insertedId} créé pour le workflow ${workflowDefinition.name} (ID: ${workflowDefinition._id}) déclenché par ${trigger._id}.`);
|
|
941
|
+
|
|
942
|
+
await workflowModule.processWorkflowRun(insertResult.insertedId, user);
|
|
943
|
+
} else {
|
|
944
|
+
console.error(`[Workflow Trigger] Échec de la création du WorkflowRun pour le workflow ${workflowDefinition._id} (Trigger: ${trigger._id}).`);
|
|
945
|
+
}
|
|
946
|
+
} catch (insertError) {
|
|
947
|
+
console.error(`[Workflow Trigger] Erreur lors de l'insertion du WorkflowRun pour le workflow ${workflowDefinition._id} (Trigger: ${trigger._id}):`, insertError);
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
} // Fin de la boucle des triggers
|
|
951
|
+
|
|
952
|
+
} catch (error) {
|
|
953
|
+
console.error(`[Workflow Trigger] Erreur générale dans triggerWorkflows pour ${targetModelName}${dataId ? ` ID: ${dataId}` : ''} (Event: ${eventType}):`, error);
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
return new Promise((resolve) => setTimeout(async () => {
|
|
958
|
+
await trigger(triggerData, user, eventType);
|
|
959
|
+
resolve();
|
|
960
|
+
}, 0)
|
|
961
|
+
);
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
/**
|
|
965
|
+
* Processes a workflowRun instance step-by-step.
|
|
966
|
+
* Fetches the run, evaluates conditions, executes actions, and transitions
|
|
967
|
+
* to the next step based on success or failure, updating the workflowRun status.
|
|
968
|
+
*
|
|
969
|
+
* @param {string|ObjectId} workflowRunId - The ID of the workflowRun to process.
|
|
970
|
+
* @param {object} user - The user context for database access.
|
|
971
|
+
* @returns {Promise<void>}
|
|
972
|
+
*/
|
|
973
|
+
// C:/Dev/hackersonline-engine/server/src/modules/workflow.js
|
|
974
|
+
|
|
975
|
+
export async function processWorkflowRun(workflowRunId, user) {
|
|
976
|
+
const dbCollection = getCollectionForUser(user);
|
|
977
|
+
const runId = typeof workflowRunId === 'string' ? new ObjectId(workflowRunId) : workflowRunId;
|
|
978
|
+
|
|
979
|
+
logger.info(`[processWorkflowRun] Starting processing for workflowRun ID: ${runId}`);
|
|
980
|
+
|
|
981
|
+
let currentRunState;
|
|
982
|
+
let contextData = {};
|
|
983
|
+
let stepExecutionsCount = {};
|
|
984
|
+
|
|
985
|
+
try {
|
|
986
|
+
currentRunState = await dbCollection.findOne({ _id: runId, _model: 'workflowRun' });
|
|
987
|
+
|
|
988
|
+
if (!currentRunState) {
|
|
989
|
+
logger.error(`[processWorkflowRun] WorkflowRun ID: ${runId} not found.`);
|
|
990
|
+
return;
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
stepExecutionsCount = currentRunState.stepExecutionsCount || {};
|
|
994
|
+
if (['completed', 'failed', 'cancelled'].includes(currentRunState.status)) {
|
|
995
|
+
logger.info(`[processWorkflowRun] WorkflowRun ID: ${runId} is already in a terminal state (${currentRunState.status}). Skipping.`);
|
|
996
|
+
return;
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
const logError = async (error) => {
|
|
1000
|
+
logger.error(error);
|
|
1001
|
+
await dbCollection.updateOne(
|
|
1002
|
+
{ _id: runId },
|
|
1003
|
+
{ $set: { status: 'failed', error, completedAt: new Date(), stepExecutionsCount } }
|
|
1004
|
+
);
|
|
1005
|
+
};
|
|
1006
|
+
|
|
1007
|
+
const workflowDefinition = await dbCollection.findOne({ _id: new ObjectId(currentRunState.workflow), _model: 'workflow' });
|
|
1008
|
+
if (!workflowDefinition) {
|
|
1009
|
+
return await logError(`Workflow definition ID: ${currentRunState.workflow} not found.`);
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
contextData = currentRunState.contextData || {};
|
|
1013
|
+
let currentStepId = currentRunState.currentStep || workflowDefinition.startStep;
|
|
1014
|
+
|
|
1015
|
+
if (!currentStepId || !isObjectId(currentStepId)) {
|
|
1016
|
+
const finalStatus = workflowDefinition.startStep ? 'failed' : 'completed';
|
|
1017
|
+
const errorMessage = workflowDefinition.startStep ? 'No valid starting step defined in workflow or run state.' : null;
|
|
1018
|
+
await dbCollection.updateOne(
|
|
1019
|
+
{ _id: runId },
|
|
1020
|
+
{ $set: { status: finalStatus, error: errorMessage, completedAt: new Date(), currentStep: null, stepExecutionsCount } }
|
|
1021
|
+
);
|
|
1022
|
+
return;
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
let stepCount = 0;
|
|
1026
|
+
while (currentStepId) {
|
|
1027
|
+
if (stepCount++ >= maxWorkflowSteps) {
|
|
1028
|
+
return await logError(`Maximum workflow step executions exceeded (${maxWorkflowSteps} max).`);
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
const execCount = (stepExecutionsCount[currentStepId] || 0) + 1;
|
|
1032
|
+
if (execCount > maxExecutionsByStep) {
|
|
1033
|
+
return await logError(`Maximum executions (${maxExecutionsByStep}) exceeded for step ${currentStepId}.`);
|
|
1034
|
+
}
|
|
1035
|
+
stepExecutionsCount[currentStepId] = execCount;
|
|
1036
|
+
logger.info(`[processWorkflowRun] Run ID: ${runId}, Current Step ID: ${currentStepId}`);
|
|
1037
|
+
|
|
1038
|
+
const currentStepDef = await dbCollection.findOne({ _id: new ObjectId(currentStepId), _model: 'workflowStep' });
|
|
1039
|
+
if (!currentStepDef) {
|
|
1040
|
+
return await logError(`Step definition ID: ${currentStepId} not found.`);
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
await dbCollection.updateOne(
|
|
1044
|
+
{ _id: runId },
|
|
1045
|
+
{ $set: { status: 'running', currentStep: currentStepId, contextData, stepExecutionsCount } }
|
|
1046
|
+
);
|
|
1047
|
+
|
|
1048
|
+
let stepSucceeded = true;
|
|
1049
|
+
let stepError = null;
|
|
1050
|
+
let conditionsMet = true;
|
|
1051
|
+
|
|
1052
|
+
try {
|
|
1053
|
+
// --- 7. Évaluation des conditions de l'étape ---
|
|
1054
|
+
if (currentStepDef.conditions && Object.keys(currentStepDef.conditions).length > 0) {
|
|
1055
|
+
const searchResult = await searchData({ user, query: { model: contextData.triggerDataModel, filter: currentStepDef.conditions, limit: 1 } });
|
|
1056
|
+
conditionsMet = searchResult && searchResult.count > 0;
|
|
1057
|
+
logger.info(`[processWorkflowRun] Run ID: ${runId}, Step ID: ${currentStepId}: Conditions evaluated. Found ${searchResult ? searchResult.count : 0} match(es). Result: ${conditionsMet}`);
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
// --- 8. Exécution des actions si les conditions sont remplies ---
|
|
1061
|
+
if (conditionsMet) {
|
|
1062
|
+
if (currentStepDef.actions && currentStepDef.actions.length > 0) {
|
|
1063
|
+
logger.info(`[processWorkflowRun] Run ID: ${runId}, Step ID: ${currentStepId}: Executing ${currentStepDef.actions.length} action(s)...`);
|
|
1064
|
+
for (const actionId of currentStepDef.actions) {
|
|
1065
|
+
if (!isObjectId(actionId)) continue;
|
|
1066
|
+
const actionDef = await dbCollection.findOne({ _id: new ObjectId(actionId), _model: 'workflowAction' });
|
|
1067
|
+
if (!actionDef) return await logError(`Action definition ${actionId} not found.`);
|
|
1068
|
+
const actionResult = await workflowModule.executeStepAction(actionDef, contextData, user, dbCollection);
|
|
1069
|
+
if (!actionResult.success) {
|
|
1070
|
+
stepSucceeded = false;
|
|
1071
|
+
stepError = actionResult.message || `Action ${actionDef.name || actionId} failed.`;
|
|
1072
|
+
break;
|
|
1073
|
+
}
|
|
1074
|
+
if (actionResult.updatedContext) {
|
|
1075
|
+
contextData = { ...contextData, ...actionResult.updatedContext };
|
|
1076
|
+
}
|
|
1077
|
+
logger.info(`[processWorkflowRun] Run ID: ${runId}, Step ID: ${currentStepId}, Action ID: ${actionId}: Executed successfully.`);
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
} else {
|
|
1081
|
+
logger.info(`[processWorkflowRun] Run ID: ${runId}, Step ID: ${currentStepId}: Conditions not met. Skipping actions.`);
|
|
1082
|
+
}
|
|
1083
|
+
} catch (error) {
|
|
1084
|
+
logger.error(`[processWorkflowRun] Run ID: ${runId}, Step ID: ${currentStepId}: Error during condition/action execution: ${error.message}`);
|
|
1085
|
+
stepSucceeded = false;
|
|
1086
|
+
stepError = error.message;
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
// --- 9. Détermination de la prochaine étape ---
|
|
1090
|
+
let nextStepId = null;
|
|
1091
|
+
let finalStatusForRun = null;
|
|
1092
|
+
|
|
1093
|
+
if (stepSucceeded && conditionsMet) {
|
|
1094
|
+
// CHEMIN SUCCÈS : Les conditions sont remplies et les actions ont réussi.
|
|
1095
|
+
logger.info(`[processWorkflowRun] Run ID: ${runId}, Step ID: ${currentStepId}: Step path succeeded.`);
|
|
1096
|
+
nextStepId = currentStepDef.onSuccessStep;
|
|
1097
|
+
if (currentStepDef.isTerminal || !nextStepId) {
|
|
1098
|
+
finalStatusForRun = 'completed';
|
|
1099
|
+
nextStepId = null;
|
|
1100
|
+
}
|
|
1101
|
+
} else {
|
|
1102
|
+
// CHEMIN ÉCHEC/BRANCHE : Une action a échoué OU les conditions n'ont pas été remplies.
|
|
1103
|
+
const reason = stepError ? `Action failed: ${stepError}` : 'Step conditions not met.';
|
|
1104
|
+
logger.warn(`[processWorkflowRun] Run ID: ${runId}, Step ID: ${currentStepId}: Taking failure/branching path. Reason: ${reason}`);
|
|
1105
|
+
nextStepId = currentStepDef.onFailureStep;
|
|
1106
|
+
|
|
1107
|
+
if (!nextStepId || !isObjectId(nextStepId)) {
|
|
1108
|
+
// Fin du workflow. Le statut est 'failed' seulement si une vraie erreur s'est produite.
|
|
1109
|
+
finalStatusForRun = stepError ? 'failed' : 'completed';
|
|
1110
|
+
nextStepId = null;
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
// --- 10. Mise à jour de l'état de l'exécution ---
|
|
1115
|
+
currentStepId = nextStepId;
|
|
1116
|
+
const updatePayload = { contextData };
|
|
1117
|
+
|
|
1118
|
+
if (finalStatusForRun) {
|
|
1119
|
+
updatePayload.status = finalStatusForRun;
|
|
1120
|
+
updatePayload.completedAt = new Date();
|
|
1121
|
+
updatePayload.currentStep = null;
|
|
1122
|
+
if (finalStatusForRun === 'failed' && stepError) {
|
|
1123
|
+
updatePayload.error = stepError;
|
|
1124
|
+
}
|
|
1125
|
+
} else {
|
|
1126
|
+
updatePayload.currentStep = currentStepId;
|
|
1127
|
+
}
|
|
1128
|
+
await dbCollection.updateOne({ _id: runId }, { $set: updatePayload });
|
|
1129
|
+
|
|
1130
|
+
if(finalStatusForRun) {
|
|
1131
|
+
logger.info(`[processWorkflowRun] Finished processing for workflowRun ID: ${runId}. Final Status: ${finalStatusForRun}`);
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
} catch (error) {
|
|
1135
|
+
logger.error(`[processWorkflowRun] Critical error during processing of workflowRun ID: ${runId}. Error: ${error.message}`, error.stack);
|
|
1136
|
+
await dbCollection.updateOne(
|
|
1137
|
+
{ _id: runId, status: { $nin: ['completed', 'failed', 'cancelled'] } },
|
|
1138
|
+
{ $set: { status: 'failed', error: `Critical error: ${error.message}`, completedAt: new Date(), stepExecutionsCount } }
|
|
1139
|
+
);
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
/**
|
|
1143
|
+
* Exécute une action de génération de contenu par IA ('GenerateAIContent').
|
|
1144
|
+
* Récupère la clé API (priorité à l'environnement de l'utilisateur), initialise un client LangChain,
|
|
1145
|
+
* formate un prompt avec les données du contexte, appelle le LLM, et retourne le résultat
|
|
1146
|
+
* pour l'ajouter au contexte du workflow.
|
|
1147
|
+
*
|
|
1148
|
+
* @param {object} action - La définition de l'action depuis le workflow.
|
|
1149
|
+
* @param {object} context - Le contexte d'exécution actuel du workflow.
|
|
1150
|
+
* @param {object} user - L'utilisateur qui exécute le workflow.
|
|
1151
|
+
* @returns {Promise<{success: boolean, updatedContext?: object, message?: string}>}
|
|
1152
|
+
*/
|
|
1153
|
+
async function executeGenerateAIContentAction(action, context, user) {
|
|
1154
|
+
const { aiProvider, aiModel, prompt } = action;
|
|
1155
|
+
|
|
1156
|
+
// 1. Récupérer la clé API (Environnement de l'utilisateur > Environnement de la machine)
|
|
1157
|
+
let apiKey;
|
|
1158
|
+
|
|
1159
|
+
const providers = {
|
|
1160
|
+
"OpenAI" : "OPENAI_API_KEY",
|
|
1161
|
+
"Google": "GOOGLE_API_KEY",
|
|
1162
|
+
}
|
|
1163
|
+
const envKeyName = providers[aiProvider];
|
|
1164
|
+
if( !envKeyName ) {
|
|
1165
|
+
return {success: false, message: i18n.t('aiContent.env', `Clé API pour ${aiProvider} (${envKeyName}) non trouvée dans l'environnement de l'utilisateur.`)};
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
// Cherche d'abord dans les variables d'environnement de l'utilisateur
|
|
1169
|
+
const envCollection = getCollectionForUser(user);
|
|
1170
|
+
const userEnvVar = await envCollection.findOne({ _model: 'env', name: envKeyName, _user: user.username });
|
|
1171
|
+
|
|
1172
|
+
if (userEnvVar && userEnvVar.value) {
|
|
1173
|
+
apiKey = userEnvVar.value;
|
|
1174
|
+
logger.debug(`[AI Action] Utilisation de la clé API de l'environnement de l'utilisateur pour ${aiProvider}.`);
|
|
1175
|
+
} else {
|
|
1176
|
+
apiKey = process.env[envKeyName];
|
|
1177
|
+
logger.debug(`[AI Action] Utilisation de la clé API de l'environnement de la machine pour ${aiProvider}.`);
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
if (!apiKey) {
|
|
1181
|
+
const message = `Clé API pour ${aiProvider} (${envKeyName}) non trouvée dans l'environnement de l'utilisateur ou de la machine.`;
|
|
1182
|
+
logger.error(`[AI Action] ${message}`);
|
|
1183
|
+
return { success: false, message };
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
// 2. Initialiser le client LLM avec LangChain
|
|
1187
|
+
let llm;
|
|
1188
|
+
try {
|
|
1189
|
+
switch (aiProvider) {
|
|
1190
|
+
case 'OpenAI':
|
|
1191
|
+
llm = new ChatOpenAI({ apiKey, modelName: aiModel, temperature: 0.7 });
|
|
1192
|
+
break;
|
|
1193
|
+
case 'GoogleGemini':
|
|
1194
|
+
llm = new ChatGoogleGenerativeAI({ apiKey, modelName: aiModel, temperature: 0.7 });
|
|
1195
|
+
break;
|
|
1196
|
+
default:
|
|
1197
|
+
throw new Error(`Fournisseur IA non supporté : ${aiProvider}`);
|
|
1198
|
+
}
|
|
1199
|
+
} catch (initError) {
|
|
1200
|
+
const message = `Échec de l'initialisation du client IA pour ${aiProvider}: ${initError.message}`;
|
|
1201
|
+
logger.error(`[AI Action] ${message}`);
|
|
1202
|
+
return { success: false, message };
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
try {
|
|
1206
|
+
const substitutedPrompt = await substituteVariables(prompt, context, user);
|
|
1207
|
+
// 3. Créer le "Prompt Template"
|
|
1208
|
+
// LangChain gère la substitution des variables comme {triggerData.name}
|
|
1209
|
+
const realPrompt = ChatPromptTemplate.fromTemplate(substitutedPrompt);
|
|
1210
|
+
|
|
1211
|
+
// 4. Créer la chaîne de traitement (Prompt + Modèle)
|
|
1212
|
+
const chain = realPrompt.pipe(llm);
|
|
1213
|
+
|
|
1214
|
+
// 5. Invoquer la chaîne avec le contexte complet
|
|
1215
|
+
// LangChain remplacera automatiquement les placeholders dans le prompt.
|
|
1216
|
+
logger.debug(`[AI Action] Invocation de l'IA avec le modèle ${aiModel}.`);
|
|
1217
|
+
const response = await chain.invoke(context);
|
|
1218
|
+
|
|
1219
|
+
// 6. Préparer le résultat pour le fusionner dans le contexte du workflow
|
|
1220
|
+
const llmOutput = response.content;
|
|
1221
|
+
const outputVariable = 'aiContent';
|
|
1222
|
+
const updatedContext = {
|
|
1223
|
+
[outputVariable]: llmOutput
|
|
1224
|
+
};
|
|
1225
|
+
|
|
1226
|
+
logger.info(`[AI Action] Contenu généré avec succès et stocké dans la variable de contexte '${outputVariable}'.`);
|
|
1227
|
+
|
|
1228
|
+
return {
|
|
1229
|
+
success: true,
|
|
1230
|
+
updatedContext // Cet objet sera fusionné au contexte principal par le moteur de workflow
|
|
1231
|
+
};
|
|
1232
|
+
|
|
1233
|
+
} catch (llmError) {
|
|
1234
|
+
const message = `Erreur durant la génération de contenu IA avec ${aiProvider}: ${llmError.message}`;
|
|
1235
|
+
logger.error(`[AI Action] ${message}`, llmError.stack);
|
|
1236
|
+
return { success: false, message };
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
|
|
1241
|
+
/**
|
|
1242
|
+
* Gère l'action d'envoi d'email d'un workflow.
|
|
1243
|
+
* @param {object} action - L'objet workflowAction.
|
|
1244
|
+
* @param {object} triggerData - Les données qui ont déclenché le workflow.
|
|
1245
|
+
* @param {object} user - L'utilisateur propriétaire du workflow.
|
|
1246
|
+
*/
|
|
1247
|
+
async function handleSendEmailAction(action, triggerData, user) {
|
|
1248
|
+
|
|
1249
|
+
logger.info(`[Workflow] Exécution de l'action sendEmail pour l'utilisateur ${user.username}.`);
|
|
1250
|
+
|
|
1251
|
+
// 1. Récupérer la configuration SMTP depuis le modèle 'env' de l'utilisateur
|
|
1252
|
+
const envVars = await searchData({
|
|
1253
|
+
user,
|
|
1254
|
+
query: { model: 'env', limit: 100 } // Limite raisonnable pour les variables d'env
|
|
1255
|
+
});
|
|
1256
|
+
|
|
1257
|
+
if (!envVars.data || envVars.data.length === 0) {
|
|
1258
|
+
throw new Error("Aucune variable d'environnement (modèle 'env') trouvée pour la configuration SMTP.");
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
const smtpConfig = envVars.data.reduce((acc, variable) => {
|
|
1262
|
+
if (['SMTP_HOST', 'SMTP_PORT', 'SMTP_USER', 'SMTP_PASS', 'SMTP_FROM'].includes(variable.name)) {
|
|
1263
|
+
acc[variable.name.replace('SMTP_', '').toLowerCase()] = variable.value;
|
|
1264
|
+
}
|
|
1265
|
+
return acc;
|
|
1266
|
+
}, {});
|
|
1267
|
+
|
|
1268
|
+
// 2. Extraire la configuration de l'action et résoudre les placeholders
|
|
1269
|
+
const { emailRecipients, emailSubject, emailContent } = action;
|
|
1270
|
+
if (!emailRecipients || !emailSubject || !emailContent) {
|
|
1271
|
+
throw new Error("SendEmail incomplete (emailRecipients, emailSubject, emailContent are needed).");
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
try {
|
|
1275
|
+
const context = { data: triggerData, user }; // Contexte pour la résolution des placeholders
|
|
1276
|
+
|
|
1277
|
+
const rto = (await Promise.all(emailRecipients.map(r => substituteVariables(r, context, user))));
|
|
1278
|
+
const rsubject = await substituteVariables(emailSubject, context, user);
|
|
1279
|
+
const rbody = await substituteVariables(emailContent, context, user);
|
|
1280
|
+
|
|
1281
|
+
// 3. Préparer les données pour sendEmail
|
|
1282
|
+
const emailData = {
|
|
1283
|
+
title: rsubject,
|
|
1284
|
+
content: rbody,
|
|
1285
|
+
};
|
|
1286
|
+
|
|
1287
|
+
await sendEmail(rto, emailData, smtpConfig, user.lang);
|
|
1288
|
+
logger.info(`[Workflow] Action sendEmail terminée avec succès pour le destinataire: ${rto}`);
|
|
1289
|
+
|
|
1290
|
+
return { success: true, message: `Email sent to ${rto.join(', ')}` };
|
|
1291
|
+
|
|
1292
|
+
} catch (error) {
|
|
1293
|
+
logger.error(`[handleSendEmailAction] Erreur lors de l'envoi de l'email : ${error.message}`, error.stack);
|
|
1294
|
+
return { success: false, message: error.message };
|
|
1295
|
+
}
|
|
1296
|
+
}
|