data-primals-engine 1.5.2 → 1.6.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 (70) hide show
  1. package/README.md +938 -915
  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 +6 -0
  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 -808
  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 -307
  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/ModelCreator.jsx +683 -686
  26. package/client/src/ModelCreator.scss +1 -1
  27. package/client/src/ModelCreatorField.jsx +950 -950
  28. package/client/src/ModelImporter.jsx +3 -0
  29. package/client/src/ModelList.jsx +280 -280
  30. package/client/src/Notification.jsx +136 -136
  31. package/client/src/PackGallery.jsx +391 -391
  32. package/client/src/PackGallery.scss +231 -231
  33. package/client/src/Pagination.jsx +143 -143
  34. package/client/src/RelationField.jsx +354 -354
  35. package/client/src/RelationSelectorWidget.jsx +172 -172
  36. package/client/src/RelationValue.jsx +2 -1
  37. package/client/src/contexts/CommandContext.jsx +260 -0
  38. package/client/src/contexts/ModelContext.jsx +4 -1
  39. package/client/src/contexts/UIContext.jsx +72 -72
  40. package/client/src/filter.js +263 -263
  41. package/client/src/index.css +90 -90
  42. package/package.json +6 -5
  43. package/perf/artillery-hooks.js +37 -37
  44. package/perf/setup.yml +25 -25
  45. package/src/constants.js +4 -0
  46. package/src/defaultModels.js +7 -0
  47. package/src/engine.js +335 -335
  48. package/src/events.js +232 -137
  49. package/src/filter.js +274 -274
  50. package/src/index.js +1 -0
  51. package/src/modules/assistant/assistant.js +225 -83
  52. package/src/modules/auth-google/index.js +50 -50
  53. package/src/modules/auth-microsoft/index.js +81 -81
  54. package/src/modules/data/data.core.js +118 -118
  55. package/src/modules/data/data.history.js +555 -555
  56. package/src/modules/data/data.operations.js +112 -9
  57. package/src/modules/data/data.relations.js +686 -686
  58. package/src/modules/data/data.routes.js +1879 -1879
  59. package/src/modules/data/data.validation.js +2 -2
  60. package/src/modules/file.js +247 -247
  61. package/src/modules/user.js +14 -9
  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/test/data.history.integration.test.js +264 -264
  67. package/test/data.integration.test.js +1281 -1206
  68. package/test/events.test.js +108 -10
  69. package/test/model.integration.test.js +377 -377
  70. package/test/user.test.js +106 -1
@@ -1,264 +1,264 @@
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;
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;
264
264
  };