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.
@@ -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: 'error', type: 'string', maxlength: 4096, hint: "Error message if the workflow run failed." }
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 {event_trigger} from "./core.js";
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: "mail.smtp2go.com",
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.MAIL_USER,
14
- pass: process.env.MAIL_PASS
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
- if (tpl === null) tpl = event_trigger("sendEmail:template",data, lang);
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
- engine.getComponent(Logger).info(`Custom UserProvider '${providerInstance.constructor.name}' has been set.`);
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
- console.log('ERROR at loading module '+ module + ' in /modules dir.', e.stack);
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 = () => {}, system = "priority", layer = "medium") => {
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
+ };
@@ -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
- const decryptedSecretAccessKey = decryptValue(s3Config.secretAccessKey);
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
- const defaultConfig = getDefaultS3Config();
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;