data-primals-engine 1.3.3 → 1.4.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/README.md +797 -782
- package/client/README.md +20 -0
- package/client/package-lock.json +717 -151
- package/client/package.json +37 -36
- package/client/src/App.jsx +9 -10
- package/client/src/App.scss +42 -1
- package/client/src/Dashboard.jsx +349 -208
- package/client/src/DashboardView.jsx +569 -547
- package/client/src/DataEditor.jsx +20 -2
- package/client/src/DataLayout.jsx +54 -13
- package/client/src/DataTable.jsx +807 -760
- package/client/src/DataTable.scss +187 -90
- package/client/src/Dialog.scss +0 -3
- package/client/src/Field.jsx +1783 -1583
- package/client/src/ModelCreator.jsx +25 -3
- package/client/src/ModelCreatorField.jsx +906 -804
- package/client/src/ModelList.jsx +20 -2
- package/client/src/constants.js +16 -4
- package/client/src/contexts/UIContext.jsx +19 -10
- package/client/src/translations.js +265 -3
- package/package.json +4 -3
- package/server.js +1 -0
- package/src/core.js +18 -0
- package/src/defaultModels.js +70 -10
- package/src/email.js +2 -1
- package/src/filter.js +260 -256
- package/src/i18n.js +503 -30
- package/src/modules/data/data.core.js +5 -1
- package/src/modules/data/data.history.js +489 -489
- package/src/modules/data/data.js +76 -10
- package/src/modules/data/data.routes.js +3 -20
- package/src/modules/user.js +19 -0
- package/src/modules/workflow.js +1808 -1817
- package/client/public/demo/26899917-d1ba-4df4-bb33-48d09a8778da.jpg +0 -0
- package/client/public/demo/4b9894c7-12cd-466d-8fd3-a2757b09ec96.jpg +0 -0
- package/client/public/demo/7ed369ac-a1a2-4b45-b0cd-4fd5bcf5a75a.jpg +0 -0
package/src/modules/workflow.js
CHANGED
|
@@ -1,1818 +1,1809 @@
|
|
|
1
|
-
import {getCollection, getCollectionForUser, isObjectId} from "./mongodb.js";
|
|
2
|
-
import schedule from "node-schedule";
|
|
3
|
-
import {ObjectId} from "mongodb";
|
|
4
|
-
import crypto from "node:crypto";
|
|
5
|
-
|
|
6
|
-
import ivm from 'isolated-vm';
|
|
7
|
-
|
|
8
|
-
import {Logger} from "../gameObject.js";
|
|
9
|
-
import {deleteData, getModel, insertData, patchData, scheduleAlerts, searchData} from "./data/index.js";
|
|
10
|
-
import {emailDefaultConfig, maxExecutionsByStep, maxWorkflowSteps, port} from "../constants.js";
|
|
11
|
-
import {ChatOpenAI} from "@langchain/openai";
|
|
12
|
-
import {ChatGoogleGenerativeAI} from "@langchain/google-genai";
|
|
13
|
-
import {ChatPromptTemplate} from "@langchain/core/prompts";
|
|
14
|
-
import { ChatDeepSeek } from "@langchain/deepseek";
|
|
15
|
-
import i18n from "../../src/i18n.js";
|
|
16
|
-
import {sendEmail} from "../email.js";
|
|
17
|
-
|
|
18
|
-
import * as workflowModule from './workflow.js';
|
|
19
|
-
import {isConditionMet} from "../filter.js";
|
|
20
|
-
import { services } from '../services/index.js';
|
|
21
|
-
import {getEnv} from "./user.js";
|
|
22
|
-
import {getHost} from "../constants.js";
|
|
23
|
-
import {providers} from "./assistant/constants.js";
|
|
24
|
-
import {ChatAnthropic} from "@langchain/anthropic";
|
|
25
|
-
import {getAIProvider} from "./assistant/assistant.js";
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
* @param {
|
|
41
|
-
* @param {object}
|
|
42
|
-
* @
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
* @param {
|
|
85
|
-
* @param {
|
|
86
|
-
* @
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
const
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
//
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
//
|
|
101
|
-
//
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
$
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
//
|
|
120
|
-
//
|
|
121
|
-
//
|
|
122
|
-
//
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
//
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
// le
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
{
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
*
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
const
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
//
|
|
208
|
-
//
|
|
209
|
-
//
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
//
|
|
214
|
-
// const
|
|
215
|
-
//
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
const
|
|
256
|
-
const
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
const
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
await jail.set('
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
await jail.set('
|
|
309
|
-
await jail.set('
|
|
310
|
-
await jail.set('
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
const
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
const
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
const
|
|
386
|
-
const
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
const
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
*
|
|
428
|
-
*
|
|
429
|
-
*
|
|
430
|
-
* @param {object}
|
|
431
|
-
* @param {object}
|
|
432
|
-
* @param {object}
|
|
433
|
-
* @
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
let
|
|
452
|
-
let
|
|
453
|
-
let
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
if
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
if
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
*
|
|
583
|
-
*
|
|
584
|
-
*
|
|
585
|
-
* @param {object}
|
|
586
|
-
* @param {object}
|
|
587
|
-
* @param {object}
|
|
588
|
-
* @
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
//
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
*
|
|
655
|
-
*
|
|
656
|
-
*
|
|
657
|
-
*
|
|
658
|
-
* @param {object}
|
|
659
|
-
* @param {object}
|
|
660
|
-
* @param {object}
|
|
661
|
-
* @
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
let
|
|
689
|
-
let
|
|
690
|
-
let
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
delete updatesObject.
|
|
743
|
-
delete updatesObject.
|
|
744
|
-
delete updatesObject.
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
//
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
}
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
*
|
|
793
|
-
*
|
|
794
|
-
*
|
|
795
|
-
*
|
|
796
|
-
* @param {object}
|
|
797
|
-
* @param {object}
|
|
798
|
-
* @param {object}
|
|
799
|
-
* @
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
let
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
//
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
}
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
*
|
|
883
|
-
*
|
|
884
|
-
*
|
|
885
|
-
* @param {object}
|
|
886
|
-
* @param {object}
|
|
887
|
-
* @
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
}
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
}
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
*
|
|
977
|
-
*
|
|
978
|
-
*
|
|
979
|
-
*
|
|
980
|
-
* @param {
|
|
981
|
-
* @
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
*
|
|
1014
|
-
*
|
|
1015
|
-
*
|
|
1016
|
-
* @param {
|
|
1017
|
-
* @param {object}
|
|
1018
|
-
* @
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
const
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
current
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
let
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
const
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
const
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
*
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
const
|
|
1156
|
-
const
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
const
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
const
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
//
|
|
1220
|
-
//
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
return value
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
const
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
const
|
|
1243
|
-
|
|
1244
|
-
:
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
*
|
|
1254
|
-
*
|
|
1255
|
-
*
|
|
1256
|
-
*
|
|
1257
|
-
* @param {object}
|
|
1258
|
-
* @param {
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
const
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
const
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
}
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
*
|
|
1392
|
-
*
|
|
1393
|
-
*
|
|
1394
|
-
*
|
|
1395
|
-
* @param {
|
|
1396
|
-
* @
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
const
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
let
|
|
1407
|
-
let
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
{
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
const
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
{
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
{
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
let
|
|
1474
|
-
let
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
const
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
let
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
updatePayload.
|
|
1597
|
-
updatePayload.
|
|
1598
|
-
updatePayload.
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
{
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
}
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
*
|
|
1619
|
-
*
|
|
1620
|
-
*
|
|
1621
|
-
*
|
|
1622
|
-
*
|
|
1623
|
-
* @param {object}
|
|
1624
|
-
* @param {object}
|
|
1625
|
-
* @
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
const
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
//
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
//
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
const
|
|
1681
|
-
const
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
}
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
*
|
|
1702
|
-
*
|
|
1703
|
-
*
|
|
1704
|
-
*
|
|
1705
|
-
*
|
|
1706
|
-
* @param {object}
|
|
1707
|
-
* @param {object}
|
|
1708
|
-
* @
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
//
|
|
1769
|
-
const
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
}
|
|
1810
|
-
}
|
|
1811
|
-
};
|
|
1812
|
-
|
|
1813
|
-
} catch (error) {
|
|
1814
|
-
const msg = `[handleSendEmailAction] Unexpected error during email processing: ${error.message}`;
|
|
1815
|
-
logger.error(msg, error.stack);
|
|
1816
|
-
return { success: false, message: msg };
|
|
1817
|
-
}
|
|
1
|
+
import {getCollection, getCollectionForUser, isObjectId} from "./mongodb.js";
|
|
2
|
+
import schedule from "node-schedule";
|
|
3
|
+
import {ObjectId} from "mongodb";
|
|
4
|
+
import crypto from "node:crypto";
|
|
5
|
+
|
|
6
|
+
import ivm from 'isolated-vm';
|
|
7
|
+
|
|
8
|
+
import {Logger} from "../gameObject.js";
|
|
9
|
+
import {deleteData, getModel, insertData, patchData, scheduleAlerts, searchData} from "./data/index.js";
|
|
10
|
+
import {emailDefaultConfig, maxExecutionsByStep, maxWorkflowSteps, port} from "../constants.js";
|
|
11
|
+
import {ChatOpenAI} from "@langchain/openai";
|
|
12
|
+
import {ChatGoogleGenerativeAI} from "@langchain/google-genai";
|
|
13
|
+
import {ChatPromptTemplate} from "@langchain/core/prompts";
|
|
14
|
+
import { ChatDeepSeek } from "@langchain/deepseek";
|
|
15
|
+
import i18n from "../../src/i18n.js";
|
|
16
|
+
import {sendEmail} from "../email.js";
|
|
17
|
+
|
|
18
|
+
import * as workflowModule from './workflow.js';
|
|
19
|
+
import {isConditionMet} from "../filter.js";
|
|
20
|
+
import { services } from '../services/index.js';
|
|
21
|
+
import {getEnv, getSmtpConfig} from "./user.js";
|
|
22
|
+
import {getHost} from "../constants.js";
|
|
23
|
+
import {providers} from "./assistant/constants.js";
|
|
24
|
+
import {ChatAnthropic} from "@langchain/anthropic";
|
|
25
|
+
import {getAIProvider} from "./assistant/assistant.js";
|
|
26
|
+
import {escapeRegex} from "../core.js";
|
|
27
|
+
|
|
28
|
+
let logger = null;
|
|
29
|
+
export async function onInit(defaultEngine) {
|
|
30
|
+
logger = defaultEngine.getComponent(Logger);
|
|
31
|
+
|
|
32
|
+
await scheduleWorkflowTriggers();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Déclenche un workflow par son nom et lui passe des données de contexte.
|
|
38
|
+
* C'est la fonction clé à exposer aux endpoints pour lancer des processus métier.
|
|
39
|
+
*
|
|
40
|
+
* @param {string} name - Le nom du workflow à exécuter.
|
|
41
|
+
* @param {object} data - Les données à injecter dans context.triggerData.
|
|
42
|
+
* @param {object} user - L'objet utilisateur qui initie l'action.
|
|
43
|
+
* @returns {Promise<{success: boolean, message?: string, runId?: ObjectId}>}
|
|
44
|
+
*/
|
|
45
|
+
export async function runWorkflowByName(name, data, user) {
|
|
46
|
+
if (!name) {
|
|
47
|
+
return { success: false, message: "Workflow name is required." };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const dbCollection = await getCollectionForUser(user);
|
|
51
|
+
|
|
52
|
+
// 1. Trouver la définition du workflow par son nom
|
|
53
|
+
const workflowDefinition = await dbCollection.findOne({ _model: 'workflow', name });
|
|
54
|
+
|
|
55
|
+
if (!workflowDefinition) {
|
|
56
|
+
const msg = `Workflow with name "${name}" not found.`;
|
|
57
|
+
logger.error(`[runWorkflowByName] ${msg}`);
|
|
58
|
+
return { success: false, message: msg };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// 2. Créer le document workflowRun
|
|
62
|
+
const workflowRunData = {
|
|
63
|
+
_model: 'workflowRun',
|
|
64
|
+
_user: user._user || user.username,
|
|
65
|
+
workflow: workflowDefinition._id,
|
|
66
|
+
contextData: { triggerData: data }, // Les données passées deviennent le triggerData
|
|
67
|
+
status: 'pending',
|
|
68
|
+
startedAt: new Date()
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const insertResult = await dbCollection.insertOne(workflowRunData);
|
|
72
|
+
logger.info(`[runWorkflowByName] Created workflowRun ${insertResult.insertedId} for workflow "${name}".`);
|
|
73
|
+
|
|
74
|
+
// 3. Lancer le traitement de manière asynchrone
|
|
75
|
+
await processWorkflowRun(insertResult.insertedId, user);
|
|
76
|
+
|
|
77
|
+
return { success: true, runId: insertResult.insertedId };
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Exécute une fonction de manière sécurisée en s'assurant qu'une seule instance
|
|
81
|
+
* s'exécute à la fois, grâce à un système de verrouillage distribué basé sur la base de données.
|
|
82
|
+
* Cette fonction est atomique et conçue pour éviter les conditions de course.
|
|
83
|
+
*
|
|
84
|
+
* @param {string} jobId - Un identifiant unique pour la tâche (ex: 'workflowTrigger_monId').
|
|
85
|
+
* @param {Function} jobFunction - La fonction asynchrone à exécuter si le verrou est acquis.
|
|
86
|
+
* @param {number} [lockDurationMinutes=5] - La durée en minutes pendant laquelle le verrou est considéré comme valide.
|
|
87
|
+
* @returns {Promise<void>}
|
|
88
|
+
*/
|
|
89
|
+
export async function runScheduledJobWithDbLock(jobId, jobFunction, lockDurationMinutes = 5) {
|
|
90
|
+
const jobsCollection = getCollection('job_locks');
|
|
91
|
+
const now = new Date();
|
|
92
|
+
const lockExpiresAt = new Date(now.getTime() + lockDurationMinutes * 60 * 1000);
|
|
93
|
+
let lockAcquired = false; // Drapeau pour savoir si nous devons libérer le verrou
|
|
94
|
+
|
|
95
|
+
// Le bloc try...finally garantit que la libération du verrou est tentée
|
|
96
|
+
// même si la fonction jobFunction lève une exception.
|
|
97
|
+
try {
|
|
98
|
+
// --- PHASE 1: ACQUISITION DU VERROU (de manière atomique) ---
|
|
99
|
+
|
|
100
|
+
// Tentative 1: Mettre à jour un verrou existant qui a expiré.
|
|
101
|
+
// C'est le cas le plus courant après la première exécution.
|
|
102
|
+
// L'opération `updateOne` est atomique.
|
|
103
|
+
const updateResult = await jobsCollection.updateOne(
|
|
104
|
+
{
|
|
105
|
+
jobId: jobId,
|
|
106
|
+
lockedUntil: { $lt: now } // Le verrou est disponible si sa date d'expiration est dans le passé
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
$set: { lockedUntil: lockExpiresAt, lastStarted: now },
|
|
110
|
+
$inc: { runCount: 1 }
|
|
111
|
+
}
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
if (updateResult.modifiedCount === 1) {
|
|
115
|
+
// Succès : nous avons mis à jour le verrou expiré et l'avons acquis.
|
|
116
|
+
lockAcquired = true;
|
|
117
|
+
logger.info(`[Lock] Verrou existant acquis pour la tâche ${jobId}.`);
|
|
118
|
+
} else {
|
|
119
|
+
// Si aucun document n'a été modifié, soit le verrou n'existe pas,
|
|
120
|
+
// soit il est actuellement détenu par un autre processus.
|
|
121
|
+
// Tentative 2: Insérer un nouveau document de verrou.
|
|
122
|
+
// Cette opération échouera avec une erreur de clé dupliquée (code 11000)
|
|
123
|
+
// si un autre processus a réussi à créer le verrou entre-temps.
|
|
124
|
+
try {
|
|
125
|
+
await jobsCollection.insertOne({
|
|
126
|
+
jobId: jobId,
|
|
127
|
+
lockedUntil: lockExpiresAt,
|
|
128
|
+
lastStarted: now,
|
|
129
|
+
runCount: 1
|
|
130
|
+
});
|
|
131
|
+
// Succès : nous avons créé un nouveau verrou et l'avons acquis.
|
|
132
|
+
lockAcquired = true;
|
|
133
|
+
logger.info(`[Lock] Nouveau verrou créé pour la tâche ${jobId}.`);
|
|
134
|
+
} catch (insertError) {
|
|
135
|
+
if (insertError.code === 11000) {
|
|
136
|
+
// Comportement attendu : un autre processus a acquis le verrou.
|
|
137
|
+
// Ce n'est pas une erreur, on saute simplement l'exécution.
|
|
138
|
+
logger.info(`[Lock] Impossible d'acquérir le verrou pour ${jobId}, un autre processus le dent. Exécution ignorée.`);
|
|
139
|
+
} else {
|
|
140
|
+
// Une erreur de base de données inattendue s'est produite.
|
|
141
|
+
throw insertError;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// --- PHASE 2: EXÉCUTION DE LA TÂCHE ---
|
|
147
|
+
if (lockAcquired) {
|
|
148
|
+
logger.info(`[Lock] Exécution de la fonction pour la tâche ${jobId}...`);
|
|
149
|
+
await jobFunction();
|
|
150
|
+
logger.info(`[Lock] La fonction pour la tâche ${jobId} s'est terminée.`);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
} catch (error) {
|
|
154
|
+
// Capture les erreurs de la `jobFunction` ou les erreurs inattendues de la base de données.
|
|
155
|
+
logger.error(`Erreur durant l'exécution de la tâche verrouillée ${jobId}:`, error);
|
|
156
|
+
} finally {
|
|
157
|
+
// --- PHASE 3: LIBÉRATION DU VERROU ---
|
|
158
|
+
if (lockAcquired) {
|
|
159
|
+
try {
|
|
160
|
+
// On libère le verrou en mettant sa date d'expiration dans le passé,
|
|
161
|
+
// le rendant immédiatement disponible pour la prochaine exécution.
|
|
162
|
+
await jobsCollection.updateOne(
|
|
163
|
+
{ jobId: jobId },
|
|
164
|
+
{ $set: { lockedUntil: new Date(0) } }
|
|
165
|
+
);
|
|
166
|
+
logger.info(`[Lock] Verrou libéré pour la tâche ${jobId}.`);
|
|
167
|
+
} catch (releaseError) {
|
|
168
|
+
// Il est crucial de logger cette erreur, car un verrou non libéré peut bloquer les futures exécutions.
|
|
169
|
+
logger.error(`CRITIQUE: Échec de la libération du verrou pour la tâche ${jobId}. Une intervention manuelle peut être nécessaire.`, releaseError);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Planifie l'exécution des workflows déclenchés par une cronExpression.
|
|
178
|
+
* Utilise runScheduledJobWithDbLock pour assurer l'exécution unique à travers plusieurs instances.
|
|
179
|
+
*/
|
|
180
|
+
export async function scheduleWorkflowTriggers() {
|
|
181
|
+
logger.info('Starting scheduling of workflow triggers...');
|
|
182
|
+
try {
|
|
183
|
+
const datasCollection = getCollection('datas'); // Ou la collection appropriée pour les workflows
|
|
184
|
+
|
|
185
|
+
// Trouver tous les workflows actifs avec une cronExpression définie
|
|
186
|
+
const workflowsToSchedule = await datasCollection.find({
|
|
187
|
+
_model: 'workflowTrigger',
|
|
188
|
+
cronExpression: { $exists: true, $ne: "" }
|
|
189
|
+
// Ajoutez d'autres conditions si nécessaire (ex: active: true)
|
|
190
|
+
}).toArray();
|
|
191
|
+
|
|
192
|
+
console.log(`Found ${workflowsToSchedule.length} workflow triggers with cron expressions to schedule.`);
|
|
193
|
+
|
|
194
|
+
for (const workflow of workflowsToSchedule) {
|
|
195
|
+
const jobId = `workflowTrigger_${workflow._id}`; // ID unique pour le verrou du job
|
|
196
|
+
const cronExpression = workflow.cronExpression;
|
|
197
|
+
if( !cronExpression )
|
|
198
|
+
continue;
|
|
199
|
+
// Planifier la tâche en utilisant node-schedule
|
|
200
|
+
schedule.scheduleJob(cronExpression, async () => {
|
|
201
|
+
console.log(`Cron triggered for job ${jobId}. Attempting to run with lock...`);
|
|
202
|
+
|
|
203
|
+
// Utiliser runScheduledJobWithDbLock pour exécuter la tâche
|
|
204
|
+
await runScheduledJobWithDbLock(
|
|
205
|
+
jobId,
|
|
206
|
+
async () => {
|
|
207
|
+
// --- Début de la logique spécifique au workflow ---
|
|
208
|
+
// C'est ici que vous mettriez le code qui doit être exécuté
|
|
209
|
+
// lorsque le workflow est déclenché par le cron.
|
|
210
|
+
// Par exemple:
|
|
211
|
+
console.log(`Executing task logic for workflow ${workflow.name} (ID: ${workflow._id})`);
|
|
212
|
+
|
|
213
|
+
// Exemple:
|
|
214
|
+
// const targetModel = workflow.targetModel;
|
|
215
|
+
// const action = workflow.action;
|
|
216
|
+
// await executeWorkflowAction(targetModel, action, workflow.parameters);
|
|
217
|
+
|
|
218
|
+
// Simule une tâche asynchrone
|
|
219
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
220
|
+
|
|
221
|
+
console.log(`Task logic completed for workflow ${workflow.name} (ID: ${workflow._id})`);
|
|
222
|
+
// --- Fin de la logique spécifique au workflow ---
|
|
223
|
+
},
|
|
224
|
+
workflow.lockDurationMinutes || 5 // Utilise la durée du workflow ou une valeur par défaut
|
|
225
|
+
);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
console.log('Finished scheduling workflow triggers.');
|
|
231
|
+
|
|
232
|
+
} catch (error) {
|
|
233
|
+
console.error('Error during scheduling of workflow triggers:', error);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
async function handleWaitAction(actionDef, contextData, user) {
|
|
238
|
+
const { duration, durationUnit } = actionDef;
|
|
239
|
+
if (!duration || !durationUnit) {
|
|
240
|
+
return { success: false, message: "Wait action requires 'duration' and 'durationUnit'." };
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Retourne un statut spécial que le moteur de workflow comprendra
|
|
244
|
+
return {
|
|
245
|
+
success: true,
|
|
246
|
+
status: 'paused', // Statut spécial
|
|
247
|
+
duration,
|
|
248
|
+
durationUnit,
|
|
249
|
+
message: `Workflow will be paused for ${duration} ${durationUnit}.`
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
export async function executeSafeJavascript(actionDef, context, user) {
|
|
255
|
+
const code = actionDef.script;
|
|
256
|
+
const collectedLogs = [];
|
|
257
|
+
const isolate = new ivm.Isolate({ memoryLimit: 128 }); // 128MB memory limit
|
|
258
|
+
|
|
259
|
+
try {
|
|
260
|
+
const vmContext = await isolate.createContext();
|
|
261
|
+
const jail = vmContext.global;
|
|
262
|
+
|
|
263
|
+
const find = async (modelName, filter) => {
|
|
264
|
+
const result = await searchData({ model: modelName, filter: JSON.parse(filter) }, user);
|
|
265
|
+
return new ivm.ExternalCopy(result).copyInto();
|
|
266
|
+
};
|
|
267
|
+
const findOne = async (modelName, filter) => {
|
|
268
|
+
const result = await searchData({ model: modelName, filter: JSON.parse(filter), limit: 1 }, user);
|
|
269
|
+
return new ivm.ExternalCopy(result.data?.[0] || null).copyInto();
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
// 1. Build the sandboxed API methods
|
|
273
|
+
await jail.set('_workflow_run', new ivm.Reference(async (name, contextData) => {
|
|
274
|
+
const result = await runWorkflowByName(name, JSON.parse(contextData), user);
|
|
275
|
+
return new ivm.ExternalCopy(result).copyInto();
|
|
276
|
+
}));
|
|
277
|
+
await jail.set('_db_create', new ivm.Reference(async (modelName, dataObject) => {
|
|
278
|
+
const result = await insertData(modelName, JSON.parse(dataObject), {}, user, false);
|
|
279
|
+
if (result.success && result.insertedIds) {
|
|
280
|
+
result.insertedIds = result.insertedIds.map(id => id.toString());
|
|
281
|
+
}
|
|
282
|
+
return new ivm.ExternalCopy(result).copyInto();
|
|
283
|
+
}));
|
|
284
|
+
await jail.set('_db_find', new ivm.Reference(find));
|
|
285
|
+
await jail.set('_db_findOne', new ivm.Reference(findOne));
|
|
286
|
+
|
|
287
|
+
await jail.set('_db_update', new ivm.Reference(async (modelName, filter, updateObject) => {
|
|
288
|
+
const result = await patchData(modelName, JSON.parse(filter), JSON.parse(updateObject), {}, user, false);
|
|
289
|
+
return new ivm.ExternalCopy(result).copyInto();
|
|
290
|
+
}));
|
|
291
|
+
await jail.set('_db_delete', new ivm.Reference(async (modelName, filter) => {
|
|
292
|
+
const result = await deleteData(modelName, JSON.parse(filter), user, false);
|
|
293
|
+
return new ivm.ExternalCopy(result).copyInto();
|
|
294
|
+
}));
|
|
295
|
+
|
|
296
|
+
const createLoggerMethod = (level) => {
|
|
297
|
+
return (...args) => {
|
|
298
|
+
const message = args.join(' ');
|
|
299
|
+
collectedLogs.push({
|
|
300
|
+
level,
|
|
301
|
+
message,
|
|
302
|
+
timestamp: new Date().toISOString()
|
|
303
|
+
});
|
|
304
|
+
logger.trace(level, '[VM Script]', message);
|
|
305
|
+
};
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
await jail.set('_log_info', createLoggerMethod('info'));
|
|
309
|
+
await jail.set('_log_warn', createLoggerMethod('warn'));
|
|
310
|
+
await jail.set('_log_error', createLoggerMethod('error'));
|
|
311
|
+
await jail.set('_env_get', new ivm.Reference(async (variableName) => {
|
|
312
|
+
if (!variableName) return null;
|
|
313
|
+
const result = await searchData({ model: 'env', filter: { name: variableName }, limit: 1 }, user);
|
|
314
|
+
return new ivm.ExternalCopy(result.data?.[0]?.value || null).copyInto();
|
|
315
|
+
}));
|
|
316
|
+
await jail.set('_env_get_all', new ivm.Reference(async () => {
|
|
317
|
+
const result = await getEnv(user);
|
|
318
|
+
return new ivm.ExternalCopy(result).copyInto();
|
|
319
|
+
}));
|
|
320
|
+
await jail.set('_http_request', new ivm.Reference(async (method, url, optionsStr) => {
|
|
321
|
+
try {
|
|
322
|
+
const options = optionsStr ? JSON.parse(optionsStr) : {};
|
|
323
|
+
const fetchOptions = {
|
|
324
|
+
method: method.toUpperCase(),
|
|
325
|
+
headers: options.headers || {},
|
|
326
|
+
body: options.body ? (typeof options.body === 'object' ? JSON.stringify(options.body) : options.body) : undefined
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
const response = await fetch(url, fetchOptions);
|
|
330
|
+
const responseBody = await response.json().catch(() => response.text());
|
|
331
|
+
|
|
332
|
+
const result = { success: response.ok, status: response.status, body: responseBody };
|
|
333
|
+
return new ivm.ExternalCopy(result).copyInto();
|
|
334
|
+
} catch (error) {
|
|
335
|
+
logger.error(`[VM http_request] Error: ${error.message}`);
|
|
336
|
+
return new ivm.ExternalCopy({ success: false, message: error.message }).copyInto();
|
|
337
|
+
}
|
|
338
|
+
}));
|
|
339
|
+
|
|
340
|
+
// Contexte sécurisé
|
|
341
|
+
const safeContext = JSON.parse(JSON.stringify(context));
|
|
342
|
+
|
|
343
|
+
await jail.set('context', new ivm.ExternalCopy(safeContext).copyInto());
|
|
344
|
+
|
|
345
|
+
// Exécution
|
|
346
|
+
const fullScript = `
|
|
347
|
+
const normalizeArgs = args => args.map(arg => {
|
|
348
|
+
if (typeof arg === 'object' && arg !== null) {
|
|
349
|
+
return JSON.stringify(arg); // Convert objects to strings
|
|
350
|
+
}
|
|
351
|
+
return arg;
|
|
352
|
+
});
|
|
353
|
+
const db = {
|
|
354
|
+
create: (...args) => _db_create.applySyncPromise(null, normalizeArgs(args)),
|
|
355
|
+
find: (...args) => _db_find.applySyncPromise(null, normalizeArgs(args)),
|
|
356
|
+
findOne: (...args) => _db_findOne.applySyncPromise(null, normalizeArgs(args)),
|
|
357
|
+
update: (...args) => _db_update.applySyncPromise(null, normalizeArgs(args)),
|
|
358
|
+
delete: (...args) => _db_delete.applySyncPromise(null, normalizeArgs(args))
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
const workflow = {
|
|
362
|
+
run: (...args) => _workflow_run.applySyncPromise(null, normalizeArgs(args))
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
const logger = {
|
|
366
|
+
info: _log_info,
|
|
367
|
+
warn: _log_warn,
|
|
368
|
+
error: _log_error
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
const env = {
|
|
372
|
+
get: _env_get,
|
|
373
|
+
getAll: _env_get_all
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
const http = {
|
|
377
|
+
request: (...args) => _http_request.applySyncPromise(null, normalizeArgs(args))
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
(async function() {
|
|
381
|
+
${code}
|
|
382
|
+
})();
|
|
383
|
+
`;
|
|
384
|
+
|
|
385
|
+
const TIMEOUT = 5000;
|
|
386
|
+
const script = await isolate.compileScript(fullScript, { timeout: TIMEOUT });
|
|
387
|
+
const result = await script.run(vmContext, {
|
|
388
|
+
timeout: TIMEOUT,
|
|
389
|
+
promise: true,
|
|
390
|
+
copy: true // Copie automatique du résultat
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
// Vérifier si le script lui-même a signalé un échec.
|
|
394
|
+
if (result && typeof result === 'object' && result.success === false) {
|
|
395
|
+
const scriptMessage = result.message || 'Le script a signalé un échec sans message.';
|
|
396
|
+
collectedLogs.push({
|
|
397
|
+
level: 'warn',
|
|
398
|
+
message: `Script reported failure: ${scriptMessage}`,
|
|
399
|
+
timestamp: new Date().toISOString()
|
|
400
|
+
});
|
|
401
|
+
return {
|
|
402
|
+
success: false,
|
|
403
|
+
message: scriptMessage,
|
|
404
|
+
logs: collectedLogs
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
return { success: true, data: result, logs: collectedLogs, updatedContext: { result } };
|
|
409
|
+
} catch (error) {
|
|
410
|
+
const errorMessage = `Script execution failed: ${error.message}`;
|
|
411
|
+
const finalErrorMessage = logger.trace('critical', `[VM Script] ${errorMessage}\n${error.stack}`);
|
|
412
|
+
collectedLogs.push({
|
|
413
|
+
level: 'critical',
|
|
414
|
+
message: finalErrorMessage,
|
|
415
|
+
timestamp: new Date().toISOString()
|
|
416
|
+
});
|
|
417
|
+
return { success: false, message: errorMessage, logs: collectedLogs };
|
|
418
|
+
} finally {
|
|
419
|
+
// 3. CRUCIAL: Dispose of the isolate to prevent memory leaks
|
|
420
|
+
if (isolate && !isolate.isDisposed) {
|
|
421
|
+
isolate.dispose();
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Handles the 'HttpRequest' workflow action.
|
|
428
|
+
* Sends an HTTP request to a specified URL with substituted data using native fetch.
|
|
429
|
+
*
|
|
430
|
+
* @param {object} actionDef - The definition of the 'Webhook' action.
|
|
431
|
+
* @param {object} contextData - The current workflow run context data.
|
|
432
|
+
* @param {object} user - The user object (peut être utilisé pour l'authentification ou le logging).
|
|
433
|
+
* @param {object} dbCollection - The MongoDB collection (moins pertinent ici, mais gardé pour la cohérence).
|
|
434
|
+
* @returns {Promise<{success: boolean, message?: string, responseStatus?: number, responseBody?: any}>} - Result of the action.
|
|
435
|
+
*/
|
|
436
|
+
async function handleHttpRequestAction(actionDef, contextData, user, dbCollection) {
|
|
437
|
+
const { name: actionName, _id: actionId, url, method = 'POST', headers: headersTemplate, body: bodyTemplate } = actionDef;
|
|
438
|
+
|
|
439
|
+
// 1. Basic Validation
|
|
440
|
+
if (!url) {
|
|
441
|
+
const msg = `[handleHttpRequestAction] Action ${actionName} (${actionId}): Missing 'url'.`;
|
|
442
|
+
logger.error(msg);
|
|
443
|
+
return { success: false, message: msg };
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
logger.info(`[handleHttpRequestAction] Action ${actionName} (${actionId}): Executing webhook. Method: ${method}`);
|
|
447
|
+
|
|
448
|
+
try {
|
|
449
|
+
// 2. Substitute Variables
|
|
450
|
+
const substitutedUrl = await substituteVariables(url, contextData, user);
|
|
451
|
+
let substitutedHeadersString;
|
|
452
|
+
let substitutedBodyString;
|
|
453
|
+
let headersObject = {};
|
|
454
|
+
let bodyObject = null;
|
|
455
|
+
|
|
456
|
+
// Substitute Headers (JSON string or object)
|
|
457
|
+
if (headersTemplate) {
|
|
458
|
+
if (typeof headersTemplate === 'string') {
|
|
459
|
+
substitutedHeadersString = await substituteVariables(headersTemplate, contextData, user);
|
|
460
|
+
} else if (typeof headersTemplate === 'object') {
|
|
461
|
+
headersObject = await substituteVariables(headersTemplate, contextData, user);
|
|
462
|
+
} else {
|
|
463
|
+
logger.warn(`[handleHttpRequestAction] Action ${actionName} (${actionId}): 'headers' has an invalid type (${typeof headersTemplate}). Ignoring.`);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// Substitute Body (JSON string or object) - only relevant for methods like POST, PUT, PATCH
|
|
468
|
+
if (bodyTemplate && ['POST', 'PUT', 'PATCH'].includes(method.toUpperCase())) {
|
|
469
|
+
if (typeof bodyTemplate === 'string') {
|
|
470
|
+
substitutedBodyString = await substituteVariables(bodyTemplate, contextData, user);
|
|
471
|
+
} else if (typeof bodyTemplate === 'object') {
|
|
472
|
+
bodyObject = await substituteVariables(bodyTemplate, contextData, user);
|
|
473
|
+
} else {
|
|
474
|
+
logger.warn(`[handleHttpRequestAction] Action ${actionName} (${actionId}): 'body' has an invalid type (${typeof bodyTemplate}). Ignoring.`);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// 3. Parse substituted JSON strings
|
|
479
|
+
if (substitutedHeadersString) {
|
|
480
|
+
try {
|
|
481
|
+
headersObject = JSON.parse(substitutedHeadersString);
|
|
482
|
+
if (typeof headersObject !== 'object' || headersObject === null) {
|
|
483
|
+
throw new Error("Parsed headers is not a valid object.");
|
|
484
|
+
}
|
|
485
|
+
} catch (parseError) {
|
|
486
|
+
logger.error(`[handleHttpRequestAction] Action ${actionName} (${actionId}): Failed to parse substituted 'headers' JSON. Error: ${parseError.message}. Using default headers. Substituted string: ${substitutedHeadersString}`);
|
|
487
|
+
headersObject = { 'Content-Type': 'application/json' }; // Fallback
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
// Ensure Content-Type if body is present and headers don't specify it
|
|
491
|
+
if (bodyObject !== null || substitutedBodyString) {
|
|
492
|
+
if (!headersObject['Content-Type'] && !headersObject['content-type']) {
|
|
493
|
+
headersObject['Content-Type'] = 'application/json';
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
if (substitutedBodyString) {
|
|
499
|
+
try {
|
|
500
|
+
// Try parsing first, maybe it's valid JSON already
|
|
501
|
+
bodyObject = JSON.parse(substitutedBodyString);
|
|
502
|
+
} catch (parseError) {
|
|
503
|
+
// If parsing fails, treat it as a plain string body
|
|
504
|
+
bodyObject = substitutedBodyString;
|
|
505
|
+
// Adjust Content-Type if it was assumed to be JSON
|
|
506
|
+
if (headersObject['Content-Type'] === 'application/json') {
|
|
507
|
+
headersObject['Content-Type'] = 'text/plain';
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// 4. Prepare Fetch Options
|
|
513
|
+
const fetchOptions = {
|
|
514
|
+
method: method.toUpperCase(),
|
|
515
|
+
headers: headersObject // Native fetch accepts an object directly
|
|
516
|
+
};
|
|
517
|
+
|
|
518
|
+
if (bodyObject !== null && ['POST', 'PUT', 'PATCH'].includes(fetchOptions.method)) {
|
|
519
|
+
// Stringify if it's an object and content type is JSON, otherwise use as is
|
|
520
|
+
if (typeof bodyObject === 'object' && headersObject['Content-Type'] === 'application/json') {
|
|
521
|
+
fetchOptions.body = JSON.stringify(bodyObject);
|
|
522
|
+
} else {
|
|
523
|
+
fetchOptions.body = bodyObject; // Use string directly
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// 5. Execute Fetch Request using native fetch
|
|
528
|
+
logger.info(`[handleHttpRequestAction] Action ${actionName} (${actionId}): Calling URL: ${substitutedUrl}`);
|
|
529
|
+
const response = await fetch(substitutedUrl, fetchOptions); // Utilisation de fetch natif
|
|
530
|
+
|
|
531
|
+
// 6. Process Response
|
|
532
|
+
let responseBody;
|
|
533
|
+
const contentType = response.headers.get('content-type');
|
|
534
|
+
try {
|
|
535
|
+
if (contentType && contentType.includes('application/json')) {
|
|
536
|
+
responseBody = await response.json();
|
|
537
|
+
} else {
|
|
538
|
+
responseBody = await response.text();
|
|
539
|
+
}
|
|
540
|
+
} catch (responseParseError) {
|
|
541
|
+
logger.error(`[handleHttpRequestAction] Action ${actionName} (${actionId}): Failed to parse response body. Error: ${responseParseError.message}`);
|
|
542
|
+
// Try reading as text again in case of error during json parsing
|
|
543
|
+
try {
|
|
544
|
+
responseBody = await response.text();
|
|
545
|
+
} catch (textError) {
|
|
546
|
+
responseBody = "[Could not parse response body]";
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
logger.info(`[handleHttpRequestAction] Action ${actionName} (${actionId}): Received response. Status: ${response.status}`);
|
|
551
|
+
|
|
552
|
+
// 7. Return Result
|
|
553
|
+
if (response.ok) { // Status code 200-299
|
|
554
|
+
return {
|
|
555
|
+
success: true,
|
|
556
|
+
message: `Webhook executed successfully. Status: ${response.status}`,
|
|
557
|
+
responseStatus: response.status,
|
|
558
|
+
responseBody: responseBody,
|
|
559
|
+
updatedContext: { httpResponse: responseBody }
|
|
560
|
+
};
|
|
561
|
+
} else {
|
|
562
|
+
// Handle non-successful responses (4xx, 5xx)
|
|
563
|
+
const errorMsg = `Webhook execution failed. Status: ${response.status}. Response: ${typeof responseBody === 'string' ? responseBody : JSON.stringify(responseBody)}`;
|
|
564
|
+
logger.error(`[handleHttpRequestAction] Action ${actionName} (${actionId}): ${errorMsg}`);
|
|
565
|
+
return {
|
|
566
|
+
success: false,
|
|
567
|
+
message: errorMsg,
|
|
568
|
+
responseStatus: response.status,
|
|
569
|
+
responseBody: responseBody
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
} catch (error) {
|
|
574
|
+
// Catch network errors or other unexpected errors during the process
|
|
575
|
+
const msg = `[handleHttpRequestAction] Action ${actionName} (${actionId}): Unexpected error during webhook execution. Error: ${error.message}`;
|
|
576
|
+
logger.error(msg, error.stack);
|
|
577
|
+
return { success: false, message: msg };
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/**
|
|
582
|
+
* Handles the 'CreateData' workflow action.
|
|
583
|
+
* Substitutes variables, validates, and inserts a new document.
|
|
584
|
+
*
|
|
585
|
+
* @param {object} actionDef - The definition of the 'CreateData' action.
|
|
586
|
+
* @param {object} contextData - The current workflow run context data.
|
|
587
|
+
* @param {object} user - The user object.
|
|
588
|
+
* @param {object} dbCollection - The MongoDB collection for the user.
|
|
589
|
+
* @returns {Promise<{success: boolean, message?: string, insertedId?: ObjectId}>} - Result of the action.
|
|
590
|
+
*/
|
|
591
|
+
async function handleCreateDataAction(actionDef, contextData, user, dbCollection) {
|
|
592
|
+
const { targetModel, dataToCreate } = actionDef;
|
|
593
|
+
|
|
594
|
+
// 1. Basic Validation
|
|
595
|
+
if (!targetModel || typeof targetModel !== 'string') {
|
|
596
|
+
const msg = `[handleCreateDataAction] Action ${actionDef.name} (${actionDef._id}): Missing or invalid 'targetModel'.`;
|
|
597
|
+
logger.error(msg);
|
|
598
|
+
return { success: false, message: msg };
|
|
599
|
+
}
|
|
600
|
+
if (!dataToCreate) {
|
|
601
|
+
const msg = `[handleCreateDataAction] Action ${actionDef.name} (${actionDef._id}): Missing 'dataToCreate' template.`;
|
|
602
|
+
logger.error(msg);
|
|
603
|
+
return { success: false, message: msg };
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
logger.info(`[handleCreateDataAction] Action ${actionDef.name} (${actionDef._id}): Creating data for model '${targetModel}'.`);
|
|
607
|
+
|
|
608
|
+
try {
|
|
609
|
+
// 2. Substitute Variables in the data template
|
|
610
|
+
let dataObject;
|
|
611
|
+
|
|
612
|
+
if (typeof dataToCreate === 'string') {
|
|
613
|
+
const substitutedDataString = await substituteVariables(dataToCreate, contextData, user);
|
|
614
|
+
try {
|
|
615
|
+
// CORRECTION : Utiliser la bonne variable (substitutedDataString)
|
|
616
|
+
dataObject = JSON.parse(substitutedDataString);
|
|
617
|
+
} catch (parseError) {
|
|
618
|
+
const msg = `Failed to parse substituted JSON string: ${substitutedDataString}. Error: ${parseError.message}`;
|
|
619
|
+
logger.error(`[handleCreateDataAction] ${msg}`);
|
|
620
|
+
return { success: false, message: msg };
|
|
621
|
+
}
|
|
622
|
+
} else if (typeof dataToCreate === 'object') {
|
|
623
|
+
// CORRECTION : Assigner le résultat de la substitution à dataObject.
|
|
624
|
+
// On passe une copie pour ne pas muter le template original.
|
|
625
|
+
dataObject = await substituteVariables(JSON.parse(JSON.stringify(dataToCreate)), contextData, user);
|
|
626
|
+
} else {
|
|
627
|
+
const msg = `[handleCreateDataAction] 'dataToCreate' has an invalid type (${typeof dataToCreate}). Expected string (JSON) or object.`;
|
|
628
|
+
logger.error(msg);
|
|
629
|
+
return { success: false, message: msg };
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
// Log pour débogage
|
|
633
|
+
logger.debug('Final data object after substitution:', dataObject);
|
|
634
|
+
|
|
635
|
+
// 3. Appeler insertData avec l'objet correctement substitué
|
|
636
|
+
const result = await insertData(targetModel, dataObject, [], user, false, true); // On attend la fin du workflow déclenché par cette création
|
|
637
|
+
|
|
638
|
+
if (result.success) {
|
|
639
|
+
return { success: true, insertedIds: result.insertedIds };
|
|
640
|
+
} else {
|
|
641
|
+
// Propage l'erreur venant de insertData
|
|
642
|
+
return { success: false, message: result.error || "Insertion failed." };
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
} catch (error) {
|
|
646
|
+
const msg = `[handleCreateDataAction] Action ${actionDef.name} (${actionDef._id}): Unexpected error during creation for model '${targetModel}'. Error: ${error.message}`;
|
|
647
|
+
logger.error(msg, error.stack);
|
|
648
|
+
return { success: false, message: msg };
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
|
|
653
|
+
/**
|
|
654
|
+
* Handles the 'UpdateData' workflow action.
|
|
655
|
+
* Finds document(s) based on a selector, substitutes variables in updates,
|
|
656
|
+
* validates, and updates the document(s) using the updateData function.
|
|
657
|
+
*
|
|
658
|
+
* @param {object} actionDef - The definition of the 'UpdateData' action.
|
|
659
|
+
* @param {object} contextData - The current workflow run context data.
|
|
660
|
+
* @param {object} user - The user object.
|
|
661
|
+
* @param {object} dbCollection - The MongoDB collection for the user (bien que updateData utilise getCollectionForUser).
|
|
662
|
+
* @returns {Promise<{success: boolean, message?: string, modifiedCount?: number, matchedCount?: number}>} - Result of the action.
|
|
663
|
+
*/
|
|
664
|
+
async function handleUpdateDataAction(actionDef, contextData, user) {
|
|
665
|
+
const { targetModel, targetSelector, fieldsToUpdate, updateMultiple = false } = actionDef; // updateMultiple optionnel, défaut false
|
|
666
|
+
|
|
667
|
+
// 1. Basic Validation
|
|
668
|
+
if (!targetModel || typeof targetModel !== 'string') {
|
|
669
|
+
const msg = `[handleUpdateDataAction] Action ${actionDef.name} (${actionDef._id}): Missing or invalid 'targetModel'.`;
|
|
670
|
+
logger.error(msg);
|
|
671
|
+
return { success: false, message: msg };
|
|
672
|
+
}
|
|
673
|
+
if (!targetSelector) {
|
|
674
|
+
const msg = `[handleUpdateDataAction] Action ${actionDef.name} (${actionDef._id}): Missing 'targetSelector'.`;
|
|
675
|
+
logger.error(msg);
|
|
676
|
+
return { success: false, message: msg };
|
|
677
|
+
}
|
|
678
|
+
if (!fieldsToUpdate) {
|
|
679
|
+
const msg = `[handleUpdateDataAction] Action ${actionDef.name} (${actionDef._id}): Missing 'fieldsToUpdate'.`;
|
|
680
|
+
logger.error(msg);
|
|
681
|
+
return { success: false, message: msg };
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
logger.info(`[handleUpdateDataAction] Action ${actionDef.name} (${actionDef._id}): Updating data for model '${targetModel}'. Multiple: ${updateMultiple}`);
|
|
685
|
+
|
|
686
|
+
try {
|
|
687
|
+
// 2. Substitute Variables in selector and updates
|
|
688
|
+
let substitutedSelectorString;
|
|
689
|
+
let substitutedUpdatesString;
|
|
690
|
+
let selectorObject;
|
|
691
|
+
let updatesObject;
|
|
692
|
+
|
|
693
|
+
// Substitute targetSelector (assuming it's a JSON string or object)
|
|
694
|
+
if (typeof targetSelector === 'string') {
|
|
695
|
+
substitutedSelectorString = await substituteVariables(targetSelector, contextData, user);
|
|
696
|
+
} else if (typeof targetSelector === 'object') {
|
|
697
|
+
selectorObject = await substituteVariables(targetSelector, contextData, user); // Substitute values within the object
|
|
698
|
+
} else {
|
|
699
|
+
const msg = `[handleUpdateDataAction] Action ${actionDef.name} (${actionDef._id}): 'targetSelector' has an invalid type (${typeof targetSelector}). Expected string (JSON) or object.`;
|
|
700
|
+
logger.error(msg);
|
|
701
|
+
return { success: false, message: msg };
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
// Substitute fieldsToUpdate (assuming it's a JSON string or object)
|
|
705
|
+
if (typeof fieldsToUpdate === 'string') {
|
|
706
|
+
substitutedUpdatesString = await substituteVariables(fieldsToUpdate, contextData, user);
|
|
707
|
+
} else if (typeof fieldsToUpdate === 'object') {
|
|
708
|
+
updatesObject = await substituteVariables(fieldsToUpdate, contextData, user); // Substitute values within the object
|
|
709
|
+
} else {
|
|
710
|
+
const msg = `[handleUpdateDataAction] Action ${actionDef.name} (${actionDef._id}): 'fieldsToUpdate' has an invalid type (${typeof fieldsToUpdate}). Expected string (JSON) or object.`;
|
|
711
|
+
logger.error(msg);
|
|
712
|
+
return { success: false, message: msg };
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
// 3. Parse substituted JSON strings
|
|
716
|
+
if (substitutedSelectorString) {
|
|
717
|
+
try {
|
|
718
|
+
selectorObject = JSON.parse(substitutedSelectorString);
|
|
719
|
+
if (typeof selectorObject !== 'object' || selectorObject === null) {
|
|
720
|
+
throw new Error("Parsed selector is not a valid object.");
|
|
721
|
+
}
|
|
722
|
+
} catch (parseError) {
|
|
723
|
+
const msg = `[handleUpdateDataAction] Action ${actionDef.name} (${actionDef._id}): Failed to parse substituted 'targetSelector' JSON. Error: ${parseError.message}. Substituted string: ${substitutedSelectorString}`;
|
|
724
|
+
logger.error(msg);
|
|
725
|
+
return { success: false, message: msg };
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
if (substitutedUpdatesString) {
|
|
729
|
+
try {
|
|
730
|
+
updatesObject = JSON.parse(substitutedUpdatesString);
|
|
731
|
+
if (typeof updatesObject !== 'object' || updatesObject === null) {
|
|
732
|
+
throw new Error("Parsed updates is not a valid object.");
|
|
733
|
+
}
|
|
734
|
+
} catch (parseError) {
|
|
735
|
+
const msg = `[handleUpdateDataAction] Action ${actionDef.name} (${actionDef._id}): Failed to parse substituted 'fieldsToUpdate' JSON. Error: ${parseError.message}. Substituted string: ${substitutedUpdatesString}`;
|
|
736
|
+
logger.error(msg);
|
|
737
|
+
return { success: false, message: msg };
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
// Remove system fields potentially included in updates by mistake
|
|
742
|
+
delete updatesObject._id;
|
|
743
|
+
delete updatesObject._model;
|
|
744
|
+
delete updatesObject._user;
|
|
745
|
+
delete updatesObject._hash;
|
|
746
|
+
|
|
747
|
+
if (Object.keys(updatesObject).length === 0) {
|
|
748
|
+
const msg = `[handleUpdateDataAction] Action ${actionDef.name} (${actionDef._id}): 'fieldsToUpdate' resulted in an empty update object after substitution/parsing. Nothing to update.`;
|
|
749
|
+
logger.warn(msg);
|
|
750
|
+
return { success: true, message: "No fields to update.", modifiedCount: 0, matchedCount: 0 };
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
const updateResult = await patchData(
|
|
754
|
+
targetModel,
|
|
755
|
+
selectorObject,
|
|
756
|
+
updatesObject,
|
|
757
|
+
{},
|
|
758
|
+
user, false
|
|
759
|
+
);
|
|
760
|
+
|
|
761
|
+
// 6. Return result
|
|
762
|
+
if (updateResult.success || updateResult.unmodified) {
|
|
763
|
+
logger.info(`[handleUpdateDataAction] Action ${actionDef.name} (${actionDef._id}): Update successful for model '${targetModel}'. Matched: ${updateResult.matchedCount}, Modified: ${updateResult.modifiedCount}`);
|
|
764
|
+
return {
|
|
765
|
+
success: true,
|
|
766
|
+
modifiedCount: updateResult.modifiedCount,
|
|
767
|
+
matchedCount: updateResult.matchedCount,
|
|
768
|
+
message: updateResult.message,
|
|
769
|
+
updatedContext: {
|
|
770
|
+
triggerData: {...contextData.triggerData || {}, ...updatesObject}
|
|
771
|
+
}
|
|
772
|
+
};
|
|
773
|
+
} else {
|
|
774
|
+
// updateData now throws errors, so this 'else' might not be reached often,
|
|
775
|
+
// but kept for safety in case it returns { success: false } in some scenarios.
|
|
776
|
+
const msg = `[handleUpdateDataAction] Action ${actionDef.name} (${actionDef._id}): updateData function reported failure. Message: ${updateResult.error}`;
|
|
777
|
+
logger.error(msg);
|
|
778
|
+
return { success: false, message: msg };
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
} catch (error) {
|
|
782
|
+
// Catch errors thrown by updateData (validation, permissions, DB errors) or other unexpected errors
|
|
783
|
+
const msg = `[handleUpdateDataAction] Action ${actionDef.name} (${actionDef._id}): Unexpected error during update for model '${targetModel}'. Error: ${error.message}`;
|
|
784
|
+
logger.error(msg, error.stack);
|
|
785
|
+
return { success: false, message: msg };
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
|
|
790
|
+
|
|
791
|
+
/**
|
|
792
|
+
* Handles the 'DeleteData' workflow action.
|
|
793
|
+
* Finds document(s) based on a selector, substitutes variables,
|
|
794
|
+
* and deletes the document(s) using the deleteData function.
|
|
795
|
+
*
|
|
796
|
+
* @param {object} actionDef - The definition of the 'DeleteData' action.
|
|
797
|
+
* @param {object} contextData - The current workflow run context data.
|
|
798
|
+
* @param {object} user - The user object.
|
|
799
|
+
* @param {object} dbCollection - The MongoDB collection for the user (bien que deleteData utilise getCollectionForUser).
|
|
800
|
+
* @returns {Promise<{success: boolean, message?: string, deletedCount?: number}>} - Result of the action.
|
|
801
|
+
*/
|
|
802
|
+
async function handleDeleteDataAction(actionDef, contextData, user, dbCollection) {
|
|
803
|
+
// deleteMultiple optionnel, défaut false (supprime un seul par défaut)
|
|
804
|
+
const { targetModel, targetSelector, deleteMultiple = false } = actionDef;
|
|
805
|
+
|
|
806
|
+
// 1. Basic Validation
|
|
807
|
+
if (!targetModel || typeof targetModel !== 'string') {
|
|
808
|
+
const msg = `[handleDeleteDataAction] Action ${actionDef.name} (${actionDef._id}): Missing or invalid 'targetModel'.`;
|
|
809
|
+
logger.error(msg);
|
|
810
|
+
return { success: false, message: msg };
|
|
811
|
+
}
|
|
812
|
+
if (!targetSelector) {
|
|
813
|
+
const msg = `[handleDeleteDataAction] Action ${actionDef.name} (${actionDef._id}): Missing 'targetSelector'.`;
|
|
814
|
+
logger.error(msg);
|
|
815
|
+
return { success: false, message: msg };
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
logger.info(`[handleDeleteDataAction] Action ${actionDef.name} (${actionDef._id}): Deleting data for model '${targetModel}'. Multiple: ${deleteMultiple}`);
|
|
819
|
+
|
|
820
|
+
try {
|
|
821
|
+
// 2. Substitute Variables in selector
|
|
822
|
+
let substitutedSelectorString;
|
|
823
|
+
let selectorObject;
|
|
824
|
+
|
|
825
|
+
// Substitute targetSelector (assuming it's a JSON string or object)
|
|
826
|
+
if (typeof targetSelector === 'string') {
|
|
827
|
+
substitutedSelectorString = await substituteVariables(targetSelector, contextData, user);
|
|
828
|
+
} else if (typeof targetSelector === 'object') {
|
|
829
|
+
selectorObject = await substituteVariables(targetSelector, contextData, user); // Substitute values within the object
|
|
830
|
+
} else {
|
|
831
|
+
const msg = `[handleDeleteDataAction] Action ${actionDef.name} (${actionDef._id}): 'targetSelector' has an invalid type (${typeof targetSelector}). Expected string (JSON) or object.`;
|
|
832
|
+
logger.error(msg);
|
|
833
|
+
return { success: false, message: msg };
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
// 3. Parse substituted JSON string
|
|
837
|
+
if (substitutedSelectorString) {
|
|
838
|
+
try {
|
|
839
|
+
selectorObject = JSON.parse(substitutedSelectorString);
|
|
840
|
+
if (typeof selectorObject !== 'object' || selectorObject === null) {
|
|
841
|
+
throw new Error("Parsed selector is not a valid object.");
|
|
842
|
+
}
|
|
843
|
+
} catch (parseError) {
|
|
844
|
+
const msg = `[handleDeleteDataAction] Action ${actionDef.name} (${actionDef._id}): Failed to parse substituted 'targetSelector' JSON. Error: ${parseError.message}. Substituted string: ${substitutedSelectorString}`;
|
|
845
|
+
logger.error(msg);
|
|
846
|
+
return { success: false, message: msg };
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
// 5. Call the centralized deleteData function (à créer dans data.js)
|
|
851
|
+
// Cette fonction devra gérer la recherche préalable pour les workflows 'DataDeleted' et la suppression des fichiers.
|
|
852
|
+
const deleteResult = await deleteData(
|
|
853
|
+
targetModel,
|
|
854
|
+
selectorObject,
|
|
855
|
+
user
|
|
856
|
+
);
|
|
857
|
+
|
|
858
|
+
// 6. Return result
|
|
859
|
+
if (deleteResult.success) {
|
|
860
|
+
logger.info(`[handleDeleteDataAction] Action ${actionDef.name} (${actionDef._id}): Delete successful for model '${targetModel}'. Deleted: ${deleteResult.deletedCount}`);
|
|
861
|
+
return {
|
|
862
|
+
success: true,
|
|
863
|
+
deletedCount: deleteResult.deletedCount,
|
|
864
|
+
message: deleteResult.message // Pass along messages like "not found"
|
|
865
|
+
};
|
|
866
|
+
} else {
|
|
867
|
+
// deleteData devrait lancer des erreurs, mais on garde ce else par sécurité.
|
|
868
|
+
const msg = `[handleDeleteDataAction] Action ${actionDef.name} (${actionDef._id}): deleteData function reported failure. Message: ${deleteResult.message}`;
|
|
869
|
+
logger.error(msg);
|
|
870
|
+
return { success: false, message: msg };
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
} catch (error) {
|
|
874
|
+
// Catch errors thrown by deleteData (permissions, DB errors) or other unexpected errors
|
|
875
|
+
const msg = `[handleDeleteDataAction] Action ${actionDef.name} (${actionDef._id}): Unexpected error during deletion for model '${targetModel}'. Error: ${error.message}`;
|
|
876
|
+
logger.error(msg, error.stack);
|
|
877
|
+
return { success: false, message: msg };
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
/**
|
|
882
|
+
* Handles the 'ExecuteServiceFunction' workflow action.
|
|
883
|
+
* Acts as a secure bridge between the workflow engine and native service modules.
|
|
884
|
+
*
|
|
885
|
+
* @param {object} actionDef - The action definition.
|
|
886
|
+
* @param {object} contextData - The current workflow context.
|
|
887
|
+
* @param {object} user - The user object.
|
|
888
|
+
* @returns {Promise<{success: boolean, message?: string, updatedContext?: object}>}
|
|
889
|
+
*/
|
|
890
|
+
async function handleExecuteServiceFunction(actionDef, contextData, user) {
|
|
891
|
+
const { serviceName, functionName, args: argsTemplate } = actionDef;
|
|
892
|
+
|
|
893
|
+
if (!serviceName || !functionName) {
|
|
894
|
+
return { success: false, message: "Action requires 'serviceName' and 'functionName'." };
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
const service = services[serviceName];
|
|
898
|
+
if (!service) {
|
|
899
|
+
return { success: false, message: `Service '${serviceName}' not found in the registry.` };
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
const func = service[functionName];
|
|
903
|
+
if (typeof func !== 'function') {
|
|
904
|
+
return { success: false, message: `Function '${functionName}' not found in service '${serviceName}'.` };
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
try {
|
|
908
|
+
// Substitute variables in the arguments array
|
|
909
|
+
const substitutedArgs = Array.isArray(argsTemplate)
|
|
910
|
+
? await substituteVariables(argsTemplate, contextData, user)
|
|
911
|
+
: [];
|
|
912
|
+
|
|
913
|
+
logger.info(`[Service Call] Calling ${serviceName}.${functionName} with ${substitutedArgs.length} argument(s).`);
|
|
914
|
+
const result = await func(...substitutedArgs, user);
|
|
915
|
+
|
|
916
|
+
return {
|
|
917
|
+
success: true,
|
|
918
|
+
updatedContext: { serviceResult: result } // Store result in context
|
|
919
|
+
};
|
|
920
|
+
} catch (error) {
|
|
921
|
+
const msg = `Error executing ${serviceName}.${functionName}: ${error.message}`;
|
|
922
|
+
logger.error(`[Service Call] ${msg}`, error.stack);
|
|
923
|
+
return { success: false, message: msg };
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
// Dans workflow.js
|
|
928
|
+
export async function executeStepAction(actionDef, contextData, user, dbCollection) {
|
|
929
|
+
logger.info(`[executeStepAction] Executing action type ${actionDef.type} for action ${actionDef._id} (${actionDef.name})`);
|
|
930
|
+
|
|
931
|
+
try {
|
|
932
|
+
let result;
|
|
933
|
+
switch (actionDef.type) {
|
|
934
|
+
case 'Log':
|
|
935
|
+
logger.info(`[Workflow Log Action] Action: ${actionDef.name}. Contexte:`, contextData);
|
|
936
|
+
result = { success: true, message: 'Log action executed successfully.' }; // <--- CORRECTION
|
|
937
|
+
break;
|
|
938
|
+
case 'HttpRequest':
|
|
939
|
+
result = await handleHttpRequestAction(actionDef, contextData, user, dbCollection);
|
|
940
|
+
break;
|
|
941
|
+
case 'CreateData':
|
|
942
|
+
result = await handleCreateDataAction(actionDef, contextData, user, dbCollection);
|
|
943
|
+
break;
|
|
944
|
+
case 'UpdateData':
|
|
945
|
+
result = await handleUpdateDataAction(actionDef, contextData, user);
|
|
946
|
+
break;
|
|
947
|
+
case 'DeleteData':
|
|
948
|
+
result = await handleDeleteDataAction(actionDef, contextData, user, dbCollection);
|
|
949
|
+
break;
|
|
950
|
+
case 'GenerateAIContent':
|
|
951
|
+
result = await executeGenerateAIContentAction(actionDef, contextData, user);
|
|
952
|
+
break;
|
|
953
|
+
case 'SendEmail':
|
|
954
|
+
result = await handleSendEmailAction(actionDef, contextData, user);
|
|
955
|
+
break;
|
|
956
|
+
case 'Wait':
|
|
957
|
+
result = await handleWaitAction(actionDef, contextData, user);
|
|
958
|
+
break;
|
|
959
|
+
case 'ExecuteScript':
|
|
960
|
+
result = await executeSafeJavascript(actionDef, contextData, user);
|
|
961
|
+
break;
|
|
962
|
+
case 'ExecuteServiceFunction':
|
|
963
|
+
result = await handleExecuteServiceFunction(actionDef, contextData, user);
|
|
964
|
+
break;
|
|
965
|
+
default:
|
|
966
|
+
logger.error(`[executeStepAction] Unknown action type: ${actionDef.type}`);
|
|
967
|
+
return { success: false, message: `Unknown action type: ${actionDef.type}` };
|
|
968
|
+
}
|
|
969
|
+
return result;
|
|
970
|
+
} catch (error) {
|
|
971
|
+
logger.error(`[executeStepAction] Error executing action ${actionDef.name} (${actionDef._id}): ${error.message}`, error.stack);
|
|
972
|
+
return { success: false, message: error.message || 'Action execution failed' };
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
/**
|
|
976
|
+
* Récupère une valeur imbriquée dans un objet en utilisant une chaîne de chemin.
|
|
977
|
+
* Gère les tableaux et les objets. Retourne undefined si le chemin n'est pas trouvé.
|
|
978
|
+
* Exemple: getNestedValue({ a: { b: [ { c: 1 } ] } }, 'a.b.0.c') -> 1
|
|
979
|
+
*
|
|
980
|
+
* @param {object} obj L'objet source.
|
|
981
|
+
* @param {string} path La chaîne de chemin (ex: 'user.address.city').
|
|
982
|
+
* @returns {*} La valeur trouvée ou undefined.
|
|
983
|
+
*/
|
|
984
|
+
function getNestedValue(obj, path) {
|
|
985
|
+
// Vérifie si l'objet ou le chemin est invalide
|
|
986
|
+
if (!obj || typeof path !== 'string') {
|
|
987
|
+
return undefined;
|
|
988
|
+
}
|
|
989
|
+
// Sépare le chemin en clés individuelles (ex: 'a.b.0.c' -> ['a', 'b', '0', 'c'])
|
|
990
|
+
const keys = path.split('.');
|
|
991
|
+
let current = obj; // Commence à la racine de l'objet
|
|
992
|
+
|
|
993
|
+
// Parcourt chaque clé dans le chemin
|
|
994
|
+
for (const key of keys) {
|
|
995
|
+
// Si à un moment donné on atteint null ou undefined, le chemin est invalide
|
|
996
|
+
if (current === null || current === undefined) {
|
|
997
|
+
return undefined;
|
|
998
|
+
}
|
|
999
|
+
// Récupère la valeur pour la clé actuelle
|
|
1000
|
+
const value = current[key];
|
|
1001
|
+
// Si la valeur est undefined, le chemin est invalide
|
|
1002
|
+
if (value === undefined) {
|
|
1003
|
+
return undefined;
|
|
1004
|
+
}
|
|
1005
|
+
// Passe au niveau suivant de l'objet/tableau
|
|
1006
|
+
current = value;
|
|
1007
|
+
}
|
|
1008
|
+
// Retourne la valeur finale trouvée
|
|
1009
|
+
return current;
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
/**
|
|
1013
|
+
* Résout un chemin de variable complexe (ex: "triggerData.order.customer.contact.email")
|
|
1014
|
+
* en construisant un pipeline d'agrégation dynamique pour tout récupérer en une seule requête.
|
|
1015
|
+
*
|
|
1016
|
+
* @param {string} pathString - Le chemin de la variable, ex: "triggerData.order.customer.contact.email".
|
|
1017
|
+
* @param {object} initialContext - L'objet de départ (le triggerData).
|
|
1018
|
+
* @param {object} user - L'objet utilisateur pour les requêtes DB.
|
|
1019
|
+
* @returns {Promise<any>} La valeur résolue.
|
|
1020
|
+
*/
|
|
1021
|
+
async function resolvePathValue(pathString, initialContext, user) {
|
|
1022
|
+
const pathParts = pathString.split('.');
|
|
1023
|
+
const rootObjectKey = pathParts.shift(); // ex: "triggerData"
|
|
1024
|
+
|
|
1025
|
+
// Si le chemin ne commence pas par triggerData ou context, essayer de résoudre directement
|
|
1026
|
+
if (rootObjectKey !== 'triggerData' && rootObjectKey !== 'context') {
|
|
1027
|
+
let current = initialContext;
|
|
1028
|
+
for (const part of [rootObjectKey, ...pathParts]) {
|
|
1029
|
+
if (current === null || typeof current === 'undefined') return undefined;
|
|
1030
|
+
current = current[part];
|
|
1031
|
+
}
|
|
1032
|
+
return current;
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
// Vérifier si c'est un chemin simple qui peut être résolu sans aggregation
|
|
1036
|
+
if (pathParts.length === 1) {
|
|
1037
|
+
return initialContext[pathParts[0]];
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
let currentModelName = initialContext._model;
|
|
1041
|
+
let currentDocId = new ObjectId(initialContext._id);
|
|
1042
|
+
const collection = await getCollectionForUser(user);
|
|
1043
|
+
|
|
1044
|
+
// Construire le pipeline d'agrégation
|
|
1045
|
+
const pipeline = [
|
|
1046
|
+
{ $match: { _id: currentDocId } }
|
|
1047
|
+
];
|
|
1048
|
+
|
|
1049
|
+
// Itérer sur chaque segment du chemin pour construire les lookups
|
|
1050
|
+
for (let i = 0; i < pathParts.length; i++) {
|
|
1051
|
+
const segment = pathParts[i];
|
|
1052
|
+
|
|
1053
|
+
// Si c'est le dernier segment, on n'a pas besoin de faire un lookup
|
|
1054
|
+
if (i === pathParts.length - 1) break;
|
|
1055
|
+
|
|
1056
|
+
const modelDef = await getModel(currentModelName, user);
|
|
1057
|
+
const fieldDef = modelDef.fields.find(f => f.name === segment);
|
|
1058
|
+
|
|
1059
|
+
if (!fieldDef || fieldDef.type !== 'relation') {
|
|
1060
|
+
// Si ce n'est pas une relation, on ne peut pas continuer le chemin
|
|
1061
|
+
return undefined;
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
const nextModelName = fieldDef.relation;
|
|
1065
|
+
const asField = `__resolved_${segment}`;
|
|
1066
|
+
|
|
1067
|
+
pipeline.push({
|
|
1068
|
+
$lookup: {
|
|
1069
|
+
from: collection.collectionName,
|
|
1070
|
+
let: { relationId: `$${segment}` },
|
|
1071
|
+
pipeline: [
|
|
1072
|
+
{
|
|
1073
|
+
$match: {
|
|
1074
|
+
$expr: {
|
|
1075
|
+
$eq: ["$_id", {
|
|
1076
|
+
$cond: {
|
|
1077
|
+
if: { $eq: [{ $type: "$$relationId" }, "string"] },
|
|
1078
|
+
then: { $toObjectId: "$$relationId" },
|
|
1079
|
+
else: "$$relationId"
|
|
1080
|
+
}
|
|
1081
|
+
}]
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
],
|
|
1086
|
+
as: asField
|
|
1087
|
+
}
|
|
1088
|
+
});
|
|
1089
|
+
|
|
1090
|
+
pipeline.push({
|
|
1091
|
+
$unwind: {
|
|
1092
|
+
path: `$${asField}`,
|
|
1093
|
+
preserveNullAndEmptyArrays: true
|
|
1094
|
+
}
|
|
1095
|
+
});
|
|
1096
|
+
|
|
1097
|
+
pipeline.push({
|
|
1098
|
+
$addFields: {
|
|
1099
|
+
[segment]: `$${asField}`
|
|
1100
|
+
}
|
|
1101
|
+
});
|
|
1102
|
+
|
|
1103
|
+
pipeline.push({ $project: { [asField]: 0 } });
|
|
1104
|
+
|
|
1105
|
+
currentModelName = nextModelName;
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
const results = await collection.aggregate(pipeline).toArray();
|
|
1109
|
+
|
|
1110
|
+
if (results.length === 0) {
|
|
1111
|
+
return undefined;
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
// Extraire la valeur finale
|
|
1115
|
+
let finalValue = results[0];
|
|
1116
|
+
for (const part of pathParts) {
|
|
1117
|
+
if (finalValue === null || typeof finalValue === 'undefined') {
|
|
1118
|
+
return undefined;
|
|
1119
|
+
}
|
|
1120
|
+
finalValue = finalValue[part];
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
return finalValue;
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
/**
|
|
1127
|
+
* Remplace les placeholders dans un template (string, object, array) par des valeurs du contextData.
|
|
1128
|
+
* Version améliorée avec support des chemins complexes via resolvePathValue.
|
|
1129
|
+
*/
|
|
1130
|
+
export async function substituteVariables(template, contextData, user) {
|
|
1131
|
+
// 1. Retourner les types non substituables tels quels
|
|
1132
|
+
if (template === null || (typeof template !== 'string' && typeof template !== 'object')) {
|
|
1133
|
+
return template;
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
// 2. Gérer les tableaux de manière récursive
|
|
1137
|
+
if (Array.isArray(template)) {
|
|
1138
|
+
return Promise.all(template.map(item => substituteVariables(item, contextData, user)));
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
// 3. Gérer les objets de manière récursive
|
|
1142
|
+
if (typeof template === 'object') {
|
|
1143
|
+
const newObj = {};
|
|
1144
|
+
for (const key in template) {
|
|
1145
|
+
if (Object.prototype.hasOwnProperty.call(template, key)) {
|
|
1146
|
+
newObj[key] = await substituteVariables(template[key], contextData, user);
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
return newObj;
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
// --- À partir d'ici, nous savons que `template` est une chaîne de caractères ---
|
|
1153
|
+
|
|
1154
|
+
// 4. Construire le contexte complet pour la substitution
|
|
1155
|
+
const dbCollection = await getCollectionForUser(user);
|
|
1156
|
+
const userEnvVars = await dbCollection.find({ _model: 'env', _user: user.username }).toArray();
|
|
1157
|
+
const userEnv = userEnvVars.reduce((acc, v) => ({ ...acc, [v.name]: v.value }), {});
|
|
1158
|
+
|
|
1159
|
+
// `contextToSearch` contient toutes les données disponibles à sa racine
|
|
1160
|
+
const contextToSearch = { ...contextData, env: userEnv };
|
|
1161
|
+
|
|
1162
|
+
// 5. Logique de résolution de valeur améliorée avec resolvePathValue
|
|
1163
|
+
const findValue = async (key) => {
|
|
1164
|
+
let path = key.trim();
|
|
1165
|
+
if (path.startsWith('context.')) {
|
|
1166
|
+
path = path.substring('context.'.length);
|
|
1167
|
+
}
|
|
1168
|
+
if (path.endsWith('._id')) {
|
|
1169
|
+
const basePath = path.slice(0, -4);
|
|
1170
|
+
const value = await findValue(basePath);
|
|
1171
|
+
return value?._id?.toString(); // Convertit l'ObjectId en string
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
// Gérer les valeurs dynamiques spéciales
|
|
1175
|
+
if (path === 'now') {
|
|
1176
|
+
return new Date().toISOString();
|
|
1177
|
+
} else if (path === 'randomUUID') {
|
|
1178
|
+
return crypto.randomUUID();
|
|
1179
|
+
} else if( path === "baseUrl" ){
|
|
1180
|
+
return process.env.NODE_ENV === 'production' ? 'https://'+getHost()+'/' : 'http://localhost:/'+port;
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
// Détecter si le chemin est complexe (contient plus d'un point)
|
|
1184
|
+
if (path.split('.').length > 1) {
|
|
1185
|
+
try {
|
|
1186
|
+
// Essayer de résoudre le chemin avec resolvePathValue
|
|
1187
|
+
const [root, ...rest] = path.split('.');
|
|
1188
|
+
// On vérifie si la racine du chemin (ex: 'triggerData') existe dans notre contexte
|
|
1189
|
+
if (contextToSearch[root]) {
|
|
1190
|
+
const resolvedValue = await resolvePathValue(
|
|
1191
|
+
rest.join('.'),
|
|
1192
|
+
contextToSearch[root], // On passe le bon objet de départ (ex: l'objet triggerData)
|
|
1193
|
+
user
|
|
1194
|
+
);
|
|
1195
|
+
if (resolvedValue !== undefined) {
|
|
1196
|
+
return resolvedValue;
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
} catch (error) {
|
|
1200
|
+
console.warn(`Erreur lors de la résolution du chemin "${path}":`, error.message);
|
|
1201
|
+
// On continue avec la méthode normale si la résolution échoue
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
// Fallback: chercher le chemin dans l'objet de contexte normal
|
|
1206
|
+
return getNestedValue(contextToSearch, path);
|
|
1207
|
+
};
|
|
1208
|
+
|
|
1209
|
+
// CAS A : La chaîne est un unique placeholder (ex: "{context.triggerData.product.price}")
|
|
1210
|
+
const singlePlaceholderMatch = template.match(/^\{([^}]+)\}$/);
|
|
1211
|
+
if (singlePlaceholderMatch) {
|
|
1212
|
+
const key = singlePlaceholderMatch[1];
|
|
1213
|
+
const value = await findValue(key);
|
|
1214
|
+
|
|
1215
|
+
if (value === undefined) {
|
|
1216
|
+
return template; // Placeholder not found, return as is.
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
// If the resolved value is a string, it might contain more placeholders.
|
|
1220
|
+
// We recursively call substituteVariables on it, but only if it's different
|
|
1221
|
+
// from the original template to prevent infinite loops.
|
|
1222
|
+
if (typeof value === 'string' && value !== template) {
|
|
1223
|
+
return substituteVariables(value, contextData, user);
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
// For non-string values or if value is same as template, return the value.
|
|
1227
|
+
return value;
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
// CAS B : La chaîne contient plusieurs placeholders ou mix texte/variables
|
|
1231
|
+
const placeholderRegex = /\{([^}]+)\}/g;
|
|
1232
|
+
const placeholders = [...template.matchAll(placeholderRegex)];
|
|
1233
|
+
|
|
1234
|
+
// Si aucun placeholder trouvé, retourner la chaîne telle quelle
|
|
1235
|
+
if (placeholders.length === 0) {
|
|
1236
|
+
return template;
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
// Remplacer chaque placeholder de manière asynchrone
|
|
1240
|
+
let result = template;
|
|
1241
|
+
for (const [match, key] of placeholders) {
|
|
1242
|
+
const value = await findValue(key);
|
|
1243
|
+
const replacement = value !== undefined
|
|
1244
|
+
? (value === null ? 'null' : typeof value === 'object' ? JSON.stringify(value) : String(value))
|
|
1245
|
+
: match;
|
|
1246
|
+
result = result.replace(match, replacement);
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
return result;
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
/**
|
|
1253
|
+
* Triggers the instantiation of a workflowRun if conditions are met.
|
|
1254
|
+
* Checks the event type and trigger's data filter.
|
|
1255
|
+
* Creates a 'workflowRun' document for later asynchronous execution.
|
|
1256
|
+
*
|
|
1257
|
+
* @param {object} triggerData - The data that triggered the workflow(s) (can be a data document or model document).
|
|
1258
|
+
* @param {object} user - The associated user.
|
|
1259
|
+
* @param {'DataAdded' | 'DataEdited' | 'DataDeleted' | 'ModelAdded' | 'ModelEdited' | 'ModelDeleted'} eventType - The event type.
|
|
1260
|
+
*/
|
|
1261
|
+
export async function triggerWorkflows(triggerData, user, eventType) {
|
|
1262
|
+
const trigger = async (triggerData, user, eventType) => {
|
|
1263
|
+
// Basic validation
|
|
1264
|
+
if (!triggerData || !user || !eventType) {
|
|
1265
|
+
console.warn("triggerWorkflows: Invalid call - missing triggerData, user, or eventType.", {
|
|
1266
|
+
hasTriggerData: !!triggerData,
|
|
1267
|
+
hasUser: !!user,
|
|
1268
|
+
eventType
|
|
1269
|
+
});
|
|
1270
|
+
return;
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
// Determine model name and data ID based on event type
|
|
1274
|
+
const targetModelName = eventType.startsWith('Model') ? triggerData.name : triggerData._model;
|
|
1275
|
+
const dataId = eventType.startsWith('Model') ? null : triggerData._id;
|
|
1276
|
+
|
|
1277
|
+
if (!targetModelName) {
|
|
1278
|
+
console.warn(`triggerWorkflows: Cannot determine model name for event ${eventType}.`, triggerData);
|
|
1279
|
+
return;
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
console.log(`[Workflow Trigger] Event: ${eventType}, Model: ${targetModelName}${dataId ? `, Data ID: ${dataId}` : ''}, User: ${user.username}`);
|
|
1283
|
+
|
|
1284
|
+
try {
|
|
1285
|
+
const dbCollection = await getCollectionForUser(user);
|
|
1286
|
+
|
|
1287
|
+
// 1. Find relevant WorkflowTriggers
|
|
1288
|
+
const workflowTriggers = await dbCollection.find({
|
|
1289
|
+
_model: 'workflowTrigger',
|
|
1290
|
+
targetModel: targetModelName,
|
|
1291
|
+
isActive: true,
|
|
1292
|
+
onEvent: eventType,
|
|
1293
|
+
$or: [{_user: user._user}, {_user: user.username}]
|
|
1294
|
+
}).toArray();
|
|
1295
|
+
|
|
1296
|
+
if (workflowTriggers.length === 0) {
|
|
1297
|
+
console.debug(`[Workflow Trigger] No active triggers found for ${targetModelName}/${eventType}.`);
|
|
1298
|
+
return;
|
|
1299
|
+
}
|
|
1300
|
+
console.debug(`[Workflow Trigger] Found ${workflowTriggers.length} potential trigger(s) for ${targetModelName}/${eventType}.`);
|
|
1301
|
+
|
|
1302
|
+
// 2. For each trigger, verify data filter and create workflowRun
|
|
1303
|
+
for (const trigger of workflowTriggers) {
|
|
1304
|
+
console.debug(`[Workflow Trigger] Evaluating trigger ${trigger._id} (${trigger.name || 'Unnamed'})...`);
|
|
1305
|
+
|
|
1306
|
+
// 3. Check data filter if applicable
|
|
1307
|
+
if (eventType.startsWith('Data') && trigger.dataFilter) {
|
|
1308
|
+
let dataFilterCondition = null;
|
|
1309
|
+
try {
|
|
1310
|
+
// dataFilter is expected to be stored as an object or valid JSON string
|
|
1311
|
+
if (typeof trigger.dataFilter === 'string') {
|
|
1312
|
+
dataFilterCondition = JSON.parse(trigger.dataFilter);
|
|
1313
|
+
} else if (typeof trigger.dataFilter === 'object' && trigger.dataFilter !== null) {
|
|
1314
|
+
dataFilterCondition = trigger.dataFilter;
|
|
1315
|
+
}
|
|
1316
|
+
} catch (parseError) {
|
|
1317
|
+
console.error(`[Workflow Trigger] JSON parsing error for dataFilter in trigger ${trigger._id}:`, parseError);
|
|
1318
|
+
continue; // Skip to next trigger if filter is invalid
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
try {
|
|
1322
|
+
const mod = await getModel(targetModelName, user);
|
|
1323
|
+
const filterMatches = isConditionMet(mod, dataFilterCondition, triggerData, [], user);
|
|
1324
|
+
|
|
1325
|
+
if (!filterMatches) {
|
|
1326
|
+
console.debug(`[Workflow Trigger] Trigger ${trigger._id}: dataFilter not satisfied by data. Skipping workflowRun creation.`);
|
|
1327
|
+
continue;
|
|
1328
|
+
}
|
|
1329
|
+
console.debug(`[Workflow Trigger] Trigger ${trigger._id}: dataFilter satisfied.`);
|
|
1330
|
+
} catch (filterError) {
|
|
1331
|
+
console.error(`[Workflow Trigger] Error evaluating dataFilter for trigger ${trigger._id}:`, filterError);
|
|
1332
|
+
continue;
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
// 4. If filters passed, create workflowRun instance
|
|
1337
|
+
if (!trigger.workflow || !isObjectId(trigger.workflow)) {
|
|
1338
|
+
console.warn(`[Workflow Trigger] Trigger ${trigger._id} has no valid associated workflow.`);
|
|
1339
|
+
continue;
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
// a. Verify workflow exists
|
|
1343
|
+
const workflowDefinition = await dbCollection.findOne({
|
|
1344
|
+
_id: new ObjectId(trigger.workflow),
|
|
1345
|
+
_model: 'workflow',
|
|
1346
|
+
$or: [{_user: user._user}, {_user: user.username}]
|
|
1347
|
+
});
|
|
1348
|
+
|
|
1349
|
+
if (!workflowDefinition) {
|
|
1350
|
+
console.warn(`[Workflow Trigger] Workflow ${trigger.workflow} associated with trigger ${trigger._id} not found.`);
|
|
1351
|
+
continue;
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
// b. Create workflowRun document
|
|
1355
|
+
const workflowRunData = {
|
|
1356
|
+
_model: 'workflowRun',
|
|
1357
|
+
_user: user._user || user.username,
|
|
1358
|
+
workflow: workflowDefinition._id,
|
|
1359
|
+
contextData: {
|
|
1360
|
+
triggerDataModel: targetModelName,
|
|
1361
|
+
triggerData: triggerData
|
|
1362
|
+
},
|
|
1363
|
+
status: 'pending',
|
|
1364
|
+
owner: null,
|
|
1365
|
+
startedAt: new Date()
|
|
1366
|
+
};
|
|
1367
|
+
|
|
1368
|
+
try {
|
|
1369
|
+
const insertResult = await dbCollection.insertOne(workflowRunData);
|
|
1370
|
+
if (insertResult.insertedId) {
|
|
1371
|
+
console.info(`[Workflow Trigger] Created workflowRun ${insertResult.insertedId} for workflow ${workflowDefinition.name} (ID: ${workflowDefinition._id}) triggered by ${trigger._id}.`);
|
|
1372
|
+
await workflowModule.processWorkflowRun(insertResult.insertedId, user);
|
|
1373
|
+
} else {
|
|
1374
|
+
console.error(`[Workflow Trigger] Failed to create workflowRun for workflow ${workflowDefinition._id} (Trigger: ${trigger._id}).`);
|
|
1375
|
+
}
|
|
1376
|
+
} catch (insertError) {
|
|
1377
|
+
console.error(`[Workflow Trigger] Error creating workflowRun for workflow ${workflowDefinition._id} (Trigger: ${trigger._id}):`, insertError);
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
} catch (error) {
|
|
1381
|
+
console.error(`[Workflow Trigger] General error in triggerWorkflows for ${targetModelName}${dataId ? ` ID: ${dataId}` : ''} (Event: ${eventType}):`, error);
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
return new Promise((resolve) => setTimeout(async () => {
|
|
1386
|
+
await trigger(triggerData, user, eventType);
|
|
1387
|
+
resolve();
|
|
1388
|
+
}, 0));
|
|
1389
|
+
}
|
|
1390
|
+
/**
|
|
1391
|
+
* Processes a workflowRun instance step-by-step.
|
|
1392
|
+
* Fetches the run, evaluates conditions, executes actions, and transitions
|
|
1393
|
+
* to the next step based on success or failure, updating the workflowRun status.
|
|
1394
|
+
*
|
|
1395
|
+
* @param {string|ObjectId} workflowRunId - The ID of the workflowRun to process.
|
|
1396
|
+
* @param {object} user - The user context for database access.
|
|
1397
|
+
* @returns {Promise<void>}
|
|
1398
|
+
*/
|
|
1399
|
+
|
|
1400
|
+
export async function processWorkflowRun(workflowRunId, user) {
|
|
1401
|
+
const dbCollection = await getCollectionForUser(user);
|
|
1402
|
+
const runId = typeof workflowRunId === 'string' ? new ObjectId(workflowRunId) : workflowRunId;
|
|
1403
|
+
|
|
1404
|
+
logger.info(`[processWorkflowRun] Starting processing for workflowRun ID: ${runId}`);
|
|
1405
|
+
|
|
1406
|
+
let currentRunState;
|
|
1407
|
+
let contextData = {};
|
|
1408
|
+
let stepExecutionsCount = {};
|
|
1409
|
+
|
|
1410
|
+
try {
|
|
1411
|
+
currentRunState = await dbCollection.findOne({ _id: runId, _model: 'workflowRun' });
|
|
1412
|
+
|
|
1413
|
+
if (!currentRunState) {
|
|
1414
|
+
logger.error(`[processWorkflowRun] WorkflowRun ID: ${runId} not found.`);
|
|
1415
|
+
return;
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
stepExecutionsCount = currentRunState.stepExecutionsCount || {};
|
|
1419
|
+
if (['completed', 'failed', 'cancelled'].includes(currentRunState.status)) {
|
|
1420
|
+
logger.info(`[processWorkflowRun] WorkflowRun ID: ${runId} is already in a terminal state (${currentRunState.status}). Skipping.`);
|
|
1421
|
+
return;
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
const logError = async (error) => {
|
|
1425
|
+
logger.error(error);
|
|
1426
|
+
await dbCollection.updateOne(
|
|
1427
|
+
{ _id: runId },
|
|
1428
|
+
{ $set: { status: 'failed', error, completedAt: new Date(), stepExecutionsCount } }
|
|
1429
|
+
);
|
|
1430
|
+
};
|
|
1431
|
+
|
|
1432
|
+
const workflowDefinition = await dbCollection.findOne({ _id: new ObjectId(currentRunState.workflow), _model: 'workflow' });
|
|
1433
|
+
if (!workflowDefinition) {
|
|
1434
|
+
return await logError(`Workflow definition ID: ${currentRunState.workflow} not found.`);
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
contextData = currentRunState.contextData || {};
|
|
1438
|
+
let currentStepId = currentRunState.currentStep || workflowDefinition.startStep;
|
|
1439
|
+
|
|
1440
|
+
if (!currentStepId || !isObjectId(currentStepId)) {
|
|
1441
|
+
const finalStatus = workflowDefinition.startStep ? 'failed' : 'completed';
|
|
1442
|
+
const errorMessage = workflowDefinition.startStep ? 'No valid starting step defined in workflow or run state.' : null;
|
|
1443
|
+
await dbCollection.updateOne(
|
|
1444
|
+
{ _id: runId },
|
|
1445
|
+
{ $set: { status: finalStatus, error: errorMessage, completedAt: new Date(), currentStep: null, stepExecutionsCount } }
|
|
1446
|
+
);
|
|
1447
|
+
return;
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
let stepCount = 0;
|
|
1451
|
+
while (currentStepId) {
|
|
1452
|
+
if (stepCount++ >= maxWorkflowSteps) {
|
|
1453
|
+
return await logError(`Maximum workflow step executions exceeded (${maxWorkflowSteps} max).`);
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
const execCount = (stepExecutionsCount[currentStepId] || 0) + 1;
|
|
1457
|
+
if (execCount > maxExecutionsByStep) {
|
|
1458
|
+
return await logError(`Maximum executions (${maxExecutionsByStep}) exceeded for step ${currentStepId}.`);
|
|
1459
|
+
}
|
|
1460
|
+
stepExecutionsCount[currentStepId] = execCount;
|
|
1461
|
+
logger.info(`[processWorkflowRun] Run ID: ${runId}, Current Step ID: ${currentStepId}`);
|
|
1462
|
+
|
|
1463
|
+
const currentStepDef = await dbCollection.findOne({ _id: new ObjectId(currentStepId), _model: 'workflowStep' });
|
|
1464
|
+
if (!currentStepDef) {
|
|
1465
|
+
return await logError(`Step definition ID: ${currentStepId} not found.`);
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
await dbCollection.updateOne(
|
|
1469
|
+
{ _id: runId },
|
|
1470
|
+
{ $set: { status: 'running', currentStep: currentStepId, contextData, stepExecutionsCount } }
|
|
1471
|
+
);
|
|
1472
|
+
|
|
1473
|
+
let stepSucceeded = true;
|
|
1474
|
+
let logInfo = null;
|
|
1475
|
+
let conditionsMet = true;
|
|
1476
|
+
|
|
1477
|
+
try {
|
|
1478
|
+
// Add logging to see the actual pipeline being executed
|
|
1479
|
+
logger.debug('Executing pipeline:', JSON.stringify(await substituteVariables(currentStepDef.conditions, contextData, user), null, 2));
|
|
1480
|
+
|
|
1481
|
+
// And log the context data to verify processedChunk exists
|
|
1482
|
+
logger.debug('Context data:', JSON.stringify(contextData, null, 2));
|
|
1483
|
+
|
|
1484
|
+
// --- 7. Évaluation des conditions de l'étape ---
|
|
1485
|
+
if (currentStepDef.conditions && Object.keys(currentStepDef.conditions).length > 0) {
|
|
1486
|
+
const substitutedConditions = await substituteVariables(currentStepDef.conditions, contextData, user);
|
|
1487
|
+
// Si un modèle est spécifié dans le contexte, la condition est une requête sur la base de données.
|
|
1488
|
+
if (contextData.triggerDataModel) {
|
|
1489
|
+
const searchResult = await searchData({ model: contextData.triggerDataModel, filter: substitutedConditions, limit: 1 }, user);
|
|
1490
|
+
conditionsMet = searchResult && searchResult.count > 0;
|
|
1491
|
+
logger.info(`[processWorkflowRun] Run ID: ${runId}, Step ID: ${currentStepId}: DB condition evaluated. Found ${searchResult ? searchResult.count : 0} match(es). Result: ${conditionsMet}`);
|
|
1492
|
+
} else {
|
|
1493
|
+
console.log({substitutedConditions, c:contextData['triggerData']['event']['type']});
|
|
1494
|
+
// Si aucun modèle n'est spécifié (ex: webhook), la condition est évaluée sur l'objet de contexte lui-même.
|
|
1495
|
+
conditionsMet = isConditionMet(null, substitutedConditions, contextData, [], user);
|
|
1496
|
+
|
|
1497
|
+
logger.info(`[processWorkflowRun] Run ID: ${runId}, Step ID: ${currentStepId}: Context condition evaluated. Operator: ${JSON.stringify(substitutedConditions)}, Result: ${conditionsMet}`);
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
// --- 8. Exécution des actions si les conditions sont remplies ---
|
|
1502
|
+
if (conditionsMet) {
|
|
1503
|
+
if (currentStepDef.actions && currentStepDef.actions.length > 0) {
|
|
1504
|
+
logger.info(`[processWorkflowRun] Run ID: ${runId}, Step ID: ${currentStepId}: Executing ${currentStepDef.actions.length} action(s)...`);
|
|
1505
|
+
for (const actionId of currentStepDef.actions) {
|
|
1506
|
+
if (!isObjectId(actionId)) continue;
|
|
1507
|
+
const actionDef = await dbCollection.findOne({ _id: new ObjectId(actionId), _model: 'workflowAction' });
|
|
1508
|
+
if (!actionDef) return await logError(`Action definition ${actionId} not found.`);
|
|
1509
|
+
const actionResult = await workflowModule.executeStepAction(actionDef, contextData, user, dbCollection);
|
|
1510
|
+
|
|
1511
|
+
if (actionResult.status === 'paused') {
|
|
1512
|
+
// L'action demande une pause !
|
|
1513
|
+
const { duration, durationUnit } = actionResult;
|
|
1514
|
+
const now = new Date();
|
|
1515
|
+
let resumeAt = new Date(now);
|
|
1516
|
+
|
|
1517
|
+
// Calculer la date de reprise
|
|
1518
|
+
const ms = { seconds: 1000, minutes: 60000, hours: 3600000, days: 86400000 };
|
|
1519
|
+
resumeAt.setTime(now.getTime() + (duration * ms[durationUnit]));
|
|
1520
|
+
|
|
1521
|
+
logger.info(`[processWorkflowRun] Run ID: ${runId} is pausing. Will resume at: ${resumeAt.toISOString()}`);
|
|
1522
|
+
|
|
1523
|
+
// Mettre à jour le workflowRun avec le statut 'paused' et la date de reprise
|
|
1524
|
+
await dbCollection.updateOne({ _id: runId }, {
|
|
1525
|
+
$set: {
|
|
1526
|
+
status: 'paused',
|
|
1527
|
+
currentStep: currentStepDef.onSuccessStep, // On prépare la prochaine étape
|
|
1528
|
+
contextData,
|
|
1529
|
+
log: actionResult.message
|
|
1530
|
+
}
|
|
1531
|
+
});
|
|
1532
|
+
|
|
1533
|
+
// Planifier le réveil du workflow
|
|
1534
|
+
schedule.scheduleJob(resumeAt, async () => {
|
|
1535
|
+
logger.info(`[Scheduler] Waking up paused workflowRun ID: ${runId}`);
|
|
1536
|
+
// On relance le traitement pour ce workflow spécifique
|
|
1537
|
+
await workflowModule.processWorkflowRun(runId, user);
|
|
1538
|
+
});
|
|
1539
|
+
|
|
1540
|
+
// Arrêter le traitement actuel de cette exécution
|
|
1541
|
+
return; // Très important de stopper la boucle ici
|
|
1542
|
+
}
|
|
1543
|
+
if (!actionResult.success) {
|
|
1544
|
+
stepSucceeded = false;
|
|
1545
|
+
logInfo = actionResult.message || `Action ${actionDef.name || actionId} failed.`;
|
|
1546
|
+
break;
|
|
1547
|
+
}else{
|
|
1548
|
+
logInfo = `Action ${actionDef.name || actionId} : ${actionResult.message}`;
|
|
1549
|
+
}
|
|
1550
|
+
if (actionResult.updatedContext) {
|
|
1551
|
+
contextData = { ...contextData, ...actionResult.updatedContext };
|
|
1552
|
+
}
|
|
1553
|
+
//console.log("action", util.inspect(actionResult, false, 8, true));
|
|
1554
|
+
logger.info(`[processWorkflowRun] Run ID: ${runId}, Step ID: ${currentStepId}, Action ID: ${actionId}: Executed successfully.`);
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
} else {
|
|
1558
|
+
logger.info(`[processWorkflowRun] Run ID: ${runId}, Step ID: ${currentStepId}: Conditions not met. Skipping actions.`);
|
|
1559
|
+
}
|
|
1560
|
+
} catch (error) {
|
|
1561
|
+
logger.error(`[processWorkflowRun] Run ID: ${runId}, Step ID: ${currentStepId}: Error during condition/action execution: ${error.message}`);
|
|
1562
|
+
stepSucceeded = false;
|
|
1563
|
+
logInfo = error.message;
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1566
|
+
// --- 9. Détermination de la prochaine étape ---
|
|
1567
|
+
let nextStepId = null;
|
|
1568
|
+
let finalStatusForRun = null;
|
|
1569
|
+
|
|
1570
|
+
if (stepSucceeded && conditionsMet) {
|
|
1571
|
+
// CHEMIN SUCCÈS : Les conditions sont remplies et les actions ont réussi.
|
|
1572
|
+
logger.info(`[processWorkflowRun] Run ID: ${runId}, Step ID: ${currentStepId}: Step path succeeded.`);
|
|
1573
|
+
nextStepId = currentStepDef.onSuccessStep;
|
|
1574
|
+
if (currentStepDef.isTerminal || !nextStepId) {
|
|
1575
|
+
finalStatusForRun = 'completed';
|
|
1576
|
+
nextStepId = null;
|
|
1577
|
+
}
|
|
1578
|
+
} else {
|
|
1579
|
+
// CHEMIN ÉCHEC/BRANCHE : Une action a échoué OU les conditions n'ont pas été remplies.
|
|
1580
|
+
const reason = logInfo ? `Action failed: ${logInfo}` : 'Step conditions not met.';
|
|
1581
|
+
logger.warn(`[processWorkflowRun] Run ID: ${runId}, Step ID: ${currentStepId}: Taking failure/branching path. Reason: ${reason}`);
|
|
1582
|
+
nextStepId = currentStepDef.onFailureStep;
|
|
1583
|
+
|
|
1584
|
+
if (!nextStepId || !isObjectId(nextStepId)) {
|
|
1585
|
+
// Fin du workflow. Le statut est 'failed' seulement si une vraie erreur s'est produite.
|
|
1586
|
+
finalStatusForRun = logInfo ? 'failed' : 'completed';
|
|
1587
|
+
nextStepId = null;
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
// --- 10. Mise à jour de l'état de l'exécution ---
|
|
1592
|
+
currentStepId = nextStepId;
|
|
1593
|
+
const updatePayload = { contextData };
|
|
1594
|
+
|
|
1595
|
+
if (finalStatusForRun) {
|
|
1596
|
+
updatePayload.status = finalStatusForRun;
|
|
1597
|
+
updatePayload.completedAt = new Date();
|
|
1598
|
+
updatePayload.currentStep = null;
|
|
1599
|
+
updatePayload.log = logInfo;
|
|
1600
|
+
} else {
|
|
1601
|
+
updatePayload.currentStep = currentStepId;
|
|
1602
|
+
}
|
|
1603
|
+
await dbCollection.updateOne({ _id: runId }, { $set: updatePayload });
|
|
1604
|
+
|
|
1605
|
+
if(finalStatusForRun) {
|
|
1606
|
+
logger.info(`[processWorkflowRun] Finished processing for workflowRun ID: ${runId}. Final Status: ${finalStatusForRun}`);
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
1609
|
+
} catch (error) {
|
|
1610
|
+
logger.error(`[processWorkflowRun] Critical error during processing of workflowRun ID: ${runId}. Error: ${error.message}`, error.stack);
|
|
1611
|
+
await dbCollection.updateOne(
|
|
1612
|
+
{ _id: runId, status: { $nin: ['completed', 'failed', 'cancelled'] } },
|
|
1613
|
+
{ $set: { status: 'failed', log: `Critical error: ${error.message}`, completedAt: new Date(), stepExecutionsCount } }
|
|
1614
|
+
);
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
/**
|
|
1618
|
+
* Executes an AI content generation action ('GenerateAIContent').
|
|
1619
|
+
* Retrieves the API key (prioritizing the user's environment), initializes a LangChain client,
|
|
1620
|
+
* formats a prompt with context data, calls the LLM, and returns the result
|
|
1621
|
+
* to be added to the workflow context.
|
|
1622
|
+
*
|
|
1623
|
+
* @param {object} action - The action definition from the workflow.
|
|
1624
|
+
* @param {object} context - The current workflow execution context.
|
|
1625
|
+
* @param {object} user - The user executing the workflow.
|
|
1626
|
+
* @returns {Promise<{success: boolean, updatedContext?: object, message?: string}>}
|
|
1627
|
+
*/
|
|
1628
|
+
async function executeGenerateAIContentAction(action, context, user) {
|
|
1629
|
+
const { aiProvider, aiModel, prompt } = action;
|
|
1630
|
+
|
|
1631
|
+
// 1. Retrieve the API key (User Environment > Machine Environment)
|
|
1632
|
+
let apiKey;
|
|
1633
|
+
|
|
1634
|
+
const envKeyName = providers[aiProvider].key;
|
|
1635
|
+
if( !envKeyName ) {
|
|
1636
|
+
return {success: false, message: i18n.t('aiContent.env', `API key for provider ${aiProvider} (${envKeyName}) not found in user environment.`)};
|
|
1637
|
+
}
|
|
1638
|
+
|
|
1639
|
+
// First look in the user's environment variables
|
|
1640
|
+
const envCollection = await getCollectionForUser(user);
|
|
1641
|
+
const userEnvVar = await envCollection.findOne({ _model: 'env', name: envKeyName, _user: user.username });
|
|
1642
|
+
|
|
1643
|
+
if (userEnvVar && userEnvVar.value) {
|
|
1644
|
+
apiKey = userEnvVar.value;
|
|
1645
|
+
logger.debug(`[AI Action] Using user environment API key for ${aiProvider}.`);
|
|
1646
|
+
} else {
|
|
1647
|
+
apiKey = process.env[envKeyName];
|
|
1648
|
+
logger.debug(`[AI Action] Using machine environment API key for ${aiProvider}.`);
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1651
|
+
if (!apiKey) {
|
|
1652
|
+
const message = `API key for ${aiProvider} (${envKeyName}) not found in user or machine environment.`;
|
|
1653
|
+
logger.error(`[AI Action] ${message}`);
|
|
1654
|
+
return { success: false, message };
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1657
|
+
// 2. Initialize the LLM client with LangChain
|
|
1658
|
+
let llm = getAIProvider(aiProvider, aiModel, apiKey);
|
|
1659
|
+
if( !llm ) {
|
|
1660
|
+
const message = `Failed to initialize AI client for ${aiProvider}: ${initError.message}`;
|
|
1661
|
+
logger.error(`[AI Action] ${message}`);
|
|
1662
|
+
return { success: false, message };
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
try {
|
|
1666
|
+
const substitutedPrompt = await substituteVariables(prompt, context, user);
|
|
1667
|
+
// 3. Create the "Prompt Template"
|
|
1668
|
+
// LangChain handles variable substitution like {triggerData.name}
|
|
1669
|
+
const realPrompt = ChatPromptTemplate.fromTemplate(substitutedPrompt);
|
|
1670
|
+
|
|
1671
|
+
// 4. Create the processing chain (Prompt + Model)
|
|
1672
|
+
const chain = realPrompt.pipe(llm);
|
|
1673
|
+
|
|
1674
|
+
// 5. Invoke the chain with the complete context
|
|
1675
|
+
// LangChain will automatically replace placeholders in the prompt.
|
|
1676
|
+
logger.debug(`[AI Action] Invoking AI with model ${aiModel}.`);
|
|
1677
|
+
const response = await chain.invoke(context);
|
|
1678
|
+
|
|
1679
|
+
// 6. Prepare the result to be merged into the workflow context
|
|
1680
|
+
const llmOutput = response.content;
|
|
1681
|
+
const outputVariable = 'aiContent';
|
|
1682
|
+
const updatedContext = {
|
|
1683
|
+
[outputVariable]: llmOutput
|
|
1684
|
+
};
|
|
1685
|
+
|
|
1686
|
+
logger.info(`[AI Action] Content generated successfully and stored in context variable '${outputVariable}'.`);
|
|
1687
|
+
|
|
1688
|
+
return {
|
|
1689
|
+
success: true,
|
|
1690
|
+
updatedContext // This object will be merged into the main context by the workflow engine
|
|
1691
|
+
};
|
|
1692
|
+
|
|
1693
|
+
} catch (llmError) {
|
|
1694
|
+
const message = `Error during AI content generation with ${aiProvider}: ${llmError.message}`;
|
|
1695
|
+
logger.error(`[AI Action] ${message}`, llmError.stack);
|
|
1696
|
+
return { success: false, message };
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
/**
|
|
1701
|
+
* Gère l'action d'envoi d'e-mail d'un workflow.
|
|
1702
|
+
* Cette version améliorée peut traiter une liste de destinataires, en envoyant un e-mail
|
|
1703
|
+
* individuel et personnalisé à chacun. Elle gère les placeholders dans le sujet et le corps
|
|
1704
|
+
* de l'e-mail en se basant sur le contexte de chaque destinataire.
|
|
1705
|
+
*
|
|
1706
|
+
* @param {object} action - La définition de l'action 'SendEmail'.
|
|
1707
|
+
* @param {object} contextData - Le contexte d'exécution actuel du workflow.
|
|
1708
|
+
* @param {object} user - L'utilisateur propriétaire du workflow.
|
|
1709
|
+
* @returns {Promise<{success: boolean, message: string, data?: {sent: string[], failed: any[]}}>}
|
|
1710
|
+
*/
|
|
1711
|
+
async function handleSendEmailAction(action, contextData, user) {
|
|
1712
|
+
logger.info(`[handleSendEmailAction] Executing for user ${user.username}.`);
|
|
1713
|
+
|
|
1714
|
+
// 1. Récupérer la configuration SMTP depuis le modèle 'env' de l'utilisateur
|
|
1715
|
+
const smtpConfig = await getSmtpConfig(user);
|
|
1716
|
+
|
|
1717
|
+
// 2. Valider la configuration de l'action
|
|
1718
|
+
const { emailRecipients, emailSubject, emailContent } = action;
|
|
1719
|
+
if (!emailRecipients || !emailSubject || !emailContent) {
|
|
1720
|
+
const msg = "SendEmail action is incomplete. 'emailRecipients', 'emailSubject', and 'emailContent' are required.";
|
|
1721
|
+
logger.error(`[handleSendEmailAction] ${msg}`);
|
|
1722
|
+
return { success: false, message: msg };
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1725
|
+
try {
|
|
1726
|
+
// 3. Résoudre la liste des destinataires. Peut être un placeholder qui retourne un tableau.
|
|
1727
|
+
let resolvedRecipients = await substituteVariables(emailRecipients, contextData, user);
|
|
1728
|
+
|
|
1729
|
+
// S'assurer que nous avons toujours un tableau à parcourir
|
|
1730
|
+
if (!Array.isArray(resolvedRecipients)) {
|
|
1731
|
+
resolvedRecipients = [resolvedRecipients];
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1734
|
+
resolvedRecipients = resolvedRecipients.flat();
|
|
1735
|
+
|
|
1736
|
+
if (resolvedRecipients.length === 0) {
|
|
1737
|
+
return { success: true, message: "No recipients found after substitution. Nothing to send." };
|
|
1738
|
+
}
|
|
1739
|
+
|
|
1740
|
+
logger.info(`[handleSendEmailAction] Preparing to send emails to ${resolvedRecipients.length} recipient(s).`);
|
|
1741
|
+
|
|
1742
|
+
const allPromises = [];
|
|
1743
|
+
const sentTo = [];
|
|
1744
|
+
const failedFor = [];
|
|
1745
|
+
|
|
1746
|
+
// 4. Itérer sur chaque destinataire pour envoyer un e-mail personnalisé
|
|
1747
|
+
for (const recipient of resolvedRecipients) {
|
|
1748
|
+
// Le destinataire peut être une simple chaîne (email) ou un objet { email: '...', nom: '...' }
|
|
1749
|
+
const recipientEmail = typeof recipient === 'object' && recipient !== null ? recipient.email : recipient;
|
|
1750
|
+
|
|
1751
|
+
if (!recipientEmail || typeof recipientEmail !== 'string') {
|
|
1752
|
+
logger.warn(`[handleSendEmailAction] Skipping an invalid recipient entry:`, recipient);
|
|
1753
|
+
failedFor.push(recipient); // Garder une trace de l'entrée invalide
|
|
1754
|
+
continue;
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1757
|
+
|
|
1758
|
+
// 5. Créer un contexte personnalisé pour ce destinataire spécifique
|
|
1759
|
+
// Cela permet d'utiliser des placeholders comme {recipient.name}
|
|
1760
|
+
const personalizedContext = { ...contextData, recipient };
|
|
1761
|
+
|
|
1762
|
+
// 6. Substituer les variables dans le sujet et le contenu pour ce destinataire
|
|
1763
|
+
const personalizedSubject = await substituteVariables(emailSubject, personalizedContext, user);
|
|
1764
|
+
const personalizedBody = await substituteVariables(emailContent, personalizedContext, user);
|
|
1765
|
+
|
|
1766
|
+
const emailData = { title: personalizedSubject, content: personalizedBody };
|
|
1767
|
+
|
|
1768
|
+
// 7. Envoyer l'e-mail et suivre son résultat
|
|
1769
|
+
const sendPromise = sendEmail([recipientEmail], emailData, smtpConfig, user.lang)
|
|
1770
|
+
.then(() => {
|
|
1771
|
+
sentTo.push(recipient);
|
|
1772
|
+
})
|
|
1773
|
+
.catch(err => {
|
|
1774
|
+
logger.error(`[handleSendEmailAction] Failed to send email to ${recipientEmail}: ${err.message}`);
|
|
1775
|
+
failedFor.push({ recipient: recipientEmail, error: err.message });
|
|
1776
|
+
});
|
|
1777
|
+
|
|
1778
|
+
allPromises.push(sendPromise);
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1781
|
+
// Attendre que toutes les tentatives d'envoi soient terminées
|
|
1782
|
+
await Promise.all(allPromises);
|
|
1783
|
+
|
|
1784
|
+
const summaryMessage = `Email process completed. Sent: ${sentTo.length}. Failed: ${failedFor.length}.`;
|
|
1785
|
+
logger.info(`[handleSendEmailAction] ${summaryMessage}`);
|
|
1786
|
+
|
|
1787
|
+
// L'action elle-même a réussi, même si certains e-mails ont échoué.
|
|
1788
|
+
// Le message de retour et les données fournissent les détails.
|
|
1789
|
+
return {
|
|
1790
|
+
success: true,
|
|
1791
|
+
message: summaryMessage,
|
|
1792
|
+
data: {
|
|
1793
|
+
sent: sentTo,
|
|
1794
|
+
failed: failedFor
|
|
1795
|
+
},
|
|
1796
|
+
updatedContext: {
|
|
1797
|
+
emailResult: {
|
|
1798
|
+
sent: sentTo,
|
|
1799
|
+
failed: failedFor
|
|
1800
|
+
}
|
|
1801
|
+
}
|
|
1802
|
+
};
|
|
1803
|
+
|
|
1804
|
+
} catch (error) {
|
|
1805
|
+
const msg = `[handleSendEmailAction] Unexpected error during email processing: ${error.message}`;
|
|
1806
|
+
logger.error(msg, error.stack);
|
|
1807
|
+
return { success: false, message: msg };
|
|
1808
|
+
}
|
|
1818
1809
|
}
|