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
package/src/providers.js CHANGED
@@ -2,6 +2,7 @@ import {compare, hash} from "bcrypt";
2
2
  import {maxTotalPrivateFilesSize} from "./constants.js";
3
3
  import {getCollection, ObjectId} from "./modules/mongodb.js";
4
4
  import {Logger} from "./gameObject.js";
5
+ import {Config} from "./config.js";
5
6
 
6
7
  /**
7
8
  * @class UserProvider
@@ -73,7 +74,7 @@ export class UserProvider {
73
74
  * @returns {Promise<number>}
74
75
  */
75
76
  async getUserStorageLimit(user) {
76
- return maxTotalPrivateFilesSize;
77
+ return Config.Get('maxTotalPrivateFilesSize', maxTotalPrivateFilesSize);
77
78
  }
78
79
 
79
80
  /**
@@ -112,23 +113,23 @@ export class UserProvider {
112
113
 
113
114
  export class DefaultUserProvider extends UserProvider {
114
115
 
115
- users= [{ username: "demo", password: "demo" }];
116
+ users= [{ username: "demo", password: "demo", temporary: true }];
116
117
 
117
118
  /**
118
119
  * Trouve un utilisateur. Gère spécifiquement les utilisateurs de démo.
119
120
  */
120
121
  async findUserByUsername(username) {
121
122
  // Si le nom d'utilisateur commence par "demo", on le considère comme un utilisateur de démo valide et volatile.
122
- if (typeof username === 'string' && username.startsWith('demo')) {
123
+ if (this.isDemoUser(username)){
123
124
  // On retourne un objet utilisateur de démo avec la structure attendue.
124
- return { username: username, password: "demo", userPlan: 'free' }; // Ajout de userPlan pour la cohérence
125
+ return { username: username, password: "demo", userPlan: 'free', temporary: true }; // Ajout de userPlan pour la cohérence
125
126
  }
126
127
  // Logique pour les vrais utilisateurs (si vous en ajoutez plus tard)
127
128
  return this.users.find(user => user.username === username);
128
129
  }
129
130
 
130
131
  async getUserStorageLimit(user) {
131
- return maxTotalPrivateFilesSize;
132
+ return Config.Get('maxTotalPrivateFilesSize', maxTotalPrivateFilesSize);
132
133
  }
133
134
 
134
135
  async getBackupFrequency(user){
@@ -139,9 +140,13 @@ export class DefaultUserProvider extends UserProvider {
139
140
  return []
140
141
  }
141
142
 
143
+ isDemoUser(user) {
144
+ return (typeof (user) === 'string' && (/^demo[0-9]{0,2}$/.test(user) || /^perf[0-9]{1,8}$/.test(user)))
145
+ }
146
+
142
147
  async validatePassword(user, password) {
143
148
  // Pour un utilisateur de démo, le mot de passe est toujours valide.
144
- if (user.username.startsWith('demo')) {
149
+ if (this.isDemoUser(user)){
145
150
  return true;
146
151
  }
147
152
  // Logique pour les vrais utilisateurs
@@ -166,7 +171,7 @@ export class DefaultUserProvider extends UserProvider {
166
171
 
167
172
  // Priorité 2: PasF de session, mais on vérifie la présence d'un cookie "username" pour la démo.
168
173
  const demoUsername = req.cookies?.username;
169
- if (demoUsername && typeof demoUsername === 'string' && demoUsername.startsWith('demo')) {
174
+ if (demoUsername && typeof demoUsername === 'string' && this.isDemoUser(demoUsername)) {
170
175
  // On a trouvé un cookie de démo. On crée l'objet utilisateur correspondant.
171
176
  const demoUser = await this.findUserByUsername(demoUsername);
172
177
  if (demoUser) {
@@ -174,6 +179,16 @@ export class DefaultUserProvider extends UserProvider {
174
179
  return;
175
180
  }
176
181
  }
182
+
183
+ // Priorité 2: PasF de session, mais on vérifie la présence d'un cookie "username" pour la démo.
184
+ const user = req.query._user;
185
+ if (user && typeof user === 'string' && this.isDemoUser(user)) {
186
+ const demoUser = await this.findUserByUsername(user);
187
+ if (demoUser) {
188
+ req.me = demoUser;
189
+ return;
190
+ }
191
+ }
177
192
  }
178
193
 
179
194
  getUserPlans(){
@@ -1,6 +1,9 @@
1
1
  // __tests__/data.integration.test.js
2
2
  import { ObjectId } from 'mongodb';
3
3
  import {vi, expect, describe, it, afterEach,beforeEach, beforeAll, afterAll} from 'vitest';
4
+ import fs from 'fs/promises';
5
+ import path from 'path';
6
+ import os from 'os';
4
7
  import { Config } from '../src/config.js';
5
8
  import {
6
9
  insertData,
@@ -17,6 +20,7 @@ import {
17
20
  import {getRandom} from "../src/core.js";
18
21
  import {generateUniqueName, initEngine} from "../src/setenv.js";
19
22
  import {purgeData} from "../src/modules/data/data.history.js";
23
+ import {removeFile} from "../src/modules/file.js";
20
24
 
21
25
  let testModelsColInstance;
22
26
  let testDatasColInstance;
@@ -88,7 +92,7 @@ async function setupTestContext() {
88
92
  {name: 'relationSingle', type: 'relation', relation: currentRelatedModelName},
89
93
  {name: 'relationMultiple', type: 'relation', relation: currentRelatedModelName, multiple: true},
90
94
  // File (metadata validation)
91
- {name: 'fileField', type: 'file', mimeTypes: ['image/png', 'application/pdf'], maxSize: 1024 * 10}, // 10KB
95
+ {name: 'fileField', type: 'file', mimeTypes: ['image/png', 'application/pdf', 'text/plain'], maxSize: 1024 * 10}, // 10KB
92
96
  // Color
93
97
  {name: 'colorField', type: 'color'},
94
98
  // Code
@@ -285,7 +289,7 @@ describe('Intégration des fonctions CRUD de données avec validation complète'
285
289
  // --- FILE VALIDATIONS (metadata) ---
286
290
  it('devrait rejeter un file avec un mimeType non autorisé', async () => {
287
291
  const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await setupTestContext();
288
- const mockFile = { name: 'test.txt', type: 'text/plain', size: 1024, guid: 'dummy-txt' };
292
+ const mockFile = { name: 'test.txt', type: 'text/csv', size: 1024, guid: 'dummy-txt' };
289
293
  const result = await insertData(comprehensiveTestModelDefinition.name, { stringRequired: 'req', fileField: mockFile }, {}, currentTestUser, false);
290
294
  expect(result.success).toBe(false);
291
295
  expect(result.error).toContain('image/png'); // Ou un message plus spécifique sur mimeTypes
@@ -414,6 +418,57 @@ describe('Intégration des fonctions CRUD de données avec validation complète'
414
418
 
415
419
  await purge();
416
420
  }, 10000); // Augmenter le timeout si nécessaire
421
+
422
+ it('devrait insérer des données avec un nouveau fichier et le sauvegarder', async () => {
423
+ const { currentTestUser, comprehensiveTestModelDefinition, purge } = await setupTestContext();
424
+
425
+ // --- Simuler un vrai fichier uploadé comme le ferait formidable/multer ---
426
+ const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'data-primals-test-'));
427
+ const filePath = path.join(tmpDir, 'test-upload.txt');
428
+ const fileContent = 'Ceci est un vrai test de fichier binaire.';
429
+ await fs.writeFile(filePath, fileContent);
430
+ const stats = await fs.stat(filePath);
431
+
432
+ const uploadedFile = {
433
+ filepath: filePath,
434
+ originalFilename: 'test-upload-on-insert.txt',
435
+ mimetype: 'text/plain',
436
+ size: stats.size,
437
+ lastModifiedDate: new Date(),
438
+ hash: 'test-hash' // Optionnel, mais souvent présent
439
+ };
440
+ // --- Fin de la simulation ---
441
+
442
+ const dataWithFile = {
443
+ stringRequired: 'DocWithNewFile',
444
+ stringUnique: 'UniqueFileValueForInsert',
445
+ passwordField: 'filePass',
446
+ enumField: 'gamma',
447
+ number: 123
448
+ // Le champ 'fileField' est absent ici, il sera fourni dans l'objet 'files'
449
+ };
450
+
451
+ const result = await insertData(comprehensiveTestModelDefinition.name, dataWithFile, { 'fileField[0]': uploadedFile }, currentTestUser, false);
452
+
453
+ expect(result.success, `L'insertion avec un nouveau fichier a échoué: ${result.error}`).toBe(true);
454
+ expect(result.insertedIds).toHaveLength(1);
455
+
456
+ const query = await searchData( { model: comprehensiveTestModelDefinition.name, filter: { _id: new ObjectId(result.insertedIds[0])}}, currentTestUser);
457
+ const insertedDoc = query.data[0];
458
+ expect(insertedDoc).not.toBeNull();
459
+ console.log(insertedDoc.fileField)
460
+ expect(insertedDoc.fileField).toBeInstanceOf(Object);
461
+ expect(insertedDoc.fileField.newFile).toBeUndefined(); // La propriété 'newFile' était une hallucination
462
+ expect(insertedDoc.fileField._id).toBeDefined();
463
+ expect(insertedDoc.fileField.name).toBe('test-upload-on-insert.txt');
464
+ expect(insertedDoc.fileField.guid).toBeDefined();
465
+
466
+ await removeFile(insertedDoc.fileField.guid, currentTestUser);
467
+
468
+ // Nettoyage du répertoire et fichier temporaire
469
+ await fs.rm(tmpDir, { recursive: true, force: true });
470
+ await purge();
471
+ });
417
472
  });
418
473
 
419
474
  describe('editData avec comprehensiveTestModel', async () => {
@@ -679,6 +734,85 @@ describe('Intégration des fonctions CRUD de données avec validation complète'
679
734
  await purge();
680
735
  });
681
736
 
737
+ // --- NOUVEAUX TESTS POUR LE SHORTHAND $find ---
738
+
739
+ it('devrait trouver un document en utilisant un filtre $find simple (shorthand)', async () => {
740
+ const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await initTest();
741
+ const searchParams = {
742
+ model: comprehensiveTestModelDefinition.name,
743
+ filter: {
744
+ relationSingle: {
745
+ "$find": { "relatedValue": 101 } // Utilisation du shorthand { champ: valeur }
746
+ }
747
+ },
748
+ depth: 1
749
+ };
750
+ const { data, count } = await searchData(searchParams, currentTestUser);
751
+ expect(count).toBe(1);
752
+ expect(data).toHaveLength(1);
753
+ expect(data[0]._id.toString()).toBe(docId1.toString());
754
+ await purge();
755
+ });
756
+
757
+ it('devrait trouver un document en utilisant un filtre $find simple avec plusieurs conditions (AND implicite)', async () => {
758
+ const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await initTest();
759
+ const searchParams = {
760
+ model: comprehensiveTestModelDefinition.name,
761
+ filter: {
762
+ relationSingle: {
763
+ "$find": {
764
+ "relatedName": "Rel_Search_A1",
765
+ "relatedValue": 101
766
+ }
767
+ }
768
+ },
769
+ depth: 1
770
+ };
771
+ const { data, count } = await searchData(searchParams, currentTestUser);
772
+ expect(count).toBe(1);
773
+ expect(data).toHaveLength(1);
774
+ expect(data[0]._id.toString()).toBe(docId1.toString());
775
+ await purge();
776
+ });
777
+
778
+ it('ne devrait pas trouver de document si une des conditions du $find simple échoue', async () => {
779
+ const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await initTest();
780
+ const searchParams = {
781
+ model: comprehensiveTestModelDefinition.name,
782
+ filter: {
783
+ relationSingle: {
784
+ "$find": {
785
+ "relatedName": "Rel_Search_A1", // Correspond
786
+ "relatedValue": 999 // Ne correspond pas
787
+ }
788
+ }
789
+ },
790
+ depth: 1
791
+ };
792
+ const { data, count } = await searchData(searchParams, currentTestUser);
793
+ expect(count).toBe(0);
794
+ expect(data).toHaveLength(0);
795
+ await purge();
796
+ });
797
+
798
+ it('devrait trouver un document via une relation multiple en utilisant un filtre $find simple', async () => {
799
+ const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await initTest();
800
+ const searchParams = {
801
+ model: comprehensiveTestModelDefinition.name,
802
+ filter: {
803
+ relationMultiple: {
804
+ "$find": { "relatedValue": 102 } // Devrait trouver docId1 car il a une relation vers relA2
805
+ }
806
+ },
807
+ depth: 1
808
+ };
809
+ const { data, count } = await searchData(searchParams, currentTestUser);
810
+ expect(count).toBe(1);
811
+ expect(data).toHaveLength(1);
812
+ expect(data[0]._id.toString()).toBe(docId1.toString());
813
+ await purge();
814
+ });
815
+
682
816
  });
683
817
 
684
818
  describe('installPack', () => {
@@ -11,7 +11,7 @@ import {
11
11
  modelsCollection as getAppModelsCollection,
12
12
  getCollectionForUser as getAppUserCollection, getCollectionForUser
13
13
  } from '../src/modules/mongodb.js';
14
- import {sleep} from "data-primals-engine/core";
14
+ import {sleep} from "../src/core.js";
15
15
  import fs from "node:fs";
16
16
  import {initEngine} from "../src/setenv.js";
17
17
  import {Config} from "../src/index.js";