data-primals-engine 1.2.6 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/CHANGELOG.md +440 -0
  2. package/README.md +782 -712
  3. package/client/src/App.jsx +13 -20
  4. package/client/src/App.scss +4 -0
  5. package/client/src/AssistantChat.jsx +5 -4
  6. package/client/src/DataEditor.jsx +1 -1
  7. package/client/src/DataLayout.jsx +2 -2
  8. package/client/src/DataTable.jsx +66 -5
  9. package/client/src/ExportDialog.jsx +2 -2
  10. package/client/src/Field.jsx +5 -4
  11. package/client/src/HistoryDialog.jsx +96 -0
  12. package/client/src/HistoryDialog.scss +73 -0
  13. package/client/src/KanbanCard.jsx +4 -2
  14. package/client/src/KanbanConfigModal.jsx +5 -7
  15. package/client/src/ModelCreator.jsx +36 -15
  16. package/client/src/ModelCreatorField.jsx +14 -15
  17. package/client/src/PackGallery.jsx +89 -9
  18. package/client/src/PackGallery.scss +58 -4
  19. package/client/src/RTETrans.jsx +11 -0
  20. package/client/src/RelationValue.jsx +3 -4
  21. package/client/src/constants.js +1 -1
  22. package/client/src/core/data.js +2 -1
  23. package/client/src/hooks/useTutorials.jsx +1 -1
  24. package/client/src/translations.js +217 -0
  25. package/package.json +9 -2
  26. package/server.js +4 -4
  27. package/src/HistoryDialog.jsx +86 -0
  28. package/src/HistoryDialog.scss +77 -0
  29. package/src/constants.js +6 -0
  30. package/src/defaultModels.js +23 -10
  31. package/src/email.js +1 -1
  32. package/src/engine.js +6 -6
  33. package/src/events.js +15 -5
  34. package/src/gameObject.js +0 -29
  35. package/src/i18n.js +1 -1
  36. package/src/modules/{assistant.js → assistant/assistant.js} +42 -27
  37. package/src/modules/assistant/constants.js +16 -0
  38. package/src/modules/data/data.history.js +304 -0
  39. package/src/modules/data/data.js +306 -238
  40. package/src/modules/data/data.routes.js +33 -7
  41. package/src/modules/mongodb.js +17 -1
  42. package/src/modules/user.js +21 -4
  43. package/src/modules/workflow.js +133 -48
  44. package/src/packs.js +1016 -9
  45. package/src/services/index.js +11 -0
  46. package/src/services/stripe.js +195 -0
  47. package/src/workers/crypto-worker.js +3 -3
  48. package/test/data.backup.integration.test.js +5 -0
  49. package/test/data.history.integration.test.js +193 -0
  50. package/test/data.integration.test.js +144 -41
  51. package/test/events.test.js +27 -27
  52. package/test/globalTeardown.js +1 -0
  53. package/test/model.integration.test.js +5 -0
  54. package/test/workflow.actions.integration.test.js +4 -2
  55. package/test/workflow.integration.test.js +3 -1
  56. package/test/workflow.robustness.test.js +2 -0
package/src/engine.js CHANGED
@@ -198,7 +198,7 @@ export const Engine = {
198
198
  logger.warn(`Aucun point d'entrée trouvé pour le module '${moduleIdentifier}'.`);
199
199
  }
200
200
  } catch (e) {
201
- logger.error(`Échec du chargement du module '${moduleIdentifier}':`, e.stack);
201
+ logger.error(`Could not load module '${moduleIdentifier}':`, e.stack);
202
202
  }
203
203
  }
204
204
 
@@ -246,12 +246,12 @@ export const Engine = {
246
246
  process.exit(1);
247
247
  });
248
248
 
249
- Event.Trigger("OnServerStart", "event", "system", engine);
249
+ await Event.Trigger("OnServerStart", "event", "system", engine);
250
250
  }
251
251
 
252
252
  engine.stop = async () => {
253
253
  await server.close();
254
- Event.Trigger("OnServerStop", "event", "system", engine);
254
+ await Event.Trigger("OnServerStop", "event", "system", engine);
255
255
  };
256
256
 
257
257
  async function setupInitialModels() {
@@ -262,7 +262,7 @@ export const Engine = {
262
262
 
263
263
  for(let i = 0; i < ms.length; ++i){
264
264
  const model = ms[i];
265
- validateModelStructure(model);
265
+ await validateModelStructure(model);
266
266
  // Création des modèles
267
267
  if( !dbModels.find(m =>m.name === model.name) )
268
268
  {
@@ -274,11 +274,11 @@ export const Engine = {
274
274
  logger.info('Model loaded (' + model.name + ')');
275
275
  }
276
276
  logger.info("All models loaded.");
277
- Event.Trigger("OnModelsLoaded", "event", "system", engine, dbModels);
277
+ await Event.Trigger("OnModelsLoaded", "event", "system", engine, dbModels);
278
278
  }
279
279
  engine.resetModels = async () => {
280
280
  await deleteModels();
281
- Event.Trigger("OnModelsDeleted", "event", "system", engine);
281
+ await Event.Trigger("OnModelsDeleted", "event", "system", engine);
282
282
  };
283
283
  return engine;
284
284
  }
package/src/events.js CHANGED
@@ -12,14 +12,20 @@ const eventLayerSystems = {
12
12
 
13
13
 
14
14
  export const Event = {
15
- Trigger: (name, system = "priority", layer = "medium", ...args) => { // Ajout des arguments system et layer
15
+ Trigger: async (name, system = "priority", layer = "medium", ...args) => { // Ajout des arguments system et layer
16
+ if (!name || typeof name !== 'string') {
17
+ throw new Error('Event name must be a non-empty string');
18
+ }
19
+
16
20
  if (!events[system] || !events[system][name] || (layer && !events[system][name][layer])) {
17
21
  //console.warn(`No trigger found for ${name} in system ${system} layer ${layer}`);
18
22
  return null;
19
23
  }
20
24
 
21
25
  const systemsToProcess = system ? [system] : Object.keys(events); // Si system est spécifié, on cible ce système uniquement, sinon tous les systèmes
22
-
26
+ console.log(`[Event] Triggering ${system}.${layer}.${name}`, {
27
+ callbacks: events[system]?.[name]?.[layer]?.length || 0
28
+ });
23
29
  let ret = null;
24
30
  for (const currentSystem of systemsToProcess) {
25
31
  if (events[currentSystem] && events[currentSystem][name]) {
@@ -30,12 +36,12 @@ export const Event = {
30
36
  if (events[currentSystem][name][currentLayer]) {
31
37
  for (const callback of events[currentSystem][name][currentLayer]) {
32
38
  try {
33
- const res = callback(...args);
39
+ const res = await callback(...args);
34
40
  if (typeof res === "object" && !Array.isArray(res)) {
35
41
  if (typeof ret !== "object") ret = {};
36
42
  ret = {...ret, ...res};
37
43
  } else if (Array.isArray(res)) {
38
- if (!Array.isArray(ret)) ret = [];
44
+ if (!ret || !Array.isArray(ret)) ret = [];
39
45
  ret = ret.concat(res);
40
46
  } else if (typeof res === "string") {
41
47
  if (typeof ret !== "string") ret = "";
@@ -50,7 +56,11 @@ export const Event = {
50
56
  ret = res || ret;
51
57
  }
52
58
  } catch (error) {
53
- console.error(`Error in callback for event ${name} in system ${currentSystem} layer ${currentLayer}:`, error);
59
+ const errorMsg = `Error in callback for event ${name} in system ${currentSystem} layer ${currentLayer}: ${error.message}`;
60
+ const newError = new Error(errorMsg);
61
+ newError.originalError = error; // Conserve l'erreur originale
62
+ newError.eventDetails = { name, system: currentSystem, layer: currentLayer };
63
+ throw newError;
54
64
  }
55
65
  }
56
66
  }
package/src/gameObject.js CHANGED
@@ -61,35 +61,6 @@ export class MovableBehaviour extends Behaviour {
61
61
  this.gameObject.x += this.speed;
62
62
  }
63
63
  }
64
-
65
- // UsableBehavior.js
66
- export class UsableBehaviour extends Behaviour {
67
- constructor(gameObject) {
68
- super(gameObject);
69
- }
70
-
71
- use() {
72
- Event.Trigger("GameObject.UsableBehavior.use", "system", "calls", this);
73
- }
74
- }
75
-
76
- const mainDriver = GameObject.Create("MainDrivers");
77
-
78
- // Exemple d'attachement de comportements
79
- mainDriver.addComponent(MovableBehaviour, 10);
80
- mainDriver.addComponent(UsableBehaviour);
81
-
82
- // Accéder et utiliser les composants
83
- const movable = mainDriver.getComponent(MovableBehaviour);
84
- if (movable) {
85
- movable.update();
86
- }
87
- const usable = mainDriver.getComponent(UsableBehaviour);
88
- if (usable) {
89
- usable.use();
90
- }
91
-
92
-
93
64
  export class Logger extends Behaviour {
94
65
  constructor(gameObject) {
95
66
  super(gameObject);
package/src/i18n.js CHANGED
@@ -1121,7 +1121,7 @@ export const translations = {
1121
1121
  "CreateData": "Créer une donnée",
1122
1122
  "DeleteData": "Supprimer une donnée",
1123
1123
  "ExecuteScript": "Exécuter un script",
1124
- "CallWebhook": "Appeler un Webhook",
1124
+ "HttpRequest": "Appeler un Webhook",
1125
1125
  "SendEmail": "Envoyer un Email",
1126
1126
  "Wait": "Attendre",
1127
1127
  "GenerateAIContent": "Générer un contenu IA",
@@ -1,17 +1,45 @@
1
- import { getCollectionForUser, modelsCollection } from "./mongodb.js";
2
- import { Logger } from "../gameObject.js";
1
+ import { getCollectionForUser, modelsCollection } from "../mongodb.js";
2
+ import { Logger } from "../../gameObject.js";
3
3
  import { ChatOpenAI } from "@langchain/openai";
4
4
  import { ChatGoogleGenerativeAI } from "@langchain/google-genai";
5
5
  import { HumanMessage, SystemMessage } from "@langchain/core/messages";
6
- import {searchData, patchData, deleteData, insertData} from "./data/index.js";
7
- import { getDataAsString } from "../data.js";
8
- import i18n from "../../src/i18n.js";
9
- import {generateLimiter} from "./user.js";
6
+ 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
10
  import rateLimit from "express-rate-limit";
11
- import {maxAIReflectiveSteps} from "../constants.js";
11
+ import {maxAIReflectiveSteps} from "../../constants.js";
12
+ import {providers} from "./constants.js";
13
+ import {ChatDeepSeek} from "@langchain/deepseek";
14
+ import {ChatAnthropic} from "@langchain/anthropic";
12
15
 
13
16
  let logger = null;
14
17
 
18
+ export const getAIProvider= (aiProvider, aiModel, apiKey)=>{
19
+ let llm;
20
+ try {
21
+ switch (aiProvider) {
22
+ case 'OpenAI':
23
+ llm = new ChatOpenAI({apiKey, model: aiModel, temperature: 0.7});
24
+ break;
25
+ case 'Google':
26
+ llm = new ChatGoogleGenerativeAI({apiKey, model: aiModel, temperature: 0.7});
27
+ break;
28
+ case 'DeepSeek':
29
+ llm = new ChatDeepSeek({apiKey, model: aiModel, temperature: 0.7});
30
+ break;
31
+ case 'Anthropic':
32
+ llm = new ChatAnthropic({apiKey, model: aiModel, temperature: 0.7});
33
+ break;
34
+ default:
35
+ throw new Error(`Unsupported AI provider: ${aiProvider}`);
36
+ }
37
+ return llm;
38
+ }
39
+ catch (e) {
40
+ return null;
41
+ }
42
+ }
15
43
  export const assistantGlobalLimiter = rateLimit({
16
44
  windowMs: 15 * 60 * 1000, // Fenêtre de 15 minutes
17
45
  max: 100, // Limite à 100 requêtes globales pour l'assistant pendant la fenêtre (vous pouvez ajuster cette valeur)
@@ -207,7 +235,7 @@ async function executeTool(action, params, user, allModels) {
207
235
  * soit en lançant la boucle de raisonnement de l'IA.
208
236
  * @param {string} message - Le message de l'utilisateur.
209
237
  * @param {Array} history - L'historique de la conversation.
210
- * @param {string} provider - Le fournisseur d'IA ('openai' ou 'google').
238
+ * @param {string} provider - Le fournisseur d'IA ('OpenAI' ou 'google').
211
239
  * @param {object} context - Contexte additionnel.
212
240
  * @param {object} user - L'objet utilisateur.
213
241
  * @param {object} confirmedAction - Une action pré-approuvée par l'utilisateur.
@@ -239,30 +267,17 @@ async function handleChatRequest(message, history, provider, context, user, conf
239
267
  // --- INITIALISATION DE L'IA ---
240
268
  let llm;
241
269
  try {
242
- const p = provider || 'openai';
243
- const providers = {"openai": "OPENAI_API_KEY", "google": "GOOGLE_API_KEY"};
244
- const envKeyName = providers[p];
270
+ const p = provider || 'OpenAI';
271
+ const envKeyName = providers[p].key;
245
272
  if (!envKeyName) return {success: false, message: `Fournisseur IA non supporté : ${p}`};
246
273
 
247
274
  const envCollection = await getCollectionForUser(user);
248
275
  const userEnvVar = await envCollection.findOne({_model: 'env', name: envKeyName, _user: user.username});
249
276
  const apiKey = userEnvVar?.value || process.env[envKeyName];
250
277
 
251
- if (!apiKey) return {success: false, message: `Clé API pour ${provider} (${envKeyName}) non trouvée.`};
252
-
253
- llm = p === 'openai'
254
- ? new ChatOpenAI({
255
- apiKey,
256
- modelName: "gpt-4o-mini",
257
- temperature: 0.2,
258
- response_format: {"type": "json_object"}
259
- })
260
- : new ChatGoogleGenerativeAI({
261
- apiKey,
262
- modelName: "gemini-1.5-pro-latest",
263
- temperature: 0.2,
264
- response_format: {"type": "json_object"}
265
- });
278
+ if (!apiKey) return {success: false, message: `Clé API pour ${p} (${envKeyName}) non trouvée.`};
279
+
280
+ llm = getAIProvider(p, providers[p]?.defaultModel, apiKey);
266
281
  } catch (initError) {
267
282
  logger.error(`[Assistant] Erreur d'initialisation du client IA: ${initError.message}`);
268
283
  return {success: false, message: `Erreur d'initialisation du client IA: ${initError.message}`};
@@ -371,7 +386,7 @@ async function executeConfirmedAction(action, params, user) {
371
386
  export async function onInit(engine) {
372
387
  logger = engine.getComponent(Logger);
373
388
 
374
- const {middlewareAuthenticator, userInitiator} = await import('./user.js');
389
+ const {middlewareAuthenticator, userInitiator} = await import('../user.js');
375
390
 
376
391
  engine.post('/api/assistant/chat', [middlewareAuthenticator, userInitiator, assistantGlobalLimiter, generateLimiter], async (req, res) => {
377
392
  // On récupère TOUTES les propriétés du body, y compris l'action confirmée
@@ -0,0 +1,16 @@
1
+
2
+ export const providers = {
3
+ "OpenAI" : {
4
+ key:"OPENAI_API_KEY",
5
+ defaultModel: 'gpt-3.5-turbo'
6
+ },
7
+ "Google": {
8
+ key:"GOOGLE_API_KEY"
9
+ },
10
+ "DeepSeek": {
11
+ key: "DEEPSEEK_API_KEY"
12
+ },
13
+ "Anthropic": {
14
+ key:"ANTHROPIC_API_KEY"
15
+ }
16
+ };
@@ -0,0 +1,304 @@
1
+ import {isPlainObject} from "../../core.js";
2
+ import {getCollection, getCollectionForUser, isObjectId, ObjectId} from "../mongodb.js";
3
+ import {getModel, handleDemoInitialization} from "./data.js";
4
+ import { Event} from "../../events.js"
5
+ import {Logger} from "../../gameObject.js";
6
+ import {hasPermission, middlewareAuthenticator, userInitiator} from "../user.js";
7
+ import {isLocalUser} from "../../data.js";
8
+
9
+ let logger;
10
+ /**
11
+ * Compare deux valeurs de manière récursive. Gère les ObjectId, les objets, les tableaux et les primitives.
12
+ * @param {*} a - Première valeur.
13
+ * @param {*} b - Deuxième valeur.
14
+ * @returns {boolean} - True si les valeurs sont sémantiquement égales.
15
+ */
16
+ function isEqual(a, b) {
17
+ if (a === b) return true;
18
+ if (a instanceof Date && b instanceof Date) return a.getTime() === b.getTime();
19
+ if (a instanceof ObjectId && b instanceof ObjectId) return a.toString() === b.toString();
20
+ if (!a || !b || (typeof a !== 'object' && typeof b !== 'object')) return a === b;
21
+ if (a.constructor !== b.constructor) return false;
22
+
23
+ if (Array.isArray(a)) {
24
+ if (a.length !== b.length) return false;
25
+ for (let i = 0; i < a.length; i++) {
26
+ if (!isEqual(a[i], b[i])) return false;
27
+ }
28
+ return true;
29
+ }
30
+
31
+ if (isPlainObject(a)) {
32
+ const keys = Object.keys(a);
33
+ if (keys.length !== Object.keys(b).length) return false;
34
+ for (const key of keys) {
35
+ if (!Object.prototype.hasOwnProperty.call(b, key) || !isEqual(a[key], b[key])) {
36
+ return false;
37
+ }
38
+ }
39
+ return true;
40
+ }
41
+
42
+ return false;
43
+ }
44
+ /**
45
+ * Calcule la différence entre deux documents pour les champs historisés.
46
+ * @param {object} beforeDoc - Le document avant modification.
47
+ * @param {object} afterDoc - Le document après modification.
48
+ * @param {string[]} historizedFields - La liste des champs à surveiller.
49
+ * @returns {object} - Un objet contenant les changements, ou un objet vide si rien n'a changé.
50
+ */
51
+ function calculateDiff(beforeDoc, afterDoc, historizedFields) {
52
+ const changes = {};
53
+ for (const fieldName of historizedFields) {
54
+ const beforeValue = beforeDoc[fieldName];
55
+ const afterValue = afterDoc[fieldName];
56
+
57
+ if (!isEqual(beforeValue, afterValue)) {
58
+ changes[fieldName] = {
59
+ from: beforeValue,
60
+ to: afterValue
61
+ };
62
+ }
63
+ }
64
+ // Retourne null si aucun changement n'a été détecté, ce qui est plus facile à vérifier qu'un objet vide.
65
+ return Object.keys(changes).length > 0 ? changes : null;
66
+ }
67
+
68
+
69
+ /**
70
+ * @route GET /api/data/history/:modelName/:recordId
71
+ * @desc Récupère l'historique d'un enregistrement spécifique pour le frontend.
72
+ * @access Private (géré par middlewareAuthenticator)
73
+ */
74
+ export async function handleGetHistoryRequest(req, res) {
75
+ const { modelName, recordId } = req.params;
76
+ const user = req.me; // Le middleware d'authentification attache l'utilisateur à req.me
77
+
78
+ try {
79
+ // 1. Vérification des permissions (similaire à searchData)
80
+ if (user && user.username !== 'demo' && isLocalUser(user) && (
81
+ !await hasPermission(["API_ADMIN", "API_SEARCH_DATA", "API_SEARCH_DATA_"+modelName], user) ||
82
+ await hasPermission(["API_SEARCH_DATA_NOT_"+modelName], user))) {
83
+ return res.status(403).json({ success: false, error: i18n.t('api.permission.searchData') });
84
+ }
85
+
86
+ // 2. Validation des entrées
87
+ if (!modelName || !recordId || !isObjectId(recordId)) {
88
+ return res.status(400).json({ success: false, error: "Invalid model name or record ID." });
89
+ }
90
+
91
+ // 3. Récupération des données depuis la collection 'history'
92
+ const historyCollection = getCollection('history');
93
+ const historyData = await historyCollection.find({
94
+ documentId: new ObjectId(recordId),
95
+ model: modelName
96
+ }).sort({ version: -1 }).toArray(); // Trier du plus récent au plus ancien
97
+
98
+ // 4. Transformation des données pour correspondre au format attendu par le composant HistoryDialog.jsx
99
+ const opMap = {
100
+ create: 'i',
101
+ update: 'u',
102
+ delete: 'd'
103
+ };
104
+
105
+ const transformedHistory = historyData.map(entry => {
106
+ const { documentId, version, operation, timestamp, user: historyUser, snapshot, changes, ...rest } = entry;
107
+
108
+ // Le contenu est soit un snapshot complet, soit un objet de changements
109
+ const dataPayload = snapshot || changes || {};
110
+
111
+ // On exclut les métadonnées du payload pour ne pas les dupliquer
112
+ const { _id: originalDocId, _model, _user, _hash, ...payloadFields } = dataPayload;
113
+
114
+ return {
115
+ ...rest, // Conserve l'_id du document d'historique lui-même
116
+ _rid: documentId,
117
+ _v: version,
118
+ _op: opMap[operation] || operation, // 'create' -> 'i', 'update' -> 'u', etc.
119
+ _updatedAt: timestamp,
120
+ _user: historyUser?.username,
121
+ ...payloadFields // Étale les champs du document (snapshot ou changes)
122
+ };
123
+ });
124
+
125
+ res.json({ success: true, data: transformedHistory });
126
+
127
+ } catch (error) {
128
+ logger.error(`[handleGetHistoryRequest] Error fetching history for model ${modelName}, record ${recordId}:`, error);
129
+ res.status(500).json({
130
+ success: false,
131
+ error: 'An internal server error occurred while fetching history.'
132
+ });
133
+ }
134
+ }
135
+
136
+
137
+ /**
138
+ * Initialise les écouteurs d'événements pour le module d'historique.
139
+ * @param {object} engine - L'instance du moteur.
140
+ */
141
+ export function onInit(engine) {
142
+ logger = engine.getComponent(Logger);
143
+
144
+ engine.get('/api/data/history/:modelName/:recordId', [middlewareAuthenticator, userInitiator], handleGetHistoryRequest);
145
+
146
+ // --- Écouteur pour la CRÉATION de données (Version 1 - Snapshot) ---
147
+ Event.Listen("OnDataAdded", async (engine, { modelName, insertedIds, user }) => {
148
+ try {
149
+ const model = await getModel(modelName, user);
150
+ if (!model?.history?.enabled) return;
151
+
152
+ const dataCollection = await getCollectionForUser(user);
153
+ const historyCollection = getCollection('history');
154
+
155
+ const newDocs = await dataCollection.find({ _id: { $in: insertedIds.map(id => new ObjectId(id)) } }).toArray();
156
+
157
+ for (const doc of newDocs) {
158
+ await historyCollection.insertOne({
159
+ documentId: doc._id,
160
+ model: modelName,
161
+ timestamp: new Date(),
162
+ user: { _id: user._id, username: user.username },
163
+ version: 1,
164
+ operation: 'create',
165
+ snapshot: doc // Pour la création, on stocke un snapshot complet
166
+ });
167
+ logger.debug(`History v1 (create) created for ${modelName} document ${doc._id}`);
168
+ }
169
+ } catch (error) {
170
+ logger.error("History Module (OnDataAdded) Error:", error);
171
+ }
172
+ }, "event", "system");
173
+
174
+ // --- Écouteur pour la MODIFICATION de données (Versions > 1 - Diff) ---
175
+ Event.Listen("OnDataEdited", async (engine, { modelName, user, before, after }) => {
176
+ try {
177
+ const model = await getModel(modelName, user);
178
+ if (!model?.history?.enabled) return;
179
+
180
+ // Détermine les champs à historiser. Si non spécifié, tous les champs le sont.
181
+ const historizedFields = model.history.fields
182
+ ? Object.keys(model.history.fields).filter(f => model.history.fields[f] === true)
183
+ : model.fields.map(f => f.name);
184
+
185
+ if (historizedFields.length === 0) return; // Pas de champs à suivre
186
+
187
+ const historyCollection = getCollection('history');
188
+
189
+ for (const afterDoc of after) {
190
+ const beforeDoc = before.find(b => b._id.toString() === afterDoc._id.toString());
191
+ if (!beforeDoc) {
192
+ logger.warn(`History Module: Could not find 'before' state for document ${afterDoc._id}. Skipping history record.`);
193
+ continue;
194
+ }
195
+
196
+ const changes = calculateDiff(beforeDoc, afterDoc, historizedFields);
197
+
198
+ // S'il n'y a aucun changement sur les champs surveillés, on ne crée pas d'entrée.
199
+ if (!changes) {
200
+ continue;
201
+ }
202
+
203
+ // Récupérer la dernière version pour incrémenter
204
+ const lastVersionDoc = await historyCollection.findOne({ documentId: afterDoc._id }, { sort: { version: -1 } });
205
+ const newVersion = lastVersionDoc ? lastVersionDoc.version + 1 : 2; // v1 est la création
206
+
207
+ await historyCollection.insertOne({
208
+ documentId: afterDoc._id,
209
+ model: modelName,
210
+ timestamp: new Date(),
211
+ user: { _id: user._id, username: user.username },
212
+ version: newVersion,
213
+ operation: 'update',
214
+ changes: changes // On stocke uniquement les différences
215
+ });
216
+ logger.debug(`History v${newVersion} (update) created for ${modelName} document ${afterDoc._id}`);
217
+ }
218
+ } catch (error) {
219
+ logger.error("History Module (OnDataEdited) Error:", error);
220
+ }
221
+ }, "event", "system");
222
+
223
+ // --- Écouteur pour la SUPPRESSION de données (Snapshot final) ---
224
+ Event.Listen("OnDataDeleted", async (engine, { modelName, user, before }) => {
225
+ // 'before' est un tableau des documents complets juste avant leur suppression.
226
+ try {
227
+ const model = await getModel(modelName, user);
228
+ if (!model?.history?.enabled) return;
229
+
230
+ const historyCollection = getCollection('history');
231
+
232
+ for (const deletedDoc of before) {
233
+ // Récupérer la dernière version pour incrémenter
234
+ const lastVersionDoc = await historyCollection.findOne({ documentId: deletedDoc._id }, { sort: { version: -1 } });
235
+ // Si aucune version n'existe, c'est peut-être un cas où l'historique a été activé après la création.
236
+ // On commence à 1, sinon on incrémente.
237
+ const newVersion = lastVersionDoc ? lastVersionDoc.version + 1 : 1;
238
+
239
+ await historyCollection.insertOne({
240
+ documentId: deletedDoc._id,
241
+ model: modelName,
242
+ timestamp: new Date(),
243
+ user: { _id: user._id, username: user.username },
244
+ version: newVersion,
245
+ operation: 'delete',
246
+ // On stocke un snapshot final du document supprimé pour audit ou restauration.
247
+ snapshot: deletedDoc
248
+ });
249
+ logger.debug(`History v${newVersion} (delete) created for ${modelName} document ${deletedDoc._id}`);
250
+ }
251
+ } catch (error) {
252
+ logger.error("History Module (OnDataDeleted) Error:", error);
253
+ }
254
+ }, "event", "system");
255
+
256
+ logger.info("History module initialized and listening for data events.");
257
+ }
258
+
259
+
260
+ /**
261
+ * Purge (supprime définitivement) des documents et tout leur historique associé.
262
+ * C'est une opération destructive à utiliser avec précaution.
263
+ * @param {object} user - L'utilisateur effectuant l'opération (pour les permissions).
264
+ * @param {string} modelName - Le nom du modèle concerné.
265
+ * @param {object} filter - Le filtre MongoDB pour trouver les documents à purger.
266
+ * @returns {Promise<{success: boolean, purgedCount: number, historyPurgedCount: number, error?: string}>}
267
+ */
268
+ export async function purgeData(user, modelName = null, filter=null) {
269
+ const logger = new Logger("purgeData");
270
+ try {
271
+ const dataCollection = await getCollectionForUser(user);
272
+ const historyCollection = getCollection('history');
273
+
274
+ let m = modelName || { _model: modelName };
275
+ const f= filter || { _user: user.username };
276
+ // 1. Trouver les documents à purger pour récupérer leurs IDs
277
+ const docsToPurge = await dataCollection.find({ ...m, ...f }).project({ _id: 1 }).toArray();
278
+ if (docsToPurge.length === 0) {
279
+ return { success: true, purgedCount: 0, historyPurgedCount: 0 };
280
+ }
281
+ const docIdsToPurge = docsToPurge.map(d => d._id);
282
+
283
+ // 2. Purger l'historique associé à ces documents
284
+ const historyResult = await historyCollection.deleteMany({ documentId: { $in: docIdsToPurge } });
285
+
286
+ // 3. Purger les documents eux-mêmes
287
+ const dataResult = await dataCollection.deleteMany({ _id: { $in: docIdsToPurge } });
288
+
289
+ logger.info(`Purged ${dataResult.deletedCount} documents and ${historyResult.deletedCount} history entries for model '${modelName}'.`);
290
+
291
+ // On pourrait aussi émettre un événement "OnDataPurged" ici si nécessaire
292
+ await Event.Trigger("OnDataPurged", { user, modelName, purgedIds: docIdsToPurge });
293
+
294
+ return {
295
+ success: true,
296
+ purgedCount: dataResult.deletedCount,
297
+ historyPurgedCount: historyResult.deletedCount
298
+ };
299
+
300
+ } catch (error) {
301
+ logger.error(`Error during data purge for model '${modelName}':`, error);
302
+ return { success: false, purgedCount: 0, historyPurgedCount: 0, error: error.message };
303
+ }
304
+ }