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.
- package/README.md +924 -913
- 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 +7 -1
- 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 -806
- 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 -285
- 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/ModelCreatorField.jsx +950 -950
- package/client/src/ModelList.jsx +7 -2
- 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 -262
- package/client/src/index.css +90 -90
- package/package.json +11 -10
- package/perf/artillery-hooks.js +37 -37
- package/perf/setup.yml +25 -25
- package/src/constants.js +4 -0
- package/src/core.js +8 -1
- package/src/engine.js +335 -293
- package/src/events.js +140 -21
- package/src/filter.js +274 -274
- package/src/index.js +3 -0
- package/src/modules/assistant/assistant.js +323 -192
- package/src/modules/assistant/constants.js +2 -1
- 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 -531
- package/src/modules/data/data.operations.js +145 -26
- 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 +1 -0
- package/src/modules/workflow.js +2 -2
- package/src/openai.jobs.js +3 -2
- 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/src/workers/import-export-worker.js +1 -1
- package/test/data.history.integration.test.js +72 -0
- package/test/data.integration.test.js +92 -1
- package/test/events.test.js +108 -10
- package/test/model.integration.test.js +377 -377
|
@@ -1,4 +1,13 @@
|
|
|
1
|
-
import {
|
|
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
|
-
|
|
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
|
|
1060
|
-
const
|
|
1061
|
-
|
|
1062
|
-
|
|
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], ...
|
|
1134
|
+
const dataToValidate = {...existingDocs[0], ...updateSetData};
|
|
1109
1135
|
await validateModelData(dataToValidate, model, false);
|
|
1110
1136
|
} else {
|
|
1111
|
-
await validateModelData(
|
|
1137
|
+
await validateModelData(updateSetData, model, true);
|
|
1112
1138
|
}
|
|
1113
1139
|
|
|
1114
|
-
//
|
|
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 (
|
|
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
|
|
1204
|
-
const finalStateForHash = {...existingDocs[0], ...
|
|
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
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
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
|
|
1223
|
-
|
|
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,
|
|
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
|
|