@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.
Files changed (98) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/IMPLEMENTATION_STATUS.md +364 -0
  3. package/README.md +31 -9
  4. package/RUNTIME_INTEGRATION.md +391 -0
  5. package/dist/ai-agent.d.ts +4 -3
  6. package/dist/ai-agent.js +10 -3
  7. package/dist/ai-agent.js.map +1 -1
  8. package/dist/app.d.ts +29 -6
  9. package/dist/app.js +117 -58
  10. package/dist/app.js.map +1 -1
  11. package/dist/formula-engine.d.ts +7 -0
  12. package/dist/formula-engine.js +9 -2
  13. package/dist/formula-engine.js.map +1 -1
  14. package/dist/formula-plugin.d.ts +52 -0
  15. package/dist/formula-plugin.js +107 -0
  16. package/dist/formula-plugin.js.map +1 -0
  17. package/dist/index.d.ts +13 -3
  18. package/dist/index.js +14 -3
  19. package/dist/index.js.map +1 -1
  20. package/dist/plugin.d.ts +89 -0
  21. package/dist/plugin.js +99 -0
  22. package/dist/plugin.js.map +1 -0
  23. package/dist/query/filter-translator.d.ts +37 -0
  24. package/dist/query/filter-translator.js +135 -0
  25. package/dist/query/filter-translator.js.map +1 -0
  26. package/dist/query/index.d.ts +22 -0
  27. package/dist/query/index.js +39 -0
  28. package/dist/query/index.js.map +1 -0
  29. package/dist/query/query-analyzer.d.ts +186 -0
  30. package/dist/query/query-analyzer.js +349 -0
  31. package/dist/query/query-analyzer.js.map +1 -0
  32. package/dist/query/query-builder.d.ts +27 -0
  33. package/dist/query/query-builder.js +71 -0
  34. package/dist/query/query-builder.js.map +1 -0
  35. package/dist/query/query-service.d.ts +150 -0
  36. package/dist/query/query-service.js +268 -0
  37. package/dist/query/query-service.js.map +1 -0
  38. package/dist/repository.d.ts +23 -2
  39. package/dist/repository.js +62 -13
  40. package/dist/repository.js.map +1 -1
  41. package/dist/util.d.ts +7 -0
  42. package/dist/util.js +18 -3
  43. package/dist/util.js.map +1 -1
  44. package/dist/validator-plugin.d.ts +56 -0
  45. package/dist/validator-plugin.js +106 -0
  46. package/dist/validator-plugin.js.map +1 -0
  47. package/dist/validator.d.ts +7 -0
  48. package/dist/validator.js +10 -8
  49. package/dist/validator.js.map +1 -1
  50. package/jest.config.js +16 -0
  51. package/package.json +8 -5
  52. package/src/ai-agent.ts +8 -0
  53. package/src/app.ts +136 -72
  54. package/src/formula-engine.ts +8 -0
  55. package/src/formula-plugin.ts +141 -0
  56. package/src/index.ts +25 -3
  57. package/src/plugin.ts +179 -0
  58. package/src/query/filter-translator.ts +147 -0
  59. package/src/query/index.ts +24 -0
  60. package/src/query/query-analyzer.ts +535 -0
  61. package/src/query/query-builder.ts +80 -0
  62. package/src/query/query-service.ts +392 -0
  63. package/src/repository.ts +81 -17
  64. package/src/util.ts +19 -3
  65. package/src/validator-plugin.ts +140 -0
  66. package/src/validator.ts +12 -5
  67. package/test/__mocks__/@objectstack/runtime.ts +255 -0
  68. package/test/app.test.ts +23 -35
  69. package/test/filter-syntax.test.ts +233 -0
  70. package/test/formula-engine.test.ts +8 -0
  71. package/test/formula-integration.test.ts +8 -0
  72. package/test/formula-plugin.test.ts +197 -0
  73. package/test/introspection.test.ts +8 -0
  74. package/test/mock-driver.ts +8 -0
  75. package/test/plugin-integration.test.ts +213 -0
  76. package/test/repository-validation.test.ts +8 -0
  77. package/test/repository.test.ts +8 -0
  78. package/test/util.test.ts +9 -1
  79. package/test/utils.ts +8 -0
  80. package/test/validator-plugin.test.ts +126 -0
  81. package/test/validator.test.ts +8 -0
  82. package/tsconfig.json +9 -0
  83. package/tsconfig.tsbuildinfo +1 -1
  84. package/dist/action.d.ts +0 -7
  85. package/dist/action.js +0 -23
  86. package/dist/action.js.map +0 -1
  87. package/dist/hook.d.ts +0 -8
  88. package/dist/hook.js +0 -25
  89. package/dist/hook.js.map +0 -1
  90. package/dist/object.d.ts +0 -3
  91. package/dist/object.js +0 -28
  92. package/dist/object.js.map +0 -1
  93. package/src/action.ts +0 -40
  94. package/src/hook.ts +0 -42
  95. package/src/object.ts +0 -26
  96. package/test/action.test.ts +0 -276
  97. package/test/hook.test.ts +0 -343
  98. 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
+ });
@@ -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
  /**
2
10
  * Formula Engine Tests
3
11
  *
@@ -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
  /**
2
10
  * Formula Integration Tests
3
11
  *
@@ -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
 
@@ -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
+ });