data-primals-engine 1.2.6 → 1.3.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/CHANGELOG.md +440 -0
- package/README.md +782 -712
- package/client/src/App.jsx +13 -20
- package/client/src/App.scss +4 -0
- package/client/src/AssistantChat.jsx +5 -4
- package/client/src/DataEditor.jsx +1 -1
- package/client/src/DataLayout.jsx +2 -2
- package/client/src/DataTable.jsx +66 -5
- package/client/src/ExportDialog.jsx +2 -2
- package/client/src/Field.jsx +5 -4
- package/client/src/HistoryDialog.jsx +96 -0
- package/client/src/HistoryDialog.scss +73 -0
- package/client/src/KanbanCard.jsx +4 -2
- package/client/src/KanbanConfigModal.jsx +5 -7
- package/client/src/ModelCreator.jsx +36 -15
- package/client/src/ModelCreatorField.jsx +14 -15
- package/client/src/PackGallery.jsx +89 -9
- package/client/src/PackGallery.scss +58 -4
- package/client/src/RTETrans.jsx +11 -0
- package/client/src/RelationValue.jsx +3 -4
- package/client/src/constants.js +1 -1
- package/client/src/core/data.js +2 -1
- package/client/src/hooks/useTutorials.jsx +1 -1
- package/client/src/translations.js +217 -0
- package/package.json +9 -2
- package/server.js +4 -4
- package/src/HistoryDialog.jsx +86 -0
- package/src/HistoryDialog.scss +77 -0
- package/src/constants.js +6 -0
- package/src/defaultModels.js +23 -10
- package/src/email.js +1 -1
- package/src/engine.js +6 -6
- package/src/events.js +15 -5
- package/src/gameObject.js +0 -29
- package/src/i18n.js +1 -1
- package/src/modules/{assistant.js → assistant/assistant.js} +42 -27
- package/src/modules/assistant/constants.js +16 -0
- package/src/modules/data/data.history.js +304 -0
- package/src/modules/data/data.js +306 -238
- package/src/modules/data/data.routes.js +33 -7
- package/src/modules/mongodb.js +17 -1
- package/src/modules/user.js +21 -4
- package/src/modules/workflow.js +133 -48
- package/src/packs.js +1016 -9
- package/src/services/index.js +11 -0
- package/src/services/stripe.js +195 -0
- package/src/workers/crypto-worker.js +3 -3
- package/test/data.backup.integration.test.js +5 -0
- package/test/data.history.integration.test.js +193 -0
- package/test/data.integration.test.js +144 -41
- package/test/events.test.js +27 -27
- package/test/globalTeardown.js +1 -0
- package/test/model.integration.test.js +5 -0
- package/test/workflow.actions.integration.test.js +4 -2
- package/test/workflow.integration.test.js +3 -1
- package/test/workflow.robustness.test.js +2 -0
package/src/modules/data/data.js
CHANGED
|
@@ -1,35 +1,25 @@
|
|
|
1
1
|
import {Logger} from "../../gameObject.js";
|
|
2
2
|
import {BSON, ObjectId} from "mongodb";
|
|
3
|
-
import
|
|
3
|
+
import {promisify} from 'node:util';
|
|
4
4
|
import crypto from "node:crypto";
|
|
5
|
-
import {
|
|
5
|
+
import {execFile} from 'node:child_process';
|
|
6
6
|
import sanitizeHtml from 'sanitize-html';
|
|
7
7
|
import * as tar from "tar";
|
|
8
8
|
import process from "node:process";
|
|
9
9
|
import {randomColor} from "randomcolor";
|
|
10
10
|
import cronstrue from 'cronstrue/i18n.js';
|
|
11
11
|
import {mkdir} from 'node:fs/promises';
|
|
12
|
-
import {
|
|
13
|
-
|
|
14
|
-
getDefaultForType,
|
|
15
|
-
getFieldValueHash,
|
|
16
|
-
getUserId,
|
|
17
|
-
isDemoUser,
|
|
18
|
-
isLocalUser
|
|
19
|
-
} from "../../data.js";
|
|
12
|
+
import {onInit as historyInit} from "./data.history.js";
|
|
13
|
+
import {anonymizeText, getDefaultForType, getFieldValueHash, getUserId, isDemoUser, isLocalUser} from "../../data.js";
|
|
20
14
|
import {
|
|
21
15
|
allowedFields,
|
|
22
16
|
dbName,
|
|
23
17
|
install,
|
|
24
18
|
maxAlertsPerUser,
|
|
25
|
-
maxBytesPerSecondThrottleData,
|
|
26
19
|
maxExportCount,
|
|
27
20
|
maxFileSize,
|
|
28
21
|
maxFilterDepth,
|
|
29
|
-
maxMagnetsDataPerModel,
|
|
30
|
-
maxMagnetsModels,
|
|
31
22
|
maxModelNameLength,
|
|
32
|
-
maxModelsPerUser,
|
|
33
23
|
maxPasswordLength,
|
|
34
24
|
maxPostData,
|
|
35
25
|
maxRelationsPerData,
|
|
@@ -43,6 +33,7 @@ import {
|
|
|
43
33
|
storageSafetyMargin
|
|
44
34
|
} from "../../constants.js";
|
|
45
35
|
import {
|
|
36
|
+
createCollection,
|
|
46
37
|
getCollection,
|
|
47
38
|
getCollectionForUser,
|
|
48
39
|
getUserCollectionName,
|
|
@@ -52,7 +43,7 @@ import {
|
|
|
52
43
|
} from "../mongodb.js";
|
|
53
44
|
import {dbUrl, MongoClient, MongoDatabase} from "../../engine.js";
|
|
54
45
|
import path from "node:path";
|
|
55
|
-
import {
|
|
46
|
+
import {getObjectHash, getRandom, isGUID, isPlainObject, randomDate} from "../../core.js";
|
|
56
47
|
import {Event} from "../../events.js";
|
|
57
48
|
import fs from "node:fs";
|
|
58
49
|
import schedule from "node-schedule";
|
|
@@ -67,12 +58,9 @@ import {
|
|
|
67
58
|
import NodeCache from "node-cache";
|
|
68
59
|
import AWS from 'aws-sdk';
|
|
69
60
|
import checkDiskSpace from "check-disk-space";
|
|
70
|
-
import {addFile,
|
|
61
|
+
import {addFile, removeFile} from "../file.js";
|
|
71
62
|
import {downloadFromS3, getUserS3Config, listS3Backups, uploadToS3} from "../bucket.js";
|
|
72
|
-
import {
|
|
73
|
-
calculateTotalUserStorageUsage,
|
|
74
|
-
hasPermission
|
|
75
|
-
} from "../user.js";
|
|
63
|
+
import {calculateTotalUserStorageUsage, hasPermission, middlewareAuthenticator} from "../user.js";
|
|
76
64
|
import {getAllPacks} from "../../packs.js";
|
|
77
65
|
import {Config} from "../../config.js";
|
|
78
66
|
import {profiles} from "../../../client/src/constants.js";
|
|
@@ -193,7 +181,7 @@ export const dataTypes = {
|
|
|
193
181
|
}
|
|
194
182
|
return value;
|
|
195
183
|
},
|
|
196
|
-
anonymize: anonymizeText
|
|
184
|
+
anonymize: (str) => anonymizeText(typeof(str) ==='object' ? JSON.stringify(str): str)
|
|
197
185
|
},
|
|
198
186
|
richtext: {
|
|
199
187
|
validate: (value, field) => {
|
|
@@ -500,53 +488,8 @@ export const getAPILang = (langs) => {
|
|
|
500
488
|
}
|
|
501
489
|
|
|
502
490
|
|
|
503
|
-
export function validateModelStructure(modelStructure) {
|
|
504
|
-
|
|
505
|
-
const objectKeys = Object.keys(modelStructure);
|
|
506
|
-
if( objectKeys.find(o => !["name", "_user", "locked", "_id", "description", "maxRequestData", "fields"].includes(o)) ){
|
|
507
|
-
throw new Error(i18n.t('api.model.invalidStructure'));
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
// Vérification du type de name
|
|
511
|
-
if (typeof modelStructure.name !== 'string' || !modelStructure.name) {
|
|
512
|
-
throw new Error(i18n.t("api.validate.requiredFieldString", ["name"]));
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
// Vérification du type de description
|
|
516
|
-
if (typeof modelStructure.description !== 'string') {
|
|
517
|
-
throw new Error(i18n.t("api.validate.fieldString", ["description"]));
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
// Vérification de la présence et du type du tableau fields
|
|
521
|
-
if (!Array.isArray(modelStructure.fields)) {
|
|
522
|
-
throw new Error(i18n.t('api.validate.fieldArray', ["fields"]));
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
// Vérification de chaque champ dans le tableau fields
|
|
526
|
-
for (const field of modelStructure.fields) {
|
|
527
|
-
validateField(field);
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
if (modelStructure.constraints) {
|
|
531
|
-
if (!Array.isArray(modelStructure.constraints)) {
|
|
532
|
-
throw new Error('Model "constraints" property must be an array.');
|
|
533
|
-
}
|
|
534
|
-
const fieldNames = new Set(modelStructure.fields.map(f => f.name));
|
|
535
|
-
for (const constraint of modelStructure.constraints) {
|
|
536
|
-
if (constraint.type === 'unique') {
|
|
537
|
-
if (!constraint.name || !Array.isArray(constraint.keys) || constraint.keys.length === 0) {
|
|
538
|
-
throw new Error('Unique constraint must have a "name" and a non-empty "keys" array.');
|
|
539
|
-
}
|
|
540
|
-
for (const key of constraint.keys) {
|
|
541
|
-
if (!fieldNames.has(key)) {
|
|
542
|
-
throw new Error(`Constraint key "${key}" in constraint "${constraint.name}" does not exist as a field in the model.`);
|
|
543
|
-
}
|
|
544
|
-
}
|
|
545
|
-
}
|
|
546
|
-
}
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
return true; // La structure du modèle est valide
|
|
491
|
+
export async function validateModelStructure(modelStructure) {
|
|
492
|
+
return await Event.Trigger("OnValidateModelStructure", "event", "system", modelStructure);
|
|
550
493
|
}
|
|
551
494
|
|
|
552
495
|
const validateField = (field) => {
|
|
@@ -905,7 +848,7 @@ async function runStatefulAlertJob(alertId) {
|
|
|
905
848
|
timestamp: new Date().toISOString(),
|
|
906
849
|
message: `Alerte '${alertDoc.name}': ${count} élément(s) correspondent à votre condition.`
|
|
907
850
|
};
|
|
908
|
-
sendSseToUser(alertDoc._user, alertPayload);
|
|
851
|
+
await sendSseToUser(alertDoc._user, alertPayload);
|
|
909
852
|
|
|
910
853
|
// Update state in DB to prevent re-notification
|
|
911
854
|
await datasCollection.updateOne(
|
|
@@ -1030,7 +973,7 @@ export const editModel = async (user, id, data) => {
|
|
|
1030
973
|
const dataModel = data;
|
|
1031
974
|
try {
|
|
1032
975
|
const collection = await getCollectionForUser(user);
|
|
1033
|
-
validateModelStructure(dataModel);
|
|
976
|
+
await validateModelStructure(dataModel);
|
|
1034
977
|
|
|
1035
978
|
const el = await modelsCollection.findOne({ $and: [
|
|
1036
979
|
{_user: {$exists: true}},
|
|
@@ -1119,8 +1062,8 @@ export const editModel = async (user, id, data) => {
|
|
|
1119
1062
|
|
|
1120
1063
|
const newModel = await modelsCollection.findOne({_id : oid});
|
|
1121
1064
|
const res = ({ success: true, data: newModel });
|
|
1122
|
-
const plugin = Event.Trigger("OnModelEdited", "event", "system", engine, newModel);
|
|
1123
|
-
Event.Trigger("OnModelEdited", "event", "user", plugin?.data || newModel);
|
|
1065
|
+
const plugin = await Event.Trigger("OnModelEdited", "event", "system", engine, newModel);
|
|
1066
|
+
await Event.Trigger("OnModelEdited", "event", "user", plugin?.data || newModel);
|
|
1124
1067
|
return plugin || res
|
|
1125
1068
|
} catch (e) {
|
|
1126
1069
|
logger.error(e);
|
|
@@ -1129,38 +1072,71 @@ export const editModel = async (user, id, data) => {
|
|
|
1129
1072
|
};
|
|
1130
1073
|
|
|
1131
1074
|
|
|
1132
|
-
export async function
|
|
1075
|
+
export async function middlewareEndpointAuthenticator(req, res, next) {
|
|
1133
1076
|
const { path } = req.params;
|
|
1134
1077
|
const method = req.method.toUpperCase();
|
|
1135
|
-
|
|
1136
|
-
const user = req.me;
|
|
1137
|
-
if (!user) {
|
|
1138
|
-
return res.status(401).json({ success: false, message: 'Authentication required.' });
|
|
1139
|
-
}
|
|
1078
|
+
const datasCollection = getCollection('datas');
|
|
1140
1079
|
|
|
1141
1080
|
try {
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
isActive: true
|
|
1149
|
-
},
|
|
1150
|
-
limit: 1
|
|
1151
|
-
}, user);
|
|
1081
|
+
const endpointDef = await datasCollection.findOne({
|
|
1082
|
+
_model: 'endpoint',
|
|
1083
|
+
path: path,
|
|
1084
|
+
method: method,
|
|
1085
|
+
isActive: true
|
|
1086
|
+
});
|
|
1152
1087
|
|
|
1153
|
-
if (
|
|
1154
|
-
logger.warn(`[Endpoint] 404 - No active endpoint found for user '${user.username}', path '${path}', method '${method}'.`);
|
|
1088
|
+
if (!endpointDef) {
|
|
1155
1089
|
return res.status(404).json({ success: false, message: 'Endpoint not found.' });
|
|
1156
1090
|
}
|
|
1157
1091
|
|
|
1158
|
-
|
|
1092
|
+
// Attacher la définition à la requête pour que le handler suivant puisse l'utiliser
|
|
1093
|
+
req.endpointDef = endpointDef;
|
|
1094
|
+
|
|
1095
|
+
// Si l'endpoint n'est PAS public, on exécute le vrai middleware d'authentification
|
|
1096
|
+
if (!endpointDef.isPublic) {
|
|
1097
|
+
// On "chaîne" vers le middleware authenticator standard.
|
|
1098
|
+
// Il se chargera de vérifier le token et de renvoyer une 401 si nécessaire.
|
|
1099
|
+
return middlewareAuthenticator(req, res, next);
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
// Si l'endpoint EST public, on passe simplement à la suite.
|
|
1103
|
+
next();
|
|
1104
|
+
|
|
1105
|
+
} catch (error) {
|
|
1106
|
+
logger.error(`[EndpointAuth] Critical error: ${error.message}`, error.stack);
|
|
1107
|
+
res.status(500).json({ success: false, message: 'Internal server error during endpoint authentication.' });
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
export async function handleCustomEndpointRequest(req, res) {
|
|
1112
|
+
const endpointDef = req.endpointDef;
|
|
1113
|
+
|
|
1114
|
+
try {
|
|
1115
|
+
let executionUser = null;
|
|
1116
|
+
|
|
1117
|
+
// 1. Déterminer le contexte utilisateur pour l'exécution
|
|
1118
|
+
if (endpointDef.isPublic) {
|
|
1119
|
+
// Pour les endpoints publics, on exécute le script en tant que propriétaire de l'endpoint.
|
|
1120
|
+
if (!endpointDef._user) {
|
|
1121
|
+
logger.error(`[Endpoint] Misconfiguration: Public endpoint '${endpointDef.name}' (ID: ${endpointDef._id}) has no owner.`);
|
|
1122
|
+
return res.status(500).json({ success: false, message: 'Endpoint misconfigured: owner missing.' });
|
|
1123
|
+
}
|
|
1124
|
+
executionUser = await engine.userProvider.findUserByUsername(endpointDef._user);
|
|
1125
|
+
if (!executionUser) {
|
|
1126
|
+
logger.error(`[Endpoint] Execution failed: Owner '${endpointDef._user}' for public endpoint '${endpointDef.name}' not found.`);
|
|
1127
|
+
return res.status(500).json({ success: false, message: 'Endpoint owner not found.' });
|
|
1128
|
+
}
|
|
1129
|
+
logger.info(`[Endpoint] Public endpoint '${endpointDef.name}' running as owner '${executionUser.username}'.`);
|
|
1130
|
+
} else {
|
|
1131
|
+
// Pour les endpoints privés, l'utilisateur a déjà été authentifié par le middleware.
|
|
1132
|
+
// req.me est garanti d'exister ici.
|
|
1133
|
+
executionUser = req.me;
|
|
1134
|
+
logger.info(`[Endpoint] Private endpoint '${endpointDef.name}' running as authenticated user '${executionUser.username}'.`);
|
|
1135
|
+
}
|
|
1159
1136
|
|
|
1160
1137
|
// 2. Préparer le contexte pour le script
|
|
1161
|
-
// On donne au script accès au corps, aux paramètres de la requête, etc.
|
|
1162
1138
|
const contextData = {
|
|
1163
|
-
request:{
|
|
1139
|
+
request: {
|
|
1164
1140
|
body: req.fields,
|
|
1165
1141
|
query: req.query,
|
|
1166
1142
|
params: req.params,
|
|
@@ -1168,34 +1144,29 @@ export async function handleCustomEndpointRequest(req, res) {
|
|
|
1168
1144
|
}
|
|
1169
1145
|
};
|
|
1170
1146
|
|
|
1171
|
-
// 3. Exécuter le code de l'endpoint
|
|
1172
|
-
logger.info(`[Endpoint] Executing endpoint '${endpointDef.name}' for user '${user.username}'.`);
|
|
1147
|
+
// 3. Exécuter le code de l'endpoint
|
|
1173
1148
|
const result = await executeSafeJavascript(
|
|
1174
|
-
{ script: endpointDef.code },
|
|
1149
|
+
{ script: endpointDef.code },
|
|
1175
1150
|
contextData,
|
|
1176
|
-
user
|
|
1151
|
+
executionUser // Use the determined user for execution
|
|
1177
1152
|
);
|
|
1178
1153
|
|
|
1179
1154
|
// 4. Envoyer la réponse
|
|
1180
1155
|
if (result.success) {
|
|
1181
|
-
// Le script a réussi, on retourne sa sortie
|
|
1182
1156
|
res.status(200).json(result.data);
|
|
1183
1157
|
} else {
|
|
1184
|
-
// Le script a échoué, on retourne une erreur 500 avec les logs
|
|
1185
1158
|
logger.error(`[Endpoint] Execution failed for '${endpointDef.name}'. Error: ${result.message}`);
|
|
1186
|
-
|
|
1187
|
-
const r = {
|
|
1159
|
+
const responseError = {
|
|
1188
1160
|
success: false,
|
|
1189
1161
|
message: 'Endpoint script execution failed.',
|
|
1190
|
-
|
|
1191
|
-
|
|
1162
|
+
details: result.message,
|
|
1163
|
+
logs: result.logs
|
|
1192
1164
|
};
|
|
1193
|
-
|
|
1194
|
-
res.status(500).json(r);
|
|
1165
|
+
res.status(500).json(responseError);
|
|
1195
1166
|
}
|
|
1196
1167
|
|
|
1197
1168
|
} catch (error) {
|
|
1198
|
-
logger.error(`[Endpoint] Critical error handling request for path '${path}': ${error.message}`,
|
|
1169
|
+
logger.error(`[Endpoint] Critical error handling request for path '${endpointDef.path}': ${error.message}`, error.stack);
|
|
1199
1170
|
res.status(500).json({ success: false, message: 'An internal server error occurred.' });
|
|
1200
1171
|
}
|
|
1201
1172
|
}
|
|
@@ -1206,12 +1177,13 @@ export async function onInit(defaultEngine) {
|
|
|
1206
1177
|
|
|
1207
1178
|
engine.use(middleware({ whitelist: mongoDBWhitelist }));
|
|
1208
1179
|
|
|
1209
|
-
let modelsCollection, datasCollection, filesCollection, packsCollection, magnetsCollection;
|
|
1180
|
+
let modelsCollection, datasCollection, filesCollection, packsCollection, magnetsCollection, historyCollection;
|
|
1210
1181
|
|
|
1211
1182
|
if( install ) {
|
|
1212
|
-
datasCollection = await
|
|
1213
|
-
|
|
1214
|
-
|
|
1183
|
+
datasCollection = await createCollection("datas");
|
|
1184
|
+
historyCollection = await createCollection("history");
|
|
1185
|
+
filesCollection = await createCollection("files");
|
|
1186
|
+
packsCollection = await createCollection("packs");
|
|
1215
1187
|
//data
|
|
1216
1188
|
const indexes = await datasCollection.indexes();
|
|
1217
1189
|
if (!indexes.find(i => i.name === 'genericPartialIndex')) {
|
|
@@ -1237,7 +1209,7 @@ export async function onInit(defaultEngine) {
|
|
|
1237
1209
|
await datasCollection.createIndex({_model: 1, _user: 1}, { name: 'modelUserIndex'});
|
|
1238
1210
|
}
|
|
1239
1211
|
|
|
1240
|
-
const jobsCollection = await
|
|
1212
|
+
const jobsCollection = await createCollection("job_locks");
|
|
1241
1213
|
if (! await jobsCollection.indexExists("jobTTLIndex") ) {
|
|
1242
1214
|
await jobsCollection.createIndex({ "lockedUntil": 1 }, { name: "jobTTLIndex", expireAfterSeconds: 0 });
|
|
1243
1215
|
}
|
|
@@ -1281,6 +1253,7 @@ export async function onInit(defaultEngine) {
|
|
|
1281
1253
|
datasCollection = getCollection("datas");
|
|
1282
1254
|
filesCollection = getCollection("files");
|
|
1283
1255
|
packsCollection = getCollection("packs");
|
|
1256
|
+
historyCollection = getCollection("history");
|
|
1284
1257
|
}
|
|
1285
1258
|
await registerRoutes(engine);
|
|
1286
1259
|
logger = engine.getComponent(Logger);
|
|
@@ -1297,6 +1270,61 @@ export async function onInit(defaultEngine) {
|
|
|
1297
1270
|
});
|
|
1298
1271
|
await scheduleAlerts();
|
|
1299
1272
|
|
|
1273
|
+
// Triggers
|
|
1274
|
+
|
|
1275
|
+
Event.Listen("OnValidateModelStructure", async (modelStructure) =>{
|
|
1276
|
+
|
|
1277
|
+
const objectKeys = Object.keys(modelStructure);
|
|
1278
|
+
|
|
1279
|
+
if( objectKeys.find(o => !["name", "_user", "history", "locked", "_id", "description", "maxRequestData", "fields"].includes(o)) ){
|
|
1280
|
+
throw new Error(i18n.t('api.model.invalidStructure'));
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
// Vérification du type de name
|
|
1284
|
+
if (typeof modelStructure.name !== 'string' || !modelStructure.name) {
|
|
1285
|
+
throw new Error(i18n.t("api.validate.requiredFieldString", ["name"]));
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
// Vérification du type de description
|
|
1289
|
+
if (typeof modelStructure.description !== 'string') {
|
|
1290
|
+
throw new Error(i18n.t("api.validate.fieldString", ["description"]));
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
// Vérification de la présence et du type du tableau fields
|
|
1294
|
+
if (!Array.isArray(modelStructure.fields)) {
|
|
1295
|
+
throw new Error(i18n.t('api.validate.fieldArray', ["fields"]));
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
// Vérification de chaque champ dans le tableau fields
|
|
1299
|
+
for (const field of modelStructure.fields) {
|
|
1300
|
+
validateField(field);
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
if (modelStructure.constraints) {
|
|
1304
|
+
if (!Array.isArray(modelStructure.constraints)) {
|
|
1305
|
+
throw new Error('Model "constraints" property must be an array.');
|
|
1306
|
+
}
|
|
1307
|
+
const fieldNames = new Set(modelStructure.fields.map(f => f.name));
|
|
1308
|
+
for (const constraint of modelStructure.constraints) {
|
|
1309
|
+
if (constraint.type === 'unique') {
|
|
1310
|
+
if (!constraint.name || !Array.isArray(constraint.keys) || constraint.keys.length === 0) {
|
|
1311
|
+
throw new Error('Unique constraint must have a "name" and a non-empty "keys" array.');
|
|
1312
|
+
}
|
|
1313
|
+
for (const key of constraint.keys) {
|
|
1314
|
+
if (!fieldNames.has(key)) {
|
|
1315
|
+
throw new Error(`Constraint key "${key}" in constraint "${constraint.name}" does not exist as a field in the model.`);
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
return true; // La structure du modèle est valide
|
|
1323
|
+
}, "event", "system");
|
|
1324
|
+
|
|
1325
|
+
|
|
1326
|
+
// Sub modules
|
|
1327
|
+
historyInit(defaultEngine);
|
|
1300
1328
|
}
|
|
1301
1329
|
|
|
1302
1330
|
export const createModel = async (data) => {
|
|
@@ -1505,7 +1533,7 @@ export const insertData = async (modelName, data, files, user, triggerWorkflow =
|
|
|
1505
1533
|
};
|
|
1506
1534
|
|
|
1507
1535
|
// Envoyer l'alerte à l'utilisateur spécifique via SSE
|
|
1508
|
-
const sent = sendSseToUser(doc._user, alertPayload);
|
|
1536
|
+
const sent = await sendSseToUser(doc._user, alertPayload);
|
|
1509
1537
|
|
|
1510
1538
|
if (sent) {
|
|
1511
1539
|
logger.info(`[Scheduled Job] Successfully sent SSE alert for job ${jobId} to user ${doc._user}.`);
|
|
@@ -1546,10 +1574,18 @@ export const insertData = async (modelName, data, files, user, triggerWorkflow =
|
|
|
1546
1574
|
// Attendre que toutes les opérations post-insertion (workflows, planification) soient tentées
|
|
1547
1575
|
await Promise.allSettled(postInsertionPromises);
|
|
1548
1576
|
}
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1577
|
+
|
|
1578
|
+
// System specific event
|
|
1579
|
+
const eventPayload = { modelName, insertedIds, user };
|
|
1580
|
+
await Event.Trigger("OnDataAdded", "event", "system", engine, eventPayload);
|
|
1581
|
+
|
|
1582
|
+
// User specific event
|
|
1583
|
+
const userPayload = {...eventPayload};
|
|
1584
|
+
delete userPayload['user'];
|
|
1585
|
+
await Event.Trigger("OnDataAdded", "event", "user", userPayload);
|
|
1586
|
+
|
|
1587
|
+
// Return valid result
|
|
1588
|
+
return { success: true, insertedIds: insertedIds.map(id => id.toString()) }; // Convertir les IDs en string pour la réponse
|
|
1553
1589
|
|
|
1554
1590
|
} catch (error) { // Attrape les erreurs de permission ou de pushDataUnsecure
|
|
1555
1591
|
logger.error(`[insertData] Main error during insertion process for model ${modelName}: ${error.message}`, error.stack);
|
|
@@ -1689,7 +1725,7 @@ async function initializeAndValidate(data, modelName, me) {
|
|
|
1689
1725
|
|
|
1690
1726
|
const model = await getModel(modelName, me);
|
|
1691
1727
|
const collection = await getCollectionForUser(me);
|
|
1692
|
-
validateModelStructure(model);
|
|
1728
|
+
await validateModelStructure(model);
|
|
1693
1729
|
|
|
1694
1730
|
return { datas, model, collection };
|
|
1695
1731
|
}
|
|
@@ -1752,7 +1788,7 @@ async function processDocuments(datas, model, collection, me) {
|
|
|
1752
1788
|
const idMap = new Map();
|
|
1753
1789
|
const allInsertedIds = [];
|
|
1754
1790
|
|
|
1755
|
-
const realData = Event.Trigger("OnDataInsert", "event", "system", datas) || datas;
|
|
1791
|
+
const realData = await Event.Trigger("OnDataInsert", "event", "system", datas) || datas;
|
|
1756
1792
|
for (const doc of realData) {
|
|
1757
1793
|
try {
|
|
1758
1794
|
const newDocId = await insertAndResolveRelations(doc, model, collection, me, idMap);
|
|
@@ -1958,7 +1994,7 @@ async function applyFieldFilters(docToProcess, model) {
|
|
|
1958
1994
|
docToProcess[field.name],
|
|
1959
1995
|
field
|
|
1960
1996
|
);
|
|
1961
|
-
const realFilter = Event.Trigger('OnDataFilter', "event", "system",filter, field, docToProcess );
|
|
1997
|
+
const realFilter = await Event.Trigger('OnDataFilter', "event", "system",filter, field, docToProcess );
|
|
1962
1998
|
docToProcess[field.name] = realFilter || filter;
|
|
1963
1999
|
}
|
|
1964
2000
|
}
|
|
@@ -1968,7 +2004,7 @@ async function applyFieldFilters(docToProcess, model) {
|
|
|
1968
2004
|
/**
|
|
1969
2005
|
* Valide la structure et le contenu du document selon le modèle
|
|
1970
2006
|
*/
|
|
1971
|
-
function validateModelData(doc, model, isPatch = false) {
|
|
2007
|
+
async function validateModelData(doc, model, isPatch = false) {
|
|
1972
2008
|
if (!isPatch) {
|
|
1973
2009
|
model.fields.forEach(field => {
|
|
1974
2010
|
const value = doc[field.name];
|
|
@@ -1990,7 +2026,7 @@ function validateModelData(doc, model, isPatch = false) {
|
|
|
1990
2026
|
|
|
1991
2027
|
const validator = dataTypes[fieldDef.type]?.validate;
|
|
1992
2028
|
const valid = validator && validator(value, fieldDef);
|
|
1993
|
-
const realValidation = Event.Trigger('OnDataValidate', "event", "system", value, fieldDef,doc );
|
|
2029
|
+
const realValidation = await Event.Trigger('OnDataValidate', "event", "system", value, fieldDef,doc );
|
|
1994
2030
|
if (!(valid || realValidation)) {
|
|
1995
2031
|
throw new Error(i18n.t('api.field.validationFailed', { field: fieldName, value }));
|
|
1996
2032
|
}
|
|
@@ -2027,9 +2063,9 @@ async function insertAndResolveRelations(doc, model, collection, me, idMap) {
|
|
|
2027
2063
|
docToProcess._id = new ObjectId(originalId);
|
|
2028
2064
|
}
|
|
2029
2065
|
|
|
2030
|
-
validateModelData(docToProcess, model);
|
|
2066
|
+
await validateModelData(docToProcess, model);
|
|
2031
2067
|
await processRelations(docToProcess, model, collection, me, idMap);
|
|
2032
|
-
validateModelData(docToProcess, model);
|
|
2068
|
+
await validateModelData(docToProcess, model);
|
|
2033
2069
|
await applyFieldFilters(docToProcess, model);
|
|
2034
2070
|
await checkUniqueFields(docToProcess, model, collection);
|
|
2035
2071
|
|
|
@@ -2271,9 +2307,9 @@ const internalEditOrPatchData = async (modelName, filter, data, files, user, isP
|
|
|
2271
2307
|
// 4. Validation adaptée pour patch ou edit (inchangé)
|
|
2272
2308
|
if (!isPatch) {
|
|
2273
2309
|
const dataToValidate = { ...existingDocs[0], ...updateData };
|
|
2274
|
-
validateModelData(dataToValidate, model, false);
|
|
2310
|
+
await validateModelData(dataToValidate, model, false);
|
|
2275
2311
|
} else {
|
|
2276
|
-
validateModelData(updateData, model, true);
|
|
2312
|
+
await validateModelData(updateData, model, true);
|
|
2277
2313
|
}
|
|
2278
2314
|
|
|
2279
2315
|
// 5. Vérification des champs uniques (inchangé)
|
|
@@ -2374,6 +2410,17 @@ const internalEditOrPatchData = async (modelName, filter, data, files, user, isP
|
|
|
2374
2410
|
const bulkResult = await collection.bulkWrite(bulkOps);
|
|
2375
2411
|
const modifiedCount = bulkResult.modifiedCount || 0;
|
|
2376
2412
|
|
|
2413
|
+
// Déclencher l'événement OnDataEdited avec les états avant/après
|
|
2414
|
+
if (modifiedCount > 0) {
|
|
2415
|
+
const updatedDocs = await collection.find({ _id: { $in: ids } }).toArray();
|
|
2416
|
+
await Event.Trigger("OnDataEdited", "event", "system", engine, {
|
|
2417
|
+
modelName,
|
|
2418
|
+
user,
|
|
2419
|
+
before: existingDocs, // Documents avant la modification
|
|
2420
|
+
after: updatedDocs // Documents après la modification
|
|
2421
|
+
});
|
|
2422
|
+
}
|
|
2423
|
+
|
|
2377
2424
|
// 11. Tâches post-mise à jour (schedules, workflows) (inchangé)
|
|
2378
2425
|
if (["workflowTrigger", "alert"].includes(modelName)) {
|
|
2379
2426
|
await handleScheduledJobs(modelName, existingDocs, collection, finalDataForSet);
|
|
@@ -2434,7 +2481,7 @@ async function handleScheduledJobs(modelName, existingDocs, collection, updateDa
|
|
|
2434
2481
|
schedule.scheduleJob(jobId, updatedDoc.cronExpression, async () => {
|
|
2435
2482
|
logger.info(`[Scheduled Job] Cron triggered for job ${jobId}`);
|
|
2436
2483
|
await runScheduledJobWithDbLock(jobId, async () => {
|
|
2437
|
-
sendSseToUser(updatedDoc._user, {
|
|
2484
|
+
await sendSseToUser(updatedDoc._user, {
|
|
2438
2485
|
type: 'cron_alert',
|
|
2439
2486
|
triggerId: updatedDoc._id.toString(),
|
|
2440
2487
|
triggerName: updatedDoc.name,
|
|
@@ -2657,8 +2704,8 @@ export const deleteData = async (modelName, filter, user ={}, triggerWorkflow, w
|
|
|
2657
2704
|
}
|
|
2658
2705
|
|
|
2659
2706
|
const res = { success: true, deletedCount }
|
|
2660
|
-
const plugin = Event.Trigger("OnDataDeleted", "event", "system", engine, {model:modelName, filter});
|
|
2661
|
-
Event.Trigger("OnDataDeleted", "event", "user", {model:modelName, filter});
|
|
2707
|
+
const plugin = await Event.Trigger("OnDataDeleted", "event", "system", engine, {model:modelName, filter});
|
|
2708
|
+
await Event.Trigger("OnDataDeleted", "event", "user", {model:modelName, filter});
|
|
2662
2709
|
return plugin || res;
|
|
2663
2710
|
|
|
2664
2711
|
} catch (error) {
|
|
@@ -3166,8 +3213,8 @@ export const searchData = async (query, user) => {
|
|
|
3166
3213
|
data = await handleFields(modelElement, data, user);
|
|
3167
3214
|
|
|
3168
3215
|
const res = {data, count: count[0]?.count || 0};
|
|
3169
|
-
const plugin = Event.Trigger("OnDataSearched", "event", "system", engine, {data, count: count[0]?.count});
|
|
3170
|
-
Event.Trigger("OnDataSearched", "event", "user", plugin || {data, count: count[0]?.count});
|
|
3216
|
+
const plugin = await Event.Trigger("OnDataSearched", "event", "system", engine, {data, count: count[0]?.count});
|
|
3217
|
+
await Event.Trigger("OnDataSearched", "event", "user", plugin || {data, count: count[0]?.count});
|
|
3171
3218
|
return plugin || res;
|
|
3172
3219
|
}
|
|
3173
3220
|
|
|
@@ -3244,7 +3291,7 @@ export const importData = async(options, files, user) => {
|
|
|
3244
3291
|
});
|
|
3245
3292
|
}
|
|
3246
3293
|
|
|
3247
|
-
validateModelStructure(modelData);
|
|
3294
|
+
await validateModelStructure(modelData);
|
|
3248
3295
|
await modelsCollection.insertOne(modelData);
|
|
3249
3296
|
logger.info(`[Model Import] Successfully imported model '${modelName}' for user ${user.username}.`);
|
|
3250
3297
|
} catch (e) {
|
|
@@ -3326,7 +3373,7 @@ export const importData = async(options, files, user) => {
|
|
|
3326
3373
|
if (insertedIdsArray && insertedIdsArray.length > 0) {
|
|
3327
3374
|
importJobs[importJobId].processedRecords += insertedIdsArray.length;
|
|
3328
3375
|
logger.debug(`[Import Job ${importJobId}] Processed chunk for '${modelName}': ${insertedIdsArray.length} records. Total processed: ${importJobs[importJobId].processedRecords}`);
|
|
3329
|
-
sendSseToUser(user.username, {
|
|
3376
|
+
await sendSseToUser(user.username, {
|
|
3330
3377
|
type: 'import_progress',
|
|
3331
3378
|
job: importJobs[importJobId]
|
|
3332
3379
|
});
|
|
@@ -3430,7 +3477,7 @@ export const importData = async(options, files, user) => {
|
|
|
3430
3477
|
const insertedIdsArray = await pushDataUnsecure(chunk, modelNameForImport, user, {});
|
|
3431
3478
|
if (insertedIdsArray && insertedIdsArray.length > 0) {
|
|
3432
3479
|
importJobs[importJobId].processedRecords += insertedIdsArray.length;
|
|
3433
|
-
sendSseToUser(user.username, {
|
|
3480
|
+
await sendSseToUser(user.username, {
|
|
3434
3481
|
type: 'import_progress',
|
|
3435
3482
|
job: importJobs[importJobId]
|
|
3436
3483
|
});
|
|
@@ -3524,7 +3571,7 @@ export const importData = async(options, files, user) => {
|
|
|
3524
3571
|
const insertedIdsArray = await pushDataUnsecure(chunk, modelNameForImport, user, {});
|
|
3525
3572
|
if (insertedIdsArray && insertedIdsArray.length > 0) {
|
|
3526
3573
|
importJobs[importJobId].processedRecords += insertedIdsArray.length;
|
|
3527
|
-
sendSseToUser(user.username, {
|
|
3574
|
+
await sendSseToUser(user.username, {
|
|
3528
3575
|
type: 'import_progress',
|
|
3529
3576
|
job: importJobs[importJobId]
|
|
3530
3577
|
});
|
|
@@ -3569,7 +3616,7 @@ export const importData = async(options, files, user) => {
|
|
|
3569
3616
|
} else {
|
|
3570
3617
|
importJobs[importJobId].status = 'completed';
|
|
3571
3618
|
}
|
|
3572
|
-
sendSseToUser(user.username, {
|
|
3619
|
+
await sendSseToUser(user.username, {
|
|
3573
3620
|
type: 'import_progress',
|
|
3574
3621
|
job: importJobs[importJobId]
|
|
3575
3622
|
});
|
|
@@ -3705,8 +3752,8 @@ export const exportData= async (options, user) =>{
|
|
|
3705
3752
|
}
|
|
3706
3753
|
|
|
3707
3754
|
const res = { success: true, data: exportResults, models: modelsToExport };
|
|
3708
|
-
const plugin = Event.Trigger("OnDataExported", "event", "system", engine, exportResults, modelsToExport);
|
|
3709
|
-
Event.Trigger("OnDataExported", "event", "user", plugin?.exportResults || exportResults, plugin?.modelsToExport || modelsToExport);
|
|
3755
|
+
const plugin = await Event.Trigger("OnDataExported", "event", "system", engine, exportResults, modelsToExport);
|
|
3756
|
+
await Event.Trigger("OnDataExported", "event", "user", plugin?.exportResults || exportResults, plugin?.modelsToExport || modelsToExport);
|
|
3710
3757
|
return plugin || res;
|
|
3711
3758
|
}
|
|
3712
3759
|
|
|
@@ -4058,7 +4105,7 @@ export const loadFromDump = async (user, options = {}) => {
|
|
|
4058
4105
|
modelsCache.flushAll();
|
|
4059
4106
|
|
|
4060
4107
|
logger.info(`[${action}] Restore successful for user ${user.username}.`);
|
|
4061
|
-
Event.Trigger("OnDataRestored", "event", "system");
|
|
4108
|
+
await Event.Trigger("OnDataRestored", "event", "system");
|
|
4062
4109
|
|
|
4063
4110
|
} finally {
|
|
4064
4111
|
// --- GUARANTEED CLEANUP ---
|
|
@@ -4242,68 +4289,94 @@ async function manageBackupRotation(user, backupFrequency, s3Config = null) { //
|
|
|
4242
4289
|
}
|
|
4243
4290
|
|
|
4244
4291
|
/**
|
|
4245
|
-
*
|
|
4246
|
-
*
|
|
4247
|
-
* via un système en deux passes optimisé.
|
|
4292
|
+
* Installs pack models and data for a user or globally.
|
|
4293
|
+
* Can accept either a pack ID (to install from database) or a direct pack JSON object.
|
|
4248
4294
|
*
|
|
4249
|
-
* @param {object} logger -
|
|
4250
|
-
* @param {string}
|
|
4251
|
-
* @param {object} user -
|
|
4252
|
-
* @param {string} lang -
|
|
4295
|
+
* @param {object} logger - Logger instance
|
|
4296
|
+
* @param {string|object} packIdentifier - Either pack ID (string) or pack JSON object
|
|
4297
|
+
* @param {object|null} user - User object (if installing for user) or null (for global install)
|
|
4298
|
+
* @param {string} [lang='en'] - Language code for localized data
|
|
4253
4299
|
* @returns {Promise<{success: boolean, summary: object, errors: Array, modifiedCount: number}>}
|
|
4254
4300
|
*/
|
|
4255
|
-
export async function installPack(
|
|
4301
|
+
export async function installPack(packIdentifier, user = null, lang = 'en', isTemporary= false) {
|
|
4302
|
+
let pack;
|
|
4256
4303
|
const packsCollection = getCollection('packs');
|
|
4257
|
-
const pack = await packsCollection.findOne({ _id: new ObjectId(packId) });
|
|
4258
4304
|
|
|
4259
|
-
if
|
|
4260
|
-
|
|
4305
|
+
// Determine if we're working with an ID or direct pack object
|
|
4306
|
+
if (typeof packIdentifier === 'string') {
|
|
4307
|
+
// Existing behavior - fetch from database
|
|
4308
|
+
pack = await packsCollection.findOne({ _id: new ObjectId(packIdentifier) });
|
|
4309
|
+
if (!pack) {
|
|
4310
|
+
throw new Error(`Pack with ID ${packIdentifier} not found.`);
|
|
4311
|
+
}
|
|
4312
|
+
} else if (typeof packIdentifier === 'object' && packIdentifier !== null) {
|
|
4313
|
+
// New behavior - use provided pack object directly
|
|
4314
|
+
pack = packIdentifier;
|
|
4315
|
+
|
|
4316
|
+
// Validate basic pack structure
|
|
4317
|
+
if (!pack.name || (!pack.models && !pack.data)) {
|
|
4318
|
+
throw new Error('Invalid pack structure - must contain at least name and models or data');
|
|
4319
|
+
}
|
|
4320
|
+
} else {
|
|
4321
|
+
throw new Error('Invalid pack identifier - must be either pack ID string or pack object');
|
|
4261
4322
|
}
|
|
4262
4323
|
|
|
4263
|
-
|
|
4324
|
+
const username = user ? user.username : null;
|
|
4325
|
+
const logPrefix = username
|
|
4326
|
+
? `Installing pack '${pack.name}' for user '${username}'`
|
|
4327
|
+
: `Installing pack '${pack.name}' globally`;
|
|
4328
|
+
|
|
4329
|
+
logger.info(`--- ${logPrefix} ---`);
|
|
4264
4330
|
|
|
4265
4331
|
const summary = {
|
|
4266
4332
|
models: { installed: [], skipped: [], failed: [] },
|
|
4267
4333
|
datas: { inserted: 0, updated: 0, skipped: 0, failed: 0 }
|
|
4268
4334
|
};
|
|
4269
4335
|
const errors = [];
|
|
4270
|
-
const collection = await getCollectionForUser(user);
|
|
4336
|
+
const collection = user ? await getCollectionForUser(user) : getCollection('data');
|
|
4271
4337
|
const tempIdToNewIdMap = {};
|
|
4272
4338
|
const linkCache = new Map();
|
|
4273
4339
|
|
|
4274
|
-
// --- PHASE 1:
|
|
4340
|
+
// --- PHASE 1: MODEL INSTALLATION ---
|
|
4275
4341
|
if (Array.isArray(pack.models)) {
|
|
4276
|
-
|
|
4277
|
-
const
|
|
4342
|
+
// For user installs, check existing models
|
|
4343
|
+
const existingModels = user
|
|
4344
|
+
? await modelsCollection.find({ _user: username }).toArray()
|
|
4345
|
+
: await modelsCollection.find({ _user: { $exists: false } }).toArray();
|
|
4346
|
+
|
|
4347
|
+
const existingModelNames = existingModels.map(m => m.name);
|
|
4278
4348
|
|
|
4279
4349
|
for (const modelOrName of pack.models) {
|
|
4280
4350
|
try {
|
|
4281
4351
|
const modelName = typeof modelOrName === 'string' ? modelOrName : modelOrName?.name;
|
|
4282
4352
|
if (!modelName) throw new Error('Model definition in pack is missing a name.');
|
|
4283
4353
|
|
|
4284
|
-
if (
|
|
4285
|
-
logger.debug(`[Model Install] Skipping '${modelName}': already exists
|
|
4354
|
+
if (existingModelNames.includes(modelName)) {
|
|
4355
|
+
logger.debug(`[Model Install] Skipping '${modelName}': already exists`);
|
|
4286
4356
|
summary.models.skipped.push(modelName);
|
|
4287
4357
|
continue;
|
|
4288
4358
|
}
|
|
4289
4359
|
|
|
4290
|
-
|
|
4291
|
-
|
|
4292
|
-
|
|
4293
|
-
|
|
4294
|
-
|
|
4295
|
-
|
|
4296
|
-
} else {
|
|
4297
|
-
modelToInstall = { ...modelOrName };
|
|
4360
|
+
const modelToInstall = typeof modelOrName === 'string'
|
|
4361
|
+
? await modelsCollection.findOne({ name: modelName, _user: { $exists: false } })
|
|
4362
|
+
: { ...modelOrName };
|
|
4363
|
+
|
|
4364
|
+
if (!modelToInstall) {
|
|
4365
|
+
throw new Error(`Model '${modelName}' not found in shared models`);
|
|
4298
4366
|
}
|
|
4299
4367
|
|
|
4300
|
-
|
|
4301
|
-
|
|
4302
|
-
|
|
4303
|
-
|
|
4368
|
+
// Prepare model for installation
|
|
4369
|
+
const preparedModel = { ...modelToInstall };
|
|
4370
|
+
if (user) preparedModel._user = username;
|
|
4371
|
+
delete preparedModel._id;
|
|
4372
|
+
preparedModel.locked = false;
|
|
4373
|
+
|
|
4374
|
+
if (preparedModel.fields) {
|
|
4375
|
+
preparedModel.fields.forEach(f => f.locked = false);
|
|
4376
|
+
}
|
|
4304
4377
|
|
|
4305
|
-
validateModelStructure(
|
|
4306
|
-
await modelsCollection.insertOne(
|
|
4378
|
+
await validateModelStructure(preparedModel);
|
|
4379
|
+
await modelsCollection.insertOne(preparedModel);
|
|
4307
4380
|
summary.models.installed.push(modelName);
|
|
4308
4381
|
|
|
4309
4382
|
} catch (e) {
|
|
@@ -4314,13 +4387,14 @@ export async function installPack(packId, user, lang) {
|
|
|
4314
4387
|
}
|
|
4315
4388
|
}
|
|
4316
4389
|
|
|
4317
|
-
// --- PHASE 2:
|
|
4318
|
-
const dataToInstall = { ...pack.data?.all, ...pack.data?.[
|
|
4319
|
-
if (!dataToInstall ||
|
|
4390
|
+
// --- PHASE 2: DATA INSTALLATION ---
|
|
4391
|
+
const dataToInstall = { ...pack.data?.all, ...pack.data?.[lang] };
|
|
4392
|
+
if (!dataToInstall || Object.keys(dataToInstall).length === 0) {
|
|
4320
4393
|
logger.warn(`Pack '${pack.name}' has no data to install.`);
|
|
4321
4394
|
return { success: errors.length === 0, summary, errors, modifiedCount: 0 };
|
|
4322
4395
|
}
|
|
4323
4396
|
|
|
4397
|
+
// Process link references (same as original)
|
|
4324
4398
|
const linkQueue = [];
|
|
4325
4399
|
for (const modelName in dataToInstall) {
|
|
4326
4400
|
if (Array.isArray(dataToInstall[modelName])) {
|
|
@@ -4330,7 +4404,6 @@ export async function installPack(packId, user, lang) {
|
|
|
4330
4404
|
|
|
4331
4405
|
for (const fieldName in docSource) {
|
|
4332
4406
|
if (isPlainObject(docSource[fieldName]) && docSource[fieldName].$link) {
|
|
4333
|
-
// CORRECTION 1: On ajoute le nom du modèle source à la file d'attente
|
|
4334
4407
|
linkQueue.push({
|
|
4335
4408
|
sourceTempId: tempId,
|
|
4336
4409
|
sourceModelName: modelName,
|
|
@@ -4343,21 +4416,21 @@ export async function installPack(packId, user, lang) {
|
|
|
4343
4416
|
}
|
|
4344
4417
|
}
|
|
4345
4418
|
|
|
4346
|
-
// ---
|
|
4347
|
-
logger.info("[Pack Install] Starting Pass 1: Batch Insertion & ID Mapping
|
|
4419
|
+
// --- PASS 1: BATCH INSERTION ---
|
|
4420
|
+
logger.info("[Pack Install] Starting Pass 1: Batch Insertion & ID Mapping");
|
|
4348
4421
|
for (const modelName in dataToInstall) {
|
|
4349
|
-
if (!
|
|
4422
|
+
if (!Array.isArray(dataToInstall[modelName])) continue;
|
|
4350
4423
|
|
|
4351
4424
|
const documents = dataToInstall[modelName];
|
|
4352
4425
|
if (documents.length === 0) continue;
|
|
4353
4426
|
|
|
4354
4427
|
const docsToInsert = [];
|
|
4355
|
-
|
|
4356
4428
|
const modelDefForHash = await getModel(modelName, user);
|
|
4357
4429
|
|
|
4358
4430
|
for (const docSource of documents) {
|
|
4359
4431
|
let docForInsert = { ...docSource };
|
|
4360
4432
|
|
|
4433
|
+
// Clear $link fields for first pass
|
|
4361
4434
|
for (const key in docForInsert) {
|
|
4362
4435
|
if (isPlainObject(docForInsert[key]) && docForInsert[key].$link) {
|
|
4363
4436
|
docForInsert[key] = null;
|
|
@@ -4368,11 +4441,18 @@ export async function installPack(packId, user, lang) {
|
|
|
4368
4441
|
delete docForInsert._id;
|
|
4369
4442
|
delete docForInsert._temp_pack_id;
|
|
4370
4443
|
|
|
4371
|
-
docForInsert._user =
|
|
4444
|
+
if (user) docForInsert._user = username;
|
|
4372
4445
|
docForInsert._model = modelName;
|
|
4373
4446
|
docForInsert._hash = getFieldValueHash(modelDefForHash, docForInsert);
|
|
4374
4447
|
|
|
4375
|
-
|
|
4448
|
+
// Check for existing document
|
|
4449
|
+
const existingQuery = {
|
|
4450
|
+
_hash: docForInsert._hash,
|
|
4451
|
+
_model: modelName
|
|
4452
|
+
};
|
|
4453
|
+
if (user) existingQuery._user = username;
|
|
4454
|
+
|
|
4455
|
+
const existingDoc = await collection.findOne(existingQuery, { projection: { _id: 1 } });
|
|
4376
4456
|
if (existingDoc) {
|
|
4377
4457
|
tempIdToNewIdMap[tempId] = existingDoc._id;
|
|
4378
4458
|
summary.datas.skipped++;
|
|
@@ -4398,7 +4478,6 @@ export async function installPack(packId, user, lang) {
|
|
|
4398
4478
|
tempIdToNewIdMap[doc._temp_pack_id_for_mapping] = result.insertedIds[index];
|
|
4399
4479
|
}
|
|
4400
4480
|
});
|
|
4401
|
-
|
|
4402
4481
|
} catch (e) {
|
|
4403
4482
|
summary.datas.failed += docsToInsert.length;
|
|
4404
4483
|
errors.push(`Error inserting batch for ${modelName}: ${e.message}`);
|
|
@@ -4407,83 +4486,79 @@ export async function installPack(packId, user, lang) {
|
|
|
4407
4486
|
}
|
|
4408
4487
|
}
|
|
4409
4488
|
|
|
4410
|
-
// ---
|
|
4411
|
-
logger.info(`[Pack Install] Starting Pass 2: Linking ${linkQueue.length} references
|
|
4489
|
+
// --- PASS 2: REFERENCE LINKING ---
|
|
4490
|
+
logger.info(`[Pack Install] Starting Pass 2: Linking ${linkQueue.length} references`);
|
|
4412
4491
|
for (const linkOp of linkQueue) {
|
|
4413
|
-
// CORRECTION 2: On récupère le nom du modèle source
|
|
4414
4492
|
const { sourceTempId, sourceModelName, fieldName, linkSelector } = linkOp;
|
|
4415
|
-
|
|
4416
4493
|
const sourceId = tempIdToNewIdMap[sourceTempId];
|
|
4494
|
+
|
|
4417
4495
|
if (!sourceId) {
|
|
4418
4496
|
logger.warn(`[LINK FAILED] Could not find newly inserted document for temp ID ${sourceTempId}. Skipping link.`);
|
|
4419
4497
|
continue;
|
|
4420
4498
|
}
|
|
4421
4499
|
|
|
4422
|
-
|
|
4423
|
-
|
|
4424
|
-
|
|
4425
|
-
const targetModelName = linkSelector._model;
|
|
4426
|
-
delete linkSelector['_model'];
|
|
4500
|
+
try {
|
|
4501
|
+
const targetModelName = linkSelector._model;
|
|
4502
|
+
delete linkSelector._model;
|
|
4427
4503
|
|
|
4428
|
-
|
|
4429
|
-
|
|
4504
|
+
const sourceModelDef = await getModel(sourceModelName, user);
|
|
4505
|
+
const fieldDef = sourceModelDef.fields.find(f => f.name === fieldName);
|
|
4430
4506
|
|
|
4431
|
-
|
|
4432
|
-
|
|
4433
|
-
|
|
4434
|
-
|
|
4435
|
-
|
|
4436
|
-
const finalSelector = { ...linkSelector };
|
|
4437
|
-
delete finalSelector['_model']; // nécessaire
|
|
4438
|
-
// CORRECTION 4: Appel corrigé à searchData
|
|
4439
|
-
const { data: targetDocs } = await searchData({model: targetModelName, filter: finalSelector }, user);
|
|
4440
|
-
|
|
4441
|
-
if (targetDocs && targetDocs.length > 0) {
|
|
4442
|
-
targetIds = targetDocs.map(d => d._id); // Récupère un tableau d'ObjectIds
|
|
4443
|
-
linkCache.set(cacheKey, targetIds);
|
|
4444
|
-
}
|
|
4507
|
+
if (!fieldDef) {
|
|
4508
|
+
logger.warn(`[LINK FAILED] Field '${fieldName}' not found in source model '${sourceModelName}'`);
|
|
4509
|
+
errors.push(`[LINK FAILED] Field '${fieldName}' not found in source model '${sourceModelName}'`);
|
|
4510
|
+
summary.datas.failed++;
|
|
4511
|
+
continue;
|
|
4445
4512
|
}
|
|
4446
4513
|
|
|
4447
|
-
|
|
4448
|
-
|
|
4449
|
-
|
|
4450
|
-
|
|
4451
|
-
|
|
4452
|
-
errors.push(`[LINK FAILED] Field '${fieldName}' not found in source model '${sourceModelName}' for doc ${sourceId}.`);
|
|
4453
|
-
summary.datas.failed++;
|
|
4454
|
-
continue;
|
|
4455
|
-
}
|
|
4456
|
-
|
|
4457
|
-
// CORRECTION 6: On gère correctement les cas multiples et uniques
|
|
4458
|
-
const valueToSet = fieldDef.multiple ? targetIds.map(id => id.toString()) : targetIds[0].toString();
|
|
4514
|
+
// Search for target documents
|
|
4515
|
+
const { data: targetDocs } = await searchData(
|
|
4516
|
+
{ model: targetModelName, filter: linkSelector },
|
|
4517
|
+
user
|
|
4518
|
+
);
|
|
4459
4519
|
|
|
4460
|
-
|
|
4461
|
-
|
|
4462
|
-
{ $set: { [fieldName]: valueToSet } }
|
|
4463
|
-
);
|
|
4464
|
-
summary.datas.updated++;
|
|
4465
|
-
} else {
|
|
4466
|
-
const errorMsg = `[LINK FAILED] Could not find target document for linking: ${JSON.stringify(linkSelector)}`;
|
|
4520
|
+
if (!targetDocs || targetDocs.length === 0) {
|
|
4521
|
+
const errorMsg = `[LINK FAILED] No target found for ${JSON.stringify(linkSelector)}`;
|
|
4467
4522
|
logger.warn(errorMsg);
|
|
4468
4523
|
errors.push(errorMsg);
|
|
4469
4524
|
summary.datas.failed++;
|
|
4525
|
+
continue;
|
|
4470
4526
|
}
|
|
4527
|
+
|
|
4528
|
+
// Update source document with reference
|
|
4529
|
+
const valueToSet = fieldDef.multiple
|
|
4530
|
+
? targetDocs.map(d => d._id.toString())
|
|
4531
|
+
: targetDocs[0]._id.toString();
|
|
4532
|
+
|
|
4533
|
+
await collection.updateOne(
|
|
4534
|
+
{ _id: sourceId },
|
|
4535
|
+
{ $set: { [fieldName]: valueToSet } }
|
|
4536
|
+
);
|
|
4537
|
+
summary.datas.updated++;
|
|
4538
|
+
|
|
4471
4539
|
} catch (e) {
|
|
4472
|
-
const errorMsg = `[LINK CRITICAL] Error
|
|
4540
|
+
const errorMsg = `[LINK CRITICAL] Error linking ${sourceModelName}.${fieldName}: ${e.message}`;
|
|
4473
4541
|
logger.error(errorMsg, e.stack);
|
|
4474
4542
|
errors.push(errorMsg);
|
|
4475
4543
|
summary.datas.failed++;
|
|
4476
4544
|
}
|
|
4477
4545
|
}
|
|
4478
4546
|
|
|
4479
|
-
|
|
4547
|
+
// Trigger event only if pack came from database (original behavior)
|
|
4548
|
+
if (typeof packIdentifier === 'string') {
|
|
4549
|
+
await Event.Trigger("OnPackInstalled", "event", "system", pack);
|
|
4550
|
+
}
|
|
4480
4551
|
|
|
4481
4552
|
const modifiedCount = summary.datas.inserted + summary.datas.updated;
|
|
4482
|
-
logger.info(`---
|
|
4483
|
-
return {
|
|
4553
|
+
logger.info(`--- ${logPrefix} completed ---`);
|
|
4554
|
+
return {
|
|
4555
|
+
success: errors.length === 0,
|
|
4556
|
+
summary,
|
|
4557
|
+
errors,
|
|
4558
|
+
modifiedCount
|
|
4559
|
+
};
|
|
4484
4560
|
}
|
|
4485
4561
|
|
|
4486
|
-
|
|
4487
4562
|
export const installAllPacks = async () => {
|
|
4488
4563
|
const packs = await getAllPacks();
|
|
4489
4564
|
await packsCollection.deleteMany({ _user: { $exists : false }});
|
|
@@ -4528,14 +4603,7 @@ export async function handleDemoInitialization(req, res) {
|
|
|
4528
4603
|
logger.info(`[Demo Init] Installing dynamically generated pack with models: [${models.join(', ')}].`);
|
|
4529
4604
|
|
|
4530
4605
|
// Create and install pack
|
|
4531
|
-
const
|
|
4532
|
-
const tempPack = { ...packToInstall, _user: 'system_temp' };
|
|
4533
|
-
const tempInsert = await packsCollection.insertOne(tempPack);
|
|
4534
|
-
const packId = tempInsert.insertedId;
|
|
4535
|
-
|
|
4536
|
-
const result = await installPack(packId, user, req.query.lang || 'en');
|
|
4537
|
-
|
|
4538
|
-
await packsCollection.deleteOne({ _id: packId });
|
|
4606
|
+
const result = await installPack(packToInstall, user, req.query.lang || 'en');
|
|
4539
4607
|
|
|
4540
4608
|
if (result.success || result.modifiedCount > 0) {
|
|
4541
4609
|
logger.info(`[Demo Init] Pack installed successfully for user '${user.username}'.`);
|