@object-ui/core 0.3.0 → 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.
Files changed (90) hide show
  1. package/.turbo/turbo-build.log +4 -0
  2. package/CHANGELOG.md +8 -0
  3. package/dist/actions/ActionRunner.d.ts +40 -0
  4. package/dist/actions/ActionRunner.js +160 -0
  5. package/dist/actions/index.d.ts +8 -0
  6. package/dist/actions/index.js +8 -0
  7. package/dist/adapters/index.d.ts +7 -0
  8. package/dist/adapters/index.js +10 -0
  9. package/dist/builder/schema-builder.d.ts +7 -0
  10. package/dist/builder/schema-builder.js +4 -6
  11. package/dist/evaluator/ExpressionCache.d.ts +101 -0
  12. package/dist/evaluator/ExpressionCache.js +135 -0
  13. package/dist/evaluator/ExpressionContext.d.ts +51 -0
  14. package/dist/evaluator/ExpressionContext.js +110 -0
  15. package/dist/evaluator/ExpressionEvaluator.d.ts +117 -0
  16. package/dist/evaluator/ExpressionEvaluator.js +220 -0
  17. package/dist/evaluator/index.d.ts +10 -0
  18. package/dist/evaluator/index.js +10 -0
  19. package/dist/index.d.ts +17 -4
  20. package/dist/index.js +16 -5
  21. package/dist/query/index.d.ts +6 -0
  22. package/dist/query/index.js +6 -0
  23. package/dist/query/query-ast.d.ts +32 -0
  24. package/dist/query/query-ast.js +268 -0
  25. package/dist/registry/PluginScopeImpl.d.ts +80 -0
  26. package/dist/registry/PluginScopeImpl.js +243 -0
  27. package/dist/registry/PluginSystem.d.ts +66 -0
  28. package/dist/registry/PluginSystem.js +142 -0
  29. package/dist/registry/Registry.d.ts +80 -4
  30. package/dist/registry/Registry.js +119 -7
  31. package/dist/types/index.d.ts +7 -0
  32. package/dist/types/index.js +7 -0
  33. package/dist/utils/filter-converter.d.ts +57 -0
  34. package/dist/utils/filter-converter.js +100 -0
  35. package/dist/validation/index.d.ts +9 -0
  36. package/dist/validation/index.js +9 -0
  37. package/dist/validation/schema-validator.d.ts +7 -0
  38. package/dist/validation/schema-validator.js +4 -6
  39. package/dist/validation/validation-engine.d.ts +70 -0
  40. package/dist/validation/validation-engine.js +363 -0
  41. package/dist/validation/validators/index.d.ts +16 -0
  42. package/dist/validation/validators/index.js +16 -0
  43. package/dist/validation/validators/object-validation-engine.d.ts +118 -0
  44. package/dist/validation/validators/object-validation-engine.js +538 -0
  45. package/package.json +26 -7
  46. package/src/actions/ActionRunner.ts +195 -0
  47. package/src/actions/index.ts +9 -0
  48. package/src/adapters/README.md +180 -0
  49. package/src/adapters/index.ts +10 -0
  50. package/src/builder/schema-builder.ts +8 -0
  51. package/src/evaluator/ExpressionCache.ts +192 -0
  52. package/src/evaluator/ExpressionContext.ts +118 -0
  53. package/src/evaluator/ExpressionEvaluator.ts +267 -0
  54. package/src/evaluator/__tests__/ExpressionCache.test.ts +135 -0
  55. package/src/evaluator/__tests__/ExpressionEvaluator.test.ts +101 -0
  56. package/src/evaluator/index.ts +11 -0
  57. package/src/index.test.ts +8 -0
  58. package/src/index.ts +18 -5
  59. package/src/query/__tests__/query-ast.test.ts +211 -0
  60. package/src/query/__tests__/window-functions.test.ts +275 -0
  61. package/src/query/index.ts +7 -0
  62. package/src/query/query-ast.ts +341 -0
  63. package/src/registry/PluginScopeImpl.ts +259 -0
  64. package/src/registry/PluginSystem.ts +161 -0
  65. package/src/registry/Registry.ts +133 -8
  66. package/src/registry/__tests__/PluginSystem.test.ts +226 -0
  67. package/src/registry/__tests__/Registry.test.ts +293 -0
  68. package/src/registry/__tests__/plugin-scope-integration.test.ts +283 -0
  69. package/src/types/index.ts +8 -0
  70. package/src/utils/__tests__/filter-converter.test.ts +118 -0
  71. package/src/utils/filter-converter.ts +133 -0
  72. package/src/validation/__tests__/object-validation-engine.test.ts +567 -0
  73. package/src/validation/__tests__/validation-engine.test.ts +102 -0
  74. package/src/validation/index.ts +10 -0
  75. package/src/validation/schema-validator.ts +8 -0
  76. package/src/validation/validation-engine.ts +461 -0
  77. package/src/validation/validators/index.ts +25 -0
  78. package/src/validation/validators/object-validation-engine.ts +722 -0
  79. package/tsconfig.tsbuildinfo +1 -1
  80. package/vitest.config.ts +2 -0
  81. package/src/builder/schema-builder.d.ts +0 -287
  82. package/src/builder/schema-builder.js +0 -505
  83. package/src/index.d.ts +0 -4
  84. package/src/index.js +0 -7
  85. package/src/registry/Registry.d.ts +0 -49
  86. package/src/registry/Registry.js +0 -36
  87. package/src/types/index.d.ts +0 -12
  88. package/src/types/index.js +0 -1
  89. package/src/validation/schema-validator.d.ts +0 -87
  90. package/src/validation/schema-validator.js +0 -280
@@ -0,0 +1,341 @@
1
+ /**
2
+ * ObjectUI - Query AST Builder
3
+ * Phase 3.3: QuerySchema AST implementation
4
+ * ObjectStack Spec v0.7.1: Window functions support
5
+ */
6
+
7
+ import type {
8
+ QueryAST,
9
+ QuerySchema,
10
+ SelectNode,
11
+ FromNode,
12
+ WhereNode,
13
+ JoinNode,
14
+ GroupByNode,
15
+ OrderByNode,
16
+ LimitNode,
17
+ OffsetNode,
18
+ AggregateNode,
19
+ WindowNode,
20
+ WindowFunction,
21
+ WindowFrame,
22
+ WindowConfig,
23
+ FieldNode,
24
+ LiteralNode,
25
+ OperatorNode,
26
+ LogicalOperator,
27
+ AdvancedFilterSchema,
28
+ AdvancedFilterCondition,
29
+ QuerySortConfig,
30
+ JoinConfig,
31
+ AggregationConfig,
32
+ } from '@object-ui/types';
33
+
34
+ /**
35
+ * Query AST Builder - Converts QuerySchema to AST
36
+ */
37
+ export class QueryASTBuilder {
38
+ build(query: QuerySchema): QueryAST {
39
+ const ast: QueryAST = {
40
+ select: this.buildSelect(query),
41
+ from: this.buildFrom(query),
42
+ };
43
+
44
+ if (query.filter) {
45
+ ast.where = this.buildWhere(query.filter);
46
+ }
47
+
48
+ if (query.joins && query.joins.length > 0) {
49
+ ast.joins = query.joins.map(join => this.buildJoin(join));
50
+ }
51
+
52
+ if (query.group_by && query.group_by.length > 0) {
53
+ ast.group_by = this.buildGroupBy(query.group_by);
54
+ }
55
+
56
+ if (query.sort && query.sort.length > 0) {
57
+ ast.order_by = this.buildOrderBy(query.sort);
58
+ }
59
+
60
+ if (query.limit !== undefined) {
61
+ ast.limit = this.buildLimit(query.limit);
62
+ }
63
+
64
+ if (query.offset !== undefined) {
65
+ ast.offset = this.buildOffset(query.offset);
66
+ }
67
+
68
+ return ast;
69
+ }
70
+
71
+ private buildSelect(query: QuerySchema): SelectNode {
72
+ const fields: (FieldNode | AggregateNode | WindowNode)[] = [];
73
+
74
+ if (query.fields && query.fields.length > 0) {
75
+ fields.push(...query.fields.map(field => this.buildField(field)));
76
+ } else if (!query.aggregations || query.aggregations.length === 0) {
77
+ // Only add '*' if there are no aggregations
78
+ fields.push(this.buildField('*'));
79
+ }
80
+
81
+ if (query.aggregations && query.aggregations.length > 0) {
82
+ fields.push(...query.aggregations.map(agg => this.buildAggregation(agg)));
83
+ }
84
+
85
+ // Add window functions (ObjectStack Spec v0.7.1)
86
+ if (query.windows && query.windows.length > 0) {
87
+ fields.push(...query.windows.map(win => this.buildWindow(win)));
88
+ }
89
+
90
+ return {
91
+ type: 'select',
92
+ fields,
93
+ distinct: false,
94
+ };
95
+ }
96
+
97
+ private buildFrom(query: QuerySchema): FromNode {
98
+ return {
99
+ type: 'from',
100
+ table: query.object,
101
+ };
102
+ }
103
+
104
+ private buildWhere(filter: AdvancedFilterSchema): WhereNode {
105
+ return {
106
+ type: 'where',
107
+ condition: this.buildFilterCondition(filter),
108
+ };
109
+ }
110
+
111
+ private buildFilterCondition(filter: AdvancedFilterSchema): OperatorNode {
112
+ const operator = filter.operator || 'and';
113
+ const operands: (OperatorNode | FieldNode | LiteralNode)[] = [];
114
+
115
+ if (filter.conditions && filter.conditions.length > 0) {
116
+ operands.push(...filter.conditions.map(cond => this.buildCondition(cond)));
117
+ }
118
+
119
+ if (filter.groups && filter.groups.length > 0) {
120
+ operands.push(...filter.groups.map(group => this.buildFilterCondition(group)));
121
+ }
122
+
123
+ return {
124
+ type: 'operator',
125
+ operator: operator as LogicalOperator,
126
+ operands,
127
+ };
128
+ }
129
+
130
+ private buildCondition(condition: AdvancedFilterCondition): OperatorNode {
131
+ const field = this.buildField(condition.field);
132
+
133
+ // Map filter operators to comparison operators
134
+ const operatorMap: Record<string, string> = {
135
+ 'equals': '=',
136
+ 'not_equals': '!=',
137
+ 'greater_than': '>',
138
+ 'greater_than_or_equal': '>=',
139
+ 'less_than': '<',
140
+ 'less_than_or_equal': '<=',
141
+ 'contains': 'contains',
142
+ 'not_contains': 'contains',
143
+ 'starts_with': 'starts_with',
144
+ 'ends_with': 'ends_with',
145
+ 'like': 'like',
146
+ 'ilike': 'ilike',
147
+ 'in': 'in',
148
+ 'not_in': 'not_in',
149
+ 'is_null': 'is_null',
150
+ 'is_not_null': 'is_not_null',
151
+ 'between': 'between',
152
+ };
153
+
154
+ const operator = operatorMap[condition.operator] || '=';
155
+
156
+ // Handle special operators
157
+ if (operator === 'between' && condition.values && condition.values.length === 2) {
158
+ return {
159
+ type: 'operator',
160
+ operator: 'between' as any,
161
+ operands: [
162
+ field,
163
+ this.buildLiteral(condition.values[0]),
164
+ this.buildLiteral(condition.values[1])
165
+ ],
166
+ };
167
+ }
168
+
169
+ if ((operator === 'in' || operator === 'not_in') && condition.values) {
170
+ return {
171
+ type: 'operator',
172
+ operator: operator as any,
173
+ operands: [field, ...condition.values.map(v => this.buildLiteral(v))],
174
+ };
175
+ }
176
+
177
+ if (operator === 'is_null' || operator === 'is_not_null') {
178
+ return {
179
+ type: 'operator',
180
+ operator: operator as any,
181
+ operands: [field],
182
+ };
183
+ }
184
+
185
+ // Standard binary operator
186
+ const value = this.buildLiteral(condition.value);
187
+ return {
188
+ type: 'operator',
189
+ operator: operator as any,
190
+ operands: [field, value],
191
+ };
192
+ }
193
+
194
+ private buildJoin(join: JoinConfig): JoinNode {
195
+ const onCondition: OperatorNode = {
196
+ type: 'operator',
197
+ operator: '=',
198
+ operands: [
199
+ this.buildField(join.on.local_field),
200
+ this.buildField(join.on.foreign_field, join.alias || join.object),
201
+ ],
202
+ };
203
+
204
+ return {
205
+ type: 'join',
206
+ join_type: join.type,
207
+ table: join.object,
208
+ alias: join.alias,
209
+ on: onCondition,
210
+ };
211
+ }
212
+
213
+ private buildGroupBy(fields: string[]): GroupByNode {
214
+ return {
215
+ type: 'group_by',
216
+ fields: fields.map(field => this.buildField(field)),
217
+ };
218
+ }
219
+
220
+ private buildOrderBy(sorts: QuerySortConfig[]): OrderByNode {
221
+ return {
222
+ type: 'order_by',
223
+ fields: sorts.map(sort => ({
224
+ field: this.buildField(sort.field),
225
+ direction: sort.order,
226
+ })),
227
+ };
228
+ }
229
+
230
+ private buildLimit(limit: number): LimitNode {
231
+ return {
232
+ type: 'limit',
233
+ value: limit,
234
+ };
235
+ }
236
+
237
+ private buildOffset(offset: number): OffsetNode {
238
+ return {
239
+ type: 'offset',
240
+ value: offset,
241
+ };
242
+ }
243
+
244
+ private buildField(field: string, table?: string): FieldNode {
245
+ const parts = field.split('.');
246
+
247
+ if (parts.length === 2) {
248
+ return {
249
+ type: 'field',
250
+ table: parts[0],
251
+ name: parts[1],
252
+ };
253
+ }
254
+
255
+ return {
256
+ type: 'field',
257
+ table,
258
+ name: field,
259
+ };
260
+ }
261
+
262
+ private buildLiteral(value: any): LiteralNode {
263
+ let dataType: 'string' | 'number' | 'boolean' | 'date' | 'null' = 'string';
264
+
265
+ if (value === null || value === undefined) {
266
+ dataType = 'null';
267
+ } else if (typeof value === 'number') {
268
+ dataType = 'number';
269
+ } else if (typeof value === 'boolean') {
270
+ dataType = 'boolean';
271
+ } else if (value instanceof Date) {
272
+ dataType = 'date';
273
+ }
274
+
275
+ return {
276
+ type: 'literal',
277
+ value,
278
+ data_type: dataType,
279
+ };
280
+ }
281
+
282
+ private buildAggregation(agg: AggregationConfig): AggregateNode {
283
+ return {
284
+ type: 'aggregate',
285
+ function: agg.function,
286
+ field: agg.field ? this.buildField(agg.field) : undefined,
287
+ alias: agg.alias,
288
+ distinct: agg.distinct,
289
+ };
290
+ }
291
+
292
+ /**
293
+ * Build window function node (ObjectStack Spec v0.7.1)
294
+ */
295
+ private buildWindow(config: WindowConfig): WindowNode {
296
+ const node: WindowNode = {
297
+ type: 'window',
298
+ function: config.function,
299
+ alias: config.alias,
300
+ };
301
+
302
+ if (config.field) {
303
+ node.field = this.buildField(config.field);
304
+ }
305
+
306
+ if (config.partitionBy && config.partitionBy.length > 0) {
307
+ node.partitionBy = config.partitionBy.map(field => this.buildField(field));
308
+ }
309
+
310
+ if (config.orderBy && config.orderBy.length > 0) {
311
+ node.orderBy = config.orderBy.map(sort => ({
312
+ field: this.buildField(sort.field),
313
+ direction: sort.direction,
314
+ }));
315
+ }
316
+
317
+ if (config.frame) {
318
+ node.frame = config.frame;
319
+ }
320
+
321
+ if (config.offset !== undefined) {
322
+ node.offset = config.offset;
323
+ }
324
+
325
+ if (config.defaultValue !== undefined) {
326
+ node.defaultValue = this.buildLiteral(config.defaultValue);
327
+ }
328
+
329
+ return node;
330
+ }
331
+
332
+ optimize(ast: QueryAST): QueryAST {
333
+ return ast;
334
+ }
335
+ }
336
+
337
+ export const defaultQueryASTBuilder = new QueryASTBuilder();
338
+
339
+ export function buildQueryAST(query: QuerySchema): QueryAST {
340
+ return defaultQueryASTBuilder.build(query);
341
+ }
@@ -0,0 +1,259 @@
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
+ * @object-ui/core - Plugin Scope Implementation
11
+ *
12
+ * Section 3.3: Implementation of scoped plugin system to prevent conflicts.
13
+ * Provides isolated component registration, state management, and event bus.
14
+ *
15
+ * @module plugin-scope-impl
16
+ * @packageDocumentation
17
+ */
18
+
19
+ import type {
20
+ PluginScope,
21
+ PluginScopeConfig,
22
+ PluginEventHandler
23
+ } from '@object-ui/types';
24
+ import type { Registry, ComponentMeta as RegistryComponentMeta } from './Registry.js';
25
+
26
+ /**
27
+ * Event Bus for scoped plugin events
28
+ */
29
+ class EventBus {
30
+ private listeners = new Map<string, Set<PluginEventHandler>>();
31
+
32
+ on(event: string, handler: PluginEventHandler): () => void {
33
+ if (!this.listeners.has(event)) {
34
+ this.listeners.set(event, new Set());
35
+ }
36
+ this.listeners.get(event)!.add(handler);
37
+
38
+ // Return unsubscribe function
39
+ return () => {
40
+ this.listeners.get(event)?.delete(handler);
41
+ if (this.listeners.get(event)?.size === 0) {
42
+ this.listeners.delete(event);
43
+ }
44
+ };
45
+ }
46
+
47
+ emit(event: string, data?: any): void {
48
+ const handlers = this.listeners.get(event);
49
+ if (handlers) {
50
+ handlers.forEach(handler => {
51
+ try {
52
+ handler(data);
53
+ } catch (error) {
54
+ console.error(`Error in event handler for "${event}":`, error);
55
+ }
56
+ });
57
+ }
58
+ }
59
+
60
+ cleanup(): void {
61
+ this.listeners.clear();
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Global event bus for cross-plugin communication
67
+ */
68
+ const globalEventBus = new EventBus();
69
+
70
+ /**
71
+ * Plugin Scope Implementation
72
+ *
73
+ * Provides isolated access to registry, state, and events for each plugin.
74
+ */
75
+ export class PluginScopeImpl implements PluginScope {
76
+ public readonly name: string;
77
+ public readonly version: string;
78
+
79
+ private registry: Registry;
80
+ private state = new Map<string, any>();
81
+ private eventBus = new EventBus();
82
+ private config: Required<PluginScopeConfig>;
83
+
84
+ constructor(
85
+ name: string,
86
+ version: string,
87
+ registry: Registry,
88
+ config?: PluginScopeConfig
89
+ ) {
90
+ this.name = name;
91
+ this.version = version;
92
+ this.registry = registry;
93
+ this.config = {
94
+ enableStateIsolation: config?.enableStateIsolation ?? true,
95
+ enableEventIsolation: config?.enableEventIsolation ?? true,
96
+ allowGlobalEvents: config?.allowGlobalEvents ?? true,
97
+ maxStateSize: config?.maxStateSize ?? 5 * 1024 * 1024, // 5MB
98
+ };
99
+ }
100
+
101
+ /**
102
+ * Register a component in the scoped namespace
103
+ */
104
+ registerComponent(type: string, component: any, meta?: any): void {
105
+ // Components are registered as "pluginName:type"
106
+ const registryMeta: RegistryComponentMeta = {
107
+ ...meta,
108
+ namespace: this.name,
109
+ };
110
+ this.registry.register(type, component, registryMeta);
111
+ }
112
+
113
+ /**
114
+ * Get a component from the scoped namespace
115
+ */
116
+ getComponent(type: string): any | undefined {
117
+ // First try scoped lookup
118
+ const scoped = this.registry.get(`${this.name}:${type}`);
119
+ if (scoped) {
120
+ return scoped;
121
+ }
122
+
123
+ // Fall back to global lookup
124
+ return this.registry.get(type);
125
+ }
126
+
127
+ /**
128
+ * Scoped state management
129
+ */
130
+ useState<T>(key: string, initialValue: T): [T, (value: T | ((prev: T) => T)) => void] {
131
+ if (!this.config.enableStateIsolation) {
132
+ throw new Error('State isolation is disabled for this plugin');
133
+ }
134
+
135
+ // Initialize state if not present
136
+ if (!this.state.has(key)) {
137
+ this.setState(key, initialValue);
138
+ }
139
+
140
+ const currentValue = this.getState<T>(key) ?? initialValue;
141
+
142
+ const setValue = (value: T | ((prev: T) => T)) => {
143
+ const newValue = typeof value === 'function'
144
+ ? (value as (prev: T) => T)(this.getState<T>(key) ?? initialValue)
145
+ : value;
146
+ this.setState(key, newValue);
147
+ };
148
+
149
+ return [currentValue, setValue];
150
+ }
151
+
152
+ /**
153
+ * Get scoped state value
154
+ */
155
+ getState<T>(key: string): T | undefined {
156
+ return this.state.get(key);
157
+ }
158
+
159
+ /**
160
+ * Set scoped state value
161
+ */
162
+ setState<T>(key: string, value: T): void {
163
+ if (!this.config.enableStateIsolation) {
164
+ throw new Error('State isolation is disabled for this plugin');
165
+ }
166
+
167
+ // Check state size limit
168
+ const stateSize = this.estimateStateSize();
169
+ const valueSize = this.estimateValueSize(value);
170
+
171
+ if (stateSize + valueSize > this.config.maxStateSize) {
172
+ throw new Error(
173
+ `Plugin "${this.name}" exceeded maximum state size of ${this.config.maxStateSize} bytes`
174
+ );
175
+ }
176
+
177
+ this.state.set(key, value);
178
+ }
179
+
180
+ /**
181
+ * Subscribe to scoped events
182
+ */
183
+ on(event: string, handler: PluginEventHandler): () => void {
184
+ if (!this.config.enableEventIsolation) {
185
+ // If isolation is disabled, use global event bus
186
+ return this.onGlobal(event, handler);
187
+ }
188
+
189
+ // Scoped event: prefix with plugin name
190
+ const scopedEvent = `${this.name}:${event}`;
191
+ return this.eventBus.on(scopedEvent, handler);
192
+ }
193
+
194
+ /**
195
+ * Emit a scoped event
196
+ */
197
+ emit(event: string, data?: any): void {
198
+ if (!this.config.enableEventIsolation) {
199
+ // If isolation is disabled, emit globally
200
+ this.emitGlobal(event, data);
201
+ return;
202
+ }
203
+
204
+ // Scoped event: prefix with plugin name
205
+ const scopedEvent = `${this.name}:${event}`;
206
+ this.eventBus.emit(scopedEvent, data);
207
+ }
208
+
209
+ /**
210
+ * Emit a global event
211
+ */
212
+ emitGlobal(event: string, data?: any): void {
213
+ if (!this.config.allowGlobalEvents) {
214
+ throw new Error('Global events are disabled for this plugin');
215
+ }
216
+ globalEventBus.emit(event, data);
217
+ }
218
+
219
+ /**
220
+ * Subscribe to global events
221
+ */
222
+ onGlobal(event: string, handler: PluginEventHandler): () => void {
223
+ if (!this.config.allowGlobalEvents) {
224
+ throw new Error('Global events are disabled for this plugin');
225
+ }
226
+ return globalEventBus.on(event, handler);
227
+ }
228
+
229
+ /**
230
+ * Clean up all plugin resources
231
+ */
232
+ cleanup(): void {
233
+ this.state.clear();
234
+ this.eventBus.cleanup();
235
+ }
236
+
237
+ /**
238
+ * Estimate total state size in bytes
239
+ */
240
+ private estimateStateSize(): number {
241
+ let size = 0;
242
+ for (const value of this.state.values()) {
243
+ size += this.estimateValueSize(value);
244
+ }
245
+ return size;
246
+ }
247
+
248
+ /**
249
+ * Estimate size of a value in bytes
250
+ */
251
+ private estimateValueSize(value: any): number {
252
+ try {
253
+ return JSON.stringify(value).length * 2; // UTF-16 encoding
254
+ } catch {
255
+ // If not serializable, use rough estimate
256
+ return 1024; // 1KB default
257
+ }
258
+ }
259
+ }