@vafast/api-client 0.1.2 → 0.1.4

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/src/index.ts DELETED
@@ -1,42 +0,0 @@
1
- /**
2
- * Vafast API Client
3
- *
4
- * 类型安全的 Eden 风格 API 客户端
5
- *
6
- * @example
7
- * ```typescript
8
- * import { eden, InferEden } from '@vafast/api-client'
9
- * import { defineRoutes, route, createHandler, Type } from 'vafast'
10
- *
11
- * // 定义路由(无需 as const)
12
- * const routes = defineRoutes([
13
- * route('GET', '/users', createHandler(...)),
14
- * route('POST', '/users', createHandler(...))
15
- * ])
16
- *
17
- * // 自动推断类型
18
- * type Api = InferEden<typeof routes>
19
- * const api = eden<Api>('http://localhost:3000')
20
- *
21
- * // 类型安全的调用
22
- * const { data } = await api.users.get({ page: 1 })
23
- * ```
24
- */
25
-
26
- // Eden 客户端(核心)
27
- export {
28
- eden,
29
- type EdenConfig,
30
- type EdenClient,
31
- type InferEden,
32
- type SSEEvent,
33
- type SSESubscribeOptions,
34
- type SSESubscription,
35
- } from './core/eden'
36
-
37
- // 类型定义
38
- export type {
39
- HTTPMethod,
40
- RequestConfig,
41
- ApiResponse,
42
- } from './types'
@@ -1,34 +0,0 @@
1
- /**
2
- * 类型定义
3
- */
4
-
5
- /** HTTP 方法 */
6
- export type HTTPMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS'
7
-
8
- /**
9
- * 请求配置
10
- */
11
- export interface RequestConfig {
12
- /** 请求头 */
13
- headers?: Record<string, string>
14
- /** 超时时间(毫秒) */
15
- timeout?: number
16
- /** 取消信号 */
17
- signal?: AbortSignal
18
- }
19
-
20
- /**
21
- * API 响应
22
- */
23
- export interface ApiResponse<T = unknown> {
24
- /** 响应数据 */
25
- data: T | null
26
- /** 错误信息 */
27
- error: Error | null
28
- /** HTTP 状态码 */
29
- status: number
30
- /** 响应头 */
31
- headers: Headers
32
- /** 原始响应对象 */
33
- response: Response
34
- }
package/test/eden.test.ts DELETED
@@ -1,425 +0,0 @@
1
- import { describe, expect, it, beforeEach, vi, afterEach } from 'vitest'
2
- import { eden } from '../src'
3
-
4
- // Mock fetch
5
- const mockFetch = vi.fn()
6
- globalThis.fetch = mockFetch
7
-
8
- describe('Eden Client', () => {
9
- beforeEach(() => {
10
- mockFetch.mockReset()
11
- mockFetch.mockResolvedValue(
12
- new Response(JSON.stringify({ id: '1', name: 'John' }), {
13
- status: 200,
14
- headers: { 'Content-Type': 'application/json' }
15
- })
16
- )
17
- })
18
-
19
- // ============= 类型定义 =============
20
-
21
- interface TestContract {
22
- users: {
23
- get: { query: { page?: number }; return: { id: string }[] }
24
- post: { body: { name: string }; return: { id: string } }
25
- ':id': {
26
- get: { return: { id: string; name: string } }
27
- put: { body: { name: string }; return: { id: string; name: string } }
28
- delete: { return: { success: boolean } }
29
- }
30
- }
31
- chat: {
32
- stream: {
33
- get: { query: { prompt: string }; return: unknown; sse: true }
34
- }
35
- }
36
- }
37
-
38
- // ============= 基础请求测试 =============
39
-
40
- describe('基础请求', () => {
41
- it('应该发送 GET 请求', async () => {
42
- const api = eden<TestContract>('http://localhost:3000')
43
-
44
- const result = await api.users.get({ page: 1 })
45
-
46
- expect(mockFetch).toHaveBeenCalled()
47
- const req = mockFetch.mock.calls[0][0] as Request
48
- expect(req.url).toContain('/users')
49
- expect(req.url).toContain('page=1')
50
- expect(req.method).toBe('GET')
51
- expect(result.status).toBe(200)
52
- })
53
-
54
- it('应该发送 POST 请求', async () => {
55
- const api = eden<TestContract>('http://localhost:3000')
56
-
57
- await api.users.post({ name: 'John' })
58
-
59
- const req = mockFetch.mock.calls[0][0] as Request
60
- expect(req.method).toBe('POST')
61
- expect(req.url).toContain('/users')
62
- })
63
-
64
- it('应该正确设置请求头', async () => {
65
- const api = eden<TestContract>('http://localhost:3000', {
66
- headers: { 'Authorization': 'Bearer token123' }
67
- })
68
-
69
- await api.users.get()
70
-
71
- const req = mockFetch.mock.calls[0][0] as Request
72
- expect(req.headers.get('Authorization')).toBe('Bearer token123')
73
- expect(req.headers.get('Content-Type')).toBe('application/json')
74
- })
75
- })
76
-
77
- // ============= 参数化路由测试 =============
78
-
79
- describe('参数化路由', () => {
80
- it('应该通过函数调用处理路径参数', async () => {
81
- const api = eden<TestContract>('http://localhost:3000')
82
-
83
- await api.users({ id: '123' }).get()
84
-
85
- const req = mockFetch.mock.calls[0][0] as Request
86
- expect(req.url).toBe('http://localhost:3000/users/123')
87
- expect(req.method).toBe('GET')
88
- })
89
-
90
- it('应该处理 PUT 请求和路径参数', async () => {
91
- const api = eden<TestContract>('http://localhost:3000')
92
-
93
- await api.users({ id: '123' }).put({ name: 'Jane' })
94
-
95
- const req = mockFetch.mock.calls[0][0] as Request
96
- expect(req.url).toBe('http://localhost:3000/users/123')
97
- expect(req.method).toBe('PUT')
98
- })
99
-
100
- it('应该处理 DELETE 请求和路径参数', async () => {
101
- const api = eden<TestContract>('http://localhost:3000')
102
-
103
- await api.users({ id: '456' }).delete()
104
-
105
- const req = mockFetch.mock.calls[0][0] as Request
106
- expect(req.url).toBe('http://localhost:3000/users/456')
107
- expect(req.method).toBe('DELETE')
108
- })
109
-
110
- it('应该对路径参数进行 URL 编码', async () => {
111
- const api = eden<TestContract>('http://localhost:3000')
112
-
113
- await api.users({ id: 'user/123' }).get()
114
-
115
- const req = mockFetch.mock.calls[0][0] as Request
116
- expect(req.url).toBe('http://localhost:3000/users/user%2F123')
117
- })
118
- })
119
-
120
- // ============= 嵌套路由测试 =============
121
-
122
- describe('嵌套路由', () => {
123
- interface NestedContract {
124
- users: {
125
- ':id': {
126
- posts: {
127
- get: { return: { id: string }[] }
128
- ':id': {
129
- get: { return: { id: string; title: string } }
130
- }
131
- }
132
- }
133
- }
134
- }
135
-
136
- it('应该处理嵌套路径', async () => {
137
- const api = eden<NestedContract>('http://localhost:3000')
138
-
139
- await api.users({ id: '123' }).posts.get()
140
-
141
- const req = mockFetch.mock.calls[0][0] as Request
142
- expect(req.url).toBe('http://localhost:3000/users/123/posts')
143
- })
144
-
145
- it('应该处理多层嵌套路径参数', async () => {
146
- const api = eden<NestedContract>('http://localhost:3000')
147
-
148
- // 动态参数统一使用 :id
149
- await api.users({ id: '123' }).posts({ id: '456' }).get()
150
-
151
- const req = mockFetch.mock.calls[0][0] as Request
152
- expect(req.url).toBe('http://localhost:3000/users/123/posts/456')
153
- })
154
- })
155
-
156
- // ============= 请求取消测试 =============
157
-
158
- describe('请求取消', () => {
159
- it('应该支持通过 AbortController 取消请求', async () => {
160
- const controller = new AbortController()
161
-
162
- mockFetch.mockImplementation(() => {
163
- return new Promise((_, reject) => {
164
- controller.signal.addEventListener('abort', () => {
165
- const error = new Error('This operation was aborted')
166
- error.name = 'AbortError'
167
- reject(error)
168
- })
169
- })
170
- })
171
-
172
- const api = eden<TestContract>('http://localhost:3000')
173
-
174
- const promise = api.users.get(undefined, { signal: controller.signal })
175
- controller.abort()
176
-
177
- const result = await promise
178
- expect(result.error).toBeTruthy()
179
- expect(result.status).toBe(0)
180
- })
181
-
182
- it('应该在超时后自动取消请求', async () => {
183
- mockFetch.mockImplementation((input: Request | string) => {
184
- return new Promise((resolve, reject) => {
185
- const timeoutId = setTimeout(() => {
186
- resolve(new Response(JSON.stringify({}), { status: 200 }))
187
- }, 5000)
188
-
189
- // 从 Request 对象获取 signal
190
- const signal = input instanceof Request ? input.signal : undefined
191
- signal?.addEventListener('abort', () => {
192
- clearTimeout(timeoutId)
193
- const error = new Error('This operation was aborted')
194
- error.name = 'AbortError'
195
- reject(error)
196
- })
197
- })
198
- })
199
-
200
- const api = eden<TestContract>('http://localhost:3000', { timeout: 100 })
201
-
202
- const result = await api.users.get()
203
-
204
- expect(result.error).toBeTruthy()
205
- expect(result.status).toBe(0)
206
- })
207
- })
208
-
209
- // ============= 拦截器测试 =============
210
-
211
- describe('拦截器', () => {
212
- it('应该执行 onRequest 拦截器', async () => {
213
- const onRequest = vi.fn((req: Request) => {
214
- return new Request(req.url, {
215
- ...req,
216
- headers: { ...Object.fromEntries(req.headers), 'X-Custom': 'value' }
217
- })
218
- })
219
-
220
- const api = eden<TestContract>('http://localhost:3000', { onRequest })
221
-
222
- await api.users.get()
223
-
224
- expect(onRequest).toHaveBeenCalled()
225
- })
226
-
227
- it('应该执行 onResponse 拦截器', async () => {
228
- const onResponse = vi.fn((response) => response)
229
-
230
- const api = eden<TestContract>('http://localhost:3000', { onResponse })
231
-
232
- await api.users.get()
233
-
234
- expect(onResponse).toHaveBeenCalled()
235
- })
236
-
237
- it('应该在错误时执行 onError 回调', async () => {
238
- mockFetch.mockResolvedValue(
239
- new Response(JSON.stringify({ error: 'Not found' }), {
240
- status: 404,
241
- headers: { 'Content-Type': 'application/json' }
242
- })
243
- )
244
-
245
- const onError = vi.fn()
246
- const api = eden<TestContract>('http://localhost:3000', { onError })
247
-
248
- await api.users.get()
249
-
250
- expect(onError).toHaveBeenCalled()
251
- })
252
- })
253
-
254
- // ============= 响应处理测试 =============
255
-
256
- describe('响应处理', () => {
257
- it('应该正确解析 JSON 响应', async () => {
258
- mockFetch.mockResolvedValue(
259
- new Response(JSON.stringify({ users: [{ id: '1' }], total: 10 }), {
260
- status: 200,
261
- headers: { 'Content-Type': 'application/json' }
262
- })
263
- )
264
-
265
- const api = eden<TestContract>('http://localhost:3000')
266
- const result = await api.users.get()
267
-
268
- expect(result.data).toEqual({ users: [{ id: '1' }], total: 10 })
269
- expect(result.status).toBe(200)
270
- expect(result.error).toBeNull()
271
- })
272
-
273
- it('应该正确解析文本响应', async () => {
274
- mockFetch.mockResolvedValue(
275
- new Response('Hello World', {
276
- status: 200,
277
- headers: { 'Content-Type': 'text/plain' }
278
- })
279
- )
280
-
281
- const api = eden<TestContract>('http://localhost:3000')
282
- const result = await api.users.get()
283
-
284
- expect(result.data).toBe('Hello World')
285
- })
286
-
287
- it('应该处理 HTTP 错误状态', async () => {
288
- mockFetch.mockResolvedValue(
289
- new Response(JSON.stringify({ message: 'Unauthorized' }), {
290
- status: 401,
291
- headers: { 'Content-Type': 'application/json' }
292
- })
293
- )
294
-
295
- const api = eden<TestContract>('http://localhost:3000')
296
- const result = await api.users.get()
297
-
298
- expect(result.status).toBe(401)
299
- expect(result.error).toBeTruthy()
300
- expect(result.error?.message).toContain('401')
301
- })
302
-
303
- it('应该处理网络错误', async () => {
304
- mockFetch.mockRejectedValue(new Error('Network error'))
305
-
306
- const api = eden<TestContract>('http://localhost:3000')
307
- const result = await api.users.get()
308
-
309
- expect(result.error?.message).toBe('Network error')
310
- expect(result.status).toBe(0)
311
- expect(result.data).toBeNull()
312
- })
313
- })
314
-
315
- // ============= Query 参数测试 =============
316
-
317
- describe('Query 参数', () => {
318
- it('应该正确构建查询字符串', async () => {
319
- const api = eden<TestContract>('http://localhost:3000')
320
-
321
- await api.users.get({ page: 2 })
322
-
323
- const req = mockFetch.mock.calls[0][0] as Request
324
- const url = new URL(req.url)
325
- expect(url.searchParams.get('page')).toBe('2')
326
- })
327
-
328
- it('应该忽略 undefined 和 null 值', async () => {
329
- // 直接使用 TestContract,测试 users.get
330
- const api = eden<TestContract>('http://localhost:3000')
331
-
332
- await api.users.get({ page: undefined })
333
-
334
- const req = mockFetch.mock.calls[0][0] as Request
335
- const url = new URL(req.url)
336
- // undefined 值不应该出现在查询字符串中
337
- expect(url.searchParams.has('page')).toBe(false)
338
- })
339
- })
340
- })
341
-
342
- // ============= SSE 测试 =============
343
-
344
- describe('SSE 订阅', () => {
345
- beforeEach(() => {
346
- mockFetch.mockReset()
347
- })
348
-
349
- it('应该调用 subscribe 方法', async () => {
350
- // 创建模拟 SSE 流
351
- const encoder = new TextEncoder()
352
- const stream = new ReadableStream({
353
- start(controller) {
354
- controller.enqueue(encoder.encode('data: {"message":"hello"}\n\n'))
355
- controller.close()
356
- }
357
- })
358
-
359
- mockFetch.mockResolvedValue(
360
- new Response(stream, {
361
- status: 200,
362
- headers: { 'Content-Type': 'text/event-stream' }
363
- })
364
- )
365
-
366
- interface SSEContract {
367
- events: {
368
- get: { query: { channel: string }; return: unknown; sse: { readonly __brand: 'SSE' } }
369
- }
370
- }
371
-
372
- const api = eden<SSEContract>('http://localhost:3000')
373
- const onMessage = vi.fn()
374
- const onClose = vi.fn()
375
-
376
- await new Promise<void>((resolve) => {
377
- api.events.subscribe(
378
- { channel: 'test' },
379
- {
380
- onMessage,
381
- onClose: () => {
382
- onClose()
383
- resolve()
384
- }
385
- }
386
- )
387
- })
388
-
389
- expect(mockFetch).toHaveBeenCalled()
390
- expect(onMessage).toHaveBeenCalledWith({ message: 'hello' })
391
- expect(onClose).toHaveBeenCalled()
392
- })
393
-
394
- it('应该支持取消订阅', async () => {
395
- const stream = new ReadableStream({
396
- start(controller) {
397
- // 永不关闭的流
398
- }
399
- })
400
-
401
- mockFetch.mockResolvedValue(
402
- new Response(stream, {
403
- status: 200,
404
- headers: { 'Content-Type': 'text/event-stream' }
405
- })
406
- )
407
-
408
- interface SSEContract {
409
- events: {
410
- get: { return: unknown; sse: { readonly __brand: 'SSE' } }
411
- }
412
- }
413
-
414
- const api = eden<SSEContract>('http://localhost:3000')
415
-
416
- const sub = api.events.subscribe({
417
- onMessage: () => {}
418
- })
419
-
420
- expect(sub.connected).toBe(false) // 还在连接中
421
-
422
- sub.unsubscribe()
423
- expect(sub.connected).toBe(false)
424
- })
425
- })
package/tsconfig.dts.json DELETED
@@ -1,20 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "preserveSymlinks": true,
4
- "target": "ES2021",
5
- "lib": ["ESNext"],
6
- "module": "ES2022",
7
- "rootDir": "./src",
8
- "moduleResolution": "node",
9
- "resolveJsonModule": true,
10
- "declaration": true,
11
- "emitDeclarationOnly": true,
12
- "outDir": "./dist",
13
- "allowSyntheticDefaultImports": true,
14
- "esModuleInterop": true,
15
- "forceConsistentCasingInFileNames": true,
16
- "strict": true,
17
- "skipLibCheck": true
18
- },
19
- "exclude": ["node_modules", "test", "example", "dist", "build.ts"]
20
- }
package/tsconfig.json DELETED
@@ -1,20 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "target": "ES2022",
4
- "module": "ESNext",
5
- "moduleResolution": "bundler",
6
- "allowSyntheticDefaultImports": true,
7
- "esModuleInterop": true,
8
- "forceConsistentCasingInFileNames": true,
9
- "strict": true,
10
- "skipLibCheck": true,
11
- "declaration": true,
12
- "declarationMap": true,
13
- "sourceMap": true,
14
- "outDir": "./dist",
15
- "rootDir": "./src",
16
- "types": []
17
- },
18
- "include": ["src/**/*"],
19
- "exclude": ["node_modules", "dist", "test"]
20
- }
package/tsup.config.ts DELETED
@@ -1,23 +0,0 @@
1
- import { defineConfig } from 'tsup'
2
-
3
- export default defineConfig([
4
- {
5
- entry: ['src/index.ts'],
6
- outDir: 'dist',
7
- format: ['esm'],
8
- target: 'node18',
9
- dts: true,
10
- clean: true,
11
- sourcemap: true,
12
- outExtension: () => ({ js: '.mjs' }),
13
- },
14
- {
15
- entry: ['src/index.ts'],
16
- outDir: 'dist/cjs',
17
- format: ['cjs'],
18
- target: 'node18',
19
- dts: false,
20
- clean: false,
21
- },
22
- ])
23
-
package/vitest.config.ts DELETED
@@ -1,8 +0,0 @@
1
- import { defineConfig } from 'vitest/config'
2
-
3
- export default defineConfig({
4
- test: {
5
- globals: false,
6
- },
7
- })
8
-