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/.github/workflows/node.js.yml +59 -0
- package/.github/workflows/npm-publish.yml +23 -0
- package/Dockerfile +15 -0
- package/DockerfileTest +15 -0
- package/README.md +217 -0
- package/package.json +4 -7
- package/src/engine.js +2 -2
- package/src/i18n.js +2274 -0
- package/src/index.js +1 -1
- package/src/migrate.js +1 -1
- package/src/modules/data.js +124 -88
- package/src/modules/mongodb.js +0 -7
- package/src/setenv.js +39 -0
- package/test/data.backup.integration.test.js +141 -0
- package/test/data.integration.test.js +796 -0
- package/test/globalSetup.js +15 -0
- package/test/globalTeardown.js +8 -0
- package/test/import_export.integration.test.js +200 -0
- package/test/model.integration.test.js +219 -0
- package/test/workflow.integration.test.js +314 -0
- package/test/workflow.robustness.test.js +195 -0
- package/vitest.config.js +16 -0
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 () => {
|
package/src/modules/data.js
CHANGED
|
@@ -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
|
|
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(
|
|
1464
|
-
return res.status(
|
|
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 (!((
|
|
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 = [
|
|
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'))
|
package/src/modules/mongodb.js
CHANGED
|
@@ -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
|
+
});
|