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.
- package/README.md +938 -915
- package/client/README.md +136 -136
- package/client/index.js +0 -1
- package/client/public/doc/API.postman_collection.json +213 -213
- package/client/public/react.svg +9 -9
- package/client/src/AddWidgetTypeModal.jsx +47 -47
- package/client/src/App.css +42 -42
- package/client/src/App.jsx +92 -150
- package/client/src/App.scss +6 -0
- package/client/src/AssistantChat.jsx +362 -363
- package/client/src/Button.jsx +12 -12
- package/client/src/Dashboard.jsx +480 -480
- package/client/src/DashboardHtmlViewItem.jsx +146 -146
- package/client/src/DashboardView.jsx +654 -654
- package/client/src/DataEditor.jsx +383 -383
- package/client/src/DataLayout.jsx +779 -808
- package/client/src/DataTable.jsx +782 -822
- package/client/src/DataTable.scss +186 -186
- package/client/src/GeolocationField.jsx +93 -93
- package/client/src/HistoryDialog.jsx +307 -307
- package/client/src/HistoryDialog.scss +319 -319
- package/client/src/HtmlViewBuilderModal.jsx +90 -90
- package/client/src/HtmlViewBuilderModal.scss +17 -17
- package/client/src/HtmlViewCard.jsx +43 -43
- package/client/src/ModelCreator.jsx +683 -686
- package/client/src/ModelCreator.scss +1 -1
- package/client/src/ModelCreatorField.jsx +950 -950
- package/client/src/ModelImporter.jsx +3 -0
- package/client/src/ModelList.jsx +280 -280
- package/client/src/Notification.jsx +136 -136
- package/client/src/PackGallery.jsx +391 -391
- package/client/src/PackGallery.scss +231 -231
- package/client/src/Pagination.jsx +143 -143
- package/client/src/RelationField.jsx +354 -354
- package/client/src/RelationSelectorWidget.jsx +172 -172
- package/client/src/RelationValue.jsx +2 -1
- package/client/src/contexts/CommandContext.jsx +260 -0
- package/client/src/contexts/ModelContext.jsx +4 -1
- package/client/src/contexts/UIContext.jsx +72 -72
- package/client/src/filter.js +263 -263
- package/client/src/index.css +90 -90
- package/package.json +6 -5
- package/perf/artillery-hooks.js +37 -37
- package/perf/setup.yml +25 -25
- package/src/constants.js +4 -0
- package/src/defaultModels.js +7 -0
- package/src/engine.js +335 -335
- package/src/events.js +232 -137
- package/src/filter.js +274 -274
- package/src/index.js +1 -0
- package/src/modules/assistant/assistant.js +225 -83
- package/src/modules/auth-google/index.js +50 -50
- package/src/modules/auth-microsoft/index.js +81 -81
- package/src/modules/data/data.core.js +118 -118
- package/src/modules/data/data.history.js +555 -555
- package/src/modules/data/data.operations.js +112 -9
- package/src/modules/data/data.relations.js +686 -686
- package/src/modules/data/data.routes.js +1879 -1879
- package/src/modules/data/data.validation.js +2 -2
- package/src/modules/file.js +247 -247
- package/src/modules/user.js +14 -9
- package/src/packs.js +2 -2
- package/src/providers.js +2 -2
- package/src/services/stripe.js +196 -196
- package/src/sso.js +193 -193
- package/test/data.history.integration.test.js +264 -264
- package/test/data.integration.test.js +1281 -1206
- package/test/events.test.js +108 -10
- package/test/model.integration.test.js +377 -377
- 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
|
-
|
|
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
|
-
|
|
990
|
-
if (currentStorageUsage
|
|
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}});
|