data-primals-engine 1.0.9 → 1.0.10
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 +217 -0
- package/package.json +1 -1
- package/src/i18n.js +2274 -0
- package/src/index.js +1 -1
- package/src/modules/data.js +112 -85
- package/test/model.integration.test.js +219 -0
package/src/index.js
CHANGED
|
@@ -10,6 +10,6 @@ export { UserProvider } from './providers.js';
|
|
|
10
10
|
|
|
11
11
|
// --- Database & Data Modules ---
|
|
12
12
|
export { datasCollection, filesCollection, modelsCollection, packsCollection } from './modules/mongodb.js';
|
|
13
|
-
export { searchData, insertData, editData, loadFromDump, dumpUserData, validateRestoreRequest, patchData, deleteData, createModel, getModel, getModels } from './modules/data.js';
|
|
13
|
+
export { searchData, insertData, editData, exportData, importData, scheduleAlerts, cancelAlerts, validateModelStructure, installPack, jobDumpUserData, loadFromDump, dumpUserData, validateRestoreRequest, patchData, deleteData, createModel, editModel, deleteModels, getModel, getModels } from './modules/data.js';
|
|
14
14
|
|
|
15
15
|
|
package/src/modules/data.js
CHANGED
|
@@ -1113,6 +1113,111 @@ function applyCronMask(cronString, mask, defaults) {
|
|
|
1113
1113
|
return newParts.join(' ');
|
|
1114
1114
|
}
|
|
1115
1115
|
|
|
1116
|
+
|
|
1117
|
+
export const editModel = async (user, id, data) => {
|
|
1118
|
+
|
|
1119
|
+
if( !(isDemoUser(user) && Config.Get("useDemoAccounts")) && isLocalUser(user) && !await hasPermission(["API_ADMIN", "API_EDIT_MODEL"], user)){
|
|
1120
|
+
return ({success: false, error: i18n.t('api.permission.editModel', 'Cannot edit models from the API')})
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
const dataModel = data;
|
|
1124
|
+
try {
|
|
1125
|
+
const collection = getCollectionForUser(user);
|
|
1126
|
+
validateModelStructure(dataModel);
|
|
1127
|
+
|
|
1128
|
+
const el = await modelsCollection.findOne({ $and: [
|
|
1129
|
+
{_user: {$exists: true}},
|
|
1130
|
+
{ _id: new ObjectId(id) },
|
|
1131
|
+
{$and: [{_user: {$exists: true}}, {$or: [{_user: user._user}, {_user: user.username}]}]
|
|
1132
|
+
}
|
|
1133
|
+
]});
|
|
1134
|
+
|
|
1135
|
+
if( !el ){
|
|
1136
|
+
return ({success: false, statusCode: 404, error: i18n.t("api.model.notFound", { model: dataModel.name })});
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
// renommage du modèle
|
|
1140
|
+
if (typeof (data.name)==='string'&&el.name !== data.name && data.name ){
|
|
1141
|
+
await collection.updateMany({ _model: el.name }, { $set: { _model: data.name }});
|
|
1142
|
+
await modelsCollection.updateMany({ 'fields' : {
|
|
1143
|
+
'$elemMatch' : { relation: el.name }
|
|
1144
|
+
}}, {
|
|
1145
|
+
$set : {
|
|
1146
|
+
'fields.$.relation' : data.name
|
|
1147
|
+
}
|
|
1148
|
+
})
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
const coll = getCollectionForUser(user);
|
|
1152
|
+
// Update indexes
|
|
1153
|
+
// Update indexes
|
|
1154
|
+
if (user.userPlan === 'premium') {
|
|
1155
|
+
let indexes = [];
|
|
1156
|
+
try {
|
|
1157
|
+
// On essaie de récupérer les index existants
|
|
1158
|
+
indexes = await coll.indexes();
|
|
1159
|
+
} catch (e) {
|
|
1160
|
+
// Si la collection n'existe pas, c'est normal.
|
|
1161
|
+
// createIndex la créera. Il n'y a juste pas d'index à supprimer.
|
|
1162
|
+
if (e.codeName !== 'NamespaceNotFound') {
|
|
1163
|
+
throw e; // On relance les autres erreurs
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
// Le reste de votre logique de gestion d'index peut maintenant s'exécuter en toute sécurité
|
|
1168
|
+
for (const field of data.fields) {
|
|
1169
|
+
const elField = el.fields.find(f => f.name === field.name);
|
|
1170
|
+
if (!elField) continue;
|
|
1171
|
+
|
|
1172
|
+
const index = indexes.find(i => i.key[field.name] === 1 &&
|
|
1173
|
+
i.partialFilterExpression?._model === el.name &&
|
|
1174
|
+
i.partialFilterExpression?._user === user.username);
|
|
1175
|
+
|
|
1176
|
+
if (elField.index !== field.index && !field.index) {
|
|
1177
|
+
if (index) {
|
|
1178
|
+
await coll.dropIndex(index.name);
|
|
1179
|
+
}
|
|
1180
|
+
} else if (elField.index !== field.index && field.index) {
|
|
1181
|
+
if (!index) {
|
|
1182
|
+
await coll.createIndex({ [field.name]: 1 }, {
|
|
1183
|
+
partialFilterExpression: {
|
|
1184
|
+
_model: data.name,
|
|
1185
|
+
_user: user.username
|
|
1186
|
+
}
|
|
1187
|
+
});
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
// suppression des données à la suppression des champs
|
|
1193
|
+
const unset = {};
|
|
1194
|
+
el.fields.filter(f=> !dataModel.fields.some(dt => dt.name === f.name)).map(f => f.name).forEach(f => {
|
|
1195
|
+
unset[f] = 1;
|
|
1196
|
+
});
|
|
1197
|
+
await collection.updateMany({ _model: el.name }, { $unset: unset });
|
|
1198
|
+
|
|
1199
|
+
// sauvegarde du modele
|
|
1200
|
+
const set = {...data};
|
|
1201
|
+
delete set['_id'];
|
|
1202
|
+
|
|
1203
|
+
const oid = new ObjectId(id);
|
|
1204
|
+
await modelsCollection.updateOne({_id: oid}, {$set: set});
|
|
1205
|
+
|
|
1206
|
+
modelsCache.del(user.username+'@@'+el.name);
|
|
1207
|
+
|
|
1208
|
+
const model = await modelsCollection.findOne({_id: oid });
|
|
1209
|
+
triggerWorkflows(model, user, 'ModelEdited').catch(workflowError => {
|
|
1210
|
+
logger.error(`Erreur asynchrone lors du déclenchement des workflows pour ${model._model} ID ${model._id}:`, workflowError);
|
|
1211
|
+
});
|
|
1212
|
+
|
|
1213
|
+
return ({ success: true, data: await modelsCollection.findOne({_id : oid}) });
|
|
1214
|
+
} catch (e) {
|
|
1215
|
+
logger.error(e);
|
|
1216
|
+
return ({ success: false, error: e.message, statusCode: 500 });
|
|
1217
|
+
}
|
|
1218
|
+
};
|
|
1219
|
+
|
|
1220
|
+
|
|
1116
1221
|
export async function onInit(defaultEngine) {
|
|
1117
1222
|
engine = defaultEngine;
|
|
1118
1223
|
logger = engine.getComponent(Logger);
|
|
@@ -1459,95 +1564,17 @@ export async function onInit(defaultEngine) {
|
|
|
1459
1564
|
});
|
|
1460
1565
|
|
|
1461
1566
|
engine.put('/api/model/:id', [middlewareAuthenticator, userInitiator, setTimeoutMiddleware(15000)], async (req, res) => {
|
|
1462
|
-
|
|
1463
|
-
if(
|
|
1464
|
-
return res.status(
|
|
1465
|
-
}
|
|
1466
|
-
|
|
1467
|
-
const dataModel = req.fields;
|
|
1468
|
-
try {
|
|
1469
|
-
const collection = getCollectionForUser(req.me);
|
|
1470
|
-
validateModelStructure(dataModel);
|
|
1471
|
-
|
|
1472
|
-
const el = await modelsCollection.findOne({ $and: [
|
|
1473
|
-
{_user: {$exists: true}},
|
|
1474
|
-
{ _id: new ObjectId(req.params.id) },
|
|
1475
|
-
{$and: [{_user: {$exists: true}}, {$or: [{_user: req.me._user}, {_user: req.me.username}]}]
|
|
1476
|
-
}
|
|
1477
|
-
]});
|
|
1478
|
-
|
|
1479
|
-
if( !el ){
|
|
1480
|
-
return res.status(404).json({error: i18n.t("api.model.notFound", { model: dataModel.name })});
|
|
1481
|
-
}
|
|
1482
|
-
|
|
1483
|
-
// renommage du modèle
|
|
1484
|
-
if (typeof (req.fields.name)==='string'&&el.name !== req.fields.name && req.fields.name ){
|
|
1485
|
-
await collection.updateMany({ _model: el.name }, { $set: { _model: req.fields.name }});
|
|
1486
|
-
await modelsCollection.updateMany({ 'fields' : {
|
|
1487
|
-
'$elemMatch' : { relation: el.name }
|
|
1488
|
-
}}, {
|
|
1489
|
-
$set : {
|
|
1490
|
-
'fields.$.relation' : req.fields.name
|
|
1491
|
-
}
|
|
1492
|
-
})
|
|
1493
|
-
}
|
|
1494
|
-
|
|
1495
|
-
// Update indexes
|
|
1496
|
-
if( req.me.userPlan === 'premium') {
|
|
1497
|
-
const indexes = await datasCollection.indexes();
|
|
1498
|
-
for (const field of dataModel.fields) {
|
|
1499
|
-
const elField = el.fields.find(f => f.name === field.name);
|
|
1500
|
-
if (!elField)
|
|
1501
|
-
continue;
|
|
1502
|
-
const index = indexes.find(i => i.key[field.name] === 1 &&
|
|
1503
|
-
i.partialFilterExpression?._model === el.name &&
|
|
1504
|
-
i.partialFilterExpression?._user === req.me.username);
|
|
1505
|
-
if (elField.index !== field.index && !field.index) {
|
|
1506
|
-
if (index) {
|
|
1507
|
-
await datasCollection.dropIndex(index.name);
|
|
1508
|
-
}
|
|
1509
|
-
} else if (elField.index !== field.index && field.index) {
|
|
1510
|
-
if (!index) {
|
|
1511
|
-
await datasCollection.createIndex({[field.name]: 1}, {
|
|
1512
|
-
partialFilterExpression: {
|
|
1513
|
-
_model: dataModel.name,
|
|
1514
|
-
_user: req.me.username
|
|
1515
|
-
}
|
|
1516
|
-
});
|
|
1517
|
-
}
|
|
1518
|
-
}
|
|
1519
|
-
}
|
|
1520
|
-
}
|
|
1521
|
-
|
|
1522
|
-
// suppression des données à la suppression des champs
|
|
1523
|
-
const unset = {};
|
|
1524
|
-
el.fields.filter(f=> !dataModel.fields.some(dt => dt.name === f.name)).map(f => f.name).forEach(f => {
|
|
1525
|
-
unset[f] = 1;
|
|
1526
|
-
});
|
|
1527
|
-
await collection.updateMany({ _model: el.name }, { $unset: unset });
|
|
1528
|
-
|
|
1529
|
-
// sauvegarde du modele
|
|
1530
|
-
const set = {...req.fields};
|
|
1531
|
-
delete set['_id'];
|
|
1532
|
-
await modelsCollection.updateOne({_id: new ObjectId(req.params.id)}, {$set: set});
|
|
1533
|
-
|
|
1534
|
-
modelsCache.del(req.me.username+'@@'+el.name);
|
|
1535
|
-
|
|
1536
|
-
const model = await modelsCollection.findOne({_id: new ObjectId(req.params.id) });
|
|
1537
|
-
triggerWorkflows(model, req.me, 'ModelEdited').catch(workflowError => {
|
|
1538
|
-
logger.error(`Erreur asynchrone lors du déclenchement des workflows pour ${model._model} ID ${model._id}:`, workflowError);
|
|
1539
|
-
});
|
|
1540
|
-
|
|
1541
|
-
res.json({ success: true, data: await modelsCollection.findOne({_id : new ObjectId(req.params.id)}) });
|
|
1542
|
-
} catch (e) {
|
|
1543
|
-
logger.error(e);
|
|
1544
|
-
res.json({ success: false });
|
|
1567
|
+
const result = await editModel(req.me, req.params.id, req.fields);
|
|
1568
|
+
if( result.success){
|
|
1569
|
+
return res.status(result.statusCode || '200').json(result);
|
|
1570
|
+
}else{
|
|
1571
|
+
return res.status(result.statusCode || '500').json(result);
|
|
1545
1572
|
}
|
|
1546
|
-
})
|
|
1573
|
+
});
|
|
1547
1574
|
|
|
1548
1575
|
engine.post('/api/data/restore', [throttle, middlewareAuthenticator, userInitiator,myFreePremiumAnonymousLimiter, setTimeoutMiddleware(60000)], async (req, res) => {
|
|
1549
1576
|
|
|
1550
|
-
if (!((
|
|
1577
|
+
if (!((user?.roles || []).includes("admin"))) {
|
|
1551
1578
|
return res.status(403).json({success: false, error: 'Cannot backup data. Contact an administrator to get back your data'})
|
|
1552
1579
|
}
|
|
1553
1580
|
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
// __tests__/data.integration.test.js
|
|
2
|
+
import { ObjectId } from 'mongodb';
|
|
3
|
+
import {expect, describe, it, beforeEach, afterEach, beforeAll, afterAll, vi} from 'vitest';
|
|
4
|
+
import { Config } from '../src/config.js';
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
modelsCollection as getAppModelsCollection,
|
|
8
|
+
datasCollection // Accès direct pour vérifications
|
|
9
|
+
} from 'data-primals-engine/modules/mongodb';
|
|
10
|
+
import {generateUniqueName, getUniquePort, initEngine} from "../src/setenv.js";
|
|
11
|
+
import {editModel} from "../src/modules/data.js";
|
|
12
|
+
import {getCollectionForUser} from "../src/modules/mongodb.js";
|
|
13
|
+
|
|
14
|
+
let testModelsColInstance;
|
|
15
|
+
let testDatasColInstance;
|
|
16
|
+
|
|
17
|
+
let testModelId;
|
|
18
|
+
|
|
19
|
+
// Cette fonction va remplacer la logique de votre beforeEach pour la création de contexte
|
|
20
|
+
async function setupTestContext() {
|
|
21
|
+
|
|
22
|
+
const currentTestModelName = generateUniqueName('relatedModel');
|
|
23
|
+
const currentRelatedModelName = generateUniqueName('comprehensiveModel');
|
|
24
|
+
|
|
25
|
+
// Créer un utilisateur unique pour ce test
|
|
26
|
+
const currentTestUser = {
|
|
27
|
+
username: generateUniqueName('testuserDataIntegration'),
|
|
28
|
+
userPlan: 'free',
|
|
29
|
+
email: generateUniqueName('test') + '@example.com'
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
const relatedModelDefinition = {
|
|
34
|
+
name: currentRelatedModelName,
|
|
35
|
+
_user: currentTestUser.username,
|
|
36
|
+
description: 'Model for testing relations',
|
|
37
|
+
fields: [
|
|
38
|
+
{name: 'relatedName', type: 'string', required: true, unique: true},
|
|
39
|
+
{name: 'relatedValue', type: 'number'}
|
|
40
|
+
],
|
|
41
|
+
maxRequestData: 10,
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const comprehensiveTestModelDefinition = {
|
|
45
|
+
name: currentTestModelName,
|
|
46
|
+
_user: currentTestUser.username,
|
|
47
|
+
description: 'A model with all field types and constraints for testing',
|
|
48
|
+
fields: [
|
|
49
|
+
// String types
|
|
50
|
+
{name: 'stringRequired', type: 'string', required: true, hint: 'A required string'},
|
|
51
|
+
{name: 'stringUnique', type: 'string', unique: true, hint: 'A unique string'},
|
|
52
|
+
{name: 'stringMaxLength', type: 'string', maxlength: 5, hint: 'String with max length 5'},
|
|
53
|
+
{name: 'stringDefault', type: 'string', default: 'defaultString'},
|
|
54
|
+
{name: 'string_tLang', type: 'string_t', hint: 'Localizable string'},
|
|
55
|
+
{name: 'richtextField', type: 'richtext', maxlength: 200},
|
|
56
|
+
{name: 'passwordField', type: 'password'},
|
|
57
|
+
{name: 'emailField', type: 'email'},
|
|
58
|
+
{name: 'phoneField', type: 'phone'},
|
|
59
|
+
{name: 'urlField', type: 'url'},
|
|
60
|
+
// Number types
|
|
61
|
+
{name: 'number', type: 'number'},
|
|
62
|
+
{name: 'numberMinMax', type: 'number', min: 10, max: 20},
|
|
63
|
+
{name: 'numberStep', type: 'number', step: 0.5},
|
|
64
|
+
{name: 'numberDefault', type: 'number', default: 42},
|
|
65
|
+
// Boolean
|
|
66
|
+
{name: 'booleanField', type: 'boolean'},
|
|
67
|
+
{name: 'booleanDefault', type: 'boolean', default: true},
|
|
68
|
+
// Date & Datetime
|
|
69
|
+
{name: 'dateField', type: 'date'},
|
|
70
|
+
{name: 'datetimeField', type: 'datetime', default: 'now'},
|
|
71
|
+
{name: 'dateMinMax', type: 'date', min: '2023-01-01', max: '2023-12-31'},
|
|
72
|
+
// Enum
|
|
73
|
+
{name: 'enumField', type: 'enum', items: ['alpha', 'beta', 'gamma']},
|
|
74
|
+
{name: 'enumDefault', type: 'enum', items: ['one', 'two'], default: 'one'},
|
|
75
|
+
// Array
|
|
76
|
+
{name: 'arrayString', type: 'array', itemsType: 'string', maxItems: 2},
|
|
77
|
+
{name: 'arrayNumber', type: 'array', itemsType: 'number', minItems: 1},
|
|
78
|
+
{name: 'arrayEnum', type: 'array', itemsType: 'enum', items: ['a', 'b']},
|
|
79
|
+
// Relation
|
|
80
|
+
{name: 'relationSingle', type: 'relation', relation: currentRelatedModelName},
|
|
81
|
+
{name: 'relationMultiple', type: 'relation', relation: currentRelatedModelName, multiple: true},
|
|
82
|
+
// File (metadata validation)
|
|
83
|
+
{name: 'fileField', type: 'file', mimeTypes: ['image/png', 'application/pdf'], maxSize: 1024 * 10}, // 10KB
|
|
84
|
+
// Color
|
|
85
|
+
{name: 'colorField', type: 'color'},
|
|
86
|
+
// Code
|
|
87
|
+
{name: 'codeJsonField', type: 'code', language: 'json'},
|
|
88
|
+
{name: 'codeJsField', type: 'code', language: 'javascript', maxlength: 100},
|
|
89
|
+
// Object (simple validation, structure not deeply validated by default dataTypes.object)
|
|
90
|
+
{name: 'objectField', type: 'object'},
|
|
91
|
+
// Model & ModelField (validation of string format, not existence)
|
|
92
|
+
{name: 'modelNameField', type: 'model'},
|
|
93
|
+
{name: 'modelFieldNameField', type: 'modelField'}, // Note: modelField type expects an object {model: 'modelName', field: 'fieldName'}
|
|
94
|
+
],
|
|
95
|
+
maxRequestData: 50,
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
// Insérer les modèles en base
|
|
99
|
+
const result= await testModelsColInstance.insertMany([
|
|
100
|
+
comprehensiveTestModelDefinition,
|
|
101
|
+
relatedModelDefinition
|
|
102
|
+
]);
|
|
103
|
+
|
|
104
|
+
testModelId = result.insertedIds[0];
|
|
105
|
+
await testDatasColInstance.deleteMany({ _user: currentTestUser.username });
|
|
106
|
+
await testDatasColInstance.deleteMany({ _model: { $in: [comprehensiveTestModelDefinition.name, 'renamedTestModel'] } });
|
|
107
|
+
// Retourner toutes les variables nécessaires pour un test
|
|
108
|
+
return {
|
|
109
|
+
currentTestUser,
|
|
110
|
+
comprehensiveTestModelDefinition,
|
|
111
|
+
relatedModelDefinition,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
describe('CRUD on model definitions and integrity tests', () => {
|
|
116
|
+
|
|
117
|
+
beforeAll(async () =>{
|
|
118
|
+
Config.Set("modules", ["mongodb", "data", "file", "bucket", "workflow","user", "assistant"]);
|
|
119
|
+
await initEngine();
|
|
120
|
+
|
|
121
|
+
// Initialize collection instances after the engine is ready
|
|
122
|
+
testModelsColInstance = getAppModelsCollection;
|
|
123
|
+
testDatasColInstance = datasCollection;
|
|
124
|
+
})
|
|
125
|
+
describe('editModel unit tests', () => {
|
|
126
|
+
|
|
127
|
+
it('should create and drop index when field.index is toggled (premium user)', async () => {
|
|
128
|
+
// --- SETUP ---
|
|
129
|
+
const { currentTestUser, comprehensiveTestModelDefinition } = await setupTestContext();
|
|
130
|
+
const premiumUser = { ...currentTestUser, userPlan: 'premium' };
|
|
131
|
+
const dataCollection = getCollectionForUser(premiumUser);
|
|
132
|
+
const fieldToIndex = 'stringUnique'; // Utiliser un champ qui existe vraiment dans le modèle
|
|
133
|
+
|
|
134
|
+
// --- VERIFICATION INITIALE ---
|
|
135
|
+
// S'assurer qu'aucun index n'existe au départ.
|
|
136
|
+
// On enveloppe l'appel dans un try/catch pour gérer le cas où la collection n'existe pas.
|
|
137
|
+
let initialIndexes = [];
|
|
138
|
+
try {
|
|
139
|
+
initialIndexes = await dataCollection.indexes();
|
|
140
|
+
} catch (e) {
|
|
141
|
+
// C'est normal si la collection n'existe pas encore. On considère qu'il n'y a pas d'index.
|
|
142
|
+
if (e.codeName !== 'NamespaceNotFound') {
|
|
143
|
+
throw e; // Relancer les autres erreurs inattendues
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
expect(initialIndexes.some(i => i.key[fieldToIndex] === 1)).toBe(false);
|
|
147
|
+
|
|
148
|
+
// --- ACTION 1 : AJOUTER UN INDEX ---
|
|
149
|
+
const modelWithIndex = {
|
|
150
|
+
...comprehensiveTestModelDefinition,
|
|
151
|
+
fields: comprehensiveTestModelDefinition.fields.map(f =>
|
|
152
|
+
f.name === fieldToIndex ? { ...f, index: true } : f
|
|
153
|
+
),
|
|
154
|
+
};
|
|
155
|
+
await editModel(premiumUser, testModelId, modelWithIndex);
|
|
156
|
+
|
|
157
|
+
// --- VERIFICATION 1 ---
|
|
158
|
+
// Maintenant, la collection et l'index doivent exister.
|
|
159
|
+
const indexesAfterCreation = await dataCollection.indexes();
|
|
160
|
+
const newIndex = indexesAfterCreation.find(i => i.key[fieldToIndex] === 1);
|
|
161
|
+
|
|
162
|
+
expect(newIndex).toBeDefined();
|
|
163
|
+
// Le filtre partiel est crucial pour que l'index ne s'applique qu'aux bonnes données
|
|
164
|
+
expect(newIndex.partialFilterExpression).toEqual({
|
|
165
|
+
_model: comprehensiveTestModelDefinition.name,
|
|
166
|
+
_user: premiumUser.username,
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// --- ACTION 2 : SUPPRIMER L'INDEX ---
|
|
170
|
+
const modelWithoutIndex = {
|
|
171
|
+
...comprehensiveTestModelDefinition,
|
|
172
|
+
fields: comprehensiveTestModelDefinition.fields.map(f =>
|
|
173
|
+
f.name === fieldToIndex ? { ...f, index: false } : f
|
|
174
|
+
),
|
|
175
|
+
};
|
|
176
|
+
await editModel(premiumUser, testModelId, modelWithoutIndex);
|
|
177
|
+
|
|
178
|
+
// --- VERIFICATION 2 ---
|
|
179
|
+
const indexesAfterDeletion = await dataCollection.indexes();
|
|
180
|
+
expect(indexesAfterDeletion.some(i => i.key[fieldToIndex] === 1)).toBe(false);
|
|
181
|
+
}, 20000); // Augmenter le timeout si nécessaire pour les opérations d'index
|
|
182
|
+
|
|
183
|
+
it('should not save extra, non-defined fields in the model definition', async () => {
|
|
184
|
+
const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition } = await setupTestContext();
|
|
185
|
+
// 1. Préparer les données avec un champ non sollicité
|
|
186
|
+
const updatedModelData = {
|
|
187
|
+
...comprehensiveTestModelDefinition,
|
|
188
|
+
description: 'An updated description',
|
|
189
|
+
extraBogusField: 'this should not be saved', // Champ arbitraire
|
|
190
|
+
anotherOne: { nested: true }
|
|
191
|
+
};
|
|
192
|
+
// 2. Appeler la fonction d'édition
|
|
193
|
+
const result = await editModel(currentTestUser, testModelId, updatedModelData);
|
|
194
|
+
expect(result.success).toBe(false);
|
|
195
|
+
});
|
|
196
|
+
it('should return an error if trying to edit a non-existent model', async () => {
|
|
197
|
+
const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition } = await setupTestContext();
|
|
198
|
+
const nonExistentId = new ObjectId();
|
|
199
|
+
const result = await editModel(currentTestUser, nonExistentId, comprehensiveTestModelDefinition);
|
|
200
|
+
expect(result.success).toBe(false);
|
|
201
|
+
expect(result.statusCode).toBe(404);
|
|
202
|
+
expect(result.error).toContain('introuvable');
|
|
203
|
+
});
|
|
204
|
+
it('should return an error if the new model structure is invalid', async () => {
|
|
205
|
+
const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition } = await setupTestContext();
|
|
206
|
+
const invalidModelData = {
|
|
207
|
+
...comprehensiveTestModelDefinition,
|
|
208
|
+
fields: [
|
|
209
|
+
{ name: 'title' } // Le champ 'type' est manquant, ce qui est invalide
|
|
210
|
+
]
|
|
211
|
+
};
|
|
212
|
+
const result = await editModel(currentTestUser, testModelId, invalidModelData);
|
|
213
|
+
expect(result.success).toBe(false);
|
|
214
|
+
// L'erreur est levée par validateModelStructure, donc le message peut varier
|
|
215
|
+
expect(result.error).toBeDefined();
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
});
|