data-primals-engine 1.4.3 → 1.5.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 (50) hide show
  1. package/README.md +878 -867
  2. package/client/package-lock.json +49 -0
  3. package/client/package.json +1 -0
  4. package/client/src/App.jsx +1 -1
  5. package/client/src/App.scss +13 -3
  6. package/client/src/AssistantChat.scss +3 -2
  7. package/client/src/DashboardView.jsx +569 -569
  8. package/client/src/DataEditor.jsx +2 -2
  9. package/client/src/DataLayout.jsx +1 -8
  10. package/client/src/DataTable.jsx +26 -4
  11. package/client/src/Field.jsx +1825 -1788
  12. package/client/src/FlexDataRenderer.jsx +2 -0
  13. package/client/src/FlexTreeUtils.js +1 -1
  14. package/client/src/KPIDialog.jsx +11 -1
  15. package/client/src/ModelCreator.jsx +1 -2
  16. package/client/src/ModelCreatorField.jsx +23 -27
  17. package/client/src/ModelList.jsx +1 -1
  18. package/client/src/constants.js +1 -1
  19. package/client/src/hooks/useTutorials.jsx +62 -65
  20. package/client/src/translations.js +2 -0
  21. package/package.json +2 -1
  22. package/perf/README.md +147 -0
  23. package/perf/artillery-hooks.js +37 -0
  24. package/perf/perf-shot-hardwork.yml +84 -0
  25. package/perf/perf-shot-search.yml +45 -0
  26. package/perf/setup.yml +26 -0
  27. package/server.js +1 -1
  28. package/src/constants.js +2 -27
  29. package/src/core.js +15 -1
  30. package/src/data.js +1 -1
  31. package/src/defaultModels.js +1544 -1540
  32. package/src/email.js +5 -2
  33. package/src/engine.js +5 -3
  34. package/src/filter.js +5 -3
  35. package/src/modules/assistant/assistant.js +3 -1
  36. package/src/modules/bucket.js +12 -15
  37. package/src/modules/data/data.backup.js +11 -8
  38. package/src/modules/data/data.js +6 -3
  39. package/src/modules/data/data.operations.js +3231 -2999
  40. package/src/modules/data/data.routes.js +1821 -1785
  41. package/src/modules/data/data.scheduling.js +2 -1
  42. package/src/modules/data/data.validation.js +4 -1
  43. package/src/modules/file.js +4 -2
  44. package/src/modules/user.js +4 -1
  45. package/src/modules/workflow.js +9 -10
  46. package/src/openai.jobs.js +2 -0
  47. package/src/packs.js +22 -5
  48. package/src/providers.js +22 -7
  49. package/test/data.integration.test.js +1060 -981
  50. package/test/import_export.integration.test.js +1 -1
@@ -10,6 +10,7 @@ import {sendSseToUser} from "./data.routes.js";
10
10
 
11
11
  import {searchData} from "./data.operations.js";
12
12
  import {Logger} from "../../gameObject.js";
13
+ import {Config} from "../../config.js";
13
14
 
14
15
 
15
16
  let engine, logger;
@@ -179,7 +180,7 @@ export async function scheduleAlerts() {
179
180
  // 4. Project (Slice): Pour chaque utilisateur, ne garder que les X premières alertes du tableau trié.
180
181
  {
181
182
  $project: {
182
- oldestAlerts: {$slice: ["$alerts", maxAlertsPerUser]}
183
+ oldestAlerts: {$slice: ["$alerts", Config.Get('maxAlertsPerUser', maxAlertsPerUser)]}
183
184
  }
184
185
  },
185
186
  // 5. Unwind: Déconstruire le tableau 'oldestAlerts' pour obtenir un flux de documents, un par alerte.
@@ -114,7 +114,10 @@ export const validateField = (field) => {
114
114
  break;
115
115
  case 'model':
116
116
  case 'modelField':
117
- allowedFieldTest([]);
117
+ allowedFieldTest(['targetModel']);
118
+ if (field.targetModel !== undefined && typeof field.targetModel !== 'string') {
119
+ throw new Error(i18n.t('api.validate.fieldString', "Le champ '{{0}}' doit être une chaîne de caractères.", ["targetModel"]));
120
+ }
118
121
  break;
119
122
  case 'object':
120
123
  allowedFieldTest([]);
@@ -15,6 +15,7 @@ import {promisify} from "node:util";
15
15
  import {calculateTotalUserStorageUsage, hasPermission} from "./user.js";
16
16
  import {Logger} from "../gameObject.js";
17
17
  import {deleteFromS3, getUserS3Config, uploadToS3} from "./bucket.js";
18
+ import {Config} from "../config.js";
18
19
 
19
20
  const pbkdf2Async = promisify(crypto.pbkdf2);
20
21
 
@@ -45,8 +46,9 @@ export const zip = async (filename) => {
45
46
  export const addFile = async (file, user) => {
46
47
  if (!file) throw new Error("Le fichier est requis");
47
48
 
48
- if (file.size > maxPrivateFileSize) {
49
- throw new Error(`La taille du fichier dépasse la limite autorisée (${maxPrivateFileSize / megabytes} Mo).`);
49
+ const m = Config.Get('maxPrivateFileSize', maxPrivateFileSize);
50
+ if (file.size > m) {
51
+ throw new Error(`La taille du fichier dépasse la limite autorisée (${m / megabytes} Mo).`);
50
52
  }
51
53
 
52
54
  if (user.username !== 'demo' && isLocalUser(user) && !await hasPermission(["API_ADMIN", "API_UPLOAD_FILE"], user)) {
@@ -15,6 +15,7 @@ import rateLimit from "express-rate-limit";
15
15
  import ivm from "isolated-vm";
16
16
  import {emailDefaultConfig} from "../constants.js";
17
17
  import {safeAssignObject} from "../core.js";
18
+ import {Config} from "../config.js";
18
19
 
19
20
  export const userInitiator = async (req, res, next) => {
20
21
 
@@ -278,6 +279,8 @@ export async function getEnv(user){
278
279
 
279
280
  export async function getSmtpConfig(user) {
280
281
 
282
+ const cfg = Config.Get('emailDefaultConfig', emailDefaultConfig);
283
+
281
284
  // 1. Récupérer la configuration SMTP depuis le modèle 'env' de l'utilisateur
282
285
  const envVars = await searchData({
283
286
  model: 'env',
@@ -289,7 +292,7 @@ export async function getSmtpConfig(user) {
289
292
  return acc;
290
293
  }, {});
291
294
  if( !smtpConfig.port )
292
- smtpConfig.port = emailDefaultConfig.port;
295
+ smtpConfig.port = cfg.port;
293
296
 
294
297
  return smtpConfig;
295
298
  }
@@ -6,12 +6,9 @@ import crypto from "node:crypto";
6
6
  import ivm from 'isolated-vm';
7
7
 
8
8
  import {Logger} from "../gameObject.js";
9
- import {deleteData, getModel, insertData, patchData, scheduleAlerts, searchData} from "./data/index.js";
10
- import {emailDefaultConfig, maxExecutionsByStep, maxWorkflowSteps, port} from "../constants.js";
11
- import {ChatOpenAI} from "@langchain/openai";
12
- import {ChatGoogleGenerativeAI} from "@langchain/google-genai";
9
+ import {deleteData, getModel, insertData, patchData, searchData} from "./data/index.js";
10
+ import { maxExecutionsByStep, maxWorkflowSteps, port} from "../constants.js";
13
11
  import {ChatPromptTemplate} from "@langchain/core/prompts";
14
- import { ChatDeepSeek } from "@langchain/deepseek";
15
12
  import i18n from "../../src/i18n.js";
16
13
  import {sendEmail} from "../email.js";
17
14
 
@@ -21,9 +18,9 @@ import { services } from '../services/index.js';
21
18
  import {getEnv, getSmtpConfig} from "./user.js";
22
19
  import {getHost} from "../constants.js";
23
20
  import {providers} from "./assistant/constants.js";
24
- import {ChatAnthropic} from "@langchain/anthropic";
25
21
  import {getAIProvider} from "./assistant/assistant.js";
26
- import {escapeRegex, safeAssignObject} from "../core.js";
22
+ import { safeAssignObject} from "../core.js";
23
+ import {Config} from "../config.js";
27
24
 
28
25
  let logger = null;
29
26
  export async function onInit(defaultEngine) {
@@ -1449,14 +1446,16 @@ export async function processWorkflowRun(workflowRunId, user) {
1449
1446
  }
1450
1447
 
1451
1448
  let stepCount = 0;
1449
+ const mw = Config.Get('maxWorkflowSteps', maxWorkflowSteps);
1452
1450
  while (currentStepId) {
1453
- if (stepCount++ >= maxWorkflowSteps) {
1451
+ if (stepCount++ >= mw) {
1454
1452
  return await logError(`Maximum workflow step executions exceeded (${maxWorkflowSteps} max).`);
1455
1453
  }
1456
1454
 
1457
1455
  const execCount = (stepExecutionsCount[currentStepId] || 0) + 1;
1458
- if (execCount > maxExecutionsByStep) {
1459
- return await logError(`Maximum executions (${maxExecutionsByStep}) exceeded for step ${currentStepId}.`);
1456
+ const m = Config.Get('maxExecutionsByStep', maxExecutionsByStep);
1457
+ if (execCount > m) {
1458
+ return await logError(`Maximum executions (${m}) exceeded for step ${currentStepId}.`);
1460
1459
  }
1461
1460
  stepExecutionsCount[currentStepId] = execCount;
1462
1461
  logger.info(`[processWorkflowRun] Run ID: ${runId}, Current Step ID: ${currentStepId}`);
@@ -28,6 +28,7 @@ export const openaiJobModel = async (lang, txt, history, existingModels = []) =>
28
28
  {
29
29
  "models": [{
30
30
  "name": "book",
31
+ "icon": "FaBook",
31
32
  "description": "*description détaillée en plusieurs phrases de l'utilité du modèle, et ses cas d'usage, ici : Modèle de référencement de livres multi-support. Peut être utilisé pour des bibliothèques personnelles, virtuelles ou municipales.*",
32
33
  "fields": [
33
34
  {"name": "title", "type": "string", "required": true, "asMain": true, "hint": "Titre du livre", color: '#FF89CC'},
@@ -40,6 +41,7 @@ export const openaiJobModel = async (lang, txt, history, existingModels = []) =>
40
41
  {
41
42
  name: "library",
42
43
  "description": "",
44
+ "icon": "FaBookOpenReader",
43
45
  fields: [
44
46
  {
45
47
  "name": "name",
package/src/packs.js CHANGED
@@ -666,6 +666,14 @@ This pack doesn't provide a full-fledged website out of the box. Instead, it lay
666
666
  "models": ["content", "webpage", "translation", "message", "channel", "taxonomy", "lang", "user", "role", "permission", "kpi", "workflow", "workflowStep", "workflowAction", "workflowTrigger"],
667
667
  "data": {
668
668
  "all": {
669
+ "kpi": [
670
+ {
671
+ "name": "kpi.registered_users",
672
+ "targetModel": "user",
673
+ "aggregationType": "count",
674
+ "icon": "FaUsers"
675
+ }
676
+ ],
669
677
  "taxonomy": [{
670
678
  name: 'Website',
671
679
  description: 'Website main category',
@@ -1439,13 +1447,13 @@ The magic happens automatically in the background:
1439
1447
  "workflowStep": [
1440
1448
  {
1441
1449
  "name": "Generate SEO description for products",
1442
- "workflow": {"$find": {"name": "Generate product description"}},
1450
+ "workflow": {"$link": {"name": "Generate product description", "_model": "workflow"}},
1443
1451
  "actions": {
1444
- "$find": {
1452
+ "$link": {
1445
1453
  $or: [
1446
1454
  {"$eq": ["$name", "Generate SEO Description from Product (OpenAI API)"]},
1447
1455
  {"$eq": ["$name", "Update Product with AI Description"]}
1448
- ]
1456
+ ], "_model": "workflowAction"
1449
1457
  }
1450
1458
  },
1451
1459
  "isTerminal": true
@@ -1453,7 +1461,7 @@ The magic happens automatically in the background:
1453
1461
  ],
1454
1462
  "workflowTrigger": [{
1455
1463
  "name": "On new product added",
1456
- "workflow": {"$find": {"$eq": ["$name", "Generate product description"]}},
1464
+ "workflow": {"$link": {"$eq": ["$name", "Generate product description"], "_model": "workflow"}},
1457
1465
  "type": "manual",
1458
1466
  "onEvent": "DataAdded",
1459
1467
  "targetModel": "product",
@@ -1489,9 +1497,18 @@ This pack provides the raw data and structure for internationalization (i18n). I
1489
1497
  b. Add entries in the \`translation\` model for this key for each language you support (e.g., key: "WELCOME_MESSAGE", lang: "en", value: "Welcome!").
1490
1498
  c. Your application's front-end can then fetch the correct translation based on the user's selected language.`,
1491
1499
  "tags": ["i18n"],
1492
- "models": ["translation", "lang"],
1500
+ "models": ["translation", "lang", "kpi", "dashboard"],
1493
1501
  "data": {
1494
1502
  "all": {
1503
+ "kpi": [
1504
+ {
1505
+ "name": "kpi.completed_translations",
1506
+ "targetModel": "translation",
1507
+ "aggregationType": "count",
1508
+ "icon": "FaLanguage",
1509
+ "color": "#3498db"
1510
+ }
1511
+ ],
1495
1512
  "lang": [
1496
1513
  {
1497
1514
  "name": "Français",
package/src/providers.js CHANGED
@@ -2,6 +2,7 @@ import {compare, hash} from "bcrypt";
2
2
  import {maxTotalPrivateFilesSize} from "./constants.js";
3
3
  import {getCollection, ObjectId} from "./modules/mongodb.js";
4
4
  import {Logger} from "./gameObject.js";
5
+ import {Config} from "./config.js";
5
6
 
6
7
  /**
7
8
  * @class UserProvider
@@ -73,7 +74,7 @@ export class UserProvider {
73
74
  * @returns {Promise<number>}
74
75
  */
75
76
  async getUserStorageLimit(user) {
76
- return maxTotalPrivateFilesSize;
77
+ return Config.Get('maxTotalPrivateFilesSize', maxTotalPrivateFilesSize);
77
78
  }
78
79
 
79
80
  /**
@@ -112,23 +113,23 @@ export class UserProvider {
112
113
 
113
114
  export class DefaultUserProvider extends UserProvider {
114
115
 
115
- users= [{ username: "demo", password: "demo" }];
116
+ users= [{ username: "demo", password: "demo", temporary: true }];
116
117
 
117
118
  /**
118
119
  * Trouve un utilisateur. Gère spécifiquement les utilisateurs de démo.
119
120
  */
120
121
  async findUserByUsername(username) {
121
122
  // Si le nom d'utilisateur commence par "demo", on le considère comme un utilisateur de démo valide et volatile.
122
- if (typeof username === 'string' && username.startsWith('demo')) {
123
+ if (this.isDemoUser(username)){
123
124
  // On retourne un objet utilisateur de démo avec la structure attendue.
124
- return { username: username, password: "demo", userPlan: 'free' }; // Ajout de userPlan pour la cohérence
125
+ return { username: username, password: "demo", userPlan: 'free', temporary: true }; // Ajout de userPlan pour la cohérence
125
126
  }
126
127
  // Logique pour les vrais utilisateurs (si vous en ajoutez plus tard)
127
128
  return this.users.find(user => user.username === username);
128
129
  }
129
130
 
130
131
  async getUserStorageLimit(user) {
131
- return maxTotalPrivateFilesSize;
132
+ return Config.Get('maxTotalPrivateFilesSize', maxTotalPrivateFilesSize);
132
133
  }
133
134
 
134
135
  async getBackupFrequency(user){
@@ -139,9 +140,13 @@ export class DefaultUserProvider extends UserProvider {
139
140
  return []
140
141
  }
141
142
 
143
+ isDemoUser(user) {
144
+ return (typeof (user) === 'string' && (/^demo[0-9]{0,2}$/.test(user) || /^perf[0-9]{1,8}$/.test(user)))
145
+ }
146
+
142
147
  async validatePassword(user, password) {
143
148
  // Pour un utilisateur de démo, le mot de passe est toujours valide.
144
- if (user.username.startsWith('demo')) {
149
+ if (this.isDemoUser(user)){
145
150
  return true;
146
151
  }
147
152
  // Logique pour les vrais utilisateurs
@@ -166,7 +171,7 @@ export class DefaultUserProvider extends UserProvider {
166
171
 
167
172
  // Priorité 2: PasF de session, mais on vérifie la présence d'un cookie "username" pour la démo.
168
173
  const demoUsername = req.cookies?.username;
169
- if (demoUsername && typeof demoUsername === 'string' && demoUsername.startsWith('demo')) {
174
+ if (demoUsername && typeof demoUsername === 'string' && this.isDemoUser(demoUsername)) {
170
175
  // On a trouvé un cookie de démo. On crée l'objet utilisateur correspondant.
171
176
  const demoUser = await this.findUserByUsername(demoUsername);
172
177
  if (demoUser) {
@@ -174,6 +179,16 @@ export class DefaultUserProvider extends UserProvider {
174
179
  return;
175
180
  }
176
181
  }
182
+
183
+ // Priorité 2: PasF de session, mais on vérifie la présence d'un cookie "username" pour la démo.
184
+ const user = req.query._user;
185
+ if (user && typeof user === 'string' && this.isDemoUser(user)) {
186
+ const demoUser = await this.findUserByUsername(user);
187
+ if (demoUser) {
188
+ req.me = demoUser;
189
+ return;
190
+ }
191
+ }
177
192
  }
178
193
 
179
194
  getUserPlans(){