typed-openapi
Version:
196 lines (177 loc) • 7.96 kB
text/typescript
import { capitalize } from "pastable/server";
import { prettify } from "./format.ts";
import type { mapOpenApiEndpoints } from "./map-openapi-endpoints.ts";
type GeneratorOptions = ReturnType<typeof mapOpenApiEndpoints>;
type GeneratorContext = Required<GeneratorOptions> & {
errorStatusCodes?: readonly number[];
};
export const generateTanstackQueryFile = async (ctx: GeneratorContext & { relativeApiClientPath: string }) => {
const endpointMethods = new Set(ctx.endpointList.map((endpoint) => endpoint.method.toLowerCase()));
const file = `
import { queryOptions } from "@tanstack/react-query"
import type { EndpointByMethod, ApiClient, SuccessStatusCode, ErrorStatusCode, InferResponseByStatus, TypedSuccessResponse } from "${ctx.relativeApiClientPath}"
import { errorStatusCodes, TypedStatusError } from "${ctx.relativeApiClientPath}"
type EndpointQueryKey<TOptions extends EndpointParameters> = [
TOptions & {
_id: string;
_infinite?: boolean;
}
];
const createQueryKey = <TOptions extends EndpointParameters>(id: string, options?: TOptions, infinite?: boolean): [
EndpointQueryKey<TOptions>[0]
] => {
const params: EndpointQueryKey<TOptions>[0] = { _id: id, } as EndpointQueryKey<TOptions>[0];
if (infinite) {
params._infinite = infinite;
}
if (options?.body) {
params.body = options.body;
}
if (options?.header) {
params.header = options.header;
}
if (options?.path) {
params.path = options.path;
}
if (options?.query) {
params.query = options.query;
}
return [
params
];
};
// <EndpointByMethod.Shorthands>
${Array.from(endpointMethods)
.map((method) => `export type ${capitalize(method)}Endpoints = EndpointByMethod["${method}"];`)
.join("\n")}
// </EndpointByMethod.Shorthands>
// <ApiClientTypes>
export type EndpointParameters = {
body?: unknown;
query?: Record<string, unknown>;
header?: Record<string, unknown>;
path?: Record<string, unknown>;
};
type RequiredKeys<T> = {
[P in keyof T]-?: undefined extends T[P] ? never : P;
}[keyof T];
type MaybeOptionalArg<T> = RequiredKeys<T> extends never ? [config?: T] : [config: T];
type InferResponseData<TEndpoint, TStatusCode> = TypedSuccessResponse<any, any, any> extends
InferResponseByStatus<TEndpoint, TStatusCode>
? Extract<InferResponseByStatus<TEndpoint, TStatusCode>, { data: {}}>["data"]
: Extract<InferResponseByStatus<TEndpoint, TStatusCode>["data"], {}>;
// </ApiClientTypes>
// <ApiClient>
export class TanstackQueryApiClient {
constructor(public client: ApiClient) { }
${Array.from(endpointMethods)
.map(
(method) => `
// <ApiClient.${method}>
${method}<
Path extends keyof ${capitalize(method)}Endpoints,
TEndpoint extends ${capitalize(method)}Endpoints[Path]
>(
path: Path,
...params: MaybeOptionalArg<TEndpoint["parameters"]>
) {
const queryKey = createQueryKey(path as string, params[0]);
const query = {
/** type-only property if you need easy access to the endpoint params */
"~endpoint": {} as TEndpoint,
queryKey,
queryFn: {} as "You need to pass .queryOptions to the useQuery hook",
queryOptions: queryOptions({
queryFn: async ({ queryKey, signal, }) => {
const requestParams = {
...(params[0] || {}),
...(queryKey[0] || {}),
overrides: { signal },
withResponse: false as const
};
const res = await this.client.${method}(path, requestParams as never);
return res as InferResponseData<TEndpoint, SuccessStatusCode>;
},
queryKey: queryKey
}),
};
return query
}
// </ApiClient.${method}>
`,
)
.join("\n")}
// <ApiClient.request>
/**
* Generic mutation method with full type-safety for any endpoint; it doesnt require parameters to be passed initially
* but instead will require them to be passed when calling the mutation.mutate() method
*/
mutation<
TMethod extends keyof EndpointByMethod,
TPath extends keyof EndpointByMethod[TMethod],
TEndpoint extends EndpointByMethod[TMethod][TPath],
TWithResponse extends boolean = false,
TSelection = TWithResponse extends true
? InferResponseByStatus<TEndpoint, SuccessStatusCode>
: InferResponseData<TEndpoint, SuccessStatusCode>,
TError = TEndpoint extends { responses: infer TResponses }
? TResponses extends Record<string | number, unknown>
? TypedStatusError<InferResponseData<TEndpoint, ErrorStatusCode>>
: Error
: Error
>(method: TMethod, path: TPath, options?: {
withResponse?: TWithResponse;
selectFn?: (res: TWithResponse extends true
? InferResponseByStatus<TEndpoint, SuccessStatusCode>
: InferResponseData<TEndpoint, SuccessStatusCode>
) => TSelection;
throwOnStatusError?: boolean
throwOnError?: boolean | ((error: TError) => boolean)
}) {
const mutationKey = [{ method, path }] as const;
const mutationFn = async (params: (TEndpoint extends { parameters: infer Parameters } ? Parameters : {}) & {
throwOnStatusError?: boolean;
overrides?: RequestInit;
}): Promise<TSelection> => {
const withResponse = options?.withResponse ?? false;
const throwOnStatusError = params.throwOnStatusError ?? options?.throwOnStatusError ?? (withResponse ? false : true);
const selectFn = options?.selectFn;
const response = await (this.client as any)[method](path, {
...params as any,
withResponse: true,
throwOnStatusError: false,
});
if (throwOnStatusError && errorStatusCodes.includes(response.status as never)) {
throw new TypedStatusError(response as never);
}
// Return just the data if withResponse is false, otherwise return the full response
const finalResponse = withResponse ? response : response.data;
const res = selectFn ? selectFn(finalResponse as any) : finalResponse;
return res as never;
};
return {
/** type-only property if you need easy access to the endpoint params */
"~endpoint": {} as TEndpoint,
mutationKey: mutationKey,
mutationFn: {} as "You need to pass .mutationOptions to the useMutation hook",
mutationOptions: {
throwOnError: options?.throwOnError as boolean | ((error: TError) => boolean),
mutationKey: mutationKey,
mutationFn: mutationFn,
} as Omit<import("@tanstack/react-query").UseMutationOptions<
TSelection,
TError,
(TEndpoint extends { parameters: infer Parameters } ? Parameters : {}) & {
withResponse?: boolean;
throwOnStatusError?: boolean;
}
>, "mutationFn"> & {
mutationFn: typeof mutationFn
},
}
}
// </ApiClient.request>
}
`;
return prettify(file);
};