Skip to content

Commit 26afa51

Browse files
committed
添加高级查询功能,支持使用 AST 进行复杂查询;更新查询选项以支持选择、过滤和排序;增加批量创建和删除操作
1 parent 53d250a commit 26afa51

File tree

2 files changed

+106
-11
lines changed

2 files changed

+106
-11
lines changed

examples/todo/src/client-test.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,19 @@ async function main() {
5555
console.log('✅ Deleted:', deleted);
5656
}
5757

58+
// 6. Advanced Query (AST)
59+
console.log('\n🧠 Testing Advanced Query (Select & AST)...');
60+
const advancedResult = await client.data.find('todo_task', {
61+
select: ['subject', 'priority'],
62+
filters: ['priority', '>=', 2],
63+
sort: ['-priority']
64+
});
65+
console.log(`🎉 Found ${advancedResult.count} high priority tasks:`);
66+
advancedResult.value.forEach((task: any) => {
67+
console.log(` - ${task.subject} (P${task.priority}) [Has keys: ${Object.keys(task).join(', ')}]`);
68+
});
69+
70+
5871

5972
} catch (error) {
6073
console.error('❌ Error during test:', error);

packages/client-sdk/src/index.ts

Lines changed: 93 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { QueryAST, FilterNode, SortNode, AggregationNode, WindowFunctionNode } from '@objectstack/spec';
2+
13
export interface ClientConfig {
24
baseUrl: string;
35
token?: string;
@@ -19,11 +21,14 @@ export interface DiscoveryResult {
1921
}
2022

2123
export interface QueryOptions {
22-
select?: string[];
23-
filters?: Record<string, any>;
24-
sort?: string | string[]; // 'name' or ['-created_at', 'name']
24+
select?: string[]; // Simplified Selection
25+
filters?: Record<string, any> | FilterNode; // Map or AST
26+
sort?: string | string[] | SortNode[]; // 'name' or ['-created_at'] or AST
2527
top?: number;
2628
skip?: number;
29+
// Advanced features
30+
aggregations?: AggregationNode[];
31+
groupBy?: string[];
2732
}
2833

2934
export interface PaginatedResult<T = any> {
@@ -82,24 +87,66 @@ export class ObjectStackClient {
8287
* Data Operations
8388
*/
8489
data = {
90+
/**
91+
* Advanced Query using ObjectStack Query Protocol
92+
* Supports both simplified options and full AST
93+
*/
94+
query: async <T = any>(object: string, query: Partial<QueryAST>): Promise<PaginatedResult<T>> => {
95+
const route = this.getRoute('data');
96+
// POST for complex query to avoid URL length limits and allow clean JSON AST
97+
// Convention: POST /api/v1/data/:object/query
98+
const res = await this.fetch(`${this.baseUrl}${route}/${object}/query`, {
99+
method: 'POST',
100+
body: JSON.stringify(query)
101+
});
102+
return res.json();
103+
},
104+
85105
find: async <T = any>(object: string, options: QueryOptions = {}): Promise<PaginatedResult<T>> => {
86106
const route = this.getRoute('data');
87107
const queryParams = new URLSearchParams();
88108

109+
// 1. Handle Pagination
89110
if (options.top) queryParams.set('top', options.top.toString());
90111
if (options.skip) queryParams.set('skip', options.skip.toString());
112+
113+
// 2. Handle Sort
91114
if (options.sort) {
92-
const sortVal = Array.isArray(options.sort) ? options.sort.join(',') : options.sort;
93-
queryParams.set('sort', sortVal);
115+
// Check if it's AST
116+
if (Array.isArray(options.sort) && typeof options.sort[0] === 'object') {
117+
queryParams.set('sort', JSON.stringify(options.sort));
118+
} else {
119+
const sortVal = Array.isArray(options.sort) ? options.sort.join(',') : options.sort;
120+
queryParams.set('sort', sortVal as string);
121+
}
94122
}
95123

96-
// Flatten simple KV pairs if filters exists
124+
// 3. Handle Select
125+
if (options.select) {
126+
queryParams.set('select', options.select.join(','));
127+
}
128+
129+
// 4. Handle Filters (Simple vs AST)
97130
if (options.filters) {
98-
Object.entries(options.filters).forEach(([k, v]) => {
99-
if (v !== undefined && v !== null) {
100-
queryParams.append(k, String(v));
101-
}
102-
});
131+
// If looks like AST (not plain object map)
132+
// TODO: robust check. safely assuming map for simplified find, and recommending .query() for AST
133+
if (this.isFilterAST(options.filters)) {
134+
queryParams.set('filters', JSON.stringify(options.filters));
135+
} else {
136+
Object.entries(options.filters).forEach(([k, v]) => {
137+
if (v !== undefined && v !== null) {
138+
queryParams.append(k, String(v));
139+
}
140+
});
141+
}
142+
}
143+
144+
// 5. Handle Aggregations & GroupBy (Pass through as JSON if present)
145+
if (options.aggregations) {
146+
queryParams.set('aggregations', JSON.stringify(options.aggregations));
147+
}
148+
if (options.groupBy) {
149+
queryParams.set('groupBy', options.groupBy.join(','));
103150
}
104151

105152
const res = await this.fetch(`${this.baseUrl}${route}/${object}?${queryParams.toString()}`);
@@ -121,6 +168,15 @@ export class ObjectStackClient {
121168
return res.json();
122169
},
123170

171+
createMany: async <T = any>(object: string, data: Partial<T>[]): Promise<T[]> => {
172+
const route = this.getRoute('data');
173+
const res = await this.fetch(`${this.baseUrl}${route}/${object}/batch`, {
174+
method: 'POST',
175+
body: JSON.stringify(data)
176+
});
177+
return res.json();
178+
},
179+
124180
update: async <T = any>(object: string, id: string, data: Partial<T>): Promise<T> => {
125181
const route = this.getRoute('data');
126182
const res = await this.fetch(`${this.baseUrl}${route}/${object}/${id}`, {
@@ -130,19 +186,45 @@ export class ObjectStackClient {
130186
return res.json();
131187
},
132188

189+
updateMany: async <T = any>(object: string, ids: string[], data: Partial<T>): Promise<number> => {
190+
// Warning: This implies updating all IDs with the SAME data
191+
const route = this.getRoute('data');
192+
const res = await this.fetch(`${this.baseUrl}${route}/${object}/batch`, {
193+
method: 'PATCH',
194+
body: JSON.stringify({ ids, data })
195+
});
196+
return res.json(); // Returns count
197+
},
198+
133199
delete: async (object: string, id: string): Promise<{ success: boolean }> => {
134200
const route = this.getRoute('data');
135201
const res = await this.fetch(`${this.baseUrl}${route}/${object}/${id}`, {
136202
method: 'DELETE'
137203
});
138204
return res.json();
205+
},
206+
207+
deleteMany: async(object: string, ids: string[]): Promise<{ count: number }> => {
208+
const route = this.getRoute('data');
209+
const res = await this.fetch(`${this.baseUrl}${route}/${object}/batch`, {
210+
method: 'DELETE',
211+
body: JSON.stringify({ ids })
212+
});
213+
return res.json();
139214
}
140215
};
141216

142217
/**
143218
* Private Helpers
144219
*/
145220

221+
private isFilterAST(filter: any): boolean {
222+
// Basic check: if array, it's [field, op, val] or [logic, node, node]
223+
// If object but not basic KV map... harder to tell without schema
224+
// For now, assume if it passes Array.isArray it's an AST root
225+
return Array.isArray(filter);
226+
}
227+
146228
private async fetch(url: string, options: RequestInit = {}): Promise<Response> {
147229
const headers: Record<string, string> = {
148230
'Content-Type': 'application/json',

0 commit comments

Comments
 (0)