Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 7 additions & 10 deletions src/tsmorph/buildCommon.mts
Original file line number Diff line number Diff line change
Expand Up @@ -158,21 +158,18 @@ export function buildMutationKeyFn(
* Example:
* export type FindPaginatedPetsInfiniteClientOptions = Omit<Options<FindPaginatedPetsData, true>, "query"> &
* { query?: Omit<NonNullable<FindPaginatedPetsData["query"]>, "page"> };
*
* The `<Method>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<unknown, true>"
: `Omit<Options<${dataTypeName}, true>, "query"> & { query?: Omit<NonNullable<${dataTypeName}["query"]>, "${ctx.pageParam}"> }`;
const dataTypeName = `${op.capitalizedMethodName}Data`;
const type = `Omit<Options<${dataTypeName}, true>, "query"> & { query?: Omit<NonNullable<${dataTypeName}["query"]>, "${ctx.pageParam}"> }`;

return {
kind: StructureKind.TypeAlias,
Expand Down
5 changes: 4 additions & 1 deletion src/tsmorph/buildQueryHooks.mts
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<Method>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
Expand Down
30 changes: 30 additions & 0 deletions tests/parseOperations.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)}/**/*`);
Expand Down
20 changes: 11 additions & 9 deletions tests/tsmorph/buildCommon.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -219,15 +219,6 @@ describe("buildCommon", () => {
'Omit<Options<FindPaginatedPetsData, true>, "query"> & { query?: Omit<NonNullable<FindPaginatedPetsData["query"]>, "page"> }',
);
});

it("should fall back to Options<unknown, true> without a Data type", () => {
const result = buildInfiniteClientOptionsType(
mockPaginatableOperation,
mockContext,
);

expect(result.type).toBe("Options<unknown, true>");
});
});

describe("buildInfiniteQueryKeyConst", () => {
Expand All @@ -254,5 +245,16 @@ describe("buildCommon", () => {
"(clientOptions: FindPaginatedPetsInfiniteClientOptions = {}, queryKey?: Array<unknown>) => [...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<unknown>) => [...useFindPaginatedPetsInfiniteKey, ...(queryKey ?? [clientOptions])]",
);
});
});
});
22 changes: 0 additions & 22 deletions tests/tsmorph/buildQueryHooks.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: [
Expand Down Expand Up @@ -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", () => {
Expand Down
Loading