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.
Files changed (36) hide show
  1. package/README.md +797 -782
  2. package/client/README.md +20 -0
  3. package/client/package-lock.json +717 -151
  4. package/client/package.json +37 -36
  5. package/client/src/App.jsx +9 -10
  6. package/client/src/App.scss +42 -1
  7. package/client/src/Dashboard.jsx +349 -208
  8. package/client/src/DashboardView.jsx +569 -547
  9. package/client/src/DataEditor.jsx +20 -2
  10. package/client/src/DataLayout.jsx +54 -13
  11. package/client/src/DataTable.jsx +807 -760
  12. package/client/src/DataTable.scss +187 -90
  13. package/client/src/Dialog.scss +0 -3
  14. package/client/src/Field.jsx +1783 -1583
  15. package/client/src/ModelCreator.jsx +25 -3
  16. package/client/src/ModelCreatorField.jsx +906 -804
  17. package/client/src/ModelList.jsx +20 -2
  18. package/client/src/constants.js +16 -4
  19. package/client/src/contexts/UIContext.jsx +19 -10
  20. package/client/src/translations.js +265 -3
  21. package/package.json +4 -3
  22. package/server.js +1 -0
  23. package/src/core.js +18 -0
  24. package/src/defaultModels.js +70 -10
  25. package/src/email.js +2 -1
  26. package/src/filter.js +260 -256
  27. package/src/i18n.js +503 -30
  28. package/src/modules/data/data.core.js +5 -1
  29. package/src/modules/data/data.history.js +489 -489
  30. package/src/modules/data/data.js +76 -10
  31. package/src/modules/data/data.routes.js +3 -20
  32. package/src/modules/user.js +19 -0
  33. package/src/modules/workflow.js +1808 -1817
  34. package/client/public/demo/26899917-d1ba-4df4-bb33-48d09a8778da.jpg +0 -0
  35. package/client/public/demo/4b9894c7-12cd-466d-8fd3-a2757b09ec96.jpg +0 -0
  36. package/client/public/demo/7ed369ac-a1a2-4b45-b0cd-4fd5bcf5a75a.jpg +0 -0
@@ -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 const cancelAlerts = async (user) => {
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
- await sendSseToUser(alertDoc._user, alertPayload);
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: new ObjectId(packIdentifier) });
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 models = (Object.keys(profiles).includes(body.profile) && profiles[body.profile]) || '';
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
- const install = !!req.fields.install;
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
- kpiDef = await datasCollection.findOne({ _id: new ObjectId(id), _model: 'kpi', _user: req.me._user || req.me.username });
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
  }
@@ -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
  }