@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.
@@ -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
+ });
@@ -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] = [];