shelving 1.173.1 → 1.174.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,9 +1,9 @@
1
1
  import { type Schema } from "../schema/Schema.js";
2
2
  import { type Data } from "../util/data.js";
3
- import type { AnyCaller } from "../util/function.js";
4
- import { type RequestMethod, type RequestOptions } from "../util/http.js";
5
- import type { URLString } from "../util/url.js";
6
- import type { EndpointCallback, EndpointHandler } from "./util.js";
3
+ import type { AnyCaller, Arguments } from "../util/function.js";
4
+ import { type RequestHandler, type RequestMethod, type RequestOptions } from "../util/http.js";
5
+ import { type URLString } from "../util/url.js";
6
+ import type { EndpointCallback } from "./util.js";
7
7
  /**
8
8
  * An abstract API resource definition, used to specify types for e.g. serverless functions.
9
9
  *
@@ -23,22 +23,11 @@ export declare class Endpoint<P, R> {
23
23
  readonly result: Schema<R>;
24
24
  constructor(method: RequestMethod, url: URLString, payload: Schema<P>, result: Schema<R>);
25
25
  /**
26
- * Return an `EndpointHandler` for this endpoint.
26
+ * Return an optional request handler for this endpoint.
27
27
  *
28
28
  * @param callback The callback function that implements the logic for this endpoint by receiving the payload and returning the response.
29
29
  */
30
- handler(callback: EndpointCallback<P, R>): EndpointHandler<P, R>;
31
- /**
32
- * Handle a request to this endpoint with a callback implementation, with a given payload and request.
33
- *
34
- * @param callback The endpoint callback function that implements the logic for this endpoint by receiving the payload and returning the response.
35
- * @param unsafePayload The payload to pass into the callback (will be validated against this endpoint's payload schema).
36
- * @param request The entire HTTP request that is being handled (payload was possibly extracted from this somehow).
37
- *
38
- * @throws `string` if the payload is invalid.
39
- * @throws `ValueError` if `callback()` returns an invalid result.
40
- */
41
- handle(callback: EndpointCallback<P, R>, unsafePayload: unknown, request: Request, caller?: AnyCaller): Promise<Response>;
30
+ handler<A extends Arguments = []>(callback: EndpointCallback<P, R, A>): RequestHandler<A>;
42
31
  /**
43
32
  * Render the URL for this endpoint with the given payload.
44
33
  * - URL might contain `{placeholder}` values that are replaced with values from the payload.
@@ -1,10 +1,14 @@
1
+ import { RequestError } from "../error/RequestError.js";
1
2
  import { ResponseError } from "../error/ResponseError.js";
2
3
  import { ValueError } from "../error/ValueError.js";
3
4
  import { UNDEFINED } from "../schema/Schema.js";
4
5
  import { isData } from "../util/data.js";
6
+ import { getDictionary } from "../util/dictionary.js";
5
7
  import { getMessage } from "../util/error.js";
6
- import { getRequest, getResponse, getResponseContent } from "../util/http.js";
7
- import { renderTemplate } from "../util/template.js";
8
+ import { getRequest, getRequestContent, getResponse, getResponseContent, } from "../util/http.js";
9
+ import { isPlainObject } from "../util/object.js";
10
+ import { matchTemplate, renderTemplate } from "../util/template.js";
11
+ import { getURL } from "../util/url.js";
8
12
  /**
9
13
  * An abstract API resource definition, used to specify types for e.g. serverless functions.
10
14
  *
@@ -29,42 +33,27 @@ export class Endpoint {
29
33
  this.result = result;
30
34
  }
31
35
  /**
32
- * Return an `EndpointHandler` for this endpoint.
36
+ * Return an optional request handler for this endpoint.
33
37
  *
34
38
  * @param callback The callback function that implements the logic for this endpoint by receiving the payload and returning the response.
35
39
  */
36
40
  handler(callback) {
37
- return { endpoint: this, callback };
38
- }
39
- /**
40
- * Handle a request to this endpoint with a callback implementation, with a given payload and request.
41
- *
42
- * @param callback The endpoint callback function that implements the logic for this endpoint by receiving the payload and returning the response.
43
- * @param unsafePayload The payload to pass into the callback (will be validated against this endpoint's payload schema).
44
- * @param request The entire HTTP request that is being handled (payload was possibly extracted from this somehow).
45
- *
46
- * @throws `string` if the payload is invalid.
47
- * @throws `ValueError` if `callback()` returns an invalid result.
48
- */
49
- async handle(callback, unsafePayload, request, caller = this.handle) {
50
- // Validate the payload against this endpoint's payload type.
51
- const payload = this.payload.validate(unsafePayload);
52
- // Call the callback with the validated payload to get the result.
53
- const unsafeResult = await callback(payload, request);
54
- try {
55
- // Convert the result to a `Response` object.
56
- return getResponse(this.result.validate(unsafeResult));
57
- }
58
- catch (thrown) {
59
- if (typeof thrown === "string")
60
- throw new ValueError(`Invalid result for ${this.toString()}:\n${thrown}`, {
61
- endpoint: this,
62
- callback,
63
- cause: thrown,
64
- caller,
65
- });
66
- throw thrown;
67
- }
41
+ const handler = (request, ...args) => {
42
+ // Ensure the request method e.g. `GET` matches the endpoint method e.g. `POST`.
43
+ if (request.method !== this.method)
44
+ return undefined;
45
+ // Ensure the request URL e.g. `/user/123` matches the endpoint path e.g. `/user/{id}`.
46
+ // Any `{placeholders}` in the endpoint path are matched against the request URL to extract parameters.
47
+ const url = getURL(request.url);
48
+ if (!url)
49
+ throw new RequestError("Invalid request URL", { received: request.url, caller: handler });
50
+ const { origin, pathname } = url;
51
+ const pathParams = matchTemplate(this.url, `${origin}${pathname}`, handler);
52
+ if (!pathParams)
53
+ return undefined;
54
+ return handleEndpoint(this, callback, request, args, handler);
55
+ };
56
+ return handler;
68
57
  }
69
58
  /**
70
59
  * Render the URL for this endpoint with the given payload.
@@ -127,6 +116,44 @@ export class Endpoint {
127
116
  return `${this.method} ${this.url}`;
128
117
  }
129
118
  }
119
+ /**
120
+ * Handle a request to this endpoint with a callback implementation.
121
+ *
122
+ * @param callback The endpoint callback function that implements the logic for this endpoint by receiving the payload and returning the response.
123
+ * @param request The entire HTTP request that is being handled (payload was possibly extracted from this somehow).
124
+ *
125
+ * @throws `string` if the payload is invalid.
126
+ * @throws `ValueError` if `callback()` returns an invalid result.
127
+ */
128
+ async function handleEndpoint(endpoint, callback, request, args, caller = handleEndpoint) {
129
+ // Parse URL and collect path/query params for payload merging.
130
+ const url = getURL(request.url);
131
+ if (!url)
132
+ throw new RequestError("Invalid request URL", { received: request.url, caller });
133
+ const { origin, pathname, searchParams } = url;
134
+ const pathParams = matchTemplate(endpoint.url, `${origin}${pathname}`, caller);
135
+ if (!pathParams)
136
+ throw new RequestError("Invalid endpoint route", { received: request.url, endpoint, caller });
137
+ const params = searchParams.size ? { ...getDictionary(searchParams), ...pathParams } : pathParams;
138
+ // Extract a data object from the request body and combine it with URL params.
139
+ const content = await getRequestContent(request, caller);
140
+ const unsafePayload = content === undefined ? params : isPlainObject(content) ? { ...content, ...params } : content;
141
+ // Validate the payload against this endpoint's payload type.
142
+ const payload = endpoint.payload.validate(unsafePayload);
143
+ // Call the callback with the validated payload to get the result.
144
+ const unsafeResult = await callback(payload, request, ...args);
145
+ try {
146
+ // Convert the result to a `Response` object.
147
+ return getResponse(endpoint.result.validate(unsafeResult));
148
+ }
149
+ catch (thrown) {
150
+ // If the result returned from the endpoint was invalid, throw an internal `ValueError`
151
+ if (typeof thrown === "string")
152
+ throw new ValueError(`Invalid result for ${endpoint.toString()}:\n${thrown}`, { endpoint, callback, cause: thrown, caller });
153
+ // Otherwise rethrow the error.
154
+ throw thrown;
155
+ }
156
+ }
130
157
  export function HEAD(url, payload = UNDEFINED, result = UNDEFINED) {
131
158
  return new Endpoint("HEAD", url, payload, result);
132
159
  }
@@ -1,40 +1,30 @@
1
- import type { AnyCaller } from "../util/function.js";
2
- import type { Endpoint } from "./Endpoint.js";
1
+ import type { Arguments } from "../util/function.js";
2
+ import type { RequestHandler, RequestHandlers } from "../util/http.js";
3
3
  /**
4
4
  * A function that handles a endpoint request, with a payload and returns a result.
5
5
  *
6
6
  * @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.
7
7
  * - Payload is validated by the payload validator for the `Endpoint`.
8
8
  * - 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.
9
- * - 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.
9
+ * - If payload is _not_ a data object (i.e. it's another JSON type like `string` or `number`) then the payload is that raw body value.
10
10
  *
11
11
  * @param request The raw `Request` object in case it needs any additional processing.
12
+ * @param args Additional arguments passed through `handleEndpoints()`.
12
13
  *
13
14
  * @returns The correct `Result` type for the `Endpoint`, or a raw `Response` object if you wish to return a custom response.
14
15
  */
15
- export type EndpointCallback<P, R> = (payload: P, request: Request) => R | Response | Promise<R | Response>;
16
+ export type EndpointCallback<P, R, A extends Arguments = []> = (payload: P, request: Request, ...args: A) => R | Response | Promise<R | Response>;
16
17
  /**
17
- * Object combining an abstract `Endpoint` and an `EndpointCallback` implementation.
18
+ * List of endpoint handlers that can match and handle requests.
18
19
  */
19
- export interface EndpointHandler<P, R> {
20
- readonly endpoint: Endpoint<P, R>;
21
- readonly callback: EndpointCallback<P, R>;
22
- }
20
+ export type EndpointHandlers<A extends Arguments = []> = ReadonlyArray<RequestHandler<A>>;
23
21
  /**
24
- * Any handler (purposefully as wide as possible for use with `extends X` or `is X` statements).
25
- */
26
- export type AnyEndpointHandler = EndpointHandler<any, any>;
27
- /**
28
- * List of `EndpointHandler` objects objects that can handle requests to an `Endpoint`.
29
- */
30
- export type EndpointHandlers = ReadonlyArray<AnyEndpointHandler>;
31
- /**
32
- * Handler a `Request` with the first matching `EndpointHandlers`.
22
+ * Handle a `Request` with the first matching endpoint handler.
33
23
  *
34
- * 1. Define your `Endpoint` objects with a method, path, payload and result validators, e.g. `GET("/test/{id}", PAYLOAD, STRING)`
35
- * 2. Make an array of `EndpointHandler` objects combining an `Endpoint` with a `callback` function
24
+ * 1. Define your `Endpoint` objects with a method, path, payload and result validators, e.g. `GET("/test/{id}", PAYLOAD, STRING)`.
25
+ * 2. Make an array of endpoint handlers created via `endpoint.handler(callback)`.
36
26
  *
37
27
  * @returns The resulting `Response` from the first handler that matches the `Request`.
38
28
  * @throws `NotFoundError` if no handler matches the `Request`.
39
29
  */
40
- export declare function handleEndpoints(request: Request, endpoints: EndpointHandlers, caller?: AnyCaller): Promise<Response>;
30
+ export declare function handleEndpoints<A extends Arguments = []>(endpoints: RequestHandlers<A>, request: Request, ...args: A): Promise<Response> | Response;
package/endpoint/util.js CHANGED
@@ -1,50 +1,26 @@
1
1
  import { NotFoundError, RequestError } from "../error/RequestError.js";
2
- import { getDictionary } from "../util/dictionary.js";
3
- import { getRequestContent } from "../util/http.js";
4
- import { isPlainObject } from "../util/object.js";
5
- import { matchTemplate } from "../util/template.js";
6
2
  import { getURL } from "../util/url.js";
7
3
  /**
8
- * Handler a `Request` with the first matching `EndpointHandlers`.
4
+ * Handle a `Request` with the first matching endpoint handler.
9
5
  *
10
- * 1. Define your `Endpoint` objects with a method, path, payload and result validators, e.g. `GET("/test/{id}", PAYLOAD, STRING)`
11
- * 2. Make an array of `EndpointHandler` objects combining an `Endpoint` with a `callback` function
6
+ * 1. Define your `Endpoint` objects with a method, path, payload and result validators, e.g. `GET("/test/{id}", PAYLOAD, STRING)`.
7
+ * 2. Make an array of endpoint handlers created via `endpoint.handler(callback)`.
12
8
  *
13
9
  * @returns The resulting `Response` from the first handler that matches the `Request`.
14
10
  * @throws `NotFoundError` if no handler matches the `Request`.
15
11
  */
16
- export function handleEndpoints(request, endpoints, caller = handleEndpoints) {
12
+ export function handleEndpoints(endpoints, request, ...args) {
13
+ const caller = handleEndpoints;
17
14
  // Parse the URL of the request.
18
15
  const url = getURL(request.url);
19
16
  if (!url)
20
17
  throw new RequestError("Invalid request URL", { received: request.url, caller });
21
- const { origin, pathname, searchParams } = url;
22
18
  // Iterate over the handlers and return the first one that matches the request.
23
- for (const { endpoint, callback } of endpoints) {
24
- // Ensure the request method e.g. `GET`, does not match the endpoint method e.g. `POST`
25
- if (request.method !== endpoint.method)
26
- continue;
27
- // Ensure the request URL e.g. `/user/123` matches the endpoint path e.g. `/user/{id}`
28
- // Any `{placeholders}` in the endpoint path are matched against the request URL to extract parameters.
29
- const pathParams = matchTemplate(endpoint.url, `${origin}${pathname}`, caller);
30
- if (!pathParams)
31
- continue;
32
- // Make a simple dictionary object from the `{placeholder}` path params and the `?a=123` query params from the URL.
33
- const combinedParams = searchParams.size ? { ...getDictionary(searchParams), ...pathParams } : pathParams;
34
- // Get the response by calling the callback.
35
- return handleEndpoint(endpoint, callback, combinedParams, request, caller);
19
+ for (const endpoint of endpoints) {
20
+ const response = endpoint(request, ...args);
21
+ if (response)
22
+ return response;
36
23
  }
37
24
  // No handler matched the request.
38
25
  throw new NotFoundError("No matching endpoint", { received: request.url, caller });
39
26
  }
40
- /** Handle an individual call to an endpoint callback. */
41
- async function handleEndpoint(endpoint, callback, params, request, caller = handleEndpoint) {
42
- // Extract a data object from the request body and validate it against the endpoint's payload type.
43
- const content = await getRequestContent(request, caller);
44
- // If content is undefined, it means the request has no body, so params are the only payload.
45
- // - If the content is a plain object, merge if with the params.
46
- // - If the content is anything else (e.g. string, number, array), return it directly (but you'll have no way to access the other params).
47
- const payload = content === undefined ? params : isPlainObject(content) ? { ...content, ...params } : content;
48
- // Call `endpoint.handle()` with the payload and request.
49
- return endpoint.handle(callback, payload, request, caller);
50
- }
package/package.json CHANGED
@@ -11,7 +11,7 @@
11
11
  "state-management",
12
12
  "query-builder"
13
13
  ],
14
- "version": "1.173.1",
14
+ "version": "1.174.0",
15
15
  "repository": {
16
16
  "type": "git",
17
17
  "url": "git+https://github.com/dhoulb/shelving.git"
@@ -48,7 +48,7 @@
48
48
  "test": "bun run test:check && bun run test:types && bun run test:unit",
49
49
  "test:check": "biome check .",
50
50
  "test:types": "tsc --noEmit",
51
- "test:unit": "bun test",
51
+ "test:unit": "bun test --concurrent",
52
52
  "fix": "bun run fix:biome",
53
53
  "fix:biome": "biome check --write .",
54
54
  "build": "bun run build:setup && bun run build:copy && bun run build:emit && bun run build:test:syntax && bun run build:test:unit",
package/util/http.d.ts CHANGED
@@ -1,10 +1,12 @@
1
1
  import { RequestError } from "../error/RequestError.js";
2
2
  import { ResponseError } from "../error/ResponseError.js";
3
3
  import { type Data } from "./data.js";
4
- import type { AnyCaller } from "./function.js";
4
+ import type { AnyCaller, Arguments } from "./function.js";
5
5
  import type { URLString } from "./url.js";
6
- /** A handler function takes a `Request` and returns a `Response` (possibly asynchronously). */
7
- export type RequestHandler = (request: Request) => Response | Promise<Response>;
6
+ /** A handler function takes a `Request` and optional extra arguments, and returns a `Response` (possibly asynchronously) or `undefined` if the request could not be handled. */
7
+ export type RequestHandler<A extends Arguments = []> = (request: Request, ...args: A) => Response | Promise<Response> | undefined;
8
+ /** A list of optional request handlers. */
9
+ export type RequestHandlers<A extends Arguments = []> = Iterable<RequestHandler<A>>;
8
10
  export declare function _getMessageJSON(message: Request | Response, MessageError: typeof RequestError | typeof ResponseError, caller: AnyCaller): Promise<unknown>;
9
11
  export declare function _getMessageFormData(message: Request | Response, MessageError: typeof RequestError | typeof ResponseError, caller: AnyCaller): Promise<unknown>;
10
12
  export declare function _getMessageContent(message: Request | Response, MessageError: typeof RequestError | typeof ResponseError, caller: AnyCaller): Promise<unknown>;