data-primals-engine 1.4.3 → 1.5.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.
- package/README.md +913 -867
- package/client/package-lock.json +49 -0
- package/client/package.json +1 -0
- package/client/src/AddWidgetTypeModal.jsx +47 -43
- package/client/src/App.jsx +3 -7
- package/client/src/App.scss +25 -3
- package/client/src/AssistantChat.jsx +363 -323
- package/client/src/AssistantChat.scss +30 -12
- package/client/src/Dashboard.jsx +480 -396
- package/client/src/Dashboard.scss +1 -1
- package/client/src/DashboardHtmlViewItem.jsx +147 -0
- package/client/src/DashboardView.jsx +104 -19
- package/client/src/DataEditor.jsx +12 -5
- package/client/src/DataLayout.jsx +805 -762
- package/client/src/DataLayout.scss +14 -0
- package/client/src/DataTable.jsx +63 -77
- package/client/src/Dialog.scss +1 -1
- package/client/src/Field.jsx +591 -322
- package/client/src/FlexDataRenderer.jsx +2 -0
- package/client/src/FlexTreeUtils.js +1 -1
- package/client/src/FlexViewCard.jsx +44 -0
- package/client/src/HistoryDialog.jsx +47 -14
- package/client/src/HtmlViewBuilderModal.jsx +91 -0
- package/client/src/HtmlViewBuilderModal.scss +18 -0
- package/client/src/HtmlViewCard.jsx +44 -0
- package/client/src/HtmlViewCard.scss +35 -0
- package/client/src/KPIDialog.jsx +11 -1
- package/client/src/KanbanCard.jsx +1 -2
- package/client/src/ModelCreator.jsx +6 -6
- package/client/src/ModelCreatorField.jsx +74 -31
- package/client/src/ModelList.jsx +93 -54
- package/client/src/Notification.jsx +136 -136
- package/client/src/Notification.scss +0 -18
- package/client/src/Pagination.jsx +5 -3
- package/client/src/RelationField.jsx +354 -258
- package/client/src/RelationSelectorWidget.jsx +173 -0
- package/client/src/constants.js +1 -1
- package/client/src/contexts/ModelContext.jsx +10 -1
- package/client/src/contexts/UIContext.jsx +72 -63
- package/client/src/filter.js +262 -212
- package/client/src/hooks/useTutorials.jsx +62 -65
- package/client/src/hooks/useValidation.js +75 -0
- package/client/src/translations.js +26 -24
- package/package.json +3 -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 +3 -28
- package/src/core.js +15 -1
- package/src/data.js +1 -1
- package/src/defaultModels.js +63 -7
- package/src/email.js +5 -2
- package/src/engine.js +5 -3
- package/src/filter.js +5 -3
- package/src/i18n.js +710 -10
- package/src/modules/assistant/assistant.js +151 -19
- package/src/modules/bucket.js +14 -16
- package/src/modules/data/data.backup.js +11 -8
- package/src/modules/data/data.core.js +118 -92
- package/src/modules/data/data.history.js +531 -492
- package/src/modules/data/data.js +9 -56
- package/src/modules/data/data.operations.js +3282 -2999
- package/src/modules/data/data.relations.js +686 -686
- package/src/modules/data/data.routes.js +118 -24
- package/src/modules/data/data.scheduling.js +2 -1
- package/src/modules/data/data.validation.js +85 -3
- package/src/modules/file.js +247 -236
- 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 +5482 -5461
- package/src/providers.js +22 -7
- package/test/data.integration.test.js +136 -2
- package/test/import_export.integration.test.js +1 -1
|
@@ -4,14 +4,15 @@ import * as util from 'node:util';
|
|
|
4
4
|
import {setTimeoutMiddleware} from '../../middlewares/timeout.js';
|
|
5
5
|
import {isDemoUser, isLocalUser} from "../../data.js";
|
|
6
6
|
import {
|
|
7
|
+
getHost,
|
|
7
8
|
install,
|
|
8
9
|
maxBytesPerSecondThrottleData,
|
|
9
10
|
maxMagnetsDataPerModel,
|
|
10
11
|
maxMagnetsModels,
|
|
11
|
-
maxModelsPerUser
|
|
12
|
+
maxModelsPerUser, maxPackData, maxPackPreviewData
|
|
12
13
|
} from "../../constants.js";
|
|
13
14
|
import {datasCollection, getCollection, getCollectionForUser, isObjectId, modelsCollection} from "../mongodb.js";
|
|
14
|
-
import {safeAssignObject, uuidv4} from "../../core.js";
|
|
15
|
+
import {countKeys, safeAssignObject, uuidv4} from "../../core.js";
|
|
15
16
|
import {Event} from "../../events.js";
|
|
16
17
|
import fs from "node:fs";
|
|
17
18
|
import i18n from "../../i18n.js";
|
|
@@ -24,15 +25,16 @@ import {Config} from "../../config.js";
|
|
|
24
25
|
import {processFilterPlaceholders} from "../../../client/src/filter.js";
|
|
25
26
|
import {tutorialsConfig} from "../../../client/src/tutorials.js";
|
|
26
27
|
import {getResource, handleDemoInitialization} from "./data.js";
|
|
27
|
-
import process from "node:process";
|
|
28
|
+
import process from "node:process";
|
|
28
29
|
import {throttleMiddleware} from "../../middlewares/throttle.js";
|
|
29
30
|
import {importJobs, modelsCache, runImportExportWorker} from "./data.core.js";
|
|
30
|
-
import {validateModelStructure} from "./data.validation.js";
|
|
31
|
+
import {validateModelData, validateModelStructure} from "./data.validation.js";
|
|
31
32
|
import {
|
|
32
33
|
deleteData,
|
|
33
34
|
editData,
|
|
34
35
|
editModel,
|
|
35
36
|
exportData,
|
|
37
|
+
flushSearchCache,
|
|
36
38
|
getModel,
|
|
37
39
|
importData,
|
|
38
40
|
insertData,
|
|
@@ -40,15 +42,13 @@ import {
|
|
|
40
42
|
patchData,
|
|
41
43
|
searchData
|
|
42
44
|
} from "./data.operations.js";
|
|
43
|
-
import {dumpUserData, loadFromDump} from "./data.backup.js";
|
|
45
|
+
import { dumpUserData, loadFromDump } from "./data.backup.js";
|
|
46
|
+
import { invalidateModelCache } from "./data.operations.js";
|
|
44
47
|
|
|
45
48
|
let logger, engine;
|
|
46
49
|
|
|
47
50
|
const sseConnections = new Map();
|
|
48
51
|
|
|
49
|
-
const throttle = throttleMiddleware(maxBytesPerSecondThrottleData);
|
|
50
|
-
|
|
51
|
-
|
|
52
52
|
|
|
53
53
|
|
|
54
54
|
async function logApiRequest(req, res, user, startTime, responseBody = null, error = null) {
|
|
@@ -146,6 +146,62 @@ export async function sendSseToUser(username, data) {
|
|
|
146
146
|
return false;
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
+
/**
|
|
150
|
+
* Handles real-time validation for a single field or a subset of fields.
|
|
151
|
+
* This is designed to be called from the frontend with a debounce mechanism.
|
|
152
|
+
*/
|
|
153
|
+
export const validateDataRealtime = async (req, res) => {
|
|
154
|
+
// `data` is an object with the field(s) to validate, e.g., { "email": "test@example.com" }
|
|
155
|
+
// `contextId` is the _id of the document being edited, to exclude it from 'unique' checks.
|
|
156
|
+
const { model: modelName, data, contextId } = req.fields || req.body;
|
|
157
|
+
const user = req.me; // User is attached by middlewareAuthenticator
|
|
158
|
+
|
|
159
|
+
if (!modelName || !data || typeof data !== 'object' || Object.keys(data).length === 0) {
|
|
160
|
+
return res.status(400).json({ success: false, error: "Request must include 'model' and a non-empty 'data' object." });
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const fieldName = Object.keys(data)[0]; // Assuming one field at a time for simplicity
|
|
164
|
+
|
|
165
|
+
try {
|
|
166
|
+
const model = await getModel(modelName, user);
|
|
167
|
+
if (!model) {
|
|
168
|
+
return res.status(404).json({ success: false, error: `Model '${modelName}' not found.` });
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// 1. Use the existing validation logic (isPatch=true for partial validation)
|
|
172
|
+
await validateModelData(data, model, true, user);
|
|
173
|
+
|
|
174
|
+
// 2. Perform 'unique' checks against the database, which validateModelData might not do.
|
|
175
|
+
const fieldDefinition = model.fields.find(f => f.name === fieldName);
|
|
176
|
+
if (fieldDefinition && fieldDefinition.unique) {
|
|
177
|
+
const fieldValue = data[fieldName];
|
|
178
|
+
|
|
179
|
+
if (fieldValue !== undefined && fieldValue !== null && fieldValue !== '') {
|
|
180
|
+
const collection = await getCollectionForUser(user);
|
|
181
|
+
const query = {
|
|
182
|
+
_user: user.username,
|
|
183
|
+
_model: modelName,
|
|
184
|
+
[fieldName]: fieldValue
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
if (contextId && isObjectId(contextId)) {
|
|
188
|
+
query._id = { $ne: new ObjectId(contextId) };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const existingDoc = await collection.findOne(query);
|
|
192
|
+
if (existingDoc) {
|
|
193
|
+
throw new Error(i18n.t("api.data.duplicateValue", { field: fieldName, value: fieldValue }));
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
res.status(200).json({ success: true, message: "Validation successful.", field: fieldName });
|
|
199
|
+
|
|
200
|
+
} catch (error) {
|
|
201
|
+
res.status(400).json({ success: false, error: error.message, field: fieldName });
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
|
|
149
205
|
|
|
150
206
|
export async function handleCustomEndpointRequest(req, res) {
|
|
151
207
|
const endpointDef = req.endpointDef;
|
|
@@ -254,12 +310,16 @@ export async function registerRoutes(defaultEngine){
|
|
|
254
310
|
engine = defaultEngine;
|
|
255
311
|
logger = engine.getComponent(Logger);
|
|
256
312
|
|
|
313
|
+
const m = Config.Get('maxBytesPerSecondThrottleData', maxBytesPerSecondThrottleData)
|
|
314
|
+
const throttle = throttleMiddleware(m);
|
|
315
|
+
|
|
257
316
|
let userMiddlewares = await engine.userProvider.getMiddlewares();
|
|
258
317
|
|
|
259
318
|
engine.all('/api/actions/:user/:path', [middlewareEndpointAuthenticator, userInitiator], handleCustomEndpointRequest);
|
|
260
319
|
engine.all('/api/actions/:path', [middlewareAuthenticator, middlewareEndpointAuthenticator, userInitiator], handleCustomEndpointRequest);
|
|
261
320
|
engine.post('/api/demo/initialize', [middlewareAuthenticator, userInitiator], handleDemoInitialization);
|
|
262
321
|
|
|
322
|
+
engine.post('/api/data/validate', [middlewareAuthenticator, userInitiator, middlewareLogger], validateDataRealtime);
|
|
263
323
|
engine.post('/api/magnets', [middlewareAuthenticator, userInitiator], async (req, res) => {
|
|
264
324
|
const user = req.me;
|
|
265
325
|
const { name, description, modelNames } = req.fields; // Noms des modèles à inclure
|
|
@@ -357,7 +417,7 @@ export async function registerRoutes(defaultEngine){
|
|
|
357
417
|
res.status(201).json({
|
|
358
418
|
success: true,
|
|
359
419
|
message: "Magnet link created successfully!",
|
|
360
|
-
url: `https
|
|
420
|
+
url: `https://${getHost()}/magnet/${magnetUuid}` // ou votre URL de dev
|
|
361
421
|
});
|
|
362
422
|
|
|
363
423
|
} catch (error) {
|
|
@@ -445,16 +505,18 @@ export async function registerRoutes(defaultEngine){
|
|
|
445
505
|
const { xpBonus, skill, achievement, notification } = tutorial.rewards;
|
|
446
506
|
|
|
447
507
|
// --- LOGIQUE CORRIGÉE ---
|
|
448
|
-
|
|
449
|
-
newData
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
508
|
+
// On part des données existantes de l'utilisateur pour ne rien écraser
|
|
509
|
+
let newData = {
|
|
510
|
+
xp: user.xp || 0,
|
|
511
|
+
achievements: [...(user.achievements || [])],
|
|
512
|
+
skills: JSON.parse(JSON.stringify(user.skills || [])), // Copie profonde pour éviter les mutations directes
|
|
513
|
+
completedTutorials: [...(user.completedTutorials || [])]
|
|
514
|
+
};
|
|
453
515
|
|
|
454
516
|
// Appliquer les récompenses directement sur l'objet newData
|
|
455
517
|
if (xpBonus) newData.xp += xpBonus;
|
|
456
518
|
if (achievement && !newData.achievements.includes(achievement)) newData.achievements.push(achievement);
|
|
457
|
-
if (skill) {
|
|
519
|
+
if (skill && skill.name) { // S'assurer que la compétence a un nom
|
|
458
520
|
const existingSkill = newData.skills.find(s => s.name === skill.name);
|
|
459
521
|
if (existingSkill) {
|
|
460
522
|
existingSkill.points += skill.points;
|
|
@@ -868,13 +930,14 @@ export async function registerRoutes(defaultEngine){
|
|
|
868
930
|
|
|
869
931
|
// get by name
|
|
870
932
|
try {
|
|
933
|
+
const m = Config.Get('maxModelsPerUser', maxModelsPerUser);
|
|
871
934
|
let models = await modelsCollection.find({$or: [{_user: {$exists: false}}]})
|
|
872
|
-
.sort({_user:-1, _id: 1 }).limit(
|
|
935
|
+
.sort({_user:-1, _id: 1 }).limit(m).toArray();
|
|
873
936
|
models = models
|
|
874
937
|
.concat(
|
|
875
938
|
await modelsCollection.find({$or: [{_user: req.me._user}, {_user: req.me.username}]})
|
|
876
939
|
.sort({_user:-1, _id: 1 })
|
|
877
|
-
.limit(
|
|
940
|
+
.limit(m).toArray());
|
|
878
941
|
res.json(models);
|
|
879
942
|
} catch (error) {
|
|
880
943
|
logger.error(error);
|
|
@@ -902,7 +965,8 @@ export async function registerRoutes(defaultEngine){
|
|
|
902
965
|
const count = await modelsCollection.count({
|
|
903
966
|
$and: [{_user: {$exists: true}}, {_user: req.me.username}]
|
|
904
967
|
});
|
|
905
|
-
|
|
968
|
+
const m = Config.Get('maxModelsPerUser', maxModelsPerUser);
|
|
969
|
+
if( count < m) {
|
|
906
970
|
if(await engine.userProvider.hasFeature(req.me, 'indexes')){
|
|
907
971
|
for (const field of modelData.fields) {
|
|
908
972
|
if( field.index ) {
|
|
@@ -987,6 +1051,9 @@ export async function registerRoutes(defaultEngine){
|
|
|
987
1051
|
return res.status(404).json({error: i18n.t( "api.model.notFound", { model: modelName})});
|
|
988
1052
|
}
|
|
989
1053
|
|
|
1054
|
+
// Invalider le cache pour ce modèle avant de le supprimer
|
|
1055
|
+
invalidateModelCache(modelName);
|
|
1056
|
+
|
|
990
1057
|
if( await engine.userProvider.hasFeature(req.me, 'indexes') ) {
|
|
991
1058
|
const indexes = await datasCollection.indexes();
|
|
992
1059
|
for (const index of indexes) {
|
|
@@ -1479,11 +1546,11 @@ export async function registerRoutes(defaultEngine){
|
|
|
1479
1546
|
sortOptions['_updatedAt'] = -1; // Tri par défaut
|
|
1480
1547
|
}
|
|
1481
1548
|
|
|
1482
|
-
// On ne renvoie pas le champ '
|
|
1549
|
+
// On ne renvoie pas le champ 'data' pour alléger la réponse de la liste
|
|
1483
1550
|
const packs = await packsCollection.find({
|
|
1484
1551
|
_user: req.query.user ? req.query.user : { $exists: false }
|
|
1485
1552
|
}, {
|
|
1486
|
-
projection: {
|
|
1553
|
+
projection: { data: 0 }
|
|
1487
1554
|
}).sort(sortOptions).toArray();
|
|
1488
1555
|
|
|
1489
1556
|
res.json(packs);
|
|
@@ -1508,8 +1575,33 @@ export async function registerRoutes(defaultEngine){
|
|
|
1508
1575
|
return res.status(404).json({ success: false, error: 'Pack not found.' });
|
|
1509
1576
|
}
|
|
1510
1577
|
|
|
1511
|
-
|
|
1512
|
-
|
|
1578
|
+
const PACK_PREVIEW_LIMIT = Config.Get('maxPackPreviewData', maxPackPreviewData);
|
|
1579
|
+
|
|
1580
|
+
const countPackEntries = (p) => {
|
|
1581
|
+
if (!p || !p.data) return 0;
|
|
1582
|
+
let totalEntries = 0;
|
|
1583
|
+
for (const langKey in p.data) {
|
|
1584
|
+
const langData = p.data[langKey];
|
|
1585
|
+
for (const modelKey in langData) {
|
|
1586
|
+
if (Array.isArray(langData[modelKey])) {
|
|
1587
|
+
totalEntries += langData[modelKey].length;
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1591
|
+
return totalEntries;
|
|
1592
|
+
};
|
|
1593
|
+
|
|
1594
|
+
const totalEntries = countPackEntries(pack);
|
|
1595
|
+
|
|
1596
|
+
if (totalEntries > PACK_PREVIEW_LIMIT) {
|
|
1597
|
+
logger.warn(`[GET /api/packs/${id}] Pack data is too large (${totalEntries} entries) and will be truncated for the client.`);
|
|
1598
|
+
const packForClient = { ...pack };
|
|
1599
|
+
delete packForClient.data;
|
|
1600
|
+
packForClient.dataTruncated = true;
|
|
1601
|
+
packForClient.totalDataEntries = totalEntries;
|
|
1602
|
+
return res.json(packForClient);
|
|
1603
|
+
}
|
|
1604
|
+
res.json(pack); // On retourne le pack complet si sa taille est raisonnable
|
|
1513
1605
|
} catch (error) {
|
|
1514
1606
|
logger.error(`[GET /api/packs/${id}] Error fetching pack details:`, error);
|
|
1515
1607
|
res.status(500).json({ success: false, error: 'Failed to fetch pack details.' });
|
|
@@ -1684,13 +1776,15 @@ export async function registerRoutes(defaultEngine){
|
|
|
1684
1776
|
const user = req.me;
|
|
1685
1777
|
const lang = req.query.lang || req.fields.lang;
|
|
1686
1778
|
|
|
1779
|
+
const packName = req.fields.packName || null;
|
|
1780
|
+
|
|
1687
1781
|
try {
|
|
1688
1782
|
// Vérification des permissions
|
|
1689
|
-
if (user
|
|
1783
|
+
if (!isDemoUser(user) && isLocalUser(user) && !await hasPermission(["API_ADMIN", "API_INSTALL_PACK"], user)) {
|
|
1690
1784
|
return res.status(403).json({ success: false, error: i18n.t('api.permission.installPack') });
|
|
1691
1785
|
}
|
|
1692
1786
|
|
|
1693
|
-
const result = await installPack({...req.fields.packData, private: true}, user, lang, { installForUser: true });
|
|
1787
|
+
const result = await installPack(packName? packName : {...req.fields.packData, private: true}, user, lang, { installForUser: true });
|
|
1694
1788
|
|
|
1695
1789
|
if (result.success) {
|
|
1696
1790
|
res.status(200).json({ success: true, message: `Pack installed successfully.`, summary: result.summary });
|
|
@@ -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.
|
|
@@ -2,6 +2,7 @@ import {Event} from "../../events.js";
|
|
|
2
2
|
import i18n from "../../i18n.js";
|
|
3
3
|
import {allowedFields, maxFileSize, maxModelNameLength, maxStringLength} from "../../constants.js";
|
|
4
4
|
import {getDefaultForType} from "../../data.js";
|
|
5
|
+
import { generateRegexFromMask } from './data.core.js';
|
|
5
6
|
|
|
6
7
|
import {dataTypes} from "./data.operations.js";
|
|
7
8
|
import {Logger} from "../../gameObject.js";
|
|
@@ -11,6 +12,63 @@ let engine, logger;
|
|
|
11
12
|
export function onInit(defaultEngine) {
|
|
12
13
|
engine = defaultEngine;
|
|
13
14
|
logger = engine.getComponent(Logger);
|
|
15
|
+
|
|
16
|
+
Event.Listen("OnValidateModelStructure", async (modelStructure) =>{
|
|
17
|
+
|
|
18
|
+
const objectKeys = Object.keys(modelStructure);
|
|
19
|
+
|
|
20
|
+
if( objectKeys.find(o => !["name", "_user", "icon", "history", "locked", "_id", "description", "maxRequestData", "fields", "tags"].includes(o)) ){
|
|
21
|
+
throw new Error(i18n.t('api.model.invalidStructure'));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Vérification du type de name
|
|
25
|
+
if (typeof modelStructure.name !== 'string' || !modelStructure.name) {
|
|
26
|
+
throw new Error(i18n.t("api.validate.requiredFieldString", ["name"]));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Vérification du type de description
|
|
30
|
+
if (typeof modelStructure.description !== 'string') {
|
|
31
|
+
throw new Error(i18n.t("api.validate.fieldString", ["description"]));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Vérification de la présence et du type du tableau fields
|
|
35
|
+
if (!Array.isArray(modelStructure.fields)) {
|
|
36
|
+
throw new Error(i18n.t('api.validate.fieldArray', ["fields"]));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Vérification de la présence et du type du tableau fields
|
|
40
|
+
if (typeof(modelStructure.tags) !== 'undefined' && (!Array.isArray(modelStructure.tags) || modelStructure.tags.some(tag => typeof tag !== 'string'))) {
|
|
41
|
+
throw new Error(i18n.t('api.validate.fieldArray', ["tags"]));
|
|
42
|
+
//todo: fieldStringArray trad
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Vérification de chaque champ dans le tableau fields
|
|
46
|
+
for (const field of modelStructure.fields) {
|
|
47
|
+
validateField(field);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (modelStructure.constraints) {
|
|
51
|
+
if (!Array.isArray(modelStructure.constraints)) {
|
|
52
|
+
throw new Error('Model "constraints" property must be an array.');
|
|
53
|
+
}
|
|
54
|
+
const fieldNames = new Set(modelStructure.fields.map(f => f.name));
|
|
55
|
+
for (const constraint of modelStructure.constraints) {
|
|
56
|
+
if (constraint.type === 'unique') {
|
|
57
|
+
if (!constraint.name || !Array.isArray(constraint.keys) || constraint.keys.length === 0) {
|
|
58
|
+
throw new Error('Unique constraint must have a "name" and a non-empty "keys" array.');
|
|
59
|
+
}
|
|
60
|
+
for (const key of constraint.keys) {
|
|
61
|
+
if (!fieldNames.has(key)) {
|
|
62
|
+
throw new Error(`Constraint key "${key}" in constraint "${constraint.name}" does not exist as a field in the model.`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return true; // La structure du modèle est valide
|
|
70
|
+
}, "event", "system");
|
|
71
|
+
|
|
14
72
|
}
|
|
15
73
|
|
|
16
74
|
export async function validateModelStructure(modelStructure) {
|
|
@@ -104,17 +162,30 @@ export const validateField = (field) => {
|
|
|
104
162
|
case 'code':
|
|
105
163
|
if (field.type === 'code')
|
|
106
164
|
allowedFieldTest(['maxlength', 'language', 'conditionBuilder', 'targetModel']);
|
|
107
|
-
else if (['string_t', 'string'].includes(field.type))
|
|
108
|
-
allowedFieldTest(['maxlength', 'multiline']);
|
|
165
|
+
else if (['string_t', 'string'].includes(field.type)) {
|
|
166
|
+
allowedFieldTest(['maxlength', 'multiline', 'mask', 'replacement']);
|
|
167
|
+
}
|
|
109
168
|
else
|
|
110
169
|
allowedFieldTest(['maxlength']);
|
|
111
170
|
if (field.maxlength !== undefined && typeof field.maxlength !== 'number') {
|
|
112
171
|
throw new Error(i18n.t('api.validate.fieldNumber', "L'attribut '{{0}}' doit être un nombre.", ["maxlength"]));
|
|
113
172
|
}
|
|
173
|
+
if (field.mask !== undefined && typeof field.mask !== 'string') {
|
|
174
|
+
throw new Error(i18n.t('api.validate.fieldString', "Le champ '{{0}}' doit être une chaîne de caractères.", ["mask"]));
|
|
175
|
+
}
|
|
176
|
+
if (field.replacement !== undefined && typeof field.replacement !== 'object') {
|
|
177
|
+
throw new Error(i18n.t('api.validate.fieldObject', "L'attribut '{{0}}' doit être un objet.", ["replacement"]));
|
|
178
|
+
}
|
|
179
|
+
if (field.mask && !field.replacement) {
|
|
180
|
+
throw new Error(i18n.t('api.validate.missingField', "L'attribut 'replacement' est requis quand 'mask' est défini."));
|
|
181
|
+
}
|
|
114
182
|
break;
|
|
115
183
|
case 'model':
|
|
116
184
|
case 'modelField':
|
|
117
|
-
allowedFieldTest([]);
|
|
185
|
+
allowedFieldTest(['targetModel']);
|
|
186
|
+
if (field.targetModel !== undefined && typeof field.targetModel !== 'string') {
|
|
187
|
+
throw new Error(i18n.t('api.validate.fieldString', "Le champ '{{0}}' doit être une chaîne de caractères.", ["targetModel"]));
|
|
188
|
+
}
|
|
118
189
|
break;
|
|
119
190
|
case 'object':
|
|
120
191
|
allowedFieldTest([]);
|
|
@@ -238,6 +309,17 @@ export async function validateModelData(doc, model, isPatch = false) {
|
|
|
238
309
|
const fieldDef = model.fields.find(f => f.name === fieldName);
|
|
239
310
|
if (!fieldDef) continue; // On ignore les champs supplémentaires
|
|
240
311
|
|
|
312
|
+
// Validation du masque si défini
|
|
313
|
+
if (fieldDef.mask && value) {
|
|
314
|
+
const regexString = generateRegexFromMask(fieldDef.mask, fieldDef.replacement);
|
|
315
|
+
if (regexString) {
|
|
316
|
+
const regex = new RegExp(regexString);
|
|
317
|
+
if (!regex.test(value)) {
|
|
318
|
+
throw new Error(i18n.t('api.field.maskValidationFailed', { field: fieldName, value: value, mask: fieldDef.mask }));
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
241
323
|
const validator = dataTypes[fieldDef.type]?.validate;
|
|
242
324
|
const valid = validator && validator(value, fieldDef);
|
|
243
325
|
const realValidation = await Event.Trigger('OnDataValidate', "event", "system", value, fieldDef, doc);
|