diff --git a/README.md b/README.md index 13ac2e8..b014cb1 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ ## Features -- Generates custom react hooks that use React Query's `useQuery`, `useSuspenseQuery` and `useMutation` hooks +- Generates custom react hooks that use React Query's `useQuery`, `useSuspenseQuery`, `useMutation` and `useInfiniteQuery` hooks - Generates query keys and functions for query caching - Generates pure TypeScript clients generated by [@hey-api/openapi-ts](https://github.com/hey-api/openapi-ts) @@ -45,18 +45,20 @@ Options: -V, --version output the version number -i, --input OpenAPI specification, can be a path, url or string content (required) -o, --output Output directory (default: "openapi") - -c, --client HTTP client to generate [fetch, xhr, node, axios, angular] (default: "fetch") + -c, --client HTTP client to generate (choices: "angular", "axios", "fetch", "node", "xhr", default: "fetch") --request Path to custom request file - --format Process output folder with formatter? ['biome', 'prettier'] - --lint Process output folder with linter? ['eslint', 'biome'] + --format Process output folder with formatter? (choices: "biome", "prettier") + --lint Process output folder with linter? (choices: "biome", "eslint") --operationId Use operation ID to generate operation names? - --serviceResponse Define shape of returned value from service calls ['body', 'response'] (default: "body") + --serviceResponse Define shape of returned value from service calls (choices: "body", "response", default: "body") --base Manually set base in OpenAPI config instead of inferring from server value - --enums Generate JavaScript objects from enum definitions? ['javascript', 'typescript'] + --enums Generate JavaScript objects from enum definitions? (choices: "javascript", "typescript") --useDateType Use Date type instead of string for date types for models, this will not convert the data to a Date object - --debug Enable debug mode - --noSchemas Disable generating schemas for request and response objects - --schemaTypes Define the type of schema generation ['form', 'json'] (default: "json") + --debug Run in debug mode? + --noSchemas Disable generating JSON schemas + --schemaType Type of JSON schema [Default: 'json'] (choices: "form", "json") + --pageParam Name of the query parameter used for pagination (default: "page") + --nextPageParam Name of the response parameter used for next page (default: "nextPage") -h, --help display help for command ``` @@ -234,6 +236,72 @@ function App() { export default App; ``` +##### Using Infinite Query hooks + +This feature will generate a function in infiniteQueries.ts when the name specified by the `pageParam` option exists in the query parameters and the name specified by the `nextPageParam` option exists in the response. + +Example Schema: + +```yml +paths: + /paginated-pets: + get: + description: | + Returns paginated pets from the system that the user has access to + operationId: findPaginatedPets + parameters: + - name: page + in: query + description: page number + required: false + schema: + type: integer + format: int32 + - name: tags + in: query + description: tags to filter by + required: false + style: form + schema: + type: array + items: + type: string + - name: limit + in: query + description: maximum number of results to return + required: false + schema: + type: integer + format: int32 + responses: + '200': + description: pet response + content: + application/json: + schema: + type: object + properties: + pets: + type: array + items: + $ref: '#/components/schemas/Pet' + nextPage: + type: integer + format: int32 + minimum: 1 +``` + +Usage of Generated Hooks: + +```ts +import { useDefaultServiceFindPaginatedPetsInfinite } from "@/openapi/queries/infiniteQueries"; + +const { data, fetchNextPage } = useDefaultServiceFindPaginatedPetsInfinite({ + limit: 10, + tags: [], +}); +``` + ##### Runtime Configuration You can modify the default values used by the generated service calls by modifying the OpenAPI configuration singleton object. diff --git a/examples/nextjs-app/app/components/PaginatedPets.tsx b/examples/nextjs-app/app/components/PaginatedPets.tsx new file mode 100644 index 0000000..bfaee46 --- /dev/null +++ b/examples/nextjs-app/app/components/PaginatedPets.tsx @@ -0,0 +1,37 @@ +"use client"; + +import { useDefaultServiceFindPaginatedPetsInfinite } from "@/openapi/queries/infiniteQueries"; +import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; +import React from "react"; + +export default function PaginatedPets() { + const { data, fetchNextPage } = useDefaultServiceFindPaginatedPetsInfinite({ + limit: 10, + tags: [], + }); + + return ( + <> +

Pet List with Pagination

+
    + {data?.pages.map((group, i) => ( + + {group.pets?.map((pet) => ( +
  • {pet.name}
  • + ))} +
    + ))} +
+ {data?.pages.at(-1)?.nextPage && ( + + )} + + + ); +} diff --git a/examples/nextjs-app/app/pets.tsx b/examples/nextjs-app/app/components/Pets.tsx similarity index 100% rename from examples/nextjs-app/app/pets.tsx rename to examples/nextjs-app/app/components/Pets.tsx diff --git a/examples/nextjs-app/app/infinite-loader/page.tsx b/examples/nextjs-app/app/infinite-loader/page.tsx new file mode 100644 index 0000000..60268fc --- /dev/null +++ b/examples/nextjs-app/app/infinite-loader/page.tsx @@ -0,0 +1,9 @@ +import PaginatedPets from "../components/PaginatedPets"; + +export default async function InfiniteLoaderPage() { + return ( +
+ +
+ ); +} diff --git a/examples/nextjs-app/app/page.tsx b/examples/nextjs-app/app/page.tsx index fa9e68f..9d4a1ac 100644 --- a/examples/nextjs-app/app/page.tsx +++ b/examples/nextjs-app/app/page.tsx @@ -4,7 +4,8 @@ import { QueryClient, dehydrate, } from "@tanstack/react-query"; -import Pets from "./pets"; +import Link from "next/link"; +import Pets from "./components/Pets"; export default async function Home() { const queryClient = new QueryClient(); @@ -19,6 +20,9 @@ export default async function Home() { + + Go to Infinite Loader → + ); } diff --git a/examples/nextjs-app/request.ts b/examples/nextjs-app/request.ts index dd12c47..98e0a7f 100644 --- a/examples/nextjs-app/request.ts +++ b/examples/nextjs-app/request.ts @@ -77,7 +77,10 @@ export const request = ( url: options.url, data: options.body, method: options.method, - params: options.path, + params: { + ...options.query, + ...options.path, + }, headers: formattedHeaders, cancelToken: source.token, }) diff --git a/examples/petstore.yaml b/examples/petstore.yaml index cfe7026..2a038a1 100644 --- a/examples/petstore.yaml +++ b/examples/petstore.yaml @@ -135,6 +135,52 @@ paths: application/json: schema: $ref: '#/components/schemas/Error' + /paginated-pets: + get: + description: | + Returns paginated pets from the system that the user has access to + operationId: findPaginatedPets + parameters: + - name: page + in: query + description: page number + required: false + schema: + type: integer + format: int32 + - name: tags + in: query + description: tags to filter by + required: false + style: form + schema: + type: array + items: + type: string + - name: limit + in: query + description: maximum number of results to return + required: false + schema: + type: integer + format: int32 + responses: + '200': + description: pet response + content: + application/json: + schema: + type: object + properties: + pets: + type: array + items: + $ref: '#/components/schemas/Pet' + nextPage: + type: integer + format: int32 + minimum: 1 + components: schemas: Pet: diff --git a/examples/react-app/request.ts b/examples/react-app/request.ts index dd12c47..98e0a7f 100644 --- a/examples/react-app/request.ts +++ b/examples/react-app/request.ts @@ -77,7 +77,10 @@ export const request = ( url: options.url, data: options.body, method: options.method, - params: options.path, + params: { + ...options.query, + ...options.path, + }, headers: formattedHeaders, cancelToken: source.token, }) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a6772a5..c09fcd4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4420,7 +4420,7 @@ snapshots: mkdirp@0.5.6: dependencies: - minimist: 1.2.6 + minimist: 1.2.8 mkdirp@1.0.4: {} diff --git a/src/cli.mts b/src/cli.mts index d5e5946..155f7f1 100644 --- a/src/cli.mts +++ b/src/cli.mts @@ -23,6 +23,8 @@ export type LimitedUserConfig = { debug?: boolean; noSchemas?: boolean; schemaType?: "form" | "json"; + pageParam: string; + nextPageParam: string; }; async function setupProgram() { @@ -90,6 +92,16 @@ async function setupProgram() { "Type of JSON schema [Default: 'json']", ).choices(["form", "json"]), ) + .option( + "--pageParam ", + "Name of the query parameter used for pagination", + "page", + ) + .option( + "--nextPageParam ", + "Name of the response parameter used for next page", + "nextPage", + ) .parse(); const options = program.opts(); diff --git a/src/constants.mts b/src/constants.mts index 6eabb5b..acb71d3 100644 --- a/src/constants.mts +++ b/src/constants.mts @@ -7,6 +7,7 @@ export const modalsFileName = "types.gen"; export const OpenApiRqFiles = { queries: "queries", + infiniteQueries: "infiniteQueries", common: "common", suspense: "suspense", index: "index", diff --git a/src/createExports.mts b/src/createExports.mts index 75166a0..74eb6c2 100644 --- a/src/createExports.mts +++ b/src/createExports.mts @@ -1,9 +1,14 @@ +import type ts from "typescript"; import { createPrefetch } from "./createPrefetch.mjs"; import { createUseMutation } from "./createUseMutation.mjs"; import { createUseQuery } from "./createUseQuery.mjs"; import type { Service } from "./service.mjs"; -export const createExports = (service: Service) => { +export const createExports = ( + service: Service, + pageParam: string, + nextPageParam: string, +) => { const { klasses } = service; const methods = klasses.flatMap((k) => k.methods); @@ -23,7 +28,9 @@ export const createExports = (service: Service) => { m.httpMethodName.toUpperCase().includes("DELETE"), ); - const allGetQueries = allGet.map((m) => createUseQuery(m)); + const allGetQueries = allGet.map((m) => + createUseQuery(m, pageParam, nextPageParam), + ); const allPrefetchQueries = allGet.map((m) => createPrefetch(m)); const allPostMutations = allPost.map((m) => createUseMutation(m)); @@ -60,6 +67,10 @@ export const createExports = (service: Service) => { const mainExports = [...mainQueries, ...mainMutations]; + const infiniteQueriesExports = allQueries + .flatMap(({ infiniteQueryHook }) => [infiniteQueryHook]) + .filter(Boolean) as ts.VariableStatement[]; + const suspenseQueries = allQueries.flatMap(({ suspenseQueryHook }) => [ suspenseQueryHook, ]); @@ -81,6 +92,10 @@ export const createExports = (service: Service) => { * Main exports are the hooks that are used in the components */ mainExports, + /** + * Infinite queries exports are the hooks that are used in the infinite scroll components + */ + infiniteQueriesExports, /** * Suspense exports are the hooks that are used in the suspense components */ diff --git a/src/createSource.mts b/src/createSource.mts index 2c4b9cf..3d8d870 100644 --- a/src/createSource.mts +++ b/src/createSource.mts @@ -6,7 +6,12 @@ import { createExports } from "./createExports.mjs"; import { createImports } from "./createImports.mjs"; import { getServices } from "./service.mjs"; -const createSourceFile = async (outputPath: string, serviceEndName: string) => { +const createSourceFile = async ( + outputPath: string, + serviceEndName: string, + pageParam: string, + nextPageParam: string, +) => { const project = new Project({ // Optionally specify compiler options, tsconfig.json, in-memory file system, and more here. // If you initialize with a tsconfig.json, then it will automatically populate the project @@ -25,7 +30,7 @@ const createSourceFile = async (outputPath: string, serviceEndName: string) => { project, }); - const exports = createExports(service); + const exports = createExports(service, pageParam, nextPageParam); const commonSource = ts.factory.createSourceFile( [...imports, ...exports.allCommon], @@ -66,6 +71,12 @@ const createSourceFile = async (outputPath: string, serviceEndName: string) => { ts.NodeFlags.None, ); + const infiniteQueriesSource = ts.factory.createSourceFile( + [commonImport, ...imports, ...exports.infiniteQueriesExports], + ts.factory.createToken(ts.SyntaxKind.EndOfFileToken), + ts.NodeFlags.None, + ); + const suspenseSource = ts.factory.createSourceFile( [commonImport, ...imports, ...exports.suspenseExports], ts.factory.createToken(ts.SyntaxKind.EndOfFileToken), @@ -86,6 +97,7 @@ const createSourceFile = async (outputPath: string, serviceEndName: string) => { return { commonSource, + infiniteQueriesSource, mainSource, suspenseSource, indexSource, @@ -97,10 +109,14 @@ export const createSource = async ({ outputPath, version, serviceEndName, + pageParam, + nextPageParam, }: { outputPath: string; version: string; serviceEndName: string; + pageParam: string; + nextPageParam: string; }) => { const queriesFile = ts.createSourceFile( `${OpenApiRqFiles.queries}.ts`, @@ -109,6 +125,13 @@ export const createSource = async ({ false, ts.ScriptKind.TS, ); + const infiniteQueriesFile = ts.createSourceFile( + `${OpenApiRqFiles.infiniteQueries}.ts`, + "", + ts.ScriptTarget.Latest, + false, + ts.ScriptKind.TS, + ); const commonFile = ts.createSourceFile( `${OpenApiRqFiles.common}.ts`, "", @@ -148,10 +171,16 @@ export const createSource = async ({ const { commonSource, mainSource, + infiniteQueriesSource, suspenseSource, indexSource, prefetchSource, - } = await createSourceFile(outputPath, serviceEndName); + } = await createSourceFile( + outputPath, + serviceEndName, + pageParam, + nextPageParam, + ); const comment = `// generated with @7nohe/openapi-react-query-codegen@${version} \n\n`; @@ -163,6 +192,14 @@ export const createSource = async ({ comment + printer.printNode(ts.EmitHint.Unspecified, mainSource, queriesFile); + const infiniteQueriesResult = + comment + + printer.printNode( + ts.EmitHint.Unspecified, + infiniteQueriesSource, + infiniteQueriesFile, + ); + const suspenseResult = comment + printer.printNode(ts.EmitHint.Unspecified, suspenseSource, suspenseFile); @@ -184,6 +221,10 @@ export const createSource = async ({ name: `${OpenApiRqFiles.common}.ts`, content: commonResult, }, + { + name: `${OpenApiRqFiles.infiniteQueries}.ts`, + content: infiniteQueriesResult, + }, { name: `${OpenApiRqFiles.queries}.ts`, content: mainResult, diff --git a/src/createUseQuery.mts b/src/createUseQuery.mts index 5378004..cebcecc 100644 --- a/src/createUseQuery.mts +++ b/src/createUseQuery.mts @@ -78,18 +78,24 @@ export const createApiResponseType = ({ }; }; -export function getRequestParamFromMethod(method: MethodDeclaration) { +export function getRequestParamFromMethod( + method: MethodDeclaration, + pageParam?: string, +) { if (!method.getParameters().length) { return null; } const params = method.getParameters().flatMap((param) => { const paramNodes = extractPropertiesFromObjectParam(param); - return paramNodes.map((refParam) => ({ - name: refParam.name, - typeName: getShortType(refParam.type.getText()), - optional: refParam.optional, - })); + + return paramNodes + .filter((p) => p.name !== pageParam) + .map((refParam) => ({ + name: refParam.name, + typeName: getShortType(refParam.type.getText()), + optional: refParam.optional, + })); }); const areAllPropertiesOptional = params.every((param) => param.optional); @@ -238,18 +244,37 @@ export function createQueryHook({ requestParams, method, className, + pageParam, + nextPageParam, }: { - queryString: "useSuspenseQuery" | "useQuery"; + queryString: "useSuspenseQuery" | "useQuery" | "useInfiniteQuery"; suffix: string; responseDataType: ts.TypeParameterDeclaration; requestParams: ts.ParameterDeclaration[]; method: MethodDeclaration; className: string; + pageParam?: string; + nextPageParam?: string; }) { const methodName = getNameFromMethod(method); const customHookName = hookNameFromMethod({ method, className }); const queryKey = createQueryKeyFromMethod({ method, className }); + if ( + queryString === "useInfiniteQuery" && + (pageParam === undefined || nextPageParam === undefined) + ) { + throw new Error( + "pageParam and nextPageParam are required for infinite queries", + ); + } + + const isInfiniteQuery = queryString === "useInfiniteQuery"; + + const responseDataTypeRef = responseDataType.default as ts.TypeReferenceNode; + const responseDataTypeIdentifier = + responseDataTypeRef.typeName as ts.Identifier; + const hookExport = ts.factory.createVariableStatement( [ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)], ts.factory.createVariableDeclarationList( @@ -261,7 +286,21 @@ export function createQueryHook({ ts.factory.createArrowFunction( undefined, ts.factory.createNodeArray([ - responseDataType, + isInfiniteQuery + ? ts.factory.createTypeParameterDeclaration( + undefined, + TData, + undefined, + ts.factory.createTypeReferenceNode( + ts.factory.createIdentifier("InfiniteData"), + [ + ts.factory.createTypeReferenceNode( + responseDataTypeIdentifier, + ), + ], + ), + ) + : responseDataType, ts.factory.createTypeParameterDeclaration( undefined, TError, @@ -297,7 +336,11 @@ export function createQueryHook({ ts.factory.createIdentifier("Omit"), [ ts.factory.createTypeReferenceNode( - ts.factory.createIdentifier("UseQueryOptions"), + ts.factory.createIdentifier( + isInfiniteQuery + ? "UseInfiniteQueryOptions" + : "UseQueryOptions", + ), [ ts.factory.createTypeReferenceNode(TData), ts.factory.createTypeReferenceNode(TError), @@ -319,10 +362,12 @@ export function createQueryHook({ EqualsOrGreaterThanToken, ts.factory.createCallExpression( ts.factory.createIdentifier(queryString), - [ - ts.factory.createTypeReferenceNode(TData), - ts.factory.createTypeReferenceNode(TError), - ], + isInfiniteQuery + ? [] + : [ + ts.factory.createTypeReferenceNode(TData), + ts.factory.createTypeReferenceNode(TError), + ], [ ts.factory.createObjectLiteralExpression([ ts.factory.createPropertyAssignment( @@ -334,16 +379,15 @@ export function createQueryHook({ method.getParameters().length ? [ ts.factory.createObjectLiteralExpression( - method - .getParameters() - .flatMap((param) => - extractPropertiesFromObjectParam(param).map( - (p) => - ts.factory.createShorthandPropertyAssignment( - ts.factory.createIdentifier(p.name), - ), + method.getParameters().flatMap((param) => + extractPropertiesFromObjectParam(param) + .filter((p) => p.name !== pageParam) + .map((p) => + ts.factory.createShorthandPropertyAssignment( + ts.factory.createIdentifier(p.name), + ), ), - ), + ), ), ts.factory.createIdentifier("queryKey"), ] @@ -355,7 +399,24 @@ export function createQueryHook({ ts.factory.createArrowFunction( undefined, undefined, - [], + isInfiniteQuery + ? [ + ts.factory.createParameterDeclaration( + undefined, + undefined, + ts.factory.createObjectBindingPattern([ + ts.factory.createBindingElement( + undefined, + undefined, + ts.factory.createIdentifier("pageParam"), + undefined, + ), + ]), + undefined, + undefined, + ), + ] + : [], undefined, EqualsOrGreaterThanToken, ts.factory.createAsExpression( @@ -374,9 +435,25 @@ export function createQueryHook({ extractPropertiesFromObjectParam( param, ).map((p) => - ts.factory.createShorthandPropertyAssignment( - ts.factory.createIdentifier(p.name), - ), + p.name === pageParam + ? ts.factory.createPropertyAssignment( + ts.factory.createIdentifier( + p.name, + ), + ts.factory.createAsExpression( + ts.factory.createIdentifier( + "pageParam", + ), + ts.factory.createKeywordTypeNode( + ts.SyntaxKind.NumberKeyword, + ), + ), + ) + : ts.factory.createShorthandPropertyAssignment( + ts.factory.createIdentifier( + p.name, + ), + ), ), ), ), @@ -387,6 +464,7 @@ export function createQueryHook({ ), ), ), + ...createInfiniteQueryParams(pageParam, nextPageParam), ts.factory.createSpreadAssignment( ts.factory.createIdentifier("options"), ), @@ -402,11 +480,11 @@ export function createQueryHook({ return hookExport; } -export const createUseQuery = ({ - className, - method, - jsDoc, -}: MethodDescription) => { +export const createUseQuery = ( + { className, method, jsDoc }: MethodDescription, + pageParam: string, + nextPageParam: string, +) => { const methodName = getNameFromMethod(method); const queryKey = createQueryKeyFromMethod({ method, className }); const { apiResponse: defaultApiResponse, responseDataType } = @@ -416,9 +494,18 @@ export const createUseQuery = ({ }); const requestParam = getRequestParamFromMethod(method); + const infiniteRequestParam = getRequestParamFromMethod(method, pageParam); const requestParams = requestParam ? [requestParam] : []; + const requestParamNames = requestParams + .filter((p) => p.name.kind === ts.SyntaxKind.ObjectBindingPattern) + .map((p) => p.name as ts.ObjectBindingPattern); + const requestParamTexts = requestParamNames + .at(0) + ?.elements.filter((e) => e.name.kind === ts.SyntaxKind.Identifier) + .map((e) => (e.name as ts.Identifier).escapedText as string); + const queryHook = createQueryHook({ queryString: "useQuery", suffix: "", @@ -427,6 +514,7 @@ export const createUseQuery = ({ method, className, }); + const suspenseQueryHook = createQueryHook({ queryString: "useSuspenseQuery", suffix: "Suspense", @@ -435,9 +523,25 @@ export const createUseQuery = ({ method, className, }); + const isInfiniteQuery = requestParamTexts?.includes(pageParam) ?? false; + const infiniteQueryHook = isInfiniteQuery + ? createQueryHook({ + queryString: "useInfiniteQuery", + suffix: "Infinite", + responseDataType, + requestParams: infiniteRequestParam ? [infiniteRequestParam] : [], + method, + className, + pageParam, + nextPageParam, + }) + : undefined; const hookWithJsDoc = addJSDocToNode(queryHook, jsDoc); const suspenseHookWithJsDoc = addJSDocToNode(suspenseQueryHook, jsDoc); + const infiniteHookWithJsDoc = infiniteQueryHook + ? addJSDocToNode(infiniteQueryHook, jsDoc) + : undefined; const returnTypeExport = createReturnTypeExport({ className, @@ -459,6 +563,7 @@ export const createUseQuery = ({ key: queryKeyExport, queryHook: hookWithJsDoc, suspenseQueryHook: suspenseHookWithJsDoc, + infiniteQueryHook: infiniteHookWithJsDoc, queryKeyFn, }; }; @@ -536,3 +641,45 @@ function queryKeyFn( false, ); } + +function createInfiniteQueryParams(pageParam?: string, nextPageParam?: string) { + if (pageParam === undefined || nextPageParam === undefined) { + return []; + } + return [ + ts.factory.createPropertyAssignment( + ts.factory.createIdentifier("initialPageParam"), + ts.factory.createNumericLiteral(1), + ), + ts.factory.createPropertyAssignment( + ts.factory.createIdentifier("getNextPageParam"), + // (response) => (response as { nextPage: number }).nextPage, + ts.factory.createArrowFunction( + undefined, + undefined, + [ + ts.factory.createParameterDeclaration( + undefined, + undefined, + ts.factory.createIdentifier("response"), + undefined, + undefined, + ), + ], + undefined, + EqualsOrGreaterThanToken, + ts.factory.createPropertyAccessExpression( + ts.factory.createParenthesizedExpression( + ts.factory.createAsExpression( + ts.factory.createIdentifier("response"), + ts.factory.createTypeReferenceNode( + ts.factory.createIdentifier(`{ ${nextPageParam}: number }`), + ), + ), + ), + ts.factory.createIdentifier(nextPageParam), + ), + ), + ), + ]; +} diff --git a/src/generate.mts b/src/generate.mts index 3f9c04f..f739a3c 100644 --- a/src/generate.mts +++ b/src/generate.mts @@ -46,6 +46,8 @@ export async function generate(options: LimitedUserConfig, version: string) { outputPath: openApiOutputPath, version, serviceEndName: "Service", // we are hard coding this because changing the service end name was depreciated in @hey-api/openapi-ts + pageParam: formattedOptions.pageParam, + nextPageParam: formattedOptions.nextPageParam, }); await print(source, formattedOptions); const queriesOutputPath = buildQueriesOutputPath(options.output); diff --git a/tests/__snapshots__/createSource.test.ts.snap b/tests/__snapshots__/createSource.test.ts.snap index 165b3b4..d6f3087 100644 --- a/tests/__snapshots__/createSource.test.ts.snap +++ b/tests/__snapshots__/createSource.test.ts.snap @@ -13,7 +13,7 @@ exports[`createSource > createSource 2`] = ` import { type QueryClient, useQuery, useSuspenseQuery, useMutation, UseQueryResult, UseQueryOptions, UseMutationOptions, UseMutationResult } from "@tanstack/react-query"; import { DefaultService } from "../requests/services.gen"; -import { Pet, NewPet, Error, FindPetsData, FindPetsResponse, AddPetData, AddPetResponse, GetNotDefinedResponse, PostNotDefinedResponse, FindPetByIdData, FindPetByIdResponse, DeletePetData, DeletePetResponse, $OpenApiTs } from "../requests/types.gen"; +import { Pet, NewPet, Error, FindPetsData, FindPetsResponse, AddPetData, AddPetResponse, GetNotDefinedResponse, PostNotDefinedResponse, FindPetByIdData, FindPetByIdResponse, DeletePetData, DeletePetResponse, FindPaginatedPetsData, FindPaginatedPetsResponse, $OpenApiTs } from "../requests/types.gen"; export type DefaultServiceFindPetsDefaultResponse = Awaited>; export type DefaultServiceFindPetsQueryResult = UseQueryResult; export const useDefaultServiceFindPetsKey = "DefaultServiceFindPets"; @@ -31,6 +31,14 @@ export const useDefaultServiceFindPetByIdKey = "DefaultServiceFindPetById"; export const UseDefaultServiceFindPetByIdKeyFn = ({ id }: { id: number; }, queryKey?: Array) => [useDefaultServiceFindPetByIdKey, ...(queryKey ?? [{ id }])]; +export type DefaultServiceFindPaginatedPetsDefaultResponse = Awaited>; +export type DefaultServiceFindPaginatedPetsQueryResult = UseQueryResult; +export const useDefaultServiceFindPaginatedPetsKey = "DefaultServiceFindPaginatedPets"; +export const UseDefaultServiceFindPaginatedPetsKeyFn = ({ limit, page, tags }: { + limit?: number; + page?: number; + tags?: string[]; +} = {}, queryKey?: Array) => [useDefaultServiceFindPaginatedPetsKey, ...(queryKey ?? [{ limit, page, tags }])]; export type DefaultServiceAddPetMutationResult = Awaited>; export type DefaultServicePostNotDefinedMutationResult = Awaited>; export type DefaultServiceDeletePetMutationResult = Awaited>; @@ -43,7 +51,7 @@ exports[`createSource > createSource 3`] = ` import * as Common from "./common"; import { type QueryClient, useQuery, useSuspenseQuery, useMutation, UseQueryResult, UseQueryOptions, UseMutationOptions, UseMutationResult } from "@tanstack/react-query"; import { DefaultService } from "../requests/services.gen"; -import { Pet, NewPet, Error, FindPetsData, FindPetsResponse, AddPetData, AddPetResponse, GetNotDefinedResponse, PostNotDefinedResponse, FindPetByIdData, FindPetByIdResponse, DeletePetData, DeletePetResponse, $OpenApiTs } from "../requests/types.gen"; +import { Pet, NewPet, Error, FindPetsData, FindPetsResponse, AddPetData, AddPetResponse, GetNotDefinedResponse, PostNotDefinedResponse, FindPetByIdData, FindPetByIdResponse, DeletePetData, DeletePetResponse, FindPaginatedPetsData, FindPaginatedPetsResponse, $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. @@ -80,6 +88,21 @@ export const useDefaultServiceFindPetById = , "queryKey" | "queryFn">) => useQuery({ queryKey: Common.UseDefaultServiceFindPetByIdKeyFn({ id }, queryKey), queryFn: () => DefaultService.findPetById({ id }) as TData, ...options }); /** +* Returns paginated pets from the system that the user has access to +* +* @param data The data for the request. +* @param data.page page number +* @param data.tags tags to filter by +* @param data.limit maximum number of results to return +* @returns unknown pet response +* @throws ApiError +*/ +export const useDefaultServiceFindPaginatedPets = = unknown[]>({ limit, page, tags }: { + limit?: number; + page?: number; + tags?: string[]; +} = {}, queryKey?: TQueryKey, options?: Omit, "queryKey" | "queryFn">) => useQuery({ queryKey: Common.UseDefaultServiceFindPaginatedPetsKeyFn({ limit, page, tags }, queryKey), queryFn: () => DefaultService.findPaginatedPets({ limit, page, tags }) as TData, ...options }); +/** * Creates a new pet in the store. Duplicates are allowed * @param data The data for the request. * @param data.requestBody Pet to add to the store @@ -121,7 +144,7 @@ exports[`createSource > createSource 4`] = ` import * as Common from "./common"; import { type QueryClient, useQuery, useSuspenseQuery, useMutation, UseQueryResult, UseQueryOptions, UseMutationOptions, UseMutationResult } from "@tanstack/react-query"; import { DefaultService } from "../requests/services.gen"; -import { Pet, NewPet, Error, FindPetsData, FindPetsResponse, AddPetData, AddPetResponse, GetNotDefinedResponse, PostNotDefinedResponse, FindPetByIdData, FindPetByIdResponse, DeletePetData, DeletePetResponse, $OpenApiTs } from "../requests/types.gen"; +import { Pet, NewPet, Error, FindPetsData, FindPetsResponse, AddPetData, AddPetResponse, GetNotDefinedResponse, PostNotDefinedResponse, FindPetByIdData, FindPetByIdResponse, DeletePetData, DeletePetResponse, FindPaginatedPetsData, FindPaginatedPetsResponse, $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. @@ -157,6 +180,21 @@ export const useDefaultServiceGetNotDefinedSuspense = = unknown[]>({ id }: { id: number; }, queryKey?: TQueryKey, options?: Omit, "queryKey" | "queryFn">) => useSuspenseQuery({ queryKey: Common.UseDefaultServiceFindPetByIdKeyFn({ id }, queryKey), queryFn: () => DefaultService.findPetById({ id }) as TData, ...options }); +/** +* Returns paginated pets from the system that the user has access to +* +* @param data The data for the request. +* @param data.page page number +* @param data.tags tags to filter by +* @param data.limit maximum number of results to return +* @returns unknown pet response +* @throws ApiError +*/ +export const useDefaultServiceFindPaginatedPetsSuspense = = unknown[]>({ limit, page, tags }: { + limit?: number; + page?: number; + tags?: string[]; +} = {}, queryKey?: TQueryKey, options?: Omit, "queryKey" | "queryFn">) => useSuspenseQuery({ queryKey: Common.UseDefaultServiceFindPaginatedPetsKeyFn({ limit, page, tags }, queryKey), queryFn: () => DefaultService.findPaginatedPets({ limit, page, tags }) as TData, ...options }); " `; @@ -166,7 +204,7 @@ exports[`createSource > createSource 5`] = ` import * as Common from "./common"; import { type QueryClient, useQuery, useSuspenseQuery, useMutation, UseQueryResult, UseQueryOptions, UseMutationOptions, UseMutationResult } from "@tanstack/react-query"; import { DefaultService } from "../requests/services.gen"; -import { Pet, NewPet, Error, FindPetsData, FindPetsResponse, AddPetData, AddPetResponse, GetNotDefinedResponse, PostNotDefinedResponse, FindPetByIdData, FindPetByIdResponse, DeletePetData, DeletePetResponse, $OpenApiTs } from "../requests/types.gen"; +import { Pet, NewPet, Error, FindPetsData, FindPetsResponse, AddPetData, AddPetResponse, GetNotDefinedResponse, PostNotDefinedResponse, FindPetByIdData, FindPetByIdResponse, DeletePetData, DeletePetResponse, FindPaginatedPetsData, FindPaginatedPetsResponse, $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. @@ -202,5 +240,20 @@ export const prefetchUseDefaultServiceGetNotDefined = (queryClient: QueryClient) export const prefetchUseDefaultServiceFindPetById = (queryClient: QueryClient, { id }: { id: number; }) => queryClient.prefetchQuery({ queryKey: Common.UseDefaultServiceFindPetByIdKeyFn({ id }), queryFn: () => DefaultService.findPetById({ id }) }); +/** +* Returns paginated pets from the system that the user has access to +* +* @param data The data for the request. +* @param data.page page number +* @param data.tags tags to filter by +* @param data.limit maximum number of results to return +* @returns unknown pet response +* @throws ApiError +*/ +export const prefetchUseDefaultServiceFindPaginatedPets = (queryClient: QueryClient, { limit, page, tags }: { + limit?: number; + page?: number; + tags?: string[]; +} = {}) => queryClient.prefetchQuery({ queryKey: Common.UseDefaultServiceFindPaginatedPetsKeyFn({ limit, page, tags }), queryFn: () => DefaultService.findPaginatedPets({ limit, page, tags }) }); " `; diff --git a/tests/__snapshots__/generate.test.ts.snap b/tests/__snapshots__/generate.test.ts.snap index 53ace37..6e3cabb 100644 --- a/tests/__snapshots__/generate.test.ts.snap +++ b/tests/__snapshots__/generate.test.ts.snap @@ -22,6 +22,14 @@ export const useDefaultServiceFindPetByIdKey = "DefaultServiceFindPetById"; export const UseDefaultServiceFindPetByIdKeyFn = ({ id }: { id: number; }, queryKey?: Array) => [useDefaultServiceFindPetByIdKey, ...(queryKey ?? [{ id }])]; +export type DefaultServiceFindPaginatedPetsDefaultResponse = Awaited>; +export type DefaultServiceFindPaginatedPetsQueryResult = UseQueryResult; +export const useDefaultServiceFindPaginatedPetsKey = "DefaultServiceFindPaginatedPets"; +export const UseDefaultServiceFindPaginatedPetsKeyFn = ({ limit, page, tags }: { + limit?: number; + page?: number; + tags?: string[]; +} = {}, queryKey?: Array) => [useDefaultServiceFindPaginatedPetsKey, ...(queryKey ?? [{ limit, page, tags }])]; export type DefaultServiceAddPetMutationResult = Awaited>; export type DefaultServicePostNotDefinedMutationResult = Awaited>; export type DefaultServiceDeletePetMutationResult = Awaited>; @@ -36,6 +44,28 @@ export * from "./queries"; " `; +exports[`generate > infiniteQueries.ts 1`] = ` +"// generated with @7nohe/openapi-react-query-codegen@1.0.0 + +import { DefaultService } from "../requests/services.gen"; +import * as Common from "./common"; +/** +* Returns paginated pets from the system that the user has access to +* +* @param data The data for the request. +* @param data.page page number +* @param data.tags tags to filter by +* @param data.limit maximum number of results to return +* @returns unknown pet response +* @throws ApiError +*/ +export const useDefaultServiceFindPaginatedPetsInfinite = , TError = unknown, TQueryKey extends Array = unknown[]>({ limit, tags }: { + limit?: number; + tags?: string[]; +} = {}, queryKey?: TQueryKey, options?: Omit, "queryKey" | "queryFn">) => useInfiniteQuery({ queryKey: Common.UseDefaultServiceFindPaginatedPetsKeyFn({ limit, tags }, queryKey), queryFn: ({ pageParam }) => DefaultService.findPaginatedPets({ limit, page: pageParam as number, tags }) as TData, initialPageParam: 1, getNextPageParam: response => (response as { nextPage: number }).nextPage, ...options }); +" +`; + exports[`generate > prefetch.ts 1`] = ` "// generated with @7nohe/openapi-react-query-codegen@1.0.0 @@ -77,6 +107,21 @@ export const prefetchUseDefaultServiceGetNotDefined = (queryClient: QueryClient) export const prefetchUseDefaultServiceFindPetById = (queryClient: QueryClient, { id }: { id: number; }) => queryClient.prefetchQuery({ queryKey: Common.UseDefaultServiceFindPetByIdKeyFn({ id }), queryFn: () => DefaultService.findPetById({ id }) }); +/** +* Returns paginated pets from the system that the user has access to +* +* @param data The data for the request. +* @param data.page page number +* @param data.tags tags to filter by +* @param data.limit maximum number of results to return +* @returns unknown pet response +* @throws ApiError +*/ +export const prefetchUseDefaultServiceFindPaginatedPets = (queryClient: QueryClient, { limit, page, tags }: { + limit?: number; + page?: number; + tags?: string[]; +} = {}) => queryClient.prefetchQuery({ queryKey: Common.UseDefaultServiceFindPaginatedPetsKeyFn({ limit, page, tags }), queryFn: () => DefaultService.findPaginatedPets({ limit, page, tags }) }); " `; @@ -123,6 +168,21 @@ export const useDefaultServiceFindPetById = , "queryKey" | "queryFn">) => useQuery({ queryKey: Common.UseDefaultServiceFindPetByIdKeyFn({ id }, queryKey), queryFn: () => DefaultService.findPetById({ id }) as TData, ...options }); /** +* Returns paginated pets from the system that the user has access to +* +* @param data The data for the request. +* @param data.page page number +* @param data.tags tags to filter by +* @param data.limit maximum number of results to return +* @returns unknown pet response +* @throws ApiError +*/ +export const useDefaultServiceFindPaginatedPets = = unknown[]>({ limit, page, tags }: { + limit?: number; + page?: number; + tags?: string[]; +} = {}, queryKey?: TQueryKey, options?: Omit, "queryKey" | "queryFn">) => useQuery({ queryKey: Common.UseDefaultServiceFindPaginatedPetsKeyFn({ limit, page, tags }, queryKey), queryFn: () => DefaultService.findPaginatedPets({ limit, page, tags }) as TData, ...options }); +/** * Creates a new pet in the store. Duplicates are allowed * @param data The data for the request. * @param data.requestBody Pet to add to the store @@ -199,5 +259,20 @@ export const useDefaultServiceGetNotDefinedSuspense = = unknown[]>({ id }: { id: number; }, queryKey?: TQueryKey, options?: Omit, "queryKey" | "queryFn">) => useSuspenseQuery({ queryKey: Common.UseDefaultServiceFindPetByIdKeyFn({ id }, queryKey), queryFn: () => DefaultService.findPetById({ id }) as TData, ...options }); +/** +* Returns paginated pets from the system that the user has access to +* +* @param data The data for the request. +* @param data.page page number +* @param data.tags tags to filter by +* @param data.limit maximum number of results to return +* @returns unknown pet response +* @throws ApiError +*/ +export const useDefaultServiceFindPaginatedPetsSuspense = = unknown[]>({ limit, page, tags }: { + limit?: number; + page?: number; + tags?: string[]; +} = {}, queryKey?: TQueryKey, options?: Omit, "queryKey" | "queryFn">) => useSuspenseQuery({ queryKey: Common.UseDefaultServiceFindPaginatedPetsKeyFn({ limit, page, tags }, queryKey), queryFn: () => DefaultService.findPaginatedPets({ limit, page, tags }) as TData, ...options }); " `; diff --git a/tests/createExports.test.ts b/tests/createExports.test.ts index cc1826e..f8accd3 100644 --- a/tests/createExports.test.ts +++ b/tests/createExports.test.ts @@ -17,7 +17,7 @@ describe(fileName, () => { }); project.addSourceFilesAtPaths(path.join(outputPath(fileName), "**", "*")); const service = await getServices(project); - const exports = createExports(service); + const exports = createExports(service, "page", "nextPage"); const commonTypes = exports.allCommon .filter((c) => c.kind === SyntaxKind.TypeAliasDeclaration) @@ -30,6 +30,8 @@ describe(fileName, () => { "DefaultServiceGetNotDefinedQueryResult", "DefaultServiceFindPetByIdDefaultResponse", "DefaultServiceFindPetByIdQueryResult", + "DefaultServiceFindPaginatedPetsDefaultResponse", + "DefaultServiceFindPaginatedPetsQueryResult", "DefaultServiceAddPetMutationResult", "DefaultServicePostNotDefinedMutationResult", "DefaultServiceDeletePetMutationResult", @@ -46,6 +48,8 @@ describe(fileName, () => { "UseDefaultServiceGetNotDefinedKeyFn", "useDefaultServiceFindPetByIdKey", "UseDefaultServiceFindPetByIdKeyFn", + "useDefaultServiceFindPaginatedPetsKey", + "UseDefaultServiceFindPaginatedPetsKeyFn", ]); const mainExports = exports.mainExports.map( @@ -56,6 +60,7 @@ describe(fileName, () => { "useDefaultServiceFindPets", "useDefaultServiceGetNotDefined", "useDefaultServiceFindPetById", + "useDefaultServiceFindPaginatedPets", "useDefaultServiceAddPet", "useDefaultServicePostNotDefined", "useDefaultServiceDeletePet", @@ -69,6 +74,7 @@ describe(fileName, () => { "useDefaultServiceFindPetsSuspense", "useDefaultServiceGetNotDefinedSuspense", "useDefaultServiceFindPetByIdSuspense", + "useDefaultServiceFindPaginatedPetsSuspense", ]); }); }); diff --git a/tests/createSource.test.ts b/tests/createSource.test.ts index abd08c9..f3f114e 100644 --- a/tests/createSource.test.ts +++ b/tests/createSource.test.ts @@ -11,6 +11,8 @@ describe(fileName, () => { outputPath: outputPath(fileName), version: "1.0.0", serviceEndName: "Service", + pageParam: "page", + nextPageParam: "nextPage", }); const indexTs = source.find((s) => s.name === "index.ts"); diff --git a/tests/generate.test.ts b/tests/generate.test.ts index 0f6ccc8..0ed4e6c 100644 --- a/tests/generate.test.ts +++ b/tests/generate.test.ts @@ -18,6 +18,8 @@ describe("generate", () => { input: path.join(__dirname, "inputs", "petstore.yaml"), output: path.join("tests", "outputs"), lint: "eslint", + pageParam: "page", + nextPageParam: "nextPage", }; await generate(options, "1.0.0"); }); @@ -38,6 +40,10 @@ describe("generate", () => { expect(readOutput("queries.ts")).toMatchSnapshot(); }); + test("infiniteQueries.ts", () => { + expect(readOutput("infiniteQueries.ts")).toMatchSnapshot(); + }); + test("index.ts", () => { expect(readOutput("index.ts")).toMatchSnapshot(); }); diff --git a/tests/inputs/petstore.yaml b/tests/inputs/petstore.yaml index cfe7026..2c067d8 100644 --- a/tests/inputs/petstore.yaml +++ b/tests/inputs/petstore.yaml @@ -135,6 +135,51 @@ paths: application/json: schema: $ref: '#/components/schemas/Error' + /paginated-pets: + get: + description: | + Returns paginated pets from the system that the user has access to + operationId: findPaginatedPets + parameters: + - name: page + in: query + description: page number + required: false + schema: + type: integer + format: int32 + - name: tags + in: query + description: tags to filter by + required: false + style: form + schema: + type: array + items: + type: string + - name: limit + in: query + description: maximum number of results to return + required: false + schema: + type: integer + format: int32 + responses: + '200': + description: pet response + content: + application/json: + schema: + type: object + properties: + pets: + type: array + items: + $ref: '#/components/schemas/Pet' + nextPage: + type: integer + format: int32 + minimum: 1 components: schemas: Pet: diff --git a/tests/service.test.ts b/tests/service.test.ts index 91a9501..d2f64b2 100644 --- a/tests/service.test.ts +++ b/tests/service.test.ts @@ -26,6 +26,7 @@ describe(fileName, () => { "postNotDefined", "findPetById", "deletePet", + "findPaginatedPets", ]); });