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.
- package/README.md +856 -797
- package/client/src/AssistantChat.jsx +48 -5
- package/client/src/AssistantChat.scss +0 -1
- package/client/src/ChartConfigModal.jsx +34 -9
- package/client/src/Dashboard.jsx +396 -349
- package/client/src/DashboardChart.jsx +91 -12
- package/client/src/DataTable.jsx +817 -807
- package/client/src/Field.jsx +1784 -1782
- package/client/src/PackGallery.jsx +391 -290
- package/client/src/PackGallery.scss +231 -210
- package/client/src/contexts/UIContext.jsx +5 -2
- package/client/src/translations.js +188 -0
- package/package.json +19 -8
- package/src/config.js +2 -2
- package/src/core.js +21 -3
- package/src/engine.js +2 -1
- package/src/events.js +4 -3
- package/src/gameObject.js +1 -1
- package/src/middlewares/middleware-mongodb.js +3 -1
- package/src/modules/assistant/assistant.js +131 -42
- package/src/modules/auth-google/index.js +51 -0
- package/src/modules/auth-microsoft/index.js +82 -0
- package/src/modules/auth-saml/index.js +90 -0
- package/src/modules/bucket.js +3 -2
- package/src/modules/data/data.backup.js +374 -0
- package/src/modules/data/data.history.js +11 -8
- package/src/modules/data/data.js +190 -4543
- package/src/modules/data/data.operations.js +2790 -0
- package/src/modules/data/data.relations.js +687 -0
- package/src/modules/data/data.routes.js +1785 -1646
- package/src/modules/data/data.scheduling.js +274 -0
- package/src/modules/data/data.validation.js +245 -0
- package/src/modules/data/index.js +29 -1
- package/src/modules/user.js +2 -1
- package/src/modules/workflow.js +3 -2
- package/src/providers.js +110 -1
- package/src/services/stripe.js +3 -2
- package/src/sso.js +194 -0
- package/test/data.integration.test.js +3 -0
- 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";
|
package/src/modules/user.js
CHANGED
|
@@ -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
|
|
273
|
+
safeAssignObject(acc, v.name, v.value);
|
|
273
274
|
return acc;
|
|
274
275
|
}, {});
|
|
275
276
|
return envObject;
|
package/src/modules/workflow.js
CHANGED
|
@@ -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
|
-
|
|
1146
|
+
const val = await substituteVariables(template[key], contextData, user);
|
|
1147
|
+
safeAssignObject(newObj, key, val);
|
|
1147
1148
|
}
|
|
1148
1149
|
}
|
|
1149
1150
|
return newObj;
|