@objectql/core 3.0.0 → 4.0.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 +32 -0
- package/IMPLEMENTATION_STATUS.md +364 -0
- package/README.md +31 -9
- package/RUNTIME_INTEGRATION.md +391 -0
- package/dist/ai-agent.d.ts +4 -3
- package/dist/ai-agent.js +10 -3
- package/dist/ai-agent.js.map +1 -1
- package/dist/app.d.ts +29 -6
- package/dist/app.js +117 -58
- package/dist/app.js.map +1 -1
- package/dist/formula-engine.d.ts +7 -0
- package/dist/formula-engine.js +9 -2
- package/dist/formula-engine.js.map +1 -1
- package/dist/formula-plugin.d.ts +52 -0
- package/dist/formula-plugin.js +107 -0
- package/dist/formula-plugin.js.map +1 -0
- package/dist/index.d.ts +13 -3
- package/dist/index.js +14 -3
- package/dist/index.js.map +1 -1
- package/dist/plugin.d.ts +89 -0
- package/dist/plugin.js +99 -0
- package/dist/plugin.js.map +1 -0
- package/dist/query/filter-translator.d.ts +37 -0
- package/dist/query/filter-translator.js +135 -0
- package/dist/query/filter-translator.js.map +1 -0
- package/dist/query/index.d.ts +22 -0
- package/dist/query/index.js +39 -0
- package/dist/query/index.js.map +1 -0
- package/dist/query/query-analyzer.d.ts +186 -0
- package/dist/query/query-analyzer.js +349 -0
- package/dist/query/query-analyzer.js.map +1 -0
- package/dist/query/query-builder.d.ts +27 -0
- package/dist/query/query-builder.js +71 -0
- package/dist/query/query-builder.js.map +1 -0
- package/dist/query/query-service.d.ts +150 -0
- package/dist/query/query-service.js +268 -0
- package/dist/query/query-service.js.map +1 -0
- package/dist/repository.d.ts +23 -2
- package/dist/repository.js +62 -13
- package/dist/repository.js.map +1 -1
- package/dist/util.d.ts +7 -0
- package/dist/util.js +18 -3
- package/dist/util.js.map +1 -1
- package/dist/validator-plugin.d.ts +56 -0
- package/dist/validator-plugin.js +106 -0
- package/dist/validator-plugin.js.map +1 -0
- package/dist/validator.d.ts +7 -0
- package/dist/validator.js +10 -8
- package/dist/validator.js.map +1 -1
- package/jest.config.js +16 -0
- package/package.json +8 -5
- package/src/ai-agent.ts +8 -0
- package/src/app.ts +136 -72
- package/src/formula-engine.ts +8 -0
- package/src/formula-plugin.ts +141 -0
- package/src/index.ts +25 -3
- package/src/plugin.ts +179 -0
- package/src/query/filter-translator.ts +147 -0
- package/src/query/index.ts +24 -0
- package/src/query/query-analyzer.ts +535 -0
- package/src/query/query-builder.ts +80 -0
- package/src/query/query-service.ts +392 -0
- package/src/repository.ts +81 -17
- package/src/util.ts +19 -3
- package/src/validator-plugin.ts +140 -0
- package/src/validator.ts +12 -5
- package/test/__mocks__/@objectstack/runtime.ts +255 -0
- package/test/app.test.ts +23 -35
- package/test/filter-syntax.test.ts +233 -0
- package/test/formula-engine.test.ts +8 -0
- package/test/formula-integration.test.ts +8 -0
- package/test/formula-plugin.test.ts +197 -0
- package/test/introspection.test.ts +8 -0
- package/test/mock-driver.ts +8 -0
- package/test/plugin-integration.test.ts +213 -0
- package/test/repository-validation.test.ts +8 -0
- package/test/repository.test.ts +8 -0
- package/test/util.test.ts +9 -1
- package/test/utils.ts +8 -0
- package/test/validator-plugin.test.ts +126 -0
- package/test/validator.test.ts +8 -0
- package/tsconfig.json +9 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/action.d.ts +0 -7
- package/dist/action.js +0 -23
- package/dist/action.js.map +0 -1
- package/dist/hook.d.ts +0 -8
- package/dist/hook.js +0 -25
- package/dist/hook.js.map +0 -1
- package/dist/object.d.ts +0 -3
- package/dist/object.js +0 -28
- package/dist/object.js.map +0 -1
- package/src/action.ts +0 -40
- package/src/hook.ts +0 -42
- package/src/object.ts +0 -26
- package/test/action.test.ts +0 -276
- package/test/hook.test.ts +0 -343
- package/test/object.test.ts +0 -183
|
@@ -0,0 +1,233 @@
|
|
|
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
|
+
import { ObjectQL } from '../src/index';
|
|
10
|
+
import { MockDriver } from './mock-driver';
|
|
11
|
+
import { ObjectConfig } from '@objectql/types';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Modern Filter Syntax Tests
|
|
15
|
+
*
|
|
16
|
+
* Tests the new object-based filter syntax from @objectstack/spec FilterCondition.
|
|
17
|
+
* This replaces the old array-based FilterExpression syntax.
|
|
18
|
+
*
|
|
19
|
+
* Note: These tests verify filter translation logic. Full filter functionality
|
|
20
|
+
* is tested in driver integration tests.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const productObject: ObjectConfig = {
|
|
24
|
+
name: 'product',
|
|
25
|
+
fields: {
|
|
26
|
+
name: { type: 'text' },
|
|
27
|
+
price: { type: 'number' },
|
|
28
|
+
category: { type: 'text' },
|
|
29
|
+
status: { type: 'text' }
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
describe('Modern Filter Syntax - Translation', () => {
|
|
34
|
+
let app: ObjectQL;
|
|
35
|
+
let driver: MockDriver;
|
|
36
|
+
|
|
37
|
+
beforeEach(async () => {
|
|
38
|
+
driver = new MockDriver();
|
|
39
|
+
app = new ObjectQL({
|
|
40
|
+
datasources: {
|
|
41
|
+
default: driver
|
|
42
|
+
},
|
|
43
|
+
objects: {
|
|
44
|
+
product: productObject
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
await app.init();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe('Filter Translation to Kernel', () => {
|
|
51
|
+
it('should accept object-based filter syntax', async () => {
|
|
52
|
+
const ctx = app.createContext({ userId: 'test', isSystem: true });
|
|
53
|
+
const repo = ctx.object('product');
|
|
54
|
+
|
|
55
|
+
// This should not throw - it accepts the new syntax
|
|
56
|
+
await expect(repo.find({
|
|
57
|
+
filters: { category: 'Electronics' }
|
|
58
|
+
})).resolves.toBeDefined();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('should accept $eq operator', async () => {
|
|
62
|
+
const ctx = app.createContext({ userId: 'test', isSystem: true });
|
|
63
|
+
const repo = ctx.object('product');
|
|
64
|
+
|
|
65
|
+
await expect(repo.find({
|
|
66
|
+
filters: { status: { $eq: 'active' } }
|
|
67
|
+
})).resolves.toBeDefined();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('should accept $ne operator', async () => {
|
|
71
|
+
const ctx = app.createContext({ userId: 'test', isSystem: true });
|
|
72
|
+
const repo = ctx.object('product');
|
|
73
|
+
|
|
74
|
+
await expect(repo.find({
|
|
75
|
+
filters: { status: { $ne: 'inactive' } }
|
|
76
|
+
})).resolves.toBeDefined();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('should accept comparison operators', async () => {
|
|
80
|
+
const ctx = app.createContext({ userId: 'test', isSystem: true });
|
|
81
|
+
const repo = ctx.object('product');
|
|
82
|
+
|
|
83
|
+
await expect(repo.find({
|
|
84
|
+
filters: { price: { $gt: 100 } }
|
|
85
|
+
})).resolves.toBeDefined();
|
|
86
|
+
|
|
87
|
+
await expect(repo.find({
|
|
88
|
+
filters: { price: { $gte: 100 } }
|
|
89
|
+
})).resolves.toBeDefined();
|
|
90
|
+
|
|
91
|
+
await expect(repo.find({
|
|
92
|
+
filters: { price: { $lt: 500 } }
|
|
93
|
+
})).resolves.toBeDefined();
|
|
94
|
+
|
|
95
|
+
await expect(repo.find({
|
|
96
|
+
filters: { price: { $lte: 500 } }
|
|
97
|
+
})).resolves.toBeDefined();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('should accept $in operator', async () => {
|
|
101
|
+
const ctx = app.createContext({ userId: 'test', isSystem: true });
|
|
102
|
+
const repo = ctx.object('product');
|
|
103
|
+
|
|
104
|
+
await expect(repo.find({
|
|
105
|
+
filters: { status: { $in: ['active', 'pending'] } }
|
|
106
|
+
})).resolves.toBeDefined();
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('should accept $nin operator', async () => {
|
|
110
|
+
const ctx = app.createContext({ userId: 'test', isSystem: true });
|
|
111
|
+
const repo = ctx.object('product');
|
|
112
|
+
|
|
113
|
+
await expect(repo.find({
|
|
114
|
+
filters: { status: { $nin: ['inactive', 'deleted'] } }
|
|
115
|
+
})).resolves.toBeDefined();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('should accept $and operator', async () => {
|
|
119
|
+
const ctx = app.createContext({ userId: 'test', isSystem: true });
|
|
120
|
+
const repo = ctx.object('product');
|
|
121
|
+
|
|
122
|
+
await expect(repo.find({
|
|
123
|
+
filters: {
|
|
124
|
+
$and: [
|
|
125
|
+
{ category: 'Electronics' },
|
|
126
|
+
{ status: 'active' }
|
|
127
|
+
]
|
|
128
|
+
}
|
|
129
|
+
})).resolves.toBeDefined();
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('should accept $or operator', async () => {
|
|
133
|
+
const ctx = app.createContext({ userId: 'test', isSystem: true });
|
|
134
|
+
const repo = ctx.object('product');
|
|
135
|
+
|
|
136
|
+
await expect(repo.find({
|
|
137
|
+
filters: {
|
|
138
|
+
$or: [
|
|
139
|
+
{ category: 'Electronics' },
|
|
140
|
+
{ category: 'Furniture' }
|
|
141
|
+
]
|
|
142
|
+
}
|
|
143
|
+
})).resolves.toBeDefined();
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('should accept nested logical operators', async () => {
|
|
147
|
+
const ctx = app.createContext({ userId: 'test', isSystem: true });
|
|
148
|
+
const repo = ctx.object('product');
|
|
149
|
+
|
|
150
|
+
await expect(repo.find({
|
|
151
|
+
filters: {
|
|
152
|
+
$and: [
|
|
153
|
+
{
|
|
154
|
+
$or: [
|
|
155
|
+
{ category: 'Electronics' },
|
|
156
|
+
{ category: 'Furniture' }
|
|
157
|
+
]
|
|
158
|
+
},
|
|
159
|
+
{ status: 'active' }
|
|
160
|
+
]
|
|
161
|
+
}
|
|
162
|
+
})).resolves.toBeDefined();
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('should accept multiple operators on same field', async () => {
|
|
166
|
+
const ctx = app.createContext({ userId: 'test', isSystem: true });
|
|
167
|
+
const repo = ctx.object('product');
|
|
168
|
+
|
|
169
|
+
await expect(repo.find({
|
|
170
|
+
filters: {
|
|
171
|
+
price: {
|
|
172
|
+
$gte: 100,
|
|
173
|
+
$lte: 500
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
})).resolves.toBeDefined();
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('should accept mixed implicit and explicit syntax', async () => {
|
|
180
|
+
const ctx = app.createContext({ userId: 'test', isSystem: true });
|
|
181
|
+
const repo = ctx.object('product');
|
|
182
|
+
|
|
183
|
+
await expect(repo.find({
|
|
184
|
+
filters: {
|
|
185
|
+
category: 'Electronics',
|
|
186
|
+
price: { $gte: 100 }
|
|
187
|
+
}
|
|
188
|
+
})).resolves.toBeDefined();
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
describe('Backward Compatibility', () => {
|
|
193
|
+
it('should still support legacy array-based filter syntax', async () => {
|
|
194
|
+
const ctx = app.createContext({ userId: 'test', isSystem: true });
|
|
195
|
+
const repo = ctx.object('product');
|
|
196
|
+
|
|
197
|
+
// Old syntax should still work
|
|
198
|
+
await expect(repo.find({
|
|
199
|
+
filters: [['category', '=', 'Electronics']] as any
|
|
200
|
+
})).resolves.toBeDefined();
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it('should support legacy complex filters with logical operators', async () => {
|
|
204
|
+
const ctx = app.createContext({ userId: 'test', isSystem: true });
|
|
205
|
+
const repo = ctx.object('product');
|
|
206
|
+
|
|
207
|
+
await expect(repo.find({
|
|
208
|
+
filters: [
|
|
209
|
+
['category', '=', 'Electronics'],
|
|
210
|
+
'and',
|
|
211
|
+
['status', '=', 'active']
|
|
212
|
+
] as any
|
|
213
|
+
})).resolves.toBeDefined();
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it('should support legacy nested filter groups', async () => {
|
|
217
|
+
const ctx = app.createContext({ userId: 'test', isSystem: true });
|
|
218
|
+
const repo = ctx.object('product');
|
|
219
|
+
|
|
220
|
+
await expect(repo.find({
|
|
221
|
+
filters: [
|
|
222
|
+
[
|
|
223
|
+
['category', '=', 'Electronics'],
|
|
224
|
+
'or',
|
|
225
|
+
['category', '=', 'Furniture']
|
|
226
|
+
],
|
|
227
|
+
'and',
|
|
228
|
+
['status', '=', 'active']
|
|
229
|
+
] as any
|
|
230
|
+
})).resolves.toBeDefined();
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
});
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectQL Formula Plugin Tests
|
|
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
|
+
import { FormulaPlugin } from '../src/formula-plugin';
|
|
10
|
+
import { ObjectStackKernel } from '@objectstack/runtime';
|
|
11
|
+
|
|
12
|
+
describe('FormulaPlugin', () => {
|
|
13
|
+
let plugin: FormulaPlugin;
|
|
14
|
+
let mockKernel: any;
|
|
15
|
+
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
// Create a mock kernel with middleware and formula provider support
|
|
18
|
+
mockKernel = {
|
|
19
|
+
use: jest.fn(),
|
|
20
|
+
registerFormulaProvider: jest.fn(),
|
|
21
|
+
};
|
|
22
|
+
plugin = new FormulaPlugin();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe('Plugin Metadata', () => {
|
|
26
|
+
it('should have correct name and version', () => {
|
|
27
|
+
expect(plugin.name).toBe('@objectql/formulas');
|
|
28
|
+
expect(plugin.version).toBe('4.0.0');
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe('Constructor', () => {
|
|
33
|
+
it('should create plugin with default config', () => {
|
|
34
|
+
const defaultPlugin = new FormulaPlugin();
|
|
35
|
+
expect(defaultPlugin).toBeDefined();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('should create plugin with custom config', () => {
|
|
39
|
+
const customPlugin = new FormulaPlugin({
|
|
40
|
+
enable_cache: true,
|
|
41
|
+
cache_ttl: 600,
|
|
42
|
+
autoEvaluateOnQuery: false,
|
|
43
|
+
});
|
|
44
|
+
expect(customPlugin).toBeDefined();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('should accept formula engine config', () => {
|
|
48
|
+
const customPlugin = new FormulaPlugin({
|
|
49
|
+
enable_monitoring: true,
|
|
50
|
+
max_execution_time: 5000,
|
|
51
|
+
});
|
|
52
|
+
expect(customPlugin).toBeDefined();
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe('Installation', () => {
|
|
57
|
+
it('should install successfully with mock kernel', async () => {
|
|
58
|
+
const ctx = { engine: mockKernel };
|
|
59
|
+
await plugin.install(ctx);
|
|
60
|
+
|
|
61
|
+
// Verify that formula provider was registered
|
|
62
|
+
expect(mockKernel.registerFormulaProvider).toHaveBeenCalled();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('should register formula provider with evaluate function', async () => {
|
|
66
|
+
const ctx = { engine: mockKernel };
|
|
67
|
+
await plugin.install(ctx);
|
|
68
|
+
|
|
69
|
+
expect(mockKernel.registerFormulaProvider).toHaveBeenCalledWith(
|
|
70
|
+
expect.objectContaining({
|
|
71
|
+
evaluate: expect.any(Function),
|
|
72
|
+
validate: expect.any(Function),
|
|
73
|
+
extractMetadata: expect.any(Function),
|
|
74
|
+
})
|
|
75
|
+
);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('should register formula middleware when auto-evaluation is enabled', async () => {
|
|
79
|
+
const pluginWithAuto = new FormulaPlugin({ autoEvaluateOnQuery: true });
|
|
80
|
+
const ctx = { engine: mockKernel };
|
|
81
|
+
|
|
82
|
+
await pluginWithAuto.install(ctx);
|
|
83
|
+
|
|
84
|
+
// Check that middleware was registered
|
|
85
|
+
expect(mockKernel.use).toHaveBeenCalledWith('afterQuery', expect.any(Function));
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('should not register formula middleware when auto-evaluation is disabled', async () => {
|
|
89
|
+
const pluginNoAuto = new FormulaPlugin({ autoEvaluateOnQuery: false });
|
|
90
|
+
const ctx = { engine: mockKernel };
|
|
91
|
+
|
|
92
|
+
await pluginNoAuto.install(ctx);
|
|
93
|
+
|
|
94
|
+
// Should not have registered afterQuery hook
|
|
95
|
+
const afterQueryCalls = mockKernel.use.mock.calls.filter(
|
|
96
|
+
(call: any[]) => call[0] === 'afterQuery'
|
|
97
|
+
);
|
|
98
|
+
expect(afterQueryCalls.length).toBe(0);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('should handle kernel without registerFormulaProvider', async () => {
|
|
102
|
+
const kernelNoProvider = {
|
|
103
|
+
use: jest.fn(),
|
|
104
|
+
};
|
|
105
|
+
const ctx = { engine: kernelNoProvider };
|
|
106
|
+
|
|
107
|
+
// Should not throw error and should set formulaEngine property
|
|
108
|
+
await expect(plugin.install(ctx)).resolves.not.toThrow();
|
|
109
|
+
expect((kernelNoProvider as any).formulaEngine).toBeDefined();
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('should handle kernel without middleware support', async () => {
|
|
113
|
+
const kernelNoMiddleware = {
|
|
114
|
+
registerFormulaProvider: jest.fn(),
|
|
115
|
+
};
|
|
116
|
+
const ctx = { engine: kernelNoMiddleware };
|
|
117
|
+
|
|
118
|
+
// Should not throw error
|
|
119
|
+
await expect(plugin.install(ctx)).resolves.not.toThrow();
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
describe('Formula Provider', () => {
|
|
124
|
+
it('should provide evaluate function that works', async () => {
|
|
125
|
+
const ctx = { engine: mockKernel };
|
|
126
|
+
await plugin.install(ctx);
|
|
127
|
+
|
|
128
|
+
// Get the registered provider
|
|
129
|
+
const provider = mockKernel.registerFormulaProvider.mock.calls[0][0];
|
|
130
|
+
|
|
131
|
+
// Test the evaluate function
|
|
132
|
+
const result = provider.evaluate('1 + 1', {
|
|
133
|
+
record: {},
|
|
134
|
+
system: {
|
|
135
|
+
today: new Date(),
|
|
136
|
+
now: new Date(),
|
|
137
|
+
year: 2026,
|
|
138
|
+
month: 1,
|
|
139
|
+
day: 22,
|
|
140
|
+
hour: 12,
|
|
141
|
+
minute: 0,
|
|
142
|
+
second: 0,
|
|
143
|
+
},
|
|
144
|
+
current_user: {},
|
|
145
|
+
is_new: false,
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
expect(result).toBeDefined();
|
|
149
|
+
expect(result.success).toBe(true);
|
|
150
|
+
// The result is coerced to 'text' by default, so it's a string "2"
|
|
151
|
+
expect(result.value).toBe('2');
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('should provide validate function that works', async () => {
|
|
155
|
+
const ctx = { engine: mockKernel };
|
|
156
|
+
await plugin.install(ctx);
|
|
157
|
+
|
|
158
|
+
// Get the registered provider
|
|
159
|
+
const provider = mockKernel.registerFormulaProvider.mock.calls[0][0];
|
|
160
|
+
|
|
161
|
+
// Test the validate function
|
|
162
|
+
const result = provider.validate('quantity * price');
|
|
163
|
+
|
|
164
|
+
expect(result).toBeDefined();
|
|
165
|
+
expect(result.valid).toBe(true);
|
|
166
|
+
expect(result.errors.length).toBe(0);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('should provide extractMetadata function that works', async () => {
|
|
170
|
+
const ctx = { engine: mockKernel };
|
|
171
|
+
await plugin.install(ctx);
|
|
172
|
+
|
|
173
|
+
// Get the registered provider
|
|
174
|
+
const provider = mockKernel.registerFormulaProvider.mock.calls[0][0];
|
|
175
|
+
|
|
176
|
+
// Test the extractMetadata function
|
|
177
|
+
const metadata = provider.extractMetadata('total', 'quantity * price', 'number');
|
|
178
|
+
|
|
179
|
+
expect(metadata).toBeDefined();
|
|
180
|
+
expect(metadata.field_name).toBe('total');
|
|
181
|
+
expect(metadata.expression).toBe('quantity * price');
|
|
182
|
+
expect(metadata.data_type).toBe('number');
|
|
183
|
+
expect(metadata.dependencies).toContain('quantity');
|
|
184
|
+
expect(metadata.dependencies).toContain('price');
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
describe('Engine Access', () => {
|
|
189
|
+
it('should expose formula engine instance', () => {
|
|
190
|
+
const engine = plugin.getEngine();
|
|
191
|
+
expect(engine).toBeDefined();
|
|
192
|
+
expect(typeof engine.evaluate).toBe('function');
|
|
193
|
+
expect(typeof engine.validate).toBe('function');
|
|
194
|
+
expect(typeof engine.extractMetadata).toBe('function');
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
});
|
|
@@ -1,3 +1,11 @@
|
|
|
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
|
+
|
|
1
9
|
import { convertIntrospectedSchemaToObjects } from '../src/util';
|
|
2
10
|
import { IntrospectedSchema, ObjectConfig } from '@objectql/types';
|
|
3
11
|
|
package/test/mock-driver.ts
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
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
|
+
|
|
1
9
|
import { Driver } from '@objectql/types';
|
|
2
10
|
|
|
3
11
|
export class MockDriver implements Driver {
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectQL Plugin Integration Tests
|
|
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
|
+
import { ObjectQLPlugin } from '../src/plugin';
|
|
10
|
+
import { ValidatorPlugin } from '../src/validator-plugin';
|
|
11
|
+
import { FormulaPlugin } from '../src/formula-plugin';
|
|
12
|
+
|
|
13
|
+
// Mock the sub-plugins
|
|
14
|
+
jest.mock('../src/validator-plugin');
|
|
15
|
+
jest.mock('../src/formula-plugin');
|
|
16
|
+
|
|
17
|
+
describe('ObjectQLPlugin Integration', () => {
|
|
18
|
+
let plugin: ObjectQLPlugin;
|
|
19
|
+
let mockKernel: any;
|
|
20
|
+
let mockContext: any;
|
|
21
|
+
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
// Clear all mocks
|
|
24
|
+
jest.clearAllMocks();
|
|
25
|
+
|
|
26
|
+
// Create a mock kernel
|
|
27
|
+
mockKernel = {
|
|
28
|
+
use: jest.fn(),
|
|
29
|
+
registerFormulaProvider: jest.fn(),
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
mockContext = { engine: mockKernel };
|
|
33
|
+
|
|
34
|
+
// Setup mock implementations
|
|
35
|
+
(ValidatorPlugin as jest.Mock).mockImplementation(() => ({
|
|
36
|
+
install: jest.fn().mockResolvedValue(undefined),
|
|
37
|
+
}));
|
|
38
|
+
|
|
39
|
+
(FormulaPlugin as jest.Mock).mockImplementation(() => ({
|
|
40
|
+
install: jest.fn().mockResolvedValue(undefined),
|
|
41
|
+
}));
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe('Plugin Metadata', () => {
|
|
45
|
+
it('should have correct name and version', () => {
|
|
46
|
+
plugin = new ObjectQLPlugin();
|
|
47
|
+
expect(plugin.name).toBe('@objectql/core');
|
|
48
|
+
expect(plugin.version).toBe('4.0.0');
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe('Constructor', () => {
|
|
53
|
+
it('should create plugin with default config', () => {
|
|
54
|
+
plugin = new ObjectQLPlugin();
|
|
55
|
+
expect(plugin).toBeDefined();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('should create plugin with custom config', () => {
|
|
59
|
+
plugin = new ObjectQLPlugin({
|
|
60
|
+
enableRepository: false,
|
|
61
|
+
enableValidator: false,
|
|
62
|
+
enableFormulas: true,
|
|
63
|
+
enableAI: false,
|
|
64
|
+
});
|
|
65
|
+
expect(plugin).toBeDefined();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('should accept validator config', () => {
|
|
69
|
+
plugin = new ObjectQLPlugin({
|
|
70
|
+
validatorConfig: {
|
|
71
|
+
language: 'zh-CN',
|
|
72
|
+
enableQueryValidation: false,
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
expect(plugin).toBeDefined();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('should accept formula config', () => {
|
|
79
|
+
plugin = new ObjectQLPlugin({
|
|
80
|
+
formulaConfig: {
|
|
81
|
+
enable_cache: true,
|
|
82
|
+
cache_ttl: 600,
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
expect(plugin).toBeDefined();
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
describe('Installation - Conditional Plugin Loading', () => {
|
|
90
|
+
it('should install validator plugin when enabled', async () => {
|
|
91
|
+
plugin = new ObjectQLPlugin({ enableValidator: true });
|
|
92
|
+
await plugin.install(mockContext);
|
|
93
|
+
|
|
94
|
+
expect(ValidatorPlugin).toHaveBeenCalled();
|
|
95
|
+
const validatorInstance = (ValidatorPlugin as jest.Mock).mock.results[0].value;
|
|
96
|
+
expect(validatorInstance.install).toHaveBeenCalledWith(mockContext);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('should not install validator plugin when disabled', async () => {
|
|
100
|
+
plugin = new ObjectQLPlugin({ enableValidator: false });
|
|
101
|
+
await plugin.install(mockContext);
|
|
102
|
+
|
|
103
|
+
expect(ValidatorPlugin).not.toHaveBeenCalled();
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('should install formula plugin when enabled', async () => {
|
|
107
|
+
plugin = new ObjectQLPlugin({ enableFormulas: true });
|
|
108
|
+
await plugin.install(mockContext);
|
|
109
|
+
|
|
110
|
+
expect(FormulaPlugin).toHaveBeenCalled();
|
|
111
|
+
const formulaInstance = (FormulaPlugin as jest.Mock).mock.results[0].value;
|
|
112
|
+
expect(formulaInstance.install).toHaveBeenCalledWith(mockContext);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('should not install formula plugin when disabled', async () => {
|
|
116
|
+
plugin = new ObjectQLPlugin({ enableFormulas: false });
|
|
117
|
+
await plugin.install(mockContext);
|
|
118
|
+
|
|
119
|
+
expect(FormulaPlugin).not.toHaveBeenCalled();
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('should pass validator config to validator plugin', async () => {
|
|
123
|
+
const validatorConfig = {
|
|
124
|
+
language: 'zh-CN',
|
|
125
|
+
enableQueryValidation: false,
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
plugin = new ObjectQLPlugin({
|
|
129
|
+
enableValidator: true,
|
|
130
|
+
validatorConfig,
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
await plugin.install(mockContext);
|
|
134
|
+
|
|
135
|
+
expect(ValidatorPlugin).toHaveBeenCalledWith(validatorConfig);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('should pass formula config to formula plugin', async () => {
|
|
139
|
+
const formulaConfig = {
|
|
140
|
+
enable_cache: true,
|
|
141
|
+
cache_ttl: 600,
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
plugin = new ObjectQLPlugin({
|
|
145
|
+
enableFormulas: true,
|
|
146
|
+
formulaConfig,
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
await plugin.install(mockContext);
|
|
150
|
+
|
|
151
|
+
expect(FormulaPlugin).toHaveBeenCalledWith(formulaConfig);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('should install multiple plugins when all enabled', async () => {
|
|
155
|
+
plugin = new ObjectQLPlugin({
|
|
156
|
+
enableValidator: true,
|
|
157
|
+
enableFormulas: true,
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
await plugin.install(mockContext);
|
|
161
|
+
|
|
162
|
+
expect(ValidatorPlugin).toHaveBeenCalled();
|
|
163
|
+
expect(FormulaPlugin).toHaveBeenCalled();
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('should not install any plugins when all disabled', async () => {
|
|
167
|
+
plugin = new ObjectQLPlugin({
|
|
168
|
+
enableRepository: false,
|
|
169
|
+
enableValidator: false,
|
|
170
|
+
enableFormulas: false,
|
|
171
|
+
enableAI: false,
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
await plugin.install(mockContext);
|
|
175
|
+
|
|
176
|
+
expect(ValidatorPlugin).not.toHaveBeenCalled();
|
|
177
|
+
expect(FormulaPlugin).not.toHaveBeenCalled();
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
describe('Lifecycle Hooks', () => {
|
|
182
|
+
it('should have onStart method', async () => {
|
|
183
|
+
plugin = new ObjectQLPlugin();
|
|
184
|
+
expect(typeof plugin.onStart).toBe('function');
|
|
185
|
+
|
|
186
|
+
// Should not throw when called
|
|
187
|
+
await expect(plugin.onStart(mockContext)).resolves.not.toThrow();
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
describe('Default Configuration', () => {
|
|
192
|
+
it('should enable all features by default', async () => {
|
|
193
|
+
plugin = new ObjectQLPlugin();
|
|
194
|
+
await plugin.install(mockContext);
|
|
195
|
+
|
|
196
|
+
// Validator and Formula should be installed by default
|
|
197
|
+
expect(ValidatorPlugin).toHaveBeenCalled();
|
|
198
|
+
expect(FormulaPlugin).toHaveBeenCalled();
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it('should treat undefined config as enabled', async () => {
|
|
202
|
+
plugin = new ObjectQLPlugin({
|
|
203
|
+
// Explicitly not setting enableValidator or enableFormulas
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
await plugin.install(mockContext);
|
|
207
|
+
|
|
208
|
+
// Both should still be installed
|
|
209
|
+
expect(ValidatorPlugin).toHaveBeenCalled();
|
|
210
|
+
expect(FormulaPlugin).toHaveBeenCalled();
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
});
|