@object-ui/core 0.3.1 → 0.5.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/.turbo/turbo-build.log +4 -0
- package/dist/actions/index.d.ts +1 -1
- package/dist/actions/index.js +1 -1
- package/dist/evaluator/ExpressionCache.d.ts +101 -0
- package/dist/evaluator/ExpressionCache.js +135 -0
- package/dist/evaluator/ExpressionEvaluator.d.ts +20 -2
- package/dist/evaluator/ExpressionEvaluator.js +34 -14
- package/dist/evaluator/index.d.ts +3 -2
- package/dist/evaluator/index.js +3 -2
- package/dist/index.d.ts +10 -7
- package/dist/index.js +9 -7
- package/dist/query/index.d.ts +6 -0
- package/dist/query/index.js +6 -0
- package/dist/query/query-ast.d.ts +32 -0
- package/dist/query/query-ast.js +268 -0
- package/dist/registry/PluginScopeImpl.d.ts +80 -0
- package/dist/registry/PluginScopeImpl.js +243 -0
- package/dist/registry/PluginSystem.d.ts +66 -0
- package/dist/registry/PluginSystem.js +142 -0
- package/dist/registry/Registry.d.ts +73 -4
- package/dist/registry/Registry.js +112 -7
- package/dist/validation/index.d.ts +9 -0
- package/dist/validation/index.js +9 -0
- package/dist/validation/validation-engine.d.ts +70 -0
- package/dist/validation/validation-engine.js +363 -0
- package/dist/validation/validators/index.d.ts +16 -0
- package/dist/validation/validators/index.js +16 -0
- package/dist/validation/validators/object-validation-engine.d.ts +118 -0
- package/dist/validation/validators/object-validation-engine.js +538 -0
- package/package.json +13 -5
- package/src/actions/index.ts +1 -1
- package/src/evaluator/ExpressionCache.ts +192 -0
- package/src/evaluator/ExpressionEvaluator.ts +33 -14
- package/src/evaluator/__tests__/ExpressionCache.test.ts +135 -0
- package/src/evaluator/index.ts +3 -2
- package/src/index.ts +10 -7
- package/src/query/__tests__/query-ast.test.ts +211 -0
- package/src/query/__tests__/window-functions.test.ts +275 -0
- package/src/query/index.ts +7 -0
- package/src/query/query-ast.ts +341 -0
- package/src/registry/PluginScopeImpl.ts +259 -0
- package/src/registry/PluginSystem.ts +161 -0
- package/src/registry/Registry.ts +125 -8
- package/src/registry/__tests__/PluginSystem.test.ts +226 -0
- package/src/registry/__tests__/Registry.test.ts +293 -0
- package/src/registry/__tests__/plugin-scope-integration.test.ts +283 -0
- package/src/validation/__tests__/object-validation-engine.test.ts +567 -0
- package/src/validation/__tests__/validation-engine.test.ts +102 -0
- package/src/validation/index.ts +10 -0
- package/src/validation/validation-engine.ts +461 -0
- package/src/validation/validators/index.ts +25 -0
- package/src/validation/validators/object-validation-engine.ts +722 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/vitest.config.ts +2 -0
- package/src/adapters/index.d.ts +0 -8
- package/src/adapters/index.js +0 -10
- package/src/builder/schema-builder.d.ts +0 -294
- package/src/builder/schema-builder.js +0 -503
- package/src/index.d.ts +0 -13
- package/src/index.js +0 -16
- package/src/registry/Registry.d.ts +0 -56
- package/src/registry/Registry.js +0 -43
- package/src/types/index.d.ts +0 -19
- package/src/types/index.js +0 -8
- package/src/utils/filter-converter.d.ts +0 -57
- package/src/utils/filter-converter.js +0 -100
- package/src/validation/schema-validator.d.ts +0 -94
- package/src/validation/schema-validator.js +0 -278
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectUI
|
|
3
|
+
* Copyright (c) 2024-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
|
+
* Plugin Scope Integration Tests
|
|
11
|
+
* Section 3.3: Test scoped plugin system to ensure isolation
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
15
|
+
import { Registry } from '../Registry';
|
|
16
|
+
import { PluginSystem } from '../PluginSystem';
|
|
17
|
+
import type { PluginScope } from '@object-ui/types';
|
|
18
|
+
|
|
19
|
+
describe('Plugin Scope Integration', () => {
|
|
20
|
+
let registry: Registry;
|
|
21
|
+
let pluginSystem: PluginSystem;
|
|
22
|
+
|
|
23
|
+
beforeEach(() => {
|
|
24
|
+
registry = new Registry();
|
|
25
|
+
pluginSystem = new PluginSystem();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe('Component Registration Isolation', () => {
|
|
29
|
+
it('should isolate component registrations between plugins', async () => {
|
|
30
|
+
const MockTableA = () => 'Table A';
|
|
31
|
+
const MockTableB = () => 'Table B';
|
|
32
|
+
|
|
33
|
+
const pluginA = {
|
|
34
|
+
name: 'plugin-a',
|
|
35
|
+
version: '1.0.0',
|
|
36
|
+
register: (scope: PluginScope) => {
|
|
37
|
+
scope.registerComponent('table', MockTableA);
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const pluginB = {
|
|
42
|
+
name: 'plugin-b',
|
|
43
|
+
version: '1.0.0',
|
|
44
|
+
register: (scope: PluginScope) => {
|
|
45
|
+
scope.registerComponent('table', MockTableB);
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
await pluginSystem.loadPlugin(pluginA, registry, true);
|
|
50
|
+
await pluginSystem.loadPlugin(pluginB, registry, true);
|
|
51
|
+
|
|
52
|
+
// Both plugins should have their own namespaced component
|
|
53
|
+
const tableA = registry.getConfig('plugin-a:table');
|
|
54
|
+
const tableB = registry.getConfig('plugin-b:table');
|
|
55
|
+
|
|
56
|
+
expect(tableA).toBeDefined();
|
|
57
|
+
expect(tableB).toBeDefined();
|
|
58
|
+
expect(tableA?.component).toBe(MockTableA);
|
|
59
|
+
expect(tableB?.component).toBe(MockTableB);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('should allow scoped component to access global components', async () => {
|
|
63
|
+
const GlobalButton = () => 'Global Button';
|
|
64
|
+
const MockGrid = () => 'Grid';
|
|
65
|
+
|
|
66
|
+
// Register a global component
|
|
67
|
+
registry.register('button', GlobalButton);
|
|
68
|
+
|
|
69
|
+
const plugin = {
|
|
70
|
+
name: 'plugin-grid',
|
|
71
|
+
version: '1.0.0',
|
|
72
|
+
register: (scope: PluginScope) => {
|
|
73
|
+
scope.registerComponent('grid', MockGrid);
|
|
74
|
+
|
|
75
|
+
// Plugin should be able to access global components
|
|
76
|
+
const button = scope.getComponent('button');
|
|
77
|
+
expect(button).toBe(GlobalButton);
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
await pluginSystem.loadPlugin(plugin, registry, true);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe('State Management Isolation', () => {
|
|
86
|
+
it('should isolate state between plugins', async () => {
|
|
87
|
+
let stateA: any;
|
|
88
|
+
let stateB: any;
|
|
89
|
+
|
|
90
|
+
const pluginA = {
|
|
91
|
+
name: 'plugin-a',
|
|
92
|
+
version: '1.0.0',
|
|
93
|
+
register: (scope: PluginScope) => {
|
|
94
|
+
const [config, setConfig] = scope.useState('config', { theme: 'dark' });
|
|
95
|
+
stateA = config;
|
|
96
|
+
setConfig({ theme: 'light' });
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const pluginB = {
|
|
101
|
+
name: 'plugin-b',
|
|
102
|
+
version: '1.0.0',
|
|
103
|
+
register: (scope: PluginScope) => {
|
|
104
|
+
const [config, setConfig] = scope.useState('config', { theme: 'auto' });
|
|
105
|
+
stateB = config;
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
await pluginSystem.loadPlugin(pluginA, registry, true);
|
|
110
|
+
await pluginSystem.loadPlugin(pluginB, registry, true);
|
|
111
|
+
|
|
112
|
+
// State should be isolated
|
|
113
|
+
const scopeA = pluginSystem.getScope('plugin-a');
|
|
114
|
+
const scopeB = pluginSystem.getScope('plugin-b');
|
|
115
|
+
|
|
116
|
+
expect(scopeA?.getState('config')).toEqual({ theme: 'light' });
|
|
117
|
+
expect(scopeB?.getState('config')).toEqual({ theme: 'auto' });
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('should enforce state size limits', async () => {
|
|
121
|
+
const plugin = {
|
|
122
|
+
name: 'plugin-heavy',
|
|
123
|
+
version: '1.0.0',
|
|
124
|
+
scopeConfig: {
|
|
125
|
+
maxStateSize: 100, // Very small limit for testing
|
|
126
|
+
},
|
|
127
|
+
register: (scope: PluginScope) => {
|
|
128
|
+
// This should throw due to size limit
|
|
129
|
+
expect(() => {
|
|
130
|
+
scope.setState('largeData', {
|
|
131
|
+
data: 'x'.repeat(1000), // Much larger than 100 bytes
|
|
132
|
+
});
|
|
133
|
+
}).toThrow(/exceeded maximum state size/);
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
await pluginSystem.loadPlugin(plugin, registry, true);
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
describe('Event Bus Isolation', () => {
|
|
142
|
+
it('should isolate events between plugins', async () => {
|
|
143
|
+
const eventsA: any[] = [];
|
|
144
|
+
const eventsB: any[] = [];
|
|
145
|
+
|
|
146
|
+
const pluginA = {
|
|
147
|
+
name: 'plugin-a',
|
|
148
|
+
version: '1.0.0',
|
|
149
|
+
register: (scope: PluginScope) => {
|
|
150
|
+
scope.on('data-updated', (data) => {
|
|
151
|
+
eventsA.push(data);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
scope.emit('data-updated', { source: 'A' });
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
const pluginB = {
|
|
159
|
+
name: 'plugin-b',
|
|
160
|
+
version: '1.0.0',
|
|
161
|
+
register: (scope: PluginScope) => {
|
|
162
|
+
scope.on('data-updated', (data) => {
|
|
163
|
+
eventsB.push(data);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
scope.emit('data-updated', { source: 'B' });
|
|
167
|
+
},
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
await pluginSystem.loadPlugin(pluginA, registry, true);
|
|
171
|
+
await pluginSystem.loadPlugin(pluginB, registry, true);
|
|
172
|
+
|
|
173
|
+
// Events should be isolated - plugin A should only see its own events
|
|
174
|
+
expect(eventsA).toHaveLength(1);
|
|
175
|
+
expect(eventsA[0]).toEqual({ source: 'A' });
|
|
176
|
+
|
|
177
|
+
expect(eventsB).toHaveLength(1);
|
|
178
|
+
expect(eventsB[0]).toEqual({ source: 'B' });
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('should support global events for cross-plugin communication', async () => {
|
|
182
|
+
const globalEvents: any[] = [];
|
|
183
|
+
|
|
184
|
+
const pluginA = {
|
|
185
|
+
name: 'plugin-a',
|
|
186
|
+
version: '1.0.0',
|
|
187
|
+
register: (scope: PluginScope) => {
|
|
188
|
+
scope.onGlobal('app-ready', (data) => {
|
|
189
|
+
globalEvents.push({ plugin: 'A', data });
|
|
190
|
+
});
|
|
191
|
+
},
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
const pluginB = {
|
|
195
|
+
name: 'plugin-b',
|
|
196
|
+
version: '1.0.0',
|
|
197
|
+
register: (scope: PluginScope) => {
|
|
198
|
+
scope.onGlobal('app-ready', (data) => {
|
|
199
|
+
globalEvents.push({ plugin: 'B', data });
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
// Emit global event
|
|
203
|
+
scope.emitGlobal('app-ready', { status: 'ready' });
|
|
204
|
+
},
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
await pluginSystem.loadPlugin(pluginA, registry, true);
|
|
208
|
+
await pluginSystem.loadPlugin(pluginB, registry, true);
|
|
209
|
+
|
|
210
|
+
// Both plugins should receive the global event
|
|
211
|
+
expect(globalEvents).toHaveLength(2);
|
|
212
|
+
expect(globalEvents[0]).toEqual({ plugin: 'A', data: { status: 'ready' } });
|
|
213
|
+
expect(globalEvents[1]).toEqual({ plugin: 'B', data: { status: 'ready' } });
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
describe('Plugin Lifecycle', () => {
|
|
218
|
+
it('should cleanup plugin resources on unload', async () => {
|
|
219
|
+
const plugin = {
|
|
220
|
+
name: 'plugin-temp',
|
|
221
|
+
version: '1.0.0',
|
|
222
|
+
register: (scope: PluginScope) => {
|
|
223
|
+
scope.setState('data', { value: 123 });
|
|
224
|
+
scope.registerComponent('temp', () => 'Temp');
|
|
225
|
+
},
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
await pluginSystem.loadPlugin(plugin, registry, true);
|
|
229
|
+
|
|
230
|
+
const scope = pluginSystem.getScope('plugin-temp');
|
|
231
|
+
expect(scope?.getState('data')).toEqual({ value: 123 });
|
|
232
|
+
|
|
233
|
+
await pluginSystem.unloadPlugin('plugin-temp');
|
|
234
|
+
|
|
235
|
+
// Scope should be cleaned up
|
|
236
|
+
expect(pluginSystem.getScope('plugin-temp')).toBeUndefined();
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('should call lifecycle hooks', async () => {
|
|
240
|
+
let loadCalled = false;
|
|
241
|
+
let unloadCalled = false;
|
|
242
|
+
|
|
243
|
+
const plugin = {
|
|
244
|
+
name: 'plugin-lifecycle',
|
|
245
|
+
version: '1.0.0',
|
|
246
|
+
register: () => {},
|
|
247
|
+
onLoad: async () => {
|
|
248
|
+
loadCalled = true;
|
|
249
|
+
},
|
|
250
|
+
onUnload: async () => {
|
|
251
|
+
unloadCalled = true;
|
|
252
|
+
},
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
await pluginSystem.loadPlugin(plugin, registry, true);
|
|
256
|
+
expect(loadCalled).toBe(true);
|
|
257
|
+
|
|
258
|
+
await pluginSystem.unloadPlugin('plugin-lifecycle');
|
|
259
|
+
expect(unloadCalled).toBe(true);
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
describe('Legacy Compatibility', () => {
|
|
264
|
+
it('should support legacy plugins without scopes', async () => {
|
|
265
|
+
const MockComponent = () => 'Legacy';
|
|
266
|
+
|
|
267
|
+
const legacyPlugin = {
|
|
268
|
+
name: 'legacy-plugin',
|
|
269
|
+
version: '1.0.0',
|
|
270
|
+
register: (reg: Registry) => {
|
|
271
|
+
reg.register('legacy', MockComponent);
|
|
272
|
+
},
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
// Load without scope
|
|
276
|
+
await pluginSystem.loadPlugin(legacyPlugin, registry, false);
|
|
277
|
+
|
|
278
|
+
// Component should be registered directly
|
|
279
|
+
const component = registry.getConfig('legacy');
|
|
280
|
+
expect(component?.component).toBe(MockComponent);
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
});
|