shelving 1.140.0 → 1.141.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.
@@ -1,10 +1,8 @@
1
- import type { AnyCaller } from "../error/BaseError.js";
2
1
  import type { Path } from "../util/path.js";
3
2
  import { type Validator } from "../util/validate.js";
3
+ import type { EndpointCallback, EndpointHandler } from "./util.js";
4
4
  /** Types for an HTTP request or response that does something. */
5
- export type ResourceMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
6
- /** A function that handles a resource request, with a payload and returns a result. */
7
- export type ResourceCallback<P, R> = (payload: P, request: Request) => R | Promise<R>;
5
+ export type EndpointMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
8
6
  /**
9
7
  * An abstract API resource definition, used to specify types for e.g. serverless functions.
10
8
  *
@@ -13,16 +11,16 @@ export type ResourceCallback<P, R> = (payload: P, request: Request) => R | Promi
13
11
  * @param payload A `Validator` for the payload of the resource.
14
12
  * @param result A `Validator` for the result of the resource.
15
13
  */
16
- export declare class Resource<P, R> implements Validator<R> {
17
- /** Resource method. */
18
- readonly method: ResourceMethod;
19
- /** Resource path, e.g. `/patient/{id}` */
14
+ export declare class Endpoint<P, R> implements Validator<R> {
15
+ /** Endpoint method. */
16
+ readonly method: EndpointMethod;
17
+ /** Endpoint path, e.g. `/patient/{id}` */
20
18
  readonly path: Path;
21
19
  /** Payload validator. */
22
20
  readonly payload: Validator<P>;
23
21
  /** Result validator. */
24
22
  readonly result: Validator<R>;
25
- constructor(method: ResourceMethod, path: Path, payload: Validator<P>, result: Validator<R>);
23
+ constructor(method: EndpointMethod, path: Path, payload: Validator<P>, result: Validator<R>);
26
24
  /**
27
25
  * Validate a payload for this resource.
28
26
  *
@@ -34,46 +32,50 @@ export declare class Resource<P, R> implements Validator<R> {
34
32
  * Validate a result for this resource.
35
33
  *
36
34
  * @returns The validated result for this resource.
37
- * @throws ValueError if the result could not be validated.
35
+ * @throws `Feedback` if the value is invalid. `Feedback` instances can be reported safely back to the end client so they know how to fix their request.
38
36
  */
39
- validate(unsafeResult: unknown, caller?: AnyCaller): R;
37
+ validate(unsafeResult: unknown): R;
38
+ /**
39
+ * Return an `EndpointHandler` for this endpoint
40
+ */
41
+ handler(callback: EndpointCallback<P, R>): EndpointHandler<P, R>;
40
42
  }
41
- /** Extract the payload type from a `Resource`. */
42
- export type PayloadType<X extends Resource<unknown, unknown>> = X extends Resource<infer Y, unknown> ? Y : never;
43
- /** Extract the result type from a `Resource`. */
44
- export type ResourceType<X extends Resource<unknown, unknown>> = X extends Resource<unknown, infer Y> ? Y : never;
43
+ /** Extract the payload type from a `Endpoint`. */
44
+ export type PayloadType<X extends Endpoint<unknown, unknown>> = X extends Endpoint<infer Y, unknown> ? Y : never;
45
+ /** Extract the result type from a `Endpoint`. */
46
+ export type EndpointType<X extends Endpoint<unknown, unknown>> = X extends Endpoint<unknown, infer Y> ? Y : never;
45
47
  /**
46
48
  * Represent a GET request to a specified path, with validated payload and return types.
47
49
  * "The GET method requests a representation of the specified resource. Requests using GET should only retrieve data and should not contain a request content."
48
50
  */
49
- export declare function GET<P, R>(path: Path, payload?: Validator<P>, result?: Validator<R>): Resource<P, R>;
50
- export declare function GET<P>(path: Path, payload: Validator<P>): Resource<P, undefined>;
51
- export declare function GET<R>(path: Path, payload: undefined, result: Validator<R>): Resource<undefined, R>;
51
+ export declare function GET<P, R>(path: Path, payload?: Validator<P>, result?: Validator<R>): Endpoint<P, R>;
52
+ export declare function GET<P>(path: Path, payload: Validator<P>): Endpoint<P, undefined>;
53
+ export declare function GET<R>(path: Path, payload: undefined, result: Validator<R>): Endpoint<undefined, R>;
52
54
  /**
53
55
  * Represent a POST request to a specified path, with validated payload and return types.
54
56
  * "The POST method submits an entity to the specified resource, often causing a change in state or side effects on the server.
55
57
  */
56
- export declare function POST<P, R>(path: Path, payload?: Validator<P>, result?: Validator<R>): Resource<P, R>;
57
- export declare function POST<P>(path: Path, payload: Validator<P>): Resource<P, undefined>;
58
- export declare function POST<R>(path: Path, payload: undefined, result: Validator<R>): Resource<undefined, R>;
58
+ export declare function POST<P, R>(path: Path, payload?: Validator<P>, result?: Validator<R>): Endpoint<P, R>;
59
+ export declare function POST<P>(path: Path, payload: Validator<P>): Endpoint<P, undefined>;
60
+ export declare function POST<R>(path: Path, payload: undefined, result: Validator<R>): Endpoint<undefined, R>;
59
61
  /**
60
62
  * Represent a PUT request to a specified path, with validated payload and return types.
61
63
  * "The PUT method replaces all current representations of the target resource with the request content."
62
64
  */
63
- export declare function PUT<P, R>(path: Path, payload?: Validator<P>, result?: Validator<R>): Resource<P, R>;
64
- export declare function PUT<P>(path: Path, payload: Validator<P>): Resource<P, undefined>;
65
- export declare function PUT<R>(path: Path, payload: undefined, result: Validator<R>): Resource<undefined, R>;
65
+ export declare function PUT<P, R>(path: Path, payload?: Validator<P>, result?: Validator<R>): Endpoint<P, R>;
66
+ export declare function PUT<P>(path: Path, payload: Validator<P>): Endpoint<P, undefined>;
67
+ export declare function PUT<R>(path: Path, payload: undefined, result: Validator<R>): Endpoint<undefined, R>;
66
68
  /**
67
69
  * Represent a PATCH request to a specified path, with validated payload and return types.
68
70
  * "The PATCH method applies partial modifications to a resource."
69
71
  */
70
- export declare function PATCH<P, R>(path: Path, payload?: Validator<P>, result?: Validator<R>): Resource<P, R>;
71
- export declare function PATCH<P>(path: Path, payload: Validator<P>): Resource<P, undefined>;
72
- export declare function PATCH<R>(path: Path, payload: undefined, result: Validator<R>): Resource<undefined, R>;
72
+ export declare function PATCH<P, R>(path: Path, payload?: Validator<P>, result?: Validator<R>): Endpoint<P, R>;
73
+ export declare function PATCH<P>(path: Path, payload: Validator<P>): Endpoint<P, undefined>;
74
+ export declare function PATCH<R>(path: Path, payload: undefined, result: Validator<R>): Endpoint<undefined, R>;
73
75
  /**
74
76
  * Represent a DELETE request to a specified path, with validated payload and return types.
75
77
  * "The DELETE method deletes the specified resource."
76
78
  */
77
- export declare function DELETE<P, R>(path: Path, payload?: Validator<P>, result?: Validator<R>): Resource<P, R>;
78
- export declare function DELETE<P>(path: Path, payload: Validator<P>): Resource<P, undefined>;
79
- export declare function DELETE<R>(path: Path, payload: undefined, result: Validator<R>): Resource<undefined, R>;
79
+ export declare function DELETE<P, R>(path: Path, payload?: Validator<P>, result?: Validator<R>): Endpoint<P, R>;
80
+ export declare function DELETE<P>(path: Path, payload: Validator<P>): Endpoint<P, undefined>;
81
+ export declare function DELETE<R>(path: Path, payload: undefined, result: Validator<R>): Endpoint<undefined, R>;
@@ -0,0 +1,64 @@
1
+ import { UNDEFINED } from "../util/validate.js";
2
+ /**
3
+ * An abstract API resource definition, used to specify types for e.g. serverless functions.
4
+ *
5
+ * @param method The method of the resource, e.g. `GET`
6
+ * @param path The path of the resource optionally including `{placeholder}` values, e.g. `/patient/{id}`
7
+ * @param payload A `Validator` for the payload of the resource.
8
+ * @param result A `Validator` for the result of the resource.
9
+ */
10
+ export class Endpoint {
11
+ /** Endpoint method. */
12
+ method;
13
+ /** Endpoint path, e.g. `/patient/{id}` */
14
+ path;
15
+ /** Payload validator. */
16
+ payload;
17
+ /** Result validator. */
18
+ result;
19
+ constructor(method, path, payload, result) {
20
+ this.method = method;
21
+ this.path = path;
22
+ this.payload = payload;
23
+ this.result = result;
24
+ }
25
+ /**
26
+ * Validate a payload for this resource.
27
+ *
28
+ * @returns The validated payload for this resource.
29
+ * @throws `Feedback` if the payload is invalid. `Feedback` instances can be reported safely back to the end client so they know how to fix their request.
30
+ */
31
+ prepare(unsafePayload) {
32
+ return this.payload.validate(unsafePayload);
33
+ }
34
+ /**
35
+ * Validate a result for this resource.
36
+ *
37
+ * @returns The validated result for this resource.
38
+ * @throws `Feedback` if the value is invalid. `Feedback` instances can be reported safely back to the end client so they know how to fix their request.
39
+ */
40
+ validate(unsafeResult) {
41
+ return this.result.validate(unsafeResult);
42
+ }
43
+ /**
44
+ * Return an `EndpointHandler` for this endpoint
45
+ */
46
+ handler(callback) {
47
+ return { endpoint: this, callback };
48
+ }
49
+ }
50
+ export function GET(path, payload, result) {
51
+ return new Endpoint("GET", path, payload || UNDEFINED, result || UNDEFINED);
52
+ }
53
+ export function POST(path, payload, result) {
54
+ return new Endpoint("POST", path, payload || UNDEFINED, result || UNDEFINED);
55
+ }
56
+ export function PUT(path, payload, result) {
57
+ return new Endpoint("PUT", path, payload || UNDEFINED, result || UNDEFINED);
58
+ }
59
+ export function PATCH(path, payload, result) {
60
+ return new Endpoint("PATCH", path, payload || UNDEFINED, result || UNDEFINED);
61
+ }
62
+ export function DELETE(path, payload, result) {
63
+ return new Endpoint("DELETE", path, payload || UNDEFINED, result || UNDEFINED);
64
+ }
package/api/index.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export * from "./Resource.js";
1
+ export * from "./Endpoint.js";
2
2
  export * from "./util.js";
package/api/index.js CHANGED
@@ -1,2 +1,2 @@
1
- export * from "./Resource.js";
1
+ export * from "./Endpoint.js";
2
2
  export * from "./util.js";
package/api/util.d.ts CHANGED
@@ -1,33 +1,32 @@
1
- import { type OptionalHandler } from "../util/http.js";
2
- import type { Resource, ResourceCallback } from "./Resource.js";
1
+ import type { Endpoint } from "./Endpoint.js";
3
2
  /**
4
- * Create a handler that matches an HTTP `Request` against a `Resource`, and returns a `Response` (possibly async) if the request matches, or `undefined` otherwise.
3
+ * A function that handles a endpoint request, with a payload and returns a result.
5
4
  *
6
- * @param resource A `Resource` instance that defines the method, path, and allowed types for the request.
7
- *
8
- * @param callback A callback function that will be called with the validated payload and should return the correct response type for the resource.
9
- * > @param payload is the parse body content of the request.
10
- * > - If the body is parsed as a `Data` object, also adds in path `{placeholders}` and `?query=` parameters in the URL.
11
- *
12
- * @returns An `OptionalHandler` that might handle a given `Request` if it matches the resource's method and path.
13
- * > @returns `undefined` if the request does not match the resource's method or path.
14
- * > @returns `Response` (possibly async) if the callback returns a valid response.
15
- * > @throws `RequestError` if the request matches but the payload from the request is invalid (this is an end user error in the request that needs to be reported back).
16
- * > @throws `ValueError` if the request matches but the value returned by the callback is invalid (this is more likely internal developer error that is reporting something wrong in the callback implementation).
5
+ * @param payload The payload of the request is the result of merging the `{placeholder}` path parameters and `?a=123` query parameters from the URL, with the body of the request.
6
+ * - Payload is validated by the payload validator for the `Endpoint`.
7
+ * - If the body of the `Request` is a data object (i.e. a plain object), then body data is merged with the path and query parameters to form a single flat object.
8
+ * - If payload is _not_ a data object (i.e. it's another JSON type like `string` or `number`) then the payload include the path and query parameters, and a key called `content` that contains the body of the request.
17
9
  */
18
- export declare function createResourceHandler<P, R>(resource: Resource<P, R>, callback: ResourceCallback<P, R>): OptionalHandler;
10
+ export type EndpointCallback<P, R> = (payload: P, request: Request) => R | Promise<R>;
19
11
  /**
20
- * Handle a `Request` to a `Resource` using a `ResourceCallback` that implements the logic for the resource.
21
- *
22
- * @param request The `Request` to handle, which may have a method and URL that match the resource's method and path.
23
- * @param resource A `Resource` instance that defines the method, path, and allowed types for the request.
24
- * @param callback A callback function that will be called with the validated payload and should return the correct response type for the resource.
25
- * > @param payload is the parse body content of the request.
26
- * > - If the body is parsed as a `Data` object, also adds in path `{placeholders}` and `?query=` parameters in the URL.
12
+ * Object combining an abstract `Endpoint` and an `EndpointCallback` implementation.
13
+ */
14
+ export interface EndpointHandler<P, R> {
15
+ readonly endpoint: Endpoint<P, R>;
16
+ readonly callback: EndpointCallback<P, R>;
17
+ }
18
+ /**
19
+ * Any handler (purposefully as wide as possible for use with `extends X` or `is X` statements).
20
+ */
21
+ export type AnyEndpointHandler = EndpointHandler<any, any>;
22
+ /**
23
+ * List of `EndpointHandler` objects objects that can handle requests to an `Endpoint`.
24
+ */
25
+ export type EndpointHandlers = ReadonlyArray<AnyEndpointHandler>;
26
+ /**
27
+ * Handler a `Request` with the first matching `OptionalHandler` in a `Handlers` array.
27
28
  *
28
- * @returns `undefined` if the request does not match the resource's method or path.
29
- * @returns `Response` (possibly async) if the callback returns a valid response.
30
- * @throws `Feedback` if the payload the client user provided is invalid. `Feedback` instances can be reported safely back to the end client so they know how to fix their request.
31
- * @throws `ValueError` if the request matches but the value returned by the callback is invalid (this is an internal error that is reporting something wrong in the callback implementation).
29
+ * @returns The resulting `Response` from the first handler that matches the `Request`.
30
+ * @throws `NotFoundError` if no handler matches the `Request`.
32
31
  */
33
- export declare function handleResourceRequest<P, R>(request: Request, resource: Resource<P, R>, callback: ResourceCallback<P, R>): Response | Promise<Response> | undefined;
32
+ export declare function handleEndpoints(request: Request, endpoints: EndpointHandlers): Promise<Response>;
package/api/util.js CHANGED
@@ -1,65 +1,53 @@
1
- import { RequestError } from "../error/RequestError.js";
2
- import { getRequestData } from "../util/http.js";
1
+ import { NotFoundError, RequestError } from "../error/RequestError.js";
2
+ import { ValueError } from "../error/ValueError.js";
3
+ import { isData } from "../util/data.js";
4
+ import { getDictionary } from "../util/dictionary.js";
5
+ import { getRequestContent } from "../util/http.js";
3
6
  import { matchTemplate } from "../util/template.js";
4
7
  import { getURL } from "../util/url.js";
8
+ import { getValid } from "../util/validate.js";
5
9
  /**
6
- * Create a handler that matches an HTTP `Request` against a `Resource`, and returns a `Response` (possibly async) if the request matches, or `undefined` otherwise.
10
+ * Handler a `Request` with the first matching `OptionalHandler` in a `Handlers` array.
7
11
  *
8
- * @param resource A `Resource` instance that defines the method, path, and allowed types for the request.
9
- *
10
- * @param callback A callback function that will be called with the validated payload and should return the correct response type for the resource.
11
- * > @param payload is the parse body content of the request.
12
- * > - If the body is parsed as a `Data` object, also adds in path `{placeholders}` and `?query=` parameters in the URL.
13
- *
14
- * @returns An `OptionalHandler` that might handle a given `Request` if it matches the resource's method and path.
15
- * > @returns `undefined` if the request does not match the resource's method or path.
16
- * > @returns `Response` (possibly async) if the callback returns a valid response.
17
- * > @throws `RequestError` if the request matches but the payload from the request is invalid (this is an end user error in the request that needs to be reported back).
18
- * > @throws `ValueError` if the request matches but the value returned by the callback is invalid (this is more likely internal developer error that is reporting something wrong in the callback implementation).
19
- */
20
- export function createResourceHandler(resource, callback) {
21
- return request => handleResourceRequest(request, resource, callback);
22
- }
23
- /**
24
- * Handle a `Request` to a `Resource` using a `ResourceCallback` that implements the logic for the resource.
25
- *
26
- * @param request The `Request` to handle, which may have a method and URL that match the resource's method and path.
27
- * @param resource A `Resource` instance that defines the method, path, and allowed types for the request.
28
- * @param callback A callback function that will be called with the validated payload and should return the correct response type for the resource.
29
- * > @param payload is the parse body content of the request.
30
- * > - If the body is parsed as a `Data` object, also adds in path `{placeholders}` and `?query=` parameters in the URL.
31
- *
32
- * @returns `undefined` if the request does not match the resource's method or path.
33
- * @returns `Response` (possibly async) if the callback returns a valid response.
34
- * @throws `Feedback` if the payload the client user provided is invalid. `Feedback` instances can be reported safely back to the end client so they know how to fix their request.
35
- * @throws `ValueError` if the request matches but the value returned by the callback is invalid (this is an internal error that is reporting something wrong in the callback implementation).
12
+ * @returns The resulting `Response` from the first handler that matches the `Request`.
13
+ * @throws `NotFoundError` if no handler matches the `Request`.
36
14
  */
37
- export function handleResourceRequest(request, resource, callback) {
38
- // Ensure the request method e.g. `GET`, does not match the resource method e.g. `POST`
39
- if (request.method !== resource.method)
40
- return undefined;
15
+ export function handleEndpoints(request, endpoints) {
41
16
  // Parse the URL of the request.
42
17
  const url = getURL(request.url);
43
18
  if (!url)
44
- throw new RequestError("Invalid request URL", { received: request.url, caller: handleResourceRequest });
45
- // Ensure the request URL e.g. `/user/123` matches the resource path e.g. `/user/{id}`
46
- // Any `{placeholders}` in the resource path are matched against the request URL to extract parameters.
47
- const params = matchTemplate(url.pathname, resource.path);
48
- if (!params)
49
- return undefined;
50
- // Extract a data object from the request body and validate it against the resource's payload type.
51
- const data = getRequestData(request);
52
- const payload = resource.prepare({ ...params, ...data });
53
- // Return the `Response` that results from calling the callback with the `Request` and the matching params.
54
- return _getHandlerResponse(resource, callback, payload, request);
19
+ throw new RequestError("Invalid request URL", { received: request.url, caller: handleEndpoints });
20
+ const { pathname, searchParams } = url;
21
+ // Iterate over the handlers and return the first one that matches the request.
22
+ for (const { endpoint, callback } of endpoints) {
23
+ // Ensure the request method e.g. `GET`, does not match the endpoint method e.g. `POST`
24
+ if (request.method !== endpoint.method)
25
+ continue;
26
+ // Ensure the request URL e.g. `/user/123` matches the endpoint path e.g. `/user/{id}`
27
+ // Any `{placeholders}` in the endpoint path are matched against the request URL to extract parameters.
28
+ const pathParams = matchTemplate(endpoint.path, pathname, handleEndpoints);
29
+ if (!pathParams)
30
+ continue;
31
+ // Merge the search params and path params.
32
+ const params = searchParams.size ? { ...getDictionary(searchParams), ...pathParams } : pathParams;
33
+ // Get the response by calling the callback.
34
+ return _getResponse(endpoint, callback, params, request);
35
+ }
36
+ throw new NotFoundError("Not found", { request, caller: handleEndpoints });
55
37
  }
56
- /** Internal async function that calls `callback()` asyncronously and validates the result. */
57
- async function _getHandlerResponse(resource, callback, payload, request) {
38
+ async function _getResponse(endpoint, callback, params, request) {
39
+ // Extract a data object from the request body and validate it against the endpoint's payload type.
40
+ const content = await getRequestContent(request, handleEndpoints);
41
+ // If content is undefined, it means the request has no body, so params are the only payload.
42
+ // If the content is a data object merge if with the params.
43
+ // If the content is not a data object (e.g. string, number, array), set a single `content` property and merge it with the params.
44
+ const unsafePayload = content === undefined ? params : isData(content) ? { ...content, ...params } : { content, ...params };
45
+ const payload = endpoint.prepare(unsafePayload);
58
46
  // Call the handler with the validated payload to get the result.
59
47
  const unsafeResult = await callback(payload, request);
60
- // Validate the result against the resource's result type.
48
+ // Validate the result against the endpoint's result type.
61
49
  // Throw a `ValueError` if the result is not valid, which indicates an internal error in the callback implementation.
62
- const result = resource.validate(unsafeResult, _getHandlerResponse);
50
+ const result = getValid(unsafeResult, endpoint, ValueError, handleEndpoints);
63
51
  // Return a new `Response` with a 200 status and the validated result data.
64
52
  return Response.json(result);
65
53
  }
package/package.json CHANGED
@@ -11,7 +11,7 @@
11
11
  "state-management",
12
12
  "query-builder"
13
13
  ],
14
- "version": "1.140.0",
14
+ "version": "1.141.0",
15
15
  "repository": "https://github.com/dhoulb/shelving",
16
16
  "author": "Dave Houlbrooke <dave@shax.com>",
17
17
  "license": "0BSD",
package/util/http.d.ts CHANGED
@@ -1,89 +1,28 @@
1
1
  import type { AnyCaller } from "../error/BaseError.js";
2
2
  import { RequestError } from "../error/RequestError.js";
3
3
  import { ResponseError } from "../error/ResponseError.js";
4
- import type { ImmutableArray } from "./array.js";
5
- import { type Data } from "./data.js";
6
- import type { Optional } from "./optional.js";
7
4
  /** A handler function takes a `Request` and returns a `Response` (possibly asynchronously). */
8
5
  export type Handler = (request: Request) => Response | Promise<Response>;
9
- /** An optional handler function _may_ match a `Request` and return a `Response`, or may return `undefined` if it doesn't match. */
10
- export type OptionalHandler = (request: Request) => Optional<Response | Promise<Response>>;
11
- /** A list of `OptionalHandler` functions that may match a `Request` */
12
- export type Handlers = ImmutableArray<OptionalHandler>;
13
6
  export declare function _getMessageJSON(message: Request | Response, MessageError: typeof RequestError | typeof ResponseError, caller: AnyCaller): Promise<unknown>;
14
- export declare function _getMessageData(message: Request | Response, MessageError: typeof RequestError | typeof ResponseError, caller: AnyCaller): Promise<Data | undefined>;
15
- export declare function _requireMessageData(message: Request | Response, MessageError: typeof RequestError | typeof ResponseError, caller: AnyCaller): Promise<Data>;
7
+ export declare function _getMessageFormData(message: Request | Response, MessageError: typeof RequestError | typeof ResponseError, caller: AnyCaller): Promise<unknown>;
16
8
  export declare function _getMessageContent(message: Request | Response, MessageError: typeof RequestError | typeof ResponseError, caller: AnyCaller): Promise<unknown>;
17
9
  /**
18
- * Parse the content of an HTTP `Request` based as JSON, or throw `RequestError` if the content could not be getd.
19
- *
20
- * @returns string (if the content type is `text/plain`)
21
- * @returns Data If the content can be parsed as a JSON object, or `undefined` if the content was empty.
22
- * @throws RequestError if the content is not `text/plain` or `application/json` with valid JSON.
23
- */
24
- export declare function getRequestJSON(message: Request): Promise<unknown>;
25
- /**
26
- * Parse the content of an HTTP `Response` based as JSON, or throw `ResponseError` if the content could not be getd.
27
- *
28
- * @returns string (if the content type is `text/plain`)
29
- * @returns Data If the content can be parsed as a JSON object, or `undefined` if the content was empty.
30
- * @throws ResponseError if the content is not `text/plain` or `application/json` with valid JSON.
31
- */
32
- export declare function getResponseJSON(message: Response): Promise<unknown>;
33
- /**
34
- * Require the content of an HTTP `Request` as a data object in JSON format, or throw `RequestError` if the content cannot not be parsed or is not a data object in JSON format.
35
- *
36
- * @returns string (if the content type is `text/plain`)
37
- * @returns Data If the content can be parsed as a JSON object, or `undefined` if the content was empty.
38
- * @throws RequestError if the content is not `text/plain` or `application/json` with valid JSON.
39
- */
40
- export declare function getRequestData(message: Request): Promise<Data | undefined>;
41
- /**
42
- * Require the content of an HTTP `Response` as a data object in JSON format, or throw `ResponseError` if the content cannot not be parsed or is not a data object in JSON format.
43
- *
44
- * @returns string (if the content type is `text/plain`)
45
- * @returns Data If the content can be parsed as a JSON object, or `undefined` if the content was empty.
46
- * @throws ResponseError if the content is not `text/plain` or `application/json` with valid JSON.
47
- */
48
- export declare function getResponseData(message: Response): Promise<Data | undefined>;
49
- /**
50
- * Require the content of an HTTP `Request` as a data object in JSON format, or throw `RequestError` if the content cannot not be parsed or is not a data object in JSON format.
51
- *
52
- * @returns string (if the content type is `text/plain`)
53
- * @returns Data If the content can be parsed as a JSON object, or `undefined` if the content was empty.
54
- * @throws RequestError if the content is not `application/json` with valid JSON that parses as a Data object.
55
- */
56
- export declare function requireRequestData(message: Request): Promise<Data>;
57
- /**
58
- * Require the content of an HTTP `Response` as a data object in JSON format, or throw `ResponseError` if the content cannot not be parsed or is not a data object in JSON format.
59
- *
60
- * @returns string (if the content type is `text/plain`)
61
- * @returns Data If the content can be parsed as a JSON object, or `undefined` if the content was empty.
62
- * @throws ResponseError if the content is not `application/json` with valid JSON that parses as a Data object.
63
- */
64
- export declare function requireResponseData(message: Response): Promise<Data>;
65
- /**
66
- * Get the content of an HTTP `Request` based on its content type, or throw `RequestError` if the content could not be parsed.
10
+ * Get the body content of an HTTP `Request` based on its content type, or throw `RequestError` if the content could not be parsed.
67
11
  *
68
12
  * @returns string If content type is `text/plain` (including empty string if it's empty).
69
- * @returns unknown If content type is `application/JSON` and has valid JSON (including `undefined` if the content is empty).
13
+ * @returns unknown If content type is `application/json` and has valid JSON (including `undefined` if the content is empty).
14
+ * @returns unknown If content type is `multipart/form-data` then convert it to a simple `Data` object.
70
15
  *
71
16
  * @throws RequestError if the content is not `text/plain`, or `application/json` with valid JSON.
72
17
  */
73
- export declare function getRequestContent(message: Request): Promise<unknown>;
18
+ export declare function getRequestContent(message: Request, caller?: AnyCaller): Promise<unknown>;
74
19
  /**
75
- * Get the content of an HTTP `Response` based on its content type, or throw `ResponseError` if the content could not be parsed.
20
+ * Get the body content of an HTTP `Response` based on its content type, or throw `ResponseError` if the content could not be parsed.
76
21
  *
77
22
  * @returns string If content type is `text/plain` (including empty string if it's empty).
78
- * @returns unknown If content type is `application/JSON` and has valid JSON (including `undefined` if the content is empty).
23
+ * @returns unknown If content type is `application/json` and has valid JSON (including `undefined` if the content is empty).
24
+ * @returns unknown If content type is `multipart/form-data` then convert it to a simple `Data` object.
79
25
  *
80
26
  * @throws RequestError if the content is not `text/plain` or `application/json` with valid JSON.
81
27
  */
82
- export declare function getResponseContent(message: Response): Promise<unknown>;
83
- /**
84
- * Match a `Request` against a `Handlers` array.
85
- *
86
- * @returns The resulting `Response` from the first handler that matches the `Request`.
87
- * @throws `NotFoundError` if no handler matches the `Request`.
88
- */
89
- export declare function handleRequest(request: Request, handlers: ImmutableArray<OptionalHandler>): Response | Promise<Response>;
28
+ export declare function getResponseContent(message: Response, caller?: AnyCaller): Promise<unknown>;
package/util/http.js CHANGED
@@ -1,6 +1,6 @@
1
- import { NotFoundError, RequestError } from "../error/RequestError.js";
1
+ import { RequestError } from "../error/RequestError.js";
2
2
  import { ResponseError } from "../error/ResponseError.js";
3
- import { isData } from "./data.js";
3
+ import { getDictionary } from "./dictionary.js";
4
4
  export async function _getMessageJSON(message, MessageError, caller) {
5
5
  const trimmed = (await message.text()).trim();
6
6
  if (!trimmed.length)
@@ -12,119 +12,47 @@ export async function _getMessageJSON(message, MessageError, caller) {
12
12
  throw new MessageError("Body must be valid JSON", { received: trimmed, caller });
13
13
  }
14
14
  }
15
- export async function _getMessageData(message, MessageError, caller) {
16
- const data = await _getMessageJSON(message, MessageError, caller);
17
- if (isData(data) || data === undefined)
18
- return data;
19
- throw new MessageError("Body must be data object or undefined", { received: data, caller });
20
- }
21
- export async function _requireMessageData(message, MessageError, caller) {
22
- const data = await _getMessageJSON(message, MessageError, caller);
23
- if (isData(data))
24
- return data;
25
- throw new MessageError("Body must be data object", { received: data, caller });
15
+ export async function _getMessageFormData(message, MessageError, caller) {
16
+ try {
17
+ return getDictionary(await message.formData());
18
+ }
19
+ catch {
20
+ throw new MessageError("Body must be valid valid form multipart data", { caller });
21
+ }
26
22
  }
27
23
  export function _getMessageContent(message, MessageError, caller) {
28
24
  const type = message.headers.get("Content-Type");
29
- if (type?.startsWith("application/json"))
30
- return _getMessageJSON(message, MessageError, caller);
31
25
  if (type?.startsWith("text/plain"))
32
26
  return message.text();
27
+ if (type?.startsWith("application/json"))
28
+ return _getMessageJSON(message, MessageError, caller);
29
+ if (type?.startsWith("multipart/form-data"))
30
+ return _getMessageFormData(message, MessageError, caller);
33
31
  throw new MessageError("Unexpected content type", { received: type, caller });
34
32
  }
35
33
  /**
36
- * Parse the content of an HTTP `Request` based as JSON, or throw `RequestError` if the content could not be getd.
37
- *
38
- * @returns string (if the content type is `text/plain`)
39
- * @returns Data If the content can be parsed as a JSON object, or `undefined` if the content was empty.
40
- * @throws RequestError if the content is not `text/plain` or `application/json` with valid JSON.
41
- */
42
- export function getRequestJSON(message) {
43
- return _getMessageJSON(message, RequestError, getRequestJSON);
44
- }
45
- /**
46
- * Parse the content of an HTTP `Response` based as JSON, or throw `ResponseError` if the content could not be getd.
47
- *
48
- * @returns string (if the content type is `text/plain`)
49
- * @returns Data If the content can be parsed as a JSON object, or `undefined` if the content was empty.
50
- * @throws ResponseError if the content is not `text/plain` or `application/json` with valid JSON.
51
- */
52
- export function getResponseJSON(message) {
53
- return _getMessageJSON(message, ResponseError, getResponseJSON);
54
- }
55
- /**
56
- * Require the content of an HTTP `Request` as a data object in JSON format, or throw `RequestError` if the content cannot not be parsed or is not a data object in JSON format.
57
- *
58
- * @returns string (if the content type is `text/plain`)
59
- * @returns Data If the content can be parsed as a JSON object, or `undefined` if the content was empty.
60
- * @throws RequestError if the content is not `text/plain` or `application/json` with valid JSON.
61
- */
62
- export function getRequestData(message) {
63
- return _getMessageData(message, RequestError, getRequestData);
64
- }
65
- /**
66
- * Require the content of an HTTP `Response` as a data object in JSON format, or throw `ResponseError` if the content cannot not be parsed or is not a data object in JSON format.
67
- *
68
- * @returns string (if the content type is `text/plain`)
69
- * @returns Data If the content can be parsed as a JSON object, or `undefined` if the content was empty.
70
- * @throws ResponseError if the content is not `text/plain` or `application/json` with valid JSON.
71
- */
72
- export function getResponseData(message) {
73
- return _getMessageData(message, ResponseError, getResponseData);
74
- }
75
- /**
76
- * Require the content of an HTTP `Request` as a data object in JSON format, or throw `RequestError` if the content cannot not be parsed or is not a data object in JSON format.
77
- *
78
- * @returns string (if the content type is `text/plain`)
79
- * @returns Data If the content can be parsed as a JSON object, or `undefined` if the content was empty.
80
- * @throws RequestError if the content is not `application/json` with valid JSON that parses as a Data object.
81
- */
82
- export function requireRequestData(message) {
83
- return _requireMessageData(message, RequestError, requireRequestData);
84
- }
85
- /**
86
- * Require the content of an HTTP `Response` as a data object in JSON format, or throw `ResponseError` if the content cannot not be parsed or is not a data object in JSON format.
87
- *
88
- * @returns string (if the content type is `text/plain`)
89
- * @returns Data If the content can be parsed as a JSON object, or `undefined` if the content was empty.
90
- * @throws ResponseError if the content is not `application/json` with valid JSON that parses as a Data object.
91
- */
92
- export function requireResponseData(message) {
93
- return _requireMessageData(message, ResponseError, requireResponseData);
94
- }
95
- /**
96
- * Get the content of an HTTP `Request` based on its content type, or throw `RequestError` if the content could not be parsed.
34
+ * Get the body content of an HTTP `Request` based on its content type, or throw `RequestError` if the content could not be parsed.
97
35
  *
98
36
  * @returns string If content type is `text/plain` (including empty string if it's empty).
99
- * @returns unknown If content type is `application/JSON` and has valid JSON (including `undefined` if the content is empty).
37
+ * @returns unknown If content type is `application/json` and has valid JSON (including `undefined` if the content is empty).
38
+ * @returns unknown If content type is `multipart/form-data` then convert it to a simple `Data` object.
100
39
  *
101
40
  * @throws RequestError if the content is not `text/plain`, or `application/json` with valid JSON.
102
41
  */
103
- export function getRequestContent(message) {
104
- return _getMessageContent(message, RequestError, getRequestContent);
42
+ export function getRequestContent(message, caller = getRequestContent) {
43
+ if (message.method === "GET" || message.method === "HEAD")
44
+ return Promise.resolve(undefined);
45
+ return _getMessageContent(message, RequestError, caller);
105
46
  }
106
47
  /**
107
- * Get the content of an HTTP `Response` based on its content type, or throw `ResponseError` if the content could not be parsed.
48
+ * Get the body content of an HTTP `Response` based on its content type, or throw `ResponseError` if the content could not be parsed.
108
49
  *
109
50
  * @returns string If content type is `text/plain` (including empty string if it's empty).
110
- * @returns unknown If content type is `application/JSON` and has valid JSON (including `undefined` if the content is empty).
51
+ * @returns unknown If content type is `application/json` and has valid JSON (including `undefined` if the content is empty).
52
+ * @returns unknown If content type is `multipart/form-data` then convert it to a simple `Data` object.
111
53
  *
112
54
  * @throws RequestError if the content is not `text/plain` or `application/json` with valid JSON.
113
55
  */
114
- export function getResponseContent(message) {
115
- return _getMessageContent(message, ResponseError, getResponseContent);
116
- }
117
- /**
118
- * Match a `Request` against a `Handlers` array.
119
- *
120
- * @returns The resulting `Response` from the first handler that matches the `Request`.
121
- * @throws `NotFoundError` if no handler matches the `Request`.
122
- */
123
- export function handleRequest(request, handlers) {
124
- for (const handler of handlers) {
125
- const response = handler(request);
126
- if (response)
127
- return response;
128
- }
129
- throw new NotFoundError("Not found", { request, caller: handleRequest });
56
+ export function getResponseContent(message, caller = getResponseContent) {
57
+ return _getMessageContent(message, ResponseError, caller);
130
58
  }
@@ -1,3 +1,4 @@
1
+ import type { AnyCaller } from "../error/BaseError.js";
1
2
  import type { ImmutableArray } from "./array.js";
2
3
  import { type ImmutableDictionary } from "./dictionary.js";
3
4
  import type { NotString } from "./string.js";
@@ -28,9 +29,10 @@ export declare function getPlaceholders(template: string): readonly string[];
28
29
  * @param templates Either a single template string, or an iterator that returns multiple template template strings.
29
30
  * - Template strings can include placeholders (e.g. `:name-${country}/{city}`).
30
31
  * @param target The string containing values, e.g. `Dave-UK/Manchester`
32
+ *
31
33
  * @return An object containing values, e.g. `{ name: "Dave", country: "UK", city: "Manchester" }`, or undefined if target didn't match the template.
32
34
  */
33
- export declare function matchTemplate(template: string, target: string): TemplateDictionary | undefined;
35
+ export declare function matchTemplate(template: string, target: string, caller?: AnyCaller): TemplateDictionary | undefined;
34
36
  /**
35
37
  * Match multiple templates against a target string and return the first match.
36
38
  */
@@ -44,4 +46,4 @@ export declare function matchTemplates(templates: Iterable<string> & NotString,
44
46
  *
45
47
  * @throws {RequiredError} If a placeholder in the template string is not specified in values.
46
48
  */
47
- export declare function renderTemplate(template: string, values: TemplateValues): string;
49
+ export declare function renderTemplate(template: string, values: TemplateValues, caller?: AnyCaller): string;
package/util/template.js CHANGED
@@ -24,7 +24,7 @@ function _splitTemplate(template, caller) {
24
24
  const placeholder = matches[i];
25
25
  const post = matches[i + 1];
26
26
  if (i > 1 && !pre.length)
27
- throw new ValueError("Placeholders must be separated by at least one character", { received: template, caller });
27
+ throw new ValueError("Template placeholders must be separated by at least one character", { received: template, caller });
28
28
  const name = placeholder === "*" ? String(asterisks++) : R_NAME.exec(placeholder)?.[0] || "";
29
29
  chunks.push({ pre, placeholder, name, post });
30
30
  }
@@ -53,11 +53,12 @@ function _getPlaceholder({ name }) {
53
53
  * @param templates Either a single template string, or an iterator that returns multiple template template strings.
54
54
  * - Template strings can include placeholders (e.g. `:name-${country}/{city}`).
55
55
  * @param target The string containing values, e.g. `Dave-UK/Manchester`
56
+ *
56
57
  * @return An object containing values, e.g. `{ name: "Dave", country: "UK", city: "Manchester" }`, or undefined if target didn't match the template.
57
58
  */
58
- export function matchTemplate(template, target) {
59
+ export function matchTemplate(template, target, caller = matchTemplate) {
59
60
  // Get separators and placeholders from template.
60
- const chunks = _splitTemplateCached(template, matchTemplate);
61
+ const chunks = _splitTemplateCached(template, caller);
61
62
  const firstChunk = chunks[0];
62
63
  // Return early if empty.
63
64
  if (!firstChunk)
@@ -101,16 +102,16 @@ export function matchTemplates(templates, target) {
101
102
  *
102
103
  * @throws {RequiredError} If a placeholder in the template string is not specified in values.
103
104
  */
104
- export function renderTemplate(template, values) {
105
- const chunks = _splitTemplateCached(template, renderTemplate);
105
+ export function renderTemplate(template, values, caller = renderTemplate) {
106
+ const chunks = _splitTemplateCached(template, caller);
106
107
  if (!chunks.length)
107
108
  return template;
108
109
  let output = template;
109
110
  for (const { name, placeholder } of chunks)
110
- output = output.replace(placeholder, _replaceTemplateKey(name, values));
111
+ output = output.replace(placeholder, _replaceTemplateKey(name, values, caller));
111
112
  return output;
112
113
  }
113
- function _replaceTemplateKey(key, values) {
114
+ function _replaceTemplateKey(key, values, caller) {
114
115
  if (typeof values === "string")
115
116
  return values;
116
117
  if (typeof values === "function")
@@ -120,5 +121,5 @@ function _replaceTemplateKey(key, values) {
120
121
  if (typeof v === "string")
121
122
  return v;
122
123
  }
123
- throw new RequiredError(`Template key "${key}" must be defined`, { received: values, key, caller: renderTemplate });
124
+ throw new RequiredError(`Template key "${key}" must be defined`, { received: values, key, caller });
124
125
  }
package/api/Resource.js DELETED
@@ -1,67 +0,0 @@
1
- import { ValueError } from "../error/ValueError.js";
2
- import { Feedback } from "../feedback/Feedback.js";
3
- import { UNDEFINED } from "../util/validate.js";
4
- /**
5
- * An abstract API resource definition, used to specify types for e.g. serverless functions.
6
- *
7
- * @param method The method of the resource, e.g. `GET`
8
- * @param path The path of the resource optionally including `{placeholder}` values, e.g. `/patient/{id}`
9
- * @param payload A `Validator` for the payload of the resource.
10
- * @param result A `Validator` for the result of the resource.
11
- */
12
- export class Resource {
13
- /** Resource method. */
14
- method;
15
- /** Resource path, e.g. `/patient/{id}` */
16
- path;
17
- /** Payload validator. */
18
- payload;
19
- /** Result validator. */
20
- result;
21
- constructor(method, path, payload, result) {
22
- this.method = method;
23
- this.path = path;
24
- this.payload = payload;
25
- this.result = result;
26
- }
27
- /**
28
- * Validate a payload for this resource.
29
- *
30
- * @returns The validated payload for this resource.
31
- * @throws `Feedback` if the payload is invalid. `Feedback` instances can be reported safely back to the end client so they know how to fix their request.
32
- */
33
- prepare(unsafePayload) {
34
- return this.payload.validate(unsafePayload);
35
- }
36
- /**
37
- * Validate a result for this resource.
38
- *
39
- * @returns The validated result for this resource.
40
- * @throws ValueError if the result could not be validated.
41
- */
42
- validate(unsafeResult, caller = this.validate) {
43
- try {
44
- return this.result.validate(unsafeResult);
45
- }
46
- catch (thrown) {
47
- if (thrown instanceof Feedback)
48
- throw new ValueError(`Invalid result for resource "${this.path}"`, { received: unsafeResult, caller });
49
- throw thrown;
50
- }
51
- }
52
- }
53
- export function GET(path, payload = UNDEFINED, result = UNDEFINED) {
54
- return new Resource("GET", path, payload, result);
55
- }
56
- export function POST(path, payload = UNDEFINED, result = UNDEFINED) {
57
- return new Resource("POST", path, payload, result);
58
- }
59
- export function PUT(path, payload = UNDEFINED, result = UNDEFINED) {
60
- return new Resource("PUT", path, payload, result);
61
- }
62
- export function PATCH(path, payload = UNDEFINED, result = UNDEFINED) {
63
- return new Resource("PATCH", path, payload, result);
64
- }
65
- export function DELETE(path, payload = UNDEFINED, result = UNDEFINED) {
66
- return new Resource("DELETE", path, payload, result);
67
- }