@marianmeres/http-utils 1.0.1 → 1.0.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/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.1",
3
+ "version": "1.0.2",
4
4
  "description": "Misc DRY http fetch related helpers",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
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
- if (!r.ok) {
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
- CONTINUE: { CODE: 100, TEXT: 'Continue' },
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
- OK: { CODE: 200, TEXT: 'OK' },
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
- before: async () => {
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
- after: async () => new Promise((resolve) => server.close(resolve)),
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',