Skip to content

Commit

Permalink
feat(type-safe-api): add option to generate hooks compatible with rea…
Browse files Browse the repository at this point in the history
…ct query v5 (#889)

* docs(type-safe-api): add missing typespec examples

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

The current hooks depend on react query v4. This adds an option to generate v5 hooks.

Additionally, honour the `esm` option in the generated hook libraries.
  • Loading branch information
cogwirrel authored Nov 28, 2024
1 parent ad383ad commit c8cfeec
Show file tree
Hide file tree
Showing 16 changed files with 2,049 additions and 22 deletions.
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

0 comments on commit c8cfeec

Please sign in to comment.