UNPKG

@zimic/fetch

Version:

Next-gen, TypeScript-first fetch-like API client

418 lines (391 loc) 15.4 kB
import { HttpRequestSchema, HttpMethod, HttpSchema, HttpSchemaPath, HttpSchemaMethod, HttpMethodSchema, HttpResponseSchemaStatusCode, HttpStatusCode, HttpResponse, HttpRequest, HttpSearchParams, HttpHeaders, AllowAnyStringInPathParams, LiteralHttpSchemaPathFromNonLiteral, JSONValue, HttpResponseBodySchema, HttpResponseHeadersSchema, HttpRequestHeadersSchema, HttpHeadersSchema, HttpSearchParamsSchema, } from '@zimic/http'; import { Default, DefaultNoExclude, IfNever, ReplaceBy } from '@zimic/utils/types'; import FetchResponseError, { AnyFetchRequestError } from '../errors/FetchResponseError'; import { JSONStringified } from './json'; import { FetchInput } from './public'; type FetchRequestInitHeaders<RequestSchema extends HttpRequestSchema> = | RequestSchema['headers'] | HttpHeaders<Default<RequestSchema['headers']>>; type FetchRequestInitWithHeaders<RequestSchema extends HttpRequestSchema> = 'headers' extends keyof RequestSchema ? [RequestSchema['headers']] extends [never] ? { headers?: undefined } : undefined extends RequestSchema['headers'] ? { headers?: FetchRequestInitHeaders<RequestSchema> } : { headers: FetchRequestInitHeaders<RequestSchema> } : { headers?: undefined }; type FetchRequestInitSearchParams<RequestSchema extends HttpRequestSchema> = | RequestSchema['searchParams'] | HttpSearchParams<Default<RequestSchema['searchParams']>>; type FetchRequestInitWithSearchParams<RequestSchema extends HttpRequestSchema> = 'searchParams' extends keyof RequestSchema ? [RequestSchema['searchParams']] extends [never] ? { searchParams?: undefined } : undefined extends RequestSchema['searchParams'] ? { searchParams?: FetchRequestInitSearchParams<RequestSchema> } : { searchParams: FetchRequestInitSearchParams<RequestSchema> } : { searchParams?: undefined }; type FetchRequestInitWithBody<RequestSchema extends HttpRequestSchema> = 'body' extends keyof RequestSchema ? [RequestSchema['body']] extends [never] ? { body?: null } : RequestSchema['body'] extends string ? undefined extends RequestSchema['body'] ? { body?: ReplaceBy<RequestSchema['body'], undefined, null> } : { body: RequestSchema['body'] } : RequestSchema['body'] extends JSONValue ? undefined extends RequestSchema['body'] ? { body?: JSONStringified<ReplaceBy<RequestSchema['body'], undefined, null>> } : { body: JSONStringified<RequestSchema['body']> } : undefined extends RequestSchema['body'] ? { body?: ReplaceBy<RequestSchema['body'], undefined, null> } : { body: RequestSchema['body'] } : { body?: null }; type FetchRequestInitPerPath<RequestSchema extends HttpRequestSchema> = FetchRequestInitWithHeaders<RequestSchema> & FetchRequestInitWithSearchParams<RequestSchema> & FetchRequestInitWithBody<RequestSchema>; /** * The options to create a {@link FetchRequest} instance, compatible with * {@link https://developer.mozilla.org/docs/Web/API/RequestInit `RequestInit`}. * * @see {@link https://github.com/zimicjs/zimic/wiki/api‐zimic‐fetch#fetch `fetch` API reference} * @see {@link https://developer.mozilla.org/docs/Web/API/RequestInit `RequestInit`} */ export type FetchRequestInit< Schema extends HttpSchema, Method extends HttpSchemaMethod<Schema>, Path extends HttpSchemaPath<Schema, Method>, Redirect extends RequestRedirect = 'follow', > = Omit<RequestInit, 'method' | 'headers' | 'body'> & { /** The HTTP method of the request. */ method: Method; /** The base URL to prefix the path of the request. */ baseURL?: string; redirect?: Redirect; } & (Path extends Path ? FetchRequestInitPerPath<Default<Default<Schema[Path][Method]>['request']>> : never); export namespace FetchRequestInit { /** The default options for each request sent by a fetch instance. */ export interface Defaults extends Omit<RequestInit, 'headers'> { baseURL: string; /** The HTTP method of the request. */ method?: HttpMethod; /** The headers of the request. */ headers?: HttpHeadersSchema; /** The search parameters of the request. */ searchParams?: HttpSearchParamsSchema; } /** A loosely typed version of {@link FetchRequestInit `FetchRequestInit`}. */ export type Loose = Partial<Defaults>; } type AllFetchResponseStatusCode<MethodSchema extends HttpMethodSchema> = HttpResponseSchemaStatusCode< Default<MethodSchema['response']> >; type FilterFetchResponseStatusCodeByError< StatusCode extends HttpStatusCode, ErrorOnly extends boolean, > = ErrorOnly extends true ? Extract<StatusCode, HttpStatusCode.ClientError | HttpStatusCode.ServerError> : StatusCode; type FilterFetchResponseStatusCodeByRedirect< StatusCode extends HttpStatusCode, Redirect extends RequestRedirect, > = Redirect extends 'error' ? FilterFetchResponseStatusCodeByRedirect<StatusCode, 'follow'> : Redirect extends 'follow' ? Exclude<StatusCode, Exclude<HttpStatusCode.Redirection, 304>> : StatusCode; type FetchResponseStatusCode< MethodSchema extends HttpMethodSchema, ErrorOnly extends boolean, Redirect extends RequestRedirect, > = FilterFetchResponseStatusCodeByRedirect< FilterFetchResponseStatusCodeByError<AllFetchResponseStatusCode<MethodSchema>, ErrorOnly>, Redirect >; type HttpRequestBodySchema<MethodSchema extends HttpMethodSchema> = ReplaceBy< ReplaceBy<IfNever<DefaultNoExclude<Default<MethodSchema['request']>['body']>, null>, undefined, null>, ArrayBuffer, Blob >; /** * A request instance typed with an HTTP schema, closely compatible with the * {@link https://developer.mozilla.org/docs/Web/API/Request native Request class}. * * On top of the properties available in native {@link https://developer.mozilla.org/docs/Web/API/Request `Request`} * instances, fetch requests have their URL automatically prefixed with the base URL of their fetch instance. Default * options are also applied, if present in the fetch instance. * * The path of the request is extracted from the URL, excluding the base URL, and is available in the `path` property. * * @example * import { type HttpSchema } from '@zimic/http'; * import { createFetch } from '@zimic/fetch'; * * interface User { * id: string; * username: string; * } * * type Schema = HttpSchema<{ * '/users': { * POST: { * request: { * headers: { 'content-type': 'application/json' }; * body: { username: string }; * }; * response: { * 201: { body: User }; * }; * }; * }; * }>; * * const fetch = createFetch<Schema>({ * baseURL: 'http://localhost:3000', * }); * * const request = new fetch.Request('/users', { * method: 'POST', * headers: { 'content-type': 'application/json' }, * body: JSON.stringify({ username: 'me' }), * }); * * console.log(request); // FetchRequest<Schema, 'POST', '/users'> * console.log(request.path); // '/users' * * @see {@link https://github.com/zimicjs/zimic/wiki/api‐zimic‐fetch#fetchrequest `FetchRequest` API reference} * @see {@link https://developer.mozilla.org/docs/Web/API/Request} */ export interface FetchRequest< Schema extends HttpSchema, Method extends HttpSchemaMethod<Schema>, Path extends HttpSchemaPath.Literal<Schema, Method>, > extends HttpRequest< HttpRequestBodySchema<Default<Schema[Path][Method]>>, HttpRequestHeadersSchema<Default<Schema[Path][Method]>> > { /** The path of the request, excluding the base URL. */ path: AllowAnyStringInPathParams<Path>; /** The HTTP method of the request. */ method: Method; } export namespace FetchRequest { /** A loosely typed version of a {@link FetchRequest `FetchRequest`}. */ export interface Loose extends Request { /** The path of the request, excluding the base URL. */ path: string; /** The HTTP method of the request. */ method: HttpMethod; /** Clones the request instance, returning a new instance with the same properties. */ clone: () => Loose; } } /** * A plain object representation of a {@link FetchRequest `FetchRequest`}, compatible with JSON. * * If the body is included in the object, it is represented as a string or null if empty. */ export type FetchRequestObject = Pick< FetchRequest.Loose, | 'url' | 'path' | 'method' | 'cache' | 'destination' | 'credentials' | 'integrity' | 'keepalive' | 'mode' | 'redirect' | 'referrer' | 'referrerPolicy' > & { /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Response/headers) */ headers: HttpHeadersSchema; /** * The body of the response, represented as a string or null if empty. * * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/body) */ body?: string | null; }; /** * A {@link FetchResponse `FetchResponse`} instance with a specific status code. * * @see {@link https://github.com/zimicjs/zimic/wiki/api‐zimic‐fetch#fetchresponse `FetchResponse` API reference} * @see {@link https://developer.mozilla.org/docs/Web/API/Response} */ export interface FetchResponsePerStatusCode< Schema extends HttpSchema, Method extends HttpSchemaMethod<Schema>, Path extends HttpSchemaPath.Literal<Schema, Method>, StatusCode extends HttpStatusCode = HttpStatusCode, > extends HttpResponse< HttpResponseBodySchema<Default<Schema[Path][Method]>, StatusCode>, StatusCode, HttpResponseHeadersSchema<Default<Schema[Path][Method]>, StatusCode> > { /** The request that originated the response. */ request: FetchRequest<Schema, Method, Path>; /** * An error representing a response with a failure status code (4XX or 5XX). It can be thrown to handle the error * upper in the call stack. * * If the response has a success status code (1XX, 2XX or 3XX), this property will be null. */ error: StatusCode extends HttpStatusCode.ClientError | HttpStatusCode.ServerError ? FetchResponseError<Schema, Method, Path> : null; } /** * A response instance typed with an HTTP schema, closely compatible with the * {@link https://developer.mozilla.org/docs/Web/API/Response native Response class}. * * On top of the properties available in native Response instances, fetch responses have a reference to the request that * originated them, available in the `request` property. * * If the response has a failure status code (4XX or 5XX), an error is available in the `error` property. * * @example * import { type HttpSchema } from '@zimic/http'; * import { createFetch } from '@zimic/fetch'; * * interface User { * id: string; * username: string; * } * * type Schema = HttpSchema<{ * '/users/:userId': { * GET: { * response: { * 200: { body: User }; * 404: { body: { message: string } }; * }; * }; * }; * }>; * * const fetch = createFetch<Schema>({ * baseURL: 'http://localhost:3000', * }); * * const response = await fetch(`/users/${userId}`, { * method: 'GET', * }); * * console.log(response); // FetchResponse<Schema, 'GET', '/users'> * * if (response.status === 404) { * const errorBody = await response.json(); // { message: string } * console.error(errorBody.message); * return null; * } else { * const user = await response.json(); // User * return user; * } * * @see {@link https://github.com/zimicjs/zimic/wiki/api‐zimic‐fetch#fetchresponse `FetchResponse` API reference} * @see {@link https://developer.mozilla.org/docs/Web/API/Response} */ export type FetchResponse< Schema extends HttpSchema, Method extends HttpSchemaMethod<Schema>, Path extends HttpSchemaPath.Literal<Schema, Method>, ErrorOnly extends boolean = false, Redirect extends RequestRedirect = 'follow', StatusCode extends FetchResponseStatusCode< Default<Schema[Path][Method]>, ErrorOnly, Redirect > = FetchResponseStatusCode<Default<Schema[Path][Method]>, ErrorOnly, Redirect>, > = StatusCode extends StatusCode ? FetchResponsePerStatusCode<Schema, Method, Path, StatusCode> : never; export namespace FetchResponse { /** A loosely typed version of a {@link FetchResponse}. */ export interface Loose extends Response { /** The request that originated the response. */ request: FetchRequest.Loose; /** * An error representing a response with a failure status code (4XX or 5XX). It can be thrown to handle the error * upper in the call stack. * * If the response has a success status code (1XX, 2XX or 3XX), this property will be null. */ error: AnyFetchRequestError | null; /** Clones the request instance, returning a new instance with the same properties. */ clone: () => Loose; } } /** * A plain object representation of a {@link FetchResponse `FetchResponse`}, compatible with JSON. * * If the body is included in the object, it is represented as a string or null if empty. */ export type FetchResponseObject = Pick< FetchResponse.Loose, 'url' | 'type' | 'status' | 'statusText' | 'ok' | 'redirected' > & { /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Response/headers) */ headers: HttpHeadersSchema; /** * The body of the response, represented as a string or null if empty. * * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/body) */ body?: string | null; }; /** * A constructor for {@link FetchRequest} instances, typed with an HTTP schema and compatible with the * {@link https://developer.mozilla.org/docs/Web/API/Request Request class constructor}. * * @example * import { type HttpSchema } from '@zimic/http'; * import { createFetch } from '@zimic/fetch'; * * type Schema = HttpSchema<{ * // ... * }>; * * const fetch = createFetch<Schema>({ * baseURL: 'http://localhost:3000', * }); * * const request = new fetch.Request('POST', '/users', { * body: JSON.stringify({ username: 'me' }), * }); * console.log(request); // FetchRequest<Schema, 'POST', '/users'> * * @param input The resource to fetch, either a path, a URL, or a {@link FetchRequest request}. If a path is provided, it * is automatically prefixed with the base URL of the fetch instance when the request is sent. If a URL or a request * is provided, it is used as is. * @param init The request options. If a path or a URL is provided as the first argument, this argument is required and * should contain at least the method of the request. If the first argument is a {@link FetchRequest request}, this * argument is optional. * @returns A promise that resolves to the response to the request. * @see {@link https://github.com/zimicjs/zimic/wiki/api‐zimic‐fetch#fetchresponse `FetchResponse` API reference} * @see {@link https://developer.mozilla.org/docs/Web/API/Request} */ export type FetchRequestConstructor<Schema extends HttpSchema> = new < Method extends HttpSchemaMethod<Schema>, Path extends HttpSchemaPath.NonLiteral<Schema, Method>, >( input: FetchInput<Schema, Method, Path>, init: FetchRequestInit<Schema, Method, LiteralHttpSchemaPathFromNonLiteral<Schema, Method, Path>>, ) => FetchRequest<Schema, Method, LiteralHttpSchemaPathFromNonLiteral<Schema, Method, Path>>;