@marianmeres/http-utils 1.0.1 → 1.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 +51 -1
- package/package.json +2 -2
- package/src/api.ts +11 -2
- package/src/status.ts +2 -2
- package/tests/api.test.js +29 -3
package/README.md
CHANGED
|
@@ -1 +1,51 @@
|
|
|
1
|
-
# http-utils
|
|
1
|
+
# @marianmeres/http-utils
|
|
2
|
+
|
|
3
|
+
A few [sweet](https://en.wikipedia.org/wiki/Syntactic_sugar) `fetch` helpers.
|
|
4
|
+
|
|
5
|
+
## Example
|
|
6
|
+
|
|
7
|
+
```javascript
|
|
8
|
+
import { HTTP_ERROR, HTTP_STATUS, createHttpApi } from '@marianmeres/http-utils';
|
|
9
|
+
|
|
10
|
+
// create api helper
|
|
11
|
+
const api = createHttpApi(
|
|
12
|
+
// optional base url
|
|
13
|
+
'https://api.example.com',
|
|
14
|
+
// optional lazy evaluated default fetch params (can be overridden per call)
|
|
15
|
+
async () => ({
|
|
16
|
+
token: await getApiTokenFromDb() // example
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
// EXAMPLE: assuming `/resource` returns json {"some":"data"}
|
|
20
|
+
const r = await api.get('/resource');
|
|
21
|
+
assert(r.some === 'data');
|
|
22
|
+
|
|
23
|
+
// EXAMPLE: assuming `/foo` returns 404 header and json {"message":"hey"}
|
|
24
|
+
// by default always throws
|
|
25
|
+
try {
|
|
26
|
+
const r = await api.get('/foo');
|
|
27
|
+
} catch (e) {
|
|
28
|
+
// see HTTP_ERROR for more
|
|
29
|
+
assert(e instanceof HTTP_ERROR.NotFound);
|
|
30
|
+
assert(e.toString() === 'HttpNotFoundError: Not Found');
|
|
31
|
+
assert(e.status === HTTP_STATUS.ERROR_CLIENT.NOT_FOUND.CODE);
|
|
32
|
+
assert(e.statusText === HTTP_STATUS.ERROR_CLIENT.NOT_FOUND.TEXT);
|
|
33
|
+
assert(e.body.message === 'hey');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// EXAMPLE: assuming `/foo` returns 404 header and json {"message":"hey"}
|
|
37
|
+
// will not throw if we pass false flag
|
|
38
|
+
const r = await api.get('/foo', { assert: false });
|
|
39
|
+
assert(r.message === 'hey');
|
|
40
|
+
|
|
41
|
+
// EXAMPLE: assuming POST to `/resource` returns OK and json {"message":"created"}
|
|
42
|
+
// the provided token below will override the one from the `getApiTokenFromDb()` call above
|
|
43
|
+
const r = await api.post('/resource', { some: 'data' }, { token: 'my-api-token' });
|
|
44
|
+
assert(r.message === 'created');
|
|
45
|
+
|
|
46
|
+
// EXAMPLE: raw Response
|
|
47
|
+
const r = await api.get('/resource', { raw: true });
|
|
48
|
+
assert(r instanceof Response);
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
See [`HTTP_STATUS`](./src/status.ts) and [`HTTP_ERROR`](./src/error.ts) for more.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@marianmeres/http-utils",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Misc DRY http fetch related helpers",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"scripts": {
|
|
13
13
|
"clean": "rimraf dist/*",
|
|
14
14
|
"prettier": "prettier --write \"{src,tests}/**/*.{js,ts,json}\"",
|
|
15
|
-
"release": "release",
|
|
15
|
+
"release": "release -v minor",
|
|
16
16
|
"release:patch": "release -v patch",
|
|
17
17
|
"test": "test-runner",
|
|
18
18
|
"build": "npm run clean && rollup -c"
|
package/src/api.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { dset } from 'dset/merge';
|
|
|
2
2
|
import { createHttpError } from './error.js';
|
|
3
3
|
|
|
4
4
|
// this is all very opinionated and may not be useful for every use case...
|
|
5
|
-
// there is no magic added over plain fetch calls
|
|
5
|
+
// there is no magic added over plain fetch calls, just more opinionated and dry api
|
|
6
6
|
|
|
7
7
|
interface BaseParams {
|
|
8
8
|
method: 'GET' | 'POST' | 'PATCH' | 'DELETE' | 'PUT';
|
|
@@ -16,6 +16,7 @@ interface FetchParams {
|
|
|
16
16
|
signal?: any;
|
|
17
17
|
credentials?: null | 'omit' | 'same-origin' | 'include';
|
|
18
18
|
raw?: null | boolean;
|
|
19
|
+
assert?: null | boolean;
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
type BaseFetchParams = BaseParams & FetchParams; // Exclude<Drinks, Soda> |
|
|
@@ -84,7 +85,9 @@ const _fetch = async (
|
|
|
84
85
|
// prettier-ignore
|
|
85
86
|
try { body = JSON.parse(body); } catch (e) {}
|
|
86
87
|
|
|
87
|
-
|
|
88
|
+
params.assert ??= true; // default is true
|
|
89
|
+
|
|
90
|
+
if (!r.ok && params.assert) {
|
|
88
91
|
throw createHttpError(r.status, null, body);
|
|
89
92
|
}
|
|
90
93
|
|
|
@@ -92,6 +95,7 @@ const _fetch = async (
|
|
|
92
95
|
};
|
|
93
96
|
|
|
94
97
|
export const createHttpApi = (
|
|
98
|
+
base: string | null,
|
|
95
99
|
defaults?: Partial<BaseFetchParams> | (() => Promise<Partial<BaseFetchParams>>)
|
|
96
100
|
) => {
|
|
97
101
|
const _merge = (a: any, b: any): any => {
|
|
@@ -118,6 +122,7 @@ export const createHttpApi = (
|
|
|
118
122
|
respHeaders = null,
|
|
119
123
|
_dumpParams = false
|
|
120
124
|
) {
|
|
125
|
+
path = `${base || ''}` + path;
|
|
121
126
|
return _fetch(
|
|
122
127
|
_merge(await _getDefs(), { ...params, method: 'GET', path }),
|
|
123
128
|
respHeaders,
|
|
@@ -133,6 +138,7 @@ export const createHttpApi = (
|
|
|
133
138
|
respHeaders = null,
|
|
134
139
|
_dumpParams = false
|
|
135
140
|
) {
|
|
141
|
+
path = `${base || ''}` + path;
|
|
136
142
|
return _fetch(
|
|
137
143
|
_merge(await _getDefs(), { ...(params || {}), data, method: 'POST', path }),
|
|
138
144
|
respHeaders,
|
|
@@ -148,6 +154,7 @@ export const createHttpApi = (
|
|
|
148
154
|
respHeaders = null,
|
|
149
155
|
_dumpParams = false
|
|
150
156
|
) {
|
|
157
|
+
path = `${base || ''}` + path;
|
|
151
158
|
return _fetch(
|
|
152
159
|
_merge(await _getDefs(), { ...(params || {}), data, method: 'PUT', path }),
|
|
153
160
|
respHeaders,
|
|
@@ -163,6 +170,7 @@ export const createHttpApi = (
|
|
|
163
170
|
respHeaders = null,
|
|
164
171
|
_dumpParams = false
|
|
165
172
|
) {
|
|
173
|
+
path = `${base || ''}` + path;
|
|
166
174
|
return _fetch(
|
|
167
175
|
_merge(await _getDefs(), { ...(params || {}), data, method: 'PATCH', path }),
|
|
168
176
|
respHeaders,
|
|
@@ -179,6 +187,7 @@ export const createHttpApi = (
|
|
|
179
187
|
respHeaders = null,
|
|
180
188
|
_dumpParams = false
|
|
181
189
|
) {
|
|
190
|
+
path = `${base || ''}` + path;
|
|
182
191
|
return _fetch(
|
|
183
192
|
_merge(await _getDefs(), { ...(params || {}), data, method: 'DELETE', path }),
|
|
184
193
|
respHeaders,
|
package/src/status.ts
CHANGED
|
@@ -2,7 +2,7 @@ export class HTTP_STATUS {
|
|
|
2
2
|
// 1xx
|
|
3
3
|
// prettier-ignore
|
|
4
4
|
static readonly INFO = {
|
|
5
|
-
|
|
5
|
+
CONTINUE: { CODE: 100, TEXT: 'Continue' },
|
|
6
6
|
SWITCHING_PROTOCOLS: { CODE: 101, TEXT: 'Switching Protocols' },
|
|
7
7
|
PROCESSING: { CODE: 102, TEXT: 'Processing' },
|
|
8
8
|
EARLY_HINTS: { CODE: 103, TEXT: 'Early Hints' },
|
|
@@ -11,7 +11,7 @@ export class HTTP_STATUS {
|
|
|
11
11
|
// 2xx
|
|
12
12
|
// prettier-ignore
|
|
13
13
|
static readonly SUCCESS = {
|
|
14
|
-
|
|
14
|
+
OK: { CODE: 200, TEXT: 'OK' },
|
|
15
15
|
NON_AUTHORITATIVE_INFO: { CODE: 203, TEXT: 'Non-Authoritative Information' },
|
|
16
16
|
ACCEPTED: { CODE: 202, TEXT: 'Accepted' },
|
|
17
17
|
NO_CONTENT: { CODE: 204, TEXT: 'No Content' },
|
package/tests/api.test.js
CHANGED
|
@@ -20,7 +20,7 @@ const collectBody = async (request) =>
|
|
|
20
20
|
});
|
|
21
21
|
|
|
22
22
|
const suite = new TestRunner(path.basename(fileURLToPath(import.meta.url)), {
|
|
23
|
-
|
|
23
|
+
beforeEach: async () => {
|
|
24
24
|
server = createServer(async (req, res) => {
|
|
25
25
|
res.setHeader('Content-Type', 'application/json');
|
|
26
26
|
if (req.url === '/echo') {
|
|
@@ -37,7 +37,7 @@ const suite = new TestRunner(path.basename(fileURLToPath(import.meta.url)), {
|
|
|
37
37
|
});
|
|
38
38
|
return new Promise((resolve) => server.listen(port, hostname, resolve));
|
|
39
39
|
},
|
|
40
|
-
|
|
40
|
+
afterEach: async () => new Promise((resolve) => server.close(resolve)),
|
|
41
41
|
});
|
|
42
42
|
|
|
43
43
|
suite.test('createHttpApi GET', async () => {
|
|
@@ -49,6 +49,15 @@ suite.test('createHttpApi GET', async () => {
|
|
|
49
49
|
assert(respHeaders.__http_status_code__ === 200);
|
|
50
50
|
});
|
|
51
51
|
|
|
52
|
+
suite.test('createHttpApi base option', async () => {
|
|
53
|
+
let api = createHttpApi(url);
|
|
54
|
+
let respHeaders = {};
|
|
55
|
+
|
|
56
|
+
let r = await api.get(`/echo`, {}, respHeaders);
|
|
57
|
+
assert(r.foo === 'bar');
|
|
58
|
+
assert(respHeaders.__http_status_code__ === 200);
|
|
59
|
+
});
|
|
60
|
+
|
|
52
61
|
suite.test('createHttpApi RAW', async () => {
|
|
53
62
|
let api = createHttpApi();
|
|
54
63
|
let headers = {};
|
|
@@ -73,6 +82,23 @@ suite.test('createHttpApi error', async () => {
|
|
|
73
82
|
assert(err.body.some.deep === 'message');
|
|
74
83
|
});
|
|
75
84
|
|
|
85
|
+
suite.test('createHttpApi error { raw: true }', async () => {
|
|
86
|
+
let api = createHttpApi();
|
|
87
|
+
|
|
88
|
+
let r = await api.get(`${url}/asdf`, { raw: true });
|
|
89
|
+
assert(r instanceof Response);
|
|
90
|
+
assert(!r.ok);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
suite.test('createHttpApi error { assert: false } does not throw', async () => {
|
|
94
|
+
let api = createHttpApi();
|
|
95
|
+
let respHeaders = {};
|
|
96
|
+
|
|
97
|
+
let r = await api.get(`${url}/asdf`, { assert: false }, respHeaders);
|
|
98
|
+
assert(r.some.deep === 'message');
|
|
99
|
+
assert(respHeaders.__http_status_code__ === 404);
|
|
100
|
+
});
|
|
101
|
+
|
|
76
102
|
suite.test('createHttpApi POST', async () => {
|
|
77
103
|
let api = createHttpApi();
|
|
78
104
|
let respHeaders = {};
|
|
@@ -83,7 +109,7 @@ suite.test('createHttpApi POST', async () => {
|
|
|
83
109
|
});
|
|
84
110
|
|
|
85
111
|
suite.test('createHttpApi merge default params', async () => {
|
|
86
|
-
let api = createHttpApi({
|
|
112
|
+
let api = createHttpApi(null, {
|
|
87
113
|
headers: { authorization: 'Bearer foo' },
|
|
88
114
|
method: 'must be ignored',
|
|
89
115
|
path: 'must be ignored',
|