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.
- package/README.md +878 -867
- package/client/package-lock.json +49 -0
- package/client/package.json +1 -0
- package/client/src/App.jsx +1 -1
- package/client/src/App.scss +13 -3
- package/client/src/AssistantChat.scss +3 -2
- package/client/src/DashboardView.jsx +569 -569
- package/client/src/DataEditor.jsx +2 -2
- package/client/src/DataLayout.jsx +1 -8
- package/client/src/DataTable.jsx +26 -4
- package/client/src/Field.jsx +1825 -1788
- package/client/src/FlexDataRenderer.jsx +2 -0
- package/client/src/FlexTreeUtils.js +1 -1
- package/client/src/KPIDialog.jsx +11 -1
- package/client/src/ModelCreator.jsx +1 -2
- package/client/src/ModelCreatorField.jsx +23 -27
- package/client/src/ModelList.jsx +1 -1
- package/client/src/constants.js +1 -1
- package/client/src/hooks/useTutorials.jsx +62 -65
- package/client/src/translations.js +2 -0
- package/package.json +2 -1
- package/perf/README.md +147 -0
- package/perf/artillery-hooks.js +37 -0
- package/perf/perf-shot-hardwork.yml +84 -0
- package/perf/perf-shot-search.yml +45 -0
- package/perf/setup.yml +26 -0
- package/server.js +1 -1
- package/src/constants.js +2 -27
- package/src/core.js +15 -1
- package/src/data.js +1 -1
- package/src/defaultModels.js +1544 -1540
- package/src/email.js +5 -2
- package/src/engine.js +5 -3
- package/src/filter.js +5 -3
- package/src/modules/assistant/assistant.js +3 -1
- package/src/modules/bucket.js +12 -15
- package/src/modules/data/data.backup.js +11 -8
- package/src/modules/data/data.js +6 -3
- package/src/modules/data/data.operations.js +3231 -2999
- package/src/modules/data/data.routes.js +1821 -1785
- package/src/modules/data/data.scheduling.js +2 -1
- package/src/modules/data/data.validation.js +4 -1
- package/src/modules/file.js +4 -2
- package/src/modules/user.js +4 -1
- package/src/modules/workflow.js +9 -10
- package/src/openai.jobs.js +2 -0
- package/src/packs.js +22 -5
- package/src/providers.js +22 -7
- package/test/data.integration.test.js +1060 -981
- 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([]);
|
package/src/modules/file.js
CHANGED
|
@@ -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
|
-
|
|
49
|
-
|
|
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)) {
|
package/src/modules/user.js
CHANGED
|
@@ -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 =
|
|
295
|
+
smtpConfig.port = cfg.port;
|
|
293
296
|
|
|
294
297
|
return smtpConfig;
|
|
295
298
|
}
|
package/src/modules/workflow.js
CHANGED
|
@@ -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,
|
|
10
|
-
import {
|
|
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 {
|
|
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++ >=
|
|
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
|
-
|
|
1459
|
-
|
|
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}`);
|
package/src/openai.jobs.js
CHANGED
|
@@ -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": {"$
|
|
1450
|
+
"workflow": {"$link": {"name": "Generate product description", "_model": "workflow"}},
|
|
1443
1451
|
"actions": {
|
|
1444
|
-
"$
|
|
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": {"$
|
|
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 (
|
|
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 (
|
|
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' &&
|
|
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(){
|