shelving 1.143.1 → 1.144.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/api/Endpoint.d.ts +11 -1
- package/api/Endpoint.js +25 -2
- package/api/util.d.ts +14 -0
- package/api/util.js +40 -19
- package/package.json +1 -1
- package/util/ansi.d.ts +24 -0
- package/util/ansi.js +28 -0
- package/util/http.d.ts +20 -2
- package/util/http.js +25 -5
- package/util/index.d.ts +1 -0
- package/util/index.js +1 -0
package/api/Endpoint.d.ts
CHANGED
|
@@ -36,9 +36,19 @@ export declare class Endpoint<P, R> implements Validator<R> {
|
|
|
36
36
|
*/
|
|
37
37
|
validate(unsafeResult: unknown): R;
|
|
38
38
|
/**
|
|
39
|
-
* Return an `EndpointHandler` for this endpoint
|
|
39
|
+
* Return an `EndpointHandler` for this endpoint.
|
|
40
|
+
*
|
|
41
|
+
* @param callback The callback function that implements the logic for this endpoint by receiving the payload and returning the response.
|
|
40
42
|
*/
|
|
41
43
|
handler(callback: EndpointCallback<P, R>): EndpointHandler<P, R>;
|
|
44
|
+
/**
|
|
45
|
+
* Handle a request to this endpoint with a callback implementation, with a given payload and request.
|
|
46
|
+
*
|
|
47
|
+
* @param callback The endpoint callback function that implements the logic for this endpoint by receiving the payload and returning the response.
|
|
48
|
+
* @param unsafePayload The payload to pass into the callback (will be validated against this endpoint's payload schema).
|
|
49
|
+
* @param request The entire HTTP request that is being handled (payload was possibly extracted from this somehow).
|
|
50
|
+
*/
|
|
51
|
+
handle(callback: EndpointCallback<P, R>, unsafePayload: unknown, request: Request): Promise<Response>;
|
|
42
52
|
}
|
|
43
53
|
/** Extract the payload type from a `Endpoint`. */
|
|
44
54
|
export type PayloadType<X extends Endpoint<unknown, unknown>> = X extends Endpoint<infer Y, unknown> ? Y : never;
|
package/api/Endpoint.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ValueError } from "../error/ValueError.js";
|
|
2
|
+
import { UNDEFINED, getValid } from "../util/validate.js";
|
|
2
3
|
/**
|
|
3
4
|
* An abstract API resource definition, used to specify types for e.g. serverless functions.
|
|
4
5
|
*
|
|
@@ -41,11 +42,33 @@ export class Endpoint {
|
|
|
41
42
|
return this.result.validate(unsafeResult);
|
|
42
43
|
}
|
|
43
44
|
/**
|
|
44
|
-
* Return an `EndpointHandler` for this endpoint
|
|
45
|
+
* Return an `EndpointHandler` for this endpoint.
|
|
46
|
+
*
|
|
47
|
+
* @param callback The callback function that implements the logic for this endpoint by receiving the payload and returning the response.
|
|
45
48
|
*/
|
|
46
49
|
handler(callback) {
|
|
47
50
|
return { endpoint: this, callback };
|
|
48
51
|
}
|
|
52
|
+
/**
|
|
53
|
+
* Handle a request to this endpoint with a callback implementation, with a given payload and request.
|
|
54
|
+
*
|
|
55
|
+
* @param callback The endpoint callback function that implements the logic for this endpoint by receiving the payload and returning the response.
|
|
56
|
+
* @param unsafePayload The payload to pass into the callback (will be validated against this endpoint's payload schema).
|
|
57
|
+
* @param request The entire HTTP request that is being handled (payload was possibly extracted from this somehow).
|
|
58
|
+
*/
|
|
59
|
+
async handle(callback, unsafePayload, request) {
|
|
60
|
+
const payload = this.prepare(unsafePayload);
|
|
61
|
+
// Call the callback with the validated payload to get the result.
|
|
62
|
+
const returned = await callback(payload, request);
|
|
63
|
+
// If the callback returned a `Response`, return it directly.
|
|
64
|
+
if (returned instanceof Response)
|
|
65
|
+
return returned;
|
|
66
|
+
// Otherwise validate the result against the endpoint's result type.
|
|
67
|
+
// Throw a `ValueError` if the result is not valid, which indicates an internal error in the callback implementation.
|
|
68
|
+
const result = getValid(returned, this, ValueError, this.handle);
|
|
69
|
+
// Return a new `Response` with a 200 status and the validated result data.
|
|
70
|
+
return Response.json(result);
|
|
71
|
+
}
|
|
49
72
|
}
|
|
50
73
|
export function GET(path, payload, result) {
|
|
51
74
|
return new Endpoint("GET", path, payload || UNDEFINED, result || UNDEFINED);
|
package/api/util.d.ts
CHANGED
|
@@ -38,3 +38,17 @@ export type EndpointHandlers = ReadonlyArray<AnyEndpointHandler>;
|
|
|
38
38
|
* @throws `NotFoundError` if no handler matches the `Request`.
|
|
39
39
|
*/
|
|
40
40
|
export declare function handleEndpoints(request: Request, endpoints: EndpointHandlers): Promise<Response>;
|
|
41
|
+
/**
|
|
42
|
+
* Correctly interpret an error thrown from an endpoint and return the correct `Response`.
|
|
43
|
+
*
|
|
44
|
+
* Returns the correct `Response` based on the type of error thrown:
|
|
45
|
+
* - If `reason` is a `Response` instance, return it directly.
|
|
46
|
+
* - If `reason` is a `Feedback` instance, return a 400 response with the feedback's message as JSON, e.g. `{ message: "Invalid input" }`
|
|
47
|
+
* - If `reason` is an `RequestError` instance, return a response with the error's message and code (but only if `debug` is true so we don't leak error details to the client).
|
|
48
|
+
* - If `reason` is an `Error` instance, return a 500 response with the error's message (but only if `debug` is true so we don't leak error details to the client).
|
|
49
|
+
* - Anything else returns a 500 response.
|
|
50
|
+
*
|
|
51
|
+
* @param reason The error thrown from the endpoint.
|
|
52
|
+
* @param debug If `true` include the error message in the response (for debugging), or `false` to return generic error codes (for security).
|
|
53
|
+
*/
|
|
54
|
+
export declare function handleEndpointError(reason: unknown, debug?: boolean): Response;
|
package/api/util.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { NotFoundError, RequestError } from "../error/RequestError.js";
|
|
2
|
-
import {
|
|
2
|
+
import { Feedback } from "../feedback/Feedback.js";
|
|
3
3
|
import { isData } from "../util/data.js";
|
|
4
4
|
import { getDictionary } from "../util/dictionary.js";
|
|
5
|
+
import { isError } from "../util/error.js";
|
|
5
6
|
import { getRequestContent } from "../util/http.js";
|
|
6
7
|
import { matchTemplate } from "../util/template.js";
|
|
7
8
|
import { getURL } from "../util/url.js";
|
|
8
|
-
import { getValid } from "../util/validate.js";
|
|
9
9
|
/**
|
|
10
10
|
* Handler a `Request` with the first matching `EndpointHandlers`.
|
|
11
11
|
*
|
|
@@ -18,9 +18,10 @@ import { getValid } from "../util/validate.js";
|
|
|
18
18
|
*/
|
|
19
19
|
export function handleEndpoints(request, endpoints) {
|
|
20
20
|
// Parse the URL of the request.
|
|
21
|
-
const
|
|
21
|
+
const requestUrl = request.url;
|
|
22
|
+
const url = getURL(requestUrl);
|
|
22
23
|
if (!url)
|
|
23
|
-
throw new RequestError("Invalid request URL", { received:
|
|
24
|
+
throw new RequestError("Invalid request URL", { received: requestUrl, caller: handleEndpoints });
|
|
24
25
|
const { pathname, searchParams } = url;
|
|
25
26
|
// Iterate over the handlers and return the first one that matches the request.
|
|
26
27
|
for (const { endpoint, callback } of endpoints) {
|
|
@@ -35,27 +36,47 @@ export function handleEndpoints(request, endpoints) {
|
|
|
35
36
|
// Make a simple dictionary object from the `{placeholder}` path params and the `?a=123` query params from the URL.
|
|
36
37
|
const params = searchParams.size ? { ...getDictionary(searchParams), ...pathParams } : pathParams;
|
|
37
38
|
// Get the response by calling the callback.
|
|
38
|
-
return
|
|
39
|
+
return handleEndpoint(endpoint, callback, params, request);
|
|
39
40
|
}
|
|
40
41
|
// No handler matched the request.
|
|
41
|
-
throw new NotFoundError("
|
|
42
|
+
throw new NotFoundError("No matching endpoint", { received: requestUrl, caller: handleEndpoints });
|
|
42
43
|
}
|
|
43
|
-
|
|
44
|
+
/** Handle an individual call to an endpoint callback. */
|
|
45
|
+
async function handleEndpoint(endpoint, callback, params, request) {
|
|
44
46
|
// Extract a data object from the request body and validate it against the endpoint's payload type.
|
|
45
47
|
const content = await getRequestContent(request, handleEndpoints);
|
|
46
48
|
// If content is undefined, it means the request has no body, so params are the only payload.
|
|
47
49
|
// If the content is a data object merge if with the params.
|
|
48
50
|
// If the content is not a data object (e.g. string, number, array), set a single `content` property and merge it with the params.
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
51
|
+
const payload = content === undefined ? params : isData(content) ? { ...content, ...params } : { content, ...params };
|
|
52
|
+
// Call `endpoint.handle()` with the payload and request.
|
|
53
|
+
return endpoint.handle(callback, payload, request);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Correctly interpret an error thrown from an endpoint and return the correct `Response`.
|
|
57
|
+
*
|
|
58
|
+
* Returns the correct `Response` based on the type of error thrown:
|
|
59
|
+
* - If `reason` is a `Response` instance, return it directly.
|
|
60
|
+
* - If `reason` is a `Feedback` instance, return a 400 response with the feedback's message as JSON, e.g. `{ message: "Invalid input" }`
|
|
61
|
+
* - If `reason` is an `RequestError` instance, return a response with the error's message and code (but only if `debug` is true so we don't leak error details to the client).
|
|
62
|
+
* - If `reason` is an `Error` instance, return a 500 response with the error's message (but only if `debug` is true so we don't leak error details to the client).
|
|
63
|
+
* - Anything else returns a 500 response.
|
|
64
|
+
*
|
|
65
|
+
* @param reason The error thrown from the endpoint.
|
|
66
|
+
* @param debug If `true` include the error message in the response (for debugging), or `false` to return generic error codes (for security).
|
|
67
|
+
*/
|
|
68
|
+
export function handleEndpointError(reason, debug = false) {
|
|
69
|
+
// Throw `Response` to do a custom response that is not logged.
|
|
70
|
+
if (reason instanceof Response)
|
|
71
|
+
return reason;
|
|
72
|
+
// Throw 'Feedback' to return `{ message: "etc" }` to the client, e.g. for input validation.
|
|
73
|
+
if (reason instanceof Feedback)
|
|
74
|
+
return Response.json(reason, { status: 422 }); // HTTP 422 Unprocessable Entity
|
|
75
|
+
// Throw `RequestError` to set a custom status code (e.g. `UnauthorizedError`).
|
|
76
|
+
const status = reason instanceof RequestError ? reason.code : 500;
|
|
77
|
+
// Throw `Error` to return `{ message: "etc" }` to the client (but only if `debug` is true so we don't leak error details to the client).
|
|
78
|
+
if (debug && isError(reason))
|
|
79
|
+
return Response.json(reason, { status });
|
|
80
|
+
// Otherwise return a generic error message.
|
|
81
|
+
return new Response(undefined, { status });
|
|
61
82
|
}
|
package/package.json
CHANGED
package/util/ansi.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export declare const ANSI_TEXT_DEFAULT = "\u001B[39m";
|
|
2
|
+
export declare const ANSI_TEXT_BLACK = "\u001B[30m";
|
|
3
|
+
export declare const ANSI_TEXT_RED = "\u001B[31m";
|
|
4
|
+
export declare const ANSI_TEXT_GREEN = "\u001B[32m";
|
|
5
|
+
export declare const ANSI_TEXT_YELLOW = "\u001B[33m";
|
|
6
|
+
export declare const ANSI_TEXT_BLUE = "\u001B[34m";
|
|
7
|
+
export declare const ANSI_TEXT_MAGENTA = "\u001B[35m";
|
|
8
|
+
export declare const ANSI_TEXT_CYAN = "\u001B[36m";
|
|
9
|
+
export declare const ANSI_TEXT_WHITE = "\u001B[37m";
|
|
10
|
+
export declare const ANSI_FILL_DEFAULT = "\u001B[49m";
|
|
11
|
+
export declare const ANSI_FILL_BLACK = "\u001B[40m";
|
|
12
|
+
export declare const ANSI_FILL_RED = "\u001B[41m";
|
|
13
|
+
export declare const ANSI_FILL_GREEN = "\u001B[42m";
|
|
14
|
+
export declare const ANSI_FILL_YELLOW = "\u001B[43m";
|
|
15
|
+
export declare const ANSI_FILL_BLUE = "\u001B[44m";
|
|
16
|
+
export declare const ANSI_FILL_MAGENTA = "\u001B[45m";
|
|
17
|
+
export declare const ANSI_FILL_CYAN = "\u001B[46m";
|
|
18
|
+
export declare const ANSI_FILL_WHITE = "\u001B[47m";
|
|
19
|
+
export declare const ANSI_BOLD = "\u001B[1m";
|
|
20
|
+
export declare const ANSI_ITALIC = "\u001B[3m";
|
|
21
|
+
export declare const ANSI_UNDERLINE = "\u001B[4m";
|
|
22
|
+
export declare const ANSI_STRIKE = "\u001B[9m";
|
|
23
|
+
export declare const ANSI_INVERSE = "\u001B[7m";
|
|
24
|
+
export declare const ANSI_RESET = "\u001B[0m";
|
package/util/ansi.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Foreground colors.
|
|
2
|
+
export const ANSI_TEXT_DEFAULT = "\x1b[39m";
|
|
3
|
+
export const ANSI_TEXT_BLACK = "\x1b[30m";
|
|
4
|
+
export const ANSI_TEXT_RED = "\x1b[31m";
|
|
5
|
+
export const ANSI_TEXT_GREEN = "\x1b[32m";
|
|
6
|
+
export const ANSI_TEXT_YELLOW = "\x1b[33m";
|
|
7
|
+
export const ANSI_TEXT_BLUE = "\x1b[34m";
|
|
8
|
+
export const ANSI_TEXT_MAGENTA = "\x1b[35m";
|
|
9
|
+
export const ANSI_TEXT_CYAN = "\x1b[36m";
|
|
10
|
+
export const ANSI_TEXT_WHITE = "\x1b[37m";
|
|
11
|
+
// Background colors.
|
|
12
|
+
export const ANSI_FILL_DEFAULT = "\x1b[49m";
|
|
13
|
+
export const ANSI_FILL_BLACK = "\x1b[40m";
|
|
14
|
+
export const ANSI_FILL_RED = "\x1b[41m";
|
|
15
|
+
export const ANSI_FILL_GREEN = "\x1b[42m";
|
|
16
|
+
export const ANSI_FILL_YELLOW = "\x1b[43m";
|
|
17
|
+
export const ANSI_FILL_BLUE = "\x1b[44m";
|
|
18
|
+
export const ANSI_FILL_MAGENTA = "\x1b[45m";
|
|
19
|
+
export const ANSI_FILL_CYAN = "\x1b[46m";
|
|
20
|
+
export const ANSI_FILL_WHITE = "\x1b[47m";
|
|
21
|
+
// Styles.
|
|
22
|
+
export const ANSI_BOLD = "\x1b[1m";
|
|
23
|
+
export const ANSI_ITALIC = "\x1b[3m";
|
|
24
|
+
export const ANSI_UNDERLINE = "\x1b[4m";
|
|
25
|
+
export const ANSI_STRIKE = "\x1b[9m";
|
|
26
|
+
export const ANSI_INVERSE = "\x1b[7m";
|
|
27
|
+
// Reset.
|
|
28
|
+
export const ANSI_RESET = "\x1b[0m";
|
package/util/http.d.ts
CHANGED
|
@@ -9,9 +9,9 @@ export declare function _getMessageContent(message: Request | Response, MessageE
|
|
|
9
9
|
/**
|
|
10
10
|
* Get the body content of an HTTP `Request` based on its content type, or throw `RequestError` if the content could not be parsed.
|
|
11
11
|
*
|
|
12
|
-
* @returns string If content type is `text/plain` (including empty string if it's empty).
|
|
13
12
|
* @returns unknown If content type is `application/json` and has valid JSON (including `undefined` if the content is empty).
|
|
14
13
|
* @returns unknown If content type is `multipart/form-data` then convert it to a simple `Data` object.
|
|
14
|
+
* @returns string If content type is `text/plain` or anything else (including `""` empty string if it's empty).
|
|
15
15
|
*
|
|
16
16
|
* @throws RequestError if the content is not `text/plain`, or `application/json` with valid JSON.
|
|
17
17
|
*/
|
|
@@ -19,10 +19,28 @@ export declare function getRequestContent(message: Request, caller?: AnyCaller):
|
|
|
19
19
|
/**
|
|
20
20
|
* Get the body content of an HTTP `Response` based on its content type, or throw `ResponseError` if the content could not be parsed.
|
|
21
21
|
*
|
|
22
|
-
* @returns string If content type is `text/plain` (including empty string if it's empty).
|
|
23
22
|
* @returns unknown If content type is `application/json` and has valid JSON (including `undefined` if the content is empty).
|
|
24
23
|
* @returns unknown If content type is `multipart/form-data` then convert it to a simple `Data` object.
|
|
24
|
+
* @returns string If content type is `text/plain` or anything else (including `""` empty string if it's empty).
|
|
25
25
|
*
|
|
26
26
|
* @throws RequestError if the content is not `text/plain` or `application/json` with valid JSON.
|
|
27
27
|
*/
|
|
28
28
|
export declare function getResponseContent(message: Response, caller?: AnyCaller): Promise<unknown>;
|
|
29
|
+
/**
|
|
30
|
+
* Get the body content of an HTTP `Request` as JSON, or throw `RequestError` if the content could not be parsed.
|
|
31
|
+
* - Doesn't check the `Content-Type` header, so it can be used for any request.
|
|
32
|
+
*
|
|
33
|
+
* @returns unknown The parsed JSON content of the request body, or `undefined` if the body is empty.
|
|
34
|
+
*
|
|
35
|
+
* @throws RequestError if the content is not valid JSON.
|
|
36
|
+
*/
|
|
37
|
+
export declare function getRequestJSON(message: Request, caller?: AnyCaller): Promise<unknown>;
|
|
38
|
+
/**
|
|
39
|
+
* Get the body content of an HTTP `Response` as JSON, or throw `ResponseError` if the content could not be parsed.
|
|
40
|
+
* - Doesn't check the `Content-Type` header, so it can be used for any response.
|
|
41
|
+
*
|
|
42
|
+
* @returns unknown The parsed JSON content of the response body, or `undefined` if the body is empty.
|
|
43
|
+
*
|
|
44
|
+
* @throws RequestError if the content is not valid JSON.
|
|
45
|
+
*/
|
|
46
|
+
export declare function getResponseJSON(message: Response, caller?: AnyCaller): Promise<unknown>;
|
package/util/http.js
CHANGED
|
@@ -22,20 +22,18 @@ export async function _getMessageFormData(message, MessageError, caller) {
|
|
|
22
22
|
}
|
|
23
23
|
export function _getMessageContent(message, MessageError, caller) {
|
|
24
24
|
const type = message.headers.get("Content-Type");
|
|
25
|
-
if (type?.startsWith("text/plain"))
|
|
26
|
-
return message.text();
|
|
27
25
|
if (type?.startsWith("application/json"))
|
|
28
26
|
return _getMessageJSON(message, MessageError, caller);
|
|
29
27
|
if (type?.startsWith("multipart/form-data"))
|
|
30
28
|
return _getMessageFormData(message, MessageError, caller);
|
|
31
|
-
|
|
29
|
+
return message.text();
|
|
32
30
|
}
|
|
33
31
|
/**
|
|
34
32
|
* Get the body content of an HTTP `Request` based on its content type, or throw `RequestError` if the content could not be parsed.
|
|
35
33
|
*
|
|
36
|
-
* @returns string If content type is `text/plain` (including empty string if it's empty).
|
|
37
34
|
* @returns unknown If content type is `application/json` and has valid JSON (including `undefined` if the content is empty).
|
|
38
35
|
* @returns unknown If content type is `multipart/form-data` then convert it to a simple `Data` object.
|
|
36
|
+
* @returns string If content type is `text/plain` or anything else (including `""` empty string if it's empty).
|
|
39
37
|
*
|
|
40
38
|
* @throws RequestError if the content is not `text/plain`, or `application/json` with valid JSON.
|
|
41
39
|
*/
|
|
@@ -47,12 +45,34 @@ export function getRequestContent(message, caller = getRequestContent) {
|
|
|
47
45
|
/**
|
|
48
46
|
* Get the body content of an HTTP `Response` based on its content type, or throw `ResponseError` if the content could not be parsed.
|
|
49
47
|
*
|
|
50
|
-
* @returns string If content type is `text/plain` (including empty string if it's empty).
|
|
51
48
|
* @returns unknown If content type is `application/json` and has valid JSON (including `undefined` if the content is empty).
|
|
52
49
|
* @returns unknown If content type is `multipart/form-data` then convert it to a simple `Data` object.
|
|
50
|
+
* @returns string If content type is `text/plain` or anything else (including `""` empty string if it's empty).
|
|
53
51
|
*
|
|
54
52
|
* @throws RequestError if the content is not `text/plain` or `application/json` with valid JSON.
|
|
55
53
|
*/
|
|
56
54
|
export function getResponseContent(message, caller = getResponseContent) {
|
|
57
55
|
return _getMessageContent(message, ResponseError, caller);
|
|
58
56
|
}
|
|
57
|
+
/**
|
|
58
|
+
* Get the body content of an HTTP `Request` as JSON, or throw `RequestError` if the content could not be parsed.
|
|
59
|
+
* - Doesn't check the `Content-Type` header, so it can be used for any request.
|
|
60
|
+
*
|
|
61
|
+
* @returns unknown The parsed JSON content of the request body, or `undefined` if the body is empty.
|
|
62
|
+
*
|
|
63
|
+
* @throws RequestError if the content is not valid JSON.
|
|
64
|
+
*/
|
|
65
|
+
export function getRequestJSON(message, caller = getRequestJSON) {
|
|
66
|
+
return _getMessageJSON(message, RequestError, caller);
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Get the body content of an HTTP `Response` as JSON, or throw `ResponseError` if the content could not be parsed.
|
|
70
|
+
* - Doesn't check the `Content-Type` header, so it can be used for any response.
|
|
71
|
+
*
|
|
72
|
+
* @returns unknown The parsed JSON content of the response body, or `undefined` if the body is empty.
|
|
73
|
+
*
|
|
74
|
+
* @throws RequestError if the content is not valid JSON.
|
|
75
|
+
*/
|
|
76
|
+
export function getResponseJSON(message, caller = getResponseJSON) {
|
|
77
|
+
return _getMessageJSON(message, ResponseError, caller);
|
|
78
|
+
}
|
package/util/index.d.ts
CHANGED
package/util/index.js
CHANGED