data-primals-engine 1.2.1 → 1.2.3
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 +69 -9
- package/client/src/DataEditor.jsx +1 -202
- package/client/src/DataLayout.jsx +4 -21
- package/client/src/DataTable.jsx +4 -2
- package/client/src/RestoreConfirmationModal.jsx +0 -137
- package/client/src/constants.js +1 -1
- package/client/src/hooks/useTutorials.jsx +3 -3
- package/package.json +2 -2
- package/src/constants.js +2 -2
- package/src/data.js +4 -4
- package/src/defaultModels.js +1 -14
- package/src/email.js +8 -6
- package/src/engine.js +12 -8
- package/src/events.js +26 -4
- package/src/filter.js +221 -0
- package/src/modules/bucket.js +115 -5
- package/src/modules/data.js +174 -101
- package/src/modules/file.js +98 -57
- package/src/modules/test +147 -0
- package/src/modules/workflow.js +266 -100
- package/src/packs.js +245 -7
- package/test/data.backup.integration.test.js +5 -2
- package/test/data.integration.test.js +10 -4
- package/test/events.test.js +202 -0
- package/test/file.test.js +193 -0
- package/test/import_export.integration.test.js +23 -19
- package/test/model.integration.test.js +20 -19
- package/test/user.test.js +30 -23
- package/test/vm.test.js +51 -0
- package/test/workflow.integration.test.js +6 -1
- package/test/workflow.robustness.test.js +6 -1
package/src/defaultModels.js
CHANGED
|
@@ -543,19 +543,6 @@ export const defaultModels = {
|
|
|
543
543
|
{ name: 'createdAt', type: 'datetime', required: true }
|
|
544
544
|
]
|
|
545
545
|
},
|
|
546
|
-
campaign: {
|
|
547
|
-
name: 'campaign',
|
|
548
|
-
"description": "",
|
|
549
|
-
fields: [
|
|
550
|
-
{ name: 'name', type: 'string_t', required: true },
|
|
551
|
-
{ name: 'description', type: 'richtext' },
|
|
552
|
-
{ name: 'startDate', type: 'datetime' },
|
|
553
|
-
{ name: 'endDate', type: 'datetime' },
|
|
554
|
-
{ name: 'type', type: 'enum', items: ['email', 'sms', 'advertisement', 'promotion'] },
|
|
555
|
-
{ name: 'status', type: 'enum', items: ['planified', 'in_progress', 'finished', 'cancelled'] },
|
|
556
|
-
{ name: 'budget', type: 'number' }
|
|
557
|
-
]
|
|
558
|
-
},
|
|
559
546
|
review: {
|
|
560
547
|
name: "review",
|
|
561
548
|
"description": "",
|
|
@@ -1014,7 +1001,7 @@ export const defaultModels = {
|
|
|
1014
1001
|
{ name: 'owner', type: 'relation', relation: 'user', required: false },
|
|
1015
1002
|
{ name: 'startedAt', type: 'datetime', required: true, hint: "Timestamp when the workflow run began." },
|
|
1016
1003
|
{ name: 'completedAt', type: 'datetime', hint: "Timestamp when the workflow run finished (successfully or failed)." },
|
|
1017
|
-
{ name: '
|
|
1004
|
+
{ name: 'log', type: 'string', maxlength: 4096, hint: "Error message if the workflow run failed." }
|
|
1018
1005
|
]
|
|
1019
1006
|
},
|
|
1020
1007
|
dashboard:{
|
package/src/email.js
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
import process from "node:process";
|
|
2
2
|
import nodemailer from "nodemailer";
|
|
3
3
|
import juice from "juice";
|
|
4
|
-
import {
|
|
4
|
+
import {Event} from "./events.js";
|
|
5
5
|
import {emailDefaultConfig} from "./constants.js";
|
|
6
6
|
|
|
7
7
|
// Le transporteur par défaut, utilisé si aucune config spécifique n'est fournie.
|
|
8
8
|
const defaultTransporter = nodemailer.createTransport({
|
|
9
|
-
host:
|
|
10
|
-
port: 587,
|
|
9
|
+
host: process.env.SMTP_HOST,
|
|
10
|
+
port: process.env.SMTP_PORT || 587,
|
|
11
11
|
secure: false,
|
|
12
12
|
auth: {
|
|
13
|
-
user: process.env.
|
|
14
|
-
pass: process.env.
|
|
13
|
+
user: process.env.SMTP_USER,
|
|
14
|
+
pass: process.env.SMTP_PASS
|
|
15
15
|
}
|
|
16
16
|
});
|
|
17
17
|
|
|
@@ -52,7 +52,9 @@ export const sendEmail = async (email = "", data, smtpConfig = null, lang, tpl =
|
|
|
52
52
|
// Choisir le transporteur à utiliser
|
|
53
53
|
const transporter = smtpConfig ? createTransporter(smtpConfig||emailDefaultConfig) : defaultTransporter;
|
|
54
54
|
|
|
55
|
-
|
|
55
|
+
Event.Listen("OnEmailTemplate", (data, lang) => data.content, "event", "system");
|
|
56
|
+
|
|
57
|
+
if (tpl === null) tpl = Event.Trigger("OnEmailTemplate", "event", "system", data, lang);
|
|
56
58
|
let html = tpl;
|
|
57
59
|
try {
|
|
58
60
|
html = juice(tpl);
|
package/src/engine.js
CHANGED
|
@@ -13,12 +13,13 @@ import {
|
|
|
13
13
|
import http from "http";
|
|
14
14
|
import cookieParser from "cookie-parser";
|
|
15
15
|
import requestIp from 'request-ip';
|
|
16
|
-
import {createModel, deleteModels, getModels, validateModelStructure} from "./modules/data.js";
|
|
16
|
+
import {createModel, deleteModels, getModels, installAllPacks, validateModelStructure} from "./modules/data.js";
|
|
17
17
|
import {defaultModels} from "./defaultModels.js";
|
|
18
18
|
import {DefaultUserProvider} from "./providers.js";
|
|
19
19
|
import formidableMiddleware from 'express-formidable';
|
|
20
20
|
import sirv from "sirv";
|
|
21
21
|
import * as tls from "node:tls";
|
|
22
|
+
import {Event} from "./events.js";
|
|
22
23
|
|
|
23
24
|
// Constants
|
|
24
25
|
const isProduction = process.env.NODE_ENV === 'production'
|
|
@@ -79,13 +80,13 @@ export const Engine = {
|
|
|
79
80
|
Create: async (options = { app : null}) => {
|
|
80
81
|
const engine = GameObject.Create("Engine");
|
|
81
82
|
console.log("Creating engine", Config.Get('modules'));
|
|
82
|
-
engine.addComponent(Logger);
|
|
83
|
+
const logger = engine.addComponent(Logger);
|
|
83
84
|
|
|
84
85
|
engine.userProvider = new DefaultUserProvider(engine);
|
|
85
86
|
|
|
86
87
|
engine.setUserProvider = (providerInstance) => {
|
|
87
88
|
engine.userProvider = providerInstance;
|
|
88
|
-
|
|
89
|
+
logger.info(`Custom UserProvider '${providerInstance.constructor.name}' has been set.`);
|
|
89
90
|
};
|
|
90
91
|
|
|
91
92
|
if (!options.app) {
|
|
@@ -130,9 +131,6 @@ export const Engine = {
|
|
|
130
131
|
return engine._modules.find(m => m.module === module);
|
|
131
132
|
};
|
|
132
133
|
|
|
133
|
-
|
|
134
|
-
const logger = engine.getComponent(Logger);
|
|
135
|
-
|
|
136
134
|
const importModule = async (module) => {
|
|
137
135
|
const moduleA = await import(module);
|
|
138
136
|
if (moduleA.onInit){
|
|
@@ -147,13 +145,13 @@ export const Engine = {
|
|
|
147
145
|
|
|
148
146
|
await Promise.all(Config.Get('modules', []).map(async module => {
|
|
149
147
|
try {
|
|
150
|
-
if( fs.existsSync(module)){
|
|
148
|
+
if( fs.existsSync(module) ){
|
|
151
149
|
return await importModule(module);
|
|
152
150
|
}else {
|
|
153
151
|
return await importModule('./modules/' + module + ".js");
|
|
154
152
|
}
|
|
155
153
|
} catch (e){
|
|
156
|
-
|
|
154
|
+
logger.info('ERROR at loading module '+ module, e.stack);
|
|
157
155
|
}
|
|
158
156
|
})).then(async e => {
|
|
159
157
|
engine._modules = e;
|
|
@@ -177,6 +175,7 @@ export const Engine = {
|
|
|
177
175
|
server.listen(port);
|
|
178
176
|
|
|
179
177
|
await setupInitialModels();
|
|
178
|
+
await installAllPacks();
|
|
180
179
|
|
|
181
180
|
if (cb)
|
|
182
181
|
await cb();
|
|
@@ -202,10 +201,13 @@ export const Engine = {
|
|
|
202
201
|
});
|
|
203
202
|
process.exit(1);
|
|
204
203
|
});
|
|
204
|
+
|
|
205
|
+
Event.Trigger("OnServerStart", "event", "system", engine);
|
|
205
206
|
}
|
|
206
207
|
|
|
207
208
|
engine.stop = async () => {
|
|
208
209
|
await server.close();
|
|
210
|
+
Event.Trigger("OnServerStop", "event", "system", engine);
|
|
209
211
|
};
|
|
210
212
|
|
|
211
213
|
async function setupInitialModels() {
|
|
@@ -228,9 +230,11 @@ export const Engine = {
|
|
|
228
230
|
logger.info('Model loaded (' + model.name + ')');
|
|
229
231
|
}
|
|
230
232
|
logger.info("All models loaded.");
|
|
233
|
+
Event.Trigger("OnModelsLoaded", "event", "system", engine, dbModels);
|
|
231
234
|
}
|
|
232
235
|
engine.resetModels = async () => {
|
|
233
236
|
await deleteModels();
|
|
237
|
+
Event.Trigger("OnModelsDeleted", "event", "system", engine);
|
|
234
238
|
};
|
|
235
239
|
return engine;
|
|
236
240
|
}
|
package/src/events.js
CHANGED
|
@@ -4,7 +4,9 @@ const events = {};
|
|
|
4
4
|
const eventLayerSystems = {
|
|
5
5
|
"priority": ["high", "medium", "low"],
|
|
6
6
|
"log": ["info", "debug", "warn", "error", "critical"],
|
|
7
|
-
"system": ["calls"]
|
|
7
|
+
"system": ["calls", "users"],
|
|
8
|
+
"event": ["system","user"],
|
|
9
|
+
"custom": ["data"]
|
|
8
10
|
};
|
|
9
11
|
|
|
10
12
|
|
|
@@ -13,11 +15,12 @@ export const Event = {
|
|
|
13
15
|
Trigger: (name, system = "priority", layer = "medium", ...args) => { // Ajout des arguments system et layer
|
|
14
16
|
if (!events[system] || !events[system][name] || (layer && !events[system][name][layer])) {
|
|
15
17
|
//console.warn(`No trigger found for ${name} in system ${system} layer ${layer}`);
|
|
16
|
-
return;
|
|
18
|
+
return null;
|
|
17
19
|
}
|
|
18
20
|
|
|
19
21
|
const systemsToProcess = system ? [system] : Object.keys(events); // Si system est spécifié, on cible ce système uniquement, sinon tous les systèmes
|
|
20
22
|
|
|
23
|
+
let ret = null;
|
|
21
24
|
for (const currentSystem of systemsToProcess) {
|
|
22
25
|
if (events[currentSystem] && events[currentSystem][name]) {
|
|
23
26
|
const layersToProcess = layer ? [layer] : eventLayerSystems[currentSystem] || Object.keys(events[currentSystem][name]); // Si layer est spécifié, on cible cette couche, sinon toutes les couches ou celles définies dans eventLayerSystems
|
|
@@ -27,7 +30,25 @@ export const Event = {
|
|
|
27
30
|
if (events[currentSystem][name][currentLayer]) {
|
|
28
31
|
for (const callback of events[currentSystem][name][currentLayer]) {
|
|
29
32
|
try {
|
|
30
|
-
callback(...args);
|
|
33
|
+
const res = callback(...args);
|
|
34
|
+
if (typeof res === "object" && !Array.isArray(res)) {
|
|
35
|
+
if (typeof ret !== "object") ret = {};
|
|
36
|
+
ret = {...ret, ...res};
|
|
37
|
+
} else if (Array.isArray(res)) {
|
|
38
|
+
if (!Array.isArray(ret)) ret = [];
|
|
39
|
+
ret = ret.concat(res);
|
|
40
|
+
} else if (typeof res === "string") {
|
|
41
|
+
if (typeof ret !== "string") ret = "";
|
|
42
|
+
ret += res;
|
|
43
|
+
} else if (typeof res === "number") {
|
|
44
|
+
if (typeof ret !== "number") ret = 0;
|
|
45
|
+
ret += res;
|
|
46
|
+
} else if (typeof res === "boolean") {
|
|
47
|
+
if (typeof ret !== "boolean") ret = true;
|
|
48
|
+
ret = res && ret;
|
|
49
|
+
} else {
|
|
50
|
+
ret = res || ret;
|
|
51
|
+
}
|
|
31
52
|
} catch (error) {
|
|
32
53
|
console.error(`Error in callback for event ${name} in system ${currentSystem} layer ${currentLayer}:`, error);
|
|
33
54
|
}
|
|
@@ -37,8 +58,9 @@ export const Event = {
|
|
|
37
58
|
}
|
|
38
59
|
}
|
|
39
60
|
}
|
|
61
|
+
return ret;
|
|
40
62
|
},
|
|
41
|
-
Listen: (name = "", callback
|
|
63
|
+
Listen: (name = "", callback, system = "priority", layer = "medium") => {
|
|
42
64
|
const validSystems = Object.keys(eventLayerSystems); // Récupération des clés pour une vérification plus performante
|
|
43
65
|
if (!validSystems.includes(system)) {
|
|
44
66
|
throw new Error(`System '${system}' does not exist. Valid systems are: ${validSystems.join(', ')}`); // Message d'erreur plus informatif
|
package/src/filter.js
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Evaluates a single condition against form data.
|
|
3
|
+
* @param {object} currentModelDef - The definition of the current model.
|
|
4
|
+
* @param {object} condition - The condition to evaluate.
|
|
5
|
+
* @param {object} formData - The form data.
|
|
6
|
+
* @param {object[]} allModels - An array of all model definitions.
|
|
7
|
+
* @param {object} user - The current user.
|
|
8
|
+
* @returns {boolean} - True if the condition is met, false otherwise.
|
|
9
|
+
*/
|
|
10
|
+
const evaluateSingleCondition = (currentModelDef, condition, formData, allModels, user) => {
|
|
11
|
+
// Condition est directement un filtre MongoDB, donc on l'applique
|
|
12
|
+
// en utilisant les opérateurs et les valeurs qu'il contient.
|
|
13
|
+
|
|
14
|
+
if (!condition || typeof condition !== 'object') {
|
|
15
|
+
console.warn("[Client Eval] Condition is not an object:", condition);
|
|
16
|
+
return true; // Permissive default
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Si la condition est de la forme {field: value}, on la transforme en {$eq: value}
|
|
20
|
+
if (!Object.keys(condition)[0].startsWith('$') && typeof condition[Object.keys(condition)[0]] !== 'object') {
|
|
21
|
+
const fieldName = Object.keys(condition)[0];
|
|
22
|
+
const value = condition[fieldName];
|
|
23
|
+
return evaluateSingleCondition(currentModelDef, {[fieldName]: {$eq: value}}, formData, allModels, user);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Gestion des opérateurs d'agrégation (commencent par $)
|
|
27
|
+
const operator = Object.keys(condition)[0];
|
|
28
|
+
if (operator === '$eq' && Array.isArray(condition[operator])) {
|
|
29
|
+
// Cas spécial pour {$eq: ['$field', value]}
|
|
30
|
+
const [fieldPath, expectedValue] = condition[operator];
|
|
31
|
+
if (typeof fieldPath === 'string' && fieldPath.startsWith('$')) {
|
|
32
|
+
const fieldName = fieldPath.substring(1); // Enlève le $ devant
|
|
33
|
+
return formData[fieldName] == expectedValue;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Si la condition contient des opérateurs logiques, on les gère ici
|
|
38
|
+
if (condition.$and || condition.$or || condition.$not || condition.$nor) {
|
|
39
|
+
console.warn("[Client Eval] Condition logique détectée dans evaluateSingleCondition, ce n'est pas attendu. Devrait être géré par isConditionMet.");
|
|
40
|
+
return true; // Permissive default
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (condition.$find) {
|
|
44
|
+
const fieldName = Object.keys(condition)[0];
|
|
45
|
+
const fieldValue = formData[fieldName];
|
|
46
|
+
const findCondition = condition.$find;
|
|
47
|
+
|
|
48
|
+
if (!Array.isArray(fieldValue)) return false;
|
|
49
|
+
|
|
50
|
+
return fieldValue.some(item => {
|
|
51
|
+
// Gestion spéciale pour la syntaxe $eq: ["$$this.field", value]
|
|
52
|
+
if (findCondition.$eq && Array.isArray(findCondition.$eq)) {
|
|
53
|
+
const [fieldPath, value] = findCondition.$eq;
|
|
54
|
+
if (fieldPath.startsWith("$$this.")) {
|
|
55
|
+
const fieldToCheck = fieldPath.replace("$$this.", "");
|
|
56
|
+
return item[fieldToCheck] == value;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Sinon, évaluation normale
|
|
61
|
+
const tempData = { ...item };
|
|
62
|
+
return evaluateSingleCondition(currentModelDef, findCondition, tempData, allModels, user);
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Si la condition contient un opérateur $exists, on le gère ici
|
|
67
|
+
if (condition.$exists !== undefined) {
|
|
68
|
+
const fieldName = Object.keys(condition)[0]; // Récupérer le nom du champ
|
|
69
|
+
const shouldExist = condition.$exists; // Récupérer la valeur de $exists (true ou false)
|
|
70
|
+
const exists = Object.prototype.hasOwnProperty.call(formData, fieldName) && formData[fieldName] !== undefined && formData[fieldName] !== null;
|
|
71
|
+
return exists === shouldExist;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Si la condition contient un opérateur $find, on le gère ici
|
|
75
|
+
if (condition.$find) {
|
|
76
|
+
// Récupérer le nom du champ
|
|
77
|
+
const fieldName = Object.keys(condition)[0];
|
|
78
|
+
const fieldValue = formData[fieldName];
|
|
79
|
+
try {
|
|
80
|
+
// Assuming evaluateSingleCondition handles $find
|
|
81
|
+
return evaluateSingleCondition(currentModelDef, condition.$find, formData, allModels, user);
|
|
82
|
+
} catch (error) {
|
|
83
|
+
console.error("Error evaluating $find condition:", condition, error);
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Récupérer le nom du champ et la condition
|
|
89
|
+
const fieldName = Object.keys(condition)[0];
|
|
90
|
+
const fieldValue = condition[fieldName];
|
|
91
|
+
|
|
92
|
+
// Récupérer la définition du champ
|
|
93
|
+
const fieldDef = currentModelDef?.fields.find(f => f.name === fieldName);
|
|
94
|
+
|
|
95
|
+
// Si la définition du champ n'est pas trouvée, on retourne true
|
|
96
|
+
if (!fieldDef) {
|
|
97
|
+
console.warn(`[Client Eval] Field definition not found for field: ${fieldName}`);
|
|
98
|
+
return true; // Permissive default
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
let targetValue = formData[fieldName];
|
|
102
|
+
let processedConditionValue = fieldValue;
|
|
103
|
+
|
|
104
|
+
// 1. Handle $exists (on the first field)
|
|
105
|
+
// 2. Convert condition value based on operator's expected input type
|
|
106
|
+
const fieldType = fieldDef?.type; // Type of the first field
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
processedConditionValue = convertValueType(fieldValue, fieldType);
|
|
110
|
+
} catch (e) {
|
|
111
|
+
logClientEvalWarning(`Error converting value type: ${e.message}`, condition);
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return evaluateComparison(fieldValue, targetValue, processedConditionValue, condition);
|
|
116
|
+
|
|
117
|
+
function logClientEvalWarning(message, details) {
|
|
118
|
+
console.warn(`[Client Eval] ${message}:`, details);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function convertValueType(value, inputType) {
|
|
122
|
+
switch (inputType) {
|
|
123
|
+
case 'number':
|
|
124
|
+
const numValue = parseFloat(value);
|
|
125
|
+
if (isNaN(numValue)) {
|
|
126
|
+
throw new Error(`Invalid number value: ${value}`);
|
|
127
|
+
}
|
|
128
|
+
return numValue;
|
|
129
|
+
case 'boolean':
|
|
130
|
+
return String(value).toLowerCase() === 'true';
|
|
131
|
+
case 'csv':
|
|
132
|
+
return String(value).split(',').map(item => item.trim()).filter(Boolean);
|
|
133
|
+
case 'text':
|
|
134
|
+
default:
|
|
135
|
+
return String(value);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function evaluateComparison(operator, targetValue, processedConditionValue, condition) {
|
|
140
|
+
try {
|
|
141
|
+
switch (typeof operator === 'object' ? Object.keys(operator)[0] : null) {
|
|
142
|
+
case '$eq': return targetValue == processedConditionValue;
|
|
143
|
+
case '$ne': return targetValue != processedConditionValue;
|
|
144
|
+
case '$gt': return targetValue > processedConditionValue;
|
|
145
|
+
case '$lt': return targetValue < processedConditionValue;
|
|
146
|
+
case '$gte': return targetValue >= processedConditionValue;
|
|
147
|
+
case '$lte': return targetValue <= processedConditionValue;
|
|
148
|
+
case '$regex':
|
|
149
|
+
if (typeof targetValue !== 'string') return false;
|
|
150
|
+
if (typeof processedConditionValue !== 'string') return false;
|
|
151
|
+
try {
|
|
152
|
+
const regex = new RegExp(processedConditionValue, 'i');
|
|
153
|
+
return regex.test(targetValue);
|
|
154
|
+
} catch (e) {
|
|
155
|
+
logClientEvalWarning(`Invalid regex pattern: ${processedConditionValue}`, condition);
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
case '$in':
|
|
159
|
+
return Array.isArray(processedConditionValue) && processedConditionValue.includes(String(targetValue));
|
|
160
|
+
case '$nin':
|
|
161
|
+
return !Array.isArray(processedConditionValue) || !processedConditionValue.includes(String(targetValue));
|
|
162
|
+
default:
|
|
163
|
+
logClientEvalWarning(`Unhandled operator in client evaluation logic: ${operator}`, condition);
|
|
164
|
+
return true; // Permissive default
|
|
165
|
+
}
|
|
166
|
+
} catch (evalError) {
|
|
167
|
+
logClientEvalWarning(`Error during client condition evaluation: ${operator}, targetValue=${targetValue}, processedConditionValue=${processedConditionValue}`, condition);
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
export const isConditionMet = (model, cond, formData, allModels, user) => {
|
|
174
|
+
const condition = cond;
|
|
175
|
+
|
|
176
|
+
if (!condition) return true;
|
|
177
|
+
|
|
178
|
+
// Cas 1: Condition simple {field: value} → transformée en {field: {$eq: value}}
|
|
179
|
+
if (typeof condition === 'object' && !Array.isArray(condition)) {
|
|
180
|
+
const keys = Object.keys(condition);
|
|
181
|
+
if (keys.length === 1 && !keys[0].startsWith('$') &&
|
|
182
|
+
typeof condition[keys[0]] !== 'object') {
|
|
183
|
+
const simpleCondition = {
|
|
184
|
+
[keys[0]]: {$eq: condition[keys[0]]}
|
|
185
|
+
};
|
|
186
|
+
return evaluateSingleCondition(model, simpleCondition, formData, allModels, user);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Cas 2: Opérateurs logiques ($and, $or, $not, $nor)
|
|
191
|
+
if (condition.$and && Array.isArray(condition.$and)) {
|
|
192
|
+
if (condition.$and.length === 0) return true;
|
|
193
|
+
return condition.$and.every(sub => isConditionMet(model, sub, formData, allModels, user));
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (condition.$or && Array.isArray(condition.$or)) {
|
|
197
|
+
if (condition.$or.length === 0) return false;
|
|
198
|
+
return condition.$or.some(sub => isConditionMet(model, sub, formData, allModels, user));
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (condition.$not) {
|
|
202
|
+
return !isConditionMet(model, condition.$not, formData, allModels, user);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (condition.$nor && Array.isArray(condition.$nor)) {
|
|
206
|
+
if (condition.$nor.length === 0) return true;
|
|
207
|
+
return !condition.$nor.some(sub => isConditionMet(model, sub, formData, allModels, user));
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Cas 3: Syntaxe d'agrégation {$eq: ['$field', value]}
|
|
211
|
+
if (Object.keys(condition).length === 1) {
|
|
212
|
+
const operator = Object.keys(condition)[0];
|
|
213
|
+
if (operator.startsWith('$') && operator !== '$find' &&
|
|
214
|
+
Array.isArray(condition[operator])) {
|
|
215
|
+
return evaluateSingleCondition(model, condition, formData, allModels, user);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Cas 4: Tous les autres cas (conditions normales avec opérateurs)
|
|
220
|
+
return evaluateSingleCondition(model, condition, formData, allModels, user);
|
|
221
|
+
};
|
package/src/modules/bucket.js
CHANGED
|
@@ -3,7 +3,6 @@ import AWS from "aws-sdk";
|
|
|
3
3
|
import fs from "node:fs";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import {decryptValue, encryptValue} from "../data.js";
|
|
6
|
-
import {MongoClient} from "../engine.js";
|
|
7
6
|
import {loadFromDump, validateRestoreRequest} from "./data.js";
|
|
8
7
|
import {Logger} from "../gameObject.js";
|
|
9
8
|
import {middlewareAuthenticator, userInitiator} from "./user.js";
|
|
@@ -12,6 +11,7 @@ import crypto from "node:crypto";
|
|
|
12
11
|
import i18n from "../../src/i18n.js";
|
|
13
12
|
import {sendEmail} from "../email.js";
|
|
14
13
|
import {throttleMiddleware} from "../middlewares/throttle.js";
|
|
14
|
+
import {getCollectionForUser} from "./mongodb.js";
|
|
15
15
|
|
|
16
16
|
const restoreRequests = {};
|
|
17
17
|
|
|
@@ -54,14 +54,77 @@ const getDefaultS3Config = () => {
|
|
|
54
54
|
bucketName: process.env.AWS_BUCKET || awsDefaultConfig.bucketName
|
|
55
55
|
};
|
|
56
56
|
}
|
|
57
|
-
const getS3Client = (s3Config) => {
|
|
58
57
|
|
|
59
|
-
|
|
58
|
+
/**
|
|
59
|
+
* Récupère la configuration S3 depuis les variables d'environnement de l'utilisateur en base de données.
|
|
60
|
+
* @param {object} user - L'objet utilisateur.
|
|
61
|
+
* @returns {Promise<object>} - Un objet contenant la configuration S3 trouvée pour l'utilisateur.
|
|
62
|
+
*/
|
|
63
|
+
async function _getUserS3ConfigFromDb(user) {
|
|
64
|
+
if (!user || !user.username) {
|
|
65
|
+
return {}; // Pas d'utilisateur, pas de configuration spécifique.
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
const collection = await getCollectionForUser(user);
|
|
70
|
+
const userEnvVars = await collection.find({
|
|
71
|
+
_model: 'env',
|
|
72
|
+
_user: user.username,
|
|
73
|
+
// On ne cherche que les clés pertinentes pour optimiser la requête
|
|
74
|
+
key: { $in: ['AWS_ACCESS_KEY_ID', 'AWS_SECRET_ACCESS_KEY', 'AWS_REGION', 'AWS_BUCKET_NAME'] }
|
|
75
|
+
}).toArray();
|
|
76
|
+
|
|
77
|
+
// Transforme le tableau de documents [{key, value}, ...] en un objet de configuration.
|
|
78
|
+
return userEnvVars.reduce((config, envVar) => {
|
|
79
|
+
if (envVar.key === 'AWS_ACCESS_KEY_ID') config.accessKeyId = envVar.value;
|
|
80
|
+
if (envVar.key === 'AWS_SECRET_ACCESS_KEY') config.secretAccessKey = envVar.value; // La clé est déjà chiffrée en BDD
|
|
81
|
+
if (envVar.key === 'AWS_REGION') config.region = envVar.value;
|
|
82
|
+
if (envVar.key === 'AWS_BUCKET_NAME') config.bucketName = envVar.value;
|
|
83
|
+
return config;
|
|
84
|
+
}, {});
|
|
60
85
|
|
|
61
|
-
|
|
86
|
+
} catch (error) {
|
|
87
|
+
logger.error(`Failed to fetch user S3 config from DB for ${user.username}:`, error);
|
|
88
|
+
return {}; // Retourne un objet vide en cas d'erreur pour utiliser le fallback.
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Récupère la configuration S3 effective en priorisant celle de l'utilisateur
|
|
94
|
+
* puis en se rabattant sur la configuration globale de l'environnement.
|
|
95
|
+
* C'est la fonction à utiliser pour toute opération S3.
|
|
96
|
+
* @param {object} user - L'objet utilisateur.
|
|
97
|
+
* @returns {Promise<object>} - L'objet de configuration S3 final.
|
|
98
|
+
*/
|
|
99
|
+
export async function getUserS3Config(user) {
|
|
100
|
+
// 1. Récupérer la configuration globale par défaut
|
|
101
|
+
const defaultConfig = {
|
|
102
|
+
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
|
|
103
|
+
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
|
|
104
|
+
region: process.env.AWS_REGION || awsDefaultConfig.region,
|
|
105
|
+
bucketName: process.env.AWS_BUCKET_NAME || awsDefaultConfig.bucketName
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
// 2. Récupérer la configuration spécifique de l'utilisateur
|
|
109
|
+
const userConfig = await _getUserS3ConfigFromDb(user);
|
|
110
|
+
|
|
111
|
+
if( userConfig.bucketName && userConfig.accessKeyId && userConfig.secretAccessKey) {
|
|
112
|
+
// 3. Fusionner les configurations, en donnant la priorité à celle de l'utilisateur
|
|
113
|
+
return {
|
|
114
|
+
accessKeyId: userConfig.accessKeyId || defaultConfig.accessKeyId,
|
|
115
|
+
secretAccessKey: userConfig.secretAccessKey || defaultConfig.secretAccessKey,
|
|
116
|
+
region: userConfig.region || defaultConfig.region,
|
|
117
|
+
bucketName: userConfig.bucketName || defaultConfig.bucketName
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
console.log({defaultConfig})
|
|
121
|
+
return defaultConfig;
|
|
122
|
+
}
|
|
123
|
+
const getS3Client = (s3Config) => {
|
|
124
|
+
|
|
125
|
+
const decryptedSecretAccessKey = s3Config.secretAccessKey ? decryptValue(s3Config.secretAccessKey): undefined;
|
|
62
126
|
|
|
63
127
|
return new AWS.S3({
|
|
64
|
-
...defaultConfig,
|
|
65
128
|
accessKeyId: s3Config.accessKeyId || defaultConfig.accessKeyId,
|
|
66
129
|
secretAccessKey: decryptedSecretAccessKey ? decryptedSecretAccessKey : defaultConfig.secretAccessKey,
|
|
67
130
|
region: s3Config.region || defaultConfig.region,
|
|
@@ -74,6 +137,10 @@ export const uploadToS3 = async (s3Config, filePath, remoteFilename) => {
|
|
|
74
137
|
const fileContent = fs.readFileSync(filePath);
|
|
75
138
|
const bucketPath = s3Config.pathPrefix ? `${s3Config.pathPrefix.replace(/\/$/, "")}/${remoteFilename}` : remoteFilename;
|
|
76
139
|
|
|
140
|
+
if( !s3.accessKeyId || !s3.secretAccessKey || !s3.bucketName) {
|
|
141
|
+
throw new Error('Missing S3 configuration');
|
|
142
|
+
}
|
|
143
|
+
|
|
77
144
|
const params = {
|
|
78
145
|
Bucket: s3Config.bucketName,
|
|
79
146
|
Key: bucketPath,
|
|
@@ -90,6 +157,7 @@ export const uploadToS3 = async (s3Config, filePath, remoteFilename) => {
|
|
|
90
157
|
}
|
|
91
158
|
};
|
|
92
159
|
|
|
160
|
+
|
|
93
161
|
export const listS3Backups = async (s3Config) => {
|
|
94
162
|
const s3 = getS3Client(s3Config);
|
|
95
163
|
const bucketPathPrefix = s3Config.pathPrefix ? `${s3Config.pathPrefix.replace(/\/$/, "")}/` : '';
|
|
@@ -133,6 +201,48 @@ export const downloadFromS3 = async (s3Config, s3FileKey, downloadPath) => {
|
|
|
133
201
|
}
|
|
134
202
|
};
|
|
135
203
|
|
|
204
|
+
export const deleteFromS3 = async (s3Config, remoteFilename) => {
|
|
205
|
+
const s3 = getS3Client(s3Config);
|
|
206
|
+
const bucketPath = s3Config.pathPrefix ? `${s3Config.pathPrefix.replace(/\/$/, "")}/${remoteFilename}` : remoteFilename;
|
|
207
|
+
|
|
208
|
+
const params = {
|
|
209
|
+
Bucket: s3Config.bucketName,
|
|
210
|
+
Key: bucketPath
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
try {
|
|
214
|
+
await s3.deleteObject(params).promise();
|
|
215
|
+
console.log(`Fichier supprimé avec succès de S3 : ${bucketPath}`);
|
|
216
|
+
} catch (err) {
|
|
217
|
+
console.error("Erreur lors de la suppression sur S3 :", err);
|
|
218
|
+
throw err;
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Crée un flux de lecture pour un objet depuis un bucket S3.
|
|
224
|
+
* @param {object} s3Config - La configuration S3 (bucketName, accessKeyId, secretAccessKey, region).
|
|
225
|
+
* @param {string} s3Key - La clé (nom du fichier) de l'objet sur S3.
|
|
226
|
+
* @returns {import('stream').Readable} - Le flux de lecture de l'objet S3.
|
|
227
|
+
*/
|
|
228
|
+
export const getS3Stream = (s3Config, s3Key) => {
|
|
229
|
+
if (!s3Config || !s3Config.bucketName || !s3Key) {
|
|
230
|
+
throw new Error("La configuration S3 et la clé de l'objet sont requises pour créer un flux.");
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// On utilise la fonction existante qui gère le déchiffrement !
|
|
234
|
+
const s3 = getS3Client(s3Config);
|
|
235
|
+
|
|
236
|
+
const params = {
|
|
237
|
+
Bucket: s3Config.bucketName,
|
|
238
|
+
Key: s3Key
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
// getObject().createReadStream() retourne directement un flux lisible.
|
|
242
|
+
// Les erreurs (ex: objet non trouvé) seront émises sur l'événement 'error' de ce flux.
|
|
243
|
+
return s3.getObject(params).createReadStream();
|
|
244
|
+
};
|
|
245
|
+
|
|
136
246
|
const throttle = throttleMiddleware(maxBytesPerSecondThrottleData);
|
|
137
247
|
|
|
138
248
|
let engine, logger;
|