data-primals-engine 1.0.3 → 1.0.5

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 CHANGED
@@ -30,13 +30,16 @@
30
30
 
31
31
  ## ⚡ Quick Start
32
32
 
33
+ ```bash
34
+ npm i data-primals-engine
35
+ ```
36
+ or
33
37
  ```bash
34
38
  git clone https://your-repo/data-primals-engine.git
35
39
  cd data-primals-engine
36
40
  npm install
37
41
  ```
38
-
39
- Create a `.env` file:
42
+ Possibly create a `.env` file:
40
43
  ```env
41
44
  MONGO_DB_URL=mongodb://127.0.0.1:27017
42
45
  ```
@@ -197,14 +200,15 @@ curl -X DELETE http://localhost:7633/api/data?_user=demo \
197
200
  ```
198
201
  data-primals-engine/
199
202
  ├── src/
200
- │ ├── engine.js
203
+ │ ├── middlewares/
204
+ │ ├── migrations/
201
205
  │ ├── modules/
202
- │ ├── providers/
203
- │ ├── constants.js
204
- │ ├── defaultModels.js
205
- ├── config/
206
- │ ├── models/
207
- └── packs/
206
+ │ ├── workers/
207
+ │ ├── engine.js // The Express engine that serves the API
208
+ │ ├── constants.js // The inner-application constants definitions
209
+ ├── packs.js // The packs that will be loaded and available with installPack() method
210
+ │ ├── defaultModels.js // The default models available to import
211
+ ├── ...
208
212
  └── server.js
209
213
  ```
210
214
 
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "data-primals-engine",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
4
4
  "description": "data-primals-engine is the package responsible from handling large amount of data in a practical and performant way. It can handle large amount of data in a practical and performant way. It can also get workflow models working (for automation), and fully supports internationalisation.",
5
5
  "main": "src/engine.js",
6
6
  "type": "module",
7
7
  "scripts": {
8
- "preinstall": "npx npm-force-resolutions",
8
+ "preinstall": "npx force-resolutions",
9
9
  "devserver": "cross-env NODE_ENV=development node server.js",
10
10
  "server": "cross-env NODE_ENV=production node server.js",
11
11
  "migrate:create": "node src/migrate.js create",
@@ -48,7 +48,6 @@
48
48
  "express-csrf-double-submit-cookie": "^1.2.1",
49
49
  "express-rate-limit": "^8.0.1",
50
50
  "express-session": "^1.18.1",
51
- "express-sitemap-xml": "^3.1.0",
52
51
  "juice": "^11.0.1",
53
52
  "mathjs": "^14.4.0",
54
53
  "mongodb": "^6.12.0",
package/server.js CHANGED
@@ -4,34 +4,8 @@
4
4
 
5
5
  //set ES modules to be loaded by the script
6
6
  import process from "node:process";
7
- import {Config} from "../data-primals-engine/src/config.js";
8
- import {Engine} from "../data-primals-engine/src/engine.js";
9
- import {BenchmarkTool, GameObject, Logger} from "../data-primals-engine/src/gameObject.js";
10
- import {
11
- createModel, DATAS_API_TOKEN,
12
- getModels, getResource,
13
- searchData,
14
- validateModelStructure
15
- } from "../data-primals-engine/src/modules/data.js";
16
- import {availableLangs, install, langs, maxBytesPerSecondThrottleFile} from "../data-primals-engine/src/constants.js";
17
- import fs from "node:fs";
18
- import {
19
- event_on, getFileExtension,
20
- shuffle,
21
- uuidv4
22
- } from "../data-primals-engine/src/core.js";
23
- import {datasCollection, filesCollection, modelsCollection, packsCollection} from "../data-primals-engine/src/modules/mongodb.js";
24
- import {translations} from "../data-primals-engine/src/i18n.js";
25
- import {getFieldValueHash} from "../data-primals-engine/src/data.js";
26
-
27
- import swaggerUi from 'swagger-ui-express';
28
-
29
- import YAML from 'yaml';
30
- import util from "node:util";
31
- import path from "node:path";
32
- import { getAllPacks} from "../data-primals-engine/src/packs.js";
33
- import {middlewareAuthenticator, userInitiator} from "../data-primals-engine/src/modules/user.js";
34
- import {defaultModels} from "../data-primals-engine/src/defaultModels.js";
7
+ import {Config, Engine, BenchmarkTool, GameObject, Logger} from "./src/index.js";
8
+
35
9
  Config.Set("modules", ["mongodb", "data", "file", "bucket", "workflow","user", "assistant", "swagger"])
36
10
  Config.Set("middlewares", []);
37
11
 
package/src/constants.js CHANGED
@@ -40,6 +40,10 @@ export const awsDefaultConfig = {
40
40
  region: 'eu-north-1',
41
41
  }
42
42
 
43
+ export const emailDefaultConfig = {
44
+ from: "Support - data@primals.net <data@primals.net>"
45
+ }
46
+
43
47
  // 10000 tiny users
44
48
  // 1000 modern users
45
49
  // 100 mega utilisateurs potentiality
package/src/data.js CHANGED
@@ -420,6 +420,7 @@ export const getFieldValueHash = (model, data) => {
420
420
  * @returns {string} - La chaîne de caractères représentant la donnée.
421
421
  */
422
422
  export const getDataAsString = (model, data, t, allModels, extended=false) => {
423
+
423
424
  // Cas de base : si le modèle ou les données sont manquants, on ne peut rien faire.
424
425
  if (!model || !data) {
425
426
  return '';
@@ -484,8 +485,14 @@ export const getDataAsString = (model, data, t, allModels, extended=false) => {
484
485
  return value.value; // Champs traduits
485
486
  }
486
487
 
487
- const translatedValue = t(value, {defaultValue: value});
488
- return translatedValue || value.toString();
488
+ if( typeof(value) === 'string' ) {
489
+ const translatedValue = t(value, {defaultValue: value});
490
+ return translatedValue || value.toString();
491
+ }
492
+ if( typeof(value) === 'object' ) {
493
+ return JSON.stringify(value, null, 2);
494
+ }
495
+ return value;
489
496
 
490
497
  }).filter(Boolean).join(', ');
491
498
 
package/src/email.js CHANGED
@@ -1,4 +1,4 @@
1
- import { translations } from "../../data-primals-engine/src/i18n.js";
1
+ import { translations } from "data-primals-engine/i18n";
2
2
  import process from "node:process";
3
3
  import nodemailer from "nodemailer";
4
4
  import juice from "juice";
@@ -44,14 +44,14 @@ const createTransporter = (smtpConfig) => {
44
44
  * @param {string|null} tpl - Le template HTML à utiliser.
45
45
  */
46
46
  export const sendEmail = async (email = "", data, smtpConfig = null, lang, tpl = null) => {
47
- const contactEmail = smtpConfig ? (smtpConfig.from || "Our company <noreply@ourdomain.tld>") : "Support - data@primals.net <data@primals.net>";
47
+ const contactEmail = smtpConfig ? (smtpConfig.from || emailDefaultConfig.from) :"Our company <noreply@ourdomain.tld>";
48
48
  const emails = Array.isArray(email) ? email : [email];
49
49
  if (emails.length === 0) return;
50
50
 
51
51
  // Choisir le transporteur à utiliser
52
52
  const transporter = smtpConfig ? createTransporter(smtpConfig) : defaultTransporter;
53
53
 
54
- if (tpl === null) tpl = etemplate1(data, lang);
54
+ if (tpl === null) tpl = event_trigger("sendEmail:template",data, lang);
55
55
  let html = tpl;
56
56
  try {
57
57
  html = juice(tpl);
@@ -80,205 +80,6 @@ export const sendEmail = async (email = "", data, smtpConfig = null, lang, tpl =
80
80
  }
81
81
  };
82
82
 
83
- export const etemplate1 = (data, lang = "de", font = "Roboto") => {
84
- const { title, content } = data;
85
- // S'assurer que les traductions existent pour la langue demandée
86
- const trs = translations[lang]?.translation || translations['en']['translation'];
87
- return (
88
- `<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
89
- <html xmlns="http://www.w3.org/1999/xhtml">
90
- <head>
91
- <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
92
- <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
93
- <title>Newsletter web.primals.net</title>
94
- <script type="application/ld+json">
95
- {
96
- "@context": "http://schema.org",
97
- "@type": "EmailMessage",
98
- "potentialAction": {
99
- "@type": "ViewAction",
100
- "url": "https://web.primals.net",
101
- "name": "web.primals.net"
102
- }
103
- }
104
- </script>
105
- <style type="text/css">
106
- /* Based on The MailChimp Reset INLINE: Yes. */
107
- /* Client-specific Styles */
108
- #outlook a {padding:0;} /* Force Outlook to provide a "view in browser" menu link. */
109
- body{width:100% !important; -webkit-text-size-adjust:100%; -ms-text-size-adjust:100%; margin:0; padding:0; color:black}
110
- /* Prevent Webkit and Windows Mobile platforms from changing default font sizes.*/
111
- .ExternalClass {width:100%;} /* Force Hotmail to display emails at full width */
112
- .ExternalClass, .ExternalClass p, .ExternalClass span, .ExternalClass font, .ExternalClass td, .ExternalClass div {line-height: 100%;}
113
- /* Forces Hotmail to display normal line spacing. More on that: http://www.emailonacid.com/forum/viewthread/43/ */
114
- #backgroundTable {margin:0; padding:0; width:100% !important; line-height: 100% !important;}
115
- /* End reset */
116
-
117
- /* Some sensible defaults for images
118
- Bring inline: Yes. */
119
- img {outline:none; text-decoration:none; -ms-interpolation-mode: bicubic;}
120
- a img {border:none;}
121
- .image_fix {display:block;}
122
-
123
- /* Yahoo paragraph fix
124
- Bring inline: Yes. */
125
- p {margin: 0;}
126
-
127
- /* Hotmail header color reset
128
- Bring inline: Yes. */
129
- h1, h2, h3, h4, h5, h6 {color: black !important;}
130
-
131
- h1 a, h2 a, h3 a, h4 a, h5 a, h6 a {color: blue !important;}
132
-
133
- h1 a:active, h2 a:active, h3 a:active, h4 a:active, h5 a:active, h6 a:active {
134
- color: red !important; /* Preferably not the same color as the normal header link color. There is limited support for psuedo classes in email clients, this was added just for good measure. */
135
- }
136
-
137
- h1 a:visited, h2 a:visited, h3 a:visited, h4 a:visited, h5 a:visited, h6 a:visited {
138
- color: purple !important; /* Preferably not the same color as the normal header link color. There is limited support for psuedo classes in email clients, this was added just for good measure. */
139
- }
140
-
141
- /* Outlook 07, 10 Padding issue fix
142
- Bring inline: No.*/
143
- table td {border-collapse: collapse; }
144
-
145
- /* Remove spacing around Outlook 07, 10 tables
146
- Bring inline: Yes */
147
-
148
- /* Styling your links has become much simpler with the new Yahoo. In fact, it falls in line with the main credo of styling in email and make sure to bring your styles inline. Your link colors will be uniform across clients when brought inline.
149
- Bring inline: Yes. */
150
- a {color: #1381b1;}
151
- p { color:black; }
152
-
153
- h2 {
154
- padding: 20px 0;
155
- }
156
-
157
-
158
- /***************************************************
159
- ****************************************************
160
- MOBILE TARGETING
161
- ****************************************************
162
- ***************************************************/
163
- @media only screen and (max-device-width: 480px) {
164
- /* Part one of controlling phone number linking for mobile. */
165
- a[href^="tel"], a[href^="sms"] {
166
- text-decoration: none;
167
- color: blue; /* or whatever your want */
168
- pointer-events: none;
169
- cursor: default;
170
- }
171
-
172
- .mobile_link a[href^="tel"], .mobile_link a[href^="sms"] {
173
- text-decoration: default;
174
- color: orange !important;
175
- pointer-events: auto;
176
- cursor: default;
177
- }
178
-
179
- }
180
-
181
- /* More Specific Targeting */
182
-
183
- @media only screen and (min-device-width: 768px) and (max-device-width: 1024px) {
184
- /* You guessed it, ipad (tablets, smaller screens, etc) */
185
- /* repeating for the ipad */
186
- a[href^="tel"], a[href^="sms"] {
187
- text-decoration: none;
188
- color: blue; /* or whatever your want */
189
- pointer-events: none;
190
- cursor: default;
191
- }
192
-
193
- .mobile_link a[href^="tel"], .mobile_link a[href^="sms"] {
194
- text-decoration: default;
195
- color: orange !important;
196
- pointer-events: auto;
197
- cursor: default;
198
- }
199
- }
200
-
201
- @media only screen and (-webkit-min-device-pixel-ratio: 2) {
202
- /* Put your iPhone 4g styles in here */
203
- }
204
-
205
- /* Android targeting */
206
- @media only screen and (-webkit-device-pixel-ratio:.75){
207
- /* Put CSS for low density (ldpi) Android layouts in here */
208
- }
209
- @media only screen and (-webkit-device-pixel-ratio:1){
210
- /* Put CSS for medium density (mdpi) Android layouts in here */
211
- }
212
- @media only screen and (-webkit-device-pixel-ratio:1.5){
213
- /* Put CSS for high density (hdpi) Android layouts in here */
214
- }
215
- /* end Android targeting */
216
-
217
- </style>
218
-
219
- <!-- Targeting Windows Mobile -->
220
- <!--[if IEMobile 7]>
221
- <style type="text/css">
222
-
223
- </style>
224
- <![endif]-->
225
-
226
- <!-- ***********************************************
227
- ****************************************************
228
- END MOBILE TARGETING
229
- ****************************************************
230
- ************************************************ -->
231
-
232
- <!--[if gte mso 9]>
233
- <style>
234
- /* Target Outlook 2007 and 2010 */
235
- </style>
236
- <![endif]-->
237
- </head>
238
- <body><style>
239
- @import url(https://fonts.googleapis.com/css?family=${font});
240
-
241
- /* Type styles for all clients */
242
- h1,h2,h3 {
243
- font-family: Helvetica, Arial, serif;
244
- text-align: center;
245
- }
246
- h3 {
247
- margin: 0;
248
- }
249
- p{
250
- font-family: Helvetica, Arial, serif;
251
- }
252
- /* Type styles for WebKit clients */
253
- @media screen and (-webkit-min-device-pixel-ratio:0) {
254
- h1,h2,h3 {
255
- font-family: ${font}, Helvetica, Arial, serif !important;
256
- }
257
- }
258
- </style>` +
259
- etable(
260
- etable(
261
- erow(
262
- etag("h2", {'text-align': 'left'}, title) +
263
- etable(
264
- erow(etag('p', {}, trs[content] || content || ''), { 'text-align': 'left' })+
265
- erow(etag('p', {}, trs['email.defaultSignature'] ||'')),
266
- { 'text-align': 'left' }),
267
- { 'text-align': 'left' }
268
- )+
269
- erow(
270
- etag('hr', {}, '')+
271
- etag('a', { href: "https://data.primals.net"}, 'https://data.primals.net')+
272
- etag('div', {}, 'Service gratuit de base de donnée en ligne')+
273
- etag("div", {}, 'Support: '+etag('a', { href:'mailto:data@primals.net'}, 'data@primals.net')),
274
- { 'text-align': 'left' }
275
- )
276
- ),
277
- { width: "1024px", 'text-align': "left" },
278
- )
279
- );
280
- };
281
-
282
83
  export const etag = (name, attrs, content = "") => {
283
84
  return (
284
85
  "<" +
package/src/index.js CHANGED
@@ -1,4 +1,4 @@
1
- // data-primals-engine/src/index.js
1
+ // data-primals-engine/index
2
2
 
3
3
  // --- Core Engine & Utilities ---
4
4
  export { Engine } from './engine.js';
@@ -7,7 +7,7 @@ import { ChatGoogleGenerativeAI } from "@langchain/google-genai";
7
7
  import { HumanMessage, SystemMessage } from "@langchain/core/messages";
8
8
  import {searchData, patchData, deleteData, insertData} from "./data.js";
9
9
  import { getDataAsString } from "../data.js";
10
- import i18n from "../../../data-primals-engine/src/i18n.js";
10
+ import i18n from "data-primals-engine/i18n";
11
11
  import {generateLimiter} from "./user.js";
12
12
  import rateLimit from "express-rate-limit";
13
13
 
@@ -24,7 +24,13 @@ export const assistantGlobalLimiter = rateLimit({
24
24
  }
25
25
  });
26
26
 
27
- const createSystemPrompt = (modelDefs, conditionBuilderExample) =>`
27
+ const createSystemPrompt = (modelDefs) => {
28
+ const cond1 = JSON.stringify({ "model": "event", "sort": "startDate:ASC", "limit": 0, "filter": { "$and": [{ "$nin": ["$tags", "festival"] }, { "$lt": ["$endDate", "$$NOW"] }] }}, null, 2);
29
+ const cond2 = JSON.stringify({ "model": "contact", "sort": "legalName:ASC", "limit": 5,"filter": {"$ne": [{ "$type": "$legalName"}, "missing"] }}, null, 2);
30
+ const cond3 = JSON.stringify({ "model": "order", "sort": "_id:DESC", "limit": 10,"filter": {"lang": { "$find": [{ "$eq": ["$$this.code", "fr"] }] } }}, null, 2);
31
+ const cond4 = JSON.stringify({ "model": "order", "sort": "updatedAt:DESC", "limit": 0,"filter": {"user": { "$find": { "roles": { "$find": [{ "$in": ["$$this.name", ["admin", "moderator"]] }] } }}}}, null, 2);
32
+
33
+ return `
28
34
  Tu es "Prior", un assistant expert en analyse de données pour la plateforme data.primals.net.
29
35
  Ta mission est d'aider l'utilisateur en répondant à ses questions sur ses données.
30
36
 
@@ -55,7 +61,8 @@ Tu as accès aux outils suivants. Tu ne dois utiliser QUE ces outils.
55
61
  5. **displayMessage**: Pour répondre directement à l'utilisateur. N'utilise cette action QUE lorsque tu as toutes les informations nécessaires pour formuler une réponse finale.
56
62
  - Utilisation: \`{ "action": "displayMessage", "params": { "message": "Ta réponse textuelle." } }\`
57
63
 
58
- La spécification suivante peut t'aider à construire les filtres) est la suivante :
64
+ La spécification, pour t'aider à construire les filtres, est la suivante :
65
+ utilise une chaine de caractère convertible en ObjectId (mongodb) lorsque le nom du champ est _id
59
66
  utilise une chaine de caracteres lorsque le type de champ est : code, string, string_t , password, url, phone, email, richtext
60
67
  utilise une structure { "iso2langcode":"content..." } pour le champ multi-traductions richtext_t
61
68
  utilise un booléen pour : boolean
@@ -76,8 +83,17 @@ PROCESSUS DE RAISONNEMENT:
76
83
  CONTEXTE ACTUEL:
77
84
  - L'utilisateur a accès aux modèles de données suivants et ne peut utiliser les filtres que sur les champs associés:
78
85
  ${modelDefs.map(m => ` - Modèle "${m.name}":\n Champs: ${JSON.stringify(m.fields.map(f => ({ name: f.name, type: f.type, hint: f.hint })), null, 2)}`).join('\n')}
79
- - Le format pour les filtres ($FILTER) est : ${conditionBuilderExample}
80
86
  - Le format de $DATA est { modelFieldName: "value", otherModelFieldName: { subObj : true } }
87
+ - Exemples de filtres "params" utilisables :
88
+ Je voudrais les événements non terminés, qui ne sont pas des festivals ou des salons :
89
+ \`${cond1}\`
90
+ Donne moi les 5 nouvelles entreprises
91
+ \`${cond2}\`
92
+ Je veux les 10 dernières traductions ajoutées dans la langue française.
93
+ \`${cond3}\`
94
+ Je veux les commandes qui ont été faites par un admin ou un modérateur
95
+ \`${cond4}\`
96
+ Ton but est de créer un filtre au plus précis, et mets toujours un limit à 1 par défaut pour éviter de faire des actions en masse non désirées.
81
97
 
82
98
 
83
99
  Règles ABSOLUES:
@@ -97,13 +113,19 @@ json
97
113
  \`\`\`
98
114
 
99
115
  Exemple de réponse INCORRECTE (INTERDIT) :
100
- "Je vais d'abord rechercher ces requêtes. Voici la commande que je vais exécuter..."`;
116
+ "Je vais d'abord rechercher ces requêtes. Voici la commande que je vais exécuter..."
117
+ `;
118
+ }
101
119
 
102
120
 
103
121
  /**
104
122
  * Gère la requête de chat, soit en exécutant une action confirmée,
105
123
  * soit en lançant la boucle de raisonnement de l'IA.
106
124
  */
125
+ /**
126
+ * Gère la requête de chat, soit en exécutant une action confirmée,
127
+ * soit en lanant la boucle de raisonnement de l'IA.
128
+ */
107
129
  async function handleChatRequest(message, history, provider, context, user, confirmedAction) {
108
130
 
109
131
  // --- GESTION D'UNE ACTION CONFIRMÉE ---
@@ -152,8 +174,7 @@ async function handleChatRequest(message, history, provider, context, user, conf
152
174
  const relevantModelNames = new Set([modelName, 'alert', 'kpi', "dashboard", "request", "translation"]);
153
175
  const modelDefs = allModels.filter(m => relevantModelNames.has(m.name));
154
176
 
155
- const conditionBuilderExample = JSON.stringify({ "$and": [{ "$eq": ["$fieldName", "value"] }] }, null, 2);
156
- const systemPrompt = createSystemPrompt(modelDefs, conditionBuilderExample);
177
+ const systemPrompt = createSystemPrompt(modelDefs);
157
178
 
158
179
  const conversationHistory = history
159
180
  .filter(msg => !(msg.from === 'bot' && msg.text.startsWith(i18n.t('assistant.welcome'))))
@@ -207,21 +228,27 @@ async function handleChatRequest(message, history, provider, context, user, conf
207
228
 
208
229
  return { success: true, displayMessage: resultString };
209
230
  }
210
-
211
231
  case 'displayMessage':
212
232
  return { success: true, displayMessage: parsedResponse.params.message };
213
-
214
233
  case 'code':
215
- return { success: true, displayMessage: "```json\n" + JSON.stringify(parsedResponse.params.message, null, 2) + "\n```" };
216
-
234
+ return { success: true, displayMessage: "Voici le code : " + parsedResponse.params.code };
217
235
  case 'post':
218
236
  case 'update':
219
- case 'delete':
237
+ case 'delete': {
238
+ const { model, filter, data } = parsedResponse.params;
239
+
240
+ // Un message générique. Les détails seront affichés par le front-end.
241
+ const confirmationMessage = i18n.t('assistant.confirmActionPrompt', "Veuillez confirmer l'action suivante :");
242
+
220
243
  return {
221
244
  success: true,
222
- displayMessage: i18n.t('assistant.confirmAction', `Je vais exécuter : ${parsedResponse.action}. Confirmez-vous ?`),
223
- confirmationRequest: parsedResponse
245
+ model,
246
+ filter,
247
+ data,
248
+ displayMessage: confirmationMessage, // Message détaillé pour l'utilisateur
249
+ confirmationRequest: parsedResponse // Action complète pour l'exécution
224
250
  };
251
+ }
225
252
 
226
253
  default:
227
254
  return {
@@ -234,7 +261,6 @@ async function handleChatRequest(message, history, provider, context, user, conf
234
261
  logger.warn("[Assistant] La boucle a atteint le nombre maximum de tours sans réponse.");
235
262
  return { success: true, displayMessage: i18n.t('assistant.loopTimeout', "Désolé, je n'ai pas réussi à terminer ma pensée. Pouvez-vous reformuler votre demande ?"), actions: [] };
236
263
  }
237
-
238
264
  /**
239
265
  * Exécute une action de modification (post, update, delete) après confirmation de l'utilisateur.
240
266
  * @param {string} action - L'action à exécuter ('post', 'update', 'delete').
@@ -9,7 +9,7 @@ import {Logger} from "../gameObject.js";
9
9
  import {middlewareAuthenticator, myFreePremiumAnonymousLimiter, userInitiator} from "./user.js";
10
10
  import {awsDefaultConfig, maxBytesPerSecondThrottleData} from "../constants.js";
11
11
  import crypto from "node:crypto";
12
- import i18n from "../../../data-primals-engine/src/i18n.js";
12
+ import i18n from "data-primals-engine/i18n";
13
13
  import {sendEmail} from "../email.js";
14
14
  import {throttleMiddleware} from "../middlewares/throttle.js";
15
15
 
@@ -188,9 +188,6 @@ export async function onInit(defaultEngine) {
188
188
  // Autres validations possibles (longueur, format de la région, etc.)
189
189
 
190
190
  try {
191
- const primalsDb = MongoClient.db("primals"); // Assure-toi que MongoClient est bien ton client MongoDB connecté
192
- const usersCollection = primalsDb.collection("users");
193
-
194
191
  const updateData = {
195
192
  's3Config.bucketName': bucketName,
196
193
  's3Config.accessKeyId': accessKeyId, // Stocké en clair (généralement acceptable)
@@ -207,18 +204,15 @@ export async function onInit(defaultEngine) {
207
204
  // Pour l'instant, on ne touche pas à s3Config.secretAccessKey si req.body.secretAccessKey est vide.
208
205
  }
209
206
 
210
- const result = await usersCollection.updateOne(
207
+ const result = await engine.userProvider.updateUser(
211
208
  { username: user.username }, // ou user._id si c'est ce que tu utilises comme identifiant unique
212
- { $set: updateData }
209
+ updateData
213
210
  );
214
211
 
215
- if (result.modifiedCount > 0 || result.matchedCount > 0) { // Succès même si rien n'a été modifié (déjà les bonnes valeurs)
216
- logger.info(`S3 configuration updated for user ${user.username}`);
212
+ if (result) { // Succès même si rien n'a été modifié (déjà les bonnes valeurs)
217
213
  res.json({ success: true, message: "S3 configuration updated successfully." });
218
214
  } else {
219
- logger.warn(`S3 configuration update: User ${user.username} not found or no changes made.`);
220
- // Si l'utilisateur n'est pas trouvé, c'est une erreur 404
221
- const userExists = await usersCollection.findOne({ username: user.username });
215
+ const userExists = engine.userProvider.findUserByUsername(user.username);
222
216
  if (!userExists) {
223
217
  return res.status(404).json({ success: false, error: "User not found." });
224
218
  }
@@ -8,7 +8,6 @@ import sanitizeHtml from 'sanitize-html';
8
8
  import * as tar from "tar";
9
9
  import process from "node:process";
10
10
  import {randomColor} from "randomcolor";
11
- import rateLimit from "express-rate-limit";
12
11
  import cronstrue from 'cronstrue/i18n.js';
13
12
  import { setTimeoutMiddleware } from '../middlewares/timeout.js';
14
13
 
@@ -65,8 +64,7 @@ import {
65
64
  import fs from "node:fs";
66
65
  import schedule from "node-schedule";
67
66
  import {middleware} from "../middlewares/middleware-mongodb.js";
68
- import ExpressSitemap from "express-sitemap-xml";
69
- import i18n from "../../../data-primals-engine/src/i18n.js";
67
+ import i18n from "data-primals-engine/i18n";
70
68
  import {
71
69
  runScheduledJobWithDbLock,
72
70
  scheduleWorkflowTriggers,
@@ -76,7 +74,6 @@ import NodeCache from "node-cache";
76
74
  import AWS from 'aws-sdk';
77
75
  import {getUserStorageLimit} from "../user.js";
78
76
  import {openaiJobModel} from "../openai.jobs.js";
79
- import {tutorialsConfig} from "../tutorials.js";
80
77
  import checkDiskSpace from "check-disk-space";
81
78
  import { fileURLToPath } from 'url';
82
79
  import { Worker } from 'worker_threads';
@@ -103,10 +100,6 @@ const sseConnections = new Map();
103
100
 
104
101
  const delay = ms => new Promise(res => setTimeout(res, ms));
105
102
 
106
- const isProduction = process.env.NODE_ENV === 'production';
107
- export const urlData = isProduction ? 'https://data.primals.net/' : 'http://localhost:5173/';
108
- export const DATAS_API_TOKEN = process.env.DATAS_API_TOKEN;
109
-
110
103
  const backupDir = process.env.BACKUP_DIR || './backups'; // Répertoire de stockage des sauvegardes
111
104
  const execAsync = promisify(exec);
112
105
 
@@ -1123,33 +1116,6 @@ export async function onInit(defaultEngine) {
1123
1116
  engine = defaultEngine;
1124
1117
  logger = engine.getComponent(Logger);
1125
1118
 
1126
- const loadFromDb = async () => {
1127
- const webpages = await searchData({user: {username:'primals', token: DATAS_API_TOKEN}, query: { depth: 2, model: 'webpage', filter:{ $eq: ['$inSitemap', true]}}});
1128
- const contents = await searchData({user: {username:'primals', token: DATAS_API_TOKEN}, query: { depth: 2, model: 'content', filter:{ $eq: ['$inSitemap', true]}}});
1129
- const alls = [
1130
- "/",
1131
- ];
1132
-
1133
- availableLangs.forEach(l => {
1134
- alls.push('/?lang='+l);
1135
- alls.push('/cgu?lang='+l);
1136
- alls.push('/'+l+'/documentation');
1137
- });
1138
-
1139
- webpages.data.forEach(w => {
1140
- alls.push('/'+w.lang.code+(w.category.identifier?'/'+w.category.identifier:'')+(w.slug||''));
1141
- });
1142
- contents.data.forEach(w => {
1143
- alls.push('/'+w.lang.code+(w.category.identifier?'/'+w.category.identifier:'')+(w.slug||''));
1144
- });
1145
-
1146
- return alls;
1147
- };
1148
-
1149
- engine.use(
1150
- ExpressSitemap(loadFromDb, "https://data.primals.net", { maxAge: 60000 }),
1151
- );
1152
-
1153
1119
  engine.use(middleware({ whitelist: [
1154
1120
  "$$NOW", "$in", "$eq", "$gt", "$gte", "$in", "$lt", "$lte", "$ne", "$nin", "$type", "$size",
1155
1121
  "$and", "$not", "$nor", "$or", "$regexMatch", "$find", "$elemMatch", "$filter", "$toString", "$toObjectId",
@@ -1160,7 +1126,6 @@ export async function onInit(defaultEngine) {
1160
1126
  "$toDate", "$toBool", "$toString", "$toInt", "$toDouble",
1161
1127
  "$dateSubtract", "$dateAdd", "$dateToString",
1162
1128
  '$year', '$month', '$week', '$dayOfMonth', '$dayOfWeek', '$dayOfYear', '$hour', '$minute', '$second', '$millisecond',
1163
-
1164
1129
  ]}));
1165
1130
 
1166
1131
  let modelsCollection, datasCollection, filesCollection, packsCollection, magnetsCollection;
@@ -1263,24 +1228,6 @@ export async function onInit(defaultEngine) {
1263
1228
  return await usersCollection.updateOne({ username: user.username }, { $set: data }, { upsert: true })
1264
1229
  }
1265
1230
 
1266
- /**
1267
- * Remplace les placeholders dans un objet filtre.
1268
- * Gère {{userId}}.
1269
- */
1270
- const processFilterPlaceholders = (filter, user) => {
1271
- const processedFilter = JSON.parse(JSON.stringify(filter));
1272
- for (const key in processedFilter) {
1273
- if (processedFilter[key] === '{{userId}}') {
1274
- // Dans le système Primals, le champ utilisateur est souvent `_user`
1275
- // et contient le nom d'utilisateur, pas l'ID. Adaptez si besoin.
1276
- processedFilter[key] = user.username;
1277
- } else if (typeof processedFilter[key] === 'object' && processedFilter[key] !== null) {
1278
- processedFilter[key] = processFilterPlaceholders(processedFilter[key], user);
1279
- }
1280
- }
1281
- return processedFilter;
1282
- };
1283
-
1284
1231
  // Dans C:/Dev/hackersonline-engine/server/src/modules/data.js, dans onInit()
1285
1232
 
1286
1233
  engine.post('/api/magnets', [middlewareAuthenticator, userInitiator], async (req, res) => {
@@ -1385,127 +1332,6 @@ export async function onInit(defaultEngine) {
1385
1332
  }
1386
1333
  });
1387
1334
 
1388
- engine.post('/api/tutorials/set-active', middlewareAuthenticator, async (req, res) => {
1389
- try {
1390
- const { tutorialState } = req.body;
1391
- const user = req.me;
1392
-
1393
- if (tutorialState !== null && (typeof tutorialState !== 'object' || !tutorialState.id)) {
1394
- return res.status(400).json({ error: 'Invalid tutorial state payload.' });
1395
- }
1396
-
1397
- // Créer une représentation de l'utilisateur mis à jour pour la réponse
1398
- const updatedData = { activeTutorial: tutorialState };
1399
-
1400
- await saveUser(user, updatedData);
1401
-
1402
- // --- MODIFICATION ---
1403
- res.json({
1404
- success: true,
1405
- updatedData // Renvoyer l'objet utilisateur complet
1406
- });
1407
-
1408
- } catch (error) {
1409
- console.error('[Tutoriel Set Active Error]', error);
1410
- res.status(500).json({ error: 'Error setting active tutorial.', details: error.message });
1411
- }
1412
- });
1413
-
1414
- // ...
1415
-
1416
- engine.post('/api/tutorials/:tutorialId/claim-rewards', middlewareAuthenticator, async (req, res) => {
1417
- try {
1418
- const { tutorialId } = req.params;
1419
- const user = req.me; // L'objet utilisateur est déjà chargé
1420
- const tutorial = tutorialsConfig.find(t => t.id === tutorialId);
1421
-
1422
- if (!tutorial) return res.status(404).json({ error: 'Tutoriel non trouvé.' });
1423
- if (!tutorial.rewards) return res.status(400).json({ error: 'Ce tutoriel n\'a pas de récompenses.' });
1424
- if (user.completedTutorials?.includes(tutorialId)) {
1425
- return res.status(400).json({ error: 'Tutoriel déjà terminé.' });
1426
- }
1427
-
1428
- const { xpBonus, skill, achievement, notification } = tutorial.rewards;
1429
-
1430
- // --- LOGIQUE CORRIGÉE ---
1431
- let newData= {};
1432
- newData.xp = newData.xp || 0;
1433
- newData.achievements = newData.achievements || [];
1434
- newData.skills = newData.skills || [];
1435
- newData.completedTutorials = newData.completedTutorials || [];
1436
-
1437
- // Appliquer les récompenses directement sur l'objet newData
1438
- if (xpBonus) newData.xp += xpBonus;
1439
- if (achievement && !newData.achievements.includes(achievement)) newData.achievements.push(achievement);
1440
- if (skill) {
1441
- const existingSkill = newData.skills.find(s => s.name === skill.name);
1442
- if (existingSkill) {
1443
- existingSkill.points += skill.points;
1444
- } else {
1445
- newData.skills.push({ name: skill.name, points: skill.points });
1446
- }
1447
- }
1448
-
1449
- newData.completedTutorials.push(tutorialId);
1450
- newData.activeTutorial = null;
1451
-
1452
- await saveUser(user, newData);
1453
-
1454
- const translatedNotification = {
1455
- title: i18n.t(notification.title, notification.title),
1456
- message: i18n.t(notification.message, notification.message),
1457
- };
1458
-
1459
- res.json({
1460
- success: true,
1461
- userUpdate: newData, // Renvoyer l'objet utilisateur complet et mis à jour
1462
- notification: translatedNotification
1463
- });
1464
-
1465
- } catch (error) {
1466
- console.error('[Tutoriel Rewards Error]', error);
1467
- res.status(500).json({ error: 'Erreur lors de l\'attribution des récompenses.', details: error.message });
1468
- }
1469
- });
1470
-
1471
-
1472
- /**
1473
- * Route pour vérifier si une condition de complétion est remplie.
1474
- * Utilise la fonction `searchData` existante pour interroger les données.
1475
- */
1476
- engine.post('/api/tutorials/check-completion', middlewareAuthenticator, async (req, res) => {
1477
- try {
1478
- const { model, filter, limit } = req.body;
1479
- const user = req.me;
1480
-
1481
- if (!model || !filter || limit === undefined) {
1482
- return res.status(400).json({ error: 'Payload de condition de complétion invalide.' });
1483
- }
1484
-
1485
- const processedFilter = processFilterPlaceholders(filter, user);
1486
-
1487
- // On utilise la fonction de recherche interne de l'application
1488
- const searchResult = await searchData({
1489
- query: {
1490
- model,
1491
- filter: processedFilter,
1492
- limit, // Optimisation : pas besoin de plus de résultats
1493
- page: 1,
1494
- },
1495
- user: user,
1496
- });
1497
-
1498
- // searchData devrait renvoyer un `count` total des documents correspondants
1499
- const isCompleted = searchResult.count >= limit;
1500
-
1501
- res.json({ isCompleted });
1502
-
1503
- } catch (error) {
1504
- console.error('[Tutoriel Check Error]', error);
1505
- res.status(500).json({ error: 'Erreur lors de la vérification de la complétion.', details: error.message });
1506
- }
1507
- });
1508
-
1509
1335
  engine.get('/api/import/progress/:jobId', [middlewareAuthenticator], async (req, res) => {
1510
1336
  const { jobId } = req.params;
1511
1337
  const user = req.me;
@@ -2,7 +2,7 @@
2
2
  // Nouvelle fonction pour ajouter un fichier privé
3
3
  import {maxPrivateFileSize, megabytes} from "../constants.js";
4
4
  import {isLocalUser} from "../data.js";
5
- import i18n from "../../../data-primals-engine/src/i18n.js";
5
+ import i18n from "data-primals-engine/i18n";
6
6
  import {getUserStorageLimit} from "../user.js";
7
7
  import {getCollection} from "./mongodb.js";
8
8
  import {getFileExtension, isGUID, uuidv4} from "../core.js";
@@ -1,4 +1,4 @@
1
- import i18n from "../../../data-primals-engine/src/i18n.js";
1
+ import i18n from "data-primals-engine/i18n";
2
2
  import {MongoClient, MongoDatabase} from "../engine.js";
3
3
  import {getCollection, getCollectionForUser, getUserCollectionName} from "./mongodb.js";
4
4
  import {dbName, plans} from "../constants.js";
@@ -10,7 +10,7 @@ import {maxExecutionsByStep, maxWorkflowSteps} from "../constants.js";
10
10
  import { ChatOpenAI } from "@langchain/openai";
11
11
  import { ChatGoogleGenerativeAI } from "@langchain/google-genai";
12
12
  import { ChatPromptTemplate } from "@langchain/core/prompts";
13
- import i18n from "../../../data-primals-engine/src/i18n.js";
13
+ import i18n from "data-primals-engine/i18n";
14
14
  import {sendEmail} from "../email.js";
15
15
 
16
16
  // 1. ADD THIS IMPORT AT THE TOP OF THE FILE
package/src/providers.js CHANGED
@@ -13,6 +13,15 @@ export class UserProvider {
13
13
  this.engine = engine;
14
14
  }
15
15
 
16
+ /**
17
+ * Met à jour un nouvel utilisateur.
18
+ * @param user
19
+ * @returns {Promise<void>}
20
+ */
21
+ async updateUser(user, data) {
22
+
23
+ }
24
+
16
25
  /**
17
26
  * Trouve un utilisateur par son nom d'utilisateur.
18
27
  * @param {string} username - Le nom d'utilisateur à rechercher.
@@ -57,6 +66,10 @@ export class DefaultUserProvider extends UserProvider {
57
66
  return true;
58
67
  }
59
68
 
69
+ async updateUser(user, data) {
70
+ this.users = this.users.map(user => user.username === user.username ? {...user, ...data} : user);
71
+ }
72
+
60
73
  async initiateUser(req) {
61
74
  req.me = this.users[0];
62
75
  }
package/src/tutorials.js DELETED
@@ -1,112 +0,0 @@
1
- // Fichier : src/tutorials.js
2
-
3
- export const tutorialsConfig = [
4
- {
5
- id: 'contact-management-basics',
6
- name: 'Gestion des contacts : Les bases',
7
- description: 'Apprenez les opérations fondamentales : créer et supprimer un contact.',
8
- icon: 'FaUserPlus',
9
- stages: [
10
- {
11
- stage: 1,
12
- tourName: 'tour-create-contact', // Nom du tour guidé à lancer
13
- name: 'Créez votre premier contact',
14
- description: 'Ajoutez un nouveau contact avec le prénom "Richard".',
15
- completionCondition: { model: 'contact', limit: 1, filter: { "firstName": "Richard" } }
16
- },
17
- {
18
- stage: 2,
19
- tourName: 'tour-delete-contact',
20
- name: 'Effacez vos traces',
21
- description: 'Maintenant, supprimez le contact "Richard" que vous venez de créer.',
22
- // La condition pour valider une suppression est de vérifier que le nombre d'éléments correspondants est 0.
23
- completionCondition: { model: 'contact', limit: 0, filter: { "firstName": "Richard" } }
24
- }
25
- ],
26
- rewards: {
27
- xpBonus: 150,
28
- achievement: 'CONTACT_MANAGER_NOVICE',
29
- notification: { title: 'Gestionnaire de contacts', message: 'Vous maîtrisez les bases de la gestion de contacts !' }
30
- }
31
- },
32
- {
33
- id: 'advanced-permissions',
34
- name: 'Permissions & rôles',
35
- description: 'Maîtrisez la gestion des droits du bout des doigts !',
36
- icon: 'FaUserShield',
37
- stages: [
38
- {
39
- stage: 1,
40
- tourName: 'tour-set-permissions',
41
- name: 'Gestion des droits utilisateurs',
42
- description: 'Trouvez l\'utilisateur "userTuto" et donnez-lui le droit "visitor".',
43
- completionCondition: {model: 'user', limit: 1, filter: {"username": "userTuto", "roles": { "$find": {"$eq":["$$this.name","visitor"]}}}}
44
- },
45
- {
46
- stage: 2,
47
- tourName: 'tour-create-role',
48
- name: 'Créer un nouveau rôle',
49
- description: 'Créez un rôle personnalisé nommé "Modérateur".',
50
- // Condition : un document dans le modèle 'role' avec le nom 'Modérateur' doit exister.
51
- // (On suppose ici l'existence d'un modèle 'role' pour gérer les rôles).
52
- completionCondition: { model: 'role', limit: 1, filter: { "name": "moderator" } }
53
- },
54
- {
55
- stage: 3,
56
- tourName: 'tour-assign-permission-to-role',
57
- name: 'Ajouter une permission au rôle',
58
- description: 'Modifiez le rôle "moderator" pour lui ajouter la permission "API_EDIT_DATA_content".',
59
- // Condition : le rôle 'Modérateur' doit avoir une permission nommée 'delete_contact'.
60
- // (On suppose que le modèle 'role' a un champ 'permissions' qui est un tableau de relations).
61
- completionCondition: { model: 'role', limit: 1, filter: { "name": "moderator", "permissions": { "$find": { "$eq": ["$$this.name", "API_EDIT_DATA_content"] } } } }
62
- },
63
- {
64
- stage: 4,
65
- tourName: 'tour-assign-role-to-user',
66
- name: 'Promouvoir un utilisateur',
67
- description: 'Maintenant, assignez votre rôle "moderator" à l\'utilisateur "userTuto".',
68
- // Condition : l'utilisateur 'userTuto' doit maintenant aussi avoir le rôle 'Modérateur'.
69
- completionCondition: { model: 'user', limit: 1, filter: { "username": "userTuto", "roles": { "$find": { "$eq": ["$$this.name", "moderator"] } } } }
70
- }
71
- ],
72
- rewards: {
73
- xpBonus: 500,
74
- achievement: 'PERMISSION_ARCHITECT',
75
- notification: { title: 'Architecte des Permissions', message: 'Vous savez maintenant comment finement gérer les accès !' }
76
- }
77
- },
78
- {
79
- id: 'requests',
80
- name: 'Suivi d\'Activité',
81
- description: 'Découvrez les succès liés à l\'activité.',
82
- icon: 'FaUserShield',
83
- stages: [
84
- {
85
- stage: 1,
86
- tourName: null, // Pas de tour guidé, l'action peut se faire n'importe où
87
- name: 'Première requête',
88
- description: 'Effectuez des actions pour générer 50 requêtes système pour débloquer votre premier succès d\'activité.',
89
- completionCondition: {model: 'request', limit: 50, filter: {}}
90
- },
91
- {
92
- stage: 2,
93
- tourName: null,
94
- name: 'Activité soutenue',
95
- description: 'Continuez votre activité et atteignez 250 requêtes pour le prochain palier.',
96
- completionCondition: {model: 'request', limit: 250, filter: {}}
97
- },
98
- {
99
- stage: 3,
100
- tourName: null,
101
- name: 'Expert en requêtes',
102
- description: 'Impressionnant ! Atteignez 1000 requêtes pour prouver votre maîtrise.',
103
- completionCondition: {model: 'request', limit: 1000, filter: {}}
104
- }
105
- ],
106
- rewards: {
107
- xpBonus: 1000,
108
- achievement: 'REQUEST_MASTER',
109
- notification: { title: 'Maître des Requêtes', message: 'Votre activité sur le réseau est remarquable !' }
110
- }
111
- }
112
- ];