data-primals-engine 1.6.5 → 1.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/README.md +160 -113
  2. package/client/index.js +3 -0
  3. package/client/package-lock.json +8121 -8824
  4. package/client/package.json +10 -3
  5. package/client/src/AssistantChat.jsx +369 -362
  6. package/client/src/DataEditor.jsx +383 -383
  7. package/client/src/DataLayout.jsx +54 -20
  8. package/client/src/ModelList.jsx +280 -280
  9. package/client/src/ViewSwitcher.scss +0 -31
  10. package/client/src/constants.js +81 -100
  11. package/client/src/contexts/CommandContext.jsx +274 -259
  12. package/client/vite.config.js +30 -30
  13. package/doc/AI-assistance.md +93 -0
  14. package/doc/Advanced-workflows.md +90 -0
  15. package/doc/Event-system.md +79 -0
  16. package/doc/Packs-gallery.md +73 -0
  17. package/package.json +30 -16
  18. package/src/constants.js +1 -1
  19. package/src/core.js +487 -477
  20. package/src/defaultModels.js +1 -1
  21. package/src/email.js +0 -2
  22. package/src/engine.js +342 -335
  23. package/src/filter.js +348 -343
  24. package/src/migrate.js +1 -1
  25. package/src/modules/assistant/assistant.js +30 -20
  26. package/src/modules/data/data.backup.js +4 -4
  27. package/src/modules/data/data.js +311 -302
  28. package/src/modules/data/data.operations.js +79 -64
  29. package/src/modules/data/data.relations.js +2 -1
  30. package/src/modules/data/data.scheduling.js +1 -1
  31. package/src/modules/mongodb.js +16 -8
  32. package/src/modules/user.js +0 -1
  33. package/src/modules/workflow.js +1828 -1815
  34. package/src/packs.js +5701 -5697
  35. package/src/profiles.js +19 -0
  36. package/swagger-en.yml +3390 -3385
  37. package/swagger-fr.yml +3385 -3380
  38. package/test/assistant.test.js +2 -2
  39. package/test/data.backup.integration.test.js +3 -1
  40. package/test/data.history.integration.test.js +0 -1
package/src/core.js CHANGED
@@ -1,478 +1,488 @@
1
- import path from "node:path";
2
- import fs from "node:fs";
3
- import crypto from "node:crypto";
4
-
5
- export const sleep = (ms = 1000) =>
6
- new Promise((resolve) => setTimeout(resolve, ms));
7
-
8
- export function escapeRegex(string) {
9
- return string.replace(/[/\-\\^$*+?.()|[\]{}]/g, '\\$&');
10
- }
11
-
12
- export const sequential = async (tasks) => {
13
- const res = [];
14
- for (const task of tasks) {
15
- const r = await task();
16
- res.push(r);
17
- }
18
- return res;
19
- };
20
-
21
- export function isValidRegex(s) {
22
- try {
23
- const m = s.match(/^([/~@;%#'])(.*?)\1([gimsuy]*)$/);
24
- return m ? !!new RegExp(m[2],m[3])
25
- : false;
26
- } catch (e) {
27
- return false
28
- }
29
- }
30
- export function escapeHtml(string){
31
- return string.replace(/(javascript|data|vbscript):/gi, '').replace(/[^\w-_. ]/gi, function (c) {
32
- return `&#${c.charCodeAt(0)};`;
33
- });
34
- }
35
-
36
- export const isUnsecureKey = (key) => {
37
- return ["__proto__", "constructor", "prototype"].includes(key);
38
- }
39
-
40
- export const parseSafeJSON = (json) => JSON.parse(json, (key, value) => isUnsecureKey(key) ? undefined : value);
41
-
42
-
43
- export const isDate = dt => {
44
- if (dt === null || typeof dt === 'undefined') return false;
45
- return String(new Date(dt)) !== 'Invalid Date';
46
- };
47
-
48
- export const safeAssignObject = (obj, key, value) => {
49
- if( !isUnsecureKey(key)){
50
- obj[key] = value;
51
- }
52
- }
53
- export function debounce(callback, delay=300){
54
- var timer;
55
- return function(){
56
- var args = arguments;
57
- var context = this;
58
- clearTimeout(timer);
59
- timer = setTimeout(function(){
60
- callback.apply(context, args);
61
- }, delay)
62
- }
63
- }
64
-
65
- export function uuidv4() {
66
- return "10000000-1000-4000-8000-100000000000".replace(/[018]/g, c =>
67
- (+c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> +c / 4).toString(16)
68
- );
69
- }
70
-
71
- export function cssProps(cssString) {
72
- if (!cssString) return {};
73
-
74
- const style = {};
75
- const declarations = cssString.split(';');
76
-
77
- declarations.forEach(declaration => {
78
- const trimmed = declaration.trim();
79
- if (!trimmed) return;
80
-
81
- const colonIndex = trimmed.indexOf(':');
82
- if (colonIndex === -1) return;
83
-
84
- let property = trimmed.slice(0, colonIndex).trim();
85
- const value = trimmed.slice(colonIndex + 1).trim();
86
-
87
- // Convert kebab-case to camelCase
88
- if (property.includes('-')) {
89
- property = property.replace(/-([a-z])/g, (match, letter) => letter.toUpperCase());
90
- }
91
-
92
- style[property] = value;
93
- });
94
-
95
- return style;
96
- }
97
-
98
-
99
- export function removeDir(dirPath) {
100
- const dirContents = fs.readdirSync(dirPath); // List dir content
101
-
102
- for (const fileOrDirPath of dirContents) {
103
- try {
104
- // Get Full path
105
- const fullPath = path.join(dirPath, fileOrDirPath);
106
- const stat = fs.statSync(fullPath);
107
- if (stat.isDirectory()) {
108
- // It's a sub directory
109
- if (fs.readdirSync(fullPath).length) removeDir(fullPath);
110
- // If the dir is not empty then remove it's contents too(recursively)
111
- fs.rmdirSync(fullPath);
112
- } else fs.unlinkSync(fullPath); // It's a file
113
- } catch (ex) {
114
- console.error(ex.message);
115
- }
116
- }
117
-
118
- }
119
-
120
- export const wordWrap = (str, max, br = '\n') => str.replace(
121
- new RegExp(`(?![^\\n]{1,${max}}$)([^\\n]{1,${max}})\\s`, 'g'), `$1${br}`
122
- );
123
-
124
- export const getObjectHash = (obj, uniqueFields = null, key = "") => {
125
- let str = "";
126
- const keysToProcess = Object.keys(obj).sort(); // Trier les clés pour la cohérence
127
-
128
- keysToProcess.forEach(k1 => {
129
- const v = obj[k1];
130
- if (v !== undefined) { // Ignorer les clés avec des valeurs undefined
131
- // Simplification de la logique: on inclut soit les champs uniques, soit tous les champs.
132
- // La sérialisation est la même dans les deux cas pour un champ donné.
133
- if (uniqueFields === null || uniqueFields.length === 0 || uniqueFields.includes(k1)) {
134
- try {
135
- // Utiliser JSON.stringify avec un replacer pour gérer plus de types si nécessaire,
136
- // ou simplement stringify directement. Attention aux types non sérialisables.
137
- str += k1 + ':' + JSON.stringify(v) + ';'; // Inclure clé + valeur sérialisée
138
- } catch (e) {
139
- // Gérer les erreurs de sérialisation (ex: objets circulaires)
140
- console.warn(`Could not stringify value for key ${k1} in getObjectHash`, e);
141
- str += k1 + ':[unserializable];';
142
- }
143
- }
144
- }
145
- });
146
-
147
- const buffer = str + key; // Ajouter la clé optionnelle à la fin
148
-
149
- // Utiliser cyrb53 pour le hachage final
150
- return cyrb53(buffer);
151
- };
152
-
153
- export function isPathRelativeTo(dir, parent) {
154
- const relative = path.relative(parent, dir);
155
- return relative && !relative.startsWith('..') && !path.isAbsolute(relative);
156
- }
157
-
158
- /**
159
- * Ensures the value is a valid GUID
160
- * @param value string value
161
- */
162
- export function isGUID(value) {
163
- return !!(typeof(value) === 'string' && value.match(/^[a-zA-Z0-9]{8}-[a-zA-Z0-9]{4}-[a-zA-Z0-9]{4}-[a-zA-Z0-9]{4}-[a-zA-Z0-9]{12}$/));
164
- }
165
-
166
- export function isPlainObject(obj) {
167
- return typeof obj === 'object' && obj !== null && !Array.isArray(obj) && !(obj instanceof Date);
168
- }
169
-
170
- export function escapeRegExp(string) {
171
- return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
172
- }
173
-
174
- export function shuffle(array) {
175
- let currentIndex = array.length;
176
-
177
- // While there remain elements to shuffle...
178
- while (currentIndex !== 0) {
179
-
180
- // Pick a remaining element...
181
- let randomIndex = Math.floor(Math.random() * currentIndex);
182
- currentIndex--;
183
-
184
- // And swap it with the current element.
185
- [array[currentIndex], array[randomIndex]] = [
186
- array[randomIndex], array[currentIndex]];
187
- }
188
- }
189
-
190
- function sfc32(a, b, c, d) {
191
- a |= 0; b |= 0; c |= 0; d |= 0;
192
- let t = (a + b | 0) + d | 0;
193
- d = d + 1 | 0;
194
- a = b ^ b >>> 9;
195
- b = c + (c << 3) | 0;
196
- c = (c << 21 | c >>> 11);
197
- c = c + t | 0;
198
- return (t >>> 0) / 4294967296;
199
- }
200
-
201
- let seed = 0;
202
- export const setSeed = (s)=>{
203
- seed = s;
204
- }
205
- function splitmix32(a) {
206
- a |= 0;
207
- a = a + 0x9e3779b9 | 0;
208
- let t = a ^ a >>> 16;
209
- t = Math.imul(t, 0x21f0aaad);
210
- t = t ^ t >>> 15;
211
- t = Math.imul(t, 0x735a2d97);
212
- return ((t = t ^ t >>> 15) >>> 0) / 4294967296;
213
- }
214
-
215
-
216
- export const getRand = () => {
217
- const result = splitmix32(seed);
218
- seed++; // Simple increment to change the state for the next call
219
- return result;
220
- };
221
-
222
- const MAX_RANGE_SIZE = 2n ** 64n
223
- const buffer = new BigUint64Array(512)
224
- let offset = buffer.length
225
-
226
- /**
227
- * Returns a cryptographically secure random integer between min and max, inclusive.
228
- *
229
- * @param {number} min - the lowest integer in the desired range (inclusive)
230
- * @param {number} max - the highest integer in the desired range (inclusive)
231
- * @returns {number} Random number
232
- */
233
-
234
- export function getRandom(min, max) {
235
- if (!(Number.isSafeInteger(min) && Number.isSafeInteger(max))) {
236
- throw Error("min and max must be safe integers")
237
- }
238
- if (min > max) {
239
- throw Error("min must be less than or equal to max")
240
- }
241
- const bmin = BigInt(min)
242
- const rangeSize = BigInt(max) - bmin + 1n
243
- const rejectionThreshold = MAX_RANGE_SIZE - (MAX_RANGE_SIZE % rangeSize)
244
- let result;
245
- do {
246
- if (offset >= buffer.length) {
247
- crypto.getRandomValues(buffer)
248
- offset = 0
249
- }
250
- result = buffer[offset++]
251
- } while (result >= rejectionThreshold)
252
- return Number(bmin + result % rangeSize)
253
- }
254
-
255
- /**
256
- * Returns a cryptographically secure random integer between min and max, inclusive.
257
- *
258
- * @param {number} minInclusive - the lowest integer in the desired range (inclusive)
259
- * @param {number} maxInclusive - the highest integer in the desired range (inclusive)
260
- * @returns {number} Random number
261
- */
262
-
263
- export function getBrowserRandom(minInclusive, maxInclusive) {
264
- const randomBuffer = new Uint32Array(1);
265
- const cr = (window.crypto || window.msCrypto);
266
- cr?.getRandomValues(randomBuffer);
267
- const r = cr ? ( randomBuffer[0] / (0xffffffff + 1) ) : getRand();
268
- return Math.floor(r * (maxInclusive - minInclusive + 1) + minInclusive);
269
- }
270
-
271
- export function randomDate(start, end) {
272
- return new Date(start.getTime() + Math.random() * (end.getTime() - start.getTime()));
273
- }
274
- export function isLightColor(color) {
275
- if( !color )
276
- return true;
277
- const hex = color.replace('#', '');
278
- const c_r = parseInt(hex.substr(0, 2), 16);
279
- const c_g = parseInt(hex.substr(2, 2), 16);
280
- const c_b = parseInt(hex.substr(4, 2), 16);
281
- const brightness = ((c_r * 299) + (c_g * 587) + (c_b * 114)) / 1000;
282
- return brightness > 155;
283
- }
284
-
285
- // Mettez cette fonction au début du fichier DataEditor.jsx ou dans un fichier utilitaire importé
286
- export const tryParseJson = (jsonString) => {
287
- if (!jsonString || typeof jsonString !== 'string') {
288
- return null; // Retourne null si la chaîne est vide, null, ou pas une chaîne
289
- }
290
- try {
291
- // Tenter de parser la chaîne JSON
292
- const parsed = JSON.parse(jsonString);
293
- // S'assurer que c'est un objet ou null (pas un simple nombre, string, etc.)
294
- return (typeof parsed === 'object' || parsed === null) ? parsed : null;
295
- } catch (e) {
296
- console.error("Failed to parse condition JSON:", e, jsonString);
297
- return null; // Retourne null en cas d'erreur de parsing
298
- }
299
- };
300
-
301
- export function isIsoDate(str) {
302
- if (!/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/.test(str)) return false;
303
- const d = new Date(str);
304
- return !isNaN(d.getTime()) && d.toISOString()===str; // valid date
305
- }
306
-
307
-
308
- Math.seed = function(s) {
309
- return function() {
310
- s = Math.sin(s) * 10000; return s - Math.floor(s);
311
- };
312
- };
313
-
314
- const cyrb53 = (str, seed = 0) => {
315
- let h1 = 0xdeadbeef ^ seed, h2 = 0x41c6ce57 ^ seed;
316
- for(let i = 0, ch; i < str.length; i++) {
317
- ch = str.charCodeAt(i);
318
- h1 = Math.imul(h1 ^ ch, 2654435761);
319
- h2 = Math.imul(h2 ^ ch, 1597334677);
320
- }
321
- h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507);
322
- h1 ^= Math.imul(h2 ^ (h2 >>> 13), 3266489909);
323
- h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507);
324
- h2 ^= Math.imul(h1 ^ (h1 >>> 13), 3266489909);
325
-
326
- return 4294967296 * (2097151 & h2) + (h1 >>> 0);
327
- };
328
-
329
- String.prototype.hashCode = function() {
330
- return cyrb53(this);
331
- }
332
-
333
- export function random(min, max) {
334
- min = Math.ceil(min);
335
- max = Math.floor(max);
336
- return Math.floor(Math.random() * (max - min + 1)) + min;
337
- }
338
-
339
-
340
- let triggers = {};
341
- export const event_trigger = (name, ...params) => {
342
- //console.log('Triggering raw event "' + name + '"');
343
- let ret = false;
344
- if (Array.isArray(triggers[name])) {
345
- triggers[name].forEach((t) => {
346
- const res = t.callback(...params);
347
- if (Array.isArray(res)) {
348
- if (!Array.isArray(ret)) ret = [];
349
- ret = ret.concat(res);
350
- } else if (typeof res === "string") {
351
- if (typeof ret !== "string") ret = "";
352
- ret += res;
353
- } else if (typeof res === "number") {
354
- if (typeof ret !== "number") ret = 0;
355
- ret += res;
356
- } else if (typeof res === "boolean") {
357
- if (typeof ret !== "boolean") ret = true;
358
- ret = res && ret;
359
- } else {
360
- ret = res || ret;
361
- }
362
- });
363
- }
364
- return ret;
365
- };
366
- export const event_on = (name, callback) => {
367
- if (!Array.isArray(triggers[name])) {
368
- safeAssignObject(triggers, name, []);
369
- }
370
- triggers[name].push({ callback });
371
- };
372
- export const event_off = (name, callback) => {
373
- if (callback && triggers[name]) {
374
- safeAssignObject(triggers, name, triggers[name].filter((f) => f.callback !== callback));
375
- } else {
376
- triggers[name] = undefined;
377
- }
378
- };
379
-
380
-
381
- export function slugify(str,replacer='-', removeNonAscii=false) {
382
-
383
- if (!str) return '';
384
-
385
- // 1. Convert to string, lowercase, and trim whitespace.
386
- str = str.toString().toLowerCase().trim();
387
-
388
- // 2. Normalize Unicode characters (e.g., è -> e). This is now always done.
389
- str = str.normalize('NFD').replace(/[\u0300-\u036f]/g, '');
390
-
391
- // 3. Remove any remaining non-ASCII characters if the flag is set.
392
- if (removeNonAscii) {
393
- str = str.replace(/[^a-z0-9\s-]/g, '');
394
- }
395
-
396
- // 4. Replace spaces with the replacer and clean up consecutive/trailing replacers.
397
- return str
398
- .replace(/\s+/g, replacer) // Replace spaces with the replacer.
399
- .replace(new RegExp(`[^a-z0-9${replacer}]`, 'g'), '') // Remove any character that is not a letter, a number, or the replacer itself.
400
- .replace(new RegExp(`${replacer}+`, 'g'), replacer); // Replace multiple replacers with a single one.
401
- }
402
-
403
- // resource : https://stackoverflow.com/questions/190852/how-can-i-get-file-extensions-with-javascript
404
- export function getFileExtension(fname) {
405
- return fname.slice((fname.lastIndexOf(".") - 1 >>> 0) + 2);
406
- }
407
-
408
- export function object_equals( x, y ) {
409
- if ( x === y ) return true;
410
- // if both x and y are null or undefined and exactly the same
411
-
412
- if ( ! ( x instanceof Object ) || ! ( y instanceof Object ) ) return false;
413
- // if they are not strictly equal, they both need to be Objects
414
-
415
- if ( x.constructor !== y.constructor ) return false;
416
- // they must have the exact same prototype chain, the closest we can do is
417
- // test there constructor.
418
-
419
- for ( var p in x ) {
420
- if ( ! x.hasOwnProperty( p ) ) continue;
421
- // other properties were tested using x.constructor === y.constructor
422
-
423
- if ( ! y.hasOwnProperty( p ) ) return false;
424
- // allows to compare x[ p ] and y[ p ] when set to undefined
425
-
426
- if ( x[ p ] === y[ p ] ) continue;
427
- // if they have the same strict value or identity then they are equal
428
-
429
- if ( typeof( x[ p ] ) !== "object" ) return false;
430
- // Numbers, Strings, Functions, Booleans must be strictly equal
431
-
432
- if ( ! object_equals( x[ p ], y[ p ] ) ) return false;
433
- // Objects and Arrays must be tested recursively
434
- }
435
-
436
- for ( p in y )
437
- if ( y.hasOwnProperty( p ) && ! x.hasOwnProperty( p ) )
438
- return false;
439
- // allows x[ p ] to be set to undefined
440
-
441
- return true;
442
- }
443
-
444
- export const isValidPath = (path) =>{
445
- return /^(?:[a-z]:)?[\/\\]{0,2}(?:[.\/\\ ](?![.\/\\\n])|[^<>:"|?*.\/\\ \n])+$/gmi.test(path);
446
- }
447
-
448
-
449
- /**
450
- * Génère une couleur de base cohérente à partir d'une chaîne de caractères.
451
- * @param {string} str La chaîne à hasher (par exemple, le libellé d'une catégorie).
452
- * @returns {string} Une couleur au format 'hsl(h, s, l)'.
453
- */
454
- export const stringToHslColor = (str) => {
455
- let hash = 0;
456
- for (let i = 0; i < str.length; i++) {
457
- hash = str.charCodeAt(i) + ((hash << 5) - hash);
458
- }
459
- // Utiliser HSL pour un meilleur contrôle sur la saturation et la luminosité
460
- const h = hash % 360; // Teinte (0-359)
461
- const s = 70; // Saturation (fixe pour des couleurs vives mais pas criardes)
462
- const l = 55; // Luminosité (fixe pour une bonne lisibilité)
463
- return `hsl(${h}, ${s}%, ${l}%)`;
464
- };
465
-
466
- export function countKeys(t) {
467
- switch (t?.constructor) {
468
- case Object: // 1
469
- return Object
470
- .values(t)
471
- .reduce((r, v) => r + 1 + countKeys(v), 0)
472
- case Array: // 2
473
- return t
474
- .reduce((r, v) => r + countKeys(v), 0)
475
- default: // 3
476
- return 0
477
- }
1
+ import path from "node:path";
2
+ import fs from "node:fs";
3
+ import crypto from "node:crypto";
4
+
5
+ export const sleep = (ms = 1000) =>
6
+ new Promise((resolve) => setTimeout(resolve, ms));
7
+
8
+ export function escapeRegex(string) {
9
+ return string.replace(/[/\-\\^$*+?.()|[\]{}]/g, '\\$&');
10
+ }
11
+
12
+ export const sequential = async (tasks) => {
13
+ const res = [];
14
+ for (const task of tasks) {
15
+ const r = await task();
16
+ res.push(r);
17
+ }
18
+ return res;
19
+ };
20
+
21
+ export function isValidRegex(s) {
22
+ try {
23
+ const m = s.match(/^([/~@;%#'])(.*?)\1([gimsuy]*)$/);
24
+ return m ? !!new RegExp(m[2],m[3])
25
+ : false;
26
+ } catch (e) {
27
+ return false
28
+ }
29
+ }
30
+ export function escapeHtml(string){
31
+ return string.replace(/(javascript|data|vbscript):/gi, '').replace(/[^\w-_. ]/gi, function (c) {
32
+ return `&#${c.charCodeAt(0)};`;
33
+ });
34
+ }
35
+
36
+ export const isUnsecureKey = (key) => {
37
+ return ["__proto__", "constructor", "prototype"].includes(key);
38
+ }
39
+
40
+ export const parseSafeJSON = (json) => JSON.parse(json, (key, value) => isUnsecureKey(key) ? undefined : value);
41
+
42
+
43
+ export const isDate = dt => {
44
+ if (dt === null || typeof dt === 'undefined') return false;
45
+ return String(new Date(dt)) !== 'Invalid Date';
46
+ };
47
+
48
+ export const safeAssignObject = (obj, key, value) => {
49
+ if( !isUnsecureKey(key)){
50
+ obj[key] = value;
51
+ }
52
+ }
53
+ export function debounce(callback, delay=300){
54
+ var timer;
55
+ return function(){
56
+ var args = arguments;
57
+ var context = this;
58
+ clearTimeout(timer);
59
+ timer = setTimeout(function(){
60
+ callback.apply(context, args);
61
+ }, delay)
62
+ }
63
+ }
64
+
65
+ export function uuidv4() {
66
+ return "10000000-1000-4000-8000-100000000000".replace(/[018]/g, c =>
67
+ (+c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> +c / 4).toString(16)
68
+ );
69
+ }
70
+
71
+ export function cssProps(cssString) {
72
+ if (!cssString) return {};
73
+
74
+ const style = {};
75
+ const declarations = cssString.split(';');
76
+
77
+ declarations.forEach(declaration => {
78
+ const trimmed = declaration.trim();
79
+ if (!trimmed) return;
80
+
81
+ const colonIndex = trimmed.indexOf(':');
82
+ if (colonIndex === -1) return;
83
+
84
+ let property = trimmed.slice(0, colonIndex).trim();
85
+ const value = trimmed.slice(colonIndex + 1).trim();
86
+
87
+ // Convert kebab-case to camelCase
88
+ if (property.includes('-')) {
89
+ property = property.replace(/-([a-z])/g, (match, letter) => letter.toUpperCase());
90
+ }
91
+
92
+ style[property] = value;
93
+ });
94
+
95
+ return style;
96
+ }
97
+
98
+
99
+ export function removeDir(dirPath) {
100
+ // --- SÉCURITÉ : Ajout de la validation du chemin ---
101
+ // Définir le répertoire de base autorisé pour les suppressions.
102
+ const allowedBaseDir = path.resolve(process.cwd()); // Exemple, à adapter
103
+ const resolvedDirPath = path.resolve(dirPath);
104
+
105
+ // S'assurer que le répertoire à supprimer est bien un sous-répertoire de la base autorisée.
106
+ if (!resolvedDirPath.startsWith(allowedBaseDir)) {
107
+ throw new Error(`Suppression non autorisée en dehors du répertoire de base : ${dirPath}`);
108
+ }
109
+
110
+ const dirContents = fs.readdirSync(dirPath); // List dir content
111
+
112
+ for (const fileOrDirPath of dirContents) {
113
+ try {
114
+ // Get Full path
115
+ const fullPath = path.join(dirPath, fileOrDirPath);
116
+ const stat = fs.statSync(fullPath);
117
+ if (stat.isDirectory()) {
118
+ // It's a sub directory
119
+ if (fs.readdirSync(fullPath).length) removeDir(fullPath);
120
+ // If the dir is not empty then remove it's contents too(recursively)
121
+ fs.rmdirSync(fullPath);
122
+ } else fs.unlinkSync(fullPath); // It's a file
123
+ } catch (ex) {
124
+ console.error(ex.message);
125
+ }
126
+ }
127
+
128
+ }
129
+
130
+ export const wordWrap = (str, max, br = '\n') => str.replace(
131
+ new RegExp(`(?![^\\n]{1,${max}}$)([^\\n]{1,${max}})\\s`, 'g'), `$1${br}`
132
+ );
133
+
134
+ export const getObjectHash = (obj, uniqueFields = null, key = "") => {
135
+ let str = "";
136
+ const keysToProcess = Object.keys(obj).sort(); // Trier les clés pour la cohérence
137
+
138
+ keysToProcess.forEach(k1 => {
139
+ const v = obj[k1];
140
+ if (v !== undefined) { // Ignorer les clés avec des valeurs undefined
141
+ // Simplification de la logique: on inclut soit les champs uniques, soit tous les champs.
142
+ // La sérialisation est la même dans les deux cas pour un champ donné.
143
+ if (uniqueFields === null || uniqueFields.length === 0 || uniqueFields.includes(k1)) {
144
+ try {
145
+ // Utiliser JSON.stringify avec un replacer pour gérer plus de types si nécessaire,
146
+ // ou simplement stringify directement. Attention aux types non sérialisables.
147
+ str += k1 + ':' + JSON.stringify(v) + ';'; // Inclure clé + valeur sérialisée
148
+ } catch (e) {
149
+ // Gérer les erreurs de sérialisation (ex: objets circulaires)
150
+ console.warn(`Could not stringify value for key ${k1} in getObjectHash`, e);
151
+ str += k1 + ':[unserializable];';
152
+ }
153
+ }
154
+ }
155
+ });
156
+
157
+ const buffer = str + key; // Ajouter la clé optionnelle à la fin
158
+
159
+ // Utiliser cyrb53 pour le hachage final
160
+ return cyrb53(buffer);
161
+ };
162
+
163
+ export function isPathRelativeTo(dir, parent) {
164
+ const relative = path.relative(parent, dir);
165
+ return relative && !relative.startsWith('..') && !path.isAbsolute(relative);
166
+ }
167
+
168
+ /**
169
+ * Ensures the value is a valid GUID
170
+ * @param value string value
171
+ */
172
+ export function isGUID(value) {
173
+ return !!(typeof(value) === 'string' && value.match(/^[a-zA-Z0-9]{8}-[a-zA-Z0-9]{4}-[a-zA-Z0-9]{4}-[a-zA-Z0-9]{4}-[a-zA-Z0-9]{12}$/));
174
+ }
175
+
176
+ export function isPlainObject(obj) {
177
+ return typeof obj === 'object' && obj !== null && !Array.isArray(obj) && !(obj instanceof Date);
178
+ }
179
+
180
+ export function escapeRegExp(string) {
181
+ return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
182
+ }
183
+
184
+ export function shuffle(array) {
185
+ let currentIndex = array.length;
186
+
187
+ // While there remain elements to shuffle...
188
+ while (currentIndex !== 0) {
189
+
190
+ // Pick a remaining element...
191
+ let randomIndex = Math.floor(Math.random() * currentIndex);
192
+ currentIndex--;
193
+
194
+ // And swap it with the current element.
195
+ [array[currentIndex], array[randomIndex]] = [
196
+ array[randomIndex], array[currentIndex]];
197
+ }
198
+ }
199
+
200
+ function sfc32(a, b, c, d) {
201
+ a |= 0; b |= 0; c |= 0; d |= 0;
202
+ let t = (a + b | 0) + d | 0;
203
+ d = d + 1 | 0;
204
+ a = b ^ b >>> 9;
205
+ b = c + (c << 3) | 0;
206
+ c = (c << 21 | c >>> 11);
207
+ c = c + t | 0;
208
+ return (t >>> 0) / 4294967296;
209
+ }
210
+
211
+ let seed = 0;
212
+ export const setSeed = (s)=>{
213
+ seed = s;
214
+ }
215
+ function splitmix32(a) {
216
+ a |= 0;
217
+ a = a + 0x9e3779b9 | 0;
218
+ let t = a ^ a >>> 16;
219
+ t = Math.imul(t, 0x21f0aaad);
220
+ t = t ^ t >>> 15;
221
+ t = Math.imul(t, 0x735a2d97);
222
+ return ((t = t ^ t >>> 15) >>> 0) / 4294967296;
223
+ }
224
+
225
+
226
+ export const getRand = () => {
227
+ const result = splitmix32(seed);
228
+ seed++; // Simple increment to change the state for the next call
229
+ return result;
230
+ };
231
+
232
+ const MAX_RANGE_SIZE = 2n ** 64n
233
+ const buffer = new BigUint64Array(512)
234
+ let offset = buffer.length
235
+
236
+ /**
237
+ * Returns a cryptographically secure random integer between min and max, inclusive.
238
+ *
239
+ * @param {number} min - the lowest integer in the desired range (inclusive)
240
+ * @param {number} max - the highest integer in the desired range (inclusive)
241
+ * @returns {number} Random number
242
+ */
243
+
244
+ export function getRandom(min, max) {
245
+ if (!(Number.isSafeInteger(min) && Number.isSafeInteger(max))) {
246
+ throw Error("min and max must be safe integers")
247
+ }
248
+ if (min > max) {
249
+ throw Error("min must be less than or equal to max")
250
+ }
251
+ const bmin = BigInt(min)
252
+ const rangeSize = BigInt(max) - bmin + 1n
253
+ const rejectionThreshold = MAX_RANGE_SIZE - (MAX_RANGE_SIZE % rangeSize)
254
+ let result;
255
+ do {
256
+ if (offset >= buffer.length) {
257
+ crypto.getRandomValues(buffer)
258
+ offset = 0
259
+ }
260
+ result = buffer[offset++]
261
+ } while (result >= rejectionThreshold)
262
+ return Number(bmin + result % rangeSize)
263
+ }
264
+
265
+ /**
266
+ * Returns a cryptographically secure random integer between min and max, inclusive.
267
+ *
268
+ * @param {number} minInclusive - the lowest integer in the desired range (inclusive)
269
+ * @param {number} maxInclusive - the highest integer in the desired range (inclusive)
270
+ * @returns {number} Random number
271
+ */
272
+
273
+ export function getBrowserRandom(minInclusive, maxInclusive) {
274
+ const randomBuffer = new Uint32Array(1);
275
+ const cr = (window.crypto || window.msCrypto);
276
+ cr?.getRandomValues(randomBuffer);
277
+ const r = cr ? ( randomBuffer[0] / (0xffffffff + 1) ) : getRand();
278
+ return Math.floor(r * (maxInclusive - minInclusive + 1) + minInclusive);
279
+ }
280
+
281
+ export function randomDate(start, end) {
282
+ return new Date(start.getTime() + Math.random() * (end.getTime() - start.getTime()));
283
+ }
284
+ export function isLightColor(color) {
285
+ if( !color )
286
+ return true;
287
+ const hex = color.replace('#', '');
288
+ const c_r = parseInt(hex.substr(0, 2), 16);
289
+ const c_g = parseInt(hex.substr(2, 2), 16);
290
+ const c_b = parseInt(hex.substr(4, 2), 16);
291
+ const brightness = ((c_r * 299) + (c_g * 587) + (c_b * 114)) / 1000;
292
+ return brightness > 155;
293
+ }
294
+
295
+ // Mettez cette fonction au début du fichier DataEditor.jsx ou dans un fichier utilitaire importé
296
+ export const tryParseJson = (jsonString) => {
297
+ if (!jsonString || typeof jsonString !== 'string') {
298
+ return null; // Retourne null si la chaîne est vide, null, ou pas une chaîne
299
+ }
300
+ try {
301
+ // Tenter de parser la chaîne JSON
302
+ const parsed = JSON.parse(jsonString);
303
+ // S'assurer que c'est un objet ou null (pas un simple nombre, string, etc.)
304
+ return (typeof parsed === 'object' || parsed === null) ? parsed : null;
305
+ } catch (e) {
306
+ console.error("Failed to parse condition JSON:", e, jsonString);
307
+ return null; // Retourne null en cas d'erreur de parsing
308
+ }
309
+ };
310
+
311
+ export function isIsoDate(str) {
312
+ if (!/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/.test(str)) return false;
313
+ const d = new Date(str);
314
+ return !isNaN(d.getTime()) && d.toISOString()===str; // valid date
315
+ }
316
+
317
+
318
+ Math.seed = function(s) {
319
+ return function() {
320
+ s = Math.sin(s) * 10000; return s - Math.floor(s);
321
+ };
322
+ };
323
+
324
+ const cyrb53 = (str, seed = 0) => {
325
+ let h1 = 0xdeadbeef ^ seed, h2 = 0x41c6ce57 ^ seed;
326
+ for(let i = 0, ch; i < str.length; i++) {
327
+ ch = str.charCodeAt(i);
328
+ h1 = Math.imul(h1 ^ ch, 2654435761);
329
+ h2 = Math.imul(h2 ^ ch, 1597334677);
330
+ }
331
+ h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507);
332
+ h1 ^= Math.imul(h2 ^ (h2 >>> 13), 3266489909);
333
+ h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507);
334
+ h2 ^= Math.imul(h1 ^ (h1 >>> 13), 3266489909);
335
+
336
+ return 4294967296 * (2097151 & h2) + (h1 >>> 0);
337
+ };
338
+
339
+ String.prototype.hashCode = function() {
340
+ return cyrb53(this);
341
+ }
342
+
343
+ export function random(min, max) {
344
+ min = Math.ceil(min);
345
+ max = Math.floor(max);
346
+ return Math.floor(Math.random() * (max - min + 1)) + min;
347
+ }
348
+
349
+
350
+ let triggers = {};
351
+ export const event_trigger = (name, ...params) => {
352
+ //console.log('Triggering raw event "' + name + '"');
353
+ let ret = false;
354
+ if (Array.isArray(triggers[name])) {
355
+ triggers[name].forEach((t) => {
356
+ const res = t.callback(...params);
357
+ if (Array.isArray(res)) {
358
+ if (!Array.isArray(ret)) ret = [];
359
+ ret = ret.concat(res);
360
+ } else if (typeof res === "string") {
361
+ if (typeof ret !== "string") ret = "";
362
+ ret += res;
363
+ } else if (typeof res === "number") {
364
+ if (typeof ret !== "number") ret = 0;
365
+ ret += res;
366
+ } else if (typeof res === "boolean") {
367
+ if (typeof ret !== "boolean") ret = true;
368
+ ret = res && ret;
369
+ } else {
370
+ ret = res || ret;
371
+ }
372
+ });
373
+ }
374
+ return ret;
375
+ };
376
+ export const event_on = (name, callback) => {
377
+ if (!Array.isArray(triggers[name])) {
378
+ safeAssignObject(triggers, name, []);
379
+ }
380
+ triggers[name].push({ callback });
381
+ };
382
+ export const event_off = (name, callback) => {
383
+ if (callback && triggers[name]) {
384
+ safeAssignObject(triggers, name, triggers[name].filter((f) => f.callback !== callback));
385
+ } else {
386
+ triggers[name] = undefined;
387
+ }
388
+ };
389
+
390
+
391
+ export function slugify(str,replacer='-', removeNonAscii=false) {
392
+
393
+ if (!str) return '';
394
+
395
+ // 1. Convert to string, lowercase, and trim whitespace.
396
+ str = str.toString().toLowerCase().trim();
397
+
398
+ // 2. Normalize Unicode characters (e.g., è -> e). This is now always done.
399
+ str = str.normalize('NFD').replace(/[\u0300-\u036f]/g, '');
400
+
401
+ // 3. Remove any remaining non-ASCII characters if the flag is set.
402
+ if (removeNonAscii) {
403
+ str = str.replace(/[^a-z0-9\s-]/g, '');
404
+ }
405
+
406
+ // 4. Replace spaces with the replacer and clean up consecutive/trailing replacers.
407
+ return str
408
+ .replace(/\s+/g, replacer) // Replace spaces with the replacer.
409
+ .replace(new RegExp(`[^a-z0-9${replacer}]`, 'g'), '') // Remove any character that is not a letter, a number, or the replacer itself.
410
+ .replace(new RegExp(`${replacer}+`, 'g'), replacer); // Replace multiple replacers with a single one.
411
+ }
412
+
413
+ // resource : https://stackoverflow.com/questions/190852/how-can-i-get-file-extensions-with-javascript
414
+ export function getFileExtension(fname) {
415
+ return fname.slice((fname.lastIndexOf(".") - 1 >>> 0) + 2);
416
+ }
417
+
418
+ export function object_equals( x, y ) {
419
+ if ( x === y ) return true;
420
+ // if both x and y are null or undefined and exactly the same
421
+
422
+ if ( ! ( x instanceof Object ) || ! ( y instanceof Object ) ) return false;
423
+ // if they are not strictly equal, they both need to be Objects
424
+
425
+ if ( x.constructor !== y.constructor ) return false;
426
+ // they must have the exact same prototype chain, the closest we can do is
427
+ // test there constructor.
428
+
429
+ for ( var p in x ) {
430
+ if ( ! x.hasOwnProperty( p ) ) continue;
431
+ // other properties were tested using x.constructor === y.constructor
432
+
433
+ if ( ! y.hasOwnProperty( p ) ) return false;
434
+ // allows to compare x[ p ] and y[ p ] when set to undefined
435
+
436
+ if ( x[ p ] === y[ p ] ) continue;
437
+ // if they have the same strict value or identity then they are equal
438
+
439
+ if ( typeof( x[ p ] ) !== "object" ) return false;
440
+ // Numbers, Strings, Functions, Booleans must be strictly equal
441
+
442
+ if ( ! object_equals( x[ p ], y[ p ] ) ) return false;
443
+ // Objects and Arrays must be tested recursively
444
+ }
445
+
446
+ for ( p in y )
447
+ if ( y.hasOwnProperty( p ) && ! x.hasOwnProperty( p ) )
448
+ return false;
449
+ // allows x[ p ] to be set to undefined
450
+
451
+ return true;
452
+ }
453
+
454
+ export const isValidPath = (path) =>{
455
+ return /^(?:[a-z]:)?[\/\\]{0,2}(?:[.\/\\ ](?![.\/\\\n])|[^<>:"|?*.\/\\ \n])+$/gmi.test(path);
456
+ }
457
+
458
+
459
+ /**
460
+ * Génère une couleur de base cohérente à partir d'une chaîne de caractères.
461
+ * @param {string} str La chaîne à hasher (par exemple, le libellé d'une catégorie).
462
+ * @returns {string} Une couleur au format 'hsl(h, s, l)'.
463
+ */
464
+ export const stringToHslColor = (str) => {
465
+ let hash = 0;
466
+ for (let i = 0; i < str.length; i++) {
467
+ hash = str.charCodeAt(i) + ((hash << 5) - hash);
468
+ }
469
+ // Utiliser HSL pour un meilleur contrôle sur la saturation et la luminosité
470
+ const h = hash % 360; // Teinte (0-359)
471
+ const s = 70; // Saturation (fixe pour des couleurs vives mais pas criardes)
472
+ const l = 55; // Luminosité (fixe pour une bonne lisibilité)
473
+ return `hsl(${h}, ${s}%, ${l}%)`;
474
+ };
475
+
476
+ export function countKeys(t) {
477
+ switch (t?.constructor) {
478
+ case Object: // 1
479
+ return Object
480
+ .values(t)
481
+ .reduce((r, v) => r + 1 + countKeys(v), 0)
482
+ case Array: // 2
483
+ return t
484
+ .reduce((r, v) => r + countKeys(v), 0)
485
+ default: // 3
486
+ return 0
487
+ }
478
488
  }