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.
- package/README.md +938 -915
- 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 +6 -0
- 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 -808
- 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 -307
- 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/ModelCreator.jsx +683 -686
- package/client/src/ModelCreator.scss +1 -1
- package/client/src/ModelCreatorField.jsx +950 -950
- package/client/src/ModelImporter.jsx +3 -0
- package/client/src/ModelList.jsx +280 -280
- 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 -263
- package/client/src/index.css +90 -90
- package/package.json +6 -5
- package/perf/artillery-hooks.js +37 -37
- package/perf/setup.yml +25 -25
- package/src/constants.js +4 -0
- package/src/defaultModels.js +7 -0
- package/src/engine.js +335 -335
- package/src/events.js +232 -137
- package/src/filter.js +274 -274
- package/src/index.js +1 -0
- package/src/modules/assistant/assistant.js +225 -83
- 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 -555
- package/src/modules/data/data.operations.js +112 -9
- 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 +14 -9
- 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/test/data.history.integration.test.js +264 -264
- package/test/data.integration.test.js +1281 -1206
- package/test/events.test.js +108 -10
- package/test/model.integration.test.js +377 -377
- 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
|
});
|