data-primals-engine 1.0.7 → 1.0.8
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/package.json +1 -1
- package/server.js +11 -2
- package/src/data.js +15 -5
- package/src/email.js +2 -0
- package/src/engine.js +41 -39
- package/src/gameObject.js +5 -7
- package/src/modules/data.js +75 -58
- package/src/modules/mongodb.js +1 -1
- package/src/modules/workflow.js +190 -58
- package/src/packs.js +26 -10
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "data-primals-engine",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.8",
|
|
4
4
|
"description": "data-primals-engine is the package responsible from handling large amount of data in a practical and performant way. It can handle large amount of data in a practical and performant way. It can also get workflow models working (for automation), and fully supports internationalisation.",
|
|
5
5
|
"main": "src/engine.js",
|
|
6
6
|
"type": "module",
|
package/server.js
CHANGED
|
@@ -13,11 +13,20 @@ const bench = GameObject.Create("Benchmark");
|
|
|
13
13
|
const timer = bench.addComponent(BenchmarkTool);
|
|
14
14
|
timer.start();
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
|
|
17
|
+
const engine = await Engine.Create();
|
|
18
|
+
|
|
19
|
+
if (process.argv.length === 3) {
|
|
20
|
+
let arg = process.argv[2];
|
|
21
|
+
if( arg === 'reset-models'){
|
|
22
|
+
console.log("resetting models");
|
|
23
|
+
await engine.resetModels();
|
|
24
|
+
}
|
|
25
|
+
}
|
|
17
26
|
|
|
18
27
|
const port = process.env.PORT || 7633;
|
|
19
28
|
engine.start(port, async (r) => {
|
|
20
29
|
const logger = engine.getComponent(Logger);
|
|
21
|
-
|
|
30
|
+
console.log("Server started on port" + port);
|
|
22
31
|
timer.stop();
|
|
23
32
|
});
|
package/src/data.js
CHANGED
|
@@ -29,8 +29,12 @@ export const isLocalUser = (user) => {
|
|
|
29
29
|
return user && user._model === 'user' && typeof(user._user) === 'string' && user._user.trim() !== '';
|
|
30
30
|
};
|
|
31
31
|
|
|
32
|
+
export const isDemoUser = (user) => {
|
|
33
|
+
return /^demo[0-9]{1,2}$/.test(user?.username);
|
|
34
|
+
}
|
|
35
|
+
|
|
32
36
|
export function getUserHash(user) {
|
|
33
|
-
if(
|
|
37
|
+
if( isDemoUser(user) ){
|
|
34
38
|
return user.username;
|
|
35
39
|
}
|
|
36
40
|
return user ? (
|
|
@@ -419,8 +423,9 @@ export const getFieldValueHash = (model, data) => {
|
|
|
419
423
|
* @param {array} allModels - Un tableau contenant les définitions de TOUS les modèles du système.
|
|
420
424
|
* @returns {string} - La chaîne de caractères représentant la donnée.
|
|
421
425
|
*/
|
|
422
|
-
export const getDataAsString = (model, data,
|
|
423
|
-
|
|
426
|
+
export const getDataAsString = (model, data, tr, allModels, extended=false) => {
|
|
427
|
+
const { t, i18n} = tr;
|
|
428
|
+
const lang = (i18n.resolvedLanguage || i18n.language).split(/[-_]/)?.[0];
|
|
424
429
|
// Cas de base : si le modèle ou les données sont manquants, on ne peut rien faire.
|
|
425
430
|
if (!model || !data) {
|
|
426
431
|
return '';
|
|
@@ -464,7 +469,6 @@ export const getDataAsString = (model, data, t, allModels, extended=false) => {
|
|
|
464
469
|
const relatedModel = allModels?.find(m => m.name === fieldDef.relation);
|
|
465
470
|
if (!relatedModel) return `[${fieldDef.relation}]`; // Modnon trouvé
|
|
466
471
|
|
|
467
|
-
console.log({relatedModel, value});
|
|
468
472
|
// Si la relation est multiple (un tableau d'objets)
|
|
469
473
|
if (Array.isArray(value)) {
|
|
470
474
|
return value
|
|
@@ -474,10 +478,16 @@ export const getDataAsString = (model, data, t, allModels, extended=false) => {
|
|
|
474
478
|
}
|
|
475
479
|
// Si la relation est simple (un seul objet)
|
|
476
480
|
else if (typeof value === 'object') {
|
|
477
|
-
console.log({relatedModel, value});
|
|
478
481
|
return getDataAsString(relatedModel, value, t, allModels); // Appel récursif
|
|
479
482
|
}
|
|
480
483
|
}
|
|
484
|
+
|
|
485
|
+
if(fieldDef.type === 'datetime'){
|
|
486
|
+
return new Date(value).toLocaleDateString(lang, {year: 'numeric', month: 'numeric', day: 'numeric', hour: 'numeric', minute: 'numeric'});
|
|
487
|
+
}
|
|
488
|
+
if(fieldDef.type === 'date'){
|
|
489
|
+
return new Date(value).toLocaleDateString(lang, {year: 'numeric', month: 'numeric', day: 'numeric'});
|
|
490
|
+
}
|
|
481
491
|
// --- FIN DE LA NOUVELLE LOGIQUE ---
|
|
482
492
|
|
|
483
493
|
// Logique existante pour les autres types de champs
|
package/src/email.js
CHANGED
|
@@ -2,6 +2,8 @@ import { translations } from "data-primals-engine/i18n";
|
|
|
2
2
|
import process from "node:process";
|
|
3
3
|
import nodemailer from "nodemailer";
|
|
4
4
|
import juice from "juice";
|
|
5
|
+
import {event_trigger} from "./core.js";
|
|
6
|
+
import {emailDefaultConfig} from "./constants.js";
|
|
5
7
|
|
|
6
8
|
// Le transporteur par défaut, utilisé si aucune config spécifique n'est fournie.
|
|
7
9
|
const defaultTransporter = nodemailer.createTransport({
|
package/src/engine.js
CHANGED
|
@@ -8,7 +8,7 @@ import {cookiesSecret, dbName} from "./constants.js";
|
|
|
8
8
|
import http from "http";
|
|
9
9
|
import cookieParser from "cookie-parser";
|
|
10
10
|
import requestIp from 'request-ip';
|
|
11
|
-
import {createModel, getModels, validateModelStructure} from "./modules/data.js";
|
|
11
|
+
import {createModel, deleteModels, getModels, validateModelStructure} from "./modules/data.js";
|
|
12
12
|
import {defaultModels} from "./defaultModels.js";
|
|
13
13
|
import {DefaultUserProvider} from "./providers.js";
|
|
14
14
|
import formidableMiddleware from 'express-formidable';
|
|
@@ -25,9 +25,10 @@ export const MongoDatabase = MongoClient.db(dbName);
|
|
|
25
25
|
|
|
26
26
|
|
|
27
27
|
export const Engine = {
|
|
28
|
-
Create:
|
|
28
|
+
Create: async (options) => {
|
|
29
29
|
const engine = GameObject.Create("Engine");
|
|
30
30
|
console.log("Creating engine", Config.Get('modules'));
|
|
31
|
+
engine.addComponent(Logger);
|
|
31
32
|
|
|
32
33
|
engine.userProvider = new DefaultUserProvider(engine);
|
|
33
34
|
|
|
@@ -71,10 +72,38 @@ export const Engine = {
|
|
|
71
72
|
return engine._modules.find(m => m.module === module);
|
|
72
73
|
};
|
|
73
74
|
|
|
75
|
+
|
|
76
|
+
const logger = engine.getComponent(Logger);
|
|
77
|
+
|
|
78
|
+
const importModule = async (module) => {
|
|
79
|
+
const moduleA = await import(module);
|
|
80
|
+
if (moduleA.onInit){
|
|
81
|
+
await moduleA.onInit(engine);
|
|
82
|
+
return {...moduleA, module};
|
|
83
|
+
}else {
|
|
84
|
+
const mod = moduleA.default();
|
|
85
|
+
await mod?.onInit(engine);
|
|
86
|
+
return { ...mod, module};
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
await Promise.all(Config.Get('modules').map(async module => {
|
|
91
|
+
try {
|
|
92
|
+
if( fs.existsSync(module)){
|
|
93
|
+
return await importModule(module);
|
|
94
|
+
}else {
|
|
95
|
+
return await importModule('./modules/' + module + ".js");
|
|
96
|
+
}
|
|
97
|
+
} catch (e){
|
|
98
|
+
console.log('ERROR at loading module '+ module + ' in /modules dir.'+ e);
|
|
99
|
+
}
|
|
100
|
+
})).then(async e => {
|
|
101
|
+
engine._modules = e;
|
|
102
|
+
return Promise.resolve();
|
|
103
|
+
});
|
|
74
104
|
let server;
|
|
75
105
|
engine.start = async (port, cb) =>{
|
|
76
106
|
// Use connect method to connect to the server
|
|
77
|
-
await MongoClient.connect();
|
|
78
107
|
|
|
79
108
|
// Start http server
|
|
80
109
|
server = http.createServer(app);
|
|
@@ -87,37 +116,11 @@ export const Engine = {
|
|
|
87
116
|
|
|
88
117
|
server.listen(port);
|
|
89
118
|
|
|
90
|
-
const importModule = async (module) => {
|
|
91
|
-
const moduleA = await import(module);
|
|
92
|
-
if (moduleA.onInit){
|
|
93
|
-
await moduleA.onInit(engine);
|
|
94
|
-
return {...moduleA, module};
|
|
95
|
-
}else {
|
|
96
|
-
const mod = moduleA.default();
|
|
97
|
-
await mod?.onInit(engine);
|
|
98
|
-
return { ...mod, module};
|
|
99
|
-
}
|
|
100
|
-
};
|
|
101
|
-
|
|
102
|
-
await Promise.all(Config.Get('modules').map(async module => {
|
|
103
|
-
try {
|
|
104
|
-
if( fs.existsSync(module)){
|
|
105
|
-
return await importModule(module);
|
|
106
|
-
}else {
|
|
107
|
-
return await importModule('./modules/' + module + ".js");
|
|
108
|
-
}
|
|
109
|
-
} catch (e){
|
|
110
|
-
console.log('ERROR at loading module '+ module + ' in /modules dir.'+ e);
|
|
111
|
-
}
|
|
112
|
-
})).then(async e => {
|
|
113
|
-
engine._modules = e;
|
|
114
|
-
if (cb)
|
|
115
|
-
return await cb();
|
|
116
|
-
return Promise.resolve();
|
|
117
|
-
});
|
|
118
|
-
|
|
119
119
|
await setupInitialModels();
|
|
120
120
|
|
|
121
|
+
if (cb)
|
|
122
|
+
await cb();
|
|
123
|
+
|
|
121
124
|
process.on('uncaughtException', function (exception) {
|
|
122
125
|
console.error(exception);
|
|
123
126
|
fs.appendFile('issues.txt', JSON.stringify({ code: exception.code, message: exception.message, stack: exception.stack }), function (err) {
|
|
@@ -128,14 +131,11 @@ export const Engine = {
|
|
|
128
131
|
process.exit(0);
|
|
129
132
|
});
|
|
130
133
|
}
|
|
131
|
-
engine.addComponent(Logger);
|
|
132
134
|
|
|
133
135
|
engine.stop = async () => {
|
|
134
136
|
await server.close();
|
|
135
137
|
};
|
|
136
138
|
|
|
137
|
-
const logger = engine.getComponent(Logger);
|
|
138
|
-
|
|
139
139
|
async function setupInitialModels() {
|
|
140
140
|
logger.info("Validating structures of default models...");
|
|
141
141
|
const ms = Object.values(Config.Get('defaultModels', defaultModels));
|
|
@@ -151,13 +151,15 @@ export const Engine = {
|
|
|
151
151
|
model.locked = true;
|
|
152
152
|
const r = await createModel(model);
|
|
153
153
|
dbModels.push({...model, _id: r.insertedId });
|
|
154
|
-
|
|
155
|
-
else
|
|
156
|
-
|
|
154
|
+
logger.info('Model inserted (' + model.name + ')');
|
|
155
|
+
}else
|
|
156
|
+
logger.info('Model loaded (' + model.name + ')');
|
|
157
157
|
}
|
|
158
158
|
logger.info("All models loaded.");
|
|
159
159
|
}
|
|
160
|
-
|
|
160
|
+
engine.resetModels = async () => {
|
|
161
|
+
await deleteModels();
|
|
162
|
+
};
|
|
161
163
|
return engine;
|
|
162
164
|
}
|
|
163
165
|
}
|
package/src/gameObject.js
CHANGED
|
@@ -70,23 +70,21 @@ export class UsableBehaviour extends Behaviour {
|
|
|
70
70
|
|
|
71
71
|
use() {
|
|
72
72
|
Event.Trigger("GameObject.UsableBehavior.use", "system", "calls", this);
|
|
73
|
-
// Logique d'utilisation (exemple)
|
|
74
|
-
console.log(this.gameObject.name + " a été utilisé !");
|
|
75
73
|
}
|
|
76
74
|
}
|
|
77
75
|
|
|
78
|
-
const
|
|
76
|
+
const mainDriver = GameObject.Create("MainDrivers");
|
|
79
77
|
|
|
80
78
|
// Exemple d'attachement de comportements
|
|
81
|
-
|
|
82
|
-
|
|
79
|
+
mainDriver.addComponent(MovableBehaviour, 10);
|
|
80
|
+
mainDriver.addComponent(UsableBehaviour);
|
|
83
81
|
|
|
84
82
|
// Accéder et utiliser les composants
|
|
85
|
-
const movable =
|
|
83
|
+
const movable = mainDriver.getComponent(MovableBehaviour);
|
|
86
84
|
if (movable) {
|
|
87
85
|
movable.update();
|
|
88
86
|
}
|
|
89
|
-
const usable =
|
|
87
|
+
const usable = mainDriver.getComponent(UsableBehaviour);
|
|
90
88
|
if (usable) {
|
|
91
89
|
usable.use();
|
|
92
90
|
}
|
package/src/modules/data.js
CHANGED
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
encryptValue,
|
|
17
17
|
getDefaultForType,
|
|
18
18
|
getFieldValueHash,
|
|
19
|
-
getUserId,
|
|
19
|
+
getUserId, isDemoUser,
|
|
20
20
|
isLocalUser
|
|
21
21
|
} from "../data.js";
|
|
22
22
|
import {
|
|
@@ -88,6 +88,7 @@ import {
|
|
|
88
88
|
import {assistantGlobalLimiter} from "./assistant.js";
|
|
89
89
|
import {getAllPacks} from "../packs.js";
|
|
90
90
|
import {throttleMiddleware} from "../middlewares/throttle.js";
|
|
91
|
+
import {Config} from "../config.js";
|
|
91
92
|
|
|
92
93
|
// Obtenir le chemin du répertoire courant de manière fiable avec ES Modules
|
|
93
94
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -147,7 +148,7 @@ export const jobDumpUserData = async () => {
|
|
|
147
148
|
|
|
148
149
|
users.forEach((user) =>
|
|
149
150
|
{
|
|
150
|
-
if(
|
|
151
|
+
if( isDemoUser(user) && Config.Get("useDemoAccounts"))
|
|
151
152
|
return;
|
|
152
153
|
try {
|
|
153
154
|
dumpUserData(user).catch(e => {
|
|
@@ -383,7 +384,7 @@ export const dataTypes = {
|
|
|
383
384
|
}
|
|
384
385
|
},
|
|
385
386
|
boolean: {
|
|
386
|
-
validate: (value) => typeof value === 'boolean',
|
|
387
|
+
validate: (value) => value === null || typeof value === 'boolean',
|
|
387
388
|
anonymize: () => {
|
|
388
389
|
return !!getRandom(0, 1);
|
|
389
390
|
}
|
|
@@ -1459,7 +1460,7 @@ export async function onInit(defaultEngine) {
|
|
|
1459
1460
|
|
|
1460
1461
|
engine.put('/api/model/:id', [middlewareAuthenticator, userInitiator, setTimeoutMiddleware(15000)], async (req, res) => {
|
|
1461
1462
|
|
|
1462
|
-
if(
|
|
1463
|
+
if( !(isDemoUser(req.me) && Config.Get("useDemoAccounts")) && isLocalUser(req.me) && !await hasPermission(["API_ADMIN", "API_EDIT_MODEL"], req.me)){
|
|
1463
1464
|
return res.status(403).json({success: false, error: i18n.t('api.permission.editModel', 'Cannot edit models from the API')})
|
|
1464
1465
|
}
|
|
1465
1466
|
|
|
@@ -1698,7 +1699,7 @@ export async function onInit(defaultEngine) {
|
|
|
1698
1699
|
|
|
1699
1700
|
engine.get('/api/models', [throttle, middlewareAuthenticator, userInitiator, middlewareLogger], async (req, res) => {
|
|
1700
1701
|
|
|
1701
|
-
if(
|
|
1702
|
+
if( !(isDemoUser(req.me) && Config.Get("useDemoAccounts")) && isLocalUser(req.me) && !await hasPermission(["API_ADMIN", "API_GET_MODELS"], req.me)){
|
|
1702
1703
|
return res.status(403).json({success: false, error: i18n.t('api.permission.getModels')})
|
|
1703
1704
|
}
|
|
1704
1705
|
|
|
@@ -1726,7 +1727,7 @@ export async function onInit(defaultEngine) {
|
|
|
1726
1727
|
return res.status(400).json({error: "Le paramètre 'name' est requis."});
|
|
1727
1728
|
}
|
|
1728
1729
|
|
|
1729
|
-
if(
|
|
1730
|
+
if( !(isDemoUser(req.me) && Config.Get("useDemoAccounts")) && isLocalUser(req.me) && !await hasPermission(["API_ADMIN", "API_GET_MODEL"], req.me) && !await hasPermission("API_GET_MODEL_"+modelName, req.me)){
|
|
1730
1731
|
return res.json({success: false, error: i18n.t('api.permission.getModel')})
|
|
1731
1732
|
}
|
|
1732
1733
|
|
|
@@ -1738,7 +1739,7 @@ export async function onInit(defaultEngine) {
|
|
|
1738
1739
|
});
|
|
1739
1740
|
engine.post('/api/model', [throttle, middlewareAuthenticator, userInitiator, middlewareLogger, myFreePremiumAnonymousLimiter], async (req, res) => {
|
|
1740
1741
|
|
|
1741
|
-
if(
|
|
1742
|
+
if( !(isDemoUser(req.me) && Config.Get("useDemoAccounts")) && isLocalUser(req.me) && !await hasPermission(["API_ADMIN", "API_ADD_MODEL"], req.me) ){
|
|
1742
1743
|
return res.status(403).json({success: false, error: i18n.t('api.permission.addModel')})
|
|
1743
1744
|
}
|
|
1744
1745
|
try {
|
|
@@ -1810,7 +1811,7 @@ export async function onInit(defaultEngine) {
|
|
|
1810
1811
|
})));
|
|
1811
1812
|
}
|
|
1812
1813
|
const install = !!req.fields.install;
|
|
1813
|
-
if( install &&
|
|
1814
|
+
if( install && (isDemoUser(req.me) && Config.Get("useDemoAccounts")) ){
|
|
1814
1815
|
|
|
1815
1816
|
await datasCollection.deleteMany({ _user: req.me.username});
|
|
1816
1817
|
await modelsCollection.deleteMany({ _user: req.me.username});
|
|
@@ -1846,7 +1847,7 @@ export async function onInit(defaultEngine) {
|
|
|
1846
1847
|
return res.status(400).json({error: "Le paramètre 'name' est requis."});
|
|
1847
1848
|
}
|
|
1848
1849
|
|
|
1849
|
-
if(
|
|
1850
|
+
if( !(isDemoUser(req.me) && Config.Get("useDemoAccounts")) && isLocalUser(req.me) && (
|
|
1850
1851
|
!await hasPermission(["API_ADMIN","API_DELETE_MODEL","API_DELETE_MODEL_"+modelName], req.me) ||
|
|
1851
1852
|
await hasPermission(["API_DELETE_MODEL_NOT_"+modelName], req.me))){
|
|
1852
1853
|
return res.status(403).json({success: false, error: i18n.t( "api.permission.deleteModel", { model: modelName})})
|
|
@@ -1910,7 +1911,7 @@ export async function onInit(defaultEngine) {
|
|
|
1910
1911
|
return res.status(404).json({ error: i18n.t('api.model.notFound', { model: modelId }) });
|
|
1911
1912
|
}
|
|
1912
1913
|
|
|
1913
|
-
if(
|
|
1914
|
+
if( !(isDemoUser(req.me) && Config.Get("useDemoAccounts")) && isLocalUser(req.me) && (
|
|
1914
1915
|
!await hasPermission(["API_ADMIN", "API_EDIT_MODEL", "API_EDIT_MODEL_"+model.name], req.me) ||
|
|
1915
1916
|
await hasPermission(["API_EDIT_MODEL_NOT_"+model.name], req.me))){
|
|
1916
1917
|
return res.status(403).json({success: false, error: i18n.t('api.permission.editModel')})
|
|
@@ -2323,7 +2324,7 @@ export async function onInit(defaultEngine) {
|
|
|
2323
2324
|
|
|
2324
2325
|
|
|
2325
2326
|
engine.post('/api/data/removeFromPack', [throttle, middlewareAuthenticator, userInitiator, myFreePremiumAnonymousLimiter], async (req, res) => {
|
|
2326
|
-
if(
|
|
2327
|
+
if( !(isDemoUser(req.me) && Config.Get("useDemoAccounts")) && isLocalUser(req.me) && !await hasPermission(["API_ADMIN", "API_CREATE_PACK"], req.me)){
|
|
2327
2328
|
return res.status(403).json({success: false, error: i18n.t('api.permission.createPack')})
|
|
2328
2329
|
}
|
|
2329
2330
|
const { itemIds } = req.fields;
|
|
@@ -2459,7 +2460,7 @@ export async function onInit(defaultEngine) {
|
|
|
2459
2460
|
const { packName, itemIds } = req.fields;
|
|
2460
2461
|
const user = req.me;
|
|
2461
2462
|
|
|
2462
|
-
if(
|
|
2463
|
+
if( !(isDemoUser(req.me) && Config.Get("useDemoAccounts")) && isLocalUser(req.me) && !await hasPermission(["API_ADMIN", "API_CREATE_PACK"], req.me)){
|
|
2463
2464
|
return res.status(403).json({success: false, error: i18n.t('api.permission.createPack')})
|
|
2464
2465
|
}
|
|
2465
2466
|
// --- Validation ---
|
|
@@ -2591,14 +2592,18 @@ export async function onInit(defaultEngine) {
|
|
|
2591
2592
|
}
|
|
2592
2593
|
|
|
2593
2594
|
export const createModel = async (data) => {
|
|
2594
|
-
return await
|
|
2595
|
+
return await getCollection('models').insertOne(data);
|
|
2596
|
+
}
|
|
2597
|
+
|
|
2598
|
+
export const deleteModels = async (filter) => {
|
|
2599
|
+
return await getCollection('models').deleteMany(filter ? filter : {_user: { $exists: false }});
|
|
2595
2600
|
}
|
|
2596
2601
|
|
|
2597
2602
|
export const getModel = async (modelName, user) => {
|
|
2598
2603
|
const modelInCache = modelsCache.get(user.username+"@@"+modelName);
|
|
2599
2604
|
if(modelInCache)
|
|
2600
2605
|
return modelInCache;
|
|
2601
|
-
const model = await
|
|
2606
|
+
const model = await getCollection('models').findOne({name: modelName, $and: [{_user: {$exists: true}}, {$or: [{_user: user._user}, {_user: user.username}]}]});
|
|
2602
2607
|
if (!model) {
|
|
2603
2608
|
throw new Error(i18n.t('api.model.notFound', {model: modelName}));
|
|
2604
2609
|
}
|
|
@@ -2606,7 +2611,7 @@ export const getModel = async (modelName, user) => {
|
|
|
2606
2611
|
return model;
|
|
2607
2612
|
}
|
|
2608
2613
|
export const getModels = async () => {
|
|
2609
|
-
return await
|
|
2614
|
+
return await getCollection('models').find({'$or': [{_user: { $exists: false}}]}).toArray();
|
|
2610
2615
|
}
|
|
2611
2616
|
|
|
2612
2617
|
|
|
@@ -2722,7 +2727,7 @@ export async function checkServerCapacity(incomingDataSize = 0) {
|
|
|
2722
2727
|
export const insertData = async (modelName, data, files, user, triggerWorkflow = true, waitForWorkflow = true) => {
|
|
2723
2728
|
|
|
2724
2729
|
// --- Vérification des permissions (inchangée) ---
|
|
2725
|
-
if (
|
|
2730
|
+
if (!(isDemoUser(user) && Config.Get("useDemoAccounts")) && isLocalUser(user) && (
|
|
2726
2731
|
!await hasPermission(["API_ADMIN", "API_ADD_DATA", "API_ADD_DATA_" + modelName], user) ||
|
|
2727
2732
|
await hasPermission(["API_ADD_DATA_NOT_" + modelName], user))) {
|
|
2728
2733
|
// Renvoyer une structure d'erreur cohérente
|
|
@@ -3161,28 +3166,29 @@ async function applyFieldFilters(docToProcess, model) {
|
|
|
3161
3166
|
/**
|
|
3162
3167
|
* Valide la structure et le contenu du document selon le modèle
|
|
3163
3168
|
*/
|
|
3164
|
-
function validateModelData(doc, model) {
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
|
|
3171
|
-
|
|
3172
|
-
|
|
3173
|
-
|
|
3169
|
+
function validateModelData(doc, model, isPatch = false) {
|
|
3170
|
+
if (!isPatch) {
|
|
3171
|
+
model.fields.forEach(field => {
|
|
3172
|
+
const value = doc[field.name];
|
|
3173
|
+
if (field.required) {
|
|
3174
|
+
if (value === undefined && !('default' in field)) {
|
|
3175
|
+
throw new Error(i18n.t('api.field.missingRequired', { field: field.name + " (" + model.name + ")" }));
|
|
3176
|
+
}
|
|
3177
|
+
if (value === '' || value === null) {
|
|
3178
|
+
throw new Error(i18n.t('api.field.requiredCannotBeEmpty', { field: field.name }));
|
|
3179
|
+
}
|
|
3174
3180
|
}
|
|
3175
|
-
}
|
|
3176
|
-
}
|
|
3181
|
+
});
|
|
3182
|
+
}
|
|
3177
3183
|
|
|
3178
|
-
// 2. Validation des types de champs
|
|
3184
|
+
// 2. Validation des types de champs (toujours exécutée pour les champs fournis)
|
|
3179
3185
|
for (const [fieldName, value] of Object.entries(doc)) {
|
|
3180
3186
|
const fieldDef = model.fields.find(f => f.name === fieldName);
|
|
3181
3187
|
if (!fieldDef) continue; // On ignore les champs supplémentaires
|
|
3182
3188
|
|
|
3183
3189
|
const validator = dataTypes[fieldDef.type]?.validate;
|
|
3184
3190
|
if (validator && !validator(value, fieldDef)) {
|
|
3185
|
-
throw new Error(i18n.t('api.field.validationFailed', { field: fieldName, value}));
|
|
3191
|
+
throw new Error(i18n.t('api.field.validationFailed', { field: fieldName, value }));
|
|
3186
3192
|
}
|
|
3187
3193
|
}
|
|
3188
3194
|
}
|
|
@@ -3358,9 +3364,11 @@ export const editData = async (modelName, filter, data, files, user, triggerWork
|
|
|
3358
3364
|
return await internalEditOrPatchData(modelName, filter, data, files, user, false, triggerWorkflow, waitForWorkflow);
|
|
3359
3365
|
};
|
|
3360
3366
|
|
|
3367
|
+
// Dans src/modules/data.js
|
|
3368
|
+
|
|
3361
3369
|
const internalEditOrPatchData = async (modelName, filter, data, files, user, isPatch, triggerWorkflow = true, waitForWorkflow = false) => {
|
|
3362
3370
|
try {
|
|
3363
|
-
// Vérification des permissions
|
|
3371
|
+
// 1. Vérification des permissions
|
|
3364
3372
|
if (user.username !== 'demo' && isLocalUser(user) && (
|
|
3365
3373
|
!await hasPermission(["API_ADMIN", "API_EDIT_DATA", "API_EDIT_DATA_" + modelName], user) ||
|
|
3366
3374
|
await hasPermission(["API_EDIT_DATA_NOT_" + modelName], user))) {
|
|
@@ -3373,22 +3381,20 @@ const internalEditOrPatchData = async (modelName, filter, data, files, user, isP
|
|
|
3373
3381
|
throw new Error(i18n.t("api.model.notFound", {model: modelName}));
|
|
3374
3382
|
}
|
|
3375
3383
|
|
|
3376
|
-
// Récupération des documents existants
|
|
3384
|
+
// 2. Récupération des documents existants et de leur hash original
|
|
3377
3385
|
const existingDocs = (await searchData({user, query: {model: modelName, filter}}))?.data;
|
|
3378
3386
|
if (!existingDocs || existingDocs.length === 0) {
|
|
3379
3387
|
return {success: false, error: i18n.t("api.data.notFound")};
|
|
3380
3388
|
}
|
|
3381
|
-
|
|
3382
3389
|
const ids = existingDocs.map(d => new ObjectId(d._id));
|
|
3390
|
+
const originalHash = existingDocs[0]._hash; // Sauvegarde du hash avant modification
|
|
3383
3391
|
|
|
3384
|
-
// Préparation des données de mise à jour
|
|
3392
|
+
// 3. Préparation des données de mise à jour (inchangé)
|
|
3385
3393
|
const updateData = {...data};
|
|
3386
3394
|
delete updateData._model;
|
|
3387
3395
|
delete updateData._user;
|
|
3388
3396
|
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
// Traitement des fichiers
|
|
3397
|
+
// Traitement des fichiers (inchangé)
|
|
3392
3398
|
const fileFields = model.fields.filter(f => f.type === 'file' || (f.type === 'array' && f.itemsType === 'file'));
|
|
3393
3399
|
for (const field of fileFields) {
|
|
3394
3400
|
if (files?.[field.name+'[0]']) {
|
|
@@ -3402,12 +3408,15 @@ const internalEditOrPatchData = async (modelName, filter, data, files, user, isP
|
|
|
3402
3408
|
}
|
|
3403
3409
|
}
|
|
3404
3410
|
|
|
3405
|
-
// Validation
|
|
3411
|
+
// 4. Validation adaptée pour patch ou edit (inchangé)
|
|
3406
3412
|
if (!isPatch) {
|
|
3407
|
-
|
|
3413
|
+
const dataToValidate = { ...existingDocs[0], ...updateData };
|
|
3414
|
+
validateModelData(dataToValidate, model, false);
|
|
3415
|
+
} else {
|
|
3416
|
+
validateModelData(updateData, model, true);
|
|
3408
3417
|
}
|
|
3409
3418
|
|
|
3410
|
-
// Vérification des champs uniques
|
|
3419
|
+
// 5. Vérification des champs uniques (inchangé)
|
|
3411
3420
|
const uniqueFields = model.fields.filter(f => f.unique);
|
|
3412
3421
|
for (const field of uniqueFields) {
|
|
3413
3422
|
if (updateData[field.name] !== undefined) {
|
|
@@ -3415,31 +3424,27 @@ const internalEditOrPatchData = async (modelName, filter, data, files, user, isP
|
|
|
3415
3424
|
_user: user._user || user.username,
|
|
3416
3425
|
_model: modelName,
|
|
3417
3426
|
[field.name]: updateData[field.name],
|
|
3418
|
-
_id: {$nin: ids}
|
|
3427
|
+
_id: {$nin: ids}
|
|
3419
3428
|
});
|
|
3420
|
-
|
|
3421
3429
|
if (existing) {
|
|
3422
|
-
throw new Error(i18n.t("api.data.duplicateValue", {
|
|
3423
|
-
field: field.name,
|
|
3424
|
-
value: updateData[field.name]
|
|
3425
|
-
}));
|
|
3430
|
+
throw new Error(i18n.t("api.data.duplicateValue", { field: field.name, value: updateData[field.name] }));
|
|
3426
3431
|
}
|
|
3427
3432
|
}
|
|
3428
3433
|
}
|
|
3434
|
+
|
|
3435
|
+
// 6. Traitement des relations (inchangé)
|
|
3429
3436
|
const relationFields = model.fields.filter(f => f.type === 'relation');
|
|
3430
3437
|
for (const field of relationFields) {
|
|
3431
3438
|
if (updateData[field.name] !== undefined) {
|
|
3432
3439
|
const relationValue = updateData[field.name];
|
|
3433
3440
|
if (relationValue !== null && typeof relationValue === 'object') {
|
|
3434
|
-
// Ajouter l'option preserveIds: true pour conserver les IDs
|
|
3435
3441
|
const insertedIds = await pushDataUnsecure(relationValue, field.relation, user, { preserveIds: true });
|
|
3436
3442
|
updateData[field.name] = field.multiple ? insertedIds || [] : insertedIds?.[0] || null;
|
|
3437
3443
|
}
|
|
3438
3444
|
}
|
|
3439
3445
|
}
|
|
3440
3446
|
|
|
3441
|
-
|
|
3442
|
-
// Gestion des mots de passe et autres filtres
|
|
3447
|
+
// 7. Application des filtres de champ (ex: hashage de mot de passe) (inchangé)
|
|
3443
3448
|
for (const field of model.fields) {
|
|
3444
3449
|
if (updateData[field.name] !== undefined && dataTypes[field.type]?.filter) {
|
|
3445
3450
|
updateData[field.name] = await dataTypes[field.type].filter(
|
|
@@ -3449,23 +3454,31 @@ const internalEditOrPatchData = async (modelName, filter, data, files, user, isP
|
|
|
3449
3454
|
}
|
|
3450
3455
|
}
|
|
3451
3456
|
|
|
3452
|
-
|
|
3457
|
+
// 8. Calcul du nouveau hash et préparation des données finales
|
|
3458
|
+
const finalStateForHash = { ...existingDocs[0], ...updateData };
|
|
3459
|
+
const newHash = getFieldValueHash(model, finalStateForHash);
|
|
3453
3460
|
|
|
3454
|
-
// On ne met à jour que les champs fournis + le nouveau hash
|
|
3455
3461
|
const finalDataForSet = {
|
|
3456
3462
|
...updateData,
|
|
3457
|
-
_hash:
|
|
3463
|
+
_hash: newHash
|
|
3458
3464
|
};
|
|
3459
3465
|
|
|
3460
|
-
|
|
3461
|
-
|
|
3462
|
-
|
|
3466
|
+
// 9. *** CORRECTION LOGIQUE ***
|
|
3467
|
+
// On ne vérifie l'unicité que si le hash a réellement changé.
|
|
3468
|
+
if (newHash !== originalHash) {
|
|
3469
|
+
const hashCheck = await checkHash(user, model, newHash, existingDocs[0]._id.toString());
|
|
3470
|
+
if (hashCheck) {
|
|
3471
|
+
// Le nouvel état du document créerait un doublon.
|
|
3472
|
+
throw new Error(i18n.t("api.data.notUniqueData"));
|
|
3473
|
+
}
|
|
3463
3474
|
}
|
|
3464
3475
|
|
|
3476
|
+
// 10. Exécution de la mise à jour (inchangé)
|
|
3465
3477
|
const bulkOps = [{ updateMany: { filter: {_id: {$in: ids}}, update: {$set: finalDataForSet} } }];
|
|
3466
3478
|
const bulkResult = await collection.bulkWrite(bulkOps);
|
|
3467
3479
|
const modifiedCount = bulkResult.modifiedCount || 0;
|
|
3468
3480
|
|
|
3481
|
+
// 11. Tâches post-mise à jour (schedules, workflows) (inchangé)
|
|
3469
3482
|
if (["workflowTrigger", "alert"].includes(modelName)) {
|
|
3470
3483
|
await handleScheduledJobs(modelName, existingDocs, collection, finalDataForSet);
|
|
3471
3484
|
}
|
|
@@ -4231,8 +4244,10 @@ export const searchData = async ({user, query}) => {
|
|
|
4231
4244
|
pipelines.push({$project: {_user: 0}});
|
|
4232
4245
|
pipelines.push({$project: {_model: 0}});
|
|
4233
4246
|
}
|
|
4234
|
-
console.log(util.inspect(pipelines, false, 29, true))
|
|
4235
4247
|
|
|
4248
|
+
console.log(util.inspect(pipelines, false, 29, true));
|
|
4249
|
+
|
|
4250
|
+
// 4. Exécuter la pipeline
|
|
4236
4251
|
const ts = parseInt(timeout, 10)/2.0 || searchRequestTimeout;
|
|
4237
4252
|
const count = await collection.aggregate([...pipelines, { $count: "count" }]).maxTimeMS(ts).toArray();
|
|
4238
4253
|
let prom = collection.aggregate(pipelines).maxTimeMS(ts);
|
|
@@ -4249,7 +4264,7 @@ export const searchData = async ({user, query}) => {
|
|
|
4249
4264
|
|
|
4250
4265
|
export const importData = async(options, files, user) => {
|
|
4251
4266
|
|
|
4252
|
-
if(
|
|
4267
|
+
if( !(isDemoUser(user) && Config.Get("useDemoAccounts")) && isLocalUser(user) && !await hasPermission(["API_ADMIN", "API_IMPORT_DATA"], user)){
|
|
4253
4268
|
return ({ success: false, error: "API_IMPORT_DATA permission needed." });
|
|
4254
4269
|
}
|
|
4255
4270
|
|
|
@@ -4602,7 +4617,7 @@ export const exportData= async (options, user) =>{
|
|
|
4602
4617
|
// Example using hasPermission:
|
|
4603
4618
|
// if (user.username !== 'demo' && isLocalUser(user) && !await hasPermission(["API_ADMIN", "API_SEARCH_DATA", "API_SEARCH_DATA_"+modelName], user)) {
|
|
4604
4619
|
// Example using checkPermission (if it exists and works similarly):
|
|
4605
|
-
if (isLocalUser(user) &&
|
|
4620
|
+
if (isLocalUser(user) && !(isDemoUser(user) && Config.Get("useDemoAccounts")) && !(await hasPermission('API_EXPORT_DATA', user))) { // Adapt this line based on your actual permission function
|
|
4606
4621
|
console.warn(`User ${userId} lacks permission to search/export model ${modelName}`);
|
|
4607
4622
|
errors.push(i18n.t('api.permission.searchData', 'Cannot search data from the API') + ` (${modelName})`);
|
|
4608
4623
|
continue; // Skip this model
|
|
@@ -5384,6 +5399,8 @@ export async function installPack(logger, packId, user, lang) {
|
|
|
5384
5399
|
if (documents.length === 0) continue;
|
|
5385
5400
|
|
|
5386
5401
|
const docsToInsert = [];
|
|
5402
|
+
console.log(modelName, user);
|
|
5403
|
+
|
|
5387
5404
|
const modelDefForHash = await getModel(modelName, user);
|
|
5388
5405
|
|
|
5389
5406
|
for (const docSource of documents) {
|
package/src/modules/mongodb.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import process from "process";
|
|
3
3
|
import {MongoClient as InternalMongoClient} from "mongodb";
|
|
4
4
|
import {Logger} from "../gameObject.js";
|
|
5
|
-
import {MongoDatabase} from "../engine.js";
|
|
5
|
+
import {MongoClient, MongoDatabase} from "../engine.js";
|
|
6
6
|
import * as tls from "node:tls";
|
|
7
7
|
import fs from "node:fs";
|
|
8
8
|
|
package/src/modules/workflow.js
CHANGED
|
@@ -365,45 +365,40 @@ async function handleCreateDataAction(actionDef, contextData, user, dbCollection
|
|
|
365
365
|
|
|
366
366
|
try {
|
|
367
367
|
// 2. Substitute Variables in the data template
|
|
368
|
-
let substitutedDataString;
|
|
369
368
|
let dataObject;
|
|
370
369
|
|
|
371
|
-
// dataToCreate might be a string (JSON) or already an object from the model definition
|
|
372
370
|
if (typeof dataToCreate === 'string') {
|
|
373
|
-
substitutedDataString = await substituteVariables(dataToCreate, contextData, user);
|
|
374
|
-
|
|
371
|
+
const substitutedDataString = await substituteVariables(dataToCreate, contextData, user);
|
|
372
|
+
try {
|
|
373
|
+
// CORRECTION : Utiliser la bonne variable (substitutedDataString)
|
|
374
|
+
dataObject = JSON.parse(substitutedDataString);
|
|
375
|
+
} catch (parseError) {
|
|
376
|
+
const msg = `Failed to parse substituted JSON string: ${substitutedDataString}. Error: ${parseError.message}`;
|
|
377
|
+
logger.error(`[handleCreateDataAction] ${msg}`);
|
|
378
|
+
return { success: false, message: msg };
|
|
379
|
+
}
|
|
375
380
|
} else if (typeof dataToCreate === 'object') {
|
|
376
|
-
//
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
// Or assume it's meant to be used directly if it came as an object
|
|
380
|
-
// Let's assume direct use for now if it's an object in the definition
|
|
381
|
-
dataObject = substitutedObject;
|
|
382
|
-
|
|
383
|
-
console.log({substitutedObject});
|
|
381
|
+
// CORRECTION : Assigner le résultat de la substitution à dataObject.
|
|
382
|
+
// On passe une copie pour ne pas muter le template original.
|
|
383
|
+
dataObject = await substituteVariables(JSON.parse(JSON.stringify(dataToCreate)), contextData, user);
|
|
384
384
|
} else {
|
|
385
|
-
const msg = `[handleCreateDataAction]
|
|
385
|
+
const msg = `[handleCreateDataAction] 'dataToCreate' has an invalid type (${typeof dataToCreate}). Expected string (JSON) or object.`;
|
|
386
386
|
logger.error(msg);
|
|
387
387
|
return { success: false, message: msg };
|
|
388
388
|
}
|
|
389
389
|
|
|
390
|
+
// Log pour débogage
|
|
391
|
+
logger.debug('Final data object after substitution:', dataObject);
|
|
390
392
|
|
|
391
|
-
// 3.
|
|
392
|
-
|
|
393
|
-
try {
|
|
394
|
-
dataObject = JSON.parse(substitutedDataString);
|
|
395
|
-
if (typeof dataObject !== 'object' || dataObject === null) {
|
|
396
|
-
throw new Error("Parsed data is not a valid object.");
|
|
397
|
-
}
|
|
398
|
-
} catch (parseError) {
|
|
399
|
-
const msg = `[handleCreateDataAction] Action ${actionDef.name} (${actionDef._id}): Failed to parse substituted 'dataToCreate' JSON. Error: ${parseError.message}. Substituted string: ${substitutedDataString}`;
|
|
400
|
-
logger.error(msg);
|
|
401
|
-
return { success: false, message: msg };
|
|
402
|
-
}
|
|
403
|
-
}
|
|
393
|
+
// 3. Appeler insertData avec l'objet correctement substitué
|
|
394
|
+
const result = await insertData(targetModel, dataObject, [], user, true, true); // On attend la fin du workflow déclenché par cette création
|
|
404
395
|
|
|
405
|
-
|
|
406
|
-
|
|
396
|
+
if (result.success) {
|
|
397
|
+
return { success: true, insertedIds: result.insertedIds };
|
|
398
|
+
} else {
|
|
399
|
+
// Propage l'erreur venant de insertData
|
|
400
|
+
return { success: false, message: result.error || "Insertion failed." };
|
|
401
|
+
}
|
|
407
402
|
|
|
408
403
|
} catch (error) {
|
|
409
404
|
const msg = `[handleCreateDataAction] Action ${actionDef.name} (${actionDef._id}): Unexpected error during creation for model '${targetModel}'. Error: ${error.message}`;
|
|
@@ -723,9 +718,124 @@ function getNestedValue(obj, path) {
|
|
|
723
718
|
// Retourne la valeur finale trouvée
|
|
724
719
|
return current;
|
|
725
720
|
}
|
|
721
|
+
|
|
722
|
+
/**
|
|
723
|
+
* Résout un chemin de variable complexe (ex: "triggerData.order.customer.contact.email")
|
|
724
|
+
* en construisant un pipeline d'agrégation dynamique pour tout récupérer en une seule requête.
|
|
725
|
+
*
|
|
726
|
+
* @param {string} pathString - Le chemin de la variable, ex: "triggerData.order.customer.contact.email".
|
|
727
|
+
* @param {object} initialContext - L'objet de départ (le triggerData).
|
|
728
|
+
* @param {object} user - L'objet utilisateur pour les requêtes DB.
|
|
729
|
+
* @returns {Promise<any>} La valeur résolue.
|
|
730
|
+
*/
|
|
731
|
+
async function resolvePathValue(pathString, initialContext, user) {
|
|
732
|
+
const pathParts = pathString.split('.');
|
|
733
|
+
const rootObjectKey = pathParts.shift(); // ex: "triggerData"
|
|
734
|
+
|
|
735
|
+
// Si le chemin ne commence pas par triggerData ou context, essayer de résoudre directement
|
|
736
|
+
if (rootObjectKey !== 'triggerData' && rootObjectKey !== 'context') {
|
|
737
|
+
let current = initialContext;
|
|
738
|
+
for (const part of [rootObjectKey, ...pathParts]) {
|
|
739
|
+
if (current === null || typeof current === 'undefined') return undefined;
|
|
740
|
+
current = current[part];
|
|
741
|
+
}
|
|
742
|
+
return current;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
// Vérifier si c'est un chemin simple qui peut être résolu sans aggregation
|
|
746
|
+
if (pathParts.length === 1) {
|
|
747
|
+
return initialContext[pathParts[0]];
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
let currentModelName = initialContext._model;
|
|
751
|
+
let currentDocId = new ObjectId(initialContext._id);
|
|
752
|
+
const collection = getCollectionForUser(user);
|
|
753
|
+
|
|
754
|
+
// Construire le pipeline d'agrégation
|
|
755
|
+
const pipeline = [
|
|
756
|
+
{ $match: { _id: currentDocId } }
|
|
757
|
+
];
|
|
758
|
+
|
|
759
|
+
// Itérer sur chaque segment du chemin pour construire les lookups
|
|
760
|
+
for (let i = 0; i < pathParts.length; i++) {
|
|
761
|
+
const segment = pathParts[i];
|
|
762
|
+
|
|
763
|
+
// Si c'est le dernier segment, on n'a pas besoin de faire un lookup
|
|
764
|
+
if (i === pathParts.length - 1) break;
|
|
765
|
+
|
|
766
|
+
const modelDef = await getModel(currentModelName, user);
|
|
767
|
+
const fieldDef = modelDef.fields.find(f => f.name === segment);
|
|
768
|
+
|
|
769
|
+
if (!fieldDef || fieldDef.type !== 'relation') {
|
|
770
|
+
// Si ce n'est pas une relation, on ne peut pas continuer le chemin
|
|
771
|
+
return undefined;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
const nextModelName = fieldDef.relation;
|
|
775
|
+
const asField = `__resolved_${segment}`;
|
|
776
|
+
|
|
777
|
+
pipeline.push({
|
|
778
|
+
$lookup: {
|
|
779
|
+
from: collection.collectionName,
|
|
780
|
+
let: { relationId: `$${segment}` },
|
|
781
|
+
pipeline: [
|
|
782
|
+
{
|
|
783
|
+
$match: {
|
|
784
|
+
$expr: {
|
|
785
|
+
$eq: ["$_id", {
|
|
786
|
+
$cond: {
|
|
787
|
+
if: { $eq: [{ $type: "$$relationId" }, "string"] },
|
|
788
|
+
then: { $toObjectId: "$$relationId" },
|
|
789
|
+
else: "$$relationId"
|
|
790
|
+
}
|
|
791
|
+
}]
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
],
|
|
796
|
+
as: asField
|
|
797
|
+
}
|
|
798
|
+
});
|
|
799
|
+
|
|
800
|
+
pipeline.push({
|
|
801
|
+
$unwind: {
|
|
802
|
+
path: `$${asField}`,
|
|
803
|
+
preserveNullAndEmptyArrays: true
|
|
804
|
+
}
|
|
805
|
+
});
|
|
806
|
+
|
|
807
|
+
pipeline.push({
|
|
808
|
+
$addFields: {
|
|
809
|
+
[segment]: `$${asField}`
|
|
810
|
+
}
|
|
811
|
+
});
|
|
812
|
+
|
|
813
|
+
pipeline.push({ $project: { [asField]: 0 } });
|
|
814
|
+
|
|
815
|
+
currentModelName = nextModelName;
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
const results = await collection.aggregate(pipeline).toArray();
|
|
819
|
+
|
|
820
|
+
if (results.length === 0) {
|
|
821
|
+
return undefined;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
// Extraire la valeur finale
|
|
825
|
+
let finalValue = results[0];
|
|
826
|
+
for (const part of pathParts) {
|
|
827
|
+
if (finalValue === null || typeof finalValue === 'undefined') {
|
|
828
|
+
return undefined;
|
|
829
|
+
}
|
|
830
|
+
finalValue = finalValue[part];
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
return finalValue;
|
|
834
|
+
}
|
|
835
|
+
|
|
726
836
|
/**
|
|
727
837
|
* Remplace les placeholders dans un template (string, object, array) par des valeurs du contextData.
|
|
728
|
-
*
|
|
838
|
+
* Version améliorée avec support des chemins complexes via resolvePathValue.
|
|
729
839
|
*/
|
|
730
840
|
export async function substituteVariables(template, contextData, user) {
|
|
731
841
|
// 1. Retourner les types non substituables tels quels
|
|
@@ -759,54 +869,76 @@ export async function substituteVariables(template, contextData, user) {
|
|
|
759
869
|
// `contextToSearch` contient toutes les données disponibles à sa racine
|
|
760
870
|
const contextToSearch = { ...contextData, env: userEnv };
|
|
761
871
|
|
|
762
|
-
// 5. Logique de résolution de valeur
|
|
763
|
-
const findValue = (key) => {
|
|
764
|
-
let value;
|
|
872
|
+
// 5. Logique de résolution de valeur améliorée avec resolvePathValue
|
|
873
|
+
const findValue = async (key) => {
|
|
765
874
|
let path = key.trim();
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
875
|
+
if (path.endsWith('._id')) {
|
|
876
|
+
const basePath = path.slice(0, -4);
|
|
877
|
+
const value = await findValue(basePath);
|
|
878
|
+
return value?._id?.toString(); // Convertit l'ObjectId en string
|
|
770
879
|
}
|
|
771
|
-
// Note : Pas de gestion spéciale pour {env.*} car `env` est déjà à la racine de contextToSearch.
|
|
772
880
|
|
|
773
881
|
// Gérer les valeurs dynamiques spéciales
|
|
774
882
|
if (path === 'now') {
|
|
775
|
-
|
|
883
|
+
return new Date().toISOString();
|
|
776
884
|
} else if (path === 'randomUUID') {
|
|
777
|
-
|
|
778
|
-
value = crypto.randomUUID();
|
|
779
|
-
} else {
|
|
780
|
-
// Chercher le chemin dans l'objet de contexte entier
|
|
781
|
-
value = getNestedValue(contextToSearch, path);
|
|
885
|
+
return crypto.randomUUID();
|
|
782
886
|
}
|
|
783
|
-
|
|
887
|
+
|
|
888
|
+
// Détecter si le chemin est complexe (contient plus d'un point)
|
|
889
|
+
if (path.split('.').length > 1) {
|
|
890
|
+
try {
|
|
891
|
+
// Essayer de résoudre le chemin avec resolvePathValue
|
|
892
|
+
const [root, ...rest] = path.split('.');
|
|
893
|
+
// On vérifie si la racine du chemin (ex: 'triggerData') existe dans notre contexte
|
|
894
|
+
if (contextToSearch[root]) {
|
|
895
|
+
const resolvedValue = await resolvePathValue(
|
|
896
|
+
rest.join('.'),
|
|
897
|
+
contextToSearch[root], // On passe le bon objet de départ (ex: l'objet triggerData)
|
|
898
|
+
user
|
|
899
|
+
);
|
|
900
|
+
if (resolvedValue !== undefined) {
|
|
901
|
+
return resolvedValue;
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
} catch (error) {
|
|
905
|
+
console.warn(`Erreur lors de la résolution du chemin "${path}":`, error.message);
|
|
906
|
+
// On continue avec la méthode normale si la résolution échoue
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
// Fallback: chercher le chemin dans l'objet de contexte normal
|
|
911
|
+
return getNestedValue(contextToSearch, path);
|
|
784
912
|
};
|
|
785
913
|
|
|
786
914
|
// CAS A : La chaîne est un unique placeholder (ex: "{context.triggerData.product.price}")
|
|
787
|
-
// Ceci préserve le type de donnée original (nombre, booléen, objet, etc.).
|
|
788
915
|
const singlePlaceholderMatch = template.match(/^\{([^}]+)\}$/);
|
|
789
916
|
if (singlePlaceholderMatch) {
|
|
790
917
|
const key = singlePlaceholderMatch[1];
|
|
791
|
-
const value = findValue(key);
|
|
792
|
-
// Si une valeur est trouvée, la retourner directement. Sinon, retourner le template original.
|
|
918
|
+
const value = await findValue(key);
|
|
793
919
|
return value !== undefined ? value : template;
|
|
794
920
|
}
|
|
795
921
|
|
|
796
|
-
// CAS B : La chaîne
|
|
797
|
-
// Le résultat sera toujours une chaîne de caractères.
|
|
922
|
+
// CAS B : La chaîne contient plusieurs placeholders ou mix texte/variables
|
|
798
923
|
const placeholderRegex = /\{([^}]+)\}/g;
|
|
799
|
-
|
|
800
|
-
const value = findValue(key);
|
|
924
|
+
const placeholders = [...template.matchAll(placeholderRegex)];
|
|
801
925
|
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
}
|
|
926
|
+
// Si aucun placeholder trouvé, retourner la chaîne telle quelle
|
|
927
|
+
if (placeholders.length === 0) {
|
|
928
|
+
return template;
|
|
929
|
+
}
|
|
807
930
|
|
|
808
|
-
|
|
809
|
-
|
|
931
|
+
// Remplacer chaque placeholder de manière asynchrone
|
|
932
|
+
let result = template;
|
|
933
|
+
for (const [match, key] of placeholders) {
|
|
934
|
+
const value = await findValue(key);
|
|
935
|
+
const replacement = value !== undefined
|
|
936
|
+
? (value === null ? 'null' : typeof value === 'object' ? JSON.stringify(value) : String(value))
|
|
937
|
+
: match;
|
|
938
|
+
result = result.replace(match, replacement);
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
return result;
|
|
810
942
|
}
|
|
811
943
|
|
|
812
944
|
/**
|
package/src/packs.js
CHANGED
|
@@ -242,20 +242,17 @@ export const getAllPacks = async () => {
|
|
|
242
242
|
},{
|
|
243
243
|
"name": "Data purging",
|
|
244
244
|
"startStep": { "$link": { "name": "Purge execution", "_model": "workflowStep" } }
|
|
245
|
+
},{
|
|
246
|
+
"name": "Shipment Notification",
|
|
247
|
+
"description": "Notifies the customer when their order has been shipped.",
|
|
248
|
+
"startStep": { "$link": { "name": "Send Shipment Email Step", "_model": "workflowStep" } }
|
|
245
249
|
}],
|
|
246
250
|
"workflowAction": [
|
|
247
|
-
{ "name": "Update order status to 'processing'", "type": "UpdateData", "targetModel": "order", "targetSelector": { "_id": "{triggerData._id}" }, "fieldsToUpdate": { "status": "processing" } },
|
|
251
|
+
{ "name": "Update order status to 'processing'", "type": "UpdateData", "targetModel": "order", "targetSelector": { "_id": { $toObjectId: "{triggerData._id}" }}, "fieldsToUpdate": { "status": "processing" } },
|
|
248
252
|
{ "name": "Create Shipment Record", "type": "CreateData",
|
|
249
253
|
"targetModel": "shipment",
|
|
250
254
|
"dataToCreate": { "order": "{triggerData._id}", "status": "preparing" } },
|
|
251
|
-
{ "name": "Update order status to 'shipped'", "type": "UpdateData", "targetModel": "order", "targetSelector": { "_id": "{triggerData._id}" }, "fieldsToUpdate": { "status": "shipped" } },
|
|
252
|
-
{
|
|
253
|
-
"name": "Send Shipping Confirmation",
|
|
254
|
-
"type": "SendEmail",
|
|
255
|
-
"emailRecipients": ["{triggerData.customer.email}"],
|
|
256
|
-
"emailSubject": "Your order #{triggerData.orderId} has been shipped!",
|
|
257
|
-
"emailContent": "Hello {triggerData.customer.firstName},<br><br>Good news! Your order is on its way. You can track it using this number: {context.CreateShipmentRecord.insertedIds[0]}.<br><br>Thank you for your purchase!"
|
|
258
|
-
},
|
|
255
|
+
{ "name": "Update order status to 'shipped'", "type": "UpdateData", "targetModel": "order", "targetSelector": { "_id": { $toObjectId: "{triggerData._id}" }}, "fieldsToUpdate": { "status": "shipped" } },
|
|
259
256
|
{
|
|
260
257
|
name: 'Delete queries older than 30 days',
|
|
261
258
|
type: 'DeleteData',
|
|
@@ -263,6 +260,14 @@ export const getAllPacks = async () => {
|
|
|
263
260
|
targetSelector: {
|
|
264
261
|
"$lt": ["$timestamp", {"$subtract": ["$$NOW", 1000*3600*24*365*5] } ]
|
|
265
262
|
}
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
"name": "Send Shipping Notification Email",
|
|
266
|
+
"type": "SendEmail",
|
|
267
|
+
// C'est ici que la magie opère !
|
|
268
|
+
"emailRecipients": ["{triggerData.order.customer.contact.email}"],
|
|
269
|
+
"emailSubject": "Your order #{triggerData.order.orderId} has been shipped!",
|
|
270
|
+
"emailContent": "Hello {triggerData.order.customer.contact.firstName},<br><br>Good news! Your order #{triggerData.order.orderId} is on its way. You can track it using this number: <strong>{triggerData.trackingNumber}</strong>.<br><br>Thank you for your purchase!"
|
|
266
271
|
}
|
|
267
272
|
],
|
|
268
273
|
"workflowStep": [
|
|
@@ -278,6 +283,12 @@ export const getAllPacks = async () => {
|
|
|
278
283
|
"actions": { "$link": { "name": "Create Shipment Record", "_model": "workflowAction" } },
|
|
279
284
|
"onSuccessStep": { "$link": { "name": "Ship Order", "_model": "workflowStep" } },
|
|
280
285
|
},
|
|
286
|
+
{
|
|
287
|
+
"name": "Send Shipment Email Step",
|
|
288
|
+
"workflow": { "$link": { "name": "Shipment Notification", "_model": "workflow" } },
|
|
289
|
+
"actions": { "$link": { "name": "Send Shipping Notification Email", "_model": "workflowAction" } },
|
|
290
|
+
"isTerminal": true
|
|
291
|
+
},
|
|
281
292
|
{
|
|
282
293
|
"name": "Ship Order",
|
|
283
294
|
"workflow": { "$link": { "name": "Order Fulfillment", "_model": "workflow" }},
|
|
@@ -300,7 +311,12 @@ export const getAllPacks = async () => {
|
|
|
300
311
|
}
|
|
301
312
|
],
|
|
302
313
|
"workflowTrigger": [{
|
|
303
|
-
"name": "New
|
|
314
|
+
"name": "On New Shipment Created",
|
|
315
|
+
"workflow": { "$link": { "name": "Shipment Notification", "_model": "workflow" } },
|
|
316
|
+
"type": "manual", // Déclenché par un événement
|
|
317
|
+
"onEvent": "DataAdded",
|
|
318
|
+
"targetModel": "shipment",
|
|
319
|
+
"isActive": true
|
|
304
320
|
},{
|
|
305
321
|
name: 'Daily data purge',
|
|
306
322
|
type: 'scheduled',
|