@objectql/core 4.0.1 → 4.0.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.
@@ -42,7 +42,7 @@ describe('Formula Integration', () => {
42
42
  },
43
43
  full_name: {
44
44
  type: 'formula',
45
- formula: 'first_name + " " + last_name',
45
+ expression: 'first_name + " " + last_name',
46
46
  data_type: 'text',
47
47
  label: 'Full Name',
48
48
  },
@@ -54,7 +54,7 @@ describe('Formula Integration', () => {
54
54
  },
55
55
  total: {
56
56
  type: 'formula',
57
- formula: 'quantity * unit_price',
57
+ expression: 'quantity * unit_price',
58
58
  data_type: 'currency',
59
59
  label: 'Total',
60
60
  },
@@ -63,7 +63,7 @@ describe('Formula Integration', () => {
63
63
  },
64
64
  status_label: {
65
65
  type: 'formula',
66
- formula: 'is_active ? "Active" : "Inactive"',
66
+ expression: 'is_active ? "Active" : "Inactive"',
67
67
  data_type: 'text',
68
68
  label: 'Status',
69
69
  },
@@ -165,7 +165,7 @@ describe('Formula Integration', () => {
165
165
  tax_rate: { type: 'percent' },
166
166
  final_price: {
167
167
  type: 'formula',
168
- formula: 'subtotal * (1 - discount_rate / 100) * (1 + tax_rate / 100)',
168
+ expression: 'subtotal * (1 - discount_rate / 100) * (1 + tax_rate / 100)',
169
169
  data_type: 'currency',
170
170
  label: 'Final Price',
171
171
  },
@@ -173,7 +173,7 @@ describe('Formula Integration', () => {
173
173
  status: { type: 'select', options: ['draft', 'confirmed', 'shipped'] },
174
174
  risk_level: {
175
175
  type: 'formula',
176
- formula: `
176
+ expression: `
177
177
  if (subtotal > 10000) {
178
178
  return 'High';
179
179
  } else if (subtotal > 1000) {
@@ -256,7 +256,7 @@ describe('Formula Integration', () => {
256
256
  price: { type: 'currency' },
257
257
  invalid_formula: {
258
258
  type: 'formula',
259
- formula: 'nonexistent_field * 2',
259
+ expression: 'nonexistent_field * 2',
260
260
  data_type: 'number',
261
261
  },
262
262
  },
@@ -0,0 +1,258 @@
1
+ /**
2
+ * ObjectQL
3
+ * Copyright (c) 2026-present ObjectStack Inc.
4
+ *
5
+ * This source code is licensed under the MIT license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ */
8
+
9
+ /**
10
+ * Formula Specification Compliance Tests
11
+ *
12
+ * Ensures that formula fields work according to the latest specification,
13
+ * supporting both 'expression' (spec-compliant) and 'formula' (legacy) properties.
14
+ */
15
+
16
+ import { FormulaEngine } from '../src/formula-engine';
17
+ import { Repository } from '../src/repository';
18
+ import type { ObjectConfig, FormulaContext } from '@objectql/types';
19
+
20
+ describe('Formula Specification Compliance', () => {
21
+ let engine: FormulaEngine;
22
+ let baseContext: FormulaContext;
23
+
24
+ beforeEach(() => {
25
+ engine = new FormulaEngine();
26
+
27
+ const now = new Date('2026-01-25T10:00:00Z');
28
+ baseContext = {
29
+ record: {
30
+ first_name: 'John',
31
+ last_name: 'Doe',
32
+ quantity: 10,
33
+ unit_price: 25.5,
34
+ },
35
+ system: {
36
+ today: new Date('2026-01-25'),
37
+ now: now,
38
+ year: 2026,
39
+ month: 1,
40
+ day: 25,
41
+ hour: 10,
42
+ minute: 0,
43
+ second: 0,
44
+ },
45
+ current_user: {
46
+ id: 'user-123',
47
+ name: 'Admin User',
48
+ email: 'admin@example.com',
49
+ role: 'admin',
50
+ },
51
+ is_new: false,
52
+ record_id: 'rec-456',
53
+ };
54
+ });
55
+
56
+ describe('Specification-compliant property: expression', () => {
57
+ it('should evaluate formula using "expression" property', () => {
58
+ const result = engine.evaluate(
59
+ 'first_name + " " + last_name',
60
+ baseContext,
61
+ 'text'
62
+ );
63
+
64
+ expect(result.success).toBe(true);
65
+ expect(result.value).toBe('John Doe');
66
+ });
67
+
68
+ it('should calculate numeric formulas using "expression"', () => {
69
+ const result = engine.evaluate(
70
+ 'quantity * unit_price',
71
+ baseContext,
72
+ 'currency'
73
+ );
74
+
75
+ expect(result.success).toBe(true);
76
+ expect(result.value).toBe(255);
77
+ });
78
+
79
+ it('should support system variables in expression', () => {
80
+ const result = engine.evaluate(
81
+ '$year',
82
+ baseContext,
83
+ 'number'
84
+ );
85
+
86
+ expect(result.success).toBe(true);
87
+ expect(result.value).toBe(2026);
88
+ });
89
+
90
+ it('should support conditional expressions', () => {
91
+ const result = engine.evaluate(
92
+ 'quantity > 5 ? "High" : "Low"',
93
+ baseContext,
94
+ 'text'
95
+ );
96
+
97
+ expect(result.success).toBe(true);
98
+ expect(result.value).toBe('High');
99
+ });
100
+ });
101
+
102
+ describe('Specification examples from formula.mdx', () => {
103
+ it('should calculate full_name as per spec example', () => {
104
+ // From spec line 105-110
105
+ const result = engine.evaluate(
106
+ 'first_name + " " + last_name',
107
+ baseContext,
108
+ 'text'
109
+ );
110
+
111
+ expect(result.success).toBe(true);
112
+ expect(result.value).toBe('John Doe');
113
+ });
114
+
115
+ it('should calculate total_amount with formula fields', () => {
116
+ // From spec: total_amount: expression: "quantity * unit_price"
117
+ const result = engine.evaluate(
118
+ 'quantity * unit_price',
119
+ baseContext,
120
+ 'currency'
121
+ );
122
+
123
+ expect(result.success).toBe(true);
124
+ expect(result.value).toBe(255);
125
+ });
126
+
127
+ it('should handle template literals', () => {
128
+ // From spec line 216-218
129
+ const result = engine.evaluate(
130
+ '`Hello, ${first_name}!`',
131
+ baseContext,
132
+ 'text'
133
+ );
134
+
135
+ expect(result.success).toBe(true);
136
+ expect(result.value).toBe('Hello, John!');
137
+ });
138
+
139
+ it('should support Math functions', () => {
140
+ // From spec line 435-438
141
+ const context = {
142
+ ...baseContext,
143
+ record: { ...baseContext.record, price: 99.7 }
144
+ };
145
+
146
+ const result = engine.evaluate(
147
+ 'Math.round(price)',
148
+ context,
149
+ 'number'
150
+ );
151
+
152
+ expect(result.success).toBe(true);
153
+ expect(result.value).toBe(100);
154
+ });
155
+ });
156
+
157
+ describe('Error handling per specification', () => {
158
+ it('should handle division by zero safely', () => {
159
+ // From spec section 7.1
160
+ const context = {
161
+ ...baseContext,
162
+ record: { total: 100, count: 0 }
163
+ };
164
+
165
+ const result = engine.evaluate(
166
+ 'count !== 0 ? total / count : 0',
167
+ context,
168
+ 'number'
169
+ );
170
+
171
+ expect(result.success).toBe(true);
172
+ expect(result.value).toBe(0);
173
+ });
174
+
175
+ it('should detect division by zero without guard', () => {
176
+ const context = {
177
+ ...baseContext,
178
+ record: { total: 100, count: 0 }
179
+ };
180
+
181
+ const result = engine.evaluate(
182
+ 'total / count',
183
+ context,
184
+ 'number'
185
+ );
186
+
187
+ expect(result.success).toBe(false);
188
+ expect(result.error).toContain('Infinity');
189
+ });
190
+
191
+ it('should handle null values with optional chaining', () => {
192
+ // From spec section 7.2
193
+ const context = {
194
+ ...baseContext,
195
+ record: { account: null }
196
+ };
197
+
198
+ const result = engine.evaluate(
199
+ 'account?.name ?? "No Account"',
200
+ context,
201
+ 'text'
202
+ );
203
+
204
+ expect(result.success).toBe(true);
205
+ expect(result.value).toBe('No Account');
206
+ });
207
+ });
208
+
209
+ describe('Data type coercion per specification', () => {
210
+ it('should coerce to number type', () => {
211
+ const context = {
212
+ ...baseContext,
213
+ record: { text_value: '42' }
214
+ };
215
+
216
+ const result = engine.evaluate(
217
+ 'Number(text_value) || 0',
218
+ context,
219
+ 'number'
220
+ );
221
+
222
+ expect(result.success).toBe(true);
223
+ expect(result.value).toBe(42);
224
+ });
225
+
226
+ it('should coerce to text type', () => {
227
+ const context = {
228
+ ...baseContext,
229
+ record: { numeric_value: 123 }
230
+ };
231
+
232
+ const result = engine.evaluate(
233
+ 'String(numeric_value)',
234
+ context,
235
+ 'text'
236
+ );
237
+
238
+ expect(result.success).toBe(true);
239
+ expect(result.value).toBe('123');
240
+ });
241
+
242
+ it('should coerce to boolean type', () => {
243
+ const context = {
244
+ ...baseContext,
245
+ record: { value: 1 }
246
+ };
247
+
248
+ const result = engine.evaluate(
249
+ 'Boolean(value)',
250
+ context,
251
+ 'boolean'
252
+ );
253
+
254
+ expect(result.success).toBe(true);
255
+ expect(result.value).toBe(true);
256
+ });
257
+ });
258
+ });