Skip to content

Commit

Permalink
feat: Generate mutation keys (#160)
Browse files Browse the repository at this point in the history
  • Loading branch information
7nohe authored Oct 13, 2024
1 parent 7120b20 commit bc01fad
Show file tree
Hide file tree
Showing 13 changed files with 339 additions and 227 deletions.
1 change: 1 addition & 0 deletions examples/react-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"dependencies": {
"@hey-api/client-axios": "^0.2.7",
"@tanstack/react-query": "^5.32.1",
"@tanstack/react-query-devtools": "^5.32.1",
"axios": "^1.7.7",
"form-data": "~4.0.0",
"react": "^18.3.1",
Expand Down
2 changes: 2 additions & 0 deletions examples/react-app/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@ import ReactDOM from "react-dom/client";
import App from "./App";
import "./index.css";
import { QueryClientProvider } from "@tanstack/react-query";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
import { queryClient } from "./queryClient";
import "./axios";

ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
<React.StrictMode>
<QueryClientProvider client={queryClient}>
<App />
<ReactQueryDevtools buttonPosition="bottom-left" />
</QueryClientProvider>
</React.StrictMode>,
);
1 change: 0 additions & 1 deletion examples/tanstack-router-app/src/routes/__root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import {
createRootRouteWithContext,
} from "@tanstack/react-router";
import { TanStackRouterDevtools } from "@tanstack/router-devtools";
import * as React from "react";

export const Route = createRootRouteWithContext<{
queryClient: QueryClient;
Expand Down
5 changes: 1 addition & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,21 +53,18 @@
},
"devDependencies": {
"@biomejs/biome": "^1.7.2",
"@types/node": "^22.7.4",
"@types/cross-spawn": "^6.0.6",
"@types/node": "^22.7.4",
"@vitest/coverage-v8": "^1.5.0",
"commander": "^12.0.0",
"glob": "^10.3.10",
"lefthook": "^1.6.10",
"rimraf": "^5.0.5",
"ts-morph": "^23.0.0",
"ts-node": "^10.9.2",
"typescript": "^5.5.4",
"vitest": "^1.5.0"
},
"peerDependencies": {
"commander": "12.x",
"glob": "10.x",
"ts-morph": "23.x",
"typescript": "5.x"
},
Expand Down
61 changes: 26 additions & 35 deletions pnpm-lock.yaml

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

165 changes: 165 additions & 0 deletions src/common.mts
Original file line number Diff line number Diff line change
Expand Up @@ -200,3 +200,168 @@ export function buildRequestsOutputPath(outputPath: string) {
export function buildQueriesOutputPath(outputPath: string) {
return path.join(outputPath, queriesOutputPath);
}

export function getQueryKeyFnName(queryKey: string) {
return `${capitalizeFirstLetter(queryKey)}Fn`;
}

/**
* Create QueryKey/MutationKey exports
*/
export function createQueryKeyExport({
methodName,
queryKey,
}: {
methodName: string;
queryKey: string;
}) {
return ts.factory.createVariableStatement(
[ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)],
ts.factory.createVariableDeclarationList(
[
ts.factory.createVariableDeclaration(
ts.factory.createIdentifier(queryKey),
undefined,
undefined,
ts.factory.createStringLiteral(
`${capitalizeFirstLetter(methodName)}`,
),
),
],
ts.NodeFlags.Const,
),
);
}

export function createQueryKeyFnExport(
queryKey: string,
method: VariableDeclaration,
type: "query" | "mutation" = "query",
) {
// Mutation keys don't require clientOptions
const params = type === "query" ? getRequestParamFromMethod(method) : null;

// override key is used to allow the user to override the the queryKey values
const overrideKey = ts.factory.createParameterDeclaration(
undefined,
undefined,
ts.factory.createIdentifier(type === "query" ? "queryKey" : "mutationKey"),
QuestionToken,
ts.factory.createTypeReferenceNode("Array<unknown>", []),
);

return ts.factory.createVariableStatement(
[ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)],
ts.factory.createVariableDeclarationList(
[
ts.factory.createVariableDeclaration(
ts.factory.createIdentifier(getQueryKeyFnName(queryKey)),
undefined,
undefined,
ts.factory.createArrowFunction(
undefined,
undefined,
params ? [params, overrideKey] : [overrideKey],
undefined,
EqualsOrGreaterThanToken,
type === "query"
? queryKeyFn(queryKey, method)
: mutationKeyFn(queryKey),
),
),
],
ts.NodeFlags.Const,
),
);
}

function queryKeyFn(
queryKey: string,
method: VariableDeclaration,
): ts.Expression {
return ts.factory.createArrayLiteralExpression(
[
ts.factory.createIdentifier(queryKey),
ts.factory.createSpreadElement(
ts.factory.createParenthesizedExpression(
ts.factory.createBinaryExpression(
ts.factory.createIdentifier("queryKey"),
ts.factory.createToken(ts.SyntaxKind.QuestionQuestionToken),
getVariableArrowFunctionParameters(method)
? // [...clientOptions]
ts.factory.createArrayLiteralExpression([
ts.factory.createIdentifier("clientOptions"),
])
: // []
ts.factory.createArrayLiteralExpression(),
),
),
),
],
false,
);
}

function mutationKeyFn(mutationKey: string): ts.Expression {
return ts.factory.createArrayLiteralExpression(
[
ts.factory.createIdentifier(mutationKey),
ts.factory.createSpreadElement(
ts.factory.createParenthesizedExpression(
ts.factory.createBinaryExpression(
ts.factory.createIdentifier("mutationKey"),
ts.factory.createToken(ts.SyntaxKind.QuestionQuestionToken),
ts.factory.createArrayLiteralExpression(),
),
),
),
],
false,
);
}

export function getRequestParamFromMethod(
method: VariableDeclaration,
pageParam?: string,
modelNames: string[] = [],
) {
if (!getVariableArrowFunctionParameters(method).length) {
return null;
}
const methodName = getNameFromVariable(method);

const params = getVariableArrowFunctionParameters(method).flatMap((param) => {
const paramNodes = extractPropertiesFromObjectParam(param);

return paramNodes
.filter((p) => p.name !== pageParam)
.map((refParam) => ({
name: refParam.name,
// TODO: Client<Request, Response, unknown, RequestOptions> -> Client<Request, Response, unknown>
typeName: getShortType(refParam.type?.getText() ?? ""),
optional: refParam.optional,
}));
});

const areAllPropertiesOptional = params.every((param) => param.optional);

return ts.factory.createParameterDeclaration(
undefined,
undefined,
ts.factory.createIdentifier("clientOptions"),
undefined,
ts.factory.createTypeReferenceNode(ts.factory.createIdentifier("Options"), [
ts.factory.createTypeReferenceNode(
modelNames.includes(`${capitalizeFirstLetter(methodName)}Data`)
? `${capitalizeFirstLetter(methodName)}Data`
: "unknown",
),
ts.factory.createTypeReferenceNode(ts.factory.createIdentifier("true")),
]),
// if all params are optional, we create an empty object literal
// so the hook can be called without any parameters
areAllPropertiesOptional
? ts.factory.createObjectLiteralExpression()
: undefined,
);
}
Loading

0 comments on commit bc01fad

Please sign in to comment.