@marianmeres/http-utils 1.1.0 → 1.2.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/dist/api.d.ts ADDED
@@ -0,0 +1,22 @@
1
+ interface BaseParams {
2
+ method: 'GET' | 'POST' | 'PATCH' | 'DELETE' | 'PUT';
3
+ path: string;
4
+ }
5
+ interface FetchParams {
6
+ data?: any;
7
+ token?: string | null;
8
+ headers?: any;
9
+ signal?: any;
10
+ credentials?: null | 'omit' | 'same-origin' | 'include';
11
+ raw?: null | boolean;
12
+ assert?: null | boolean;
13
+ }
14
+ type BaseFetchParams = BaseParams & FetchParams;
15
+ export declare const createHttpApi: (base: string | null, defaults?: Partial<BaseFetchParams> | (() => Promise<Partial<BaseFetchParams>>)) => {
16
+ get(path: string, params?: FetchParams, respHeaders?: null, _dumpParams?: boolean): Promise<string | BaseFetchParams | Response>;
17
+ post(path: string, data?: null, params?: FetchParams, respHeaders?: null, _dumpParams?: boolean): Promise<string | BaseFetchParams | Response>;
18
+ put(path: string, data?: null, params?: FetchParams, respHeaders?: null, _dumpParams?: boolean): Promise<string | BaseFetchParams | Response>;
19
+ patch(path: string, data?: null, params?: FetchParams, respHeaders?: null, _dumpParams?: boolean): Promise<string | BaseFetchParams | Response>;
20
+ del(path: string, data?: null, params?: FetchParams, respHeaders?: null, _dumpParams?: boolean): Promise<string | BaseFetchParams | Response>;
21
+ };
22
+ export {};
@@ -0,0 +1,87 @@
1
+ declare class HttpError extends Error {
2
+ name: string;
3
+ status: number;
4
+ statusText: string;
5
+ body: string | null;
6
+ }
7
+ declare class BadRequest extends HttpError {
8
+ name: string;
9
+ status: number;
10
+ statusText: string;
11
+ }
12
+ declare class Unauthorized extends HttpError {
13
+ name: string;
14
+ status: number;
15
+ statusText: string;
16
+ }
17
+ declare class Forbidden extends HttpError {
18
+ name: string;
19
+ status: number;
20
+ statusText: string;
21
+ }
22
+ declare class NotFound extends HttpError {
23
+ name: string;
24
+ status: number;
25
+ statusText: string;
26
+ }
27
+ declare class MethodNotAllowed extends HttpError {
28
+ name: string;
29
+ status: number;
30
+ statusText: string;
31
+ }
32
+ declare class RequestTimeout extends HttpError {
33
+ name: string;
34
+ status: number;
35
+ statusText: string;
36
+ }
37
+ declare class Conflict extends HttpError {
38
+ name: string;
39
+ status: number;
40
+ statusText: string;
41
+ }
42
+ declare class Gone extends HttpError {
43
+ name: string;
44
+ status: number;
45
+ statusText: string;
46
+ }
47
+ declare class ImATeapot extends HttpError {
48
+ name: string;
49
+ status: number;
50
+ statusText: string;
51
+ }
52
+ declare class InternalServerError extends HttpError {
53
+ name: string;
54
+ }
55
+ declare class NotImplemented extends HttpError {
56
+ name: string;
57
+ status: number;
58
+ statusText: string;
59
+ }
60
+ declare class BadGateway extends HttpError {
61
+ name: string;
62
+ status: number;
63
+ statusText: string;
64
+ }
65
+ declare class ServiceUnavailable extends HttpError {
66
+ name: string;
67
+ status: number;
68
+ statusText: string;
69
+ }
70
+ export declare const HTTP_ERROR: {
71
+ HttpError: typeof HttpError;
72
+ BadRequest: typeof BadRequest;
73
+ Unauthorized: typeof Unauthorized;
74
+ Forbidden: typeof Forbidden;
75
+ NotFound: typeof NotFound;
76
+ MethodNotAllowed: typeof MethodNotAllowed;
77
+ RequestTimeout: typeof RequestTimeout;
78
+ Conflict: typeof Conflict;
79
+ Gone: typeof Gone;
80
+ ImATeapot: typeof ImATeapot;
81
+ InternalServerError: typeof InternalServerError;
82
+ NotImplemented: typeof NotImplemented;
83
+ BadGateway: typeof BadGateway;
84
+ ServiceUnavailable: typeof ServiceUnavailable;
85
+ };
86
+ export declare const createHttpError: (code: number | string, message?: string | null, body?: any, cause?: any) => BadRequest | Unauthorized | Forbidden | NotFound | MethodNotAllowed | RequestTimeout | Conflict | Gone | ImATeapot | InternalServerError | NotImplemented | BadGateway | ServiceUnavailable;
87
+ export {};
package/dist/index.cjs ADDED
@@ -0,0 +1,340 @@
1
+ 'use strict';
2
+
3
+ var merge = require('dset/merge');
4
+
5
+ class HTTP_STATUS {
6
+ // 1xx
7
+ // prettier-ignore
8
+ static INFO = {
9
+ CONTINUE: { CODE: 100, TEXT: 'Continue' },
10
+ SWITCHING_PROTOCOLS: { CODE: 101, TEXT: 'Switching Protocols' },
11
+ PROCESSING: { CODE: 102, TEXT: 'Processing' },
12
+ EARLY_HINTS: { CODE: 103, TEXT: 'Early Hints' },
13
+ };
14
+ // 2xx
15
+ // prettier-ignore
16
+ static SUCCESS = {
17
+ OK: { CODE: 200, TEXT: 'OK' },
18
+ NON_AUTHORITATIVE_INFO: { CODE: 203, TEXT: 'Non-Authoritative Information' },
19
+ ACCEPTED: { CODE: 202, TEXT: 'Accepted' },
20
+ NO_CONTENT: { CODE: 204, TEXT: 'No Content' },
21
+ RESET_CONTENT: { CODE: 205, TEXT: 'Reset Content' },
22
+ PARTIAL_CONTENT: { CODE: 206, TEXT: 'Partial Content' },
23
+ MULTI_STATUS: { CODE: 207, TEXT: 'Multi-Status' },
24
+ ALREADY_REPORTED: { CODE: 208, TEXT: 'Already Reported' },
25
+ IM_USED: { CODE: 226, TEXT: 'IM Used' }, // ?
26
+ };
27
+ // 3xx
28
+ // prettier-ignore
29
+ static REDIRECT = {
30
+ MUTLIPLE_CHOICES: { CODE: 300, TEXT: 'Multiple Choices' },
31
+ MOVED_PERMANENTLY: { CODE: 301, TEXT: 'Moved Permanently' },
32
+ FOUND: { CODE: 302, TEXT: 'Found' },
33
+ SEE_OTHER: { CODE: 303, TEXT: 'See Other' },
34
+ NOT_MODIFIED: { CODE: 304, TEXT: 'Not Modified' },
35
+ TEMPORARY_REDIRECT: { CODE: 307, TEXT: 'Temporary Redirect' },
36
+ PERMANENT_REDIRECT: { CODE: 308, TEXT: 'Permanent Redirect' },
37
+ };
38
+ // 4xx
39
+ // prettier-ignore
40
+ static ERROR_CLIENT = {
41
+ BAD_REQUEST: { CODE: 400, TEXT: 'Bad Request' },
42
+ UNAUTHORIZED: { CODE: 401, TEXT: 'Unauthorized' },
43
+ PAYMENT_REQUIRED_EXPERIMENTAL: { CODE: 402, TEXT: 'Payment Required Experimental' },
44
+ FORBIDDEN: { CODE: 403, TEXT: 'Forbidden' },
45
+ NOT_FOUND: { CODE: 404, TEXT: 'Not Found' },
46
+ METHOD_NOT_ALLOWED: { CODE: 405, TEXT: 'Method Not Allowed' },
47
+ NOT_ACCEPTABLE: { CODE: 406, TEXT: 'Not Acceptable' },
48
+ PROXY_AUTHENTICATION_REQUIRED: { CODE: 407, TEXT: 'Proxy Authentication Required' },
49
+ REQUEST_TIMEOUT: { CODE: 408, TEXT: 'Request Timeout' },
50
+ CONFLICT: { CODE: 409, TEXT: 'Conflict' },
51
+ GONE: { CODE: 410, TEXT: 'Gone' },
52
+ LENGTH_REQUIRED: { CODE: 411, TEXT: 'Length Required' },
53
+ PRECONDITION_FAILED: { CODE: 412, TEXT: 'Precondition Failed' },
54
+ PAYLOAD_TOO_LARGE: { CODE: 413, TEXT: 'Payload Too Large' },
55
+ URI_TOO_LONG: { CODE: 414, TEXT: 'URI Too Long' },
56
+ UNSUPPORTED_MEDIA_TYPE: { CODE: 415, TEXT: 'Unsupported Media Type' },
57
+ RANGE_NOT_SATISFIABLE: { CODE: 416, TEXT: 'Range Not Satisfiable' },
58
+ EXPECTATION_FAILED: { CODE: 417, TEXT: 'Expectation Failed' },
59
+ IM_A_TEAPOT: { CODE: 418, TEXT: "I'm a teapot" },
60
+ MISDIRECTED_REQUEST: { CODE: 421, TEXT: 'Misdirected Request' },
61
+ UNPROCESSABLE_CONTENT: { CODE: 422, TEXT: 'Unprocessable Content' },
62
+ LOCKED: { CODE: 423, TEXT: 'Locked' },
63
+ FAILED_DEPENDENCY: { CODE: 424, TEXT: 'Failed Dependency' },
64
+ TOO_EARLY_EXPERIMENTAL: { CODE: 425, TEXT: 'Too Early Experimental' },
65
+ UPGRADE_REQUIRED: { CODE: 426, TEXT: 'Upgrade Required' },
66
+ PRECONDITION_REQUIRED: { CODE: 428, TEXT: 'Precondition Required' },
67
+ TOO_MANY_REQUESTS: { CODE: 429, TEXT: 'Too Many Requests' },
68
+ REQUEST_HEADER_FIELDS_TOO_LARGE: { CODE: 431, TEXT: 'Request Header Fields Too Large' },
69
+ UNAVAILABLE_FOR_LEGAL_REASONS: { CODE: 451, TEXT: 'Unavailable For Legal Reasons' },
70
+ };
71
+ // 5xx
72
+ // prettier-ignore
73
+ static ERROR_SERVER = {
74
+ INTERNAL_SERVER_ERROR: { CODE: 500, TEXT: 'Internal Server Error' },
75
+ NOT_IMPLEMENTED: { CODE: 501, TEXT: 'Not Implemented' },
76
+ BAD_GATEWAY: { CODE: 502, TEXT: 'Bad Gateway' },
77
+ SERVICE_UNAVAILABLE: { CODE: 503, TEXT: 'Service Unavailable' },
78
+ GATEWAY_TIMEOUT: { CODE: 504, TEXT: 'Gateway Timeout' },
79
+ HTTP_VERSION_NOT_SUPPORTED: { CODE: 505, TEXT: 'HTTP Version Not Supported' },
80
+ VARIANT_ALSO_NEGOTIATES: { CODE: 506, TEXT: 'Variant Also Negotiates' },
81
+ INSUFFICIENT_STORAGE: { CODE: 507, TEXT: 'Insufficient Storage' },
82
+ LOOP_DETECTED: { CODE: 508, TEXT: 'Loop Detected' },
83
+ NOT_EXTENDED: { CODE: 510, TEXT: 'Not Extended' },
84
+ NETWORK_AUTH_REQUIRED: { CODE: 511, TEXT: 'Network Authentication Required' },
85
+ };
86
+ //
87
+ static findByCode(code) {
88
+ const keys = [
89
+ 'INFO',
90
+ 'SUCCESS',
91
+ 'REDIRECT',
92
+ 'ERROR_CLIENT',
93
+ 'ERROR_SERVER',
94
+ ];
95
+ for (const _TYPE of keys) {
96
+ for (const [_KEY, data] of Object.entries(HTTP_STATUS[_TYPE])) {
97
+ if (data.CODE == code) {
98
+ return { ...data, _TYPE, _KEY };
99
+ }
100
+ }
101
+ }
102
+ return null;
103
+ }
104
+ }
105
+
106
+ // opinionated base for all
107
+ class HttpError extends Error {
108
+ name = 'HttpError';
109
+ status = HTTP_STATUS.ERROR_SERVER.INTERNAL_SERVER_ERROR.CODE;
110
+ statusText = HTTP_STATUS.ERROR_SERVER.INTERNAL_SERVER_ERROR.TEXT;
111
+ body = null;
112
+ }
113
+ // some more specific instances of the well known ones...
114
+ // client
115
+ class BadRequest extends HttpError {
116
+ name = 'HttpBadRequestError';
117
+ status = HTTP_STATUS.ERROR_CLIENT.BAD_REQUEST.CODE;
118
+ statusText = HTTP_STATUS.ERROR_CLIENT.BAD_REQUEST.TEXT;
119
+ }
120
+ class Unauthorized extends HttpError {
121
+ name = 'HttpUnauthorizedError';
122
+ status = HTTP_STATUS.ERROR_CLIENT.UNAUTHORIZED.CODE;
123
+ statusText = HTTP_STATUS.ERROR_CLIENT.UNAUTHORIZED.TEXT;
124
+ }
125
+ class Forbidden extends HttpError {
126
+ name = 'HttpForbiddenError';
127
+ status = HTTP_STATUS.ERROR_CLIENT.FORBIDDEN.CODE;
128
+ statusText = HTTP_STATUS.ERROR_CLIENT.FORBIDDEN.TEXT;
129
+ }
130
+ class NotFound extends HttpError {
131
+ name = 'HttpNotFoundError';
132
+ status = HTTP_STATUS.ERROR_CLIENT.NOT_FOUND.CODE;
133
+ statusText = HTTP_STATUS.ERROR_CLIENT.NOT_FOUND.TEXT;
134
+ }
135
+ class MethodNotAllowed extends HttpError {
136
+ name = 'HttpMethodNotAllowedError';
137
+ status = HTTP_STATUS.ERROR_CLIENT.METHOD_NOT_ALLOWED.CODE;
138
+ statusText = HTTP_STATUS.ERROR_CLIENT.METHOD_NOT_ALLOWED.TEXT;
139
+ }
140
+ class RequestTimeout extends HttpError {
141
+ name = 'HttpRequestTimeoutError';
142
+ status = HTTP_STATUS.ERROR_CLIENT.REQUEST_TIMEOUT.CODE;
143
+ statusText = HTTP_STATUS.ERROR_CLIENT.REQUEST_TIMEOUT.TEXT;
144
+ }
145
+ class Conflict extends HttpError {
146
+ name = 'HttpConflictError';
147
+ status = HTTP_STATUS.ERROR_CLIENT.CONFLICT.CODE;
148
+ statusText = HTTP_STATUS.ERROR_CLIENT.CONFLICT.TEXT;
149
+ }
150
+ class Gone extends HttpError {
151
+ name = 'HttpGoneError';
152
+ status = HTTP_STATUS.ERROR_CLIENT.GONE.CODE;
153
+ statusText = HTTP_STATUS.ERROR_CLIENT.GONE.TEXT;
154
+ }
155
+ class ImATeapot extends HttpError {
156
+ name = 'HttpImATeapotError';
157
+ status = HTTP_STATUS.ERROR_CLIENT.IM_A_TEAPOT.CODE;
158
+ statusText = HTTP_STATUS.ERROR_CLIENT.IM_A_TEAPOT.TEXT;
159
+ }
160
+ // server
161
+ class InternalServerError extends HttpError {
162
+ name = 'HttpInternalServerError';
163
+ }
164
+ class NotImplemented extends HttpError {
165
+ name = 'HttpServiceUnavailableError';
166
+ status = HTTP_STATUS.ERROR_SERVER.NOT_IMPLEMENTED.CODE;
167
+ statusText = HTTP_STATUS.ERROR_SERVER.NOT_IMPLEMENTED.TEXT;
168
+ }
169
+ class BadGateway extends HttpError {
170
+ name = 'HttpBadGatewayError';
171
+ status = HTTP_STATUS.ERROR_SERVER.BAD_GATEWAY.CODE;
172
+ statusText = HTTP_STATUS.ERROR_SERVER.BAD_GATEWAY.TEXT;
173
+ }
174
+ class ServiceUnavailable extends HttpError {
175
+ name = 'HttpServiceUnavailableError';
176
+ status = HTTP_STATUS.ERROR_SERVER.SERVICE_UNAVAILABLE.CODE;
177
+ statusText = HTTP_STATUS.ERROR_SERVER.SERVICE_UNAVAILABLE.TEXT;
178
+ }
179
+ //
180
+ const HTTP_ERROR = {
181
+ // base
182
+ HttpError,
183
+ // client
184
+ BadRequest,
185
+ Unauthorized,
186
+ Forbidden,
187
+ NotFound,
188
+ MethodNotAllowed,
189
+ RequestTimeout,
190
+ Conflict,
191
+ Gone,
192
+ ImATeapot,
193
+ // server
194
+ InternalServerError,
195
+ NotImplemented,
196
+ BadGateway,
197
+ ServiceUnavailable,
198
+ };
199
+ const _wellKnownCtorMap = {
200
+ '400': BadRequest,
201
+ '401': Unauthorized,
202
+ '403': Forbidden,
203
+ '404': NotFound,
204
+ '405': MethodNotAllowed,
205
+ '408': RequestTimeout,
206
+ '409': Conflict,
207
+ '410': Gone,
208
+ '418': ImATeapot,
209
+ //
210
+ '500': InternalServerError,
211
+ '501': NotImplemented,
212
+ '502': BadGateway,
213
+ '503': ServiceUnavailable,
214
+ };
215
+ // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/cause
216
+ const createHttpError = (code, message, body, cause) => {
217
+ const fallback = HTTP_STATUS.ERROR_SERVER.INTERNAL_SERVER_ERROR;
218
+ code = Number(code);
219
+ if (isNaN(code) || !(code >= 400 && code < 600))
220
+ code = fallback.CODE;
221
+ const found = HTTP_STATUS.findByCode(code);
222
+ const statusText = found?.TEXT ?? fallback.TEXT;
223
+ // opinionated convention
224
+ if (typeof body === 'string') {
225
+ // prettier-ignore
226
+ try {
227
+ body = JSON.parse(body);
228
+ }
229
+ catch (e) { }
230
+ }
231
+ // try to find the well known one, otherwise fallback to generic
232
+ const ctor = _wellKnownCtorMap[`${code}`] ?? HttpError;
233
+ let e = new ctor(message || statusText, { cause });
234
+ e.status = found?.CODE ?? fallback.CODE;
235
+ e.statusText = statusText;
236
+ e.body = body;
237
+ return e;
238
+ };
239
+
240
+ const _fetchRaw = async ({ method, path, data = null, token = null, headers = null, signal = null, credentials, }) => {
241
+ headers = Object.entries(headers || {}).map(([k, v]) => ({ [k.toLowerCase()]: v }));
242
+ const opts = { method, credentials, headers, signal };
243
+ if (data) {
244
+ const isObj = typeof data === 'object';
245
+ // multipart/form-data -- no explicit Content-Type
246
+ if (data instanceof FormData) {
247
+ opts.body = data;
248
+ }
249
+ // cover 99% use cases (may not fit all)
250
+ else {
251
+ // if not stated, assuming json
252
+ if (isObj || !headers['content-type']) {
253
+ opts.headers['content-type'] = 'application/json';
254
+ }
255
+ opts.body = JSON.stringify(data);
256
+ }
257
+ }
258
+ // opinionated convention
259
+ if (token) {
260
+ opts.headers['authorization'] = `Bearer ${token}`;
261
+ }
262
+ return await fetch(path, opts);
263
+ };
264
+ const _fetch = async (params, respHeaders = null, _dumpParams = false) => {
265
+ if (_dumpParams)
266
+ return params;
267
+ const r = await _fetchRaw(params);
268
+ if (params.raw)
269
+ return r;
270
+ // quick-n-dirty reference to headers (so it's still accessible over this api wrap)
271
+ if (respHeaders) {
272
+ Object.assign(respHeaders, [...r.headers.entries()].reduce((m, [k, v]) => ({ ...m, [k]: v }), {}),
273
+ // adding status/text under special keys
274
+ {
275
+ __http_status_code__: r.status,
276
+ __http_status_text__: r.statusText,
277
+ });
278
+ }
279
+ let body = await r.text();
280
+ // prettier-ignore
281
+ try {
282
+ body = JSON.parse(body);
283
+ }
284
+ catch (e) { }
285
+ params.assert ??= true; // default is true
286
+ if (!r.ok && params.assert) {
287
+ throw createHttpError(r.status, null, body);
288
+ }
289
+ return body;
290
+ };
291
+ const createHttpApi = (base, defaults) => {
292
+ const _merge = (a, b) => {
293
+ const wrap = { result: a };
294
+ merge.dset(wrap, 'result', b);
295
+ return wrap.result;
296
+ };
297
+ const _getDefs = async () => {
298
+ return new Promise(async (resolve) => {
299
+ if (typeof defaults === 'function') {
300
+ resolve(await defaults());
301
+ }
302
+ else {
303
+ resolve(defaults || {});
304
+ }
305
+ });
306
+ };
307
+ return {
308
+ // GET
309
+ async get(path, params, respHeaders = null, _dumpParams = false) {
310
+ path = `${base || ''}` + path;
311
+ return _fetch(_merge(await _getDefs(), { ...params, method: 'GET', path }), respHeaders, _dumpParams);
312
+ },
313
+ // POST
314
+ async post(path, data = null, params, respHeaders = null, _dumpParams = false) {
315
+ path = `${base || ''}` + path;
316
+ return _fetch(_merge(await _getDefs(), { ...(params || {}), data, method: 'POST', path }), respHeaders, _dumpParams);
317
+ },
318
+ // PUT
319
+ async put(path, data = null, params, respHeaders = null, _dumpParams = false) {
320
+ path = `${base || ''}` + path;
321
+ return _fetch(_merge(await _getDefs(), { ...(params || {}), data, method: 'PUT', path }), respHeaders, _dumpParams);
322
+ },
323
+ // PATCH
324
+ async patch(path, data = null, params, respHeaders = null, _dumpParams = false) {
325
+ path = `${base || ''}` + path;
326
+ return _fetch(_merge(await _getDefs(), { ...(params || {}), data, method: 'PATCH', path }), respHeaders, _dumpParams);
327
+ },
328
+ // DELETE
329
+ // https://stackoverflow.com/questions/299628/is-an-entity-body-allowed-for-an-http-delete-request
330
+ async del(path, data = null, params, respHeaders = null, _dumpParams = false) {
331
+ path = `${base || ''}` + path;
332
+ return _fetch(_merge(await _getDefs(), { ...(params || {}), data, method: 'DELETE', path }), respHeaders, _dumpParams);
333
+ },
334
+ };
335
+ };
336
+
337
+ exports.HTTP_ERROR = HTTP_ERROR;
338
+ exports.HTTP_STATUS = HTTP_STATUS;
339
+ exports.createHttpApi = createHttpApi;
340
+ exports.createHttpError = createHttpError;