@objectql/core 1.8.4 → 1.9.0
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/CHANGELOG.md +17 -0
- package/dist/formula-engine.d.ts +95 -0
- package/dist/formula-engine.js +426 -0
- package/dist/formula-engine.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/repository.d.ts +6 -0
- package/dist/repository.js +65 -2
- package/dist/repository.js.map +1 -1
- package/package.json +2 -2
- package/src/formula-engine.ts +564 -0
- package/src/index.ts +1 -0
- package/src/repository.ts +80 -3
- package/test/formula-engine.test.ts +717 -0
- package/test/formula-integration.test.ts +278 -0
- package/test/mock-driver.ts +4 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Formula Integration Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests formula evaluation within repository queries
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { ObjectQL } from '../src/app';
|
|
8
|
+
import { MockDriver } from './mock-driver';
|
|
9
|
+
|
|
10
|
+
describe('Formula Integration', () => {
|
|
11
|
+
let app: ObjectQL;
|
|
12
|
+
let mockDriver: MockDriver;
|
|
13
|
+
|
|
14
|
+
beforeEach(async () => {
|
|
15
|
+
mockDriver = new MockDriver();
|
|
16
|
+
|
|
17
|
+
app = new ObjectQL({
|
|
18
|
+
datasources: {
|
|
19
|
+
default: mockDriver
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
// Register an object with formula fields
|
|
24
|
+
app.registerObject({
|
|
25
|
+
name: 'contact',
|
|
26
|
+
fields: {
|
|
27
|
+
first_name: {
|
|
28
|
+
type: 'text',
|
|
29
|
+
required: true,
|
|
30
|
+
},
|
|
31
|
+
last_name: {
|
|
32
|
+
type: 'text',
|
|
33
|
+
required: true,
|
|
34
|
+
},
|
|
35
|
+
full_name: {
|
|
36
|
+
type: 'formula',
|
|
37
|
+
formula: 'first_name + " " + last_name',
|
|
38
|
+
data_type: 'text',
|
|
39
|
+
label: 'Full Name',
|
|
40
|
+
},
|
|
41
|
+
quantity: {
|
|
42
|
+
type: 'number',
|
|
43
|
+
},
|
|
44
|
+
unit_price: {
|
|
45
|
+
type: 'currency',
|
|
46
|
+
},
|
|
47
|
+
total: {
|
|
48
|
+
type: 'formula',
|
|
49
|
+
formula: 'quantity * unit_price',
|
|
50
|
+
data_type: 'currency',
|
|
51
|
+
label: 'Total',
|
|
52
|
+
},
|
|
53
|
+
is_active: {
|
|
54
|
+
type: 'boolean',
|
|
55
|
+
},
|
|
56
|
+
status_label: {
|
|
57
|
+
type: 'formula',
|
|
58
|
+
formula: 'is_active ? "Active" : "Inactive"',
|
|
59
|
+
data_type: 'text',
|
|
60
|
+
label: 'Status',
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
await app.init();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
describe('Formula Evaluation in Queries', () => {
|
|
69
|
+
it('should evaluate formula fields in find results', async () => {
|
|
70
|
+
// Setup mock data
|
|
71
|
+
mockDriver.setMockData('contact', [
|
|
72
|
+
{
|
|
73
|
+
_id: '1',
|
|
74
|
+
first_name: 'John',
|
|
75
|
+
last_name: 'Doe',
|
|
76
|
+
quantity: 10,
|
|
77
|
+
unit_price: 25.5,
|
|
78
|
+
is_active: true,
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
_id: '2',
|
|
82
|
+
first_name: 'Jane',
|
|
83
|
+
last_name: 'Smith',
|
|
84
|
+
quantity: 5,
|
|
85
|
+
unit_price: 30,
|
|
86
|
+
is_active: false,
|
|
87
|
+
},
|
|
88
|
+
]);
|
|
89
|
+
|
|
90
|
+
const ctx = app.createContext({ isSystem: true });
|
|
91
|
+
const results = await ctx.object('contact').find({});
|
|
92
|
+
|
|
93
|
+
expect(results).toHaveLength(2);
|
|
94
|
+
|
|
95
|
+
// Check first record
|
|
96
|
+
expect(results[0].full_name).toBe('John Doe');
|
|
97
|
+
expect(results[0].total).toBe(255); // 10 * 25.5
|
|
98
|
+
expect(results[0].status_label).toBe('Active');
|
|
99
|
+
|
|
100
|
+
// Check second record
|
|
101
|
+
expect(results[1].full_name).toBe('Jane Smith');
|
|
102
|
+
expect(results[1].total).toBe(150); // 5 * 30
|
|
103
|
+
expect(results[1].status_label).toBe('Inactive');
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('should evaluate formula fields in findOne result', async () => {
|
|
107
|
+
mockDriver.setMockData('contact', [
|
|
108
|
+
{
|
|
109
|
+
_id: '1',
|
|
110
|
+
first_name: 'John',
|
|
111
|
+
last_name: 'Doe',
|
|
112
|
+
quantity: 10,
|
|
113
|
+
unit_price: 25.5,
|
|
114
|
+
is_active: true,
|
|
115
|
+
},
|
|
116
|
+
]);
|
|
117
|
+
|
|
118
|
+
const ctx = app.createContext({ isSystem: true });
|
|
119
|
+
const result = await ctx.object('contact').findOne('1');
|
|
120
|
+
|
|
121
|
+
expect(result).toBeDefined();
|
|
122
|
+
expect(result.full_name).toBe('John Doe');
|
|
123
|
+
expect(result.total).toBe(255);
|
|
124
|
+
expect(result.status_label).toBe('Active');
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('should handle null values in formulas', async () => {
|
|
128
|
+
mockDriver.setMockData('contact', [
|
|
129
|
+
{
|
|
130
|
+
_id: '1',
|
|
131
|
+
first_name: 'John',
|
|
132
|
+
last_name: 'Doe',
|
|
133
|
+
quantity: null,
|
|
134
|
+
unit_price: 25.5,
|
|
135
|
+
is_active: true,
|
|
136
|
+
},
|
|
137
|
+
]);
|
|
138
|
+
|
|
139
|
+
const ctx = app.createContext({ isSystem: true });
|
|
140
|
+
const result = await ctx.object('contact').findOne('1');
|
|
141
|
+
|
|
142
|
+
expect(result).toBeDefined();
|
|
143
|
+
expect(result.full_name).toBe('John Doe');
|
|
144
|
+
// In JavaScript, null * number = 0 (null is coerced to 0)
|
|
145
|
+
expect(result.total).toBe(0);
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
describe('Complex Formula Examples', () => {
|
|
150
|
+
beforeEach(async () => {
|
|
151
|
+
// Register an object with more complex formulas
|
|
152
|
+
app.registerObject({
|
|
153
|
+
name: 'order',
|
|
154
|
+
fields: {
|
|
155
|
+
subtotal: { type: 'currency' },
|
|
156
|
+
discount_rate: { type: 'percent' },
|
|
157
|
+
tax_rate: { type: 'percent' },
|
|
158
|
+
final_price: {
|
|
159
|
+
type: 'formula',
|
|
160
|
+
formula: 'subtotal * (1 - discount_rate / 100) * (1 + tax_rate / 100)',
|
|
161
|
+
data_type: 'currency',
|
|
162
|
+
label: 'Final Price',
|
|
163
|
+
},
|
|
164
|
+
created_at: { type: 'date' },
|
|
165
|
+
status: { type: 'select', options: ['draft', 'confirmed', 'shipped'] },
|
|
166
|
+
risk_level: {
|
|
167
|
+
type: 'formula',
|
|
168
|
+
formula: `
|
|
169
|
+
if (subtotal > 10000) {
|
|
170
|
+
return 'High';
|
|
171
|
+
} else if (subtotal > 1000) {
|
|
172
|
+
return 'Medium';
|
|
173
|
+
} else {
|
|
174
|
+
return 'Low';
|
|
175
|
+
}
|
|
176
|
+
`,
|
|
177
|
+
data_type: 'text',
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
await app.init();
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('should calculate complex financial formulas', async () => {
|
|
186
|
+
mockDriver.setMockData('order', [
|
|
187
|
+
{
|
|
188
|
+
_id: '1',
|
|
189
|
+
subtotal: 5000,
|
|
190
|
+
discount_rate: 10,
|
|
191
|
+
tax_rate: 8,
|
|
192
|
+
status: 'confirmed',
|
|
193
|
+
created_at: new Date('2026-01-01'),
|
|
194
|
+
},
|
|
195
|
+
]);
|
|
196
|
+
|
|
197
|
+
const ctx = app.createContext({ isSystem: true });
|
|
198
|
+
const result = await ctx.object('order').findOne('1');
|
|
199
|
+
|
|
200
|
+
expect(result).toBeDefined();
|
|
201
|
+
expect(result.final_price).toBeCloseTo(4860, 1);
|
|
202
|
+
expect(result.risk_level).toBe('Medium');
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it('should handle conditional logic in formulas', async () => {
|
|
206
|
+
mockDriver.setMockData('order', [
|
|
207
|
+
{
|
|
208
|
+
_id: '1',
|
|
209
|
+
subtotal: 500,
|
|
210
|
+
discount_rate: 0,
|
|
211
|
+
tax_rate: 0,
|
|
212
|
+
status: 'draft',
|
|
213
|
+
created_at: new Date('2026-01-01'),
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
_id: '2',
|
|
217
|
+
subtotal: 5000,
|
|
218
|
+
discount_rate: 0,
|
|
219
|
+
tax_rate: 0,
|
|
220
|
+
status: 'confirmed',
|
|
221
|
+
created_at: new Date('2026-01-01'),
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
_id: '3',
|
|
225
|
+
subtotal: 15000,
|
|
226
|
+
discount_rate: 0,
|
|
227
|
+
tax_rate: 0,
|
|
228
|
+
status: 'shipped',
|
|
229
|
+
created_at: new Date('2026-01-01'),
|
|
230
|
+
},
|
|
231
|
+
]);
|
|
232
|
+
|
|
233
|
+
const ctx = app.createContext({ isSystem: true });
|
|
234
|
+
const results = await ctx.object('order').find({});
|
|
235
|
+
|
|
236
|
+
expect(results[0].risk_level).toBe('Low');
|
|
237
|
+
expect(results[1].risk_level).toBe('Medium');
|
|
238
|
+
expect(results[2].risk_level).toBe('High');
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
describe('Formula Error Handling', () => {
|
|
243
|
+
beforeEach(async () => {
|
|
244
|
+
app.registerObject({
|
|
245
|
+
name: 'product',
|
|
246
|
+
fields: {
|
|
247
|
+
name: { type: 'text' },
|
|
248
|
+
price: { type: 'currency' },
|
|
249
|
+
invalid_formula: {
|
|
250
|
+
type: 'formula',
|
|
251
|
+
formula: 'nonexistent_field * 2',
|
|
252
|
+
data_type: 'number',
|
|
253
|
+
},
|
|
254
|
+
},
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
await app.init();
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it('should handle formula evaluation errors gracefully', async () => {
|
|
261
|
+
mockDriver.setMockData('product', [
|
|
262
|
+
{
|
|
263
|
+
_id: '1',
|
|
264
|
+
name: 'Widget',
|
|
265
|
+
price: 100,
|
|
266
|
+
},
|
|
267
|
+
]);
|
|
268
|
+
|
|
269
|
+
const ctx = app.createContext({ isSystem: true });
|
|
270
|
+
const result = await ctx.object('product').findOne('1');
|
|
271
|
+
|
|
272
|
+
expect(result).toBeDefined();
|
|
273
|
+
expect(result.name).toBe('Widget');
|
|
274
|
+
// Formula failed, should be null
|
|
275
|
+
expect(result.invalid_formula).toBeNull();
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
});
|
package/test/mock-driver.ts
CHANGED
|
@@ -6,6 +6,10 @@ export class MockDriver implements Driver {
|
|
|
6
6
|
|
|
7
7
|
constructor() {}
|
|
8
8
|
|
|
9
|
+
setMockData(objectName: string, data: any[]) {
|
|
10
|
+
this.data[objectName] = data;
|
|
11
|
+
}
|
|
12
|
+
|
|
9
13
|
private getData(objectName: string) {
|
|
10
14
|
if (!this.data[objectName]) {
|
|
11
15
|
this.data[objectName] = [];
|