UNPKG

shelving

Version:

Toolkit for using data in JavaScript.

82 lines (81 loc) 4.89 kB
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; } }