data-primals-engine 1.5.1 → 1.5.2
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 +2 -0
- package/client/src/App.scss +1 -1
- package/client/src/DataLayout.jsx +2 -0
- package/client/src/HistoryDialog.jsx +24 -2
- package/client/src/ModelList.jsx +280 -275
- package/client/src/filter.js +1 -0
- package/package.json +6 -6
- package/src/core.js +8 -1
- package/src/engine.js +85 -43
- package/src/events.js +137 -113
- package/src/index.js +3 -0
- package/src/modules/assistant/assistant.js +123 -134
- package/src/modules/assistant/constants.js +2 -1
- package/src/modules/data/data.history.js +32 -8
- package/src/modules/data/data.operations.js +3381 -3282
- package/src/modules/data/data.relations.js +1 -1
- package/src/modules/data/data.routes.js +3 -3
- package/src/modules/user.js +1 -0
- package/src/modules/workflow.js +2 -2
- package/src/openai.jobs.js +3 -2
- package/src/sso.js +2 -2
- package/src/workers/import-export-worker.js +1 -1
- package/test/data.history.integration.test.js +264 -192
- package/test/data.integration.test.js +1206 -1115
|
@@ -2,7 +2,7 @@ import {getDefaultForType, getFieldValueHash} from "../../data.js";
|
|
|
2
2
|
import {Event} from "../../events.js";
|
|
3
3
|
import {getCollectionForUser, isObjectId} from "../mongodb.js";
|
|
4
4
|
import {ObjectId} from "mongodb";
|
|
5
|
-
import {isPlainObject} from "../../core.js";
|
|
5
|
+
import {isPlainObject, parseSafeJSON} from "../../core.js";
|
|
6
6
|
import {dataTypes, getModel, searchData} from "./data.operations.js";
|
|
7
7
|
import {validateModelData} from "./data.validation.js";
|
|
8
8
|
import i18n from "../../i18n.js";
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
maxModelsPerUser, maxPackData, maxPackPreviewData
|
|
13
13
|
} from "../../constants.js";
|
|
14
14
|
import {datasCollection, getCollection, getCollectionForUser, isObjectId, modelsCollection} from "../mongodb.js";
|
|
15
|
-
import {countKeys, safeAssignObject, uuidv4} from "../../core.js";
|
|
15
|
+
import {countKeys, parseSafeJSON, safeAssignObject, uuidv4} from "../../core.js";
|
|
16
16
|
import {Event} from "../../events.js";
|
|
17
17
|
import fs from "node:fs";
|
|
18
18
|
import i18n from "../../i18n.js";
|
|
@@ -1192,7 +1192,7 @@ export async function registerRoutes(defaultEngine){
|
|
|
1192
1192
|
totalCount = result.length > 0 ? result[0]['count'] : 0;
|
|
1193
1193
|
} catch (totalError) {
|
|
1194
1194
|
if (totalError instanceof SyntaxError) {
|
|
1195
|
-
console.error(`>>> ERREUR
|
|
1195
|
+
console.error(`>>> ERREUR parseSafeJSON (totalMatchFormula) pour KPI ${id}:`, kpiDef.totalMatchFormula, totalError);
|
|
1196
1196
|
} else {
|
|
1197
1197
|
console.error(`>>> ERREUR Aggregate (totalCount) pour KPI ${id}:`, totalError);
|
|
1198
1198
|
}
|
|
@@ -1206,7 +1206,7 @@ export async function registerRoutes(defaultEngine){
|
|
|
1206
1206
|
const parsedMatch = kpiDef.matchFormula || {}; // Assurer un objet vide
|
|
1207
1207
|
matchFilter = { ...matchFilter, ...parsedMatch };
|
|
1208
1208
|
} catch (matchError) {
|
|
1209
|
-
console.error(`>>> ERREUR
|
|
1209
|
+
console.error(`>>> ERREUR parseSafeJSON (matchFormula) pour KPI ${id}:`, kpiDef.matchFormula, matchError);
|
|
1210
1210
|
throw matchError; // Relancer
|
|
1211
1211
|
}
|
|
1212
1212
|
}
|
package/src/modules/user.js
CHANGED
package/src/modules/workflow.js
CHANGED
|
@@ -19,7 +19,7 @@ import {getEnv, getSmtpConfig} from "./user.js";
|
|
|
19
19
|
import {getHost} from "../constants.js";
|
|
20
20
|
import {providers} from "./assistant/constants.js";
|
|
21
21
|
import {getAIProvider} from "./assistant/assistant.js";
|
|
22
|
-
import { safeAssignObject} from "../core.js";
|
|
22
|
+
import {parseSafeJSON, safeAssignObject} from "../core.js";
|
|
23
23
|
import {Config} from "../config.js";
|
|
24
24
|
|
|
25
25
|
let logger = null;
|
|
@@ -1655,7 +1655,7 @@ async function executeGenerateAIContentAction(action, context, user) {
|
|
|
1655
1655
|
}
|
|
1656
1656
|
|
|
1657
1657
|
// 2. Initialize the LLM client with LangChain
|
|
1658
|
-
let llm = getAIProvider(aiProvider, aiModel, apiKey);
|
|
1658
|
+
let llm = await getAIProvider(aiProvider, aiModel, apiKey);
|
|
1659
1659
|
if( !llm ) {
|
|
1660
1660
|
const message = `Failed to initialize AI client for ${aiProvider}: ${initError.message}`;
|
|
1661
1661
|
logger.error(`[AI Action] ${message}`);
|
package/src/openai.jobs.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import OpenAI from "openai";
|
|
2
|
+
import {parseSafeJSON} from "./core.js";
|
|
2
3
|
|
|
3
4
|
|
|
4
5
|
let engine = null;
|
|
@@ -132,8 +133,8 @@ export const openaiJobModel = async (lang, txt, history, existingModels = []) =>
|
|
|
132
133
|
const aiResponse = completion.choices[0].message.content;
|
|
133
134
|
|
|
134
135
|
try {
|
|
135
|
-
//
|
|
136
|
-
return
|
|
136
|
+
// parseSafeJSON fonctionnera que le contenu soit un objet ou un tableau
|
|
137
|
+
return parseSafeJSON(aiResponse); // On retourne directement le résultat parsé
|
|
137
138
|
} catch (e) {
|
|
138
139
|
console.error("Erreur de parsing du JSON de l'IA:", e);
|
|
139
140
|
throw new Error("Réponse invalide de l'IA.");
|
package/src/sso.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import passport from 'passport';
|
|
2
2
|
import session from 'express-session';
|
|
3
|
-
import {
|
|
3
|
+
import {Behaviour, Logger} from './gameObject.js';
|
|
4
4
|
import { cookiesSecret } from "./constants.js";
|
|
5
5
|
import {UserProvider} from "./providers.js";
|
|
6
6
|
import {getCollection} from "./modules/mongodb.js";
|
|
@@ -110,7 +110,7 @@ export class Sso extends Behaviour {
|
|
|
110
110
|
throw new Error("PassportAuth component requires an ssoUserProvider to be initialized.");
|
|
111
111
|
}
|
|
112
112
|
this.#ssoUserProvider = ssoUserProvider;
|
|
113
|
-
this.#logger = this.gameObject.getComponent(
|
|
113
|
+
this.#logger = this.gameObject.getComponent(Logger); // Assumant que Logger est un composant
|
|
114
114
|
|
|
115
115
|
const app = this.gameObject; // this.gameObject est l'instance de l'engine (Express app)
|
|
116
116
|
|
|
@@ -1,193 +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 {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
let
|
|
14
|
-
let
|
|
15
|
-
let
|
|
16
|
-
let
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
await
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
await
|
|
47
|
-
await
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
await
|
|
54
|
-
await
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
fields
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
{ name: '
|
|
75
|
-
{ name: '
|
|
76
|
-
{ name: '
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
expect(historyRecord
|
|
97
|
-
expect(historyRecord.
|
|
98
|
-
expect(historyRecord.
|
|
99
|
-
expect(historyRecord.
|
|
100
|
-
expect(historyRecord.
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
expect(historyRecord.snapshot
|
|
105
|
-
expect(historyRecord.snapshot.
|
|
106
|
-
expect(historyRecord.snapshot.
|
|
107
|
-
expect(historyRecord.snapshot.
|
|
108
|
-
expect(historyRecord.
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
//
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
{ name: '
|
|
129
|
-
{ name: '
|
|
130
|
-
{ name: '
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
const
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
expect(historyRecord
|
|
150
|
-
expect(historyRecord.
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
expect(changes
|
|
156
|
-
expect(changes.price
|
|
157
|
-
expect(changes.price.
|
|
158
|
-
expect(changes.
|
|
159
|
-
expect(changes.
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
fields:
|
|
171
|
-
|
|
172
|
-
{ name: '
|
|
173
|
-
{ name: '
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
const
|
|
181
|
-
const
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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
|
+
});
|
|
193
265
|
});
|