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
6 changes: 3 additions & 3 deletions docs/src/content/docs/guides/cli-options.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -77,9 +77,9 @@ The available options are:

### --useDateType

> **Deprecated:** This option is currently accepted but has no effect. It will be removed in a future version.

Use Date type instead of string for date. The default value is `false`.
Use `Date` instead of `string` for `date` and `date-time` model properties.
Generated SDK transformers also convert matching response values into `Date`
objects at runtime. The default value is `false`.

### --debug

Expand Down
34 changes: 34 additions & 0 deletions docs/src/content/docs/guides/usage.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -103,8 +103,42 @@ function App() {
export default App;
```

Generated query functions forward TanStack Query's `AbortSignal` to the HTTP
client. Requests are therefore cancelled when a query becomes stale or when
you call `queryClient.cancelQueries`.

Mutations can be cancelled explicitly by passing a signal in the generated
client options:

```tsx
const controller = new AbortController();
const { mutate } = useAddPet();

mutate({
body: { name: "Fluffy" },
signal: controller.signal,
});

controller.abort();
```

Invalidating queries after a mutation is important to ensure the cache is updated with the new data. This is done by calling the `queryClient.invalidateQueries` function with the query key used by the query hook.

Mutation results contain the complete SDK response, not only the response
body. This makes response headers available in `onSuccess` or from
`mutateAsync`:

```tsx
const { mutateAsync } = useDownloadReport();
const result = await mutateAsync({ body: { reportId } });

// @hey-api/client-fetch
const disposition = result.response.headers.get("content-disposition");

// @hey-api/client-axios
const axiosDisposition = result.headers["content-disposition"];
```

Learn more about invalidating queries [here](https://tanstack.com/query/latest/docs/framework/react/guides/query-invalidation).

To ensure the query key is created the same way as the query hook, you can use the query key function exported by the generated query hooks.
Expand Down
29 changes: 27 additions & 2 deletions examples/react-app/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import "./App.css";
import { useState } from "react";
import { useEffect, useRef, useState } from "react";

import {
UseFindPetsKeyFn,
Expand All @@ -26,7 +26,15 @@ function App() {
const { data: notDefined } = useGetNotDefined<undefined>();
const { mutate: mutateNotDefined } = usePostNotDefined<undefined>();

const { mutate: addPet, isError } = useAddPet();
const { mutate: addPet, isError, isPending: isAddingPet } = useAddPet();
const addPetAbortController = useRef<AbortController | null>(null);

useEffect(
() => () => {
addPetAbortController.current?.abort();
},
[],
);

const [text, setText] = useState<string>("");
const [errorText, setErrorText] = useState<string>();
Expand All @@ -53,9 +61,14 @@ function App() {
<button
type="button"
onClick={() => {
addPetAbortController.current?.abort();
const controller = new AbortController();
addPetAbortController.current = controller;

addPet(
{
body: { name: text },
signal: controller.signal,
},
{
onSuccess: () => {
Expand All @@ -68,12 +81,24 @@ function App() {
console.log(error.response);
setErrorText(`Error: ${error.response?.data.message}`);
},
onSettled: () => {
if (addPetAbortController.current === controller) {
addPetAbortController.current = null;
}
},
},
);
}}
>
Create a pet
</button>
<button
type="button"
disabled={!isAddingPet}
onClick={() => addPetAbortController.current?.abort()}
>
Cancel pet creation
</button>
{isError && (
<p
style={{
Expand Down
2 changes: 1 addition & 1 deletion src/cli.mts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ async function setupProgram() {
)
.option(
"--useDateType",
"Use Date type instead of string for date types for models, this will not convert the data to a Date object",
"Use Date for date/date-time model properties and convert response values to Date objects",
)
.option("--debug", "Run in debug mode?")
.option("--noSchemas", "Disable generating JSON schemas")
Expand Down
19 changes: 15 additions & 4 deletions src/generate.mts
Original file line number Diff line number Diff line change
Expand Up @@ -29,23 +29,34 @@ export async function generate(options: LimitedUserConfig, version: string) {
}
: "@hey-api/typescript";

const sdkPlugin: NonNullable<UserConfig["plugins"]>[number] =
formattedOptions.noOperationId
const sdkPlugin: NonNullable<UserConfig["plugins"]>[number] = {
name: "@hey-api/sdk" as const,
...(formattedOptions.noOperationId
? {
name: "@hey-api/sdk" as const,
// `operationId: false` was deprecated in favor of `operations.nesting`
operations: {
nesting: "id" as const,
},
}
: "@hey-api/sdk";
: {}),
...(formattedOptions.useDateType
? { transformer: "@hey-api/transformers" as const }
: {}),
};

const plugins: NonNullable<UserConfig["plugins"]>[number][] = [
clientPlugin,
typescriptPlugin,
sdkPlugin,
];

if (formattedOptions.useDateType) {
plugins.push({
name: "@hey-api/transformers",
dates: "date",
});
}

// Conditionally add schemas plugin
if (!formattedOptions.noSchemas) {
plugins.push(
Expand Down
62 changes: 53 additions & 9 deletions src/parseOperations.mts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,36 @@ import type {
GenerationContext,
OperationInfo,
OperationParameter,
PageParamTypeKind,
} from "./types.mjs";

type PageParamInfo = {
type: string;
typeKind: PageParamTypeKind;
};

function getPageParamTypeKind(type: ts.Type): PageParamTypeKind {
const types = type.isUnion()
? type.types.filter(
(item) => !(item.flags & (ts.TypeFlags.Null | ts.TypeFlags.Undefined)),
)
: [type];

if (
types.length > 0 &&
types.every((item) => item.flags & ts.TypeFlags.StringLike)
) {
return "string";
}
if (
types.length > 0 &&
types.every((item) => item.flags & ts.TypeFlags.NumberLike)
) {
return "number";
}
return "other";
}

/**
* Extract parameter information from a method's variable declaration.
*/
Expand Down Expand Up @@ -43,14 +71,18 @@ function extractParameters(
* Get paginatable methods by checking if their Data type has the pageParam in query property.
* Uses TypeScript compiler API for accurate AST traversal.
*/
function getPaginatableMethods(project: Project, pageParam: string): string[] {
function getPaginatableMethods(
project: Project,
pageParam: string,
): Map<string, PageParamInfo> {
const modelsFile = project
.getSourceFiles()
.find((sf) => sf.getFilePath().includes(modelsFileName));

if (!modelsFile) return [];
if (!modelsFile) return new Map();

const paginatableMethods: string[] = [];
const paginatableMethods = new Map<string, PageParamInfo>();
const typeChecker = project.getTypeChecker().compilerObject;
const modelDeclarations = modelsFile.getExportedDeclarations();
const entries = modelDeclarations.entries();

Expand All @@ -77,17 +109,27 @@ function getPaginatableMethods(project: Project, pageParam: string): string[] {
const queryType = (query as ts.PropertySignature).type;
if (!queryType || queryType.kind !== ts.SyntaxKind.TypeLiteral) continue;

const hasPageParam = (queryType as ts.TypeLiteralNode).members.some(
(m) => m.name?.getText() === pageParam,
const pageParamNode = (queryType as ts.TypeLiteralNode).members.find(
(m): m is ts.PropertySignature =>
ts.isPropertySignature(m) && m.name?.getText() === pageParam,
);

if (hasPageParam) {
if (pageParamNode) {
// Extract method name from Data type name (e.g., "FindPetsData" -> "findPets")
const methodName = key.slice(0, -4); // Remove "Data" suffix
// Convert first letter to lowercase
const methodNameLower =
methodName.charAt(0).toLowerCase() + methodName.slice(1);
paginatableMethods.push(methodNameLower);
const pageParamType = pageParamNode.type?.getText(
modelsFile.compilerNode,
);
const resolvedType = typeChecker.getTypeAtLocation(
pageParamNode.type ?? pageParamNode,
);
paginatableMethods.set(methodNameLower, {
type: pageParamType ?? "unknown",
typeKind: getPageParamTypeKind(resolvedType),
});
}
}

Expand Down Expand Up @@ -115,8 +157,8 @@ export async function parseOperations(
const sdkParams = getVariableArrowFunctionParameters(desc.method);
const allParamsOptional =
sdkParams.length === 0 || sdkParams[0].isOptional();
const isPaginatable =
httpMethod === "GET" && paginatableMethods.includes(methodName);
const pageParamInfo = paginatableMethods.get(methodName);
const isPaginatable = httpMethod === "GET" && pageParamInfo !== undefined;

return {
methodName,
Expand All @@ -127,6 +169,8 @@ export async function parseOperations(
parameters,
allParamsOptional,
isPaginatable,
pageParamType: isPaginatable ? pageParamInfo.type : undefined,
pageParamTypeKind: isPaginatable ? pageParamInfo.typeKind : undefined,
};
});
}
Expand Down
Loading
Loading