data-primals-engine 1.5.1 → 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 (70) hide show
  1. package/README.md +924 -913
  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 +7 -1
  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 -806
  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 -285
  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 +7 -2
  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 -262
  38. package/client/src/index.css +90 -90
  39. package/package.json +11 -10
  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/core.js +8 -1
  44. package/src/engine.js +335 -293
  45. package/src/events.js +140 -21
  46. package/src/filter.js +274 -274
  47. package/src/index.js +3 -0
  48. package/src/modules/assistant/assistant.js +323 -192
  49. package/src/modules/assistant/constants.js +2 -1
  50. package/src/modules/auth-google/index.js +50 -50
  51. package/src/modules/auth-microsoft/index.js +81 -81
  52. package/src/modules/data/data.core.js +118 -118
  53. package/src/modules/data/data.history.js +555 -531
  54. package/src/modules/data/data.operations.js +145 -26
  55. package/src/modules/data/data.relations.js +686 -686
  56. package/src/modules/data/data.routes.js +1879 -1879
  57. package/src/modules/data/data.validation.js +2 -2
  58. package/src/modules/file.js +247 -247
  59. package/src/modules/user.js +1 -0
  60. package/src/modules/workflow.js +2 -2
  61. package/src/openai.jobs.js +3 -2
  62. package/src/packs.js +2 -2
  63. package/src/providers.js +2 -2
  64. package/src/services/stripe.js +196 -196
  65. package/src/sso.js +193 -193
  66. package/src/workers/import-export-worker.js +1 -1
  67. package/test/data.history.integration.test.js +72 -0
  68. package/test/data.integration.test.js +92 -1
  69. package/test/events.test.js +108 -10
  70. package/test/model.integration.test.js +377 -377
@@ -1,4 +1,13 @@
1
- import {getObjectHash, getRand, getRandom, isPlainObject, randomDate, safeAssignObject, setSeed} from "../../core.js";
1
+ import {
2
+ getObjectHash,
3
+ getRand,
4
+ getRandom,
5
+ isPlainObject,
6
+ parseSafeJSON,
7
+ randomDate,
8
+ safeAssignObject,
9
+ setSeed
10
+ } from "../../core.js";
2
11
  import {
3
12
  maxExportCount,
4
13
  maxFileSize,
@@ -114,7 +123,7 @@ export const dataTypes = {
114
123
  return value;
115
124
  else if (typeof (value) === 'string') {
116
125
  try {
117
- return JSON.parse(value);
126
+ return JSON.parse (value);
118
127
  } catch (e) {
119
128
  return null;
120
129
  }
@@ -788,7 +797,11 @@ export const insertData = async (modelName, data, files, user, triggerWorkflow =
788
797
  await Event.Trigger("OnDataAdded", "event", "user", userPayload);
789
798
 
790
799
  // Return valid result
791
- return {success: true, insertedIds: insertedIds.map(id => id.toString())}; // Convertir les IDs en string pour la réponse
800
+ // --- CORRECTION ---
801
+ // On renvoie l'objet complet du premier document inséré (cas le plus courant pour l'undo)
802
+ // et la liste complète des IDs pour les cas de bulk insert.
803
+ const firstInsertedDoc = insertedDocs.length > 0 ? insertedDocs[0] : null;
804
+ return {success: true, data: firstInsertedDoc, insertedIds: insertedIds.map(id => id.toString())};
792
805
 
793
806
  } catch (error) { // Attrape les erreurs de permission ou de pushDataUnsecure
794
807
  logger.error(`[insertData] Main error during insertion process for model ${modelName}: ${error.message}`, error.stack);
@@ -1055,11 +1068,24 @@ const internalEditOrPatchData = async (modelName, filter, data, files, user, isP
1055
1068
  }
1056
1069
  const ids = existingDocs.map(d => new ObjectId(d._id));
1057
1070
  const originalHash = existingDocs[0]._hash; // Sauvegarde du hash avant modification
1058
-
1059
- // 3. Préparation des données de mise à jour (inchangé)
1060
- const updateData = {...data};
1061
- delete updateData._model;
1062
- delete updateData._user;
1071
+
1072
+ // 3. Préparation des données de mise à jour en séparant les opérateurs
1073
+ const updateSetData = {};
1074
+ const updatePushData = {};
1075
+
1076
+ for (const key in data) {
1077
+ if (key.startsWith('_')) continue;
1078
+
1079
+ if (isPlainObject(data[key]) && data[key].$push !== undefined) {
1080
+ // C'est une opération $push
1081
+ updatePushData[key] = data[key].$push;
1082
+ } else {
1083
+ // C'est une opération $set (par défaut)
1084
+ updateSetData[key] = data[key];
1085
+ }
1086
+ }
1087
+
1088
+ const updateData = updateSetData; // Pour la compatibilité avec le code existant
1063
1089
 
1064
1090
  // Traitement des fichiers
1065
1091
  const fileFields = model.fields.filter(f => f.type === 'file' || (f.type === 'array' && f.itemsType === 'file'));
@@ -1105,13 +1131,38 @@ const internalEditOrPatchData = async (modelName, filter, data, files, user, isP
1105
1131
 
1106
1132
  // 4. Validation adaptée pour patch ou edit
1107
1133
  if (!isPatch) {
1108
- const dataToValidate = {...existingDocs[0], ...updateData};
1134
+ const dataToValidate = {...existingDocs[0], ...updateSetData};
1109
1135
  await validateModelData(dataToValidate, model, false);
1110
1136
  } else {
1111
- await validateModelData(updateData, model, true);
1137
+ await validateModelData(updateSetData, model, true);
1112
1138
  }
1113
1139
 
1114
- // 5. Vérification des champs uniques
1140
+ // 4.1 Validation pour les opérations $push
1141
+ for (const fieldName in updatePushData) {
1142
+ const field = model.fields.find(f => f.name === fieldName);
1143
+ if (!field) throw new Error(`Le champ '${fieldName}' pour l'opération $push n'existe pas dans le modèle '${model.name}'.`);
1144
+ if (field.type !== 'array') throw new Error(`L'opération $push ne peut être utilisée que sur des champs de type 'array'. Le champ '${fieldName}' est de type '${field.type}'.`);
1145
+
1146
+ const valueToPush = updatePushData[fieldName];
1147
+ let itemsToValidate;
1148
+
1149
+ if (Array.isArray(valueToPush)) {
1150
+ // Cas: { $push: { myArray: ["val1", "val2"] } }
1151
+ itemsToValidate = valueToPush;
1152
+ } else {
1153
+ // Cas: { $push: { myArray: "val1" } }
1154
+ itemsToValidate = [valueToPush];
1155
+ }
1156
+
1157
+ for (const item of itemsToValidate) {
1158
+ if (!dataTypes[field.itemsType]?.validate(item, { ...field, type: field.itemsType })) {
1159
+ throw new Error(`Valeur invalide fournie pour le tableau '${fieldName}'. L'élément ${JSON.stringify(item)} n'est pas un(e) '${field.itemsType}' valide.`);
1160
+ }
1161
+ }
1162
+ }
1163
+
1164
+
1165
+ // 5. Vérification des champs uniques (s'applique uniquement aux opérations $set)
1115
1166
  const uniqueFields = model.fields.filter(f => f.unique);
1116
1167
  for (const field of uniqueFields) {
1117
1168
  if (updateData[field.name] !== undefined) {
@@ -1130,7 +1181,7 @@ const internalEditOrPatchData = async (modelName, filter, data, files, user, isP
1130
1181
  }
1131
1182
  }
1132
1183
 
1133
- // 6. Traitement des relations
1184
+ // 6. Traitement des relations (pour $set)
1134
1185
  const relationFields = model.fields.filter(f => f.type === 'relation');
1135
1186
  for (const field of relationFields) {
1136
1187
  if (updateData[field.name] !== undefined) {
@@ -1154,7 +1205,7 @@ const internalEditOrPatchData = async (modelName, filter, data, files, user, isP
1154
1205
  }
1155
1206
  }
1156
1207
 
1157
- // 7. Application des filtres de champ (ex: hashage de mot de passe)
1208
+ // 7. Application des filtres de champ (pour $set)
1158
1209
  for (const field of model.fields) {
1159
1210
  // On saute les champs 'file' car ils ont déjà été traités
1160
1211
  if (field.type !== 'file' && field.itemsType !== 'file' && updateData[field.name] !== undefined && dataTypes[field.type]?.filter) {
@@ -1200,14 +1251,40 @@ const internalEditOrPatchData = async (modelName, filter, data, files, user, isP
1200
1251
  }
1201
1252
  }
1202
1253
 
1203
- // 8. Calcul du nouveau hash et préparation des données finales
1204
- const finalStateForHash = {...existingDocs[0], ...updateData};
1254
+ // 8. Calcul du nouveau hash en simulant l'état final
1255
+ const finalStateForHash = { ...existingDocs[0], ...updateSetData };
1256
+ for (const fieldName in updatePushData) {
1257
+ let itemsToAdd;
1258
+ const pushValue = updatePushData[fieldName];
1259
+ if (isPlainObject(pushValue) && pushValue.$each) {
1260
+ itemsToAdd = pushValue.$each;
1261
+ } else if (Array.isArray(pushValue)) {
1262
+ itemsToAdd = pushValue;
1263
+ } else {
1264
+ itemsToAdd = [pushValue];
1265
+ }
1266
+
1267
+ // S'assure que le champ de base est un tableau avant de pousser
1268
+ if (!Array.isArray(finalStateForHash[fieldName])) {
1269
+ // Utilise la valeur existante si c'est un tableau, sinon initialise un tableau vide
1270
+ finalStateForHash[fieldName] = Array.isArray(existingDocs[0]?.[fieldName]) ? [...existingDocs[0][fieldName]] : [];
1271
+ }
1272
+ finalStateForHash[fieldName].push(...itemsToAdd);
1273
+ }
1205
1274
  const newHash = getFieldValueHash(model, finalStateForHash);
1206
1275
 
1207
- const finalDataForSet = {
1208
- ...updateData,
1209
- _hash: newHash
1210
- };
1276
+ // Préparation de l'objet de mise à jour final pour MongoDB
1277
+ const finalUpdateOperation = {};
1278
+ if (Object.keys(updateSetData).length > 0) {
1279
+ finalUpdateOperation.$set = { ...updateSetData, _hash: newHash };
1280
+ }
1281
+ if (Object.keys(updatePushData).length > 0) {
1282
+ finalUpdateOperation.$push = updatePushData;
1283
+ // Si on a que du $push, il faut quand même mettre à jour le hash
1284
+ if (!finalUpdateOperation.$set) {
1285
+ finalUpdateOperation.$set = { _hash: newHash };
1286
+ }
1287
+ }
1211
1288
 
1212
1289
  // 9. *** CORRECTION LOGIQUE ***
1213
1290
  // On ne vérifie l'unicité que si le hash a réellement changé.
@@ -1219,8 +1296,36 @@ const internalEditOrPatchData = async (modelName, filter, data, files, user, isP
1219
1296
  }
1220
1297
  }
1221
1298
 
1222
- // 10. Exécution de la mise à jour (inchangé)
1223
- const bulkOps = [{updateMany: {filter: {_id: {$in: ids}}, update: {$set: finalDataForSet}}}];
1299
+ // 10. Exécution de la mise à jour.
1300
+ // Si une opération $push est présente, nous devons utiliser une pipeline d'agrégation
1301
+ // pour gérer de manière robuste le cas où le champ cible est `null` ou n'existe pas.
1302
+ let updateCommand;
1303
+ if (Object.keys(updatePushData).length > 0) {
1304
+ const setStage = finalUpdateOperation.$set ? [{$set: finalUpdateOperation.$set}] : [];
1305
+ const pushStage = [{$set: {}}]; // Étape pour construire les opérations de push
1306
+
1307
+ for (const fieldName in finalUpdateOperation.$push) {
1308
+ const pushValue = finalUpdateOperation.$push[fieldName];
1309
+ let valuesToConcat;
1310
+
1311
+ if (isPlainObject(pushValue) && pushValue.$each) {
1312
+ valuesToConcat = pushValue.$each;
1313
+ } else if (Array.isArray(pushValue)) {
1314
+ valuesToConcat = pushValue;
1315
+ } else {
1316
+ valuesToConcat = [pushValue];
1317
+ }
1318
+
1319
+ // Construit l'expression pour ajouter les éléments au tableau, en le créant s'il est null.
1320
+ pushStage[0].$set[fieldName] = {
1321
+ $concatArrays: [{ $ifNull: [`$${fieldName}`, []] }, valuesToConcat]
1322
+ };
1323
+ }
1324
+ updateCommand = [...setStage, ...pushStage];
1325
+ } else {
1326
+ updateCommand = finalUpdateOperation;
1327
+ }
1328
+ const bulkOps = [{ updateMany: { filter: { _id: { $in: ids } }, update: updateCommand } }];
1224
1329
  const bulkResult = await collection.bulkWrite(bulkOps);
1225
1330
  const modifiedCount = bulkResult.modifiedCount || 0;
1226
1331
 
@@ -1249,7 +1354,7 @@ const internalEditOrPatchData = async (modelName, filter, data, files, user, isP
1249
1354
 
1250
1355
  // 11. Tâches post-mise à jour (schedules, workflows) (inchangé)
1251
1356
  if (["workflowTrigger", "alert"].includes(modelName)) {
1252
- await handleScheduledJobs(modelName, existingDocs, collection, finalDataForSet);
1357
+ await handleScheduledJobs(modelName, existingDocs, collection, finalUpdateOperation.$set || {});
1253
1358
  }
1254
1359
 
1255
1360
  if (triggerWorkflow && modifiedCount > 0) {
@@ -1316,6 +1421,12 @@ export const deleteData = async (modelName, filter, user = {}, triggerWorkflow,
1316
1421
  return ({success: true, deletedCount: 0, message: "No documents found to delete."});
1317
1422
  }
1318
1423
 
1424
+ // --- AMÉLIORATION ---
1425
+ // On détermine le nom du modèle à partir du premier document trouvé.
1426
+ // C'est crucial car la fonction peut être appelée sans `modelName` explicite (ex: via /api/data/:ids).
1427
+ // On suppose que toutes les suppressions dans un même appel concernent le même modèle.
1428
+ const modelNameToInvalidate = modelName || documentsToDelete[0]?._model;
1429
+
1319
1430
  const finalIdsToDelete = []; // IDs des documents qui seront effectivement supprimés
1320
1431
  const idsToInvalidate = []; // *** AJOUT: IDs à invalider dans le cache ***
1321
1432
 
@@ -1471,9 +1582,19 @@ export const deleteData = async (modelName, filter, user = {}, triggerWorkflow,
1471
1582
  _id: {$in: finalIdsToDelete}
1472
1583
  // Le filtre _user est déjà implicite car on a fetch les documents de l'utilisateur
1473
1584
  });
1474
- invalidateIdsCache(modelName, idsToInvalidate);
1475
- console.log(`Invalidated cache for ${idsToInvalidate.length} deleted documents in model: ${modelName}`);
1476
1585
 
1586
+ // SOLUTION RADICALE : On invalide TOUT le cache pour ce modèle.
1587
+ // C'est crucial car la suppression affecte les comptes, la pagination, etc.
1588
+ // --- AMÉLIORATION : Invalider aussi les modèles liés ---
1589
+ // Si on supprime un document, toutes les recherches sur les modèles qui le référençaient
1590
+ // deviennent invalides.
1591
+ const relatedModels = await findRelatedModels(modelNameToInvalidate, user);
1592
+ for (const relatedModel of relatedModels) {
1593
+ invalidateModelCache(relatedModel.name);
1594
+ logger.info(`[Cache Invalidation] Also invalidated cache for related model: ${relatedModel.name}`);
1595
+ }
1596
+
1597
+ if (modelNameToInvalidate) invalidateModelCache(modelNameToInvalidate);
1477
1598
  deletedCount = result.deletedCount;
1478
1599
  logger.info(`[deleteData] Successfully deleted ${deletedCount} documents for user ${user?.username}.`);
1479
1600
  } else {
@@ -2281,8 +2402,6 @@ const invalidateIdsCache = (model, ids) => {
2281
2402
  const cacheKeys = cacheRelations.get(idKey);
2282
2403
  cacheKeys.forEach(key => searchCache.del(key));
2283
2404
  cacheRelations.delete(idKey);
2284
-
2285
- console.log(`Invalidated cache for ID: ${id} in model: ${model}`);
2286
2405
  }
2287
2406
  });
2288
2407