data-primals-engine 1.3.3 → 1.4.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 +797 -782
- package/client/README.md +20 -0
- package/client/package-lock.json +717 -151
- package/client/package.json +37 -36
- package/client/src/App.jsx +9 -10
- package/client/src/App.scss +42 -1
- package/client/src/Dashboard.jsx +349 -208
- package/client/src/DashboardView.jsx +569 -547
- package/client/src/DataEditor.jsx +20 -2
- package/client/src/DataLayout.jsx +54 -13
- package/client/src/DataTable.jsx +807 -760
- package/client/src/DataTable.scss +187 -90
- package/client/src/Dialog.scss +0 -3
- package/client/src/Field.jsx +1783 -1583
- package/client/src/ModelCreator.jsx +25 -3
- package/client/src/ModelCreatorField.jsx +906 -804
- package/client/src/ModelList.jsx +20 -2
- package/client/src/constants.js +16 -4
- package/client/src/contexts/UIContext.jsx +19 -10
- package/client/src/translations.js +265 -3
- package/package.json +4 -3
- package/server.js +1 -0
- package/src/core.js +18 -0
- package/src/defaultModels.js +70 -10
- package/src/email.js +2 -1
- package/src/filter.js +260 -256
- package/src/i18n.js +503 -30
- package/src/modules/data/data.core.js +5 -1
- package/src/modules/data/data.history.js +489 -489
- package/src/modules/data/data.js +76 -10
- package/src/modules/data/data.routes.js +3 -20
- package/src/modules/user.js +19 -0
- package/src/modules/workflow.js +1808 -1817
- package/client/public/demo/26899917-d1ba-4df4-bb33-48d09a8778da.jpg +0 -0
- package/client/public/demo/4b9894c7-12cd-466d-8fd3-a2757b09ec96.jpg +0 -0
- package/client/public/demo/7ed369ac-a1a2-4b45-b0cd-4fd5bcf5a75a.jpg +0 -0
package/src/modules/data/data.js
CHANGED
|
@@ -43,7 +43,7 @@ import {
|
|
|
43
43
|
} from "../mongodb.js";
|
|
44
44
|
import {dbUrl, MongoClient, MongoDatabase} from "../../engine.js";
|
|
45
45
|
import path from "node:path";
|
|
46
|
-
import {getObjectHash, getRandom, isGUID, isPlainObject, randomDate} from "../../core.js";
|
|
46
|
+
import {getObjectHash, getRandom, isGUID, isPlainObject, randomDate, sequential} from "../../core.js";
|
|
47
47
|
import {Event} from "../../events.js";
|
|
48
48
|
import fs from "node:fs";
|
|
49
49
|
import schedule from "node-schedule";
|
|
@@ -52,7 +52,7 @@ import i18n from "../../i18n.js";
|
|
|
52
52
|
import {
|
|
53
53
|
executeSafeJavascript,
|
|
54
54
|
runScheduledJobWithDbLock,
|
|
55
|
-
scheduleWorkflowTriggers,
|
|
55
|
+
scheduleWorkflowTriggers, substituteVariables,
|
|
56
56
|
triggerWorkflows
|
|
57
57
|
} from "../workflow.js";
|
|
58
58
|
import NodeCache from "node-cache";
|
|
@@ -60,13 +60,14 @@ import AWS from 'aws-sdk';
|
|
|
60
60
|
import checkDiskSpace from "check-disk-space";
|
|
61
61
|
import {addFile, removeFile} from "../file.js";
|
|
62
62
|
import {downloadFromS3, getUserS3Config, listS3Backups, uploadToS3} from "../bucket.js";
|
|
63
|
-
import {calculateTotalUserStorageUsage, hasPermission, middlewareAuthenticator} from "../user.js";
|
|
63
|
+
import {calculateTotalUserStorageUsage, getSmtpConfig, hasPermission, middlewareAuthenticator} from "../user.js";
|
|
64
64
|
import {getAllPacks} from "../../packs.js";
|
|
65
65
|
import {Config} from "../../config.js";
|
|
66
66
|
import {profiles} from "../../../client/src/constants.js";
|
|
67
67
|
import {registerRoutes, sendSseToUser} from "./data.routes.js";
|
|
68
68
|
import {importJobs, modelsCache, mongoDBWhitelist, runCryptoWorkerTask, runImportExportWorker} from "./data.core.js";
|
|
69
69
|
import readXlsxFile from "read-excel-file/node";
|
|
70
|
+
import {sendEmail} from "../../email.js";
|
|
70
71
|
|
|
71
72
|
let engine;
|
|
72
73
|
let logger;
|
|
@@ -543,7 +544,7 @@ const validateField = (field) => {
|
|
|
543
544
|
break;
|
|
544
545
|
}
|
|
545
546
|
case 'number':
|
|
546
|
-
allowedFieldTest(['min', 'max', 'step', 'unit']);
|
|
547
|
+
allowedFieldTest(['min', 'max', 'step', 'unit', 'delay', 'gauge', 'percent']);
|
|
547
548
|
if (field.min !== undefined && typeof field.min !== 'number') {
|
|
548
549
|
throw new Error(i18n.t('api.validate.fieldNumber', "L'attribut '{{0}}' doit être un nombre.", ["min"]));
|
|
549
550
|
}
|
|
@@ -559,6 +560,15 @@ const validateField = (field) => {
|
|
|
559
560
|
if (field.unit !== undefined && typeof field.unit !== 'string') {
|
|
560
561
|
throw new Error(i18n.t('api.validate.fieldString', "Le champ '{{0}}' doit être une chaîne de caractères.", ["unit"]));
|
|
561
562
|
}
|
|
563
|
+
if (field.delay !== undefined && typeof field.delay !== 'boolean') {
|
|
564
|
+
throw new Error(i18n.t('api.validate.fieldBoolean', "Le champ '{{0}}' doit être un booléen.", ["unit"]));
|
|
565
|
+
}
|
|
566
|
+
if (field.gauge !== undefined && typeof field.gauge !== 'boolean') {
|
|
567
|
+
throw new Error(i18n.t('api.validate.fieldBoolean', "L'attribut '{{0}}' doit être un booléen.", ["gauge"]));
|
|
568
|
+
}
|
|
569
|
+
if (field.percent !== undefined && typeof field.percent !== 'boolean') {
|
|
570
|
+
throw new Error(i18n.t('api.validate.fieldBoolean', "L'attribut '{{0}}' doit être un booléen.", ["percent"]));
|
|
571
|
+
}
|
|
562
572
|
break;
|
|
563
573
|
case 'string':
|
|
564
574
|
case 'string_t':
|
|
@@ -780,7 +790,7 @@ function convertDataTypes(dataArray, modelFields, sourceType = 'csv') {
|
|
|
780
790
|
});
|
|
781
791
|
}
|
|
782
792
|
|
|
783
|
-
export
|
|
793
|
+
export const cancelAlerts = async (user) => {
|
|
784
794
|
|
|
785
795
|
const datasCollection = getCollection('datas'); // Alerts are in the global collection
|
|
786
796
|
|
|
@@ -840,15 +850,56 @@ async function runStatefulAlertJob(alertId) {
|
|
|
840
850
|
if (count > 0) {
|
|
841
851
|
logger.info(`[Scheduled Job] Condition met for alert ${alertDoc.name} (ID: ${alertId}). Sending notification and updating state.`);
|
|
842
852
|
|
|
853
|
+
let emailSent = false;
|
|
854
|
+
try {
|
|
855
|
+
const user = await engine.userProvider.findUserByUsername(alertDoc._user);
|
|
856
|
+
if (user && user.email) {
|
|
857
|
+
const smtpConfig = await getSmtpConfig(user);
|
|
858
|
+
if (alertDoc.sendEmail && smtpConfig) {
|
|
859
|
+
const userLang = user.lang || 'en';
|
|
860
|
+
let emailContent, msg;
|
|
861
|
+
if (alertDoc.message){
|
|
862
|
+
if (alertDoc.message[userLang])
|
|
863
|
+
msg= alertDoc.message[userLang];
|
|
864
|
+
else
|
|
865
|
+
msg = alertDoc.message[Object.keys(alertDoc.message)[0]];
|
|
866
|
+
emailContent = await substituteVariables(msg, { count, alert: alertDoc });
|
|
867
|
+
} else {
|
|
868
|
+
// Sinon, utiliser le message par défaut
|
|
869
|
+
emailContent = i18n.t('alert.email.content', `L'alerte '${alertDoc.name}' s'est déclenchée. ${count} élément(s) correspondent à votre condition.`, { name: alertDoc.name, count: count });
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
await sendEmail(
|
|
873
|
+
user.email,
|
|
874
|
+
{
|
|
875
|
+
title: i18n.t('alert.email.title', `Alerte: ${alertDoc.name}`),
|
|
876
|
+
content: emailContent
|
|
877
|
+
},
|
|
878
|
+
smtpConfig,
|
|
879
|
+
userLang
|
|
880
|
+
);
|
|
881
|
+
emailSent = true;
|
|
882
|
+
logger.info(`[Scheduled Job] Email notification sent for alert ${alertId} to ${user.email}.`);
|
|
883
|
+
} else if (alertDoc.sendEmail) {
|
|
884
|
+
logger.warn(`[Scheduled Job] Could not send email for alert ${alertId}. SMTP config is missing or incomplete for user ${user.username}.`);
|
|
885
|
+
}
|
|
886
|
+
} else {
|
|
887
|
+
logger.warn(`[Scheduled Job] Could not send email for alert ${alertId}. User ${alertDoc._user} not found or has no email address.`);
|
|
888
|
+
}
|
|
889
|
+
} catch (emailError) {
|
|
890
|
+
logger.error(`[Scheduled Job] Failed to send email for alert ${alertId}:`, emailError);
|
|
891
|
+
}
|
|
892
|
+
|
|
843
893
|
// Send notification
|
|
844
894
|
const alertPayload = {
|
|
845
895
|
type: 'cron_alert',
|
|
846
896
|
triggerId: alertDoc._id.toString(),
|
|
847
897
|
triggerName: alertDoc.name,
|
|
848
898
|
timestamp: new Date().toISOString(),
|
|
849
|
-
message: `Alerte '${alertDoc.name}': ${count} élément(s) correspondent à votre condition
|
|
899
|
+
message: `Alerte '${alertDoc.name}': ${count} élément(s) correspondent à votre condition.`,
|
|
900
|
+
emailSent
|
|
850
901
|
};
|
|
851
|
-
|
|
902
|
+
sendSseToUser(alertDoc._user, alertPayload);
|
|
852
903
|
|
|
853
904
|
// Update state in DB to prevent re-notification
|
|
854
905
|
await datasCollection.updateOne(
|
|
@@ -1279,7 +1330,7 @@ export async function onInit(defaultEngine) {
|
|
|
1279
1330
|
|
|
1280
1331
|
const objectKeys = Object.keys(modelStructure);
|
|
1281
1332
|
|
|
1282
|
-
if( objectKeys.find(o => !["name", "_user", "history", "locked", "_id", "description", "maxRequestData", "fields"].includes(o)) ){
|
|
1333
|
+
if( objectKeys.find(o => !["name", "_user", "icon", "history", "locked", "_id", "description", "maxRequestData", "fields"].includes(o)) ){
|
|
1283
1334
|
throw new Error(i18n.t('api.model.invalidStructure'));
|
|
1284
1335
|
}
|
|
1285
1336
|
|
|
@@ -4319,8 +4370,14 @@ export async function installPack(packIdentifier, user = null, lang = 'en', isTe
|
|
|
4319
4370
|
|
|
4320
4371
|
// Determine if we're working with an ID or direct pack object
|
|
4321
4372
|
if (typeof packIdentifier === 'string') {
|
|
4373
|
+
let p;
|
|
4374
|
+
try {
|
|
4375
|
+
p = new ObjectId(packIdentifier);
|
|
4376
|
+
} catch (e) {
|
|
4377
|
+
p = packIdentifier;
|
|
4378
|
+
}
|
|
4322
4379
|
// Existing behavior - fetch from database
|
|
4323
|
-
pack = await packsCollection.findOne({ _id:
|
|
4380
|
+
pack = await packsCollection.findOne({ $or:[{ _id: p}, { name: packIdentifier }] });
|
|
4324
4381
|
if (!pack) {
|
|
4325
4382
|
throw new Error(`Pack with ID ${packIdentifier} not found.`);
|
|
4326
4383
|
}
|
|
@@ -4359,6 +4416,8 @@ export async function installPack(packIdentifier, user = null, lang = 'en', isTe
|
|
|
4359
4416
|
? await modelsCollection.find({ _user: username }).toArray()
|
|
4360
4417
|
: await modelsCollection.find({ _user: { $exists: false } }).toArray();
|
|
4361
4418
|
|
|
4419
|
+
console.log("EXISTING", existingModels);
|
|
4420
|
+
|
|
4362
4421
|
const existingModelNames = existingModels.map(m => m.name);
|
|
4363
4422
|
|
|
4364
4423
|
for (const modelOrName of pack.models) {
|
|
@@ -4583,7 +4642,8 @@ export const installAllPacks = async () => {
|
|
|
4583
4642
|
export async function handleDemoInitialization(req, res) {
|
|
4584
4643
|
const user = req.me;
|
|
4585
4644
|
const body = req.fields;
|
|
4586
|
-
const
|
|
4645
|
+
const packs = body.packs;
|
|
4646
|
+
const models = (Object.keys(profiles).includes(body.profile) && profiles[body.profile].models) || '';
|
|
4587
4647
|
if (!isDemoUser(user)) {
|
|
4588
4648
|
return res.status(403).json({ success: false, error: "This action is only for demo users." });
|
|
4589
4649
|
}
|
|
@@ -4617,10 +4677,16 @@ export async function handleDemoInitialization(req, res) {
|
|
|
4617
4677
|
|
|
4618
4678
|
logger.info(`[Demo Init] Installing dynamically generated pack with models: [${models.join(', ')}].`);
|
|
4619
4679
|
|
|
4680
|
+
await sequential(packs.map(p => {
|
|
4681
|
+
return () => installPack(p, user, req.query.lang || 'en');
|
|
4682
|
+
}));
|
|
4683
|
+
|
|
4620
4684
|
// Create and install pack
|
|
4621
4685
|
const result = await installPack(packToInstall, user, req.query.lang || 'en');
|
|
4622
4686
|
|
|
4623
4687
|
if (result.success || result.modifiedCount > 0) {
|
|
4688
|
+
|
|
4689
|
+
await Event.Trigger('OnDemoUserAdded', "event", "system", req.me.username);
|
|
4624
4690
|
logger.info(`[Demo Init] Pack installed successfully for user '${user.username}'.`);
|
|
4625
4691
|
res.status(200).json({ success: true, message: "Demo environment initialized successfully.", summary: result.summary });
|
|
4626
4692
|
} else {
|
|
@@ -858,25 +858,7 @@ export async function registerRoutes(engine){
|
|
|
858
858
|
return Promise.reject();
|
|
859
859
|
})));
|
|
860
860
|
}
|
|
861
|
-
|
|
862
|
-
if( install && (isDemoUser(req.me) && Config.Get("useDemoAccounts")) ){
|
|
863
|
-
|
|
864
|
-
await datasCollection.deleteMany({ _user: req.me.username});
|
|
865
|
-
await modelsCollection.deleteMany({ _user: req.me.username});
|
|
866
|
-
const files = await filesCollection.find({ mainUser: req.me.username}).toArray();
|
|
867
|
-
try {
|
|
868
|
-
files.forEach(file =>removeFile(file.guid, req.me));
|
|
869
|
-
} catch (e) {
|
|
870
|
-
|
|
871
|
-
}
|
|
872
|
-
|
|
873
|
-
await cancelAlerts(req.me);
|
|
874
|
-
|
|
875
|
-
await getPromise();
|
|
876
|
-
await Event.Trigger('jobAddUserData', req.me.username);
|
|
877
|
-
}else{
|
|
878
|
-
await getPromise();
|
|
879
|
-
}
|
|
861
|
+
await getPromise();
|
|
880
862
|
|
|
881
863
|
if( ids.length > 0 )
|
|
882
864
|
res.status(201).json({ success: true, imported: ids }); // 201 Created
|
|
@@ -1018,7 +1000,8 @@ export async function registerRoutes(engine){
|
|
|
1018
1000
|
|
|
1019
1001
|
// 1. Récupérer la définition du KPI
|
|
1020
1002
|
try {
|
|
1021
|
-
|
|
1003
|
+
const coll = await getCollectionForUser(req.me);
|
|
1004
|
+
kpiDef = await coll.findOne({ _id: new ObjectId(id), _model: 'kpi', _user: req.me._user || req.me.username });
|
|
1022
1005
|
if (!kpiDef) {
|
|
1023
1006
|
return res.status(404).json({ success: false, error: 'KPI definition not found' });
|
|
1024
1007
|
}
|
package/src/modules/user.js
CHANGED
|
@@ -13,6 +13,7 @@ import {getAPILang, searchData} from "./data/index.js";
|
|
|
13
13
|
import {Logger} from "../gameObject.js";
|
|
14
14
|
import rateLimit from "express-rate-limit";
|
|
15
15
|
import ivm from "isolated-vm";
|
|
16
|
+
import {emailDefaultConfig} from "../constants.js";
|
|
16
17
|
|
|
17
18
|
export const userInitiator = async (req, res, next) => {
|
|
18
19
|
|
|
@@ -272,4 +273,22 @@ export async function getEnv(user){
|
|
|
272
273
|
return acc;
|
|
273
274
|
}, {});
|
|
274
275
|
return envObject;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
export async function getSmtpConfig(user) {
|
|
279
|
+
|
|
280
|
+
// 1. Récupérer la configuration SMTP depuis le modèle 'env' de l'utilisateur
|
|
281
|
+
const envVars = await searchData({
|
|
282
|
+
model: 'env',
|
|
283
|
+
filter: { $in: ['$name', ['SMTP_HOST', 'SMTP_PORT', 'SMTP_USER', 'SMTP_PASS', 'SMTP_FROM']] }
|
|
284
|
+
}, user);
|
|
285
|
+
|
|
286
|
+
const smtpConfig = envVars.data.reduce((acc, variable) => {
|
|
287
|
+
acc[variable.name.replace('SMTP_', '').toLowerCase()] = variable.value;
|
|
288
|
+
return acc;
|
|
289
|
+
}, {});
|
|
290
|
+
if( !smtpConfig.port )
|
|
291
|
+
smtpConfig.port = emailDefaultConfig.port;
|
|
292
|
+
|
|
293
|
+
return smtpConfig;
|
|
275
294
|
}
|