data-primals-engine 1.6.3 → 1.7.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 +52 -900
- package/client/index.js +3 -0
- package/client/package-lock.json +7563 -8811
- package/client/package.json +11 -1
- package/client/src/App.scss +29 -0
- package/client/src/AssistantChat.jsx +369 -362
- package/client/src/DataEditor.jsx +383 -383
- package/client/src/DataLayout.jsx +54 -20
- package/client/src/ModelList.jsx +280 -280
- package/client/src/ViewSwitcher.scss +0 -31
- package/client/src/_variables.scss +3 -0
- package/client/src/constants.js +81 -100
- package/client/src/contexts/CommandContext.jsx +274 -259
- package/client/vite.config.js +30 -30
- package/doc/AI-assistance.md +93 -0
- package/doc/Advanced-workflows.md +90 -0
- package/doc/Event-system.md +79 -0
- package/doc/Packs-gallery.md +73 -0
- package/doc/automation-workflows.md +102 -0
- package/doc/core-concepts.md +33 -0
- package/doc/custom-api-endpoints.md +40 -0
- package/doc/dashboards-kpis-charts.md +49 -0
- package/doc/data-management.md +120 -0
- package/doc/data-models.md +75 -0
- package/doc/index.md +14 -0
- package/doc/roles-permissions.md +43 -0
- package/doc/users.md +30 -0
- package/package.json +20 -10
- package/src/client.js +6 -4
- package/src/constants.js +1 -1
- package/src/core.js +31 -12
- package/src/defaultModels.js +1 -1
- package/src/engine.js +342 -335
- package/src/filter.js +72 -173
- package/src/migrate.js +1 -1
- package/src/modules/assistant/assistant.js +30 -20
- package/src/modules/data/data.backup.js +4 -4
- package/src/modules/data/data.js +8 -7
- package/src/modules/data/data.operations.js +186 -133
- package/src/modules/data/data.relations.js +3 -2
- package/src/modules/data/data.scheduling.js +1 -1
- package/src/modules/mongodb.js +17 -8
- package/src/modules/swagger.js +25 -5
- package/src/modules/user.js +108 -79
- package/src/modules/workflow.js +138 -3
- package/src/packs.js +5697 -5697
- package/src/profiles.js +19 -0
- package/swagger-en.yml +3391 -1550
- package/swagger-fr.yml +3385 -2896
- package/test/assistant.test.js +2 -2
- package/test/core.test.js +341 -0
- package/test/data.backup.integration.test.js +3 -1
- package/test/data.history.integration.test.js +0 -1
- package/test/data.integration.test.js +137 -2
- package/test/user.test.js +33 -29
|
@@ -24,7 +24,7 @@ import {
|
|
|
24
24
|
optionsSanitizer,
|
|
25
25
|
searchRequestTimeout
|
|
26
26
|
} from "../../constants.js";
|
|
27
|
-
import {anonymizeText, getFieldValueHash, getUserId, isDemoUser, isLocalUser} from "../../data.js";
|
|
27
|
+
import {anonymizeText, encryptValue, getFieldValueHash, getUserId, isDemoUser, isLocalUser} from "../../data.js";
|
|
28
28
|
import sanitizeHtml from "sanitize-html";
|
|
29
29
|
import {importJobs, modelsCache, runCryptoWorkerTask, runImportExportWorker} from "./data.core.js";
|
|
30
30
|
import {
|
|
@@ -37,7 +37,7 @@ import {
|
|
|
37
37
|
} from "../mongodb.js";
|
|
38
38
|
import i18n from "../../i18n.js";
|
|
39
39
|
import tinycolor from 'tinycolor2';
|
|
40
|
-
import {Config} from "../../config.js";
|
|
40
|
+
import { Config } from "../../config.js";
|
|
41
41
|
import {calculateTotalUserStorageUsage, hasPermission} from "../user.js";
|
|
42
42
|
import {BSON, ObjectId} from "mongodb";
|
|
43
43
|
import {runScheduledJobWithDbLock, triggerWorkflows} from "../workflow.js";
|
|
@@ -45,7 +45,7 @@ import schedule from "node-schedule";
|
|
|
45
45
|
import {sendSseToUser} from "./data.routes.js";
|
|
46
46
|
import {applyCronMask, handleScheduledJobs, runStatefulAlertJob} from "./data.scheduling.js";
|
|
47
47
|
import {Event} from "../../events.js";
|
|
48
|
-
import {getAllPacks} from "../../packs.js";
|
|
48
|
+
import { getAllPacks } from "../../packs.js";
|
|
49
49
|
import {validateModelData, validateModelStructure} from "./data.validation.js";
|
|
50
50
|
import {addFile, removeFile} from "../file.js";
|
|
51
51
|
import NodeCache from "node-cache";
|
|
@@ -61,7 +61,8 @@ import {
|
|
|
61
61
|
processDocuments
|
|
62
62
|
} from "./data.relations.js";
|
|
63
63
|
import crypto from 'crypto';
|
|
64
|
-
import cronstrue from 'cronstrue/i18n.js'
|
|
64
|
+
import cronstrue from 'cronstrue/i18n.js'
|
|
65
|
+
import { isConditionMet } from '../../filter.js';
|
|
65
66
|
import util from "node:util";
|
|
66
67
|
|
|
67
68
|
const delay = ms => new Promise(res => setTimeout(res, ms));
|
|
@@ -70,7 +71,7 @@ const IMPORT_CHUNK_DELAY_MS = 1000; // Délai en millisecondes entre le traiteme
|
|
|
70
71
|
|
|
71
72
|
export const dataTypes = {
|
|
72
73
|
object: {
|
|
73
|
-
|
|
74
|
+
validate: (value, field) => {
|
|
74
75
|
return value === null || isPlainObject(value);
|
|
75
76
|
}
|
|
76
77
|
},
|
|
@@ -111,7 +112,13 @@ export const dataTypes = {
|
|
|
111
112
|
const ml = Math.min(Math.max(field.maxlength, 0), m);
|
|
112
113
|
return value === null || typeof value === 'string' && (!ml || value.length <= ml)
|
|
113
114
|
},
|
|
114
|
-
anonymize: anonymizeText
|
|
115
|
+
anonymize: anonymizeText,
|
|
116
|
+
filter: async (value, field) => {
|
|
117
|
+
if (field.encrypted && value) {
|
|
118
|
+
return encryptValue(value);
|
|
119
|
+
}
|
|
120
|
+
return value;
|
|
121
|
+
}
|
|
115
122
|
},
|
|
116
123
|
code: {
|
|
117
124
|
validate: (value, field) => {
|
|
@@ -141,8 +148,12 @@ export const dataTypes = {
|
|
|
141
148
|
const ml = Math.min(Math.max(field.maxlength, 0), m);
|
|
142
149
|
return value === null || typeof value === 'string' && (!ml || value.length <= ml)
|
|
143
150
|
},
|
|
144
|
-
filter: async (value) => {
|
|
145
|
-
|
|
151
|
+
filter: async (value, field) => {
|
|
152
|
+
const sanitized = sanitizeHtml(value, optionsSanitizer);
|
|
153
|
+
if (field.encrypted && sanitized) {
|
|
154
|
+
return encryptValue(sanitized);
|
|
155
|
+
}
|
|
156
|
+
return sanitized;
|
|
146
157
|
},
|
|
147
158
|
anonymize: anonymizeText
|
|
148
159
|
},
|
|
@@ -151,7 +162,7 @@ export const dataTypes = {
|
|
|
151
162
|
if (value === null)
|
|
152
163
|
return true;
|
|
153
164
|
const m = Config.Get('maxStringLength', maxStringLength);
|
|
154
|
-
const ml = Math.min(Math.max(field.maxlength, 0),
|
|
165
|
+
const ml = Math.min(Math.max(field.maxlength, 0), m);
|
|
155
166
|
// La valeur peut être une chaîne de caractères...
|
|
156
167
|
if (typeof value === 'string') {
|
|
157
168
|
return !ml || value.length <= ml;
|
|
@@ -693,14 +704,27 @@ export const getModel = async (modelName, user) => {
|
|
|
693
704
|
export const getModels = async () => {
|
|
694
705
|
return await getCollection('models')?.find({'$or': [{_user: {$exists: false}}]}).toArray() || [];
|
|
695
706
|
}
|
|
696
|
-
export const insertData = async (modelName, data, files, user, triggerWorkflow = true, waitForWorkflow = true) => {
|
|
707
|
+
export const insertData = async (modelName, data, files, user, triggerWorkflow = true, waitForWorkflow = true, checkPermissions = true) => {
|
|
697
708
|
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
await hasPermission(["API_ADD_DATA_NOT_" + modelName], user)
|
|
702
|
-
|
|
703
|
-
|
|
709
|
+
if (checkPermissions) {
|
|
710
|
+
// --- Vérification des permissions ---
|
|
711
|
+
const permissionFilter = await hasPermission(["API_ADMIN", "API_ADD_DATA", "API_ADD_DATA_" + modelName], user);
|
|
712
|
+
const hasDeniedPermission = await hasPermission(["API_ADD_DATA_NOT_" + modelName], user);
|
|
713
|
+
|
|
714
|
+
if (!(isDemoUser(user) && Config.Get("useDemoAccounts")) && isLocalUser(user) && (!permissionFilter || hasDeniedPermission)) {
|
|
715
|
+
return { success: false, error: i18n.t('api.permission.addData', "You do not have permission to add this data."), statusCode: 403 };
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
// Si un filtre de permission existe, valider les données entrantes
|
|
719
|
+
if (typeof permissionFilter === 'object' && Object.keys(permissionFilter).length > 0) {
|
|
720
|
+
const dataArray = Array.isArray(data) ? data : [data];
|
|
721
|
+
const model = await getModel(modelName, user);
|
|
722
|
+
for (const doc of dataArray) {
|
|
723
|
+
if (!isConditionMet(model, permissionFilter, doc, [], user)) {
|
|
724
|
+
return { success: false, error: i18n.t('api.permission.addData.filterViolation', "The data you are trying to add does not meet the required permission criteria."), statusCode: 403 };
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
}
|
|
704
728
|
}
|
|
705
729
|
|
|
706
730
|
const collection = await getCollectionForUser(user);
|
|
@@ -836,7 +860,7 @@ export const insertData = async (modelName, data, files, user, triggerWorkflow =
|
|
|
836
860
|
return {success: true, data: firstInsertedDoc, insertedIds: insertedIds.map(id => id.toString())};
|
|
837
861
|
|
|
838
862
|
} catch (error) { // Attrape les erreurs de permission ou de pushDataUnsecure
|
|
839
|
-
logger
|
|
863
|
+
logger?.error(`[insertData] Main error during insertion process for model ${modelName}: ${error.message}`, error.stack);
|
|
840
864
|
// Renvoyer une structure d'erreur cohérente
|
|
841
865
|
return {
|
|
842
866
|
success: false,
|
|
@@ -1084,27 +1108,40 @@ export const editData = async (modelName, filter, data, files, user, triggerWork
|
|
|
1084
1108
|
const internalEditOrPatchData = async (modelName, filter, data, files, user, isPatch, triggerWorkflow = true, waitForWorkflow = false) => {
|
|
1085
1109
|
try {
|
|
1086
1110
|
// 1. Vérification des permissions
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1111
|
+
const permissionResult = await hasPermission(["API_ADMIN", "API_EDIT_DATA", "API_EDIT_DATA_" + modelName], user);
|
|
1112
|
+
const hasDeniedPermission = await hasPermission(["API_EDIT_DATA_NOT_" + modelName], user);
|
|
1113
|
+
|
|
1114
|
+
if (user.username !== 'demo' && isLocalUser(user) && (!permissionResult || hasDeniedPermission)) {
|
|
1115
|
+
throw new Error(i18n.t("api.permission.editData", "You do not have permission to edit this data."));
|
|
1091
1116
|
}
|
|
1092
1117
|
|
|
1118
|
+
|
|
1093
1119
|
const collection = await getCollectionForUser(user);
|
|
1094
|
-
const model = await modelsCollection.findOne({name: modelName, _user: user.username});
|
|
1120
|
+
const model = await modelsCollection.findOne({$and : [{name: modelName}, {$or: [{_user: user._user}, {_user:user.username}]}]});
|
|
1095
1121
|
if (!model) {
|
|
1096
1122
|
throw new Error(i18n.t("api.model.notFound", {model: modelName}));
|
|
1097
1123
|
}
|
|
1098
1124
|
|
|
1099
|
-
// 2.
|
|
1100
|
-
|
|
1125
|
+
// 2. Combinaison du filtre de la requête et du filtre de permission
|
|
1126
|
+
let combinedFilter = filter;
|
|
1127
|
+
if (permissionResult && typeof permissionResult === 'object' && Object.keys(permissionResult).length > 0) {
|
|
1128
|
+
combinedFilter = {
|
|
1129
|
+
$and: [filter, permissionResult]
|
|
1130
|
+
};
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
// 3. Récupération des documents à modifier en utilisant le filtre combiné
|
|
1134
|
+
const docsToEditQuery = await searchData({model: modelName, filter: combinedFilter}, user);
|
|
1135
|
+
let existingDocs = docsToEditQuery.data;
|
|
1136
|
+
|
|
1101
1137
|
if (!existingDocs || existingDocs.length === 0) {
|
|
1102
|
-
|
|
1138
|
+
throw new Error(i18n.t("api.data.notFound"));
|
|
1103
1139
|
}
|
|
1140
|
+
|
|
1104
1141
|
const ids = existingDocs.map(d => new ObjectId(d._id));
|
|
1105
1142
|
const originalHash = existingDocs[0]._hash; // Sauvegarde du hash avant modification
|
|
1106
1143
|
|
|
1107
|
-
//
|
|
1144
|
+
// 4. Préparation des données de mise à jour en séparant les opérateurs
|
|
1108
1145
|
const updateSetData = {};
|
|
1109
1146
|
const updatePushData = {};
|
|
1110
1147
|
|
|
@@ -1164,15 +1201,10 @@ const internalEditOrPatchData = async (modelName, filter, data, files, user, isP
|
|
|
1164
1201
|
}
|
|
1165
1202
|
}
|
|
1166
1203
|
|
|
1167
|
-
//
|
|
1168
|
-
|
|
1169
|
-
const dataToValidate = {...existingDocs[0], ...updateSetData};
|
|
1170
|
-
await validateModelData(dataToValidate, model, false);
|
|
1171
|
-
} else {
|
|
1172
|
-
await validateModelData(updateSetData, model, true);
|
|
1173
|
-
}
|
|
1204
|
+
// 5. Validation adaptée pour patch ou edit
|
|
1205
|
+
await validateModelData(isPatch ? updateSetData : { ...existingDocs[0], ...updateSetData }, model, isPatch);
|
|
1174
1206
|
|
|
1175
|
-
//
|
|
1207
|
+
// 5.1 Validation pour les opérations $push
|
|
1176
1208
|
for (const fieldName in updatePushData) {
|
|
1177
1209
|
const field = model.fields.find(f => f.name === fieldName);
|
|
1178
1210
|
if (!field) throw new Error(`Le champ '${fieldName}' pour l'opération $push n'existe pas dans le modèle '${model.name}'.`);
|
|
@@ -1197,7 +1229,7 @@ const internalEditOrPatchData = async (modelName, filter, data, files, user, isP
|
|
|
1197
1229
|
}
|
|
1198
1230
|
|
|
1199
1231
|
|
|
1200
|
-
//
|
|
1232
|
+
// 6. Vérification des champs uniques (s'applique uniquement aux opérations $set)
|
|
1201
1233
|
const uniqueFields = model.fields.filter(f => f.unique);
|
|
1202
1234
|
for (const field of uniqueFields) {
|
|
1203
1235
|
if (updateData[field.name] !== undefined) {
|
|
@@ -1216,7 +1248,7 @@ const internalEditOrPatchData = async (modelName, filter, data, files, user, isP
|
|
|
1216
1248
|
}
|
|
1217
1249
|
}
|
|
1218
1250
|
|
|
1219
|
-
//
|
|
1251
|
+
// 7. Traitement des relations (pour $set)
|
|
1220
1252
|
const relationFields = model.fields.filter(f => f.type === 'relation');
|
|
1221
1253
|
for (const field of relationFields) {
|
|
1222
1254
|
if (updateData[field.name] !== undefined) {
|
|
@@ -1240,7 +1272,7 @@ const internalEditOrPatchData = async (modelName, filter, data, files, user, isP
|
|
|
1240
1272
|
}
|
|
1241
1273
|
}
|
|
1242
1274
|
|
|
1243
|
-
//
|
|
1275
|
+
// 8. Application des filtres de champ (pour $set)
|
|
1244
1276
|
for (const field of model.fields) {
|
|
1245
1277
|
// On saute les champs 'file' car ils ont déjà été traités
|
|
1246
1278
|
if (field.type !== 'file' && field.itemsType !== 'file' && updateData[field.name] !== undefined && dataTypes[field.type]?.filter) {
|
|
@@ -1286,7 +1318,7 @@ const internalEditOrPatchData = async (modelName, filter, data, files, user, isP
|
|
|
1286
1318
|
}
|
|
1287
1319
|
}
|
|
1288
1320
|
|
|
1289
|
-
//
|
|
1321
|
+
// 9. Vérification des limites de stockage avant la mise à jour
|
|
1290
1322
|
const originalDocsSize = calculateDataSize(existingDocs);
|
|
1291
1323
|
const finalStateForSize = existingDocs.map(doc => {
|
|
1292
1324
|
const updatedDoc = { ...doc, ...updateSetData };
|
|
@@ -1323,7 +1355,7 @@ const internalEditOrPatchData = async (modelName, filter, data, files, user, isP
|
|
|
1323
1355
|
}
|
|
1324
1356
|
}
|
|
1325
1357
|
|
|
1326
|
-
//
|
|
1358
|
+
// 10. Calcul du nouveau hash en simulant l'état final
|
|
1327
1359
|
const finalStateForHash = { ...existingDocs[0], ...updateSetData };
|
|
1328
1360
|
for (const fieldName in updatePushData) {
|
|
1329
1361
|
let itemsToAdd;
|
|
@@ -1358,7 +1390,7 @@ const internalEditOrPatchData = async (modelName, filter, data, files, user, isP
|
|
|
1358
1390
|
}
|
|
1359
1391
|
}
|
|
1360
1392
|
|
|
1361
|
-
//
|
|
1393
|
+
// 11. *** CORRECTION LOGIQUE ***
|
|
1362
1394
|
// On ne vérifie l'unicité que si le hash a réellement changé.
|
|
1363
1395
|
if (newHash !== originalHash) {
|
|
1364
1396
|
const hashCheck = await checkHash(user, model, newHash, existingDocs[0]._id.toString());
|
|
@@ -1368,7 +1400,7 @@ const internalEditOrPatchData = async (modelName, filter, data, files, user, isP
|
|
|
1368
1400
|
}
|
|
1369
1401
|
}
|
|
1370
1402
|
|
|
1371
|
-
//
|
|
1403
|
+
// 12. Exécution de la mise à jour.
|
|
1372
1404
|
// Si une opération $push est présente, nous devons utiliser une pipeline d'agrégation
|
|
1373
1405
|
// pour gérer de manière robuste le cas où le champ cible est `null` ou n'existe pas.
|
|
1374
1406
|
let updateCommand;
|
|
@@ -1430,7 +1462,7 @@ const internalEditOrPatchData = async (modelName, filter, data, files, user, isP
|
|
|
1430
1462
|
});
|
|
1431
1463
|
}
|
|
1432
1464
|
|
|
1433
|
-
//
|
|
1465
|
+
// 13. Tâches post-mise à jour (schedules, workflows) (inchangé)
|
|
1434
1466
|
if (["workflowTrigger", "alert"].includes(modelName)) {
|
|
1435
1467
|
await handleScheduledJobs(modelName, existingDocs, collection, finalUpdateOperation.$set || {});
|
|
1436
1468
|
}
|
|
@@ -1452,47 +1484,72 @@ const internalEditOrPatchData = async (modelName, filter, data, files, user, isP
|
|
|
1452
1484
|
};
|
|
1453
1485
|
|
|
1454
1486
|
} catch (error) {
|
|
1455
|
-
logger
|
|
1487
|
+
logger?.error("Erreur lors de la mise à jour de la ressource :", error);
|
|
1456
1488
|
return {success: false, error: error.message};
|
|
1457
1489
|
}
|
|
1458
1490
|
};
|
|
1459
1491
|
export const deleteData = async (modelName, filter, user = {}, triggerWorkflow, waitForWorkflow = false) => {
|
|
1460
1492
|
|
|
1461
1493
|
try {
|
|
1462
|
-
const
|
|
1494
|
+
const permissionFilter = await hasPermission(["API_ADMIN", "API_DELETE_DATA", "API_DELETE_DATA_" + modelName], user);
|
|
1495
|
+
const hasDeniedPermission = await hasPermission(["API_DELETE_DATA_NOT_" + modelName], user);
|
|
1463
1496
|
|
|
1464
|
-
|
|
1497
|
+
if (user?.username !== 'demo' && isLocalUser(user) && (!permissionFilter || hasDeniedPermission)) {
|
|
1498
|
+
// Si l'utilisateur n'a pas la permission de base, on arrête tout de suite.
|
|
1499
|
+
return { success: false, error: i18n.t("api.permission.deleteData", "You do not have permission to delete this data."), deletedCount: 0 };
|
|
1500
|
+
}
|
|
1465
1501
|
|
|
1466
|
-
|
|
1467
|
-
let findFilter = [];
|
|
1468
|
-
if (user)
|
|
1469
|
-
findFilter.push({
|
|
1470
|
-
'$eq': ["$_user", user.username]
|
|
1471
|
-
});
|
|
1502
|
+
const collection = await getCollectionForUser(user);
|
|
1472
1503
|
|
|
1473
|
-
//
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
}
|
|
1504
|
+
// 1. Construire le filtre de recherche.
|
|
1505
|
+
// Nous allons utiliser un seul objet $match avec un $and global pour combiner toutes les conditions.
|
|
1506
|
+
const matchConditions = [];
|
|
1477
1507
|
|
|
1478
|
-
//
|
|
1479
|
-
if (
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1508
|
+
// Conditions de base (modèle et utilisateur)
|
|
1509
|
+
if (user) {
|
|
1510
|
+
matchConditions.push({ $expr: { '$eq': ["$_user", user.username] } });
|
|
1511
|
+
}
|
|
1512
|
+
if (modelName) {
|
|
1513
|
+
matchConditions.push({ _model: modelName });
|
|
1514
|
+
}
|
|
1483
1515
|
|
|
1484
|
-
//
|
|
1485
|
-
if (filter &&
|
|
1486
|
-
//
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1516
|
+
// Filtre de la requête (par ID ou autre)
|
|
1517
|
+
if (Array.isArray(filter) && filter.length > 0 && isObjectId(filter[0])) {
|
|
1518
|
+
// Filtre par tableau d'IDs
|
|
1519
|
+
matchConditions.push({ _id: { "$in": filter.map(m => new ObjectId(m)) } });
|
|
1520
|
+
} else if (filter && typeof filter === 'object' && Object.keys(filter).length > 0) {
|
|
1521
|
+
const firstKey = Object.keys(filter)[0];
|
|
1522
|
+
// Si le filtre est une expression d'agrégation (commence par $), on l'encapsule dans $expr
|
|
1523
|
+
if (firstKey.startsWith('$')) {
|
|
1524
|
+
matchConditions.push({ $expr: filter });
|
|
1525
|
+
} else {
|
|
1526
|
+
// Sinon, on le traite comme un filtre de document standard
|
|
1527
|
+
const processedFilter = {};
|
|
1528
|
+
for (const key in filter) {
|
|
1529
|
+
if (key === '_id' && !isObjectId(filter[key])) {
|
|
1530
|
+
// S'assurer que la conversion ne se fait que si la chaîne est un ID valide
|
|
1531
|
+
if (ObjectId.isValid(filter[key])) {
|
|
1532
|
+
processedFilter[key] = new ObjectId(filter[key]);
|
|
1533
|
+
} else {
|
|
1534
|
+
processedFilter[key] = filter[key]; // Garder la valeur originale si non valide
|
|
1535
|
+
}
|
|
1536
|
+
} else {
|
|
1537
|
+
processedFilter[key] = filter[key];
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
matchConditions.push(processedFilter);
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
1491
1543
|
|
|
1544
|
+
// Filtre de permission
|
|
1545
|
+
if (permissionFilter && typeof permissionFilter === 'object') {
|
|
1546
|
+
matchConditions.push(permissionFilter);
|
|
1492
1547
|
}
|
|
1493
1548
|
|
|
1549
|
+
const findFilter = matchConditions.length > 0 ? { $and: matchConditions } : {};
|
|
1550
|
+
|
|
1494
1551
|
// 2. Récupérer les documents à supprimer pour vérifier leur type et annuler les schedules
|
|
1495
|
-
const documentsToDelete = await collection.aggregate([{$match:
|
|
1552
|
+
const documentsToDelete = await collection.aggregate([{ $match: findFilter }]).toArray();
|
|
1496
1553
|
|
|
1497
1554
|
if (documentsToDelete.length === 0) {
|
|
1498
1555
|
logger.info(`[deleteData] No documents found matching the criteria for user ${user?.username}.`);
|
|
@@ -1531,15 +1588,6 @@ export const deleteData = async (modelName, filter, user = {}, triggerWorkflow,
|
|
|
1531
1588
|
await Promise.allSettled(deletePromises);
|
|
1532
1589
|
}
|
|
1533
1590
|
|
|
1534
|
-
// Vérification des permissions (pour chaque document trouvé)
|
|
1535
|
-
if (user?.username !== 'demo' && isLocalUser(user) && (
|
|
1536
|
-
!await hasPermission(["API_ADMIN", "API_DELETE_DATA", "API_DELETE_DATA_" + docToDelete._model], user) ||
|
|
1537
|
-
await hasPermission(["API_DELETE_DATA_NOT_" + docToDelete._model], user))) {
|
|
1538
|
-
// Si l'utilisateur n'a pas la permission pour CE document spécifique, on l'ignore
|
|
1539
|
-
logger.warn(`[deleteData] User ${user.username} lacks permission to delete document ${docToDelete._id} of model ${docToDelete._model}. Skipping.`);
|
|
1540
|
-
continue; // Passe au document suivant
|
|
1541
|
-
}
|
|
1542
|
-
|
|
1543
1591
|
// *** Ajout de l'annulation du schedule pour workflowTrigger ***
|
|
1544
1592
|
if (docToDelete._model === 'workflowTrigger') {
|
|
1545
1593
|
const jobId = `workflowTrigger_${docToDelete._id}`;
|
|
@@ -1678,9 +1726,10 @@ export const deleteData = async (modelName, filter, user = {}, triggerWorkflow,
|
|
|
1678
1726
|
} else {
|
|
1679
1727
|
logger.info(`[deleteData] No documents to delete for user ${user?.username} after permission checks or matching criteria.`);
|
|
1680
1728
|
}
|
|
1681
|
-
|
|
1682
1729
|
const res = { success: true, deletedCount };
|
|
1683
1730
|
|
|
1731
|
+
if (!modelNameToInvalidate)
|
|
1732
|
+
return res;
|
|
1684
1733
|
// --- CORRECTION ---
|
|
1685
1734
|
// The event payload must match what the history listener expects: { modelName, user, before }.
|
|
1686
1735
|
// 'documentsToDelete' contains the full documents before they were deleted.
|
|
@@ -1807,7 +1856,6 @@ export const searchData = async (query, user) => {
|
|
|
1807
1856
|
// Vérifier si les données sont en cache
|
|
1808
1857
|
const cachedData = searchCache.get(cacheKey);
|
|
1809
1858
|
if (cachedData) {
|
|
1810
|
-
console.log('Cache hit for key:', cacheKey);
|
|
1811
1859
|
return cachedData;
|
|
1812
1860
|
}
|
|
1813
1861
|
|
|
@@ -3052,7 +3100,6 @@ export const exportData = async (options, user) => {
|
|
|
3052
3100
|
* Installs pack models and data for a user or globally.
|
|
3053
3101
|
* Can accept either a pack ID (to install from database) or a direct pack JSON object.
|
|
3054
3102
|
*
|
|
3055
|
-
* @param {object} logger - Logger instance
|
|
3056
3103
|
* @param {string|object} packIdentifier - Either pack ID (string) or pack JSON object
|
|
3057
3104
|
* @param {object|null} user - User object (if installing for user) or null (for global install)
|
|
3058
3105
|
* @param {string} [lang='en'] - Language code for localized data
|
|
@@ -3061,7 +3108,6 @@ export const exportData = async (options, user) => {
|
|
|
3061
3108
|
export async function installPack(packIdentifier, user = null, lang = 'en', options = {}) {
|
|
3062
3109
|
let pack;
|
|
3063
3110
|
const packsCollection = getCollection('packs');
|
|
3064
|
-
|
|
3065
3111
|
// Determine if we're working with an ID or direct pack object
|
|
3066
3112
|
if (typeof packIdentifier === 'string') {
|
|
3067
3113
|
let p;
|
|
@@ -3101,12 +3147,19 @@ export async function installPack(packIdentifier, user = null, lang = 'en', opti
|
|
|
3101
3147
|
throw new Error('Invalid pack identifier - must be either pack ID string or pack object');
|
|
3102
3148
|
}
|
|
3103
3149
|
|
|
3150
|
+
// Vérifier si le pack est déjà installé pour cet utilisateur
|
|
3151
|
+
const existingPack = await packsCollection.findOne({ name: pack.name, _user: user?.username });
|
|
3152
|
+
if (existingPack) {
|
|
3153
|
+
logger?.warn(`Pack '${pack.name}' is already installed for user '${user?.username}'. Skipping installation.`);
|
|
3154
|
+
return { success: true, summary: {}, errors: [], modifiedCount: 0 };
|
|
3155
|
+
}
|
|
3156
|
+
|
|
3104
3157
|
const username = user ? user.username : null;
|
|
3105
3158
|
const logPrefix = username
|
|
3106
3159
|
? `Installing pack '${pack.name}' for user '${username}'`
|
|
3107
3160
|
: `Installing pack '${pack.name}' globally`;
|
|
3108
3161
|
|
|
3109
|
-
logger
|
|
3162
|
+
logger?.info(`--- ${logPrefix} ---`);
|
|
3110
3163
|
|
|
3111
3164
|
const summary = {
|
|
3112
3165
|
models: {installed: [], skipped: [], failed: []},
|
|
@@ -3121,9 +3174,8 @@ export async function installPack(packIdentifier, user = null, lang = 'en', opti
|
|
|
3121
3174
|
if (Array.isArray(pack.models)) {
|
|
3122
3175
|
// For user installs, check existing models
|
|
3123
3176
|
const existingModels = user
|
|
3124
|
-
? await modelsCollection.find({_user: username}).toArray()
|
|
3177
|
+
? await modelsCollection.find({_user: user.username}).toArray()
|
|
3125
3178
|
: await modelsCollection.find({_user: {$exists: false}}).toArray();
|
|
3126
|
-
|
|
3127
3179
|
const existingModelNames = existingModels.map(m => m.name);
|
|
3128
3180
|
|
|
3129
3181
|
for (let i = 0; i < pack.models.length; i++) {
|
|
@@ -3132,8 +3184,9 @@ export async function installPack(packIdentifier, user = null, lang = 'en', opti
|
|
|
3132
3184
|
const modelName = typeof modelOrName === 'string' ? modelOrName : modelOrName?.name;
|
|
3133
3185
|
if (!modelName) throw new Error('Model definition in pack is missing a name.');
|
|
3134
3186
|
|
|
3135
|
-
|
|
3136
|
-
|
|
3187
|
+
const r = typeof user?.username === 'string' && await modelsCollection.findOne({name: modelName, _user: user.username || /.*/});
|
|
3188
|
+
if (r ) {
|
|
3189
|
+
logger?.debug(`[Model Install] Skipping '${modelName}': already exists`);
|
|
3137
3190
|
summary.models.skipped.push(modelName);
|
|
3138
3191
|
continue;
|
|
3139
3192
|
}
|
|
@@ -3160,7 +3213,7 @@ export async function installPack(packIdentifier, user = null, lang = 'en', opti
|
|
|
3160
3213
|
await modelsCollection.insertOne(preparedModel);
|
|
3161
3214
|
summary.models.installed.push(modelName);
|
|
3162
3215
|
} catch (e) {
|
|
3163
|
-
logger
|
|
3216
|
+
logger?.error(e);
|
|
3164
3217
|
const modelName = typeof modelOrName === 'string' ? modelOrName : modelOrName?.name || 'unknown';
|
|
3165
3218
|
errors.push(`Failed to install model '${modelName}': ${e.message}`);
|
|
3166
3219
|
summary.models.failed.push(modelName);
|
|
@@ -3171,8 +3224,8 @@ export async function installPack(packIdentifier, user = null, lang = 'en', opti
|
|
|
3171
3224
|
// --- PHASE 2: DATA INSTALLATION ---
|
|
3172
3225
|
const dataToInstall = {...pack.data?.all, ...pack.data?.[lang]};
|
|
3173
3226
|
if (!dataToInstall || Object.keys(dataToInstall).length === 0) {
|
|
3174
|
-
logger
|
|
3175
|
-
return {success:
|
|
3227
|
+
logger?.warn(`Pack '${pack.name}' has no data to install.`);
|
|
3228
|
+
return {success: errors.length === 0, summary, errors, modifiedCount: 0};
|
|
3176
3229
|
}
|
|
3177
3230
|
|
|
3178
3231
|
// Process link references (same as original)
|
|
@@ -3205,49 +3258,49 @@ export async function installPack(packIdentifier, user = null, lang = 'en', opti
|
|
|
3205
3258
|
const documents = dataToInstall[modelName];
|
|
3206
3259
|
if (documents.length === 0) continue;
|
|
3207
3260
|
|
|
3208
|
-
|
|
3209
|
-
|
|
3261
|
+
try {
|
|
3262
|
+
const docsToInsert = [];
|
|
3263
|
+
const modelDefForHash = await getModel(modelName, user);
|
|
3210
3264
|
|
|
3211
|
-
|
|
3212
|
-
|
|
3265
|
+
for (const docSource of documents) {
|
|
3266
|
+
let docForInsert = {...docSource};
|
|
3213
3267
|
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3268
|
+
// Clear $link fields for first pass
|
|
3269
|
+
for (const key in docForInsert) {
|
|
3270
|
+
if (isPlainObject(docForInsert[key]) && docForInsert[key].$link) {
|
|
3271
|
+
docForInsert[key] = null;
|
|
3272
|
+
}
|
|
3218
3273
|
}
|
|
3219
|
-
}
|
|
3220
3274
|
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
|
|
3275
|
+
const tempId = docForInsert._temp_pack_id;
|
|
3276
|
+
delete docForInsert._id;
|
|
3277
|
+
delete docForInsert._temp_pack_id;
|
|
3224
3278
|
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
docForInsert._model = modelName;
|
|
3228
|
-
docForInsert._hash = getFieldValueHash(modelDefForHash, docForInsert);
|
|
3279
|
+
if (user) docForInsert._user = username;
|
|
3229
3280
|
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
3281
|
+
docForInsert._model = modelName;
|
|
3282
|
+
docForInsert._hash = getFieldValueHash(modelDefForHash, docForInsert);
|
|
3283
|
+
|
|
3284
|
+
// Check for existing document
|
|
3285
|
+
const existingQuery = {
|
|
3286
|
+
_hash: docForInsert._hash,
|
|
3287
|
+
_model: modelName
|
|
3288
|
+
};
|
|
3235
3289
|
|
|
3236
|
-
|
|
3237
|
-
|
|
3290
|
+
if (docForInsert._env) existingQuery._env = docForInsert._env;
|
|
3291
|
+
if (user) existingQuery._user = username;
|
|
3238
3292
|
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3293
|
+
const existingDoc = await collection.findOne(existingQuery, {projection: {_id: 1}});
|
|
3294
|
+
if (existingDoc) {
|
|
3295
|
+
tempIdToNewIdMap[tempId] = existingDoc._id;
|
|
3296
|
+
summary.datas.skipped++;
|
|
3297
|
+
} else {
|
|
3298
|
+
docForInsert._temp_pack_id_for_mapping = tempId;
|
|
3299
|
+
docsToInsert.push(docForInsert);
|
|
3300
|
+
}
|
|
3246
3301
|
}
|
|
3247
|
-
}
|
|
3248
3302
|
|
|
3249
|
-
|
|
3250
|
-
try {
|
|
3303
|
+
if (docsToInsert.length > 0) {
|
|
3251
3304
|
const finalDocsToInsert = docsToInsert.map(d => {
|
|
3252
3305
|
const doc = {...d};
|
|
3253
3306
|
delete doc._temp_pack_id_for_mapping;
|
|
@@ -3265,22 +3318,22 @@ export async function installPack(packIdentifier, user = null, lang = 'en', opti
|
|
|
3265
3318
|
tempIdToNewIdMap[doc._temp_pack_id_for_mapping] = result.insertedIds[index];
|
|
3266
3319
|
}
|
|
3267
3320
|
});
|
|
3268
|
-
} catch (e) {
|
|
3269
|
-
summary.datas.failed += docsToInsert.length;
|
|
3270
|
-
errors.push(`Error inserting batch for ${modelName}: ${e.message}`);
|
|
3271
|
-
logger.error(`[Pack Install] Error on insertMany for model ${modelName}:`, e);
|
|
3272
3321
|
}
|
|
3322
|
+
} catch (e) {
|
|
3323
|
+
summary.datas.failed += documents.length;
|
|
3324
|
+
errors.push(`Error processing model ${modelName}: ${e.message}`);
|
|
3325
|
+
logger?.error(`[Pack Install] Error processing model ${modelName}:`, e);
|
|
3273
3326
|
}
|
|
3274
3327
|
}
|
|
3275
3328
|
|
|
3276
3329
|
// --- PASS 2: REFERENCE LINKING ---
|
|
3277
|
-
logger
|
|
3330
|
+
logger?.info(`[Pack Install] Starting Pass 2: Linking ${linkQueue.length} references`);
|
|
3278
3331
|
for (const linkOp of linkQueue) {
|
|
3279
3332
|
const {sourceTempId, sourceModelName, fieldName, linkSelector} = linkOp;
|
|
3280
3333
|
const sourceId = tempIdToNewIdMap[sourceTempId];
|
|
3281
3334
|
|
|
3282
3335
|
if (!sourceId) {
|
|
3283
|
-
logger
|
|
3336
|
+
logger?.warn(`[LINK FAILED] Could not find newly inserted document for temp ID ${sourceTempId}. Skipping link.`);
|
|
3284
3337
|
continue;
|
|
3285
3338
|
}
|
|
3286
3339
|
|
|
@@ -3292,7 +3345,7 @@ export async function installPack(packIdentifier, user = null, lang = 'en', opti
|
|
|
3292
3345
|
const fieldDef = sourceModelDef.fields.find(f => f.name === fieldName);
|
|
3293
3346
|
|
|
3294
3347
|
if (!fieldDef) {
|
|
3295
|
-
logger
|
|
3348
|
+
logger?.warn(`[LINK FAILED] Field '${fieldName}' not found in source model '${sourceModelName}'`);
|
|
3296
3349
|
errors.push(`[LINK FAILED] Field '${fieldName}' not found in source model '${sourceModelName}'`);
|
|
3297
3350
|
summary.datas.failed++;
|
|
3298
3351
|
continue;
|
|
@@ -3306,7 +3359,7 @@ export async function installPack(packIdentifier, user = null, lang = 'en', opti
|
|
|
3306
3359
|
|
|
3307
3360
|
if (!targetDocs || targetDocs.length === 0) {
|
|
3308
3361
|
const errorMsg = `[LINK FAILED] No target found for ${JSON.stringify(linkSelector)}`;
|
|
3309
|
-
logger
|
|
3362
|
+
logger?.warn(errorMsg);
|
|
3310
3363
|
errors.push(errorMsg);
|
|
3311
3364
|
summary.datas.failed++;
|
|
3312
3365
|
continue;
|
|
@@ -3325,7 +3378,7 @@ export async function installPack(packIdentifier, user = null, lang = 'en', opti
|
|
|
3325
3378
|
|
|
3326
3379
|
} catch (e) {
|
|
3327
3380
|
const errorMsg = `[LINK CRITICAL] Error linking ${sourceModelName}.${fieldName}: ${e.message}`;
|
|
3328
|
-
logger
|
|
3381
|
+
logger?.error(errorMsg, e.stack);
|
|
3329
3382
|
errors.push(errorMsg);
|
|
3330
3383
|
summary.datas.failed++;
|
|
3331
3384
|
}
|
|
@@ -3334,7 +3387,7 @@ export async function installPack(packIdentifier, user = null, lang = 'en', opti
|
|
|
3334
3387
|
if (options.installForUser && user?.username) {
|
|
3335
3388
|
if (pack.name)
|
|
3336
3389
|
await packsCollection.deleteOne({name: pack.name, _user: user.username});
|
|
3337
|
-
logger
|
|
3390
|
+
logger?.info(`--- Creating pack '${pack.name}' for user... ---`);
|
|
3338
3391
|
const packToCreate = {...pack, _id: undefined, private: true, _user: user.username};
|
|
3339
3392
|
await packsCollection.insertOne(packToCreate);
|
|
3340
3393
|
}
|
|
@@ -3345,7 +3398,7 @@ export async function installPack(packIdentifier, user = null, lang = 'en', opti
|
|
|
3345
3398
|
}
|
|
3346
3399
|
|
|
3347
3400
|
const modifiedCount = summary.datas.inserted + summary.datas.updated;
|
|
3348
|
-
logger
|
|
3401
|
+
logger?.info(`--- ${logPrefix} completed ---`);
|
|
3349
3402
|
return {
|
|
3350
3403
|
success: errors.length === 0,
|
|
3351
3404
|
summary,
|
|
@@ -534,7 +534,7 @@ function prepareDocument(doc, model, me) {
|
|
|
534
534
|
}
|
|
535
535
|
|
|
536
536
|
docToProcess._model = model.name;
|
|
537
|
-
docToProcess._user = me.
|
|
537
|
+
docToProcess._user = me.username || me._user;
|
|
538
538
|
docToProcess._hash = getFieldValueHash(model, docToProcess);
|
|
539
539
|
|
|
540
540
|
return docToProcess;
|
|
@@ -581,7 +581,8 @@ export const checkHash = async (me, model, hash, excludeId = null) => {
|
|
|
581
581
|
const query = {
|
|
582
582
|
_model: model.name,
|
|
583
583
|
_hash: hash,
|
|
584
|
-
...(excludeId && {_id: {$ne: new ObjectId(excludeId)}})
|
|
584
|
+
...(excludeId && {_id: {$ne: new ObjectId(excludeId)}}),
|
|
585
|
+
_user: me.username
|
|
585
586
|
};
|
|
586
587
|
|
|
587
588
|
console.log("Query being executed:", JSON.stringify(query, null, 2));
|