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.
Files changed (77) hide show
  1. package/README.md +913 -867
  2. package/client/package-lock.json +49 -0
  3. package/client/package.json +1 -0
  4. package/client/src/AddWidgetTypeModal.jsx +47 -43
  5. package/client/src/App.jsx +3 -7
  6. package/client/src/App.scss +25 -3
  7. package/client/src/AssistantChat.jsx +363 -323
  8. package/client/src/AssistantChat.scss +30 -12
  9. package/client/src/Dashboard.jsx +480 -396
  10. package/client/src/Dashboard.scss +1 -1
  11. package/client/src/DashboardHtmlViewItem.jsx +147 -0
  12. package/client/src/DashboardView.jsx +104 -19
  13. package/client/src/DataEditor.jsx +12 -5
  14. package/client/src/DataLayout.jsx +805 -762
  15. package/client/src/DataLayout.scss +14 -0
  16. package/client/src/DataTable.jsx +63 -77
  17. package/client/src/Dialog.scss +1 -1
  18. package/client/src/Field.jsx +591 -322
  19. package/client/src/FlexDataRenderer.jsx +2 -0
  20. package/client/src/FlexTreeUtils.js +1 -1
  21. package/client/src/FlexViewCard.jsx +44 -0
  22. package/client/src/HistoryDialog.jsx +47 -14
  23. package/client/src/HtmlViewBuilderModal.jsx +91 -0
  24. package/client/src/HtmlViewBuilderModal.scss +18 -0
  25. package/client/src/HtmlViewCard.jsx +44 -0
  26. package/client/src/HtmlViewCard.scss +35 -0
  27. package/client/src/KPIDialog.jsx +11 -1
  28. package/client/src/KanbanCard.jsx +1 -2
  29. package/client/src/ModelCreator.jsx +6 -6
  30. package/client/src/ModelCreatorField.jsx +74 -31
  31. package/client/src/ModelList.jsx +93 -54
  32. package/client/src/Notification.jsx +136 -136
  33. package/client/src/Notification.scss +0 -18
  34. package/client/src/Pagination.jsx +5 -3
  35. package/client/src/RelationField.jsx +354 -258
  36. package/client/src/RelationSelectorWidget.jsx +173 -0
  37. package/client/src/constants.js +1 -1
  38. package/client/src/contexts/ModelContext.jsx +10 -1
  39. package/client/src/contexts/UIContext.jsx +72 -63
  40. package/client/src/filter.js +262 -212
  41. package/client/src/hooks/useTutorials.jsx +62 -65
  42. package/client/src/hooks/useValidation.js +75 -0
  43. package/client/src/translations.js +26 -24
  44. package/package.json +3 -1
  45. package/perf/README.md +147 -0
  46. package/perf/artillery-hooks.js +37 -0
  47. package/perf/perf-shot-hardwork.yml +84 -0
  48. package/perf/perf-shot-search.yml +45 -0
  49. package/perf/setup.yml +26 -0
  50. package/server.js +1 -1
  51. package/src/constants.js +3 -28
  52. package/src/core.js +15 -1
  53. package/src/data.js +1 -1
  54. package/src/defaultModels.js +63 -7
  55. package/src/email.js +5 -2
  56. package/src/engine.js +5 -3
  57. package/src/filter.js +5 -3
  58. package/src/i18n.js +710 -10
  59. package/src/modules/assistant/assistant.js +151 -19
  60. package/src/modules/bucket.js +14 -16
  61. package/src/modules/data/data.backup.js +11 -8
  62. package/src/modules/data/data.core.js +118 -92
  63. package/src/modules/data/data.history.js +531 -492
  64. package/src/modules/data/data.js +9 -56
  65. package/src/modules/data/data.operations.js +3282 -2999
  66. package/src/modules/data/data.relations.js +686 -686
  67. package/src/modules/data/data.routes.js +118 -24
  68. package/src/modules/data/data.scheduling.js +2 -1
  69. package/src/modules/data/data.validation.js +85 -3
  70. package/src/modules/file.js +247 -236
  71. package/src/modules/user.js +4 -1
  72. package/src/modules/workflow.js +9 -10
  73. package/src/openai.jobs.js +2 -0
  74. package/src/packs.js +5482 -5461
  75. package/src/providers.js +22 -7
  76. package/test/data.integration.test.js +136 -2
  77. 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://data.primals.net/magnet/${magnetUuid}` // ou votre URL de dev
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
- let newData= {};
449
- newData.xp = newData.xp || 0;
450
- newData.achievements = newData.achievements || [];
451
- newData.skills = newData.skills || [];
452
- newData.completedTutorials = newData.completedTutorials || [];
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(maxModelsPerUser).toArray();
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(maxModelsPerUser).toArray());
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
- if( count < maxModelsPerUser) {
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 'datas' pour alléger la réponse de la liste
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: { datas: 0 }
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
- // On retourne le pack complet, incluant les données pour la vue de détail
1512
- res.json(pack);
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.username !== 'demo' && isLocalUser(user) && !await hasPermission(["API_ADMIN", "API_INSTALL_PACK"], 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);