shelving 1.182.1 → 1.184.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/README.md +1 -1
- package/api/cache/APICache.d.ts +8 -8
- package/api/cache/EndpointCache.d.ts +2 -2
- package/api/endpoint/util.js +2 -2
- package/api/index.d.ts +2 -0
- package/api/index.js +2 -0
- package/api/provider/APIProvider.d.ts +23 -22
- package/api/provider/APIProvider.js +0 -75
- package/api/provider/ClientAPIProvider.d.ts +38 -0
- package/api/provider/ClientAPIProvider.js +68 -0
- package/api/provider/DebugAPIProvider.d.ts +2 -2
- package/api/provider/JSONAPIProvider.d.ts +16 -0
- package/api/provider/JSONAPIProvider.js +23 -0
- package/api/provider/MockAPIProvider.d.ts +9 -18
- package/api/provider/MockAPIProvider.js +17 -14
- package/api/provider/MockEndpointAPIProvider.d.ts +4 -10
- package/api/provider/MockEndpointAPIProvider.js +2 -2
- package/api/provider/ThroughAPIProvider.d.ts +8 -8
- package/api/provider/ThroughAPIProvider.js +1 -4
- package/api/provider/ValidationAPIProvider.d.ts +2 -2
- package/api/provider/ValidationAPIProvider.js +1 -4
- package/api/provider/XMLAPIProvider.d.ts +17 -0
- package/api/provider/XMLAPIProvider.js +22 -0
- package/api/store/EndpointStore.d.ts +2 -2
- package/package.json +6 -6
- package/react/createAPIContext.d.ts +5 -4
- package/util/http.d.ts +109 -24
- package/util/http.js +162 -45
- package/util/index.d.ts +1 -0
- package/util/index.js +1 -0
- package/util/object.js +2 -0
- package/util/xml.d.ts +30 -0
- package/util/xml.js +54 -0
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Shelving
|
|
2
2
|
|
|
3
|
-
[](https://conventionalcommits.org) [](https://conventionalcommits.org) [](https://github.com/dhoulb/shelving/actions/workflows/release.yaml) [](https://www.npmjs.com/package/shelving)
|
|
4
4
|
|
|
5
5
|
Shelving is a TypeScript toolkit for working with typed data. At its core it is a schema validation library — every schema has a `validate()` method that returns a typed value or throws a human-readable error. On top of that it provides a database provider abstraction, an API provider abstraction, observable state stores, React integration, and a large set of typed utility functions.
|
|
6
6
|
|
package/api/cache/APICache.d.ts
CHANGED
|
@@ -5,20 +5,20 @@ import { EndpointCache } from "./EndpointCache.js";
|
|
|
5
5
|
* Cache of `EndpointCache` objects for multiple endpoints.
|
|
6
6
|
* - Use `get(endpoint)` to retrieve or create the `EndpointCache` for a given endpoint, then `get(payload)` on that to get a specific `EndpointStore`.
|
|
7
7
|
*/
|
|
8
|
-
export declare class APICache implements Disposable {
|
|
8
|
+
export declare class APICache<P, R> implements Disposable {
|
|
9
9
|
private readonly _caches;
|
|
10
|
-
readonly provider: APIProvider
|
|
11
|
-
constructor(provider: APIProvider);
|
|
10
|
+
readonly provider: APIProvider<P, R>;
|
|
11
|
+
constructor(provider: APIProvider<P, R>);
|
|
12
12
|
private _get;
|
|
13
13
|
/** Get (or create) the `EndpointCache` for the given endpoint. */
|
|
14
|
-
get<P, R>(endpoint: Endpoint<
|
|
14
|
+
get<PP extends P, RR extends R>(endpoint: Endpoint<PP, RR>): EndpointCache<PP, RR>;
|
|
15
15
|
/** Invalidate a specific store for an endpoint. */
|
|
16
|
-
invalidate<P, R>(endpoint: Endpoint<
|
|
16
|
+
invalidate<PP extends P, RR extends R>(endpoint: Endpoint<PP, RR>, payload: PP): void;
|
|
17
17
|
/** Invalidate all stores for an endpoint. */
|
|
18
|
-
invalidateAll<P, R>(endpoint: Endpoint<
|
|
18
|
+
invalidateAll<PP extends P, RR extends R>(endpoint: Endpoint<PP, RR>): void;
|
|
19
19
|
/** Trigger a refetch on a specific store for an endpoint. */
|
|
20
|
-
refetch<P, R>(endpoint: Endpoint<
|
|
20
|
+
refetch<PP extends P, RR extends R>(endpoint: Endpoint<PP, RR>, payload: PP): void;
|
|
21
21
|
/** Trigger a refetch on all stores for an endpoint. */
|
|
22
|
-
refetchAll<P, R>(endpoint: Endpoint<
|
|
22
|
+
refetchAll<PP extends P, RR extends R>(endpoint: Endpoint<PP, RR>): void;
|
|
23
23
|
[Symbol.dispose](): void;
|
|
24
24
|
}
|
|
@@ -9,8 +9,8 @@ import { EndpointStore } from "../store/EndpointStore.js";
|
|
|
9
9
|
export declare class EndpointCache<P = unknown, R = unknown> implements Disposable {
|
|
10
10
|
private readonly _stores;
|
|
11
11
|
readonly endpoint: Endpoint<P, R>;
|
|
12
|
-
readonly provider: APIProvider
|
|
13
|
-
constructor(endpoint: Endpoint<P, R>, provider: APIProvider);
|
|
12
|
+
readonly provider: APIProvider<P, R>;
|
|
13
|
+
constructor(endpoint: Endpoint<P, R>, provider: APIProvider<P, R>);
|
|
14
14
|
/** Get (or create) the `EndpointStore` for the given payload. */
|
|
15
15
|
get(payload: P, caller?: AnyCaller): EndpointStore<P, R>;
|
|
16
16
|
/** Invalidate a specific store. */
|
package/api/endpoint/util.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { MethodNotAllowedError, NotFoundError } from "../../error/RequestError.js";
|
|
2
2
|
import { ValueError } from "../../error/ValueError.js";
|
|
3
3
|
import { getDictionary } from "../../util/dictionary.js";
|
|
4
|
-
import {
|
|
4
|
+
import { getResponse, isRequestMethod, parseRequestBody } from "../../util/http.js";
|
|
5
5
|
import { isPlainObject } from "../../util/object.js";
|
|
6
6
|
import { requireURL } from "../../util/url.js";
|
|
7
7
|
export function handleEndpoints(base, handlers, request, context, caller = handleEndpoints) {
|
|
@@ -30,7 +30,7 @@ export function handleEndpoints(base, handlers, request, context, caller = handl
|
|
|
30
30
|
async function _handleEndpoint({ endpoint, callback },
|
|
31
31
|
/** Params we already matched/parsed from the URL. */
|
|
32
32
|
params, request, context, caller) {
|
|
33
|
-
const content = await
|
|
33
|
+
const content = await parseRequestBody(request, caller);
|
|
34
34
|
const unsafePayload = content === undefined ? params : isPlainObject(content) ? { ...content, ...params } : content;
|
|
35
35
|
const payload = endpoint.payload.validate(unsafePayload);
|
|
36
36
|
const unsafeResult = await callback(payload, request, context);
|
package/api/index.d.ts
CHANGED
|
@@ -4,8 +4,10 @@ export * from "./endpoint/Endpoint.js";
|
|
|
4
4
|
export * from "./endpoint/util.js";
|
|
5
5
|
export * from "./provider/APIProvider.js";
|
|
6
6
|
export * from "./provider/DebugAPIProvider.js";
|
|
7
|
+
export * from "./provider/JSONAPIProvider.js";
|
|
7
8
|
export * from "./provider/MockAPIProvider.js";
|
|
8
9
|
export * from "./provider/MockEndpointAPIProvider.js";
|
|
9
10
|
export * from "./provider/ThroughAPIProvider.js";
|
|
10
11
|
export * from "./provider/ValidationAPIProvider.js";
|
|
12
|
+
export * from "./provider/XMLAPIProvider.js";
|
|
11
13
|
export * from "./store/EndpointStore.js";
|
package/api/index.js
CHANGED
|
@@ -4,8 +4,10 @@ export * from "./endpoint/Endpoint.js";
|
|
|
4
4
|
export * from "./endpoint/util.js";
|
|
5
5
|
export * from "./provider/APIProvider.js";
|
|
6
6
|
export * from "./provider/DebugAPIProvider.js";
|
|
7
|
+
export * from "./provider/JSONAPIProvider.js";
|
|
7
8
|
export * from "./provider/MockAPIProvider.js";
|
|
8
9
|
export * from "./provider/MockEndpointAPIProvider.js";
|
|
9
10
|
export * from "./provider/ThroughAPIProvider.js";
|
|
10
11
|
export * from "./provider/ValidationAPIProvider.js";
|
|
12
|
+
export * from "./provider/XMLAPIProvider.js";
|
|
11
13
|
export * from "./store/EndpointStore.js";
|
|
@@ -1,21 +1,11 @@
|
|
|
1
1
|
import type { AnyCaller } from "../../util/function.js";
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
2
|
+
import type { RequestOptions } from "../../util/http.js";
|
|
3
|
+
import type { URL, URLString } from "../../util/url.js";
|
|
4
4
|
import type { Endpoint } from "../endpoint/Endpoint.js";
|
|
5
|
-
/** Options for an `APIProvider`. */
|
|
6
|
-
export interface APIProviderOptions {
|
|
7
|
-
/** The common base URL for all rendered endpoint requests. */
|
|
8
|
-
readonly url: PossibleURL;
|
|
9
|
-
/** Options used for HTTP requests created with `this.getRequest()` and `this.fetch()` */
|
|
10
|
-
readonly options?: RequestOptions;
|
|
11
|
-
}
|
|
12
5
|
/** Provider for API endpoints rooted at a common base URL. */
|
|
13
|
-
export declare class APIProvider {
|
|
14
|
-
/** The
|
|
15
|
-
readonly url: URLString;
|
|
16
|
-
/** Default options used for HTTP requests created with `this.getRequest()` and `this.fetch()` */
|
|
17
|
-
readonly options: RequestOptions;
|
|
18
|
-
constructor({ url, options }: APIProviderOptions);
|
|
6
|
+
export declare abstract class APIProvider<P = unknown, R = unknown> {
|
|
7
|
+
/** The base URL for this API. */
|
|
8
|
+
abstract readonly url: URLString;
|
|
19
9
|
/**
|
|
20
10
|
* Render the full final URL for an API request to a given endpoint with a given payload.
|
|
21
11
|
* - Includes `?query` params if this is a `HEAD` or `GET` request.
|
|
@@ -23,22 +13,33 @@ export declare class APIProvider {
|
|
|
23
13
|
* @throws {RequiredError} if this endpoint's path has `{placeholders}` but `payload` is not a data object.
|
|
24
14
|
* @throws {RequiredError} if this is a `HEAD` or `GET` request but `payload` is not a data object.
|
|
25
15
|
*/
|
|
26
|
-
renderURL<P, R>(endpoint: Endpoint<
|
|
16
|
+
abstract renderURL<PP extends P, RR extends R>(endpoint: Endpoint<PP, RR>, payload: PP, caller?: AnyCaller): URL;
|
|
27
17
|
/**
|
|
28
18
|
* Create a `Request` that targets this endpoint with a given base URL.
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
* -
|
|
19
|
+
*
|
|
20
|
+
* @param payload The payload to embed into the `Request` to send to the endpoint.
|
|
21
|
+
* - Path `{placeholders}` are rendered from `payload`
|
|
22
|
+
* - For `GET` and `HEAD`, remaining `payload` fields are appended as `?query` params.
|
|
23
|
+
* - For all other requests, `payload` is sent as the body.
|
|
24
|
+
*
|
|
25
|
+
* @param options The `RequestOptions` to use when creating the `Request`
|
|
26
|
+
* - Merges `options` with `this.options` to make the final request options.
|
|
27
|
+
*
|
|
28
|
+
* @returns The created request.
|
|
29
|
+
* - Merges `options` with `this.options` to make the final request options.
|
|
30
|
+
* - Includes an `AbortSignal` based on `this.timeout` if it's set to a number in milliseconds.
|
|
31
|
+
* - The timeout `AbortSignal` is merged with any manual signal set in `
|
|
32
32
|
*
|
|
33
33
|
* @throws {RequiredError} if this endpoint's path has `{placeholders}` but `payload` is not a data object.
|
|
34
34
|
* @throws {RequiredError} if this is a `HEAD` or `GET` request but `payload` is not a data object.
|
|
35
35
|
*/
|
|
36
|
-
getRequest<P, R>(endpoint: Endpoint<
|
|
36
|
+
abstract getRequest<PP extends P, RR extends R>(endpoint: Endpoint<PP, RR>, payload: PP, options?: RequestOptions, caller?: AnyCaller): Request;
|
|
37
37
|
/**
|
|
38
38
|
* Parse an HTTP `Response` for this endpoint.
|
|
39
39
|
* - Non-2xx responses become `ResponseError`.
|
|
40
40
|
* - Does not validate the result against the endpoint schema — use `ValidationAPIProvider` for that.
|
|
41
41
|
*/
|
|
42
|
-
parseResponse<P, R>(_endpoint: Endpoint<
|
|
43
|
-
|
|
42
|
+
abstract parseResponse<PP extends P, RR extends R>(_endpoint: Endpoint<PP, RR>, response: Response, caller?: AnyCaller): Promise<RR>;
|
|
43
|
+
/** Send a payload to an `Endpoint` and retrieve the result. */
|
|
44
|
+
abstract fetch<PP extends P, RR extends R>(endpoint: Endpoint<PP, RR>, payload: PP, options?: RequestOptions, caller?: AnyCaller): Promise<RR>;
|
|
44
45
|
}
|
|
@@ -1,78 +1,3 @@
|
|
|
1
|
-
import { ResponseError } from "../../error/ResponseError.js";
|
|
2
|
-
import { isArrayItem } from "../../util/array.js";
|
|
3
|
-
import { getMessage } from "../../util/error.js";
|
|
4
|
-
import { assertHeadMethodPayload, getRequest, getResponseContent, HTTP_HEAD_METHODS, mergeRequestOptions, } from "../../util/http.js";
|
|
5
|
-
import { omitProps } from "../../util/object.js";
|
|
6
|
-
import { withURIParams } from "../../util/uri.js";
|
|
7
|
-
import { requireBaseURL, requireURL } from "../../util/url.js";
|
|
8
1
|
/** Provider for API endpoints rooted at a common base URL. */
|
|
9
2
|
export class APIProvider {
|
|
10
|
-
/** The common base URL for all rendered endpoint requests. */
|
|
11
|
-
url;
|
|
12
|
-
/** Default options used for HTTP requests created with `this.getRequest()` and `this.fetch()` */
|
|
13
|
-
options;
|
|
14
|
-
constructor({ url, options = {} }) {
|
|
15
|
-
this.url = requireBaseURL(url, undefined, APIProvider);
|
|
16
|
-
this.options = options;
|
|
17
|
-
}
|
|
18
|
-
/**
|
|
19
|
-
* Render the full final URL for an API request to a given endpoint with a given payload.
|
|
20
|
-
* - Includes `?query` params if this is a `HEAD` or `GET` request.
|
|
21
|
-
*
|
|
22
|
-
* @throws {RequiredError} if this endpoint's path has `{placeholders}` but `payload` is not a data object.
|
|
23
|
-
* @throws {RequiredError} if this is a `HEAD` or `GET` request but `payload` is not a data object.
|
|
24
|
-
*/
|
|
25
|
-
renderURL(endpoint, payload, caller = this.renderURL) {
|
|
26
|
-
const url = requireURL(`.${endpoint.renderPath(payload, caller)}`, this.url, caller);
|
|
27
|
-
// HEAD or GET have no body (but payload can only be data object).
|
|
28
|
-
if (isArrayItem(HTTP_HEAD_METHODS, endpoint.method)) {
|
|
29
|
-
assertHeadMethodPayload(payload, endpoint.method, caller);
|
|
30
|
-
if (payload) {
|
|
31
|
-
const params = endpoint.placeholders.length ? omitProps(payload, ...endpoint.placeholders) : payload; // Omit any params that were already embedded as `{placeholders}`
|
|
32
|
-
return withURIParams(url, params, caller);
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
return url;
|
|
36
|
-
}
|
|
37
|
-
/**
|
|
38
|
-
* Create a `Request` that targets this endpoint with a given base URL.
|
|
39
|
-
* - Path `{placeholders}` are rendered from the payload.
|
|
40
|
-
* - For `GET` and `HEAD`, remaining payload fields are appended as `?query` params.
|
|
41
|
-
* - For all other requests, payload is sent as the body.
|
|
42
|
-
*
|
|
43
|
-
* @throws {RequiredError} if this endpoint's path has `{placeholders}` but `payload` is not a data object.
|
|
44
|
-
* @throws {RequiredError} if this is a `HEAD` or `GET` request but `payload` is not a data object.
|
|
45
|
-
*/
|
|
46
|
-
getRequest(endpoint, payload, options, caller = this.getRequest) {
|
|
47
|
-
// Render the path into the base URL.
|
|
48
|
-
const url = this.renderURL(endpoint, payload, caller);
|
|
49
|
-
// HEAD or GET requests have no payload because it was already rendered into the URL as `?query` params.
|
|
50
|
-
if (isArrayItem(HTTP_HEAD_METHODS, endpoint.method)) {
|
|
51
|
-
return getRequest(endpoint.method, url, undefined, options);
|
|
52
|
-
}
|
|
53
|
-
// Placeholders are rendered into the path so get omitted from the body payload.
|
|
54
|
-
if (endpoint.placeholders.length) {
|
|
55
|
-
const params = omitProps(payload, ...endpoint.placeholders); // Omit any params that were already embedded as `{placeholders}`
|
|
56
|
-
return getRequest(endpoint.method, url, params, options);
|
|
57
|
-
}
|
|
58
|
-
// No placeholders.
|
|
59
|
-
return getRequest(endpoint.method, url, payload, options);
|
|
60
|
-
}
|
|
61
|
-
/**
|
|
62
|
-
* Parse an HTTP `Response` for this endpoint.
|
|
63
|
-
* - Non-2xx responses become `ResponseError`.
|
|
64
|
-
* - Does not validate the result against the endpoint schema — use `ValidationAPIProvider` for that.
|
|
65
|
-
*/
|
|
66
|
-
async parseResponse(_endpoint, response, caller = this.parseResponse) {
|
|
67
|
-
const { ok, status } = response;
|
|
68
|
-
const content = await getResponseContent(response, caller);
|
|
69
|
-
if (!ok)
|
|
70
|
-
throw new ResponseError(getMessage(content) ?? `Error ${status}`, { code: status, cause: response, caller });
|
|
71
|
-
return content;
|
|
72
|
-
}
|
|
73
|
-
async fetch(endpoint, payload, options, caller = this.fetch) {
|
|
74
|
-
const request = this.getRequest(endpoint, payload, mergeRequestOptions(this.options, options), caller);
|
|
75
|
-
const response = await fetch(request);
|
|
76
|
-
return this.parseResponse(endpoint, response, caller);
|
|
77
|
-
}
|
|
78
3
|
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { AnyCaller } from "../../util/function.js";
|
|
2
|
+
import { type RequestBodyMethod, type RequestHeadMethod, type RequestOptions } from "../../util/http.js";
|
|
3
|
+
import type { Nullish } from "../../util/null.js";
|
|
4
|
+
import { type PossibleURIParams } from "../../util/uri.js";
|
|
5
|
+
import { type PossibleURL, type URL, type URLString } from "../../util/url.js";
|
|
6
|
+
import type { Endpoint } from "../endpoint/Endpoint.js";
|
|
7
|
+
import type { APIProvider } from "./APIProvider.js";
|
|
8
|
+
export declare class ClientAPIProvider<P = unknown, R = unknown> implements APIProvider<P, R> {
|
|
9
|
+
/** The common base URL for all rendered endpoint requests. */
|
|
10
|
+
readonly url: URLString;
|
|
11
|
+
/** Default options used for HTTP requests created with `this.getRequest()` and `this.fetch()` */
|
|
12
|
+
readonly options: RequestOptions;
|
|
13
|
+
/** Timeout in milliseconds, or `undefined` for no timeout. */
|
|
14
|
+
readonly timeout: number | undefined;
|
|
15
|
+
constructor({ url, options, timeout }: ClientAPIProviderOptions);
|
|
16
|
+
renderURL<PP extends P, RR extends R>(endpoint: Endpoint<PP, RR>, payload: PP, caller?: AnyCaller): URL;
|
|
17
|
+
getRequest<PP extends P, RR extends R>(endpoint: Endpoint<PP, RR>, payload: PP, options?: RequestOptions, caller?: AnyCaller): Request;
|
|
18
|
+
/** Internal implementation function for `getRequest()` used for requests that have no body. */
|
|
19
|
+
protected _getHeadRequest(method: RequestHeadMethod, //
|
|
20
|
+
url: PossibleURL, params: Nullish<PossibleURIParams>, options: RequestOptions, caller: AnyCaller): Request;
|
|
21
|
+
/** Internal implementation function for `getRequest()` used for requests that have a body. */
|
|
22
|
+
protected _getBodyRequest(method: RequestBodyMethod, //
|
|
23
|
+
url: PossibleURL, payload: P, options: RequestOptions, caller: AnyCaller): Request;
|
|
24
|
+
parseResponse<PP extends P, RR extends R>(_endpoint: Endpoint<PP, RR>, response: Response, caller?: AnyCaller): Promise<RR>;
|
|
25
|
+
fetch<PP extends P, RR extends R>(endpoint: Endpoint<PP, RR>, payload: PP, options?: RequestOptions, caller?: AnyCaller): Promise<RR>;
|
|
26
|
+
}
|
|
27
|
+
/** Options for an `APIProvider`. */
|
|
28
|
+
export interface ClientAPIProviderOptions {
|
|
29
|
+
/** The common base URL for all rendered endpoint requests. */
|
|
30
|
+
readonly url: PossibleURL;
|
|
31
|
+
/**
|
|
32
|
+
* Options used for HTTP requests created with `this.getRequest()` and `this.fetch()`
|
|
33
|
+
* - Omits `signal` because it's not relevant at the provider level.
|
|
34
|
+
*/
|
|
35
|
+
readonly options?: Omit<RequestOptions, "signal">;
|
|
36
|
+
/** Timeout in milliseconds, or `undefined` for no timeout. */
|
|
37
|
+
readonly timeout?: number | undefined;
|
|
38
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { ResponseError } from "../../error/ResponseError.js";
|
|
2
|
+
import { isData } from "../../util/data.js";
|
|
3
|
+
import { getMessage } from "../../util/error.js";
|
|
4
|
+
import { assertRequestHeadPayload, getHeadRequest, getRequest, isRequestHeadMethod, mergeRequestOptions, parseResponseBody, } from "../../util/http.js";
|
|
5
|
+
import { omitProps } from "../../util/object.js";
|
|
6
|
+
import { withURIParams } from "../../util/uri.js";
|
|
7
|
+
import { requireBaseURL, requireURL } from "../../util/url.js";
|
|
8
|
+
export class ClientAPIProvider {
|
|
9
|
+
/** The common base URL for all rendered endpoint requests. */
|
|
10
|
+
url;
|
|
11
|
+
/** Default options used for HTTP requests created with `this.getRequest()` and `this.fetch()` */
|
|
12
|
+
options;
|
|
13
|
+
/** Timeout in milliseconds, or `undefined` for no timeout. */
|
|
14
|
+
timeout;
|
|
15
|
+
constructor({ url, options = {}, timeout }) {
|
|
16
|
+
this.url = requireBaseURL(url, undefined, ClientAPIProvider);
|
|
17
|
+
this.options = options;
|
|
18
|
+
this.timeout = timeout;
|
|
19
|
+
}
|
|
20
|
+
renderURL(endpoint, payload, caller = this.renderURL) {
|
|
21
|
+
const url = requireURL(`.${endpoint.renderPath(payload, caller)}`, this.url, caller);
|
|
22
|
+
// HEAD or GET have no body (but payload can only be data object).
|
|
23
|
+
if (isRequestHeadMethod(endpoint.method)) {
|
|
24
|
+
assertRequestHeadPayload(payload, endpoint.method, caller);
|
|
25
|
+
if (payload) {
|
|
26
|
+
const params = endpoint.placeholders.length ? omitProps(payload, ...endpoint.placeholders) : payload; // Omit any params that have already been embedded as `{placeholders}`.
|
|
27
|
+
return withURIParams(url, params, caller);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return url;
|
|
31
|
+
}
|
|
32
|
+
getRequest(endpoint, payload, options, caller = this.getRequest) {
|
|
33
|
+
// Render the path into the base URL.
|
|
34
|
+
const url = this.renderURL(endpoint, payload, caller);
|
|
35
|
+
// Merge the param options with `this.options`
|
|
36
|
+
// If we have a timeout set, create an `AbortSignal` for it.
|
|
37
|
+
const signal = this.timeout ? AbortSignal.timeout(this.timeout) : null;
|
|
38
|
+
const mergedOptions = mergeRequestOptions({ signal, ...this.options }, options);
|
|
39
|
+
// HEAD or GET requests need no payload because it was already rendered into the URL as `?query` params by `this.renderURL()`
|
|
40
|
+
if (isRequestHeadMethod(endpoint.method))
|
|
41
|
+
return this._getHeadRequest(endpoint.method, url, undefined, mergedOptions, caller);
|
|
42
|
+
// Body request.
|
|
43
|
+
const body = isData(payload) ? omitProps(payload, ...endpoint.placeholders) : payload; // Omit any params that have already been embedded as `{placeholders}`.
|
|
44
|
+
return this._getBodyRequest(endpoint.method, url, body, mergedOptions, caller);
|
|
45
|
+
}
|
|
46
|
+
/** Internal implementation function for `getRequest()` used for requests that have no body. */
|
|
47
|
+
_getHeadRequest(method, //
|
|
48
|
+
url, params, options, caller) {
|
|
49
|
+
return getHeadRequest(method, url, params, options, caller);
|
|
50
|
+
}
|
|
51
|
+
/** Internal implementation function for `getRequest()` used for requests that have a body. */
|
|
52
|
+
_getBodyRequest(method, //
|
|
53
|
+
url, payload, options, caller) {
|
|
54
|
+
return getRequest(method, url, payload, options, caller);
|
|
55
|
+
}
|
|
56
|
+
async parseResponse(_endpoint, response, caller = this.parseResponse) {
|
|
57
|
+
const { ok, status } = response;
|
|
58
|
+
const content = await parseResponseBody(response, caller);
|
|
59
|
+
if (!ok)
|
|
60
|
+
throw new ResponseError(getMessage(content) ?? `Error ${status}`, { code: status, cause: response, caller });
|
|
61
|
+
return content;
|
|
62
|
+
}
|
|
63
|
+
async fetch(endpoint, payload, options, caller = this.fetch) {
|
|
64
|
+
const request = this.getRequest(endpoint, payload, options, caller);
|
|
65
|
+
const response = await fetch(request);
|
|
66
|
+
return this.parseResponse(endpoint, response, caller);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -3,6 +3,6 @@ import type { RequestOptions } from "../../util/http.js";
|
|
|
3
3
|
import type { Endpoint } from "../endpoint/Endpoint.js";
|
|
4
4
|
import { ThroughAPIProvider } from "./ThroughAPIProvider.js";
|
|
5
5
|
/** Provider that logs API operations to the console. */
|
|
6
|
-
export declare class DebugAPIProvider extends ThroughAPIProvider {
|
|
7
|
-
fetch<P, R>(endpoint: Endpoint<
|
|
6
|
+
export declare class DebugAPIProvider<P, R> extends ThroughAPIProvider<P, R> {
|
|
7
|
+
fetch<PP extends P, RR extends R>(endpoint: Endpoint<PP, RR>, payload: PP, options?: RequestOptions, caller?: AnyCaller): Promise<RR>;
|
|
8
8
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { AnyCaller } from "../../util/function.js";
|
|
2
|
+
import { type RequestBodyMethod, type RequestOptions } from "../../util/http.js";
|
|
3
|
+
import type { PossibleURL } from "../../util/url.js";
|
|
4
|
+
import type { Endpoint } from "../endpoint/Endpoint.js";
|
|
5
|
+
import { ClientAPIProvider } from "./ClientAPIProvider.js";
|
|
6
|
+
/** API provider that always sends request bodies as JSON and parses responses as JSON. */
|
|
7
|
+
export declare class JSONAPIProvider<P = unknown, R = unknown> extends ClientAPIProvider<P, R> {
|
|
8
|
+
protected _getBodyRequest(method: RequestBodyMethod, url: PossibleURL, payload: P, options: RequestOptions, caller: AnyCaller): Request;
|
|
9
|
+
/**
|
|
10
|
+
* Parse a JSON `Response` for an endpoint.
|
|
11
|
+
*
|
|
12
|
+
* - Non-2xx responses become `ResponseError`.
|
|
13
|
+
* - JSON is parsed even if the server omitted or mis-set the response content type.
|
|
14
|
+
*/
|
|
15
|
+
parseResponse<PP extends P, RR extends R>(_endpoint: Endpoint<PP, RR>, response: Response, caller?: AnyCaller): Promise<RR>;
|
|
16
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { ResponseError } from "../../error/ResponseError.js";
|
|
2
|
+
import { getMessage } from "../../util/error.js";
|
|
3
|
+
import { getJSONRequest, parseResponseJSON } from "../../util/http.js";
|
|
4
|
+
import { ClientAPIProvider } from "./ClientAPIProvider.js";
|
|
5
|
+
/** API provider that always sends request bodies as JSON and parses responses as JSON. */
|
|
6
|
+
export class JSONAPIProvider extends ClientAPIProvider {
|
|
7
|
+
_getBodyRequest(method, url, payload, options, caller) {
|
|
8
|
+
return getJSONRequest(method, url, payload, options, caller);
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Parse a JSON `Response` for an endpoint.
|
|
12
|
+
*
|
|
13
|
+
* - Non-2xx responses become `ResponseError`.
|
|
14
|
+
* - JSON is parsed even if the server omitted or mis-set the response content type.
|
|
15
|
+
*/
|
|
16
|
+
async parseResponse(_endpoint, response, caller = this.parseResponse) {
|
|
17
|
+
const { ok, status } = response;
|
|
18
|
+
const content = await parseResponseJSON(response, caller);
|
|
19
|
+
if (!ok)
|
|
20
|
+
throw new ResponseError(getMessage(content) ?? `Error ${status}`, { code: status, cause: response, caller });
|
|
21
|
+
return content;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -1,34 +1,25 @@
|
|
|
1
1
|
import type { AnyCaller } from "../../util/function.js";
|
|
2
|
-
import {
|
|
2
|
+
import type { RequestHandler, RequestOptions } from "../../util/http.js";
|
|
3
3
|
import type { AnyEndpoint, Endpoint } from "../endpoint/Endpoint.js";
|
|
4
|
-
import {
|
|
4
|
+
import { ClientAPIProvider } from "./ClientAPIProvider.js";
|
|
5
|
+
import { ThroughAPIProvider } from "./ThroughAPIProvider.js";
|
|
5
6
|
/** A structured log entry emitted by `MockAPIProvider` for one of its provider operations. */
|
|
6
7
|
export type MockAPICall = {
|
|
7
8
|
readonly type: "fetch";
|
|
8
9
|
readonly endpoint: AnyEndpoint;
|
|
9
|
-
readonly options: RequestOptions;
|
|
10
10
|
readonly payload: unknown;
|
|
11
11
|
readonly request: Request;
|
|
12
12
|
readonly response: Response;
|
|
13
13
|
readonly result: unknown;
|
|
14
14
|
};
|
|
15
15
|
/**
|
|
16
|
-
*
|
|
17
|
-
* -
|
|
16
|
+
* Provider that logs API calls without sending network requests.
|
|
17
|
+
* - Extends `ThroughAPIProvider` to delegate request building and response parsing to a source `APIProvider`.
|
|
18
|
+
* - The source provider's `fetch()` is never called — this provider intercepts all fetches and routes them through a `RequestHandler`.
|
|
18
19
|
*/
|
|
19
|
-
export
|
|
20
|
-
/** Optional URL, defaults to `"https://api.mock.com"` */
|
|
21
|
-
url?: APIProviderOptions["url"];
|
|
22
|
-
}
|
|
23
|
-
/** Provider that logs API calls without sending network requests. */
|
|
24
|
-
export declare class MockAPIProvider extends APIProvider {
|
|
20
|
+
export declare class MockAPIProvider<P = unknown, R = unknown> extends ThroughAPIProvider<P, R> {
|
|
25
21
|
readonly calls: MockAPICall[];
|
|
26
22
|
readonly handler: RequestHandler;
|
|
27
|
-
constructor(handler
|
|
28
|
-
|
|
29
|
-
* Log a `fetch()` call without using the network.
|
|
30
|
-
* - If `getResult` is configured, its return value is returned as-is (no schema validation).
|
|
31
|
-
* - Otherwise `undefined` is returned.
|
|
32
|
-
*/
|
|
33
|
-
fetch<P, R>(endpoint: Endpoint<P, R>, payload: P, _options?: RequestOptions, caller?: AnyCaller): Promise<R>;
|
|
23
|
+
constructor(handler?: RequestHandler, source?: ClientAPIProvider<P, R>);
|
|
24
|
+
fetch<PP extends P, RR extends R>(endpoint: Endpoint<PP, RR>, payload: PP, options?: RequestOptions, caller?: AnyCaller): Promise<RR>;
|
|
34
25
|
}
|
|
@@ -1,24 +1,27 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
/**
|
|
4
|
-
|
|
1
|
+
import { ClientAPIProvider } from "./ClientAPIProvider.js";
|
|
2
|
+
import { ThroughAPIProvider } from "./ThroughAPIProvider.js";
|
|
3
|
+
/** Default handler just echoes back the input request as text. */
|
|
4
|
+
async function _passthroughHandler(request) {
|
|
5
|
+
return new Response(await request.text());
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Provider that logs API calls without sending network requests.
|
|
9
|
+
* - Extends `ThroughAPIProvider` to delegate request building and response parsing to a source `APIProvider`.
|
|
10
|
+
* - The source provider's `fetch()` is never called — this provider intercepts all fetches and routes them through a `RequestHandler`.
|
|
11
|
+
*/
|
|
12
|
+
export class MockAPIProvider extends ThroughAPIProvider {
|
|
5
13
|
calls = [];
|
|
6
14
|
handler;
|
|
7
|
-
constructor(handler, { url
|
|
8
|
-
super(
|
|
15
|
+
constructor(handler = _passthroughHandler, source = new ClientAPIProvider({ url: "https://api.mock.com" })) {
|
|
16
|
+
super(source);
|
|
9
17
|
this.handler = handler;
|
|
10
18
|
}
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
* - If `getResult` is configured, its return value is returned as-is (no schema validation).
|
|
14
|
-
* - Otherwise `undefined` is returned.
|
|
15
|
-
*/
|
|
16
|
-
async fetch(endpoint, payload, _options = {}, caller = this.fetch) {
|
|
17
|
-
const options = mergeRequestOptions(this.options, _options);
|
|
19
|
+
// Log a `fetch()` call without using the network.
|
|
20
|
+
async fetch(endpoint, payload, options = {}, caller = this.fetch) {
|
|
18
21
|
const request = this.getRequest(endpoint, payload, options, caller);
|
|
19
22
|
const response = await this.handler(request);
|
|
20
23
|
const result = await this.parseResponse(endpoint, response, caller);
|
|
21
|
-
this.calls.push({ type: "fetch", endpoint, payload,
|
|
24
|
+
this.calls.push({ type: "fetch", endpoint, payload, request, response, result });
|
|
22
25
|
return result;
|
|
23
26
|
}
|
|
24
27
|
}
|
|
@@ -1,12 +1,6 @@
|
|
|
1
1
|
import { type EndpointHandlers } from "../endpoint/util.js";
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
* Construction options for a `MockAPIProvider`
|
|
5
|
-
* - Same as options for a normal `MockAPIProviderOptions`, but with a `context` property for the endpoints.
|
|
6
|
-
*/
|
|
7
|
-
export interface MockEndpointAPIProviderOptions<C = void> extends MockAPIProviderOptions {
|
|
8
|
-
context: C;
|
|
9
|
-
}
|
|
2
|
+
import type { ClientAPIProvider } from "./ClientAPIProvider.js";
|
|
3
|
+
import { MockAPIProvider } from "./MockAPIProvider.js";
|
|
10
4
|
/**
|
|
11
5
|
* Provider that mocks an API that calls and matches an array of `EndpointHandler` objects returned from `Endpoint.handler()`
|
|
12
6
|
* - Used to test server-side API code, calls against an API made up of multiple `Endpoint` instances.
|
|
@@ -18,6 +12,6 @@ export interface MockEndpointAPIProviderOptions<C = void> extends MockAPIProvide
|
|
|
18
12
|
* const result = await api.fetch(endpoint, 4); // Mock a call to the endpoint through the provider.
|
|
19
13
|
* expect(result).toBe(16);
|
|
20
14
|
*/
|
|
21
|
-
export declare class MockEndpointAPIProvider<C> extends MockAPIProvider {
|
|
22
|
-
constructor(handlers: EndpointHandlers<C>,
|
|
15
|
+
export declare class MockEndpointAPIProvider<P, R, C> extends MockAPIProvider<P, R> {
|
|
16
|
+
constructor(handlers: EndpointHandlers<C>, context: C, source?: ClientAPIProvider<P, R>);
|
|
23
17
|
}
|
|
@@ -12,7 +12,7 @@ import { MockAPIProvider } from "./MockAPIProvider.js";
|
|
|
12
12
|
* expect(result).toBe(16);
|
|
13
13
|
*/
|
|
14
14
|
export class MockEndpointAPIProvider extends MockAPIProvider {
|
|
15
|
-
constructor(handlers,
|
|
16
|
-
super(request => handleEndpoints(this.url, handlers, request, context),
|
|
15
|
+
constructor(handlers, context, source) {
|
|
16
|
+
super(request => handleEndpoints(this.url, handlers, request, context, this.fetch), source);
|
|
17
17
|
}
|
|
18
18
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { AnyCaller } from "../../util/function.js";
|
|
2
2
|
import type { RequestOptions } from "../../util/http.js";
|
|
3
|
+
import type { Sourceable } from "../../util/source.js";
|
|
3
4
|
import type { URL, URLString } from "../../util/url.js";
|
|
4
5
|
import type { Endpoint } from "../endpoint/Endpoint.js";
|
|
5
6
|
import type { APIProvider } from "./APIProvider.js";
|
|
@@ -7,13 +8,12 @@ import type { APIProvider } from "./APIProvider.js";
|
|
|
7
8
|
* Provider wrapper that delegates API operations to a source provider.
|
|
8
9
|
* - Extend this when you want to intercept only selected API operations, such as injecting auth headers or logging.
|
|
9
10
|
*/
|
|
10
|
-
export declare class ThroughAPIProvider implements APIProvider {
|
|
11
|
-
readonly source: APIProvider;
|
|
11
|
+
export declare class ThroughAPIProvider<P, R> implements APIProvider<P, R>, Sourceable<APIProvider<P, R>> {
|
|
12
12
|
get url(): URLString;
|
|
13
|
-
|
|
14
|
-
constructor(source: APIProvider);
|
|
15
|
-
renderURL<P, R>(endpoint: Endpoint<
|
|
16
|
-
getRequest<P, R>(endpoint: Endpoint<
|
|
17
|
-
parseResponse<P, R>(endpoint: Endpoint<
|
|
18
|
-
fetch<P, R>(endpoint: Endpoint<
|
|
13
|
+
readonly source: APIProvider<P, R>;
|
|
14
|
+
constructor(source: APIProvider<P, R>);
|
|
15
|
+
renderURL<PP extends P, RR extends R>(endpoint: Endpoint<PP, RR>, payload: PP, caller?: AnyCaller): URL;
|
|
16
|
+
getRequest<PP extends P, RR extends R>(endpoint: Endpoint<PP, RR>, payload: PP, options?: RequestOptions, caller?: AnyCaller): Request;
|
|
17
|
+
parseResponse<PP extends P, RR extends R>(endpoint: Endpoint<PP, RR>, response: Response, caller?: AnyCaller): Promise<RR>;
|
|
18
|
+
fetch<PP extends P, RR extends R>(endpoint: Endpoint<PP, RR>, payload: PP, options?: RequestOptions, caller?: AnyCaller): Promise<RR>;
|
|
19
19
|
}
|
|
@@ -3,13 +3,10 @@
|
|
|
3
3
|
* - Extend this when you want to intercept only selected API operations, such as injecting auth headers or logging.
|
|
4
4
|
*/
|
|
5
5
|
export class ThroughAPIProvider {
|
|
6
|
-
source;
|
|
7
6
|
get url() {
|
|
8
7
|
return this.source.url;
|
|
9
8
|
}
|
|
10
|
-
|
|
11
|
-
return this.source.options;
|
|
12
|
-
}
|
|
9
|
+
source;
|
|
13
10
|
constructor(source) {
|
|
14
11
|
this.source = source;
|
|
15
12
|
}
|
|
@@ -3,6 +3,6 @@ import type { RequestOptions } from "../../util/http.js";
|
|
|
3
3
|
import type { Endpoint } from "../endpoint/Endpoint.js";
|
|
4
4
|
import { ThroughAPIProvider } from "./ThroughAPIProvider.js";
|
|
5
5
|
/** Validate an asynchronous source provider (source can have any type because validation guarantees the type). */
|
|
6
|
-
export declare class ValidationAPIProvider extends ThroughAPIProvider {
|
|
7
|
-
fetch<P, R>(endpoint: Endpoint<
|
|
6
|
+
export declare class ValidationAPIProvider<P, R> extends ThroughAPIProvider<P, R> {
|
|
7
|
+
fetch<PP extends P, RR extends R>(endpoint: Endpoint<PP, RR>, payload: PP, options?: RequestOptions, caller?: AnyCaller): Promise<RR>;
|
|
8
8
|
}
|
|
@@ -4,16 +4,13 @@ import { ThroughAPIProvider } from "./ThroughAPIProvider.js";
|
|
|
4
4
|
export class ValidationAPIProvider extends ThroughAPIProvider {
|
|
5
5
|
async fetch(endpoint, payload, options, caller = this.fetch) {
|
|
6
6
|
// Validate payload — let thrown strings bubble up as user-readable messages for e.g. form handlers.
|
|
7
|
-
const validPayload =
|
|
7
|
+
const validPayload = endpoint.payload.validate(payload);
|
|
8
8
|
// Call through to source (raw transport, no schema validation).
|
|
9
9
|
const content = await this.source.fetch(endpoint, validPayload, options, caller);
|
|
10
10
|
// Validate result — wrap in ResponseError as this is a server/transport problem, not user error.
|
|
11
11
|
return _validateResult(endpoint, content, caller);
|
|
12
12
|
}
|
|
13
13
|
}
|
|
14
|
-
function _validatePayload(endpoint, payload) {
|
|
15
|
-
return endpoint.payload.validate(payload);
|
|
16
|
-
}
|
|
17
14
|
function _validateResult(endpoint, content, caller) {
|
|
18
15
|
try {
|
|
19
16
|
return endpoint.result.validate(content);
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Data } from "../../util/data.js";
|
|
2
|
+
import type { AnyCaller } from "../../util/function.js";
|
|
3
|
+
import { type RequestBodyMethod, type RequestOptions } from "../../util/http.js";
|
|
4
|
+
import type { PossibleURL } from "../../util/url.js";
|
|
5
|
+
import type { Endpoint } from "../endpoint/Endpoint.js";
|
|
6
|
+
import { ClientAPIProvider } from "./ClientAPIProvider.js";
|
|
7
|
+
/** API provider that always sends request bodies as XML and parses responses as plain text. */
|
|
8
|
+
export declare class XMLAPIProvider<P extends Data = Data, R = string> extends ClientAPIProvider<P, R> {
|
|
9
|
+
protected _getBodyRequest(method: RequestBodyMethod, url: PossibleURL, payload: P, options: RequestOptions, caller: AnyCaller): Request;
|
|
10
|
+
/**
|
|
11
|
+
* Parse a text `Response` for an endpoint.
|
|
12
|
+
*
|
|
13
|
+
* - Non-2xx responses become `ResponseError`.
|
|
14
|
+
* - The response body is always returned as raw text.
|
|
15
|
+
*/
|
|
16
|
+
parseResponse<PP extends P, RR extends R>(_endpoint: Endpoint<PP, RR>, response: Response, caller?: AnyCaller): Promise<RR>;
|
|
17
|
+
}
|