diff --git a/src/tsmorph/buildCommon.mts b/src/tsmorph/buildCommon.mts index e81aa52..4b9877a 100644 --- a/src/tsmorph/buildCommon.mts +++ b/src/tsmorph/buildCommon.mts @@ -158,21 +158,18 @@ export function buildMutationKeyFn( * Example: * export type FindPaginatedPetsInfiniteClientOptions = Omit, "query"> & * { query?: Omit, "page"> }; + * + * The `Data` type is always in scope here: `parseOperations` only marks + * an operation paginatable when it found the page parameter inside that very + * type, and it discovers it through the same exported declarations that become + * `ctx.modelNames`. So there is no missing-Data case to fall back on. */ export function buildInfiniteClientOptionsType( op: OperationInfo, ctx: GenerationContext, ): TypeAliasDeclarationStructure { - const dataTypeName = ctx.modelNames.includes( - `${op.capitalizedMethodName}Data`, - ) - ? `${op.capitalizedMethodName}Data` - : "unknown"; - - const type = - dataTypeName === "unknown" - ? "Options" - : `Omit, "query"> & { query?: Omit, "${ctx.pageParam}"> }`; + const dataTypeName = `${op.capitalizedMethodName}Data`; + const type = `Omit, "query"> & { query?: Omit, "${ctx.pageParam}"> }`; return { kind: StructureKind.TypeAlias, diff --git a/src/tsmorph/buildQueryHooks.mts b/src/tsmorph/buildQueryHooks.mts index b35fa51..24e766e 100644 --- a/src/tsmorph/buildQueryHooks.mts +++ b/src/tsmorph/buildQueryHooks.mts @@ -94,12 +94,15 @@ export function getPageType(op: OperationInfo): string { * TQueryFnData the infinite options are instantiated with (#203). Spelled as * a cast rather than `!` because generated code is linted downstream, and a * non-null assertion trips biome's noNonNullAssertion. + * + * Only ever called for paginatable operations, so the `Data` type is + * guaranteed to exist — see buildInfiniteClientOptionsType for why. */ export function buildPagedQueryFn( op: OperationInfo, ctx: GenerationContext, ): string { - const dataTypeName = getDataTypeName(op, ctx); + const dataTypeName = `${op.capitalizedMethodName}Data`; const thenClause = `.then(response => response.data as ${getPageType(op)})`; const pageParamType = getPageParamType(op); // When the initial page param is omitted, the first request must send no diff --git a/tests/parseOperations.test.ts b/tests/parseOperations.test.ts index 2a704aa..1eee9f7 100644 --- a/tests/parseOperations.test.ts +++ b/tests/parseOperations.test.ts @@ -70,6 +70,36 @@ describe("parseOperations", () => { expect(findPaginatedPets?.pageParamTypeKind).toBe("number"); }); + // The infinite-query builders spell the Data type as `${capitalizedMethodName}Data` + // with no fallback, because `getPaginatableMethods` only marks an operation + // paginatable after finding the page parameter inside that very type, and it reads + // the same exported declarations that become `modelNames`. That coupling is what + // makes the fallback unnecessary, so it is pinned here rather than left implicit: + // resolving Data types by any other route must keep this invariant or the + // generated code will reference a type that does not exist. + it("should expose a Data type in modelNames for every paginatable operation", async () => { + const project = new Project({ skipAddingFilesFromTsConfig: true }); + project.addSourceFilesAtPaths(`${outputPath(fileName)}/**/*`); + + const operations = await parseOperations(project, "page"); + const ctx = buildGenerationContext( + project, + "@hey-api/client-fetch", + "page", + "nextPage", + "1", + false, + "1.0.0", + ); + + const paginatable = operations.filter((op) => op.isPaginatable); + expect(paginatable.length).toBeGreaterThan(0); + + for (const op of paginatable) { + expect(ctx.modelNames).toContain(`${op.capitalizedMethodName}Data`); + } + }); + it("should extract parameters correctly", async () => { const project = new Project({ skipAddingFilesFromTsConfig: true }); project.addSourceFilesAtPaths(`${outputPath(fileName)}/**/*`); diff --git a/tests/tsmorph/buildCommon.test.ts b/tests/tsmorph/buildCommon.test.ts index 3a5579f..4508631 100644 --- a/tests/tsmorph/buildCommon.test.ts +++ b/tests/tsmorph/buildCommon.test.ts @@ -219,15 +219,6 @@ describe("buildCommon", () => { 'Omit, "query"> & { query?: Omit, "page"> }', ); }); - - it("should fall back to Options without a Data type", () => { - const result = buildInfiniteClientOptionsType( - mockPaginatableOperation, - mockContext, - ); - - expect(result.type).toBe("Options"); - }); }); describe("buildInfiniteQueryKeyConst", () => { @@ -254,5 +245,16 @@ describe("buildCommon", () => { "(clientOptions: FindPaginatedPetsInfiniteClientOptions = {}, queryKey?: Array) => [...useFindPaginatedPetsInfiniteKey, ...(queryKey ?? [clientOptions])]", ); }); + + it("should omit the default value when the operation has required params", () => { + const result = buildInfiniteQueryKeyFn({ + ...mockPaginatableOperation, + allParamsOptional: false, + }); + + expect(result.declarations[0].initializer).toBe( + "(clientOptions: FindPaginatedPetsInfiniteClientOptions, queryKey?: Array) => [...useFindPaginatedPetsInfiniteKey, ...(queryKey ?? [clientOptions])]", + ); + }); }); }); diff --git a/tests/tsmorph/buildQueryHooks.test.ts b/tests/tsmorph/buildQueryHooks.test.ts index 8304816..a8afcb8 100644 --- a/tests/tsmorph/buildQueryHooks.test.ts +++ b/tests/tsmorph/buildQueryHooks.test.ts @@ -62,16 +62,6 @@ const mockNoParamsOperation: OperationInfo = { isPaginatable: false, }; -const mockPaginatableNoDataOperation: OperationInfo = { - methodName: "listThings", - capitalizedMethodName: "ListThings", - httpMethod: "GET", - isDeprecated: false, - parameters: [], - allParamsOptional: true, - isPaginatable: true, -}; - const mockFetchContext: GenerationContext = { client: "@hey-api/client-fetch", modelNames: [ @@ -320,18 +310,6 @@ describe("buildQueryHooks", () => { "(response as { nextPage: Cursor }).nextPage", ); }); - - it("should use unknown data type when not present in modelNames", () => { - const result = buildUseInfiniteQueryHook( - mockPaginatableNoDataOperation, - mockUnknownDataContext, - ); - const initializer = result?.declarations[0].initializer as string; - - expect(initializer).toContain( - "clientOptions: Common.ListThingsInfiniteClientOptions = {}", - ); - }); }); describe("buildPrefetchFn", () => {