data-primals-engine 1.4.2 → 1.5.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 +878 -856
- package/client/package-lock.json +82 -0
- package/client/package.json +2 -0
- package/client/src/App.jsx +1 -1
- package/client/src/App.scss +25 -7
- package/client/src/AssistantChat.scss +3 -2
- package/client/src/ConditionBuilder.jsx +1 -1
- package/client/src/ConditionBuilder2.jsx +2 -1
- package/client/src/DashboardView.jsx +569 -569
- package/client/src/DataEditor.jsx +376 -368
- package/client/src/DataLayout.jsx +4 -10
- package/client/src/DataTable.jsx +858 -817
- package/client/src/Field.jsx +1825 -1784
- package/client/src/FlexDataRenderer.jsx +2 -0
- package/client/src/FlexTreeUtils.js +1 -1
- package/client/src/GeolocationField.jsx +94 -0
- package/client/src/KPIDialog.jsx +11 -1
- package/client/src/ModelCreator.jsx +1 -2
- package/client/src/ModelCreatorField.jsx +24 -27
- package/client/src/ModelList.jsx +1 -1
- package/client/src/RelationField.jsx +2 -2
- package/client/src/constants.js +3 -3
- package/client/src/filter.js +0 -155
- package/client/src/hooks/useTutorials.jsx +62 -65
- package/client/src/translations.js +14 -2
- package/package.json +2 -1
- package/perf/README.md +147 -0
- package/perf/artillery-hooks.js +37 -0
- package/perf/perf-shot-hardwork.yml +84 -0
- package/perf/perf-shot-search.yml +45 -0
- package/perf/setup.yml +26 -0
- package/server.js +1 -1
- package/src/constants.js +264 -31
- package/src/core.js +15 -1
- package/src/data.js +1 -1
- package/src/defaultModels.js +1544 -1540
- package/src/email.js +5 -2
- package/src/engine.js +10 -3
- package/src/filter.js +274 -260
- package/src/i18n.js +187 -177
- package/src/modules/assistant/assistant.js +3 -1
- package/src/modules/bucket.js +12 -15
- package/src/modules/data/data.backup.js +11 -8
- package/src/modules/data/data.core.js +2 -1
- package/src/modules/data/data.js +6 -3
- package/src/modules/data/data.operations.js +610 -168
- package/src/modules/data/data.routes.js +1821 -1785
- package/src/modules/data/data.scheduling.js +2 -1
- package/src/modules/data/data.validation.js +7 -1
- package/src/modules/file.js +4 -2
- package/src/modules/user.js +4 -1
- package/src/modules/workflow.js +9 -10
- package/src/openai.jobs.js +2 -0
- package/src/packs.js +22 -5
- package/src/providers.js +22 -7
- package/swagger-en.yml +133 -0
- package/test/data.integration.test.js +1060 -981
- package/test/import_export.integration.test.js +1 -1
- package/test/model.integration.test.js +377 -221
|
@@ -1,1786 +1,1822 @@
|
|
|
1
|
-
import {Logger} from "../../gameObject.js";
|
|
2
|
-
import {ObjectId} from "mongodb";
|
|
3
|
-
import * as util from 'node:util';
|
|
4
|
-
import {setTimeoutMiddleware} from '../../middlewares/timeout.js';
|
|
5
|
-
import {isDemoUser, isLocalUser} from "../../data.js";
|
|
6
|
-
import {
|
|
7
|
-
install,
|
|
8
|
-
maxBytesPerSecondThrottleData,
|
|
9
|
-
maxMagnetsDataPerModel,
|
|
10
|
-
maxMagnetsModels,
|
|
11
|
-
maxModelsPerUser
|
|
12
|
-
} from "../../constants.js";
|
|
13
|
-
import {datasCollection, getCollection, getCollectionForUser, isObjectId, modelsCollection} from "../mongodb.js";
|
|
14
|
-
import {safeAssignObject, uuidv4} from "../../core.js";
|
|
15
|
-
import {Event} from "../../events.js";
|
|
16
|
-
import fs from "node:fs";
|
|
17
|
-
import i18n from "../../i18n.js";
|
|
18
|
-
import {executeSafeJavascript, triggerWorkflows} from "../workflow.js";
|
|
19
|
-
import {openaiJobModel} from "../../openai.jobs.js";
|
|
20
|
-
import {getS3Stream, getUserS3Config} from "../bucket.js";
|
|
21
|
-
import {generateLimiter, hasPermission, middlewareAuthenticator, userInitiator} from "../user.js";
|
|
22
|
-
import {assistantGlobalLimiter} from "../assistant/assistant.js";
|
|
23
|
-
import {Config} from "../../config.js";
|
|
24
|
-
import {processFilterPlaceholders} from "../../../client/src/filter.js";
|
|
25
|
-
import {tutorialsConfig} from "../../../client/src/tutorials.js";
|
|
26
|
-
import {getResource, handleDemoInitialization} from "./data.js";
|
|
27
|
-
import process from "node:process";
|
|
28
|
-
import {throttleMiddleware} from "../../middlewares/throttle.js";
|
|
29
|
-
import {importJobs, modelsCache, runImportExportWorker} from "./data.core.js";
|
|
30
|
-
import {validateModelStructure} from "./data.validation.js";
|
|
31
|
-
import {
|
|
32
|
-
deleteData,
|
|
33
|
-
editData,
|
|
34
|
-
editModel,
|
|
35
|
-
exportData,
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
delete headers['
|
|
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
|
-
// -
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
const
|
|
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
|
-
* @param {
|
|
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
|
-
|
|
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
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
const
|
|
217
|
-
const
|
|
218
|
-
const
|
|
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
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
engine.
|
|
260
|
-
|
|
261
|
-
engine.
|
|
262
|
-
|
|
263
|
-
engine.post('/api/
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
const
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
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
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
const
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
if (
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
newData
|
|
452
|
-
|
|
453
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
586
|
-
{ $
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
res.
|
|
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
|
-
res.status(
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
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
|
-
|
|
689
|
-
|
|
690
|
-
|
|
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
|
-
|
|
743
|
-
|
|
744
|
-
|
|
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
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
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
|
-
if
|
|
853
|
-
return res.json({
|
|
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
|
-
|
|
886
|
-
|
|
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
|
-
return res.status(
|
|
980
|
-
}
|
|
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
|
-
const
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
}
|
|
1033
|
-
|
|
1034
|
-
//
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
}
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
if (
|
|
1058
|
-
return res.status(404).json({ error:
|
|
1059
|
-
|
|
1060
|
-
const
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
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
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
}
|
|
1170
|
-
}
|
|
1171
|
-
const
|
|
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
|
-
*
|
|
1213
|
-
*
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
}
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
// ---
|
|
1256
|
-
const
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
const
|
|
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
|
-
if (!
|
|
1319
|
-
|
|
1320
|
-
if (
|
|
1321
|
-
|
|
1322
|
-
|
|
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
|
-
groupStage.value = { '$
|
|
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
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
//
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
//
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
console.
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
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
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
const
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
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
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
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
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
res.status(
|
|
1699
|
-
}
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
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
|
-
if (
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1
|
+
import {Logger} from "../../gameObject.js";
|
|
2
|
+
import {ObjectId} from "mongodb";
|
|
3
|
+
import * as util from 'node:util';
|
|
4
|
+
import {setTimeoutMiddleware} from '../../middlewares/timeout.js';
|
|
5
|
+
import {isDemoUser, isLocalUser} from "../../data.js";
|
|
6
|
+
import {
|
|
7
|
+
install,
|
|
8
|
+
maxBytesPerSecondThrottleData,
|
|
9
|
+
maxMagnetsDataPerModel,
|
|
10
|
+
maxMagnetsModels,
|
|
11
|
+
maxModelsPerUser, maxPackData, maxPackPreviewData
|
|
12
|
+
} from "../../constants.js";
|
|
13
|
+
import {datasCollection, getCollection, getCollectionForUser, isObjectId, modelsCollection} from "../mongodb.js";
|
|
14
|
+
import {countKeys, safeAssignObject, uuidv4} from "../../core.js";
|
|
15
|
+
import {Event} from "../../events.js";
|
|
16
|
+
import fs from "node:fs";
|
|
17
|
+
import i18n from "../../i18n.js";
|
|
18
|
+
import {executeSafeJavascript, triggerWorkflows} from "../workflow.js";
|
|
19
|
+
import {openaiJobModel} from "../../openai.jobs.js";
|
|
20
|
+
import {getS3Stream, getUserS3Config} from "../bucket.js";
|
|
21
|
+
import {generateLimiter, hasPermission, middlewareAuthenticator, userInitiator} from "../user.js";
|
|
22
|
+
import {assistantGlobalLimiter} from "../assistant/assistant.js";
|
|
23
|
+
import {Config} from "../../config.js";
|
|
24
|
+
import {processFilterPlaceholders} from "../../../client/src/filter.js";
|
|
25
|
+
import {tutorialsConfig} from "../../../client/src/tutorials.js";
|
|
26
|
+
import {getResource, handleDemoInitialization} from "./data.js";
|
|
27
|
+
import process from "node:process";
|
|
28
|
+
import {throttleMiddleware} from "../../middlewares/throttle.js";
|
|
29
|
+
import {importJobs, modelsCache, runImportExportWorker} from "./data.core.js";
|
|
30
|
+
import {validateModelStructure} from "./data.validation.js";
|
|
31
|
+
import {
|
|
32
|
+
deleteData,
|
|
33
|
+
editData,
|
|
34
|
+
editModel,
|
|
35
|
+
exportData,
|
|
36
|
+
flushSearchCache,
|
|
37
|
+
getModel,
|
|
38
|
+
importData,
|
|
39
|
+
insertData,
|
|
40
|
+
installPack,
|
|
41
|
+
patchData,
|
|
42
|
+
searchData
|
|
43
|
+
} from "./data.operations.js";
|
|
44
|
+
import { dumpUserData, loadFromDump } from "./data.backup.js";
|
|
45
|
+
import { invalidateModelCache } from "./data.operations.js";
|
|
46
|
+
|
|
47
|
+
let logger, engine;
|
|
48
|
+
|
|
49
|
+
const sseConnections = new Map();
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
async function logApiRequest(req, res, user, startTime, responseBody = null, error = null) {
|
|
54
|
+
const endTime = process.hrtime(startTime);
|
|
55
|
+
const latencyMs = (endTime[0] * 1e3 + endTime[1] * 1e-6); // Calculer la latence en ms
|
|
56
|
+
|
|
57
|
+
const headers = req.headers;
|
|
58
|
+
delete headers['Cookie'];
|
|
59
|
+
delete headers['Authorization'];
|
|
60
|
+
|
|
61
|
+
// 1. Préparer l'objet de données pour le modèle 'request'
|
|
62
|
+
const logEntryData = {
|
|
63
|
+
// Champs requis
|
|
64
|
+
timestamp: new Date(),
|
|
65
|
+
method: req.method,
|
|
66
|
+
url: req.originalUrl || req.url, // Utiliser originalUrl si disponible (Express)
|
|
67
|
+
status: res.statusCode,
|
|
68
|
+
latencyMs: parseFloat(latencyMs.toFixed(3)), // Arrondir et convertir en nombre
|
|
69
|
+
|
|
70
|
+
// Champs optionnels
|
|
71
|
+
ip: req.clientIp.substring('::ffff:'.length) || req.clientIp, // Obtenir l'IP du client
|
|
72
|
+
//requestHeaders: JSON.stringify(req.headers).substring(0, maxStringLength), // Optionnel: Peut être volumineux
|
|
73
|
+
requestBody: req.fields,
|
|
74
|
+
responseBody: res.statusCode >= 400 && responseBody ? JSON.stringify(responseBody).substring(0, maxStringLength) : null, // Optionnel: Peut être volumineux
|
|
75
|
+
error: error ? String(error.message || error) : null // Message d'erreur si applicable
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
// 2. Appeler insertData pour enregistrer le log
|
|
80
|
+
// - 'request' est le nom du modèle
|
|
81
|
+
// - logEntryData contient les données
|
|
82
|
+
// - [] car pas de fichiers associés à ce log
|
|
83
|
+
// - null pour l'utilisateur (le log est système) ou 'user' si tu veux lier l'action à l'utilisateur
|
|
84
|
+
// - false pour ne pas bypasser la validation (important !)
|
|
85
|
+
const result = await insertData('request', logEntryData, [], user._user ? { username: user._user } : user, false);
|
|
86
|
+
|
|
87
|
+
if (result.success) {
|
|
88
|
+
console.log(`[API Log] Request logged successfully. ID: ${result.insertedIds?.join(', ')}`);
|
|
89
|
+
} else {
|
|
90
|
+
// Gérer l'échec de l'insertion du log (ne devrait pas bloquer la réponse principale)
|
|
91
|
+
console.error(`[API Log] Failed to log request: ${result.error}`);
|
|
92
|
+
}
|
|
93
|
+
} catch (insertError) {
|
|
94
|
+
// Gérer les erreurs inattendues lors de l'insertion
|
|
95
|
+
console.error(`[API Log] Unexpected error during logging: ${insertError.message}`, insertError.stack);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
const middlewareLogger = async (req, res, next) => {
|
|
101
|
+
const startTime = process.hrtime();
|
|
102
|
+
let responseBodyChunk = null; // Pour capturer le corps de la réponse si nécessaire
|
|
103
|
+
|
|
104
|
+
//
|
|
105
|
+
//Optionnel: Capturer le corps de la réponse (peut impacter la performance)
|
|
106
|
+
const originalSend = res.send;
|
|
107
|
+
res.send = function (body) {
|
|
108
|
+
responseBodyChunk = body; // Capture le corps avant l'envoi
|
|
109
|
+
originalSend.call(this, body);
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
res.on('finish', async () => {
|
|
113
|
+
try {
|
|
114
|
+
await logApiRequest(req, res, req.me, startTime, ( !req.hideApiLogs ) ? JSON.parse(responseBodyChunk) : { message: "The request log has been encrypted because of a clear password in the request."}, res.locals.error);
|
|
115
|
+
} catch (e) {
|
|
116
|
+
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
res.on('error', async (err) => {
|
|
121
|
+
// Logger aussi en cas d'erreur avant 'finish'
|
|
122
|
+
try{
|
|
123
|
+
await logApiRequest(req, res, req.me, startTime, null, err);
|
|
124
|
+
} catch (e) {
|
|
125
|
+
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
next();
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Envoie un SSE à un utilisateur spécifique.
|
|
134
|
+
* @param {string} username - Le nom de l'utilisateur à qui envoyer l'événement.
|
|
135
|
+
* @param {object} data - L'objet de données à envoyer.
|
|
136
|
+
* @returns {boolean} - True si l'événement a été envoyé, false sinon.
|
|
137
|
+
*/
|
|
138
|
+
export async function sendSseToUser(username, data) {
|
|
139
|
+
const res = sseConnections.get(username);
|
|
140
|
+
if (res) {
|
|
141
|
+
const ssePlugin = await Event.Trigger("sendSseToUser", "system", "calls", data) || data;
|
|
142
|
+
res.write(`data: ${JSON.stringify(ssePlugin)}\n\n`);
|
|
143
|
+
return true;
|
|
144
|
+
}
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
export async function handleCustomEndpointRequest(req, res) {
|
|
150
|
+
const endpointDef = req.endpointDef;
|
|
151
|
+
|
|
152
|
+
try {
|
|
153
|
+
let executionUser = null;
|
|
154
|
+
|
|
155
|
+
// 1. Déterminer le contexte utilisateur pour l'exécution
|
|
156
|
+
if (endpointDef.isPublic) {
|
|
157
|
+
// Pour les endpoints publics, on exécute le script en tant que propriétaire de l'endpoint.
|
|
158
|
+
if (!endpointDef._user) {
|
|
159
|
+
logger.error(`[Endpoint] Misconfiguration: Public endpoint '${endpointDef.name}' (ID: ${endpointDef._id}) has no owner.`);
|
|
160
|
+
return res.status(500).json({success: false, message: 'Endpoint misconfigured: owner missing.'});
|
|
161
|
+
}
|
|
162
|
+
executionUser = await engine.userProvider.findUserByUsername(endpointDef._user);
|
|
163
|
+
if (!executionUser) {
|
|
164
|
+
logger.error(`[Endpoint] Execution failed: Owner '${endpointDef._user}' for public endpoint '${endpointDef.name}' not found.`);
|
|
165
|
+
return res.status(500).json({success: false, message: 'Endpoint owner not found.'});
|
|
166
|
+
}
|
|
167
|
+
logger.info(`[Endpoint] Public endpoint '${endpointDef.name}' running as owner '${executionUser.username}'.`);
|
|
168
|
+
} else {
|
|
169
|
+
// Pour les endpoints privés, l'utilisateur a déjà été authentifié par le middleware.
|
|
170
|
+
// req.me est garanti d'exister ici.
|
|
171
|
+
executionUser = req.me;
|
|
172
|
+
logger.info(`[Endpoint] Private endpoint '${endpointDef.name}' running as authenticated user '${executionUser.username}'.`);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// 2. Préparer le contexte pour le script
|
|
176
|
+
const contextData = {
|
|
177
|
+
request: {
|
|
178
|
+
// MODIFICATION: Utiliser req.body si disponible (pour les requêtes JSON comme les webhooks Stripe),
|
|
179
|
+
// sinon, utiliser req.fields (pour les données de formulaire).
|
|
180
|
+
body: (req.body && Object.keys(req.body).length > 0) ? req.body : (req.fields || {}),
|
|
181
|
+
query: req.query,
|
|
182
|
+
params: req.params,
|
|
183
|
+
headers: req.headers
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
// 3. Exécuter le code de l'endpoint
|
|
188
|
+
const result = await executeSafeJavascript(
|
|
189
|
+
{script: endpointDef.code},
|
|
190
|
+
contextData,
|
|
191
|
+
executionUser // Use the determined user for execution
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
// 4. Envoyer la réponse
|
|
195
|
+
if (result.success) {
|
|
196
|
+
res.status(200).json(result.data);
|
|
197
|
+
} else {
|
|
198
|
+
logger.error(`[Endpoint] Execution failed for '${endpointDef.name}'. Error: ${result.message}`);
|
|
199
|
+
const responseError = {
|
|
200
|
+
success: false,
|
|
201
|
+
message: 'Endpoint script execution failed.',
|
|
202
|
+
details: result.message,
|
|
203
|
+
logs: result.logs
|
|
204
|
+
};
|
|
205
|
+
res.status(500).json(responseError);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
} catch (error) {
|
|
209
|
+
logger.error(`[Endpoint] Critical error handling request for path '${endpointDef.path}': ${error.message}`, error.stack);
|
|
210
|
+
res.status(500).json({success: false, message: 'An internal server error occurred.'});
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export async function middlewareEndpointAuthenticator(req, res, next) {
|
|
215
|
+
const {path} = req.params;
|
|
216
|
+
const method = req.method.toUpperCase();
|
|
217
|
+
const user = await engine.userProvider.findUserByUsername(req.query._user || req.params.user || req.me.username);
|
|
218
|
+
const datasCollection = await getCollectionForUser(user);
|
|
219
|
+
|
|
220
|
+
try {
|
|
221
|
+
const endpointDef = await datasCollection.findOne({
|
|
222
|
+
_model: 'endpoint',
|
|
223
|
+
path: path,
|
|
224
|
+
method: method,
|
|
225
|
+
isActive: true
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
if (!endpointDef) {
|
|
229
|
+
return res.status(404).json({success: false, message: 'Endpoint not found.'});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Attacher la définition à la requête pour que le handler suivant puisse l'utiliser
|
|
233
|
+
req.endpointDef = endpointDef;
|
|
234
|
+
|
|
235
|
+
// Si l'endpoint n'est PAS public, on exécute le vrai middleware d'authentification
|
|
236
|
+
if (!endpointDef.isPublic) {
|
|
237
|
+
// On "chaîne" vers le middleware authenticator standard.
|
|
238
|
+
// Il se chargera de vérifier le token et de renvoyer une 401 si nécessaire.
|
|
239
|
+
return middlewareAuthenticator(req, res, next);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Si l'endpoint EST public, on passe simplement à la suite.
|
|
243
|
+
next();
|
|
244
|
+
|
|
245
|
+
} catch (error) {
|
|
246
|
+
logger.error(`[EndpointAuth] Critical error: ${error.message}`, error.stack);
|
|
247
|
+
res.status(500).json({success: false, message: 'Internal server error during endpoint authentication.'});
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export async function registerRoutes(defaultEngine){
|
|
252
|
+
|
|
253
|
+
engine = defaultEngine;
|
|
254
|
+
logger = engine.getComponent(Logger);
|
|
255
|
+
|
|
256
|
+
const m = Config.Get('maxBytesPerSecondThrottleData', maxBytesPerSecondThrottleData)
|
|
257
|
+
const throttle = throttleMiddleware(m);
|
|
258
|
+
|
|
259
|
+
let userMiddlewares = await engine.userProvider.getMiddlewares();
|
|
260
|
+
|
|
261
|
+
engine.all('/api/actions/:user/:path', [middlewareEndpointAuthenticator, userInitiator], handleCustomEndpointRequest);
|
|
262
|
+
engine.all('/api/actions/:path', [middlewareAuthenticator, middlewareEndpointAuthenticator, userInitiator], handleCustomEndpointRequest);
|
|
263
|
+
engine.post('/api/demo/initialize', [middlewareAuthenticator, userInitiator], handleDemoInitialization);
|
|
264
|
+
|
|
265
|
+
engine.post('/api/magnets', [middlewareAuthenticator, userInitiator], async (req, res) => {
|
|
266
|
+
const user = req.me;
|
|
267
|
+
const { name, description, modelNames } = req.fields; // Noms des modèles à inclure
|
|
268
|
+
|
|
269
|
+
if (!name || !Array.isArray(modelNames) || modelNames.length === 0) {
|
|
270
|
+
return res.status(400).json({ error: 'Name and a list of model names are required.' });
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
try {
|
|
274
|
+
const magnetId = new ObjectId(); // ID unique pour ce groupe de données/modèles
|
|
275
|
+
const magnetUuid = uuidv4(); // UUID pour le lien public
|
|
276
|
+
|
|
277
|
+
const modelsToCopy = await modelsCollection.find({
|
|
278
|
+
name: { $in: modelNames },
|
|
279
|
+
_user: user.username
|
|
280
|
+
}).limit(maxMagnetsModels).toArray();
|
|
281
|
+
|
|
282
|
+
if (modelsToCopy.length !== modelNames.length) {
|
|
283
|
+
return res.status(404).json({ error: "One or more specified models were not found." });
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// 1. Copier les modèles et les marquer avec le magnetId
|
|
287
|
+
const newModels = modelsToCopy.map(m => {
|
|
288
|
+
const { _id, ...modelData } = m;
|
|
289
|
+
return {
|
|
290
|
+
...modelData,
|
|
291
|
+
magnetId: magnetId.toString(),
|
|
292
|
+
locked: true, // Les modèles d'un magnet sont en lecture seule
|
|
293
|
+
_user: null // Le propriétaire est le magnet, pas un utilisateur
|
|
294
|
+
};
|
|
295
|
+
});
|
|
296
|
+
await modelsCollection.insertMany(newModels);
|
|
297
|
+
|
|
298
|
+
const coll = await getCollectionForUser(user);
|
|
299
|
+
// 2. Copier les données associes marquer
|
|
300
|
+
// C'est la partie la plus complexe à cause des relations
|
|
301
|
+
const idMap = {}; // Map: old_id -> new_id
|
|
302
|
+
for (const model of modelsToCopy) {
|
|
303
|
+
const datasToCopy = await coll.find({
|
|
304
|
+
_model: model.name,
|
|
305
|
+
_user: user.username
|
|
306
|
+
}).limit(maxMagnetsDataPerModel).toArray();
|
|
307
|
+
|
|
308
|
+
if (datasToCopy.length === 0) continue;
|
|
309
|
+
|
|
310
|
+
// Passe 1: Insérer les documents sans leurs relations pour obtenir les nouveaux IDs
|
|
311
|
+
const newDatas = datasToCopy.map(d => {
|
|
312
|
+
const { _id, ...data } = d;
|
|
313
|
+
idMap[d._id.toString()] = new ObjectId(); // Pré-générer le nouvel ID
|
|
314
|
+
return {
|
|
315
|
+
...data,
|
|
316
|
+
_id: idMap[d._id.toString()],
|
|
317
|
+
magnetId: magnetId.toString(),
|
|
318
|
+
_user: null
|
|
319
|
+
};
|
|
320
|
+
});
|
|
321
|
+
const coll = await getCollectionForUser(user);
|
|
322
|
+
await coll.insertMany(newDatas);
|
|
323
|
+
|
|
324
|
+
// Passe 2: Mettre à jour les relations dans les documents fraîchement copiés
|
|
325
|
+
for (const newDoc of newDatas) {
|
|
326
|
+
const updatePayload = {};
|
|
327
|
+
for (const field of model.fields) {
|
|
328
|
+
if (field.type === 'relation' && newDoc[field.name]) {
|
|
329
|
+
if (Array.isArray(newDoc[field.name])) {
|
|
330
|
+
safeAssignObject()
|
|
331
|
+
newDoc[field.name] = newDoc[field.name]
|
|
332
|
+
.map(oldId => idMap[oldId.toString()]?.toString())
|
|
333
|
+
.filter(Boolean);
|
|
334
|
+
} else {
|
|
335
|
+
newDoc[field.name] = idMap[newDoc[field.name].toString()]?.toString() || null;
|
|
336
|
+
}
|
|
337
|
+
updatePayload[field.name] = newDoc[field.name];
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
if (Object.keys(updatePayload).length > 0) {
|
|
341
|
+
const coll = await getCollectionForUser(user);
|
|
342
|
+
await coll.updateOne({ _id: newDoc._id }, { $set: updatePayload });
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// 3. Créer l'enregistrement du magnet lui-même
|
|
348
|
+
const magnetData = {
|
|
349
|
+
uuid: magnetUuid,
|
|
350
|
+
name,
|
|
351
|
+
description,
|
|
352
|
+
owner: user.username,
|
|
353
|
+
createdAt: new Date(),
|
|
354
|
+
models: modelNames,
|
|
355
|
+
_id: magnetId
|
|
356
|
+
};
|
|
357
|
+
await getCollection('magnets').insertOne(magnetData); // Nouvelle collection 'magnets'
|
|
358
|
+
|
|
359
|
+
res.status(201).json({
|
|
360
|
+
success: true,
|
|
361
|
+
message: "Magnet link created successfully!",
|
|
362
|
+
url: `https://data.primals.net/magnet/${magnetUuid}` // ou votre URL de dev
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
} catch (error) {
|
|
366
|
+
logger.error("Error creating magnet link:", error);
|
|
367
|
+
res.status(500).json({ error: "An internal server error occurred." });
|
|
368
|
+
}
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Route pour vérifier si une condition de complétion est remplie.
|
|
373
|
+
* Utilise la fonction `searchData` existante pour interroger les données.
|
|
374
|
+
*/
|
|
375
|
+
engine.post('/api/tutorials/check-completion', middlewareAuthenticator, async (req, res) => {
|
|
376
|
+
try {
|
|
377
|
+
const { model, filter, limit } = req.fields;
|
|
378
|
+
const user = req.me;
|
|
379
|
+
|
|
380
|
+
if (!model || !filter || limit === undefined) {
|
|
381
|
+
return res.status(400).json({ error: 'Payload de condition de complétion invalide.' });
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const processedFilter = processFilterPlaceholders(filter, user);
|
|
385
|
+
|
|
386
|
+
// On utilise la fonction de recherche interne de l'application
|
|
387
|
+
const searchResult = await searchData({
|
|
388
|
+
model,
|
|
389
|
+
filter: processedFilter,
|
|
390
|
+
limit, // Optimisation : pas besoin de plus de résultats
|
|
391
|
+
page: 1
|
|
392
|
+
}, user);
|
|
393
|
+
|
|
394
|
+
// searchData devrait renvoyer un `count` total des documents correspondants
|
|
395
|
+
const isCompleted = searchResult.count >= limit;
|
|
396
|
+
|
|
397
|
+
res.json({ isCompleted });
|
|
398
|
+
|
|
399
|
+
} catch (error) {
|
|
400
|
+
console.error('[Tutoriel Check Error]', error);
|
|
401
|
+
res.status(500).json({ error: 'Erreur lors de la vérification de la complétion.', details: error.message });
|
|
402
|
+
}
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
engine.post('/api/tutorials/set-active', middlewareAuthenticator, async (req, res) => {
|
|
408
|
+
try {
|
|
409
|
+
const { tutorialState } = req.fields;
|
|
410
|
+
const user = req.me;
|
|
411
|
+
|
|
412
|
+
if (tutorialState !== null && (typeof tutorialState !== 'object' || !tutorialState.id)) {
|
|
413
|
+
return res.status(400).json({ error: 'Invalid tutorial state payload.' });
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// Créer une représentation de l'utilisateur mis à jour pour la réponse
|
|
417
|
+
const updatedData = { activeTutorial: tutorialState };
|
|
418
|
+
|
|
419
|
+
await engine.userProvider.updateUser(user, updatedData);
|
|
420
|
+
|
|
421
|
+
// --- MODIFICATION ---
|
|
422
|
+
res.json({
|
|
423
|
+
success: true,
|
|
424
|
+
updatedData // Renvoyer l'objet utilisateur complet
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
} catch (error) {
|
|
428
|
+
console.error('[Tutoriel Set Active Error]', error);
|
|
429
|
+
res.status(500).json({ error: 'Error setting active tutorial.', details: error.message });
|
|
430
|
+
}
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
// ...
|
|
434
|
+
|
|
435
|
+
engine.post('/api/tutorials/:tutorialId/claim-rewards', middlewareAuthenticator, async (req, res) => {
|
|
436
|
+
try {
|
|
437
|
+
const { tutorialId } = req.params;
|
|
438
|
+
const user = req.me; // L'objet utilisateur est déjà chargé
|
|
439
|
+
const tutorial = tutorialsConfig.find(t => t.id === tutorialId);
|
|
440
|
+
|
|
441
|
+
if (!tutorial) return res.status(404).json({ error: 'Tutoriel non trouvé.' });
|
|
442
|
+
if (!tutorial.rewards) return res.status(400).json({ error: 'Ce tutoriel n\'a pas de récompenses.' });
|
|
443
|
+
if (user.completedTutorials?.includes(tutorialId)) {
|
|
444
|
+
return res.status(400).json({ error: 'Tutoriel déjà terminé.' });
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
const { xpBonus, skill, achievement, notification } = tutorial.rewards;
|
|
448
|
+
|
|
449
|
+
// --- LOGIQUE CORRIGÉE ---
|
|
450
|
+
// On part des données existantes de l'utilisateur pour ne rien écraser
|
|
451
|
+
let newData = {
|
|
452
|
+
xp: user.xp || 0,
|
|
453
|
+
achievements: [...(user.achievements || [])],
|
|
454
|
+
skills: JSON.parse(JSON.stringify(user.skills || [])), // Copie profonde pour éviter les mutations directes
|
|
455
|
+
completedTutorials: [...(user.completedTutorials || [])]
|
|
456
|
+
};
|
|
457
|
+
|
|
458
|
+
// Appliquer les récompenses directement sur l'objet newData
|
|
459
|
+
if (xpBonus) newData.xp += xpBonus;
|
|
460
|
+
if (achievement && !newData.achievements.includes(achievement)) newData.achievements.push(achievement);
|
|
461
|
+
if (skill && skill.name) { // S'assurer que la compétence a un nom
|
|
462
|
+
const existingSkill = newData.skills.find(s => s.name === skill.name);
|
|
463
|
+
if (existingSkill) {
|
|
464
|
+
existingSkill.points += skill.points;
|
|
465
|
+
} else {
|
|
466
|
+
newData.skills.push({ name: skill.name, points: skill.points });
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
newData.completedTutorials.push(tutorialId);
|
|
471
|
+
newData.activeTutorial = null;
|
|
472
|
+
|
|
473
|
+
await engine.userProvider.updateUser(user, newData);
|
|
474
|
+
|
|
475
|
+
const translatedNotification = {
|
|
476
|
+
title: i18n.t(notification.title, notification.title),
|
|
477
|
+
message: i18n.t(notification.message, notification.message)
|
|
478
|
+
};
|
|
479
|
+
|
|
480
|
+
res.json({
|
|
481
|
+
success: true,
|
|
482
|
+
userUpdate: newData, // Renvoyer l'objet utilisateur complet et mis à jour
|
|
483
|
+
notification: translatedNotification
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
} catch (error) {
|
|
487
|
+
console.error('[Tutoriel Rewards Error]', error);
|
|
488
|
+
res.status(500).json({ error: 'Erreur lors de l\'attribution des récompenses.', details: error.message });
|
|
489
|
+
}
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
engine.get('/api/import/progress/:jobId', middlewareAuthenticator, async (req, res) => {
|
|
493
|
+
const { jobId } = req.params;
|
|
494
|
+
const user = req.me;
|
|
495
|
+
|
|
496
|
+
// Vérification d'autorisation: s'assurer que l'utilisateur est bien le propriétaire de la tâche
|
|
497
|
+
if (!importJobs[jobId] || importJobs[jobId].userId !== user.username) {
|
|
498
|
+
return res.status(403).send('Forbidden');
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
res.setHeader('Content-Type', 'text/event-stream');
|
|
502
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
503
|
+
res.setHeader('Connection', 'keep-alive');
|
|
504
|
+
res.setHeader('X-Accel-Buffering', 'no'); // Important pour désactiver le buffering de certains proxys (ex: Nginx)
|
|
505
|
+
|
|
506
|
+
const sendProgress = () => {
|
|
507
|
+
const job = importJobs[jobId];
|
|
508
|
+
if (job) {
|
|
509
|
+
res.write(`data: ${JSON.stringify(job)}\n\n`);
|
|
510
|
+
if (job.status === 'completed' || job.status === 'failed') {
|
|
511
|
+
res.end(); // Terminer le stream lorsque la tâche est terminée
|
|
512
|
+
delete importJobs[jobId]; // Nettoyer la tâche de la mémoire
|
|
513
|
+
}
|
|
514
|
+
} else {
|
|
515
|
+
// Si la tâche n'est plus là (déjà nettoyée ou jamais existé)
|
|
516
|
+
res.write(`data: ${JSON.stringify({ status: 'not_found', message: 'Job not found or already completed.' })}\n\n`);
|
|
517
|
+
res.end();
|
|
518
|
+
}
|
|
519
|
+
};
|
|
520
|
+
|
|
521
|
+
// Envoyer la progression initiale immédiatement
|
|
522
|
+
sendProgress();
|
|
523
|
+
|
|
524
|
+
// Mettre en place un intervalle pour envoyer des mises à jour régulières
|
|
525
|
+
// Pour une application plus complexe, vous pourriez utiliser un EventEmitter
|
|
526
|
+
// pour déclencher des mises à jour uniquement quand la progression change.
|
|
527
|
+
const intervalId = setInterval(sendProgress, 1000); // Mettre à jour toutes les secondes
|
|
528
|
+
|
|
529
|
+
// Nettoyer l'intervalle lorsque le client se déconnecte
|
|
530
|
+
req.on('close', () => {
|
|
531
|
+
clearInterval(intervalId);
|
|
532
|
+
logger.debug(`SSE client disconnected for job ${jobId}`);
|
|
533
|
+
});
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
engine.get('/api/alerts/subscribe', [middlewareAuthenticator], (req, res) => {
|
|
537
|
+
const user = req.me;
|
|
538
|
+
|
|
539
|
+
// Configuration des headers pour une connexion SSE
|
|
540
|
+
res.setHeader('Content-Type', 'text/event-stream');
|
|
541
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
542
|
+
res.setHeader('Connection', 'keep-alive');
|
|
543
|
+
res.setHeader('X-Accel-Buffering', 'no'); // Important pour Nginx
|
|
544
|
+
|
|
545
|
+
// Stocker la connexion de l'utilisateur
|
|
546
|
+
sseConnections.set(user.username, res);
|
|
547
|
+
logger.info(`User ${user.username} subscribed to SSE alerts.`);
|
|
548
|
+
|
|
549
|
+
// Envoyer un message de confirmation
|
|
550
|
+
res.write(`data: ${JSON.stringify({ type: 'connection_established', message: 'Subscribed to alerts.' })}\n\n`);
|
|
551
|
+
|
|
552
|
+
// Gérer la déconnexion du client
|
|
553
|
+
req.on('close', () => {
|
|
554
|
+
sseConnections.delete(user.username);
|
|
555
|
+
logger.info(`User ${user.username} disconnected from SSE alerts.`);
|
|
556
|
+
});
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
engine.post('/api/data/import', [middlewareAuthenticator, userInitiator, ...userMiddlewares, setTimeoutMiddleware(60000)], async (req, res) => {
|
|
560
|
+
// ... (vérifications de permissions existantes) ...
|
|
561
|
+
const result = await importData(req.fields, req.files, req.me);
|
|
562
|
+
if( result.success ){
|
|
563
|
+
res.status(202).json(result);
|
|
564
|
+
}else{
|
|
565
|
+
res.status(500).json(result);
|
|
566
|
+
}
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
|
|
570
|
+
engine.post('/api/model/search', [middlewareAuthenticator, userInitiator], async (req, res) => {
|
|
571
|
+
const { query } = req.fields;
|
|
572
|
+
const user = req.me;
|
|
573
|
+
|
|
574
|
+
if (!query || typeof query !== 'string') {
|
|
575
|
+
return res.status(400).json({ success: false, error: 'A search query string is required.' });
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
try {
|
|
579
|
+
// Utilise une expression régulière pour une recherche insensible à la casse
|
|
580
|
+
const searchRegex = new RegExp(query, 'i');
|
|
581
|
+
|
|
582
|
+
const results = await modelsCollection.find({
|
|
583
|
+
// Cherche dans les modèles de l'utilisateur OU les modèles partagés
|
|
584
|
+
$or: [
|
|
585
|
+
{ _user: user.username },
|
|
586
|
+
{ _user: { $exists: false } }
|
|
587
|
+
],
|
|
588
|
+
// Cherche dans le nom OU la description
|
|
589
|
+
$and: [
|
|
590
|
+
{ $or: [
|
|
591
|
+
{ name: { $regex: searchRegex } },
|
|
592
|
+
{ description: { $regex: searchRegex } }
|
|
593
|
+
]}
|
|
594
|
+
]
|
|
595
|
+
}, {
|
|
596
|
+
// On ne retourne que les informations utiles pour l'IA, pas tout le modèle
|
|
597
|
+
projection: {
|
|
598
|
+
name: 1,
|
|
599
|
+
description: 1,
|
|
600
|
+
_id: 0
|
|
601
|
+
}
|
|
602
|
+
}).limit(10).toArray(); // Limite à 10 résultats pour ne pas surcharger
|
|
603
|
+
|
|
604
|
+
res.json({ success: true, models: results });
|
|
605
|
+
|
|
606
|
+
} catch (error) {
|
|
607
|
+
logger.error(`[Model Search] Error searching models for query "${query}":`, error);
|
|
608
|
+
res.status(500).json({ success: false, error: 'An internal server error occurred.' });
|
|
609
|
+
}
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
engine.post('/api/model/generate', [middlewareAuthenticator, userInitiator, assistantGlobalLimiter, generateLimiter, setTimeoutMiddleware(30000)], async (req, res) => {
|
|
613
|
+
// --- NOUVELLE LOGIQUE : Accepter le prompt ET un modèle existant ---
|
|
614
|
+
const { prompt, history = [], existingModel } = req.fields;
|
|
615
|
+
const user = req.me;
|
|
616
|
+
const lang = req.query.lang || 'en';
|
|
617
|
+
|
|
618
|
+
if (!prompt) {
|
|
619
|
+
return res.status(400).json({ error: 'Prompt is required' });
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
try {
|
|
623
|
+
const existingModelsCursor = modelsCollection.find({
|
|
624
|
+
$or: [
|
|
625
|
+
{ _user: user.username },
|
|
626
|
+
{ _user: { $exists: false } }
|
|
627
|
+
]
|
|
628
|
+
});
|
|
629
|
+
const existingModels = await existingModelsCursor.toArray();
|
|
630
|
+
const modelNames = existingModels.map(m => m.name);
|
|
631
|
+
|
|
632
|
+
let finalPrompt = prompt; // Par défaut, on utilise le prompt de l'utilisateur
|
|
633
|
+
|
|
634
|
+
if (existingModel && typeof existingModel === 'object') {
|
|
635
|
+
// Si un modèle existant est fourni, on crée une instruction d'édition
|
|
636
|
+
// C'est l'injonction d'édition que vous souhaitiez.
|
|
637
|
+
logger.info(`[AI Edit] Génération d'une modification pour le modèle : ${existingModel.name}`);
|
|
638
|
+
finalPrompt = `Based on the user's request, improve or modify the following JSON model definition. The user request is: "${prompt}".\n\nHere is the original JSON model to edit:\n${JSON.stringify(existingModel, null, 2)}`;
|
|
639
|
+
} else {
|
|
640
|
+
logger.info(`[AI Create] Génération d'un nouveau modèle depuis le prompt.`);
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
// On passe le prompt final (soit de création, soit d'édition) à l'IA
|
|
644
|
+
const generatedModels = await openaiJobModel(lang, finalPrompt, history, modelNames);
|
|
645
|
+
|
|
646
|
+
if (typeof(generatedModels) !== 'object' || !generatedModels.models) {
|
|
647
|
+
console.error("La réponse de l'IA n'est pas un objet avec une clé 'models':", generatedModels);
|
|
648
|
+
return res.status(500).json({ success: false, error: "Erreur de format de réponse de l'IA." });
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
res.status(200).json(generatedModels.models);
|
|
652
|
+
|
|
653
|
+
} catch (error) {
|
|
654
|
+
console.error("Error during AI model generation:", error);
|
|
655
|
+
res.status(500).json({ error: "An error occurred while generating the model.", details: error.message });
|
|
656
|
+
}
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
engine.put('/api/model/:id', [middlewareAuthenticator, userInitiator, setTimeoutMiddleware(15000)], async (req, res) => {
|
|
660
|
+
const result = await editModel(req.me, req.params.id, req.fields);
|
|
661
|
+
if( result.success){
|
|
662
|
+
return res.status(result.statusCode || 200).json(result);
|
|
663
|
+
}else{
|
|
664
|
+
return res.status(result.statusCode || 500).json(result);
|
|
665
|
+
}
|
|
666
|
+
});
|
|
667
|
+
|
|
668
|
+
engine.post('/api/data/restore', [throttle, middlewareAuthenticator, userInitiator, ...userMiddlewares, setTimeoutMiddleware(60000)], async (req, res) => {
|
|
669
|
+
|
|
670
|
+
if (!((user?.roles || []).includes("admin"))) {
|
|
671
|
+
return res.status(403).json({success: false, error: 'Cannot backup data. Contact an administrator to get back your data'})
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
try {
|
|
675
|
+
await loadFromDump({username: req.query.restoredUser});
|
|
676
|
+
res.status(200).json({success: true});
|
|
677
|
+
} catch (e) {
|
|
678
|
+
logger.error(e);
|
|
679
|
+
res.status(500).json({success: false, error: e.message});
|
|
680
|
+
}
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
engine.post('/api/data/dump', [throttle, middlewareAuthenticator, ...userMiddlewares, setTimeoutMiddleware(60000)], async (req, res) => {
|
|
684
|
+
|
|
685
|
+
if (!((req.me?.roles || []).includes("admin"))) {
|
|
686
|
+
return res.status(403).json({success: false, error: 'Cannot dump data.'})
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
try {
|
|
690
|
+
if( typeof(req.query.restoredUser) === 'string' && req.query.restoredUser.length < 100 ) {
|
|
691
|
+
await dumpUserData({username: req.query.restoredUser});
|
|
692
|
+
res.status(200).json({success: true});
|
|
693
|
+
}else{
|
|
694
|
+
throw new Error("User restoredUser must be defined in the URL query params.")
|
|
695
|
+
}
|
|
696
|
+
} catch (e) {
|
|
697
|
+
logger.error(e);
|
|
698
|
+
res.status(500).json({success: false, error: e.message});
|
|
699
|
+
}
|
|
700
|
+
});
|
|
701
|
+
|
|
702
|
+
engine.post('/api/data', [throttle, middlewareAuthenticator, userInitiator, middlewareLogger, ...userMiddlewares, setTimeoutMiddleware(15000)], async (req, res) => {
|
|
703
|
+
const body = req.files ? req.fields : req.fields;
|
|
704
|
+
const modelName = body.model; // Les données à insérer/mettre à jour (assurez-vous de valider et nettoyer ces données côté client et serveur !)
|
|
705
|
+
const data = body.data || (body._data && JSON.parse(body._data));
|
|
706
|
+
|
|
707
|
+
try {
|
|
708
|
+
const model = await getModel(modelName, req.me);
|
|
709
|
+
if( model.fields.some(f => f.type ==='password'))
|
|
710
|
+
req.hideApiLogs = true;
|
|
711
|
+
const json = await insertData(modelName, data, req.files, req.me);
|
|
712
|
+
res.status(200).json(json);
|
|
713
|
+
} catch (e) {
|
|
714
|
+
res.status(400).json({ success: false, error: e.message });
|
|
715
|
+
}
|
|
716
|
+
});
|
|
717
|
+
|
|
718
|
+
engine.post('/api/data/search', [throttle, middlewareAuthenticator, userInitiator, middlewareLogger, ...userMiddlewares, setTimeoutMiddleware(30000)], async (req, res) => {
|
|
719
|
+
const { pack } = req.fields;
|
|
720
|
+
|
|
721
|
+
try {
|
|
722
|
+
const {data, count} = await searchData({...req.query, model: req.fields.model || req.query.model, filter: req.fields.filter, pack}, req.me);
|
|
723
|
+
|
|
724
|
+
if( req.query.attachment ) {
|
|
725
|
+
res.attachment(req.query.attachment);
|
|
726
|
+
res.json(data.map(d=>{
|
|
727
|
+
let fd = {...d};
|
|
728
|
+
Object.keys(d).forEach(di =>{
|
|
729
|
+
if (['_model', '_user'].includes(di)){
|
|
730
|
+
delete fd[di];
|
|
731
|
+
}
|
|
732
|
+
});
|
|
733
|
+
return fd;
|
|
734
|
+
}));
|
|
735
|
+
}else
|
|
736
|
+
{
|
|
737
|
+
res.json({data, count: count});
|
|
738
|
+
}
|
|
739
|
+
} catch (error) {
|
|
740
|
+
logger.error(error);
|
|
741
|
+
res.status(400).json({ success: false, error: error.message });
|
|
742
|
+
}
|
|
743
|
+
});
|
|
744
|
+
|
|
745
|
+
engine.delete('/api/data/:ids', [throttle, middlewareAuthenticator, userInitiator, middlewareLogger, setTimeoutMiddleware(15000)], async (req, res) => {
|
|
746
|
+
const ids = req.params.ids.split(',');
|
|
747
|
+
const r = await deleteData(req.fields.model, ids, req.me);
|
|
748
|
+
if( r.error) {
|
|
749
|
+
return res.status(r.statusCode || 400).json(r);
|
|
750
|
+
}else{
|
|
751
|
+
return res.status(r.statusCode || 200).json(r);
|
|
752
|
+
}
|
|
753
|
+
});
|
|
754
|
+
|
|
755
|
+
engine.delete('/api/data', [throttle, middlewareAuthenticator, userInitiator, middlewareLogger, setTimeoutMiddleware(15000)], async (req, res) => {
|
|
756
|
+
const r = await deleteData(req.fields.model, req.fields.filter, req.me);
|
|
757
|
+
if( r.error) {
|
|
758
|
+
return res.status(r.statusCode || 400).json(r);
|
|
759
|
+
}else{
|
|
760
|
+
return res.status(r.statusCode || 200).json(r);
|
|
761
|
+
}
|
|
762
|
+
});
|
|
763
|
+
|
|
764
|
+
// --- Export Endpoint ---
|
|
765
|
+
engine.post('/api/data/export', [middlewareAuthenticator, throttle, userInitiator, ...userMiddlewares, setTimeoutMiddleware(60000)], async (req, res) => {
|
|
766
|
+
try {
|
|
767
|
+
const results = await exportData({...req.fields, depth:req.query.depth, lang: req.query.lang}, req.me);
|
|
768
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
769
|
+
const filename = `export-${req.fields.models.join('-')}-${timestamp}.json`;
|
|
770
|
+
|
|
771
|
+
if (!results.success) {
|
|
772
|
+
res.status(400).json(results); // Renvoyer l'objet d'erreur complet
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
const dataToSerialize = req.query.withModels ? {data: results.data, models: results.models } : results.data;
|
|
777
|
+
|
|
778
|
+
// --- La sérialisation est maintenant déléguée au worker ---
|
|
779
|
+
const jsonString = await runImportExportWorker('stringify-json', { data: dataToSerialize });
|
|
780
|
+
|
|
781
|
+
// On envoie directement la chaîne de caractères JSON, sans que Express ne la re-traite.
|
|
782
|
+
res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
|
|
783
|
+
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
|
784
|
+
res.send(jsonString);
|
|
785
|
+
|
|
786
|
+
} catch (error) {
|
|
787
|
+
console.error("General Export Error:", error);
|
|
788
|
+
logger?.error("General Export Error:", error);
|
|
789
|
+
return res.status(500).json({ success: false, error: i18n.t('api.data.error', 'An error occurred while processing the request.') });
|
|
790
|
+
}
|
|
791
|
+
});
|
|
792
|
+
|
|
793
|
+
engine.patch('/api/data', [throttle, middlewareAuthenticator, userInitiator, middlewareLogger], async (req, res) => {
|
|
794
|
+
const filter = req.fields.filter;
|
|
795
|
+
const hash = req.params.id; // Récupérer l'identifiant de la ressource à modifier
|
|
796
|
+
const data = req.fields.data || (req.fields._data && JSON.parse(req.fields._data));
|
|
797
|
+
const r = await patchData(req.fields.model, filter || hash, data, req.files, req.me);
|
|
798
|
+
if (r.error) {
|
|
799
|
+
res.status(400).json(r);
|
|
800
|
+
} else {
|
|
801
|
+
res.status(200).json(r);
|
|
802
|
+
}
|
|
803
|
+
});
|
|
804
|
+
|
|
805
|
+
engine.put('/api/data', [throttle, middlewareAuthenticator, userInitiator, middlewareLogger], async (req, res) => {
|
|
806
|
+
try {
|
|
807
|
+
const filter = req.fields.filter;
|
|
808
|
+
const hash = req.params.id; // Récupérer l'identifiant de la ressource à modifier
|
|
809
|
+
const data = req.fields.data || (req.fields._data && JSON.parse(req.fields._data));
|
|
810
|
+
const r = await editData(req.fields.model, filter || hash, data, req.files, req.me)
|
|
811
|
+
if (r.error)
|
|
812
|
+
res.status(400).json(r);
|
|
813
|
+
else
|
|
814
|
+
res.status(200).json(r);
|
|
815
|
+
} catch (e) {
|
|
816
|
+
res.status(500).json({error: e.message});
|
|
817
|
+
}
|
|
818
|
+
});
|
|
819
|
+
|
|
820
|
+
engine.patch('/api/data/:id', [throttle, middlewareAuthenticator, userInitiator, middlewareLogger], async (req, res) => {
|
|
821
|
+
const filter = req.fields.filter;
|
|
822
|
+
const hash = req.params.id; // Récupérer l'identifiant de la ressource à modifier
|
|
823
|
+
const data = req.fields.data || (req.fields._data && JSON.parse(req.fields._data));
|
|
824
|
+
const r = await patchData(req.fields.model, filter || hash, data, req.files, req.me);
|
|
825
|
+
if (r.error) {
|
|
826
|
+
res.status(400).json(r);
|
|
827
|
+
} else {
|
|
828
|
+
res.status(200).json(r);
|
|
829
|
+
}
|
|
830
|
+
});
|
|
831
|
+
|
|
832
|
+
engine.put('/api/data/:id', [throttle, middlewareAuthenticator, userInitiator, middlewareLogger], async (req, res) => {
|
|
833
|
+
try {
|
|
834
|
+
const filter = req.fields.filter;
|
|
835
|
+
const hash = req.params.id; // Récupérer l'identifiant de la ressource à modifier
|
|
836
|
+
const data = req.fields.data || (req.fields._data && JSON.parse(req.fields._data));
|
|
837
|
+
const r = await editData(req.fields.model, filter || { "$eq": ["$_id", { "$toObjectId": hash}]}, data, req.files, req.me)
|
|
838
|
+
if (r.error)
|
|
839
|
+
res.status(400).json(r);
|
|
840
|
+
else
|
|
841
|
+
res.status(200).json(r);
|
|
842
|
+
} catch (e) {
|
|
843
|
+
res.status(500).json({error: e.message});
|
|
844
|
+
}
|
|
845
|
+
});
|
|
846
|
+
|
|
847
|
+
engine.get('/api/model', [throttle, middlewareAuthenticator, userInitiator,middlewareLogger], async (req, res) => {
|
|
848
|
+
|
|
849
|
+
// get by name
|
|
850
|
+
try {
|
|
851
|
+
const modelName = req.query.name; // Récupérer le nom du modèle depuis les paramètres de la requête
|
|
852
|
+
if (!modelName) {
|
|
853
|
+
return res.status(400).json({error: "Le paramètre 'name' est requis."});
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
if( !(isDemoUser(req.me) && Config.Get("useDemoAccounts")) && isLocalUser(req.me) && !await hasPermission(["API_ADMIN", "API_GET_MODEL"], req.me) && !await hasPermission("API_GET_MODEL_"+modelName, req.me)){
|
|
857
|
+
return res.json({success: false, error: i18n.t('api.permission.getModel')})
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
const model = await getModel(modelName, req.me);
|
|
861
|
+
res.json(model);
|
|
862
|
+
} catch (error) {
|
|
863
|
+
logger.error(error);
|
|
864
|
+
res.status(404).json({ success: false, error: error.message });
|
|
865
|
+
}
|
|
866
|
+
});
|
|
867
|
+
engine.get('/api/models', [throttle, middlewareAuthenticator, userInitiator, middlewareLogger], async (req, res) => {
|
|
868
|
+
|
|
869
|
+
if( !(isDemoUser(req.me) && Config.Get("useDemoAccounts")) && isLocalUser(req.me) && !await hasPermission(["API_ADMIN", "API_GET_MODELS"], req.me)){
|
|
870
|
+
return res.status(403).json({success: false, error: i18n.t('api.permission.getModels')})
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
// get by name
|
|
874
|
+
try {
|
|
875
|
+
const m = Config.Get('maxModelsPerUser', maxModelsPerUser);
|
|
876
|
+
let models = await modelsCollection.find({$or: [{_user: {$exists: false}}]})
|
|
877
|
+
.sort({_user:-1, _id: 1 }).limit(m).toArray();
|
|
878
|
+
models = models
|
|
879
|
+
.concat(
|
|
880
|
+
await modelsCollection.find({$or: [{_user: req.me._user}, {_user: req.me.username}]})
|
|
881
|
+
.sort({_user:-1, _id: 1 })
|
|
882
|
+
.limit(m).toArray());
|
|
883
|
+
res.json(models);
|
|
884
|
+
} catch (error) {
|
|
885
|
+
logger.error(error);
|
|
886
|
+
res.status(500).json({ success: false, error: error.message });
|
|
887
|
+
}
|
|
888
|
+
});
|
|
889
|
+
engine.post('/api/model', [throttle, middlewareAuthenticator, userInitiator, middlewareLogger, ...userMiddlewares], async (req, res) => {
|
|
890
|
+
|
|
891
|
+
if( !(isDemoUser(req.me) && Config.Get("useDemoAccounts")) && isLocalUser(req.me) && !await hasPermission(["API_ADMIN", "API_ADD_MODEL"], req.me) ){
|
|
892
|
+
return res.status(403).json({success: false, error: i18n.t('api.permission.addModel')})
|
|
893
|
+
}
|
|
894
|
+
try {
|
|
895
|
+
const modelData = req.fields;
|
|
896
|
+
await validateModelStructure(modelData);
|
|
897
|
+
|
|
898
|
+
|
|
899
|
+
const existingModel = await modelsCollection.findOne({name: modelData.name, $and: [{_user: {$exists: true}}, {$or: [{_user: req.me._user}, {_user: req.me.username}]}] });
|
|
900
|
+
if (existingModel) {
|
|
901
|
+
/*await modelsCollection.updateOne({name: modelData.name,_user: req.query._user }, { $set: modelData });
|
|
902
|
+
res.status(200).json({updated: true});*/
|
|
903
|
+
res.status(400).json({ success: false, msg: i18n.t('api.model.alreadyExists') });
|
|
904
|
+
}else {
|
|
905
|
+
modelData._user = req.me._user || req.me.username;
|
|
906
|
+
|
|
907
|
+
const count = await modelsCollection.count({
|
|
908
|
+
$and: [{_user: {$exists: true}}, {_user: req.me.username}]
|
|
909
|
+
});
|
|
910
|
+
const m = Config.Get('maxModelsPerUser', maxModelsPerUser);
|
|
911
|
+
if( count < m) {
|
|
912
|
+
if(await engine.userProvider.hasFeature(req.me, 'indexes')){
|
|
913
|
+
for (const field of modelData.fields) {
|
|
914
|
+
if( field.index ) {
|
|
915
|
+
await datasCollection.createIndex({[field.name]: 1}, {
|
|
916
|
+
partialFilterExpression: {
|
|
917
|
+
_model: modelData.name,
|
|
918
|
+
_user: req.me.username
|
|
919
|
+
}
|
|
920
|
+
});
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
const result = await modelsCollection.insertOne(modelData);
|
|
925
|
+
|
|
926
|
+
const model = await modelsCollection.findOne({_id: result.insertedId });
|
|
927
|
+
triggerWorkflows(model, req.me, 'ModelAdded').catch(workflowError => {
|
|
928
|
+
logger.error(`Erreur asynchrone lors du déclenchement des workflows pour ${model._model} ID ${model._id}:`, workflowError);
|
|
929
|
+
});
|
|
930
|
+
|
|
931
|
+
modelsCache.del(req.me.username+'@@'+modelData.name);
|
|
932
|
+
|
|
933
|
+
res.status(201).json({success: !!result.insertedId, insertedId: result.insertedId}); // 201 Created
|
|
934
|
+
}else{
|
|
935
|
+
res.status(400).json({success: false, msg: i18n.t('api.model.maxModels')});
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
} catch (error) {
|
|
939
|
+
logger.error(error);
|
|
940
|
+
res.status(400).json({success: false, error: error.message});
|
|
941
|
+
}
|
|
942
|
+
});
|
|
943
|
+
|
|
944
|
+
engine.post('/api/models/import', [throttle, middlewareAuthenticator, userInitiator, middlewareLogger, ...userMiddlewares], async (req, res) => {
|
|
945
|
+
|
|
946
|
+
if( isLocalUser(req.me) && !await hasPermission(["API_ADMIN","API_IMPORT_MODEL"], req.me)){
|
|
947
|
+
return res.status(403).json({success: false, error: i18n.t('api.permission.importModels')})
|
|
948
|
+
}
|
|
949
|
+
try {
|
|
950
|
+
const modelsToImport = await modelsCollection.find({name: { $in: req.fields.models }, _user: { $exists: false } }).toArray();
|
|
951
|
+
const ids = [];
|
|
952
|
+
|
|
953
|
+
const getPromise = () => {
|
|
954
|
+
return Promise.allSettled(modelsToImport.map(model => modelsCollection.findOne({name: model.name, _user: req.me.username }).then(m => {
|
|
955
|
+
if( !m) {
|
|
956
|
+
return modelsCollection.insertOne({...model, _id: undefined, locked: undefined, fields: model.fields.map(f => ({...f, locked: undefined})), _user: req.me.username}).then(r =>{
|
|
957
|
+
if( r.insertedId ) ids.push(model.name);
|
|
958
|
+
})
|
|
959
|
+
}
|
|
960
|
+
return Promise.reject();
|
|
961
|
+
})));
|
|
962
|
+
}
|
|
963
|
+
await getPromise();
|
|
964
|
+
|
|
965
|
+
if( ids.length > 0 )
|
|
966
|
+
res.status(201).json({ success: true, imported: ids }); // 201 Created
|
|
967
|
+
else
|
|
968
|
+
res.status(500).json({ success: false });
|
|
969
|
+
} catch (error) {
|
|
970
|
+
logger.error(error);
|
|
971
|
+
res.status(400).json({success: false, error, badRequest: true});
|
|
972
|
+
}
|
|
973
|
+
});
|
|
974
|
+
|
|
975
|
+
engine.delete('/api/model', [throttle, middlewareAuthenticator, userInitiator, middlewareLogger], async (req, res) => {
|
|
976
|
+
|
|
977
|
+
const modelName = req.query.name;
|
|
978
|
+
if (!modelName) {
|
|
979
|
+
return res.status(400).json({error: "Le paramètre 'name' est requis."});
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
if( !(isDemoUser(req.me) && Config.Get("useDemoAccounts")) && isLocalUser(req.me) && (
|
|
983
|
+
!await hasPermission(["API_ADMIN","API_DELETE_MODEL","API_DELETE_MODEL_"+modelName], req.me) ||
|
|
984
|
+
await hasPermission(["API_DELETE_MODEL_NOT_"+modelName], req.me))){
|
|
985
|
+
return res.status(403).json({success: false, error: i18n.t( "api.permission.deleteModel", { model: modelName})})
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
// delete the model (statut archivage + date butoire)
|
|
989
|
+
// error otherwise
|
|
990
|
+
try {
|
|
991
|
+
const model = await modelsCollection.findOne({name: modelName, $and: [{_user: {$exists: true}}, {$or: [{_user: req.me._user}, {_user: req.me.username}]}] });
|
|
992
|
+
if (!model) {
|
|
993
|
+
return res.status(404).json({error: i18n.t( "api.model.notFound", { model: modelName})});
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
// Invalider le cache pour ce modèle avant de le supprimer
|
|
997
|
+
invalidateModelCache(modelName);
|
|
998
|
+
|
|
999
|
+
if( await engine.userProvider.hasFeature(req.me, 'indexes') ) {
|
|
1000
|
+
const indexes = await datasCollection.indexes();
|
|
1001
|
+
for (const index of indexes) {
|
|
1002
|
+
if (index.partialFilterExpression?._model === model.name &&
|
|
1003
|
+
index.partialFilterExpression?._user === req.me.username) {
|
|
1004
|
+
await datasCollection.dropIndex(index.name);
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
const filter= {name: modelName, $and: [{_user: {$exists: true}}, {$or: [{_user: req.me._user}, {_user: req.me.username}]}]};
|
|
1010
|
+
|
|
1011
|
+
const modelDeleted = await modelsCollection.findOne(filter);
|
|
1012
|
+
triggerWorkflows(modelDeleted, req.me, 'ModelDeleted').catch(workflowError => {
|
|
1013
|
+
logger.error(`Erreur asynchrone lors du déclenchement des workflows pour ${modelDeleted._model} ID ${modelDeleted._id}:`, workflowError);
|
|
1014
|
+
});
|
|
1015
|
+
|
|
1016
|
+
modelsCache.del(req.me.username+'@@'+model.name);
|
|
1017
|
+
|
|
1018
|
+
const result = await modelsCollection.deleteOne(filter);
|
|
1019
|
+
if( result )
|
|
1020
|
+
res.status(200).json({success: true});
|
|
1021
|
+
else
|
|
1022
|
+
res.status(500).json({success: false, message: i18n.t('api.model.deleteFailed')});
|
|
1023
|
+
} catch (error) {
|
|
1024
|
+
// ... (gestion des erreurs)
|
|
1025
|
+
logger.error(error);
|
|
1026
|
+
}
|
|
1027
|
+
});
|
|
1028
|
+
engine.patch('/api/model/:modelId/renameField', [throttle, middlewareAuthenticator, userInitiator, middlewareLogger], async (req, res) => {
|
|
1029
|
+
|
|
1030
|
+
try {
|
|
1031
|
+
const modelId = req.params.modelId;
|
|
1032
|
+
const { oldFieldName, newFieldName } = req.fields;
|
|
1033
|
+
|
|
1034
|
+
// Basic validation
|
|
1035
|
+
if (!oldFieldName || !newFieldName) {
|
|
1036
|
+
return res.status(400).json({ error: '`oldFieldName` and `newFieldName` are required.' });
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
if( ["_hash", "_id", "_model", "_user"].includes(newFieldName) ){
|
|
1040
|
+
return res.status(400).json({ error: 'Reserved field name : ' + newFieldName });
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
// Find the model
|
|
1044
|
+
const model = await modelsCollection.findOne({ _id: new ObjectId(modelId) });
|
|
1045
|
+
if (!model) {
|
|
1046
|
+
return res.status(404).json({ error: i18n.t('api.model.notFound', { model: modelId }) });
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
if( !(isDemoUser(req.me) && Config.Get("useDemoAccounts")) && isLocalUser(req.me) && (
|
|
1050
|
+
!await hasPermission(["API_ADMIN", "API_EDIT_MODEL", "API_EDIT_MODEL_"+model.name], req.me) ||
|
|
1051
|
+
await hasPermission(["API_EDIT_MODEL_NOT_"+model.name], req.me))){
|
|
1052
|
+
return res.status(403).json({success: false, error: i18n.t('api.permission.editModel')})
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
// Check if old field exists
|
|
1056
|
+
const oldField = model.fields.find(f => f.name === oldFieldName);
|
|
1057
|
+
if (!oldField) {
|
|
1058
|
+
return res.status(404).json({ error: `Field '${oldFieldName}' not found in the model.` });
|
|
1059
|
+
}
|
|
1060
|
+
const newField = model.fields.find(f => f.name === newFieldName);
|
|
1061
|
+
if (newField) {
|
|
1062
|
+
return res.status(404).json({ error: `A Field with the name '${newFieldName}' already exist in the model.` });
|
|
1063
|
+
}
|
|
1064
|
+
const result = await modelsCollection.updateOne({ _id: new ObjectId(modelId), 'fields.name': oldFieldName }, { $set: { 'fields.$.name': newFieldName } })
|
|
1065
|
+
|
|
1066
|
+
if (result.modifiedCount !== 1)
|
|
1067
|
+
return res.status(404).json({ error: i18n.t('api.model.notFound', {model: model.name})});
|
|
1068
|
+
|
|
1069
|
+
const collection = await getCollectionForUser(req.me);
|
|
1070
|
+
|
|
1071
|
+
await collection.updateMany(
|
|
1072
|
+
{ _model: model.name },
|
|
1073
|
+
{ $rename: { [oldFieldName]: newFieldName } }
|
|
1074
|
+
);
|
|
1075
|
+
|
|
1076
|
+
const set = {};
|
|
1077
|
+
model.fields.forEach(f => {
|
|
1078
|
+
if( f.name === oldFieldName ){
|
|
1079
|
+
set[f.name] = newFieldName;
|
|
1080
|
+
}
|
|
1081
|
+
})
|
|
1082
|
+
await modelsCollection.updateOne({ _id: new ObjectId(modelId) }, { $set: set });
|
|
1083
|
+
|
|
1084
|
+
modelsCache.del(req.me.username+'@@'+model.name);
|
|
1085
|
+
|
|
1086
|
+
res.json({ success: true, message: `Field '${oldFieldName}' renamed to '${newFieldName}' in model '${model.name}'.` });
|
|
1087
|
+
} catch (error) {
|
|
1088
|
+
logger.error(error);
|
|
1089
|
+
res.status(500).json({ error: 'An error occurred while renaming the field.' });
|
|
1090
|
+
}
|
|
1091
|
+
});
|
|
1092
|
+
|
|
1093
|
+
|
|
1094
|
+
// Endpoint pour calculer la valeur d'UN KPI spécifique par son ID
|
|
1095
|
+
engine.get('/api/kpis/calculate/:id', [throttle, middlewareAuthenticator, userInitiator], async (req, res) => {
|
|
1096
|
+
const { id } = req.params;
|
|
1097
|
+
|
|
1098
|
+
if (!ObjectId.isValid(id)) {
|
|
1099
|
+
return res.status(400).json({ success: false, error: 'Invalid KPI ID format' });
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
let kpiDef; // Déclarer en dehors pour e accessible dans le catch final
|
|
1103
|
+
|
|
1104
|
+
try { // <--- TRY PRINCIPAL
|
|
1105
|
+
|
|
1106
|
+
// 1. Récupérer la définition du KPI
|
|
1107
|
+
try {
|
|
1108
|
+
const coll = await getCollectionForUser(req.me);
|
|
1109
|
+
kpiDef = await coll.findOne({ _id: new ObjectId(id), _model: 'kpi', _user: req.me._user || req.me.username });
|
|
1110
|
+
if (!kpiDef) {
|
|
1111
|
+
return res.status(404).json({ success: false, error: 'KPI definition not found' });
|
|
1112
|
+
}
|
|
1113
|
+
} catch (dbError) {
|
|
1114
|
+
console.error(">>> ERREUR LORS DE findOne KPI:", dbError); // Log spécifique
|
|
1115
|
+
throw dbError; // Relancer pour être attrapé par le catch principal
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
// ---> TODO: Vérifier droits sur targetModel
|
|
1119
|
+
|
|
1120
|
+
// 2. Construire le pipeline
|
|
1121
|
+
const pipeline = [];
|
|
1122
|
+
let matchFilter = { _model: kpiDef.targetModel, $or: [{_user: req.me._user}, {_user: req.me.username}] };
|
|
1123
|
+
let totalCount = null;
|
|
1124
|
+
|
|
1125
|
+
// Calcul du totalCount
|
|
1126
|
+
if (kpiDef.showTotal || kpiDef.showPercentTotal) {
|
|
1127
|
+
try {
|
|
1128
|
+
const parsedTotalMatch = kpiDef.totalMatchFormula || {}; // Assurer un objet vide si null/undefined
|
|
1129
|
+
const totalPipeline = [
|
|
1130
|
+
{ "$match": { ...matchFilter, ...parsedTotalMatch } },
|
|
1131
|
+
{ "$count": 'count' }
|
|
1132
|
+
];
|
|
1133
|
+
const result = await datasCollection.aggregate(totalPipeline).toArray();
|
|
1134
|
+
totalCount = result.length > 0 ? result[0]['count'] : 0;
|
|
1135
|
+
} catch (totalError) {
|
|
1136
|
+
if (totalError instanceof SyntaxError) {
|
|
1137
|
+
console.error(`>>> ERREUR JSON.parse (totalMatchFormula) pour KPI ${id}:`, kpiDef.totalMatchFormula, totalError);
|
|
1138
|
+
} else {
|
|
1139
|
+
console.error(`>>> ERREUR Aggregate (totalCount) pour KPI ${id}:`, totalError);
|
|
1140
|
+
}
|
|
1141
|
+
throw totalError; // Relancer
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
// Filtre principal
|
|
1146
|
+
if (kpiDef.matchFormula) {
|
|
1147
|
+
try {
|
|
1148
|
+
const parsedMatch = kpiDef.matchFormula || {}; // Assurer un objet vide
|
|
1149
|
+
matchFilter = { ...matchFilter, ...parsedMatch };
|
|
1150
|
+
} catch (matchError) {
|
|
1151
|
+
console.error(`>>> ERREUR JSON.parse (matchFormula) pour KPI ${id}:`, kpiDef.matchFormula, matchError);
|
|
1152
|
+
throw matchError; // Relancer
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
pipeline.push({ $match: matchFilter });
|
|
1156
|
+
|
|
1157
|
+
// 3. Stage d'agrégation principal
|
|
1158
|
+
let resultValue = 0;
|
|
1159
|
+
const resultFieldName = 'calculatedValue';
|
|
1160
|
+
|
|
1161
|
+
try { // <--- Try spécifique pour l'agrégation principale
|
|
1162
|
+
if (kpiDef.aggregationType === 'count') {
|
|
1163
|
+
pipeline.push({ $count: resultFieldName });
|
|
1164
|
+
const result = await datasCollection.aggregate(pipeline).toArray();
|
|
1165
|
+
resultValue = result.length > 0 ? result[0][resultFieldName] : 0;
|
|
1166
|
+
|
|
1167
|
+
} else if (['sum', 'avg', 'min', 'max'].includes(kpiDef.aggregationType)) {
|
|
1168
|
+
if (!kpiDef.aggregationField) {
|
|
1169
|
+
return res.status(400).json({ success: false, error: `aggregationField is required for type ${kpiDef.aggregationType}` });
|
|
1170
|
+
}
|
|
1171
|
+
const mongoOperator = `$${kpiDef.aggregationType}`;
|
|
1172
|
+
const targetField = `$${kpiDef.aggregationField}`;
|
|
1173
|
+
|
|
1174
|
+
pipeline.push({
|
|
1175
|
+
$group: {
|
|
1176
|
+
_id: null,
|
|
1177
|
+
[resultFieldName]: { [mongoOperator]: targetField }
|
|
1178
|
+
}
|
|
1179
|
+
});
|
|
1180
|
+
const result = await datasCollection.aggregate(pipeline).toArray();
|
|
1181
|
+
resultValue = result.length > 0 ? result[0][resultFieldName] : 0;
|
|
1182
|
+
if (kpiDef.aggregationType === 'avg' && result.length === 0) {
|
|
1183
|
+
resultValue = 0;
|
|
1184
|
+
}
|
|
1185
|
+
} else {
|
|
1186
|
+
return res.status(400).json({ success: false, error: `Unsupported aggregationType: ${kpiDef.aggregationType}` });
|
|
1187
|
+
}
|
|
1188
|
+
} catch (aggError) {
|
|
1189
|
+
console.error(`>>> ERREUR Aggregate (principal) pour KPI ${id}:`, aggError); // Log spécifique
|
|
1190
|
+
throw aggError; // Relancer
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
// 4. Retourner la valeur
|
|
1194
|
+
res.json({ success: true, value: resultValue, totalCount: totalCount });
|
|
1195
|
+
|
|
1196
|
+
} catch (error) { // <--- CATCH PRINCIPAL
|
|
1197
|
+
|
|
1198
|
+
// Tentative de log via le logger standard (qui peut encore échouer si l'erreur est vraiment étrange)
|
|
1199
|
+
try {
|
|
1200
|
+
logger.error(`KPI Calculation Error for ID ${id} (Caught in main handler):`, error);
|
|
1201
|
+
} catch (loggerError) {
|
|
1202
|
+
console.error(">>> ERREUR DANS logger.error LUI-MEME:", loggerError);
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
// Réponse d'erreur générique
|
|
1206
|
+
const errorMsg = kpiDef ? `Internal server error during KPI calculation for '${kpiDef.name}'` : 'Internal server error during KPI calculation';
|
|
1207
|
+
res.status(500).json({ success: false, error: errorMsg });
|
|
1208
|
+
}
|
|
1209
|
+
});
|
|
1210
|
+
/**
|
|
1211
|
+
* Route: POST /api/charts/aggregate
|
|
1212
|
+
* Description: Aggregates data for chart generation based on provided configuration.
|
|
1213
|
+
* Body: {
|
|
1214
|
+
* title: string, // Chart title (not directly used in aggregation)
|
|
1215
|
+
* model: string, // The name of the model to query
|
|
1216
|
+
* type: string, // Chart type ('bar', 'line', 'pie', 'doughnut')
|
|
1217
|
+
* xAxis?: string, // Field for X-axis (for bar/line charts)
|
|
1218
|
+
* yAxis?: string, // Field for Y-axis (numeric, for bar/line charts)
|
|
1219
|
+
* groupBy?: string, // Field for grouping (enum, for pie/doughnut charts)
|
|
1220
|
+
* aggregationType?: string // Aggregation type for Y-axis ('sum', 'avg', 'count', etc.)
|
|
1221
|
+
* }
|
|
1222
|
+
* Returns: Array of aggregated data points (e.g., [{ label: '...', value: ... }]) or an error.
|
|
1223
|
+
*/
|
|
1224
|
+
|
|
1225
|
+
engine.post('/api/charts/aggregate', [throttle, middlewareAuthenticator, userInitiator, ...userMiddlewares, setTimeoutMiddleware(15000)], async (req, res) => {
|
|
1226
|
+
// --- Récupérer groupByLabelField ---
|
|
1227
|
+
const { model: modelName, type, xAxis, yAxis, groupBy, aggregationType, groupByLabelField, filter: chartFilter } = req.fields;
|
|
1228
|
+
|
|
1229
|
+
// --- Validation (inchangée) ---
|
|
1230
|
+
const isGroupingChart = ['pie', 'doughnut'].includes(type);
|
|
1231
|
+
if (!modelName || !type) {
|
|
1232
|
+
return res.status(400).json({ error: '`model` and `type` are required.' });
|
|
1233
|
+
}
|
|
1234
|
+
if (isGroupingChart && !groupBy) {
|
|
1235
|
+
return res.status(400).json({ error: '`groupBy` is required for pie/doughnut charts.' });
|
|
1236
|
+
}
|
|
1237
|
+
if (!isGroupingChart && !xAxis) {
|
|
1238
|
+
return res.status(400).json({ error: '`xAxis` is required for bar/line charts.' });
|
|
1239
|
+
}
|
|
1240
|
+
// Default aggregationType if not provided
|
|
1241
|
+
const effectiveAggregationType = aggregationType || 'count';
|
|
1242
|
+
|
|
1243
|
+
// --- MODIF: 'value' requiert aussi yAxis ---
|
|
1244
|
+
// Define requiresYAxis based on the effective aggregation type
|
|
1245
|
+
const requiresYAxis = effectiveAggregationType && !['count'].includes(effectiveAggregationType); // 'value' requires yAxis
|
|
1246
|
+
|
|
1247
|
+
// Validate yAxis presence if aggregation requires it
|
|
1248
|
+
if (requiresYAxis && !yAxis) {
|
|
1249
|
+
return res.status(400).json({ error: `\`yAxis\` is required for aggregation type '${effectiveAggregationType}'.` });
|
|
1250
|
+
}
|
|
1251
|
+
// --- FIN MODIF ---
|
|
1252
|
+
|
|
1253
|
+
|
|
1254
|
+
try {
|
|
1255
|
+
// --- Check Model Existence (inchangé) ---
|
|
1256
|
+
const modelDefinition = await modelsCollection.findOne({
|
|
1257
|
+
name: modelName,
|
|
1258
|
+
$or: [{ _user: { $exists: false } }, { _user: req.me._user || req.me.username }]
|
|
1259
|
+
});
|
|
1260
|
+
if (!modelDefinition) {
|
|
1261
|
+
return res.status(404).json({ error: `Model '${modelName}' not found or not accessible.` });
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
// --- Trouver la définition du champ groupBy ET vérifier s'il est multiple (inchangé) ---
|
|
1265
|
+
const groupByFieldDefinition = modelDefinition.fields.find(f => f.name === groupBy);
|
|
1266
|
+
const isRelationGroupBy = isGroupingChart && groupByFieldDefinition?.type === 'relation';
|
|
1267
|
+
const isMultipleRelation = isRelationGroupBy && groupByFieldDefinition?.multiple === true;
|
|
1268
|
+
|
|
1269
|
+
// --- Build Aggregation Pipeline ---
|
|
1270
|
+
const collection = await getCollectionForUser(req.me);
|
|
1271
|
+
|
|
1272
|
+
// --- MODIFICATION ICI pour inclure chartFilter ---
|
|
1273
|
+
let initialMatchStage = { $and : [{
|
|
1274
|
+
_model: modelName},{
|
|
1275
|
+
_user: req.me.username
|
|
1276
|
+
}]};
|
|
1277
|
+
if (chartFilter && typeof chartFilter === 'object' && Object.keys(chartFilter).length > 0) {
|
|
1278
|
+
// Fusionner le filtre fourni avec le filtre de base
|
|
1279
|
+
// Assurez-vous que chartFilter est bien "sanitized" ou construit de manière sécurisée
|
|
1280
|
+
// pour éviter les injections NoSQL si une partie est générée dynamiquement.
|
|
1281
|
+
// La fonction cleanFilter que vous avez pourrait être utile ici si le filtre vient d'une UI complexe.
|
|
1282
|
+
initialMatchStage = { $and: [...initialMatchStage['$and'], { $expr: chartFilter }] };
|
|
1283
|
+
logger.debug(`[charts/aggregate] Applying custom filter:`, util.inspect(initialMatchStage, false, 8, true));
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
|
|
1287
|
+
const pipeline = [{ $match: initialMatchStage }];
|
|
1288
|
+
|
|
1289
|
+
// --- Validation des opérateurs d'agrégation ---
|
|
1290
|
+
const validAggregations = {
|
|
1291
|
+
// count is handled separately by $sum: 1
|
|
1292
|
+
sum: '$sum',
|
|
1293
|
+
avg: '$avg',
|
|
1294
|
+
min: '$min',
|
|
1295
|
+
max: '$max'
|
|
1296
|
+
// median needs special handling later
|
|
1297
|
+
// *** AJOUT: 'value' sera géré par $first (ou $last) ***
|
|
1298
|
+
};
|
|
1299
|
+
const mongoAggregationOperator = validAggregations[effectiveAggregationType];
|
|
1300
|
+
|
|
1301
|
+
// *** MODIF: Mettre à jour la vérification pour accepter 'value' ***
|
|
1302
|
+
if (effectiveAggregationType && !mongoAggregationOperator && !['median', 'value', 'count'].includes(effectiveAggregationType)) {
|
|
1303
|
+
return res.status(400).json({ error: `Unsupported aggregationType: ${effectiveAggregationType}` });
|
|
1304
|
+
}
|
|
1305
|
+
// La validation de yAxis est déjà faite plus haut avec requiresYAxis
|
|
1306
|
+
|
|
1307
|
+
|
|
1308
|
+
// --- Construction du stage $group ---
|
|
1309
|
+
let groupStage = { _id: null, value: {} };
|
|
1310
|
+
let idFieldForGrouping = null;
|
|
1311
|
+
|
|
1312
|
+
if (isGroupingChart) {
|
|
1313
|
+
// --- Logique pour Pie/Doughnut (Relation ou autre) ---
|
|
1314
|
+
if (isRelationGroupBy) {
|
|
1315
|
+
// ... (logique existante pour gérer les relations simples et multiples, inchangée) ...
|
|
1316
|
+
// --- LOGIQUE EXISTANTE POUR RELATION SIMPLE/MULTIPLE ---
|
|
1317
|
+
const relatedModelName = groupByFieldDefinition.relation;
|
|
1318
|
+
if (!relatedModelName) { return res.status(400).json({ error: `Relation model not defined for groupBy field '${groupBy}'.` }); }
|
|
1319
|
+
const relatedModelDef = await modelsCollection.findOne({ name: relatedModelName, $or: [{ _user: { $exists: false } }, { _user: req.me._user || req.me.username }] });
|
|
1320
|
+
if (!relatedModelDef && groupByLabelField) { logger.warn(`[charts/aggregate] Related model '${relatedModelName}' not found, but proceeding with specified groupByLabelField '${groupByLabelField}'.`); }
|
|
1321
|
+
else if (!relatedModelDef && !groupByLabelField) { return res.status(400).json({ error: `Related model '${relatedModelName}' not found and no groupByLabelField specified.` }); }
|
|
1322
|
+
let labelField = groupByLabelField;
|
|
1323
|
+
if (!labelField && relatedModelDef) {
|
|
1324
|
+
const defaultLabelField = relatedModelDef.fields.find(f => f.asMain && ['string', 'string_t', 'enum'].includes(f.type))?.name || relatedModelDef.fields.find(f => f.name === 'name' && ['string', 'string_t', 'enum'].includes(f.type))?.name || relatedModelDef.fields.find(f => f.name === 'title' && ['string', 'string_t', 'enum'].includes(f.type))?.name;
|
|
1325
|
+
if (defaultLabelField) { labelField = defaultLabelField; logger.debug(`[charts/aggregate] Using default label field '${labelField}' for relation '${relatedModelName}'.`); }
|
|
1326
|
+
}
|
|
1327
|
+
if (!labelField) labelField = '_id';
|
|
1328
|
+
|
|
1329
|
+
if (isMultipleRelation) {
|
|
1330
|
+
pipeline.push({ $unwind: { path: `$${groupBy}`, preserveNullAndEmptyArrays: true } });
|
|
1331
|
+
pipeline.push({ $addFields: { [`${groupBy}_oid`]: { $cond: { if: { $eq: [{ $type: `$${groupBy}` }, "string"] }, then: { $toObjectId: `$${groupBy}` }, else: `$${groupBy}` } } } });
|
|
1332
|
+
pipeline.push({ $lookup: { from: collection.collectionName, localField: `${groupBy}_oid`, foreignField: '_id', as: 'relatedDoc' } });
|
|
1333
|
+
pipeline.push({ $unwind: { path: '$relatedDoc', preserveNullAndEmptyArrays: true } });
|
|
1334
|
+
idFieldForGrouping = { $ifNull: [`$relatedDoc.${labelField}`, null] };
|
|
1335
|
+
} else {
|
|
1336
|
+
pipeline.push({ $addFields: { [`${groupBy}_oid`]: { $cond: { if: { $eq: [{ $type: `$${groupBy}` }, "string"] }, then: { $toObjectId: `$${groupBy}` }, else: `$${groupBy}` } } } });
|
|
1337
|
+
pipeline.push({ $lookup: { from: collection.collectionName, localField: `${groupBy}_oid`, foreignField: '_id', as: 'relatedDoc' } });
|
|
1338
|
+
pipeline.push({ $unwind: { path: '$relatedDoc', preserveNullAndEmptyArrays: true } });
|
|
1339
|
+
idFieldForGrouping = { $ifNull: [`$relatedDoc.${labelField}`, null] };
|
|
1340
|
+
}
|
|
1341
|
+
// --- FIN LOGIQUE RELATION ---
|
|
1342
|
+
} else {
|
|
1343
|
+
// --- Logique pour Enum/String/Date etc. (inchangée) ---
|
|
1344
|
+
idFieldForGrouping = `$${groupBy}`;
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
// --- Définir l'opération d'agrégation pour la valeur ---
|
|
1348
|
+
if (effectiveAggregationType === 'count') {
|
|
1349
|
+
groupStage.value = { '$sum': 1 };
|
|
1350
|
+
}
|
|
1351
|
+
// *** AJOUT: Gérer le cas 'value' ***
|
|
1352
|
+
else if (effectiveAggregationType === 'value') {
|
|
1353
|
+
// Utiliser $first pour prendre la valeur du champ yAxis du premier document du groupe.
|
|
1354
|
+
// Note: Si l'ordre est important, un $sort peut être nécessaire AVANT le $group.
|
|
1355
|
+
groupStage.value = { $first: `$${yAxis}` };
|
|
1356
|
+
}
|
|
1357
|
+
// *** FIN AJOUT ***
|
|
1358
|
+
else if (requiresYAxis) { // Pour sum, avg, min, max
|
|
1359
|
+
// Convertir yAxis en nombre avant l'agrégation
|
|
1360
|
+
pipeline.push({
|
|
1361
|
+
$addFields: { numericYValue: { $cond: { if: { $ne: [`$${yAxis}`, null] }, then: { $toDouble: `$${yAxis}` }, else: null } } }
|
|
1362
|
+
});
|
|
1363
|
+
groupStage.value = { [mongoAggregationOperator]: '$numericYValue' };
|
|
1364
|
+
}
|
|
1365
|
+
// Note: 'median' n'est pas géré pour les graphiques de groupement dans ce code
|
|
1366
|
+
|
|
1367
|
+
} else {
|
|
1368
|
+
// --- Logique pour Bar/Line ---
|
|
1369
|
+
idFieldForGrouping = `$${xAxis}`;
|
|
1370
|
+
|
|
1371
|
+
if (effectiveAggregationType === 'count') {
|
|
1372
|
+
groupStage.value = { '$sum': 1 };
|
|
1373
|
+
}
|
|
1374
|
+
else if (effectiveAggregationType === 'value') {
|
|
1375
|
+
// Utiliser $first pour prendre la valeur du champ yAxis du premier document du groupe.
|
|
1376
|
+
groupStage.value = { $first: `$${yAxis}` };
|
|
1377
|
+
// Note: Si l'ordre est important (ex: dernier point d'une série temporelle),
|
|
1378
|
+
// un $sort sur le champ date/heure AVANT $group et utiliser $last ici serait nécessaire.
|
|
1379
|
+
}
|
|
1380
|
+
else if (requiresYAxis) { // Pour sum, avg, min, max, median
|
|
1381
|
+
pipeline.push({
|
|
1382
|
+
$addFields: { numericYValue: { $cond: { if: { $ne: [`$${yAxis}`, null] }, then: { $toDouble: `$${yAxis}` }, else: null } } }
|
|
1383
|
+
});
|
|
1384
|
+
|
|
1385
|
+
if (effectiveAggregationType === 'median') {
|
|
1386
|
+
groupStage.valuesForMedian = { $push: '$numericYValue' }; // Collecter pour median
|
|
1387
|
+
delete groupStage.value; // Supprimer le placeholder 'value'
|
|
1388
|
+
} else {
|
|
1389
|
+
groupStage.value = { [mongoAggregationOperator]: '$numericYValue' };
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
// --- Assembler et exécuter le pipeline ---
|
|
1395
|
+
groupStage._id = idFieldForGrouping; // Assigner le champ de groupement
|
|
1396
|
+
pipeline.push({ $group: groupStage });
|
|
1397
|
+
|
|
1398
|
+
// --- Handle Median Calculation (if applicable, inchangé) ---
|
|
1399
|
+
if (!isGroupingChart && effectiveAggregationType === 'median') {
|
|
1400
|
+
// ... (logique existante pour calculer la médiane après le $group, inchangée) ...
|
|
1401
|
+
pipeline.push(
|
|
1402
|
+
{ $project: { _id: 1, sortedValues: { $sortArray: { input: "$valuesForMedian", sortBy: 1 } } } },
|
|
1403
|
+
{ $addFields: { count: { $size: "$sortedValues" }, midIndex: { $floor: { $divide: [{ $subtract: [{ $size: "$sortedValues" }, 1] }, 2] } } } },
|
|
1404
|
+
{ $addFields: { value: { $cond: { if: { $eq: ["$count", 0] }, then: 0, else: { $cond: { if: { $eq: [{ $mod: ["$count", 2] }, 1] }, then: { $arrayElemAt: ["$sortedValues", "$midIndex"] }, else: { $avg: [ { $arrayElemAt: ["$sortedValues", "$midIndex"] }, { $arrayElemAt: ["$sortedValues", { $add: ["$midIndex", 1] }] } ] } } } } } } }
|
|
1405
|
+
);
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
|
|
1409
|
+
// Projection finale pour formater la sortie (inchangée)
|
|
1410
|
+
pipeline.push({
|
|
1411
|
+
$project: {
|
|
1412
|
+
_id: 0,
|
|
1413
|
+
label: { $ifNull: ['$_id', i18n.t('charts.labelNull', '(Non défini)')] },
|
|
1414
|
+
value: '$value' // Le champ 'value' contient maintenant soit l'agrégat, soit la valeur brute ($first)
|
|
1415
|
+
}
|
|
1416
|
+
});
|
|
1417
|
+
|
|
1418
|
+
// Tri final par label (inchangé)
|
|
1419
|
+
const sort = typeof(req.query.sort) === 'string' ? req.query.sort : { _id: -1 };
|
|
1420
|
+
pipeline.push({ $sort: sort });
|
|
1421
|
+
pipeline.push({ $limit: 500 });
|
|
1422
|
+
|
|
1423
|
+
console.log("[charts/aggregate] Pipeline:", util.inspect(pipeline, false, 8, true)); // Garder ce log pour vérifier
|
|
1424
|
+
|
|
1425
|
+
// --- Execute Aggregation ---
|
|
1426
|
+
const results = await collection.aggregate(pipeline).toArray();
|
|
1427
|
+
// --- Send Response ---
|
|
1428
|
+
res.json(results);
|
|
1429
|
+
|
|
1430
|
+
} catch (error) {
|
|
1431
|
+
// ... (gestion des erreurs existante, inchangée) ...
|
|
1432
|
+
console.error("Error during chart aggregation:", error);
|
|
1433
|
+
logger.error("Error during chart aggregation:", error);
|
|
1434
|
+
if (error.message.includes('relation model not defined')) { return res.status(400).json({ error: error.message }); }
|
|
1435
|
+
if (error.codeName === 'TypeMismatch' || error.message.includes('$toDouble') || error.message.includes('must be numeric')) {
|
|
1436
|
+
// *** MODIF: S'assurer que yAxis est bien le champ problématique pour 'value' ***
|
|
1437
|
+
const problematicField = requiresYAxis ? yAxis : (isGroupingChart ? groupBy : xAxis);
|
|
1438
|
+
return res.status(400).json({ error: `Field type mismatch during aggregation. Ensure '${problematicField}' contains compatible data for the operation. Details: ${error.message}` });
|
|
1439
|
+
}
|
|
1440
|
+
res.status(500).json({ error: 'An error occurred during data aggregation.' });
|
|
1441
|
+
}
|
|
1442
|
+
});
|
|
1443
|
+
|
|
1444
|
+
|
|
1445
|
+
engine.post('/api/data/removeFromPack', [throttle, middlewareAuthenticator, userInitiator, ...userMiddlewares], async (req, res) => {
|
|
1446
|
+
if( !(isDemoUser(req.me) && Config.Get("useDemoAccounts")) && isLocalUser(req.me) && !await hasPermission(["API_ADMIN", "API_CREATE_PACK"], req.me)){
|
|
1447
|
+
return res.status(403).json({success: false, error: i18n.t('api.permission.createPack')})
|
|
1448
|
+
}
|
|
1449
|
+
const { itemIds } = req.fields;
|
|
1450
|
+
if (!Array.isArray(itemIds) || itemIds.length === 0 || !itemIds.every(isObjectId)) { // Assurez-vous que isObjectId est importé/défini
|
|
1451
|
+
return res.status(400).json({ success: false, error: 'itemIds must be a non-empty array of valid ObjectIds.' });
|
|
1452
|
+
}
|
|
1453
|
+
const objectIds = itemIds.map(id => new ObjectId(id));
|
|
1454
|
+
const collection = await getCollectionForUser(req.me); // Obtenir la collection de l'utilisateur
|
|
1455
|
+
|
|
1456
|
+
const results = await collection.find({
|
|
1457
|
+
_id: { $in: objectIds },
|
|
1458
|
+
_user: req.me._user || req.me.username
|
|
1459
|
+
}).toArray();
|
|
1460
|
+
const result = await collection.deleteMany(
|
|
1461
|
+
{
|
|
1462
|
+
_hash: { $in: results.map(r => r._hash) },
|
|
1463
|
+
_pack: { $exists: true },
|
|
1464
|
+
_user: req.me._user || req.me.username
|
|
1465
|
+
}
|
|
1466
|
+
);
|
|
1467
|
+
|
|
1468
|
+
if (result.deletedCount > 0) {
|
|
1469
|
+
res.json({ success: true, modifiedCount: result.deletedCount });
|
|
1470
|
+
} else {
|
|
1471
|
+
// Gérer le cas où aucun document n'a été modifié (peut-être qu'ils n'avaient pas de _pack)
|
|
1472
|
+
res.json({ success: true, modifiedCount: 0, message: "No items found or modified." });
|
|
1473
|
+
}
|
|
1474
|
+
})
|
|
1475
|
+
|
|
1476
|
+
// GET /api/packs - Liste les packs pour la galerie depuis la collection "packs"
|
|
1477
|
+
engine.get('/api/packs', [throttle, middlewareAuthenticator, userInitiator], async (req, res) => {
|
|
1478
|
+
try {
|
|
1479
|
+
// On accède directement à la collection 'packs'
|
|
1480
|
+
const packsCollection = getCollection('packs');
|
|
1481
|
+
const { sortBy = '_updatedAt', order = -1 } = req.query; // Ajout du tri pour la galerie
|
|
1482
|
+
|
|
1483
|
+
const sortOptions = {};
|
|
1484
|
+
// Validation simple du champ de tri pour la sécurité
|
|
1485
|
+
if (['name', 'stars', '_createdAt', '_updatedAt'].includes(sortBy)) {
|
|
1486
|
+
sortOptions[sortBy] = parseInt(order, 10) === 1 ? 1 : -1;
|
|
1487
|
+
} else {
|
|
1488
|
+
sortOptions['_updatedAt'] = -1; // Tri par défaut
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
// On ne renvoie pas le champ 'data' pour alléger la réponse de la liste
|
|
1492
|
+
const packs = await packsCollection.find({
|
|
1493
|
+
_user: req.query.user ? req.query.user : { $exists: false }
|
|
1494
|
+
}, {
|
|
1495
|
+
projection: { data: 0 }
|
|
1496
|
+
}).sort(sortOptions).toArray();
|
|
1497
|
+
|
|
1498
|
+
res.json(packs);
|
|
1499
|
+
} catch (error) {
|
|
1500
|
+
logger.error('[GET /api/packs] Error fetching packs:', error);
|
|
1501
|
+
res.status(500).json({ success: false, error: 'Failed to fetch packs.' });
|
|
1502
|
+
}
|
|
1503
|
+
});
|
|
1504
|
+
|
|
1505
|
+
// GET /api/packs/:id - Récupère les détails complets d'un pack
|
|
1506
|
+
engine.get('/api/packs/:id', [throttle, middlewareAuthenticator, userInitiator], async (req, res) => {
|
|
1507
|
+
const { id } = req.params;
|
|
1508
|
+
if (!isObjectId(id)) {
|
|
1509
|
+
return res.status(400).json({ success: false, error: 'Invalid pack ID format.' });
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
try {
|
|
1513
|
+
const packsCollection = getCollection('packs');
|
|
1514
|
+
const pack = await packsCollection.findOne({ _id: new ObjectId(id) });
|
|
1515
|
+
|
|
1516
|
+
if (!pack) {
|
|
1517
|
+
return res.status(404).json({ success: false, error: 'Pack not found.' });
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
const PACK_PREVIEW_LIMIT = Config.Get('maxPackPreviewData', maxPackPreviewData);
|
|
1521
|
+
|
|
1522
|
+
const countPackEntries = (p) => {
|
|
1523
|
+
if (!p || !p.data) return 0;
|
|
1524
|
+
let totalEntries = 0;
|
|
1525
|
+
for (const langKey in p.data) {
|
|
1526
|
+
const langData = p.data[langKey];
|
|
1527
|
+
for (const modelKey in langData) {
|
|
1528
|
+
if (Array.isArray(langData[modelKey])) {
|
|
1529
|
+
totalEntries += langData[modelKey].length;
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
return totalEntries;
|
|
1534
|
+
};
|
|
1535
|
+
|
|
1536
|
+
const totalEntries = countPackEntries(pack);
|
|
1537
|
+
|
|
1538
|
+
if (totalEntries > PACK_PREVIEW_LIMIT) {
|
|
1539
|
+
logger.warn(`[GET /api/packs/${id}] Pack data is too large (${totalEntries} entries) and will be truncated for the client.`);
|
|
1540
|
+
const packForClient = { ...pack };
|
|
1541
|
+
delete packForClient.data;
|
|
1542
|
+
packForClient.dataTruncated = true;
|
|
1543
|
+
packForClient.totalDataEntries = totalEntries;
|
|
1544
|
+
return res.json(packForClient);
|
|
1545
|
+
}
|
|
1546
|
+
res.json(pack); // On retourne le pack complet si sa taille est raisonnable
|
|
1547
|
+
} catch (error) {
|
|
1548
|
+
logger.error(`[GET /api/packs/${id}] Error fetching pack details:`, error);
|
|
1549
|
+
res.status(500).json({ success: false, error: 'Failed to fetch pack details.' });
|
|
1550
|
+
}
|
|
1551
|
+
});
|
|
1552
|
+
|
|
1553
|
+
// --- NOUVEL ENDPOINT POUR METTRE À JOUR UN PACK (ex: public/privé) ---
|
|
1554
|
+
engine.patch('/api/packs/:id', [throttle, middlewareAuthenticator, userInitiator], async (req, res) => {
|
|
1555
|
+
const { id } = req.params;
|
|
1556
|
+
const { private:pr } = req.fields; // Le front-end enverra { isPrivate: true/false }
|
|
1557
|
+
const user = req.me;
|
|
1558
|
+
|
|
1559
|
+
if (!isObjectId(id)) {
|
|
1560
|
+
return res.status(400).json({ success: false, error: 'Invalid pack ID format.' });
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
if (typeof pr !== 'boolean') {
|
|
1564
|
+
return res.status(400).json({ success: false, error: 'A boolean `private` field is required.' });
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
try {
|
|
1568
|
+
const packsCollection = getCollection('packs');
|
|
1569
|
+
const pack = await packsCollection.findOne({ _id: new ObjectId(id) });
|
|
1570
|
+
|
|
1571
|
+
if (!pack) {
|
|
1572
|
+
return res.status(404).json({ success: false, error: 'Pack not found.' });
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
// Vérification des permissions : seul le propriétaire peut modifier
|
|
1576
|
+
if (pack._user !== user.username) {
|
|
1577
|
+
return res.status(403).json({ success: false, error: 'You do not have permission to edit this pack.' });
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1580
|
+
await packsCollection.updateOne(
|
|
1581
|
+
{ _id: new ObjectId(id) },
|
|
1582
|
+
{ $set: { private: pr, _updatedAt: new Date() } } // Mettre à jour le statut et la date de modification
|
|
1583
|
+
);
|
|
1584
|
+
|
|
1585
|
+
logger.info("Pack updated successfully.", pr);
|
|
1586
|
+
|
|
1587
|
+
const updatedPack = await packsCollection.findOne({ _id: new ObjectId(id) });
|
|
1588
|
+
res.json({ success: true, pack: updatedPack });
|
|
1589
|
+
|
|
1590
|
+
} catch (error) {
|
|
1591
|
+
logger.error(`[PATCH /api/packs/${id}] Error updating pack:`, error);
|
|
1592
|
+
res.status(500).json({ success: false, error: 'Failed to update pack.' });
|
|
1593
|
+
}
|
|
1594
|
+
});
|
|
1595
|
+
|
|
1596
|
+
// --- Endpoint DELETE pour supprimer un pack ---
|
|
1597
|
+
engine.delete('/api/packs/:packName', [throttle, middlewareAuthenticator, userInitiator], async (req, res) => {
|
|
1598
|
+
const { packName } = req.params;
|
|
1599
|
+
const { model } = req.query; // Récupmodèle depuis les query params
|
|
1600
|
+
const user = req.me;
|
|
1601
|
+
|
|
1602
|
+
// --- Validation ---
|
|
1603
|
+
if (!packName) {
|
|
1604
|
+
return res.status(400).json({ success: false, error: 'Pack name is required in URL path.' });
|
|
1605
|
+
}
|
|
1606
|
+
if (!model) {
|
|
1607
|
+
return res.status(400).json({ success: false, error: 'Model query parameter is required.' });
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
try {
|
|
1611
|
+
// --- Vérification des permissions (Adaptez selon vos règles) ---
|
|
1612
|
+
// Exemple : Seul l'admin ou le propriétaire peut supprimer
|
|
1613
|
+
const isAdmin = await hasPermission(["API_ADMIN", "API_DELETE_PACK"], user); // Assurez-vous que hasPermission est bien défini et fonctionnel
|
|
1614
|
+
const isOwner = true; // Pour cet exemple, on suppose que l'utilisateur est propriétaire. Adaptez la logique si nécessaire.
|
|
1615
|
+
|
|
1616
|
+
if (user.username !== 'demo' && !isAdmin && !isOwner) { // Adaptez cette condition
|
|
1617
|
+
return res.status(403).json({ success: false, error: i18n.t('api.permission.deletePack', 'Permission denied to delete this pack.') });
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
// --- Logique de suppression ---
|
|
1621
|
+
const collection = await getCollectionForUser(user); // Obtenir la collection spécifique à l'utilisateur
|
|
1622
|
+
|
|
1623
|
+
const deleteFilter = {
|
|
1624
|
+
_pack: packName,
|
|
1625
|
+
_model: model,
|
|
1626
|
+
_user: user.username // Assurer que l'utilisateur ne supprime que ses propres données de pack
|
|
1627
|
+
};
|
|
1628
|
+
|
|
1629
|
+
const result = await collection.deleteMany(deleteFilter);
|
|
1630
|
+
|
|
1631
|
+
if (result.deletedCount > 0) {
|
|
1632
|
+
logger.info(`User ${user.username} deleted ${result.deletedCount} items from pack '${packName}' for model '${model}'.`);
|
|
1633
|
+
res.json({ success: true, deletedCount: result.deletedCount });
|
|
1634
|
+
} else {
|
|
1635
|
+
logger.warn(`User ${user.username} tried to delete pack '${packName}' for model '${model}', but no matching items found or deletion failed.`);
|
|
1636
|
+
// Renvoyer succès même si rien n'a été trouvé peut être acceptable
|
|
1637
|
+
res.json({ success: true, deletedCount: 0, message: 'No items found for this pack and model belonging to the user.' });
|
|
1638
|
+
// Ou renvoyer une erreur 404 si le pack n'existe pas du tout pour cet utilisateur/modèle
|
|
1639
|
+
// return res.status(404).json({ success: false, error: 'Pack not found for this user and model.' });
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1642
|
+
} catch (error) {
|
|
1643
|
+
logger.error(`Error deleting pack '${packName}' for model '${model}' by user ${user.username}:`, error);
|
|
1644
|
+
res.status(500).json({ success: false, error: 'An internal server error occurred.' });
|
|
1645
|
+
}
|
|
1646
|
+
});
|
|
1647
|
+
|
|
1648
|
+
engine.post('/api/data/addToPack', [throttle, middlewareAuthenticator, userInitiator,...userMiddlewares], async (req, res) => {
|
|
1649
|
+
const { packName, itemIds } = req.fields;
|
|
1650
|
+
const user = req.me;
|
|
1651
|
+
|
|
1652
|
+
if( !(isDemoUser(req.me) && Config.Get("useDemoAccounts")) && isLocalUser(req.me) && !await hasPermission(["API_ADMIN", "API_CREATE_PACK"], req.me)){
|
|
1653
|
+
return res.status(403).json({success: false, error: i18n.t('api.permission.createPack')})
|
|
1654
|
+
}
|
|
1655
|
+
// --- Validation ---
|
|
1656
|
+
if (!packName || typeof packName !== 'string' || packName.trim() === '') {
|
|
1657
|
+
return res.status(400).json({ success: false, error: 'Pack name is required and must be a non-empty string.' });
|
|
1658
|
+
}
|
|
1659
|
+
if (!Array.isArray(itemIds) || itemIds.length === 0 || !itemIds.every(isObjectId)) { // Assurez-vous que isObjectId est importé/défini
|
|
1660
|
+
return res.status(400).json({ success: false, error: 'itemIds must be a non-empty array of valid ObjectIds.' });
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
// --- Logique Métier ---
|
|
1664
|
+
try {
|
|
1665
|
+
const collection = await getCollectionForUser(user); // Récupère la collection de l'utilisateur
|
|
1666
|
+
const objectIds = itemIds.map(id => new ObjectId(id)); // Convertit les strings en ObjectIds
|
|
1667
|
+
|
|
1668
|
+
// Met à jour le champ _pack pour les documents sélectionnés appartenant à l'utilisateur
|
|
1669
|
+
const copyData = await collection.find({
|
|
1670
|
+
_id: { $in: objectIds },
|
|
1671
|
+
_user: user.username
|
|
1672
|
+
}).toArray();
|
|
1673
|
+
|
|
1674
|
+
|
|
1675
|
+
const result = await collection.insertMany(copyData.map(c => ({...c, _id: undefined, _pack: packName})));
|
|
1676
|
+
res.json({ success: true, modifiedCount: result.insertedCount, matchedCount: result.insertedCount });
|
|
1677
|
+
|
|
1678
|
+
} catch (error) {
|
|
1679
|
+
logger.error(`Error adding items to pack for user ${user.username}, pack '${packName}':`, error);
|
|
1680
|
+
res.status(500).json({ success: false, error: 'An internal server error occurred.' });
|
|
1681
|
+
}
|
|
1682
|
+
});
|
|
1683
|
+
|
|
1684
|
+
|
|
1685
|
+
// --- NOUVEL ENDPOINT D'INSTALLATION DE PACK ---
|
|
1686
|
+
engine.post('/api/packs/:id/install', [throttle, middlewareAuthenticator, userInitiator, setTimeoutMiddleware(60000)], async (req, res) => {
|
|
1687
|
+
const { id } = req.params;
|
|
1688
|
+
const user = req.me;
|
|
1689
|
+
const lang = req.query.lang;
|
|
1690
|
+
|
|
1691
|
+
if (!isObjectId(id)) {
|
|
1692
|
+
return res.status(400).json({ success: false, error: 'Invalid pack ID format.' });
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
try {
|
|
1696
|
+
// Vérification des permissions
|
|
1697
|
+
if (user.username !== 'demo' && isLocalUser(user) && !await hasPermission(["API_ADMIN", "API_INSTALL_PACK"], user)) {
|
|
1698
|
+
return res.status(403).json({ success: false, error: i18n.t('api.permission.installPack') });
|
|
1699
|
+
}
|
|
1700
|
+
|
|
1701
|
+
const result = await installPack(id, user, lang);
|
|
1702
|
+
|
|
1703
|
+
if (result.success) {
|
|
1704
|
+
res.status(200).json({ success: true, message: `Pack installed successfully.`, summary: result.summary });
|
|
1705
|
+
} else if (!result.success && !result.modifiedCount) {
|
|
1706
|
+
res.status(200).json({ success: true, message: `No data to insert.`, summary: result.summary });
|
|
1707
|
+
} else {
|
|
1708
|
+
res.status(400).json({ success: false, error: 'Pack installation had errors.', errors: result.errors, summary: result.summary });
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1711
|
+
} catch (error) {
|
|
1712
|
+
logger.error(`[POST /api/packs/${id}/install] Critical error:`, error);
|
|
1713
|
+
res.status(500).json({ success: false, error: error.message || 'An internal server error occurred.' });
|
|
1714
|
+
}
|
|
1715
|
+
});
|
|
1716
|
+
engine.post('/api/packs/install', [throttle, middlewareAuthenticator, userInitiator, setTimeoutMiddleware(60000)], async (req, res) => {
|
|
1717
|
+
const { id } = req.params;
|
|
1718
|
+
const user = req.me;
|
|
1719
|
+
const lang = req.query.lang || req.fields.lang;
|
|
1720
|
+
|
|
1721
|
+
const packName = req.fields.packName || null;
|
|
1722
|
+
|
|
1723
|
+
try {
|
|
1724
|
+
// Vérification des permissions
|
|
1725
|
+
if (!isDemoUser(user) && isLocalUser(user) && !await hasPermission(["API_ADMIN", "API_INSTALL_PACK"], user)) {
|
|
1726
|
+
return res.status(403).json({ success: false, error: i18n.t('api.permission.installPack') });
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
const result = await installPack(packName? packName : {...req.fields.packData, private: true}, user, lang, { installForUser: true });
|
|
1730
|
+
|
|
1731
|
+
if (result.success) {
|
|
1732
|
+
res.status(200).json({ success: true, message: `Pack installed successfully.`, summary: result.summary });
|
|
1733
|
+
} else if (!result.success && !result.modifiedCount) {
|
|
1734
|
+
res.status(200).json({ success: true, message: `No data to insert.`, summary: result.summary });
|
|
1735
|
+
} else {
|
|
1736
|
+
res.status(400).json({ success: false, error: 'Pack installation had errors.', errors: result.errors, summary: result.summary });
|
|
1737
|
+
}
|
|
1738
|
+
|
|
1739
|
+
} catch (error) {
|
|
1740
|
+
logger.error(`[POST /api/packs/${id}/install] Critical error:`, error);
|
|
1741
|
+
res.status(500).json({ success: false, error: error.message || 'An internal server error occurred.' });
|
|
1742
|
+
}
|
|
1743
|
+
});
|
|
1744
|
+
/*
|
|
1745
|
+
engine.post('/api/packs/install', [throttle, middlewareAuthenticator, userInitiator, ...userMiddlewares], async (req, res) => {
|
|
1746
|
+
|
|
1747
|
+
const { pack } = req.fields;
|
|
1748
|
+
const initialModelName = req.query.model; // The starting model
|
|
1749
|
+
const user = req.me;
|
|
1750
|
+
const collection = await getCollectionForUser(user);
|
|
1751
|
+
|
|
1752
|
+
try {
|
|
1753
|
+
// --- Permission Check ---
|
|
1754
|
+
if (user.username !== 'demo' && isLocalUser(user) && !await hasPermission(["API_ADMIN", "API_INSTALL_PACK"], user)) {
|
|
1755
|
+
return res.status(403).json({ success: false, error: i18n.t('api.permission.installPack') });
|
|
1756
|
+
}
|
|
1757
|
+
|
|
1758
|
+
// --- Input Validation ---
|
|
1759
|
+
if (!pack) { return res.status(400).json({ success: false, error: 'Pack name is required.' }); }
|
|
1760
|
+
if (!initialModelName) { return res.status(400).json({ success: false, error: 'Model name query parameter is required.' }); }
|
|
1761
|
+
|
|
1762
|
+
// --- Initialize Shared State ---
|
|
1763
|
+
const idMap = {}; // Shared map: oldObjectIdString -> newObjectId
|
|
1764
|
+
const processedModels = new Set(); // Shared set to track processed models and detect cycles
|
|
1765
|
+
|
|
1766
|
+
logger.info(`--- Starting Recursive Pack Installation --- User: ${user.username}, Pack: '${pack}', Initial Model: '${initialModelName}'`);
|
|
1767
|
+
|
|
1768
|
+
|
|
1769
|
+
// --- Start Recursive Installation ---
|
|
1770
|
+
await installPack(logger, pack, initialModelName, user, collection);
|
|
1771
|
+
|
|
1772
|
+
logger.info(`--- Recursive Pack Installation Completed --- User: ${user.username}, Pack: '${pack}'. Final ID Map size: ${Object.keys(idMap).length}. Processed models: [${Array.from(processedModels).join(', ')}]`);
|
|
1773
|
+
|
|
1774
|
+
res.status(200).json({ success: true, message: `Pack '${pack}' installation process completed starting from model '${initialModelName}'.` });
|
|
1775
|
+
|
|
1776
|
+
} catch (error) {
|
|
1777
|
+
logger.error(`Critical error during pack installation for pack '${pack}', model '${initialModelName}':`, error);
|
|
1778
|
+
res.status(500).json({ success: false, error: error.message || 'An internal server error occurred during pack installation.' });
|
|
1779
|
+
}
|
|
1780
|
+
});
|
|
1781
|
+
*/
|
|
1782
|
+
|
|
1783
|
+
engine.get('/resources/:guid', [middlewareAuthenticator, userInitiator, throttle], async (req, res) => {
|
|
1784
|
+
try {
|
|
1785
|
+
const { guid } = req.params;
|
|
1786
|
+
const user = req.me;
|
|
1787
|
+
const resourceInfo = await getResource(guid, user); // Passez l'objet utilisateur
|
|
1788
|
+
res.setHeader('Content-Type', resourceInfo.mimeType);
|
|
1789
|
+
|
|
1790
|
+
if (resourceInfo.storage === 's3') {
|
|
1791
|
+
const s3Config = await getUserS3Config(user);
|
|
1792
|
+
const s3Stream = getS3Stream(s3Config, resourceInfo.s3Key);
|
|
1793
|
+
|
|
1794
|
+
s3Stream.on('error', (s3Error) => {
|
|
1795
|
+
logger.error(`S3 stream error for resource ${guid}:`, s3Error);
|
|
1796
|
+
res.status(404).json({ error: 'Resource file not found in storage.' });
|
|
1797
|
+
});
|
|
1798
|
+
|
|
1799
|
+
s3Stream.pipe(res);
|
|
1800
|
+
} else { // Stockage 'local'
|
|
1801
|
+
const fileStream = fs.createReadStream(resourceInfo.filepath);
|
|
1802
|
+
fileStream.on('error', (streamError) => {
|
|
1803
|
+
console.error(`Stream error for resource ${guid}:`, streamError); // ou logger.error
|
|
1804
|
+
res.status(404).json({error: 'Resource file not found on server.'});
|
|
1805
|
+
});
|
|
1806
|
+
fileStream.pipe(res);
|
|
1807
|
+
}
|
|
1808
|
+
|
|
1809
|
+
} catch (error) {
|
|
1810
|
+
console.error(`Error serving resource ${req.params.guid}:`, error); // ou logger.error
|
|
1811
|
+
if (error.message.includes("n'êtes pas autorisé")) {
|
|
1812
|
+
res.status(403).json({ error: 'Forbidden: You are not authorized to access this file.' });
|
|
1813
|
+
} else if (error.message.includes("non trouvé")) { // "Fichier non trouvé" ou "GUID du fichier n'est pas valide"
|
|
1814
|
+
res.status(404).json({ error: 'Not Found: The requested resource does not exist or the GUID is invalid.' });
|
|
1815
|
+
} else {
|
|
1816
|
+
res.status(500).json({ error: 'Internal server error while serving the resource.' });
|
|
1817
|
+
}
|
|
1818
|
+
}
|
|
1819
|
+
});
|
|
1820
|
+
|
|
1821
|
+
logger.info("Data module loaded");
|
|
1786
1822
|
}
|