UNPKG

@openapi-qraft/react

Version:

OpenAPI client for React, providing type-safe requests and dynamic TanStack Query React Hooks via a modular, Proxy-based architecture.

623 lines (573 loc) 18.8 kB
import type { QraftServiceOperationsToken } from '@openapi-qraft/tanstack-query-react-types'; import type { QueryClient } from '@tanstack/react-query'; import type * as callbacks from './callbacks/index.js'; import type * as operationInvokeModule from './callbacks/operationInvokeFn.js'; import type { OperationSchema, RequestFnInfo, RequestFnResponse, } from './lib/requestFn.js'; import { createRecursiveProxy } from './lib/createRecursiveProxy.js'; export interface CreateAPIBasicClientOptions { requestFn: ( schema: OperationSchema, requestInfo: RequestFnInfo ) => Promise<RequestFnResponse<any, any>>; baseUrl: string; } export interface CreateAPIBasicQueryClientOptions { queryClient: QueryClient; } export interface CreateAPIQueryClientOptions extends CreateAPIBasicClientOptions, CreateAPIBasicQueryClientOptions {} /** * @deprecated use `CreateAPIClientOptions` instead */ export type QraftClientOptions = | CreateAPIBasicClientOptions | CreateAPIQueryClientOptions; export type CreateAPIClientOptions = | CreateAPIBasicClientOptions | CreateAPIBasicQueryClientOptions | CreateAPIQueryClientOptions; /** * Creates a QueryClient compatible API Client which contains all operations * such as `useQuery`, `useMutation`. * * @example Fetching data with QueryClient * ```ts * const api = qraftAPIClient(services, callbacks, { * requestFn: requestFn, * baseUrl: 'https://api.example.com', * queryClient: new QueryClient(), * }); * * api.service.operation({ * parameters: { path: { id: 1 } }, * }); * ``` */ export function qraftAPIClient< Services extends UnionServiceOperationsDeclaration<Services>, Callbacks extends ServiceMethods, >( services: Services, callbacks: Callbacks, options: CreateAPIQueryClientOptions ): APIDefaultQueryClientServices<Services>; /** * Creates a QueryClient compatible API Client which contains all operations * such as `useQuery`, `useMutation`. * * @example Fetching data with QueryClient * ```ts * const api = qraftAPIClient(services, callbacks, { * requestFn: requestFn, * baseUrl: 'https://api.example.com', * queryClient: new QueryClient(), * }); * * api.service.operation({ * parameters: { path: { id: 1 } }, * }); * ``` */ export function qraftAPIClient< Services extends UnionServiceOperationsDeclaration<Services>, Callbacks extends PartialServiceMethods, >( services: Services, callbacks: Callbacks, options: CreateAPIQueryClientOptions ): APIQueryClientServices<Services, Callbacks>; /** * Creates a QueryClient compatible API Client which contains * only state management manipulations such as `useIsMutating`, * `setQueryData`, `getQueryData` and `invalidateQueries`. * * @example Invalidating queries with QueryClient * ```ts * const api = qraftAPIClient(services, callbacks, { * // an instance of QueryClient shared between all clients * queryClient: sharedQueryClient * }); * * api.service.operation.invalidateQueries({ * parameters: { path: { id: 1 } }, * }); */ export function qraftAPIClient< Services extends UnionServiceOperationsDeclaration<Services>, Callbacks extends PartialServiceMethods, >( services: Services, callbacks: Callbacks, options: CreateAPIBasicQueryClientOptions ): APIBasicQueryClientServices<Services, Callbacks>; /** * Creates a basic API Client which contains all hooks and methods * that don't require an explicitly provided QueryClient. * Hooks like `useQuery` and `useMutation` will automatically retrieve * the QueryClient from the `<QueryClientProvider />` context. * * @example Fetching data with QueryClient from context * ```ts * const api = qraftAPIClient(services, callbacks, { * requestFn: requestFn, * baseUrl: 'https://api.example.com', * }); * * // QueryClient will be retrieved from React context * api.service.operation.useQuery({ * parameters: { path: { id: 1 } }, * }); * ``` * * @example Fetching data without QueryClient * ```ts * const api = qraftAPIClient(services, callbacks, { * requestFn: requestFn, * baseUrl: 'https://api.example.com', * }); * * api.service.operation({ * parameters: { path: { id: 1 } }, * }); * ``` */ export function qraftAPIClient< Services extends UnionServiceOperationsDeclaration<Services>, Callbacks extends PartialServiceMethods, >( services: Services, callbacks: Callbacks, options: CreateAPIBasicClientOptions ): APIBasicClientServices<Services, Callbacks>; /** * Creates a utility API Client which contains utility operations * such as `getQueryKey`, `getInfiniteQueryKey`, `getMutationKey` and state hooks * like `useIsFetching` and `useMutationData`. * * @example Using state hooks * ```ts * const api = qraftAPIClient(services, callbacks); * * // Check if any query is currently fetching * const isFetching = api.service.operation.useIsFetching(); * ``` * * @example Getting query keys with utility client * ```ts * const api = qraftAPIClient(services, callbacks); * * api.service.operation.getQueryKey({ * parameters: { path: { id: 1 } }, * }); * ``` */ export function qraftAPIClient< Services extends UnionServiceOperationsDeclaration<Services>, Callbacks extends Pick<PartialServiceMethods, UtilityOperationCallbacks>, >( services: Services, callbacks: Callbacks ): APIUtilityClientServices<Services, Callbacks>; export function qraftAPIClient< Services extends UnionServiceOperationsDeclaration<Services>, Callbacks extends PartialServiceMethods, >( services: Services, callbacks: Callbacks, options?: CreateAPIClientOptions ): | APIQueryClientServices<Services, Callbacks> | APIDefaultQueryClientServices<Services> | APIBasicQueryClientServices<Services, Callbacks> | APIUtilityClientServices<Services, Callbacks> { const stringTag = 'QraftAPIClient'; const toString = (path: (symbol | string)[]): string => { return `[${stringTag}${path.length ? ' ' + path.join('.') : ''}]`; }; const primitiveMethods = { [Symbol.toPrimitive]( this: undefined, path: (string | symbol)[], hint: 'string' | 'number' | 'default' ): unknown { if (hint === 'number') return NaN; if (hint === 'string') return toString(path); return getByPath(services, path); }, valueOf(this: undefined, path: (string | symbol)[]): unknown { return getByPath(services, path); }, toJSON(this: undefined, path: (string | symbol)[]): string { return JSON.stringify(getByPath(services, path)); }, toString(this: undefined, path: (string | symbol)[]): string { return toString(path); }, }; return createRecursiveProxy( function getCallback(getPath, key) { if (Object.prototype.hasOwnProperty.call(primitiveMethods, key)) { return primitiveMethods[key as keyof typeof primitiveMethods].bind( undefined, getPath ); } else if (key === Symbol.toStringTag) { return stringTag; } if (key !== 'schema') return; if ( getPath.length !== 2 && !('schema' in services) && getPath.length !== 1 && !(services[getPath[0] as never] as OperationsDeclaration<any>)?.schema ) return; const serviceOperation = getByPath(services, getPath); if (!isServiceOperation(serviceOperation)) throw new Error(`Service operation not found: ${getPath.join('.')}`); return serviceOperation.schema; }, function applyCallback(applyPath, args) { const { path, callbackName } = extractCallbackDetails( applyPath, services ); assertValidCallbackName(callbackName, callbacks); const serviceOperation = getByPath(services, path); if (!isServiceOperation(serviceOperation)) throw new Error(`Service operation not found: ${path.join('.')}`); if ( callbackName !== 'operationInvokeFn' && callbackName !== 'getQueryKey' && callbackName !== 'getMutationKey' && callbackName !== 'getInfiniteQueryKey' && callbackName !== 'useInfiniteQuery' && callbackName !== 'useQueries' && callbackName !== 'useQuery' && callbackName !== 'useSuspenseInfiniteQuery' && callbackName !== 'useSuspenseQueries' && callbackName !== 'useSuspenseQuery' && callbackName !== 'useIsFetching' && callbackName !== 'useMutation' && callbackName !== 'useIsMutating' && callbackName !== 'useMutationState' ) if (!options || !('queryClient' in options && options.queryClient)) throw new Error( `'qraft.<service>.<operation>.${String(callbackName)}()' requires 'queryClient' in 'createAPIClient(...)' options.` ); // @ts-expect-error - Too complex union type return callbacks[callbackName](options, serviceOperation.schema, args); }, [] ) as never; } function assertValidCallbackName< Callbacks extends Record<string, (...rest: any[]) => unknown>, >( callbackName: string | number | symbol, callbacks: Callbacks ): asserts callbackName is keyof Callbacks { if (!(callbackName in callbacks)) { if ( (callbackName as InvokeOperationCallback) === ('operationInvokeFn' satisfies InvokeOperationCallback) ) throw new Error( `Callback 'operationInvokeFn' is required for executing 'qraft.<service>.<operation>()', but it is not provided in the 'callbacks' object.` ); throw new Error( `Callback for 'qraft.<service>.<operation>.${String(callbackName)}()' is not provided in the 'callbacks' object.` ); } } /** * Extracts Callback details from the applyPath */ function extractCallbackDetails( applyPath: (string | symbol)[], services: UnionServiceOperationsDeclaration<any> ) { // client.<service>.<operation>() [OperationInvokeFn] // <service>.<operation>() [OperationInvokeFn] // client() [OperationInvokeFn] // client() if ( (applyPath.length === 2 && !(services[applyPath[0] as never] as OperationsDeclaration<any>) ?.schema) || (applyPath.length === 0 && 'schema' in services) ) { return { path: applyPath, callbackName: 'operationInvokeFn' satisfies InvokeOperationCallback, }; } else { // client.<service>.<operation>.<method>() // <service>.<operation>.<method>() // client.<method>() return { path: applyPath.slice(0, -1), // The last arg is for instance `.useMutation` or `.useQuery()` callbackName: applyPath[applyPath.length - 1], }; } } function isServiceOperation( input: unknown ): input is { schema: OperationSchema } { return input !== null && typeof input === 'object' && 'schema' in input; } function getByPath(obj: Record<string, unknown>, path: (string | symbol)[]) { return path.reduce<unknown>((acc, key) => { if (acc && typeof acc === 'object' && key in acc) return acc[key as keyof typeof acc]; }, obj); } type ServiceMethods = typeof callbacks; type PartialServiceMethods = Partial<ServiceMethods>; type OperationDeclaration = { schema: OperationSchema; [QraftServiceOperationsToken]: Partial< Record< | QueryOperationCallbacks | QueryOperationStateCallbacks | MutationOperationHookCallbacks | MutationOperationStateCallbacks | UtilityOperationCallbacks, any > > & { types: any; schema: OperationSchema; (...args: any[]): any; }; }; type OperationsDeclaration<Operations> = { [operation in keyof Operations]: OperationDeclaration; }; type ServicesDeclaration<Services> = { [service in keyof Services]: OperationsDeclaration<Services[service]>; }; export type UnionServiceOperationsDeclaration<Services> = | ServicesDeclaration<Services> | OperationsDeclaration<Services> | OperationDeclaration; type QueryOperationHookCallbacks = Extract< keyof ServiceMethods, | 'useInfiniteQuery' | 'useQueries' | 'useQuery' | 'useSuspenseInfiniteQuery' | 'useSuspenseQueries' | 'useSuspenseQuery' >; type QueryOperationCallbacks = | Extract< keyof ServiceMethods, | 'fetchInfiniteQuery' | 'fetchQuery' | 'prefetchInfiniteQuery' | 'prefetchQuery' | 'refetchQueries' | 'ensureQueryData' | 'ensureInfiniteQueryData' > | QueryOperationHookCallbacks; type QueryOperationStateHookCallbacks = Extract< keyof ServiceMethods, 'useIsFetching' >; type QueryOperationStateCallbacks = | Extract< keyof ServiceMethods, | 'cancelQueries' | 'getInfiniteQueryData' | 'getInfiniteQueryState' | 'getQueriesData' | 'getQueryData' | 'getQueryState' | 'invalidateQueries' | 'isFetching' | 'removeQueries' | 'resetQueries' | 'setInfiniteQueryData' | 'setQueriesData' | 'setQueryData' > | QueryOperationStateHookCallbacks; type MutationOperationHookCallbacks = Extract< keyof ServiceMethods, 'useMutation' >; type MutationOperationStateHookCallbacks = Extract< keyof ServiceMethods, 'useIsMutating' | 'useMutationState' >; type MutationOperationStateCallbacks = | Extract<keyof ServiceMethods, 'isMutating'> | MutationOperationStateHookCallbacks; type InvokeOperationCallback = Extract< keyof typeof operationInvokeModule, 'operationInvokeFn' >; type UtilityOperationCallbacks = Extract< keyof ServiceMethods, | 'getQueryKey' | 'getInfiniteQueryKey' | 'getMutationKey' | QueryOperationStateHookCallbacks | MutationOperationStateHookCallbacks >; type OperationCallbackList = | QueryOperationCallbacks | QueryOperationStateCallbacks | MutationOperationHookCallbacks | MutationOperationStateCallbacks | InvokeOperationCallback | UtilityOperationCallbacks; export type APIQueryClientServices< Services extends UnionServiceOperationsDeclaration<Services>, Callbacks extends PartialServiceMethods, > = ServicesFilteredByCallbacks< Services, Extract<keyof Callbacks, OperationCallbackList> >; export type APIDefaultQueryClientServices< Services extends UnionServiceOperationsDeclaration<Services>, > = Services extends OperationDeclaration ? Services[QraftServiceOperationsToken] : Services extends OperationsDeclaration<Services> ? { [operation in keyof Services]: Services[operation][QraftServiceOperationsToken]; } : Services extends ServicesDeclaration<Services> ? { [service in keyof Services]: { [operation in keyof Services[service]]: Services[service][operation][QraftServiceOperationsToken]; }; } : never; export type APIBasicQueryClientServices< Services extends UnionServiceOperationsDeclaration<Services>, Callbacks extends PartialServiceMethods, > = ServicesFilteredByCallbacks< Services, Extract< keyof Callbacks, | QueryOperationStateCallbacks | MutationOperationStateCallbacks | UtilityOperationCallbacks > >; export type APIBasicClientServices< Services extends UnionServiceOperationsDeclaration<Services>, Callbacks extends PartialServiceMethods, > = ServicesFilteredByCallbacks< Services, Extract< keyof Callbacks, | InvokeOperationCallback | UtilityOperationCallbacks | QueryOperationHookCallbacks | QueryOperationStateHookCallbacks | MutationOperationStateHookCallbacks | MutationOperationHookCallbacks > >; export type APIUtilityClientServices< Services extends UnionServiceOperationsDeclaration<Services>, Callbacks extends Pick<PartialServiceMethods, UtilityOperationCallbacks>, > = ServicesFilteredByUtilityCallbacks< Services, Extract<keyof Callbacks, UtilityOperationCallbacks> >; type ServicesFilteredByCallbacks< Services extends UnionServiceOperationsDeclaration<Services>, CallbackList extends OperationCallbackList, > = Services extends OperationDeclaration ? Pick< Services[QraftServiceOperationsToken], Extract< keyof Services[QraftServiceOperationsToken], CallbackList | 'schema' | 'types' > > & OperationInvokeFn<Services[QraftServiceOperationsToken], CallbackList> : Services extends OperationsDeclaration<Services> ? { [operation in keyof Services]: Pick< Services[operation][QraftServiceOperationsToken], Extract< keyof Services[operation][QraftServiceOperationsToken], CallbackList | 'schema' | 'types' > > & OperationInvokeFn< Services[operation][QraftServiceOperationsToken], CallbackList >; } : Services extends ServicesDeclaration<Services> ? { [serviceName in keyof Services]: { [operation in keyof Services[serviceName]]: Pick< Services[serviceName][operation][QraftServiceOperationsToken], Extract< keyof Services[serviceName][operation][QraftServiceOperationsToken], CallbackList | 'schema' | 'types' > > & OperationInvokeFn< Services[serviceName][operation][QraftServiceOperationsToken], CallbackList >; }; } : never; type OperationInvokeFn< InvokeFn extends (...args: any[]) => any, CallbackList extends OperationCallbackList, > = InvokeOperationCallback extends CallbackList ? InvokeFn extends (...args: infer Args) => infer Result ? { (...args: Args): Result } : Record<string, never> : Record<string, never>; type ServicesFilteredByUtilityCallbacks< Services extends UnionServiceOperationsDeclaration<Services>, CallbackList extends UtilityOperationCallbacks, > = Services extends OperationDeclaration ? Pick< Services[QraftServiceOperationsToken], Extract< keyof Services[QraftServiceOperationsToken], CallbackList | 'schema' | 'types' > > : Services extends OperationsDeclaration<Services> ? { [operation in keyof Services]: Pick< Services[operation][QraftServiceOperationsToken], Extract< keyof Services[operation][QraftServiceOperationsToken], CallbackList | 'schema' | 'types' > >; } : Services extends ServicesDeclaration<Services> ? { [serviceName in keyof Services]: { [operation in keyof Services[serviceName]]: Pick< Services[serviceName][operation][QraftServiceOperationsToken], Extract< keyof Services[serviceName][operation][QraftServiceOperationsToken], CallbackList | 'schema' | 'types' > >; }; } : never;