@devup-api/fetch 0.1.0
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/README.md +24 -0
- package/dist/__tests__/api.test.d.ts +2 -0
- package/dist/__tests__/api.test.d.ts.map +1 -0
- package/dist/__tests__/create-api.test.d.ts +2 -0
- package/dist/__tests__/create-api.test.d.ts.map +1 -0
- package/dist/__tests__/index.test.d.ts +2 -0
- package/dist/__tests__/index.test.d.ts.map +1 -0
- package/dist/__tests__/response-converter.test.d.ts +2 -0
- package/dist/__tests__/response-converter.test.d.ts.map +1 -0
- package/dist/__tests__/url-map.test.d.ts +2 -0
- package/dist/__tests__/url-map.test.d.ts.map +1 -0
- package/dist/__tests__/utils.test.d.ts +2 -0
- package/dist/__tests__/utils.test.d.ts.map +1 -0
- package/dist/api.d.ts +31 -0
- package/dist/api.d.ts.map +1 -0
- package/dist/create-api.d.ts +3 -0
- package/dist/create-api.d.ts.map +1 -0
- package/dist/index.cjs +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1 -0
- package/dist/response-converter.d.ts +13 -0
- package/dist/response-converter.d.ts.map +1 -0
- package/dist/url-map.d.ts +5 -0
- package/dist/url-map.d.ts.map +1 -0
- package/dist/utils.d.ts +3 -0
- package/dist/utils.d.ts.map +1 -0
- package/package.json +25 -0
- package/src/__tests__/api.test.ts +245 -0
- package/src/__tests__/create-api.test.ts +25 -0
- package/src/__tests__/index.test.ts +10 -0
- package/src/__tests__/response-converter.test.ts +158 -0
- package/src/__tests__/url-map.test.ts +182 -0
- package/src/__tests__/utils.test.ts +108 -0
- package/src/api.ts +303 -0
- package/src/create-api.ts +8 -0
- package/src/index.ts +2 -0
- package/src/response-converter.ts +44 -0
- package/src/url-map.ts +13 -0
- package/src/utils.ts +18 -0
- package/tsconfig.json +34 -0
|
@@ -0,0 +1,182 @@
|
|
|
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
|
+
})
|
|
@@ -0,0 +1,108 @@
|
|
|
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
ADDED
|
@@ -0,0 +1,303 @@
|
|
|
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/index.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
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
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
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
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
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
|
+
}
|