diff --git a/README.md b/README.md index 9acf1ea..f23ee85 100644 --- a/README.md +++ b/README.md @@ -42,24 +42,25 @@ Usage: openapi-rq [options] Generate React Query code based on OpenAPI 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 (choices: "angular", "axios", "fetch", "node", "xhr", default: "fetch") - --request Path to custom request file - --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 (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', 'typescript+namespace'] - --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 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") + -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 (choices: "angular", "axios", "fetch", "node", "xhr", default: "fetch") + --request Path to custom request file + --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 (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', 'typescript+namespace'] + --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 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") + --initialPageParam Initial value for the pagination parameter (default: "1") -h, --help display help for command ``` @@ -241,6 +242,8 @@ export default App; 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. +The `initialPageParam` option can be specified to set the intial page to load, defaults to 1. The `nextPageParam` supports dot notation for nested values (i.e. `meta.next`). + Example Schema: ```yml diff --git a/src/cli.mts b/src/cli.mts index c9918d7..c4ec10c 100644 --- a/src/cli.mts +++ b/src/cli.mts @@ -25,6 +25,7 @@ export type LimitedUserConfig = { schemaType?: "form" | "json"; pageParam: string; nextPageParam: string; + initialPageParam: string | number; }; async function setupProgram() { @@ -102,6 +103,11 @@ async function setupProgram() { "Name of the response parameter used for next page", "nextPage", ) + .option( + "--initialPageParam ", + "Initial page value to query", + "initialPageParam", + ) .parse(); const options = program.opts(); diff --git a/src/common.mts b/src/common.mts index 6d31415..0a65cdf 100644 --- a/src/common.mts +++ b/src/common.mts @@ -163,9 +163,12 @@ export function formatOptions(options: LimitedUserConfig) { } else if (!Number.isNaN(parsedNumber)) { (acc as unknown as Record)[typedKey] = parsedNumber; } else { - (acc as unknown as Record)[ - typedKey - ] = typedValue; + ( + acc as unknown as Record< + string, + string | number | undefined | boolean + > + )[typedKey] = typedValue; } return acc; }, diff --git a/src/createExports.mts b/src/createExports.mts index 74eb6c2..58ddc0a 100644 --- a/src/createExports.mts +++ b/src/createExports.mts @@ -8,6 +8,7 @@ export const createExports = ( service: Service, pageParam: string, nextPageParam: string, + initialPageParam: string, ) => { const { klasses } = service; const methods = klasses.flatMap((k) => k.methods); @@ -29,7 +30,7 @@ export const createExports = ( ); const allGetQueries = allGet.map((m) => - createUseQuery(m, pageParam, nextPageParam), + createUseQuery(m, pageParam, nextPageParam, initialPageParam), ); const allPrefetchQueries = allGet.map((m) => createPrefetch(m)); diff --git a/src/createSource.mts b/src/createSource.mts index 3d8d870..8a0a6b0 100644 --- a/src/createSource.mts +++ b/src/createSource.mts @@ -11,6 +11,7 @@ const createSourceFile = async ( serviceEndName: string, pageParam: string, nextPageParam: string, + initialPageParam: string, ) => { const project = new Project({ // Optionally specify compiler options, tsconfig.json, in-memory file system, and more here. @@ -30,7 +31,12 @@ const createSourceFile = async ( project, }); - const exports = createExports(service, pageParam, nextPageParam); + const exports = createExports( + service, + pageParam, + nextPageParam, + initialPageParam, + ); const commonSource = ts.factory.createSourceFile( [...imports, ...exports.allCommon], @@ -111,12 +117,14 @@ export const createSource = async ({ serviceEndName, pageParam, nextPageParam, + initialPageParam, }: { outputPath: string; version: string; serviceEndName: string; pageParam: string; nextPageParam: string; + initialPageParam: string; }) => { const queriesFile = ts.createSourceFile( `${OpenApiRqFiles.queries}.ts`, @@ -180,6 +188,7 @@ export const createSource = async ({ serviceEndName, pageParam, nextPageParam, + initialPageParam, ); const comment = `// generated with @7nohe/openapi-react-query-codegen@${version} \n\n`; diff --git a/src/createUseQuery.mts b/src/createUseQuery.mts index db16dc2..2dd225e 100644 --- a/src/createUseQuery.mts +++ b/src/createUseQuery.mts @@ -229,6 +229,7 @@ export function createQueryHook({ className, pageParam, nextPageParam, + initialPageParam, }: { queryString: "useSuspenseQuery" | "useQuery" | "useInfiniteQuery"; suffix: string; @@ -238,6 +239,7 @@ export function createQueryHook({ className: string; pageParam?: string; nextPageParam?: string; + initialPageParam?: string; }) { const methodName = getNameFromMethod(method); const customHookName = hookNameFromMethod({ method, className }); @@ -447,7 +449,11 @@ export function createQueryHook({ ), ), ), - ...createInfiniteQueryParams(pageParam, nextPageParam), + ...createInfiniteQueryParams( + pageParam, + nextPageParam, + initialPageParam, + ), ts.factory.createSpreadAssignment( ts.factory.createIdentifier("options"), ), @@ -467,6 +473,7 @@ export const createUseQuery = ( { className, method, jsDoc }: MethodDescription, pageParam: string, nextPageParam: string, + initialPageParam: string, ) => { const methodName = getNameFromMethod(method); const queryKey = createQueryKeyFromMethod({ method, className }); @@ -517,6 +524,7 @@ export const createUseQuery = ( className, pageParam, nextPageParam, + initialPageParam, }) : undefined; @@ -625,14 +633,18 @@ function queryKeyFn( ); } -function createInfiniteQueryParams(pageParam?: string, nextPageParam?: string) { +function createInfiniteQueryParams( + pageParam?: string, + nextPageParam?: string, + initialPageParam = "1", +) { if (pageParam === undefined || nextPageParam === undefined) { return []; } return [ ts.factory.createPropertyAssignment( ts.factory.createIdentifier("initialPageParam"), - ts.factory.createNumericLiteral(1), + ts.factory.createStringLiteral(initialPageParam), ), ts.factory.createPropertyAssignment( ts.factory.createIdentifier("getNextPageParam"), @@ -655,9 +667,18 @@ function createInfiniteQueryParams(pageParam?: string, nextPageParam?: string) { ts.factory.createParenthesizedExpression( ts.factory.createAsExpression( ts.factory.createIdentifier("response"), - ts.factory.createTypeReferenceNode( - ts.factory.createIdentifier(`{ ${nextPageParam}: number }`), - ), + nextPageParam.split(".").reduceRight((acc, segment) => { + return ts.factory.createTypeLiteralNode([ + ts.factory.createPropertySignature( + undefined, + ts.factory.createIdentifier(segment), + undefined, + acc, + ), + ]); + }, ts.factory.createKeywordTypeNode( + ts.SyntaxKind.NumberKeyword, + ) as ts.TypeNode), ), ), ts.factory.createIdentifier(nextPageParam), diff --git a/src/generate.mts b/src/generate.mts index 691d119..6026d4f 100644 --- a/src/generate.mts +++ b/src/generate.mts @@ -49,6 +49,7 @@ export async function generate(options: LimitedUserConfig, version: string) { 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, + initialPageParam: formattedOptions.initialPageParam.toString(), }); await print(source, formattedOptions); const queriesOutputPath = buildQueriesOutputPath(options.output); diff --git a/tests/__snapshots__/generate.test.ts.snap b/tests/__snapshots__/generate.test.ts.snap index a5a0fa9..7b9140c 100644 --- a/tests/__snapshots__/generate.test.ts.snap +++ b/tests/__snapshots__/generate.test.ts.snap @@ -62,7 +62,13 @@ import * as Common from "./common"; 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 }); +} = {}, 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: "initial", getNextPageParam: response => (response as { + meta: { + next: number; + }; + }).meta.next, ...options +}); " `; diff --git a/tests/createExports.test.ts b/tests/createExports.test.ts index f8accd3..2784f49 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, "page", "nextPage"); + const exports = createExports(service, "page", "nextPage", "initial"); const commonTypes = exports.allCommon .filter((c) => c.kind === SyntaxKind.TypeAliasDeclaration) diff --git a/tests/generate.test.ts b/tests/generate.test.ts index 26359de..056c741 100644 --- a/tests/generate.test.ts +++ b/tests/generate.test.ts @@ -20,7 +20,8 @@ describe("generate", () => { output: path.join("tests", "outputs"), lint: "eslint", pageParam: "page", - nextPageParam: "nextPage", + nextPageParam: "meta.next", + initialPageParam: "initial", }; await generate(options, "1.0.0"); });