data-primals-engine 1.5.1 → 1.6.0

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