data-primals-engine 1.5.0 → 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.
Files changed (63) hide show
  1. package/README.md +37 -0
  2. package/client/src/AddWidgetTypeModal.jsx +47 -43
  3. package/client/src/App.jsx +2 -6
  4. package/client/src/App.scss +13 -1
  5. package/client/src/AssistantChat.jsx +363 -323
  6. package/client/src/AssistantChat.scss +27 -10
  7. package/client/src/Dashboard.jsx +480 -396
  8. package/client/src/Dashboard.scss +1 -1
  9. package/client/src/DashboardHtmlViewItem.jsx +147 -0
  10. package/client/src/DashboardView.jsx +654 -569
  11. package/client/src/DataEditor.jsx +10 -3
  12. package/client/src/DataLayout.jsx +807 -755
  13. package/client/src/DataLayout.scss +14 -0
  14. package/client/src/DataTable.jsx +39 -75
  15. package/client/src/Dialog.scss +1 -1
  16. package/client/src/Field.jsx +2057 -1825
  17. package/client/src/FlexViewCard.jsx +44 -0
  18. package/client/src/HistoryDialog.jsx +69 -14
  19. package/client/src/HtmlViewBuilderModal.jsx +91 -0
  20. package/client/src/HtmlViewBuilderModal.scss +18 -0
  21. package/client/src/HtmlViewCard.jsx +44 -0
  22. package/client/src/HtmlViewCard.scss +35 -0
  23. package/client/src/KanbanCard.jsx +1 -2
  24. package/client/src/ModelCreator.jsx +5 -4
  25. package/client/src/ModelCreatorField.jsx +51 -4
  26. package/client/src/ModelList.jsx +280 -236
  27. package/client/src/Notification.jsx +136 -136
  28. package/client/src/Notification.scss +0 -18
  29. package/client/src/Pagination.jsx +5 -3
  30. package/client/src/RelationField.jsx +354 -258
  31. package/client/src/RelationSelectorWidget.jsx +173 -0
  32. package/client/src/contexts/ModelContext.jsx +10 -1
  33. package/client/src/contexts/UIContext.jsx +72 -63
  34. package/client/src/filter.js +263 -212
  35. package/client/src/hooks/useValidation.js +75 -0
  36. package/client/src/translations.js +24 -24
  37. package/package.json +7 -6
  38. package/src/constants.js +1 -1
  39. package/src/core.js +8 -1
  40. package/src/defaultModels.js +1596 -1544
  41. package/src/engine.js +85 -43
  42. package/src/events.js +137 -113
  43. package/src/i18n.js +710 -10
  44. package/src/index.js +3 -0
  45. package/src/modules/assistant/assistant.js +253 -134
  46. package/src/modules/assistant/constants.js +2 -1
  47. package/src/modules/bucket.js +2 -1
  48. package/src/modules/data/data.core.js +118 -92
  49. package/src/modules/data/data.history.js +555 -492
  50. package/src/modules/data/data.js +3 -53
  51. package/src/modules/data/data.operations.js +3381 -3231
  52. package/src/modules/data/data.relations.js +686 -686
  53. package/src/modules/data/data.routes.js +1879 -1821
  54. package/src/modules/data/data.validation.js +81 -2
  55. package/src/modules/file.js +247 -238
  56. package/src/modules/user.js +1 -0
  57. package/src/modules/workflow.js +2 -2
  58. package/src/openai.jobs.js +3 -2
  59. package/src/packs.js +5482 -5478
  60. package/src/sso.js +2 -2
  61. package/src/workers/import-export-worker.js +1 -1
  62. package/test/data.history.integration.test.js +264 -192
  63. package/test/data.integration.test.js +149 -3
package/src/sso.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import passport from 'passport';
2
2
  import session from 'express-session';
3
- import { Behaviour } from './gameObject.js';
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('Logger'); // Assumant que Logger est un composant
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,5 +1,5 @@
1
1
  import { parentPort } from 'worker_threads';
2
- import { parse } from 'csv-parse/sync'; // On utilise la version synchrone, c'est ok dans un worker
2
+ import { parse } from 'csv-parse/sync';
3
3
 
4
4
  parentPort.on('message', ({ action, payload }) => {
5
5
  try {
@@ -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 { Config } from '../src/config.js';
5
- import { sleep } from '../src/core.js';
6
- import { insertData, editData, deleteModels } from '../src/index.js';
7
- import { getCollection, getCollectionForUser } from '../src/modules/mongodb.js';
8
- import { generateUniqueName, initEngine } from "../src/setenv.js";
9
- import {purgeData} from "../src/modules/data/data.history.js";
10
- import {MongoDatabase} from "../src/engine.js";
11
-
12
- let engine;
13
- let testUser;
14
- let historyCollection;
15
- let datasCollection;
16
- let modelsCollection;
17
-
18
- describe('Data History Module Integration Tests', () => {
19
-
20
- beforeAll(async () => {
21
- // IMPORTANT: Add the history module to the engine configuration for tests
22
- Config.Set("modules", ["mongodb", "data", "file", "bucket", "workflow", "user", "assistant"]);
23
- engine = await initEngine();
24
-
25
- // Setup a single user for all tests in this suite
26
- testUser = {
27
- username: generateUniqueName('testUserHistory'),
28
- userPlan: 'free',
29
- email: generateUniqueName('test') + '@example.com'
30
- };
31
-
32
- // Initialize collection instances
33
- historyCollection = getCollection('history');
34
- datasCollection = await getCollectionForUser(testUser);
35
- modelsCollection = getCollection('models');
36
- });
37
-
38
- afterAll(async () =>{
39
- await purgeData(testUser);
40
- await datasCollection.drop();
41
- })
42
-
43
- // Clean up collections before each test to ensure isolation
44
- beforeEach(async () => {
45
- await historyCollection.deleteMany({ 'user.username': testUser.username });
46
- await datasCollection.deleteMany({ _user: testUser.username });
47
- await modelsCollection.deleteMany({ _user: testUser.username });
48
- });
49
-
50
- afterAll(async () => {
51
- // Final cleanup
52
- await historyCollection.deleteMany({ 'user.username': testUser.username });
53
- await datasCollection.deleteMany({ _user: testUser.username });
54
- await modelsCollection.deleteMany({ _user: testUser.username });
55
- });
56
-
57
- it('should create a full snapshot history record on document creation', async () => {
58
- // 1. Define and create a model with history enabled
59
- const modelName = generateUniqueName('productHistory');
60
- const productModelDef = {
61
- name: modelName,
62
- description:"",
63
- _user: testUser.username,
64
- history: {
65
- enabled: true,
66
- // For creation, all fields are snapshotted regardless of this config
67
- fields: {
68
- price: true,
69
- stock: true
70
- }
71
- },
72
- fields: [
73
- { name: 'name', type: 'string', required: true },
74
- { name: 'price', type: 'number' },
75
- { name: 'stock', type: 'number' },
76
- { name: 'description', type: 'string' }
77
- ]
78
- };
79
- await modelsCollection.insertOne(productModelDef);
80
-
81
- // 2. Insert a new document
82
- const initialData = {
83
- name: 'Super Widget',
84
- price: 99.99,
85
- stock: 100,
86
- description: 'A very super widget.'
87
- };
88
- const insertResult = await insertData(modelName, initialData, {}, testUser);
89
- expect(insertResult.success).toBe(true);
90
- const docId = new ObjectId(insertResult.insertedIds[0]);
91
-
92
- // 3. Verify the history record
93
- const historyRecord = await historyCollection.findOne({ documentId: new ObjectId(docId) });
94
-
95
- expect(historyRecord).not.toBeNull();
96
- expect(historyRecord.documentId.toString()).toBe(docId.toString());
97
- expect(historyRecord.model).toBe(modelName);
98
- expect(historyRecord.version).toBe(1);
99
- expect(historyRecord.operation).toBe('create');
100
- expect(historyRecord.user.username).toBe(testUser.username);
101
-
102
- // 4. Verify the snapshot
103
- expect(historyRecord.snapshot).not.toBeNull();
104
- expect(historyRecord.snapshot.name).toBe('Super Widget');
105
- expect(historyRecord.snapshot.price).toBe(99.99);
106
- expect(historyRecord.snapshot.stock).toBe(100);
107
- expect(historyRecord.snapshot.description).toBe('A very super widget.');
108
- expect(historyRecord.changes).toBeUndefined(); // No 'changes' field on creation
109
- });
110
-
111
- it('should create a diff history record on document update for historized fields only', async () => {
112
- // 1. Define and create a model with specific history fields
113
- const modelName = generateUniqueName('productHistorySelective');
114
- const productModelDef = {
115
- name: modelName,
116
- description:"",
117
- _user: testUser.username,
118
- history: {
119
- enabled: true,
120
- fields: {
121
- price: true, // Track this
122
- stock: true // Track this
123
- // 'description' is NOT tracked
124
- }
125
- },
126
- fields: [
127
- { name: 'name', type: 'string', required: true },
128
- { name: 'price', type: 'number' },
129
- { name: 'stock', type: 'number' },
130
- { name: 'description', type: 'string' }
131
- ]
132
- };
133
- await modelsCollection.insertOne(productModelDef);
134
-
135
- // 2. Insert the initial document
136
- const initialData = { name: 'Selective Widget', price: 50, stock: 200, description: 'Initial description.' };
137
- const insertResult = await insertData(modelName, initialData, {}, testUser);
138
- const docId = new ObjectId(insertResult.insertedIds[0]);
139
-
140
- // 3. Edit the document: change one historized field and one non-historized field
141
- const updateData = { price: 55.5, description: 'Updated description.' };
142
- const editResult = await editData(modelName, { _id: docId }, updateData, {}, testUser);
143
- expect(editResult.success).toBe(true);
144
-
145
- // 4. Verify the new history record (v2)
146
- const historyRecord = await historyCollection.findOne({ documentId: docId, version: 2 });
147
-
148
- expect(historyRecord).not.toBeNull();
149
- expect(historyRecord.operation).toBe('update');
150
- expect(historyRecord.snapshot).toBeUndefined(); // No 'snapshot' on update
151
-
152
- // 5. Verify the 'changes' object
153
- const changes = historyRecord.changes;
154
- expect(changes).not.toBeNull();
155
- expect(changes.price).toBeDefined();
156
- expect(changes.price.from).toBe(50);
157
- expect(changes.price.to).toBe(55.5);
158
- expect(changes.description).toBeUndefined();
159
- expect(changes.stock).toBeUndefined();
160
- });
161
-
162
- it('should NOT create a history record if only non-historized fields are updated', async () => {
163
- // 1. Setup model
164
- const modelName = generateUniqueName('productHistoryNoOp');
165
- const productModelDef = {
166
- name: modelName,
167
- description:"",
168
- _user: testUser.username,
169
- history: { enabled: true, fields: { price: true } },
170
- fields: [
171
- { name: 'name', type: 'string', required: true },
172
- { name: 'price', type: 'number' },
173
- { name: 'description', type: 'string' }
174
- ]
175
- };
176
- await modelsCollection.insertOne(productModelDef);
177
-
178
- // 2. Insert initial document
179
- const initialData = { name: 'No-Op Widget', price: 10, description: 'Initial.' };
180
- const insertResult = await insertData(modelName, initialData, {}, testUser);
181
- const docId = new ObjectId(insertResult.insertedIds[0]);
182
-
183
- // 3. Edit ONLY a non-historized field
184
- const updateData = { description: 'This change should not be recorded.' };
185
- await editData(modelName, { _id: docId }, updateData, {}, testUser);
186
-
187
- await sleep(2000);
188
-
189
- // 4. Verify that NO new history record was created (only the 'create' record exists)
190
- const historyCount = await historyCollection.countDocuments({ documentId: docId });
191
- expect(historyCount).toBe(1);
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
  });