data-primals-engine 1.3.4 → 1.4.1

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 (42) hide show
  1. package/README.md +72 -2
  2. package/client/README.md +20 -0
  3. package/client/package-lock.json +712 -147
  4. package/client/package.json +38 -37
  5. package/client/src/App.jsx +9 -10
  6. package/client/src/App.scss +7 -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 +24 -12
  11. package/client/src/DataTable.jsx +807 -760
  12. package/client/src/DataTable.scss +187 -90
  13. package/client/src/Field.jsx +1783 -1656
  14. package/client/src/ModelCreator.jsx +2 -2
  15. package/client/src/ModelCreatorField.jsx +906 -804
  16. package/client/src/ModelList.jsx +2 -2
  17. package/client/src/PackGallery.jsx +391 -290
  18. package/client/src/PackGallery.scss +231 -210
  19. package/client/src/constants.js +16 -4
  20. package/client/src/translations.js +16750 -16392
  21. package/package.json +14 -11
  22. package/src/core.js +9 -0
  23. package/src/defaultModels.js +18 -10
  24. package/src/email.js +2 -1
  25. package/src/gameObject.js +1 -1
  26. package/src/i18n.js +501 -28
  27. package/src/modules/auth-google/index.js +51 -0
  28. package/src/modules/auth-microsoft/index.js +82 -0
  29. package/src/modules/auth-saml/index.js +90 -0
  30. package/src/modules/data/data.core.js +5 -1
  31. package/src/modules/data/data.history.js +489 -489
  32. package/src/modules/data/data.js +86 -12
  33. package/src/modules/data/data.routes.js +1691 -1663
  34. package/src/modules/user.js +19 -0
  35. package/src/modules/workflow.js +2 -12
  36. package/src/providers.js +110 -1
  37. package/src/sso.js +194 -0
  38. package/test/data.integration.test.js +3 -0
  39. package/test/user.test.js +1 -1
  40. package/client/public/demo/26899917-d1ba-4df4-bb33-48d09a8778da.jpg +0 -0
  41. package/client/public/demo/4b9894c7-12cd-466d-8fd3-a2757b09ec96.jpg +0 -0
  42. 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(
@@ -4313,14 +4364,20 @@ async function manageBackupRotation(user, backupFrequency, s3Config = null) { //
4313
4364
  * @param {string} [lang='en'] - Language code for localized data
4314
4365
  * @returns {Promise<{success: boolean, summary: object, errors: Array, modifiedCount: number}>}
4315
4366
  */
4316
- export async function installPack(packIdentifier, user = null, lang = 'en', isTemporary= false) {
4367
+ export async function installPack(packIdentifier, user = null, lang = 'en', options = {}) {
4317
4368
  let pack;
4318
4369
  const packsCollection = getCollection('packs');
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({ $and: [{ _user: {$exists: false} }, {private: false}, {$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) {
@@ -4406,7 +4465,7 @@ export async function installPack(packIdentifier, user = null, lang = 'en', isTe
4406
4465
  const dataToInstall = { ...pack.data?.all, ...pack.data?.[lang] };
4407
4466
  if (!dataToInstall || Object.keys(dataToInstall).length === 0) {
4408
4467
  logger.warn(`Pack '${pack.name}' has no data to install.`);
4409
- return { success: errors.length === 0, summary, errors, modifiedCount: 0 };
4468
+ return { success: false, summary, errors, modifiedCount: 0 };
4410
4469
  }
4411
4470
 
4412
4471
  // Process link references (same as original)
@@ -4559,6 +4618,14 @@ export async function installPack(packIdentifier, user = null, lang = 'en', isTe
4559
4618
  }
4560
4619
  }
4561
4620
 
4621
+ if( options.installForUser && user?.username ){
4622
+ if( pack.name )
4623
+ await packsCollection.deleteOne({ name: pack.name, _user: user.username });
4624
+ logger.info(`--- Creating pack '${pack.name}' for user... ---`);
4625
+ const packToCreate = {...pack, _id: undefined, private: true, _user: user.username };
4626
+ await packsCollection.insertOne(packToCreate);
4627
+ }
4628
+
4562
4629
  // Trigger event only if pack came from database (original behavior)
4563
4630
  if (typeof packIdentifier === 'string') {
4564
4631
  await Event.Trigger("OnPackInstalled", "event", "system", pack);
@@ -4577,13 +4644,14 @@ export async function installPack(packIdentifier, user = null, lang = 'en', isTe
4577
4644
  export const installAllPacks = async () => {
4578
4645
  const packs = await getAllPacks();
4579
4646
  await packsCollection.deleteMany({ _user: { $exists : false }});
4580
- await packsCollection.insertMany(packs);
4647
+ await packsCollection.insertMany(packs.map(p =>({...p, private: false })));
4581
4648
  }
4582
4649
 
4583
4650
  export async function handleDemoInitialization(req, res) {
4584
4651
  const user = req.me;
4585
4652
  const body = req.fields;
4586
- const models = (Object.keys(profiles).includes(body.profile) && profiles[body.profile]) || '';
4653
+ const packs = body.packs;
4654
+ const models = (Object.keys(profiles).includes(body.profile) && profiles[body.profile].models) || '';
4587
4655
  if (!isDemoUser(user)) {
4588
4656
  return res.status(403).json({ success: false, error: "This action is only for demo users." });
4589
4657
  }
@@ -4617,10 +4685,16 @@ export async function handleDemoInitialization(req, res) {
4617
4685
 
4618
4686
  logger.info(`[Demo Init] Installing dynamically generated pack with models: [${models.join(', ')}].`);
4619
4687
 
4688
+ await sequential(packs.map(p => {
4689
+ return () => installPack(p, user, req.query.lang || 'en');
4690
+ }));
4691
+
4620
4692
  // Create and install pack
4621
4693
  const result = await installPack(packToInstall, user, req.query.lang || 'en');
4622
4694
 
4623
4695
  if (result.success || result.modifiedCount > 0) {
4696
+
4697
+ await Event.Trigger('OnDemoUserAdded', "event", "system", req.me.username);
4624
4698
  logger.info(`[Demo Init] Pack installed successfully for user '${user.username}'.`);
4625
4699
  res.status(200).json({ success: true, message: "Demo environment initialized successfully.", summary: result.summary });
4626
4700
  } else {