Skip to content

Commit 53d250a

Browse files
committed
更新客户端 SDK 示例,修复连接 URL;增强数据查询功能,支持分页和排序;添加 CRUD 操作示例
1 parent 6b1a3c3 commit 53d250a

File tree

2 files changed

+109
-33
lines changed

2 files changed

+109
-33
lines changed

examples/todo/src/client-test.ts

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ async function main() {
55

66
// 1. Initialize Client
77
const client = new ObjectStackClient({
8-
baseUrl: 'http://localhost:3004'
8+
baseUrl: 'http://127.0.0.1:3004'
99
});
1010

1111
try {
@@ -21,12 +21,40 @@ async function main() {
2121

2222
// 4. Query Data
2323
console.log('💾 Querying Data...');
24-
const result = await client.data.find('todo_task', {});
24+
const result = await client.data.find('todo_task', {
25+
top: 10,
26+
sort: 'status'
27+
});
2528

26-
console.log(`🎉 Found ${result.data.length} tasks:`);
27-
result.data.forEach((task: any) => {
28-
console.log(` - [${task.status}] ${task.title}`);
29+
console.log(`🎉 Found ${result.count} tasks:`);
30+
result.value.forEach((task: any) => {
31+
console.log(` - [${task.is_completed ? 'x' : ' '}] ${task.subject} (Priority: ${task.priority})`);
32+
});
33+
34+
// 5. CRUD Operations
35+
console.log('\n✨ Creating new task...');
36+
const newTask = await client.data.create('todo_task', {
37+
subject: 'Test SDK Create',
38+
is_completed: false,
39+
priority: 3
2940
});
41+
console.log('✅ Created:', newTask);
42+
43+
// Update
44+
if (newTask && (newTask as any).id) {
45+
console.log('🔄 Updating task...');
46+
const updated = await client.data.update('todo_task', (newTask as any).id, {
47+
subject: 'Test SDK Create (Updated)',
48+
is_completed: true
49+
});
50+
console.log('✅ Updated:', updated);
51+
52+
// Delete
53+
console.log('🗑️ Deleting task...');
54+
const deleted = await client.data.delete('todo_task', (newTask as any).id);
55+
console.log('✅ Deleted:', deleted);
56+
}
57+
3058

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

packages/client-sdk/src/index.ts

Lines changed: 76 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
export interface ClientConfig {
22
baseUrl: string;
33
token?: string;
4+
/**
5+
* Custom fetch implementation (e.g. node-fetch or for Next.js caching)
6+
*/
7+
fetch?: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
48
}
59

610
export interface DiscoveryResult {
@@ -9,29 +13,48 @@ export interface DiscoveryResult {
913
metadata: string;
1014
data: string;
1115
auth: string;
16+
ui: string;
1217
};
1318
capabilities?: Record<string, boolean>;
1419
}
1520

21+
export interface QueryOptions {
22+
select?: string[];
23+
filters?: Record<string, any>;
24+
sort?: string | string[]; // 'name' or ['-created_at', 'name']
25+
top?: number;
26+
skip?: number;
27+
}
28+
29+
export interface PaginatedResult<T = any> {
30+
value: T[];
31+
count: number;
32+
}
33+
1634
export class ObjectStackClient {
1735
private baseUrl: string;
1836
private token?: string;
37+
private fetchImpl: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
1938
private routes?: DiscoveryResult['routes'];
2039

2140
constructor(config: ClientConfig) {
2241
this.baseUrl = config.baseUrl.replace(/\/$/, ''); // Remove trailing slash
2342
this.token = config.token;
43+
this.fetchImpl = config.fetch || globalThis.fetch.bind(globalThis);
2444
}
2545

2646
/**
2747
* Initialize the client by discovering server capabilities and routes.
2848
*/
2949
async connect() {
3050
try {
51+
// Connect to the discovery endpoint
52+
// During boot, we might not know routes, so we check convention /api/v1 first
3153
const res = await this.fetch(`${this.baseUrl}/api/v1`);
54+
3255
const data = await res.json();
3356
this.routes = data.routes;
34-
return data;
57+
return data as DiscoveryResult;
3558
} catch (e) {
3659
console.error('Failed to connect to ObjectStack Server', e);
3760
throw e;
@@ -49,9 +72,8 @@ export class ObjectStackClient {
4972
},
5073

5174
getView: async (object: string, type: 'list' | 'form' = 'list') => {
52-
// UI routes might not be in discovery map yet, assume convention or add to server
53-
// Convention from server/src/index.ts: /api/v1/ui/view/:object
54-
const res = await this.fetch(`${this.baseUrl}/api/v1/ui/view/${object}?type=${type}`);
75+
const route = this.getRoute('ui');
76+
const res = await this.fetch(`${this.baseUrl}${route}/view/${object}?type=${type}`);
5577
return res.json();
5678
}
5779
};
@@ -60,20 +82,37 @@ export class ObjectStackClient {
6082
* Data Operations
6183
*/
6284
data = {
63-
find: async (object: string, query: any = {}) => {
85+
find: async <T = any>(object: string, options: QueryOptions = {}): Promise<PaginatedResult<T>> => {
6486
const route = this.getRoute('data');
65-
const queryString = new URLSearchParams(query).toString();
66-
const res = await this.fetch(`${this.baseUrl}${route}/${object}?${queryString}`);
87+
const queryParams = new URLSearchParams();
88+
89+
if (options.top) queryParams.set('top', options.top.toString());
90+
if (options.skip) queryParams.set('skip', options.skip.toString());
91+
if (options.sort) {
92+
const sortVal = Array.isArray(options.sort) ? options.sort.join(',') : options.sort;
93+
queryParams.set('sort', sortVal);
94+
}
95+
96+
// Flatten simple KV pairs if filters exists
97+
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+
});
103+
}
104+
105+
const res = await this.fetch(`${this.baseUrl}${route}/${object}?${queryParams.toString()}`);
67106
return res.json();
68107
},
69108

70-
get: async (object: string, id: string) => {
109+
get: async <T = any>(object: string, id: string): Promise<T> => {
71110
const route = this.getRoute('data');
72111
const res = await this.fetch(`${this.baseUrl}${route}/${object}/${id}`);
73112
return res.json();
74113
},
75114

76-
create: async (object: string, data: any) => {
115+
create: async <T = any>(object: string, data: Partial<T>): Promise<T> => {
77116
const route = this.getRoute('data');
78117
const res = await this.fetch(`${this.baseUrl}${route}/${object}`, {
79118
method: 'POST',
@@ -82,7 +121,7 @@ export class ObjectStackClient {
82121
return res.json();
83122
},
84123

85-
update: async (object: string, id: string, data: any) => {
124+
update: async <T = any>(object: string, id: string, data: Partial<T>): Promise<T> => {
86125
const route = this.getRoute('data');
87126
const res = await this.fetch(`${this.baseUrl}${route}/${object}/${id}`, {
88127
method: 'PATCH',
@@ -91,7 +130,7 @@ export class ObjectStackClient {
91130
return res.json();
92131
},
93132

94-
delete: async (object: string, id: string) => {
133+
delete: async (object: string, id: string): Promise<{ success: boolean }> => {
95134
const route = this.getRoute('data');
96135
const res = await this.fetch(`${this.baseUrl}${route}/${object}/${id}`, {
97136
method: 'DELETE'
@@ -100,32 +139,41 @@ export class ObjectStackClient {
100139
}
101140
};
102141

103-
private getRoute(key: keyof DiscoveryResult['routes']): string {
104-
if (!this.routes) {
105-
throw new Error('Client not connected. Call client.connect() first.');
106-
}
107-
return this.routes[key];
108-
}
142+
/**
143+
* Private Helpers
144+
*/
109145

110-
private async fetch(url: string, options: RequestInit = {}) {
146+
private async fetch(url: string, options: RequestInit = {}): Promise<Response> {
111147
const headers: Record<string, string> = {
112-
'Content-Type': 'application/json',
113-
...(options.headers as any || {})
148+
'Content-Type': 'application/json',
149+
...(options.headers as Record<string, string> || {}),
114150
};
115151

116152
if (this.token) {
117153
headers['Authorization'] = `Bearer ${this.token}`;
118154
}
119155

120-
const res = await fetch(url, {
121-
...options,
122-
headers
123-
});
124-
125-
if (res.status >= 400) {
126-
throw new Error(`API Error ${res.status}: ${res.statusText}`);
156+
const res = await this.fetchImpl(url, { ...options, headers });
157+
158+
if (!res.ok) {
159+
let errorBody;
160+
try {
161+
errorBody = await res.json();
162+
} catch {
163+
errorBody = { message: res.statusText };
164+
}
165+
throw new Error(`[ObjectStack] Request failed: ${res.status} ${JSON.stringify(errorBody)}`);
127166
}
128-
167+
129168
return res;
130169
}
170+
171+
private getRoute(key: keyof DiscoveryResult['routes']): string {
172+
if (!this.routes) {
173+
// Fallback for strictness, but we allow bootstrapping
174+
console.warn(`[ObjectStackClient] Accessing ${key} route before connect(). Using default /api/v1/${key}`);
175+
return `/api/v1/${key}`;
176+
}
177+
return this.routes[key] || `/api/v1/${key}`;
178+
}
131179
}

0 commit comments

Comments
 (0)