import type { Denormalize, EndpointExtraOptions, EndpointInstanceInterface, Schema, FetchFunction, ResolveType, } from '@data-client/endpoint'; import type { ExtractCollection } from './extractCollection.js'; import { OptionsToBodyArgument, OptionsToFunction, } from './OptionsToFunction.js'; import { PathArgs, SoftPathArgs } from './pathTypes.js'; import { EndpointUpdateFunction } from './RestEndpointTypeHelp.js'; export type ContentType = 'json' | 'blob' | 'text' | 'arrayBuffer' | 'stream'; interface ContentTypeMap { blob: Blob; text: string; arrayBuffer: ArrayBuffer; stream: ReadableStream; json: any; } type ContentReturnType = ContentTypeMap[C]; type ContentSchemaGuard = O extends { content: 'blob' | 'text' | 'arrayBuffer' | 'stream' } ? { schema?: undefined } : {}; 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 { /** @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; /** @see https://dataclient.io/rest/api/RestEndpoint#content */ readonly content?: ContentType; /* fetch lifecycles */ /* before-fetch */ /** Builds the URL to fetch * @see https://dataclient.io/rest/api/RestEndpoint#url */ url(...args: Parameters): string; /** Encode the searchParams component of the url * @see https://dataclient.io/rest/api/RestEndpoint#searchToString */ searchToString(searchParams: Record): 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, ): Promise | RequestInit; /** Called by getRequestInit to determine HTTP Headers * @see https://dataclient.io/rest/api/RestEndpoint#getHeaders */ getHeaders(headers: HeadersInit): Promise | HeadersInit; /* after-fetch */ /** Performs the fetch call * @see https://dataclient.io/rest/api/RestEndpoint#fetchResponse */ fetchResponse(input: RequestInfo, init: RequestInit): Promise; /** Takes the Response and parses via .text() or .json() * @see https://dataclient.io/rest/api/RestEndpoint#parseResponse */ parseResponse(response: Response): Promise; /** Perform any transforms with the parsed result. * @see https://dataclient.io/rest/api/RestEndpoint#process */ process(value: any, ...args: Parameters): ResolveType; /* 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 > & ContentSchemaGuard, ): RestExtendedEndpoint; } 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 { /** 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, A extends any[], >( this: E, removeCursor: (...args: A) => readonly [...Parameters], ): PaginationEndpoint; paginated< E extends RestInstanceBase, C extends string, >( this: E, cursorField: C, ): PaginationFieldEndpoint; /** 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['push'], Omit & { body: | OptionsToAdderBodyArgument['push']> | OptionsToAdderBodyArgument['push']>[] | FormData; } >; /** Create a new item (POST) and `unshift` to the beginning * @see https://dataclient.io/rest/api/RestEndpoint#unshift */ unshift: AddEndpoint< F, ExtractCollection['unshift'], Omit & { body: | OptionsToAdderBodyArgument['unshift']> | OptionsToAdderBodyArgument['unshift']>[] | FormData; } >; /** Create new item(s) (POST) and `Object.assign` merge * @see https://dataclient.io/rest/api/RestEndpoint#assign */ assign: AddEndpoint< F, ExtractCollection, Omit & { body: | Record< string, OptionsToAdderBodyArgument['assign']> > | FormData; } >; /** Remove item(s) (PATCH) from collection * @see https://dataclient.io/rest/api/RestEndpoint#remove */ remove: RemoveEndpoint< F, ExtractCollection['remove'], Omit & { body: | Partial['remove']>> | Partial< OptionsToAdderBodyArgument['remove']> >[] | FormData; } >; /** Move item between collections (PATCH) - removes from old, adds to new * @see https://dataclient.io/rest/api/RestEndpoint#move */ move: MoveEndpoint< F, ExtractCollection['move'], { path: 'movePath' extends keyof O ? O['movePath'] & string : O['path']; body: | Partial['move']>> | FormData; } >; } export type RestEndpointExtendOptions< O extends PartialRestGenerics, E extends { body?: any; path?: string; schema?: Schema; method?: string }, F extends FetchFunction, > = RestEndpointOptions< OptionsToFunction, 'schema' extends keyof O ? Extract : 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> : O['searchParams'] & PathArgs> : PathArgs>, 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 : 'method' extends keyof O ? MethodToSide : E['sideEffect'], O['process'] extends {} ? ReturnType : 'content' extends keyof O ? ContentReturnType : ResolveType, { path: Exclude; 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> : O['searchParams'] & PathArgs> : PathArgs>, OptionsToBodyArgument< O, 'method' extends keyof O ? O['method'] : E['method'] >, 'schema' extends keyof O ? O['schema'] : E['schema'], 'sideEffect' extends keyof O ? Extract : 'method' extends keyof O ? MethodToSide : E['sideEffect'], O['process'] extends {} ? ReturnType : 'content' extends keyof O ? ContentReturnType : ResolveType, { 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; } > : 'searchParams' extends keyof O ? RestType< [O['searchParams']] extends [undefined] ? PathArgs> : O['searchParams'] & PathArgs>, OptionsToBodyArgument< E, 'method' extends keyof O ? O['method'] : E['method'] >, 'schema' extends keyof O ? O['schema'] : E['schema'], 'sideEffect' extends keyof O ? Extract : 'method' extends keyof O ? MethodToSide : E['sideEffect'], O['process'] extends {} ? ReturnType : 'content' extends keyof O ? ContentReturnType : ResolveType, { 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; } > : RestInstance< F, 'schema' extends keyof O ? O['schema'] : E['schema'], 'sideEffect' extends keyof O ? Extract : 'method' extends keyof O ? MethodToSide : E['sideEffect'], { path: 'path' extends keyof O ? Exclude : 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) => O['process'] extends {} ? Promise> : 'content' extends keyof O ? Promise> : ReturnType, 'schema' extends keyof O ? O['schema'] : E['schema'], 'sideEffect' extends keyof O ? Extract : 'method' extends keyof O ? MethodToSide : E['sideEffect'] > > & Omit & Omit; 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; /** @see https://dataclient.io/rest/api/RestEndpoint#content */ readonly content?: ContentType; } /** 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>, E['schema'], E['sideEffect'], Pick & { searchParams: Omit>; } >; /** Merge pagination field C into body, making it required */ type PaginationIntoBody = Body & { [K in C]: string | number | boolean; }; /** Paginated searchParams type */ type PaginatedSearchParams< E extends { searchParams?: any; path?: string }, C extends string, > = { [K in C]: string | number | boolean; } & E['searchParams'] & PathArgs>; /** searchParams version: pagination in searchParams, optional body support */ type PaginationFieldInSearchParams< E extends FetchFunction & RestGenerics & { sideEffect?: boolean | undefined }, C extends string, > = RestInstanceBase< // Union allows calling with just searchParams or with searchParams + body | ParamFetchNoBody, ResolveType> | ParamFetchWithBody< PaginatedSearchParams, NonNullable, ResolveType >, E['schema'], E['sideEffect'], Pick & { searchParams: { [K in C]: string | number | boolean; } & E['searchParams']; } > & { paginationField: C }; /** body version: pagination field is in body (body required) */ type PaginationFieldInBody< E extends FetchFunction & RestGenerics & { sideEffect?: boolean | undefined }, C extends string, > = RestInstanceBase< ParamFetchWithBody< E['searchParams'] & PathArgs>, PaginationIntoBody, ResolveType >, E['schema'], E['sideEffect'], Pick & { body: PaginationIntoBody; } > & { paginationField: C }; /** Retrieves the next page of results by pagination field */ export type PaginationFieldEndpoint< E extends FetchFunction & RestGenerics & { sideEffect?: boolean | undefined }, C extends string, > = // If body can be undefined or pagination field not in body, use searchParams undefined extends E['body'] ? PaginationFieldInSearchParams : // If pagination field C is a key of body, merge into body C extends keyof E['body'] ? PaginationFieldInBody : // Otherwise use searchParams PaginationFieldInSearchParams; 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> : O['searchParams'] & PathArgs> : PathArgs>, O['body'], ResolveType >, S, true, Omit & { method: 'POST' } >; export type RemoveEndpoint< 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> : O['searchParams'] & PathArgs> : PathArgs>, O['body'], ResolveType >, S, true, Omit & { method: 'PATCH' } >; export type MoveEndpoint< F extends FetchFunction = FetchFunction, S extends Schema | undefined = any, O extends { path: string; body: any; } = { path: string; body: any }, > = RestInstanceBase< RestFetch>, O['body'], ResolveType>, S, true, Omit & { method: 'PATCH' } >; type OptionsBodyDefault = 'body' extends keyof O ? O : O['method'] extends 'POST' | 'PUT' | 'PATCH' ? O & { body: any } : O & { body: undefined }; /** When `method` is omitted from `O`, infer it (must stay aligned with `OptionsToBodyArgument`). */ type InferRestMethodWhenOmitted = O extends { sideEffect: true } ? 'POST' : 'body' extends keyof O ? [O['body']] extends [undefined] ? 'GET' : 'POST' : 'GET'; type MethodArgForBodyInference = 'method' extends keyof O ? O['method'] : InferRestMethodWhenOmitted; type OptionsToAdderBodyArgument = 'body' extends keyof O ? O['body'] : Partial>; export interface RestEndpointOptions< F extends FetchFunction = FetchFunction, S extends Schema | undefined = undefined, > extends EndpointExtraOptions { /** 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; /** Prepares RequestInit used in fetch. This is sent to fetchResponse() * @see https://dataclient.io/rest/api/RestEndpoint#getRequestInit */ getRequestInit?(body: any): Promise | RequestInit; /** Performs the fetch call * @see https://dataclient.io/rest/api/RestEndpoint#fetchResponse */ fetchResponse?(input: RequestInfo, init: RequestInit): Promise; /** Takes the Response and parses via .text() or .json() * @see https://dataclient.io/rest/api/RestEndpoint#parseResponse */ parseResponse?(response: Response): Promise; /** @see https://dataclient.io/rest/api/RestEndpoint#content */ content?: ContentType; sideEffect?: boolean | undefined; name?: string; signal?: AbortSignal; fetch?: F; key?(...args: Parameters): string; url?(...args: Parameters): string; update?: EndpointUpdateFunction; } // When subclassing RestEndpoint with `O extends RestGenerics = any`, O defaults // to `any`. The `unknown extends O ? any` guard catches O=any before it reaches // PathArgs (see #3782). SoftPathArgs collapses PathArgs to `unknown` // when a concrete body is present, preventing union overloads that break // getOptimisticResponse callbacks. Method inference treats explicit body as POST. export type RestEndpointConstructorOptions = RestEndpointOptions< RestFetch< unknown extends O ? any : 'searchParams' extends keyof O ? [O['searchParams']] extends [undefined] ? SoftPathArgs : O['searchParams'] & SoftPathArgs : SoftPathArgs, OptionsToBodyArgument>, O['process'] extends {} ? ReturnType : 'content' extends keyof O ? ContentReturnType : any /*Denormalize*/ >, 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< unknown extends O ? any : 'searchParams' extends keyof O ? [O['searchParams']] extends [undefined] ? PathArgs : O['searchParams'] & PathArgs : PathArgs, OptionsToBodyArgument>, O['process'] extends {} ? ReturnType : 'content' extends keyof O ? ContentReturnType : any /*Denormalize*/ >, 'schema' extends keyof O ? O['schema'] : undefined, 'sideEffect' extends keyof O ? Extract : MethodToSide>, 'method' extends keyof O ? O : O & { method: InferRestMethodWhenOmitted; } > {} export interface RestEndpointConstructor { /** Simplifies endpoint definitions that follow REST patterns * * @see https://dataclient.io/rest/api/RestEndpoint */ new ({ method, sideEffect, name, ...options }: RestEndpointConstructorOptions & Readonly & ContentSchemaGuard): RestEndpoint; readonly prototype: RestInstanceBase; } export type MethodToSide = 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, S, M, O>, Body extends {} ? RestTypeWithBody : RestTypeNoBody >; export type RestTypeWithBody< UrlParams = any, S extends Schema | undefined = Schema | undefined, M extends boolean | undefined = boolean | undefined, Body = any, R = any /*Denormalize*/, O extends { path: string; body?: any; searchParams?: any; } = { path: string; body: any }, > = RestInstance, S, M, O>; export type RestTypeNoBody< UrlParams = any, S extends Schema | undefined = Schema | undefined, M extends boolean | undefined = boolean | undefined, R = any /*Denormalize*/, O extends { path: string; body?: undefined; searchParams?: any; } = { path: string; body: undefined }, > = RestInstance, S, M, O>; /** Simple parameters, and body fetch functions */ export type RestFetch< UrlParams, Body = {}, Resolve = any, > = IfTypeScriptLooseNull< | ParamFetchNoBody | ParamFetchWithBody, Body extends {} ? ParamFetchWithBody : ParamFetchNoBody >; export type ParamFetchWithBody = // we must always allow undefined in a union and give it a type without params P extends undefined ? (this: EndpointInstanceInterface, body: B) => Promise : // 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 : | ((this: EndpointInstanceInterface, params: P, body: B) => Promise) | ((this: EndpointInstanceInterface, body: B) => Promise) : (this: EndpointInstanceInterface, params: P, body: B) => Promise; export type ParamFetchNoBody = // we must always allow undefined in a union and give it a type without params P extends undefined ? (this: EndpointInstanceInterface) => Promise : // 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 : | ((this: EndpointInstanceInterface, params: P) => Promise) | ((this: EndpointInstanceInterface) => Promise) : (this: EndpointInstanceInterface, params: P) => Promise; // same algorithm, but for Args (aka readonly any[]) export type ParamToArgs

= P extends undefined ? [] : {} extends P ? keyof P extends never ? [] : [] | [P] : [P]; type IfTypeScriptLooseNull = 1 | undefined extends 1 ? Y : N; export type KeyofRestEndpoint = keyof RestInstance; export type FromFallBack = K extends keyof O ? O[K] : E[K]; export type FetchMutate< A extends readonly any[] = [any, {}] | [{}], R = any, > = (this: RestInstance, ...args: A) => Promise; export type FetchGet = ( this: RestInstance, ...args: A ) => Promise; export type Defaults = { [K in keyof O | keyof D]: K extends keyof O ? Exclude : D[Extract]; }; 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['searchParams'] & PathArgs : PathArgs, 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['searchParams'] & PathArgs : PathArgs, O['schema'], true, O['body'], any, O & { body: any; method: 'POST' | 'PUT' | 'PATCH' | 'DELETE' } >;