data-primals-engine 1.5.2 → 1.6.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.
Files changed (63) hide show
  1. package/README.md +924 -915
  2. package/client/README.md +136 -136
  3. package/client/index.js +0 -1
  4. package/client/public/doc/API.postman_collection.json +213 -213
  5. package/client/public/react.svg +9 -9
  6. package/client/src/AddWidgetTypeModal.jsx +47 -47
  7. package/client/src/App.css +42 -42
  8. package/client/src/App.jsx +92 -150
  9. package/client/src/App.scss +6 -0
  10. package/client/src/AssistantChat.jsx +362 -363
  11. package/client/src/Button.jsx +12 -12
  12. package/client/src/Dashboard.jsx +480 -480
  13. package/client/src/DashboardHtmlViewItem.jsx +146 -146
  14. package/client/src/DashboardView.jsx +654 -654
  15. package/client/src/DataEditor.jsx +383 -383
  16. package/client/src/DataLayout.jsx +779 -808
  17. package/client/src/DataTable.jsx +782 -822
  18. package/client/src/DataTable.scss +186 -186
  19. package/client/src/GeolocationField.jsx +93 -93
  20. package/client/src/HistoryDialog.jsx +307 -307
  21. package/client/src/HistoryDialog.scss +319 -319
  22. package/client/src/HtmlViewBuilderModal.jsx +90 -90
  23. package/client/src/HtmlViewBuilderModal.scss +17 -17
  24. package/client/src/HtmlViewCard.jsx +43 -43
  25. package/client/src/ModelCreatorField.jsx +950 -950
  26. package/client/src/ModelList.jsx +280 -280
  27. package/client/src/Notification.jsx +136 -136
  28. package/client/src/PackGallery.jsx +391 -391
  29. package/client/src/PackGallery.scss +231 -231
  30. package/client/src/Pagination.jsx +143 -143
  31. package/client/src/RelationField.jsx +354 -354
  32. package/client/src/RelationSelectorWidget.jsx +172 -172
  33. package/client/src/RelationValue.jsx +2 -1
  34. package/client/src/contexts/CommandContext.jsx +260 -0
  35. package/client/src/contexts/ModelContext.jsx +4 -1
  36. package/client/src/contexts/UIContext.jsx +72 -72
  37. package/client/src/filter.js +263 -263
  38. package/client/src/index.css +90 -90
  39. package/package.json +6 -5
  40. package/perf/artillery-hooks.js +37 -37
  41. package/perf/setup.yml +25 -25
  42. package/src/constants.js +4 -0
  43. package/src/engine.js +335 -335
  44. package/src/events.js +232 -137
  45. package/src/filter.js +274 -274
  46. package/src/modules/assistant/assistant.js +225 -83
  47. package/src/modules/auth-google/index.js +50 -50
  48. package/src/modules/auth-microsoft/index.js +81 -81
  49. package/src/modules/data/data.core.js +118 -118
  50. package/src/modules/data/data.history.js +555 -555
  51. package/src/modules/data/data.operations.js +3401 -3381
  52. package/src/modules/data/data.relations.js +686 -686
  53. package/src/modules/data/data.routes.js +1879 -1879
  54. package/src/modules/data/data.validation.js +2 -2
  55. package/src/modules/file.js +247 -247
  56. package/src/packs.js +2 -2
  57. package/src/providers.js +2 -2
  58. package/src/services/stripe.js +196 -196
  59. package/src/sso.js +193 -193
  60. package/test/data.history.integration.test.js +264 -264
  61. package/test/data.integration.test.js +1206 -1206
  62. package/test/events.test.js +108 -10
  63. package/test/model.integration.test.js +377 -377
@@ -1,687 +1,687 @@
1
- import {getDefaultForType, getFieldValueHash} from "../../data.js";
2
- import {Event} from "../../events.js";
3
- import {getCollectionForUser, isObjectId} from "../mongodb.js";
4
- import {ObjectId} from "mongodb";
5
- import {isPlainObject, parseSafeJSON} from "../../core.js";
6
- import {dataTypes, getModel, searchData} from "./data.operations.js";
7
- import {validateModelData} from "./data.validation.js";
8
- import i18n from "../../i18n.js";
9
- import {addFile, removeFile} from "../file.js";
10
- import {Logger} from "../../gameObject.js";
11
- import NodeCache from "node-cache";
12
-
13
- let depthFilter = 0;
14
-
15
- // Création du cache avec des options configurables
16
- export const relationCache = new NodeCache({
17
- stdTTL: 3600, // TTL par défaut de 1 heure (en secondes)
18
- checkperiod: 600, // Vérification des éléments expirés toutes les 10 minutes
19
- useClones: false // Pour des performances optimales avec des ObjectId
20
- });
21
-
22
- let engine, logger;
23
- export function onInit(defaultEngine) {
24
- engine = defaultEngine;
25
- logger = engine.getComponent(Logger);
26
- }
27
-
28
- export function convertDataTypes(dataArray, modelFields, sourceType = 'csv') {
29
- return dataArray.map(record => {
30
- const convertedRecord = {...record};
31
- for (const field of modelFields) {
32
- if (convertedRecord.hasOwnProperty(field.name)) {
33
- let value = convertedRecord[field.name];
34
-
35
- // Gérer les chaînes vides pour les champs non requis
36
- if (typeof value === 'string' && value === '' && !field.required) {
37
- convertedRecord[field.name] = getDefaultForType(field);
38
- continue;
39
- }
40
- // Si la valeur est null ou undefined, on la laisse telle quelle, la validation s'en chargera
41
- if (value === null || value === undefined) {
42
- continue;
43
- }
44
-
45
- switch (field.type) {
46
- case 'number':
47
- if (typeof value !== 'number') { // Convertir si ce n'est pas déjà un nombre
48
- const num = parseFloat(value);
49
- if (!isNaN(num)) {
50
- convertedRecord[field.name] = num;
51
- } else {
52
- logger.warn(`Import: Impossible de parser le nombre pour le champ ${field.name}, valeur: ${value}. Utilisation de la valeur par défaut/null.`);
53
- convertedRecord[field.name] = getDefaultForType(field);
54
- }
55
- }
56
- break;
57
- case 'boolean':
58
- if (typeof value !== 'boolean') {
59
- convertedRecord[field.name] = ['true', '1', 'yes', 'on'].includes(String(value).toLowerCase());
60
- }
61
- break;
62
- case 'date':
63
- case 'datetime':
64
- if (String(value).toLowerCase() === 'now') {
65
- convertedRecord[field.name] = 'now';
66
- } else {
67
- const parsedDate = new Date(value);
68
- if (!isNaN(parsedDate.getTime())) {
69
- convertedRecord[field.name] = field.type === 'date' ? parsedDate.toISOString().split("T")[0] : parsedDate.toISOString();
70
- } else if (value) { // Ne pas logger si la valeur était initialement vide/null
71
- logger.warn(`Import: Impossible de parser la date pour le champ ${field.name}, valeur: ${value}. La validation ulture s'en chargera.`);
72
- }
73
- }
74
- break;
75
- case 'array':
76
- if (['csv', 'excel'].includes(sourceType) && typeof value === 'string') {
77
- const arrayValues = value.split(/[,;]/).map(item => item.trim()).filter(item => item !== '');
78
- if (field.itemsType === 'number') {
79
- convertedRecord[field.name] = arrayValues.map(v => parseFloat(v)).filter(v => !isNaN(v));
80
- } else {
81
- convertedRecord[field.name] = arrayValues;
82
- }
83
- } else if (sourceType === 'json' && typeof value === 'string') {
84
- try {
85
- const parsedArray = JSON.parse(value);
86
- if (Array.isArray(parsedArray)) {
87
- convertedRecord[field.name] = parsedArray;
88
- // TODO: Potentiellement convertir les éléments de parsedArray ici si nécessaire
89
- } else {
90
- logger.warn(`Import: La chaîne JSON pour le champ tableau ${field.name} n'a pas été parsée en tableau. Valeur: ${value}.`);
91
- }
92
- } catch (e) {
93
- logger.warn(`Import: Impossible de parser la chaîne JSON pour le champ tableau ${field.name}. Valeur: ${value}.`);
94
- }
95
- }
96
- // Si c'est déjà un tableau (cas JSON typique), on suppose que les types des éléments sont corrects
97
- // ou seront validés par pushDataUnsecure.
98
- else if (!Array.isArray(convertedRecord[field.name])) {
99
- convertedRecord[field.name] = getDefaultForType(field);
100
- }
101
- break;
102
- case 'object':
103
- if (['csv', 'excel'].includes(sourceType)) {
104
- try {
105
- convertedRecord[field.name] = JSON.parse(value);
106
- } catch (e) {
107
- logger.warn(`Import: Impossible de parser la chaîne JSON pour le champ objet ${field.name}. Valeur: ${value}.`);
108
- }
109
- }
110
- break;
111
- case 'code':
112
- if (['csv', 'excel'].includes(sourceType) && typeof value === 'string') {
113
- try {
114
- convertedRecord[field.name] = JSON.parse(value);
115
- } catch (e) {
116
- logger.warn(`Import: Impossible de parser la chaîne JSON pour le champ code (json) ${field.name}. Valeur: ${value}.`);
117
- }
118
- }
119
- break;
120
- }
121
- }
122
- }
123
- return convertedRecord;
124
- });
125
- }
126
-
127
- export const removeValue = (obj, containsKey, removeParent = false) => {
128
- // Base case: If the object is not an object or array, return it as is.
129
- if (!isPlainObject(obj) && !Array.isArray(obj)) {
130
- return obj;
131
- }
132
-
133
- for (const key in obj) {
134
- if (Object.prototype.hasOwnProperty.call(obj, key)) {
135
- if (removeParent) {
136
- const value = obj[key]?.[containsKey];
137
- if (value !== undefined) {
138
- delete obj[key];
139
- } else {
140
- removeValue(obj[key], containsKey);
141
- }
142
- } else if (containsKey === key) {
143
- delete obj[key];
144
- } else {
145
- removeValue(obj[key], containsKey);
146
- }
147
- }
148
- }
149
- return obj;
150
- };
151
- export const changeValue = (obj, keyToChange, changeFunction, excludeKeys = [], depth = 0, parentKey = '') => {
152
- if (!depth) {
153
- depthFilter = 0;
154
- }
155
- if (!isPlainObject(obj) && !Array.isArray(obj)) {
156
- return obj;
157
- }
158
-
159
- const newObj = Array.isArray(obj) ? [] : {};
160
-
161
- for (const key in obj) {
162
- if (Object.prototype.hasOwnProperty.call(obj, key)) {
163
- const topLevel = depthFilter === 0;
164
- if (key === keyToChange) {
165
- depthFilter++;
166
- }
167
- const value = obj[key];
168
- if (value instanceof RegExp) {
169
- newObj[key] = value;
170
- continue;
171
- }
172
- if (isPlainObject(value) && !excludeKeys.includes(key)) {
173
- newObj[key] = changeValue(value, keyToChange, changeFunction, excludeKeys, depth + 1, key);
174
- } else if (Array.isArray(value) && !excludeKeys.includes(key)) {
175
- newObj[key] = value.map(item => {
176
- if (isPlainObject(item)) {
177
- return changeValue(item, keyToChange, changeFunction, excludeKeys, depth + 1, key);
178
- }
179
- return item;
180
- });
181
- } else {
182
- newObj[key] = value;
183
- }
184
- if (key === keyToChange) {
185
- if (typeof changeFunction === 'function') {
186
- const newValue = changeFunction(parentKey, newObj[key], topLevel);
187
- if (newValue !== undefined) {
188
- if (isPlainObject(newValue)) {
189
- return newValue;
190
- } else {
191
- newObj[key] = newValue;
192
- }
193
- } else {
194
- //delete newObj[key];
195
-
196
- }
197
- }
198
- }
199
- }
200
- }
201
- return newObj;
202
- };
203
-
204
- export async function processDocuments(datas, model, collection, me) {
205
- const idMap = new Map();
206
- const allInsertedIds = [];
207
-
208
- const realData = await Event.Trigger("OnDataInsert", "event", "system", datas) || datas;
209
- for (const doc of realData) {
210
- try {
211
- const newDocId = await insertAndResolveRelations(doc, model, collection, me, idMap);
212
- if (newDocId) {
213
- allInsertedIds.push(newDocId.toString());
214
- }
215
- } catch (error) {
216
- // Modification clé ici : on ne catch plus les erreurs de validation
217
- throw error;
218
- }
219
- }
220
-
221
- return {allInsertedIds, idMap};
222
- }
223
-
224
- /**
225
- * Traite toutes les relations du document
226
- */
227
- export async function processRelations(docToProcess, model, collection, me, idMap) {
228
- const batchFinds = [];
229
-
230
- // Phase 1: Préparation des requêtes
231
- for (const field of model.fields) {
232
- if (field.type !== 'relation') continue;
233
-
234
- const value = docToProcess[field.name];
235
- if (value?.$find) {
236
- batchFinds.push({
237
- field: field.name,
238
- promise: searchData({
239
- filter: value.$find,
240
- limit: field.multiple ? 0 : 1,
241
- model: field.relation
242
- }, me),
243
- multiple: field.multiple
244
- });
245
- }
246
- }
247
-
248
- // Phase 2: Exécution parallèle
249
- const findResults = await Promise.all(batchFinds.map(f => f.promise));
250
-
251
- // Phase 3: Traitement des résultats
252
- findResults.forEach((result, index) => {
253
- const {field, multiple} = batchFinds[index];
254
- if (result.data?.length > 0) {
255
- // Cas où des documents sont trouvés
256
- docToProcess[field] = multiple
257
- ? result.data.map(r => r._id.toString())
258
- : result.data[0]._id.toString();
259
- } else {
260
- // Cas où AUCUN document n'est trouvé : il faut nettoyer le champ !
261
- docToProcess[field] = multiple ? [] : null;
262
- }
263
- });
264
-
265
-
266
- for (const field of model.fields) {
267
- if (field.type !== 'relation') continue;
268
-
269
- const fieldName = field.name;
270
- const relationValue = docToProcess[fieldName];
271
- if (!relationValue || typeof relationValue !== 'object') continue;
272
-
273
- const relatedModel = await getModel(field.relation, me);
274
-
275
- if (!Array.isArray(relationValue) && relationValue['$find']) {
276
-
277
- } else if (Array.isArray(relationValue)) {
278
- // Relation multiple (tableau)
279
- docToProcess[fieldName] = await processMultipleRelations(
280
- relationValue,
281
- relatedModel,
282
- collection,
283
- me,
284
- idMap
285
- );
286
- } else if (isPlainObject(relationValue)) {
287
- // Relation simple (objet)
288
- docToProcess[fieldName] = await processSingleRelation(
289
- relationValue,
290
- relatedModel,
291
- collection,
292
- me,
293
- idMap
294
- );
295
- }
296
- }
297
- }
298
-
299
- /**
300
- * Traite une relation multiple (tableau)
301
- */
302
- async function processMultipleRelations(items, relatedModel, collection, me, idMap) {
303
- const newRelationIds = await Promise.all(
304
- items.map(item => processRelationItem(item, relatedModel, collection, me, idMap))
305
- );
306
- return newRelationIds.filter(id => id).map(id => id.toString());
307
- }
308
-
309
- /**
310
- * Traite une relation simple (objet)
311
- */
312
- async function processSingleRelation(item, relatedModel, collection, me, idMap) {
313
- const newId = await processRelationItem(item, relatedModel, collection, me, idMap);
314
- return newId ? newId.toString() : null;
315
- }
316
-
317
- async function processRelationItem(item, relatedModel, collection, me, idMap) {
318
- // Cas 1: ID existant (string ou ObjectId)
319
- if (isObjectId(item) || typeof item === 'string') {
320
- const originalId = typeof item === 'string' ? item : item.toString();
321
-
322
- // Vérifier si cet ID a déjà été mappé (cas d'une référence circulaire)
323
- if (idMap.has(originalId)) {
324
- return idMap.get(originalId);
325
- }
326
-
327
- // Sinon, vérifier si l'ID existe en base
328
- const existing = await collection.findOne({
329
- _id: new ObjectId(originalId),
330
- _model: relatedModel.name,
331
- $or: [{_user: me._user || me.username}, {_user: {$exists: false}}]
332
- });
333
-
334
- if (existing) {
335
- return existing._id; // Conserver l'ID original
336
- }
337
- }
338
-
339
- // Cas 2: Objet complet à importer
340
- if (isPlainObject(item)) {
341
- const relationDoc = prepareDocument(item, relatedModel, me);
342
- applyDefaultValues(relationDoc, relatedModel);
343
-
344
- // Si l'objet a un _id, essayer de le conserver
345
- if (item._id) {
346
- const originalId = item._id.toString();
347
-
348
- // Vérifier si l'ID existe déjà en base
349
- const existing = await collection.findOne({
350
- _id: new ObjectId(originalId),
351
- _model: relatedModel.name,
352
- $or: [{_user: me._user || me.username}, {_user: {$exists: false}}]
353
- });
354
-
355
- if (existing) {
356
- return existing._id; // Utiliser l'ID existant
357
- }
358
-
359
- // Si l'ID n'existe pas encore, l'utiliser pour le nouvel insert
360
- relationDoc._id = new ObjectId(originalId);
361
- }
362
-
363
- const relationHash = relationDoc._hash;
364
- const cacheKey = `${relatedModel.name}:${relationHash}`;
365
-
366
- // Vérification dans le cache
367
- const cachedId = relationCache.get(cacheKey);
368
- if (cachedId !== undefined) {
369
- return cachedId;
370
- }
371
-
372
- // Vérification en base de données par hash
373
- const existingByHash = await collection.findOne({
374
- _hash: relationHash,
375
- _model: relatedModel.name,
376
- _user: relationDoc._user
377
- }, {projection: {_id: 1}});
378
-
379
- if (existingByHash) {
380
- relationCache.set(cacheKey, existingByHash._id);
381
- return existingByHash._id;
382
- }
383
-
384
- const newId = await insertAndResolveRelations(item, relatedModel, collection, me, idMap);
385
- relationCache.set(cacheKey, newId);
386
- return newId;
387
- }
388
-
389
- return null;
390
- }
391
-
392
- // Fonction pour vider le cache si besoin
393
- function clearRelationCache() {
394
- relationCache.flushAll();
395
- }
396
-
397
- /**
398
- * Applique les filtres de champ définis dans le modèle
399
- */
400
- async function applyFieldFilters(docToProcess, model) {
401
- for (const field of model.fields) {
402
- docToProcess[field.name] = typeof (docToProcess[field.name]) === 'undefined' || docToProcess[field.name] === null ? field.default : docToProcess[field.name];
403
- if (dataTypes[field.type]?.filter) {
404
- const filter = await dataTypes[field.type].filter(
405
- docToProcess[field.name],
406
- field
407
- );
408
- const realFilter = await Event.Trigger('OnDataFilter', "event", "system", filter, field, docToProcess);
409
- docToProcess[field.name] = realFilter || filter;
410
- }
411
- }
412
- }
413
-
414
- /**
415
- * Applique les valeurs par défaut aux champs manquants
416
- */
417
- function applyDefaultValues(doc, model) {
418
- for (const field of model.fields) {
419
- // Si le champ n'est pas défini et a une valeur par défaut
420
- if (!(field.name in doc) && 'default' in field) {
421
- doc[field.name] = typeof field.default === 'function'
422
- ? field.default()
423
- : field.default;
424
- }
425
- }
426
- }
427
-
428
- async function insertAndResolveRelations(doc, model, collection, me, idMap) {
429
- const originalId = doc._id?.toString();
430
-
431
- // Si cet ID a déjà été traité, retourner le nouvel ID mappé
432
- if (originalId && idMap.has(originalId)) {
433
- return idMap.get(originalId);
434
- }
435
-
436
- const docToProcess = prepareDocument(doc, model, me);
437
- applyDefaultValues(docToProcess, model);
438
-
439
- // Si le document a un _id original et qu'il n'existe pas encore, le conserver
440
- if (originalId && !await collection.findOne({_id: new ObjectId(originalId)})) {
441
- docToProcess._id = new ObjectId(originalId);
442
- }
443
-
444
- await validateModelData(docToProcess, model);
445
- await processRelations(docToProcess, model, collection, me, idMap);
446
- await validateModelData(docToProcess, model);
447
- await applyFieldFilters(docToProcess, model);
448
- await checkUniqueFields(docToProcess, model, collection);
449
-
450
- const existingDoc = await findExistingDocument(docToProcess, collection);
451
- if (existingDoc) {
452
- cacheDocumentId(originalId, existingDoc._id, idMap);
453
- return existingDoc._id;
454
- }
455
-
456
- for (const field of model.fields) {
457
- if (field.type === 'relation' && field.relationFilter && docToProcess[field.name]) {
458
-
459
- const relatedIds = Array.isArray(docToProcess[field.name])
460
- ? docToProcess[field.name]
461
- : [docToProcess[field.name]];
462
-
463
- // Préparer un filtre global : match si _id dans relatedIds ET respecte relationFilter
464
- const validationQuery = {
465
- $and: [
466
- {$in: ['$_id', relatedIds.map(id => ({$toObjectId: id}))]},
467
- field.relationFilter
468
- ]
469
- };
470
-
471
- const relatedDocs = await searchData({
472
- filter: validationQuery,
473
- model: field.relation,
474
- limit: relatedIds.length
475
- }, me);
476
-
477
- if ((relatedDocs?.count || 0) !== relatedIds.length) {
478
- const invalidIds = relatedIds.filter(id =>
479
- !relatedDocs.data.some(doc => doc._id.toString() === id.toString())
480
- );
481
- throw new Error(
482
- i18n.t(
483
- 'api.data.relationFilterFailed',
484
- 'Les valeurs {{values}} pour le champ {{field}} ne respectent pas le filtre de relation défini.',
485
- {field: field.name, values: invalidIds.join(', ')}
486
- )
487
- );
488
- }
489
- }
490
- }
491
-
492
-
493
- // Insertion en conservant éventuellement l'ID original
494
- const result = docToProcess._id
495
- ? await collection.insertOne(docToProcess)
496
- : await collection.insertOne(docToProcess);
497
-
498
- const insertedId = result.insertedId;
499
- cacheDocumentId(originalId, insertedId, idMap);
500
-
501
- return insertedId;
502
- }
503
-
504
- // Nouvelle fonction pour vérifier les champs uniques
505
- async function checkUniqueFields(doc, model, collection) {
506
- const uniqueFields = model.fields.filter(f => f.unique);
507
-
508
- for (const field of uniqueFields) {
509
- const value = doc[field.name];
510
- if (value === undefined || value === null) continue;
511
-
512
- const existing = await collection.findOne({
513
- [field.name]: value,
514
- _model: model.name,
515
- _user: doc._user
516
- });
517
-
518
- if (existing) {
519
- // Utilisation de i18n pour un message d'erreur standardisé
520
- throw new Error(i18n.t('api.data.duplicateValue', {field: field.name, value: value}));
521
- }
522
- }
523
- }
524
-
525
- function prepareDocument(doc, model, me) {
526
- const docToProcess = {...doc};
527
- delete docToProcess._id;
528
-
529
- // AJOUT: Nettoyage des champs non définis dans le modèle
530
- for (const key of Object.keys(docToProcess)) {
531
- if (!model.fields.some(f => f.name === key) && !key.startsWith('_')) {
532
- delete docToProcess[key];
533
- }
534
- }
535
-
536
- docToProcess._model = model.name;
537
- docToProcess._user = me._user || me.username;
538
- docToProcess._hash = getFieldValueHash(model, docToProcess);
539
-
540
- return docToProcess;
541
- }
542
-
543
- /**
544
- * Cherche un document existant par son hash
545
- */
546
- async function findExistingDocument(docToProcess, collection) {
547
- return await collection.findOne({
548
- _hash: docToProcess._hash,
549
- _model: docToProcess._model,
550
- _user: docToProcess._user
551
- });
552
- }
553
-
554
- /**
555
- * Insère le document dans la collection
556
- */
557
- async function insertDocument(docToProcess, collection) {
558
- const result = await collection.insertOne(docToProcess);
559
- return result.insertedId;
560
- }
561
-
562
- /**
563
- * Met en cache la correspondance d'ID
564
- */
565
- function cacheDocumentId(originalId, newId, idMap) {
566
- if (originalId && newId) {
567
- idMap.set(originalId, newId);
568
- }
569
- }
570
-
571
- /**
572
- * Gestion des fichiers (à implémenter selon besoins)
573
- */
574
- export async function handleFilesIfNeeded(insertedIds, files, model, collection) {
575
- // Implémentation spécifique à votre application
576
- // Ex: association des fichiers uploadés aux documents insérés
577
- }
578
-
579
- export const checkHash = async (me, model, hash, excludeId = null) => {
580
- const collection = await getCollectionForUser(me);
581
- const query = {
582
- _model: model.name,
583
- _hash: hash,
584
- ...(excludeId && {_id: {$ne: new ObjectId(excludeId)}})
585
- };
586
-
587
- console.log("Query being executed:", JSON.stringify(query, null, 2));
588
-
589
- const count = await collection.countDocuments(query);
590
- return count > 0;
591
- };
592
-
593
- // Fonctions helper
594
- export async function processFileArray(files, currentFiles, user) {
595
- const newFiles = await Promise.allSettled(
596
- Object.keys(files).map(f => files[f]).map(async (file, i) => {
597
- const oldFile = currentFiles.find(f => f.name === file.name);
598
- if (oldFile && !file.newFile) return oldFile;
599
- if (file.guid) return file;
600
- if (!file.newFile) return Promise.reject();
601
- return await addFile(files[i], user);
602
- })
603
- ).then(results => results.map(r => r.value).filter(Boolean));
604
-
605
- // Suppression des anciens fichiers non réutilisés
606
- await Promise.allSettled(
607
- currentFiles
608
- .filter(f => !newFiles.some(nf => nf._id === f._id))
609
- .map(f => removeFile(f, user))
610
- );
611
-
612
- return newFiles;
613
- }
614
-
615
- function handleCalculationExpression(calcExpression, fi, modelElement, calculationName) {
616
- // Check if the calculation expression involves an operator
617
- if (typeof calcExpression === 'object' && calcExpression !== null && Object.keys(calcExpression).length === 1) {
618
- const operator = Object.keys(calcExpression)[0];
619
- const operands = calcExpression[operator];
620
-
621
- // Validation of isValidAggregationOperator
622
- if (!isValidAggregationOperator(operator)) {
623
- logger.warn(`Invalid aggregation operator '${operator}' in calculation. Skipping.`);
624
- return null;
625
- }
626
-
627
- // Check Operand Count and Apply $ifNull handling
628
- if (Array.isArray(operands)) {
629
- const handledOperands = operands.map(operand => handleOperand(operand, fi, modelElement, calculationName));
630
- if (handledOperands.some(op => op === null)) { // Skip if any operand is invalid
631
- return null;
632
- }
633
- return {[operator]: handledOperands};
634
- } else {
635
- logger.warn(`Invalid operands for operator '${operator}'. Expected an array. Skipping.`);
636
- return null;
637
- }
638
- } else if (typeof calcExpression === 'string' && calcExpression.startsWith('$')) {
639
- // This is a field reference
640
- return handleOperand(calcExpression, fi, modelElement, calculationName);
641
- } else {
642
- // This is a constant value, return as is.
643
- return calcExpression;
644
- }
645
- }
646
-
647
- function handleOperand(operand, fi, modelElement, calculationName) {
648
- if (typeof operand === 'object' && operand !== null && Object.keys(operand).length === 1) {
649
- // Nested Calculation: recursively handle
650
- return handleCalculationExpression(operand, fi, modelElement, calculationName);
651
- } else if (typeof operand === 'string' && operand.startsWith('$')) {
652
- // Field Reference: check field existence
653
- const fieldName = operand.slice(1);
654
- if (!isValidFieldReference(fieldName, modelElement)) {
655
- logger.warn(`Invalid field reference '${fieldName}' in calculation. Skipping.`);
656
- return null;
657
- }
658
- return operand;
659
- } else {
660
- // Constant Value
661
- return operand;
662
- }
663
- }
664
-
665
- function isValidFieldReference(fieldName, modelElement) {
666
- // Check if the field exists in the model
667
- return modelElement.fields.some(field => field.name === fieldName);
668
- }
669
-
670
- function isValidAggregationOperator(operator) {
671
- const arithmeticOperators = [
672
- '$add', '$subtract', '$multiply', '$divide', '$mod', '$pow',
673
- '$abs', '$ceil', '$floor', '$round', '$trunc', '$exp', '$log', '$log10'
674
- ];
675
- const comparisonOperators = [
676
- '$eq', '$gt', '$gte', '$lt', '$lte', '$ne'
677
- // ... (others like $cmp, $strcasecmp, etc.)
678
- ];
679
- const stringOperators = [
680
- '$concat', '$strLenCP', '$substrCP', '$toLower', '$toUpper'
681
- // ... (others)
682
- ];
683
- const conditionalOperators = ['$cond', '$ifNull'];
684
- // Add more categories and operators as needed
685
-
686
- return [...arithmeticOperators, ...comparisonOperators, ...stringOperators, ...conditionalOperators].includes(operator);
1
+ import {getDefaultForType, getFieldValueHash} from "../../data.js";
2
+ import {Event} from "../../events.js";
3
+ import {getCollectionForUser, isObjectId} from "../mongodb.js";
4
+ import {ObjectId} from "mongodb";
5
+ import {isPlainObject, parseSafeJSON} from "../../core.js";
6
+ import {dataTypes, getModel, searchData} from "./data.operations.js";
7
+ import {validateModelData} from "./data.validation.js";
8
+ import i18n from "../../i18n.js";
9
+ import {addFile, removeFile} from "../file.js";
10
+ import {Logger} from "../../gameObject.js";
11
+ import NodeCache from "node-cache";
12
+
13
+ let depthFilter = 0;
14
+
15
+ // Création du cache avec des options configurables
16
+ export const relationCache = new NodeCache({
17
+ stdTTL: 3600, // TTL par défaut de 1 heure (en secondes)
18
+ checkperiod: 600, // Vérification des éléments expirés toutes les 10 minutes
19
+ useClones: false // Pour des performances optimales avec des ObjectId
20
+ });
21
+
22
+ let engine, logger;
23
+ export function onInit(defaultEngine) {
24
+ engine = defaultEngine;
25
+ logger = engine.getComponent(Logger);
26
+ }
27
+
28
+ export function convertDataTypes(dataArray, modelFields, sourceType = 'csv') {
29
+ return dataArray.map(record => {
30
+ const convertedRecord = {...record};
31
+ for (const field of modelFields) {
32
+ if (convertedRecord.hasOwnProperty(field.name)) {
33
+ let value = convertedRecord[field.name];
34
+
35
+ // Gérer les chaînes vides pour les champs non requis
36
+ if (typeof value === 'string' && value === '' && !field.required) {
37
+ convertedRecord[field.name] = getDefaultForType(field);
38
+ continue;
39
+ }
40
+ // Si la valeur est null ou undefined, on la laisse telle quelle, la validation s'en chargera
41
+ if (value === null || value === undefined) {
42
+ continue;
43
+ }
44
+
45
+ switch (field.type) {
46
+ case 'number':
47
+ if (typeof value !== 'number') { // Convertir si ce n'est pas déjà un nombre
48
+ const num = parseFloat(value);
49
+ if (!isNaN(num)) {
50
+ convertedRecord[field.name] = num;
51
+ } else {
52
+ logger.warn(`Import: Impossible de parser le nombre pour le champ ${field.name}, valeur: ${value}. Utilisation de la valeur par défaut/null.`);
53
+ convertedRecord[field.name] = getDefaultForType(field);
54
+ }
55
+ }
56
+ break;
57
+ case 'boolean':
58
+ if (typeof value !== 'boolean') {
59
+ convertedRecord[field.name] = ['true', '1', 'yes', 'on'].includes(String(value).toLowerCase());
60
+ }
61
+ break;
62
+ case 'date':
63
+ case 'datetime':
64
+ if (String(value).toLowerCase() === 'now') {
65
+ convertedRecord[field.name] = 'now';
66
+ } else {
67
+ const parsedDate = new Date(value);
68
+ if (!isNaN(parsedDate.getTime())) {
69
+ convertedRecord[field.name] = field.type === 'date' ? parsedDate.toISOString().split("T")[0] : parsedDate.toISOString();
70
+ } else if (value) { // Ne pas logger si la valeur était initialement vide/null
71
+ logger.warn(`Import: Impossible de parser la date pour le champ ${field.name}, valeur: ${value}. La validation ulture s'en chargera.`);
72
+ }
73
+ }
74
+ break;
75
+ case 'array':
76
+ if (['csv', 'excel'].includes(sourceType) && typeof value === 'string') {
77
+ const arrayValues = value.split(/[,;]/).map(item => item.trim()).filter(item => item !== '');
78
+ if (field.itemsType === 'number') {
79
+ convertedRecord[field.name] = arrayValues.map(v => parseFloat(v)).filter(v => !isNaN(v));
80
+ } else {
81
+ convertedRecord[field.name] = arrayValues;
82
+ }
83
+ } else if (sourceType === 'json' && typeof value === 'string') {
84
+ try {
85
+ const parsedArray = JSON.parse(value);
86
+ if (Array.isArray(parsedArray)) {
87
+ convertedRecord[field.name] = parsedArray;
88
+ // TODO: Potentiellement convertir les éléments de parsedArray ici si nécessaire
89
+ } else {
90
+ logger.warn(`Import: La chaîne JSON pour le champ tableau ${field.name} n'a pas été parsée en tableau. Valeur: ${value}.`);
91
+ }
92
+ } catch (e) {
93
+ logger.warn(`Import: Impossible de parser la chaîne JSON pour le champ tableau ${field.name}. Valeur: ${value}.`);
94
+ }
95
+ }
96
+ // Si c'est déjà un tableau (cas JSON typique), on suppose que les types des éléments sont corrects
97
+ // ou seront validés par pushDataUnsecure.
98
+ else if (!Array.isArray(convertedRecord[field.name])) {
99
+ convertedRecord[field.name] = getDefaultForType(field);
100
+ }
101
+ break;
102
+ case 'object':
103
+ if (['csv', 'excel'].includes(sourceType)) {
104
+ try {
105
+ convertedRecord[field.name] = JSON.parse(value);
106
+ } catch (e) {
107
+ logger.warn(`Import: Impossible de parser la chaîne JSON pour le champ objet ${field.name}. Valeur: ${value}.`);
108
+ }
109
+ }
110
+ break;
111
+ case 'code':
112
+ if (['csv', 'excel'].includes(sourceType) && typeof value === 'string') {
113
+ try {
114
+ convertedRecord[field.name] = JSON.parse(value);
115
+ } catch (e) {
116
+ logger.warn(`Import: Impossible de parser la chaîne JSON pour le champ code (json) ${field.name}. Valeur: ${value}.`);
117
+ }
118
+ }
119
+ break;
120
+ }
121
+ }
122
+ }
123
+ return convertedRecord;
124
+ });
125
+ }
126
+
127
+ export const removeValue = (obj, containsKey, removeParent = false) => {
128
+ // Base case: If the object is not an object or array, return it as is.
129
+ if (!isPlainObject(obj) && !Array.isArray(obj)) {
130
+ return obj;
131
+ }
132
+
133
+ for (const key in obj) {
134
+ if (Object.prototype.hasOwnProperty.call(obj, key)) {
135
+ if (removeParent) {
136
+ const value = obj[key]?.[containsKey];
137
+ if (value !== undefined) {
138
+ delete obj[key];
139
+ } else {
140
+ removeValue(obj[key], containsKey);
141
+ }
142
+ } else if (containsKey === key) {
143
+ delete obj[key];
144
+ } else {
145
+ removeValue(obj[key], containsKey);
146
+ }
147
+ }
148
+ }
149
+ return obj;
150
+ };
151
+ export const changeValue = (obj, keyToChange, changeFunction, excludeKeys = [], depth = 0, parentKey = '') => {
152
+ if (!depth) {
153
+ depthFilter = 0;
154
+ }
155
+ if (!isPlainObject(obj) && !Array.isArray(obj)) {
156
+ return obj;
157
+ }
158
+
159
+ const newObj = Array.isArray(obj) ? [] : {};
160
+
161
+ for (const key in obj) {
162
+ if (Object.prototype.hasOwnProperty.call(obj, key)) {
163
+ const topLevel = depthFilter === 0;
164
+ if (key === keyToChange) {
165
+ depthFilter++;
166
+ }
167
+ const value = obj[key];
168
+ if (value instanceof RegExp) {
169
+ newObj[key] = value;
170
+ continue;
171
+ }
172
+ if (isPlainObject(value) && !excludeKeys.includes(key)) {
173
+ newObj[key] = changeValue(value, keyToChange, changeFunction, excludeKeys, depth + 1, key);
174
+ } else if (Array.isArray(value) && !excludeKeys.includes(key)) {
175
+ newObj[key] = value.map(item => {
176
+ if (isPlainObject(item)) {
177
+ return changeValue(item, keyToChange, changeFunction, excludeKeys, depth + 1, key);
178
+ }
179
+ return item;
180
+ });
181
+ } else {
182
+ newObj[key] = value;
183
+ }
184
+ if (key === keyToChange) {
185
+ if (typeof changeFunction === 'function') {
186
+ const newValue = changeFunction(parentKey, newObj[key], topLevel);
187
+ if (newValue !== undefined) {
188
+ if (isPlainObject(newValue)) {
189
+ return newValue;
190
+ } else {
191
+ newObj[key] = newValue;
192
+ }
193
+ } else {
194
+ //delete newObj[key];
195
+
196
+ }
197
+ }
198
+ }
199
+ }
200
+ }
201
+ return newObj;
202
+ };
203
+
204
+ export async function processDocuments(datas, model, collection, me) {
205
+ const idMap = new Map();
206
+ const allInsertedIds = [];
207
+
208
+ const realData = await Event.Trigger("OnDataInsert", "event", "system", datas) || datas;
209
+ for (const doc of realData) {
210
+ try {
211
+ const newDocId = await insertAndResolveRelations(doc, model, collection, me, idMap);
212
+ if (newDocId) {
213
+ allInsertedIds.push(newDocId.toString());
214
+ }
215
+ } catch (error) {
216
+ // Modification clé ici : on ne catch plus les erreurs de validation
217
+ throw error;
218
+ }
219
+ }
220
+
221
+ return {allInsertedIds, idMap};
222
+ }
223
+
224
+ /**
225
+ * Traite toutes les relations du document
226
+ */
227
+ export async function processRelations(docToProcess, model, collection, me, idMap) {
228
+ const batchFinds = [];
229
+
230
+ // Phase 1: Préparation des requêtes
231
+ for (const field of model.fields) {
232
+ if (field.type !== 'relation') continue;
233
+
234
+ const value = docToProcess[field.name];
235
+ if (value?.$find) {
236
+ batchFinds.push({
237
+ field: field.name,
238
+ promise: searchData({
239
+ filter: value.$find,
240
+ limit: field.multiple ? 0 : 1,
241
+ model: field.relation
242
+ }, me),
243
+ multiple: field.multiple
244
+ });
245
+ }
246
+ }
247
+
248
+ // Phase 2: Exécution parallèle
249
+ const findResults = await Promise.all(batchFinds.map(f => f.promise));
250
+
251
+ // Phase 3: Traitement des résultats
252
+ findResults.forEach((result, index) => {
253
+ const {field, multiple} = batchFinds[index];
254
+ if (result.data?.length > 0) {
255
+ // Cas où des documents sont trouvés
256
+ docToProcess[field] = multiple
257
+ ? result.data.map(r => r._id.toString())
258
+ : result.data[0]._id.toString();
259
+ } else {
260
+ // Cas où AUCUN document n'est trouvé : il faut nettoyer le champ !
261
+ docToProcess[field] = multiple ? [] : null;
262
+ }
263
+ });
264
+
265
+
266
+ for (const field of model.fields) {
267
+ if (field.type !== 'relation') continue;
268
+
269
+ const fieldName = field.name;
270
+ const relationValue = docToProcess[fieldName];
271
+ if (!relationValue || typeof relationValue !== 'object') continue;
272
+
273
+ const relatedModel = await getModel(field.relation, me);
274
+
275
+ if (!Array.isArray(relationValue) && relationValue['$find']) {
276
+
277
+ } else if (Array.isArray(relationValue)) {
278
+ // Relation multiple (tableau)
279
+ docToProcess[fieldName] = await processMultipleRelations(
280
+ relationValue,
281
+ relatedModel,
282
+ collection,
283
+ me,
284
+ idMap
285
+ );
286
+ } else if (isPlainObject(relationValue)) {
287
+ // Relation simple (objet)
288
+ docToProcess[fieldName] = await processSingleRelation(
289
+ relationValue,
290
+ relatedModel,
291
+ collection,
292
+ me,
293
+ idMap
294
+ );
295
+ }
296
+ }
297
+ }
298
+
299
+ /**
300
+ * Traite une relation multiple (tableau)
301
+ */
302
+ async function processMultipleRelations(items, relatedModel, collection, me, idMap) {
303
+ const newRelationIds = await Promise.all(
304
+ items.map(item => processRelationItem(item, relatedModel, collection, me, idMap))
305
+ );
306
+ return newRelationIds.filter(id => id).map(id => id.toString());
307
+ }
308
+
309
+ /**
310
+ * Traite une relation simple (objet)
311
+ */
312
+ async function processSingleRelation(item, relatedModel, collection, me, idMap) {
313
+ const newId = await processRelationItem(item, relatedModel, collection, me, idMap);
314
+ return newId ? newId.toString() : null;
315
+ }
316
+
317
+ async function processRelationItem(item, relatedModel, collection, me, idMap) {
318
+ // Cas 1: ID existant (string ou ObjectId)
319
+ if (isObjectId(item) || typeof item === 'string') {
320
+ const originalId = typeof item === 'string' ? item : item.toString();
321
+
322
+ // Vérifier si cet ID a déjà été mappé (cas d'une référence circulaire)
323
+ if (idMap.has(originalId)) {
324
+ return idMap.get(originalId);
325
+ }
326
+
327
+ // Sinon, vérifier si l'ID existe en base
328
+ const existing = await collection.findOne({
329
+ _id: new ObjectId(originalId),
330
+ _model: relatedModel.name,
331
+ $or: [{_user: me._user || me.username}, {_user: {$exists: false}}]
332
+ });
333
+
334
+ if (existing) {
335
+ return existing._id; // Conserver l'ID original
336
+ }
337
+ }
338
+
339
+ // Cas 2: Objet complet à importer
340
+ if (isPlainObject(item)) {
341
+ const relationDoc = prepareDocument(item, relatedModel, me);
342
+ applyDefaultValues(relationDoc, relatedModel);
343
+
344
+ // Si l'objet a un _id, essayer de le conserver
345
+ if (item._id) {
346
+ const originalId = item._id.toString();
347
+
348
+ // Vérifier si l'ID existe déjà en base
349
+ const existing = await collection.findOne({
350
+ _id: new ObjectId(originalId),
351
+ _model: relatedModel.name,
352
+ $or: [{_user: me._user || me.username}, {_user: {$exists: false}}]
353
+ });
354
+
355
+ if (existing) {
356
+ return existing._id; // Utiliser l'ID existant
357
+ }
358
+
359
+ // Si l'ID n'existe pas encore, l'utiliser pour le nouvel insert
360
+ relationDoc._id = new ObjectId(originalId);
361
+ }
362
+
363
+ const relationHash = relationDoc._hash;
364
+ const cacheKey = `${relatedModel.name}:${relationHash}`;
365
+
366
+ // Vérification dans le cache
367
+ const cachedId = relationCache.get(cacheKey);
368
+ if (cachedId !== undefined) {
369
+ return cachedId;
370
+ }
371
+
372
+ // Vérification en base de données par hash
373
+ const existingByHash = await collection.findOne({
374
+ _hash: relationHash,
375
+ _model: relatedModel.name,
376
+ _user: relationDoc._user
377
+ }, {projection: {_id: 1}});
378
+
379
+ if (existingByHash) {
380
+ relationCache.set(cacheKey, existingByHash._id);
381
+ return existingByHash._id;
382
+ }
383
+
384
+ const newId = await insertAndResolveRelations(item, relatedModel, collection, me, idMap);
385
+ relationCache.set(cacheKey, newId);
386
+ return newId;
387
+ }
388
+
389
+ return null;
390
+ }
391
+
392
+ // Fonction pour vider le cache si besoin
393
+ function clearRelationCache() {
394
+ relationCache.flushAll();
395
+ }
396
+
397
+ /**
398
+ * Applique les filtres de champ définis dans le modèle
399
+ */
400
+ async function applyFieldFilters(docToProcess, model) {
401
+ for (const field of model.fields) {
402
+ docToProcess[field.name] = typeof (docToProcess[field.name]) === 'undefined' || docToProcess[field.name] === null ? field.default : docToProcess[field.name];
403
+ if (dataTypes[field.type]?.filter) {
404
+ const filter = await dataTypes[field.type].filter(
405
+ docToProcess[field.name],
406
+ field
407
+ );
408
+ const realFilter = await Event.Trigger('OnDataFilter', "event", "system", filter, field, docToProcess);
409
+ docToProcess[field.name] = realFilter || filter;
410
+ }
411
+ }
412
+ }
413
+
414
+ /**
415
+ * Applique les valeurs par défaut aux champs manquants
416
+ */
417
+ function applyDefaultValues(doc, model) {
418
+ for (const field of model.fields) {
419
+ // Si le champ n'est pas défini et a une valeur par défaut
420
+ if (!(field.name in doc) && 'default' in field) {
421
+ doc[field.name] = typeof field.default === 'function'
422
+ ? field.default()
423
+ : field.default;
424
+ }
425
+ }
426
+ }
427
+
428
+ async function insertAndResolveRelations(doc, model, collection, me, idMap) {
429
+ const originalId = doc._id?.toString();
430
+
431
+ // Si cet ID a déjà été traité, retourner le nouvel ID mappé
432
+ if (originalId && idMap.has(originalId)) {
433
+ return idMap.get(originalId);
434
+ }
435
+
436
+ const docToProcess = prepareDocument(doc, model, me);
437
+ applyDefaultValues(docToProcess, model);
438
+
439
+ // Si le document a un _id original et qu'il n'existe pas encore, le conserver
440
+ if (originalId && !await collection.findOne({_id: new ObjectId(originalId)})) {
441
+ docToProcess._id = new ObjectId(originalId);
442
+ }
443
+
444
+ await validateModelData(docToProcess, model);
445
+ await processRelations(docToProcess, model, collection, me, idMap);
446
+ await validateModelData(docToProcess, model);
447
+ await applyFieldFilters(docToProcess, model);
448
+ await checkUniqueFields(docToProcess, model, collection);
449
+
450
+ const existingDoc = await findExistingDocument(docToProcess, collection);
451
+ if (existingDoc) {
452
+ cacheDocumentId(originalId, existingDoc._id, idMap);
453
+ return existingDoc._id;
454
+ }
455
+
456
+ for (const field of model.fields) {
457
+ if (field.type === 'relation' && field.relationFilter && docToProcess[field.name]) {
458
+
459
+ const relatedIds = Array.isArray(docToProcess[field.name])
460
+ ? docToProcess[field.name]
461
+ : [docToProcess[field.name]];
462
+
463
+ // Préparer un filtre global : match si _id dans relatedIds ET respecte relationFilter
464
+ const validationQuery = {
465
+ $and: [
466
+ {$in: ['$_id', relatedIds.map(id => ({$toObjectId: id}))]},
467
+ field.relationFilter
468
+ ]
469
+ };
470
+
471
+ const relatedDocs = await searchData({
472
+ filter: validationQuery,
473
+ model: field.relation,
474
+ limit: relatedIds.length
475
+ }, me);
476
+
477
+ if ((relatedDocs?.count || 0) !== relatedIds.length) {
478
+ const invalidIds = relatedIds.filter(id =>
479
+ !relatedDocs.data.some(doc => doc._id.toString() === id.toString())
480
+ );
481
+ throw new Error(
482
+ i18n.t(
483
+ 'api.data.relationFilterFailed',
484
+ 'Les valeurs {{values}} pour le champ {{field}} ne respectent pas le filtre de relation défini.',
485
+ {field: field.name, values: invalidIds.join(', ')}
486
+ )
487
+ );
488
+ }
489
+ }
490
+ }
491
+
492
+
493
+ // Insertion en conservant éventuellement l'ID original
494
+ const result = docToProcess._id
495
+ ? await collection.insertOne(docToProcess)
496
+ : await collection.insertOne(docToProcess);
497
+
498
+ const insertedId = result.insertedId;
499
+ cacheDocumentId(originalId, insertedId, idMap);
500
+
501
+ return insertedId;
502
+ }
503
+
504
+ // Nouvelle fonction pour vérifier les champs uniques
505
+ async function checkUniqueFields(doc, model, collection) {
506
+ const uniqueFields = model.fields.filter(f => f.unique);
507
+
508
+ for (const field of uniqueFields) {
509
+ const value = doc[field.name];
510
+ if (value === undefined || value === null) continue;
511
+
512
+ const existing = await collection.findOne({
513
+ [field.name]: value,
514
+ _model: model.name,
515
+ _user: doc._user
516
+ });
517
+
518
+ if (existing) {
519
+ // Utilisation de i18n pour un message d'erreur standardisé
520
+ throw new Error(i18n.t('api.data.duplicateValue', {field: field.name, value: value}));
521
+ }
522
+ }
523
+ }
524
+
525
+ function prepareDocument(doc, model, me) {
526
+ const docToProcess = {...doc};
527
+ delete docToProcess._id;
528
+
529
+ // AJOUT: Nettoyage des champs non définis dans le modèle
530
+ for (const key of Object.keys(docToProcess)) {
531
+ if (!model.fields.some(f => f.name === key) && !key.startsWith('_')) {
532
+ delete docToProcess[key];
533
+ }
534
+ }
535
+
536
+ docToProcess._model = model.name;
537
+ docToProcess._user = me._user || me.username;
538
+ docToProcess._hash = getFieldValueHash(model, docToProcess);
539
+
540
+ return docToProcess;
541
+ }
542
+
543
+ /**
544
+ * Cherche un document existant par son hash
545
+ */
546
+ async function findExistingDocument(docToProcess, collection) {
547
+ return await collection.findOne({
548
+ _hash: docToProcess._hash,
549
+ _model: docToProcess._model,
550
+ _user: docToProcess._user
551
+ });
552
+ }
553
+
554
+ /**
555
+ * Insère le document dans la collection
556
+ */
557
+ async function insertDocument(docToProcess, collection) {
558
+ const result = await collection.insertOne(docToProcess);
559
+ return result.insertedId;
560
+ }
561
+
562
+ /**
563
+ * Met en cache la correspondance d'ID
564
+ */
565
+ function cacheDocumentId(originalId, newId, idMap) {
566
+ if (originalId && newId) {
567
+ idMap.set(originalId, newId);
568
+ }
569
+ }
570
+
571
+ /**
572
+ * Gestion des fichiers (à implémenter selon besoins)
573
+ */
574
+ export async function handleFilesIfNeeded(insertedIds, files, model, collection) {
575
+ // Implémentation spécifique à votre application
576
+ // Ex: association des fichiers uploadés aux documents insérés
577
+ }
578
+
579
+ export const checkHash = async (me, model, hash, excludeId = null) => {
580
+ const collection = await getCollectionForUser(me);
581
+ const query = {
582
+ _model: model.name,
583
+ _hash: hash,
584
+ ...(excludeId && {_id: {$ne: new ObjectId(excludeId)}})
585
+ };
586
+
587
+ console.log("Query being executed:", JSON.stringify(query, null, 2));
588
+
589
+ const count = await collection.countDocuments(query);
590
+ return count > 0;
591
+ };
592
+
593
+ // Fonctions helper
594
+ export async function processFileArray(files, currentFiles, user) {
595
+ const newFiles = await Promise.allSettled(
596
+ Object.keys(files).map(f => files[f]).map(async (file, i) => {
597
+ const oldFile = currentFiles.find(f => f.name === file.name);
598
+ if (oldFile && !file.newFile) return oldFile;
599
+ if (file.guid) return file;
600
+ if (!file.newFile) return Promise.reject();
601
+ return await addFile(files[i], user);
602
+ })
603
+ ).then(results => results.map(r => r.value).filter(Boolean));
604
+
605
+ // Suppression des anciens fichiers non réutilisés
606
+ await Promise.allSettled(
607
+ currentFiles
608
+ .filter(f => !newFiles.some(nf => nf._id === f._id))
609
+ .map(f => removeFile(f, user))
610
+ );
611
+
612
+ return newFiles;
613
+ }
614
+
615
+ function handleCalculationExpression(calcExpression, fi, modelElement, calculationName) {
616
+ // Check if the calculation expression involves an operator
617
+ if (typeof calcExpression === 'object' && calcExpression !== null && Object.keys(calcExpression).length === 1) {
618
+ const operator = Object.keys(calcExpression)[0];
619
+ const operands = calcExpression[operator];
620
+
621
+ // Validation of isValidAggregationOperator
622
+ if (!isValidAggregationOperator(operator)) {
623
+ logger.warn(`Invalid aggregation operator '${operator}' in calculation. Skipping.`);
624
+ return null;
625
+ }
626
+
627
+ // Check Operand Count and Apply $ifNull handling
628
+ if (Array.isArray(operands)) {
629
+ const handledOperands = operands.map(operand => handleOperand(operand, fi, modelElement, calculationName));
630
+ if (handledOperands.some(op => op === null)) { // Skip if any operand is invalid
631
+ return null;
632
+ }
633
+ return {[operator]: handledOperands};
634
+ } else {
635
+ logger.warn(`Invalid operands for operator '${operator}'. Expected an array. Skipping.`);
636
+ return null;
637
+ }
638
+ } else if (typeof calcExpression === 'string' && calcExpression.startsWith('$')) {
639
+ // This is a field reference
640
+ return handleOperand(calcExpression, fi, modelElement, calculationName);
641
+ } else {
642
+ // This is a constant value, return as is.
643
+ return calcExpression;
644
+ }
645
+ }
646
+
647
+ function handleOperand(operand, fi, modelElement, calculationName) {
648
+ if (typeof operand === 'object' && operand !== null && Object.keys(operand).length === 1) {
649
+ // Nested Calculation: recursively handle
650
+ return handleCalculationExpression(operand, fi, modelElement, calculationName);
651
+ } else if (typeof operand === 'string' && operand.startsWith('$')) {
652
+ // Field Reference: check field existence
653
+ const fieldName = operand.slice(1);
654
+ if (!isValidFieldReference(fieldName, modelElement)) {
655
+ logger.warn(`Invalid field reference '${fieldName}' in calculation. Skipping.`);
656
+ return null;
657
+ }
658
+ return operand;
659
+ } else {
660
+ // Constant Value
661
+ return operand;
662
+ }
663
+ }
664
+
665
+ function isValidFieldReference(fieldName, modelElement) {
666
+ // Check if the field exists in the model
667
+ return modelElement.fields.some(field => field.name === fieldName);
668
+ }
669
+
670
+ function isValidAggregationOperator(operator) {
671
+ const arithmeticOperators = [
672
+ '$add', '$subtract', '$multiply', '$divide', '$mod', '$pow',
673
+ '$abs', '$ceil', '$floor', '$round', '$trunc', '$exp', '$log', '$log10'
674
+ ];
675
+ const comparisonOperators = [
676
+ '$eq', '$gt', '$gte', '$lt', '$lte', '$ne'
677
+ // ... (others like $cmp, $strcasecmp, etc.)
678
+ ];
679
+ const stringOperators = [
680
+ '$concat', '$strLenCP', '$substrCP', '$toLower', '$toUpper'
681
+ // ... (others)
682
+ ];
683
+ const conditionalOperators = ['$cond', '$ifNull'];
684
+ // Add more categories and operators as needed
685
+
686
+ return [...arithmeticOperators, ...comparisonOperators, ...stringOperators, ...conditionalOperators].includes(operator);
687
687
  }