data-primals-engine 1.5.2 → 1.6.1

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 +938 -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/ModelCreator.jsx +683 -686
  26. package/client/src/ModelCreator.scss +1 -1
  27. package/client/src/ModelCreatorField.jsx +950 -950
  28. package/client/src/ModelImporter.jsx +3 -0
  29. package/client/src/ModelList.jsx +280 -280
  30. package/client/src/Notification.jsx +136 -136
  31. package/client/src/PackGallery.jsx +391 -391
  32. package/client/src/PackGallery.scss +231 -231
  33. package/client/src/Pagination.jsx +143 -143
  34. package/client/src/RelationField.jsx +354 -354
  35. package/client/src/RelationSelectorWidget.jsx +172 -172
  36. package/client/src/RelationValue.jsx +2 -1
  37. package/client/src/contexts/CommandContext.jsx +260 -0
  38. package/client/src/contexts/ModelContext.jsx +4 -1
  39. package/client/src/contexts/UIContext.jsx +72 -72
  40. package/client/src/filter.js +263 -263
  41. package/client/src/index.css +90 -90
  42. package/package.json +6 -5
  43. package/perf/artillery-hooks.js +37 -37
  44. package/perf/setup.yml +25 -25
  45. package/src/constants.js +4 -0
  46. package/src/defaultModels.js +7 -0
  47. package/src/engine.js +335 -335
  48. package/src/events.js +232 -137
  49. package/src/filter.js +274 -274
  50. package/src/index.js +1 -0
  51. package/src/modules/assistant/assistant.js +225 -83
  52. package/src/modules/auth-google/index.js +50 -50
  53. package/src/modules/auth-microsoft/index.js +81 -81
  54. package/src/modules/data/data.core.js +118 -118
  55. package/src/modules/data/data.history.js +555 -555
  56. package/src/modules/data/data.operations.js +112 -9
  57. package/src/modules/data/data.relations.js +686 -686
  58. package/src/modules/data/data.routes.js +1879 -1879
  59. package/src/modules/data/data.validation.js +2 -2
  60. package/src/modules/file.js +247 -247
  61. package/src/modules/user.js +14 -9
  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/test/data.history.integration.test.js +264 -264
  67. package/test/data.integration.test.js +1281 -1206
  68. package/test/events.test.js +108 -10
  69. package/test/model.integration.test.js +377 -377
  70. package/test/user.test.js +106 -1
@@ -123,7 +123,7 @@ export const dataTypes = {
123
123
  return value;
124
124
  else if (typeof (value) === 'string') {
125
125
  try {
126
- return JSON.parse(value);
126
+ return JSON.parse (value);
127
127
  } catch (e) {
128
128
  return null;
129
129
  }
@@ -459,6 +459,12 @@ export const dataTypes = {
459
459
  }
460
460
  };
461
461
 
462
+ const userStorageCache = new NodeCache({
463
+ stdTTL: 600, // 10 minutes
464
+ checkperiod: 120,
465
+ useClones: false
466
+ });
467
+
462
468
 
463
469
 
464
470
  let engine, logger;
@@ -466,6 +472,29 @@ export function onInit(defaultEngine) {
466
472
  engine = defaultEngine;
467
473
  logger = engine.getComponent(Logger);
468
474
 
475
+ // Précalculer périodiquement l'utilisation du stockage pour chaque utilisateur
476
+ const updateUserStorageUsage = async () => {
477
+ logger.debug('[Storage] Starting periodic user storage calculation...');
478
+ try {
479
+ const usersCollection = getCollection('users');
480
+ const users = await usersCollection.find({}, { projection: { username: 1 } }).toArray();
481
+
482
+ for (const user of users) {
483
+ try {
484
+ const usage = await calculateTotalUserStorageUsage(user);
485
+ userStorageCache.set(user.username, usage);
486
+ } catch (userError) {
487
+ logger.error(`[Storage] Failed to calculate storage for user ${user.username}:`, userError);
488
+ }
489
+ }
490
+ logger.debug(`[Storage] User storage calculation finished for ${users.length} users.`);
491
+ } catch (e) {
492
+ logger.error('[Storage] Error during periodic user storage calculation:', e);
493
+ }
494
+ };
495
+
496
+ setInterval(updateUserStorageUsage, 300000); // Toutes les 5 minutes
497
+ updateUserStorageUsage(); // Lancer une première fois au démarrage
469
498
  // Nettoyer périodiquement les relations obsolètes
470
499
  setInterval(() => {
471
500
  // Nettoyer les relations pour les clés qui n'existent plus
@@ -797,7 +826,11 @@ export const insertData = async (modelName, data, files, user, triggerWorkflow =
797
826
  await Event.Trigger("OnDataAdded", "event", "user", userPayload);
798
827
 
799
828
  // Return valid result
800
- return {success: true, insertedIds: insertedIds.map(id => id.toString())}; // Convertir les IDs en string pour la réponse
829
+ // --- CORRECTION ---
830
+ // On renvoie l'objet complet du premier document inséré (cas le plus courant pour l'undo)
831
+ // et la liste complète des IDs pour les cas de bulk insert.
832
+ const firstInsertedDoc = insertedDocs.length > 0 ? insertedDocs[0] : null;
833
+ return {success: true, data: firstInsertedDoc, insertedIds: insertedIds.map(id => id.toString())};
801
834
 
802
835
  } catch (error) { // Attrape les erreurs de permission ou de pushDataUnsecure
803
836
  logger.error(`[insertData] Main error during insertion process for model ${modelName}: ${error.message}`, error.stack);
@@ -986,8 +1019,11 @@ async function checkLimits(datas, model, collection, me) {
986
1019
  const userStorageLimit = await engine.userProvider.getUserStorageLimit(me);
987
1020
 
988
1021
  // Vérification des limites utilisateur
989
- const currentStorageUsage = await calculateTotalUserStorageUsage(me);
990
- if (currentStorageUsage + incomingDataSize > userStorageLimit) {
1022
+ let currentStorageUsage = userStorageCache.get(me.username);
1023
+ if (currentStorageUsage === undefined) {
1024
+ currentStorageUsage = await calculateTotalUserStorageUsage(me); // Fallback si le cache est vide
1025
+ }
1026
+ if ((currentStorageUsage || 0) + incomingDataSize > userStorageLimit) {
991
1027
  throw new Error(i18n.t("api.data.storageLimitExceeded", {
992
1028
  limit: Math.round(userStorageLimit / megabytes)
993
1029
  }));
@@ -1070,7 +1106,7 @@ const internalEditOrPatchData = async (modelName, filter, data, files, user, isP
1070
1106
  const updatePushData = {};
1071
1107
 
1072
1108
  for (const key in data) {
1073
- if (key.startsWith('_')) continue;
1109
+ if (key.startsWith('_') && !['_env', '_pack'].includes(key)) continue;
1074
1110
 
1075
1111
  if (isPlainObject(data[key]) && data[key].$push !== undefined) {
1076
1112
  // C'est une opération $push
@@ -1247,6 +1283,43 @@ const internalEditOrPatchData = async (modelName, filter, data, files, user, isP
1247
1283
  }
1248
1284
  }
1249
1285
 
1286
+ // 8. Vérification des limites de stockage avant la mise à jour
1287
+ const originalDocsSize = calculateDataSize(existingDocs);
1288
+ const finalStateForSize = existingDocs.map(doc => {
1289
+ const updatedDoc = { ...doc, ...updateSetData };
1290
+ for (const fieldName in updatePushData) {
1291
+ let itemsToAdd;
1292
+ const pushValue = updatePushData[fieldName];
1293
+ if (isPlainObject(pushValue) && pushValue.$each) {
1294
+ itemsToAdd = pushValue.$each;
1295
+ } else if (Array.isArray(pushValue)) {
1296
+ itemsToAdd = pushValue;
1297
+ } else {
1298
+ itemsToAdd = [pushValue];
1299
+ }
1300
+ if (!Array.isArray(updatedDoc[fieldName])) {
1301
+ updatedDoc[fieldName] = [];
1302
+ }
1303
+ updatedDoc[fieldName].push(...itemsToAdd);
1304
+ }
1305
+ return updatedDoc;
1306
+ });
1307
+
1308
+ const finalDocsSize = calculateDataSize(finalStateForSize);
1309
+ const sizeDelta = finalDocsSize - originalDocsSize;
1310
+
1311
+ if (sizeDelta > 0) {
1312
+ const userStorageLimit = await engine.userProvider.getUserStorageLimit(user);
1313
+ const currentStorageUsage = await calculateTotalUserStorageUsage(user);
1314
+ if (currentStorageUsage + sizeDelta > userStorageLimit) {
1315
+ throw new Error(i18n.t("api.data.storageLimitExceeded", { limit: Math.round(userStorageLimit / megabytes) }));
1316
+ }
1317
+ const serverCapacity = await checkServerCapacity(sizeDelta);
1318
+ if (!serverCapacity.isSufficient) {
1319
+ throw new Error(i18n.t("api.data.serverStorageFull"));
1320
+ }
1321
+ }
1322
+
1250
1323
  // 8. Calcul du nouveau hash en simulant l'état final
1251
1324
  const finalStateForHash = { ...existingDocs[0], ...updateSetData };
1252
1325
  for (const fieldName in updatePushData) {
@@ -1417,6 +1490,12 @@ export const deleteData = async (modelName, filter, user = {}, triggerWorkflow,
1417
1490
  return ({success: true, deletedCount: 0, message: "No documents found to delete."});
1418
1491
  }
1419
1492
 
1493
+ // --- AMÉLIORATION ---
1494
+ // On détermine le nom du modèle à partir du premier document trouvé.
1495
+ // C'est crucial car la fonction peut être appelée sans `modelName` explicite (ex: via /api/data/:ids).
1496
+ // On suppose que toutes les suppressions dans un même appel concernent le même modèle.
1497
+ const modelNameToInvalidate = modelName || documentsToDelete[0]?._model;
1498
+
1420
1499
  const finalIdsToDelete = []; // IDs des documents qui seront effectivement supprimés
1421
1500
  const idsToInvalidate = []; // *** AJOUT: IDs à invalider dans le cache ***
1422
1501
 
@@ -1572,9 +1651,19 @@ export const deleteData = async (modelName, filter, user = {}, triggerWorkflow,
1572
1651
  _id: {$in: finalIdsToDelete}
1573
1652
  // Le filtre _user est déjà implicite car on a fetch les documents de l'utilisateur
1574
1653
  });
1575
- invalidateIdsCache(modelName, idsToInvalidate);
1576
- console.log(`Invalidated cache for ${idsToInvalidate.length} deleted documents in model: ${modelName}`);
1577
1654
 
1655
+ // SOLUTION RADICALE : On invalide TOUT le cache pour ce modèle.
1656
+ // C'est crucial car la suppression affecte les comptes, la pagination, etc.
1657
+ // --- AMÉLIORATION : Invalider aussi les modèles liés ---
1658
+ // Si on supprime un document, toutes les recherches sur les modèles qui le référençaient
1659
+ // deviennent invalides.
1660
+ const relatedModels = await findRelatedModels(modelNameToInvalidate, user);
1661
+ for (const relatedModel of relatedModels) {
1662
+ invalidateModelCache(relatedModel.name);
1663
+ logger.info(`[Cache Invalidation] Also invalidated cache for related model: ${relatedModel.name}`);
1664
+ }
1665
+
1666
+ if (modelNameToInvalidate) invalidateModelCache(modelNameToInvalidate);
1578
1667
  deletedCount = result.deletedCount;
1579
1668
  logger.info(`[deleteData] Successfully deleted ${deletedCount} documents for user ${user?.username}.`);
1580
1669
  } else {
@@ -1674,7 +1763,8 @@ const generateCacheKey = (query, user) => {
1674
1763
  filter: query.filter,
1675
1764
  depth: query.depth,
1676
1765
  autoExpand: query.autoExpand,
1677
- pack: query.pack
1766
+ pack: query.pack,
1767
+ env: query.env
1678
1768
  },
1679
1769
  user: user.username
1680
1770
  };
@@ -1690,7 +1780,7 @@ const searchCache = new NodeCache({
1690
1780
  });
1691
1781
 
1692
1782
  export const searchData = async (query, user) => {
1693
- const {page, limit, sort, model, pipelinesPosition, pipelines: customPipelines = [], ids, timeout, pack} = query;
1783
+ const {page, limit, sort, model, pipelinesPosition, pipelines: customPipelines = [], ids, timeout, pack, env} = query;
1694
1784
 
1695
1785
  if (user && user.username !== 'demo' && isLocalUser(user) && (
1696
1786
  !await hasPermission(["API_ADMIN", "API_SEARCH_DATA", "API_SEARCH_DATA_" + model], user) ||
@@ -2142,8 +2232,18 @@ export const searchData = async (query, user) => {
2142
2232
  }
2143
2233
  }
2144
2234
 
2235
+ let envMatchStage;
2236
+ if (env === 'production') {
2237
+ envMatchStage = {$match: {$or: [{'_env': 'production'}, {'_env': {$exists: false}}]}};
2238
+ } else if (env) {
2239
+ envMatchStage = {$match: {'_env': env}};
2240
+ } else {
2241
+ envMatchStage = {$match: {'_env': {$exists: false}}};
2242
+ }
2243
+
2145
2244
  return pipelines.concat(
2146
2245
  [
2246
+ envMatchStage,
2147
2247
  {$match: {'_pack': pack ? pack : {$exists: false}}},
2148
2248
  {$match: {$expr: dataNoRelation}}
2149
2249
  ],
@@ -3108,6 +3208,7 @@ export async function installPack(packIdentifier, user = null, lang = 'en', opti
3108
3208
  delete docForInsert._temp_pack_id;
3109
3209
 
3110
3210
  if (user) docForInsert._user = username;
3211
+
3111
3212
  docForInsert._model = modelName;
3112
3213
  docForInsert._hash = getFieldValueHash(modelDefForHash, docForInsert);
3113
3214
 
@@ -3116,6 +3217,8 @@ export async function installPack(packIdentifier, user = null, lang = 'en', opti
3116
3217
  _hash: docForInsert._hash,
3117
3218
  _model: modelName
3118
3219
  };
3220
+
3221
+ if (docForInsert._env) existingQuery._env = docForInsert._env;
3119
3222
  if (user) existingQuery._user = username;
3120
3223
 
3121
3224
  const existingDoc = await collection.findOne(existingQuery, {projection: {_id: 1}});