Skip to content

Commit 90b63fd

Browse files
Copilothotlong
andcommitted
feat: Add Phase 3 runtime implementations
- Added ValidationEngine for sync/async validation with cross-field support - Added QueryASTBuilder for building SQL-like query ASTs - Implemented support for: - Custom validation functions - Async validation with debouncing - Cross-field validation - Query AST construction from QuerySchema - Joins, aggregations, sorting, filtering - Query optimization capabilities - Updated core package exports to include new modules Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
1 parent 8e4fb2d commit 90b63fd

5 files changed

Lines changed: 695 additions & 1 deletion

File tree

packages/core/src/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,12 @@
88

99
export * from './types';
1010
export * from './registry/Registry';
11-
export * from './validation/schema-validator';
11+
export * from './validation';
1212
export * from './builder/schema-builder';
1313
export * from './utils/filter-converter';
1414
export * from './evaluator';
1515
export * from './actions';
16+
export * from './query';
1617
// export * from './data-scope'; // TODO
1718
// export * from './validators'; // TODO
1819

packages/core/src/query/index.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
/**
2+
* @object-ui/core - Query Module
3+
*
4+
* Phase 3.3: Query AST builder and utilities
5+
*/
6+
7+
export * from './query-ast';
Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
/**
2+
* ObjectUI - Query AST Builder
3+
* Phase 3.3: QuerySchema AST implementation
4+
*/
5+
6+
import type {
7+
QueryAST,
8+
QuerySchema,
9+
SelectNode,
10+
FromNode,
11+
WhereNode,
12+
JoinNode,
13+
GroupByNode,
14+
OrderByNode,
15+
LimitNode,
16+
OffsetNode,
17+
AggregateNode,
18+
FieldNode,
19+
LiteralNode,
20+
OperatorNode,
21+
LogicalOperator,
22+
AdvancedFilterSchema,
23+
AdvancedFilterCondition,
24+
QuerySortConfig,
25+
JoinConfig,
26+
AggregationConfig,
27+
} from '@object-ui/types';
28+
29+
/**
30+
* Query AST Builder - Converts QuerySchema to AST
31+
*/
32+
export class QueryASTBuilder {
33+
build(query: QuerySchema): QueryAST {
34+
const ast: QueryAST = {
35+
select: this.buildSelect(query),
36+
from: this.buildFrom(query),
37+
};
38+
39+
if (query.filter) {
40+
ast.where = this.buildWhere(query.filter);
41+
}
42+
43+
if (query.joins && query.joins.length > 0) {
44+
ast.joins = query.joins.map(join => this.buildJoin(join));
45+
}
46+
47+
if (query.group_by && query.group_by.length > 0) {
48+
ast.group_by = this.buildGroupBy(query.group_by);
49+
}
50+
51+
if (query.sort && query.sort.length > 0) {
52+
ast.order_by = this.buildOrderBy(query.sort);
53+
}
54+
55+
if (query.limit !== undefined) {
56+
ast.limit = this.buildLimit(query.limit);
57+
}
58+
59+
if (query.offset !== undefined) {
60+
ast.offset = this.buildOffset(query.offset);
61+
}
62+
63+
return ast;
64+
}
65+
66+
private buildSelect(query: QuerySchema): SelectNode {
67+
const fields: (FieldNode | AggregateNode)[] = [];
68+
69+
if (query.fields && query.fields.length > 0) {
70+
fields.push(...query.fields.map(field => this.buildField(field)));
71+
} else {
72+
fields.push(this.buildField('*'));
73+
}
74+
75+
if (query.aggregations && query.aggregations.length > 0) {
76+
fields.push(...query.aggregations.map(agg => this.buildAggregation(agg)));
77+
}
78+
79+
return {
80+
type: 'select',
81+
fields,
82+
distinct: false,
83+
};
84+
}
85+
86+
private buildFrom(query: QuerySchema): FromNode {
87+
return {
88+
type: 'from',
89+
table: query.object,
90+
};
91+
}
92+
93+
private buildWhere(filter: AdvancedFilterSchema): WhereNode {
94+
return {
95+
type: 'where',
96+
condition: this.buildFilterCondition(filter),
97+
};
98+
}
99+
100+
private buildFilterCondition(filter: AdvancedFilterSchema): OperatorNode {
101+
const operator = filter.operator || 'and';
102+
const operands: (OperatorNode | FieldNode | LiteralNode)[] = [];
103+
104+
if (filter.conditions && filter.conditions.length > 0) {
105+
operands.push(...filter.conditions.map(cond => this.buildCondition(cond)));
106+
}
107+
108+
if (filter.groups && filter.groups.length > 0) {
109+
operands.push(...filter.groups.map(group => this.buildFilterCondition(group)));
110+
}
111+
112+
return {
113+
type: 'operator',
114+
operator: operator as LogicalOperator,
115+
operands,
116+
};
117+
}
118+
119+
private buildCondition(condition: AdvancedFilterCondition): OperatorNode {
120+
const field = this.buildField(condition.field);
121+
const value = this.buildLiteral(condition.value);
122+
123+
return {
124+
type: 'operator',
125+
operator: '=',
126+
operands: [field, value],
127+
};
128+
}
129+
130+
private buildJoin(join: JoinConfig): JoinNode {
131+
const onCondition: OperatorNode = {
132+
type: 'operator',
133+
operator: '=',
134+
operands: [
135+
this.buildField(join.on.local_field),
136+
this.buildField(join.on.foreign_field, join.alias || join.object),
137+
],
138+
};
139+
140+
return {
141+
type: 'join',
142+
join_type: join.type,
143+
table: join.object,
144+
alias: join.alias,
145+
on: onCondition,
146+
};
147+
}
148+
149+
private buildGroupBy(fields: string[]): GroupByNode {
150+
return {
151+
type: 'group_by',
152+
fields: fields.map(field => this.buildField(field)),
153+
};
154+
}
155+
156+
private buildOrderBy(sorts: QuerySortConfig[]): OrderByNode {
157+
return {
158+
type: 'order_by',
159+
fields: sorts.map(sort => ({
160+
field: this.buildField(sort.field),
161+
direction: sort.order,
162+
})),
163+
};
164+
}
165+
166+
private buildLimit(limit: number): LimitNode {
167+
return {
168+
type: 'limit',
169+
value: limit,
170+
};
171+
}
172+
173+
private buildOffset(offset: number): OffsetNode {
174+
return {
175+
type: 'offset',
176+
value: offset,
177+
};
178+
}
179+
180+
private buildField(field: string, table?: string): FieldNode {
181+
const parts = field.split('.');
182+
183+
if (parts.length === 2) {
184+
return {
185+
type: 'field',
186+
table: parts[0],
187+
name: parts[1],
188+
};
189+
}
190+
191+
return {
192+
type: 'field',
193+
table,
194+
name: field,
195+
};
196+
}
197+
198+
private buildLiteral(value: any): LiteralNode {
199+
let dataType: 'string' | 'number' | 'boolean' | 'date' | 'null' = 'string';
200+
201+
if (value === null || value === undefined) {
202+
dataType = 'null';
203+
} else if (typeof value === 'number') {
204+
dataType = 'number';
205+
} else if (typeof value === 'boolean') {
206+
dataType = 'boolean';
207+
} else if (value instanceof Date) {
208+
dataType = 'date';
209+
}
210+
211+
return {
212+
type: 'literal',
213+
value,
214+
data_type: dataType,
215+
};
216+
}
217+
218+
private buildAggregation(agg: AggregationConfig): AggregateNode {
219+
return {
220+
type: 'aggregate',
221+
function: agg.function,
222+
field: agg.field ? this.buildField(agg.field) : undefined,
223+
alias: agg.alias,
224+
distinct: agg.distinct,
225+
};
226+
}
227+
228+
optimize(ast: QueryAST): QueryAST {
229+
return ast;
230+
}
231+
}
232+
233+
export const defaultQueryASTBuilder = new QueryASTBuilder();
234+
235+
export function buildQueryAST(query: QuerySchema): QueryAST {
236+
return defaultQueryASTBuilder.build(query);
237+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
/**
2+
* @object-ui/core - Validation Module
3+
*
4+
* Phase 3.5: Validation engine
5+
*/
6+
7+
export * from './validation-engine';
8+
export * from './schema-validator';

0 commit comments

Comments
 (0)