data-primals-engine 1.4.1 → 1.4.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.
Files changed (49) hide show
  1. package/README.md +37 -22
  2. package/client/package-lock.json +33 -0
  3. package/client/package.json +1 -0
  4. package/client/src/App.scss +12 -4
  5. package/client/src/AssistantChat.jsx +48 -5
  6. package/client/src/AssistantChat.scss +0 -1
  7. package/client/src/ChartConfigModal.jsx +34 -9
  8. package/client/src/ConditionBuilder.jsx +1 -1
  9. package/client/src/ConditionBuilder2.jsx +2 -1
  10. package/client/src/Dashboard.jsx +396 -349
  11. package/client/src/DashboardChart.jsx +91 -12
  12. package/client/src/DataEditor.jsx +376 -368
  13. package/client/src/DataLayout.jsx +3 -2
  14. package/client/src/DataTable.jsx +60 -31
  15. package/client/src/Field.jsx +1788 -1782
  16. package/client/src/GeolocationField.jsx +94 -0
  17. package/client/src/ModelCreatorField.jsx +1 -0
  18. package/client/src/RelationField.jsx +2 -2
  19. package/client/src/constants.js +2 -2
  20. package/client/src/contexts/UIContext.jsx +5 -2
  21. package/client/src/filter.js +0 -155
  22. package/client/src/translations.js +16828 -16750
  23. package/package.json +10 -2
  24. package/src/config.js +2 -2
  25. package/src/constants.js +262 -4
  26. package/src/core.js +21 -3
  27. package/src/defaultModels.js +12 -12
  28. package/src/engine.js +7 -1
  29. package/src/events.js +4 -3
  30. package/src/filter.js +272 -260
  31. package/src/i18n.js +187 -177
  32. package/src/middlewares/middleware-mongodb.js +3 -1
  33. package/src/modules/assistant/assistant.js +131 -42
  34. package/src/modules/bucket.js +3 -2
  35. package/src/modules/data/data.backup.js +374 -0
  36. package/src/modules/data/data.core.js +2 -1
  37. package/src/modules/data/data.history.js +11 -8
  38. package/src/modules/data/data.js +190 -4551
  39. package/src/modules/data/data.operations.js +3000 -0
  40. package/src/modules/data/data.relations.js +687 -0
  41. package/src/modules/data/data.routes.js +132 -38
  42. package/src/modules/data/data.scheduling.js +274 -0
  43. package/src/modules/data/data.validation.js +248 -0
  44. package/src/modules/data/index.js +29 -1
  45. package/src/modules/user.js +2 -1
  46. package/src/modules/workflow.js +3 -2
  47. package/src/services/stripe.js +3 -2
  48. package/swagger-en.yml +133 -0
  49. package/test/model.integration.test.js +377 -221
@@ -1,222 +1,378 @@
1
- // __tests__/data.integration.test.js
2
- import { ObjectId } from 'mongodb';
3
- import {expect, describe, it, beforeAll} from 'vitest';
4
- import { Config } from '../src/config.js';
5
-
6
- import {
7
- getCollection // Accès direct pour vérifications
8
- } from '../src/modules/mongodb.js';
9
- import {generateUniqueName, initEngine} from "../src/setenv.js";
10
- import {editModel} 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
-
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
+ });
222
378
  });