data-primals-engine 1.1.8-rc.1 → 1.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "data-primals-engine",
3
- "version": "1.1.8-rc.1",
3
+ "version": "1.1.8",
4
4
  "description": "data-primals-engine is the package responsible from handling large amount of data in a practical and performant way. It can handle large amount of data in a practical and performant way. It can also get workflow models working (for automation), and fully supports internationalisation.",
5
5
  "main": "src/engine.js",
6
6
  "type": "module",
@@ -43,9 +43,9 @@
43
43
  "./*": "./src/*.js"
44
44
  },
45
45
  "peerDependencies": {
46
+ "express": "^5.1.0",
46
47
  "react": ">=18.0.0",
47
- "react-query": ">=3.0.0",
48
- "express": "^5.1.0"
48
+ "react-query": ">=3.0.0"
49
49
  },
50
50
  "dependencies": {
51
51
  "@langchain/core": "^0.3.66",
@@ -69,6 +69,7 @@
69
69
  "express-rate-limit": "^8.0.1",
70
70
  "express-session": "^1.18.2",
71
71
  "i18next-browser-languagedetector": "^8.2.0",
72
+ "isolated-vm": "^4.7.2",
72
73
  "juice": "^11.0.1",
73
74
  "mathjs": "^14.6.0",
74
75
  "mongodb": "^6.18.0",
@@ -88,7 +89,6 @@
88
89
  "tar": "^7.4.3",
89
90
  "uniqid": "^5.4.0",
90
91
  "vitest": "^3.2.4",
91
- "isolated-vm": "^4.7.2",
92
92
  "yaml": "^2.8.0"
93
93
  },
94
94
  "devDependencies": {
package/src/core.js CHANGED
@@ -340,12 +340,13 @@ export const event_off = (name, callback) => {
340
340
  };
341
341
 
342
342
 
343
- export function slugify(str) {
343
+ export function slugify(str,replacer='-', replaceUnicode=false) {
344
344
  str = str.replace(/^\s+|\s+$/g, ''); // trim leading/trailing white space
345
345
  str = str.toLowerCase(); // convert string to lowercase
346
- str = str.replace(/[^a-z0-9 -]/g, '') // remove any non-alphanumeric characters
347
- .replace(/\s+/g, '-') // replace spaces with hyphens
348
- .replace(/-+/g, '-'); // remove consecutive hyphens
346
+ if( replaceUnicode)
347
+ str = str.replace(/[^a-z0-9 -]/g, ''); // remove any non-alphanumeric characters
348
+ str = str.replace(/\s+/g, replacer) // replace spaces with hyphens
349
+ .replace(/-+/g, replacer); // remove consecutive hyphens
349
350
  return str;
350
351
  }
351
352
 
@@ -388,4 +389,8 @@ export function object_equals( x, y ) {
388
389
  // allows x[ p ] to be set to undefined
389
390
 
390
391
  return true;
391
- }
392
+ }
393
+
394
+ export const isValidPath = (path) =>{
395
+ return /^(?:[a-z]:)?[\/\\]{0,2}(?:[.\/\\ ](?![.\/\\\n])|[^<>:"|?*.\/\\ \n])+$/gmi.test(path);
396
+ }
@@ -3,7 +3,7 @@ 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";
@@ -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
@@ -5217,6 +5218,7 @@ const handleFields = async (model, data, user, isRecursiveCall = false) => {
5217
5218
  }
5218
5219
  };
5219
5220
 
5221
+ let restoreRequests = {};
5220
5222
  export const validateRestoreRequest = (username, token) => {
5221
5223
  const request = restoreRequests[username];
5222
5224
  if (!request) {
@@ -5247,20 +5249,21 @@ export const loadFromDump = async (user, options = {}) => {
5247
5249
  throw new Error("No encryption key found for this user. Cannot restore.");
5248
5250
  }
5249
5251
 
5252
+ const userId = getObjectHash({user: user.username});
5250
5253
  // --- La logique pour trouver le fichier de backup (local ou S3) reste la même ---
5251
5254
  // (le code existant pour trouver/télécharger le backupFilePath va ici)
5252
5255
  // ...
5253
5256
  let backupFilePath; // Assurez-vous que cette variable est bien définie avec le chemin du fichier .tar.gz
5254
5257
  // Exemple simplifié :
5255
5258
  const backupDir = getBackupDir();
5256
- const backupFilenameRegex = new RegExp(`^backup_${user.username}_(\\d+)\\.tar\\.gz$`);
5259
+ const backupFilenameRegex = new RegExp(`^backup_${userId}_(\\d+)\\.tar\\.gz$`);
5257
5260
  const backupFiles = fs.readdirSync(backupDir).filter(filename => backupFilenameRegex.test(filename));
5258
5261
  if (backupFiles.length === 0) throw new Error(`Aucun fichier de sauvegarde local trouvé pour l'utilisateur ${user.username}.`);
5259
5262
  const latestBackupFile = backupFiles.sort((a, b) => parseInt(b.match(backupFilenameRegex)[1], 10) - parseInt(a.match(backupFilenameRegex)[1], 10))[0];
5260
5263
  backupFilePath = path.join(backupDir, latestBackupFile);
5261
5264
  // --- Fin de la logique de recherche de fichier ---
5262
5265
 
5263
- const tmpRestoreDir = path.join(backupDir, `tmp_restore_${user.username}_${Date.now()}`);
5266
+ const tmpRestoreDir = path.join(backupDir, `tmp_restore_${userId}_${Date.now()}`);
5264
5267
 
5265
5268
  try {
5266
5269
  await runCryptoWorkerTask('decrypt', { filePath: backupFilePath, password: encryptedKey });
@@ -5297,17 +5300,25 @@ export const loadFromDump = async (user, options = {}) => {
5297
5300
  }
5298
5301
 
5299
5302
  let command;
5303
+ const args = [
5304
+ '--uri', dbUrl,
5305
+ '--db', dbName
5306
+ ];
5307
+
5300
5308
  if (modelsOnly) {
5301
- // Restaure uniquement la collection des modèles
5302
- command = `mongorestore --uri="${dbUrl}" --db=${dbName} --nsInclude="${dbName}.models" "${restoreSourceDir}"`;
5309
+ args.push('--nsInclude', `${dbName}.models`);
5303
5310
  } else {
5304
- // Restauration complète
5305
- command = `mongorestore --uri="${dbUrl}" --db=${dbName} --nsInclude="${dbName}.datas" --nsInclude="${dbName}.models" "${restoreSourceDir}"`;
5311
+ // mongorestore accepte plusieurs fois l'option --nsInclude
5312
+ args.push('--nsInclude', `${dbName}.datas`);
5313
+ args.push('--nsInclude', `${dbName}.models`);
5306
5314
  }
5315
+ // Le répertoire source est le dernier argument
5316
+ args.push(restoreSourceDir);
5307
5317
 
5308
- logger.info(`[${action}] Executing restore command: ${command}`);
5309
- await execAsync(command);
5310
5318
 
5319
+ logger.info(`[${action}] Executing restore command: ${command}`);
5320
+ await execFileAsync('mongorestore', args);
5321
+
5311
5322
  // --- Tâches Post-Restauration ---
5312
5323
  await scheduleAlerts();
5313
5324
  await scheduleWorkflowTriggers();
@@ -5346,121 +5357,90 @@ const readKeyFromFile = (user) => {
5346
5357
  return null;
5347
5358
  };
5348
5359
 
5360
+ // C:/Dev/data-primals-engine/src/modules/data.js
5361
+
5349
5362
  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'
5363
+ const s3Config = user.configS3;
5353
5364
  const backupDir = getBackupDir();
5365
+ const userId = getObjectHash({ user: user.username });
5366
+ const backupFilename = `backup_${userId}`;
5367
+ const timestamp = Date.now();
5368
+
5369
+ // Déclarer les chemins ici pour qu'ils soient accessibles dans tout le scope de la fonction
5370
+ const localTempDumpDir = path.join(backupDir, `${backupFilename}_${timestamp}_temp`);
5371
+ const finalArchiveName = `${backupFilename}_${timestamp}.tar.gz`;
5372
+ const localArchivePath = path.join(backupDir, finalArchiveName);
5354
5373
 
5355
5374
  let encryptedKey = readKeyFromFile(user);
5356
5375
  if (!encryptedKey) {
5357
5376
  encryptedKey = generateAndStoreKey(user);
5358
- logger.info('Encryption key générée.');
5359
- } else {
5360
- logger.info('Encryption key chargée.');
5361
5377
  }
5362
5378
 
5363
5379
  try {
5364
- // Déterminer la fréquence de la sauvegarde
5365
5380
  const backupFrequency = await engine.userProvider.getBackupFrequency(user);
5366
-
5367
5381
  logger.info(`Fréquence de sauvegarde : ${backupFrequency}.`);
5368
5382
 
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
5383
  const collections = await MongoDatabase.listCollections().toArray();
5383
- let query;
5384
- let col;
5385
5384
  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);
5385
+ const collsToBackup = [await getUserCollectionName(user), 'models'];
5386
+ if (collsToBackup.includes(collection.name)) {
5387
+ const query = { _user: user.username };
5388
+ const args = [
5389
+ '--uri', dbUrl,
5390
+ '--db', dbName,
5391
+ '--out', localTempDumpDir,
5392
+ '--collection', collection.name,
5393
+ '--query', JSON.stringify(query)
5394
+ ];
5395
+ logger.info(`Exécution de la commande : mongodump ${args.join(' ')}`);
5396
+ await execFileAsync('mongodump', args);
5399
5397
  }
5400
-
5401
5398
  }
5402
5399
 
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éé
5400
+ const dumpSourceDir = path.join(localTempDumpDir, dbName);
5407
5401
  if (fs.existsSync(dumpSourceDir)) {
5408
5402
  await tar.create({ gzip: true, file: localArchivePath, C: localTempDumpDir }, [dbName]);
5409
5403
  logger.info(`Archive de sauvegarde locale créée : ${localArchivePath}`);
5410
5404
  } else {
5411
- throw new Error(`Le répertoire de dump ${dumpSourceDir} n'a pas été trouvé après mongodump.`);
5405
+ logger.warn(`Le répertoire de dump ${dumpSourceDir} était vide. Aucune archive n'a été créée.`);
5406
+ // On s'arrête ici car il n'y a rien à traiter
5407
+ return Promise.resolve();
5412
5408
  }
5413
5409
 
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
5410
+ await encryptFile(localArchivePath, encryptedKey);
5416
5411
 
5417
5412
  if (s3Config && s3Config.bucketName) {
5418
- logger.info(`Téléversement de la sauvegarde vers S3 pour l'utilisateur ${user.username}. Bucket: ${s3Config.bucketName}`);
5419
5413
  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.`);
5414
+ fs.unlinkSync(localArchivePath); // Supprime l'archive locale après l'upload
5424
5415
  } 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é.`);
5416
+ logger.info(`Aucune configuration S3 trouvée. La sauvegarde reste locale : ${localArchivePath}.`);
5434
5417
  }
5435
5418
 
5436
5419
  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
5420
+ await manageBackupRotation(user, backupFrequency, s3Config);
5440
5421
 
5441
- return Promise.resolve();
5442
5422
  } catch (error) {
5443
5423
  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
5424
+ // Nettoyage de l'archive si elle a été créée avant l'erreur
5450
5425
  if (fs.existsSync(localArchivePath)) {
5451
5426
  fs.unlinkSync(localArchivePath);
5452
5427
  }
5453
- throw error; // Relancer l'erreur
5428
+ throw error; // Relancer l'erreur pour que l'appelant soit informé
5429
+ } finally {
5430
+ // --- NETTOYAGE GARANTI ---
5431
+ // Ce bloc s'exécute toujours, que la sauvegarde réussisse ou échoue.
5432
+ if (fs.existsSync(localTempDumpDir)) {
5433
+ fs.rmSync(localTempDumpDir, { recursive: true, force: true });
5434
+ logger.info(`Répertoire de dump temporaire ${localTempDumpDir} supprimé.`);
5435
+ }
5454
5436
  }
5455
- }
5456
-
5437
+ };
5457
5438
  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
-
5439
+ const userId = getObjectHash({user:user.username});
5460
5440
  let filesToManage = [];
5461
5441
 
5462
5442
  if (s3Config && s3Config.bucketName) {
5463
- logger.info(`Gestion de la rotation des sauvegardes S3 pour ${userId}.`);
5443
+ logger.info(`Gestion de la rotation des sauvegardes S3 pour ${userid}.`);
5464
5444
  const s3Backups = await listS3Backups(s3Config);
5465
5445
  // Filtrer pour ne garder que les backups de cet utilisateur et trier
5466
5446
  filesToManage = s3Backups
@@ -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
  });