data-primals-engine 1.0.8 → 1.0.9

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.
@@ -0,0 +1,59 @@
1
+ # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node
2
+ # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs
3
+
4
+ name: Node.js CI
5
+
6
+ on:
7
+ push:
8
+ branches: [ "develop", "main" ]
9
+ pull_request:
10
+ branches: [ "develop", "main" ]
11
+
12
+ jobs:
13
+ build:
14
+
15
+ runs-on: ubuntu-latest
16
+ container:
17
+ image: node:18 # Specify the Docker version you needx
18
+ env:
19
+ NODE_OPTIONS: "--max_old_space_size=4096"
20
+ NODE_ENV: development
21
+ ports:
22
+ - 7635
23
+ - 27017
24
+ strategy:
25
+ matrix:
26
+ node-version: [18.x]
27
+ # See supported Node.js release schedule at https://nodejs.org/en/about/releases/
28
+ services:
29
+ mongodb:
30
+ image: mongo:5.0
31
+ ports:
32
+ - 27017:27017
33
+ options: >-
34
+ --health-cmd "mongo --eval 'db.serverStatus()'"
35
+ --health-interval 10s
36
+ --health-timeout 5s
37
+ --health-retries 5
38
+ steps:
39
+ - uses: actions/checkout@v4
40
+
41
+ - name: Install MongoDB Tools
42
+ run: |
43
+ wget -qO - https://www.mongodb.org/static/pgp/server-5.0.asc | apt-key add -
44
+ echo "deb [ arch=amd64,arm64 ] https://repo.mongodb.org/apt/ubuntu focal/mongodb-org/5.0 multiverse" | tee /etc/apt/sources.list.d/mongodb-org-5.0.list
45
+ apt-get update
46
+ apt-get install -y mongodb-org-tools
47
+
48
+ - name: Set up Node.js
49
+ uses: actions/setup-node@v2
50
+ with:
51
+ node-version: '18.18.2'
52
+
53
+ - name: Install dependencies
54
+ run: npm install
55
+
56
+ - name: Run tests
57
+ run: npm test
58
+
59
+
@@ -0,0 +1,23 @@
1
+ name: Publish package to GitHub Packages
2
+ on:
3
+ release:
4
+ types: [published]
5
+ jobs:
6
+ build:
7
+ runs-on: ubuntu-latest
8
+ permissions:
9
+ contents: read
10
+ packages: write
11
+ steps:
12
+ - uses: actions/checkout@v4
13
+ # Setup .npmrc file to publish to GitHub Packages
14
+ - uses: actions/setup-node@v4
15
+ with:
16
+ node-version: '20.x'
17
+ registry-url: 'https://npm.pkg.github.com'
18
+ # Defaults to the user or organization that owns the workflow file
19
+ scope: '@octocat'
20
+ - run: npm ci
21
+ - run: npm publish
22
+ env:
23
+ NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
package/Dockerfile ADDED
@@ -0,0 +1,15 @@
1
+ # Base image
2
+ FROM node:18.18.2
3
+
4
+ # Set working directory
5
+ WORKDIR /
6
+
7
+ # Copy package files and install dependencies
8
+ COPY package.json ./
9
+ RUN npm install
10
+
11
+ # Copy application code
12
+ COPY . .
13
+
14
+ # Expose the application port
15
+ EXPOSE 7633
package/DockerfileTest ADDED
@@ -0,0 +1,15 @@
1
+ # Base image
2
+ FROM node:18.18.2
3
+
4
+ # Set working directory
5
+ WORKDIR /
6
+
7
+ # Copy package files and install dependencies
8
+ COPY package.json ./
9
+ RUN npm install
10
+
11
+ # Copy application code
12
+ COPY . .
13
+
14
+ # Expose the application port
15
+ EXPOSE 7635
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "data-primals-engine",
3
- "version": "1.0.8",
3
+ "version": "1.0.9",
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",
7
7
  "scripts": {
8
+ "test": "cross-env MONGO_DB_URL=\"mongodb://localhost:27017\" PORT=7635 vitest --no-file-parallelism",
8
9
  "preinstall": "npx force-resolutions",
9
10
  "devserver": "cross-env NODE_ENV=development node server.js",
10
11
  "server": "cross-env NODE_ENV=production node server.js",
@@ -39,11 +40,9 @@
39
40
  "chalk": "^5.4.1",
40
41
  "check-disk-space": "^3.4.0",
41
42
  "compression": "^1.8.0",
42
- "connect-mongo": "^5.1.0",
43
43
  "cookie-parser": "^1.4.7",
44
44
  "cronstrue": "^3.1.0",
45
45
  "csv-parse": "^5.6.0",
46
- "csv-parser": "^3.2.0",
47
46
  "date-fns": "^4.1.0",
48
47
  "express": "^4.21.2",
49
48
  "express-csrf-double-submit-cookie": "^1.2.1",
@@ -61,7 +60,6 @@
61
60
  "nodemailer": "^6.10.0",
62
61
  "openai": "^5.8.2",
63
62
  "randomcolor": "^0.6.2",
64
- "rate-limiter-flexible": "^5.0.5",
65
63
  "react-helmet": "^6.1.0",
66
64
  "react-i18next": "^15.4.1",
67
65
  "react-markdown": "^10.1.0",
@@ -70,10 +68,9 @@
70
68
  "sirv": "^3.0.1",
71
69
  "swagger-ui-express": "^5.0.1",
72
70
  "tar": "^7.4.3",
73
- "tar-fs": "^3.0.8",
74
71
  "uniqid": "^5.4.0",
75
- "yaml": "^2.8.0",
76
- "zlib": "^1.0.5"
72
+ "vitest": "^3.2.3",
73
+ "yaml": "^2.8.0"
77
74
  },
78
75
  "devDependencies": {
79
76
  "@eslint/js": "^9.17.0",
package/src/engine.js CHANGED
@@ -17,7 +17,7 @@ import formidableMiddleware from 'express-formidable';
17
17
  const isProduction = process.env.NODE_ENV === 'production'
18
18
 
19
19
  // Connection URL
20
- export const dbUrl = process.env.MONGO_DB_URL || 'mongodb://127.0.0.1:27017';
20
+ export const dbUrl = process.env.CI ? 'mongodb://mongodb:27017' : (process.env.MONGO_DB_URL || 'mongodb://127.0.0.1:27017');
21
21
  export const MongoClient = new InternalMongoClient(dbUrl, { maxPoolSize: 20 });
22
22
 
23
23
  // Database Name
@@ -87,7 +87,7 @@ export const Engine = {
87
87
  }
88
88
  };
89
89
 
90
- await Promise.all(Config.Get('modules').map(async module => {
90
+ await Promise.all(Config.Get('modules', []).map(async module => {
91
91
  try {
92
92
  if( fs.existsSync(module)){
93
93
  return await importModule(module);
package/src/migrate.js CHANGED
@@ -15,7 +15,7 @@ Config.Set("modules", ["mongodb"]);
15
15
  const MIGRATIONS_DIR = path.resolve(process.cwd(), 'server', 'src', 'migrations');
16
16
  const MIGRATIONS_COLLECTION = 'migrations_log';
17
17
 
18
- const engine = Engine.Create();
18
+ const engine = await Engine.Create();
19
19
  const port = process.env.MIGRATE_PORT || 7640;
20
20
 
21
21
  engine.start(port, async () => {
@@ -101,7 +101,7 @@ const sseConnections = new Map();
101
101
 
102
102
  const delay = ms => new Promise(res => setTimeout(res, ms));
103
103
 
104
- const backupDir = process.env.BACKUP_DIR || './backups'; // Répertoire de stockage des sauvegardes
104
+ const getBackupDir = () => process.env.BACKUP_DIR || './backups'; // Répertoire de stockage des sauvegardes
105
105
  const execAsync = promisify(exec);
106
106
 
107
107
  let importJobs = {};
@@ -3003,10 +3003,14 @@ async function processRelations(docToProcess, model, collection, me, idMap) {
3003
3003
  // Phase 3: Traitement des résultats
3004
3004
  findResults.forEach((result, index) => {
3005
3005
  const { field, multiple } = batchFinds[index];
3006
- if (result.data?.length) {
3006
+ if (result.data?.length > 0) {
3007
+ // Cas où des documents sont trouvés
3007
3008
  docToProcess[field] = multiple
3008
3009
  ? result.data.map(r => r._id.toString())
3009
3010
  : result.data[0]._id.toString();
3011
+ } else {
3012
+ // Cas où AUCUN document n'est trouvé : il faut nettoyer le champ !
3013
+ docToProcess[field] = multiple ? [] : null;
3010
3014
  }
3011
3015
  });
3012
3016
 
@@ -4940,6 +4944,7 @@ export const loadFromDump = async (user, options = {}) => {
4940
4944
  // ...
4941
4945
  let backupFilePath; // Assurez-vous que cette variable est bien définie avec le chemin du fichier .tar.gz
4942
4946
  // Exemple simplifié :
4947
+ const backupDir = getBackupDir();
4943
4948
  const backupFilenameRegex = new RegExp(`^backup_${user.username}_(\\d+)\\.tar\\.gz$`);
4944
4949
  const backupFiles = fs.readdirSync(backupDir).filter(filename => backupFilenameRegex.test(filename));
4945
4950
  if (backupFiles.length === 0) throw new Error(`Aucun fichier de sauvegarde local trouvé pour l'utilisateur ${user.username}.`);
@@ -5016,6 +5021,7 @@ export const loadFromDump = async (user, options = {}) => {
5016
5021
 
5017
5022
  // Fonction pour générer une clé aléatoire et la stocker dans un fichier
5018
5023
  const generateAndStoreKey = (user) => {
5024
+ const backupDir = getBackupDir();
5019
5025
  const keyFile = path.join(backupDir, getObjectHash({id:getUserId(user)})+'_encryption.key');
5020
5026
  const key = crypto.randomBytes(16).toString('hex');
5021
5027
  fs.writeFileSync(keyFile, key, { mode: 0o600 }); // Permissions strictes
@@ -5024,6 +5030,7 @@ const generateAndStoreKey = (user) => {
5024
5030
 
5025
5031
  // Fonction pour lire la clé depuis le fichier
5026
5032
  const readKeyFromFile = (user) => {
5033
+ const backupDir = getBackupDir();
5027
5034
  const keyFile = path.join(backupDir, getObjectHash({id:getUserId(user)})+'_encryption.key');
5028
5035
  if (fs.existsSync(keyFile)) {
5029
5036
  return fs.readFileSync(keyFile, 'utf8');
@@ -5035,6 +5042,7 @@ export const dumpUserData = async (user) => {
5035
5042
  // Déterminer la clé de chiffrement
5036
5043
  // Pour cet exemple, on simule la config S3. Remplace par la vraie récupération.
5037
5044
  const s3Config = user.configS3; // Supposons que l'objet 'user' passé contient déjà 'configS3'
5045
+ const backupDir = getBackupDir();
5038
5046
 
5039
5047
  let encryptedKey = readKeyFromFile(user);
5040
5048
  if (!encryptedKey) {
@@ -5080,7 +5088,7 @@ export const dumpUserData = async (user) => {
5080
5088
  let col;
5081
5089
  for (const collection of collections) {
5082
5090
 
5083
- const colls = ['datas', 'models'];
5091
+ const colls = [getUserCollectionName(user), 'models'];
5084
5092
  if( colls.includes(collection.name) ){
5085
5093
 
5086
5094
  // Exécuter mongodump avec les filtres appropriés
@@ -5166,6 +5174,7 @@ async function manageBackupRotation(user, backupFrequency, s3Config = null) { //
5166
5174
 
5167
5175
  } else {
5168
5176
  logger.info(`Gestion de la rotation des sauvegardes locales pour ${userId}.`);
5177
+ const backupDir = getBackupDir();
5169
5178
  const localFiles = fs.readdirSync(backupDir);
5170
5179
  filesToManage = localFiles
5171
5180
  .filter(f => !fs.lstatSync(path.join(backupDir, f)).isDirectory() && f.startsWith(`backup_${userId}_`) && f.endsWith('.tar.gz'))
@@ -23,13 +23,6 @@ export async function onInit(defaultEngine) {
23
23
  } catch (e) {
24
24
 
25
25
  }
26
- // Create a SecureContext object
27
- // Connection URL
28
- const dbUrl = process.env.MONGO_DB_URL || 'mongodb://localhost:27017';
29
- const MongoClient = new InternalMongoClient(dbUrl, {
30
- tls: false, maxPoolSize: 20
31
- });
32
- await MongoClient.connect();
33
26
 
34
27
  modelsCollection = MongoDatabase.collection("models");
35
28
  datasCollection = MongoDatabase.collection("datas");
package/src/setenv.js ADDED
@@ -0,0 +1,39 @@
1
+ import {getRandom} from "data-primals-engine/core";
2
+ import process from "node:process";
3
+ import {Engine} from "./index.js";
4
+
5
+ let ports = [], engineInstance, mongod;
6
+ export const getUniquePort = () =>{
7
+ let d, it=0;
8
+ do{
9
+ d = getRandom(10000, 20000);
10
+ ++it;
11
+ } while( ports.includes(d) && it < 10000);
12
+ return d;
13
+ }
14
+
15
+ // --- Utilitaires pour les tests ---
16
+ export const generateUniqueName = (baseName) => `${baseName}_${getRandom(1000, 9999)}_${Date.now()}`;
17
+ export const initEngine = async () => {
18
+ if( engineInstance )
19
+ return engineInstance;
20
+
21
+ process.env.OPENAI_API_KEY = "O000";
22
+
23
+ const port = process.env.PORT || getUniquePort(); // Different port for this test suite
24
+ engineInstance = await Engine.Create();
25
+ await engineInstance.start(port);
26
+ return engineInstance;
27
+ }
28
+
29
+
30
+ /**
31
+ * Stops the application engine and the in-memory MongoDB instance.
32
+ */
33
+ export const stopEngine = async () => {
34
+ if (engineInstance) {
35
+ await engineInstance.stop();
36
+ engineInstance = null;
37
+ console.log("Test engine stopped.");
38
+ }
39
+ };
@@ -0,0 +1,141 @@
1
+ // test/data.backup.integration.test.js
2
+
3
+ import path from "node:path";
4
+ import { Config } from '../src/config.js';
5
+
6
+ import { ObjectId } from 'mongodb';
7
+ import {expect, describe, it, beforeAll, afterAll, beforeEach} from 'vitest';
8
+ import { vi } from 'vitest'
9
+ import { Buffer } from 'node:buffer'; // Explicitly import Buffer
10
+ import crypto from 'node:crypto'; //Explicitly import crypto
11
+
12
+ import {
13
+ createModel,
14
+ getModel, insertData
15
+ } from 'data-primals-engine/modules/data';
16
+
17
+ import {
18
+ modelsCollection as getAppModelsCollection,
19
+ getCollectionForUser as getAppUserCollection,
20
+ } from 'data-primals-engine/modules/mongodb';
21
+ import { Engine } from "data-primals-engine/engine";
22
+ import process from "node:process";
23
+
24
+ import { dumpUserData, loadFromDump, getUserHash } from 'data-primals-engine/modules/data';
25
+ import fs from "node:fs";
26
+ import {getRandom} from "data-primals-engine/core";
27
+ import {getUniquePort, initEngine, stopEngine} from "../src/setenv.js";
28
+
29
+ vi.mock('data-primals-engine/engine', async(importOriginal) => {
30
+ const mod = await importOriginal() // type is inferred
31
+ return {
32
+ ...mod
33
+ };
34
+ });
35
+
36
+ // Mock data and settings
37
+ const mockUser = {
38
+ username: 'testuserBackup',
39
+ _user: 'testuserBackup',
40
+ userPlan: 'premium',
41
+ email: 'testBackup@example.com',
42
+ configS3: {
43
+ bucketName: null
44
+ }
45
+ };
46
+ const testDbName = 'testIntegrationDbHO_Backup';
47
+ const testModelDefinition = {
48
+ name: 'backupTestModel',
49
+ _user: mockUser.username,
50
+ description: 'Model for testing backup/restore',
51
+ fields: [
52
+ { name: 'testField', type: 'string', required: true },
53
+ { name: 'optionalField', type: 'number' },
54
+ ],
55
+ maxRequestData: 10,
56
+ };
57
+
58
+ let testModelsColInstance;
59
+ let testDatasColInstance;
60
+ let engineInstance;
61
+ let testDatasApi;
62
+
63
+ const backupDir = path.resolve('./test-backups'); // Use an absolute path
64
+
65
+ beforeAll(async () => {
66
+
67
+ process.env.BACKUP_DIR = backupDir; // Set backup directory
68
+
69
+ // Create the backup directory if it doesn't exist
70
+ if (!fs.existsSync(backupDir)) {
71
+ fs.mkdirSync(backupDir, { recursive: true });
72
+ }
73
+
74
+ // Delete any existing files in the backup directory
75
+ fs.readdirSync(backupDir).forEach(file => {
76
+ fs.unlinkSync(path.join(backupDir, file));
77
+ });
78
+ vi.stubEnv('S3_CONFIG_ENCRYPTION_KEY', '00000000000000000000000000000000');
79
+ vi.stubEnv('OPENAI_API_KEY', '00000000000000000000000000000000');
80
+ // You might need to create a model first if your dumpUserData requires it
81
+ await createModel(testModelDefinition);
82
+ }, 45000);
83
+
84
+ afterAll(async () => {
85
+
86
+ delete process.env.DB_URL;
87
+ delete process.env.DB_NAME;
88
+
89
+ // Clean up test backups
90
+ if (fs.existsSync(backupDir)) {
91
+ fs.readdirSync(backupDir).forEach(file => {
92
+ fs.unlinkSync(path.join(backupDir, file));
93
+ });
94
+ // Optional: fs.rmdirSync(backupDir); // Remove the directory itself
95
+ }
96
+ });
97
+
98
+ beforeAll(async () =>{
99
+ Config.Set("modules", ["mongodb", "data", "file", "bucket", "workflow","user", "assistant"]);
100
+ await initEngine();
101
+ })
102
+ beforeEach(async () => {
103
+ testModelsColInstance = getAppModelsCollection;
104
+ testDatasColInstance = getAppUserCollection(mockUser);
105
+ });
106
+
107
+ describe('Data Backup and Restore Integration', () => {
108
+ it('should dump and restore user data successfully, and verify data integrity', async () => { // Le nom du test est plus précis
109
+ // 1. Insérer des données à sauvegarder
110
+ const initialData = { testField: 'Initial Value', optionalField: 123 };
111
+ const insertResult = await insertData(testModelDefinition.name, initialData, {}, mockUser, false);
112
+ expect(insertResult.success).toBe(true);
113
+ const insertedId = insertResult.insertedIds[0];
114
+
115
+ // Vérifier que les données existent avant la sauvegarde
116
+ let docBeforeBackup = await testDatasColInstance.findOne({ _id: new ObjectId(insertedId) });
117
+ expect(docBeforeBackup).not.toBeNull();
118
+ expect(docBeforeBackup.testField).toBe('Initial Value');
119
+
120
+ // 2. Sauvegarder les données
121
+ await dumpUserData(mockUser);
122
+
123
+ // 3. Simuler une suppression totale des données
124
+ await testDatasColInstance.deleteMany({ _user: mockUser.username });
125
+ let docAfterDelete = await testDatasColInstance.findOne({ _id: new ObjectId(insertedId) });
126
+ expect(docAfterDelete).toBeNull();
127
+
128
+ // 4. Restaurer les données
129
+ await loadFromDump(mockUser);
130
+
131
+ // 5. **VÉRIFICATION FINALE** : S'assurer que les données sont restaurées correctement
132
+ const countAfterRestore = await testDatasColInstance.countDocuments({ _user: mockUser.username });
133
+ expect(countAfterRestore).toBeGreaterThan(0);
134
+
135
+ const docAfterRestore = await testDatasColInstance.findOne({ _user: mockUser.username, _model: testModelDefinition.name });
136
+ expect(docAfterRestore).not.toBeNull();
137
+ expect(docAfterRestore.testField).toBe('Initial Value');
138
+ expect(docAfterRestore.optionalField).toBe(123);
139
+
140
+ }, 15000); // Timeout augmenté pour les opérations de fichiers
141
+ });