data-primals-engine 1.6.2-rc1 → 1.6.3

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.
@@ -14,6 +14,14 @@ export const defaultModels = {
14
14
  {
15
15
  "name": "description",
16
16
  "type": "richtext"
17
+ },
18
+ {
19
+ "name": "filter",
20
+ "type": "code",
21
+ "language": "json",
22
+ "conditionBuilder": true,
23
+ "required": false,
24
+ "hint": "Filtre JSON qui restreint la portée de cette permission. Le modèle cible est déduit du nom de la permission (ex: 'product.edit')."
17
25
  }
18
26
  ]
19
27
  },
@@ -1111,6 +1119,12 @@ export const defaultModels = {
1111
1119
  hint: "The current status of the workflow execution."
1112
1120
  },
1113
1121
  { name: 'stepExecutionsCount', type: 'object' },
1122
+ {
1123
+ name: 'history',
1124
+ type: 'array',
1125
+ itemsType: 'object',
1126
+ hint: "Stores the detailed execution history of each step and action."
1127
+ },
1114
1128
  { name: 'currentStep', type: 'relation', relation: 'workflowStep', hint: "The step currently being executed or waited on." },
1115
1129
  { name: 'owner', type: 'relation', relation: 'user', required: false },
1116
1130
  { name: 'startedAt', type: 'datetime', required: true, hint: "Timestamp when the workflow run began." },
package/src/filter.js CHANGED
@@ -1,3 +1,7 @@
1
+ import {getCollectionForUser, isObjectId} from "./modules/mongodb.js";
2
+ import {getModel} from "./modules/data/index.js";
3
+ import {ObjectId} from "mongodb";
4
+ import {safeAssignObject} from "./core.js";
1
5
 
2
6
  let safeRegex = null; // Par défaut, aucune fonction de validation n'est définie.
3
7
 
@@ -9,6 +13,42 @@ export function setSafeRegex(validator) {
9
13
  safeRegex = validator;
10
14
  }
11
15
 
16
+ /**
17
+ * Récupère une valeur imbriquée dans un objet en utilisant une chaîne de chemin.
18
+ * Gère les tableaux et les objets. Retourne undefined si le chemin n'est pas trouvé.
19
+ * Exemple: getNestedValue({ a: { b: [ { c: 1 } ] } }, 'a.b.0.c') -> 1
20
+ *
21
+ * @param {object} obj L'objet source.
22
+ * @param {string} path La chaîne de chemin (ex: 'user.address.city').
23
+ * @returns {*} La valeur trouvée ou undefined.
24
+ */
25
+ function getNestedValue(obj, path) {
26
+ // Vérifie si l'objet ou le chemin est invalide
27
+ if (!obj || typeof path !== 'string') {
28
+ return undefined;
29
+ }
30
+ // Sépare le chemin en clés individuelles (ex: 'a.b.0.c' -> ['a', 'b', '0', 'c'])
31
+ const keys = path.split('.');
32
+ let current = obj; // Commence à la racine de l'objet
33
+
34
+ // Parcourt chaque clé dans le chemin
35
+ for (const key of keys) {
36
+ // Si à un moment donné on atteint null ou undefined, le chemin est invalide
37
+ if (current === null || current === undefined) {
38
+ return undefined;
39
+ }
40
+ // Récupère la valeur pour la clé actuelle
41
+ const value = current[key];
42
+ // Si la valeur est undefined, le chemin est invalide
43
+ if (value === undefined) {
44
+ return undefined;
45
+ }
46
+ // Passe au niveau suivant de l'objet/tableau
47
+ current = value;
48
+ }
49
+ // Retourne la valeur finale trouvée
50
+ return current;
51
+ }
12
52
  /**
13
53
  * Evaluates a single condition against form data.
14
54
  * @param {object} currentModelDef - The definition of the current model.
@@ -272,4 +312,133 @@ export const isConditionMet = (model, cond, formData, allModels, user,checkRegex
272
312
  }
273
313
 
274
314
  return evaluateSingleCondition(model, condition, formData, allModels, user, checkRegex);
275
- };
315
+ };
316
+
317
+
318
+
319
+ /**
320
+ * Remplace les placeholders dans un template (string, object, array) par des valeurs du contextData.
321
+ * Version améliorée avec support des chemins complexes via resolvePathValue.
322
+ */
323
+ export async function substituteVariables(template, contextData, user) {
324
+ // 1. Retourner les types non substituables tels quels
325
+ if (template === null || (typeof template !== 'string' && typeof template !== 'object')) {
326
+ return template;
327
+ }
328
+
329
+ // 2. Gérer les tableaux de manière récursive
330
+ if (Array.isArray(template)) {
331
+ return Promise.all(template.map(item => substituteVariables(item, contextData, user)));
332
+ }
333
+
334
+ // 3. Gérer les objets de manière récursive
335
+ if (typeof template === 'object') {
336
+ const newObj = {};
337
+ for (const key in template) {
338
+ if (Object.prototype.hasOwnProperty.call(template, key)) {
339
+ const val = await substituteVariables(template[key], contextData, user);
340
+ safeAssignObject(newObj, key, val);
341
+ }
342
+ }
343
+ return newObj;
344
+ }
345
+
346
+ // --- À partir d'ici, nous savons que `template` est une chaîne de caractères ---
347
+
348
+ // 4. Construire le contexte complet pour la substitution
349
+ const dbCollection = await getCollectionForUser(user);
350
+ const userEnvVars = await dbCollection.find({ _model: 'env', _user: user.username }).toArray();
351
+ const userEnv = userEnvVars.reduce((acc, v) => ({ ...acc, [v.name]: v.value }), {});
352
+
353
+ // `contextToSearch` contient toutes les données disponibles à sa racine
354
+ const contextToSearch = { ...contextData, env: userEnv };
355
+
356
+ // 5. Logique de résolution de valeur améliorée avec resolvePathValue
357
+ const findValue = async (key) => {
358
+ let path = key.trim();
359
+ if (path.startsWith('context.')) {
360
+ path = path.substring('context.'.length);
361
+ }
362
+ if (path.endsWith('._id')) {
363
+ const basePath = path.slice(0, -4);
364
+ const value = await findValue(basePath);
365
+ return value?._id?.toString(); // Convertit l'ObjectId en string
366
+ }
367
+
368
+ // Gérer les valeurs dynamiques spéciales
369
+ if (path === 'now') {
370
+ return new Date().toISOString();
371
+ } else if (path === 'randomUUID') {
372
+ return crypto.randomUUID();
373
+ } else if( path === "baseUrl" ){
374
+ return process.env.NODE_ENV === 'production' ? 'https://'+getHost()+'/' : 'http://localhost:/'+port;
375
+ }
376
+
377
+ // Détecter si le chemin est complexe (contient plus d'un point)
378
+ if (path.split('.').length > 1) {
379
+ try {
380
+ // Essayer de résoudre le chemin avec resolvePathValue
381
+ const [root, ...rest] = path.split('.');
382
+ // On vérifie si la racine du chemin (ex: 'triggerData') existe dans notre contexte
383
+ if (contextToSearch[root]) {
384
+ const resolvedValue = await resolvePathValue(
385
+ rest.join('.'),
386
+ contextToSearch[root], // On passe le bon objet de départ (ex: l'objet triggerData)
387
+ user
388
+ );
389
+ if (resolvedValue !== undefined) {
390
+ return resolvedValue;
391
+ }
392
+ }
393
+ } catch (error) {
394
+ console.warn(`Erreur lors de la résolution du chemin "${path}":`, error.message);
395
+ // On continue avec la méthode normale si la résolution échoue
396
+ }
397
+ }
398
+
399
+ // Fallback: chercher le chemin dans l'objet de contexte normal
400
+ return getNestedValue(contextToSearch, path);
401
+ };
402
+
403
+ // CAS A : La chaîne est un unique placeholder (ex: "{context.triggerData.product.price}")
404
+ const singlePlaceholderMatch = template.match(/^\{([^}]+)\}$/);
405
+ if (singlePlaceholderMatch) {
406
+ const key = singlePlaceholderMatch[1];
407
+ const value = await findValue(key);
408
+
409
+ if (value === undefined) {
410
+ return template; // Placeholder not found, return as is.
411
+ }
412
+
413
+ // If the resolved value is a string, it might contain more placeholders.
414
+ // We recursively call substituteVariables on it, but only if it's different
415
+ // from the original template to prevent infinite loops.
416
+ if (typeof value === 'string' && value !== template) {
417
+ return substituteVariables(value, contextData, user);
418
+ }
419
+
420
+ // For non-string values or if value is same as template, return the value.
421
+ return value;
422
+ }
423
+
424
+ // CAS B : La chaîne contient plusieurs placeholders ou mix texte/variables
425
+ const placeholderRegex = /\{([^}]+)\}/g;
426
+ const placeholders = [...template.matchAll(placeholderRegex)];
427
+
428
+ // Si aucun placeholder trouvé, retourner la chaîne telle quelle
429
+ if (placeholders.length === 0) {
430
+ return template;
431
+ }
432
+
433
+ // Remplacer chaque placeholder de manière asynchrone
434
+ let result = template;
435
+ for (const [match, key] of placeholders) {
436
+ const value = await findValue(key);
437
+ const replacement = value !== undefined
438
+ ? (value === null ? 'null' : typeof value === 'object' ? JSON.stringify(value) : String(value))
439
+ : match;
440
+ result = result.replace(match, replacement);
441
+ }
442
+
443
+ return result;
444
+ }
@@ -1,7 +1,8 @@
1
1
  import { getCollectionForUser, modelsCollection } from "../mongodb.js";
2
2
  import { Logger } from "../../gameObject.js";
3
3
  import { HumanMessage, SystemMessage } from "@langchain/core/messages";
4
- import {searchData, patchData, deleteData, insertData} from "../data/index.js";
4
+ import { searchData, patchData, deleteData, insertData } from "../data/index.js";
5
+ import { getAIProvider } from "./providers.js";
5
6
  import {providers} from "./constants.js";
6
7
  import {Config} from "../../config.js";
7
8
  import { Event } from "../../events.js";
@@ -13,38 +14,6 @@ import {parseSafeJSON} from "../../core.js";
13
14
 
14
15
  let logger = null;
15
16
 
16
- export const getAIProvider= async (aiProvider, aiModel, apiKey)=>{
17
- const maxTokens = Config.Get('assistant.maxTokens', assistantConfig.maxTokens);
18
- try {
19
- switch (aiProvider) {
20
- case 'OpenAI': {
21
- const { ChatOpenAI } = await import("@langchain/openai");
22
- return new ChatOpenAI({apiKey, model: aiModel, temperature: 0.7, maxTokens});
23
- }
24
- case 'Google': {
25
- const { ChatGoogleGenerativeAI } = await import("@langchain/google-genai");
26
- return new ChatGoogleGenerativeAI({apiKey, model: aiModel, temperature: 0.7, maxTokens});
27
- }
28
- case 'DeepSeek': {
29
- const { ChatDeepSeek } = await import("@langchain/deepseek");
30
- return new ChatDeepSeek({apiKey, model: aiModel, temperature: 0.7, maxTokens});
31
- }
32
- case 'Anthropic': {
33
- const { ChatAnthropic } = await import("@langchain/anthropic");
34
- return new ChatAnthropic({apiKey, model: aiModel, temperature: 0.7, maxTokens});
35
- }
36
- default:
37
- throw new Error(`Unsupported AI provider: ${aiProvider}`);
38
- }
39
- } catch (e) {
40
- if (e.code === 'ERR_MODULE_NOT_FOUND') {
41
- logger.error(`[Assistant] The package for the '${aiProvider}' provider is not installed. Please run 'npm install @langchain/${aiProvider.toLowerCase()}' to use this provider.`);
42
- throw new Error(`The AI provider '${aiProvider}' is not installed. Please ask the administrator to install the corresponding package.`);
43
- }
44
- logger.error(`[Assistant] Error initializing AI provider '${aiProvider}': ${e.message}`);
45
- throw e; // Re-throw other errors
46
- }
47
- }
48
17
  export const assistantGlobalLimiter = rateLimit({
49
18
  windowMs: 15 * 60 * 1000, // Fenêtre de 15 minutes
50
19
  max: 100, // Limite à 100 requêtes globales pour l'assistant pendant la fenêtre (vous pouvez ajuster cette valeur)
@@ -101,7 +70,7 @@ const createSystemPrompt = (modelDefs, lang) => {
101
70
 
102
71
  const date = new Date();
103
72
  const dt = date.toISOString();
104
- return `
73
+ return Config.Get('systemPrompt', `
105
74
  Tu es "Prior", un assistant expert en analyse de données pour le moteur data-primals-engine..
106
75
  Ta mission est d'aider l'utilisateur en répondant à ses questions sur ses données.
107
76
 
@@ -333,7 +302,7 @@ Ma question: Bonjour, je voudrais les requêtes effectuées aujourd'hui sur le m
333
302
  Ta réponse: [
334
303
  { "action" : "search_models", "params": { "query": "request" } },
335
304
  { "action" : "search", "params" : { "model": "request", "filter": { "$and": [{"$gte": ["$createdAt", "${dt}"]}, {"$regexMatch": { "input": "$url", "regex": "content"}}] }, "limit" : 10, "sort" : "_id:DESC" } }
336
- ]`;
305
+ ]`);
337
306
  }
338
307
 
339
308
  /**
@@ -475,7 +444,7 @@ export async function handleChatRequest(params, user, sendEvent = null) {
475
444
  return errorResult;
476
445
  }
477
446
 
478
- const systemPrompt = await Event.Trigger('OnSystemPrompt', 'event', 'user', user);
447
+ const systemPrompt = (await Event.Trigger('OnSystemPrompt', 'event', 'user', user)) || createSystemPrompt([], user.lang || 'en');
479
448
 
480
449
  const conversationHistory = (history || [])
481
450
  .filter(msg => msg.text && !(msg.from === 'bot' && msg.text.startsWith(i18n.t('assistant.welcome'))))
@@ -559,11 +528,12 @@ export async function handleChatRequest(params, user, sendEvent = null) {
559
528
  return result;
560
529
  }
561
530
 
562
- let hasContinued = false;
531
+ const toolResults = [];
532
+ let finalActionResult = null;
533
+
563
534
  for (const command of commands) {
564
535
  logger.debug(`[Assistant] Action décidée par l'IA: ${command.action}`, command);
565
536
  if (sendEvent) sendEvent('action', { action: command.action, params: command.params });
566
- conversationHistory.push(new SystemMessage(JSON.stringify(command)));
567
537
 
568
538
  const { action, params: parsedParams } = command;
569
539
 
@@ -576,40 +546,52 @@ export async function handleChatRequest(params, user, sendEvent = null) {
576
546
  // Outils pour le raisonnement interne de l'IA
577
547
  if (['search_models'].includes(action)) {
578
548
  const toolResult = await executeTool(action, parsedParams, user, allModels);
579
- if (sendEvent) sendEvent('tool_result', { action, result: toolResult });
580
- conversationHistory.push(new SystemMessage(`Résultat de l'outil '${action}':\n${toolResult}`));
581
- hasContinued = true; // On indique qu'on doit continuer la boucle principale
582
- continue; // On passe à la commande suivante dans la liste de l'IA
549
+ toolResults.push({ action, result: toolResult });
550
+ continue; // Passe à la commande suivante sans vérifier si c'est une action finale
583
551
  }
584
552
 
585
553
  // Actions finales
586
- const res = await Event.Trigger('OnChatAction', 'event', 'user', action, params, command, llmOptions, user);
554
+ const res = await Event.Trigger('OnChatAction', 'event', 'user', action, parsedParams, command, llmOptions, user, params);
587
555
 
588
- if (!res) {
556
+ if (res) {
557
+ // On a trouvé une action finale. On la stocke et on arrête de chercher.
558
+ finalActionResult = res;
559
+ break; // Sort de la boucle des commandes
560
+ } else {
589
561
  // Si l'action n'est reconnue par aucune des logiques ci-dessus
590
562
  logger.warn(`[Assistant] Action non reconnue reçue de l'IA: ${action}`);
591
- const result = {
563
+ finalActionResult = {
592
564
  success: true,
593
565
  displayMessage: i18n.t('assistant.unknownAction', "Désolé, je ne comprends pas la commande '{{action}}'.", { action })
594
566
  };
595
- if (sendEvent) {
596
- sendEvent('final_result', result);
597
- return;
598
- }
599
- return result;
567
+ break; // Sortir de la boucle des commandes
600
568
  }
601
- // Une action finale a été trouvée, on retourne son résultat et on arrête le traitement.
569
+ }
570
+
571
+ // Si une action finale a été exécutée, on retourne son résultat.
572
+ if (finalActionResult) {
602
573
  if (sendEvent) {
603
- sendEvent('final_result', res);
574
+ sendEvent('final_result', finalActionResult);
604
575
  return;
605
576
  }
606
- return res;
577
+ return finalActionResult;
607
578
  }
608
579
 
609
- // Si on a traité des outils de raisonnement, on relance la boucle de l'IA
610
- if (hasContinued) {
580
+ // Si on a uniquement des résultats d'outils, on les ajoute à l'historique et on continue la boucle.
581
+ if (toolResults.length > 0) {
582
+ let toolResponse = "";
583
+ for (const tool of toolResults) {
584
+ if (sendEvent) sendEvent('tool_result', { action: tool.action, result: tool.result });
585
+ toolResponse += `Résultat de l'outil '${tool.action}':\n${tool.result}\n\n`;
586
+ }
587
+ conversationHistory.push(new SystemMessage(toolResponse));
611
588
  continue;
612
589
  }
590
+
591
+ // Si l'IA ne retourne ni outil ni action finale reconnue, on arrête.
592
+ if (commands.length > 0) {
593
+ logger.warn(`[Assistant] L'IA a retourné des commandes mais aucune n'était un outil ou une action finale reconnue. Commandes:`, commands);
594
+ }
613
595
  }
614
596
 
615
597
  // Si la boucle se termine sans une action finale
@@ -656,7 +638,7 @@ async function executeConfirmedAction(action, params, user) {
656
638
  * @param {object} user - L'objet utilisateur.
657
639
  * @returns {Promise<object|boolean>} - L'objet de réponse final ou false si l'action n'est pas reconnue.
658
640
  */
659
- async function handleFinalChatAction(action, params, parsedResponse, user) {
641
+ async function handleFinalChatAction(action, params, parsedResponse, llmOptions, user) {
660
642
  // Action de génération de graphique, gérée par le front-end
661
643
  if (action === 'generateChart') {
662
644
  return {success: true, chartConfig: params};
@@ -0,0 +1,38 @@
1
+ import { Config } from "../../config.js";
2
+ import { assistantConfig } from "../../constants.js";
3
+ import { Logger } from "../../gameObject.js";
4
+
5
+ const logger = new Logger("AssistantProviders");
6
+
7
+ export const getAIProvider = async (aiProvider, aiModel, apiKey) => {
8
+ const maxTokens = Config.Get('assistant.maxTokens', assistantConfig.maxTokens);
9
+ try {
10
+ switch (aiProvider) {
11
+ case 'OpenAI': {
12
+ const { ChatOpenAI } = await import("@langchain/openai");
13
+ return new ChatOpenAI({ apiKey, model: aiModel, temperature: 0.7, maxTokens });
14
+ }
15
+ case 'Google': {
16
+ const { ChatGoogleGenerativeAI } = await import("@langchain/google-genai");
17
+ return new ChatGoogleGenerativeAI({ apiKey, model: aiModel, temperature: 0.7, maxTokens });
18
+ }
19
+ case 'DeepSeek': {
20
+ const { ChatDeepSeek } = await import("@langchain/deepseek");
21
+ return new ChatDeepSeek({ apiKey, model: aiModel, temperature: 0.7, maxTokens });
22
+ }
23
+ case 'Anthropic': {
24
+ const { ChatAnthropic } = await import("@langchain/anthropic");
25
+ return new ChatAnthropic({ apiKey, model: aiModel, temperature: 0.7, maxTokens });
26
+ }
27
+ default:
28
+ throw new Error(`Unsupported AI provider: ${aiProvider}`);
29
+ }
30
+ } catch (e) {
31
+ if (e.code === 'ERR_MODULE_NOT_FOUND') {
32
+ logger.error(`[Assistant] The package for the '${aiProvider}' provider is not installed. Please run 'npm install @langchain/${aiProvider.toLowerCase()}' to use this provider.`);
33
+ throw new Error(`The AI provider '${aiProvider}' is not installed. Please ask the administrator to install the corresponding package.`);
34
+ }
35
+ logger.error(`[Assistant] Error initializing AI provider '${aiProvider}': ${e.message}`);
36
+ throw e; // Re-throw other errors
37
+ }
38
+ }
@@ -323,17 +323,14 @@ export function onInit(defaultEngine) {
323
323
  engine.post('/api/data/history/:modelName/:recordId/revert/:version', [middlewareAuthenticator, userInitiator], handleRevertToRevisionRequest);
324
324
 
325
325
  // --- Écouteur pour la CRÉATION de données (Version 1 - Snapshot) ---
326
- Event.Listen("OnDataAdded", async (engine, { modelName, insertedIds, user }) => {
326
+ Event.Listen("OnDataAdded", async (engine, { modelName, insertedDocs, user }) => {
327
327
  try {
328
328
  const model = await getModel(modelName, user);
329
329
  if (!model?.history?.enabled) return;
330
330
 
331
- const dataCollection = await getCollectionForUser(user);
332
331
  const historyCollection = getCollection('history');
333
332
 
334
- const newDocs = await dataCollection.find({ _id: { $in: insertedIds.map(id => new ObjectId(id)) } }).toArray();
335
-
336
- for (const doc of newDocs) {
333
+ for (const doc of insertedDocs) {
337
334
  await historyCollection.insertOne({
338
335
  documentId: doc._id,
339
336
  model: modelName,
@@ -394,7 +391,11 @@ export function onInit(defaultEngine) {
394
391
  }
395
392
 
396
393
  // If this is the first history record for this document, add a snapshot of the 'before' state.
397
- if (!lastVersionDoc) {
394
+ // --- FIX ---
395
+ // The check should be based on the calculated version number, not on the existence of lastVersionDoc,
396
+ // which could be affected by race conditions in a busy environment. If we are creating version 1,
397
+ // it means no prior history exists for this document, so we must add the snapshot.
398
+ if (newVersion === 1) {
398
399
  historyEntry.snapshot = beforeDoc;
399
400
  logger.debug(`History v${newVersion} (update with initial snapshot) created for ${modelName} document ${afterDoc._id}`);
400
401
  } else {
@@ -817,7 +817,7 @@ export const insertData = async (modelName, data, files, user, triggerWorkflow =
817
817
  }
818
818
 
819
819
  // System specific event
820
- const eventPayload = {modelName, insertedIds, user};
820
+ const eventPayload = {modelName, insertedDocs, user};
821
821
  await Event.Trigger("OnDataAdded", "event", "system", engine, eventPayload);
822
822
 
823
823
  // User specific event
@@ -829,7 +829,10 @@ export const insertData = async (modelName, data, files, user, triggerWorkflow =
829
829
  // --- CORRECTION ---
830
830
  // On renvoie l'objet complet du premier document inséré (cas le plus courant pour l'undo)
831
831
  // et la liste complète des IDs pour les cas de bulk insert.
832
- const firstInsertedDoc = insertedDocs.length > 0 ? insertedDocs[0] : null;
832
+ let firstInsertedDoc = insertedDocs.length > 0 ? { ...insertedDocs[0] } : null;
833
+ if (firstInsertedDoc?._id) {
834
+ firstInsertedDoc._id = firstInsertedDoc._id.toString();
835
+ }
833
836
  return {success: true, data: firstInsertedDoc, insertedIds: insertedIds.map(id => id.toString())};
834
837
 
835
838
  } catch (error) { // Attrape les erreurs de permission ou de pushDataUnsecure
@@ -1419,6 +1422,12 @@ const internalEditOrPatchData = async (modelName, filter, data, files, user, isP
1419
1422
  before: existingDocs, // Documents avant la modification
1420
1423
  after: updatedDocs // Documents après la modification
1421
1424
  });
1425
+ await Event.Trigger("OnDataEdited", "event", "user", engine, {
1426
+ modelName,
1427
+ user,
1428
+ before: existingDocs, // Documents avant la modification
1429
+ after: updatedDocs // Documents après la modification
1430
+ });
1422
1431
  }
1423
1432
 
1424
1433
  // 11. Tâches post-mise à jour (schedules, workflows) (inchangé)
@@ -1670,9 +1679,15 @@ export const deleteData = async (modelName, filter, user = {}, triggerWorkflow,
1670
1679
  logger.info(`[deleteData] No documents to delete for user ${user?.username} after permission checks or matching criteria.`);
1671
1680
  }
1672
1681
 
1673
- const res = {success: true, deletedCount}
1674
- const plugin = await Event.Trigger("OnDataDeleted", "event", "system", engine, {model: modelName, filter});
1675
- await Event.Trigger("OnDataDeleted", "event", "user", {model: modelName, filter});
1682
+ const res = { success: true, deletedCount };
1683
+
1684
+ // --- CORRECTION ---
1685
+ // The event payload must match what the history listener expects: { modelName, user, before }.
1686
+ // 'documentsToDelete' contains the full documents before they were deleted.
1687
+ const eventPayload = { modelName: modelNameToInvalidate, user, before: documentsToDelete };
1688
+ const plugin = await Event.Trigger("OnDataDeleted", "event", "system", engine, eventPayload);
1689
+ await Event.Trigger("OnDataDeleted", "event", "user", eventPayload);
1690
+
1676
1691
  return plugin || res;
1677
1692
 
1678
1693
  } catch (error) {