Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(type-safe-api): add option to generate hooks compatible with react query v5 #889

Merged
merged 2 commits into from
Nov 28, 2024
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
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,19 @@ By configuring `handlers.languages` in your `TypeSafeApiProject` and annotating
}
```

=== "TYPESPEC"

Use the `@handler` decorator, and specify the language you wish to implement this operation in.

```hl_lines="3"
@get
@route("/hello")
@handler({ language: "typescript" })
op SayHello(@query name: string): {
message: string;
};
```

=== "OPENAPI"

Use the `x-handler` vendor extension, specifying the language you wish to implement this operation in.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,21 @@ You can generate `useInfiniteQuery` hooks instead of `useQuery` hooks for pagina
}
```

=== "TYPESPEC"

In TypeSpec, use the `@extension` decorator to add the `x-paginated` vendor extension.

```
@get
@route("/pets")
@extension("x-paginated", { inputToken: "nextToken", outputToken: "nextToken" })
op ListPets(@query nextToken?: string): {
pets: Pet[];
nextToken?: string;
};`
```


=== "OPENAPI"

In OpenAPI, use the `x-paginaged` vendor extension in your operation, making sure both `inputToken` and `outputToken` are specified:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,18 @@ By configuring `handlers.languages` in your `TypeSafeWebSocketApiProject` and an
}
```

=== "TYPESPEC"

Use the `@handler` decorator, and specify the language you wish to implement this operation in.

```tsp hl_lines="1-2"
@async({ direction: "client_to_server" })
@handler({ language: "typescript" })
op SayHello(
name: string,
): void;
```

=== "OPENAPI"

Use the `x-handler` vendor extension, specifying the language you wish to implement this operation in.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
###/TSAPI_WRITE_FILE###/* tslint:disable */
/* eslint-disable */
<%_ services.forEach((service) => { _%>
export * from './<%- service.className %>';
export * from './<%- service.className %>Hooks';
export * from './<%- service.className %>ClientProvider';
export * from './<%- service.className %><%_ if (metadata.esm) { _%>.js<%_ } _%>';
export * from './<%- service.className %>Hooks<%_ if (metadata.esm) { _%>.js<%_ } _%>';
export * from './<%- service.className %>ClientProvider<%_ if (metadata.esm) { _%>.js<%_ } _%>';
<%_ }); _%>
Original file line number Diff line number Diff line change
Expand Up @@ -11,25 +11,29 @@ import {
QueryClient,
QueryClientProvider,
} from "@tanstack/react-query";
import { <%- service.className %> } from "./<%- service.className %>";
import { <%- service.className %>ClientContext } from "./<%- service.className %>Hooks";
import { <%- service.className %> } from "./<%- service.className %><%_ if (metadata.esm) { _%>.js<%_ } _%>";
import { <%- service.className %>ClientContext } from "./<%- service.className %>Hooks<%_ if (metadata.esm) { _%>.js<%_ } _%>";

const queryClient = new QueryClient();

<%_ if (!metadata.queryV5) { _%>
/**
* Default QueryClient context for <%- service.className %>
*/
export const <%- service.className %>DefaultContext = React.createContext<QueryClient | undefined>(
undefined
);

<%_ } _%>
/**
* Properties for the <%- service.className %>ClientProvider
*/
export interface <%- service.className %>ClientProviderProps {
readonly apiClient: <%- service.className %>;
readonly client?: QueryClient;
<%_ if (!metadata.queryV5) { _%>
readonly context?: React.Context<QueryClient | undefined>;
<%_ } _%>
readonly children?: React.ReactNode;
}

Expand All @@ -40,11 +44,13 @@ export interface <%- service.className %>ClientProviderProps {
export const <%- service.className %>ClientProvider = ({
apiClient,
client = queryClient,
<%_ if (!metadata.queryV5) { _%>
context = <%- service.className %>DefaultContext,
<%_ } _%>
children,
}: <%- service.className %>ClientProviderProps): JSX.Element => {
return (
<QueryClientProvider client={client} context={context}>
<QueryClientProvider client={client}<% if (!metadata.queryV5) { %> context={context}<% } %>>
<<%- service.className %>ClientContext.Provider value={apiClient}>
{children}
</<%- service.className %>ClientContext.Provider>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import type {
<%_ service.modelImports.forEach((modelImport) => { _%>
<%- modelImport %>,
<%_ }); _%>
} from '../models';
} from '../models<%_ if (metadata.esm) { _%>/index.js<%_ } _%>';
<%_ } _%>
// Import request parameter interfaces
import {
Expand All @@ -21,13 +21,18 @@ import {
<%- operation.operationIdPascalCase %>Request,
<%_ } _%>
<%_ }); _%>
} from '..';
} from '..<%_ if (metadata.esm) { _%>/index.js<%_ } _%>';

import { ResponseError } from '../runtime';
import { <%- service.className %> } from './<%- service.className %>';
import { <%- service.className %>DefaultContext } from "./<%- service.className %>ClientProvider";
import { ResponseError } from '../runtime<%_ if (metadata.esm) { _%>.js<%_ } _%>';
import { <%- service.className %> } from './<%- service.className %><%_ if (metadata.esm) { _%>.js<%_ } _%>';
<%_ if (!metadata.queryV5) { _%>
import { <%- service.className %>DefaultContext } from "./<%- service.className %>ClientProvider<%_ if (metadata.esm) { _%>.js<%_ } _%>";
<%_ } _%>

import {
<%_ if (metadata.queryV5) { _%>
InitialPageParam,
<%_ } _%>
useQuery,
UseQueryResult,
UseQueryOptions,
Expand Down Expand Up @@ -61,17 +66,28 @@ export const use<%- operation.operationIdPascalCase %> = <TError = ResponseError
<%_ if (operation.parameters.length > 0) { _%>
params: <%- operation.operationIdPascalCase %>Request,
<%_ } _%>
options?: Omit<UseInfiniteQueryOptions<<%- resultType %>, TError>, 'queryKey' | 'queryFn' | 'getNextPageParam'>
options?: Omit<UseInfiniteQueryOptions<<%- resultType %>, TError>, 'queryKey' | 'queryFn' | 'getNextPageParam'<% if (metadata.queryV5) { %> | 'initialPageParam'<% } %>><% if (metadata.queryV5) { %> & (
InitialPageParam<<%- operation.operationIdPascalCase %>Request['<%- paginationInputParam.typescriptName %>']>
)<% } %>
): UseInfiniteQueryResult<<%- resultType %>, TError> => {
const api = useContext(<%- service.className %>ClientContext);
if (!api) {
throw NO_API_ERROR;
}
<%_ if (metadata.queryV5) { _%>
return useInfiniteQuery({
queryKey: ["<%- operation.name %>"<% if (operation.parameters.length > 0) { %>, params<% } %>],
queryFn: ({ pageParam }) => api.<%- operation.name %>({ <% if (operation.parameters.length > 0) { %>...params, <% } %><%- paginationInputParam.typescriptName %>: pageParam as any }),
getNextPageParam: (response) => response.<%- pagination.outputToken %>,
...options,
});
<%_ } else { _%>
return useInfiniteQuery(["<%- operation.name %>"<% if (operation.parameters.length > 0) { %>, params<% } %>], ({ pageParam }) => api.<%- operation.name %>({ <% if (operation.parameters.length > 0) { %>...params, <% } %><%- paginationInputParam.typescriptName %>: pageParam }), {
getNextPageParam: (response) => response.<%- pagination.outputToken %>,
context: <%- service.className %>DefaultContext,
...options as any,
});
<%_ } _%>
};
<%_ } else { _%>
/**
Expand All @@ -87,10 +103,18 @@ export const use<%- operation.operationIdPascalCase %> = <TError = ResponseError
if (!api) {
throw NO_API_ERROR;
}
<%_ if (metadata.queryV5) { _%>
return useQuery({
queryKey: ["<%- operation.name %>"<% if (operation.parameters.length > 0) { %>, params<% } %>],
queryFn: () => api.<%- operation.name %>(<% if (operation.parameters.length > 0) { %>params<% } %>),
...options,
});
<%_ } else { _%>
return useQuery(["<%- operation.name %>"<% if (operation.parameters.length > 0) { %>, params<% } %>], () => api.<%- operation.name %>(<% if (operation.parameters.length > 0) { %>params<% } %>), {
context: <%- service.className %>DefaultContext,
...options,
});
<%_ } _%>
};
<%_ } _%>
<%_ } else { _%>
Expand All @@ -104,10 +128,17 @@ export const use<%- operation.operationIdPascalCase %> = <TError = ResponseError
if (!api) {
throw NO_API_ERROR;
}
<%_ if (metadata.queryV5) { _%>
return useMutation({
mutationFn: (<% if (operation.parameters.length > 0) { %>params: <%- operation.operationIdPascalCase %>Request<% } %>) => api.<%- operation.name %>(<% if (operation.parameters.length > 0) { %>params<% } %>),
...options,
});
<%_ } else { _%>
return useMutation((<% if (operation.parameters.length > 0) { %>params: <%- operation.operationIdPascalCase %>Request<% } %>) => api.<%- operation.name %>(<% if (operation.parameters.length > 0) { %>params<% } %>), {
context: <%- service.className %>DefaultContext,
...options,
});
<%_ } _%>
};
<%_ } _%>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@
}
###/TSAPI_WRITE_FILE###/* tslint:disable */
/* eslint-disable */
export * from './runtime';
export * from './apis';
export * from './models';
export * from './runtime<%_ if (metadata.esm) { _%>.js<%_ } _%>';
export * from './apis<%_ if (metadata.esm) { _%>/index.js<%_ } _%>';
export * from './models<%_ if (metadata.esm) { _%>/index.js<%_ } _%>';
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
} from "<%- metadata.websocketClientPackageName %>";
import {
<%- serviceClassName %>WebSocketClientContext,
} from "./provider";
} from "./provider<%_ if (metadata.esm) { _%>.js<%_ } _%>";
import { useContext, useEffect, useCallback, DependencyList } from "react";

const NO_CLIENT_ERROR = new Error(`<%- serviceClassName %>WebSocketClient is missing. Please ensure you have instantiated the <%- serviceClassName %>WebSocketClientProvider with a client instance.`);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,5 @@
}
###/TSAPI_WRITE_FILE###/* tslint:disable */
/* eslint-disable */
export * from './hooks/hooks';
export * from './hooks/provider';
export * from './hooks/hooks<%_ if (metadata.esm) { _%>.js<%_ } _%>';
export * from './hooks/provider<%_ if (metadata.esm) { _%>.js<%_ } _%>';
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,20 @@ import { CodegenOptions } from "../components/utils";
* Configuration for the generated typescript client project
*/
export interface GeneratedTypescriptReactQueryHooksProjectOptions
extends GeneratedTypescriptLibraryProjectOptions {}
extends GeneratedTypescriptLibraryProjectOptions {
/**
* Set to true to use @tanstack/react-query version 5.x
* @default false - @tanstack/react-query version 4.x is used
*/
readonly useReactQueryV5?: boolean;
}

/**
* Typescript project containing generated react-query hooks
*/
export class TypescriptReactQueryHooksLibrary extends GeneratedTypescriptLibraryProject {
private readonly useReactQueryV5?: boolean;

constructor(options: GeneratedTypescriptReactQueryHooksProjectOptions) {
super({
...options,
Expand All @@ -27,9 +35,14 @@ export class TypescriptReactQueryHooksLibrary extends GeneratedTypescriptLibrary
},
},
});
this.useReactQueryV5 = options.useReactQueryV5;

// Add dependencies on react-query and react
this.addDeps("@tanstack/react-query@^4"); // Pin at 4 for now - requires generated code updates to upgrade to 5
if (this.useReactQueryV5) {
this.addDeps("@tanstack/react-query@^5");
} else {
this.addDeps("@tanstack/react-query@^4");
}
this.addDevDeps("react", "@types/react");
this.addPeerDeps("react");
}
Expand All @@ -44,6 +57,7 @@ export class TypescriptReactQueryHooksLibrary extends GeneratedTypescriptLibrary
],
metadata: {
srcDir: this.srcdir,
queryV5: !!this.useReactQueryV5,
},
};
}
Expand Down
8 changes: 7 additions & 1 deletion packages/type-safe-api/src/project/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,13 @@ export interface GeneratedJavaHandlersOptions
*/
export interface GeneratedTypeScriptReactQueryHooksOptions
extends TypeScriptProjectOptions,
GeneratedProjectOptions {}
GeneratedProjectOptions {
/**
* Set to true to use @tanstack/react-query version 5.x
* @default false - @tanstack/react-query version 4.x is used
*/
readonly useReactQueryV5?: boolean;
}

/**
* Options for configuring a generated typescript websocket client library project
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading