data-primals-engine 1.3.0 → 1.3.2
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/Dockerfile +19 -9
- package/DockerfileTest +19 -9
- package/README.md +58 -24
- package/client/package-lock.json +172 -1
- package/client/package.json +1 -0
- package/client/src/App.scss +4 -0
- package/client/src/CalendarConfigModal.jsx +60 -21
- package/client/src/CalendarView.jsx +107 -0
- package/client/src/CalendarView.scss +9 -0
- package/client/src/DataLayout.jsx +54 -27
- package/client/src/DataTable.jsx +19 -2
- package/client/src/Dialog.scss +4 -3
- package/client/src/Field.jsx +3 -2
- package/client/src/HistoryDialog.jsx +253 -0
- package/client/src/HistoryDialog.scss +320 -0
- package/client/src/ModelCreator.jsx +36 -15
- package/client/src/ModelCreatorField.jsx +5 -6
- package/client/src/ViewSwitcher.jsx +6 -6
- package/client/src/hooks/useTutorials.jsx +1 -1
- package/client/src/translations.js +298 -1
- package/package.json +2 -2
- 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/modules/data/data.history.js +490 -0
- package/src/modules/data/data.js +139 -108
- package/src/modules/data/data.routes.js +4 -4
- package/src/modules/mongodb.js +17 -1
- package/src/modules/user.js +9 -3
- package/src/packs.js +2 -1
- package/src/services/stripe.js +67 -13
- 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 +79 -39
- 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 +2 -0
- package/test/workflow.integration.test.js +2 -0
- 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, middlewareAuthenticator
|
|
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";
|
|
@@ -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);
|
|
@@ -1234,12 +1177,13 @@ export async function onInit(defaultEngine) {
|
|
|
1234
1177
|
|
|
1235
1178
|
engine.use(middleware({ whitelist: mongoDBWhitelist }));
|
|
1236
1179
|
|
|
1237
|
-
let modelsCollection, datasCollection, filesCollection, packsCollection, magnetsCollection;
|
|
1180
|
+
let modelsCollection, datasCollection, filesCollection, packsCollection, magnetsCollection, historyCollection;
|
|
1238
1181
|
|
|
1239
1182
|
if( install ) {
|
|
1240
|
-
datasCollection = await
|
|
1241
|
-
|
|
1242
|
-
|
|
1183
|
+
datasCollection = await createCollection("datas");
|
|
1184
|
+
historyCollection = await createCollection("history");
|
|
1185
|
+
filesCollection = await createCollection("files");
|
|
1186
|
+
packsCollection = await createCollection("packs");
|
|
1243
1187
|
//data
|
|
1244
1188
|
const indexes = await datasCollection.indexes();
|
|
1245
1189
|
if (!indexes.find(i => i.name === 'genericPartialIndex')) {
|
|
@@ -1265,7 +1209,7 @@ export async function onInit(defaultEngine) {
|
|
|
1265
1209
|
await datasCollection.createIndex({_model: 1, _user: 1}, { name: 'modelUserIndex'});
|
|
1266
1210
|
}
|
|
1267
1211
|
|
|
1268
|
-
const jobsCollection = await
|
|
1212
|
+
const jobsCollection = await createCollection("job_locks");
|
|
1269
1213
|
if (! await jobsCollection.indexExists("jobTTLIndex") ) {
|
|
1270
1214
|
await jobsCollection.createIndex({ "lockedUntil": 1 }, { name: "jobTTLIndex", expireAfterSeconds: 0 });
|
|
1271
1215
|
}
|
|
@@ -1309,6 +1253,7 @@ export async function onInit(defaultEngine) {
|
|
|
1309
1253
|
datasCollection = getCollection("datas");
|
|
1310
1254
|
filesCollection = getCollection("files");
|
|
1311
1255
|
packsCollection = getCollection("packs");
|
|
1256
|
+
historyCollection = getCollection("history");
|
|
1312
1257
|
}
|
|
1313
1258
|
await registerRoutes(engine);
|
|
1314
1259
|
logger = engine.getComponent(Logger);
|
|
@@ -1325,6 +1270,61 @@ export async function onInit(defaultEngine) {
|
|
|
1325
1270
|
});
|
|
1326
1271
|
await scheduleAlerts();
|
|
1327
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);
|
|
1328
1328
|
}
|
|
1329
1329
|
|
|
1330
1330
|
export const createModel = async (data) => {
|
|
@@ -1533,7 +1533,7 @@ export const insertData = async (modelName, data, files, user, triggerWorkflow =
|
|
|
1533
1533
|
};
|
|
1534
1534
|
|
|
1535
1535
|
// Envoyer l'alerte à l'utilisateur spécifique via SSE
|
|
1536
|
-
const sent = sendSseToUser(doc._user, alertPayload);
|
|
1536
|
+
const sent = await sendSseToUser(doc._user, alertPayload);
|
|
1537
1537
|
|
|
1538
1538
|
if (sent) {
|
|
1539
1539
|
logger.info(`[Scheduled Job] Successfully sent SSE alert for job ${jobId} to user ${doc._user}.`);
|
|
@@ -1574,10 +1574,18 @@ export const insertData = async (modelName, data, files, user, triggerWorkflow =
|
|
|
1574
1574
|
// Attendre que toutes les opérations post-insertion (workflows, planification) soient tentées
|
|
1575
1575
|
await Promise.allSettled(postInsertionPromises);
|
|
1576
1576
|
}
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
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
|
|
1581
1589
|
|
|
1582
1590
|
} catch (error) { // Attrape les erreurs de permission ou de pushDataUnsecure
|
|
1583
1591
|
logger.error(`[insertData] Main error during insertion process for model ${modelName}: ${error.message}`, error.stack);
|
|
@@ -1717,7 +1725,7 @@ async function initializeAndValidate(data, modelName, me) {
|
|
|
1717
1725
|
|
|
1718
1726
|
const model = await getModel(modelName, me);
|
|
1719
1727
|
const collection = await getCollectionForUser(me);
|
|
1720
|
-
validateModelStructure(model);
|
|
1728
|
+
await validateModelStructure(model);
|
|
1721
1729
|
|
|
1722
1730
|
return { datas, model, collection };
|
|
1723
1731
|
}
|
|
@@ -1780,7 +1788,7 @@ async function processDocuments(datas, model, collection, me) {
|
|
|
1780
1788
|
const idMap = new Map();
|
|
1781
1789
|
const allInsertedIds = [];
|
|
1782
1790
|
|
|
1783
|
-
const realData = Event.Trigger("OnDataInsert", "event", "system", datas) || datas;
|
|
1791
|
+
const realData = await Event.Trigger("OnDataInsert", "event", "system", datas) || datas;
|
|
1784
1792
|
for (const doc of realData) {
|
|
1785
1793
|
try {
|
|
1786
1794
|
const newDocId = await insertAndResolveRelations(doc, model, collection, me, idMap);
|
|
@@ -1986,7 +1994,7 @@ async function applyFieldFilters(docToProcess, model) {
|
|
|
1986
1994
|
docToProcess[field.name],
|
|
1987
1995
|
field
|
|
1988
1996
|
);
|
|
1989
|
-
const realFilter = Event.Trigger('OnDataFilter', "event", "system",filter, field, docToProcess );
|
|
1997
|
+
const realFilter = await Event.Trigger('OnDataFilter', "event", "system",filter, field, docToProcess );
|
|
1990
1998
|
docToProcess[field.name] = realFilter || filter;
|
|
1991
1999
|
}
|
|
1992
2000
|
}
|
|
@@ -1996,7 +2004,7 @@ async function applyFieldFilters(docToProcess, model) {
|
|
|
1996
2004
|
/**
|
|
1997
2005
|
* Valide la structure et le contenu du document selon le modèle
|
|
1998
2006
|
*/
|
|
1999
|
-
function validateModelData(doc, model, isPatch = false) {
|
|
2007
|
+
async function validateModelData(doc, model, isPatch = false) {
|
|
2000
2008
|
if (!isPatch) {
|
|
2001
2009
|
model.fields.forEach(field => {
|
|
2002
2010
|
const value = doc[field.name];
|
|
@@ -2018,7 +2026,7 @@ function validateModelData(doc, model, isPatch = false) {
|
|
|
2018
2026
|
|
|
2019
2027
|
const validator = dataTypes[fieldDef.type]?.validate;
|
|
2020
2028
|
const valid = validator && validator(value, fieldDef);
|
|
2021
|
-
const realValidation = Event.Trigger('OnDataValidate', "event", "system", value, fieldDef,doc );
|
|
2029
|
+
const realValidation = await Event.Trigger('OnDataValidate', "event", "system", value, fieldDef,doc );
|
|
2022
2030
|
if (!(valid || realValidation)) {
|
|
2023
2031
|
throw new Error(i18n.t('api.field.validationFailed', { field: fieldName, value }));
|
|
2024
2032
|
}
|
|
@@ -2055,9 +2063,9 @@ async function insertAndResolveRelations(doc, model, collection, me, idMap) {
|
|
|
2055
2063
|
docToProcess._id = new ObjectId(originalId);
|
|
2056
2064
|
}
|
|
2057
2065
|
|
|
2058
|
-
validateModelData(docToProcess, model);
|
|
2066
|
+
await validateModelData(docToProcess, model);
|
|
2059
2067
|
await processRelations(docToProcess, model, collection, me, idMap);
|
|
2060
|
-
validateModelData(docToProcess, model);
|
|
2068
|
+
await validateModelData(docToProcess, model);
|
|
2061
2069
|
await applyFieldFilters(docToProcess, model);
|
|
2062
2070
|
await checkUniqueFields(docToProcess, model, collection);
|
|
2063
2071
|
|
|
@@ -2299,9 +2307,9 @@ const internalEditOrPatchData = async (modelName, filter, data, files, user, isP
|
|
|
2299
2307
|
// 4. Validation adaptée pour patch ou edit (inchangé)
|
|
2300
2308
|
if (!isPatch) {
|
|
2301
2309
|
const dataToValidate = { ...existingDocs[0], ...updateData };
|
|
2302
|
-
validateModelData(dataToValidate, model, false);
|
|
2310
|
+
await validateModelData(dataToValidate, model, false);
|
|
2303
2311
|
} else {
|
|
2304
|
-
validateModelData(updateData, model, true);
|
|
2312
|
+
await validateModelData(updateData, model, true);
|
|
2305
2313
|
}
|
|
2306
2314
|
|
|
2307
2315
|
// 5. Vérification des champs uniques (inchangé)
|
|
@@ -2325,9 +2333,21 @@ const internalEditOrPatchData = async (modelName, filter, data, files, user, isP
|
|
|
2325
2333
|
for (const field of relationFields) {
|
|
2326
2334
|
if (updateData[field.name] !== undefined) {
|
|
2327
2335
|
const relationValue = updateData[field.name];
|
|
2328
|
-
if
|
|
2329
|
-
|
|
2330
|
-
|
|
2336
|
+
// Only process relations if the value is an object or an array containing at least one object.
|
|
2337
|
+
// An array of strings (ObjectIDs) should be passed through as-is for the update.
|
|
2338
|
+
let shouldProcessRelation = false;
|
|
2339
|
+
if (Array.isArray(relationValue)) {
|
|
2340
|
+
// If any item in the array is a plain object, we need to process the whole array
|
|
2341
|
+
// to handle potential nested creations or lookups.
|
|
2342
|
+
if (relationValue.some(item => isPlainObject(item))) {
|
|
2343
|
+
shouldProcessRelation = true;
|
|
2344
|
+
}
|
|
2345
|
+
} else if (isPlainObject(relationValue)) {
|
|
2346
|
+
shouldProcessRelation = true;
|
|
2347
|
+
}
|
|
2348
|
+
if (shouldProcessRelation) {
|
|
2349
|
+
const insertedIds = await pushDataUnsecure(relationValue, field.relation, user);
|
|
2350
|
+
updateData[field.name] = field.multiple ? insertedIds || [] : (insertedIds?.[0] || null);
|
|
2331
2351
|
}
|
|
2332
2352
|
}
|
|
2333
2353
|
}
|
|
@@ -2402,6 +2422,17 @@ const internalEditOrPatchData = async (modelName, filter, data, files, user, isP
|
|
|
2402
2422
|
const bulkResult = await collection.bulkWrite(bulkOps);
|
|
2403
2423
|
const modifiedCount = bulkResult.modifiedCount || 0;
|
|
2404
2424
|
|
|
2425
|
+
// Déclencher l'événement OnDataEdited avec les états avant/après
|
|
2426
|
+
if (modifiedCount > 0) {
|
|
2427
|
+
const updatedDocs = await collection.find({ _id: { $in: ids } }).toArray();
|
|
2428
|
+
await Event.Trigger("OnDataEdited", "event", "system", engine, {
|
|
2429
|
+
modelName,
|
|
2430
|
+
user,
|
|
2431
|
+
before: existingDocs, // Documents avant la modification
|
|
2432
|
+
after: updatedDocs // Documents après la modification
|
|
2433
|
+
});
|
|
2434
|
+
}
|
|
2435
|
+
|
|
2405
2436
|
// 11. Tâches post-mise à jour (schedules, workflows) (inchangé)
|
|
2406
2437
|
if (["workflowTrigger", "alert"].includes(modelName)) {
|
|
2407
2438
|
await handleScheduledJobs(modelName, existingDocs, collection, finalDataForSet);
|
|
@@ -2462,7 +2493,7 @@ async function handleScheduledJobs(modelName, existingDocs, collection, updateDa
|
|
|
2462
2493
|
schedule.scheduleJob(jobId, updatedDoc.cronExpression, async () => {
|
|
2463
2494
|
logger.info(`[Scheduled Job] Cron triggered for job ${jobId}`);
|
|
2464
2495
|
await runScheduledJobWithDbLock(jobId, async () => {
|
|
2465
|
-
sendSseToUser(updatedDoc._user, {
|
|
2496
|
+
await sendSseToUser(updatedDoc._user, {
|
|
2466
2497
|
type: 'cron_alert',
|
|
2467
2498
|
triggerId: updatedDoc._id.toString(),
|
|
2468
2499
|
triggerName: updatedDoc.name,
|
|
@@ -2685,8 +2716,8 @@ export const deleteData = async (modelName, filter, user ={}, triggerWorkflow, w
|
|
|
2685
2716
|
}
|
|
2686
2717
|
|
|
2687
2718
|
const res = { success: true, deletedCount }
|
|
2688
|
-
const plugin = Event.Trigger("OnDataDeleted", "event", "system", engine, {model:modelName, filter});
|
|
2689
|
-
Event.Trigger("OnDataDeleted", "event", "user", {model:modelName, filter});
|
|
2719
|
+
const plugin = await Event.Trigger("OnDataDeleted", "event", "system", engine, {model:modelName, filter});
|
|
2720
|
+
await Event.Trigger("OnDataDeleted", "event", "user", {model:modelName, filter});
|
|
2690
2721
|
return plugin || res;
|
|
2691
2722
|
|
|
2692
2723
|
} catch (error) {
|
|
@@ -3194,8 +3225,8 @@ export const searchData = async (query, user) => {
|
|
|
3194
3225
|
data = await handleFields(modelElement, data, user);
|
|
3195
3226
|
|
|
3196
3227
|
const res = {data, count: count[0]?.count || 0};
|
|
3197
|
-
const plugin = Event.Trigger("OnDataSearched", "event", "system", engine, {data, count: count[0]?.count});
|
|
3198
|
-
Event.Trigger("OnDataSearched", "event", "user", plugin || {data, count: count[0]?.count});
|
|
3228
|
+
const plugin = await Event.Trigger("OnDataSearched", "event", "system", engine, {data, count: count[0]?.count});
|
|
3229
|
+
await Event.Trigger("OnDataSearched", "event", "user", plugin || {data, count: count[0]?.count});
|
|
3199
3230
|
return plugin || res;
|
|
3200
3231
|
}
|
|
3201
3232
|
|
|
@@ -3272,7 +3303,7 @@ export const importData = async(options, files, user) => {
|
|
|
3272
3303
|
});
|
|
3273
3304
|
}
|
|
3274
3305
|
|
|
3275
|
-
validateModelStructure(modelData);
|
|
3306
|
+
await validateModelStructure(modelData);
|
|
3276
3307
|
await modelsCollection.insertOne(modelData);
|
|
3277
3308
|
logger.info(`[Model Import] Successfully imported model '${modelName}' for user ${user.username}.`);
|
|
3278
3309
|
} catch (e) {
|
|
@@ -3354,7 +3385,7 @@ export const importData = async(options, files, user) => {
|
|
|
3354
3385
|
if (insertedIdsArray && insertedIdsArray.length > 0) {
|
|
3355
3386
|
importJobs[importJobId].processedRecords += insertedIdsArray.length;
|
|
3356
3387
|
logger.debug(`[Import Job ${importJobId}] Processed chunk for '${modelName}': ${insertedIdsArray.length} records. Total processed: ${importJobs[importJobId].processedRecords}`);
|
|
3357
|
-
sendSseToUser(user.username, {
|
|
3388
|
+
await sendSseToUser(user.username, {
|
|
3358
3389
|
type: 'import_progress',
|
|
3359
3390
|
job: importJobs[importJobId]
|
|
3360
3391
|
});
|
|
@@ -3458,7 +3489,7 @@ export const importData = async(options, files, user) => {
|
|
|
3458
3489
|
const insertedIdsArray = await pushDataUnsecure(chunk, modelNameForImport, user, {});
|
|
3459
3490
|
if (insertedIdsArray && insertedIdsArray.length > 0) {
|
|
3460
3491
|
importJobs[importJobId].processedRecords += insertedIdsArray.length;
|
|
3461
|
-
sendSseToUser(user.username, {
|
|
3492
|
+
await sendSseToUser(user.username, {
|
|
3462
3493
|
type: 'import_progress',
|
|
3463
3494
|
job: importJobs[importJobId]
|
|
3464
3495
|
});
|
|
@@ -3552,7 +3583,7 @@ export const importData = async(options, files, user) => {
|
|
|
3552
3583
|
const insertedIdsArray = await pushDataUnsecure(chunk, modelNameForImport, user, {});
|
|
3553
3584
|
if (insertedIdsArray && insertedIdsArray.length > 0) {
|
|
3554
3585
|
importJobs[importJobId].processedRecords += insertedIdsArray.length;
|
|
3555
|
-
sendSseToUser(user.username, {
|
|
3586
|
+
await sendSseToUser(user.username, {
|
|
3556
3587
|
type: 'import_progress',
|
|
3557
3588
|
job: importJobs[importJobId]
|
|
3558
3589
|
});
|
|
@@ -3597,7 +3628,7 @@ export const importData = async(options, files, user) => {
|
|
|
3597
3628
|
} else {
|
|
3598
3629
|
importJobs[importJobId].status = 'completed';
|
|
3599
3630
|
}
|
|
3600
|
-
sendSseToUser(user.username, {
|
|
3631
|
+
await sendSseToUser(user.username, {
|
|
3601
3632
|
type: 'import_progress',
|
|
3602
3633
|
job: importJobs[importJobId]
|
|
3603
3634
|
});
|
|
@@ -3733,8 +3764,8 @@ export const exportData= async (options, user) =>{
|
|
|
3733
3764
|
}
|
|
3734
3765
|
|
|
3735
3766
|
const res = { success: true, data: exportResults, models: modelsToExport };
|
|
3736
|
-
const plugin = Event.Trigger("OnDataExported", "event", "system", engine, exportResults, modelsToExport);
|
|
3737
|
-
Event.Trigger("OnDataExported", "event", "user", plugin?.exportResults || exportResults, plugin?.modelsToExport || modelsToExport);
|
|
3767
|
+
const plugin = await Event.Trigger("OnDataExported", "event", "system", engine, exportResults, modelsToExport);
|
|
3768
|
+
await Event.Trigger("OnDataExported", "event", "user", plugin?.exportResults || exportResults, plugin?.modelsToExport || modelsToExport);
|
|
3738
3769
|
return plugin || res;
|
|
3739
3770
|
}
|
|
3740
3771
|
|
|
@@ -4086,7 +4117,7 @@ export const loadFromDump = async (user, options = {}) => {
|
|
|
4086
4117
|
modelsCache.flushAll();
|
|
4087
4118
|
|
|
4088
4119
|
logger.info(`[${action}] Restore successful for user ${user.username}.`);
|
|
4089
|
-
Event.Trigger("OnDataRestored", "event", "system");
|
|
4120
|
+
await Event.Trigger("OnDataRestored", "event", "system");
|
|
4090
4121
|
|
|
4091
4122
|
} finally {
|
|
4092
4123
|
// --- GUARANTEED CLEANUP ---
|
|
@@ -4356,7 +4387,7 @@ export async function installPack(packIdentifier, user = null, lang = 'en', isTe
|
|
|
4356
4387
|
preparedModel.fields.forEach(f => f.locked = false);
|
|
4357
4388
|
}
|
|
4358
4389
|
|
|
4359
|
-
validateModelStructure(preparedModel);
|
|
4390
|
+
await validateModelStructure(preparedModel);
|
|
4360
4391
|
await modelsCollection.insertOne(preparedModel);
|
|
4361
4392
|
summary.models.installed.push(modelName);
|
|
4362
4393
|
|
|
@@ -4527,7 +4558,7 @@ export async function installPack(packIdentifier, user = null, lang = 'en', isTe
|
|
|
4527
4558
|
|
|
4528
4559
|
// Trigger event only if pack came from database (original behavior)
|
|
4529
4560
|
if (typeof packIdentifier === 'string') {
|
|
4530
|
-
Event.Trigger("OnPackInstalled", "event", "system", pack);
|
|
4561
|
+
await Event.Trigger("OnPackInstalled", "event", "system", pack);
|
|
4531
4562
|
}
|
|
4532
4563
|
|
|
4533
4564
|
const modifiedCount = summary.datas.inserted + summary.datas.updated;
|
|
@@ -144,10 +144,10 @@ const middlewareLogger = async (req, res, next) => {
|
|
|
144
144
|
* @param {object} data - L'objet de données à envoyer.
|
|
145
145
|
* @returns {boolean} - True si l'événement a été envoyé, false sinon.
|
|
146
146
|
*/
|
|
147
|
-
export function sendSseToUser(username, data) {
|
|
147
|
+
export async function sendSseToUser(username, data) {
|
|
148
148
|
const res = sseConnections.get(username);
|
|
149
149
|
if (res) {
|
|
150
|
-
const ssePlugin = Event.Trigger("sendSseToUser", "system", "calls", data) || data;
|
|
150
|
+
const ssePlugin = await Event.Trigger("sendSseToUser", "system", "calls", data) || data;
|
|
151
151
|
res.write(`data: ${JSON.stringify(ssePlugin)}\n\n`);
|
|
152
152
|
return true;
|
|
153
153
|
}
|
|
@@ -791,7 +791,7 @@ export async function registerRoutes(engine){
|
|
|
791
791
|
}
|
|
792
792
|
try {
|
|
793
793
|
const modelData = req.fields;
|
|
794
|
-
validateModelStructure(modelData);
|
|
794
|
+
await validateModelStructure(modelData);
|
|
795
795
|
|
|
796
796
|
|
|
797
797
|
const existingModel = await modelsCollection.findOne({name: modelData.name, $and: [{_user: {$exists: true}}, {$or: [{_user: req.me._user}, {_user: req.me.username}]}] });
|
|
@@ -872,7 +872,7 @@ export async function registerRoutes(engine){
|
|
|
872
872
|
await cancelAlerts(req.me);
|
|
873
873
|
|
|
874
874
|
await getPromise();
|
|
875
|
-
Event.Trigger('jobAddUserData', req.me.username);
|
|
875
|
+
await Event.Trigger('jobAddUserData', req.me.username);
|
|
876
876
|
}else{
|
|
877
877
|
await getPromise();
|
|
878
878
|
}
|
package/src/modules/mongodb.js
CHANGED
|
@@ -8,6 +8,7 @@ export let modelsCollection, datasCollection, filesCollection, packsCollection;
|
|
|
8
8
|
export {ObjectId};
|
|
9
9
|
|
|
10
10
|
let engine, logger;
|
|
11
|
+
let colls= [];
|
|
11
12
|
export async function onInit(defaultEngine) {
|
|
12
13
|
engine = defaultEngine;
|
|
13
14
|
logger = engine.getComponent(Logger);
|
|
@@ -17,9 +18,24 @@ export async function onInit(defaultEngine) {
|
|
|
17
18
|
filesCollection = getCollection("files");
|
|
18
19
|
packsCollection = getCollection("packs");
|
|
19
20
|
|
|
21
|
+
colls = await MongoDatabase.listCollections().toArray();
|
|
22
|
+
|
|
20
23
|
logger.info("MongoDB collections loaded.");
|
|
21
|
-
}
|
|
24
|
+
}
|
|
22
25
|
|
|
26
|
+
export const getCollections= async (forceRefresh)=>{
|
|
27
|
+
if( !forceRefresh )
|
|
28
|
+
return colls;
|
|
29
|
+
colls = await MongoDatabase.listCollections().toArray();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const createCollection = async (coll)=>{
|
|
33
|
+
const found =colls.find(f => f.name === coll);
|
|
34
|
+
if( found){
|
|
35
|
+
return getCollection(coll);
|
|
36
|
+
}
|
|
37
|
+
return await MongoDatabase.createCollection(coll);
|
|
38
|
+
}
|
|
23
39
|
|
|
24
40
|
export const isObjectId = (id) => {
|
|
25
41
|
return (typeof(id) === 'string' && id.match(/^[0-9a-fA-F]{24}$/));
|
package/src/modules/user.js
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import i18n from "../i18n.js";
|
|
2
2
|
import {MongoDatabase} from "../engine.js";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
createCollection,
|
|
5
|
+
getCollection,
|
|
6
|
+
getCollectionForUser,
|
|
7
|
+
getCollections,
|
|
8
|
+
getUserCollectionName
|
|
9
|
+
} from "./mongodb.js";
|
|
4
10
|
import {isLocalUser} from "../data.js";
|
|
5
11
|
import {ObjectId} from "mongodb";
|
|
6
12
|
import {getAPILang, searchData} from "./data/index.js";
|
|
@@ -21,11 +27,11 @@ export const userInitiator = async (req, res, next) => {
|
|
|
21
27
|
i18n.changeLanguage(lang);
|
|
22
28
|
|
|
23
29
|
if (await engine.userProvider.hasFeature(req.me, 'indexes')) {
|
|
24
|
-
const collections = await
|
|
30
|
+
const collections = await getCollections();
|
|
25
31
|
const collectionNames = collections.map(c => c.name);
|
|
26
32
|
const coll = await getUserCollectionName(req.me);
|
|
27
33
|
if (collectionNames.includes(coll)) {
|
|
28
|
-
const collection = await
|
|
34
|
+
const collection = await createCollection(coll);
|
|
29
35
|
const indexes = await collection.indexes();
|
|
30
36
|
if (!indexes.find(i => i.name === 'genericPartialIndex')) {
|
|
31
37
|
await collection.createIndex({"$**": 1}, {
|
package/src/packs.js
CHANGED
|
@@ -4259,7 +4259,8 @@ This ensures that your application only processes legitimate requests from Strip
|
|
|
4259
4259
|
"all": {
|
|
4260
4260
|
"env": [
|
|
4261
4261
|
{ "name": "STRIPE_SECRET_KEY", "value": "sk_test_YOUR_SECRET_KEY" },
|
|
4262
|
-
{ "name": "STRIPE_WEBHOOK_SECRET", "value": "whsec_YOUR_WEBHOOK_SECRET" }
|
|
4262
|
+
{ "name": "STRIPE_WEBHOOK_SECRET", "value": "whsec_YOUR_WEBHOOK_SECRET" },
|
|
4263
|
+
{ "name": "APP_BASE_URL", "value": "https://yourdomain.tld" }
|
|
4263
4264
|
],
|
|
4264
4265
|
"endpoint": [
|
|
4265
4266
|
{
|