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.
- package/README.md +65 -22
- package/client/src/AssistantChat.scss +0 -1
- package/client/src/ChartConfigModal.jsx +0 -1
- package/client/src/ConditionBuilder.jsx +0 -2
- package/client/src/ContentView.jsx +0 -1
- package/client/src/CronBuilder.scss +0 -1
- package/client/src/CronPartBuilder.jsx +0 -1
- package/client/src/Dashboard.jsx +0 -1
- package/client/src/Dashboard.scss +0 -1
- package/client/src/DashboardChart.jsx +0 -1
- package/client/src/DashboardView.jsx +0 -1
- package/client/src/DataEditor.jsx +0 -4
- package/client/src/DisplayFlexNodeRenderer.jsx +0 -1
- package/client/src/DocumentationPageLayout.jsx +0 -2
- package/client/src/DocumentationPageLayout.scss +0 -1
- package/client/src/FlexNode.jsx +0 -1
- package/client/src/KanbanView.scss +0 -1
- package/client/src/RestoreDialog.jsx +0 -1
- package/client/src/TourSpotlight.jsx +0 -1
- package/client/src/hooks/useDragAndDrop.js +0 -1
- package/issues.txt +0 -0
- package/package.json +4 -4
- package/src/constants.js +6 -0
- package/src/core.js +10 -5
- package/src/data.js +0 -2
- package/src/migrate.js +0 -1
- package/src/modules/assistant.js +211 -133
- package/src/modules/data.js +149 -150
- package/src/modules/workflow.js +10 -12
- package/src/providers.js +1 -1
- package/src/workers/crypto-worker.js +0 -1
- package/test/data.backup.integration.test.js +3 -1
- package/test/data.integration.test.js +53 -70
- package/test/workflow.integration.test.js +1 -1
package/src/modules/data.js
CHANGED
|
@@ -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
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
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
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
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",
|
|
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
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
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({
|
|
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,
|
|
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,
|
|
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
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
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({
|
|
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
|
-
|
|
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 (
|
|
3874
|
-
findFilter.push({"$in": ["$_id",
|
|
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 (
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
4962
|
-
|
|
4963
|
-
|
|
4964
|
-
|
|
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_${
|
|
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_${
|
|
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
|
-
|
|
5302
|
-
command = `mongorestore --uri="${dbUrl}" --db=${dbName} --nsInclude="${dbName}.models" "${restoreSourceDir}"`;
|
|
5335
|
+
args.push('--nsInclude', `${dbName}.models`);
|
|
5303
5336
|
} else {
|
|
5304
|
-
//
|
|
5305
|
-
|
|
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
|
-
|
|
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
|
-
|
|
5388
|
-
|
|
5389
|
-
|
|
5390
|
-
|
|
5391
|
-
|
|
5392
|
-
|
|
5393
|
-
|
|
5394
|
-
|
|
5395
|
-
|
|
5396
|
-
|
|
5397
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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;
|
|
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 ${
|
|
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({
|
|
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;
|
package/src/modules/workflow.js
CHANGED
|
@@ -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({
|
|
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({
|
|
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,
|
|
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({
|
|
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({
|
|
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({
|
|
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({
|
|
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
|
-
|
|
1482
|
-
|
|
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
|
|
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.
|
|
@@ -86,7 +86,7 @@ afterAll(async () => {
|
|
|
86
86
|
fs.readdirSync(backupDir).forEach(file => {
|
|
87
87
|
fs.unlinkSync(path.join(backupDir, file));
|
|
88
88
|
});
|
|
89
|
-
|
|
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
|
});
|