@vafast/api-client 0.1.1 → 0.1.2

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.
@@ -1,304 +0,0 @@
1
- import { describe, expect, it, beforeEach, mock } from 'bun:test'
2
- import {
3
- createTypedClient,
4
- createRouteBasedClient,
5
- createSimpleClient,
6
- type TypedApiClient
7
- } from '../src'
8
-
9
- // Mock VafastApiClient
10
- const MockVafastApiClient = mock(() => ({
11
- get: mock(() => Promise.resolve({ data: 'test', error: null, status: 200, headers: new Headers(), response: new Response() })),
12
- post: mock(() => Promise.resolve({ data: 'created', error: null, status: 201, headers: new Headers(), response: new Response() })),
13
- put: mock(() => Promise.resolve({ data: 'updated', error: null, status: 200, headers: new Headers(), response: new Response() })),
14
- delete: mock(() => Promise.resolve({ data: 'deleted', error: null, status: 200, headers: new Headers(), response: new Response() })),
15
- patch: mock(() => Promise.resolve({ data: 'patched', error: null, status: 200, headers: new Headers(), response: new Response() })),
16
- head: mock(() => Promise.resolve({ data: null, error: null, status: 200, headers: new Headers(), response: new Response() })),
17
- options: mock(() => Promise.resolve({ data: null, error: null, status: 200, headers: new Headers(), response: new Response() }))
18
- }))
19
-
20
- // Mock server type
21
- interface MockServer {
22
- routes: {
23
- '/users': {
24
- GET: { query: { page?: number; limit?: number } }
25
- POST: { body: { name: string; email: string } }
26
- }
27
- '/users/:id': {
28
- GET: { params: { id: string } }
29
- PUT: { params: { id: string }; body: Partial<{ name: string; email: string }> }
30
- DELETE: { params: { id: string } }
31
- }
32
- '/posts': {
33
- GET: { query: { author?: string; category?: string } }
34
- POST: { body: { title: string; content: string; authorId: string } }
35
- }
36
- '/posts/:id': {
37
- GET: { params: { id: string } }
38
- PUT: { params: { id: string }; body: Partial<{ title: string; content: string }> }
39
- DELETE: { params: { id: string } }
40
- }
41
- }
42
- }
43
-
44
- describe('Typed Client', () => {
45
- let mockServer: MockServer
46
-
47
- beforeEach(() => {
48
- mockServer = {
49
- routes: {
50
- '/users': {
51
- GET: { query: { page: 1, limit: 10 } },
52
- POST: { body: { name: 'John', email: 'john@example.com' } }
53
- },
54
- '/users/:id': {
55
- GET: { params: { id: '123' } },
56
- PUT: { params: { id: '123' }, body: { name: 'John Updated' } },
57
- DELETE: { params: { id: '123' } }
58
- },
59
- '/posts': {
60
- GET: { query: { author: 'user123', category: 'tech' } },
61
- POST: { body: { title: 'Test Post', content: 'Content', authorId: 'user123' } }
62
- },
63
- '/posts/:id': {
64
- GET: { params: { id: '456' } },
65
- PUT: { params: { id: '456' }, body: { title: 'Updated Post' } },
66
- DELETE: { params: { id: '456' } }
67
- }
68
- }
69
- }
70
- })
71
-
72
- describe('createTypedClient', () => {
73
- it('should create typed client from server', () => {
74
- const typedClient = createTypedClient<MockServer>(mockServer as any, {
75
- baseURL: 'https://api.example.com'
76
- })
77
- expect(typedClient).toBeDefined()
78
- })
79
-
80
- it('should support HTTP method calls', async () => {
81
- const typedClient = createTypedClient<MockServer>(mockServer as any)
82
-
83
- // Test GET method
84
- const getResponse = await typedClient.get('/users', { page: 1, limit: 10 })
85
- expect(getResponse).toBeDefined()
86
- // Note: The actual response will depend on the implementation
87
- expect(getResponse).toHaveProperty('data')
88
-
89
- // Test POST method
90
- const postResponse = await typedClient.post('/users', { name: 'Jane', email: 'jane@example.com' })
91
- expect(postResponse).toBeDefined()
92
- expect(postResponse).toHaveProperty('data')
93
-
94
- // Test PUT method
95
- const putResponse = await typedClient.put('/users/123', { name: 'Jane Updated' })
96
- expect(putResponse).toBeDefined()
97
- expect(putResponse).toHaveProperty('data')
98
-
99
- // Test DELETE method
100
- const deleteResponse = await typedClient.delete('/users/123')
101
- expect(deleteResponse).toBeDefined()
102
- expect(deleteResponse).toHaveProperty('data')
103
-
104
- // Test PATCH method
105
- const patchResponse = await typedClient.patch('/users/123', { name: 'Jane Patched' })
106
- expect(patchResponse).toBeDefined()
107
- expect(patchResponse).toHaveProperty('data')
108
-
109
- // Test HEAD method
110
- const headResponse = await typedClient.head('/users')
111
- expect(headResponse).toBeDefined()
112
- expect(headResponse).toHaveProperty('data')
113
-
114
- // Test OPTIONS method
115
- const optionsResponse = await typedClient.options('/users')
116
- expect(optionsResponse).toBeDefined()
117
- expect(optionsResponse).toHaveProperty('data')
118
- })
119
-
120
- it('should support path-based calls', async () => {
121
- const typedClient = createTypedClient<MockServer>(mockServer as any)
122
-
123
- // Test path segments
124
- const usersClient = (typedClient as any).users
125
- expect(usersClient).toBeDefined()
126
-
127
- // Test nested path segments
128
- const postsClient = (typedClient as any).posts
129
- expect(postsClient).toBeDefined()
130
- })
131
-
132
- it('should handle dynamic path parameters', async () => {
133
- const typedClient = createTypedClient<MockServer>(mockServer as any)
134
-
135
- // Test dynamic path with parameters
136
- const userClient = (typedClient as any).users
137
- if (typeof userClient === 'function') {
138
- const response = await userClient({ id: '123' })
139
- expect(response).toBeDefined()
140
- }
141
- })
142
- })
143
-
144
- describe('createRouteBasedClient', () => {
145
- it('should create route-based client', () => {
146
- const routeClient = createRouteBasedClient<MockServer>(mockServer as any, {
147
- baseURL: 'https://api.example.com'
148
- })
149
- expect(routeClient).toBeDefined()
150
- })
151
-
152
- it('should support dynamic path handling', async () => {
153
- const routeClient = createRouteBasedClient<MockServer>(mockServer as any)
154
-
155
- // Test dynamic path creation
156
- const dynamicPath = (routeClient as any).users
157
- expect(dynamicPath).toBeDefined()
158
-
159
- if (typeof dynamicPath === 'function') {
160
- const response = await dynamicPath({ id: '123' })
161
- expect(response).toBeDefined()
162
- }
163
- })
164
- })
165
-
166
- describe('createSimpleClient', () => {
167
- it('should create simple client', () => {
168
- const simpleClient = createSimpleClient<MockServer>(mockServer as any, {
169
- baseURL: 'https://api.example.com'
170
- })
171
- expect(simpleClient).toBeDefined()
172
- })
173
-
174
- it('should have all HTTP methods', () => {
175
- const simpleClient = createSimpleClient<MockServer>(mockServer as any)
176
-
177
- expect(typeof simpleClient.get).toBe('function')
178
- expect(typeof simpleClient.post).toBe('function')
179
- expect(typeof simpleClient.put).toBe('function')
180
- expect(typeof simpleClient.delete).toBe('function')
181
- expect(typeof simpleClient.patch).toBe('function')
182
- expect(typeof simpleClient.head).toBe('function')
183
- expect(typeof simpleClient.options).toBe('function')
184
- })
185
-
186
- it('should make HTTP requests correctly', async () => {
187
- const simpleClient = createSimpleClient<MockServer>(mockServer as any)
188
-
189
- // Test GET request
190
- const getResponse = await simpleClient.get('/users', { page: 1, limit: 10 })
191
- expect(getResponse).toBeDefined()
192
- expect(getResponse).toHaveProperty('data')
193
-
194
- // Test POST request
195
- const postResponse = await simpleClient.post('/users', { name: 'John', email: 'john@example.com' })
196
- expect(postResponse).toBeDefined()
197
- expect(postResponse).toHaveProperty('data')
198
-
199
- // Test PUT request
200
- const putResponse = await simpleClient.put('/users/123', { name: 'John Updated' })
201
- expect(putResponse).toBeDefined()
202
- expect(putResponse).toHaveProperty('data')
203
-
204
- // Test DELETE request
205
- const deleteResponse = await simpleClient.delete('/users/123')
206
- expect(deleteResponse).toBeDefined()
207
- expect(deleteResponse).toHaveProperty('data')
208
- })
209
-
210
- it('should handle query parameters', async () => {
211
- const simpleClient = createSimpleClient<MockServer>(mockServer as any)
212
-
213
- const response = await simpleClient.get('/users', { page: 2, limit: 20, search: 'john' })
214
- expect(response).toBeDefined()
215
- })
216
-
217
- it('should handle request body', async () => {
218
- const simpleClient = createSimpleClient<MockServer>(mockServer as any)
219
-
220
- const response = await simpleClient.post('/users', {
221
- name: 'Jane Doe',
222
- email: 'jane@example.com',
223
- age: 25
224
- })
225
- expect(response).toBeDefined()
226
- })
227
-
228
- it('should handle custom request config', async () => {
229
- const simpleClient = createSimpleClient<MockServer>(mockServer as any)
230
-
231
- const response = await simpleClient.get('/users', { page: 1, limit: 10 }, {
232
- headers: { 'X-Custom-Header': 'test' },
233
- timeout: 10000
234
- })
235
- expect(response).toBeDefined()
236
- })
237
- })
238
-
239
- describe('Type Safety', () => {
240
- it('should maintain type safety for server types', () => {
241
- // This test ensures TypeScript compilation works correctly
242
- const typedClient: TypedApiClient<MockServer> = createTypedClient<MockServer>(mockServer as any)
243
- expect(typedClient).toBeDefined()
244
- })
245
-
246
- it('should support generic type constraints', () => {
247
- // Test with different server types
248
- interface SimpleServer {
249
- routes: {
250
- '/health': {
251
- GET: {}
252
- }
253
- }
254
- }
255
-
256
- const simpleServer: SimpleServer = {
257
- routes: {
258
- '/health': { GET: {} }
259
- }
260
- }
261
-
262
- const client = createTypedClient<SimpleServer>(simpleServer as any)
263
- expect(client).toBeDefined()
264
- })
265
- })
266
-
267
- describe('Error Handling', () => {
268
- it('should handle client creation errors gracefully', () => {
269
- // Test with invalid server
270
- const invalidClient = createTypedClient(null as any)
271
- expect(invalidClient).toBeDefined()
272
- })
273
-
274
- it('should handle missing routes gracefully', () => {
275
- const emptyServer = { routes: {} }
276
- const client = createTypedClient(emptyServer as any)
277
- expect(client).toBeDefined()
278
- })
279
- })
280
-
281
- describe('Integration', () => {
282
- it('should work with different server configurations', () => {
283
- const configs = [
284
- { baseURL: 'https://api1.example.com' },
285
- { baseURL: 'https://api2.example.com', timeout: 5000 },
286
- { baseURL: 'https://api3.example.com', retries: 3 }
287
- ]
288
-
289
- configs.forEach(config => {
290
- const client = createSimpleClient<MockServer>(mockServer as any, config)
291
- expect(client).toBeDefined()
292
- })
293
- })
294
-
295
- it('should support method chaining patterns', () => {
296
- const client = createSimpleClient<MockServer>(mockServer as any)
297
-
298
- // Test that methods return the client for chaining
299
- expect(client).toBeDefined()
300
- expect(typeof client.get).toBe('function')
301
- expect(typeof client.post).toBe('function')
302
- })
303
- })
304
- })
@@ -1,363 +0,0 @@
1
- import { describe, expect, it } from 'bun:test'
2
- import {
3
- buildQueryString,
4
- replacePathParams,
5
- isFile,
6
- isFileUpload,
7
- hasFiles,
8
- createFormData,
9
- deepMerge,
10
- delay,
11
- exponentialBackoff,
12
- validateStatus,
13
- parseResponse,
14
- createError,
15
- cloneRequest,
16
- isRetryableError
17
- } from '../src'
18
-
19
- describe('Utils', () => {
20
- describe('buildQueryString', () => {
21
- it('should build query string from object', () => {
22
- const params = { page: 1, limit: 10, search: 'john' }
23
- const result = buildQueryString(params)
24
- expect(result).toBe('?page=1&limit=10&search=john')
25
- })
26
-
27
- it('should handle empty object', () => {
28
- const result = buildQueryString({})
29
- expect(result).toBe('')
30
- })
31
-
32
- it('should handle undefined and null values', () => {
33
- const params = { page: 1, limit: undefined, search: null, active: true }
34
- const result = buildQueryString(params)
35
- expect(result).toBe('?page=1&active=true')
36
- })
37
-
38
- it('should handle array values', () => {
39
- const params = { tags: ['js', 'ts'], page: 1 }
40
- const result = buildQueryString(params)
41
- expect(result).toBe('?tags=js&tags=ts&page=1')
42
- })
43
-
44
- it('should handle boolean values', () => {
45
- const params = { active: true, verified: false }
46
- const result = buildQueryString(params)
47
- expect(result).toBe('?active=true&verified=false')
48
- })
49
- })
50
-
51
- describe('replacePathParams', () => {
52
- it('should replace path parameters', () => {
53
- const path = '/users/:id/posts/:postId'
54
- const params = { id: '123', postId: '456' }
55
- const result = replacePathParams(path, params)
56
- expect(result).toBe('/users/123/posts/456')
57
- })
58
-
59
- it('should handle path with no parameters', () => {
60
- const path = '/users'
61
- const params = {}
62
- const result = replacePathParams(path, params)
63
- expect(result).toBe('/users')
64
- })
65
-
66
- it('should handle numeric parameters', () => {
67
- const path = '/users/:id'
68
- const params = { id: 123 }
69
- const result = replacePathParams(path, params)
70
- expect(result).toBe('/users/123')
71
- })
72
-
73
- it('should handle missing parameters', () => {
74
- const path = '/users/:id/posts/:postId'
75
- const params = { id: '123' }
76
- const result = replacePathParams(path, params)
77
- expect(result).toBe('/users/123/posts/:postId')
78
- })
79
- })
80
-
81
- describe('isFile', () => {
82
- it('should identify File objects', () => {
83
- const file = new File(['content'], 'test.txt')
84
- expect(isFile(file)).toBe(true)
85
- })
86
-
87
- it('should identify Blob objects', () => {
88
- const blob = new Blob(['content'])
89
- expect(isFile(blob)).toBe(true)
90
- })
91
-
92
- it('should reject non-file objects', () => {
93
- expect(isFile('string')).toBe(false)
94
- expect(isFile(123)).toBe(false)
95
- expect(isFile({})).toBe(false)
96
- expect(isFile(null)).toBe(false)
97
- expect(isFile(undefined)).toBe(false)
98
- })
99
- })
100
-
101
- describe('isFileUpload', () => {
102
- it('should identify FileUpload objects', () => {
103
- const fileUpload = {
104
- file: new File(['content'], 'test.txt'),
105
- filename: 'custom.txt',
106
- contentType: 'text/plain'
107
- }
108
- expect(isFileUpload(fileUpload)).toBe(true)
109
- })
110
-
111
- it('should reject non-FileUpload objects', () => {
112
- expect(isFileUpload({})).toBe(false)
113
- expect(isFileUpload({ file: 'string' })).toBe(false)
114
- expect(isFileUpload(null)).toBe(false)
115
- expect(isFileUpload(undefined)).toBe(false)
116
- })
117
- })
118
-
119
- describe('hasFiles', () => {
120
- it('should detect files in object', () => {
121
- const obj = {
122
- name: 'test',
123
- file: new File(['content'], 'test.txt'),
124
- metadata: { size: 100 }
125
- }
126
- expect(hasFiles(obj)).toBe(true)
127
- })
128
-
129
- it('should detect files in nested objects', () => {
130
- const obj = {
131
- user: {
132
- avatar: new File(['content'], 'avatar.jpg'),
133
- profile: { name: 'John' }
134
- }
135
- }
136
- expect(hasFiles(obj)).toBe(true)
137
- })
138
-
139
- it('should detect files in arrays', () => {
140
- const obj = {
141
- files: [new File(['content'], 'file1.txt'), new File(['content'], 'file2.txt')]
142
- }
143
- expect(hasFiles(obj)).toBe(true)
144
- })
145
-
146
- it('should not detect files in object without files', () => {
147
- const obj = {
148
- name: 'test',
149
- age: 25,
150
- active: true
151
- }
152
- expect(hasFiles(obj)).toBe(false)
153
- })
154
- })
155
-
156
- describe('createFormData', () => {
157
- it('should create FormData from object', () => {
158
- const data = {
159
- name: 'John',
160
- age: 25,
161
- file: new File(['content'], 'test.txt')
162
- }
163
- const formData = createFormData(data)
164
- expect(formData).toBeInstanceOf(globalThis.FormData)
165
- })
166
-
167
- it('should handle FileUpload objects', () => {
168
- const data = {
169
- name: 'John',
170
- avatar: {
171
- file: new File(['content'], 'avatar.jpg'),
172
- filename: 'custom.jpg'
173
- }
174
- }
175
- const formData = createFormData(data)
176
- expect(formData).toBeInstanceOf(globalThis.FormData)
177
- })
178
-
179
- it('should handle array values', () => {
180
- const data = {
181
- tags: ['js', 'ts'],
182
- files: [new File(['content'], 'file1.txt'), new File(['content'], 'file2.txt')]
183
- }
184
- const formData = createFormData(data)
185
- expect(formData).toBeInstanceOf(globalThis.FormData)
186
- })
187
-
188
- it('should skip undefined and null values', () => {
189
- const data = {
190
- name: 'John',
191
- age: undefined,
192
- email: null,
193
- active: true
194
- }
195
- const formData = createFormData(data)
196
- expect(formData).toBeInstanceOf(globalThis.FormData)
197
- })
198
- })
199
-
200
- describe('deepMerge', () => {
201
- it('should merge objects deeply', () => {
202
- const target = { a: 1, b: { c: 2, d: 3 } }
203
- const source = { b: { d: 4, e: 5 }, f: 6 }
204
- const result = deepMerge(target, source)
205
- expect(result).toEqual({ a: 1, b: { c: 2, d: 4, e: 5 }, f: 6 })
206
- })
207
-
208
- it('should not modify original objects', () => {
209
- const target = { a: 1, b: { c: 2 } }
210
- const source = { b: { d: 3 } }
211
- const result = deepMerge(target, source)
212
- expect(target).toEqual({ a: 1, b: { c: 2 } })
213
- expect(result).toEqual({ a: 1, b: { c: 2, d: 3 } })
214
- })
215
-
216
- it('should handle empty objects', () => {
217
- const target = {}
218
- const source = {}
219
- const result = deepMerge(target, source)
220
- expect(result).toEqual({})
221
- })
222
-
223
- it('should handle null and undefined', () => {
224
- const target = { a: 1, b: { c: 2 } }
225
- const source = { b: null, c: undefined }
226
- const result = deepMerge(target, source)
227
- expect(result).toEqual({ a: 1, b: null, c: undefined })
228
- })
229
- })
230
-
231
- describe('delay', () => {
232
- it('should delay execution', async () => {
233
- const start = Date.now()
234
- await delay(100)
235
- const end = Date.now()
236
- expect(end - start).toBeGreaterThanOrEqual(90)
237
- })
238
- })
239
-
240
- describe('exponentialBackoff', () => {
241
- it('should calculate exponential backoff', () => {
242
- const result1 = exponentialBackoff(1, 1000, 10000)
243
- const result2 = exponentialBackoff(2, 1000, 10000)
244
- const result3 = exponentialBackoff(3, 1000, 10000)
245
-
246
- expect(result1).toBeGreaterThanOrEqual(1000)
247
- expect(result2).toBeGreaterThanOrEqual(2000)
248
- expect(result3).toBeGreaterThanOrEqual(4000)
249
- })
250
-
251
- it('should respect max delay', () => {
252
- const result = exponentialBackoff(10, 1000, 5000)
253
- // Allow some tolerance for random jitter
254
- expect(result).toBeLessThanOrEqual(6000)
255
- })
256
- })
257
-
258
- describe('validateStatus', () => {
259
- it('should validate successful status codes', () => {
260
- expect(validateStatus(200)).toBe(true)
261
- expect(validateStatus(201)).toBe(true)
262
- expect(validateStatus(299)).toBe(true)
263
- })
264
-
265
- it('should reject error status codes', () => {
266
- expect(validateStatus(400)).toBe(false)
267
- expect(validateStatus(500)).toBe(false)
268
- expect(validateStatus(404)).toBe(false)
269
- })
270
- })
271
-
272
- describe('parseResponse', () => {
273
- it('should parse JSON responses', async () => {
274
- const response = new Response(JSON.stringify({ data: 'test' }), {
275
- headers: { 'Content-Type': 'application/json' }
276
- })
277
- const result = await parseResponse(response)
278
- expect(result).toEqual({ data: 'test' })
279
- })
280
-
281
- it('should parse text responses', async () => {
282
- const response = new Response('Hello World', {
283
- headers: { 'Content-Type': 'text/plain' }
284
- })
285
- const result = await parseResponse(response)
286
- expect(result).toBe('Hello World')
287
- })
288
-
289
- it('should parse array buffer responses', async () => {
290
- const buffer = new ArrayBuffer(8)
291
- const response = new Response(buffer, {
292
- headers: { 'Content-Type': 'application/octet-stream' }
293
- })
294
- const result = await parseResponse(response)
295
- expect(result).toBeInstanceOf(ArrayBuffer)
296
- })
297
-
298
- it('should handle responses without content-type', async () => {
299
- const response = new Response('Hello World')
300
- const result = await parseResponse(response)
301
- expect(result).toBe('Hello World')
302
- })
303
- })
304
-
305
- describe('createError', () => {
306
- it('should create error with status and data', () => {
307
- const error = createError(404, 'Not Found', { resource: 'user' })
308
- expect(error).toBeInstanceOf(Error)
309
- expect((error as any).status).toBe(404)
310
- expect((error as any).data).toEqual({ resource: 'user' })
311
- expect((error as any).name).toBe('ApiError')
312
- })
313
-
314
- it('should create error without data', () => {
315
- const error = createError(500, 'Internal Server Error')
316
- expect(error).toBeInstanceOf(Error)
317
- expect((error as any).status).toBe(500)
318
- expect((error as any).data).toBeUndefined()
319
- })
320
- })
321
-
322
- describe('cloneRequest', () => {
323
- it('should clone request object', () => {
324
- const original = new Request('https://example.com', {
325
- method: 'POST',
326
- headers: { 'Content-Type': 'application/json' },
327
- body: JSON.stringify({ test: true })
328
- })
329
- const cloned = cloneRequest(original)
330
- expect(cloned).toBeInstanceOf(Request)
331
- expect(cloned.url).toBe(original.url)
332
- expect(cloned.method).toBe(original.method)
333
- })
334
- })
335
-
336
- describe('isRetryableError', () => {
337
- it('should identify retryable HTTP status codes', () => {
338
- expect(isRetryableError(new Error(), 408)).toBe(true)
339
- expect(isRetryableError(new Error(), 429)).toBe(true)
340
- expect(isRetryableError(new Error(), 500)).toBe(true)
341
- expect(isRetryableError(new Error(), 502)).toBe(true)
342
- expect(isRetryableError(new Error(), 503)).toBe(true)
343
- expect(isRetryableError(new Error(), 504)).toBe(true)
344
- })
345
-
346
- it('should reject non-retryable status codes', () => {
347
- expect(isRetryableError(new Error(), 400)).toBe(false)
348
- expect(isRetryableError(new Error(), 401)).toBe(false)
349
- expect(isRetryableError(new Error(), 403)).toBe(false)
350
- expect(isRetryableError(new Error(), 404)).toBe(false)
351
- })
352
-
353
- it('should identify network errors as retryable', () => {
354
- const networkError = new Error('fetch failed')
355
- expect(isRetryableError(networkError)).toBe(true)
356
- })
357
-
358
- it('should reject other errors as non-retryable', () => {
359
- const otherError = new Error('Validation failed')
360
- expect(isRetryableError(otherError)).toBe(false)
361
- })
362
- })
363
- })