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..8069014 100644 --- a/examples/react-app/src/main.tsx +++ b/examples/react-app/src/main.tsx @@ -4,11 +4,18 @@ import App from "./App"; import "./index.css"; import { QueryClientProvider } from "@tanstack/react-query"; import { queryClient } from "./queryClient"; +import { prefetchUseDefaultServiceFindPets } from "../openapi/queries/prefetch"; -ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( - - - - - -); +async function PrefetchData() { + await prefetchUseDefaultServiceFindPets(queryClient); +} + +PrefetchData().then(() => { + 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..71266a2 --- /dev/null +++ b/src/createPrefetch.mts @@ -0,0 +1,159 @@ +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 prefetch function for a query + */ +function createPrefetchHook({ + requestParams, + method, + className, +}: { + requestParams: ts.ParameterDeclaration[]; + method: MethodDeclaration; + className: string; +}) { + const methodName = getNameFromMethod(method); + const queryName = hookNameFromMethod({ method, className }); + const customHookName = `prefetch${queryName.charAt(0).toUpperCase() + queryName.slice(1)}`; + 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), + 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..9ab2bcb 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 prefetchUseDefaultServiceFindPets = (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 prefetchUseDefaultServiceGetNotDefined = (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 prefetchUseDefaultServiceFindPetById = (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..b62daa0 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 prefetchUseDefaultServiceFindPets = (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 prefetchUseDefaultServiceGetNotDefined = (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 prefetchUseDefaultServiceFindPetById = (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(); + }); });