data-primals-engine 1.6.3 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +52 -900
- package/client/index.js +3 -0
- package/client/package-lock.json +7563 -8811
- package/client/package.json +11 -1
- package/client/src/App.scss +29 -0
- package/client/src/AssistantChat.jsx +369 -362
- package/client/src/DataEditor.jsx +383 -383
- package/client/src/DataLayout.jsx +54 -20
- package/client/src/ModelList.jsx +280 -280
- package/client/src/ViewSwitcher.scss +0 -31
- package/client/src/_variables.scss +3 -0
- package/client/src/constants.js +81 -100
- package/client/src/contexts/CommandContext.jsx +274 -259
- package/client/vite.config.js +30 -30
- package/doc/AI-assistance.md +93 -0
- package/doc/Advanced-workflows.md +90 -0
- package/doc/Event-system.md +79 -0
- package/doc/Packs-gallery.md +73 -0
- package/doc/automation-workflows.md +102 -0
- package/doc/core-concepts.md +33 -0
- package/doc/custom-api-endpoints.md +40 -0
- package/doc/dashboards-kpis-charts.md +49 -0
- package/doc/data-management.md +120 -0
- package/doc/data-models.md +75 -0
- package/doc/index.md +14 -0
- package/doc/roles-permissions.md +43 -0
- package/doc/users.md +30 -0
- package/package.json +20 -10
- package/src/client.js +6 -4
- package/src/constants.js +1 -1
- package/src/core.js +31 -12
- package/src/defaultModels.js +1 -1
- package/src/engine.js +342 -335
- package/src/filter.js +72 -173
- package/src/migrate.js +1 -1
- package/src/modules/assistant/assistant.js +30 -20
- package/src/modules/data/data.backup.js +4 -4
- package/src/modules/data/data.js +8 -7
- package/src/modules/data/data.operations.js +186 -133
- package/src/modules/data/data.relations.js +3 -2
- package/src/modules/data/data.scheduling.js +1 -1
- package/src/modules/mongodb.js +17 -8
- package/src/modules/swagger.js +25 -5
- package/src/modules/user.js +108 -79
- package/src/modules/workflow.js +138 -3
- package/src/packs.js +5697 -5697
- package/src/profiles.js +19 -0
- package/swagger-en.yml +3391 -1550
- package/swagger-fr.yml +3385 -2896
- package/test/assistant.test.js +2 -2
- package/test/core.test.js +341 -0
- package/test/data.backup.integration.test.js +3 -1
- package/test/data.history.integration.test.js +0 -1
- package/test/data.integration.test.js +137 -2
- package/test/user.test.js +33 -29
|
@@ -21,7 +21,7 @@ export function onInit(defaultEngine) {
|
|
|
21
21
|
|
|
22
22
|
export const cancelAlerts = async (user) => {
|
|
23
23
|
|
|
24
|
-
const datasCollection = getCollection('datas'); // Alerts are in the global collection
|
|
24
|
+
const datasCollection = getCollection(Config.Get('dataCollection', 'datas')); // Alerts are in the global collection
|
|
25
25
|
|
|
26
26
|
// 1. Fetch the latest state of the alert
|
|
27
27
|
const alertDocs = await datasCollection.find({_user: user.username, _model: 'alert'}).toArray();
|
package/src/modules/mongodb.js
CHANGED
|
@@ -2,31 +2,37 @@
|
|
|
2
2
|
import {Logger} from "../gameObject.js";
|
|
3
3
|
import {MongoDatabase} from "../engine.js";
|
|
4
4
|
import {ObjectId} from "mongodb";
|
|
5
|
+
import {isLocalUser} from "../data.js";
|
|
6
|
+
import {Event} from "../events.js";
|
|
7
|
+
import {Config} from "../config.js";
|
|
5
8
|
|
|
6
9
|
export let modelsCollection, datasCollection, filesCollection, packsCollection;
|
|
7
10
|
|
|
8
11
|
export {ObjectId};
|
|
9
12
|
|
|
10
|
-
let engine, logger
|
|
13
|
+
let engine, logger, currentDb
|
|
14
|
+
|
|
11
15
|
let colls= [];
|
|
12
16
|
export async function onInit(defaultEngine) {
|
|
13
17
|
engine = defaultEngine;
|
|
14
18
|
logger = engine.getComponent(Logger);
|
|
15
19
|
|
|
20
|
+
currentDb = MongoDatabase();
|
|
16
21
|
modelsCollection = getCollection("models");
|
|
17
|
-
datasCollection = getCollection(
|
|
22
|
+
datasCollection = getCollection(Config.Get('dataCollection', 'datas'));
|
|
18
23
|
filesCollection = getCollection("files");
|
|
19
24
|
packsCollection = getCollection("packs");
|
|
20
25
|
|
|
21
|
-
colls = await
|
|
26
|
+
colls = await currentDb.listCollections().toArray();
|
|
22
27
|
|
|
28
|
+
await Event.Trigger("OnDatabaseLoaded", "system", "calls", engine)
|
|
23
29
|
logger.info("MongoDB collections loaded.");
|
|
24
30
|
}
|
|
25
31
|
|
|
26
32
|
export const getCollections= async (forceRefresh)=>{
|
|
27
33
|
if( !forceRefresh )
|
|
28
34
|
return colls;
|
|
29
|
-
colls = await
|
|
35
|
+
colls = await currentDb.listCollections().toArray();
|
|
30
36
|
}
|
|
31
37
|
|
|
32
38
|
export const createCollection = async (coll)=>{
|
|
@@ -34,7 +40,7 @@ export const createCollection = async (coll)=>{
|
|
|
34
40
|
if( found){
|
|
35
41
|
return getCollection(coll);
|
|
36
42
|
}
|
|
37
|
-
return await
|
|
43
|
+
return await currentDb.createCollection(coll);
|
|
38
44
|
}
|
|
39
45
|
|
|
40
46
|
export const isObjectId = (id) => {
|
|
@@ -43,15 +49,18 @@ export const isObjectId = (id) => {
|
|
|
43
49
|
|
|
44
50
|
|
|
45
51
|
export const getCollection = (str) => {
|
|
46
|
-
return
|
|
52
|
+
return currentDb.collection(str);
|
|
47
53
|
}
|
|
48
54
|
|
|
49
|
-
|
|
55
|
+
export const getDatabase = () => {
|
|
56
|
+
return currentDb;
|
|
57
|
+
}
|
|
50
58
|
|
|
51
59
|
// New function to determine the collection name for a user
|
|
52
60
|
export const getUserCollectionName = async (user) => {
|
|
53
61
|
const feat = await engine.userProvider.hasFeature(user, 'indexes');
|
|
54
|
-
|
|
62
|
+
const dataCollectionName = Config.Get('dataCollection', 'datas');
|
|
63
|
+
return feat ? (isLocalUser(user) ? `${dataCollectionName}_${user._user}` :`${dataCollectionName}_${user.username}` ) : dataCollectionName;
|
|
55
64
|
};
|
|
56
65
|
|
|
57
66
|
|
package/src/modules/swagger.js
CHANGED
|
@@ -10,9 +10,29 @@ let engine, logger;
|
|
|
10
10
|
export async function onInit(defaultEngine) {
|
|
11
11
|
engine = defaultEngine;
|
|
12
12
|
logger = engine.getComponent(Logger);
|
|
13
|
-
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
13
|
+
|
|
14
|
+
const swaggerDocs = {
|
|
15
|
+
en: YAML.parse(fs.readFileSync(process.cwd() + '/swagger-en.yml', 'utf8')),
|
|
16
|
+
fr: YAML.parse(fs.readFileSync(process.cwd() + '/swagger-fr.yml', 'utf8'))
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
// Sers les assets de swagger-ui
|
|
20
|
+
engine.use('/api-docs', swaggerUi.serve);
|
|
21
|
+
|
|
22
|
+
// Route pour la langue par défaut (ex: /api-docs)
|
|
23
|
+
engine.get('/api-docs', (req, res) => {
|
|
24
|
+
return swaggerUi.setup(swaggerDocs['en'])(req, res);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
// Route pour une langue spécifique (ex: /api-docs/fr)
|
|
28
|
+
engine.get('/api-docs/:lang', (req, res) => {
|
|
29
|
+
const lang = req.params.lang;
|
|
30
|
+
|
|
31
|
+
// Si la langue demandée n'existe pas, on redirige vers la version anglaise.
|
|
32
|
+
if (!swaggerDocs[lang]) {
|
|
33
|
+
return res.redirect('/api-docs/en');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return swaggerUi.setup(swaggerDocs[lang])(req, res);
|
|
37
|
+
});
|
|
18
38
|
};
|
package/src/modules/user.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import i18n from "../i18n.js";
|
|
2
|
-
import {MongoDatabase} from "../engine.js";
|
|
3
2
|
import {
|
|
4
3
|
createCollection,
|
|
5
4
|
getCollection,
|
|
@@ -15,8 +14,10 @@ import rateLimit from "express-rate-limit";
|
|
|
15
14
|
import ivm from "isolated-vm";
|
|
16
15
|
import {emailDefaultConfig} from "../constants.js";
|
|
17
16
|
import {safeAssignObject} from "../core.js";
|
|
18
|
-
import {substituteVariables} from "
|
|
17
|
+
import {substituteVariables} from "./workflow.js";
|
|
18
|
+
import NodeCache from "node-cache";
|
|
19
19
|
import {Config} from "../config.js";
|
|
20
|
+
import { Event } from '../events.js';
|
|
20
21
|
|
|
21
22
|
export const userInitiator = async (req, res, next) => {
|
|
22
23
|
|
|
@@ -99,7 +100,28 @@ let logger,engine;
|
|
|
99
100
|
export async function onInit(defaultEngine) {
|
|
100
101
|
engine = defaultEngine;
|
|
101
102
|
logger = engine.getComponent(Logger);
|
|
103
|
+
|
|
104
|
+
const invalidatePermissionsCache = (engine, {modelName, insertedDocs, user}) => {
|
|
105
|
+
const modelsToWatch = ['role', 'permission', 'userPermission'];
|
|
106
|
+
|
|
107
|
+
// Le payload contient le nom du modèle qui a été modifié.
|
|
108
|
+
if (modelName && modelsToWatch.includes(modelName)) {
|
|
109
|
+
logger.info(`[Permissions] Invalidating permissions cache due to change in '${modelName}'.`);
|
|
110
|
+
permissionsCache.flushAll();
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
// On attache notre fonction d'invalidation aux événements système.
|
|
115
|
+
Event.Listen("OnDataAdded", invalidatePermissionsCache, "event", "system");
|
|
116
|
+
Event.Listen("OnDataEdited", invalidatePermissionsCache, "event", "system");
|
|
117
|
+
Event.Listen("OnDataDeleted", invalidatePermissionsCache, "event", "system");
|
|
102
118
|
}
|
|
119
|
+
/**
|
|
120
|
+
* Cache pour les permissions des utilisateurs.
|
|
121
|
+
* TTL de 10 minutes, vérification toutes les 2 minutes.
|
|
122
|
+
*/
|
|
123
|
+
const permissionsCache = new NodeCache({ stdTTL: 600, checkperiod: 120, useClones: false });
|
|
124
|
+
|
|
103
125
|
/**
|
|
104
126
|
* Calcule et retourne l'ensemble des permissions actives pour un utilisateur.
|
|
105
127
|
* Cette fonction interne est la pierre angulaire de la nouvelle logique de permission.
|
|
@@ -110,6 +132,14 @@ export async function onInit(defaultEngine) {
|
|
|
110
132
|
* @private
|
|
111
133
|
*/
|
|
112
134
|
export async function getUserActivePermissions(user, env = null) {
|
|
135
|
+
// Clé de cache unique pour l'utilisateur et l'environnement
|
|
136
|
+
const cacheKey = `${user._user || user.username}:${user._id.toString()}:${env || 'global'}`;
|
|
137
|
+
const cachedPermissions = permissionsCache.get(cacheKey);
|
|
138
|
+
if (cachedPermissions) {
|
|
139
|
+
logger.debug(`[Permissions] Cache hit for user ${user.username}`);
|
|
140
|
+
return cachedPermissions;
|
|
141
|
+
}
|
|
142
|
+
logger.debug(`[Permissions] Cache miss for user ${user.username}. Calculating permissions...`);
|
|
113
143
|
const datasCollection = await getCollectionForUser(user);
|
|
114
144
|
const now = new Date();
|
|
115
145
|
const activePermissions = new Map();
|
|
@@ -117,88 +147,87 @@ export async function getUserActivePermissions(user, env = null) {
|
|
|
117
147
|
// --- ÉTAPE 1: Récupérer les permissions de base des rôles ---
|
|
118
148
|
if (user.roles && user.roles.length > 0) {
|
|
119
149
|
const roleIds = user.roles.map(id => new ObjectId(id));
|
|
120
|
-
|
|
121
|
-
const rolePermissions = await datasCollection.aggregate([
|
|
122
|
-
{ $match: { _id: { $in: roleIds }, _model: "role", _user: user.username } },
|
|
123
|
-
{ $unwind: "$permissions" },
|
|
124
|
-
{
|
|
125
|
-
$lookup: {
|
|
126
|
-
from: datasCollection.collectionName, // Utiliser la même collection
|
|
127
|
-
let: { permissionIdStr: "$permissions" }, // La permission est un string
|
|
128
|
-
pipeline: [
|
|
129
|
-
{ $match: {
|
|
130
|
-
$expr: { $eq: [ { $toString: "$_id" }, "$$permissionIdStr" ] }
|
|
131
|
-
}}
|
|
132
|
-
],
|
|
133
|
-
as: "permissionDoc"
|
|
134
|
-
}
|
|
135
|
-
},
|
|
136
|
-
{ $unwind: "$permissionDoc" },
|
|
137
|
-
{ $project: { name: "$permissionDoc.name", filter: "$permissionDoc.filter" } }
|
|
138
|
-
]).toArray();
|
|
139
150
|
|
|
140
|
-
|
|
151
|
+
// Étape 1: Récupérer tous les IDs de permission des rôles de l'utilisateur.
|
|
152
|
+
const roles = await datasCollection.find(
|
|
153
|
+
{ _id: { $in: roleIds }, _model: "role", _user: user._user || user.username },
|
|
154
|
+
{ projection: { permissions: 1, _user: 1 } }
|
|
155
|
+
).toArray();
|
|
156
|
+
|
|
157
|
+
// Aplatir tous les tableaux de permissions en un seul et supprimer les doublons.
|
|
158
|
+
const permissionIdStrings = [...new Set(roles.flatMap(role => role.permissions || []))];
|
|
159
|
+
const permissionObjectIds = permissionIdStrings.map(id => new ObjectId(id));
|
|
160
|
+
|
|
161
|
+
// Étape 2: Récupérer tous les documents de permission correspondants en une seule requête.
|
|
162
|
+
const rolePermissions = await datasCollection.find(
|
|
163
|
+
{ _id: { $in: permissionObjectIds }, _model: "permission", _user: user._user || user.username },
|
|
164
|
+
{ projection: { name: 1, filter: 1 } }
|
|
165
|
+
).toArray();
|
|
166
|
+
|
|
167
|
+
rolePermissions.forEach(p => {
|
|
168
|
+
if (p.name) {
|
|
169
|
+
// On stocke l'objet permission complet pour un accès ultérieur
|
|
170
|
+
activePermissions.set(p.name, { filter: p.filter ?? null, details: p });
|
|
171
|
+
}
|
|
172
|
+
});
|
|
141
173
|
}
|
|
142
174
|
|
|
175
|
+
// --- NOUVELLE ÉTAPE : Pré-charger les détails de toutes les permissions d'exception ---
|
|
176
|
+
// Cela garantit que nous avons les noms de permission même pour les révocations.
|
|
177
|
+
const exceptionPermissionIds = (await datasCollection.distinct("permission", {
|
|
178
|
+
_model: "userPermission",
|
|
179
|
+
user: user._id.toString(),
|
|
180
|
+
_user: user._user || user.username
|
|
181
|
+
})).map(id => new ObjectId(id));
|
|
182
|
+
|
|
183
|
+
const exceptionPermsDetails = await datasCollection.find(
|
|
184
|
+
{ _id: { $in: exceptionPermissionIds }, _model: "permission" },
|
|
185
|
+
{ projection: { name: 1, filter: 1 } }
|
|
186
|
+
).toArray();
|
|
187
|
+
|
|
188
|
+
const allPermDetailsMap = new Map(exceptionPermsDetails.map(p => [p._id.toString(), p]));
|
|
189
|
+
|
|
143
190
|
// --- ÉTAPE 2: Appliquer les exceptions de permission ---
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
as: 'permissionDoc'
|
|
173
|
-
}
|
|
174
|
-
},
|
|
175
|
-
{ $unwind: '$permissionDoc' },
|
|
176
|
-
{
|
|
177
|
-
$project: { // On sélectionne les champs dont on a besoin
|
|
178
|
-
isGranted: 1,
|
|
179
|
-
filter: 1, // On ajoute le filtre de l'exception
|
|
180
|
-
permissionDoc: 1
|
|
191
|
+
const exceptionsQuery = {
|
|
192
|
+
_model: "userPermission",
|
|
193
|
+
user: user._id.toString(),
|
|
194
|
+
_user: user._user || user.username,
|
|
195
|
+
$and: [
|
|
196
|
+
{ $or: [{ expiresAt: { $exists: false } }, { expiresAt: { $gt: now } }] },
|
|
197
|
+
{ $or: [{ env: { $exists: false } }, { env: env }] }
|
|
198
|
+
]
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
const exceptions = await datasCollection.find(exceptionsQuery, {
|
|
202
|
+
projection: { permission: 1, isGranted: 1, filter: 1 } // Le filtre sur l'exception
|
|
203
|
+
}).toArray();
|
|
204
|
+
|
|
205
|
+
if (exceptions.length > 0) {
|
|
206
|
+
for (const exception of exceptions) {
|
|
207
|
+
// Utiliser la map pré-chargée qui contient toutes les permissions d'exception
|
|
208
|
+
const permDetails = allPermDetailsMap.get(exception.permission);
|
|
209
|
+
if (!permDetails?.name) continue;
|
|
210
|
+
|
|
211
|
+
if (exception.isGranted) {
|
|
212
|
+
// Priorité 1: Le filtre défini sur l'exception elle-même.
|
|
213
|
+
// Priorité 2: Le filtre défini sur la permission de base.
|
|
214
|
+
const finalFilter = exception.filter ?? permDetails.filter ?? null;
|
|
215
|
+
// On met à jour l'entrée existante ou on en crée une nouvelle
|
|
216
|
+
activePermissions.set(permDetails.name, { filter: finalFilter, details: permDetails });
|
|
217
|
+
} else {
|
|
218
|
+
activePermissions.delete(permDetails.name);
|
|
181
219
|
}
|
|
182
220
|
}
|
|
183
|
-
]).toArray();
|
|
184
|
-
|
|
185
|
-
// Appliquer les exceptions
|
|
186
|
-
for (const exception of exceptions) {
|
|
187
|
-
const permissionName = exception.permissionDoc?.name;
|
|
188
|
-
if (!permissionName) continue;
|
|
189
|
-
|
|
190
|
-
if (exception.isGranted) {
|
|
191
|
-
// Priorité 1: Le filtre défini sur l'exception elle-même.
|
|
192
|
-
// Priorité 2: Le filtre défini sur la permission de base.
|
|
193
|
-
// Priorité 3: null s'il n'y a aucun filtre.
|
|
194
|
-
const finalFilter = exception.filter ?? exception.permissionDoc.filter ?? null;
|
|
195
|
-
activePermissions.set(permissionName, finalFilter);
|
|
196
|
-
} else {
|
|
197
|
-
activePermissions.delete(permissionName);
|
|
198
|
-
}
|
|
199
221
|
}
|
|
200
222
|
|
|
201
|
-
|
|
223
|
+
// Mettre le résultat en cache avant de le retourner
|
|
224
|
+
// On ne stocke que le nom et le filtre, pas l'objet 'details' complet
|
|
225
|
+
const finalPermissionsToCache = new Map();
|
|
226
|
+
for (const [name, permData] of activePermissions.entries()) {
|
|
227
|
+
finalPermissionsToCache.set(name, permData.filter);
|
|
228
|
+
}
|
|
229
|
+
permissionsCache.set(cacheKey, finalPermissionsToCache);
|
|
230
|
+
return finalPermissionsToCache;
|
|
202
231
|
}
|
|
203
232
|
|
|
204
233
|
/**
|
|
@@ -221,15 +250,15 @@ export async function hasPermission(permissionNames, user, env = null, req = nul
|
|
|
221
250
|
}
|
|
222
251
|
|
|
223
252
|
try {
|
|
253
|
+
// 1. Obtenir l'ensemble final et à jour des permissions de l'utilisateur
|
|
254
|
+
const activePermissions = await getUserActivePermissions(user, env);
|
|
255
|
+
|
|
224
256
|
const requiredPermissions = Array.isArray(permissionNames) ? permissionNames : [permissionNames];
|
|
225
257
|
// Si aucune permission n'est requise, on autorise
|
|
226
258
|
if (requiredPermissions.length === 0) {
|
|
227
259
|
return true;
|
|
228
260
|
}
|
|
229
261
|
|
|
230
|
-
// 1. Obtenir l'ensemble final et à jour des permissions de l'utilisateur
|
|
231
|
-
const activePermissions = await getUserActivePermissions(user, env);
|
|
232
|
-
|
|
233
262
|
// 2. Chercher la première permission correspondante
|
|
234
263
|
for (const pName of requiredPermissions) {
|
|
235
264
|
if (activePermissions.has(pName)) {
|
package/src/modules/workflow.js
CHANGED
|
@@ -18,7 +18,144 @@ import {getEnv, getSmtpConfig} from "./user.js";
|
|
|
18
18
|
import { providers } from "./assistant/constants.js";
|
|
19
19
|
import { getAIProvider } from "./assistant/providers.js";
|
|
20
20
|
import {Config} from "../config.js";
|
|
21
|
-
import {
|
|
21
|
+
import {safeAssignObject} from "../core.js";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Récupère une valeur imbriquée dans un objet (ex: 'user.address.city').
|
|
25
|
+
*/
|
|
26
|
+
const getNestedValue = (obj, path) => {
|
|
27
|
+
if (!path || !obj) return undefined;
|
|
28
|
+
return path.split('.').reduce((acc, part) => acc && acc[part] !== undefined ? acc[part] : undefined, obj);
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Remplace les placeholders dans un template (string, object, array) par des valeurs du contextData.
|
|
34
|
+
* Version améliorée avec support des chemins complexes via resolvePathValue.
|
|
35
|
+
*/
|
|
36
|
+
export async function substituteVariables(template, contextData, user) {
|
|
37
|
+
// 1. Retourner les types non substituables tels quels
|
|
38
|
+
if (template === null || (typeof template !== 'string' && typeof template !== 'object')) {
|
|
39
|
+
return template;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// 2. Gérer les tableaux de manière récursive
|
|
43
|
+
if (Array.isArray(template)) {
|
|
44
|
+
return Promise.all(template.map(item => substituteVariables(item, contextData, user)));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// 3. Gérer les objets de manière récursive
|
|
48
|
+
if (typeof template === 'object') {
|
|
49
|
+
const newObj = {};
|
|
50
|
+
for (const key in template) {
|
|
51
|
+
if (Object.prototype.hasOwnProperty.call(template, key)) {
|
|
52
|
+
const val = await substituteVariables(template[key], contextData, user);
|
|
53
|
+
safeAssignObject(newObj, key, val);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return newObj;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// --- À partir d'ici, nous savons que `template` est une chaîne de caractères ---
|
|
60
|
+
|
|
61
|
+
// 4. Construire le contexte complet pour la substitution
|
|
62
|
+
const dbCollection = await getCollectionForUser(user);
|
|
63
|
+
const userEnvVars = await dbCollection.find({ _model: 'env', _user: user.username }).toArray();
|
|
64
|
+
const userEnv = userEnvVars.reduce((acc, v) => ({ ...acc, [v.name]: v.value }), {});
|
|
65
|
+
|
|
66
|
+
// `contextToSearch` contient toutes les données disponibles à sa racine
|
|
67
|
+
const contextToSearch = { ...contextData, env: userEnv };
|
|
68
|
+
|
|
69
|
+
// 5. Logique de résolution de valeur améliorée avec resolvePathValue
|
|
70
|
+
const findValue = async (key) => {
|
|
71
|
+
let path = key.trim();
|
|
72
|
+
if (path.startsWith('context.')) {
|
|
73
|
+
path = path.substring('context.'.length);
|
|
74
|
+
}
|
|
75
|
+
if (path.endsWith('._id')) {
|
|
76
|
+
const basePath = path.slice(0, -4);
|
|
77
|
+
const value = await findValue(basePath);
|
|
78
|
+
return value?._id?.toString(); // Convertit l'ObjectId en string
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Gérer les valeurs dynamiques spéciales
|
|
82
|
+
if (path === 'now') {
|
|
83
|
+
return new Date().toISOString();
|
|
84
|
+
} else if (path === 'randomUUID') {
|
|
85
|
+
return crypto.randomUUID();
|
|
86
|
+
} else if( path === "baseUrl" ){
|
|
87
|
+
return process.env.NODE_ENV === 'production' ? 'https://'+getHost()+'/' : 'http://localhost:/'+port;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Détecter si le chemin est complexe (contient plus d'un point)
|
|
91
|
+
if (path.split('.').length > 1) {
|
|
92
|
+
try {
|
|
93
|
+
// Essayer de résoudre le chemin avec resolvePathValue
|
|
94
|
+
const [root, ...rest] = path.split('.');
|
|
95
|
+
// On vérifie si la racine du chemin (ex: 'triggerData') existe dans notre contexte
|
|
96
|
+
if (contextToSearch[root]) {
|
|
97
|
+
const resolvedValue = await resolvePathValue(
|
|
98
|
+
rest.join('.'),
|
|
99
|
+
contextToSearch[root], // On passe le bon objet de départ (ex: l'objet triggerData)
|
|
100
|
+
user
|
|
101
|
+
);
|
|
102
|
+
if (resolvedValue !== undefined) {
|
|
103
|
+
return resolvedValue;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
} catch (error) {
|
|
107
|
+
console.warn(`Erreur lors de la résolution du chemin "${path}":`, error.message);
|
|
108
|
+
// On continue avec la méthode normale si la résolution échoue
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Fallback: chercher le chemin dans l'objet de contexte normal
|
|
113
|
+
return getNestedValue(contextToSearch, path);
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
// CAS A : La chaîne est un unique placeholder (ex: "{context.triggerData.product.price}")
|
|
117
|
+
const singlePlaceholderMatch = template.match(/^\{([^}]+)\}$/);
|
|
118
|
+
if (singlePlaceholderMatch) {
|
|
119
|
+
const key = singlePlaceholderMatch[1];
|
|
120
|
+
const value = await findValue(key);
|
|
121
|
+
|
|
122
|
+
if (value === undefined) {
|
|
123
|
+
return template; // Placeholder not found, return as is.
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// If the resolved value is a string, it might contain more placeholders.
|
|
127
|
+
// We recursively call substituteVariables on it, but only if it's different
|
|
128
|
+
// from the original template to prevent infinite loops.
|
|
129
|
+
if (typeof value === 'string' && value !== template) {
|
|
130
|
+
return substituteVariables(value, contextData, user);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// For non-string values or if value is same as template, return the value.
|
|
134
|
+
return value;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// CAS B : La chaîne contient plusieurs placeholders ou mix texte/variables
|
|
138
|
+
const placeholderRegex = /\{([^}]+)\}/g;
|
|
139
|
+
const placeholders = [...template.matchAll(placeholderRegex)];
|
|
140
|
+
|
|
141
|
+
// Si aucun placeholder trouvé, retourner la chaîne telle quelle
|
|
142
|
+
if (placeholders.length === 0) {
|
|
143
|
+
return template;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Remplacer chaque placeholder de manière asynchrone
|
|
147
|
+
let result = template;
|
|
148
|
+
for (const [match, key] of placeholders) {
|
|
149
|
+
const value = await findValue(key);
|
|
150
|
+
const replacement = value !== undefined
|
|
151
|
+
? (value === null ? 'null' : typeof value === 'object' ? JSON.stringify(value) : String(value))
|
|
152
|
+
: match;
|
|
153
|
+
result = result.replace(match, replacement);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return result;
|
|
157
|
+
}
|
|
158
|
+
|
|
22
159
|
|
|
23
160
|
let logger = null;
|
|
24
161
|
export async function onInit(defaultEngine) {
|
|
@@ -27,8 +164,6 @@ export async function onInit(defaultEngine) {
|
|
|
27
164
|
await scheduleWorkflowTriggers();
|
|
28
165
|
}
|
|
29
166
|
|
|
30
|
-
export { substituteVariables } from "../filter.js";
|
|
31
|
-
|
|
32
167
|
/**
|
|
33
168
|
* Déclenche un workflow par son nom et lui passe des données de contexte.
|
|
34
169
|
* C'est la fonction clé à exposer aux endpoints pour lancer des processus métier.
|