data-primals-engine 1.1.8-rc.1 → 1.2.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.
@@ -3,14 +3,14 @@ import {BSON, ObjectId} from "mongodb";
3
3
  import * as util from 'node:util';
4
4
  import {promisify} from 'node:util';
5
5
  import crypto from "node:crypto";
6
- import {exec} from 'node:child_process';
6
+ import {exec,execFile} from 'node:child_process';
7
7
  import sanitizeHtml from 'sanitize-html';
8
8
  import * as tar from "tar";
9
9
  import process from "node:process";
10
10
  import {randomColor} from "randomcolor";
11
11
  import cronstrue from 'cronstrue/i18n.js';
12
12
  import { setTimeoutMiddleware } from '../middlewares/timeout.js';
13
-
13
+ import { mkdir } from 'node:fs/promises';
14
14
  import {
15
15
  anonymizeText,
16
16
  encryptValue,
@@ -105,6 +105,7 @@ const delay = ms => new Promise(res => setTimeout(res, ms));
105
105
 
106
106
  const getBackupDir = () => process.env.BACKUP_DIR || './backups'; // Répertoire de stockage des sauvegardes
107
107
  const execAsync = promisify(exec);
108
+ const execFileAsync = promisify(execFile);
108
109
 
109
110
  let importJobs = {};
110
111
  const IMPORT_CHUNK_SIZE = 100; // Nombre d'enregistrements à traiter par lot
@@ -980,13 +981,10 @@ async function runStatefulAlertJob(alertId) {
980
981
  // 3. Evaluate the trigger condition
981
982
  const apiFilter = (alertDoc.triggerCondition);
982
983
  const { count } = await searchData({
983
- user: { username: alertDoc._user },
984
- query: {
985
- model: alertDoc.targetModel,
986
- filter: apiFilter,
987
- limit: 1
988
- }
989
- });
984
+ model: alertDoc.targetModel,
985
+ filter: apiFilter,
986
+ limit: 1
987
+ }, { username: alertDoc._user });
990
988
 
991
989
  // 4. If condition is met, send notification and update state
992
990
  if (count > 0) {
@@ -1232,17 +1230,14 @@ export async function handleCustomEndpointRequest(req, res) {
1232
1230
  try {
1233
1231
  // 1. Trouver l'endpoint correspondant dans la base de données
1234
1232
  const endpointSearch = await searchData({
1235
- user,
1236
- query: {
1237
- model: 'endpoint',
1238
- filter: {
1239
- path: path,
1240
- method: method,
1241
- isActive: true
1242
- },
1243
- limit: 1
1244
- }
1245
- });
1233
+ model: 'endpoint',
1234
+ filter: {
1235
+ path: path,
1236
+ method: method,
1237
+ isActive: true
1238
+ },
1239
+ limit: 1
1240
+ }, user);
1246
1241
 
1247
1242
  if (endpointSearch.count === 0) {
1248
1243
  logger.warn(`[Endpoint] 404 - No active endpoint found for user '${user.username}', path '${path}', method '${method}'.`);
@@ -1381,6 +1376,9 @@ export async function onInit(defaultEngine) {
1381
1376
  }
1382
1377
 
1383
1378
 
1379
+ // Create the uploads directories
1380
+ await mkdir(path.join("uploads", "tmp"),{ recursive: true});
1381
+
1384
1382
  }else {
1385
1383
  modelsCollection = getCollection("models");
1386
1384
  datasCollection = getCollection("datas");
@@ -1395,7 +1393,7 @@ export async function onInit(defaultEngine) {
1395
1393
  schedule.scheduleJob("0 0 * * *", async () => {
1396
1394
  const dt = new Date();
1397
1395
  dt.setTime(dt.getTime()-1000*3600*24*14);
1398
- await deleteData("request", [], {"$lt": ["$timestamp",dt.toISOString()]}, null, false);
1396
+ await deleteData("request", {"$lt": ["$timestamp",dt.toISOString()]}, null, false);
1399
1397
  });
1400
1398
  await scheduleWorkflowTriggers();
1401
1399
 
@@ -1526,14 +1524,11 @@ export async function onInit(defaultEngine) {
1526
1524
 
1527
1525
  // On utilise la fonction de recherche interne de l'application
1528
1526
  const searchResult = await searchData({
1529
- query: {
1530
- model,
1531
- filter: processedFilter,
1532
- limit, // Optimisation : pas besoin de plus de résultats
1533
- page: 1
1534
- },
1535
- user: user
1536
- });
1527
+ model,
1528
+ filter: processedFilter,
1529
+ limit, // Optimisation : pas besoin de plus de résultats
1530
+ page: 1
1531
+ }, user);
1537
1532
 
1538
1533
  // searchData devrait renvoyer un `count` total des documents correspondants
1539
1534
  const isCompleted = searchResult.count >= limit;
@@ -1709,6 +1704,48 @@ export async function onInit(defaultEngine) {
1709
1704
  });
1710
1705
 
1711
1706
 
1707
+ engine.post('/api/model/search', [middlewareAuthenticator, userInitiator], async (req, res) => {
1708
+ const { query } = req.fields;
1709
+ const user = req.me;
1710
+
1711
+ if (!query || typeof query !== 'string') {
1712
+ return res.status(400).json({ success: false, error: 'A search query string is required.' });
1713
+ }
1714
+
1715
+ try {
1716
+ // Utilise une expression régulière pour une recherche insensible à la casse
1717
+ const searchRegex = new RegExp(query, 'i');
1718
+
1719
+ const results = await modelsCollection.find({
1720
+ // Cherche dans les modèles de l'utilisateur OU les modèles partagés
1721
+ $or: [
1722
+ { _user: user.username },
1723
+ { _user: { $exists: false } }
1724
+ ],
1725
+ // Cherche dans le nom OU la description
1726
+ $and: [
1727
+ { $or: [
1728
+ { name: { $regex: searchRegex } },
1729
+ { description: { $regex: searchRegex } }
1730
+ ]}
1731
+ ]
1732
+ }, {
1733
+ // On ne retourne que les informations utiles pour l'IA, pas tout le modèle
1734
+ projection: {
1735
+ name: 1,
1736
+ description: 1,
1737
+ _id: 0
1738
+ }
1739
+ }).limit(10).toArray(); // Limite à 10 résultats pour ne pas surcharger
1740
+
1741
+ res.json({ success: true, models: results });
1742
+
1743
+ } catch (error) {
1744
+ logger.error(`[Model Search] Error searching models for query "${query}":`, error);
1745
+ res.status(500).json({ success: false, error: 'An internal server error occurred.' });
1746
+ }
1747
+ });
1748
+
1712
1749
  engine.post('/api/model/generate', [middlewareAuthenticator, userInitiator, assistantGlobalLimiter, generateLimiter, setTimeoutMiddleware(30000)], async (req, res) => {
1713
1750
  // --- NOUVELLE LOGIQUE : Accepter le prompt ET un modèle existant ---
1714
1751
  const { prompt, history = [], existingModel } = req.fields;
@@ -1819,7 +1856,7 @@ export async function onInit(defaultEngine) {
1819
1856
  const { pack } = req.fields;
1820
1857
 
1821
1858
  try {
1822
- const {data, count} = await searchData({query: {...req.query, model: req.fields.model || req.query.model, filter: req.fields.filter, pack}, user: req.me });
1859
+ const {data, count} = await searchData({...req.query, model: req.fields.model || req.query.model, filter: req.fields.filter, pack}, req.me);
1823
1860
 
1824
1861
  if( req.query.attachment ) {
1825
1862
  res.attachment(req.query.attachment);
@@ -1844,7 +1881,7 @@ export async function onInit(defaultEngine) {
1844
1881
 
1845
1882
  engine.delete('/api/data/:ids', [throttle, middlewareAuthenticator, userInitiator, middlewareLogger, setTimeoutMiddleware(15000)], async (req, res) => {
1846
1883
  const ids = req.params.ids.split(',');
1847
- const r = await deleteData(req.fields.model, ids, {}, req.me);
1884
+ const r = await deleteData(req.fields.model, ids, req.me);
1848
1885
  if( r.error) {
1849
1886
  return res.status(r.statusCode || 400).json(r);
1850
1887
  }else{
@@ -1853,7 +1890,7 @@ export async function onInit(defaultEngine) {
1853
1890
  });
1854
1891
 
1855
1892
  engine.delete('/api/data', [throttle, middlewareAuthenticator, userInitiator, middlewareLogger, setTimeoutMiddleware(15000)], async (req, res) => {
1856
- const r = await deleteData(req.fields.model, [], req.fields.filter, req.me);
1893
+ const r = await deleteData(req.fields.model, req.fields.filter, req.me);
1857
1894
  if( r.error) {
1858
1895
  return res.status(r.statusCode || 400).json(r);
1859
1896
  }else{
@@ -2343,12 +2380,6 @@ export async function onInit(defaultEngine) {
2343
2380
  * }
2344
2381
  * Returns: Array of aggregated data points (e.g., [{ label: '...', value: ... }]) or an error.
2345
2382
  */
2346
- // Dans server/src/modules/data.js, modifiez la route POST /api/charts/aggregate
2347
-
2348
- // Dans server/src/modules/data.js, modifiez la route POST /api/charts/aggregate
2349
- // C:/Dev/hackersonline-engine/server/src/modules/data.js
2350
-
2351
- // ... (autres imports et code)
2352
2383
 
2353
2384
  engine.post('/api/charts/aggregate', [throttle, middlewareAuthenticator, userInitiator, ...userMiddlewares, setTimeoutMiddleware(15000)], async (req, res) => {
2354
2385
  // --- Récupérer groupByLabelField ---
@@ -3235,13 +3266,10 @@ async function processRelations(docToProcess, model, collection, me, idMap) {
3235
3266
  batchFinds.push({
3236
3267
  field: field.name,
3237
3268
  promise: searchData({
3238
- user: me,
3239
- query: {
3240
- filter: value.$find,
3241
- limit: field.multiple ? 0 : 1,
3242
- model: field.relation
3243
- }
3244
- }),
3269
+ filter: value.$find,
3270
+ limit: field.multiple ? 0 : 1,
3271
+ model: field.relation
3272
+ }, me),
3245
3273
  multiple: field.multiple
3246
3274
  });
3247
3275
  }
@@ -3654,7 +3682,7 @@ const internalEditOrPatchData = async (modelName, filter, data, files, user, isP
3654
3682
  }
3655
3683
 
3656
3684
  // 2. Récupération des documents existants et de leur hash original
3657
- const existingDocs = (await searchData({user, query: {model: modelName, filter}}))?.data;
3685
+ const existingDocs = (await searchData({model: modelName, filter}, user))?.data;
3658
3686
  if (!existingDocs || existingDocs.length === 0) {
3659
3687
  return {success: false, error: i18n.t("api.data.notFound")};
3660
3688
  }
@@ -3855,7 +3883,8 @@ async function handleScheduledJobs(modelName, existingDocs, collection, updateDa
3855
3883
  }
3856
3884
  }
3857
3885
 
3858
- export const deleteData = async (modelName, ids = [], filter, user ={}, triggerWorkflow, waitForWorkflow = false) => {
3886
+
3887
+ export const deleteData = async (modelName, filter, user ={}, triggerWorkflow, waitForWorkflow = false) => {
3859
3888
 
3860
3889
  try {
3861
3890
  const collection = await getCollectionForUser(user);
@@ -3870,8 +3899,8 @@ export const deleteData = async (modelName, ids = [], filter, user ={}, triggerW
3870
3899
  });
3871
3900
 
3872
3901
  // Ajouter le filtre par IDs si fourni
3873
- if (ids && ids.length > 0) {
3874
- findFilter.push({"$in": ["$_id", ids.map(m => new ObjectId(m))]});
3902
+ if (Array.isArray(filter) && filter.length > 0) {
3903
+ findFilter.push({"$in": ["$_id", filter.map(m => new ObjectId(m))]});
3875
3904
  }
3876
3905
 
3877
3906
  // Ajouter le filtre par nom de modèle si fourni (utile si 'filter' est utilisé seul)
@@ -4070,7 +4099,7 @@ export const deleteData = async (modelName, ids = [], filter, user ={}, triggerW
4070
4099
 
4071
4100
  // ... (le reste du fichier data.js)
4072
4101
 
4073
- export const searchData = async ({user, query}) => {
4102
+ export const searchData = async (query, user) => {
4074
4103
  const { page, limit, sort, model, ids, timeout, pack } = query; // Les filtres de la requête (attention aux injections MongoDB !)
4075
4104
 
4076
4105
  if( user.username !== 'demo' && isLocalUser(user) && (
@@ -4609,8 +4638,10 @@ export const importData = async(options, files, user) => {
4609
4638
  // let allProcessedData = [];
4610
4639
  // let modelNameForImport = '';
4611
4640
  // --- FIN DE LA MODIFICATION ---
4612
-
4613
- if (file.mimetype === 'application/json' || file.originalFilename.endsWith('.json')) {
4641
+ if( !file.name ){
4642
+ throw new Error("No file provided.");
4643
+ }
4644
+ if (file.type === 'application/json' || file.name.endsWith('.json')) {
4614
4645
  fileProcessed = true;
4615
4646
  const jsonData = await runImportExportWorker('parse-json', { fileContent: fileContent.toString() });
4616
4647
 
@@ -4751,7 +4782,7 @@ export const importData = async(options, files, user) => {
4751
4782
  }
4752
4783
  // --- FIN DE LA MODIFICATION PRINCIPALE ---
4753
4784
 
4754
- } else if (file.mimetype === 'text/csv' || file.originalFilename.endsWith('.csv')) {
4785
+ } else if (file.type === 'text/csv' || file.name.endsWith('.csv')) {
4755
4786
  // --- Logique CSV (inchangée, mais maintenant elle est séparée de la logique JSON) ---
4756
4787
  fileProcessed = true;
4757
4788
  const modelNameForImport = options.model;
@@ -4958,16 +4989,13 @@ export const exportData= async (options, user) =>{
4958
4989
 
4959
4990
  // --- Fetch Data using searchData ---
4960
4991
  const searchParams = {
4961
- user: user,
4962
- query: {
4963
- model: modelName,
4964
- filter: modelSpecificFilter,
4965
- depth: parsedDepth,
4966
- limit: remainingLimit
4967
- }
4992
+ model: modelName,
4993
+ filter: modelSpecificFilter,
4994
+ depth: parsedDepth,
4995
+ limit: remainingLimit
4968
4996
  };
4969
4997
 
4970
- const { data: resultData, count } = await searchData(searchParams);
4998
+ const { data: resultData, count } = await searchData(searchParams, user);
4971
4999
 
4972
5000
  if (resultData && resultData.length > 0) {
4973
5001
  exportResults[modelName] = resultData;
@@ -5072,7 +5100,6 @@ function isValidAggregationOperator(operator) {
5072
5100
 
5073
5101
  return [...arithmeticOperators, ...comparisonOperators, ...stringOperators, ...conditionalOperators].includes(operator);
5074
5102
  }
5075
- // C:/Dev/hackersonline-engine/server/src/modules/data.js
5076
5103
 
5077
5104
  /**
5078
5105
  * Gère les traductions spécifiques à l'utilisateur et traite les données de manière récursive
@@ -5217,6 +5244,7 @@ const handleFields = async (model, data, user, isRecursiveCall = false) => {
5217
5244
  }
5218
5245
  };
5219
5246
 
5247
+ let restoreRequests = {};
5220
5248
  export const validateRestoreRequest = (username, token) => {
5221
5249
  const request = restoreRequests[username];
5222
5250
  if (!request) {
@@ -5247,20 +5275,21 @@ export const loadFromDump = async (user, options = {}) => {
5247
5275
  throw new Error("No encryption key found for this user. Cannot restore.");
5248
5276
  }
5249
5277
 
5278
+ const userId = getObjectHash({user: user.username});
5250
5279
  // --- La logique pour trouver le fichier de backup (local ou S3) reste la même ---
5251
5280
  // (le code existant pour trouver/télécharger le backupFilePath va ici)
5252
5281
  // ...
5253
5282
  let backupFilePath; // Assurez-vous que cette variable est bien définie avec le chemin du fichier .tar.gz
5254
5283
  // Exemple simplifié :
5255
5284
  const backupDir = getBackupDir();
5256
- const backupFilenameRegex = new RegExp(`^backup_${user.username}_(\\d+)\\.tar\\.gz$`);
5285
+ const backupFilenameRegex = new RegExp(`^backup_${userId}_(\\d+)\\.tar\\.gz$`);
5257
5286
  const backupFiles = fs.readdirSync(backupDir).filter(filename => backupFilenameRegex.test(filename));
5258
5287
  if (backupFiles.length === 0) throw new Error(`Aucun fichier de sauvegarde local trouvé pour l'utilisateur ${user.username}.`);
5259
5288
  const latestBackupFile = backupFiles.sort((a, b) => parseInt(b.match(backupFilenameRegex)[1], 10) - parseInt(a.match(backupFilenameRegex)[1], 10))[0];
5260
5289
  backupFilePath = path.join(backupDir, latestBackupFile);
5261
5290
  // --- Fin de la logique de recherche de fichier ---
5262
5291
 
5263
- const tmpRestoreDir = path.join(backupDir, `tmp_restore_${user.username}_${Date.now()}`);
5292
+ const tmpRestoreDir = path.join(backupDir, `tmp_restore_${userId}_${Date.now()}`);
5264
5293
 
5265
5294
  try {
5266
5295
  await runCryptoWorkerTask('decrypt', { filePath: backupFilePath, password: encryptedKey });
@@ -5297,17 +5326,25 @@ export const loadFromDump = async (user, options = {}) => {
5297
5326
  }
5298
5327
 
5299
5328
  let command;
5329
+ const args = [
5330
+ '--uri', dbUrl,
5331
+ '--db', dbName
5332
+ ];
5333
+
5300
5334
  if (modelsOnly) {
5301
- // Restaure uniquement la collection des modèles
5302
- command = `mongorestore --uri="${dbUrl}" --db=${dbName} --nsInclude="${dbName}.models" "${restoreSourceDir}"`;
5335
+ args.push('--nsInclude', `${dbName}.models`);
5303
5336
  } else {
5304
- // Restauration complète
5305
- command = `mongorestore --uri="${dbUrl}" --db=${dbName} --nsInclude="${dbName}.datas" --nsInclude="${dbName}.models" "${restoreSourceDir}"`;
5337
+ // mongorestore accepte plusieurs fois l'option --nsInclude
5338
+ args.push('--nsInclude', `${dbName}.datas`);
5339
+ args.push('--nsInclude', `${dbName}.models`);
5306
5340
  }
5341
+ // Le répertoire source est le dernier argument
5342
+ args.push(restoreSourceDir);
5307
5343
 
5308
- logger.info(`[${action}] Executing restore command: ${command}`);
5309
- await execAsync(command);
5310
5344
 
5345
+ logger.info(`[${action}] Executing restore command: ${command}`);
5346
+ await execFileAsync('mongorestore', args);
5347
+
5311
5348
  // --- Tâches Post-Restauration ---
5312
5349
  await scheduleAlerts();
5313
5350
  await scheduleWorkflowTriggers();
@@ -5347,120 +5384,87 @@ const readKeyFromFile = (user) => {
5347
5384
  };
5348
5385
 
5349
5386
  export const dumpUserData = async (user) => {
5350
- // Déterminer la clé de chiffrement
5351
- // Pour cet exemple, on simule la config S3. Remplace par la vraie récupération.
5352
- const s3Config = user.configS3; // Supposons que l'objet 'user' passé contient déjà 'configS3'
5387
+ const s3Config = user.configS3;
5353
5388
  const backupDir = getBackupDir();
5389
+ const userId = getObjectHash({ user: user.username });
5390
+ const backupFilename = `backup_${userId}`;
5391
+ const timestamp = Date.now();
5392
+
5393
+ // Déclarer les chemins ici pour qu'ils soient accessibles dans tout le scope de la fonction
5394
+ const localTempDumpDir = path.join(backupDir, `${backupFilename}_${timestamp}_temp`);
5395
+ const finalArchiveName = `${backupFilename}_${timestamp}.tar.gz`;
5396
+ const localArchivePath = path.join(backupDir, finalArchiveName);
5354
5397
 
5355
5398
  let encryptedKey = readKeyFromFile(user);
5356
5399
  if (!encryptedKey) {
5357
5400
  encryptedKey = generateAndStoreKey(user);
5358
- logger.info('Encryption key générée.');
5359
- } else {
5360
- logger.info('Encryption key chargée.');
5361
5401
  }
5362
5402
 
5363
5403
  try {
5364
- // Déterminer la fréquence de la sauvegarde
5365
5404
  const backupFrequency = await engine.userProvider.getBackupFrequency(user);
5366
-
5367
5405
  logger.info(`Fréquence de sauvegarde : ${backupFrequency}.`);
5368
5406
 
5369
- // Définir le nom du fichier de sauvegarde et les chemins
5370
- const backupFilename = `backup_${user.username}`; //nom corrigé
5371
- const backupFileBasePath = path.join(backupDir, backupFilename); //chemin corrigé
5372
- const backupFilePath = `${backupFileBasePath}_${Date.now()}`;
5373
- const backupFilenameBase = `backup_${user.username}`;
5374
- const timestamp = Date.now();
5375
- const localTempDumpDir = path.join(backupDir, `${backupFilenameBase}_${timestamp}_temp`); // Répertoire temporaire local
5376
- const finalArchiveName = `${backupFilenameBase}_${timestamp}.tar.gz`; // Nom de l'archive finale
5377
- const localArchivePath = path.join(backupDir, finalArchiveName); // Chemin
5378
-
5379
- logger.info(`Chemin du fichier de sauvegarde : ${backupFilePath}.`);
5380
-
5381
- // Ajoute les filtres sur les collections et l'utilisateur
5382
5407
  const collections = await MongoDatabase.listCollections().toArray();
5383
- let query;
5384
- let col;
5385
5408
  for (const collection of collections) {
5386
-
5387
- const colls = [await getUserCollectionName(user), 'models'];
5388
- if( colls.includes(collection.name) ){
5389
-
5390
- // Exécuter mongodump avec les filtres appropriés
5391
- let command = `mongodump --uri="${dbUrl}" --db=${dbName} --out=${localTempDumpDir} `;
5392
-
5393
- query = JSON.stringify({_user : user.username }); // Filtrer par _user
5394
- col = ` --collection=${collection.name}`;
5395
- command += `${col} --query ${JSON.stringify(query)}`;
5396
-
5397
- logger.info(`Exécution de la commande : ${command}`);
5398
- await execAsync(command);
5409
+ const collsToBackup = [await getUserCollectionName(user), 'models'];
5410
+ if (collsToBackup.includes(collection.name)) {
5411
+ const query = { _user: user.username };
5412
+ const args = [
5413
+ '--uri', dbUrl,
5414
+ '--db', dbName,
5415
+ '--out', localTempDumpDir,
5416
+ '--collection', collection.name,
5417
+ '--query', JSON.stringify(query)
5418
+ ];
5419
+ logger.info(`Exécution de la commande : mongodump ${args.join(' ')}`);
5420
+ await execFileAsync('mongodump', args);
5399
5421
  }
5400
-
5401
5422
  }
5402
5423
 
5403
-
5404
- // Zipper le contenu du répertoire de dump temporaire
5405
- // Le répertoire zippé sera `localTempDumpDir/${dbName}` si mongodump a créé ce sous-dossier
5406
- const dumpSourceDir = path.join(localTempDumpDir, dbName); // Le répertoire que mongodump a créé
5424
+ const dumpSourceDir = path.join(localTempDumpDir, dbName);
5407
5425
  if (fs.existsSync(dumpSourceDir)) {
5408
5426
  await tar.create({ gzip: true, file: localArchivePath, C: localTempDumpDir }, [dbName]);
5409
5427
  logger.info(`Archive de sauvegarde locale créée : ${localArchivePath}`);
5410
5428
  } else {
5411
- throw new Error(`Le répertoire de dump ${dumpSourceDir} n'a pas été trouvé après mongodump.`);
5429
+ logger.warn(`Le répertoire de dump ${dumpSourceDir} était vide. Aucune archive n'a été créée.`);
5430
+ // On s'arrête ici car il n'y a rien à traiter
5431
+ return Promise.resolve();
5412
5432
  }
5413
5433
 
5414
- // Chiffrer le fichier local avant l'upload si tu le souhaites (optionnel si le bucket S3 est déjà chiffré)
5415
- await encryptFile(localArchivePath, encryptedKey); // Ta fonction existante
5434
+ await encryptFile(localArchivePath, encryptedKey);
5416
5435
 
5417
5436
  if (s3Config && s3Config.bucketName) {
5418
- logger.info(`Téléversement de la sauvegarde vers S3 pour l'utilisateur ${user.username}. Bucket: ${s3Config.bucketName}`);
5419
5437
  await uploadToS3(s3Config, localArchivePath, finalArchiveName);
5420
- logger.info(`Sauvegarde téléversée avec succès vers S3 pour ${user.username}.`);
5421
- // Optionnel: supprimer l'archive locale après l'upload S3 réussi
5422
- fs.unlinkSync(localArchivePath);
5423
- logger.info(`Archive locale ${localArchivePath} supprimée après l'upload S3.`);
5438
+ fs.unlinkSync(localArchivePath); // Supprime l'archive locale après l'upload
5424
5439
  } else {
5425
- logger.info(`Aucune configuration S3 trouvée pour ${user.username}. La sauvegarde reste locale : ${localArchivePath}.`);
5426
- // Si pas de S3, le fichier chiffré localement (si encryptFile a été appelé) reste.
5427
- // Si tu ne chiffres pas localement et pas de S3, c'est l'archive .tar.gz qui reste.
5428
- }
5429
-
5430
- // Supprimer le répertoire de dump temporaire
5431
- if (fs.existsSync(localTempDumpDir)) {
5432
- fs.rmSync(localTempDumpDir, { recursive: true, force: true });
5433
- logger.info(`Répertoire de dump temporaire ${localTempDumpDir} supprimé.`);
5440
+ logger.info(`Aucune configuration S3 trouvée. La sauvegarde reste locale : ${localArchivePath}.`);
5434
5441
  }
5435
5442
 
5436
5443
  logger.info(`Sauvegarde réussie pour l'utilisateur ${user.username}.`);
5437
- // La gestion de la rotation des sauvegardes devra être adaptée
5438
- // si les sauvegardes sont sur S3 (lister depuis S3, supprimer depuis S3).
5439
- await manageBackupRotation(user, backupFrequency, s3Config); // Passer s3Config
5444
+ await manageBackupRotation(user, backupFrequency, s3Config);
5440
5445
 
5441
- return Promise.resolve();
5442
5446
  } catch (error) {
5443
5447
  logger.error(`Erreur lors de la sauvegarde pour l'utilisateur ${user.username}:`, error);
5444
- // Nettoyage en cas d'erreur
5445
- const localTempDumpDir = path.join(backupDir, `backup_${user.username}_${Date.now()}_temp`); // Recalculer pour être sûr
5446
- if (fs.existsSync(localTempDumpDir)) {
5447
- fs.rmSync(localTempDumpDir, { recursive: true, force: true });
5448
- }
5449
- const localArchivePath = path.join(backupDir, `backup_${user.username}_${Date.now()}.tar.gz`); // Recalculer
5448
+ // Nettoyage de l'archive si elle a été créée avant l'erreur
5450
5449
  if (fs.existsSync(localArchivePath)) {
5451
5450
  fs.unlinkSync(localArchivePath);
5452
5451
  }
5453
- throw error; // Relancer l'erreur
5452
+ throw error; // Relancer l'erreur pour que l'appelant soit informé
5453
+ } finally {
5454
+ // --- NETTOYAGE GARANTI ---
5455
+ // Ce bloc s'exécute toujours, que la sauvegarde réussisse ou échoue.
5456
+ if (fs.existsSync(localTempDumpDir)) {
5457
+ fs.rmSync(localTempDumpDir, { recursive: true, force: true });
5458
+ logger.info(`Répertoire de dump temporaire ${localTempDumpDir} supprimé.`);
5459
+ }
5454
5460
  }
5455
- }
5456
-
5461
+ };
5457
5462
  async function manageBackupRotation(user, backupFrequency, s3Config = null) { // Accepter s3Config
5458
- const userId = user.username; // ou user._id selon ce que tu utilises dans les noms de fichiers
5459
-
5463
+ const userId = getObjectHash({user:user.username});
5460
5464
  let filesToManage = [];
5461
5465
 
5462
5466
  if (s3Config && s3Config.bucketName) {
5463
- logger.info(`Gestion de la rotation des sauvegardes S3 pour ${userId}.`);
5467
+ logger.info(`Gestion de la rotation des sauvegardes S3 pour ${userid}.`);
5464
5468
  const s3Backups = await listS3Backups(s3Config);
5465
5469
  // Filtrer pour ne garder que les backups de cet utilisateur et trier
5466
5470
  filesToManage = s3Backups
@@ -5797,7 +5801,7 @@ export async function installPack(packId, user, lang) {
5797
5801
  const finalSelector = { ...linkSelector };
5798
5802
  delete finalSelector['_model']; // nécessaire
5799
5803
  // CORRECTION 4: Appel corrigé à searchData
5800
- const { data: targetDocs } = await searchData({ user, query: { model: targetModelName, filter: finalSelector } });
5804
+ const { data: targetDocs } = await searchData({model: targetModelName, filter: finalSelector }, user);
5801
5805
 
5802
5806
  if (targetDocs && targetDocs.length > 0) {
5803
5807
  targetIds = targetDocs.map(d => d._id); // Récupère un tableau d'ObjectIds
@@ -5852,11 +5856,6 @@ export const installAllPacks = async () => {
5852
5856
  await packsCollection.insertMany(packs);
5853
5857
  }
5854
5858
 
5855
- // Dans C:/Dev/data-primals-engine/src/modules/data.js
5856
- // Dans C:/Dev/data-primals-engine/src/modules/data.js
5857
-
5858
- // ... (imports inchangés)
5859
-
5860
5859
  export async function handleDemoInitialization(req, res) {
5861
5860
  const user = req.me;
5862
5861
  const body = req.fields;
@@ -197,15 +197,15 @@ export async function executeSafeJavascript(actionDef, context, user) {
197
197
  await jail.set('db', createJailFunction({
198
198
  create: (modelName, dataObject) => insertData(modelName, dataObject, {}, user),
199
199
  find: async (modelName, filter) => {
200
- const result = await searchData({ user, query: { model: modelName, filter } });
200
+ const result = await searchData({ model: modelName, filter }, user);
201
201
  return new ivm.ExternalCopy(result.data).copyInto();
202
202
  },
203
203
  findOne: async (modelName, filter) => {
204
- const result = await searchData({ user, query: { model: modelName, filter, limit: 1 } });
204
+ const result = await searchData({ model: modelName, filter, limit: 1 }, user);
205
205
  return new ivm.ExternalCopy(result.data?.[0] || null).copyInto();
206
206
  },
207
207
  update: (modelName, filter, updateObject) => patchData(modelName, filter, updateObject, {}, user),
208
- delete: (modelName, filter) => deleteData(modelName, null, filter, user)
208
+ delete: (modelName, filter) => deleteData(modelName, filter, user)
209
209
  }));
210
210
 
211
211
  const createLoggerFunction = (level) => {
@@ -228,11 +228,11 @@ export async function executeSafeJavascript(actionDef, context, user) {
228
228
  await jail.set('env', createJailFunction({
229
229
  get: async (variableName) => {
230
230
  if (!variableName) return null;
231
- const result = await searchData({ user, query: { model: 'env', filter: { name: variableName }, limit: 1 } });
231
+ const result = await searchData({ model: 'env', filter: { name: variableName }, limit: 1 }, user);
232
232
  return new ivm.ExternalCopy(result.data?.[0]?.value || null).copyInto();
233
233
  },
234
234
  getAll: async () => {
235
- const result = await searchData({ user, query: { model: 'env' } });
235
+ const result = await searchData({ model: 'env' }, user);
236
236
  const envObject = result.data.reduce((acc, v) => {
237
237
  acc[v.name] = v.value;
238
238
  return acc;
@@ -708,7 +708,7 @@ async function handleDeleteDataAction(actionDef, contextData, user, dbCollection
708
708
  // 5. Call the centralized deleteData function (à créer dans data.js)
709
709
  // Cette fonction devra gérer la recherche préalable pour les workflows 'DataDeleted' et la suppression des fichiers.
710
710
  const deleteResult = await deleteData(
711
- targetModel, [],
711
+ targetModel,
712
712
  selectorObject,
713
713
  user
714
714
  );
@@ -1115,7 +1115,7 @@ export async function triggerWorkflows(triggerData, user, eventType) {
1115
1115
 
1116
1116
  // Exécuter la vérification dans la base de données
1117
1117
  // Utilisation de countDocuments pour une vérification rapide
1118
- const matchCount = await searchData({ user, query: { model: targetModelName, filter: finalFilter, limit: 1 } });
1118
+ const matchCount = await searchData({ model: targetModelName, filter: finalFilter, limit: 1 }, user);
1119
1119
 
1120
1120
  if (!matchCount.count) {
1121
1121
  console.debug(`[Workflow Trigger] Trigger ${trigger._id}: dataFilter non satisfait par la donnée ${dataId}. WorkflowRun non créé.`);
@@ -1198,7 +1198,6 @@ export async function triggerWorkflows(triggerData, user, eventType) {
1198
1198
  * @param {object} user - The user context for database access.
1199
1199
  * @returns {Promise<void>}
1200
1200
  */
1201
- // C:/Dev/hackersonline-engine/server/src/modules/workflow.js
1202
1201
 
1203
1202
  export async function processWorkflowRun(workflowRunId, user) {
1204
1203
  const dbCollection = await getCollectionForUser(user);
@@ -1280,7 +1279,7 @@ export async function processWorkflowRun(workflowRunId, user) {
1280
1279
  try {
1281
1280
  // --- 7. Évaluation des conditions de l'étape ---
1282
1281
  if (currentStepDef.conditions && Object.keys(currentStepDef.conditions).length > 0) {
1283
- const searchResult = await searchData({ user, query: { model: contextData.triggerDataModel, filter: currentStepDef.conditions, limit: 1 } });
1282
+ const searchResult = await searchData({ model: contextData.triggerDataModel, filter: currentStepDef.conditions, limit: 1}, user);
1284
1283
  conditionsMet = searchResult && searchResult.count > 0;
1285
1284
  logger.info(`[processWorkflowRun] Run ID: ${runId}, Step ID: ${currentStepId}: Conditions evaluated. Found ${searchResult ? searchResult.count : 0} match(es). Result: ${conditionsMet}`);
1286
1285
  }
@@ -1478,9 +1477,8 @@ async function handleSendEmailAction(action, triggerData, user) {
1478
1477
 
1479
1478
  // 1. Récupérer la configuration SMTP depuis le modèle 'env' de l'utilisateur
1480
1479
  const envVars = await searchData({
1481
- user,
1482
- query: { model: 'env', limit: 100 } // Limite raisonnable pour les variables d'env
1483
- });
1480
+ model: 'env', limit: 100 } // Limite raisonnable pour les variables d'env
1481
+ , user);
1484
1482
 
1485
1483
  if (!envVars.data || envVars.data.length === 0) {
1486
1484
  throw new Error("Aucune variable d'environnement (modèle 'env') trouvée pour la configuration SMTP.");
package/src/providers.js CHANGED
@@ -91,7 +91,7 @@ export class UserProvider {
91
91
  }
92
92
 
93
93
  async hasFeature(user, feature) {
94
- return this.plans[user.userPlan]?.features.some(f => f === feature);
94
+ return this.plans[user?.userPlan]?.features.some(f => f === feature);
95
95
  }
96
96
 
97
97
  // Ajoutez ici d'autres méthodes nécessaires : findUserById, createUser, etc.
@@ -1,4 +1,3 @@
1
- // C:/Dev/hackersonline-engine/server/src/workers/crypto-worker.js
2
1
  import { parentPort } from 'worker_threads';
3
2
  import crypto from 'node:crypto';
4
3
  import fs from 'node:fs/promises';
@@ -86,7 +86,7 @@ afterAll(async () => {
86
86
  fs.readdirSync(backupDir).forEach(file => {
87
87
  fs.unlinkSync(path.join(backupDir, file));
88
88
  });
89
- // Optional: fs.rmdirSync(backupDir); // Remove the directory itself
89
+ fs.rmdirSync(backupDir); // Remove the directory itself
90
90
  }
91
91
  });
92
92
 
@@ -132,5 +132,7 @@ describe('Data Backup and Restore Integration', () => {
132
132
  expect(docAfterRestore.testField).toBe('Initial Value');
133
133
  expect(docAfterRestore.optionalField).toBe(123);
134
134
 
135
+ console.log("Tests passed for backup and restore");
136
+
135
137
  }, 15000); // Timeout augmenté pour les opérations de fichiers
136
138
  });