shelving
Version:
Toolkit for using data in JavaScript.
82 lines (81 loc) • 4.89 kB
JavaScript
import { ResponseError } from "../../error/ResponseError.js";
import { isData } from "../../util/data.js";
import { getMessage } from "../../util/error.js";
import { assertRequestHeadPayload, createHeadRequest, createRequest, isRequestHeadMethod, mergeRequestOptions, parseResponseBody, } from "../../util/http.js";
import { omitProps } from "../../util/object.js";
import { withURIParams } from "../../util/uri.js";
import { requireBaseURL, requireURL } from "../../util/url.js";
import { APIProvider } from "./APIProvider.js";
/**
* A client-side API provider that sends requests over the network using `fetch()`.
* - Can be used on a server environment to make outgoing API calls, or in a browser environment to call a server API.
* - Renders endpoint paths and query params into the URL and sends body payloads as JSON.
* - Parses JSON responses and throws `ResponseError` for non-2xx responses.
* - Extendable with custom request-building and response-parsing logic by overriding `createRequest()` and `parseResponse()`.
* - Wrap in `ValidationAPIProvider` to add automatic validation of request payloads and response results against endpoint schemas.
*/
export class ClientAPIProvider extends APIProvider {
/** The common base URL for all rendered endpoint requests. */
url;
/** Default options used for HTTP requests created with `this.createRequest()` and `this.fetch()` */
options;
/** Timeout in milliseconds before the request is aborted, or `0` for no timeout. */
timeout;
constructor({ url, options = {}, timeout = 20_000 }) {
super();
this.url = requireBaseURL(url, ClientAPIProvider);
this.options = options;
this.timeout = timeout;
}
renderURL(endpoint, payload, caller = this.renderURL) {
// Construct the full URL from `this.url` and the rendered path.
// Adding the `.` turns the absolute path from `renderPath()` into a relative URL.
// `requireURL()` resolves that path relative to `this.url`
// Note that `requireURL()` rendering treats paths as folders, e.g. in `/a/b/c` the path will be relative to `c` not `b`
const url = requireURL(`.${endpoint.renderPath(payload, caller)}`, this.url, caller);
// HEAD or GET have no body (but payload can only be data object).
if (isRequestHeadMethod(endpoint.method)) {
assertRequestHeadPayload(payload, endpoint.method, caller);
if (payload) {
const params = endpoint.placeholders.length ? omitProps(payload, ...endpoint.placeholders) : payload; // Omit any params that have already been embedded as `{placeholders}`.
return withURIParams(url, params, caller);
}
}
return url;
}
createRequest(endpoint, payload, options, caller = this.createRequest) {
// Render the path into the base URL.
const url = this.renderURL(endpoint, payload, caller);
// Merge the param options with `this.options`
// If we have a timeout set, create an `AbortSignal` for it.
const signal = this.timeout ? AbortSignal.timeout(this.timeout) : null;
const mergedOptions = mergeRequestOptions({ signal, ...this.options }, options);
// HEAD or GET requests need no payload because it was already rendered into the URL as `?query` params by `this.renderURL()`
if (isRequestHeadMethod(endpoint.method))
return this._createHeadRequest(endpoint.method, url, undefined, mergedOptions, caller);
// Body request.
const body = isData(payload) ? omitProps(payload, ...endpoint.placeholders) : payload; // Omit any params that have already been embedded as `{placeholders}`.
return this._createBodyRequest(endpoint.method, url, body, mergedOptions, caller);
}
/** Internal implementation function for `createRequest()` used for requests that have no body. */
_createHeadRequest(method, //
url, params, options, caller) {
return createHeadRequest(method, url, params, options, caller);
}
/** Internal implementation function for `createRequest()` used for requests that have a body. */
_createBodyRequest(method, //
url, payload, options, caller) {
return createRequest(method, url, payload, options, caller);
}
// Override to set default functionality of a client provider to send requests over the network with `fetch()` and parse responses with `parseResponse()`.
async fetch(request) {
return fetch(request);
}
async parseResponse(_endpoint, response, caller = this.parseResponse) {
const { ok, status } = response;
const content = await parseResponseBody(response, caller);
if (!ok)
throw new ResponseError(getMessage(content) ?? `Error ${status}`, { code: status, cause: response, caller });
return content;
}
}