data-primals-engine 1.0.4 → 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 +13 -9
- package/package.json +1 -2
- package/server.js +2 -28
- package/src/data.js +9 -2
- package/src/email.js +1 -1
- package/src/index.js +1 -1
- package/src/modules/assistant.js +41 -15
- package/src/modules/bucket.js +1 -1
- package/src/modules/data.js +1 -175
- package/src/modules/file.js +1 -1
- package/src/modules/user.js +1 -1
- package/src/modules/workflow.js +1 -1
- package/src/tutorials.js +0 -112
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
|
-
│ ├──
|
|
203
|
+
│ ├── middlewares/
|
|
204
|
+
│ ├── migrations/
|
|
201
205
|
│ ├── modules/
|
|
202
|
-
│ ├──
|
|
203
|
-
│ ├──
|
|
204
|
-
│ ├──
|
|
205
|
-
├──
|
|
206
|
-
│ ├── models
|
|
207
|
-
│
|
|
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,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "data-primals-engine",
|
|
3
|
-
"version": "1.0.
|
|
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",
|
|
@@ -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 "
|
|
8
|
-
|
|
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/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
|
-
|
|
488
|
-
|
|
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
package/src/index.js
CHANGED
package/src/modules/assistant.js
CHANGED
|
@@ -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 "
|
|
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
|
|
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
|
|
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
|
|
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: "
|
|
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
|
-
|
|
223
|
-
|
|
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').
|
package/src/modules/bucket.js
CHANGED
|
@@ -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 "
|
|
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
|
|
package/src/modules/data.js
CHANGED
|
@@ -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
|
|
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 engine.userProvider.updateUser(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;
|
package/src/modules/file.js
CHANGED
|
@@ -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 "
|
|
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";
|
package/src/modules/user.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import i18n from "
|
|
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";
|
package/src/modules/workflow.js
CHANGED
|
@@ -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 "
|
|
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/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
|
-
];
|