data-primals-engine 1.0.8 → 1.0.10

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/src/index.js CHANGED
@@ -10,6 +10,6 @@ export { UserProvider } from './providers.js';
10
10
 
11
11
  // --- Database & Data Modules ---
12
12
  export { datasCollection, filesCollection, modelsCollection, packsCollection } from './modules/mongodb.js';
13
- export { searchData, insertData, editData, loadFromDump, dumpUserData, validateRestoreRequest, patchData, deleteData, createModel, getModel, getModels } from './modules/data.js';
13
+ export { searchData, insertData, editData, exportData, importData, scheduleAlerts, cancelAlerts, validateModelStructure, installPack, jobDumpUserData, loadFromDump, dumpUserData, validateRestoreRequest, patchData, deleteData, createModel, editModel, deleteModels, getModel, getModels } from './modules/data.js';
14
14
 
15
15
 
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 = {};
@@ -1113,6 +1113,111 @@ function applyCronMask(cronString, mask, defaults) {
1113
1113
  return newParts.join(' ');
1114
1114
  }
1115
1115
 
1116
+
1117
+ export const editModel = async (user, id, data) => {
1118
+
1119
+ if( !(isDemoUser(user) && Config.Get("useDemoAccounts")) && isLocalUser(user) && !await hasPermission(["API_ADMIN", "API_EDIT_MODEL"], user)){
1120
+ return ({success: false, error: i18n.t('api.permission.editModel', 'Cannot edit models from the API')})
1121
+ }
1122
+
1123
+ const dataModel = data;
1124
+ try {
1125
+ const collection = getCollectionForUser(user);
1126
+ validateModelStructure(dataModel);
1127
+
1128
+ const el = await modelsCollection.findOne({ $and: [
1129
+ {_user: {$exists: true}},
1130
+ { _id: new ObjectId(id) },
1131
+ {$and: [{_user: {$exists: true}}, {$or: [{_user: user._user}, {_user: user.username}]}]
1132
+ }
1133
+ ]});
1134
+
1135
+ if( !el ){
1136
+ return ({success: false, statusCode: 404, error: i18n.t("api.model.notFound", { model: dataModel.name })});
1137
+ }
1138
+
1139
+ // renommage du modèle
1140
+ if (typeof (data.name)==='string'&&el.name !== data.name && data.name ){
1141
+ await collection.updateMany({ _model: el.name }, { $set: { _model: data.name }});
1142
+ await modelsCollection.updateMany({ 'fields' : {
1143
+ '$elemMatch' : { relation: el.name }
1144
+ }}, {
1145
+ $set : {
1146
+ 'fields.$.relation' : data.name
1147
+ }
1148
+ })
1149
+ }
1150
+
1151
+ const coll = getCollectionForUser(user);
1152
+ // Update indexes
1153
+ // Update indexes
1154
+ if (user.userPlan === 'premium') {
1155
+ let indexes = [];
1156
+ try {
1157
+ // On essaie de récupérer les index existants
1158
+ indexes = await coll.indexes();
1159
+ } catch (e) {
1160
+ // Si la collection n'existe pas, c'est normal.
1161
+ // createIndex la créera. Il n'y a juste pas d'index à supprimer.
1162
+ if (e.codeName !== 'NamespaceNotFound') {
1163
+ throw e; // On relance les autres erreurs
1164
+ }
1165
+ }
1166
+
1167
+ // Le reste de votre logique de gestion d'index peut maintenant s'exécuter en toute sécurité
1168
+ for (const field of data.fields) {
1169
+ const elField = el.fields.find(f => f.name === field.name);
1170
+ if (!elField) continue;
1171
+
1172
+ const index = indexes.find(i => i.key[field.name] === 1 &&
1173
+ i.partialFilterExpression?._model === el.name &&
1174
+ i.partialFilterExpression?._user === user.username);
1175
+
1176
+ if (elField.index !== field.index && !field.index) {
1177
+ if (index) {
1178
+ await coll.dropIndex(index.name);
1179
+ }
1180
+ } else if (elField.index !== field.index && field.index) {
1181
+ if (!index) {
1182
+ await coll.createIndex({ [field.name]: 1 }, {
1183
+ partialFilterExpression: {
1184
+ _model: data.name,
1185
+ _user: user.username
1186
+ }
1187
+ });
1188
+ }
1189
+ }
1190
+ }
1191
+ }
1192
+ // suppression des données à la suppression des champs
1193
+ const unset = {};
1194
+ el.fields.filter(f=> !dataModel.fields.some(dt => dt.name === f.name)).map(f => f.name).forEach(f => {
1195
+ unset[f] = 1;
1196
+ });
1197
+ await collection.updateMany({ _model: el.name }, { $unset: unset });
1198
+
1199
+ // sauvegarde du modele
1200
+ const set = {...data};
1201
+ delete set['_id'];
1202
+
1203
+ const oid = new ObjectId(id);
1204
+ await modelsCollection.updateOne({_id: oid}, {$set: set});
1205
+
1206
+ modelsCache.del(user.username+'@@'+el.name);
1207
+
1208
+ const model = await modelsCollection.findOne({_id: oid });
1209
+ triggerWorkflows(model, user, 'ModelEdited').catch(workflowError => {
1210
+ logger.error(`Erreur asynchrone lors du déclenchement des workflows pour ${model._model} ID ${model._id}:`, workflowError);
1211
+ });
1212
+
1213
+ return ({ success: true, data: await modelsCollection.findOne({_id : oid}) });
1214
+ } catch (e) {
1215
+ logger.error(e);
1216
+ return ({ success: false, error: e.message, statusCode: 500 });
1217
+ }
1218
+ };
1219
+
1220
+
1116
1221
  export async function onInit(defaultEngine) {
1117
1222
  engine = defaultEngine;
1118
1223
  logger = engine.getComponent(Logger);
@@ -1459,95 +1564,17 @@ export async function onInit(defaultEngine) {
1459
1564
  });
1460
1565
 
1461
1566
  engine.put('/api/model/:id', [middlewareAuthenticator, userInitiator, setTimeoutMiddleware(15000)], async (req, res) => {
1462
-
1463
- if( !(isDemoUser(req.me) && Config.Get("useDemoAccounts")) && isLocalUser(req.me) && !await hasPermission(["API_ADMIN", "API_EDIT_MODEL"], req.me)){
1464
- return res.status(403).json({success: false, error: i18n.t('api.permission.editModel', 'Cannot edit models from the API')})
1465
- }
1466
-
1467
- const dataModel = req.fields;
1468
- try {
1469
- const collection = getCollectionForUser(req.me);
1470
- validateModelStructure(dataModel);
1471
-
1472
- const el = await modelsCollection.findOne({ $and: [
1473
- {_user: {$exists: true}},
1474
- { _id: new ObjectId(req.params.id) },
1475
- {$and: [{_user: {$exists: true}}, {$or: [{_user: req.me._user}, {_user: req.me.username}]}]
1476
- }
1477
- ]});
1478
-
1479
- if( !el ){
1480
- return res.status(404).json({error: i18n.t("api.model.notFound", { model: dataModel.name })});
1481
- }
1482
-
1483
- // renommage du modèle
1484
- if (typeof (req.fields.name)==='string'&&el.name !== req.fields.name && req.fields.name ){
1485
- await collection.updateMany({ _model: el.name }, { $set: { _model: req.fields.name }});
1486
- await modelsCollection.updateMany({ 'fields' : {
1487
- '$elemMatch' : { relation: el.name }
1488
- }}, {
1489
- $set : {
1490
- 'fields.$.relation' : req.fields.name
1491
- }
1492
- })
1493
- }
1494
-
1495
- // Update indexes
1496
- if( req.me.userPlan === 'premium') {
1497
- const indexes = await datasCollection.indexes();
1498
- for (const field of dataModel.fields) {
1499
- const elField = el.fields.find(f => f.name === field.name);
1500
- if (!elField)
1501
- continue;
1502
- const index = indexes.find(i => i.key[field.name] === 1 &&
1503
- i.partialFilterExpression?._model === el.name &&
1504
- i.partialFilterExpression?._user === req.me.username);
1505
- if (elField.index !== field.index && !field.index) {
1506
- if (index) {
1507
- await datasCollection.dropIndex(index.name);
1508
- }
1509
- } else if (elField.index !== field.index && field.index) {
1510
- if (!index) {
1511
- await datasCollection.createIndex({[field.name]: 1}, {
1512
- partialFilterExpression: {
1513
- _model: dataModel.name,
1514
- _user: req.me.username
1515
- }
1516
- });
1517
- }
1518
- }
1519
- }
1520
- }
1521
-
1522
- // suppression des données à la suppression des champs
1523
- const unset = {};
1524
- el.fields.filter(f=> !dataModel.fields.some(dt => dt.name === f.name)).map(f => f.name).forEach(f => {
1525
- unset[f] = 1;
1526
- });
1527
- await collection.updateMany({ _model: el.name }, { $unset: unset });
1528
-
1529
- // sauvegarde du modele
1530
- const set = {...req.fields};
1531
- delete set['_id'];
1532
- await modelsCollection.updateOne({_id: new ObjectId(req.params.id)}, {$set: set});
1533
-
1534
- modelsCache.del(req.me.username+'@@'+el.name);
1535
-
1536
- const model = await modelsCollection.findOne({_id: new ObjectId(req.params.id) });
1537
- triggerWorkflows(model, req.me, 'ModelEdited').catch(workflowError => {
1538
- logger.error(`Erreur asynchrone lors du déclenchement des workflows pour ${model._model} ID ${model._id}:`, workflowError);
1539
- });
1540
-
1541
- res.json({ success: true, data: await modelsCollection.findOne({_id : new ObjectId(req.params.id)}) });
1542
- } catch (e) {
1543
- logger.error(e);
1544
- res.json({ success: false });
1567
+ const result = await editModel(req.me, req.params.id, req.fields);
1568
+ if( result.success){
1569
+ return res.status(result.statusCode || '200').json(result);
1570
+ }else{
1571
+ return res.status(result.statusCode || '500').json(result);
1545
1572
  }
1546
- })
1573
+ });
1547
1574
 
1548
1575
  engine.post('/api/data/restore', [throttle, middlewareAuthenticator, userInitiator,myFreePremiumAnonymousLimiter, setTimeoutMiddleware(60000)], async (req, res) => {
1549
1576
 
1550
- if (!((req.me?.roles || []).includes("admin"))) {
1577
+ if (!((user?.roles || []).includes("admin"))) {
1551
1578
  return res.status(403).json({success: false, error: 'Cannot backup data. Contact an administrator to get back your data'})
1552
1579
  }
1553
1580
 
@@ -3003,10 +3030,14 @@ async function processRelations(docToProcess, model, collection, me, idMap) {
3003
3030
  // Phase 3: Traitement des résultats
3004
3031
  findResults.forEach((result, index) => {
3005
3032
  const { field, multiple } = batchFinds[index];
3006
- if (result.data?.length) {
3033
+ if (result.data?.length > 0) {
3034
+ // Cas où des documents sont trouvés
3007
3035
  docToProcess[field] = multiple
3008
3036
  ? result.data.map(r => r._id.toString())
3009
3037
  : result.data[0]._id.toString();
3038
+ } else {
3039
+ // Cas où AUCUN document n'est trouvé : il faut nettoyer le champ !
3040
+ docToProcess[field] = multiple ? [] : null;
3010
3041
  }
3011
3042
  });
3012
3043
 
@@ -4940,6 +4971,7 @@ export const loadFromDump = async (user, options = {}) => {
4940
4971
  // ...
4941
4972
  let backupFilePath; // Assurez-vous que cette variable est bien définie avec le chemin du fichier .tar.gz
4942
4973
  // Exemple simplifié :
4974
+ const backupDir = getBackupDir();
4943
4975
  const backupFilenameRegex = new RegExp(`^backup_${user.username}_(\\d+)\\.tar\\.gz$`);
4944
4976
  const backupFiles = fs.readdirSync(backupDir).filter(filename => backupFilenameRegex.test(filename));
4945
4977
  if (backupFiles.length === 0) throw new Error(`Aucun fichier de sauvegarde local trouvé pour l'utilisateur ${user.username}.`);
@@ -5016,6 +5048,7 @@ export const loadFromDump = async (user, options = {}) => {
5016
5048
 
5017
5049
  // Fonction pour générer une clé aléatoire et la stocker dans un fichier
5018
5050
  const generateAndStoreKey = (user) => {
5051
+ const backupDir = getBackupDir();
5019
5052
  const keyFile = path.join(backupDir, getObjectHash({id:getUserId(user)})+'_encryption.key');
5020
5053
  const key = crypto.randomBytes(16).toString('hex');
5021
5054
  fs.writeFileSync(keyFile, key, { mode: 0o600 }); // Permissions strictes
@@ -5024,6 +5057,7 @@ const generateAndStoreKey = (user) => {
5024
5057
 
5025
5058
  // Fonction pour lire la clé depuis le fichier
5026
5059
  const readKeyFromFile = (user) => {
5060
+ const backupDir = getBackupDir();
5027
5061
  const keyFile = path.join(backupDir, getObjectHash({id:getUserId(user)})+'_encryption.key');
5028
5062
  if (fs.existsSync(keyFile)) {
5029
5063
  return fs.readFileSync(keyFile, 'utf8');
@@ -5035,6 +5069,7 @@ export const dumpUserData = async (user) => {
5035
5069
  // Déterminer la clé de chiffrement
5036
5070
  // Pour cet exemple, on simule la config S3. Remplace par la vraie récupération.
5037
5071
  const s3Config = user.configS3; // Supposons que l'objet 'user' passé contient déjà 'configS3'
5072
+ const backupDir = getBackupDir();
5038
5073
 
5039
5074
  let encryptedKey = readKeyFromFile(user);
5040
5075
  if (!encryptedKey) {
@@ -5080,7 +5115,7 @@ export const dumpUserData = async (user) => {
5080
5115
  let col;
5081
5116
  for (const collection of collections) {
5082
5117
 
5083
- const colls = ['datas', 'models'];
5118
+ const colls = [getUserCollectionName(user), 'models'];
5084
5119
  if( colls.includes(collection.name) ){
5085
5120
 
5086
5121
  // Exécuter mongodump avec les filtres appropriés
@@ -5166,6 +5201,7 @@ async function manageBackupRotation(user, backupFrequency, s3Config = null) { //
5166
5201
 
5167
5202
  } else {
5168
5203
  logger.info(`Gestion de la rotation des sauvegardes locales pour ${userId}.`);
5204
+ const backupDir = getBackupDir();
5169
5205
  const localFiles = fs.readdirSync(backupDir);
5170
5206
  filesToManage = localFiles
5171
5207
  .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
+ });