data-primals-engine 1.5.1 → 1.5.2
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 +2 -0
- package/client/src/App.scss +1 -1
- package/client/src/DataLayout.jsx +2 -0
- package/client/src/HistoryDialog.jsx +24 -2
- package/client/src/ModelList.jsx +280 -275
- package/client/src/filter.js +1 -0
- package/package.json +6 -6
- package/src/core.js +8 -1
- package/src/engine.js +85 -43
- package/src/events.js +137 -113
- package/src/index.js +3 -0
- package/src/modules/assistant/assistant.js +123 -134
- package/src/modules/assistant/constants.js +2 -1
- package/src/modules/data/data.history.js +32 -8
- package/src/modules/data/data.operations.js +3381 -3282
- package/src/modules/data/data.relations.js +1 -1
- package/src/modules/data/data.routes.js +3 -3
- package/src/modules/user.js +1 -0
- package/src/modules/workflow.js +2 -2
- package/src/openai.jobs.js +3 -2
- package/src/sso.js +2 -2
- package/src/workers/import-export-worker.js +1 -1
- package/test/data.history.integration.test.js +264 -192
- package/test/data.integration.test.js +1206 -1115
|
@@ -1,44 +1,47 @@
|
|
|
1
1
|
import { getCollectionForUser, modelsCollection } from "../mongodb.js";
|
|
2
2
|
import { Logger } from "../../gameObject.js";
|
|
3
|
-
import { ChatOpenAI } from "@langchain/openai";
|
|
4
|
-
import { ChatGoogleGenerativeAI } from "@langchain/google-genai";
|
|
5
3
|
import { HumanMessage, SystemMessage } from "@langchain/core/messages";
|
|
6
4
|
import {searchData, patchData, deleteData, insertData} from "../data/index.js";
|
|
7
|
-
import { getDataAsString } from "../../data.js";
|
|
8
|
-
import i18n from "../../i18n.js";
|
|
9
|
-
import {generateLimiter} from "../user.js";
|
|
10
|
-
import rateLimit from "express-rate-limit";
|
|
11
|
-
import {maxAIReflectiveSteps} from "../../constants.js";
|
|
12
5
|
import {providers} from "./constants.js";
|
|
13
|
-
import {ChatDeepSeek} from "@langchain/deepseek";
|
|
14
|
-
import {ChatAnthropic} from "@langchain/anthropic";
|
|
15
6
|
import {Config} from "../../config.js";
|
|
7
|
+
import { Event } from "../../events.js";
|
|
8
|
+
import rateLimit from "express-rate-limit";
|
|
9
|
+
import {generateLimiter} from "../user.js";
|
|
10
|
+
import {maxAIReflectiveSteps} from "../../constants.js";
|
|
11
|
+
import i18n from "../../i18n.js";
|
|
12
|
+
import {parseSafeJSON} from "../../core.js";
|
|
16
13
|
|
|
17
14
|
let logger = null;
|
|
18
15
|
|
|
19
|
-
export const getAIProvider= (aiProvider, aiModel, apiKey)=>{
|
|
20
|
-
let llm;
|
|
16
|
+
export const getAIProvider= async (aiProvider, aiModel, apiKey)=>{
|
|
21
17
|
try {
|
|
22
18
|
switch (aiProvider) {
|
|
23
|
-
case 'OpenAI':
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
19
|
+
case 'OpenAI': {
|
|
20
|
+
const { ChatOpenAI } = await import("@langchain/openai");
|
|
21
|
+
return new ChatOpenAI({apiKey, model: aiModel, temperature: 0.7});
|
|
22
|
+
}
|
|
23
|
+
case 'Google': {
|
|
24
|
+
const { ChatGoogleGenerativeAI } = await import("@langchain/google-genai");
|
|
25
|
+
return new ChatGoogleGenerativeAI({apiKey, model: aiModel, temperature: 0.7});
|
|
26
|
+
}
|
|
27
|
+
case 'DeepSeek': {
|
|
28
|
+
const { ChatDeepSeek } = await import("@langchain/deepseek");
|
|
29
|
+
return new ChatDeepSeek({apiKey, model: aiModel, temperature: 0.7});
|
|
30
|
+
}
|
|
31
|
+
case 'Anthropic': {
|
|
32
|
+
const { ChatAnthropic } = await import("@langchain/anthropic");
|
|
33
|
+
return new ChatAnthropic({apiKey, model: aiModel, temperature: 0.7});
|
|
34
|
+
}
|
|
35
35
|
default:
|
|
36
36
|
throw new Error(`Unsupported AI provider: ${aiProvider}`);
|
|
37
37
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
38
|
+
} catch (e) {
|
|
39
|
+
if (e.code === 'ERR_MODULE_NOT_FOUND') {
|
|
40
|
+
logger.error(`[Assistant] The package for the '${aiProvider}' provider is not installed. Please run 'npm install @langchain/${aiProvider.toLowerCase()}' to use this provider.`);
|
|
41
|
+
throw new Error(`The AI provider '${aiProvider}' is not installed. Please ask the administrator to install the corresponding package.`);
|
|
42
|
+
}
|
|
43
|
+
logger.error(`[Assistant] Error initializing AI provider '${aiProvider}': ${e.message}`);
|
|
44
|
+
throw e; // Re-throw other errors
|
|
42
45
|
}
|
|
43
46
|
}
|
|
44
47
|
export const assistantGlobalLimiter = rateLimit({
|
|
@@ -317,14 +320,10 @@ COMMANDE FINALE :
|
|
|
317
320
|
* @returns {object} Le filtre corrigé.
|
|
318
321
|
*/
|
|
319
322
|
function correctAIFilter(filter) {
|
|
320
|
-
if (!filter || typeof filter !== 'object' || Array.isArray(filter))
|
|
321
|
-
return filter; // Pas un objet à corriger
|
|
322
|
-
}
|
|
323
|
+
if (!filter || typeof filter !== 'object' || Array.isArray(filter)) return filter;
|
|
323
324
|
|
|
324
325
|
const keys = Object.keys(filter);
|
|
325
|
-
if (keys.length <= 1)
|
|
326
|
-
return filter; // Rien à corriger
|
|
327
|
-
}
|
|
326
|
+
if (keys.length <= 1) return filter;
|
|
328
327
|
|
|
329
328
|
// Vérifie si toutes les clés sont des opérateurs (commencent par '$')
|
|
330
329
|
const allKeysAreOperators = keys.every(key => key.startsWith('$'));
|
|
@@ -335,7 +334,7 @@ function correctAIFilter(filter) {
|
|
|
335
334
|
return { '$and': andConditions };
|
|
336
335
|
}
|
|
337
336
|
|
|
338
|
-
return filter;
|
|
337
|
+
return filter;
|
|
339
338
|
}
|
|
340
339
|
|
|
341
340
|
/**
|
|
@@ -350,30 +349,6 @@ async function executeTool(action, params, user, allModels) {
|
|
|
350
349
|
logger.debug(`[Assistant] Exécution de l'outil: ${action} avec les paramètres:`, params);
|
|
351
350
|
try {
|
|
352
351
|
switch (action) {
|
|
353
|
-
case 'search': {
|
|
354
|
-
const modelDef = allModels.find(m => m.name === params.model);
|
|
355
|
-
if (!modelDef) {
|
|
356
|
-
return `Erreur: Le modèle '${params.model}' n'a pas été trouvé. Impossible de lancer la recherche.`;
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
const searchResult = await searchData({
|
|
360
|
-
model: params.model,
|
|
361
|
-
filter: params.filter,
|
|
362
|
-
limit: params.limit || 10,
|
|
363
|
-
sort: params.sort
|
|
364
|
-
}, user);
|
|
365
|
-
|
|
366
|
-
if (searchResult.data.length === 0) {
|
|
367
|
-
return i18n.t('assistant.noResults', "Aucun résultat trouvé.");
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
const resultString = i18n.t('assistant.searchResults', "Voici les résultats :") +
|
|
371
|
-
searchResult.data.map(item =>
|
|
372
|
-
`\n- ${getDataAsString(modelDef, item, { i18n, t: i18n.t }, allModels, true)}`
|
|
373
|
-
).join('');
|
|
374
|
-
|
|
375
|
-
return resultString;
|
|
376
|
-
}
|
|
377
352
|
case 'search_models': {
|
|
378
353
|
const { main: foundModels, related: relatedModels } = await searchModels(params.query, user);
|
|
379
354
|
|
|
@@ -402,16 +377,13 @@ async function executeTool(action, params, user, allModels) {
|
|
|
402
377
|
/**
|
|
403
378
|
* Gère la requête de chat, soit en exécutant une action confirmée,
|
|
404
379
|
* soit en lançant la boucle de raisonnement de l'IA.
|
|
405
|
-
|
|
406
|
-
* @param {Array} history - L'historique de la conversation.
|
|
407
|
-
* @param {string} provider - Le fournisseur d'IA ('OpenAI' ou 'google').
|
|
408
|
-
* @param {object} context - Contexte additionnel.
|
|
380
|
+
@param {object} params - La liste des paramètres envoyés par la requete
|
|
409
381
|
* @param {object} user - L'objet utilisateur.
|
|
410
|
-
* @param {object} confirmedAction - Une action pré-approuvée par l'utilisateur.
|
|
411
382
|
* @returns {Promise<object>} La réponse de l'assistant.
|
|
412
383
|
*/
|
|
413
|
-
async function handleChatRequest(
|
|
384
|
+
export async function handleChatRequest(params, user) {
|
|
414
385
|
|
|
386
|
+
const { message, history, provider, confirmedAction } = params;
|
|
415
387
|
const allModels = await modelsCollection.find({$or: [{_user: {$exists: false}}, {_user: user.username}]}).toArray();
|
|
416
388
|
|
|
417
389
|
// --- GESTION D'UNE ACTION CONFIRMÉE ---
|
|
@@ -435,8 +407,9 @@ async function handleChatRequest(message, history, provider, context, user, conf
|
|
|
435
407
|
|
|
436
408
|
// --- INITIALISATION DE L'IA ---
|
|
437
409
|
let llm;
|
|
410
|
+
let llmOptions = { };
|
|
438
411
|
try {
|
|
439
|
-
const p = provider ||
|
|
412
|
+
const p = Config.Get('assistant.provider', provider ||'OpenAI');
|
|
440
413
|
const envKeyName = providers[p].key;
|
|
441
414
|
if (!envKeyName) return {success: false, message: `Fournisseur IA non supporté : ${p}`};
|
|
442
415
|
|
|
@@ -446,15 +419,17 @@ async function handleChatRequest(message, history, provider, context, user, conf
|
|
|
446
419
|
|
|
447
420
|
if (!apiKey) return {success: false, message: `Clé API pour ${p} (${envKeyName}) non trouvée.`};
|
|
448
421
|
|
|
449
|
-
|
|
422
|
+
const model = Config.Get('assistant.model', providers[p]?.defaultModel);
|
|
423
|
+
llmOptions = { provider: p, model, apiKey };
|
|
424
|
+
llm = await getAIProvider(llmOptions.provider, llmOptions.model, llmOptions.apiKey);
|
|
450
425
|
} catch (initError) {
|
|
451
426
|
logger.error(`[Assistant] Erreur d'initialisation du client IA: ${initError.message}`);
|
|
452
427
|
return {success: false, message: `Erreur d'initialisation du client IA: ${initError.message}`};
|
|
453
428
|
}
|
|
454
429
|
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
const conversationHistory = history
|
|
430
|
+
const systemPrompt = await Event.Trigger('OnSystemPrompt', 'event', 'user', user);
|
|
431
|
+
|
|
432
|
+
const conversationHistory = (history || [])
|
|
458
433
|
.filter(msg => msg.text && !(msg.from === 'bot' && msg.text.startsWith(i18n.t('assistant.welcome'))))
|
|
459
434
|
.map(msg => new (msg.from === 'user' ? HumanMessage : SystemMessage)(msg.text));
|
|
460
435
|
|
|
@@ -479,7 +454,7 @@ async function handleChatRequest(message, history, provider, context, user, conf
|
|
|
479
454
|
|
|
480
455
|
if (match && match[0]) {
|
|
481
456
|
// Si un JSON est trouvé, on tente de le parser
|
|
482
|
-
parsedResponse =
|
|
457
|
+
parsedResponse = parseSafeJSON(match[0]);
|
|
483
458
|
} else {
|
|
484
459
|
// Aucun JSON trouvé, c'est probablement une réponse textuelle simple.
|
|
485
460
|
return { success: true, displayMessage: llmOutput };
|
|
@@ -497,80 +472,32 @@ async function handleChatRequest(message, history, provider, context, user, conf
|
|
|
497
472
|
logger.debug(`[Assistant] Action décidée par l'IA: ${parsedResponse.action}`, parsedResponse);
|
|
498
473
|
conversationHistory.push(new SystemMessage(JSON.stringify(parsedResponse)));
|
|
499
474
|
|
|
500
|
-
const { action, params } = parsedResponse;
|
|
475
|
+
const { action, params:parsedParams } = parsedResponse;
|
|
501
476
|
|
|
502
477
|
// Correction automatique du filtre généré par l'IA, qui hallucine parfois
|
|
503
|
-
if (
|
|
504
|
-
logger.debug(`[Assistant] Filtre original de l'IA: ${JSON.stringify(
|
|
505
|
-
|
|
478
|
+
if (parsedParams && parsedParams.filter) {
|
|
479
|
+
logger.debug(`[Assistant] Filtre original de l'IA: ${JSON.stringify(parsedParams.filter)}`);
|
|
480
|
+
parsedParams.filter = correctAIFilter(parsedParams.filter);
|
|
506
481
|
}
|
|
507
482
|
|
|
508
|
-
//
|
|
509
|
-
if (action
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
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 } };
|
|
483
|
+
// Outils pour le raisonnement interne de l'IA
|
|
484
|
+
if (['search_models'].includes(action)) { // On a enlevé 'search' d'ici
|
|
485
|
+
const toolResult = await executeTool(action, parsedParams, user, allModels);
|
|
486
|
+
conversationHistory.push(new SystemMessage(`Résultat de l'outil '${action}':\n${toolResult}`));
|
|
487
|
+
continue; // On continue la boucle pour que l'IA puisse raisonner avec ce nouveau résultat
|
|
529
488
|
}
|
|
530
489
|
|
|
531
|
-
|
|
532
|
-
if (action === 'search') {
|
|
533
|
-
const searchResult = await searchData({
|
|
534
|
-
model: params.model,
|
|
535
|
-
filter: params.filter,
|
|
536
|
-
limit: params.limit || 10,
|
|
537
|
-
sort: params.sort
|
|
538
|
-
}, user);
|
|
539
|
-
if (searchResult.data?.length > 0) {
|
|
540
|
-
return { success: true, dataResult: { model: params.model, data: searchResult.data } };
|
|
541
|
-
} else {
|
|
542
|
-
return { success: true, displayMessage: i18n.t('assistant.search.noResults', "Je n'ai trouvé aucun résultat.") };
|
|
543
|
-
}
|
|
544
|
-
}
|
|
490
|
+
const res = await Event.Trigger('OnChatAction', 'event', 'user', action, params, parsedResponse, llmOptions, user);
|
|
545
491
|
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
492
|
+
if( !res ) {
|
|
493
|
+
// Si l'action n'est reconnue par aucune des logiques ci-dessus
|
|
494
|
+
logger.warn(`[Assistant] Action non reconnue reçue de l'IA: ${action}`);
|
|
549
495
|
return {
|
|
550
496
|
success: true,
|
|
551
|
-
displayMessage:
|
|
552
|
-
confirmationRequest: parsedResponse
|
|
497
|
+
displayMessage: i18n.t('assistant.unknownAction', "Désolé, je ne comprends pas la commande '{{action}}'.", {action})
|
|
553
498
|
};
|
|
554
499
|
}
|
|
555
|
-
|
|
556
|
-
// Action finale pour afficher un message
|
|
557
|
-
if (action === 'displayMessage') {
|
|
558
|
-
return { success: true, displayMessage: params.message };
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
// Outils pour le raisonnement interne de l'IA
|
|
562
|
-
if (['search_models'].includes(action)) { // On a enlevé 'search' d'ici
|
|
563
|
-
const toolResult = await executeTool(action, params, user, allModels);
|
|
564
|
-
conversationHistory.push(new SystemMessage(`Résultat de l'outil '${action}':\n${toolResult}`));
|
|
565
|
-
continue; // On continue la boucle pour que l'IA puisse raisonner avec ce nouveau résultat
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
// Si l'action n'est reconnue par aucune des logiques ci-dessus
|
|
569
|
-
logger.warn(`[Assistant] Action non reconnue reçue de l'IA: ${action}`);
|
|
570
|
-
return {
|
|
571
|
-
success: true,
|
|
572
|
-
displayMessage: i18n.t('assistant.unknownAction', "Désolé, je ne comprends pas la commande '{{action}}'.", { action })
|
|
573
|
-
};
|
|
500
|
+
return res;
|
|
574
501
|
}
|
|
575
502
|
|
|
576
503
|
// Si la boucle se termine sans une action finale
|
|
@@ -603,12 +530,74 @@ async function executeConfirmedAction(action, params, user) {
|
|
|
603
530
|
}
|
|
604
531
|
}
|
|
605
532
|
|
|
533
|
+
/**
|
|
534
|
+
* Gère une action finale décidée par l'IA et retourne une réponse pour le client.
|
|
535
|
+
* Cette fonction est appelée via le système d'événements 'OnChatAction'.
|
|
536
|
+
* @param {string} action - L'action à exécuter.
|
|
537
|
+
* @param {object} params - Les paramètres de l'action.
|
|
538
|
+
* @param {object} parsedResponse - La réponse JSON originale de l'IA, pour les demandes de confirmation.
|
|
539
|
+
* @param {object} user - L'objet utilisateur.
|
|
540
|
+
* @returns {Promise<object|boolean>} - L'objet de réponse final ou false si l'action n'est pas reconnue.
|
|
541
|
+
*/
|
|
542
|
+
async function handleFinalChatAction(action, params, parsedResponse, user) {
|
|
543
|
+
// Action de génération de graphique, gérée par le front-end
|
|
544
|
+
if (action === 'generateChart') {
|
|
545
|
+
return {success: true, chartConfig: params};
|
|
546
|
+
}
|
|
547
|
+
// Action de génération de vue HTML
|
|
548
|
+
if (action === 'generateHtmlView') {
|
|
549
|
+
const viewData = await searchData({
|
|
550
|
+
model: params.model,
|
|
551
|
+
filter: params.filter,
|
|
552
|
+
limit: params.limit || 10,
|
|
553
|
+
depth: 2 // Pour avoir accès aux relations de premier niveau dans les templates
|
|
554
|
+
}, user);
|
|
555
|
+
|
|
556
|
+
if (viewData.data.length === 0) {
|
|
557
|
+
return {
|
|
558
|
+
success: true,
|
|
559
|
+
displayMessage: i18n.t('assistant.htmlView.noResult', "Je n'ai trouvé aucune donnée correspondante pour cette vue.")
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
return {success: true, htmlViewConfig: {...params, data: viewData.data}};
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// Action de recherche à afficher, gérée par le front-end
|
|
566
|
+
if (action === 'search') {
|
|
567
|
+
const searchResult = await searchData({ model: params.model, filter: params.filter, limit: params.limit || 10, sort: params.sort }, user);
|
|
568
|
+
if (searchResult.data?.length > 0) {
|
|
569
|
+
return {success: true, dataResult: {model: params.model, data: searchResult.data}};
|
|
570
|
+
} else {
|
|
571
|
+
return { success: true, displayMessage: i18n.t('assistant.search.noResults', "Je n'ai trouvé aucun résultat.") };
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// Actions nécessitant une confirmation de l'utilisateur
|
|
576
|
+
if (['post', 'update', 'delete'].includes(action)) {
|
|
577
|
+
return { success: true, displayMessage: i18n.t('assistant.confirmActionPrompt', "Veuillez confirmer l'action suivante :"), confirmationRequest: parsedResponse };
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// Action finale pour afficher un message
|
|
581
|
+
if (action === 'displayMessage') {
|
|
582
|
+
return {success: true, displayMessage: params.message};
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
return false; // Action non reconnue par cet écouteur
|
|
586
|
+
}
|
|
587
|
+
|
|
606
588
|
|
|
607
589
|
export async function onInit(engine) {
|
|
608
590
|
logger = engine.getComponent(Logger);
|
|
609
591
|
|
|
610
592
|
const {middlewareAuthenticator, userInitiator} = await import('../user.js');
|
|
611
593
|
|
|
594
|
+
Event.Listen('OnSystemPrompt', (user) => {
|
|
595
|
+
return createSystemPrompt([], user.lang || 'en');
|
|
596
|
+
}, 'event', 'user');
|
|
597
|
+
// Enregistre le gestionnaire central pour les actions finales du chat.
|
|
598
|
+
// D'autres modules pourraient également écouter cet événement pour étendre les fonctionnalités.
|
|
599
|
+
Event.Listen('OnChatAction', handleFinalChatAction,"event", "user");
|
|
600
|
+
|
|
612
601
|
engine.post('/api/assistant/chat', [middlewareAuthenticator, userInitiator, assistantGlobalLimiter, generateLimiter], async (req, res) => {
|
|
613
602
|
// On récupère TOUTES les propriétés du body, y compris l'action confirmée
|
|
614
603
|
const {message, history, provider, context, confirmedAction} = req.fields;
|
|
@@ -630,7 +619,7 @@ export async function onInit(engine) {
|
|
|
630
619
|
}
|
|
631
620
|
|
|
632
621
|
try {
|
|
633
|
-
const result = await handleChatRequest(
|
|
622
|
+
const result = await handleChatRequest(req.fields, req.me);
|
|
634
623
|
res.json(result);
|
|
635
624
|
} catch (error) {
|
|
636
625
|
logger.error(`[Endpoint /api/assistant/chat] Erreur inattendue: ${error.message}`, error.stack);
|
|
@@ -163,7 +163,7 @@ export async function handleRevertToRevisionRequest(req, res) {
|
|
|
163
163
|
*/
|
|
164
164
|
export async function handleGetHistoryRequest(req, res) {
|
|
165
165
|
const { modelName, recordId } = req.params;
|
|
166
|
-
const { limit = 10, page = 1 } = req.query;
|
|
166
|
+
const { limit = 10, page = 1, startDate, endDate } = req.query;
|
|
167
167
|
const user = req.me; // Le middleware d'authentification attache l'utilisateur à req.me
|
|
168
168
|
|
|
169
169
|
try {
|
|
@@ -181,24 +181,48 @@ export async function handleGetHistoryRequest(req, res) {
|
|
|
181
181
|
|
|
182
182
|
// 3. Récupération des données depuis la collection 'history'
|
|
183
183
|
const historyCollection = getCollection('history');
|
|
184
|
-
const
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
184
|
+
const filterConditions = [
|
|
185
|
+
{ "$eq": ["$documentId", new ObjectId(recordId)]},
|
|
186
|
+
{ "$eq": ["$model", modelName]}
|
|
187
|
+
];
|
|
188
|
+
|
|
189
|
+
// Ajout du filtre par plage de dates
|
|
190
|
+
if (startDate) {
|
|
191
|
+
try {
|
|
192
|
+
filterConditions.push({ $gte: ["$timestamp", new Date(startDate)] });
|
|
193
|
+
} catch (e) { /* ignore invalid date */ }
|
|
194
|
+
}
|
|
195
|
+
if (endDate) {
|
|
196
|
+
try {
|
|
197
|
+
const endOfDay = new Date(endDate);
|
|
198
|
+
endOfDay.setUTCHours(23, 59, 59, 999); // Inclusif pour toute la journée de fin
|
|
199
|
+
filterConditions.push({ $lte: ["$timestamp", endOfDay] });
|
|
200
|
+
} catch (e) { /* ignore invalid date */ }
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const filter = { "$and": filterConditions };
|
|
188
204
|
|
|
189
205
|
const limitInt = parseInt(limit, 10);
|
|
190
206
|
const pageInt = parseInt(page, 10);
|
|
191
207
|
const skip = (pageInt - 1) * limitInt;
|
|
192
208
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
209
|
+
// Utiliser une agrégation pour le comptage afin de s'assurer que le filtre $expr est correctement appliqué.
|
|
210
|
+
const countPipeline = [
|
|
211
|
+
{ $match: { $expr: filter } },
|
|
212
|
+
{ $count: "count" }
|
|
213
|
+
];
|
|
214
|
+
|
|
215
|
+
const [countResult, historyData] = await Promise.all([
|
|
216
|
+
historyCollection.aggregate(countPipeline).toArray(),
|
|
217
|
+
historyCollection.aggregate([{$match: {$expr: filter}}])
|
|
196
218
|
.sort({ version: -1 })
|
|
197
219
|
.skip(skip)
|
|
198
220
|
.limit(limitInt)
|
|
199
221
|
.toArray()
|
|
200
222
|
]);
|
|
201
223
|
|
|
224
|
+
const totalCount = countResult.length > 0 ? countResult[0].count : 0;
|
|
225
|
+
|
|
202
226
|
// 4. Transformation des données pour correspondre au format attendu par le composant HistoryDialog.jsx
|
|
203
227
|
const opMap = {
|
|
204
228
|
create: 'i',
|