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.
- package/README.md +913 -867
- package/client/package-lock.json +49 -0
- package/client/package.json +1 -0
- package/client/src/AddWidgetTypeModal.jsx +47 -43
- package/client/src/App.jsx +3 -7
- package/client/src/App.scss +25 -3
- package/client/src/AssistantChat.jsx +363 -323
- package/client/src/AssistantChat.scss +30 -12
- package/client/src/Dashboard.jsx +480 -396
- package/client/src/Dashboard.scss +1 -1
- package/client/src/DashboardHtmlViewItem.jsx +147 -0
- package/client/src/DashboardView.jsx +104 -19
- package/client/src/DataEditor.jsx +12 -5
- package/client/src/DataLayout.jsx +805 -762
- package/client/src/DataLayout.scss +14 -0
- package/client/src/DataTable.jsx +63 -77
- package/client/src/Dialog.scss +1 -1
- package/client/src/Field.jsx +591 -322
- package/client/src/FlexDataRenderer.jsx +2 -0
- package/client/src/FlexTreeUtils.js +1 -1
- package/client/src/FlexViewCard.jsx +44 -0
- package/client/src/HistoryDialog.jsx +47 -14
- package/client/src/HtmlViewBuilderModal.jsx +91 -0
- package/client/src/HtmlViewBuilderModal.scss +18 -0
- package/client/src/HtmlViewCard.jsx +44 -0
- package/client/src/HtmlViewCard.scss +35 -0
- package/client/src/KPIDialog.jsx +11 -1
- package/client/src/KanbanCard.jsx +1 -2
- package/client/src/ModelCreator.jsx +6 -6
- package/client/src/ModelCreatorField.jsx +74 -31
- package/client/src/ModelList.jsx +93 -54
- package/client/src/Notification.jsx +136 -136
- package/client/src/Notification.scss +0 -18
- package/client/src/Pagination.jsx +5 -3
- package/client/src/RelationField.jsx +354 -258
- package/client/src/RelationSelectorWidget.jsx +173 -0
- package/client/src/constants.js +1 -1
- package/client/src/contexts/ModelContext.jsx +10 -1
- package/client/src/contexts/UIContext.jsx +72 -63
- package/client/src/filter.js +262 -212
- package/client/src/hooks/useTutorials.jsx +62 -65
- package/client/src/hooks/useValidation.js +75 -0
- package/client/src/translations.js +26 -24
- package/package.json +3 -1
- package/perf/README.md +147 -0
- package/perf/artillery-hooks.js +37 -0
- package/perf/perf-shot-hardwork.yml +84 -0
- package/perf/perf-shot-search.yml +45 -0
- package/perf/setup.yml +26 -0
- package/server.js +1 -1
- package/src/constants.js +3 -28
- package/src/core.js +15 -1
- package/src/data.js +1 -1
- package/src/defaultModels.js +63 -7
- package/src/email.js +5 -2
- package/src/engine.js +5 -3
- package/src/filter.js +5 -3
- package/src/i18n.js +710 -10
- package/src/modules/assistant/assistant.js +151 -19
- package/src/modules/bucket.js +14 -16
- package/src/modules/data/data.backup.js +11 -8
- package/src/modules/data/data.core.js +118 -92
- package/src/modules/data/data.history.js +531 -492
- package/src/modules/data/data.js +9 -56
- package/src/modules/data/data.operations.js +3282 -2999
- package/src/modules/data/data.relations.js +686 -686
- package/src/modules/data/data.routes.js +118 -24
- package/src/modules/data/data.scheduling.js +2 -1
- package/src/modules/data/data.validation.js +85 -3
- package/src/modules/file.js +247 -236
- package/src/modules/user.js +4 -1
- package/src/modules/workflow.js +9 -10
- package/src/openai.jobs.js +2 -0
- package/src/packs.js +5482 -5461
- package/src/providers.js +22 -7
- package/test/data.integration.test.js +136 -2
- 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 (
|
|
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 (
|
|
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' &&
|
|
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/
|
|
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 "
|
|
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";
|