data-primals-engine 1.5.0 → 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 (52) hide show
  1. package/README.md +35 -0
  2. package/client/src/AddWidgetTypeModal.jsx +47 -43
  3. package/client/src/App.jsx +2 -6
  4. package/client/src/App.scss +12 -0
  5. package/client/src/AssistantChat.jsx +363 -323
  6. package/client/src/AssistantChat.scss +27 -10
  7. package/client/src/Dashboard.jsx +480 -396
  8. package/client/src/Dashboard.scss +1 -1
  9. package/client/src/DashboardHtmlViewItem.jsx +147 -0
  10. package/client/src/DashboardView.jsx +654 -569
  11. package/client/src/DataEditor.jsx +10 -3
  12. package/client/src/DataLayout.jsx +805 -755
  13. package/client/src/DataLayout.scss +14 -0
  14. package/client/src/DataTable.jsx +39 -75
  15. package/client/src/Dialog.scss +1 -1
  16. package/client/src/Field.jsx +2057 -1825
  17. package/client/src/FlexViewCard.jsx +44 -0
  18. package/client/src/HistoryDialog.jsx +47 -14
  19. package/client/src/HtmlViewBuilderModal.jsx +91 -0
  20. package/client/src/HtmlViewBuilderModal.scss +18 -0
  21. package/client/src/HtmlViewCard.jsx +44 -0
  22. package/client/src/HtmlViewCard.scss +35 -0
  23. package/client/src/KanbanCard.jsx +1 -2
  24. package/client/src/ModelCreator.jsx +5 -4
  25. package/client/src/ModelCreatorField.jsx +51 -4
  26. package/client/src/ModelList.jsx +92 -53
  27. package/client/src/Notification.jsx +136 -136
  28. package/client/src/Notification.scss +0 -18
  29. package/client/src/Pagination.jsx +5 -3
  30. package/client/src/RelationField.jsx +354 -258
  31. package/client/src/RelationSelectorWidget.jsx +173 -0
  32. package/client/src/contexts/ModelContext.jsx +10 -1
  33. package/client/src/contexts/UIContext.jsx +72 -63
  34. package/client/src/filter.js +262 -212
  35. package/client/src/hooks/useValidation.js +75 -0
  36. package/client/src/translations.js +24 -24
  37. package/package.json +2 -1
  38. package/src/constants.js +1 -1
  39. package/src/defaultModels.js +1596 -1544
  40. package/src/i18n.js +710 -10
  41. package/src/modules/assistant/assistant.js +148 -18
  42. package/src/modules/bucket.js +2 -1
  43. package/src/modules/data/data.core.js +118 -92
  44. package/src/modules/data/data.history.js +531 -492
  45. package/src/modules/data/data.js +3 -53
  46. package/src/modules/data/data.operations.js +77 -26
  47. package/src/modules/data/data.relations.js +686 -686
  48. package/src/modules/data/data.routes.js +1879 -1821
  49. package/src/modules/data/data.validation.js +81 -2
  50. package/src/modules/file.js +247 -238
  51. package/src/packs.js +5482 -5478
  52. package/test/data.integration.test.js +1115 -1060
@@ -53,14 +53,40 @@ export const assistantGlobalLimiter = rateLimit({
53
53
  });
54
54
 
55
55
  async function searchModels(query, user) {
56
- if (!query) return [];
56
+ if (!query) return { main: [], related: [] };
57
57
  const searchRegex = new RegExp(query, 'i');
58
- return await modelsCollection.find({
58
+ const mainModels = await modelsCollection.find({
59
59
  $or: [{ _user: user.username }, { _user: { $exists: false } }],
60
60
  $and: [{ $or: [{ name: { $regex: searchRegex } }, { description: { $regex: searchRegex } }] }]
61
61
  }, {
62
62
  projection: { name: 1, description: 1, fields: 1, _id: 0 }
63
63
  }).limit(10).toArray();
64
+
65
+ if (mainModels.length === 0) {
66
+ return { main: [], related: [] };
67
+ }
68
+
69
+ const relatedModelNames = new Set();
70
+ mainModels.forEach(model => {
71
+ model.fields.forEach(field => {
72
+ if (field.type === 'relation' && field.relation) {
73
+ relatedModelNames.add(field.relation);
74
+ }
75
+ });
76
+ });
77
+
78
+ const mainModelNames = new Set(mainModels.map(m => m.name));
79
+ const finalRelatedModelNames = [...relatedModelNames].filter(name => !mainModelNames.has(name));
80
+
81
+ let relatedModels = [];
82
+ if (finalRelatedModelNames.length > 0) {
83
+ relatedModels = await modelsCollection.find({
84
+ name: { $in: finalRelatedModelNames },
85
+ $or: [{ _user: user.username }, { _user: { $exists: false } }]
86
+ }, { projection: { name: 1, description: 1, fields: 1, _id: 0 } }).toArray();
87
+ }
88
+
89
+ return { main: mainModels, related: relatedModels };
64
90
  }
65
91
 
66
92
  const createSystemPrompt = (modelDefs, lang) => {
@@ -87,7 +113,7 @@ Tu as accès aux outils et actions suivants.
87
113
 
88
114
  OUTILS DE RAISONNEMENT INTERNE: (Utilisés pour collecter de l'information avant de décider de l'action finale)
89
115
  1. **search_models**: Pour rechercher les modèles de données disponibles.
90
- - Utilisation: { "action": "search_models", "params": { "query": "^regexToSearchFor$" } }
116
+ - Utilisation: { "action": "search_models", "params": { "query": "^regexToSearchFor$" } }. Il retourne aussi la structure des modèles directement liés.
91
117
  ====
92
118
 
93
119
  ACTIONS FINALES: (Actions qui terminent ta réflexion et renvoient un résultat à l'utilisateur)
@@ -119,9 +145,24 @@ ACTIONS FINALES: (Actions qui terminent ta réflexion et renvoient un résultat
119
145
  - \`yAxis\` (string): (Optionnel, sauf pour sum/avg/min/max) Le champ numérique à agréger du modèle.
120
146
  - \`filter\` (object): (Optionnel, filtre de la recherche, même écriture stricte que pour les filtres de recherche (voir plus bas pour les exemples)
121
147
 
148
+ 7. **generateHtmlView**: Pour créer une vue personnalisée en utilisant un template HTML.
149
+ - Utilisation: { "action": "generateHtmlView", "params": { ...config } }
150
+ - Le paramètre \`config\` doit contenir :
151
+ - \`title\` (string): Un titre pour la vue.
152
+ - \`model\` (string): Le nom du modèle de données à utiliser.
153
+ - \`template\` (string): Un template au format Handlebars.js. **RÈGLE CRITIQUE : N'utilise QUE les noms de champs (\`fieldName\`) exacts fournis par l'outil \`search_models\` pour le modèle principal ET pour ses modèles liés. N'invente JAMAIS de champs.**
154
+ Pour une liste, tu DOIS utiliser une boucle \`{{#each data}}...{{/each}}\`.
155
+ À l'intérieur d'une boucle, accède aux champs avec \`{{this.fieldName}}\`.
156
+ Pour les champs de type \`string_t\` ou \`richtext_t\`, accède à la traduction avec \`{{this.fieldName.value}}\`.
157
+ Les champs de type 'relation' sont automatiquement peuplés (hydratés), tu peux donc accéder à leurs propriétés directement (ex: \`{{this.relationField.name}}\` ou \`{{this.relationField.name.value}}\` si le champ 'name' de la relation est un 'string_t').
158
+ - \`css\` (string): (Optionnel) Du CSS riche et créatif pour styliser le template. N'hésite pas à utiliser des dégradés, des ombres, des animations et des polices de caractères pour un rendu professionnel et attrayant. **Règle absolue : tu dois préfixer TOUS tes sélecteurs avec \`#{{containerId}}\` pour isoler les styles.**
159
+ - \`filter\` (object): (Optionnel) Un filtre pour sélectionner les documents à afficher.
160
+ - \`limit\` (number): (Optionnel, défaut 10) Le nombre maximum de documents à récupérer.
161
+
122
162
  Voici le mémo pour assigner des valeurs aux champs des modèles,avec ces types de données :
123
163
  utilise une chaine de caractère convertible en ObjectId (mongodb) lorsque le nom du champ est _id
124
- utilise une chaine de caracteres lorsque le type de champ est : string, string_t , password, url, phone, email, richtext
164
+ utilise une chaine de caracteres lorsque le type de champ est : string , password, url, phone, email, richtext
165
+ utilise un objet { key: "trKey", value: "Translation"} lorsque le champ est string_t
125
166
  utilise un filtre en retour si le type de champ est code et language='json' et conditionBuilder=true
126
167
  utilise une chaine si c'est un type de champ code par défaut.
127
168
  utilise une structure { "iso2langcode":"content..." } pour le champ multi-traductions richtext_t
@@ -135,13 +176,11 @@ utilise un tableau d'_ids pour remplir les champs relation multiple=true
135
176
  utilise la valeur en héxadecimal, ex: '#FF0000' pour les champs de type : color
136
177
  utilise les valeurs de cron standard '* * * * * *' pour : cronSchedule
137
178
 
138
- PROCESSUS DE RAISONNEMENT:
139
- a- L'utilisateur pose une question, ou demande une action de ta part.
140
- b- Utilise l'outil **search_models** pour trouver le(s) modèle(s) qui correspondent à la question.
141
- Si tu as déjà fait la recherche dans la conversation, garde la définition initiale et n'effectue pas de recherche, va directement à l'étape c
142
- c- Une fois la réponse retournée et intégrée, tu devras utiliser les autres outils (search, post, etc.) dans la conversation pour satisfaire la question initiale, en utilisant les informations des modèles précédents (COMMANDE FINALE)
143
- Si tu n'as aucune commande à exécuter directement, réponds simplement à l'utilisateur avec "displayMessage".
144
-
179
+ PROCESSUS DE RAISONNEMENT STRICT:
180
+ a- L'utilisateur pose une question.
181
+ b- **Étape 1 (Obligatoire):** Appelle l'outil \`search_models\` pour obtenir la structure EXACTE du modèle de données. C'est ta seule source de vérité pour les noms de champs.
182
+ c- **Étape 2 (Obligatoire):** Analyse la réponse de \`search_models\` que le système t'a fournie.
183
+ d- **Étape 3 (Commande Finale):** Construis ta commande finale (\`search\`, \`generateHtmlView\`, etc.). **Règle absolue :** Pour les filtres et les templates, tu ne dois utiliser QUE les noms de champs (\`name\`) et les types (\`type\`) que tu as lus dans la réponse de \`search_models\` à l'étape c. N'invente RIEN.
145
184
  CONTEXTE ACTUEL:
146
185
  - Date du jour de la conversation : ${dt}
147
186
  - La langue ISO à utiliser dans la conversation : ${lang}
@@ -183,6 +222,67 @@ COMMANDE FINALE :
183
222
  }
184
223
  }
185
224
 
225
+ Question: Crée un tableau de bord des rôles et permissions avec un design sophistiqué.
226
+ Ta réponse: { "action" : "search_models", "params": { "query": "role" } }
227
+ COMMANDE FINALE :
228
+ {
229
+ "action": "generateHtmlView",
230
+ "params": {
231
+ "title": "Matrice des Rôles et Permissions",
232
+ "model": "role",
233
+ "template": "<div class='roles-container'>{{#each data}}<div class='role-card'><div class='role-header'><span class='role-icon'>🛡️</span><h3>{{this.name.value}}</h3></div><ul class='permissions-list'>{{#each this.permissions}}<li data-tooltip-html='{{this.description}}'><span class='permission-name'>{{this.name.value}}</span></li>{{/each}}</ul></div>{{/each}}</div>",
234
+ "css": "#{{containerId}} .roles-container{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:25px;padding:25px;font-family:'Poppins',sans-serif;background:#111827}#{{containerId}} .role-card{background:rgba(31,41,55,.5);border-radius:16px;padding:20px;position:relative;overflow:hidden;border:1px solid transparent;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);transition:transform .3s ease,box-shadow .3s ease}#{{containerId}} .role-card::before{content:'';position:absolute;top:0;right:0;bottom:0;left:0;z-index:-1;margin:-1px;border-radius:inherit;background:conic-gradient(from 180deg at 50% 50%,#2a8af6 0deg,#a855f7 180deg,#f59e0b 360deg);animation:rotate-gradient 5s linear infinite}@keyframes rotate-gradient{0%{transform:rotate(0deg)}100%{transform:rotate(360deg)}}#{{containerId}} .role-card:hover{transform:translateY(-8px);box-shadow:0 20px 30px rgba(0,0,0,.2)}#{{containerId}} .role-header{display:flex;align-items:center;gap:12px;margin-bottom:15px;padding-bottom:10px;border-bottom:1px solid rgba(255,255,255,.1)}#{{containerId}} .role-header .role-icon{font-size:1.8em}#{{containerId}} .role-header h3{margin:0;font-size:1.4em;font-weight:600;color:#f9fafb}#{{containerId}} .permissions-list{list-style:none;padding:0;margin:0;max-height:200px;overflow-y:auto}#{{containerId}} .permissions-list::-webkit-scrollbar{width:6px}#{{containerId}} .permissions-list::-webkit-scrollbar-track{background:rgba(255,255,255,.05);border-radius:3px}#{{containerId}} .permissions-list::-webkit-scrollbar-thumb{background:#4f46e5;border-radius:3px}#{{containerId}} .permissions-list::-webkit-scrollbar-thumb:hover{background:#6366f1}#{{containerId}} .permissions-list li{padding:8px 12px;margin-bottom:6px;background:rgba(255,255,255,.05);border-radius:8px;color:#d1d5db;cursor:help;transition:background-color .2s ease;font-size:.95em}#{{containerId}} .permissions-list li:hover{background:rgba(79,70,229,.5);color:#fff}",
235
+ "filter": {},
236
+ "limit": 10
237
+ }
238
+ }
239
+
240
+ Question: Affiche une carte de visite stylisée pour le premier contact.
241
+ Ta réponse: { "action" : "search_models", "params": { "query": "contact" } }
242
+ COMMANDE FINALE :
243
+ {
244
+ "action": "generateHtmlView",
245
+ "params": {
246
+ "title": "Carte de Visite",
247
+ "model": "contact",
248
+ "template": "<div class='card-container'><h3>{{this.firstName}} {{this.lastName}}</h3><p>{{this.email}}</p></div>",
249
+ "css": "#{{containerId}} .card-container { border: 1px solid #ccc; border-radius: 8px; padding: 16px; background-color: #f9f9f9; } #{{containerId}} h3 { margin-top: 0; color: #333; }",
250
+ "filter": {},
251
+ "limit": 1
252
+ }
253
+ }
254
+
255
+ Question: Montre-moi un catalogue des produits.
256
+ Ta réponse: { "action" : "search_models", "params": { "query": "product" } }
257
+ COMMANDE FINALE :
258
+ {
259
+ "action": "generateHtmlView",
260
+ "params": {
261
+ "title": "Catalogue Produits",
262
+ "model": "product",
263
+ "template": "<div class='product-grid'>{{#each data}}<div class='product-card'><div class='product-image-container'><img src='{{this.image.0.url}}' alt='{{this.name.value}}' class='product-image'><span class='product-brand'>{{this.brand.name}}</span></div><div class='product-info'><h3 class='product-name'>{{this.name.value}}</h3><p class='product-category'>{{this.category.name.value}}</p><div class='product-footer'><span class='product-price'>{{this.price}} {{this.currency.symbol}}</span><button class='add-to-cart-btn'>Ajouter au panier</button></div></div></div>{{/each}}</div>",
264
+ "css": "#{{containerId}} .product-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(250px,1fr));gap:2rem;padding:2rem;font-family:'Lato',sans-serif;background-color:#f8f9fa}#{{containerId}} .product-card{background-color:#fff;border-radius:12px;box-shadow:0 4px 6px rgba(0,0,0,.05),0 1px 3px rgba(0,0,0,.05);overflow:hidden;transition:transform .3s ease,box-shadow .3s ease;display:flex;flex-direction:column}#{{containerId}} .product-card:hover{transform:translateY(-5px);box-shadow:0 12px 20px rgba(0,0,0,.08),0 3px 8px rgba(0,0,0,.06)}#{{containerId}} .product-image-container{position:relative;width:100%;padding-top:100%}#{{containerId}} .product-image{position:absolute;top:0;left:0;width:100%;height:100%;object-fit:cover}#{{containerId}} .product-brand{position:absolute;top:10px;right:10px;background-color:rgba(0,0,0,.6);color:#fff;padding:4px 8px;border-radius:6px;font-size:.8em;font-weight:700}#{{containerId}} .product-info{padding:15px;display:flex;flex-direction:column;flex-grow:1}#{{containerId}} .product-name{font-size:1.1em;font-weight:700;color:#343a40;margin:0 0 5px 0;line-height:1.3}#{{containerId}} .product-category{font-size:.85em;color:#6c757d;margin:0 0 15px 0;flex-grow:1}#{{containerId}} .product-footer{display:flex;justify-content:space-between;align-items:center;border-top:1px solid #e9ecef;padding-top:10px;margin-top:auto}#{{containerId}} .product-price{font-size:1.2em;font-weight:700;color:#007bff}#{{containerId}} .add-to-cart-btn{background-color:#007bff;color:#fff;border:none;padding:8px 12px;border-radius:8px;cursor:pointer;font-weight:600;transition:background-color .2s ease}#{{containerId}} .add-to-cart-btn:hover{background-color:#0056b3}",
265
+ "filter": {},
266
+ "limit": 12
267
+ }
268
+ }
269
+
270
+ Question: Affiche-moi les dernières requêtes API avec un design futuriste.
271
+ Ta réponse: { "action" : "search_models", "params": { "query": "request" } }
272
+ COMMANDE FINALE :
273
+ {
274
+ "action": "generateHtmlView",
275
+ "params": {
276
+ "title": "Journal des Requêtes API",
277
+ "model": "request",
278
+ "template": "<div class=\"requests-grid\">{{#each data}}<div class=\"request-card status-{{this.status}}\"><div class=\"card-header\"><span class=\"method-badge method-{{this.method}}\">{{this.method}}</span><span class=\"status-code\" data-tooltip-html=\"Code de statut HTTP\">{{this.status}}</span></div><div class=\"card-body\"><p class=\"url\" data-tooltip-html=\"URL de la requête\">{{this.url}}</p></div><div class=\"card-footer\"><span class=\"latency\" data-tooltip-html=\"Latence de la réponse\">⏱️ {{this.latencyMs}} ms</span><span class=\"timestamp\" data-tooltip-html=\"Date et heure\">{{this.timestamp}}</span></div><div class=\"glow-effect\"></div></div>{{/each}}</div>",
279
+ "css": "#{{containerId}} .requests-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:20px;font-family:'Orbitron',sans-serif;padding:10px}#{{containerId}} .request-card{background:rgba(10,25,47,.8);border:1px solid #00aaff;border-radius:12px;padding:15px;position:relative;overflow:hidden;transition:transform .3s ease,box-shadow .3s ease;backdrop-filter:blur(5px);-webkit-backdrop-filter:blur(5px)}#{{containerId}} .request-card:hover{transform:translateY(-5px);box-shadow:0 10px 20px rgba(0,170,255,.3)}#{{containerId}} .glow-effect{position:absolute;top:0;left:0;width:100%;height:100%;background:radial-gradient(circle at 50% 0,rgba(0,170,255,.2),transparent 70%);animation:pulse 4s infinite ease-in-out;pointer-events:none}@keyframes pulse{0%,100%{opacity:.5}50%{opacity:1}}#{{containerId}} .card-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;border-bottom:1px solid rgba(0,170,255,.2);padding-bottom:8px}#{{containerId}} .method-badge{padding:4px 8px;border-radius:5px;font-weight:700;font-size:.9em;color:#fff;text-shadow:0 0 5px currentColor}#{{containerId}} .method-GET{background-color:#00aaff}#{{containerId}} .method-POST{background-color:#4caf50}#{{containerId}} .method-PUT{background-color:#ff9800}#{{containerId}} .method-DELETE{background-color:#f44336}#{{containerId}} .method-PATCH{background-color:#9c27b0}#{{containerId}} .status-code{font-size:1.2em;font-weight:700;color:#fff}#{{containerId}} .status-200 .status-code{color:#4caf50;text-shadow:0 0 8px #4caf50}#{{containerId}} .status-404 .status-code{color:#f44336;text-shadow:0 0 8px #f44336}#{{containerId}} .status-500 .status-code{color:#ff9800;text-shadow:0 0 8px #ff9800}#{{containerId}} .card-body .url{color:#e0e0e0;font-size:.95em;word-break:break-all;margin:0}#{{containerId}} .card-footer{display:flex;justify-content:space-between;align-items:center;margin-top:15px;font-size:.8em;color:#88a1b9}#{{containerId}} .latency,#{{containerId}} .timestamp{display:flex;align-items:center;gap:5px}",
280
+ "filter": {},
281
+ "sort": { "_id": -1 },
282
+ "limit": 12
283
+ }
284
+ }
285
+
186
286
  Absolute rules:
187
287
  ====
188
288
  NEVER use the non-aggregated syntax of the MongoDB operators :
@@ -275,11 +375,17 @@ async function executeTool(action, params, user, allModels) {
275
375
  return resultString;
276
376
  }
277
377
  case 'search_models': {
278
- const foundModels = await searchModels(params.query, user);
378
+ const { main: foundModels, related: relatedModels } = await searchModels(params.query, user);
279
379
 
280
380
  if (foundModels.length > 0) {
281
- return "J'ai trouvé les modèles suivants qui pourraient correspondre : " +
381
+ let responseText = "J'ai trouvé les modèles suivants qui pourraient correspondre : " +
282
382
  foundModels.map(m => `\n- Modèle "${m.name}": ${m.description || 'Pas de description.'}\n- Champs: ${m.fields.map(f => JSON.stringify(f, null, 2))}`).join('');
383
+
384
+ if (relatedModels.length > 0) {
385
+ responseText += "\n\nPour votre information, voici la structure des modèles liés que vous pouvez utiliser dans les templates :";
386
+ responseText += relatedModels.map(m => `\n- Modèle lié "${m.name}":\n- Champs: ${m.fields.map(f => JSON.stringify(f, null, 2))}`).join('');
387
+ }
388
+ return responseText;
283
389
  } else {
284
390
  return "Je n'ai trouvé aucun modèle correspondant à votre recherche.";
285
391
  }
@@ -367,18 +473,25 @@ async function handleChatRequest(message, history, provider, context, user, conf
367
473
  // Parsing JSON robuste
368
474
  let parsedResponse;
369
475
  try {
476
+ // Tente d'extraire le JSON de la réponse, même s'il est entouré de texte.
477
+ const jsonRegex = /\{[\s\S]*\}/s; // 's' flag pour que '.' matche les nouvelles lignes
478
+ const match = llmOutput.match(jsonRegex);
370
479
 
371
- parsedResponse = JSON.parse(llmOutput);
480
+ if (match && match[0]) {
481
+ // Si un JSON est trouvé, on tente de le parser
482
+ parsedResponse = JSON.parse(match[0]);
483
+ } else {
484
+ // Aucun JSON trouvé, c'est probablement une réponse textuelle simple.
485
+ return { success: true, displayMessage: llmOutput };
486
+ }
372
487
 
373
488
  if (!parsedResponse.action || !parsedResponse.params) {
374
489
  throw new Error("Réponse JSON invalide: 'action' ou 'params' manquant.");
375
490
  }
376
491
  } catch (parseError) {
377
492
  logger.error(`[Assistant] Erreur de parsing de la réponse de l'IA: ${parseError.message}. Réponse brute: "${llmOutput}"`);
378
- return {
379
- success: true,
380
- displayMessage: llmOutput || i18n.t('assistant.invalidResponse', "Désolé, je n'ai pas pu formuler une réponse correcte. Veuillez réessayer.")
381
- };
493
+ // Si le parsing échoue, on renvoie le message brut de l'IA, qui est peut-être une réponse textuelle valide.
494
+ return { success: true, displayMessage: llmOutput };
382
495
  }
383
496
 
384
497
  logger.debug(`[Assistant] Action décidée par l'IA: ${parsedResponse.action}`, parsedResponse);
@@ -398,6 +511,23 @@ async function handleChatRequest(message, history, provider, context, user, conf
398
511
  return { success: true, chartConfig: params };
399
512
  }
400
513
 
514
+ // Action de génération de vue HTML
515
+ if (action === 'generateHtmlView') {
516
+ const viewData = await searchData({
517
+ model: params.model,
518
+ filter: params.filter,
519
+ limit: params.limit || 10,
520
+ depth: 2 // Pour avoir accès aux relations de premier niveau dans les templates
521
+ }, user);
522
+
523
+ if (viewData.data.length === 0) {
524
+ return { success: true, displayMessage: i18n.t('assistant.htmlView.noResult', "Je n'ai trouvé aucune donnée correspondante pour cette vue.") };
525
+ }
526
+
527
+ // On retourne la configuration de la vue ET les données au client.
528
+ return { success: true, htmlViewConfig: { ...params, data: viewData.data } };
529
+ }
530
+
401
531
  // NOUVEAU: Action de recherche à afficher, gérée par le front-end
402
532
  if (action === 'search') {
403
533
  const searchResult = await searchData({
@@ -6,7 +6,7 @@ import {decryptValue, encryptValue} from "../data.js";
6
6
  import {loadFromDump, validateRestoreRequest} from "./data/index.js";
7
7
  import {Logger} from "../gameObject.js";
8
8
  import {middlewareAuthenticator, userInitiator} from "./user.js";
9
- import {awsDefaultConfig, maxBytesPerSecondThrottleData} from "../constants.js";
9
+ import {awsDefaultConfig, getHost, maxBytesPerSecondThrottleData} from "../constants.js";
10
10
  import crypto from "node:crypto";
11
11
  import i18n from "../../src/i18n.js";
12
12
  import {sendEmail} from "../email.js";
@@ -38,6 +38,7 @@ export const requestRestore = async (user, lang) => {
38
38
  content: i18n.t('email.backup.restoreRequest.content', {
39
39
  user: user?.username,
40
40
  fullToken: fullRestoreToken,
41
+ host: getHost(),
41
42
  modelsToken: modelsRestoreToken
42
43
  })
43
44
  });
@@ -1,93 +1,119 @@
1
- import NodeCache from "node-cache";
2
- import path from "node:path";
3
- import {Worker} from 'worker_threads';
4
- import {fileURLToPath} from "node:url";
5
- export const modelsCache = new NodeCache( { stdTTL: 100, checkperiod: 120 } );
6
-
7
- const __filename = fileURLToPath(import.meta.url);
8
- const __dirname = path.dirname(__filename);
9
-
10
- export const mongoDBWhitelist = [
11
- "$$NOW", "$in", "$eq", "$gt", "$gte", "$in", "$lt", "$lte", "$ne", "$nin", "$type", "$size",
12
- "$and", "$not", "$nor", "$or", "$regexMatch", "$find", "$elemMatch", "$filter", "$toString", "$toObjectId",
13
- "$concat",
14
- '$add', '$subtract', '$multiply', '$divide', '$mod', '$pow', "$sqrt",
15
- "$rand",
16
- "$abs", '$sin', '$cos', '$tan', '$asin', '$acos', '$atan',
17
- "$toDate", "$toBool", "$toString", "$toInt", "$toDouble",
18
- "$dateDiff", "$dateSubtract", "$dateAdd", "$dateToString",
19
- '$year', '$month', '$week', '$dayOfMonth', '$dayOfWeek', '$dayOfYear', '$hour', '$minute', '$second', '$millisecond',
20
- '$geoNear', '$regex', '$text', '$nearSphere','$geoWithin','$geoIntersects'
21
- ];
22
- export let importJobs = {};
23
-
24
-
25
- /**
26
- * Exécute une tâche d'import/export (parsing, stringify) dans un worker thread.
27
- * @param {('parse-json'|'parse-csv'|'stringify-json')} action - L'action à effectuer.
28
- * @param {object} payload - Les données nécessaires pour l'action.
29
- * @returns {Promise<any>} - Une promesse qui se résout avec les données traitées.
30
- */
31
- export function runImportExportWorker(action, payload) {
32
- return new Promise((resolve, reject) => {
33
- const workerPath = path.resolve(process.cwd(), './src/workers/import-export-worker.js');
34
- const worker = new Worker(workerPath);
35
-
36
- worker.postMessage({ action, payload });
37
-
38
- worker.on('message', (result) => {
39
- if (result.success) {
40
- resolve(result.data);
41
- } else {
42
- // Correction : On s'assure de toujours passer une chaîne de caractères à new Error()
43
- const errorMessage = result.error || `Import/Export Worker failed with an unknown error. Action: ${action}.`;
44
- reject(new Error(errorMessage));
45
- }
46
- worker.terminate();
47
- });
48
-
49
- worker.on('error', (err) => {
50
- reject(err);
51
- worker.terminate();
52
- });
53
-
54
- worker.on('exit', (code) => {
55
- if (code !== 0) {
56
- reject(new Error(`Import/Export Worker stopped with exit code ${code}`));
57
- }
58
- });
59
- });
60
- }
61
- /** Exécute une tâche de cryptographie dans un worker thread.
62
- * @param {('encrypt'|'decrypt'|'hash')} action - L'action à effectuer.
63
- * @param {object} payload - Les données nécessaires pour l'action.
64
- * @returns {Promise<any>} - Une promesse qui se résout avec le résultat (si pertinent).
65
- */
66
- export function runCryptoWorkerTask(action, payload) {
67
- return new Promise((resolve, reject) => {
68
- const workerPath = path.resolve(__dirname, '../../workers/crypto-worker.js');
69
- const worker = new Worker(workerPath);
70
-
71
- worker.postMessage({ action, payload });
72
-
73
- worker.on('message', (result) => {
74
- if (result.success) {
75
- resolve(result.data); // Résout avec les données (ex: le hash) ou undefined si pas de retour
76
- } else {
77
- reject(new Error(result.error));
78
- }
79
- worker.terminate();
80
- });
81
-
82
- worker.on('error', (err) => {
83
- reject(err);
84
- worker.terminate();
85
- });
86
-
87
- worker.on('exit', (code) => {
88
- if (code !== 0) {
89
- reject(new Error(`Crypto Worker stopped with exit code ${code}`));
90
- }
91
- });
92
- });
1
+ import NodeCache from "node-cache";
2
+ import path from "node:path";
3
+ import {Worker} from 'worker_threads';
4
+ import {fileURLToPath} from "node:url";
5
+ export const modelsCache = new NodeCache( { stdTTL: 100, checkperiod: 120 } );
6
+
7
+ const __filename = fileURLToPath(import.meta.url);
8
+ const __dirname = path.dirname(__filename);
9
+
10
+ /**
11
+ * Helper to escape characters for regex.
12
+ * @param {string} str The string to escape.
13
+ * @returns {string} The escaped string.
14
+ */
15
+ function escapeRegex(str) {
16
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
17
+ }
18
+
19
+ /**
20
+ * Generates a regular expression from a mask pattern and replacement rules.
21
+ * @param {string} mask - The mask pattern, e.g., '+0 (___) ___-__-__'.
22
+ * @param {object} replacement - An object mapping placeholder characters to regex patterns, e.g., { "_": "\\d" }.
23
+ * @returns {string|null} - A regular expression string or null if inputs are invalid.
24
+ */
25
+ export function generateRegexFromMask(mask, replacement) {
26
+ if (!mask || !replacement || typeof replacement !== 'object') return null;
27
+
28
+ let regex = '^';
29
+ for (const char of mask) {
30
+ regex += replacement[char] ? `(${replacement[char]})` : escapeRegex(char);
31
+ }
32
+ regex += '$';
33
+ return regex;
34
+ }
35
+
36
+ export const mongoDBWhitelist = [
37
+ "$$NOW", "$in", "$eq", "$gt", "$gte", "$in", "$lt", "$lte", "$ne", "$nin", "$type", "$size",
38
+ "$and", "$not", "$nor", "$or", "$regexMatch", "$find", "$elemMatch", "$filter", "$toString", "$toObjectId",
39
+ "$concat",
40
+ '$add', '$subtract', '$multiply', '$divide', '$mod', '$pow', "$sqrt",
41
+ "$rand",
42
+ "$abs", '$sin', '$cos', '$tan', '$asin', '$acos', '$atan',
43
+ "$toDate", "$toBool", "$toString", "$toInt", "$toDouble",
44
+ "$dateDiff", "$dateSubtract", "$dateAdd", "$dateToString",
45
+ '$year', '$month', '$week', '$dayOfMonth', '$dayOfWeek', '$dayOfYear', '$hour', '$minute', '$second', '$millisecond',
46
+ '$geoNear', '$regex', '$text', '$nearSphere','$geoWithin','$geoIntersects'
47
+ ];
48
+ export let importJobs = {};
49
+
50
+
51
+ /**
52
+ * Exécute une tâche d'import/export (parsing, stringify) dans un worker thread.
53
+ * @param {('parse-json'|'parse-csv'|'stringify-json')} action - L'action à effectuer.
54
+ * @param {object} payload - Les données nécessaires pour l'action.
55
+ * @returns {Promise<any>} - Une promesse qui se résout avec les données traitées.
56
+ */
57
+ export function runImportExportWorker(action, payload) {
58
+ return new Promise((resolve, reject) => {
59
+ const workerPath = path.resolve(process.cwd(), './src/workers/import-export-worker.js');
60
+ const worker = new Worker(workerPath);
61
+
62
+ worker.postMessage({ action, payload });
63
+
64
+ worker.on('message', (result) => {
65
+ if (result.success) {
66
+ resolve(result.data);
67
+ } else {
68
+ // Correction : On s'assure de toujours passer une chaîne de caractères à new Error()
69
+ const errorMessage = result.error || `Import/Export Worker failed with an unknown error. Action: ${action}.`;
70
+ reject(new Error(errorMessage));
71
+ }
72
+ worker.terminate();
73
+ });
74
+
75
+ worker.on('error', (err) => {
76
+ reject(err);
77
+ worker.terminate();
78
+ });
79
+
80
+ worker.on('exit', (code) => {
81
+ if (code !== 0) {
82
+ reject(new Error(`Import/Export Worker stopped with exit code ${code}`));
83
+ }
84
+ });
85
+ });
86
+ }
87
+ /** Exécute une tâche de cryptographie dans un worker thread.
88
+ * @param {('encrypt'|'decrypt'|'hash')} action - L'action à effectuer.
89
+ * @param {object} payload - Les données nécessaires pour l'action.
90
+ * @returns {Promise<any>} - Une promesse qui se résout avec le résultat (si pertinent).
91
+ */
92
+ export function runCryptoWorkerTask(action, payload) {
93
+ return new Promise((resolve, reject) => {
94
+ const workerPath = path.resolve(__dirname, '../../workers/crypto-worker.js');
95
+ const worker = new Worker(workerPath);
96
+
97
+ worker.postMessage({ action, payload });
98
+
99
+ worker.on('message', (result) => {
100
+ if (result.success) {
101
+ resolve(result.data); // Résout avec les données (ex: le hash) ou undefined si pas de retour
102
+ } else {
103
+ reject(new Error(result.error));
104
+ }
105
+ worker.terminate();
106
+ });
107
+
108
+ worker.on('error', (err) => {
109
+ reject(err);
110
+ worker.terminate();
111
+ });
112
+
113
+ worker.on('exit', (code) => {
114
+ if (code !== 0) {
115
+ reject(new Error(`Crypto Worker stopped with exit code ${code}`));
116
+ }
117
+ });
118
+ });
93
119
  }