data-primals-engine 1.7.2 → 1.7.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.
- package/README.md +160 -160
- package/client/package-lock.json +8430 -8430
- package/client/src/APIInfo.jsx +1 -1
- package/client/src/App.jsx +2 -1
- package/client/src/App.scss +1635 -1626
- package/client/src/AssistantChat.jsx +1 -0
- package/client/src/CalendarView.jsx +1 -0
- package/client/src/ContentView.jsx +3 -3
- package/client/src/Dashboard.jsx +5 -2
- package/client/src/DashboardChart.jsx +1 -0
- package/client/src/DashboardFlexViewItem.jsx +1 -0
- package/client/src/DashboardHtmlViewItem.jsx +1 -0
- package/client/src/DashboardView.jsx +2 -0
- package/client/src/DataEditor.jsx +0 -1
- package/client/src/DataImporter.jsx +489 -468
- package/client/src/DataLayout.jsx +6 -3
- package/client/src/DataTable.jsx +4 -3
- package/client/src/Dialog.jsx +92 -90
- package/client/src/Dialog.scss +122 -116
- package/client/src/DisplayFlexNodeRenderer.jsx +1 -0
- package/client/src/FlexBuilderControls.jsx +1 -0
- package/client/src/FlexBuilderPreview.jsx +1 -1
- package/client/src/FlexNode.jsx +1 -1
- package/client/src/HistoryDialog.jsx +3 -2
- package/client/src/KPIWidget.jsx +1 -1
- package/client/src/KanbanView.jsx +1 -0
- package/client/src/ModelCreator.jsx +4 -0
- package/client/src/ModelList.jsx +1 -0
- package/client/src/PackGallery.jsx +5 -4
- package/client/src/RTETrans.jsx +1 -1
- package/client/src/RelationField.jsx +2 -0
- package/client/src/RelationSelectorWidget.jsx +2 -0
- package/client/src/RelationValue.jsx +1 -0
- package/client/src/RestoreDialog.jsx +1 -0
- package/client/src/WorkflowEditor.jsx +3 -0
- package/client/src/contexts/CommandContext.jsx +2 -1
- package/client/src/contexts/ModelContext.jsx +3 -3
- package/client/src/hooks/data.js +1 -0
- package/client/src/hooks/useValidation.js +1 -0
- package/client/src/translations.js +24 -24
- package/doc/AI-assistance.md +87 -63
- package/doc/Concepts.md +122 -0
- package/doc/Custom-Endpoints.md +31 -0
- package/doc/Event-system.md +13 -14
- package/doc/Home.md +33 -0
- package/doc/Modules.md +83 -0
- package/doc/Packs-gallery.md +8 -23
- package/doc/Workflows.md +32 -0
- package/doc/automation-workflows.md +141 -102
- package/doc/dashboards-kpis-charts.md +47 -49
- package/doc/data-management.md +126 -120
- package/doc/data-models.md +68 -75
- package/doc/roles-permissions.md +144 -43
- package/doc/sharding-replication.md +158 -0
- package/doc/users.md +54 -30
- package/package.json +1 -1
- package/server.js +37 -37
- package/src/constants.js +7 -8
- package/src/data.js +521 -520
- package/src/email.js +157 -154
- package/src/engine.js +117 -7
- package/src/gameObject.js +6 -0
- package/src/i18n.js +0 -1
- package/src/modules/auth-google/index.js +53 -50
- package/src/modules/bucket.js +346 -339
- package/src/modules/data/data.backup.js +400 -376
- package/src/modules/data/data.cluster.js +410 -42
- package/src/modules/data/data.operations.js +3666 -3635
- package/src/modules/data/data.replication.js +106 -64
- package/src/modules/data/data.routes.js +87 -64
- package/src/modules/data/data.scheduling.js +2 -1
- package/src/modules/file.js +248 -247
- package/src/modules/user.js +11 -1
- package/src/sso.js +91 -25
- package/test/cluster.test.js +221 -0
- package/test/core.test.js +0 -2
- package/test/replication.test.js +163 -0
- package/doc/core-concepts.md +0 -33
package/src/data.js
CHANGED
|
@@ -1,521 +1,522 @@
|
|
|
1
|
-
import {getObjectHash} from "./core.js";
|
|
2
|
-
import process from "node:process";
|
|
3
|
-
import crypto from "node:crypto";
|
|
4
|
-
import {mainFieldsTypes} from "./constants.js";
|
|
5
|
-
|
|
6
|
-
const IV_LENGTH = 16;
|
|
7
|
-
export function encryptValue(text) {
|
|
8
|
-
if (!process.env.ENCRYPTION_KEY) throw new Error("ENCRYPTION_KEY is not set");
|
|
9
|
-
let iv = crypto.randomBytes(IV_LENGTH);
|
|
10
|
-
let cipher = crypto.createCipheriv('aes-256-cbc', Buffer.from(process.env.ENCRYPTION_KEY), iv);
|
|
11
|
-
let encrypted = cipher.update(text);
|
|
12
|
-
encrypted = Buffer.concat([encrypted, cipher.final()]);
|
|
13
|
-
return iv.toString('hex') + ':' + encrypted.toString('hex');
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export function decryptValue(text) {
|
|
17
|
-
if (!process.env.ENCRYPTION_KEY) throw new Error("ENCRYPTION_KEY is not set");
|
|
18
|
-
if (!text || typeof text !== 'string' || !text.includes(':')) return text; // ou throw error
|
|
19
|
-
let textParts = text.split(':');
|
|
20
|
-
let iv = Buffer.from(textParts.shift(), 'hex');
|
|
21
|
-
let encryptedText = Buffer.from(textParts.join(':'), 'hex');
|
|
22
|
-
let decipher = crypto.createDecipheriv('aes-256-cbc', Buffer.from(process.env.ENCRYPTION_KEY), iv);
|
|
23
|
-
let decrypted = decipher.update(encryptedText);
|
|
24
|
-
decrypted = Buffer.concat([decrypted, decipher.final()]);
|
|
25
|
-
return decrypted.toString();
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export const isLocalUser = (user) => {
|
|
29
|
-
return user && user._model === 'user' && typeof(user._user) === 'string' && user._user.trim() !== '';
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
export const isDemoUser = (user) => {
|
|
33
|
-
return user?.temporary;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export function getUserHash(user) {
|
|
37
|
-
if( isDemoUser(user) ){
|
|
38
|
-
return user.username;
|
|
39
|
-
}
|
|
40
|
-
if( user.hash )
|
|
41
|
-
return user.hash;
|
|
42
|
-
return user ? (
|
|
43
|
-
isLocalUser(user) ? getObjectHash({id: 'LOCAL_USER'+user._user+user.username}) : getObjectHash({id: user.hash})
|
|
44
|
-
) : 0;
|
|
45
|
-
}
|
|
46
|
-
export function getUserId(user) {
|
|
47
|
-
return user ? (
|
|
48
|
-
isLocalUser(user) ? ('LOCAL_USER'+user.username)?.hashCode() : user.username
|
|
49
|
-
) : 0;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export const getUserName = (user) => {
|
|
53
|
-
return isLocalUser(user) ? user._id + '_'+user._user : user.username;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Crée une fonction de génération de nombres pseudo-aléatoires basée sur une graine (seed).
|
|
58
|
-
* Utilise un simple générateur congruentiel linéaire (LCG).
|
|
59
|
-
* @param {number} seed - La graine initiale.
|
|
60
|
-
* @returns {function(): number} Une fonction qui retourne un nombre entre 0 et 1.
|
|
61
|
-
*/
|
|
62
|
-
function createSeededRandom(seed) {
|
|
63
|
-
let state = seed;
|
|
64
|
-
return function() {
|
|
65
|
-
// Algorithme LCG simple. Pas pour la cryptographie, mais suffisant pour du "hasard" déterministe.
|
|
66
|
-
state = (state * 9301 + 49297) % 233280;
|
|
67
|
-
return state / 233280.0;
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Anonymise une chaîne de caractères en remplaçant chaque caractère
|
|
73
|
-
* par un caractère aléatoire d'un jeu défini.
|
|
74
|
-
* Cette méthode est déterministe si une graine (seed) est fournie.
|
|
75
|
-
*
|
|
76
|
-
* @param {string} text Le texte à anonymiser.
|
|
77
|
-
* @param {boolean} [preserveSpaces=false] Si true, les espaces ne seront pas remplacés.
|
|
78
|
-
* @param {string|number|null} [seed=null] Une graine pour rendre l'anonymisation déterministe. Peut être le _hash de la donnée.
|
|
79
|
-
* @returns {string} Le texte anonymisé, ou une chaîne vide si l'entrée est invalide.
|
|
80
|
-
*/
|
|
81
|
-
export function anonymizeText(text, preserveSpaces = false, seed = null) {
|
|
82
|
-
if (typeof text !== 'string' || text.length === 0) {
|
|
83
|
-
return "";
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
text = encryptValue(text);
|
|
87
|
-
|
|
88
|
-
// Jeu de caractères pour le remplacement (vous pouvez l'adapter)
|
|
89
|
-
const charSet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789*#@!?";
|
|
90
|
-
let anonymizedResult = "";
|
|
91
|
-
|
|
92
|
-
let random;
|
|
93
|
-
if (seed !== null) {
|
|
94
|
-
// Convertir la graine (qui peut être une chaîne comme _hash) en un nombre simple.
|
|
95
|
-
let numericSeed = 0;
|
|
96
|
-
if (typeof seed === 'string') {
|
|
97
|
-
for (let i = 0; i < seed.length; i++) {
|
|
98
|
-
// Simple hash to number. Using a bitwise operation to keep it a 32-bit integer.
|
|
99
|
-
numericSeed = (((numericSeed << 5) - numericSeed) + seed.charCodeAt(i)) | 0;
|
|
100
|
-
}
|
|
101
|
-
} else if (typeof seed === 'number') {
|
|
102
|
-
numericSeed = seed;
|
|
103
|
-
} else {
|
|
104
|
-
// Fallback pour d'autres types, bien que non attendu
|
|
105
|
-
numericSeed = new Date().getTime();
|
|
106
|
-
}
|
|
107
|
-
random = createSeededRandom(numericSeed);
|
|
108
|
-
} else {
|
|
109
|
-
// Si aucune graine n'est fournie, utiliser le générateur aléatoire standard.
|
|
110
|
-
random = Math.random;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
for (let i = 0; i < text.length; i++) {
|
|
114
|
-
const originalChar = text[i];
|
|
115
|
-
|
|
116
|
-
// Conserver les espaces si demandé
|
|
117
|
-
if (preserveSpaces && (originalChar === ' ' || originalChar === '\t' || originalChar === '\n' || originalChar === '\r')) {
|
|
118
|
-
anonymizedResult += originalChar;
|
|
119
|
-
} else {
|
|
120
|
-
// Remplacer par un caractère aléatoire du jeu en utilisant le générateur choisi
|
|
121
|
-
const randomIndex = Math.floor(random() * charSet.length);
|
|
122
|
-
anonymizedResult += charSet[randomIndex];
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
return anonymizedResult;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
/**
|
|
131
|
-
* Get the value from an object based on a dot-separated path.
|
|
132
|
-
* @param {object} obj - The object to traverse.
|
|
133
|
-
* @param {string} path - The dot-separated path (e.g., "user.profile.name").
|
|
134
|
-
* @returns {*} The value at the given path, or undefined if not found.
|
|
135
|
-
*/
|
|
136
|
-
export const getFieldPathValue = (obj, path) => {
|
|
137
|
-
if (!obj || !path) return undefined;
|
|
138
|
-
const properties = path.split('.');
|
|
139
|
-
return properties.reduce((prev, curr) => (prev && prev[curr] !== undefined) ? prev[curr] : undefined, obj);
|
|
140
|
-
};
|
|
141
|
-
|
|
142
|
-
/**
|
|
143
|
-
* Apply a filter condition to an array of data objects.
|
|
144
|
-
* Filter format is a simple object mapping field paths to values or filter operators.
|
|
145
|
-
* Supported operators: $eq, $ne, $in (array), $exists (boolean).
|
|
146
|
-
* @param {array} data - The array of data objects.
|
|
147
|
-
* @param {object|null} filter - The filter object.
|
|
148
|
-
* @returns {array} The filtered array.
|
|
149
|
-
*/
|
|
150
|
-
export const applyFilter = (data, filter) => {
|
|
151
|
-
if (!filter || Object.keys(filter).length === 0 || !Array.isArray(data)) return data;
|
|
152
|
-
return data.filter(item => {
|
|
153
|
-
for (const key in filter) {
|
|
154
|
-
// Ensure the key exists in the filter and is not from prototype chain
|
|
155
|
-
if (Object.prototype.hasOwnProperty.call(filter, key)) {
|
|
156
|
-
const filterValue = filter[key];
|
|
157
|
-
const itemValue = getFieldPathValue(item, key);
|
|
158
|
-
|
|
159
|
-
if (typeof filterValue === 'object' && filterValue !== null) {
|
|
160
|
-
if (filterValue.$in && Array.isArray(filterValue.$in)) {
|
|
161
|
-
if (!filterValue.$in.includes(itemValue)) return false;
|
|
162
|
-
} else if (filterValue.$eq !== undefined) {
|
|
163
|
-
if (itemValue !== filterValue.$eq) return false;
|
|
164
|
-
} else if (filterValue.$ne !== undefined) {
|
|
165
|
-
if (itemValue === filterValue.$ne) return false;
|
|
166
|
-
} else if (filterValue.$exists !== undefined) {
|
|
167
|
-
const hasValue = itemValue !== undefined && itemValue !== null;
|
|
168
|
-
if (filterValue.$exists && !hasValue) return false;
|
|
169
|
-
if (!filterValue.$exists && hasValue) return false;
|
|
170
|
-
}
|
|
171
|
-
// Add support for other operators if needed
|
|
172
|
-
// else if (filterValue.$gt !== undefined) { ... }
|
|
173
|
-
} else {
|
|
174
|
-
// Default equality check for non-object filter values
|
|
175
|
-
if (itemValue !== filterValue) return false;
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
return true;
|
|
180
|
-
});
|
|
181
|
-
};
|
|
182
|
-
|
|
183
|
-
export function getDefaultForType(field) {
|
|
184
|
-
if(field.default)
|
|
185
|
-
return typeof(field.default) === 'function' ? field.default() : field.default;
|
|
186
|
-
switch (field.type) {
|
|
187
|
-
case 'string':
|
|
188
|
-
case 'string_t':
|
|
189
|
-
case 'richtext':
|
|
190
|
-
case 'password':
|
|
191
|
-
case 'email':
|
|
192
|
-
case 'phone':
|
|
193
|
-
case 'url':
|
|
194
|
-
return '';
|
|
195
|
-
case 'model':
|
|
196
|
-
return '';
|
|
197
|
-
case 'modelField':
|
|
198
|
-
return { model: '', field: '' };
|
|
199
|
-
case 'color':
|
|
200
|
-
return null;
|
|
201
|
-
case 'number':
|
|
202
|
-
return 0;
|
|
203
|
-
case 'date':
|
|
204
|
-
case 'datetime':
|
|
205
|
-
return null;
|
|
206
|
-
case 'boolean':
|
|
207
|
-
return false;
|
|
208
|
-
case 'file':
|
|
209
|
-
return null;
|
|
210
|
-
case 'array':
|
|
211
|
-
return [];
|
|
212
|
-
case 'enum':
|
|
213
|
-
return field.required && field.items && field.items.length > 0 ? field.items[0] : null;
|
|
214
|
-
case 'object':
|
|
215
|
-
return {};
|
|
216
|
-
case 'relation':
|
|
217
|
-
return field.multiple ? [] : null;
|
|
218
|
-
default:
|
|
219
|
-
|
|
220
|
-
return undefined;
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
/**
|
|
226
|
-
* Maps ConditionBuilder operators to MongoDB aggregation expression operators
|
|
227
|
-
* or indicates special handling.
|
|
228
|
-
*/
|
|
229
|
-
const operatorToExprOperatorMap = {
|
|
230
|
-
'$eq': '$eq',
|
|
231
|
-
'$ne': '$ne',
|
|
232
|
-
'$gt': '$gt',
|
|
233
|
-
'$gte': '$gte',
|
|
234
|
-
'$lt': '$lt',
|
|
235
|
-
'$lte': '$lte',
|
|
236
|
-
'$in': '$in',
|
|
237
|
-
'$nin': '$nin', // Special handling with $not/$in
|
|
238
|
-
'$regex': '$regexMatch', // Use aggregation regex operator
|
|
239
|
-
'$exists': '$exists' // Special handling with $type or similar
|
|
240
|
-
};
|
|
241
|
-
|
|
242
|
-
// Helper to build the nested $find structure
|
|
243
|
-
function buildNestedFindStructure(pathSegments, finalPayload) {
|
|
244
|
-
// Base case: If no segments left, return the final condition payload
|
|
245
|
-
if (!pathSegments || pathSegments.length === 0) {
|
|
246
|
-
return finalPayload;
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
const currentSegment = pathSegments[0];
|
|
250
|
-
const remainingSegments = pathSegments.slice(1);
|
|
251
|
-
|
|
252
|
-
// Recursively build the structure
|
|
253
|
-
return {
|
|
254
|
-
[currentSegment]: {
|
|
255
|
-
'$find': buildNestedFindStructure(remainingSegments, finalPayload)
|
|
256
|
-
}
|
|
257
|
-
};
|
|
258
|
-
}
|
|
259
|
-
// ... (imports and other functions) ...
|
|
260
|
-
|
|
261
|
-
/**
|
|
262
|
-
* Builds the core condition payload (operators and values, or logical groups)
|
|
263
|
-
* from a ConditionBuilder condition object or sub-object.
|
|
264
|
-
* It handles the transformation of simple conditions and logical groups into
|
|
265
|
-
* the format expected by the API filter, using $expr for comparisons and $find for nesting.
|
|
266
|
-
*
|
|
267
|
-
* @param {object} conditionNode - A node from the ConditionBuilder structure.
|
|
268
|
-
* @returns {object | null} The condition payload object or null if invalid.
|
|
269
|
-
*/
|
|
270
|
-
function buildApiConditionPayloadRecursive(conditionNode) {
|
|
271
|
-
if (!conditionNode || typeof conditionNode !== 'object') {
|
|
272
|
-
console.warn("Invalid node in buildApiConditionPayloadRecursive:", conditionNode);
|
|
273
|
-
return null;
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
// --- Handle Logical Operators ($and, $or) ---
|
|
277
|
-
if (conditionNode.$and && Array.isArray(conditionNode.$and)) {
|
|
278
|
-
const subPayloads = conditionNode.$and.map(buildApiConditionPayloadRecursive).filter(p => p !== null);
|
|
279
|
-
if (subPayloads.length === 0) return null;
|
|
280
|
-
if (subPayloads.length === 1) return subPayloads[0];
|
|
281
|
-
return { '$and': subPayloads };
|
|
282
|
-
}
|
|
283
|
-
if (conditionNode.$or && Array.isArray(conditionNode.$or)) {
|
|
284
|
-
const subPayloads = conditionNode.$or.map(buildApiConditionPayloadRecursive).filter(p => p !== null);
|
|
285
|
-
if (subPayloads.length === 0) return null;
|
|
286
|
-
if (subPayloads.length === 1) return subPayloads[0];
|
|
287
|
-
return { '$or': subPayloads };
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
// --- Handle Simple Condition { model?, path, op, value } ---
|
|
291
|
-
if (conditionNode.path && Array.isArray(conditionNode.path) && conditionNode.op) {
|
|
292
|
-
const path = conditionNode.path;
|
|
293
|
-
|
|
294
|
-
if (path.length === 0) {
|
|
295
|
-
console.warn("Simple condition node has empty path:", conditionNode);
|
|
296
|
-
return null;
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
// --- Build the core $expr payload ---
|
|
300
|
-
const finalFieldName = path[path.length - 1];
|
|
301
|
-
const segmentsForNesting = path.slice(0, -1); // Path segments before the last one
|
|
302
|
-
|
|
303
|
-
// ***** CORRECTED LOGIC *****
|
|
304
|
-
// Determine the field path for the $expr based on nesting level
|
|
305
|
-
const fieldPathForExpr = segmentsForNesting.length === 0
|
|
306
|
-
? `$${finalFieldName}` // Top-level field: $fieldName
|
|
307
|
-
: `$$this.${finalFieldName}`; // Nested field (inside $find): $$this.fieldName
|
|
308
|
-
// ***** END CORRECTION *****
|
|
309
|
-
|
|
310
|
-
const operator = conditionNode.op;
|
|
311
|
-
const value = conditionNode.value;
|
|
312
|
-
const exprOperator = operatorToExprOperatorMap[operator];
|
|
313
|
-
|
|
314
|
-
let exprPayload; // This will hold the { $operator: [ field, value ] } part
|
|
315
|
-
|
|
316
|
-
if (!exprOperator) {
|
|
317
|
-
console.warn(`Unsupported operator for $expr conversion: ${operator}`);
|
|
318
|
-
return null;
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
// --- Build the specific $expr based on the operator ---
|
|
322
|
-
switch (exprOperator) {
|
|
323
|
-
case '$
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
//
|
|
327
|
-
//
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
else if (value === '
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
*
|
|
389
|
-
*
|
|
390
|
-
*
|
|
391
|
-
*
|
|
392
|
-
* @
|
|
393
|
-
*
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
const
|
|
420
|
-
const
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
*
|
|
428
|
-
*
|
|
429
|
-
*
|
|
430
|
-
* @param {object}
|
|
431
|
-
* @param {
|
|
432
|
-
* @param {
|
|
433
|
-
* @
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
const
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
//
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
const
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
.
|
|
486
|
-
.
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
//
|
|
520
|
-
|
|
1
|
+
import {getObjectHash} from "./core.js";
|
|
2
|
+
import process from "node:process";
|
|
3
|
+
import crypto from "node:crypto";
|
|
4
|
+
import {mainFieldsTypes} from "./constants.js";
|
|
5
|
+
|
|
6
|
+
const IV_LENGTH = 16;
|
|
7
|
+
export function encryptValue(text) {
|
|
8
|
+
if (!process.env.ENCRYPTION_KEY) throw new Error("ENCRYPTION_KEY is not set");
|
|
9
|
+
let iv = crypto.randomBytes(IV_LENGTH);
|
|
10
|
+
let cipher = crypto.createCipheriv('aes-256-cbc', Buffer.from(process.env.ENCRYPTION_KEY), iv);
|
|
11
|
+
let encrypted = cipher.update(text);
|
|
12
|
+
encrypted = Buffer.concat([encrypted, cipher.final()]);
|
|
13
|
+
return iv.toString('hex') + ':' + encrypted.toString('hex');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function decryptValue(text) {
|
|
17
|
+
if (!process.env.ENCRYPTION_KEY) throw new Error("ENCRYPTION_KEY is not set");
|
|
18
|
+
if (!text || typeof text !== 'string' || !text.includes(':')) return text; // ou throw error
|
|
19
|
+
let textParts = text.split(':');
|
|
20
|
+
let iv = Buffer.from(textParts.shift(), 'hex');
|
|
21
|
+
let encryptedText = Buffer.from(textParts.join(':'), 'hex');
|
|
22
|
+
let decipher = crypto.createDecipheriv('aes-256-cbc', Buffer.from(process.env.ENCRYPTION_KEY), iv);
|
|
23
|
+
let decrypted = decipher.update(encryptedText);
|
|
24
|
+
decrypted = Buffer.concat([decrypted, decipher.final()]);
|
|
25
|
+
return decrypted.toString();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const isLocalUser = (user) => {
|
|
29
|
+
return user && user._model === 'user' && typeof(user._user) === 'string' && user._user.trim() !== '';
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export const isDemoUser = (user) => {
|
|
33
|
+
return user?.temporary;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function getUserHash(user) {
|
|
37
|
+
if( isDemoUser(user) ){
|
|
38
|
+
return user.username;
|
|
39
|
+
}
|
|
40
|
+
if( user.hash )
|
|
41
|
+
return user.hash;
|
|
42
|
+
return user ? (
|
|
43
|
+
isLocalUser(user) ? getObjectHash({id: 'LOCAL_USER'+user._user+user.username}) : getObjectHash({id: user.hash})
|
|
44
|
+
) : 0;
|
|
45
|
+
}
|
|
46
|
+
export function getUserId(user) {
|
|
47
|
+
return user ? (
|
|
48
|
+
isLocalUser(user) ? ('LOCAL_USER'+user.username)?.hashCode() : user.username
|
|
49
|
+
) : 0;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export const getUserName = (user) => {
|
|
53
|
+
return isLocalUser(user) ? user._id + '_'+user._user : user.username;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Crée une fonction de génération de nombres pseudo-aléatoires basée sur une graine (seed).
|
|
58
|
+
* Utilise un simple générateur congruentiel linéaire (LCG).
|
|
59
|
+
* @param {number} seed - La graine initiale.
|
|
60
|
+
* @returns {function(): number} Une fonction qui retourne un nombre entre 0 et 1.
|
|
61
|
+
*/
|
|
62
|
+
function createSeededRandom(seed) {
|
|
63
|
+
let state = seed;
|
|
64
|
+
return function() {
|
|
65
|
+
// Algorithme LCG simple. Pas pour la cryptographie, mais suffisant pour du "hasard" déterministe.
|
|
66
|
+
state = (state * 9301 + 49297) % 233280;
|
|
67
|
+
return state / 233280.0;
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Anonymise une chaîne de caractères en remplaçant chaque caractère
|
|
73
|
+
* par un caractère aléatoire d'un jeu défini.
|
|
74
|
+
* Cette méthode est déterministe si une graine (seed) est fournie.
|
|
75
|
+
*
|
|
76
|
+
* @param {string} text Le texte à anonymiser.
|
|
77
|
+
* @param {boolean} [preserveSpaces=false] Si true, les espaces ne seront pas remplacés.
|
|
78
|
+
* @param {string|number|null} [seed=null] Une graine pour rendre l'anonymisation déterministe. Peut être le _hash de la donnée.
|
|
79
|
+
* @returns {string} Le texte anonymisé, ou une chaîne vide si l'entrée est invalide.
|
|
80
|
+
*/
|
|
81
|
+
export function anonymizeText(text, preserveSpaces = false, seed = null) {
|
|
82
|
+
if (typeof text !== 'string' || text.length === 0) {
|
|
83
|
+
return "";
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
text = encryptValue(text);
|
|
87
|
+
|
|
88
|
+
// Jeu de caractères pour le remplacement (vous pouvez l'adapter)
|
|
89
|
+
const charSet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789*#@!?";
|
|
90
|
+
let anonymizedResult = "";
|
|
91
|
+
|
|
92
|
+
let random;
|
|
93
|
+
if (seed !== null) {
|
|
94
|
+
// Convertir la graine (qui peut être une chaîne comme _hash) en un nombre simple.
|
|
95
|
+
let numericSeed = 0;
|
|
96
|
+
if (typeof seed === 'string') {
|
|
97
|
+
for (let i = 0; i < seed.length; i++) {
|
|
98
|
+
// Simple hash to number. Using a bitwise operation to keep it a 32-bit integer.
|
|
99
|
+
numericSeed = (((numericSeed << 5) - numericSeed) + seed.charCodeAt(i)) | 0;
|
|
100
|
+
}
|
|
101
|
+
} else if (typeof seed === 'number') {
|
|
102
|
+
numericSeed = seed;
|
|
103
|
+
} else {
|
|
104
|
+
// Fallback pour d'autres types, bien que non attendu
|
|
105
|
+
numericSeed = new Date().getTime();
|
|
106
|
+
}
|
|
107
|
+
random = createSeededRandom(numericSeed);
|
|
108
|
+
} else {
|
|
109
|
+
// Si aucune graine n'est fournie, utiliser le générateur aléatoire standard.
|
|
110
|
+
random = Math.random;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
for (let i = 0; i < text.length; i++) {
|
|
114
|
+
const originalChar = text[i];
|
|
115
|
+
|
|
116
|
+
// Conserver les espaces si demandé
|
|
117
|
+
if (preserveSpaces && (originalChar === ' ' || originalChar === '\t' || originalChar === '\n' || originalChar === '\r')) {
|
|
118
|
+
anonymizedResult += originalChar;
|
|
119
|
+
} else {
|
|
120
|
+
// Remplacer par un caractère aléatoire du jeu en utilisant le générateur choisi
|
|
121
|
+
const randomIndex = Math.floor(random() * charSet.length);
|
|
122
|
+
anonymizedResult += charSet[randomIndex];
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return anonymizedResult;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Get the value from an object based on a dot-separated path.
|
|
132
|
+
* @param {object} obj - The object to traverse.
|
|
133
|
+
* @param {string} path - The dot-separated path (e.g., "user.profile.name").
|
|
134
|
+
* @returns {*} The value at the given path, or undefined if not found.
|
|
135
|
+
*/
|
|
136
|
+
export const getFieldPathValue = (obj, path) => {
|
|
137
|
+
if (!obj || !path) return undefined;
|
|
138
|
+
const properties = path.split('.');
|
|
139
|
+
return properties.reduce((prev, curr) => (prev && prev[curr] !== undefined) ? prev[curr] : undefined, obj);
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Apply a filter condition to an array of data objects.
|
|
144
|
+
* Filter format is a simple object mapping field paths to values or filter operators.
|
|
145
|
+
* Supported operators: $eq, $ne, $in (array), $exists (boolean).
|
|
146
|
+
* @param {array} data - The array of data objects.
|
|
147
|
+
* @param {object|null} filter - The filter object.
|
|
148
|
+
* @returns {array} The filtered array.
|
|
149
|
+
*/
|
|
150
|
+
export const applyFilter = (data, filter) => {
|
|
151
|
+
if (!filter || Object.keys(filter).length === 0 || !Array.isArray(data)) return data;
|
|
152
|
+
return data.filter(item => {
|
|
153
|
+
for (const key in filter) {
|
|
154
|
+
// Ensure the key exists in the filter and is not from prototype chain
|
|
155
|
+
if (Object.prototype.hasOwnProperty.call(filter, key)) {
|
|
156
|
+
const filterValue = filter[key];
|
|
157
|
+
const itemValue = getFieldPathValue(item, key);
|
|
158
|
+
|
|
159
|
+
if (typeof filterValue === 'object' && filterValue !== null) {
|
|
160
|
+
if (filterValue.$in && Array.isArray(filterValue.$in)) {
|
|
161
|
+
if (!filterValue.$in.includes(itemValue)) return false;
|
|
162
|
+
} else if (filterValue.$eq !== undefined) {
|
|
163
|
+
if (itemValue !== filterValue.$eq) return false;
|
|
164
|
+
} else if (filterValue.$ne !== undefined) {
|
|
165
|
+
if (itemValue === filterValue.$ne) return false;
|
|
166
|
+
} else if (filterValue.$exists !== undefined) {
|
|
167
|
+
const hasValue = itemValue !== undefined && itemValue !== null;
|
|
168
|
+
if (filterValue.$exists && !hasValue) return false;
|
|
169
|
+
if (!filterValue.$exists && hasValue) return false;
|
|
170
|
+
}
|
|
171
|
+
// Add support for other operators if needed
|
|
172
|
+
// else if (filterValue.$gt !== undefined) { ... }
|
|
173
|
+
} else {
|
|
174
|
+
// Default equality check for non-object filter values
|
|
175
|
+
if (itemValue !== filterValue) return false;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return true;
|
|
180
|
+
});
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
export function getDefaultForType(field) {
|
|
184
|
+
if(field.default)
|
|
185
|
+
return typeof(field.default) === 'function' ? field.default() : field.default;
|
|
186
|
+
switch (field.type) {
|
|
187
|
+
case 'string':
|
|
188
|
+
case 'string_t':
|
|
189
|
+
case 'richtext':
|
|
190
|
+
case 'password':
|
|
191
|
+
case 'email':
|
|
192
|
+
case 'phone':
|
|
193
|
+
case 'url':
|
|
194
|
+
return '';
|
|
195
|
+
case 'model':
|
|
196
|
+
return '';
|
|
197
|
+
case 'modelField':
|
|
198
|
+
return { model: '', field: '' };
|
|
199
|
+
case 'color':
|
|
200
|
+
return null;
|
|
201
|
+
case 'number':
|
|
202
|
+
return 0;
|
|
203
|
+
case 'date':
|
|
204
|
+
case 'datetime':
|
|
205
|
+
return null;
|
|
206
|
+
case 'boolean':
|
|
207
|
+
return false;
|
|
208
|
+
case 'file':
|
|
209
|
+
return null;
|
|
210
|
+
case 'array':
|
|
211
|
+
return [];
|
|
212
|
+
case 'enum':
|
|
213
|
+
return field.required && field.items && field.items.length > 0 ? field.items[0] : null;
|
|
214
|
+
case 'object':
|
|
215
|
+
return {};
|
|
216
|
+
case 'relation':
|
|
217
|
+
return field.multiple ? [] : null;
|
|
218
|
+
default:
|
|
219
|
+
|
|
220
|
+
return undefined;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Maps ConditionBuilder operators to MongoDB aggregation expression operators
|
|
227
|
+
* or indicates special handling.
|
|
228
|
+
*/
|
|
229
|
+
const operatorToExprOperatorMap = {
|
|
230
|
+
'$eq': '$eq',
|
|
231
|
+
'$ne': '$ne',
|
|
232
|
+
'$gt': '$gt',
|
|
233
|
+
'$gte': '$gte',
|
|
234
|
+
'$lt': '$lt',
|
|
235
|
+
'$lte': '$lte',
|
|
236
|
+
'$in': '$in',
|
|
237
|
+
'$nin': '$nin', // Special handling with $not/$in
|
|
238
|
+
'$regex': '$regexMatch', // Use aggregation regex operator
|
|
239
|
+
'$exists': '$exists' // Special handling with $type or similar
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
// Helper to build the nested $find structure
|
|
243
|
+
function buildNestedFindStructure(pathSegments, finalPayload) {
|
|
244
|
+
// Base case: If no segments left, return the final condition payload
|
|
245
|
+
if (!pathSegments || pathSegments.length === 0) {
|
|
246
|
+
return finalPayload;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const currentSegment = pathSegments[0];
|
|
250
|
+
const remainingSegments = pathSegments.slice(1);
|
|
251
|
+
|
|
252
|
+
// Recursively build the structure
|
|
253
|
+
return {
|
|
254
|
+
[currentSegment]: {
|
|
255
|
+
'$find': buildNestedFindStructure(remainingSegments, finalPayload)
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
// ... (imports and other functions) ...
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Builds the core condition payload (operators and values, or logical groups)
|
|
263
|
+
* from a ConditionBuilder condition object or sub-object.
|
|
264
|
+
* It handles the transformation of simple conditions and logical groups into
|
|
265
|
+
* the format expected by the API filter, using $expr for comparisons and $find for nesting.
|
|
266
|
+
*
|
|
267
|
+
* @param {object} conditionNode - A node from the ConditionBuilder structure.
|
|
268
|
+
* @returns {object | null} The condition payload object or null if invalid.
|
|
269
|
+
*/
|
|
270
|
+
function buildApiConditionPayloadRecursive(conditionNode) {
|
|
271
|
+
if (!conditionNode || typeof conditionNode !== 'object') {
|
|
272
|
+
console.warn("Invalid node in buildApiConditionPayloadRecursive:", conditionNode);
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// --- Handle Logical Operators ($and, $or) ---
|
|
277
|
+
if (conditionNode.$and && Array.isArray(conditionNode.$and)) {
|
|
278
|
+
const subPayloads = conditionNode.$and.map(buildApiConditionPayloadRecursive).filter(p => p !== null);
|
|
279
|
+
if (subPayloads.length === 0) return null;
|
|
280
|
+
if (subPayloads.length === 1) return subPayloads[0];
|
|
281
|
+
return { '$and': subPayloads };
|
|
282
|
+
}
|
|
283
|
+
if (conditionNode.$or && Array.isArray(conditionNode.$or)) {
|
|
284
|
+
const subPayloads = conditionNode.$or.map(buildApiConditionPayloadRecursive).filter(p => p !== null);
|
|
285
|
+
if (subPayloads.length === 0) return null;
|
|
286
|
+
if (subPayloads.length === 1) return subPayloads[0];
|
|
287
|
+
return { '$or': subPayloads };
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// --- Handle Simple Condition { model?, path, op, value } ---
|
|
291
|
+
if (conditionNode.path && Array.isArray(conditionNode.path) && conditionNode.op) {
|
|
292
|
+
const path = conditionNode.path;
|
|
293
|
+
|
|
294
|
+
if (path.length === 0) {
|
|
295
|
+
console.warn("Simple condition node has empty path:", conditionNode);
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// --- Build the core $expr payload ---
|
|
300
|
+
const finalFieldName = path[path.length - 1];
|
|
301
|
+
const segmentsForNesting = path.slice(0, -1); // Path segments before the last one
|
|
302
|
+
|
|
303
|
+
// ***** CORRECTED LOGIC *****
|
|
304
|
+
// Determine the field path for the $expr based on nesting level
|
|
305
|
+
const fieldPathForExpr = segmentsForNesting.length === 0
|
|
306
|
+
? `$${finalFieldName}` // Top-level field: $fieldName
|
|
307
|
+
: `$$this.${finalFieldName}`; // Nested field (inside $find): $$this.fieldName
|
|
308
|
+
// ***** END CORRECTION *****
|
|
309
|
+
|
|
310
|
+
const operator = conditionNode.op;
|
|
311
|
+
const value = conditionNode.value;
|
|
312
|
+
const exprOperator = operatorToExprOperatorMap[operator];
|
|
313
|
+
|
|
314
|
+
let exprPayload; // This will hold the { $operator: [ field, value ] } part
|
|
315
|
+
|
|
316
|
+
if (!exprOperator) {
|
|
317
|
+
console.warn(`Unsupported operator for $expr conversion: ${operator}`);
|
|
318
|
+
return null;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// --- Build the specific $expr based on the operator ---
|
|
322
|
+
switch (exprOperator) {
|
|
323
|
+
case '$regex': // Ajout pour gérer l'alias
|
|
324
|
+
case '$nin':
|
|
325
|
+
const ninValueArray = Array.isArray(value) ? value : String(value).split(',').map(s => s.trim()).filter(Boolean);
|
|
326
|
+
// Note: $nin at the top level is often better handled *outside* $expr
|
|
327
|
+
// but for consistency with $find, we keep it inside $expr here.
|
|
328
|
+
// If segmentsForNesting.length === 0, a standard query { field: { $nin: [...] } } might be more efficient.
|
|
329
|
+
exprPayload = { '$not': { '$in': [fieldPathForExpr, ninValueArray] } };
|
|
330
|
+
break;
|
|
331
|
+
case '$in':
|
|
332
|
+
const inValueArray = Array.isArray(value) ? value : String(value).split(',').map(s => s.trim()).filter(Boolean);
|
|
333
|
+
// Similar note as $nin regarding top-level efficiency.
|
|
334
|
+
exprPayload = { '$in': [fieldPathForExpr, inValueArray] };
|
|
335
|
+
break;
|
|
336
|
+
case '$regexMatch':
|
|
337
|
+
exprPayload = {
|
|
338
|
+
'$regexMatch': {
|
|
339
|
+
input: fieldPathForExpr,
|
|
340
|
+
regex: String(value),
|
|
341
|
+
options: 'i'
|
|
342
|
+
}
|
|
343
|
+
};
|
|
344
|
+
break;
|
|
345
|
+
case '$exists':
|
|
346
|
+
const shouldExist = typeof value === 'boolean' ? value : String(value).toLowerCase() === 'true';
|
|
347
|
+
// Using $ne/$eq with $type is a common way to check existence in $expr
|
|
348
|
+
exprPayload = {
|
|
349
|
+
[shouldExist ? '$ne' : '$eq']: [ { $type: fieldPathForExpr }, "undefined" ]
|
|
350
|
+
};
|
|
351
|
+
// Note: For top-level, { field: { $exists: true/false } } is standard.
|
|
352
|
+
break;
|
|
353
|
+
default:
|
|
354
|
+
// Standard binary operators ($eq, $ne, $gt, $gte, $lt, $lte)
|
|
355
|
+
let processedValue = value;
|
|
356
|
+
// Basic type guessing for common string values
|
|
357
|
+
if (operator === '$eq' || operator === '$ne') {
|
|
358
|
+
if (value === 'true') processedValue = true;
|
|
359
|
+
else if (value === 'false') processedValue = false;
|
|
360
|
+
else if (value === 'null') processedValue = null;
|
|
361
|
+
// Avoid automatic number conversion for eq/ne unless explicitly needed
|
|
362
|
+
} else if (['$gt', '$gte', '$lt', '$lte'].includes(operator)) {
|
|
363
|
+
// Attempt number conversion for comparison operators
|
|
364
|
+
const num = Number(value);
|
|
365
|
+
if (!isNaN(num) && String(value).trim() !== '') {
|
|
366
|
+
processedValue = num;
|
|
367
|
+
}
|
|
368
|
+
// TODO: Consider adding date conversion if field type is known
|
|
369
|
+
}
|
|
370
|
+
exprPayload = { [exprOperator]: [fieldPathForExpr, processedValue] };
|
|
371
|
+
break;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// The final payload for the innermost level is the $expr object
|
|
375
|
+
const corePayload = exprPayload;
|
|
376
|
+
|
|
377
|
+
// --- Structure the filter using nested $find based on the path segments before the last one ---
|
|
378
|
+
return buildNestedFindStructure(segmentsForNesting, corePayload);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Log warning for unknown structures
|
|
382
|
+
console.warn("Unknown structure encountered in buildApiConditionPayloadRecursive:", conditionNode);
|
|
383
|
+
return null;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Transforms a ConditionBuilder condition object into the specific filter format
|
|
389
|
+
* for the /api/data/search endpoint.
|
|
390
|
+
* The final format is { filter: { ... } }.
|
|
391
|
+
*
|
|
392
|
+
* @param {object | null | undefined} condition - The condition object from ConditionBuilder.
|
|
393
|
+
* @returns {object} The filter object in the format { filter: { ... } }.
|
|
394
|
+
* Returns { filter: {} } if the condition is invalid/empty.
|
|
395
|
+
*/
|
|
396
|
+
export function conditionToApiSearchFilter(condition) {
|
|
397
|
+
// Handle null or invalid input condition
|
|
398
|
+
if (!condition || typeof condition !== 'object') {
|
|
399
|
+
return { filter: {} }; // Return empty filter for invalid input
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// Build the main filter content using the recursive payload builder
|
|
403
|
+
const filterContent = buildApiConditionPayloadRecursive(condition);
|
|
404
|
+
|
|
405
|
+
// Return the final structure, ensuring filterContent is not null
|
|
406
|
+
return { filter: filterContent || {} };
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
export const getFieldValueHash = (model, data) => {
|
|
411
|
+
// Prioritize composite unique constraints if they exist
|
|
412
|
+
const uniqueConstraints = model.constraints?.filter(c => c.type === 'unique') || [];
|
|
413
|
+
|
|
414
|
+
if (uniqueConstraints.length > 0) {
|
|
415
|
+
// Combine keys from all unique constraints to create a truly unique hash.
|
|
416
|
+
const constraintKeys = [...new Set(uniqueConstraints.flatMap(c => c.keys))];
|
|
417
|
+
return getObjectHash(data, constraintKeys, 'constraint');
|
|
418
|
+
}
|
|
419
|
+
const uniqueFields = model.fields.filter(f => f.unique).map(m => m.name);
|
|
420
|
+
const fs = model.fields.map(m => m.name);
|
|
421
|
+
const fields = ['_model', '_user', ...(uniqueFields.length ? uniqueFields : fs)];
|
|
422
|
+
return getObjectHash(data, fields);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Obtient une représentation textuelle lisible d'un enregistrement de données.
|
|
428
|
+
* Gère les relations de manière récursive pour afficher des informations pertinentes.
|
|
429
|
+
*
|
|
430
|
+
* @param {object} model - La définition du modèle pour la `data` actuelle.
|
|
431
|
+
* @param {object} data - L'enregistrement de données à convertir en chaîne de caractères.
|
|
432
|
+
* @param {function} t - La fonction de traduction i18next.
|
|
433
|
+
* @param {array} allModels - Un tableau contenant les définitions de TOUS les modèles du système.
|
|
434
|
+
* @returns {string} - La chaîne de caractères représentant la donnée.
|
|
435
|
+
*/
|
|
436
|
+
export const getDataAsString = (model, data, tr, allModels, extended=false) => {
|
|
437
|
+
const { t, i18n} = tr;
|
|
438
|
+
const lang = (i18n.resolvedLanguage || i18n.language).split(/[-_]/)?.[0];
|
|
439
|
+
// Cas de base : si le modèle ou les données sont manquants, on ne peut rien faire.
|
|
440
|
+
if (!model || !data) {
|
|
441
|
+
return '';
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// 1. Déterminer les champs à afficher
|
|
445
|
+
// On privilégie les champs marqués comme "asMain"
|
|
446
|
+
let parts;
|
|
447
|
+
if( extended )
|
|
448
|
+
parts = model.fields.map(m => m.name);
|
|
449
|
+
else
|
|
450
|
+
parts = model.fields.filter(f => f.asMain).map(m => m.name);
|
|
451
|
+
|
|
452
|
+
// 2. Si aucun champ "asMain", on cherche des champs standards (nom, titre, etc.)
|
|
453
|
+
if (!parts.length) {
|
|
454
|
+
for (const main of mainFieldsTypes) {
|
|
455
|
+
const f = model.fields?.find((f) => f.type === main)?.name;
|
|
456
|
+
if (f) {
|
|
457
|
+
parts.push(f);
|
|
458
|
+
break; // On ne prend que le premier trouvé
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// 3. Si toujours rien, on se rabat sur l'_id
|
|
464
|
+
if (!parts.length) {
|
|
465
|
+
parts.push('_id');
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// 4. Construire la chaîne de caractères finale
|
|
469
|
+
const resultString = parts.map(fieldName => {
|
|
470
|
+
const fieldDef = model.fields.find(f => f.name === fieldName);
|
|
471
|
+
const value = data[fieldName];
|
|
472
|
+
|
|
473
|
+
if (!fieldDef || value === null || value === undefined) {
|
|
474
|
+
return null; // Ignore les champs ou valeurs non définis
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// --- NOUVELLE LOGIQUE POUR LES RELATIONS ---
|
|
478
|
+
if (fieldDef.type === 'relation' && value) {
|
|
479
|
+
const relatedModel = allModels?.find(m => m.name === fieldDef.relation);
|
|
480
|
+
if (!relatedModel) return `[${fieldDef.relation}]`; // Modnon trouvé
|
|
481
|
+
|
|
482
|
+
// Si la relation est multiple (un tableau d'objets)
|
|
483
|
+
if (Array.isArray(value)) {
|
|
484
|
+
return value
|
|
485
|
+
.map(item => getDataAsString(relatedModel, item, t, allModels)) // Appel récursif pour chaque item
|
|
486
|
+
.filter(Boolean)
|
|
487
|
+
.join(', ');
|
|
488
|
+
}
|
|
489
|
+
// Si la relation est simple (un seul objet)
|
|
490
|
+
else if (typeof value === 'object') {
|
|
491
|
+
return getDataAsString(relatedModel, value, t, allModels); // Appel récursif
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
if(fieldDef.type === 'datetime'){
|
|
496
|
+
return new Date(value).toLocaleDateString(lang, {year: 'numeric', month: 'numeric', day: 'numeric', hour: 'numeric', minute: 'numeric'});
|
|
497
|
+
}
|
|
498
|
+
if(fieldDef.type === 'date'){
|
|
499
|
+
return new Date(value).toLocaleDateString(lang, {year: 'numeric', month: 'numeric', day: 'numeric'});
|
|
500
|
+
}
|
|
501
|
+
// --- FIN DE LA NOUVELLE LOGIQUE ---
|
|
502
|
+
|
|
503
|
+
// Logique existante pour les autres types de champs
|
|
504
|
+
if (value.value !== undefined) {
|
|
505
|
+
return value.value; // Champs traduits
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
if( typeof(value) === 'string' ) {
|
|
509
|
+
const translatedValue = t(value, {defaultValue: value});
|
|
510
|
+
return translatedValue || value.toString();
|
|
511
|
+
}
|
|
512
|
+
if( typeof(value) === 'object' ) {
|
|
513
|
+
return JSON.stringify(value, null, 2);
|
|
514
|
+
}
|
|
515
|
+
return value;
|
|
516
|
+
|
|
517
|
+
}).filter(Boolean).join(', ');
|
|
518
|
+
|
|
519
|
+
// Si après tout ça la chaîne est vide (ex: l'ID est le seul champ mais on ne veut pas l'afficher seul)
|
|
520
|
+
// On retourne l'ID pour avoir au moins un identifiant.
|
|
521
|
+
return resultString || data._id;
|
|
521
522
|
}
|