diff --git a/README.md b/README.md index b5f87bd..8141630 100644 --- a/README.md +++ b/README.md @@ -71,13 +71,16 @@ $ openapi-rq -i ./petstore.yaml ``` - openapi - queries - - index.ts <- main file that exports common types, variables, and hooks + - index.ts <- main file that exports common types, variables, and queries. Does not export suspense or prefetch hooks - common.ts <- common types - queries.ts <- generated query hooks - suspenses.ts <- generated suspense hooks + - prefetch.ts <- generated prefetch hooks learn more about prefetching in in link below - requests <- output code generated by @hey-api/openapi-ts ``` +- [Prefetching docs](https://tanstack.com/query/latest/docs/framework/react/guides/advanced-ssr#prefetching-and-dehydrating-data) + ### In your app ```tsx diff --git a/examples/react-app/src/main.tsx b/examples/react-app/src/main.tsx index b8a928c..30ad103 100644 --- a/examples/react-app/src/main.tsx +++ b/examples/react-app/src/main.tsx @@ -4,10 +4,17 @@ import App from "./App"; import "./index.css"; import { QueryClientProvider } from "@tanstack/react-query"; import { queryClient } from "./queryClient"; +import { useDefaultServiceFindPetsPrefetch } from "../openapi/queries/prefetch"; + +const PrefetchData = () => { + useDefaultServiceFindPetsPrefetch(queryClient); + return null; +}; ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( + diff --git a/src/constants.mts b/src/constants.mts index 1bed51e..6eabb5b 100644 --- a/src/constants.mts +++ b/src/constants.mts @@ -4,3 +4,11 @@ export const requestsOutputPath = "requests"; export const serviceFileName = "services.gen"; export const modalsFileName = "types.gen"; + +export const OpenApiRqFiles = { + queries: "queries", + common: "common", + suspense: "suspense", + index: "index", + prefetch: "prefetch", +} as const; diff --git a/src/createExports.mts b/src/createExports.mts index d769875..aad6c0b 100644 --- a/src/createExports.mts +++ b/src/createExports.mts @@ -1,6 +1,7 @@ import { createUseQuery } from "./createUseQuery.mjs"; import { createUseMutation } from "./createUseMutation.mjs"; import { Service } from "./service.mjs"; +import { createPrefetch } from "./createPrefetch.mjs"; export const createExports = (service: Service) => { const { klasses } = service; @@ -23,6 +24,7 @@ export const createExports = (service: Service) => { ); const allGetQueries = allGet.map((m) => createUseQuery(m)); + const allPrefetchQueries = allGet.map((m) => createPrefetch(m)); const allPostMutations = allPost.map((m) => createUseMutation(m)); const allPutMutations = allPut.map((m) => createUseMutation(m)); @@ -59,6 +61,12 @@ export const createExports = (service: Service) => { const suspenseExports = [...suspenseQueries]; + const allPrefetches = allPrefetchQueries + .map(({ prefetchHook }) => [prefetchHook]) + .flat(); + + const allPrefetchExports = [...allPrefetches]; + return { /** * Common types and variables between queries (regular and suspense) and mutations @@ -72,5 +80,9 @@ export const createExports = (service: Service) => { * Suspense exports are the hooks that are used in the suspense components */ suspenseExports, + /** + * Prefetch exports are the hooks that are used in the prefetch components + */ + allPrefetchExports, }; }; diff --git a/src/createPrefetch.mts b/src/createPrefetch.mts new file mode 100644 index 0000000..d9d5e88 --- /dev/null +++ b/src/createPrefetch.mts @@ -0,0 +1,160 @@ +import ts from "typescript"; +import { MethodDeclaration } from "ts-morph"; +import { + BuildCommonTypeName, + extractPropertiesFromObjectParam, + getNameFromMethod, +} from "./common.mjs"; +import { type MethodDescription } from "./common.mjs"; +import { + createQueryKeyFromMethod, + getRequestParamFromMethod, + hookNameFromMethod, +} from "./createUseQuery.mjs"; +import { addJSDocToNode } from "./util.mjs"; + +/** + * Creates a custom hook for a query + * @param queryString The type of query to use from react-query + * @param suffix The suffix to append to the hook name + */ +function createPrefetchHook({ + requestParams, + method, + className, +}: { + requestParams: ts.ParameterDeclaration[]; + method: MethodDeclaration; + className: string; +}) { + const methodName = getNameFromMethod(method); + const customHookName = hookNameFromMethod({ method, className }); + const queryKey = createQueryKeyFromMethod({ method, className }); + + // const + const hookExport = ts.factory.createVariableStatement( + // export + [ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)], + ts.factory.createVariableDeclarationList( + [ + ts.factory.createVariableDeclaration( + ts.factory.createIdentifier(`${customHookName}Prefetch`), + undefined, + undefined, + ts.factory.createArrowFunction( + undefined, + undefined, + [ + ts.factory.createParameterDeclaration( + undefined, + undefined, + "queryClient", + undefined, + ts.factory.createTypeReferenceNode( + ts.factory.createIdentifier("QueryClient") + ) + ), + ...requestParams, + ], + undefined, + ts.factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), + ts.factory.createCallExpression( + ts.factory.createIdentifier("queryClient.prefetchQuery"), + undefined, + [ + ts.factory.createObjectLiteralExpression([ + ts.factory.createPropertyAssignment( + ts.factory.createIdentifier("queryKey"), + ts.factory.createArrayLiteralExpression( + [ + BuildCommonTypeName(queryKey), + method.getParameters().length + ? ts.factory.createArrayLiteralExpression([ + ts.factory.createObjectLiteralExpression( + method + .getParameters() + .map((param) => + extractPropertiesFromObjectParam(param).map( + (p) => + ts.factory.createShorthandPropertyAssignment( + ts.factory.createIdentifier(p.name) + ) + ) + ) + .flat() + ), + ]) + : ts.factory.createArrayLiteralExpression([]), + ], + false + ) + ), + ts.factory.createPropertyAssignment( + ts.factory.createIdentifier("queryFn"), + ts.factory.createArrowFunction( + undefined, + undefined, + [], + undefined, + ts.factory.createToken( + ts.SyntaxKind.EqualsGreaterThanToken + ), + ts.factory.createCallExpression( + ts.factory.createPropertyAccessExpression( + ts.factory.createIdentifier(className), + ts.factory.createIdentifier(methodName) + ), + undefined, + method.getParameters().length + ? [ + ts.factory.createObjectLiteralExpression( + method + .getParameters() + .map((param) => + extractPropertiesFromObjectParam(param).map( + (p) => + ts.factory.createShorthandPropertyAssignment( + ts.factory.createIdentifier(p.name) + ) + ) + ) + .flat() + ), + ] + : undefined + ) + ) + ), + ]), + ] + ) + ) + ), + ], + ts.NodeFlags.Const + ) + ); + return hookExport; +} + +export const createPrefetch = ({ + className, + method, + jsDoc, +}: MethodDescription) => { + const requestParam = getRequestParamFromMethod(method); + + const requestParams = requestParam ? [requestParam] : []; + + const prefetchHook = createPrefetchHook({ + requestParams, + method, + className, + }); + + const hookWithJsDoc = addJSDocToNode(prefetchHook, jsDoc); + + return { + prefetchHook: hookWithJsDoc, + }; +}; diff --git a/src/createSource.mts b/src/createSource.mts index 25ac103..f87b7b0 100644 --- a/src/createSource.mts +++ b/src/createSource.mts @@ -1,9 +1,10 @@ import ts from "typescript"; +import { Project } from "ts-morph"; +import { join } from "path"; +import { OpenApiRqFiles } from "./constants.mjs"; import { createImports } from "./createImports.mjs"; import { createExports } from "./createExports.mjs"; import { getServices } from "./service.mjs"; -import { Project } from "ts-morph"; -import { join } from "path"; const createSourceFile = async (outputPath: string, serviceEndName: string) => { const project = new Project({ @@ -77,11 +78,18 @@ const createSourceFile = async (outputPath: string, serviceEndName: string) => { ts.NodeFlags.None ); + const prefetchSource = ts.factory.createSourceFile( + [commonImport, ...imports, ...exports.allPrefetchExports], + ts.factory.createToken(ts.SyntaxKind.EndOfFileToken), + ts.NodeFlags.None + ); + return { commonSource, mainSource, suspenseSource, indexSource, + prefetchSource, }; }; @@ -95,21 +103,21 @@ export const createSource = async ({ serviceEndName: string; }) => { const queriesFile = ts.createSourceFile( - "queries.ts", + `${OpenApiRqFiles.queries}.ts`, "", ts.ScriptTarget.Latest, false, ts.ScriptKind.TS ); const commonFile = ts.createSourceFile( - "common.ts", + `${OpenApiRqFiles.common}.ts`, "", ts.ScriptTarget.Latest, false, ts.ScriptKind.TS ); const suspenseFile = ts.createSourceFile( - "suspense.ts", + `${OpenApiRqFiles.suspense}.ts`, "", ts.ScriptTarget.Latest, false, @@ -117,7 +125,15 @@ export const createSource = async ({ ); const indexFile = ts.createSourceFile( - "index.ts", + `${OpenApiRqFiles.index}.ts`, + "", + ts.ScriptTarget.Latest, + false, + ts.ScriptKind.TS + ); + + const prefetchFile = ts.createSourceFile( + `${OpenApiRqFiles.prefetch}.ts`, "", ts.ScriptTarget.Latest, false, @@ -129,8 +145,13 @@ export const createSource = async ({ removeComments: false, }); - const { commonSource, mainSource, suspenseSource, indexSource } = - await createSourceFile(outputPath, serviceEndName); + const { + commonSource, + mainSource, + suspenseSource, + indexSource, + prefetchSource, + } = await createSourceFile(outputPath, serviceEndName); const comment = `// generated with @7nohe/openapi-react-query-codegen@${version} \n\n`; @@ -150,6 +171,10 @@ export const createSource = async ({ comment + printer.printNode(ts.EmitHint.Unspecified, indexSource, indexFile); + const prefetchResult = + comment + + printer.printNode(ts.EmitHint.Unspecified, prefetchSource, prefetchFile); + return [ { name: "index.ts", @@ -167,5 +192,9 @@ export const createSource = async ({ name: "suspense.ts", content: suspenseResult, }, + { + name: "prefetch.ts", + content: prefetchResult, + }, ]; }; diff --git a/src/createUseQuery.mts b/src/createUseQuery.mts index a8e827a..ef9d2b2 100644 --- a/src/createUseQuery.mts +++ b/src/createUseQuery.mts @@ -200,7 +200,7 @@ export function createQueryKeyExport({ ); } -function hookNameFromMethod({ +export function hookNameFromMethod({ method, className, }: { @@ -211,7 +211,7 @@ function hookNameFromMethod({ return `use${className}${capitalizeFirstLetter(methodName)}`; } -function createQueryKeyFromMethod({ +export function createQueryKeyFromMethod({ method, className, }: { @@ -228,7 +228,7 @@ function createQueryKeyFromMethod({ * @param queryString The type of query to use from react-query * @param suffix The suffix to append to the hook name */ -function createQueryHook({ +export function createQueryHook({ queryString, suffix, responseDataType, diff --git a/tests/__snapshots__/createSource.test.ts.snap b/tests/__snapshots__/createSource.test.ts.snap index cfea912..770e84a 100644 --- a/tests/__snapshots__/createSource.test.ts.snap +++ b/tests/__snapshots__/createSource.test.ts.snap @@ -151,3 +151,48 @@ export const useDefaultServiceFindPetByIdSuspense = , "queryKey" | "queryFn">) => useSuspenseQuery({ queryKey: [Common.useDefaultServiceFindPetByIdKey, ...(queryKey ?? [{ id }])], queryFn: () => DefaultService.findPetById({ id }) as TData, ...options }); " `; + +exports[`createSource > createSource 5`] = ` +"// generated with @7nohe/openapi-react-query-codegen@1.0.0 + +import * as Common from "./common"; +import { useQuery, useSuspenseQuery, useMutation, UseQueryResult, UseQueryOptions, UseMutationOptions, UseMutationResult } from "@tanstack/react-query"; +import { DefaultService } from "../requests/services.gen"; +import { Pet, NewPet, Error, $OpenApiTs } from "../requests/types.gen"; +/** +* Returns all pets from the system that the user has access to +* Nam sed condimentum est. Maecenas tempor sagittis sapien, nec rhoncus sem sagittis sit amet. Aenean at gravida augue, ac iaculis sem. Curabitur odio lorem, ornare eget elementum nec, cursus id lectus. Duis mi turpis, pulvinar ac eros ac, tincidunt varius justo. In hac habitasse platea dictumst. Integer at adipiscing ante, a sagittis ligula. Aenean pharetra tempor ante molestie imperdiet. Vivamus id aliquam diam. Cras quis velit non tortor eleifend sagittis. Praesent at enim pharetra urna volutpat venenatis eget eget mauris. In eleifend fermentum facilisis. Praesent enim enim, gravida ac sodales sed, placerat id erat. Suspendisse lacus dolor, consectetur non augue vel, vehicula interdum libero. Morbi euismod sagittis libero sed lacinia. +* +* Sed tempus felis lobortis leo pulvinar rutrum. Nam mattis velit nisl, eu condimentum ligula luctus nec. Phasellus semper velit eget aliquet faucibus. In a mattis elit. Phasellus vel urna viverra, condimentum lorem id, rhoncus nibh. Ut pellentesque posuere elementum. Sed a varius odio. Morbi rhoncus ligula libero, vel eleifend nunc tristique vitae. Fusce et sem dui. Aenean nec scelerisque tortor. Fusce malesuada accumsan magna vel tempus. Quisque mollis felis eu dolor tristique, sit amet auctor felis gravida. Sed libero lorem, molestie sed nisl in, accumsan tempor nisi. Fusce sollicitudin massa ut lacinia mattis. Sed vel eleifend lorem. Pellentesque vitae felis pretium, pulvinar elit eu, euismod sapien. +* +* @param data The data for the request. +* @param data.tags tags to filter by +* @param data.limit maximum number of results to return +* @returns Pet pet response +* @returns Error unexpected error +* @throws ApiError +*/ +export const useDefaultServiceFindPetsPrefetch = (queryClient: QueryClient, { limit, tags }: { + limit?: number; + tags?: string[]; +} = {}) => queryClient.prefetchQuery({ queryKey: [Common.useDefaultServiceFindPetsKey, [{ limit, tags }]], queryFn: () => DefaultService.findPets({ limit, tags }) }); +/** +* @deprecated +* This path is not fully defined. +* @returns unknown unexpected error +* @throws ApiError +*/ +export const useDefaultServiceGetNotDefinedPrefetch = (queryClient: QueryClient) => queryClient.prefetchQuery({ queryKey: [Common.useDefaultServiceGetNotDefinedKey, []], queryFn: () => DefaultService.getNotDefined() }); +/** +* Returns a user based on a single ID, if the user does not have access to the pet +* @param data The data for the request. +* @param data.id ID of pet to fetch +* @returns Pet pet response +* @returns Error unexpected error +* @throws ApiError +*/ +export const useDefaultServiceFindPetByIdPrefetch = (queryClient: QueryClient, { id }: { + id: number; +}) => queryClient.prefetchQuery({ queryKey: [Common.useDefaultServiceFindPetByIdKey, [{ id }]], queryFn: () => DefaultService.findPetById({ id }) }); +" +`; diff --git a/tests/__snapshots__/generate.test.ts.snap b/tests/__snapshots__/generate.test.ts.snap index 99bf153..982362f 100644 --- a/tests/__snapshots__/generate.test.ts.snap +++ b/tests/__snapshots__/generate.test.ts.snap @@ -28,6 +28,49 @@ export * from "./queries"; " `; +exports[`generate > prefetch.ts 1`] = ` +"// generated with @7nohe/openapi-react-query-codegen@1.0.0 + +import { DefaultService } from "../requests/services.gen"; +import * as Common from "./common"; +/** +* Returns all pets from the system that the user has access to +* Nam sed condimentum est. Maecenas tempor sagittis sapien, nec rhoncus sem sagittis sit amet. Aenean at gravida augue, ac iaculis sem. Curabitur odio lorem, ornare eget elementum nec, cursus id lectus. Duis mi turpis, pulvinar ac eros ac, tincidunt varius justo. In hac habitasse platea dictumst. Integer at adipiscing ante, a sagittis ligula. Aenean pharetra tempor ante molestie imperdiet. Vivamus id aliquam diam. Cras quis velit non tortor eleifend sagittis. Praesent at enim pharetra urna volutpat venenatis eget eget mauris. In eleifend fermentum facilisis. Praesent enim enim, gravida ac sodales sed, placerat id erat. Suspendisse lacus dolor, consectetur non augue vel, vehicula interdum libero. Morbi euismod sagittis libero sed lacinia. +* +* Sed tempus felis lobortis leo pulvinar rutrum. Nam mattis velit nisl, eu condimentum ligula luctus nec. Phasellus semper velit eget aliquet faucibus. In a mattis elit. Phasellus vel urna viverra, condimentum lorem id, rhoncus nibh. Ut pellentesque posuere elementum. Sed a varius odio. Morbi rhoncus ligula libero, vel eleifend nunc tristique vitae. Fusce et sem dui. Aenean nec scelerisque tortor. Fusce malesuada accumsan magna vel tempus. Quisque mollis felis eu dolor tristique, sit amet auctor felis gravida. Sed libero lorem, molestie sed nisl in, accumsan tempor nisi. Fusce sollicitudin massa ut lacinia mattis. Sed vel eleifend lorem. Pellentesque vitae felis pretium, pulvinar elit eu, euismod sapien. +* +* @param data The data for the request. +* @param data.tags tags to filter by +* @param data.limit maximum number of results to return +* @returns Pet pet response +* @returns Error unexpected error +* @throws ApiError +*/ +export const useDefaultServiceFindPetsPrefetch = (queryClient: QueryClient, { limit, tags }: { + limit?: number; + tags?: string[]; +} = {}) => queryClient.prefetchQuery({ queryKey: [Common.useDefaultServiceFindPetsKey, [{ limit, tags }]], queryFn: () => DefaultService.findPets({ limit, tags }) }); +/** +* @deprecated +* This path is not fully defined. +* @returns unknown unexpected error +* @throws ApiError +*/ +export const useDefaultServiceGetNotDefinedPrefetch = (queryClient: QueryClient) => queryClient.prefetchQuery({ queryKey: [Common.useDefaultServiceGetNotDefinedKey, []], queryFn: () => DefaultService.getNotDefined() }); +/** +* Returns a user based on a single ID, if the user does not have access to the pet +* @param data The data for the request. +* @param data.id ID of pet to fetch +* @returns Pet pet response +* @returns Error unexpected error +* @throws ApiError +*/ +export const useDefaultServiceFindPetByIdPrefetch = (queryClient: QueryClient, { id }: { + id: number; +}) => queryClient.prefetchQuery({ queryKey: [Common.useDefaultServiceFindPetByIdKey, [{ id }]], queryFn: () => DefaultService.findPetById({ id }) }); +" +`; + exports[`generate > queries.ts 1`] = ` "// generated with @7nohe/openapi-react-query-codegen@1.0.0 diff --git a/tests/createSource.test.ts b/tests/createSource.test.ts index 429b476..abd08c9 100644 --- a/tests/createSource.test.ts +++ b/tests/createSource.test.ts @@ -1,12 +1,11 @@ import { afterAll, beforeAll, describe, expect, test } from "vitest"; -import { createSource } from '../src/createSource.mjs' +import { createSource } from "../src/createSource.mjs"; import { cleanOutputs, generateTSClients, outputPath } from "./utils"; const fileName = "createSource"; describe(fileName, () => { beforeAll(async () => await generateTSClients(fileName)); afterAll(async () => await cleanOutputs(fileName)); - test("createSource", async () => { const source = await createSource({ outputPath: outputPath(fileName), @@ -25,5 +24,8 @@ describe(fileName, () => { const suspenseTs = source.find((s) => s.name === "suspense.ts"); expect(suspenseTs?.content).toMatchSnapshot(); + + const prefetchTs = source.find((s) => s.name === "prefetch.ts"); + expect(prefetchTs?.content).toMatchSnapshot(); }); }); diff --git a/tests/generate.test.ts b/tests/generate.test.ts index eaba6a9..25b8d84 100644 --- a/tests/generate.test.ts +++ b/tests/generate.test.ts @@ -45,4 +45,8 @@ describe("generate", () => { test("suspense.ts", () => { expect(readOutput("suspense.ts")).toMatchSnapshot(); }); + + test("prefetch.ts", () => { + expect(readOutput("prefetch.ts")).toMatchSnapshot(); + }); });