data-primals-engine 1.5.2 → 1.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/README.md +938 -915
  2. package/client/README.md +136 -136
  3. package/client/index.js +0 -1
  4. package/client/public/doc/API.postman_collection.json +213 -213
  5. package/client/public/react.svg +9 -9
  6. package/client/src/AddWidgetTypeModal.jsx +47 -47
  7. package/client/src/App.css +42 -42
  8. package/client/src/App.jsx +92 -150
  9. package/client/src/App.scss +6 -0
  10. package/client/src/AssistantChat.jsx +362 -363
  11. package/client/src/Button.jsx +12 -12
  12. package/client/src/Dashboard.jsx +480 -480
  13. package/client/src/DashboardHtmlViewItem.jsx +146 -146
  14. package/client/src/DashboardView.jsx +654 -654
  15. package/client/src/DataEditor.jsx +383 -383
  16. package/client/src/DataLayout.jsx +779 -808
  17. package/client/src/DataTable.jsx +782 -822
  18. package/client/src/DataTable.scss +186 -186
  19. package/client/src/GeolocationField.jsx +93 -93
  20. package/client/src/HistoryDialog.jsx +307 -307
  21. package/client/src/HistoryDialog.scss +319 -319
  22. package/client/src/HtmlViewBuilderModal.jsx +90 -90
  23. package/client/src/HtmlViewBuilderModal.scss +17 -17
  24. package/client/src/HtmlViewCard.jsx +43 -43
  25. package/client/src/ModelCreator.jsx +683 -686
  26. package/client/src/ModelCreator.scss +1 -1
  27. package/client/src/ModelCreatorField.jsx +950 -950
  28. package/client/src/ModelImporter.jsx +3 -0
  29. package/client/src/ModelList.jsx +280 -280
  30. package/client/src/Notification.jsx +136 -136
  31. package/client/src/PackGallery.jsx +391 -391
  32. package/client/src/PackGallery.scss +231 -231
  33. package/client/src/Pagination.jsx +143 -143
  34. package/client/src/RelationField.jsx +354 -354
  35. package/client/src/RelationSelectorWidget.jsx +172 -172
  36. package/client/src/RelationValue.jsx +2 -1
  37. package/client/src/contexts/CommandContext.jsx +260 -0
  38. package/client/src/contexts/ModelContext.jsx +4 -1
  39. package/client/src/contexts/UIContext.jsx +72 -72
  40. package/client/src/filter.js +263 -263
  41. package/client/src/index.css +90 -90
  42. package/package.json +6 -5
  43. package/perf/artillery-hooks.js +37 -37
  44. package/perf/setup.yml +25 -25
  45. package/src/constants.js +4 -0
  46. package/src/defaultModels.js +7 -0
  47. package/src/engine.js +335 -335
  48. package/src/events.js +232 -137
  49. package/src/filter.js +274 -274
  50. package/src/index.js +1 -0
  51. package/src/modules/assistant/assistant.js +225 -83
  52. package/src/modules/auth-google/index.js +50 -50
  53. package/src/modules/auth-microsoft/index.js +81 -81
  54. package/src/modules/data/data.core.js +118 -118
  55. package/src/modules/data/data.history.js +555 -555
  56. package/src/modules/data/data.operations.js +112 -9
  57. package/src/modules/data/data.relations.js +686 -686
  58. package/src/modules/data/data.routes.js +1879 -1879
  59. package/src/modules/data/data.validation.js +2 -2
  60. package/src/modules/file.js +247 -247
  61. package/src/modules/user.js +14 -9
  62. package/src/packs.js +2 -2
  63. package/src/providers.js +2 -2
  64. package/src/services/stripe.js +196 -196
  65. package/src/sso.js +193 -193
  66. package/test/data.history.integration.test.js +264 -264
  67. package/test/data.integration.test.js +1281 -1206
  68. package/test/events.test.js +108 -10
  69. package/test/model.integration.test.js +377 -377
  70. package/test/user.test.js +106 -1
@@ -1,265 +1,265 @@
1
- // __tests__/data.history.integration.test.js
2
- import { ObjectId } from 'mongodb';
3
- import { expect, describe, it, beforeAll, afterAll, beforeEach } from 'vitest';
4
- import { handleGetHistoryRequest } from '../src/modules/data/data.history.js';
5
- import { Config } from '../src/config.js';
6
- import { sleep } from '../src/core.js';
7
- import { insertData, editData, deleteModels } from '../src/index.js';
8
- import { getCollection, getCollectionForUser } from '../src/modules/mongodb.js';
9
- import { generateUniqueName, initEngine } from "../src/setenv.js";
10
- import {purgeData} from "../src/modules/data/data.history.js";
11
- import {MongoDatabase} from "../src/engine.js";
12
-
13
- let engine;
14
- let testUser;
15
- let historyCollection;
16
- let datasCollection;
17
- let modelsCollection;
18
-
19
- describe('Data History Module Integration Tests', () => {
20
-
21
- beforeAll(async () => {
22
- // IMPORTANT: Add the history module to the engine configuration for tests
23
- Config.Set("modules", ["mongodb", "data", "file", "bucket", "workflow", "user", "assistant"]);
24
- engine = await initEngine();
25
-
26
- // Setup a single user for all tests in this suite
27
- testUser = {
28
- username: generateUniqueName('testUserHistory'),
29
- userPlan: 'free',
30
- email: generateUniqueName('test') + '@example.com'
31
- };
32
-
33
- // Initialize collection instances
34
- historyCollection = getCollection('history');
35
- datasCollection = await getCollectionForUser(testUser);
36
- modelsCollection = getCollection('models');
37
- });
38
-
39
- afterAll(async () =>{
40
- await purgeData(testUser);
41
- await datasCollection.drop();
42
- })
43
-
44
- // Clean up collections before each test to ensure isolation
45
- beforeEach(async () => {
46
- await historyCollection.deleteMany({ 'user.username': testUser.username });
47
- await datasCollection.deleteMany({ _user: testUser.username });
48
- await modelsCollection.deleteMany({ _user: testUser.username });
49
- });
50
-
51
- afterAll(async () => {
52
- // Final cleanup
53
- await historyCollection.deleteMany({ 'user.username': testUser.username });
54
- await datasCollection.deleteMany({ _user: testUser.username });
55
- await modelsCollection.deleteMany({ _user: testUser.username });
56
- });
57
-
58
- it('should create a full snapshot history record on document creation', async () => {
59
- // 1. Define and create a model with history enabled
60
- const modelName = generateUniqueName('productHistory');
61
- const productModelDef = {
62
- name: modelName,
63
- description:"",
64
- _user: testUser.username,
65
- history: {
66
- enabled: true,
67
- // For creation, all fields are snapshotted regardless of this config
68
- fields: {
69
- price: true,
70
- stock: true
71
- }
72
- },
73
- fields: [
74
- { name: 'name', type: 'string', required: true },
75
- { name: 'price', type: 'number' },
76
- { name: 'stock', type: 'number' },
77
- { name: 'description', type: 'string' }
78
- ]
79
- };
80
- await modelsCollection.insertOne(productModelDef);
81
-
82
- // 2. Insert a new document
83
- const initialData = {
84
- name: 'Super Widget',
85
- price: 99.99,
86
- stock: 100,
87
- description: 'A very super widget.'
88
- };
89
- const insertResult = await insertData(modelName, initialData, {}, testUser);
90
- expect(insertResult.success).toBe(true);
91
- const docId = new ObjectId(insertResult.insertedIds[0]);
92
-
93
- // 3. Verify the history record
94
- const historyRecord = await historyCollection.findOne({ documentId: new ObjectId(docId) });
95
-
96
- expect(historyRecord).not.toBeNull();
97
- expect(historyRecord.documentId.toString()).toBe(docId.toString());
98
- expect(historyRecord.model).toBe(modelName);
99
- expect(historyRecord.version).toBe(1);
100
- expect(historyRecord.operation).toBe('create');
101
- expect(historyRecord.user.username).toBe(testUser.username);
102
-
103
- // 4. Verify the snapshot
104
- expect(historyRecord.snapshot).not.toBeNull();
105
- expect(historyRecord.snapshot.name).toBe('Super Widget');
106
- expect(historyRecord.snapshot.price).toBe(99.99);
107
- expect(historyRecord.snapshot.stock).toBe(100);
108
- expect(historyRecord.snapshot.description).toBe('A very super widget.');
109
- expect(historyRecord.changes).toBeUndefined(); // No 'changes' field on creation
110
- });
111
-
112
- it('should create a diff history record on document update for historized fields only', async () => {
113
- // 1. Define and create a model with specific history fields
114
- const modelName = generateUniqueName('productHistorySelective');
115
- const productModelDef = {
116
- name: modelName,
117
- description:"",
118
- _user: testUser.username,
119
- history: {
120
- enabled: true,
121
- fields: {
122
- price: true, // Track this
123
- stock: true // Track this
124
- // 'description' is NOT tracked
125
- }
126
- },
127
- fields: [
128
- { name: 'name', type: 'string', required: true },
129
- { name: 'price', type: 'number' },
130
- { name: 'stock', type: 'number' },
131
- { name: 'description', type: 'string' }
132
- ]
133
- };
134
- await modelsCollection.insertOne(productModelDef);
135
-
136
- // 2. Insert the initial document
137
- const initialData = { name: 'Selective Widget', price: 50, stock: 200, description: 'Initial description.' };
138
- const insertResult = await insertData(modelName, initialData, {}, testUser);
139
- const docId = new ObjectId(insertResult.insertedIds[0]);
140
-
141
- // 3. Edit the document: change one historized field and one non-historized field
142
- const updateData = { price: 55.5, description: 'Updated description.' };
143
- const editResult = await editData(modelName, { _id: docId }, updateData, {}, testUser);
144
- expect(editResult.success).toBe(true);
145
-
146
- // 4. Verify the new history record (v2)
147
- const historyRecord = await historyCollection.findOne({ documentId: docId, version: 2 });
148
-
149
- expect(historyRecord).not.toBeNull();
150
- expect(historyRecord.operation).toBe('update');
151
- expect(historyRecord.snapshot).toBeUndefined(); // No 'snapshot' on update
152
-
153
- // 5. Verify the 'changes' object
154
- const changes = historyRecord.changes;
155
- expect(changes).not.toBeNull();
156
- expect(changes.price).toBeDefined();
157
- expect(changes.price.from).toBe(50);
158
- expect(changes.price.to).toBe(55.5);
159
- expect(changes.description).toBeUndefined();
160
- expect(changes.stock).toBeUndefined();
161
- });
162
-
163
- it('should NOT create a history record if only non-historized fields are updated', async () => {
164
- // 1. Setup model
165
- const modelName = generateUniqueName('productHistoryNoOp');
166
- const productModelDef = {
167
- name: modelName,
168
- description:"",
169
- _user: testUser.username,
170
- history: { enabled: true, fields: { price: true } },
171
- fields: [
172
- { name: 'name', type: 'string', required: true },
173
- { name: 'price', type: 'number' },
174
- { name: 'description', type: 'string' }
175
- ]
176
- };
177
- await modelsCollection.insertOne(productModelDef);
178
-
179
- // 2. Insert initial document
180
- const initialData = { name: 'No-Op Widget', price: 10, description: 'Initial.' };
181
- const insertResult = await insertData(modelName, initialData, {}, testUser);
182
- const docId = new ObjectId(insertResult.insertedIds[0]);
183
-
184
- // 3. Edit ONLY a non-historized field
185
- const updateData = { description: 'This change should not be recorded.' };
186
- await editData(modelName, { _id: docId }, updateData, {}, testUser);
187
-
188
- await sleep(2000);
189
-
190
- // 4. Verify that NO new history record was created (only the 'create' record exists)
191
- const historyCount = await historyCollection.countDocuments({ documentId: docId });
192
- expect(historyCount).toBe(1);
193
- });
194
-
195
- it('should filter history records by date range', async () => {
196
- // 1. Setup model
197
- const modelName = generateUniqueName('productHistoryDateFilter');
198
- const productModelDef = {
199
- name: modelName,
200
- description: "",
201
- _user: testUser.username,
202
- history: { enabled: true },
203
- fields: [{ name: 'name', type: 'string' }]
204
- };
205
- await modelsCollection.insertOne(productModelDef);
206
-
207
- // 2. Insert initial document
208
- const insertResult = await insertData(modelName, { name: 'Time-traveling Widget' }, {}, testUser);
209
- const docId = new ObjectId(insertResult.insertedIds[0]);
210
-
211
- // 3. Create history entries at different times
212
- // To simulate different timestamps, we'll manually insert history records
213
- // as editData() would create them too close together in time.
214
- await historyCollection.updateOne({ documentId: docId, version: 1 }, { $set: { timestamp: new Date('2023-01-10T10:00:00Z') } });
215
-
216
- await historyCollection.insertOne({
217
- documentId: docId, model: modelName, version: 2, operation: 'update',
218
- timestamp: new Date('2023-02-15T12:00:00Z'), user: { username: testUser.username }, changes: { name: { from: 'v1', to: 'v2' } }
219
- });
220
- await historyCollection.insertOne({
221
- documentId: docId, model: modelName, version: 3, operation: 'update',
222
- timestamp: new Date('2023-02-20T14:00:00Z'), user: { username: testUser.username }, changes: { name: { from: 'v2', to: 'v3' } }
223
- });
224
- await historyCollection.insertOne({
225
- documentId: docId, model: modelName, version: 4, operation: 'update',
226
- timestamp: new Date('2023-03-05T16:00:00Z'), user: { username: testUser.username }, changes: { name: { from: 'v3', to: 'v4' } }
227
- });
228
-
229
- // Mock Express req/res objects
230
- const mockReq = (query) => ({
231
- params: { modelName, recordId: docId.toString() },
232
- query,
233
- me: testUser
234
- });
235
- const mockRes = () => {
236
- const res = {};
237
- res.status = (code) => { res.statusCode = code; return res; };
238
- res.json = (data) => { res.body = data; return res; };
239
- return res;
240
- };
241
-
242
- // 4. Test cases
243
- // Case A: Filter for February
244
- let req = mockReq({ startDate: '2023-02-01', endDate: '2023-02-28' });
245
- let res = mockRes();
246
- await handleGetHistoryRequest(req, res);
247
- expect(res.body.success).toBe(true);
248
- expect(res.body.count).toBe(2);
249
- expect(res.body.data.map(d => d._v).sort()).toEqual([2, 3]);
250
-
251
- // Case B: Filter starting from Feb 15th
252
- req = mockReq({ startDate: '2023-02-15' });
253
- res = mockRes();
254
- await handleGetHistoryRequest(req, res);
255
- expect(res.body.success).toBe(true);
256
- expect(res.body.count).toBe(3); // v2, v3, v4
257
-
258
- // Case C: Filter up to Feb 15th (inclusive)
259
- req = mockReq({ endDate: '2023-02-15' });
260
- res = mockRes();
261
- await handleGetHistoryRequest(req, res);
262
- expect(res.body.success).toBe(true);
263
- expect(res.body.count).toBe(2); // v1, v2
264
- });
1
+ // __tests__/data.history.integration.test.js
2
+ import { ObjectId } from 'mongodb';
3
+ import { expect, describe, it, beforeAll, afterAll, beforeEach } from 'vitest';
4
+ import { handleGetHistoryRequest } from '../src/modules/data/data.history.js';
5
+ import { Config } from '../src/config.js';
6
+ import { sleep } from '../src/core.js';
7
+ import { insertData, editData, deleteModels } from '../src/index.js';
8
+ import { getCollection, getCollectionForUser } from '../src/modules/mongodb.js';
9
+ import { generateUniqueName, initEngine } from "../src/setenv.js";
10
+ import {purgeData} from "../src/modules/data/data.history.js";
11
+ import {MongoDatabase} from "../src/engine.js";
12
+
13
+ let engine;
14
+ let testUser;
15
+ let historyCollection;
16
+ let datasCollection;
17
+ let modelsCollection;
18
+
19
+ describe('Data History Module Integration Tests', () => {
20
+
21
+ beforeAll(async () => {
22
+ // IMPORTANT: Add the history module to the engine configuration for tests
23
+ Config.Set("modules", ["mongodb", "data", "file", "bucket", "workflow", "user", "assistant"]);
24
+ engine = await initEngine();
25
+
26
+ // Setup a single user for all tests in this suite
27
+ testUser = {
28
+ username: generateUniqueName('testUserHistory'),
29
+ userPlan: 'free',
30
+ email: generateUniqueName('test') + '@example.com'
31
+ };
32
+
33
+ // Initialize collection instances
34
+ historyCollection = getCollection('history');
35
+ datasCollection = await getCollectionForUser(testUser);
36
+ modelsCollection = getCollection('models');
37
+ });
38
+
39
+ afterAll(async () =>{
40
+ await purgeData(testUser);
41
+ await datasCollection.drop();
42
+ })
43
+
44
+ // Clean up collections before each test to ensure isolation
45
+ beforeEach(async () => {
46
+ await historyCollection.deleteMany({ 'user.username': testUser.username });
47
+ await datasCollection.deleteMany({ _user: testUser.username });
48
+ await modelsCollection.deleteMany({ _user: testUser.username });
49
+ });
50
+
51
+ afterAll(async () => {
52
+ // Final cleanup
53
+ await historyCollection.deleteMany({ 'user.username': testUser.username });
54
+ await datasCollection.deleteMany({ _user: testUser.username });
55
+ await modelsCollection.deleteMany({ _user: testUser.username });
56
+ });
57
+
58
+ it('should create a full snapshot history record on document creation', async () => {
59
+ // 1. Define and create a model with history enabled
60
+ const modelName = generateUniqueName('productHistory');
61
+ const productModelDef = {
62
+ name: modelName,
63
+ description:"",
64
+ _user: testUser.username,
65
+ history: {
66
+ enabled: true,
67
+ // For creation, all fields are snapshotted regardless of this config
68
+ fields: {
69
+ price: true,
70
+ stock: true
71
+ }
72
+ },
73
+ fields: [
74
+ { name: 'name', type: 'string', required: true },
75
+ { name: 'price', type: 'number' },
76
+ { name: 'stock', type: 'number' },
77
+ { name: 'description', type: 'string' }
78
+ ]
79
+ };
80
+ await modelsCollection.insertOne(productModelDef);
81
+
82
+ // 2. Insert a new document
83
+ const initialData = {
84
+ name: 'Super Widget',
85
+ price: 99.99,
86
+ stock: 100,
87
+ description: 'A very super widget.'
88
+ };
89
+ const insertResult = await insertData(modelName, initialData, {}, testUser);
90
+ expect(insertResult.success).toBe(true);
91
+ const docId = new ObjectId(insertResult.insertedIds[0]);
92
+
93
+ // 3. Verify the history record
94
+ const historyRecord = await historyCollection.findOne({ documentId: new ObjectId(docId) });
95
+
96
+ expect(historyRecord).not.toBeNull();
97
+ expect(historyRecord.documentId.toString()).toBe(docId.toString());
98
+ expect(historyRecord.model).toBe(modelName);
99
+ expect(historyRecord.version).toBe(1);
100
+ expect(historyRecord.operation).toBe('create');
101
+ expect(historyRecord.user.username).toBe(testUser.username);
102
+
103
+ // 4. Verify the snapshot
104
+ expect(historyRecord.snapshot).not.toBeNull();
105
+ expect(historyRecord.snapshot.name).toBe('Super Widget');
106
+ expect(historyRecord.snapshot.price).toBe(99.99);
107
+ expect(historyRecord.snapshot.stock).toBe(100);
108
+ expect(historyRecord.snapshot.description).toBe('A very super widget.');
109
+ expect(historyRecord.changes).toBeUndefined(); // No 'changes' field on creation
110
+ });
111
+
112
+ it('should create a diff history record on document update for historized fields only', async () => {
113
+ // 1. Define and create a model with specific history fields
114
+ const modelName = generateUniqueName('productHistorySelective');
115
+ const productModelDef = {
116
+ name: modelName,
117
+ description:"",
118
+ _user: testUser.username,
119
+ history: {
120
+ enabled: true,
121
+ fields: {
122
+ price: true, // Track this
123
+ stock: true // Track this
124
+ // 'description' is NOT tracked
125
+ }
126
+ },
127
+ fields: [
128
+ { name: 'name', type: 'string', required: true },
129
+ { name: 'price', type: 'number' },
130
+ { name: 'stock', type: 'number' },
131
+ { name: 'description', type: 'string' }
132
+ ]
133
+ };
134
+ await modelsCollection.insertOne(productModelDef);
135
+
136
+ // 2. Insert the initial document
137
+ const initialData = { name: 'Selective Widget', price: 50, stock: 200, description: 'Initial description.' };
138
+ const insertResult = await insertData(modelName, initialData, {}, testUser);
139
+ const docId = new ObjectId(insertResult.insertedIds[0]);
140
+
141
+ // 3. Edit the document: change one historized field and one non-historized field
142
+ const updateData = { price: 55.5, description: 'Updated description.' };
143
+ const editResult = await editData(modelName, { _id: docId }, updateData, {}, testUser);
144
+ expect(editResult.success).toBe(true);
145
+
146
+ // 4. Verify the new history record (v2)
147
+ const historyRecord = await historyCollection.findOne({ documentId: docId, version: 2 });
148
+
149
+ expect(historyRecord).not.toBeNull();
150
+ expect(historyRecord.operation).toBe('update');
151
+ expect(historyRecord.snapshot).toBeUndefined(); // No 'snapshot' on update
152
+
153
+ // 5. Verify the 'changes' object
154
+ const changes = historyRecord.changes;
155
+ expect(changes).not.toBeNull();
156
+ expect(changes.price).toBeDefined();
157
+ expect(changes.price.from).toBe(50);
158
+ expect(changes.price.to).toBe(55.5);
159
+ expect(changes.description).toBeUndefined();
160
+ expect(changes.stock).toBeUndefined();
161
+ });
162
+
163
+ it('should NOT create a history record if only non-historized fields are updated', async () => {
164
+ // 1. Setup model
165
+ const modelName = generateUniqueName('productHistoryNoOp');
166
+ const productModelDef = {
167
+ name: modelName,
168
+ description:"",
169
+ _user: testUser.username,
170
+ history: { enabled: true, fields: { price: true } },
171
+ fields: [
172
+ { name: 'name', type: 'string', required: true },
173
+ { name: 'price', type: 'number' },
174
+ { name: 'description', type: 'string' }
175
+ ]
176
+ };
177
+ await modelsCollection.insertOne(productModelDef);
178
+
179
+ // 2. Insert initial document
180
+ const initialData = { name: 'No-Op Widget', price: 10, description: 'Initial.' };
181
+ const insertResult = await insertData(modelName, initialData, {}, testUser);
182
+ const docId = new ObjectId(insertResult.insertedIds[0]);
183
+
184
+ // 3. Edit ONLY a non-historized field
185
+ const updateData = { description: 'This change should not be recorded.' };
186
+ await editData(modelName, { _id: docId }, updateData, {}, testUser);
187
+
188
+ await sleep(2000);
189
+
190
+ // 4. Verify that NO new history record was created (only the 'create' record exists)
191
+ const historyCount = await historyCollection.countDocuments({ documentId: docId });
192
+ expect(historyCount).toBe(1);
193
+ });
194
+
195
+ it('should filter history records by date range', async () => {
196
+ // 1. Setup model
197
+ const modelName = generateUniqueName('productHistoryDateFilter');
198
+ const productModelDef = {
199
+ name: modelName,
200
+ description: "",
201
+ _user: testUser.username,
202
+ history: { enabled: true },
203
+ fields: [{ name: 'name', type: 'string' }]
204
+ };
205
+ await modelsCollection.insertOne(productModelDef);
206
+
207
+ // 2. Insert initial document
208
+ const insertResult = await insertData(modelName, { name: 'Time-traveling Widget' }, {}, testUser);
209
+ const docId = new ObjectId(insertResult.insertedIds[0]);
210
+
211
+ // 3. Create history entries at different times
212
+ // To simulate different timestamps, we'll manually insert history records
213
+ // as editData() would create them too close together in time.
214
+ await historyCollection.updateOne({ documentId: docId, version: 1 }, { $set: { timestamp: new Date('2023-01-10T10:00:00Z') } });
215
+
216
+ await historyCollection.insertOne({
217
+ documentId: docId, model: modelName, version: 2, operation: 'update',
218
+ timestamp: new Date('2023-02-15T12:00:00Z'), user: { username: testUser.username }, changes: { name: { from: 'v1', to: 'v2' } }
219
+ });
220
+ await historyCollection.insertOne({
221
+ documentId: docId, model: modelName, version: 3, operation: 'update',
222
+ timestamp: new Date('2023-02-20T14:00:00Z'), user: { username: testUser.username }, changes: { name: { from: 'v2', to: 'v3' } }
223
+ });
224
+ await historyCollection.insertOne({
225
+ documentId: docId, model: modelName, version: 4, operation: 'update',
226
+ timestamp: new Date('2023-03-05T16:00:00Z'), user: { username: testUser.username }, changes: { name: { from: 'v3', to: 'v4' } }
227
+ });
228
+
229
+ // Mock Express req/res objects
230
+ const mockReq = (query) => ({
231
+ params: { modelName, recordId: docId.toString() },
232
+ query,
233
+ me: testUser
234
+ });
235
+ const mockRes = () => {
236
+ const res = {};
237
+ res.status = (code) => { res.statusCode = code; return res; };
238
+ res.json = (data) => { res.body = data; return res; };
239
+ return res;
240
+ };
241
+
242
+ // 4. Test cases
243
+ // Case A: Filter for February
244
+ let req = mockReq({ startDate: '2023-02-01', endDate: '2023-02-28' });
245
+ let res = mockRes();
246
+ await handleGetHistoryRequest(req, res);
247
+ expect(res.body.success).toBe(true);
248
+ expect(res.body.count).toBe(2);
249
+ expect(res.body.data.map(d => d._v).sort()).toEqual([2, 3]);
250
+
251
+ // Case B: Filter starting from Feb 15th
252
+ req = mockReq({ startDate: '2023-02-15' });
253
+ res = mockRes();
254
+ await handleGetHistoryRequest(req, res);
255
+ expect(res.body.success).toBe(true);
256
+ expect(res.body.count).toBe(3); // v2, v3, v4
257
+
258
+ // Case C: Filter up to Feb 15th (inclusive)
259
+ req = mockReq({ endDate: '2023-02-15' });
260
+ res = mockRes();
261
+ await handleGetHistoryRequest(req, res);
262
+ expect(res.body.success).toBe(true);
263
+ expect(res.body.count).toBe(2); // v1, v2
264
+ });
265
265
  });