@devup-api/fetch 0.1.0 → 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.
- package/package.json +6 -2
- package/dist/__tests__/api.test.d.ts +0 -2
- package/dist/__tests__/api.test.d.ts.map +0 -1
- package/dist/__tests__/create-api.test.d.ts +0 -2
- package/dist/__tests__/create-api.test.d.ts.map +0 -1
- package/dist/__tests__/index.test.d.ts +0 -2
- package/dist/__tests__/index.test.d.ts.map +0 -1
- package/dist/__tests__/response-converter.test.d.ts +0 -2
- package/dist/__tests__/response-converter.test.d.ts.map +0 -1
- package/dist/__tests__/url-map.test.d.ts +0 -2
- package/dist/__tests__/url-map.test.d.ts.map +0 -1
- package/dist/__tests__/utils.test.d.ts +0 -2
- package/dist/__tests__/utils.test.d.ts.map +0 -1
- package/src/__tests__/api.test.ts +0 -245
- package/src/__tests__/create-api.test.ts +0 -25
- package/src/__tests__/index.test.ts +0 -10
- package/src/__tests__/response-converter.test.ts +0 -158
- package/src/__tests__/url-map.test.ts +0 -182
- package/src/__tests__/utils.test.ts +0 -108
- package/src/api.ts +0 -303
- package/src/create-api.ts +0 -8
- package/src/index.ts +0 -2
- package/src/response-converter.ts +0 -44
- package/src/url-map.ts +0 -13
- package/src/utils.ts +0 -18
- package/tsconfig.json +0 -34
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@devup-api/fetch",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"license": "Apache-2.0",
|
|
4
5
|
"type": "module",
|
|
5
6
|
"exports": {
|
|
6
7
|
".": {
|
|
@@ -9,6 +10,9 @@
|
|
|
9
10
|
"types": "./dist/index.d.ts"
|
|
10
11
|
}
|
|
11
12
|
},
|
|
13
|
+
"files": [
|
|
14
|
+
"dist"
|
|
15
|
+
],
|
|
12
16
|
"scripts": {
|
|
13
17
|
"build": "tsc && bun build --target node --outfile=dist/index.js src/index.ts --production --packages=external && bun build --target node --outfile=dist/index.cjs --format=cjs src/index.ts --production --packages=external"
|
|
14
18
|
},
|
|
@@ -16,7 +20,7 @@
|
|
|
16
20
|
"access": "public"
|
|
17
21
|
},
|
|
18
22
|
"dependencies": {
|
|
19
|
-
"@devup-api/core": "0.1.
|
|
23
|
+
"@devup-api/core": "0.1.2"
|
|
20
24
|
},
|
|
21
25
|
"devDependencies": {
|
|
22
26
|
"@types/node": "^24.10",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"api.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/api.test.ts"],"names":[],"mappings":""}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"create-api.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/create-api.test.ts"],"names":[],"mappings":""}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/index.test.ts"],"names":[],"mappings":""}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"response-converter.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/response-converter.test.ts"],"names":[],"mappings":""}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"url-map.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/url-map.test.ts"],"names":[],"mappings":""}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"utils.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/utils.test.ts"],"names":[],"mappings":""}
|
|
@@ -1,245 +0,0 @@
|
|
|
1
|
-
import { afterEach, beforeEach, expect, mock, test } from 'bun:test'
|
|
2
|
-
import { DevupApi } from '../api'
|
|
3
|
-
|
|
4
|
-
const originalFetch = globalThis.fetch
|
|
5
|
-
|
|
6
|
-
beforeEach(() => {
|
|
7
|
-
globalThis.fetch = mock(() =>
|
|
8
|
-
Promise.resolve(
|
|
9
|
-
new Response(JSON.stringify({ success: true }), {
|
|
10
|
-
status: 200,
|
|
11
|
-
headers: { 'Content-Type': 'application/json' },
|
|
12
|
-
}),
|
|
13
|
-
),
|
|
14
|
-
) as unknown as typeof fetch
|
|
15
|
-
})
|
|
16
|
-
|
|
17
|
-
afterEach(() => {
|
|
18
|
-
globalThis.fetch = originalFetch
|
|
19
|
-
})
|
|
20
|
-
|
|
21
|
-
test.each([
|
|
22
|
-
['https://api.example.com', 'https://api.example.com'],
|
|
23
|
-
['https://api.example.com/', 'https://api.example.com'],
|
|
24
|
-
['http://localhost:3000', 'http://localhost:3000'],
|
|
25
|
-
['http://localhost:3000/', 'http://localhost:3000'],
|
|
26
|
-
] as const)('constructor removes trailing slash: %s -> %s', (baseUrl, expected) => {
|
|
27
|
-
const api = new DevupApi(baseUrl)
|
|
28
|
-
expect(api.getBaseUrl()).toBe(expected)
|
|
29
|
-
})
|
|
30
|
-
|
|
31
|
-
test.each([
|
|
32
|
-
[undefined, {}],
|
|
33
|
-
[{}, {}],
|
|
34
|
-
[
|
|
35
|
-
{ headers: { Authorization: 'Bearer token' } },
|
|
36
|
-
{ headers: { Authorization: 'Bearer token' } },
|
|
37
|
-
],
|
|
38
|
-
] as const)('constructor accepts defaultOptions: %s -> %s', (defaultOptions, expected) => {
|
|
39
|
-
const api = new DevupApi('https://api.example.com', defaultOptions)
|
|
40
|
-
expect(api.getDefaultOptions()).toEqual(expected)
|
|
41
|
-
})
|
|
42
|
-
|
|
43
|
-
test.each([
|
|
44
|
-
[{}, {}],
|
|
45
|
-
[
|
|
46
|
-
{ headers: { 'Content-Type': 'application/json' } },
|
|
47
|
-
{ headers: { 'Content-Type': 'application/json' } },
|
|
48
|
-
],
|
|
49
|
-
] as const)('setDefaultOptions updates defaultOptions: %s -> %s', (options, expected) => {
|
|
50
|
-
const api = new DevupApi('https://api.example.com')
|
|
51
|
-
api.setDefaultOptions(options)
|
|
52
|
-
expect(api.getDefaultOptions()).toEqual(expected)
|
|
53
|
-
})
|
|
54
|
-
|
|
55
|
-
test.each([
|
|
56
|
-
['GET', 'get'],
|
|
57
|
-
['GET', 'GET'],
|
|
58
|
-
['POST', 'post'],
|
|
59
|
-
['POST', 'POST'],
|
|
60
|
-
['PUT', 'put'],
|
|
61
|
-
['PUT', 'PUT'],
|
|
62
|
-
['DELETE', 'delete'],
|
|
63
|
-
['DELETE', 'DELETE'],
|
|
64
|
-
['PATCH', 'patch'],
|
|
65
|
-
['PATCH', 'PATCH'],
|
|
66
|
-
] as const)('HTTP method %s calls request with correct method', async (expectedMethod, methodName) => {
|
|
67
|
-
const api = new DevupApi('https://api.example.com')
|
|
68
|
-
const mockFetch = globalThis.fetch as unknown as ReturnType<typeof mock>
|
|
69
|
-
|
|
70
|
-
await api[methodName]('/test' as never)
|
|
71
|
-
|
|
72
|
-
expect(mockFetch).toHaveBeenCalledTimes(1)
|
|
73
|
-
const call = mockFetch.mock.calls[0]
|
|
74
|
-
expect(call).toBeDefined()
|
|
75
|
-
if (call) {
|
|
76
|
-
const request = call[0] as Request
|
|
77
|
-
expect(request.method).toBe(expectedMethod)
|
|
78
|
-
}
|
|
79
|
-
})
|
|
80
|
-
|
|
81
|
-
test('request serializes plain object body to JSON', async () => {
|
|
82
|
-
const api = new DevupApi('https://api.example.com')
|
|
83
|
-
const mockFetch = globalThis.fetch as unknown as ReturnType<typeof mock>
|
|
84
|
-
|
|
85
|
-
await api.post(
|
|
86
|
-
'/test' as never,
|
|
87
|
-
{
|
|
88
|
-
body: { name: 'test', value: 123 },
|
|
89
|
-
} as never,
|
|
90
|
-
)
|
|
91
|
-
|
|
92
|
-
expect(mockFetch).toHaveBeenCalledTimes(1)
|
|
93
|
-
const call = mockFetch.mock.calls[0]
|
|
94
|
-
expect(call).toBeDefined()
|
|
95
|
-
if (call) {
|
|
96
|
-
const request = call[0] as Request
|
|
97
|
-
const body = await request.text()
|
|
98
|
-
expect(body).toBe(JSON.stringify({ name: 'test', value: 123 }))
|
|
99
|
-
}
|
|
100
|
-
})
|
|
101
|
-
|
|
102
|
-
test('request does not serialize non-plain object body', async () => {
|
|
103
|
-
const api = new DevupApi('https://api.example.com')
|
|
104
|
-
const mockFetch = globalThis.fetch as unknown as ReturnType<typeof mock>
|
|
105
|
-
const formData = new FormData()
|
|
106
|
-
formData.append('file', 'test')
|
|
107
|
-
|
|
108
|
-
await api.post(
|
|
109
|
-
'/test' as never,
|
|
110
|
-
{
|
|
111
|
-
body: formData,
|
|
112
|
-
} as never,
|
|
113
|
-
)
|
|
114
|
-
|
|
115
|
-
expect(mockFetch).toHaveBeenCalledTimes(1)
|
|
116
|
-
const call = mockFetch.mock.calls[0]
|
|
117
|
-
expect(call).toBeDefined()
|
|
118
|
-
if (call) {
|
|
119
|
-
const request = call[0] as Request
|
|
120
|
-
// FormData should not be serialized with JSON.stringify and should be passed as-is
|
|
121
|
-
// Request body should not be null
|
|
122
|
-
expect(request.body).not.toBeNull()
|
|
123
|
-
// FormData is automatically set to multipart/form-data
|
|
124
|
-
// body should exist
|
|
125
|
-
expect(request.body).toBeDefined()
|
|
126
|
-
}
|
|
127
|
-
})
|
|
128
|
-
|
|
129
|
-
test('request merges defaultOptions with request options', async () => {
|
|
130
|
-
const api = new DevupApi('https://api.example.com', {
|
|
131
|
-
headers: { 'X-Default': 'default-value' },
|
|
132
|
-
})
|
|
133
|
-
const mockFetch = globalThis.fetch as unknown as ReturnType<typeof mock>
|
|
134
|
-
|
|
135
|
-
await api.get(
|
|
136
|
-
'/test' as never,
|
|
137
|
-
{
|
|
138
|
-
headers: { 'X-Request': 'request-value' },
|
|
139
|
-
} as never,
|
|
140
|
-
)
|
|
141
|
-
|
|
142
|
-
expect(mockFetch).toHaveBeenCalledTimes(1)
|
|
143
|
-
const call = mockFetch.mock.calls[0]
|
|
144
|
-
expect(call).toBeDefined()
|
|
145
|
-
if (call) {
|
|
146
|
-
const request = call[0] as Request
|
|
147
|
-
// Headers are merged, but we can't easily test the merged result
|
|
148
|
-
// So we just verify the request was made
|
|
149
|
-
expect(request).toBeDefined()
|
|
150
|
-
}
|
|
151
|
-
})
|
|
152
|
-
|
|
153
|
-
test('request uses params to replace path parameters', async () => {
|
|
154
|
-
const api = new DevupApi('https://api.example.com')
|
|
155
|
-
const mockFetch = globalThis.fetch as unknown as ReturnType<typeof mock>
|
|
156
|
-
|
|
157
|
-
await api.get(
|
|
158
|
-
'/users/{id}' as never,
|
|
159
|
-
{
|
|
160
|
-
params: { id: '123' },
|
|
161
|
-
} as never,
|
|
162
|
-
)
|
|
163
|
-
|
|
164
|
-
expect(mockFetch).toHaveBeenCalledTimes(1)
|
|
165
|
-
const call = mockFetch.mock.calls[0]
|
|
166
|
-
expect(call).toBeDefined()
|
|
167
|
-
if (call) {
|
|
168
|
-
const request = call[0] as Request
|
|
169
|
-
expect(request.url).toBe('https://api.example.com/users/123')
|
|
170
|
-
}
|
|
171
|
-
})
|
|
172
|
-
|
|
173
|
-
test('request returns response with data on success', async () => {
|
|
174
|
-
globalThis.fetch = mock(() =>
|
|
175
|
-
Promise.resolve(
|
|
176
|
-
new Response(JSON.stringify({ id: 1, name: 'test' }), {
|
|
177
|
-
status: 200,
|
|
178
|
-
headers: { 'Content-Type': 'application/json' },
|
|
179
|
-
}),
|
|
180
|
-
),
|
|
181
|
-
) as unknown as typeof fetch
|
|
182
|
-
|
|
183
|
-
const api = new DevupApi('https://api.example.com')
|
|
184
|
-
const result = (await api.get('/test' as never)) as {
|
|
185
|
-
data?: unknown
|
|
186
|
-
error?: unknown
|
|
187
|
-
response: Response
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
expect('data' in result).toBe(true)
|
|
191
|
-
if ('data' in result && result.data !== undefined) {
|
|
192
|
-
expect(result.data).toEqual({ id: 1, name: 'test' })
|
|
193
|
-
}
|
|
194
|
-
expect('error' in result).toBe(false)
|
|
195
|
-
expect(result.response).toBeDefined()
|
|
196
|
-
expect(result.response.ok).toBe(true)
|
|
197
|
-
})
|
|
198
|
-
|
|
199
|
-
test('request returns response with error on failure', async () => {
|
|
200
|
-
globalThis.fetch = mock(() =>
|
|
201
|
-
Promise.resolve(
|
|
202
|
-
new Response(JSON.stringify({ message: 'Not found' }), {
|
|
203
|
-
status: 404,
|
|
204
|
-
headers: { 'Content-Type': 'application/json' },
|
|
205
|
-
}),
|
|
206
|
-
),
|
|
207
|
-
) as unknown as typeof fetch
|
|
208
|
-
|
|
209
|
-
const api = new DevupApi('https://api.example.com')
|
|
210
|
-
const result = (await api.get('/test' as never)) as {
|
|
211
|
-
data?: unknown
|
|
212
|
-
error?: unknown
|
|
213
|
-
response: Response
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
expect('error' in result).toBe(true)
|
|
217
|
-
if ('error' in result && result.error !== undefined) {
|
|
218
|
-
expect(result.error).toEqual({ message: 'Not found' })
|
|
219
|
-
}
|
|
220
|
-
expect('data' in result).toBe(false)
|
|
221
|
-
expect(result.response).toBeDefined()
|
|
222
|
-
expect(result.response.ok).toBe(false)
|
|
223
|
-
})
|
|
224
|
-
|
|
225
|
-
test('request handles 204 No Content response', async () => {
|
|
226
|
-
globalThis.fetch = mock(() =>
|
|
227
|
-
Promise.resolve(
|
|
228
|
-
new Response(null, {
|
|
229
|
-
status: 204,
|
|
230
|
-
}),
|
|
231
|
-
),
|
|
232
|
-
) as unknown as typeof fetch
|
|
233
|
-
|
|
234
|
-
const api = new DevupApi('https://api.example.com')
|
|
235
|
-
const result = await api.delete('/test' as never)
|
|
236
|
-
|
|
237
|
-
if ('data' in result) {
|
|
238
|
-
expect(result.data).toBeUndefined()
|
|
239
|
-
}
|
|
240
|
-
if ('error' in result) {
|
|
241
|
-
expect(result.error).toBeUndefined()
|
|
242
|
-
}
|
|
243
|
-
expect(result.response).toBeDefined()
|
|
244
|
-
expect(result.response.status).toBe(204)
|
|
245
|
-
})
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import { expect, test } from 'bun:test'
|
|
2
|
-
import { DevupApi } from '../api'
|
|
3
|
-
import { createApi } from '../create-api'
|
|
4
|
-
|
|
5
|
-
test.each([
|
|
6
|
-
['https://api.example.com'],
|
|
7
|
-
['https://api.example.com/'],
|
|
8
|
-
['http://localhost:3000'],
|
|
9
|
-
['http://localhost:3000/'],
|
|
10
|
-
] as const)('createApi returns DevupApi instance: %s', (baseUrl) => {
|
|
11
|
-
const api = createApi(baseUrl)
|
|
12
|
-
expect(api).toBeInstanceOf(DevupApi)
|
|
13
|
-
})
|
|
14
|
-
|
|
15
|
-
test.each([
|
|
16
|
-
['https://api.example.com', undefined],
|
|
17
|
-
['https://api.example.com', {}],
|
|
18
|
-
['https://api.example.com', { headers: { Authorization: 'Bearer token' } }],
|
|
19
|
-
] as const)('createApi accepts defaultOptions: %s', (baseUrl, defaultOptions) => {
|
|
20
|
-
const api = createApi(baseUrl, defaultOptions)
|
|
21
|
-
expect(api).toBeInstanceOf(DevupApi)
|
|
22
|
-
if (defaultOptions) {
|
|
23
|
-
expect(api.getDefaultOptions()).toEqual(defaultOptions)
|
|
24
|
-
}
|
|
25
|
-
})
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import { expect, test } from 'bun:test'
|
|
2
|
-
import * as indexModule from '../index'
|
|
3
|
-
|
|
4
|
-
// Type imports to verify types are exported (compile-time check)
|
|
5
|
-
|
|
6
|
-
test('index.ts exports', () => {
|
|
7
|
-
expect({ ...indexModule }).toEqual({
|
|
8
|
-
createApi: expect.any(Function),
|
|
9
|
-
})
|
|
10
|
-
})
|
|
@@ -1,158 +0,0 @@
|
|
|
1
|
-
import { expect, test } from 'bun:test'
|
|
2
|
-
import { convertResponse } from '../response-converter'
|
|
3
|
-
|
|
4
|
-
test.each([
|
|
5
|
-
['json', 'json'],
|
|
6
|
-
['text', 'text'],
|
|
7
|
-
['stream', 'stream'],
|
|
8
|
-
] as const)('convertResponse parses successful response with parseAs=%s', async (parseAs) => {
|
|
9
|
-
const request = new Request('https://api.example.com/test', { method: 'GET' })
|
|
10
|
-
const response = new Response(JSON.stringify({ id: 1, name: 'test' }), {
|
|
11
|
-
status: 200,
|
|
12
|
-
headers: { 'Content-Type': 'application/json' },
|
|
13
|
-
})
|
|
14
|
-
|
|
15
|
-
const result = await convertResponse(request, response, parseAs)
|
|
16
|
-
|
|
17
|
-
expect('data' in result).toBe(true)
|
|
18
|
-
if ('data' in result) {
|
|
19
|
-
if (parseAs === 'stream') {
|
|
20
|
-
expect(result.data).toBeDefined()
|
|
21
|
-
} else if (parseAs === 'text') {
|
|
22
|
-
expect(typeof result.data).toBe('string')
|
|
23
|
-
} else {
|
|
24
|
-
expect(result.data).toEqual({ id: 1, name: 'test' })
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
expect(result.response).toBe(response)
|
|
28
|
-
})
|
|
29
|
-
|
|
30
|
-
test('convertResponse handles 204 No Content with success', async () => {
|
|
31
|
-
const request = new Request('https://api.example.com/test', {
|
|
32
|
-
method: 'DELETE',
|
|
33
|
-
})
|
|
34
|
-
const response = new Response(null, {
|
|
35
|
-
status: 204,
|
|
36
|
-
})
|
|
37
|
-
|
|
38
|
-
const result = await convertResponse(request, response)
|
|
39
|
-
|
|
40
|
-
if ('data' in result) {
|
|
41
|
-
expect(result.data).toBeUndefined()
|
|
42
|
-
}
|
|
43
|
-
expect(result.response).toBe(response)
|
|
44
|
-
})
|
|
45
|
-
|
|
46
|
-
test('convertResponse handles 204 No Content with error', async () => {
|
|
47
|
-
const request = new Request('https://api.example.com/test', {
|
|
48
|
-
method: 'DELETE',
|
|
49
|
-
})
|
|
50
|
-
const response = new Response(null, {
|
|
51
|
-
status: 204,
|
|
52
|
-
statusText: 'No Content',
|
|
53
|
-
})
|
|
54
|
-
|
|
55
|
-
// Mock response.ok to be false
|
|
56
|
-
Object.defineProperty(response, 'ok', {
|
|
57
|
-
value: false,
|
|
58
|
-
writable: false,
|
|
59
|
-
})
|
|
60
|
-
|
|
61
|
-
const result = await convertResponse(request, response)
|
|
62
|
-
|
|
63
|
-
if ('error' in result) {
|
|
64
|
-
expect(result.error).toBeUndefined()
|
|
65
|
-
}
|
|
66
|
-
expect(result.response).toBe(response)
|
|
67
|
-
})
|
|
68
|
-
|
|
69
|
-
test('convertResponse handles HEAD request', async () => {
|
|
70
|
-
const request = new Request('https://api.example.com/test', {
|
|
71
|
-
method: 'HEAD',
|
|
72
|
-
})
|
|
73
|
-
const response = new Response(null, {
|
|
74
|
-
status: 200,
|
|
75
|
-
})
|
|
76
|
-
|
|
77
|
-
const result = await convertResponse(request, response)
|
|
78
|
-
|
|
79
|
-
if ('data' in result) {
|
|
80
|
-
expect(result.data).toBeUndefined()
|
|
81
|
-
}
|
|
82
|
-
expect(result.response).toBe(response)
|
|
83
|
-
})
|
|
84
|
-
|
|
85
|
-
test('convertResponse handles Content-Length: 0', async () => {
|
|
86
|
-
const request = new Request('https://api.example.com/test', { method: 'GET' })
|
|
87
|
-
const response = new Response(null, {
|
|
88
|
-
status: 200,
|
|
89
|
-
headers: { 'Content-Length': '0' },
|
|
90
|
-
})
|
|
91
|
-
|
|
92
|
-
const result = await convertResponse(request, response)
|
|
93
|
-
|
|
94
|
-
if ('data' in result) {
|
|
95
|
-
expect(result.data).toBeUndefined()
|
|
96
|
-
}
|
|
97
|
-
expect(result.response).toBe(response)
|
|
98
|
-
})
|
|
99
|
-
|
|
100
|
-
test('convertResponse handles error response with JSON', async () => {
|
|
101
|
-
const request = new Request('https://api.example.com/test', { method: 'GET' })
|
|
102
|
-
const response = new Response(JSON.stringify({ message: 'Not found' }), {
|
|
103
|
-
status: 404,
|
|
104
|
-
headers: { 'Content-Type': 'application/json' },
|
|
105
|
-
})
|
|
106
|
-
|
|
107
|
-
const result = await convertResponse(request, response)
|
|
108
|
-
|
|
109
|
-
if ('error' in result) {
|
|
110
|
-
expect(result.error).toEqual({ message: 'Not found' })
|
|
111
|
-
}
|
|
112
|
-
expect(result.response).toBe(response)
|
|
113
|
-
})
|
|
114
|
-
|
|
115
|
-
test('convertResponse handles error response with non-JSON text', async () => {
|
|
116
|
-
const request = new Request('https://api.example.com/test', { method: 'GET' })
|
|
117
|
-
const response = new Response('Internal Server Error', {
|
|
118
|
-
status: 500,
|
|
119
|
-
headers: { 'Content-Type': 'text/plain' },
|
|
120
|
-
})
|
|
121
|
-
|
|
122
|
-
const result = await convertResponse(request, response)
|
|
123
|
-
|
|
124
|
-
if ('error' in result) {
|
|
125
|
-
expect(result.error).toBe('Internal Server Error')
|
|
126
|
-
}
|
|
127
|
-
expect(result.response).toBe(response)
|
|
128
|
-
})
|
|
129
|
-
|
|
130
|
-
test('convertResponse handles error response with invalid JSON', async () => {
|
|
131
|
-
const request = new Request('https://api.example.com/test', { method: 'GET' })
|
|
132
|
-
const response = new Response('Invalid JSON{', {
|
|
133
|
-
status: 400,
|
|
134
|
-
headers: { 'Content-Type': 'application/json' },
|
|
135
|
-
})
|
|
136
|
-
|
|
137
|
-
const result = await convertResponse(request, response)
|
|
138
|
-
|
|
139
|
-
if ('error' in result) {
|
|
140
|
-
expect(result.error).toBe('Invalid JSON{')
|
|
141
|
-
}
|
|
142
|
-
expect(result.response).toBe(response)
|
|
143
|
-
})
|
|
144
|
-
|
|
145
|
-
test('convertResponse handles non-204 error with Content-Length: 0', async () => {
|
|
146
|
-
const request = new Request('https://api.example.com/test', { method: 'GET' })
|
|
147
|
-
const response = new Response(null, {
|
|
148
|
-
status: 500,
|
|
149
|
-
headers: { 'Content-Length': '0' },
|
|
150
|
-
})
|
|
151
|
-
|
|
152
|
-
const result = await convertResponse(request, response)
|
|
153
|
-
|
|
154
|
-
if ('error' in result) {
|
|
155
|
-
expect(result.error).toBeUndefined()
|
|
156
|
-
}
|
|
157
|
-
expect(result.response).toBe(response)
|
|
158
|
-
})
|
|
@@ -1,182 +0,0 @@
|
|
|
1
|
-
import { expect, test } from 'bun:test'
|
|
2
|
-
|
|
3
|
-
const urlMap = {
|
|
4
|
-
getUsers: { method: 'GET' as const, url: '/users' },
|
|
5
|
-
createUser: { method: 'POST' as const, url: '/users' },
|
|
6
|
-
updateUser: { method: 'PUT' as const, url: '/users/{id}' },
|
|
7
|
-
deleteUser: { method: 'DELETE' as const, url: '/users/{id}' },
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
test.each([
|
|
11
|
-
['getUsers', '/users', JSON.stringify(urlMap)],
|
|
12
|
-
['createUser', '/users', JSON.stringify(urlMap)],
|
|
13
|
-
['updateUser', '/users/{id}', JSON.stringify(urlMap)],
|
|
14
|
-
['deleteUser', '/users/{id}', JSON.stringify(urlMap)],
|
|
15
|
-
] as const)('getUrl returns url for existing key: %s -> %s', async (key, expected, envValue) => {
|
|
16
|
-
process.env.DEVUP_API_URL_MAP = envValue
|
|
17
|
-
// Add query parameter to bypass module cache and reload
|
|
18
|
-
const { getUrl } = await import(`../url-map?t=${Date.now()}`)
|
|
19
|
-
expect(getUrl(key)).toBe(expected)
|
|
20
|
-
})
|
|
21
|
-
|
|
22
|
-
test.each([
|
|
23
|
-
['nonExistentKey', 'nonExistentKey', JSON.stringify(urlMap)],
|
|
24
|
-
['unknown', 'unknown', JSON.stringify(urlMap)],
|
|
25
|
-
['', '', JSON.stringify(urlMap)],
|
|
26
|
-
['/users', '/users', JSON.stringify(urlMap)],
|
|
27
|
-
] as const)('getUrl returns key itself when key does not exist: %s -> %s', async (key, expected, envValue) => {
|
|
28
|
-
process.env.DEVUP_API_URL_MAP = envValue
|
|
29
|
-
const { getUrl } = await import(`../url-map?t=${Date.now()}`)
|
|
30
|
-
expect(getUrl(key)).toBe(expected)
|
|
31
|
-
})
|
|
32
|
-
|
|
33
|
-
test.each([
|
|
34
|
-
['getUsers', { method: 'GET', url: '/users' }, JSON.stringify(urlMap)],
|
|
35
|
-
['createUser', { method: 'POST', url: '/users' }, JSON.stringify(urlMap)],
|
|
36
|
-
['updateUser', { method: 'PUT', url: '/users/{id}' }, JSON.stringify(urlMap)],
|
|
37
|
-
[
|
|
38
|
-
'deleteUser',
|
|
39
|
-
{ method: 'DELETE', url: '/users/{id}' },
|
|
40
|
-
JSON.stringify(urlMap),
|
|
41
|
-
],
|
|
42
|
-
] as const)('getUrlWithMethod returns UrlMapValue for existing key: %s -> %s', async (key, expected, envValue) => {
|
|
43
|
-
process.env.DEVUP_API_URL_MAP = envValue
|
|
44
|
-
const { getUrlWithMethod } = await import(`../url-map?t=${Date.now()}`)
|
|
45
|
-
expect(getUrlWithMethod(key)).toEqual(expected)
|
|
46
|
-
})
|
|
47
|
-
|
|
48
|
-
test.each([
|
|
49
|
-
[
|
|
50
|
-
'nonExistentKey',
|
|
51
|
-
{ method: 'GET', url: 'nonExistentKey' },
|
|
52
|
-
JSON.stringify(urlMap),
|
|
53
|
-
],
|
|
54
|
-
['unknown', { method: 'GET', url: 'unknown' }, JSON.stringify(urlMap)],
|
|
55
|
-
['', { method: 'GET', url: '' }, JSON.stringify(urlMap)],
|
|
56
|
-
['/users', { method: 'GET', url: '/users' }, JSON.stringify(urlMap)],
|
|
57
|
-
] as const)('getUrlWithMethod returns default for non-existent key: %s -> %s', async (key, expected, envValue) => {
|
|
58
|
-
process.env.DEVUP_API_URL_MAP = envValue
|
|
59
|
-
const { getUrlWithMethod } = await import(`../url-map?t=${Date.now()}`)
|
|
60
|
-
expect(getUrlWithMethod(key)).toEqual(expected)
|
|
61
|
-
})
|
|
62
|
-
|
|
63
|
-
test.each([
|
|
64
|
-
['anyKey', 'anyKey', '{}'],
|
|
65
|
-
['test', 'test', '{}'],
|
|
66
|
-
] as const)('getUrl works with empty URL map: %s -> %s', async (key, expected, envValue) => {
|
|
67
|
-
process.env.DEVUP_API_URL_MAP = envValue
|
|
68
|
-
const { getUrl } = await import(`../url-map?t=${Date.now()}`)
|
|
69
|
-
expect(getUrl(key)).toBe(expected)
|
|
70
|
-
})
|
|
71
|
-
|
|
72
|
-
test.each([
|
|
73
|
-
['anyKey', { method: 'GET', url: 'anyKey' }, '{}'],
|
|
74
|
-
['test', { method: 'GET', url: 'test' }, '{}'],
|
|
75
|
-
] as const)('getUrlWithMethod works with empty URL map: %s -> %s', async (key, expected, envValue) => {
|
|
76
|
-
process.env.DEVUP_API_URL_MAP = envValue
|
|
77
|
-
const { getUrlWithMethod } = await import(`../url-map?t=${Date.now()}`)
|
|
78
|
-
expect(getUrlWithMethod(key)).toEqual(expected)
|
|
79
|
-
})
|
|
80
|
-
|
|
81
|
-
test.each([
|
|
82
|
-
['anyKey', 'anyKey'],
|
|
83
|
-
['test', 'test'],
|
|
84
|
-
] as const)('getUrl works when DEVUP_API_URL_MAP is not set: %s -> %s', async (key, expected) => {
|
|
85
|
-
delete process.env.DEVUP_API_URL_MAP
|
|
86
|
-
const { getUrl } = await import(`../url-map?t=${Date.now()}`)
|
|
87
|
-
expect(getUrl(key)).toBe(expected)
|
|
88
|
-
})
|
|
89
|
-
|
|
90
|
-
test.each([
|
|
91
|
-
['anyKey', { method: 'GET', url: 'anyKey' }],
|
|
92
|
-
['test', { method: 'GET', url: 'test' }],
|
|
93
|
-
] as const)('getUrlWithMethod works when DEVUP_API_URL_MAP is not set: %s -> %s', async (key, expected) => {
|
|
94
|
-
delete process.env.DEVUP_API_URL_MAP
|
|
95
|
-
const { getUrlWithMethod } = await import(`../url-map?t=${Date.now()}`)
|
|
96
|
-
expect(getUrlWithMethod(key)).toEqual(expected)
|
|
97
|
-
})
|
|
98
|
-
|
|
99
|
-
test('getUrl handles key that exists but url property is missing', async () => {
|
|
100
|
-
const urlMapWithoutUrl = {
|
|
101
|
-
testKey: { method: 'GET' as const },
|
|
102
|
-
}
|
|
103
|
-
process.env.DEVUP_API_URL_MAP = JSON.stringify(urlMapWithoutUrl)
|
|
104
|
-
const { getUrl } = await import(`../url-map?t=${Date.now() + Math.random()}`)
|
|
105
|
-
// When url property is missing, optional chaining returns undefined, so key is returned
|
|
106
|
-
expect(getUrl('testKey')).toBe('testKey')
|
|
107
|
-
})
|
|
108
|
-
|
|
109
|
-
test('DEVUP_API_URL_MAP constant is exported and accessible', async () => {
|
|
110
|
-
const testUrlMap = { testKey: { method: 'GET' as const, url: '/test' } }
|
|
111
|
-
process.env.DEVUP_API_URL_MAP = JSON.stringify(testUrlMap)
|
|
112
|
-
const urlMapModule = await import(
|
|
113
|
-
`../url-map?t=${Date.now() + Math.random()}`
|
|
114
|
-
)
|
|
115
|
-
expect(urlMapModule).toHaveProperty('DEVUP_API_URL_MAP')
|
|
116
|
-
expect(typeof urlMapModule.DEVUP_API_URL_MAP).toBe('object')
|
|
117
|
-
// Directly access the constant to ensure it's covered
|
|
118
|
-
const urlMap = urlMapModule.DEVUP_API_URL_MAP
|
|
119
|
-
expect(urlMap).toEqual(testUrlMap)
|
|
120
|
-
// Verify it's used by getUrl function
|
|
121
|
-
expect(urlMapModule.getUrl('testKey')).toBe('/test')
|
|
122
|
-
})
|
|
123
|
-
|
|
124
|
-
test('DEVUP_API_URL_MAP uses fallback when env var is undefined', async () => {
|
|
125
|
-
delete process.env.DEVUP_API_URL_MAP
|
|
126
|
-
const urlMapModule = await import(
|
|
127
|
-
`../url-map?t=${Date.now() + Math.random()}`
|
|
128
|
-
)
|
|
129
|
-
// Directly access the constant to ensure the fallback path is covered
|
|
130
|
-
const urlMap = urlMapModule.DEVUP_API_URL_MAP
|
|
131
|
-
expect(urlMap).toEqual({})
|
|
132
|
-
expect(urlMapModule.getUrl('anyKey')).toBe('anyKey')
|
|
133
|
-
expect(urlMapModule.getUrlWithMethod('anyKey')).toEqual({
|
|
134
|
-
method: 'GET',
|
|
135
|
-
url: 'anyKey',
|
|
136
|
-
})
|
|
137
|
-
})
|
|
138
|
-
|
|
139
|
-
test('DEVUP_API_URL_MAP uses fallback when env var is empty string', async () => {
|
|
140
|
-
process.env.DEVUP_API_URL_MAP = ''
|
|
141
|
-
const urlMapModule = await import(
|
|
142
|
-
`../url-map?t=${Date.now() + Math.random()}`
|
|
143
|
-
)
|
|
144
|
-
// Directly access the constant to ensure the fallback path is covered
|
|
145
|
-
const urlMap = urlMapModule.DEVUP_API_URL_MAP
|
|
146
|
-
expect(urlMap).toEqual({})
|
|
147
|
-
expect(urlMapModule.getUrl('anyKey')).toBe('anyKey')
|
|
148
|
-
expect(urlMapModule.getUrlWithMethod('anyKey')).toEqual({
|
|
149
|
-
method: 'GET',
|
|
150
|
-
url: 'anyKey',
|
|
151
|
-
})
|
|
152
|
-
})
|
|
153
|
-
|
|
154
|
-
test('getUrl handles key where DEVUP_API_URL_MAP[key] exists but url is undefined', async () => {
|
|
155
|
-
const urlMapWithUndefinedUrl = {
|
|
156
|
-
testKey: { method: 'GET' as const },
|
|
157
|
-
}
|
|
158
|
-
process.env.DEVUP_API_URL_MAP = JSON.stringify(urlMapWithUndefinedUrl)
|
|
159
|
-
const { getUrl } = await import(`../url-map?t=${Date.now() + Math.random()}`)
|
|
160
|
-
// When url property is missing, optional chaining returns undefined, so key is returned
|
|
161
|
-
expect(getUrl('testKey')).toBe('testKey')
|
|
162
|
-
})
|
|
163
|
-
|
|
164
|
-
test('getUrl handles key where DEVUP_API_URL_MAP[key] exists but url is null', async () => {
|
|
165
|
-
const urlMapWithNullUrl = {
|
|
166
|
-
testKey: { method: 'GET' as const, url: null as unknown as string },
|
|
167
|
-
}
|
|
168
|
-
process.env.DEVUP_API_URL_MAP = JSON.stringify(urlMapWithNullUrl)
|
|
169
|
-
const { getUrl } = await import(`../url-map?t=${Date.now() + Math.random()}`)
|
|
170
|
-
// When url is null, optional chaining returns null, so key is returned
|
|
171
|
-
expect(getUrl('testKey')).toBe('testKey')
|
|
172
|
-
})
|
|
173
|
-
|
|
174
|
-
test('getUrl handles key where DEVUP_API_URL_MAP[key] exists but url is empty string', async () => {
|
|
175
|
-
const urlMapWithEmptyUrl = {
|
|
176
|
-
testKey: { method: 'GET' as const, url: '' },
|
|
177
|
-
}
|
|
178
|
-
process.env.DEVUP_API_URL_MAP = JSON.stringify(urlMapWithEmptyUrl)
|
|
179
|
-
const { getUrl } = await import(`../url-map?t=${Date.now() + Math.random()}`)
|
|
180
|
-
// When url is empty string, it's falsy, so key is returned
|
|
181
|
-
expect(getUrl('testKey')).toBe('testKey')
|
|
182
|
-
})
|
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
import { expect, test } from 'bun:test'
|
|
2
|
-
import { getApiEndpoint, isPlainObject } from '../utils'
|
|
3
|
-
|
|
4
|
-
test.each([
|
|
5
|
-
[{}, true],
|
|
6
|
-
[{ a: 1 }, true],
|
|
7
|
-
[{ a: 1, b: 'test' }, true],
|
|
8
|
-
[{ nested: { value: 1 } }, true],
|
|
9
|
-
])('returns true for plain objects: %s', (obj, expected) => {
|
|
10
|
-
expect(isPlainObject(obj)).toBe(expected)
|
|
11
|
-
})
|
|
12
|
-
|
|
13
|
-
test.each([
|
|
14
|
-
[null, false],
|
|
15
|
-
[undefined, false],
|
|
16
|
-
[[], false],
|
|
17
|
-
[[1, 2, 3], false],
|
|
18
|
-
[new Date(), false],
|
|
19
|
-
[/test/, false],
|
|
20
|
-
[new Map(), false],
|
|
21
|
-
[new Set(), false],
|
|
22
|
-
['string', false],
|
|
23
|
-
[123, false],
|
|
24
|
-
[true, false],
|
|
25
|
-
[false, false],
|
|
26
|
-
[() => {}, false],
|
|
27
|
-
[function test() {}, false],
|
|
28
|
-
])('returns false for non-plain objects: %s', (obj, expected) => {
|
|
29
|
-
expect(isPlainObject(obj)).toBe(expected)
|
|
30
|
-
})
|
|
31
|
-
|
|
32
|
-
test.each([
|
|
33
|
-
[Object.create(null), false, 'Object.create(null)'],
|
|
34
|
-
[Object.create(Object.prototype), true, 'Object.create(Object.prototype)'],
|
|
35
|
-
])('handles special object creation: %s', (obj, expected) => {
|
|
36
|
-
expect(isPlainObject(obj)).toBe(expected)
|
|
37
|
-
})
|
|
38
|
-
|
|
39
|
-
test.each([
|
|
40
|
-
[
|
|
41
|
-
(() => {
|
|
42
|
-
class TestClass {
|
|
43
|
-
value = 1
|
|
44
|
-
}
|
|
45
|
-
return new TestClass()
|
|
46
|
-
})(),
|
|
47
|
-
false,
|
|
48
|
-
'class instance',
|
|
49
|
-
],
|
|
50
|
-
[
|
|
51
|
-
(() => {
|
|
52
|
-
const proto = { customProp: 'value' }
|
|
53
|
-
return Object.create(proto)
|
|
54
|
-
})(),
|
|
55
|
-
false,
|
|
56
|
-
'object with custom prototype',
|
|
57
|
-
],
|
|
58
|
-
])('returns false for non-plain objects: %s', (obj, expected) => {
|
|
59
|
-
expect(isPlainObject(obj)).toBe(expected)
|
|
60
|
-
})
|
|
61
|
-
|
|
62
|
-
test.each([
|
|
63
|
-
[
|
|
64
|
-
'https://api.example.com',
|
|
65
|
-
'/users',
|
|
66
|
-
undefined,
|
|
67
|
-
'https://api.example.com/users',
|
|
68
|
-
],
|
|
69
|
-
['https://api.example.com', '/users', {}, 'https://api.example.com/users'],
|
|
70
|
-
[
|
|
71
|
-
'https://api.example.com',
|
|
72
|
-
'/users/{id}',
|
|
73
|
-
{ id: '123' },
|
|
74
|
-
'https://api.example.com/users/123',
|
|
75
|
-
],
|
|
76
|
-
[
|
|
77
|
-
'https://api.example.com',
|
|
78
|
-
'/users/{userId}/posts/{postId}',
|
|
79
|
-
{ userId: '123', postId: '456' },
|
|
80
|
-
'https://api.example.com/users/123/posts/456',
|
|
81
|
-
],
|
|
82
|
-
[
|
|
83
|
-
'https://api.example.com',
|
|
84
|
-
'/users/{id}',
|
|
85
|
-
{ id: '123', name: 'test' },
|
|
86
|
-
'https://api.example.com/users/123',
|
|
87
|
-
],
|
|
88
|
-
[
|
|
89
|
-
'https://api.example.com',
|
|
90
|
-
'/users',
|
|
91
|
-
{ id: '123' },
|
|
92
|
-
'https://api.example.com/users',
|
|
93
|
-
],
|
|
94
|
-
[
|
|
95
|
-
'http://localhost:3000',
|
|
96
|
-
'/api/v1/users/{id}',
|
|
97
|
-
{ id: '999' },
|
|
98
|
-
'http://localhost:3000/api/v1/users/999',
|
|
99
|
-
],
|
|
100
|
-
[
|
|
101
|
-
'https://api.example.com',
|
|
102
|
-
'/users/{id}/profile',
|
|
103
|
-
{ id: '123' },
|
|
104
|
-
'https://api.example.com/users/123/profile',
|
|
105
|
-
],
|
|
106
|
-
])('getApiEndpoint: baseUrl=%s, path=%s, params=%s -> %s', (baseUrl, path, params, expected) => {
|
|
107
|
-
expect(getApiEndpoint(baseUrl, path, params)).toBe(expected)
|
|
108
|
-
})
|
package/src/api.ts
DELETED
|
@@ -1,303 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
Additional,
|
|
3
|
-
DevupApiRequestInit,
|
|
4
|
-
DevupApiStruct,
|
|
5
|
-
DevupApiStructKey,
|
|
6
|
-
DevupDeleteApiStruct,
|
|
7
|
-
DevupDeleteApiStructKey,
|
|
8
|
-
DevupGetApiStruct,
|
|
9
|
-
DevupGetApiStructKey,
|
|
10
|
-
DevupPatchApiStruct,
|
|
11
|
-
DevupPatchApiStructKey,
|
|
12
|
-
DevupPostApiStruct,
|
|
13
|
-
DevupPostApiStructKey,
|
|
14
|
-
DevupPutApiStruct,
|
|
15
|
-
DevupPutApiStructKey,
|
|
16
|
-
ExtractValue,
|
|
17
|
-
RequiredOptions,
|
|
18
|
-
} from '@devup-api/core'
|
|
19
|
-
import { convertResponse } from './response-converter'
|
|
20
|
-
import { getUrlWithMethod } from './url-map'
|
|
21
|
-
import { getApiEndpoint, isPlainObject } from './utils'
|
|
22
|
-
|
|
23
|
-
type DevupApiResponse<T, E = unknown> =
|
|
24
|
-
| {
|
|
25
|
-
data: T
|
|
26
|
-
error?: undefined
|
|
27
|
-
response: Response
|
|
28
|
-
}
|
|
29
|
-
| {
|
|
30
|
-
data?: undefined
|
|
31
|
-
error: E
|
|
32
|
-
response: Response
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export class DevupApi {
|
|
36
|
-
private baseUrl: string
|
|
37
|
-
private defaultOptions: DevupApiRequestInit
|
|
38
|
-
|
|
39
|
-
constructor(baseUrl: string, defaultOptions: DevupApiRequestInit = {}) {
|
|
40
|
-
this.baseUrl = baseUrl.replace(/\/$/, '')
|
|
41
|
-
this.defaultOptions = defaultOptions
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
get<
|
|
45
|
-
T extends DevupGetApiStructKey,
|
|
46
|
-
O extends Additional<T, DevupGetApiStruct>,
|
|
47
|
-
>(
|
|
48
|
-
path: T,
|
|
49
|
-
...options: [RequiredOptions<O>] extends [never]
|
|
50
|
-
? [options?: DevupApiRequestInit]
|
|
51
|
-
: [options: DevupApiRequestInit & Omit<O, 'response' | 'error'>]
|
|
52
|
-
): Promise<
|
|
53
|
-
DevupApiResponse<
|
|
54
|
-
ExtractValue<O, 'response'>,
|
|
55
|
-
ExtractValue<O, 'error', unknown>
|
|
56
|
-
>
|
|
57
|
-
> {
|
|
58
|
-
return this.request(path, {
|
|
59
|
-
method: 'GET',
|
|
60
|
-
...options[0],
|
|
61
|
-
} as DevupApiRequestInit & Omit<O, 'response'>)
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
GET<
|
|
65
|
-
T extends DevupGetApiStructKey,
|
|
66
|
-
O extends Additional<T, DevupGetApiStruct>,
|
|
67
|
-
>(
|
|
68
|
-
path: T,
|
|
69
|
-
...options: [RequiredOptions<O>] extends [never]
|
|
70
|
-
? [options?: DevupApiRequestInit]
|
|
71
|
-
: [options: DevupApiRequestInit & Omit<O, 'response'>]
|
|
72
|
-
): Promise<
|
|
73
|
-
DevupApiResponse<
|
|
74
|
-
ExtractValue<O, 'response'>,
|
|
75
|
-
ExtractValue<O, 'error', unknown>
|
|
76
|
-
>
|
|
77
|
-
> {
|
|
78
|
-
return this.request(path, {
|
|
79
|
-
method: 'GET',
|
|
80
|
-
...options[0],
|
|
81
|
-
} as DevupApiRequestInit & Omit<O, 'response'>)
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
post<
|
|
85
|
-
T extends DevupPostApiStructKey,
|
|
86
|
-
O extends Additional<T, DevupPostApiStruct>,
|
|
87
|
-
>(
|
|
88
|
-
path: T,
|
|
89
|
-
...options: [RequiredOptions<O>] extends [never]
|
|
90
|
-
? [options?: DevupApiRequestInit]
|
|
91
|
-
: [options: DevupApiRequestInit & Omit<O, 'response'>]
|
|
92
|
-
): Promise<
|
|
93
|
-
DevupApiResponse<
|
|
94
|
-
ExtractValue<O, 'response'>,
|
|
95
|
-
ExtractValue<O, 'error', unknown>
|
|
96
|
-
>
|
|
97
|
-
> {
|
|
98
|
-
return this.request(path, {
|
|
99
|
-
method: 'POST',
|
|
100
|
-
...options[0],
|
|
101
|
-
} as DevupApiRequestInit & Omit<O, 'response'>)
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
POST<
|
|
105
|
-
T extends DevupPostApiStructKey,
|
|
106
|
-
O extends Additional<T, DevupPostApiStruct>,
|
|
107
|
-
>(
|
|
108
|
-
path: T,
|
|
109
|
-
...options: [RequiredOptions<O>] extends [never]
|
|
110
|
-
? [options?: DevupApiRequestInit]
|
|
111
|
-
: [options: DevupApiRequestInit & Omit<O, 'response'>]
|
|
112
|
-
): Promise<
|
|
113
|
-
DevupApiResponse<
|
|
114
|
-
ExtractValue<O, 'response'>,
|
|
115
|
-
ExtractValue<O, 'error', unknown>
|
|
116
|
-
>
|
|
117
|
-
> {
|
|
118
|
-
return this.request(path, {
|
|
119
|
-
method: 'POST',
|
|
120
|
-
...options[0],
|
|
121
|
-
} as DevupApiRequestInit & Omit<O, 'response'>)
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
put<
|
|
125
|
-
T extends DevupPutApiStructKey,
|
|
126
|
-
O extends Additional<T, DevupPutApiStruct>,
|
|
127
|
-
>(
|
|
128
|
-
path: T,
|
|
129
|
-
...options: [RequiredOptions<O>] extends [never]
|
|
130
|
-
? [options?: DevupApiRequestInit]
|
|
131
|
-
: [options: DevupApiRequestInit & Omit<O, 'response'>]
|
|
132
|
-
): Promise<
|
|
133
|
-
DevupApiResponse<
|
|
134
|
-
ExtractValue<O, 'response'>,
|
|
135
|
-
ExtractValue<O, 'error', unknown>
|
|
136
|
-
>
|
|
137
|
-
> {
|
|
138
|
-
return this.request(path, {
|
|
139
|
-
method: 'PUT',
|
|
140
|
-
...options[0],
|
|
141
|
-
} as DevupApiRequestInit & Omit<O, 'response'>)
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
PUT<
|
|
145
|
-
T extends DevupPutApiStructKey,
|
|
146
|
-
O extends Additional<T, DevupPutApiStruct>,
|
|
147
|
-
>(
|
|
148
|
-
path: T,
|
|
149
|
-
...options: [RequiredOptions<O>] extends [never]
|
|
150
|
-
? [options?: DevupApiRequestInit]
|
|
151
|
-
: [options: DevupApiRequestInit & Omit<O, 'response'>]
|
|
152
|
-
): Promise<
|
|
153
|
-
DevupApiResponse<
|
|
154
|
-
ExtractValue<O, 'response'>,
|
|
155
|
-
ExtractValue<O, 'error', unknown>
|
|
156
|
-
>
|
|
157
|
-
> {
|
|
158
|
-
return this.request(path, {
|
|
159
|
-
method: 'PUT',
|
|
160
|
-
...options[0],
|
|
161
|
-
} as DevupApiRequestInit & Omit<O, 'response'>)
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
delete<
|
|
165
|
-
T extends DevupDeleteApiStructKey,
|
|
166
|
-
O extends Additional<T, DevupDeleteApiStruct>,
|
|
167
|
-
>(
|
|
168
|
-
path: T,
|
|
169
|
-
...options: [RequiredOptions<O>] extends [never]
|
|
170
|
-
? [options?: DevupApiRequestInit]
|
|
171
|
-
: [options: DevupApiRequestInit & Omit<O, 'response'>]
|
|
172
|
-
): Promise<
|
|
173
|
-
DevupApiResponse<
|
|
174
|
-
ExtractValue<O, 'response'>,
|
|
175
|
-
ExtractValue<O, 'error', unknown>
|
|
176
|
-
>
|
|
177
|
-
> {
|
|
178
|
-
return this.request(path, {
|
|
179
|
-
method: 'DELETE',
|
|
180
|
-
...options[0],
|
|
181
|
-
} as DevupApiRequestInit & Omit<O, 'response'>)
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
DELETE<
|
|
185
|
-
T extends DevupDeleteApiStructKey,
|
|
186
|
-
O extends Additional<T, DevupDeleteApiStruct>,
|
|
187
|
-
>(
|
|
188
|
-
path: T,
|
|
189
|
-
...options: [RequiredOptions<O>] extends [never]
|
|
190
|
-
? [options?: DevupApiRequestInit]
|
|
191
|
-
: [options: DevupApiRequestInit & Omit<O, 'response'>]
|
|
192
|
-
): Promise<
|
|
193
|
-
DevupApiResponse<
|
|
194
|
-
ExtractValue<O, 'response'>,
|
|
195
|
-
ExtractValue<O, 'error', unknown>
|
|
196
|
-
>
|
|
197
|
-
> {
|
|
198
|
-
return this.request(path, {
|
|
199
|
-
method: 'DELETE',
|
|
200
|
-
...options[0],
|
|
201
|
-
} as DevupApiRequestInit & Omit<O, 'response'>)
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
patch<
|
|
205
|
-
T extends DevupPatchApiStructKey,
|
|
206
|
-
O extends Additional<T, DevupPatchApiStruct>,
|
|
207
|
-
>(
|
|
208
|
-
path: T,
|
|
209
|
-
...options: [RequiredOptions<O>] extends [never]
|
|
210
|
-
? [options?: DevupApiRequestInit]
|
|
211
|
-
: [options: DevupApiRequestInit & Omit<O, 'response'>]
|
|
212
|
-
): Promise<
|
|
213
|
-
DevupApiResponse<
|
|
214
|
-
ExtractValue<O, 'response'>,
|
|
215
|
-
ExtractValue<O, 'error', unknown>
|
|
216
|
-
>
|
|
217
|
-
> {
|
|
218
|
-
return this.request(path, {
|
|
219
|
-
method: 'PATCH',
|
|
220
|
-
...options[0],
|
|
221
|
-
} as DevupApiRequestInit & Omit<O, 'response'>)
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
PATCH<
|
|
225
|
-
T extends DevupPatchApiStructKey,
|
|
226
|
-
O extends Additional<T, DevupPatchApiStruct>,
|
|
227
|
-
>(
|
|
228
|
-
path: T,
|
|
229
|
-
...options: [RequiredOptions<O>] extends [never]
|
|
230
|
-
? [options?: DevupApiRequestInit]
|
|
231
|
-
: [options: DevupApiRequestInit & Omit<O, 'response'>]
|
|
232
|
-
): Promise<
|
|
233
|
-
DevupApiResponse<
|
|
234
|
-
ExtractValue<O, 'response'>,
|
|
235
|
-
ExtractValue<O, 'error', unknown>
|
|
236
|
-
>
|
|
237
|
-
> {
|
|
238
|
-
return this.request(path, {
|
|
239
|
-
method: 'PATCH',
|
|
240
|
-
...options[0],
|
|
241
|
-
} as DevupApiRequestInit & Omit<O, 'response'>)
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
request<T extends DevupApiStructKey, O extends Additional<T, DevupApiStruct>>(
|
|
245
|
-
path: T,
|
|
246
|
-
...options: [RequiredOptions<O>] extends [never]
|
|
247
|
-
? [options?: DevupApiRequestInit]
|
|
248
|
-
: [options: DevupApiRequestInit & Omit<O, 'response'>]
|
|
249
|
-
): Promise<
|
|
250
|
-
DevupApiResponse<
|
|
251
|
-
ExtractValue<O, 'response'>,
|
|
252
|
-
ExtractValue<O, 'error', unknown>
|
|
253
|
-
>
|
|
254
|
-
> {
|
|
255
|
-
const { method, url } = getUrlWithMethod(path)
|
|
256
|
-
const mergedOptions = {
|
|
257
|
-
...this.defaultOptions,
|
|
258
|
-
...options[0],
|
|
259
|
-
}
|
|
260
|
-
const requestOptions = {
|
|
261
|
-
...mergedOptions,
|
|
262
|
-
method: mergedOptions.method || method,
|
|
263
|
-
}
|
|
264
|
-
if (requestOptions.body && isPlainObject(requestOptions.body)) {
|
|
265
|
-
requestOptions.body = JSON.stringify(requestOptions.body)
|
|
266
|
-
}
|
|
267
|
-
const request = new Request(
|
|
268
|
-
getApiEndpoint(
|
|
269
|
-
this.baseUrl,
|
|
270
|
-
url,
|
|
271
|
-
(
|
|
272
|
-
requestOptions as {
|
|
273
|
-
params?: Record<
|
|
274
|
-
string,
|
|
275
|
-
string | number | boolean | null | undefined
|
|
276
|
-
>
|
|
277
|
-
}
|
|
278
|
-
).params,
|
|
279
|
-
),
|
|
280
|
-
requestOptions as RequestInit,
|
|
281
|
-
)
|
|
282
|
-
return fetch(request).then((response) =>
|
|
283
|
-
convertResponse(request, response),
|
|
284
|
-
) as Promise<
|
|
285
|
-
DevupApiResponse<
|
|
286
|
-
ExtractValue<O, 'response'>,
|
|
287
|
-
ExtractValue<O, 'error', unknown>
|
|
288
|
-
>
|
|
289
|
-
>
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
setDefaultOptions(options: DevupApiRequestInit) {
|
|
293
|
-
this.defaultOptions = options
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
getBaseUrl() {
|
|
297
|
-
return this.baseUrl
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
getDefaultOptions() {
|
|
301
|
-
return this.defaultOptions
|
|
302
|
-
}
|
|
303
|
-
}
|
package/src/create-api.ts
DELETED
package/src/index.ts
DELETED
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* OPENAPI-TYPESCRIPT
|
|
3
|
-
* @param request
|
|
4
|
-
* @param response
|
|
5
|
-
* @param parseAs
|
|
6
|
-
* @returns
|
|
7
|
-
*/
|
|
8
|
-
export async function convertResponse(
|
|
9
|
-
request: Request,
|
|
10
|
-
response: Response,
|
|
11
|
-
parseAs: 'stream' | 'json' | 'text' = 'json',
|
|
12
|
-
): Promise<{
|
|
13
|
-
data?: unknown | undefined
|
|
14
|
-
error?: unknown | undefined
|
|
15
|
-
response: Response
|
|
16
|
-
}> {
|
|
17
|
-
if (
|
|
18
|
-
response.status === 204 ||
|
|
19
|
-
request.method === 'HEAD' ||
|
|
20
|
-
response.headers.get('Content-Length') === '0'
|
|
21
|
-
) {
|
|
22
|
-
return response.ok
|
|
23
|
-
? { data: undefined, response }
|
|
24
|
-
: { error: undefined, response }
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
// parse response (falling back to .text() when necessary)
|
|
28
|
-
if (response.ok) {
|
|
29
|
-
// if "stream", skip parsing entirely
|
|
30
|
-
if (parseAs === 'stream') {
|
|
31
|
-
return { data: response.body, response }
|
|
32
|
-
}
|
|
33
|
-
return { data: await response[parseAs](), response }
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// handle errors
|
|
37
|
-
let error = await response.text()
|
|
38
|
-
try {
|
|
39
|
-
error = JSON.parse(error) // attempt to parse as JSON
|
|
40
|
-
} catch {
|
|
41
|
-
// noop
|
|
42
|
-
}
|
|
43
|
-
return { error, response }
|
|
44
|
-
}
|
package/src/url-map.ts
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import type { UrlMapValue } from '@devup-api/core'
|
|
2
|
-
|
|
3
|
-
export const DEVUP_API_URL_MAP: Record<string, UrlMapValue> = JSON.parse(
|
|
4
|
-
process.env.DEVUP_API_URL_MAP || '{}',
|
|
5
|
-
)
|
|
6
|
-
|
|
7
|
-
export function getUrl(key: string): string {
|
|
8
|
-
return DEVUP_API_URL_MAP[key]?.url || key
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export function getUrlWithMethod(key: string): UrlMapValue {
|
|
12
|
-
return DEVUP_API_URL_MAP[key] || { method: 'GET', url: key }
|
|
13
|
-
}
|
package/src/utils.ts
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
export function isPlainObject(obj: unknown): obj is object {
|
|
2
|
-
if (obj === null || typeof obj !== 'object') return false
|
|
3
|
-
|
|
4
|
-
const proto = Object.getPrototypeOf(obj)
|
|
5
|
-
return proto === Object.prototype
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export function getApiEndpoint(
|
|
9
|
-
baseUrl: string,
|
|
10
|
-
path: string,
|
|
11
|
-
params?: object,
|
|
12
|
-
): string {
|
|
13
|
-
let ret = `${baseUrl}${path}`
|
|
14
|
-
for (const [key, value] of Object.entries(params ?? {})) {
|
|
15
|
-
ret = ret.replace(`{${key}}`, value)
|
|
16
|
-
}
|
|
17
|
-
return ret
|
|
18
|
-
}
|
package/tsconfig.json
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
// Environment setup & latest features
|
|
4
|
-
"lib": ["ESNext"],
|
|
5
|
-
"target": "ESNext",
|
|
6
|
-
"module": "Preserve",
|
|
7
|
-
"moduleDetection": "force",
|
|
8
|
-
"jsx": "react-jsx",
|
|
9
|
-
"allowJs": true,
|
|
10
|
-
|
|
11
|
-
// Bundler mode
|
|
12
|
-
"moduleResolution": "bundler",
|
|
13
|
-
"verbatimModuleSyntax": true,
|
|
14
|
-
"emitDeclarationOnly": true,
|
|
15
|
-
|
|
16
|
-
// Best practices
|
|
17
|
-
"strict": true,
|
|
18
|
-
"skipLibCheck": true,
|
|
19
|
-
"noFallthroughCasesInSwitch": true,
|
|
20
|
-
"noUncheckedIndexedAccess": true,
|
|
21
|
-
"noImplicitOverride": true,
|
|
22
|
-
|
|
23
|
-
// Some stricter flags (disabled by default)
|
|
24
|
-
"noUnusedLocals": false,
|
|
25
|
-
"noUnusedParameters": false,
|
|
26
|
-
"noPropertyAccessFromIndexSignature": false,
|
|
27
|
-
"declaration": true,
|
|
28
|
-
"declarationMap": true,
|
|
29
|
-
"outDir": "dist",
|
|
30
|
-
"rootDir": "src"
|
|
31
|
-
},
|
|
32
|
-
"include": ["src/**/*.ts"],
|
|
33
|
-
"exclude": ["dist", "node_modules"]
|
|
34
|
-
}
|