@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 CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "@devup-api/fetch",
3
- "version": "0.1.0",
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.0"
23
+ "@devup-api/core": "0.1.2"
20
24
  },
21
25
  "devDependencies": {
22
26
  "@types/node": "^24.10",
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=api.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"api.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/api.test.ts"],"names":[],"mappings":""}
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=create-api.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"create-api.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/create-api.test.ts"],"names":[],"mappings":""}
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=index.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/index.test.ts"],"names":[],"mappings":""}
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=response-converter.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"response-converter.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/response-converter.test.ts"],"names":[],"mappings":""}
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=url-map.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"url-map.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/url-map.test.ts"],"names":[],"mappings":""}
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=utils.test.d.ts.map
@@ -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
@@ -1,8 +0,0 @@
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 DELETED
@@ -1,2 +0,0 @@
1
- export * from '@devup-api/core'
2
- export { createApi } from './create-api'
@@ -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
- }