Skip to content

Commit

Permalink
feat(prefetch): add prefetch query hook generation
Browse files Browse the repository at this point in the history
closes: #91
  • Loading branch information
seriouslag committed Apr 25, 2024
1 parent 125c5b1 commit e6c8645
Show file tree
Hide file tree
Showing 11 changed files with 333 additions and 21 deletions.
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,13 +71,16 @@ $ openapi-rq -i ./petstore.yaml
```
- openapi
- queries
- index.ts <- main file that exports common types, variables, and hooks
- index.ts <- main file that exports common types, variables, and queries. Does not export suspense or prefetch hooks
- common.ts <- common types
- queries.ts <- generated query hooks
- suspenses.ts <- generated suspense hooks
- prefetch.ts <- generated prefetch hooks learn more about prefetching in in link below
- requests <- output code generated by @hey-api/openapi-ts
```

- [Prefetching docs](https://tanstack.com/query/latest/docs/framework/react/guides/advanced-ssr#prefetching-and-dehydrating-data)

### In your app

```tsx
Expand Down
21 changes: 14 additions & 7 deletions examples/react-app/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,18 @@ import App from "./App";
import "./index.css";
import { QueryClientProvider } from "@tanstack/react-query";
import { queryClient } from "./queryClient";
import { prefetchUseDefaultServiceFindPets } from "../openapi/queries/prefetch";

ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
<React.StrictMode>
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>
</React.StrictMode>
);
async function PrefetchData() {
await prefetchUseDefaultServiceFindPets(queryClient);
}

PrefetchData().then(() => {
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
<React.StrictMode>
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>
</React.StrictMode>
);
});
8 changes: 8 additions & 0 deletions src/constants.mts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,11 @@ export const requestsOutputPath = "requests";

export const serviceFileName = "services.gen";
export const modalsFileName = "types.gen";

export const OpenApiRqFiles = {
queries: "queries",
common: "common",
suspense: "suspense",
index: "index",
prefetch: "prefetch",
} as const;
12 changes: 12 additions & 0 deletions src/createExports.mts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { createUseQuery } from "./createUseQuery.mjs";
import { createUseMutation } from "./createUseMutation.mjs";
import { Service } from "./service.mjs";
import { createPrefetch } from "./createPrefetch.mjs";

export const createExports = (service: Service) => {
const { klasses } = service;
Expand All @@ -23,6 +24,7 @@ export const createExports = (service: Service) => {
);

const allGetQueries = allGet.map((m) => createUseQuery(m));
const allPrefetchQueries = allGet.map((m) => createPrefetch(m));

const allPostMutations = allPost.map((m) => createUseMutation(m));
const allPutMutations = allPut.map((m) => createUseMutation(m));
Expand Down Expand Up @@ -59,6 +61,12 @@ export const createExports = (service: Service) => {

const suspenseExports = [...suspenseQueries];

const allPrefetches = allPrefetchQueries
.map(({ prefetchHook }) => [prefetchHook])
.flat();

const allPrefetchExports = [...allPrefetches];

return {
/**
* Common types and variables between queries (regular and suspense) and mutations
Expand All @@ -72,5 +80,9 @@ export const createExports = (service: Service) => {
* Suspense exports are the hooks that are used in the suspense components
*/
suspenseExports,
/**
* Prefetch exports are the hooks that are used in the prefetch components
*/
allPrefetchExports,
};
};
159 changes: 159 additions & 0 deletions src/createPrefetch.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import ts from "typescript";
import { MethodDeclaration } from "ts-morph";
import {
BuildCommonTypeName,
extractPropertiesFromObjectParam,
getNameFromMethod,
} from "./common.mjs";
import { type MethodDescription } from "./common.mjs";
import {
createQueryKeyFromMethod,
getRequestParamFromMethod,
hookNameFromMethod,
} from "./createUseQuery.mjs";
import { addJSDocToNode } from "./util.mjs";

/**
* Creates a prefetch function for a query
*/
function createPrefetchHook({
requestParams,
method,
className,
}: {
requestParams: ts.ParameterDeclaration[];
method: MethodDeclaration;
className: string;
}) {
const methodName = getNameFromMethod(method);
const queryName = hookNameFromMethod({ method, className });
const customHookName = `prefetch${queryName.charAt(0).toUpperCase() + queryName.slice(1)}`;
const queryKey = createQueryKeyFromMethod({ method, className });

// const
const hookExport = ts.factory.createVariableStatement(
// export
[ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)],
ts.factory.createVariableDeclarationList(
[
ts.factory.createVariableDeclaration(
ts.factory.createIdentifier(customHookName),
undefined,
undefined,
ts.factory.createArrowFunction(
undefined,
undefined,
[
ts.factory.createParameterDeclaration(
undefined,
undefined,
"queryClient",
undefined,
ts.factory.createTypeReferenceNode(
ts.factory.createIdentifier("QueryClient")
)
),
...requestParams,
],
undefined,
ts.factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken),
ts.factory.createCallExpression(
ts.factory.createIdentifier("queryClient.prefetchQuery"),
undefined,
[
ts.factory.createObjectLiteralExpression([
ts.factory.createPropertyAssignment(
ts.factory.createIdentifier("queryKey"),
ts.factory.createArrayLiteralExpression(
[
BuildCommonTypeName(queryKey),
method.getParameters().length
? ts.factory.createArrayLiteralExpression([
ts.factory.createObjectLiteralExpression(
method
.getParameters()
.map((param) =>
extractPropertiesFromObjectParam(param).map(
(p) =>
ts.factory.createShorthandPropertyAssignment(
ts.factory.createIdentifier(p.name)
)
)
)
.flat()
),
])
: ts.factory.createArrayLiteralExpression([]),
],
false
)
),
ts.factory.createPropertyAssignment(
ts.factory.createIdentifier("queryFn"),
ts.factory.createArrowFunction(
undefined,
undefined,
[],
undefined,
ts.factory.createToken(
ts.SyntaxKind.EqualsGreaterThanToken
),
ts.factory.createCallExpression(
ts.factory.createPropertyAccessExpression(
ts.factory.createIdentifier(className),
ts.factory.createIdentifier(methodName)
),
undefined,
method.getParameters().length
? [
ts.factory.createObjectLiteralExpression(
method
.getParameters()
.map((param) =>
extractPropertiesFromObjectParam(param).map(
(p) =>
ts.factory.createShorthandPropertyAssignment(
ts.factory.createIdentifier(p.name)
)
)
)
.flat()
),
]
: undefined
)
)
),
]),
]
)
)
),
],
ts.NodeFlags.Const
)
);
return hookExport;
}

export const createPrefetch = ({
className,
method,
jsDoc,
}: MethodDescription) => {
const requestParam = getRequestParamFromMethod(method);

const requestParams = requestParam ? [requestParam] : [];

const prefetchHook = createPrefetchHook({
requestParams,
method,
className,
});

const hookWithJsDoc = addJSDocToNode(prefetchHook, jsDoc);

return {
prefetchHook: hookWithJsDoc,
};
};
45 changes: 37 additions & 8 deletions src/createSource.mts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import ts from "typescript";
import { Project } from "ts-morph";
import { join } from "path";
import { OpenApiRqFiles } from "./constants.mjs";
import { createImports } from "./createImports.mjs";
import { createExports } from "./createExports.mjs";
import { getServices } from "./service.mjs";
import { Project } from "ts-morph";
import { join } from "path";

const createSourceFile = async (outputPath: string, serviceEndName: string) => {
const project = new Project({
Expand Down Expand Up @@ -77,11 +78,18 @@ const createSourceFile = async (outputPath: string, serviceEndName: string) => {
ts.NodeFlags.None
);

const prefetchSource = ts.factory.createSourceFile(
[commonImport, ...imports, ...exports.allPrefetchExports],
ts.factory.createToken(ts.SyntaxKind.EndOfFileToken),
ts.NodeFlags.None
);

return {
commonSource,
mainSource,
suspenseSource,
indexSource,
prefetchSource,
};
};

Expand All @@ -95,29 +103,37 @@ export const createSource = async ({
serviceEndName: string;
}) => {
const queriesFile = ts.createSourceFile(
"queries.ts",
`${OpenApiRqFiles.queries}.ts`,
"",
ts.ScriptTarget.Latest,
false,
ts.ScriptKind.TS
);
const commonFile = ts.createSourceFile(
"common.ts",
`${OpenApiRqFiles.common}.ts`,
"",
ts.ScriptTarget.Latest,
false,
ts.ScriptKind.TS
);
const suspenseFile = ts.createSourceFile(
"suspense.ts",
`${OpenApiRqFiles.suspense}.ts`,
"",
ts.ScriptTarget.Latest,
false,
ts.ScriptKind.TS
);

const indexFile = ts.createSourceFile(
"index.ts",
`${OpenApiRqFiles.index}.ts`,
"",
ts.ScriptTarget.Latest,
false,
ts.ScriptKind.TS
);

const prefetchFile = ts.createSourceFile(
`${OpenApiRqFiles.prefetch}.ts`,
"",
ts.ScriptTarget.Latest,
false,
Expand All @@ -129,8 +145,13 @@ export const createSource = async ({
removeComments: false,
});

const { commonSource, mainSource, suspenseSource, indexSource } =
await createSourceFile(outputPath, serviceEndName);
const {
commonSource,
mainSource,
suspenseSource,
indexSource,
prefetchSource,
} = await createSourceFile(outputPath, serviceEndName);

const comment = `// generated with @7nohe/openapi-react-query-codegen@${version} \n\n`;

Expand All @@ -150,6 +171,10 @@ export const createSource = async ({
comment +
printer.printNode(ts.EmitHint.Unspecified, indexSource, indexFile);

const prefetchResult =
comment +
printer.printNode(ts.EmitHint.Unspecified, prefetchSource, prefetchFile);

return [
{
name: "index.ts",
Expand All @@ -167,5 +192,9 @@ export const createSource = async ({
name: "suspense.ts",
content: suspenseResult,
},
{
name: "prefetch.ts",
content: prefetchResult,
},
];
};
6 changes: 3 additions & 3 deletions src/createUseQuery.mts
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ export function createQueryKeyExport({
);
}

function hookNameFromMethod({
export function hookNameFromMethod({
method,
className,
}: {
Expand All @@ -211,7 +211,7 @@ function hookNameFromMethod({
return `use${className}${capitalizeFirstLetter(methodName)}`;
}

function createQueryKeyFromMethod({
export function createQueryKeyFromMethod({
method,
className,
}: {
Expand All @@ -228,7 +228,7 @@ function createQueryKeyFromMethod({
* @param queryString The type of query to use from react-query
* @param suffix The suffix to append to the hook name
*/
function createQueryHook({
export function createQueryHook({
queryString,
suffix,
responseDataType,
Expand Down

0 comments on commit e6c8645

Please sign in to comment.