data-primals-engine 1.7.1 → 1.7.3
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 +9 -9
- package/client/package-lock.json +8430 -8121
- package/client/package.json +7 -4
- package/client/src/APIInfo.jsx +1 -1
- package/client/src/App.jsx +2 -1
- package/client/src/App.scss +1635 -1626
- package/client/src/AssistantChat.jsx +2 -3
- package/client/src/CalendarView.jsx +1 -0
- package/client/src/ContentView.jsx +3 -3
- package/client/src/Dashboard.jsx +5 -2
- package/client/src/DashboardChart.jsx +1 -0
- package/client/src/DashboardFlexViewItem.jsx +1 -0
- package/client/src/DashboardHtmlViewItem.jsx +1 -0
- package/client/src/DashboardView.jsx +2 -0
- package/client/src/DataEditor.jsx +0 -1
- package/client/src/DataImporter.jsx +489 -468
- package/client/src/DataLayout.jsx +25 -23
- package/client/src/DataTable.jsx +6 -5
- package/client/src/Dialog.jsx +92 -90
- package/client/src/Dialog.scss +122 -116
- package/client/src/DisplayFlexNodeRenderer.jsx +1 -0
- package/client/src/DocumentationPageLayout.scss +1 -1
- package/client/src/FlexBuilderControls.jsx +1 -0
- package/client/src/FlexBuilderPreview.jsx +1 -1
- package/client/src/FlexNode.jsx +1 -1
- package/client/src/HistoryDialog.jsx +3 -2
- package/client/src/KPIWidget.jsx +1 -1
- package/client/src/KanbanView.jsx +1 -0
- package/client/src/ModelCreator.jsx +4 -0
- package/client/src/ModelList.jsx +1 -0
- package/client/src/PackGallery.jsx +5 -4
- package/client/src/RTETrans.jsx +1 -1
- package/client/src/RelationField.jsx +2 -0
- package/client/src/RelationSelectorWidget.jsx +2 -0
- package/client/src/RelationValue.jsx +1 -0
- package/client/src/RestoreDialog.jsx +1 -0
- package/client/src/ViewSwitcher.jsx +1 -1
- package/client/src/WorkflowEditor.jsx +3 -0
- package/client/src/contexts/CommandContext.jsx +2 -1
- package/client/src/contexts/ModelContext.jsx +3 -3
- package/client/src/hooks/data.js +1 -0
- package/client/src/hooks/useValidation.js +1 -0
- package/client/src/translations.js +24 -24
- package/client/vite.config.js +31 -30
- package/doc/AI-assistance.md +87 -63
- package/doc/Concepts.md +122 -0
- package/doc/Custom-Endpoints.md +31 -0
- package/doc/Event-system.md +13 -14
- package/doc/Home.md +33 -0
- package/doc/Modules.md +83 -0
- package/doc/Packs-gallery.md +8 -23
- package/doc/Workflows.md +32 -0
- package/doc/automation-workflows.md +141 -102
- package/doc/dashboards-kpis-charts.md +47 -49
- package/doc/data-management.md +126 -120
- package/doc/data-models.md +68 -75
- package/doc/roles-permissions.md +144 -43
- package/doc/sharding-replication.md +158 -0
- package/doc/users.md +54 -30
- package/package.json +27 -17
- package/server.js +37 -37
- package/src/ai.jobs.js +135 -0
- package/src/constants.js +560 -545
- package/src/data.js +521 -518
- package/src/email.js +157 -154
- package/src/engine.js +167 -49
- package/src/gameObject.js +6 -0
- package/src/i18n.js +0 -1
- package/src/modules/assistant/assistant.js +782 -763
- package/src/modules/assistant/constants.js +23 -16
- package/src/modules/assistant/providers.js +77 -37
- package/src/modules/auth-google/index.js +53 -50
- package/src/modules/bucket.js +346 -335
- package/src/modules/data/data.backup.js +400 -376
- package/src/modules/data/data.cluster.js +559 -0
- package/src/modules/data/data.core.js +11 -8
- package/src/modules/data/data.js +311 -311
- package/src/modules/data/data.operations.js +3666 -3555
- package/src/modules/data/data.relations.js +1 -0
- package/src/modules/data/data.replication.js +125 -0
- package/src/modules/data/data.routes.js +2206 -1879
- package/src/modules/data/data.scheduling.js +2 -1
- package/src/modules/file.js +248 -247
- package/src/modules/mongodb.js +76 -73
- package/src/modules/user.js +18 -2
- package/src/modules/worker-script-runner.js +97 -0
- package/src/modules/workflow.js +177 -52
- package/src/packs.js +5701 -5701
- package/src/providers.js +298 -297
- package/src/sso.js +91 -25
- package/test/assistant.test.js +207 -206
- package/test/cluster.test.js +221 -0
- package/test/core.test.js +0 -2
- package/test/data.integration.test.js +1425 -1416
- package/test/import_export.integration.test.js +210 -210
- package/test/replication.test.js +163 -0
- package/test/workflow.actions.integration.test.js +487 -475
- package/test/workflow.integration.test.js +332 -329
- package/doc/core-concepts.md +0 -33
|
@@ -1,211 +1,211 @@
|
|
|
1
|
-
import {expect, describe, it, beforeEach, beforeAll, afterAll, vi, afterEach} from 'vitest';
|
|
2
|
-
|
|
3
|
-
// --- Importations des modules de votre application ---
|
|
4
|
-
import {
|
|
5
|
-
insertData,
|
|
6
|
-
exportData,
|
|
7
|
-
importData
|
|
8
|
-
} from '../src/index.js';
|
|
9
|
-
|
|
10
|
-
import {
|
|
11
|
-
modelsCollection as getAppModelsCollection,
|
|
12
|
-
getCollectionForUser as getAppUserCollection, getCollectionForUser
|
|
13
|
-
} from '../src/modules/mongodb.js';
|
|
14
|
-
import {sleep} from "../src/core.js";
|
|
15
|
-
import fs from "node:fs";
|
|
16
|
-
import {initEngine} from "../src/setenv.js";
|
|
17
|
-
import {Config} from "../src/index.js";
|
|
18
|
-
|
|
19
|
-
// --- Données Mock ---
|
|
20
|
-
const mockUser = {
|
|
21
|
-
username: 'testuserImpex',
|
|
22
|
-
_user: 'testuserImpex',
|
|
23
|
-
userPlan: 'premium',
|
|
24
|
-
email: 'testImpex@example.com'
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
const impexTestModel = {
|
|
28
|
-
name: 'impex_products',
|
|
29
|
-
description: 'test',
|
|
30
|
-
_user: mockUser.username,
|
|
31
|
-
fields: [
|
|
32
|
-
{ name: 'name', type: 'string', required: true },
|
|
33
|
-
{ name: 'sku', type: 'string', unique: true },
|
|
34
|
-
{ name: 'price', type: 'number', required: true },
|
|
35
|
-
{ name: 'inStock', type: 'boolean', default: true }
|
|
36
|
-
]
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
// --- Setup de l'environnement de test ---
|
|
40
|
-
let testModelsColInstance;
|
|
41
|
-
let testDatasColInstance;
|
|
42
|
-
|
|
43
|
-
function blobToFile(theBlob, fileName){
|
|
44
|
-
//A Blob() is almost a File() - it's just missing the two properties below which we will add
|
|
45
|
-
theBlob.lastModifiedDate = new Date();
|
|
46
|
-
theBlob.name = fileName;
|
|
47
|
-
return theBlob;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
beforeAll(async () =>{
|
|
51
|
-
Config.Set("modules", ["mongodb", "data", "file", "bucket", "workflow","user", "assistant"]);
|
|
52
|
-
await initEngine();
|
|
53
|
-
})
|
|
54
|
-
|
|
55
|
-
beforeEach(async() =>{
|
|
56
|
-
// tell vitest we use mocked time
|
|
57
|
-
vi.useFakeTimers({ shouldAdvanceTime: true })
|
|
58
|
-
})
|
|
59
|
-
|
|
60
|
-
afterEach(() => {
|
|
61
|
-
vi.runOnlyPendingTimers();
|
|
62
|
-
// restoring date after each test run
|
|
63
|
-
vi.useRealTimers()
|
|
64
|
-
})
|
|
65
|
-
afterAll(async () => {
|
|
66
|
-
const coll = await getCollectionForUser(mockUser);
|
|
67
|
-
await coll.drop();
|
|
68
|
-
})
|
|
69
|
-
// --- Début des tests ---
|
|
70
|
-
describe('Intégration des fonctions d\'Import/Export', () => {
|
|
71
|
-
|
|
72
|
-
// Préparation avant chaque test du bloc
|
|
73
|
-
beforeEach(async () => {
|
|
74
|
-
testModelsColInstance = getAppModelsCollection;
|
|
75
|
-
testDatasColInstance = await getAppUserCollection(mockUser);
|
|
76
|
-
|
|
77
|
-
// Nettoyage complet pour un état propre
|
|
78
|
-
await testDatasColInstance.deleteMany({ _user: "testuserImpex"});
|
|
79
|
-
|
|
80
|
-
if( await testModelsColInstance.find({ name: impexTestModel.name, _user: mockUser.username }).count() === 0) {
|
|
81
|
-
await testModelsColInstance.insertOne(impexTestModel);
|
|
82
|
-
}
|
|
83
|
-
// Insérer des données de base pour les tests d'exportation
|
|
84
|
-
await insertData(impexTestModel.name, [
|
|
85
|
-
{ name: 'Produit A', sku: 'SKU-A', price: 10.50, inStock: true },
|
|
86
|
-
{ name: 'Produit B', sku: 'SKU-B', price: 25.00, inStock: false },
|
|
87
|
-
{ name: 'Produit C', sku: 'SKU-C', price: 99.99, inStock: true }
|
|
88
|
-
], {}, mockUser, false);
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
describe('Export de données', () => {
|
|
92
|
-
it('devrait exporter les données en format JSON', async () => {
|
|
93
|
-
|
|
94
|
-
const res= await exportData({
|
|
95
|
-
models: [impexTestModel.name],
|
|
96
|
-
depth: 1
|
|
97
|
-
}, mockUser);
|
|
98
|
-
|
|
99
|
-
expect(res.success).toBeTruthy();
|
|
100
|
-
|
|
101
|
-
const data = res.data[impexTestModel.name];
|
|
102
|
-
expect(data).toBeInstanceOf(Array);
|
|
103
|
-
expect(data).toHaveLength(3);
|
|
104
|
-
expect(data[0]).toMatchObject({ name: 'Produit A', sku: 'SKU-A', price: 10.50 });
|
|
105
|
-
// Les champs système (_id, _model, _user, _hash) ne devraient pas être exportés par défaut
|
|
106
|
-
expect(data[0]).not.toHaveProperty('_model');
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
it('devrait lever une exception si le modèle n\'existe pas', async () => {
|
|
110
|
-
// La fonction devrait rejeter la promesse ou lancer une erreur
|
|
111
|
-
await expect((await exportData('model_inexistant', 'json', mockUser)).success).toBeFalsy();
|
|
112
|
-
});
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
describe('Import de données', () => {
|
|
116
|
-
it('devrait importer des données depuis une chaîne JSON', async () => {
|
|
117
|
-
|
|
118
|
-
const jsonDataToImport = [
|
|
119
|
-
{ name: 'Produit D', sku: 'SKU-D', price: 1.00, inStock: true },
|
|
120
|
-
{ name: 'Produit E', sku: 'SKU-E', price: 2.00 } // inStock utilisera la valeur par défaut
|
|
121
|
-
];
|
|
122
|
-
const jsonString = JSON.stringify(jsonDataToImport);
|
|
123
|
-
|
|
124
|
-
// Exécution de la fonction d'import (hypothétique)x
|
|
125
|
-
var blob = new Blob([jsonString], {type: "application/json"});
|
|
126
|
-
fs.writeFileSync('test.json', jsonString);
|
|
127
|
-
blob.path = 'test.json';
|
|
128
|
-
blob.originalFilename = 'test.json';
|
|
129
|
-
const result = await importData({model: impexTestModel.name}, {file: blobToFile(blob, "test.json")}, mockUser);
|
|
130
|
-
|
|
131
|
-
// Vérifications du résultat de l'opération
|
|
132
|
-
expect(result.success).toBe(true);
|
|
133
|
-
expect(result.
|
|
134
|
-
|
|
135
|
-
await sleep(5000);
|
|
136
|
-
|
|
137
|
-
// Vérification directe en base de données
|
|
138
|
-
const importedDocs = await testDatasColInstance.find({
|
|
139
|
-
_model: impexTestModel.name,
|
|
140
|
-
sku: { $in: ['SKU-D', 'SKU-E'] }
|
|
141
|
-
}).toArray();
|
|
142
|
-
|
|
143
|
-
expect(importedDocs).toHaveLength(2);
|
|
144
|
-
const docD = importedDocs.find(d => d.sku === 'SKU-D');
|
|
145
|
-
const docE = importedDocs.find(d => d.sku === 'SKU-E');
|
|
146
|
-
|
|
147
|
-
expect(docD.name).toBe('Produit D');
|
|
148
|
-
expect(docD.inStock).toBe(true); // Vérification de la valeur par défaut
|
|
149
|
-
expect(docE.price).toBe(2.00);
|
|
150
|
-
|
|
151
|
-
}, 20000);
|
|
152
|
-
|
|
153
|
-
it('devrait importer des données depuis une chaîne CSV et convertir les types', async () => {
|
|
154
|
-
const csvStringToImport = `name,sku,price,inStock\nProduit F,SKU-F,3.55,true\nProduit G,SKU-G,4.99,false`;
|
|
155
|
-
|
|
156
|
-
// Exécution de la fonction d'import (hypothétique)x
|
|
157
|
-
var blob = new Blob([csvStringToImport], {type: "text/csv"});
|
|
158
|
-
fs.writeFileSync('test.csv', csvStringToImport);
|
|
159
|
-
blob.path = 'test.csv';
|
|
160
|
-
blob.originalFilename = 'test.csv';
|
|
161
|
-
|
|
162
|
-
// Exécution de la fonction d'import
|
|
163
|
-
const result = await importData({model:impexTestModel.name}, {file: blobToFile(blob, "test.csv")}, mockUser);
|
|
164
|
-
|
|
165
|
-
// Vérifications du résultat
|
|
166
|
-
expect(result.success).toBe(true);
|
|
167
|
-
expect(result.
|
|
168
|
-
|
|
169
|
-
await sleep(5000);
|
|
170
|
-
// Vérification en base de données
|
|
171
|
-
const importedDocs = await testDatasColInstance.find({
|
|
172
|
-
_model: impexTestModel.name,
|
|
173
|
-
sku: { $in: ['SKU-F', 'SKU-G'] }
|
|
174
|
-
}).toArray();
|
|
175
|
-
|
|
176
|
-
expect(importedDocs).toHaveLength(2);
|
|
177
|
-
const docF = importedDocs.find(d => d.sku === 'SKU-F');
|
|
178
|
-
const docG = importedDocs.find(d => d.sku === 'SKU-G');
|
|
179
|
-
|
|
180
|
-
// Vérification de la conversion des types
|
|
181
|
-
expect(docF.price).toBe(3.55);
|
|
182
|
-
expect(docF.inStock).toBe(true);
|
|
183
|
-
expect(docG.inStock).toBe(false);
|
|
184
|
-
}, 20000);
|
|
185
|
-
|
|
186
|
-
it('devrait rejeter les lignes invalides et rapporter les erreurs', async () => {
|
|
187
|
-
const csvStringToImport = `name,sku,price,inStock
|
|
188
|
-
Valide H,SKU-H,10,true
|
|
189
|
-
,SKU-I,20,true
|
|
190
|
-
Valide J,SKU-J,,false
|
|
191
|
-
Valide K,SKU-A,40,true`;
|
|
192
|
-
|
|
193
|
-
// Exécution de la fonction d'import (hypothétique)x
|
|
194
|
-
const blob = new Blob([csvStringToImport], {type: "text/csv"});
|
|
195
|
-
fs.writeFileSync('test.csv', csvStringToImport);
|
|
196
|
-
blob.path = 'test.csv';
|
|
197
|
-
blob.originalFilename = 'test.csv';
|
|
198
|
-
|
|
199
|
-
const result = await importData({ model: impexTestModel.name }, {file: blobToFile(blob,"test.csv")}, mockUser);
|
|
200
|
-
|
|
201
|
-
// L'initiation du job doit réussir
|
|
202
|
-
expect(result.success).toBe(true);
|
|
203
|
-
expect(result.
|
|
204
|
-
|
|
205
|
-
await sleep(5000);
|
|
206
|
-
// Vérifier que seule les données valides sont en BDD
|
|
207
|
-
const count = await testDatasColInstance.countDocuments({ _model: impexTestModel.name, sku: 'SKU-H' });
|
|
208
|
-
expect(count).toBe(1); // Seule la ligne valide 'SKU-H' doit être insérée.
|
|
209
|
-
}, 20000);
|
|
210
|
-
});
|
|
1
|
+
import {expect, describe, it, beforeEach, beforeAll, afterAll, vi, afterEach} from 'vitest';
|
|
2
|
+
|
|
3
|
+
// --- Importations des modules de votre application ---
|
|
4
|
+
import {
|
|
5
|
+
insertData,
|
|
6
|
+
exportData,
|
|
7
|
+
importData
|
|
8
|
+
} from '../src/index.js';
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
modelsCollection as getAppModelsCollection,
|
|
12
|
+
getCollectionForUser as getAppUserCollection, getCollectionForUser
|
|
13
|
+
} from '../src/modules/mongodb.js';
|
|
14
|
+
import {sleep} from "../src/core.js";
|
|
15
|
+
import fs from "node:fs";
|
|
16
|
+
import {initEngine} from "../src/setenv.js";
|
|
17
|
+
import {Config} from "../src/index.js";
|
|
18
|
+
|
|
19
|
+
// --- Données Mock ---
|
|
20
|
+
const mockUser = {
|
|
21
|
+
username: 'testuserImpex',
|
|
22
|
+
_user: 'testuserImpex',
|
|
23
|
+
userPlan: 'premium',
|
|
24
|
+
email: 'testImpex@example.com'
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const impexTestModel = {
|
|
28
|
+
name: 'impex_products',
|
|
29
|
+
description: 'test',
|
|
30
|
+
_user: mockUser.username,
|
|
31
|
+
fields: [
|
|
32
|
+
{ name: 'name', type: 'string', required: true },
|
|
33
|
+
{ name: 'sku', type: 'string', unique: true },
|
|
34
|
+
{ name: 'price', type: 'number', required: true },
|
|
35
|
+
{ name: 'inStock', type: 'boolean', default: true }
|
|
36
|
+
]
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// --- Setup de l'environnement de test ---
|
|
40
|
+
let testModelsColInstance;
|
|
41
|
+
let testDatasColInstance;
|
|
42
|
+
|
|
43
|
+
function blobToFile(theBlob, fileName){
|
|
44
|
+
//A Blob() is almost a File() - it's just missing the two properties below which we will add
|
|
45
|
+
theBlob.lastModifiedDate = new Date();
|
|
46
|
+
theBlob.name = fileName;
|
|
47
|
+
return theBlob;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
beforeAll(async () =>{
|
|
51
|
+
Config.Set("modules", ["mongodb", "data", "file", "bucket", "workflow","user", "assistant"]);
|
|
52
|
+
await initEngine();
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
beforeEach(async() =>{
|
|
56
|
+
// tell vitest we use mocked time
|
|
57
|
+
vi.useFakeTimers({ shouldAdvanceTime: true })
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
afterEach(() => {
|
|
61
|
+
vi.runOnlyPendingTimers();
|
|
62
|
+
// restoring date after each test run
|
|
63
|
+
vi.useRealTimers()
|
|
64
|
+
})
|
|
65
|
+
afterAll(async () => {
|
|
66
|
+
const coll = await getCollectionForUser(mockUser);
|
|
67
|
+
await coll.drop();
|
|
68
|
+
})
|
|
69
|
+
// --- Début des tests ---
|
|
70
|
+
describe('Intégration des fonctions d\'Import/Export', () => {
|
|
71
|
+
|
|
72
|
+
// Préparation avant chaque test du bloc
|
|
73
|
+
beforeEach(async () => {
|
|
74
|
+
testModelsColInstance = getAppModelsCollection;
|
|
75
|
+
testDatasColInstance = await getAppUserCollection(mockUser);
|
|
76
|
+
|
|
77
|
+
// Nettoyage complet pour un état propre
|
|
78
|
+
await testDatasColInstance.deleteMany({ _user: "testuserImpex"});
|
|
79
|
+
|
|
80
|
+
if( await testModelsColInstance.find({ name: impexTestModel.name, _user: mockUser.username }).count() === 0) {
|
|
81
|
+
await testModelsColInstance.insertOne(impexTestModel);
|
|
82
|
+
}
|
|
83
|
+
// Insérer des données de base pour les tests d'exportation
|
|
84
|
+
await insertData(impexTestModel.name, [
|
|
85
|
+
{ name: 'Produit A', sku: 'SKU-A', price: 10.50, inStock: true },
|
|
86
|
+
{ name: 'Produit B', sku: 'SKU-B', price: 25.00, inStock: false },
|
|
87
|
+
{ name: 'Produit C', sku: 'SKU-C', price: 99.99, inStock: true }
|
|
88
|
+
], {}, mockUser, false);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe('Export de données', () => {
|
|
92
|
+
it('devrait exporter les données en format JSON', async () => {
|
|
93
|
+
|
|
94
|
+
const res= await exportData({
|
|
95
|
+
models: [impexTestModel.name],
|
|
96
|
+
depth: 1
|
|
97
|
+
}, mockUser);
|
|
98
|
+
|
|
99
|
+
expect(res.success).toBeTruthy();
|
|
100
|
+
|
|
101
|
+
const data = res.data[impexTestModel.name];
|
|
102
|
+
expect(data).toBeInstanceOf(Array);
|
|
103
|
+
expect(data).toHaveLength(3);
|
|
104
|
+
expect(data[0]).toMatchObject({ name: 'Produit A', sku: 'SKU-A', price: 10.50 });
|
|
105
|
+
// Les champs système (_id, _model, _user, _hash) ne devraient pas être exportés par défaut
|
|
106
|
+
expect(data[0]).not.toHaveProperty('_model');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('devrait lever une exception si le modèle n\'existe pas', async () => {
|
|
110
|
+
// La fonction devrait rejeter la promesse ou lancer une erreur
|
|
111
|
+
await expect((await exportData('model_inexistant', 'json', mockUser)).success).toBeFalsy();
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
describe('Import de données', () => {
|
|
116
|
+
it('devrait importer des données depuis une chaîne JSON', async () => {
|
|
117
|
+
|
|
118
|
+
const jsonDataToImport = [
|
|
119
|
+
{ name: 'Produit D', sku: 'SKU-D', price: 1.00, inStock: true },
|
|
120
|
+
{ name: 'Produit E', sku: 'SKU-E', price: 2.00 } // inStock utilisera la valeur par défaut
|
|
121
|
+
];
|
|
122
|
+
const jsonString = JSON.stringify(jsonDataToImport);
|
|
123
|
+
|
|
124
|
+
// Exécution de la fonction d'import (hypothétique)x
|
|
125
|
+
var blob = new Blob([jsonString], {type: "application/json"});
|
|
126
|
+
fs.writeFileSync('test.json', jsonString);
|
|
127
|
+
blob.path = 'test.json';
|
|
128
|
+
blob.originalFilename = 'test.json';
|
|
129
|
+
const result = await importData({model: impexTestModel.name}, {file: blobToFile(blob, "test.json")}, mockUser);
|
|
130
|
+
|
|
131
|
+
// Vérifications du résultat de l'opération
|
|
132
|
+
expect(result.success).toBe(true);
|
|
133
|
+
expect(result.jobId).not.toBeNull();
|
|
134
|
+
|
|
135
|
+
await sleep(5000);
|
|
136
|
+
|
|
137
|
+
// Vérification directe en base de données
|
|
138
|
+
const importedDocs = await testDatasColInstance.find({
|
|
139
|
+
_model: impexTestModel.name,
|
|
140
|
+
sku: { $in: ['SKU-D', 'SKU-E'] }
|
|
141
|
+
}).toArray();
|
|
142
|
+
|
|
143
|
+
expect(importedDocs).toHaveLength(2);
|
|
144
|
+
const docD = importedDocs.find(d => d.sku === 'SKU-D');
|
|
145
|
+
const docE = importedDocs.find(d => d.sku === 'SKU-E');
|
|
146
|
+
|
|
147
|
+
expect(docD.name).toBe('Produit D');
|
|
148
|
+
expect(docD.inStock).toBe(true); // Vérification de la valeur par défaut
|
|
149
|
+
expect(docE.price).toBe(2.00);
|
|
150
|
+
|
|
151
|
+
}, 20000);
|
|
152
|
+
|
|
153
|
+
it('devrait importer des données depuis une chaîne CSV et convertir les types', async () => {
|
|
154
|
+
const csvStringToImport = `name,sku,price,inStock\nProduit F,SKU-F,3.55,true\nProduit G,SKU-G,4.99,false`;
|
|
155
|
+
|
|
156
|
+
// Exécution de la fonction d'import (hypothétique)x
|
|
157
|
+
var blob = new Blob([csvStringToImport], {type: "text/csv"});
|
|
158
|
+
fs.writeFileSync('test.csv', csvStringToImport);
|
|
159
|
+
blob.path = 'test.csv';
|
|
160
|
+
blob.originalFilename = 'test.csv';
|
|
161
|
+
|
|
162
|
+
// Exécution de la fonction d'import
|
|
163
|
+
const result = await importData({model:impexTestModel.name}, {file: blobToFile(blob, "test.csv")}, mockUser);
|
|
164
|
+
|
|
165
|
+
// Vérifications du résultat
|
|
166
|
+
expect(result.success).toBe(true);
|
|
167
|
+
expect(result.jobId).not.toBeNull();
|
|
168
|
+
|
|
169
|
+
await sleep(5000);
|
|
170
|
+
// Vérification en base de données
|
|
171
|
+
const importedDocs = await testDatasColInstance.find({
|
|
172
|
+
_model: impexTestModel.name,
|
|
173
|
+
sku: { $in: ['SKU-F', 'SKU-G'] }
|
|
174
|
+
}).toArray();
|
|
175
|
+
|
|
176
|
+
expect(importedDocs).toHaveLength(2);
|
|
177
|
+
const docF = importedDocs.find(d => d.sku === 'SKU-F');
|
|
178
|
+
const docG = importedDocs.find(d => d.sku === 'SKU-G');
|
|
179
|
+
|
|
180
|
+
// Vérification de la conversion des types
|
|
181
|
+
expect(docF.price).toBe(3.55);
|
|
182
|
+
expect(docF.inStock).toBe(true);
|
|
183
|
+
expect(docG.inStock).toBe(false);
|
|
184
|
+
}, 20000);
|
|
185
|
+
|
|
186
|
+
it('devrait rejeter les lignes invalides et rapporter les erreurs', async () => {
|
|
187
|
+
const csvStringToImport = `name,sku,price,inStock
|
|
188
|
+
Valide H,SKU-H,10,true
|
|
189
|
+
,SKU-I,20,true
|
|
190
|
+
Valide J,SKU-J,,false
|
|
191
|
+
Valide K,SKU-A,40,true`;
|
|
192
|
+
|
|
193
|
+
// Exécution de la fonction d'import (hypothétique)x
|
|
194
|
+
const blob = new Blob([csvStringToImport], {type: "text/csv"});
|
|
195
|
+
fs.writeFileSync('test.csv', csvStringToImport);
|
|
196
|
+
blob.path = 'test.csv';
|
|
197
|
+
blob.originalFilename = 'test.csv';
|
|
198
|
+
|
|
199
|
+
const result = await importData({ model: impexTestModel.name }, {file: blobToFile(blob,"test.csv")}, mockUser);
|
|
200
|
+
|
|
201
|
+
// L'initiation du job doit réussir
|
|
202
|
+
expect(result.success).toBe(true);
|
|
203
|
+
expect(result.jobId).not.toBeNull();
|
|
204
|
+
|
|
205
|
+
await sleep(5000);
|
|
206
|
+
// Vérifier que seule les données valides sont en BDD
|
|
207
|
+
const count = await testDatasColInstance.countDocuments({ _model: impexTestModel.name, sku: 'SKU-H' });
|
|
208
|
+
expect(count).toBe(1); // Seule la ligne valide 'SKU-H' doit être insérée.
|
|
209
|
+
}, 20000);
|
|
210
|
+
});
|
|
211
211
|
});
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { queueReplication, onInit, stopReplicationQueue } from '../src/modules/data/data.replication.js';
|
|
3
|
+
import * as clusterModule from '../src/modules/data/data.cluster.js';
|
|
4
|
+
import { Logger } from "../src/index.js"
|
|
5
|
+
|
|
6
|
+
// Mock Logger to prevent console output
|
|
7
|
+
vi.mock('../src/gameObject.js', () => ({
|
|
8
|
+
Logger: class {
|
|
9
|
+
info = vi.fn();
|
|
10
|
+
warn = vi.fn();
|
|
11
|
+
error = vi.fn();
|
|
12
|
+
debug = vi.fn();
|
|
13
|
+
}
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
describe('Data Replication Logic', () => {
|
|
17
|
+
let mockEngine;
|
|
18
|
+
let getReplicaNodesForUserSpy;
|
|
19
|
+
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
vi.useFakeTimers();
|
|
22
|
+
|
|
23
|
+
// Mock the engine and its sendToPeer function
|
|
24
|
+
mockEngine = {
|
|
25
|
+
peers: [
|
|
26
|
+
{ id: 'self', url: 'http://self:3000' },
|
|
27
|
+
{ id: 'replica-1', url: 'http://replica-1:3000' },
|
|
28
|
+
{ id: 'replica-2', url: 'http://replica-2:3000' }
|
|
29
|
+
],
|
|
30
|
+
getComponent: vi.fn().mockReturnValue(new Logger('mock')),
|
|
31
|
+
sendToPeer: vi.fn().mockResolvedValue({ ok: true }),
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// Spy on getReplicaNodesForUser to control its output
|
|
35
|
+
getReplicaNodesForUserSpy = vi.spyOn(clusterModule, 'getReplicaNodesForUser');
|
|
36
|
+
|
|
37
|
+
// Initialize the replication module with the mock engine
|
|
38
|
+
onInit(mockEngine);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
afterEach(() => {
|
|
42
|
+
vi.useRealTimers();
|
|
43
|
+
vi.restoreAllMocks();
|
|
44
|
+
stopReplicationQueue();
|
|
45
|
+
// Clear the queue manually for test isolation
|
|
46
|
+
const replicationQueue = []; // This is a simplified way to clear it for tests
|
|
47
|
+
while (replicationQueue.length > 0) {
|
|
48
|
+
replicationQueue.pop();
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('should queue an operation correctly', async () => {
|
|
53
|
+
const operation = 'insert';
|
|
54
|
+
const modelName = 'testModel';
|
|
55
|
+
const user = { username: 'testuser' };
|
|
56
|
+
const payload = { data: { name: 'test' } };
|
|
57
|
+
|
|
58
|
+
// This test is tricky because replicationQueue is not exported.
|
|
59
|
+
// We'll test its effect indirectly via processReplicationQueue.
|
|
60
|
+
queueReplication(operation, modelName, user, payload);
|
|
61
|
+
|
|
62
|
+
// We can't directly inspect the queue, so we'll let the processing logic run
|
|
63
|
+
// and check if the mocks were called.
|
|
64
|
+
getReplicaNodesForUserSpy.mockReturnValue([{ id: 'replica-1' }]);
|
|
65
|
+
|
|
66
|
+
await vi.advanceTimersByTimeAsync(200); // Advance time to trigger processReplicationQueue
|
|
67
|
+
|
|
68
|
+
// Check that the processing function tried to get replicas
|
|
69
|
+
expect(getReplicaNodesForUserSpy).toHaveBeenCalledWith(user.username);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('should process the queue and send operations in batches to correct replicas', async () => {
|
|
73
|
+
const user1 = { username: 'user1' };
|
|
74
|
+
const user2 = { username: 'user2' };
|
|
75
|
+
|
|
76
|
+
// Mock so user1 replicates to replica-1 and user2 to replica-2
|
|
77
|
+
getReplicaNodesForUserSpy.mockImplementation((username) => {
|
|
78
|
+
if (username === 'user1') return [{ id: 'replica-1' }];
|
|
79
|
+
if (username === 'user2') return [{ id: 'replica-2' }];
|
|
80
|
+
return [];
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// Queue operations for different users
|
|
84
|
+
queueReplication('insert', 'modelA', user1, { data: { a: 1 } });
|
|
85
|
+
queueReplication('update', 'modelB', user2, { data: { b: 2 } });
|
|
86
|
+
queueReplication('delete', 'modelC', user1, { ids: ['123'] });
|
|
87
|
+
|
|
88
|
+
// Advance timers to trigger the processing
|
|
89
|
+
await vi.advanceTimersByTimeAsync(200);
|
|
90
|
+
|
|
91
|
+
// Assertions
|
|
92
|
+
expect(mockEngine.sendToPeer).toHaveBeenCalledTimes(2);
|
|
93
|
+
|
|
94
|
+
// Check call for replica-1
|
|
95
|
+
expect(mockEngine.sendToPeer).toHaveBeenCalledWith(
|
|
96
|
+
'replica-1',
|
|
97
|
+
'/api/internal/replicate',
|
|
98
|
+
expect.objectContaining({
|
|
99
|
+
operations: expect.arrayContaining([
|
|
100
|
+
expect.objectContaining({ operation: 'insert', modelName: 'modelA', user: user1 }),
|
|
101
|
+
expect.objectContaining({ operation: 'delete', modelName: 'modelC', user: user1 })
|
|
102
|
+
])
|
|
103
|
+
})
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
// Check call for replica-2
|
|
107
|
+
expect(mockEngine.sendToPeer).toHaveBeenCalledWith(
|
|
108
|
+
'replica-2',
|
|
109
|
+
'/api/internal/replicate',
|
|
110
|
+
expect.objectContaining({
|
|
111
|
+
operations: expect.arrayContaining([
|
|
112
|
+
expect.objectContaining({ operation: 'update', modelName: 'modelB', user: user2 })
|
|
113
|
+
])
|
|
114
|
+
})
|
|
115
|
+
);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('should not process queue if CLUSTER_SHARED_DATABASE is true', async () => {
|
|
119
|
+
process.env.CLUSTER_SHARED_DATABASE = 'true';
|
|
120
|
+
|
|
121
|
+
queueReplication('insert', 'modelA', { username: 'user1' }, { data: { a: 1 } });
|
|
122
|
+
|
|
123
|
+
await vi.advanceTimersByTimeAsync(200);
|
|
124
|
+
|
|
125
|
+
expect(mockEngine.sendToPeer).not.toHaveBeenCalled();
|
|
126
|
+
|
|
127
|
+
delete process.env.CLUSTER_SHARED_DATABASE; // Cleanup
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('should queue a failed batch for retry and re-send it on the next cycle', async () => {
|
|
131
|
+
const user = { username: 'retry-user' };
|
|
132
|
+
const payload = { data: { name: 'important-data' } };
|
|
133
|
+
|
|
134
|
+
// Configurer le mock pour qu'il échoue la première fois et réussisse la seconde
|
|
135
|
+
mockEngine.sendToPeer
|
|
136
|
+
.mockRejectedValueOnce(new Error('Simulated Network Error'))
|
|
137
|
+
.mockResolvedValue({ ok: true });
|
|
138
|
+
|
|
139
|
+
// Toutes les opérations pour cet utilisateur vont vers 'replica-1'
|
|
140
|
+
getReplicaNodesForUserSpy.mockReturnValue([{ id: 'replica-1' }]);
|
|
141
|
+
|
|
142
|
+
// 1. Mettre une opération en file d'attente
|
|
143
|
+
queueReplication('insert', 'retryModel', user, payload);
|
|
144
|
+
|
|
145
|
+
// 2. Premier cycle : processReplicationQueue est appelé, sendToPeer échoue
|
|
146
|
+
// Advance just enough to trigger one processing cycle (assuming interval is 100ms)
|
|
147
|
+
await vi.advanceTimersByTimeAsync(100);
|
|
148
|
+
|
|
149
|
+
// Vérifier que l'échec a été loggué et que la tentative a eu lieu
|
|
150
|
+
expect(mockEngine.sendToPeer).toHaveBeenCalledTimes(1);
|
|
151
|
+
|
|
152
|
+
// 3. Deuxième cycle : retryFailedBatches est appelé, sendToPeer réussit
|
|
153
|
+
await vi.advanceTimersByTimeAsync(100);
|
|
154
|
+
|
|
155
|
+
// Vérifier que la nouvelle tentative a eu lieu
|
|
156
|
+
expect(mockEngine.sendToPeer).toHaveBeenCalledTimes(2);
|
|
157
|
+
|
|
158
|
+
// Vérifier que les deux appels étaient pour la même réplique avec les mêmes données
|
|
159
|
+
const firstCallArgs = mockEngine.sendToPeer.mock.calls[0];
|
|
160
|
+
const secondCallArgs = mockEngine.sendToPeer.mock.calls[1];
|
|
161
|
+
expect(firstCallArgs).toEqual(secondCallArgs);
|
|
162
|
+
});
|
|
163
|
+
});
|