data-primals-engine 1.4.0 → 1.4.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.
Files changed (40) hide show
  1. package/README.md +856 -797
  2. package/client/src/AssistantChat.jsx +48 -5
  3. package/client/src/AssistantChat.scss +0 -1
  4. package/client/src/ChartConfigModal.jsx +34 -9
  5. package/client/src/Dashboard.jsx +396 -349
  6. package/client/src/DashboardChart.jsx +91 -12
  7. package/client/src/DataTable.jsx +817 -807
  8. package/client/src/Field.jsx +1784 -1782
  9. package/client/src/PackGallery.jsx +391 -290
  10. package/client/src/PackGallery.scss +231 -210
  11. package/client/src/contexts/UIContext.jsx +5 -2
  12. package/client/src/translations.js +188 -0
  13. package/package.json +19 -8
  14. package/src/config.js +2 -2
  15. package/src/core.js +21 -3
  16. package/src/engine.js +2 -1
  17. package/src/events.js +4 -3
  18. package/src/gameObject.js +1 -1
  19. package/src/middlewares/middleware-mongodb.js +3 -1
  20. package/src/modules/assistant/assistant.js +131 -42
  21. package/src/modules/auth-google/index.js +51 -0
  22. package/src/modules/auth-microsoft/index.js +82 -0
  23. package/src/modules/auth-saml/index.js +90 -0
  24. package/src/modules/bucket.js +3 -2
  25. package/src/modules/data/data.backup.js +374 -0
  26. package/src/modules/data/data.history.js +11 -8
  27. package/src/modules/data/data.js +190 -4543
  28. package/src/modules/data/data.operations.js +2790 -0
  29. package/src/modules/data/data.relations.js +687 -0
  30. package/src/modules/data/data.routes.js +1785 -1646
  31. package/src/modules/data/data.scheduling.js +274 -0
  32. package/src/modules/data/data.validation.js +245 -0
  33. package/src/modules/data/index.js +29 -1
  34. package/src/modules/user.js +2 -1
  35. package/src/modules/workflow.js +3 -2
  36. package/src/providers.js +110 -1
  37. package/src/services/stripe.js +3 -2
  38. package/src/sso.js +194 -0
  39. package/test/data.integration.test.js +3 -0
  40. package/test/user.test.js +1 -1
@@ -0,0 +1,274 @@
1
+ import {getCollection} from "../mongodb.js";
2
+ import schedule from "node-schedule";
3
+ import {maxAlertsPerUser} from "../../constants.js";
4
+ import {ObjectId} from "mongodb";
5
+ import {getSmtpConfig} from "../user.js";
6
+ import {runScheduledJobWithDbLock, substituteVariables} from "../workflow.js";
7
+ import i18n from "../../i18n.js";
8
+ import {sendEmail} from "../../email.js";
9
+ import {sendSseToUser} from "./data.routes.js";
10
+
11
+ import {searchData} from "./data.operations.js";
12
+ import {Logger} from "../../gameObject.js";
13
+
14
+
15
+ let engine, logger;
16
+ export function onInit(defaultEngine) {
17
+ engine = defaultEngine;
18
+ logger = engine.getComponent(Logger);
19
+ }
20
+
21
+ export const cancelAlerts = async (user) => {
22
+
23
+ const datasCollection = getCollection('datas'); // Alerts are in the global collection
24
+
25
+ // 1. Fetch the latest state of the alert
26
+ const alertDocs = await datasCollection.find({_user: user.username, _model: 'alert'}).toArray();
27
+ alertDocs.forEach(doc => {
28
+ const jobId = `alert_${doc._id}`;
29
+ schedule.scheduledJobs[jobId]?.cancel();
30
+ });
31
+ }
32
+
33
+ /**
34
+ * Executes a stateful alert job. It checks if a notification for an alert
35
+ * has already been sent and only sends one if the condition is met and no
36
+ * recent notification exists. The state is tracked via the 'lastNotifiedAt'
37
+ * field in the alert document.
38
+ * @param {string|ObjectId} alertId - The ID of the alert to process.
39
+ */
40
+ export async function runStatefulAlertJob(alertId) {
41
+ const jobId = `alert_${alertId}`;
42
+ logger.info(`[Scheduled Job] Cron triggered for stateful alert job ${jobId}.`);
43
+
44
+ try {
45
+ const datasCollection = getCollection('datas'); // Alerts are in the global collection
46
+
47
+ // 1. Fetch the latest state of the alert
48
+ const alertDoc = await datasCollection.findOne({_id: new ObjectId(alertId)});
49
+
50
+ // Safety checks
51
+ if (!alertDoc) {
52
+ logger.warn(`[Scheduled Job] Alert ${alertId} not found. Cancelling job.`);
53
+ schedule.scheduledJobs[jobId]?.cancel();
54
+ return;
55
+ }
56
+
57
+ if (!alertDoc.isActive || !alertDoc.frequency) {
58
+ logger.info(`[Scheduled Job] Alert ${alertId} is no longer active or has no frequency. Cancelling job.`);
59
+ schedule.scheduledJobs[jobId]?.cancel();
60
+ return;
61
+ }
62
+
63
+ // 2. Check if a notification has already been sent for this state
64
+ if (alertDoc.lastNotifiedAt) {
65
+ logger.debug(`[Scheduled Job] Notification for alert ${alertId} has already been sent. Skipping evaluation.`);
66
+ return;
67
+ }
68
+
69
+ // 3. Evaluate the trigger condition
70
+ const apiFilter = (alertDoc.triggerCondition);
71
+ const {count} = await searchData({
72
+ model: alertDoc.targetModel,
73
+ filter: apiFilter,
74
+ limit: 1
75
+ }, {username: alertDoc._user});
76
+
77
+ // 4. If condition is met, send notification and update state
78
+ if (count > 0) {
79
+ logger.info(`[Scheduled Job] Condition met for alert ${alertDoc.name} (ID: ${alertId}). Sending notification and updating state.`);
80
+
81
+ let emailSent = false;
82
+ try {
83
+ const user = await engine.userProvider.findUserByUsername(alertDoc._user);
84
+ if (user && user.email) {
85
+ const smtpConfig = await getSmtpConfig(user);
86
+ if (alertDoc.sendEmail && smtpConfig) {
87
+ const userLang = user.lang || 'en';
88
+ let emailContent, msg;
89
+ if (alertDoc.message) {
90
+ if (alertDoc.message[userLang])
91
+ msg = alertDoc.message[userLang];
92
+ else
93
+ msg = alertDoc.message[Object.keys(alertDoc.message)[0]];
94
+ emailContent = await substituteVariables(msg, {count, alert: alertDoc});
95
+ } else {
96
+ // Sinon, utiliser le message par défaut
97
+ emailContent = i18n.t('alert.email.content', `L'alerte '${alertDoc.name}' s'est déclenchée. ${count} élément(s) correspondent à votre condition.`, {
98
+ name: alertDoc.name,
99
+ count: count
100
+ });
101
+ }
102
+
103
+ await sendEmail(
104
+ user.email,
105
+ {
106
+ title: i18n.t('alert.email.title', `Alerte: ${alertDoc.name}`),
107
+ content: emailContent
108
+ },
109
+ smtpConfig,
110
+ userLang
111
+ );
112
+ emailSent = true;
113
+ logger.info(`[Scheduled Job] Email notification sent for alert ${alertId} to ${user.email}.`);
114
+ } else if (alertDoc.sendEmail) {
115
+ logger.warn(`[Scheduled Job] Could not send email for alert ${alertId}. SMTP config is missing or incomplete for user ${user.username}.`);
116
+ }
117
+ } else {
118
+ logger.warn(`[Scheduled Job] Could not send email for alert ${alertId}. User ${alertDoc._user} not found or has no email address.`);
119
+ }
120
+ } catch (emailError) {
121
+ logger.error(`[Scheduled Job] Failed to send email for alert ${alertId}:`, emailError);
122
+ }
123
+
124
+ // Send notification
125
+ const alertPayload = {
126
+ type: 'cron_alert',
127
+ triggerId: alertDoc._id.toString(),
128
+ triggerName: alertDoc.name,
129
+ timestamp: new Date().toISOString(),
130
+ message: `Alerte '${alertDoc.name}': ${count} élément(s) correspondent à votre condition.`,
131
+ emailSent
132
+ };
133
+ sendSseToUser(alertDoc._user, alertPayload);
134
+
135
+ // Update state in DB to prevent re-notification
136
+ await datasCollection.updateOne(
137
+ {_id: new ObjectId(alertId)},
138
+ {$set: {lastNotifiedAt: new Date()}}
139
+ );
140
+ } else {
141
+ logger.debug(`[Scheduled Job] Condition not met for alert ${alertId}. No notification sent.`);
142
+ }
143
+
144
+ } catch (error) {
145
+ logger.error(`[Scheduled Job] Error processing stateful alert job ${jobId}:`, error);
146
+ }
147
+ }
148
+
149
+ export async function scheduleAlerts() {
150
+ logger.info('[scheduleAlerts] Starting scheduling of oldest active alerts per user...');
151
+ try {
152
+ const datasCollection = getCollection('datas');
153
+
154
+ // --- NOUVELLE LOGIQUE AVEC AGRÉGATION ---
155
+ const aggregationPipeline = [
156
+ // 1. Match: Ne sélectionner que les alertes actives avec une fréquence définie.
157
+ {
158
+ $match: {
159
+ _model: 'alert',
160
+ isActive: true,
161
+ frequency: {$exists: true, $ne: ""}
162
+ }
163
+ },
164
+ // 2. Sort: Trier les alertes par utilisateur, puis par date de création (les plus anciennes en premier).
165
+ // L'ObjectId contient un timestamp, donc trier par _id est équivalent à trier par date de création.
166
+ {
167
+ $sort: {
168
+ _user: 1, // Grouper par utilisateur
169
+ _id: 1 // Trier par date de création (ascendant)
170
+ }
171
+ },
172
+ // 3. Group: Regrouper toutes les alertes par utilisateur dans un tableau.
173
+ {
174
+ $group: {
175
+ _id: "$_user", // La clé de groupement est le nom de l'utilisateur
176
+ alerts: {$push: "$$ROOT"} // $$ROOT pousse le document entier dans le tableau 'alerts'
177
+ }
178
+ },
179
+ // 4. Project (Slice): Pour chaque utilisateur, ne garder que les X premières alertes du tableau trié.
180
+ {
181
+ $project: {
182
+ oldestAlerts: {$slice: ["$alerts", maxAlertsPerUser]}
183
+ }
184
+ },
185
+ // 5. Unwind: Déconstruire le tableau 'oldestAlerts' pour obtenir un flux de documents, un par alerte.
186
+ {
187
+ $unwind: "$oldestAlerts"
188
+ },
189
+ // 6. ReplaceRoot: Remplacer la structure du document par le contenu de l'alerte elle-même.
190
+ {
191
+ $replaceRoot: {newRoot: "$oldestAlerts"}
192
+ }
193
+ ];
194
+
195
+ const alertsToSchedule = await datasCollection.aggregate(aggregationPipeline).toArray();
196
+ // --- FIN DE LA NOUVELLE LOGIQUE ---
197
+
198
+ logger.info(`[scheduleAlerts] Found ${alertsToSchedule.length} oldest active alerts across all users to schedule.`);
199
+
200
+ for (const alertDoc of alertsToSchedule) {
201
+ const jobId = `alert_${alertDoc._id}`;
202
+ try {
203
+ // Annuler une tâche existante pour la même alerte si elle existe (logique de sécurité)
204
+ if (schedule.scheduledJobs[jobId]) {
205
+ schedule.scheduledJobs[jobId].cancel();
206
+ }
207
+ // Planifier la tâche avec la nouvelle logique stateful
208
+ schedule.scheduleJob(jobId, alertDoc.frequency, () => runStatefulAlertJob(alertDoc._id));
209
+ } catch (scheduleError) {
210
+ logger.error(`[scheduleAlerts] Failed to schedule job ${jobId} for alert ${alertDoc._id}. Error: ${scheduleError.message}`);
211
+ }
212
+ }
213
+ logger.info('[scheduleAlerts] Finished scheduling alerts.');
214
+ } catch (error) {
215
+ logger.error('[scheduleAlerts] A critical error occurred during alert scheduling:', error);
216
+ }
217
+ }
218
+
219
+
220
+ /**
221
+ * Applique un masque et des valeurs par défaut à une expression cron.
222
+ * @param {string} cronString - L'expression cron d'entrbooléens. `false` pour désactiver et appliquer la valeur par défaut.
223
+ * @param {string[]} defaults - Un tableau de 5 chaînes de caractères pour les valeurs par défaut.
224
+ * @returns {string} - L'expression cron modifiée.
225
+ */
226
+ export function applyCronMask(cronString, mask, defaults) {
227
+ if (typeof cronString !== 'string' || cronString.trim() === '' || !mask || !defaults) {
228
+ return cronString;
229
+ }
230
+ const parts = cronString.split(' ');
231
+ if (parts.length < 5) {
232
+ return cronString; // Laisse la validation standard gérer les formats incorrects
233
+ }
234
+
235
+ const newParts = parts.slice(0, 5).map((part, index) => {
236
+ // Si le masque à cet index est `false`, on force la valeur par défaut.
237
+ if (mask[index] === false) {
238
+ return defaults[index];
239
+ }
240
+ // Sinon, on garde la valeur de l'utilisateur.
241
+ return part;
242
+ });
243
+
244
+ return newParts.join(' ');
245
+ }
246
+
247
+ export async function handleScheduledJobs(modelName, existingDocs, collection, updateData) {
248
+ for (const doc of existingDocs) {
249
+ const jobId = `${modelName}_${doc._id}`;
250
+ const existingJob = schedule.scheduledJobs[jobId];
251
+ if (existingJob) existingJob.cancel();
252
+
253
+ const updatedDoc = {...doc, ...updateData};
254
+ if (modelName === 'workflowTrigger' && updatedDoc.isActive && updatedDoc.cronExpression) {
255
+ schedule.scheduleJob(jobId, updatedDoc.cronExpression, async () => {
256
+ logger.info(`[Scheduled Job] Cron triggered for job ${jobId}`);
257
+ await runScheduledJobWithDbLock(jobId, async () => {
258
+ sendSseToUser(updatedDoc._user, {
259
+ type: 'cron_alert',
260
+ triggerId: updatedDoc._id.toString(),
261
+ triggerName: updatedDoc.name,
262
+ timestamp: new Date().toISOString(),
263
+ message: `L'alerte planifiée '${updatedDoc.name || 'Sans nom'}' a été déclenchée.`
264
+ });
265
+ }, updatedDoc.lockDurationMinutes || 5);
266
+ });
267
+ }
268
+
269
+ if (modelName === 'alert' && updatedDoc.isActive && updatedDoc.frequency) {
270
+ await collection.updateOne({_id: updatedDoc._id}, {$set: {lastNotifiedAt: null}});
271
+ schedule.scheduleJob(jobId, updatedDoc.frequency, () => runStatefulAlertJob(updatedDoc._id));
272
+ }
273
+ }
274
+ }
@@ -0,0 +1,245 @@
1
+ import {Event} from "../../events.js";
2
+ import i18n from "../../i18n.js";
3
+ import {allowedFields, maxFileSize, maxModelNameLength, maxStringLength} from "../../constants.js";
4
+ import {getDefaultForType} from "../../data.js";
5
+
6
+ import {dataTypes} from "./data.operations.js";
7
+ import {Logger} from "../../gameObject.js";
8
+
9
+
10
+ let engine, logger;
11
+ export function onInit(defaultEngine) {
12
+ engine = defaultEngine;
13
+ logger = engine.getComponent(Logger);
14
+ }
15
+
16
+ export async function validateModelStructure(modelStructure) {
17
+ return await Event.Trigger("OnValidateModelStructure", "event", "system", modelStructure);
18
+ }
19
+
20
+ export const validateField = (field) => {
21
+
22
+ const allowedFieldTest = (fields) => {
23
+ // Check for unknown fields
24
+ const unknownFields = Object.keys(field).filter(f => ![...allowedFields, ...fields].includes(f));
25
+
26
+ if (unknownFields.length > 0) {
27
+ throw new Error(i18n.t('api.validate.unknowField', `Propriété(s) non reconnue(s): '{{0}}' pour le champ '{{1}}'`, [unknownFields.join(', '), field.name]));
28
+ }
29
+
30
+ const fieldInvalid = Object.keys(fields).find(f => JSON.stringify(field[f] || '').length > maxStringLength);
31
+ if (fieldInvalid) {
32
+ throw new Error(i18n.t('api.validate.invalidField', `Champ(s) non valide(s): '{{0}}'`, [fieldInvalid.name]));
33
+ }
34
+ }
35
+
36
+ // Check for required fields
37
+ if (!field.name || typeof field.name !== 'string') {
38
+ throw new Error(i18n.t('api.validate.requiredFieldString', "Le champ '{{0}}' est requis et doit être une chaîne de caractères.", ["name"]));
39
+ }
40
+ if (!field.type || typeof field.type !== 'string') {
41
+ throw new Error(i18n.t('api.validate.requiredFieldString', "Le champ '{{0}}' est requis et doit être une chaîne de caractères.", ["type"]));
42
+ }
43
+
44
+ // Check for specific field types
45
+ switch (field.type) {
46
+ case 'relation':
47
+ allowedFieldTest(['relation', 'multiple', 'relationFilter']);
48
+ if (!field.relation || typeof field.relation !== 'string' || field.relation.length > maxModelNameLength) {
49
+ throw new Error(i18n.t('api.validate.requiredFieldString', "Le champ '{{0}}' est requis et doit être une chaîne de caractères.", ["relation"]));
50
+ }
51
+ if (field.multiple !== undefined && typeof field.multiple !== 'boolean') {
52
+ throw new Error(i18n.t('api.validate.fieldBoolean', "L'attribut '{{0}}' doit être un booléen.", ["multiple"]));
53
+ }
54
+ if (field.relationFilter && typeof field.relationFilter !== 'object') {
55
+ throw new Error(i18n.t('api.validate.fieldObject', "L'attribut '{{0}}' doit être un objet.", ["relationFilter"]));
56
+ }
57
+ break;
58
+ case 'enum': {
59
+ allowedFieldTest(['items']);
60
+ if (!field.items || !Array.isArray(field.items) || field.items.length === 0) {
61
+ throw new Error(i18n.t('api.validate.fieldStringArray', "L'attribut '{{0}}' doit être un tableau de chaines de caractères.", ["items"]));
62
+ }
63
+ let id = field.items.findIndex(item => typeof item !== 'string');
64
+ if (id !== -1) {
65
+ throw new Error(i18n.t('api.validate.fieldString', "Le champ '{{0}}' doit être une chaîne de caractères.", ["items[" + id + "]"]));
66
+ }
67
+ break;
68
+ }
69
+ case 'number':
70
+ allowedFieldTest(['min', 'max', 'step', 'unit', 'delay', 'gauge', 'percent']);
71
+ if (field.min !== undefined && typeof field.min !== 'number') {
72
+ throw new Error(i18n.t('api.validate.fieldNumber', "L'attribut '{{0}}' doit être un nombre.", ["min"]));
73
+ }
74
+ if (field.max !== undefined && typeof field.max !== 'number') {
75
+ throw new Error(i18n.t('api.validate.fieldNumber', "L'attribut '{{0}}' doit être un nombre.", ["max"]));
76
+ }
77
+ if (field.max < field.min) {
78
+ throw new Error(i18n.t('api.validate.inferiorTo', "L'attribut '{{0}}' doit être inférieur à l'attribut '{{1}}'.", ["min", "max"]));
79
+ }
80
+ if (field.step !== undefined && typeof field.step !== 'number') {
81
+ throw new Error(i18n.t('api.validate.fieldNumber', "L'attribut '{{0}}' doit être un nombre.", ["step"]));
82
+ }
83
+ if (field.unit !== undefined && typeof field.unit !== 'string') {
84
+ throw new Error(i18n.t('api.validate.fieldString', "Le champ '{{0}}' doit être une chaîne de caractères.", ["unit"]));
85
+ }
86
+ if (field.delay !== undefined && typeof field.delay !== 'boolean') {
87
+ throw new Error(i18n.t('api.validate.fieldBoolean', "Le champ '{{0}}' doit être un booléen.", ["unit"]));
88
+ }
89
+ if (field.gauge !== undefined && typeof field.gauge !== 'boolean') {
90
+ throw new Error(i18n.t('api.validate.fieldBoolean', "L'attribut '{{0}}' doit être un booléen.", ["gauge"]));
91
+ }
92
+ if (field.percent !== undefined && typeof field.percent !== 'boolean') {
93
+ throw new Error(i18n.t('api.validate.fieldBoolean', "L'attribut '{{0}}' doit être un booléen.", ["percent"]));
94
+ }
95
+ break;
96
+ case 'string':
97
+ case 'string_t':
98
+ case 'richtext':
99
+ case 'richtext_t':
100
+ case 'url':
101
+ case 'email':
102
+ case 'phone':
103
+ case 'password':
104
+ case 'code':
105
+ if (field.type === 'code')
106
+ allowedFieldTest(['maxlength', 'language', 'conditionBuilder', 'targetModel']);
107
+ else if (['string_t', 'string'].includes(field.type))
108
+ allowedFieldTest(['maxlength', 'multiline']);
109
+ else
110
+ allowedFieldTest(['maxlength']);
111
+ if (field.maxlength !== undefined && typeof field.maxlength !== 'number') {
112
+ throw new Error(i18n.t('api.validate.fieldNumber', "L'attribut '{{0}}' doit être un nombre.", ["maxlength"]));
113
+ }
114
+ break;
115
+ case 'model':
116
+ case 'modelField':
117
+ allowedFieldTest([]);
118
+ break;
119
+ case 'object':
120
+ allowedFieldTest([]);
121
+ break;
122
+ case 'boolean':
123
+ allowedFieldTest([]);
124
+ break;
125
+ case 'date':
126
+ case 'datetime': {
127
+ allowedFieldTest(['min', 'max']);
128
+ if (field.min !== undefined && typeof field.min !== 'string') {
129
+ throw new Error(i18n.t('api.validate.fieldString', "Le champ '{{0}}' doit être une chaîne de caractères.", ["min"]));
130
+ }
131
+ if (field.max !== undefined && typeof field.max !== 'string') {
132
+ throw new Error(i18n.t('api.validate.fieldString', "Le champ '{{0}}' doit être une chaîne de caractères.", ["max"]));
133
+ }
134
+ const dtMin = field.min ? new Date(field.min) : null;
135
+ const dtMax = field.max ? new Date(field.max) : null;
136
+ if (dtMin && dtMax && dtMin > dtMax) {
137
+ throw new Error(i18n.t('api.validate.inferiorTo', "L'attribut '{{0}}' doit être inférieur à l'attribut '{{1}}'.", ["min", "max"]));
138
+ }
139
+ break;
140
+ }
141
+ case 'image':
142
+ case 'file': {
143
+ allowedFieldTest(['mimeTypes', 'maxSize']);
144
+ if (field.mimeTypes !== undefined && !Array.isArray(field.mimeTypes)) {
145
+ throw new Error(i18n.t('api.validate.fieldStringArray', "L'attribut '{{0}}' doit être un tableau de chaines de caractères.", ["mimeTypes"]));
146
+ }
147
+ let id;
148
+ if (field.mimeTypes !== undefined && (id = field.mimeTypes.findIndex(item => typeof item !== 'string')) !== -1) {
149
+ throw new Error(i18n.t('api.validate.fieldString', "Le champ '{{0}}' doit être une chaîne de caractères.", ["mimeTypes[" + id + "]"]));
150
+ }
151
+ if (field.maxSize !== undefined && typeof field.maxSize !== 'number') {
152
+ throw new Error(i18n.t('api.validate.fieldNumber', "L'attribut '{{0}}' doit être un nombre.", ["maxSize"]));
153
+ }
154
+ if (field.maxSize !== undefined && field.maxSize > maxFileSize) {
155
+ throw new Error(i18n.t('api.validate.fileSize', `L'attribut 'maxSize' ne doit pas dépasser {{0}} octets.`, [maxFileSize]));
156
+ }
157
+ break;
158
+ }
159
+ case 'color':
160
+ allowedFieldTest([]);
161
+ return true;
162
+ case 'cronSchedule':
163
+ allowedFieldTest(['cronMask']);
164
+ return true;
165
+ case 'calculated':
166
+ allowedFieldTest(['calculation']);
167
+ return true;
168
+ case 'array':
169
+ if (!field.itemsType || typeof field.itemsType !== 'string') {
170
+ throw new Error(i18n.t('api.validate.fieldString', "Le champ '{{0}}' doit être une chaîne de caractères.", ["itemsType"]));
171
+ }
172
+ if (!dataTypes[field.itemsType]) {
173
+ throw new Error(i18n.t('api.validate.invalidField', `Champ(s) non valide(s): '{{0}}'`, ["itemsType"]));
174
+ }
175
+ if (field.minItems !== undefined && typeof field.minItems !== 'number') {
176
+ throw new Error(i18n.t('api.validate.fieldNumber', "L'attribut '{{0}}' doit être un nombre.", ["minItems"]));
177
+ }
178
+ if (field.maxItems !== undefined && typeof field.maxItems !== 'number') {
179
+ throw new Error(i18n.t('api.validate.fieldNumber', "L'attribut '{{0}}' doit être un nombre.", ["maxItems"]));
180
+ }
181
+ break;
182
+ default:
183
+ throw new Error(i18n.t('api.validate.unknowType', `Le type '{{0}}' n'est pas reconnu.`, [field.type]));
184
+ }
185
+
186
+ // Check for optional fields
187
+ if (field.required !== undefined && typeof field.required !== 'boolean') {
188
+ throw new Error(i18n.t('api.validate.fieldBoolean', "L'attribut '{{0}}' doit être un booléen.", ["required"]));
189
+ }
190
+ if (field.hint !== undefined && typeof field.hint !== 'string') {
191
+ throw new Error(i18n.t('api.validate.fieldString', "Le champ '{{0}}' doit être une chaîne de caractères.", ["hint"]));
192
+ }
193
+ if (field.default !== undefined && field.default !== null && typeof field.default !== typeof getDefaultForType(field) && typeof field.default !== 'function') {
194
+ throw new Error(i18n.t('api.validate.sameType', `L'attribut '{{0}}' doit être du même type que l'attribut '{{0}}' (${field.type}).`, ['default', 'type']));
195
+ }
196
+ if (field.validate !== undefined && typeof field.validate !== 'function') {
197
+ throw new Error(i18n.t('api.validate.fieldFunction', "L'attribut '{{0}}' doit être une fonction.", ['validate']));
198
+ }
199
+ if (field.unique !== undefined && typeof field.unique !== 'boolean') {
200
+ throw new Error(i18n.t('api.validate.fieldBoolean', "L'attribut '{{0}}' doit être un booléen.", ["unique"]));
201
+ }
202
+ if (field.placeholder !== undefined && typeof field.placeholder !== 'string') {
203
+ throw new Error(i18n.t('api.validate.fieldString', "Le champ '{{0}}' doit être une chaîne de caractères.", ["placeholder"]));
204
+ }
205
+ if (field.asMain !== undefined && typeof field.asMain !== 'boolean') {
206
+ throw new Error(i18n.t('api.validate.fieldBoolean', "L'attribut '{{0}}' doit être un booléen.", ["asMain"]));
207
+ }
208
+ if (field.unit !== undefined && typeof field.unit !== 'string') {
209
+ throw new Error(i18n.t('api.validate.fieldString', "Le champ '{{0}}' doit être une chaîne de caractères.", ["unit"]));
210
+ }
211
+
212
+ return true;
213
+ };
214
+
215
+ /**
216
+ * Valide la structure et le contenu du document selon le modèle
217
+ */
218
+ export async function validateModelData(doc, model, isPatch = false) {
219
+ if (!isPatch) {
220
+ model.fields.forEach(field => {
221
+ const value = doc[field.name];
222
+ if (field.required) {
223
+ if (value === undefined && !('default' in field)) {
224
+ throw new Error(i18n.t('api.field.missingRequired', {field: field.name + " (" + model.name + ")"}));
225
+ }
226
+ if (value === '' || value === null) {
227
+ throw new Error(i18n.t('api.field.requiredCannotBeEmpty', {field: field.name}));
228
+ }
229
+ }
230
+ });
231
+ }
232
+
233
+ // 2. Validation des types de champs (toujours exécutée pour les champs fournis)
234
+ for (const [fieldName, value] of Object.entries(doc)) {
235
+ const fieldDef = model.fields.find(f => f.name === fieldName);
236
+ if (!fieldDef) continue; // On ignore les champs supplémentaires
237
+
238
+ const validator = dataTypes[fieldDef.type]?.validate;
239
+ const valid = validator && validator(value, fieldDef);
240
+ const realValidation = await Event.Trigger('OnDataValidate', "event", "system", value, fieldDef, doc);
241
+ if (!(valid || realValidation)) {
242
+ throw new Error(i18n.t('api.field.validationFailed', {field: fieldName, value}));
243
+ }
244
+ }
245
+ }
@@ -1 +1,29 @@
1
- export * from './data.js';
1
+ export * from './data.js';
2
+ export {validateModelData} from "./data.validation.js";
3
+ export {validateField} from "./data.validation.js";
4
+ export {validateModelStructure} from "./data.validation.js";
5
+ export {scheduleAlerts} from "./data.scheduling.js";
6
+ export {runStatefulAlertJob} from "./data.scheduling.js";
7
+ export {cancelAlerts} from "./data.scheduling.js";
8
+ export {installAllPacks} from "./data.operations.js";
9
+ export {installPack} from "./data.operations.js";
10
+ export {exportData} from "./data.operations.js";
11
+ export {importData} from "./data.operations.js";
12
+ export {searchData} from "./data.operations.js";
13
+ export {deleteData} from "./data.operations.js";
14
+ export {editData} from "./data.operations.js";
15
+ export {patchData} from "./data.operations.js";
16
+ export {pushDataUnsecure} from "./data.operations.js";
17
+ export {insertData} from "./data.operations.js";
18
+ export {getModels} from "./data.operations.js";
19
+ export {getModel} from "./data.operations.js";
20
+ export {deleteModels} from "./data.operations.js";
21
+ export {createModel} from "./data.operations.js";
22
+ export {editModel} from "./data.operations.js";
23
+ export {dataTypes} from "./data.operations.js";
24
+ export {dumpUserData} from "./data.backup.js";
25
+ export {loadFromDump} from "./data.backup.js";
26
+ export {validateRestoreRequest} from "./data.backup.js";
27
+ export {jobDumpUserData} from "./data.backup.js";
28
+ export {handleCustomEndpointRequest} from "./data.routes.js";
29
+ export {middlewareEndpointAuthenticator} from "./data.routes.js";
@@ -14,6 +14,7 @@ import {Logger} from "../gameObject.js";
14
14
  import rateLimit from "express-rate-limit";
15
15
  import ivm from "isolated-vm";
16
16
  import {emailDefaultConfig} from "../constants.js";
17
+ import {safeAssignObject} from "../core.js";
17
18
 
18
19
  export const userInitiator = async (req, res, next) => {
19
20
 
@@ -269,7 +270,7 @@ export async function calculateTotalUserStorageUsage(user) {
269
270
  export async function getEnv(user){
270
271
  const result = await searchData({ model: 'env' }, user);
271
272
  const envObject = result.data.reduce((acc, v) => {
272
- acc[v.name] = v.value;
273
+ safeAssignObject(acc, v.name, v.value);
273
274
  return acc;
274
275
  }, {});
275
276
  return envObject;
@@ -23,7 +23,7 @@ import {getHost} from "../constants.js";
23
23
  import {providers} from "./assistant/constants.js";
24
24
  import {ChatAnthropic} from "@langchain/anthropic";
25
25
  import {getAIProvider} from "./assistant/assistant.js";
26
- import {escapeRegex} from "../core.js";
26
+ import {escapeRegex, safeAssignObject} from "../core.js";
27
27
 
28
28
  let logger = null;
29
29
  export async function onInit(defaultEngine) {
@@ -1143,7 +1143,8 @@ export async function substituteVariables(template, contextData, user) {
1143
1143
  const newObj = {};
1144
1144
  for (const key in template) {
1145
1145
  if (Object.prototype.hasOwnProperty.call(template, key)) {
1146
- newObj[key] = await substituteVariables(template[key], contextData, user);
1146
+ const val = await substituteVariables(template[key], contextData, user);
1147
+ safeAssignObject(newObj, key, val);
1147
1148
  }
1148
1149
  }
1149
1150
  return newObj;