UNPKG

@zimic/fetch

Version:

Next-gen TypeScript-first Fetch client

244 lines (202 loc) 9.25 kB
import { HttpSchema, HttpSchemaMethod, HttpSchemaPath, HttpRequest, HttpRequestBodySchema, HttpRequestHeadersSchema, HttpHeaders, HttpSearchParams, HttpMethod, AllowAnyStringInPathParams, LiteralHttpSchemaPathFromNonLiteral, HttpHeadersInit, HttpSearchParamsInit, HttpHeadersSchema, } from '@zimic/http'; import { Default, PossiblePromise } from '@zimic/utils/types'; import { excludeNonPathParams, joinURL } from '@zimic/utils/url'; import { Fetch, FetchInput } from '../types/public'; import { getOrSetBoundBodyMethod, isBodyMethod, withIncludedBodyIfAvailable } from '../utils/objects'; import { FetchRequestBodySchema, FetchRequestInit, FetchRequestObject, FetchRequestObjectOptions } from './types'; /** @see {@link https://zimic.dev/docs/fetch/api/fetch-request `FetchRequest` API reference} */ export interface FetchRequest< Schema extends HttpSchema, Method extends HttpSchemaMethod<Schema>, Path extends HttpSchemaPath.Literal<Schema, Method>, > extends HttpRequest< HttpRequestBodySchema<Default<Schema[Path][Method]>>, Default<HttpRequestHeadersSchema<Default<Schema[Path][Method]>>> > { raw: Request; path: AllowAnyStringInPathParams<Path>; method: Method; clone: () => FetchRequest<Schema, Method, Path>; /** @see {@link https://zimic.dev/docs/fetch/api/fetch-request#requesttoobject `request.toObject()` API reference} */ toObject: ((options: FetchRequestObjectOptions.WithBody) => Promise<FetchRequestObject>) & ((options?: FetchRequestObjectOptions.WithoutBody) => FetchRequestObject) & ((options?: FetchRequestObjectOptions) => PossiblePromise<FetchRequestObject>); } export namespace FetchRequest { /** A loosely typed version of a {@link FetchRequest `FetchRequest`}. */ export interface Loose extends Request { raw: Request; path: string; method: HttpMethod; clone: () => Loose; /** @see {@link https://zimic.dev/docs/fetch/api/fetch-request#requesttoobject `request.toObject()` API reference} */ toObject: ((options: FetchRequestObjectOptions.WithBody) => Promise<FetchRequestObject>) & ((options?: FetchRequestObjectOptions.WithoutBody) => FetchRequestObject) & ((options?: FetchRequestObjectOptions) => PossiblePromise<FetchRequestObject>); } } interface FetchRequestClass { new < Schema extends HttpSchema, Method extends HttpSchemaMethod<Schema>, Path extends HttpSchemaPath.NonLiteral<Schema, Method>, >( fetch: Fetch<Schema>, input: FetchInput<Schema, Method, Path>, init?: FetchRequestInit<Schema, Method, LiteralHttpSchemaPathFromNonLiteral<Schema, Method, Path>>, ): FetchRequest<Schema, Method, LiteralHttpSchemaPathFromNonLiteral<Schema, Method, Path>>; prototype: Request; // eslint-disable-next-line @typescript-eslint/no-explicit-any [Symbol.hasInstance]: (instance: unknown) => instance is FetchRequest<any, any, any>; } const FETCH_REQUEST_BRAND = Symbol.for('FetchRequest'); const FETCH_REQUEST_EXTRA_PROPERTIES = [FETCH_REQUEST_BRAND, 'raw', 'path', 'toObject'] as const; type FetchRequestExtraProperty = (typeof FETCH_REQUEST_EXTRA_PROPERTIES)[number]; function createFetchRequestClass() { const FetchRequestClass = function FetchRequest< Schema extends HttpSchema, Method extends HttpSchemaMethod<Schema>, Path extends HttpSchemaPath.NonLiteral<Schema, Method>, >( fetch: Fetch<Schema>, input: FetchInput<Schema, Method, Path>, init?: FetchRequestInit<Schema, Method, LiteralHttpSchemaPathFromNonLiteral<Schema, Method, Path>> & { headers?: HttpHeadersInit<Default<Schema[Path][Method]>>; searchParams?: HttpSearchParamsInit<Default<Schema[Path][Method]>>; body?: FetchRequestBodySchema<Default<Default<Schema[Path][Method]>['request']>>; }, ) { let actualInput: URL | Request; const actualInit = { baseURL: init?.baseURL ?? fetch.baseURL, method: init?.method ?? fetch.method, headers: new HttpHeaders(fetch.headers), searchParams: new HttpSearchParams(fetch.searchParams), body: (init?.body ?? fetch.body) as BodyInit | null, mode: init?.mode ?? fetch.mode, cache: init?.cache ?? fetch.cache, credentials: init?.credentials ?? fetch.credentials, integrity: init?.integrity ?? fetch.integrity, keepalive: init?.keepalive ?? fetch.keepalive, priority: init?.priority ?? fetch.priority, redirect: init?.redirect ?? fetch.redirect, referrer: init?.referrer ?? fetch.referrer, referrerPolicy: init?.referrerPolicy ?? fetch.referrerPolicy, signal: init?.signal ?? fetch.signal, window: init?.window === undefined ? fetch.window : init.window, duplex: init?.duplex ?? fetch.duplex, }; if (init?.headers !== undefined) { actualInit.headers.assign(new HttpHeaders(init.headers)); } let url: URL; const baseURL = new URL(actualInit.baseURL); if (input instanceof Request) { const request = input as Request; actualInit.headers.assign(new HttpHeaders<FetchRequestInit.DefaultHeaders<Schema>>(request.headers)); url = new URL(input.url); actualInput = request instanceof FetchRequestClass ? request.raw : request; } else { url = new URL(input instanceof URL ? input : joinURL(baseURL, input)); actualInit.searchParams.assign( new HttpSearchParams<FetchRequestInit.DefaultSearchParams<Schema>>(url.searchParams), ); if (init?.searchParams !== undefined) { actualInit.searchParams.assign(new HttpSearchParams(init.searchParams)); } url.search = actualInit.searchParams.toString(); actualInput = url; } const request = new Request(actualInput, actualInit); const baseURLWithoutTrailingSlash = baseURL.toString().replace(/\/$/, ''); const path = excludeNonPathParams(url).toString().replace(baseURLWithoutTrailingSlash, ''); function clone() { return new FetchRequestClass(fetch, request.clone() as FetchInput<Schema, Method, Path>); } function toObject(options: { includeBody: true }): Promise<FetchRequestObject>; function toObject(options?: { includeBody?: false }): FetchRequestObject; function toObject(options?: { includeBody?: boolean }): PossiblePromise<FetchRequestObject>; function toObject(options?: { includeBody?: boolean }): PossiblePromise<FetchRequestObject> { const requestObject: FetchRequestObject = { url: request.url, path, method: request.method as HttpMethod, headers: HttpHeaders.prototype.toObject.call(request.headers) as HttpHeadersSchema, cache: request.cache, destination: request.destination, credentials: request.credentials, integrity: request.integrity, keepalive: request.keepalive, mode: request.mode, redirect: request.redirect, referrer: request.referrer, referrerPolicy: request.referrerPolicy, }; if (!options?.includeBody) { return requestObject; } return withIncludedBodyIfAvailable(request, requestObject); } type FetchRequestInstance = FetchRequest<Schema, Method, LiteralHttpSchemaPathFromNonLiteral<Schema, Method, Path>>; const fetchRequest = new Proxy(request, { get(target, property) { if (property === FETCH_REQUEST_BRAND) { return true; } if (property === ('raw' satisfies keyof FetchRequestInstance)) { return request satisfies FetchRequestInstance['raw']; } if (property === ('path' satisfies keyof FetchRequestInstance)) { return path as AllowAnyStringInPathParams< LiteralHttpSchemaPathFromNonLiteral<Schema, Method, Path> > satisfies FetchRequestInstance['path']; } if (property === ('clone' satisfies keyof FetchRequestInstance)) { return clone satisfies FetchRequestInstance['clone']; } if (property === ('toObject' satisfies keyof FetchRequestInstance)) { return toObject satisfies FetchRequestInstance['toObject']; } // Fallback other properties to the original `Request` instance. const value = Reflect.get(target, property, target) as unknown; if (isBodyMethod(property, value)) { return getOrSetBoundBodyMethod(request, property, value); } return value; }, has(target, property) { return ( FETCH_REQUEST_EXTRA_PROPERTIES.includes(property as FetchRequestExtraProperty) || Reflect.has(target, property) ); }, }) as unknown as FetchRequest<Schema, Method, LiteralHttpSchemaPathFromNonLiteral<Schema, Method, Path>>; return fetchRequest; } as unknown as FetchRequestClass; Object.defineProperty(FetchRequestClass, Symbol.hasInstance, { value(instance: unknown): boolean { return instance instanceof Request && FETCH_REQUEST_BRAND in instance && instance[FETCH_REQUEST_BRAND] === true; }, writable: false, enumerable: false, configurable: false, }); Object.setPrototypeOf(FetchRequestClass.prototype, Request.prototype); return FetchRequestClass; } export const FetchRequest = createFetchRequestClass();