@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.
Files changed (41) hide show
  1. package/README.md +24 -0
  2. package/dist/__tests__/api.test.d.ts +2 -0
  3. package/dist/__tests__/api.test.d.ts.map +1 -0
  4. package/dist/__tests__/create-api.test.d.ts +2 -0
  5. package/dist/__tests__/create-api.test.d.ts.map +1 -0
  6. package/dist/__tests__/index.test.d.ts +2 -0
  7. package/dist/__tests__/index.test.d.ts.map +1 -0
  8. package/dist/__tests__/response-converter.test.d.ts +2 -0
  9. package/dist/__tests__/response-converter.test.d.ts.map +1 -0
  10. package/dist/__tests__/url-map.test.d.ts +2 -0
  11. package/dist/__tests__/url-map.test.d.ts.map +1 -0
  12. package/dist/__tests__/utils.test.d.ts +2 -0
  13. package/dist/__tests__/utils.test.d.ts.map +1 -0
  14. package/dist/api.d.ts +31 -0
  15. package/dist/api.d.ts.map +1 -0
  16. package/dist/create-api.d.ts +3 -0
  17. package/dist/create-api.d.ts.map +1 -0
  18. package/dist/index.cjs +1 -0
  19. package/dist/index.d.ts +3 -0
  20. package/dist/index.d.ts.map +1 -0
  21. package/dist/index.js +1 -0
  22. package/dist/response-converter.d.ts +13 -0
  23. package/dist/response-converter.d.ts.map +1 -0
  24. package/dist/url-map.d.ts +5 -0
  25. package/dist/url-map.d.ts.map +1 -0
  26. package/dist/utils.d.ts +3 -0
  27. package/dist/utils.d.ts.map +1 -0
  28. package/package.json +25 -0
  29. package/src/__tests__/api.test.ts +245 -0
  30. package/src/__tests__/create-api.test.ts +25 -0
  31. package/src/__tests__/index.test.ts +10 -0
  32. package/src/__tests__/response-converter.test.ts +158 -0
  33. package/src/__tests__/url-map.test.ts +182 -0
  34. package/src/__tests__/utils.test.ts +108 -0
  35. package/src/api.ts +303 -0
  36. package/src/create-api.ts +8 -0
  37. package/src/index.ts +2 -0
  38. package/src/response-converter.ts +44 -0
  39. package/src/url-map.ts +13 -0
  40. package/src/utils.ts +18 -0
  41. 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
+ }
@@ -0,0 +1,8 @@
1
+ import { DevupApi } from './api'
2
+
3
+ export function createApi(
4
+ baseUrl: string,
5
+ defaultOptions?: RequestInit,
6
+ ): DevupApi {
7
+ return new DevupApi(baseUrl, defaultOptions)
8
+ }
package/src/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ export * from '@devup-api/core'
2
+ export { createApi } from './create-api'
@@ -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
+ }