data-primals-engine 1.5.1 → 1.6.0
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 +924 -913
- package/client/README.md +136 -136
- package/client/index.js +0 -1
- package/client/public/doc/API.postman_collection.json +213 -213
- package/client/public/react.svg +9 -9
- package/client/src/AddWidgetTypeModal.jsx +47 -47
- package/client/src/App.css +42 -42
- package/client/src/App.jsx +92 -150
- package/client/src/App.scss +7 -1
- package/client/src/AssistantChat.jsx +362 -363
- package/client/src/Button.jsx +12 -12
- package/client/src/Dashboard.jsx +480 -480
- package/client/src/DashboardHtmlViewItem.jsx +146 -146
- package/client/src/DashboardView.jsx +654 -654
- package/client/src/DataEditor.jsx +383 -383
- package/client/src/DataLayout.jsx +779 -806
- package/client/src/DataTable.jsx +782 -822
- package/client/src/DataTable.scss +186 -186
- package/client/src/GeolocationField.jsx +93 -93
- package/client/src/HistoryDialog.jsx +307 -285
- package/client/src/HistoryDialog.scss +319 -319
- package/client/src/HtmlViewBuilderModal.jsx +90 -90
- package/client/src/HtmlViewBuilderModal.scss +17 -17
- package/client/src/HtmlViewCard.jsx +43 -43
- package/client/src/ModelCreatorField.jsx +950 -950
- package/client/src/ModelList.jsx +7 -2
- package/client/src/Notification.jsx +136 -136
- package/client/src/PackGallery.jsx +391 -391
- package/client/src/PackGallery.scss +231 -231
- package/client/src/Pagination.jsx +143 -143
- package/client/src/RelationField.jsx +354 -354
- package/client/src/RelationSelectorWidget.jsx +172 -172
- package/client/src/RelationValue.jsx +2 -1
- package/client/src/contexts/CommandContext.jsx +260 -0
- package/client/src/contexts/ModelContext.jsx +4 -1
- package/client/src/contexts/UIContext.jsx +72 -72
- package/client/src/filter.js +263 -262
- package/client/src/index.css +90 -90
- package/package.json +11 -10
- package/perf/artillery-hooks.js +37 -37
- package/perf/setup.yml +25 -25
- package/src/constants.js +4 -0
- package/src/core.js +8 -1
- package/src/engine.js +335 -293
- package/src/events.js +140 -21
- package/src/filter.js +274 -274
- package/src/index.js +3 -0
- package/src/modules/assistant/assistant.js +323 -192
- package/src/modules/assistant/constants.js +2 -1
- package/src/modules/auth-google/index.js +50 -50
- package/src/modules/auth-microsoft/index.js +81 -81
- package/src/modules/data/data.core.js +118 -118
- package/src/modules/data/data.history.js +555 -531
- package/src/modules/data/data.operations.js +145 -26
- package/src/modules/data/data.relations.js +686 -686
- package/src/modules/data/data.routes.js +1879 -1879
- package/src/modules/data/data.validation.js +2 -2
- package/src/modules/file.js +247 -247
- package/src/modules/user.js +1 -0
- package/src/modules/workflow.js +2 -2
- package/src/openai.jobs.js +3 -2
- package/src/packs.js +2 -2
- package/src/providers.js +2 -2
- package/src/services/stripe.js +196 -196
- package/src/sso.js +193 -193
- package/src/workers/import-export-worker.js +1 -1
- package/test/data.history.integration.test.js +72 -0
- package/test/data.integration.test.js +92 -1
- package/test/events.test.js +108 -10
- package/test/model.integration.test.js +377 -377
|
@@ -1,378 +1,378 @@
|
|
|
1
|
-
// __tests__/data.integration.test.js
|
|
2
|
-
import { ObjectId } from 'mongodb';
|
|
3
|
-
import {expect,beforeEach, describe, it, beforeAll} from 'vitest';
|
|
4
|
-
import { Config } from '../src/config.js';
|
|
5
|
-
|
|
6
|
-
import {
|
|
7
|
-
getCollection, modelsCollection // Accès direct pour vérifications
|
|
8
|
-
} from '../src/modules/mongodb.js';
|
|
9
|
-
import {generateUniqueName, initEngine} from "../src/setenv.js";
|
|
10
|
-
import {editModel, searchData} from "../src/index.js";
|
|
11
|
-
import {getCollectionForUser} from "../src/modules/mongodb.js";
|
|
12
|
-
import {purgeData} from "../src/modules/data/data.history.js";
|
|
13
|
-
|
|
14
|
-
let testModelsColInstance;
|
|
15
|
-
let testModelId;
|
|
16
|
-
// Cette fonction va remplacer la logique de votre beforeEach pour la création de contexte
|
|
17
|
-
async function setupTestContext() {
|
|
18
|
-
|
|
19
|
-
const currentTestModelName = generateUniqueName('relatedModel');
|
|
20
|
-
const currentRelatedModelName = generateUniqueName('comprehensiveModel');
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
// Créer un utilisateur unique pour ce test
|
|
24
|
-
const currentTestUser = {
|
|
25
|
-
username: generateUniqueName('testuserModelIntegration'),
|
|
26
|
-
userPlan: 'free',
|
|
27
|
-
email: generateUniqueName('test') + '@example.com'
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
testModelsColInstance = getCollection("models");
|
|
31
|
-
const testDatasColInstance = await getCollectionForUser(currentTestUser);
|
|
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
|
-
|
|
106
|
-
await purgeData(currentTestUser, comprehensiveTestModelDefinition.name);
|
|
107
|
-
await purgeData(currentTestUser, relatedModelDefinition.name);
|
|
108
|
-
|
|
109
|
-
await testDatasColInstance.deleteMany({ _user: currentTestUser.username });
|
|
110
|
-
await testDatasColInstance.deleteMany({ _model: { $in: [comprehensiveTestModelDefinition.name, 'renamedTestModel'] } });
|
|
111
|
-
// Retourner toutes les variables nécessaires pour un test
|
|
112
|
-
|
|
113
|
-
return {
|
|
114
|
-
currentTestUser,
|
|
115
|
-
coll:testDatasColInstance,
|
|
116
|
-
comprehensiveTestModelDefinition,
|
|
117
|
-
relatedModelDefinition
|
|
118
|
-
};
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
describe('CRUD on model definitions and integrity tests', () => {
|
|
123
|
-
|
|
124
|
-
beforeAll(async () =>{
|
|
125
|
-
Config.Set("modules", ["mongodb", "data", "file", "bucket", "workflow","user", "assistant"]);
|
|
126
|
-
await initEngine();
|
|
127
|
-
})
|
|
128
|
-
|
|
129
|
-
describe('editModel unit tests', () => {
|
|
130
|
-
|
|
131
|
-
it('should create and drop index when field.index is toggled (premium user)', async () => {
|
|
132
|
-
// --- SETUP ---
|
|
133
|
-
const { coll, currentTestUser, comprehensiveTestModelDefinition } = await setupTestContext();
|
|
134
|
-
const fieldToIndex = 'stringUnique'; // Utiliser un champ qui existe vraiment dans le modèle
|
|
135
|
-
|
|
136
|
-
// --- FIX: Ensure the collection exists before any operation ---
|
|
137
|
-
// By inserting and deleting a dummy document, we force MongoDB to create the
|
|
138
|
-
// collection and its default indexes. This prevents the "ns does not exist"
|
|
139
|
-
// error in asynchronous listeners (like workflow triggers).
|
|
140
|
-
const dummyDoc = await coll.insertOne({ _model: 'dummy', _user: currentTestUser.username });
|
|
141
|
-
await coll.deleteOne({ _id: dummyDoc.insertedId });
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
// --- VERIFICATION INITIALE ---
|
|
145
|
-
// S'assurer qu'aucun index n'existe au départ.
|
|
146
|
-
// Cet appel ne plantera plus car la collection est maintenant créée.
|
|
147
|
-
const initialIndexes = await coll.indexes();
|
|
148
|
-
expect(initialIndexes.some(i => i.key[fieldToIndex] === 1)).toBe(false);
|
|
149
|
-
|
|
150
|
-
// --- ACTION 1 : AJOUTER UN INDEX ---
|
|
151
|
-
const modelWithIndex = {
|
|
152
|
-
...comprehensiveTestModelDefinition,
|
|
153
|
-
fields: comprehensiveTestModelDefinition.fields.map(f =>
|
|
154
|
-
f.name === fieldToIndex ? { ...f, index: true } : f
|
|
155
|
-
)
|
|
156
|
-
};
|
|
157
|
-
await editModel(currentTestUser, testModelId, modelWithIndex);
|
|
158
|
-
|
|
159
|
-
// --- VERIFICATION 1 ---
|
|
160
|
-
// Maintenant, la collection et l'index doivent exister.
|
|
161
|
-
const indexesAfterCreation = await coll.indexes();
|
|
162
|
-
const newIndex = indexesAfterCreation.find(i => i.key[fieldToIndex] === 1);
|
|
163
|
-
|
|
164
|
-
expect(newIndex).toBeDefined();
|
|
165
|
-
// Le filtre partiel est crucial pour que l'index ne s'applique qu'aux bonnes données
|
|
166
|
-
expect(newIndex.partialFilterExpression).toEqual({
|
|
167
|
-
_model: comprehensiveTestModelDefinition.name,
|
|
168
|
-
_user: currentTestUser.username
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
// --- ACTION 2 : SUPPRIMER L'INDEX ---
|
|
172
|
-
const modelWithoutIndex = {
|
|
173
|
-
...comprehensiveTestModelDefinition,
|
|
174
|
-
fields: comprehensiveTestModelDefinition.fields.map(f =>
|
|
175
|
-
f.name === fieldToIndex ? { ...f, index: false } : f
|
|
176
|
-
)
|
|
177
|
-
};
|
|
178
|
-
await editModel(currentTestUser, testModelId, modelWithoutIndex);
|
|
179
|
-
|
|
180
|
-
// --- VERIFICATION 2 ---
|
|
181
|
-
const indexesAfterDeletion = await coll.indexes();
|
|
182
|
-
expect(indexesAfterDeletion.some(i => i.key[fieldToIndex] === 1)).toBe(false);
|
|
183
|
-
|
|
184
|
-
}, 20000);
|
|
185
|
-
|
|
186
|
-
it('should not save extra, non-defined fields in the model definition', async () => {
|
|
187
|
-
const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition } = await setupTestContext();
|
|
188
|
-
// 1. Préparer les données avec un champ non sollicité
|
|
189
|
-
const updatedModelData = {
|
|
190
|
-
...comprehensiveTestModelDefinition,
|
|
191
|
-
description: 'An updated description',
|
|
192
|
-
extraBogusField: 'this should not be saved', // Champ arbitraire
|
|
193
|
-
anotherOne: { nested: true }
|
|
194
|
-
};
|
|
195
|
-
// 2. Appeler la fonction d'édition
|
|
196
|
-
const result = await editModel(currentTestUser, testModelId, updatedModelData);
|
|
197
|
-
expect(result.success).toBe(false);
|
|
198
|
-
});
|
|
199
|
-
it('should return an error if trying to edit a non-existent model', async () => {
|
|
200
|
-
const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition } = await setupTestContext();
|
|
201
|
-
const nonExistentId = new ObjectId();
|
|
202
|
-
const result = await editModel(currentTestUser, nonExistentId, comprehensiveTestModelDefinition);
|
|
203
|
-
expect(result.success).toBe(false);
|
|
204
|
-
expect(result.statusCode).toBe(404);
|
|
205
|
-
expect(result.error).toContain('introuvable');
|
|
206
|
-
});
|
|
207
|
-
it('should return an error if the new model structure is invalid', async () => {
|
|
208
|
-
const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition } = await setupTestContext();
|
|
209
|
-
const invalidModelData = {
|
|
210
|
-
...comprehensiveTestModelDefinition,
|
|
211
|
-
fields: [
|
|
212
|
-
{ name: 'title' } // Le champ 'type' est manquant, ce qui est invalide
|
|
213
|
-
]
|
|
214
|
-
};
|
|
215
|
-
const result = await editModel(currentTestUser, testModelId, invalidModelData);
|
|
216
|
-
expect(result.success).toBe(false);
|
|
217
|
-
// L'erreur est levée par validateModelStructure, donc le message peut varier
|
|
218
|
-
expect(result.error).toBeDefined();
|
|
219
|
-
});
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
describe('Index Management via editModel', () => {
|
|
223
|
-
it('should create a regular index on a field', async () => {
|
|
224
|
-
const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition } = await setupTestContext();
|
|
225
|
-
const modelDef = {
|
|
226
|
-
name: 'indexedModel',
|
|
227
|
-
description: '',
|
|
228
|
-
_user: currentTestUser.username,
|
|
229
|
-
fields: [{ name: 'indexedField', type: 'string', index: true }]
|
|
230
|
-
};
|
|
231
|
-
const { insertedId } = await modelsCollection.insertOne(modelDef);
|
|
232
|
-
|
|
233
|
-
await editModel(currentTestUser, insertedId, modelDef);
|
|
234
|
-
|
|
235
|
-
const dataColl = await getCollectionForUser(currentTestUser);
|
|
236
|
-
const indexes = await dataColl.indexes();
|
|
237
|
-
console.log(indexes);
|
|
238
|
-
const createdIndex = indexes.find(idx => idx.key.indexedField === 1);
|
|
239
|
-
|
|
240
|
-
expect(createdIndex).toBeDefined();
|
|
241
|
-
expect(createdIndex.name).toBe('indexedField_regular_idx');
|
|
242
|
-
expect(createdIndex.partialFilterExpression).toEqual({
|
|
243
|
-
_model: 'indexedModel',
|
|
244
|
-
_user: currentTestUser.username
|
|
245
|
-
});
|
|
246
|
-
});
|
|
247
|
-
|
|
248
|
-
it('should create a 2dsphere index for geolocation fields', async () => {
|
|
249
|
-
const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition } = await setupTestContext();
|
|
250
|
-
const modelDef = {
|
|
251
|
-
name: 'geoModel',
|
|
252
|
-
description: '',
|
|
253
|
-
_user: currentTestUser.username,
|
|
254
|
-
fields: [{ name: 'location', type: 'geolocation', index: true, indexType: '2dsphere' }]
|
|
255
|
-
};
|
|
256
|
-
const { insertedId } = await modelsCollection.insertOne(modelDef);
|
|
257
|
-
|
|
258
|
-
await editModel(currentTestUser, insertedId, modelDef);
|
|
259
|
-
|
|
260
|
-
const dataColl = await getCollectionForUser(currentTestUser);
|
|
261
|
-
const indexes = await dataColl.indexes();
|
|
262
|
-
console.log(indexes);
|
|
263
|
-
const geoIndex = indexes.find(idx => idx.key.location === '2dsphere');
|
|
264
|
-
|
|
265
|
-
expect(geoIndex).toBeDefined();
|
|
266
|
-
expect(geoIndex.name).toBe('location_2dsphere_idx');
|
|
267
|
-
});
|
|
268
|
-
|
|
269
|
-
it('should create a single compound text index for multiple text fields', async () => {
|
|
270
|
-
const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition } = await setupTestContext();
|
|
271
|
-
const modelDef = {
|
|
272
|
-
name: 'textSearchModel',
|
|
273
|
-
description: '',
|
|
274
|
-
_user: currentTestUser.username,
|
|
275
|
-
fields: [
|
|
276
|
-
{ name: 'title', type: 'string', index: true, indexType: 'text' },
|
|
277
|
-
{ name: 'content', type: 'richtext', index: true, indexType: 'text' }
|
|
278
|
-
]
|
|
279
|
-
};
|
|
280
|
-
const { insertedId } = await modelsCollection.insertOne(modelDef);
|
|
281
|
-
|
|
282
|
-
await editModel(currentTestUser, insertedId, modelDef);
|
|
283
|
-
|
|
284
|
-
const dataColl = await getCollectionForUser(currentTestUser);
|
|
285
|
-
const indexes = await dataColl.indexes();
|
|
286
|
-
const textIndex = indexes.find(idx => idx.name === `_text_search_idx_${modelDef.name}`);
|
|
287
|
-
|
|
288
|
-
expect(textIndex).toBeDefined();
|
|
289
|
-
expect(textIndex.key._fts).toBe('text');
|
|
290
|
-
expect(textIndex.weights).toEqual({ title: 1, content: 1 });
|
|
291
|
-
});
|
|
292
|
-
|
|
293
|
-
it('should drop an index when a field is un-indexed', async () => {
|
|
294
|
-
const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition } = await setupTestContext();
|
|
295
|
-
const initialModelDef = {
|
|
296
|
-
name: 'toggleIndexModel',
|
|
297
|
-
description: '',
|
|
298
|
-
_user: currentTestUser.username,
|
|
299
|
-
fields: [{ name: 'tempIndexField', type: 'string', index: true }]
|
|
300
|
-
};
|
|
301
|
-
const { insertedId } = await modelsCollection.insertOne(initialModelDef);
|
|
302
|
-
await editModel(currentTestUser, insertedId, initialModelDef);
|
|
303
|
-
|
|
304
|
-
const dataColl = await getCollectionForUser(currentTestUser);
|
|
305
|
-
let indexes = await dataColl.indexes();
|
|
306
|
-
expect(indexes.some(idx => idx.key.tempIndexField === 1)).toBe(true);
|
|
307
|
-
|
|
308
|
-
const updatedModelDef = { ...initialModelDef, fields: [{ name: 'tempIndexField', type: 'string', index: false }] };
|
|
309
|
-
await editModel(currentTestUser, insertedId, updatedModelDef);
|
|
310
|
-
|
|
311
|
-
indexes = await dataColl.indexes();
|
|
312
|
-
expect(indexes.some(idx => idx.key.tempIndexField === 1)).toBe(false);
|
|
313
|
-
});
|
|
314
|
-
});
|
|
315
|
-
|
|
316
|
-
describe('Special Pipeline Execution via searchData', () => {
|
|
317
|
-
let currentTestUser;
|
|
318
|
-
|
|
319
|
-
beforeEach(async () => {
|
|
320
|
-
const context = await setupTestContext();
|
|
321
|
-
currentTestUser = context.currentTestUser;
|
|
322
|
-
const testModel = {
|
|
323
|
-
name: 'searchTestModel',
|
|
324
|
-
description: '',
|
|
325
|
-
_user: currentTestUser.username,
|
|
326
|
-
fields: [
|
|
327
|
-
{ name: 'name', type: 'string', index: true, indexType: 'text' },
|
|
328
|
-
{ name: 'description', type: 'string', index: true, indexType: 'text' },
|
|
329
|
-
{ name: 'value', type: 'number' },
|
|
330
|
-
{ name: 'location', type: 'geolocation', index: true, indexType: '2dsphere' }
|
|
331
|
-
]
|
|
332
|
-
};
|
|
333
|
-
const { insertedId } = await modelsCollection.insertOne(testModel);
|
|
334
|
-
await editModel(currentTestUser, insertedId, testModel);
|
|
335
|
-
|
|
336
|
-
const testData = [
|
|
337
|
-
{ _model: 'searchTestModel', _user: currentTestUser.username, name: 'First Item', description: 'A test document about MongoDB.', value: 10, location: { type: 'Point', coordinates: [-73.9667, 40.78] } },
|
|
338
|
-
{ _model: 'searchTestModel', _user: currentTestUser.username, name: 'Second TEST Item', description: 'Another document for testing.', value: 20, location: { type: 'Point', coordinates: [-74.0, 40.71] } },
|
|
339
|
-
{ _model: 'searchTestModel', _user: currentTestUser.username, name: 'Third Thing', description: 'Completely different.', value: 30, location: { type: 'Point', coordinates: [0, 0] } }
|
|
340
|
-
];
|
|
341
|
-
const dataColl = await getCollectionForUser(currentTestUser);
|
|
342
|
-
await dataColl.insertMany(testData);
|
|
343
|
-
await new Promise(resolve => setTimeout(resolve, 200)); // Allow indexes to build
|
|
344
|
-
});
|
|
345
|
-
|
|
346
|
-
it('should execute a $regex query correctly', async () => {
|
|
347
|
-
const { data, count } = await searchData({ model: 'searchTestModel', filter: { name: { $regex: 'item', $options: 'i' } } }, currentTestUser);
|
|
348
|
-
expect(count).toBe(2);
|
|
349
|
-
expect(data.some(d => d.name === 'First Item')).toBe(true);
|
|
350
|
-
});
|
|
351
|
-
|
|
352
|
-
it('should execute a $text search query correctly', async () => {
|
|
353
|
-
const { data, count } = await searchData({ model: 'searchTestModel', filter: { $text: { $search: 'mongodb' } } }, currentTestUser);
|
|
354
|
-
expect(count).toBe(1);
|
|
355
|
-
expect(data[0].name).toBe('First Item');
|
|
356
|
-
});
|
|
357
|
-
|
|
358
|
-
it('should execute a $nearSphere query correctly', async () => {
|
|
359
|
-
const { data, count } = await searchData({ model: 'searchTestModel', filter: { location: { $nearSphere: { $geometry: { type: 'Point', coordinates: [-73.9, 40.7] }, $maxDistance: 20000 } } } }, currentTestUser);
|
|
360
|
-
expect(count).toBe(2);
|
|
361
|
-
expect(data.some(d => d.name === 'Third Thing')).toBe(false);
|
|
362
|
-
});
|
|
363
|
-
|
|
364
|
-
it('should execute a $geoNear stage and sort by distance', async () => {
|
|
365
|
-
const { data, count } = await searchData({ model: 'searchTestModel', filter: { $geoNear: { near: { type: 'Point', coordinates: [-74.0, 40.71] }, distanceField: "dist.calculated", spherical: true } } }, currentTestUser);
|
|
366
|
-
expect(count).toBe(3);
|
|
367
|
-
expect(data[0].name).toBe('Second TEST Item');
|
|
368
|
-
expect(data[0].dist.calculated).toBe(0);
|
|
369
|
-
});
|
|
370
|
-
|
|
371
|
-
it('should handle a mix of special and standard operators in an $or clause', async () => {
|
|
372
|
-
const { data, count } = await searchData({ model: 'searchTestModel', filter: { $or: [{ name: { $regex: 'Third' } }, { value: { $gt: 15 } }] } }, currentTestUser);
|
|
373
|
-
expect(count).toBe(2);
|
|
374
|
-
expect(data.some(d => d.name === 'Second TEST Item')).toBe(true);
|
|
375
|
-
expect(data.some(d => d.name === 'Third Thing')).toBe(true);
|
|
376
|
-
});
|
|
377
|
-
});
|
|
1
|
+
// __tests__/data.integration.test.js
|
|
2
|
+
import { ObjectId } from 'mongodb';
|
|
3
|
+
import {expect,beforeEach, describe, it, beforeAll} from 'vitest';
|
|
4
|
+
import { Config } from '../src/config.js';
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
getCollection, modelsCollection // Accès direct pour vérifications
|
|
8
|
+
} from '../src/modules/mongodb.js';
|
|
9
|
+
import {generateUniqueName, initEngine} from "../src/setenv.js";
|
|
10
|
+
import {editModel, searchData} from "../src/index.js";
|
|
11
|
+
import {getCollectionForUser} from "../src/modules/mongodb.js";
|
|
12
|
+
import {purgeData} from "../src/modules/data/data.history.js";
|
|
13
|
+
|
|
14
|
+
let testModelsColInstance;
|
|
15
|
+
let testModelId;
|
|
16
|
+
// Cette fonction va remplacer la logique de votre beforeEach pour la création de contexte
|
|
17
|
+
async function setupTestContext() {
|
|
18
|
+
|
|
19
|
+
const currentTestModelName = generateUniqueName('relatedModel');
|
|
20
|
+
const currentRelatedModelName = generateUniqueName('comprehensiveModel');
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
// Créer un utilisateur unique pour ce test
|
|
24
|
+
const currentTestUser = {
|
|
25
|
+
username: generateUniqueName('testuserModelIntegration'),
|
|
26
|
+
userPlan: 'free',
|
|
27
|
+
email: generateUniqueName('test') + '@example.com'
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
testModelsColInstance = getCollection("models");
|
|
31
|
+
const testDatasColInstance = await getCollectionForUser(currentTestUser);
|
|
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
|
+
|
|
106
|
+
await purgeData(currentTestUser, comprehensiveTestModelDefinition.name);
|
|
107
|
+
await purgeData(currentTestUser, relatedModelDefinition.name);
|
|
108
|
+
|
|
109
|
+
await testDatasColInstance.deleteMany({ _user: currentTestUser.username });
|
|
110
|
+
await testDatasColInstance.deleteMany({ _model: { $in: [comprehensiveTestModelDefinition.name, 'renamedTestModel'] } });
|
|
111
|
+
// Retourner toutes les variables nécessaires pour un test
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
currentTestUser,
|
|
115
|
+
coll:testDatasColInstance,
|
|
116
|
+
comprehensiveTestModelDefinition,
|
|
117
|
+
relatedModelDefinition
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
describe('CRUD on model definitions and integrity tests', () => {
|
|
123
|
+
|
|
124
|
+
beforeAll(async () =>{
|
|
125
|
+
Config.Set("modules", ["mongodb", "data", "file", "bucket", "workflow","user", "assistant"]);
|
|
126
|
+
await initEngine();
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
describe('editModel unit tests', () => {
|
|
130
|
+
|
|
131
|
+
it('should create and drop index when field.index is toggled (premium user)', async () => {
|
|
132
|
+
// --- SETUP ---
|
|
133
|
+
const { coll, currentTestUser, comprehensiveTestModelDefinition } = await setupTestContext();
|
|
134
|
+
const fieldToIndex = 'stringUnique'; // Utiliser un champ qui existe vraiment dans le modèle
|
|
135
|
+
|
|
136
|
+
// --- FIX: Ensure the collection exists before any operation ---
|
|
137
|
+
// By inserting and deleting a dummy document, we force MongoDB to create the
|
|
138
|
+
// collection and its default indexes. This prevents the "ns does not exist"
|
|
139
|
+
// error in asynchronous listeners (like workflow triggers).
|
|
140
|
+
const dummyDoc = await coll.insertOne({ _model: 'dummy', _user: currentTestUser.username });
|
|
141
|
+
await coll.deleteOne({ _id: dummyDoc.insertedId });
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
// --- VERIFICATION INITIALE ---
|
|
145
|
+
// S'assurer qu'aucun index n'existe au départ.
|
|
146
|
+
// Cet appel ne plantera plus car la collection est maintenant créée.
|
|
147
|
+
const initialIndexes = await coll.indexes();
|
|
148
|
+
expect(initialIndexes.some(i => i.key[fieldToIndex] === 1)).toBe(false);
|
|
149
|
+
|
|
150
|
+
// --- ACTION 1 : AJOUTER UN INDEX ---
|
|
151
|
+
const modelWithIndex = {
|
|
152
|
+
...comprehensiveTestModelDefinition,
|
|
153
|
+
fields: comprehensiveTestModelDefinition.fields.map(f =>
|
|
154
|
+
f.name === fieldToIndex ? { ...f, index: true } : f
|
|
155
|
+
)
|
|
156
|
+
};
|
|
157
|
+
await editModel(currentTestUser, testModelId, modelWithIndex);
|
|
158
|
+
|
|
159
|
+
// --- VERIFICATION 1 ---
|
|
160
|
+
// Maintenant, la collection et l'index doivent exister.
|
|
161
|
+
const indexesAfterCreation = await coll.indexes();
|
|
162
|
+
const newIndex = indexesAfterCreation.find(i => i.key[fieldToIndex] === 1);
|
|
163
|
+
|
|
164
|
+
expect(newIndex).toBeDefined();
|
|
165
|
+
// Le filtre partiel est crucial pour que l'index ne s'applique qu'aux bonnes données
|
|
166
|
+
expect(newIndex.partialFilterExpression).toEqual({
|
|
167
|
+
_model: comprehensiveTestModelDefinition.name,
|
|
168
|
+
_user: currentTestUser.username
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// --- ACTION 2 : SUPPRIMER L'INDEX ---
|
|
172
|
+
const modelWithoutIndex = {
|
|
173
|
+
...comprehensiveTestModelDefinition,
|
|
174
|
+
fields: comprehensiveTestModelDefinition.fields.map(f =>
|
|
175
|
+
f.name === fieldToIndex ? { ...f, index: false } : f
|
|
176
|
+
)
|
|
177
|
+
};
|
|
178
|
+
await editModel(currentTestUser, testModelId, modelWithoutIndex);
|
|
179
|
+
|
|
180
|
+
// --- VERIFICATION 2 ---
|
|
181
|
+
const indexesAfterDeletion = await coll.indexes();
|
|
182
|
+
expect(indexesAfterDeletion.some(i => i.key[fieldToIndex] === 1)).toBe(false);
|
|
183
|
+
|
|
184
|
+
}, 20000);
|
|
185
|
+
|
|
186
|
+
it('should not save extra, non-defined fields in the model definition', async () => {
|
|
187
|
+
const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition } = await setupTestContext();
|
|
188
|
+
// 1. Préparer les données avec un champ non sollicité
|
|
189
|
+
const updatedModelData = {
|
|
190
|
+
...comprehensiveTestModelDefinition,
|
|
191
|
+
description: 'An updated description',
|
|
192
|
+
extraBogusField: 'this should not be saved', // Champ arbitraire
|
|
193
|
+
anotherOne: { nested: true }
|
|
194
|
+
};
|
|
195
|
+
// 2. Appeler la fonction d'édition
|
|
196
|
+
const result = await editModel(currentTestUser, testModelId, updatedModelData);
|
|
197
|
+
expect(result.success).toBe(false);
|
|
198
|
+
});
|
|
199
|
+
it('should return an error if trying to edit a non-existent model', async () => {
|
|
200
|
+
const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition } = await setupTestContext();
|
|
201
|
+
const nonExistentId = new ObjectId();
|
|
202
|
+
const result = await editModel(currentTestUser, nonExistentId, comprehensiveTestModelDefinition);
|
|
203
|
+
expect(result.success).toBe(false);
|
|
204
|
+
expect(result.statusCode).toBe(404);
|
|
205
|
+
expect(result.error).toContain('introuvable');
|
|
206
|
+
});
|
|
207
|
+
it('should return an error if the new model structure is invalid', async () => {
|
|
208
|
+
const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition } = await setupTestContext();
|
|
209
|
+
const invalidModelData = {
|
|
210
|
+
...comprehensiveTestModelDefinition,
|
|
211
|
+
fields: [
|
|
212
|
+
{ name: 'title' } // Le champ 'type' est manquant, ce qui est invalide
|
|
213
|
+
]
|
|
214
|
+
};
|
|
215
|
+
const result = await editModel(currentTestUser, testModelId, invalidModelData);
|
|
216
|
+
expect(result.success).toBe(false);
|
|
217
|
+
// L'erreur est levée par validateModelStructure, donc le message peut varier
|
|
218
|
+
expect(result.error).toBeDefined();
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
describe('Index Management via editModel', () => {
|
|
223
|
+
it('should create a regular index on a field', async () => {
|
|
224
|
+
const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition } = await setupTestContext();
|
|
225
|
+
const modelDef = {
|
|
226
|
+
name: 'indexedModel',
|
|
227
|
+
description: '',
|
|
228
|
+
_user: currentTestUser.username,
|
|
229
|
+
fields: [{ name: 'indexedField', type: 'string', index: true }]
|
|
230
|
+
};
|
|
231
|
+
const { insertedId } = await modelsCollection.insertOne(modelDef);
|
|
232
|
+
|
|
233
|
+
await editModel(currentTestUser, insertedId, modelDef);
|
|
234
|
+
|
|
235
|
+
const dataColl = await getCollectionForUser(currentTestUser);
|
|
236
|
+
const indexes = await dataColl.indexes();
|
|
237
|
+
console.log(indexes);
|
|
238
|
+
const createdIndex = indexes.find(idx => idx.key.indexedField === 1);
|
|
239
|
+
|
|
240
|
+
expect(createdIndex).toBeDefined();
|
|
241
|
+
expect(createdIndex.name).toBe('indexedField_regular_idx');
|
|
242
|
+
expect(createdIndex.partialFilterExpression).toEqual({
|
|
243
|
+
_model: 'indexedModel',
|
|
244
|
+
_user: currentTestUser.username
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it('should create a 2dsphere index for geolocation fields', async () => {
|
|
249
|
+
const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition } = await setupTestContext();
|
|
250
|
+
const modelDef = {
|
|
251
|
+
name: 'geoModel',
|
|
252
|
+
description: '',
|
|
253
|
+
_user: currentTestUser.username,
|
|
254
|
+
fields: [{ name: 'location', type: 'geolocation', index: true, indexType: '2dsphere' }]
|
|
255
|
+
};
|
|
256
|
+
const { insertedId } = await modelsCollection.insertOne(modelDef);
|
|
257
|
+
|
|
258
|
+
await editModel(currentTestUser, insertedId, modelDef);
|
|
259
|
+
|
|
260
|
+
const dataColl = await getCollectionForUser(currentTestUser);
|
|
261
|
+
const indexes = await dataColl.indexes();
|
|
262
|
+
console.log(indexes);
|
|
263
|
+
const geoIndex = indexes.find(idx => idx.key.location === '2dsphere');
|
|
264
|
+
|
|
265
|
+
expect(geoIndex).toBeDefined();
|
|
266
|
+
expect(geoIndex.name).toBe('location_2dsphere_idx');
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it('should create a single compound text index for multiple text fields', async () => {
|
|
270
|
+
const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition } = await setupTestContext();
|
|
271
|
+
const modelDef = {
|
|
272
|
+
name: 'textSearchModel',
|
|
273
|
+
description: '',
|
|
274
|
+
_user: currentTestUser.username,
|
|
275
|
+
fields: [
|
|
276
|
+
{ name: 'title', type: 'string', index: true, indexType: 'text' },
|
|
277
|
+
{ name: 'content', type: 'richtext', index: true, indexType: 'text' }
|
|
278
|
+
]
|
|
279
|
+
};
|
|
280
|
+
const { insertedId } = await modelsCollection.insertOne(modelDef);
|
|
281
|
+
|
|
282
|
+
await editModel(currentTestUser, insertedId, modelDef);
|
|
283
|
+
|
|
284
|
+
const dataColl = await getCollectionForUser(currentTestUser);
|
|
285
|
+
const indexes = await dataColl.indexes();
|
|
286
|
+
const textIndex = indexes.find(idx => idx.name === `_text_search_idx_${modelDef.name}`);
|
|
287
|
+
|
|
288
|
+
expect(textIndex).toBeDefined();
|
|
289
|
+
expect(textIndex.key._fts).toBe('text');
|
|
290
|
+
expect(textIndex.weights).toEqual({ title: 1, content: 1 });
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it('should drop an index when a field is un-indexed', async () => {
|
|
294
|
+
const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition } = await setupTestContext();
|
|
295
|
+
const initialModelDef = {
|
|
296
|
+
name: 'toggleIndexModel',
|
|
297
|
+
description: '',
|
|
298
|
+
_user: currentTestUser.username,
|
|
299
|
+
fields: [{ name: 'tempIndexField', type: 'string', index: true }]
|
|
300
|
+
};
|
|
301
|
+
const { insertedId } = await modelsCollection.insertOne(initialModelDef);
|
|
302
|
+
await editModel(currentTestUser, insertedId, initialModelDef);
|
|
303
|
+
|
|
304
|
+
const dataColl = await getCollectionForUser(currentTestUser);
|
|
305
|
+
let indexes = await dataColl.indexes();
|
|
306
|
+
expect(indexes.some(idx => idx.key.tempIndexField === 1)).toBe(true);
|
|
307
|
+
|
|
308
|
+
const updatedModelDef = { ...initialModelDef, fields: [{ name: 'tempIndexField', type: 'string', index: false }] };
|
|
309
|
+
await editModel(currentTestUser, insertedId, updatedModelDef);
|
|
310
|
+
|
|
311
|
+
indexes = await dataColl.indexes();
|
|
312
|
+
expect(indexes.some(idx => idx.key.tempIndexField === 1)).toBe(false);
|
|
313
|
+
});
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
describe('Special Pipeline Execution via searchData', () => {
|
|
317
|
+
let currentTestUser;
|
|
318
|
+
|
|
319
|
+
beforeEach(async () => {
|
|
320
|
+
const context = await setupTestContext();
|
|
321
|
+
currentTestUser = context.currentTestUser;
|
|
322
|
+
const testModel = {
|
|
323
|
+
name: 'searchTestModel',
|
|
324
|
+
description: '',
|
|
325
|
+
_user: currentTestUser.username,
|
|
326
|
+
fields: [
|
|
327
|
+
{ name: 'name', type: 'string', index: true, indexType: 'text' },
|
|
328
|
+
{ name: 'description', type: 'string', index: true, indexType: 'text' },
|
|
329
|
+
{ name: 'value', type: 'number' },
|
|
330
|
+
{ name: 'location', type: 'geolocation', index: true, indexType: '2dsphere' }
|
|
331
|
+
]
|
|
332
|
+
};
|
|
333
|
+
const { insertedId } = await modelsCollection.insertOne(testModel);
|
|
334
|
+
await editModel(currentTestUser, insertedId, testModel);
|
|
335
|
+
|
|
336
|
+
const testData = [
|
|
337
|
+
{ _model: 'searchTestModel', _user: currentTestUser.username, name: 'First Item', description: 'A test document about MongoDB.', value: 10, location: { type: 'Point', coordinates: [-73.9667, 40.78] } },
|
|
338
|
+
{ _model: 'searchTestModel', _user: currentTestUser.username, name: 'Second TEST Item', description: 'Another document for testing.', value: 20, location: { type: 'Point', coordinates: [-74.0, 40.71] } },
|
|
339
|
+
{ _model: 'searchTestModel', _user: currentTestUser.username, name: 'Third Thing', description: 'Completely different.', value: 30, location: { type: 'Point', coordinates: [0, 0] } }
|
|
340
|
+
];
|
|
341
|
+
const dataColl = await getCollectionForUser(currentTestUser);
|
|
342
|
+
await dataColl.insertMany(testData);
|
|
343
|
+
await new Promise(resolve => setTimeout(resolve, 200)); // Allow indexes to build
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
it('should execute a $regex query correctly', async () => {
|
|
347
|
+
const { data, count } = await searchData({ model: 'searchTestModel', filter: { name: { $regex: 'item', $options: 'i' } } }, currentTestUser);
|
|
348
|
+
expect(count).toBe(2);
|
|
349
|
+
expect(data.some(d => d.name === 'First Item')).toBe(true);
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
it('should execute a $text search query correctly', async () => {
|
|
353
|
+
const { data, count } = await searchData({ model: 'searchTestModel', filter: { $text: { $search: 'mongodb' } } }, currentTestUser);
|
|
354
|
+
expect(count).toBe(1);
|
|
355
|
+
expect(data[0].name).toBe('First Item');
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
it('should execute a $nearSphere query correctly', async () => {
|
|
359
|
+
const { data, count } = await searchData({ model: 'searchTestModel', filter: { location: { $nearSphere: { $geometry: { type: 'Point', coordinates: [-73.9, 40.7] }, $maxDistance: 20000 } } } }, currentTestUser);
|
|
360
|
+
expect(count).toBe(2);
|
|
361
|
+
expect(data.some(d => d.name === 'Third Thing')).toBe(false);
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
it('should execute a $geoNear stage and sort by distance', async () => {
|
|
365
|
+
const { data, count } = await searchData({ model: 'searchTestModel', filter: { $geoNear: { near: { type: 'Point', coordinates: [-74.0, 40.71] }, distanceField: "dist.calculated", spherical: true } } }, currentTestUser);
|
|
366
|
+
expect(count).toBe(3);
|
|
367
|
+
expect(data[0].name).toBe('Second TEST Item');
|
|
368
|
+
expect(data[0].dist.calculated).toBe(0);
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
it('should handle a mix of special and standard operators in an $or clause', async () => {
|
|
372
|
+
const { data, count } = await searchData({ model: 'searchTestModel', filter: { $or: [{ name: { $regex: 'Third' } }, { value: { $gt: 15 } }] } }, currentTestUser);
|
|
373
|
+
expect(count).toBe(2);
|
|
374
|
+
expect(data.some(d => d.name === 'Second TEST Item')).toBe(true);
|
|
375
|
+
expect(data.some(d => d.name === 'Third Thing')).toBe(true);
|
|
376
|
+
});
|
|
377
|
+
});
|
|
378
378
|
});
|