data-primals-engine 1.4.3 → 1.5.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 (77) hide show
  1. package/README.md +913 -867
  2. package/client/package-lock.json +49 -0
  3. package/client/package.json +1 -0
  4. package/client/src/AddWidgetTypeModal.jsx +47 -43
  5. package/client/src/App.jsx +3 -7
  6. package/client/src/App.scss +25 -3
  7. package/client/src/AssistantChat.jsx +363 -323
  8. package/client/src/AssistantChat.scss +30 -12
  9. package/client/src/Dashboard.jsx +480 -396
  10. package/client/src/Dashboard.scss +1 -1
  11. package/client/src/DashboardHtmlViewItem.jsx +147 -0
  12. package/client/src/DashboardView.jsx +104 -19
  13. package/client/src/DataEditor.jsx +12 -5
  14. package/client/src/DataLayout.jsx +805 -762
  15. package/client/src/DataLayout.scss +14 -0
  16. package/client/src/DataTable.jsx +63 -77
  17. package/client/src/Dialog.scss +1 -1
  18. package/client/src/Field.jsx +591 -322
  19. package/client/src/FlexDataRenderer.jsx +2 -0
  20. package/client/src/FlexTreeUtils.js +1 -1
  21. package/client/src/FlexViewCard.jsx +44 -0
  22. package/client/src/HistoryDialog.jsx +47 -14
  23. package/client/src/HtmlViewBuilderModal.jsx +91 -0
  24. package/client/src/HtmlViewBuilderModal.scss +18 -0
  25. package/client/src/HtmlViewCard.jsx +44 -0
  26. package/client/src/HtmlViewCard.scss +35 -0
  27. package/client/src/KPIDialog.jsx +11 -1
  28. package/client/src/KanbanCard.jsx +1 -2
  29. package/client/src/ModelCreator.jsx +6 -6
  30. package/client/src/ModelCreatorField.jsx +74 -31
  31. package/client/src/ModelList.jsx +93 -54
  32. package/client/src/Notification.jsx +136 -136
  33. package/client/src/Notification.scss +0 -18
  34. package/client/src/Pagination.jsx +5 -3
  35. package/client/src/RelationField.jsx +354 -258
  36. package/client/src/RelationSelectorWidget.jsx +173 -0
  37. package/client/src/constants.js +1 -1
  38. package/client/src/contexts/ModelContext.jsx +10 -1
  39. package/client/src/contexts/UIContext.jsx +72 -63
  40. package/client/src/filter.js +262 -212
  41. package/client/src/hooks/useTutorials.jsx +62 -65
  42. package/client/src/hooks/useValidation.js +75 -0
  43. package/client/src/translations.js +26 -24
  44. package/package.json +3 -1
  45. package/perf/README.md +147 -0
  46. package/perf/artillery-hooks.js +37 -0
  47. package/perf/perf-shot-hardwork.yml +84 -0
  48. package/perf/perf-shot-search.yml +45 -0
  49. package/perf/setup.yml +26 -0
  50. package/server.js +1 -1
  51. package/src/constants.js +3 -28
  52. package/src/core.js +15 -1
  53. package/src/data.js +1 -1
  54. package/src/defaultModels.js +63 -7
  55. package/src/email.js +5 -2
  56. package/src/engine.js +5 -3
  57. package/src/filter.js +5 -3
  58. package/src/i18n.js +710 -10
  59. package/src/modules/assistant/assistant.js +151 -19
  60. package/src/modules/bucket.js +14 -16
  61. package/src/modules/data/data.backup.js +11 -8
  62. package/src/modules/data/data.core.js +118 -92
  63. package/src/modules/data/data.history.js +531 -492
  64. package/src/modules/data/data.js +9 -56
  65. package/src/modules/data/data.operations.js +3282 -2999
  66. package/src/modules/data/data.relations.js +686 -686
  67. package/src/modules/data/data.routes.js +118 -24
  68. package/src/modules/data/data.scheduling.js +2 -1
  69. package/src/modules/data/data.validation.js +85 -3
  70. package/src/modules/file.js +247 -236
  71. package/src/modules/user.js +4 -1
  72. package/src/modules/workflow.js +9 -10
  73. package/src/openai.jobs.js +2 -0
  74. package/src/packs.js +5482 -5461
  75. package/src/providers.js +22 -7
  76. package/test/data.integration.test.js +136 -2
  77. package/test/import_export.integration.test.js +1 -1
@@ -1,213 +1,263 @@
1
-
2
-
3
- export const convertInputValue = (value) => {
4
- if (typeof value !== 'string') return value;
5
-
6
- if (value.startsWith('$')) {
7
- // C'est une référence de champ
8
- return value;
9
- }
10
-
11
- // Si la valeur est une chaîne entourée de guillemets, on la conserve telle quelle
12
- if (/^['"].*['"]$/.test(value)) {
13
- return value.slice(1, -1); // Retire les guillemets
14
- }
15
-
16
- // Conversion en nombre si possible
17
- if (!isNaN(value) && value.trim() !== '') {
18
- return Number(value);
19
- }
20
-
21
- if (value.toLowerCase() === 'null') return null;
22
-
23
- // Conversion en booléen pour 'true' et 'false'
24
- if (value.toLowerCase() === 'true') return true;
25
- if (value.toLowerCase() === 'false') return false;
26
-
27
- // Par défaut, retourne la chaîne telle quelle
28
- return value;
29
- };
30
-
31
- const parseNode = (node, pathPrefix = '$') => {
32
- if (node && node.path && Array.isArray(node.path)) {
33
- return node;
34
- }
35
-
36
- if (!node || typeof node !== 'object' || Array.isArray(node)) {
37
- return null;
38
- }
39
-
40
- const keys = Object.keys(node);
41
- if (keys.length === 0) return null;
42
-
43
- // Gestion des $find
44
- if (keys.length === 1 && keys[0] === '$find') {
45
- const findCondition = node.$find;
46
- const parsedCondition = parseNode(findCondition, pathPrefix === '$' ? '$$this.' : pathPrefix);
47
-
48
- if (parsedCondition) {
49
- return {
50
- ...parsedCondition,
51
- path: parsedCondition.path || [],
52
- op: '$find',
53
- value: parsedCondition
54
- };
55
- }
56
- }
57
-
58
- // Gestion des opérateurs logiques
59
- if (keys.length === 1 && (keys[0] === '$and' || keys[0] === '$or')) {
60
- const children = node[keys[0]]
61
- .map(childNode => parseNode(childNode, pathPrefix))
62
- .filter(Boolean);
63
- return { [keys[0]]: children };
64
- }
65
-
66
- // Gestion des opérateurs de comparaison
67
- if (keys[0].startsWith('$')) {
68
- const op = keys[0];
69
- const args = node[op];
70
-
71
- // Cas spécial pour regexMatch
72
- if (op === '$regexMatch' && typeof args === 'object') {
73
- const transformExpr = reverseTransformExpr(args.input, pathPrefix === '$$this.');
74
- return {
75
- path: transformExpr.path || [args.input.substring(pathPrefix.length).split('.')],
76
- op: '$regex',
77
- value: args.regex,
78
- transform: transformExpr.transform
79
- };
80
- }
81
-
82
- // Cas spécial pour exists
83
- if ((op === '$ne' || op === '$eq') && Array.isArray(args) && args.length === 2 &&
84
- typeof args[0] === 'object' && args[0].$type && args[1] === 'missing') {
85
- const transformExpr = reverseTransformExpr(args[0].$type, pathPrefix === '$$this.');
86
- return {
87
- path: transformExpr.path || [args[0].$type.substring(pathPrefix.length).split('.')],
88
- op: '$exists',
89
- value: op === '$ne',
90
- transform: transformExpr.transform
91
- };
92
- }
93
-
94
- // Cas normal avec transformation
95
- if (Array.isArray(args) && args.length === 2) {
96
- const transformExpr = reverseTransformExpr(args[0], pathPrefix === '$$this.');
97
- return {
98
- path: transformExpr.path || [args[0].substring(pathPrefix.length).split('.')],
99
- op,
100
- value: args[1],
101
- transform: transformExpr.transform
102
- };
103
- }
104
-
105
- // Cas des transformations pures
106
- const transformExpr = reverseTransformExpr(node, pathPrefix === '$$this.');
107
- if (transformExpr && transformExpr.transform) {
108
- return {
109
- path: transformExpr.path || [keys[0].substring(1)],
110
- transform: transformExpr.transform
111
- };
112
- }
113
- }
114
-
115
- // Gestion des champs simples
116
- const path = keys[0];
117
- if (typeof path === 'string' && !path.startsWith('$')) {
118
- const condition = node[path];
119
- if (typeof condition === 'object' && condition !== null && !Array.isArray(condition)) {
120
- if (condition.$find) {
121
- const parsedInnerNode = parseNode(condition.$find, '$$this.');
122
- if (parsedInnerNode) {
123
- return {
124
- ...parsedInnerNode,
125
- path: [path, ...(parsedInnerNode.path || [])]
126
- };
127
- }
128
- } else {
129
- const parsedCondition = parseNode(condition, pathPrefix);
130
- if (parsedCondition) {
131
- return {
132
- ...parsedCondition,
133
- path: [path, ...(parsedCondition.path || [])]
134
- };
135
- }
136
- }
137
- } else {
138
- return {
139
- path: [path],
140
- op: '$eq',
141
- value: condition
142
- };
143
- }
144
- }
145
-
146
- return null;
147
- };
148
-
149
-
150
- export const buildRootFromExpr = (query) => {
151
- if (!query || typeof query !== 'object' || Array.isArray(query)) {
152
- return { $and: [] };
153
- }
154
-
155
- const exprToParse = query.$expr || query;
156
- const parsedRoot = parseNode(exprToParse);
157
-
158
- if (!parsedRoot) return { $and: [] };
159
- if (parsedRoot.$and || parsedRoot.$or) return parsedRoot;
160
-
161
- return { $and: [parsedRoot] };
162
- };
163
-
164
- export const pagedFilterToMongoConds = (pagedFilters, model) => {
165
- const modelFilter = pagedFilters?.[model.name] || {};
166
- const filterKeys = Object.keys(modelFilter);
167
-
168
- // Si le filtre est vide, on retourne un tableau vide.
169
- if (filterKeys.length === 0) {
170
- return [];
171
- }
172
-
173
- // Détecte si c'est un filtre avancé (contient des opérateurs logiques de haut niveau comme $and, $or, etc.)
174
- const isAdvancedFilter = filterKeys.some(key => key.startsWith('$'));
175
-
176
- if (isAdvancedFilter) {
177
- // C'est un filtre avancé du ConditionBuilder.
178
- // Il est déjà une condition MongoDB complète. On le met dans un tableau pour l'insérer dans le $and global.
179
- // Si le filtre avancé est vide (ex: { $and: [] }), on le retourne quand même pour que le ConditionBuilder puisse s'initialiser correctement.
180
- return [modelFilter];
181
- } else {
182
- // C'est un filtre simple (par colonne).
183
- // On transforme { champ1: cond1, champ2: cond2 } en [{ champ1: cond1 }, { champ2: cond2 }]
184
- const c = [];
185
- filterKeys.forEach(fieldName => {
186
- if (model.fields.some(f => f.name === fieldName)) {
187
- const condition = modelFilter[fieldName];
188
- if (condition && typeof condition === 'object' && Object.keys(condition).length > 0) {
189
- c.push(condition);
190
- }
191
- }
192
- });
193
- return c;
194
- }
195
- };
196
-
197
- /**
198
- * Remplace les placeholders dans un objet filtre.
199
- * Gère {{userId}}.
200
- */
201
- export const processFilterPlaceholders = (filter, user) => {
202
- const processedFilter = JSON.parse(JSON.stringify(filter));
203
- for (const key in processedFilter) {
204
- if (processedFilter[key] === '{{userId}}') {
205
- // Dans le système Primals, le champ utilisateur est souvent `_user`
206
- // et contient le nom d'utilisateur, pas l'ID. Adaptez si besoin.
207
- processedFilter[key] = user.username;
208
- } else if (typeof processedFilter[key] === 'object' && processedFilter[key] !== null) {
209
- processedFilter[key] = processFilterPlaceholders(processedFilter[key], user);
210
- }
211
- }
212
- return processedFilter;
1
+ /**
2
+ * Finds the matching {{/each}} for a {{#each ...}} tag at a given position.
3
+ * It correctly handles nested {{#each}} blocks.
4
+ * @param {string} template - The template string.
5
+ * @param {number} startIndex - The starting position of the opening {{#each ...}} tag.
6
+ * @returns {number} The position of the start of the matching {{/each}} tag, or -1 if not found.
7
+ */
8
+ function findMatchingEndEach(template, startIndex) {
9
+ const openTag = '{{#each';
10
+ const closeTag = '{{/each}}';
11
+ let level = 1;
12
+ let searchPos = startIndex + openTag.length;
13
+
14
+ while (level > 0 && searchPos < template.length) {
15
+ const nextClose = template.indexOf(closeTag, searchPos);
16
+ if (nextClose === -1) {
17
+ return -1; // Unmatched opening tag
18
+ }
19
+
20
+ const nextOpen = template.indexOf(openTag, searchPos);
21
+
22
+ if (nextOpen !== -1 && nextOpen < nextClose) {
23
+ level++;
24
+ searchPos = nextOpen + openTag.length;
25
+ } else {
26
+ level--;
27
+ searchPos = nextClose + closeTag.length;
28
+ if (level === 0) {
29
+ return nextClose; // Found the matching closing tag
30
+ }
31
+ }
32
+ }
33
+ return -1; // No matching closing tag found
34
+ }
35
+
36
+ /**
37
+ * Renders a simple HTML template by substituting placeholders.
38
+ * This is a robust version that correctly handles nested {{#each}} loops.
39
+ *
40
+ * @param {string} templateString The template with placeholders.
41
+ * @param {Array<object>|object} data The data to inject.
42
+ * @returns {string} The rendered HTML string.
43
+ */
44
+
45
+ /**
46
+ * Renders a simple HTML template by substituting placeholders.
47
+ * This is a robust version that correctly handles nested {{#each}} loops.
48
+ *
49
+ * @param {string} templateString The template with placeholders.
50
+ * @param {Array<object>|object} data The data to inject.
51
+ * @returns {string} The rendered HTML string.
52
+ */
53
+ export const convertInputValue = (value) => {
54
+ if (typeof value !== 'string') return value;
55
+
56
+ if (value.startsWith('$')) {
57
+ // C'est une référence de champ
58
+ return value;
59
+ }
60
+
61
+ // Si la valeur est une chaîne entourée de guillemets, on la conserve telle quelle
62
+ if (/^['"].*['"]$/.test(value)) {
63
+ return value.slice(1, -1); // Retire les guillemets
64
+ }
65
+
66
+ // Conversion en nombre si possible
67
+ if (!isNaN(value) && value.trim() !== '') {
68
+ return Number(value);
69
+ }
70
+
71
+ if (value.toLowerCase() === 'null') return null;
72
+
73
+ // Conversion en booléen pour 'true' et 'false'
74
+ if (value.toLowerCase() === 'true') return true;
75
+ if (value.toLowerCase() === 'false') return false;
76
+
77
+ // Par défaut, retourne la chaîne telle quelle
78
+ return value;
79
+ };
80
+
81
+ const parseNode = (node, pathPrefix = '$') => {
82
+ if (node && node.path && Array.isArray(node.path)) {
83
+ return node;
84
+ }
85
+
86
+ if (!node || typeof node !== 'object' || Array.isArray(node)) {
87
+ return null;
88
+ }
89
+
90
+ const keys = Object.keys(node);
91
+ if (keys.length === 0) return null;
92
+
93
+ // Gestion des $find
94
+ if (keys.length === 1 && keys[0] === '$find') {
95
+ const findCondition = node.$find;
96
+ const parsedCondition = parseNode(findCondition, pathPrefix === '$' ? '$$this.' : pathPrefix);
97
+
98
+ if (parsedCondition) {
99
+ return {
100
+ ...parsedCondition,
101
+ path: parsedCondition.path || [],
102
+ op: '$find',
103
+ value: parsedCondition
104
+ };
105
+ }
106
+ }
107
+
108
+ // Gestion des opérateurs logiques
109
+ if (keys.length === 1 && (keys[0] === '$and' || keys[0] === '$or')) {
110
+ const children = node[keys[0]]
111
+ .map(childNode => parseNode(childNode, pathPrefix))
112
+ .filter(Boolean);
113
+ return { [keys[0]]: children };
114
+ }
115
+
116
+ // Gestion des opérateurs de comparaison
117
+ if (keys[0].startsWith('$')) {
118
+ const op = keys[0];
119
+ const args = node[op];
120
+
121
+ // Cas spécial pour regexMatch
122
+ if (op === '$regexMatch' && typeof args === 'object') {
123
+ const transformExpr = reverseTransformExpr(args.input, pathPrefix === '$$this.');
124
+ return {
125
+ path: transformExpr.path || [args.input.substring(pathPrefix.length).split('.')],
126
+ op: '$regex',
127
+ value: args.regex,
128
+ transform: transformExpr.transform
129
+ };
130
+ }
131
+
132
+ // Cas spécial pour exists
133
+ if ((op === '$ne' || op === '$eq') && Array.isArray(args) && args.length === 2 &&
134
+ typeof args[0] === 'object' && args[0].$type && args[1] === 'missing') {
135
+ const transformExpr = reverseTransformExpr(args[0].$type, pathPrefix === '$$this.');
136
+ return {
137
+ path: transformExpr.path || [args[0].$type.substring(pathPrefix.length).split('.')],
138
+ op: '$exists',
139
+ value: op === '$ne',
140
+ transform: transformExpr.transform
141
+ };
142
+ }
143
+
144
+ // Cas normal avec transformation
145
+ if (Array.isArray(args) && args.length === 2) {
146
+ const transformExpr = reverseTransformExpr(args[0], pathPrefix === '$$this.');
147
+ return {
148
+ path: transformExpr.path || [args[0].substring(pathPrefix.length).split('.')],
149
+ op,
150
+ value: args[1],
151
+ transform: transformExpr.transform
152
+ };
153
+ }
154
+
155
+ // Cas des transformations pures
156
+ const transformExpr = reverseTransformExpr(node, pathPrefix === '$$this.');
157
+ if (transformExpr && transformExpr.transform) {
158
+ return {
159
+ path: transformExpr.path || [keys[0].substring(1)],
160
+ transform: transformExpr.transform
161
+ };
162
+ }
163
+ }
164
+
165
+ // Gestion des champs simples
166
+ const path = keys[0];
167
+ if (typeof path === 'string' && !path.startsWith('$')) {
168
+ const condition = node[path];
169
+ if (typeof condition === 'object' && condition !== null && !Array.isArray(condition)) {
170
+ if (condition.$find) {
171
+ const parsedInnerNode = parseNode(condition.$find, '$$this.');
172
+ if (parsedInnerNode) {
173
+ return {
174
+ ...parsedInnerNode,
175
+ path: [path, ...(parsedInnerNode.path || [])]
176
+ };
177
+ }
178
+ } else {
179
+ const parsedCondition = parseNode(condition, pathPrefix);
180
+ if (parsedCondition) {
181
+ return {
182
+ ...parsedCondition,
183
+ path: [path, ...(parsedCondition.path || [])]
184
+ };
185
+ }
186
+ }
187
+ } else {
188
+ return {
189
+ path: [path],
190
+ op: '$eq',
191
+ value: condition
192
+ };
193
+ }
194
+ }
195
+
196
+ return null;
197
+ };
198
+
199
+
200
+ export const buildRootFromExpr = (query) => {
201
+ if (!query || typeof query !== 'object' || Array.isArray(query)) {
202
+ return { $and: [] };
203
+ }
204
+
205
+ const exprToParse = query.$expr || query;
206
+ const parsedRoot = parseNode(exprToParse);
207
+
208
+ if (!parsedRoot) return { $and: [] };
209
+ if (parsedRoot.$and || parsedRoot.$or) return parsedRoot;
210
+
211
+ return { $and: [parsedRoot] };
212
+ };
213
+
214
+ export const pagedFilterToMongoConds = (pagedFilters, model) => {
215
+ const modelFilter = pagedFilters?.[model.name] || {};
216
+ const filterKeys = Object.keys(modelFilter);
217
+
218
+ // Si le filtre est vide, on retourne un tableau vide.
219
+ if (filterKeys.length === 0) {
220
+ return [];
221
+ }
222
+
223
+ // Détecte si c'est un filtre avancé (contient des opérateurs logiques de haut niveau comme $and, $or, etc.)
224
+ const isAdvancedFilter = filterKeys.some(key => key.startsWith('$'));
225
+
226
+ if (isAdvancedFilter) {
227
+ // C'est un filtre avancé du ConditionBuilder.
228
+ // Il est déjà une condition MongoDB complète. On le met dans un tableau pour l'insérer dans le $and global.
229
+ // Si le filtre avancé est vide (ex: { $and: [] }), on le retourne quand même pour que le ConditionBuilder puisse s'initialiser correctement.
230
+ return [modelFilter];
231
+ } else {
232
+ // C'est un filtre simple (par colonne).
233
+ // On transforme { champ1: cond1, champ2: cond2 } en [{ champ1: cond1 }, { champ2: cond2 }]
234
+ const c = [];
235
+ filterKeys.forEach(fieldName => {
236
+ if (model.fields.some(f => f.name === fieldName)) {
237
+ const condition = modelFilter[fieldName];
238
+ if (condition && typeof condition === 'object' && Object.keys(condition).length > 0) {
239
+ c.push(condition);
240
+ }
241
+ }
242
+ });
243
+ return c;
244
+ }
245
+ };
246
+
247
+ /**
248
+ * Remplace les placeholders dans un objet filtre.
249
+ * Gère {{userId}}.
250
+ */
251
+ export const processFilterPlaceholders = (filter, user) => {
252
+ const processedFilter = JSON.parse(JSON.stringify(filter));
253
+ for (const key in processedFilter) {
254
+ if (processedFilter[key] === '{{userId}}') {
255
+ // Dans le système Primals, le champ utilisateur est souvent `_user`
256
+ // et contient le nom d'utilisateur, pas l'ID. Adaptez si besoin.
257
+ processedFilter[key] = user.username;
258
+ } else if (typeof processedFilter[key] === 'object' && processedFilter[key] !== null) {
259
+ processedFilter[key] = processFilterPlaceholders(processedFilter[key], user);
260
+ }
261
+ }
262
+ return processedFilter;
213
263
  };