UNPKG

@data-client/rest

Version:

Quickly define typed REST resources and endpoints

677 lines (641 loc) 22.6 kB
import type { EndpointExtraOptions, EndpointInstanceInterface, Schema, FetchFunction, ResolveType, } from '@data-client/endpoint'; import type { ExtractCollection } from './extractCollection.js'; import { OptionsToBodyArgument, OptionsToFunction, } from './OptionsToFunction.js'; import { PathArgs } from './pathTypes.js'; import { EndpointUpdateFunction } from './RestEndpointTypeHelp.js'; export interface RestInstanceBase< F extends FetchFunction = FetchFunction, S extends Schema | undefined = any, M extends boolean | undefined = boolean | undefined, O extends { path: string; body?: any; searchParams?: any; method?: string; } = { path: string }, > extends EndpointInstanceInterface<F, S, M> { /** @see https://dataclient.io/rest/api/RestEndpoint#body */ readonly body?: 'body' extends keyof O ? O['body'] : any; /** @see https://dataclient.io/rest/api/RestEndpoint#searchParams */ readonly searchParams?: 'searchParams' extends keyof O ? O['searchParams'] : // unknown is identity with '&' type operator unknown; /** Pattern to construct url based on Url Parameters * @see https://dataclient.io/rest/api/RestEndpoint#path */ readonly path: O['path']; /** Prepended to all urls * @see https://dataclient.io/rest/api/RestEndpoint#urlPrefix */ readonly urlPrefix: string; readonly requestInit: RequestInit; /** HTTP request method * @see https://dataclient.io/rest/api/RestEndpoint#method */ readonly method: (O & { method: string })['method']; readonly signal: AbortSignal | undefined; /** @see https://dataclient.io/rest/api/RestEndpoint#paginationField */ readonly paginationField?: string; /* fetch lifecycles */ /* before-fetch */ /** Builds the URL to fetch * @see https://dataclient.io/rest/api/RestEndpoint#url */ url(...args: Parameters<F>): string; /** Encode the searchParams component of the url * @see https://dataclient.io/rest/api/RestEndpoint#searchToString */ searchToString(searchParams: Record<string, any>): string; /** Prepares RequestInit used in fetch. This is sent to fetchResponse() * @see https://dataclient.io/rest/api/RestEndpoint#getRequestInit */ getRequestInit( this: any, body?: RequestInit['body'] | Record<string, unknown>, ): Promise<RequestInit> | RequestInit; /** Called by getRequestInit to determine HTTP Headers * @see https://dataclient.io/rest/api/RestEndpoint#getHeaders */ getHeaders(headers: HeadersInit): Promise<HeadersInit> | HeadersInit; /* after-fetch */ /** Performs the fetch call * @see https://dataclient.io/rest/api/RestEndpoint#fetchResponse */ fetchResponse(input: RequestInfo, init: RequestInit): Promise<Response>; /** Takes the Response and parses via .text() or .json() * @see https://dataclient.io/rest/api/RestEndpoint#parseResponse */ parseResponse(response: Response): Promise<any>; /** Perform any transforms with the parsed result. * @see https://dataclient.io/rest/api/RestEndpoint#process */ process(value: any, ...args: Parameters<F>): ResolveType<F>; /* utilities */ /** Returns true if the provided (fetch) key matches this endpoint. * @see https://dataclient.io/rest/api/RestEndpoint#testKey */ testKey(key: string): boolean; /* extenders */ // TODO: figure out better way than wrapping whole options in Readonly<> + making O extend from {} // this is just a hack to handle when no members of PartialRestGenerics are present // Note: Using overloading (like paginated did) struggles because typescript does not have a clear way of distinguishing one // should be used from the other (due to same problem with every member being partial) /** Creates a child endpoint that inherits from this while overriding provided `options`. * @see https://dataclient.io/rest/api/RestEndpoint#extend */ extend< E extends RestInstanceBase, ExtendOptions extends PartialRestGenerics | {}, >( this: E, options: Readonly< RestEndpointExtendOptions<ExtendOptions, E, F> & ExtendOptions >, ): RestExtendedEndpoint<ExtendOptions, E>; } export interface RestInstance< F extends FetchFunction = FetchFunction, S extends Schema | undefined = any, M extends boolean | undefined = boolean | undefined, O extends { path: string; body?: any; searchParams?: any; method?: string; paginationField?: string; } = { path: string }, > extends RestInstanceBase<F, S, M, O> { /** Creates an Endpoint to append the next page extending a list for pagination * @see https://dataclient.io/rest/api/RestEndpoint#paginated */ paginated< E extends RestInstanceBase<FetchFunction, any, undefined>, A extends any[], >( this: E, removeCursor: (...args: A) => readonly [...Parameters<E>], ): PaginationEndpoint<E, A>; paginated< E extends RestInstanceBase<FetchFunction, any, undefined>, C extends string, >( this: E, cursorField: C, ): PaginationFieldEndpoint<E, C>; /** Concatinate the next page of results (GET) * @see https://dataclient.io/rest/api/RestEndpoint#getPage */ getPage: 'paginationField' extends keyof O ? O['paginationField'] extends string ? PaginationFieldEndpoint< F & { schema: S; sideEffect: M } & O, O['paginationField'] > : undefined : undefined; /** Create a new item (POST) and `push` to the end * @see https://dataclient.io/rest/api/RestEndpoint#push */ push: AddEndpoint< F, ExtractCollection<S>['push'], Exclude<O, 'body' | 'method'> & { body: | OptionsToAdderBodyArgument<O> | OptionsToAdderBodyArgument<O>[] | FormData; } >; /** Create a new item (POST) and `unshift` to the beginning * @see https://dataclient.io/rest/api/RestEndpoint#unshift */ unshift: AddEndpoint< F, ExtractCollection<S>['unshift'], Exclude<O, 'body' | 'method'> & { body: | OptionsToAdderBodyArgument<O> | OptionsToAdderBodyArgument<O>[] | FormData; } >; /** Create new item(s) (POST) and `Object.assign` merge * @see https://dataclient.io/rest/api/RestEndpoint#assign */ assign: AddEndpoint< F, ExtractCollection<S>, Exclude<O, 'body' | 'method'> & { body: Record<string, OptionsToAdderBodyArgument<O>> | FormData; } >; } export type RestEndpointExtendOptions< O extends PartialRestGenerics, E extends { body?: any; path?: string; schema?: Schema; method?: string }, F extends FetchFunction, > = RestEndpointOptions< OptionsToFunction<O, E, F>, 'schema' extends keyof O ? Extract<O['schema'], Schema | undefined> : E['schema'] > & Partial< Omit< E, KeyofRestEndpoint | keyof PartialRestGenerics | keyof RestEndpointOptions > >; type OptionsToRestEndpoint< O extends PartialRestGenerics, E extends RestInstanceBase & { body?: any; paginationField?: string }, F extends FetchFunction, > = 'path' extends keyof O ? RestType< 'searchParams' extends keyof O ? O['searchParams'] extends undefined ? PathArgs<Exclude<O['path'], undefined>> : O['searchParams'] & PathArgs<Exclude<O['path'], undefined>> : PathArgs<Exclude<O['path'], undefined>>, OptionsToBodyArgument< 'body' extends keyof O ? O : E, 'method' extends keyof O ? O['method'] : E['method'] >, 'schema' extends keyof O ? O['schema'] : E['schema'], 'sideEffect' extends keyof O ? Extract<O['sideEffect'], undefined | true> : 'method' extends keyof O ? MethodToSide<O['method']> : E['sideEffect'], O['process'] extends {} ? ReturnType<O['process']> : ResolveType<F>, { path: Exclude<O['path'], undefined>; body: 'body' extends keyof O ? O['body'] : E['body']; searchParams: 'searchParams' extends keyof O ? O['searchParams'] : E['searchParams']; method: 'method' extends keyof O ? O['method'] : E['method']; paginationField: 'paginationField' extends keyof O ? O['paginationField'] : E['paginationField']; } > : 'body' extends keyof O ? RestType< 'searchParams' extends keyof O ? O['searchParams'] extends undefined ? PathArgs<Exclude<O['path'], undefined>> : O['searchParams'] & PathArgs<Exclude<E['path'], undefined>> : PathArgs<Exclude<E['path'], undefined>>, OptionsToBodyArgument< O, 'method' extends keyof O ? O['method'] : E['method'] >, 'schema' extends keyof O ? O['schema'] : E['schema'], 'sideEffect' extends keyof O ? Extract<O['sideEffect'], undefined | true> : 'method' extends keyof O ? MethodToSide<O['method']> : E['sideEffect'], O['process'] extends {} ? ReturnType<O['process']> : ResolveType<F>, { path: E['path']; body: O['body']; searchParams: 'searchParams' extends keyof O ? O['searchParams'] : E['searchParams']; method: 'method' extends keyof O ? O['method'] : E['method']; paginationField: 'paginationField' extends keyof O ? O['paginationField'] : Extract<E['paginationField'], string>; } > : 'searchParams' extends keyof O ? RestType< O['searchParams'] extends undefined ? PathArgs<Exclude<O['path'], undefined>> : O['searchParams'] & PathArgs<Exclude<E['path'], undefined>>, OptionsToBodyArgument< E, 'method' extends keyof O ? O['method'] : E['method'] >, 'schema' extends keyof O ? O['schema'] : E['schema'], 'sideEffect' extends keyof O ? Extract<O['sideEffect'], undefined | true> : 'method' extends keyof O ? MethodToSide<O['method']> : E['sideEffect'], O['process'] extends {} ? ReturnType<O['process']> : ResolveType<F>, { path: E['path']; body: E['body']; searchParams: O['searchParams']; method: 'method' extends keyof O ? O['method'] : E['method']; paginationField: 'paginationField' extends keyof O ? O['paginationField'] : Extract<E['paginationField'], string>; } > : RestInstance< F, 'schema' extends keyof O ? O['schema'] : E['schema'], 'sideEffect' extends keyof O ? Extract<O['sideEffect'], undefined | true> : 'method' extends keyof O ? MethodToSide<O['method']> : E['sideEffect'], { path: 'path' extends keyof O ? Exclude<O['path'], undefined> : E['path']; body: 'body' extends keyof O ? O['body'] : E['body']; searchParams: 'searchParams' extends keyof O ? O['searchParams'] : E['searchParams']; method: 'method' extends keyof O ? O['method'] : E['method']; paginationField: 'paginationField' extends keyof O ? O['paginationField'] : E['paginationField']; } >; export type RestExtendedEndpoint< O extends PartialRestGenerics, E extends RestInstanceBase & { getPage?: unknown }, > = OptionsToRestEndpoint< O, E & (E extends { getPage: { paginationField: string } } ? { paginationField: E['getPage']['paginationField'] } : unknown), RestInstance< ( ...args: Parameters<E> ) => O['process'] extends {} ? Promise<ReturnType<O['process']>> : ReturnType<E>, 'schema' extends keyof O ? O['schema'] : E['schema'], 'method' extends keyof O ? MethodToSide<O['method']> : E['sideEffect'] > > & Omit<O, KeyofRestEndpoint> & Omit<E, KeyofRestEndpoint | keyof O>; export interface PartialRestGenerics { /** @see https://dataclient.io/rest/api/RestEndpoint#path */ readonly path?: string; /** @see https://dataclient.io/rest/api/RestEndpoint#schema */ readonly schema?: Schema | undefined; /** @see https://dataclient.io/rest/api/RestEndpoint#method */ readonly method?: string; /** Only used for types */ /** @see https://dataclient.io/rest/api/RestEndpoint#body */ body?: any; /** Only used for types */ /** @see https://dataclient.io/rest/api/RestEndpoint#searchParams */ searchParams?: any; /** @see https://dataclient.io/rest/api/RestEndpoint#paginationfield */ readonly paginationField?: string; /** @see https://dataclient.io/rest/api/RestEndpoint#process */ process?(value: any, ...args: any): any; } /** Generic types when constructing a RestEndpoint * * @see https://dataclient.io/rest/api/RestEndpoint#inheritance */ export interface RestGenerics extends PartialRestGenerics { readonly path: string; } export type PaginationEndpoint< E extends FetchFunction & RestGenerics & { sideEffect?: boolean | undefined }, A extends any[], > = RestInstanceBase< ParamFetchNoBody<A[0], ResolveType<E>>, E['schema'], E['sideEffect'], Pick<E, 'path' | 'searchParams' | 'body'> & { searchParams: Omit<A[0], keyof PathArgs<E['path']>>; } >; export type PaginationFieldEndpoint< E extends FetchFunction & RestGenerics & { sideEffect?: boolean | undefined }, C extends string, > = RestInstanceBase< ParamFetchNoBody< { [K in C]: string | number | boolean } & E['searchParams'] & PathArgs<Exclude<E['path'], undefined>>, ResolveType<E> >, E['schema'], E['sideEffect'], Pick<E, 'path' | 'searchParams' | 'body'> & { searchParams: { [K in C]: string | number | boolean } & E['searchParams']; } > & { paginationField: C }; export type AddEndpoint< F extends FetchFunction = FetchFunction, S extends Schema | undefined = any, O extends { path: string; body: any; searchParams?: any; } = { path: string; body: any }, > = RestInstanceBase< RestFetch< 'searchParams' extends keyof O ? O['searchParams'] extends undefined ? PathArgs<Exclude<O['path'], undefined>> : O['searchParams'] & PathArgs<Exclude<O['path'], undefined>> : PathArgs<Exclude<O['path'], undefined>>, O['body'], ResolveType<F> >, S, true, Omit<O, 'method'> & { method: 'POST' } >; type OptionsBodyDefault<O extends RestGenerics> = 'body' extends keyof O ? O : O['method'] extends 'POST' | 'PUT' | 'PATCH' ? O & { body: any } : O & { body: undefined }; type OptionsToAdderBodyArgument<O extends { body?: any }> = 'body' extends keyof O ? O['body'] : any; export interface RestEndpointOptions< F extends FetchFunction = FetchFunction, S extends Schema | undefined = undefined, > extends EndpointExtraOptions<F> { /** Prepended to all urls * @see https://dataclient.io/rest/api/RestEndpoint#urlPrefix */ urlPrefix?: string; requestInit?: RequestInit; /** Called by getRequestInit to determine HTTP Headers * @see https://dataclient.io/rest/api/RestEndpoint#getHeaders */ getHeaders?(headers: HeadersInit): Promise<HeadersInit> | HeadersInit; /** Prepares RequestInit used in fetch. This is sent to fetchResponse() * @see https://dataclient.io/rest/api/RestEndpoint#getRequestInit */ getRequestInit?(body: any): Promise<RequestInit> | RequestInit; /** Performs the fetch call * @see https://dataclient.io/rest/api/RestEndpoint#fetchResponse */ fetchResponse?(input: RequestInfo, init: RequestInit): Promise<any>; /** Takes the Response and parses via .text() or .json() * @see https://dataclient.io/rest/api/RestEndpoint#parseResponse */ parseResponse?(response: Response): Promise<any>; sideEffect?: boolean | undefined; name?: string; signal?: AbortSignal; fetch?: F; key?(...args: Parameters<F>): string; url?(...args: Parameters<F>): string; update?: EndpointUpdateFunction<F, S>; } export type RestEndpointConstructorOptions<O extends RestGenerics = any> = RestEndpointOptions< RestFetch< 'searchParams' extends keyof O ? O['searchParams'] extends undefined ? PathArgs<O['path']> : O['searchParams'] & PathArgs<O['path']> : PathArgs<O['path']>, OptionsToBodyArgument< O, 'method' extends keyof O ? O['method'] : O extends { sideEffect: true } ? 'POST' : 'GET' >, O['process'] extends {} ? ReturnType<O['process']> : any /*Denormalize<O['schema']>*/ >, O['schema'] >; /** Simplifies endpoint definitions that follow REST patterns * * @see https://dataclient.io/rest/api/RestEndpoint */ export interface RestEndpoint<O extends RestGenerics = any> extends RestInstance< RestFetch< 'searchParams' extends keyof O ? O['searchParams'] extends undefined ? PathArgs<O['path']> : O['searchParams'] & PathArgs<O['path']> : PathArgs<O['path']>, OptionsToBodyArgument< O, 'method' extends keyof O ? O['method'] : O extends { sideEffect: true } ? 'POST' : 'GET' >, O['process'] extends {} ? ReturnType<O['process']> : any /*Denormalize<O['schema']>*/ >, 'schema' extends keyof O ? O['schema'] : undefined, 'sideEffect' extends keyof O ? Extract<O['sideEffect'], undefined | true> : MethodToSide<O['method']>, 'method' extends keyof O ? O : O & { method: O extends { sideEffect: true } ? 'POST' : 'GET'; } > {} export interface RestEndpointConstructor { /** Simplifies endpoint definitions that follow REST patterns * * @see https://dataclient.io/rest/api/RestEndpoint */ new <O extends RestGenerics = any>({ method, sideEffect, name, ...options }: RestEndpointConstructorOptions<O> & Readonly<O>): RestEndpoint<O>; readonly prototype: RestInstanceBase; } export type MethodToSide<M> = M extends string ? M extends 'GET' ? undefined : true : undefined; /** RestEndpoint types simplified */ export type RestType< UrlParams = any, Body = any, S extends Schema | undefined = Schema | undefined, M extends boolean | undefined = boolean | undefined, R = any, O extends { path: string; body?: any; searchParams?: any; paginationField?: string; } = { path: string; paginationField: string }, > = IfTypeScriptLooseNull< RestInstance<RestFetch<UrlParams, Body, R>, S, M, O>, Body extends {} ? RestTypeWithBody<UrlParams, S, M, Body, R, O> : RestTypeNoBody<UrlParams, S, M, R, O> >; export type RestTypeWithBody< UrlParams = any, S extends Schema | undefined = Schema | undefined, M extends boolean | undefined = boolean | undefined, Body = any, R = any /*Denormalize<S>*/, O extends { path: string; body?: any; searchParams?: any; } = { path: string; body: any }, > = RestInstance<ParamFetchWithBody<UrlParams, Body, R>, S, M, O>; export type RestTypeNoBody< UrlParams = any, S extends Schema | undefined = Schema | undefined, M extends boolean | undefined = boolean | undefined, R = any /*Denormalize<S>*/, O extends { path: string; body?: undefined; searchParams?: any; } = { path: string; body: undefined }, > = RestInstance<ParamFetchNoBody<UrlParams, R>, S, M, O>; /** Simple parameters, and body fetch functions */ export type RestFetch< UrlParams, Body = {}, Resolve = any, > = IfTypeScriptLooseNull< | ParamFetchNoBody<UrlParams, Resolve> | ParamFetchWithBody<UrlParams, Body, Resolve>, Body extends {} ? ParamFetchWithBody<UrlParams, Body, Resolve> : ParamFetchNoBody<UrlParams, Resolve> >; export type ParamFetchWithBody<P, B = {}, R = any> = // we must always allow undefined in a union and give it a type without params P extends undefined ? (this: EndpointInstanceInterface, body: B) => Promise<R> : // even with loose null, this will only be true when all members are optional {} extends P ? // this safely handles PathArgs with no members that results in a simple `unknown` type keyof P extends never ? (this: EndpointInstanceInterface, body: B) => Promise<R> : | ((this: EndpointInstanceInterface, params: P, body: B) => Promise<R>) | ((this: EndpointInstanceInterface, body: B) => Promise<R>) : (this: EndpointInstanceInterface, params: P, body: B) => Promise<R>; export type ParamFetchNoBody<P, R = any> = // we must always allow undefined in a union and give it a type without params P extends undefined ? (this: EndpointInstanceInterface) => Promise<R> : // even with loose null, this will only be true when all members are optional {} extends P ? // this safely handles PathArgs with no members that results in a simple `unknown` type keyof P extends never ? (this: EndpointInstanceInterface) => Promise<R> : | ((this: EndpointInstanceInterface, params: P) => Promise<R>) | ((this: EndpointInstanceInterface) => Promise<R>) : (this: EndpointInstanceInterface, params: P) => Promise<R>; // same algorithm, but for Args (aka readonly any[]) export type ParamToArgs<P> = P extends undefined ? [] : {} extends P ? keyof P extends never ? [] : [] | [P] : [P]; type IfTypeScriptLooseNull<Y, N> = 1 | undefined extends 1 ? Y : N; export type KeyofRestEndpoint = keyof RestInstance; export type FromFallBack<K extends keyof E, O, E> = K extends keyof O ? O[K] : E[K]; export type FetchMutate< A extends readonly any[] = [any, {}] | [{}], R = any, > = (this: RestInstance, ...args: A) => Promise<R>; export type FetchGet<A extends readonly any[] = [any], R = any> = ( this: RestInstance, ...args: A ) => Promise<R>; export type Defaults<O, D> = { [K in keyof O | keyof D]: K extends keyof O ? Exclude<O[K], undefined> : D[Extract<K, keyof D>]; }; export type GetEndpoint< O extends { readonly path: string; readonly schema: Schema; /** Only used for types */ readonly searchParams?: any; readonly paginationField?: string; } = { path: string; schema: Schema; }, > = RestTypeNoBody< 'searchParams' extends keyof O ? O['searchParams'] extends undefined ? PathArgs<O['path']> : O['searchParams'] & PathArgs<O['path']> : PathArgs<O['path']>, O['schema'], undefined, any, O & { method: 'GET' } >; export type MutateEndpoint< O extends { readonly path: string; readonly schema: Schema; /** Only used for types */ readonly searchParams?: any; /** Only used for types */ readonly body?: any; } = { path: string; body: any; schema: Schema; }, > = RestTypeWithBody< 'searchParams' extends keyof O ? O['searchParams'] extends undefined ? PathArgs<O['path']> : O['searchParams'] & PathArgs<O['path']> : PathArgs<O['path']>, O['schema'], true, O['body'], any, O & { body: any; method: 'POST' | 'PUT' | 'PATCH' | 'DELETE' } >;