Skip to content

Commit bc01fad

Browse files
authored
feat: Generate mutation keys (#160)
1 parent 7120b20 commit bc01fad

File tree

13 files changed

+339
-227
lines changed

13 files changed

+339
-227
lines changed

examples/react-app/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
"dependencies": {
1616
"@hey-api/client-axios": "^0.2.7",
1717
"@tanstack/react-query": "^5.32.1",
18+
"@tanstack/react-query-devtools": "^5.32.1",
1819
"axios": "^1.7.7",
1920
"form-data": "~4.0.0",
2021
"react": "^18.3.1",

examples/react-app/src/main.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,15 @@ import ReactDOM from "react-dom/client";
33
import App from "./App";
44
import "./index.css";
55
import { QueryClientProvider } from "@tanstack/react-query";
6+
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
67
import { queryClient } from "./queryClient";
78
import "./axios";
89

910
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
1011
<React.StrictMode>
1112
<QueryClientProvider client={queryClient}>
1213
<App />
14+
<ReactQueryDevtools buttonPosition="bottom-left" />
1315
</QueryClientProvider>
1416
</React.StrictMode>,
1517
);

examples/tanstack-router-app/src/routes/__root.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import {
66
createRootRouteWithContext,
77
} from "@tanstack/react-router";
88
import { TanStackRouterDevtools } from "@tanstack/router-devtools";
9-
import * as React from "react";
109

1110
export const Route = createRootRouteWithContext<{
1211
queryClient: QueryClient;

package.json

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,21 +53,18 @@
5353
},
5454
"devDependencies": {
5555
"@biomejs/biome": "^1.7.2",
56-
"@types/node": "^22.7.4",
5756
"@types/cross-spawn": "^6.0.6",
57+
"@types/node": "^22.7.4",
5858
"@vitest/coverage-v8": "^1.5.0",
5959
"commander": "^12.0.0",
60-
"glob": "^10.3.10",
6160
"lefthook": "^1.6.10",
6261
"rimraf": "^5.0.5",
6362
"ts-morph": "^23.0.0",
64-
"ts-node": "^10.9.2",
6563
"typescript": "^5.5.4",
6664
"vitest": "^1.5.0"
6765
},
6866
"peerDependencies": {
6967
"commander": "12.x",
70-
"glob": "10.x",
7168
"ts-morph": "23.x",
7269
"typescript": "5.x"
7370
},

pnpm-lock.yaml

Lines changed: 26 additions & 35 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/common.mts

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,3 +200,168 @@ export function buildRequestsOutputPath(outputPath: string) {
200200
export function buildQueriesOutputPath(outputPath: string) {
201201
return path.join(outputPath, queriesOutputPath);
202202
}
203+
204+
export function getQueryKeyFnName(queryKey: string) {
205+
return `${capitalizeFirstLetter(queryKey)}Fn`;
206+
}
207+
208+
/**
209+
* Create QueryKey/MutationKey exports
210+
*/
211+
export function createQueryKeyExport({
212+
methodName,
213+
queryKey,
214+
}: {
215+
methodName: string;
216+
queryKey: string;
217+
}) {
218+
return ts.factory.createVariableStatement(
219+
[ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)],
220+
ts.factory.createVariableDeclarationList(
221+
[
222+
ts.factory.createVariableDeclaration(
223+
ts.factory.createIdentifier(queryKey),
224+
undefined,
225+
undefined,
226+
ts.factory.createStringLiteral(
227+
`${capitalizeFirstLetter(methodName)}`,
228+
),
229+
),
230+
],
231+
ts.NodeFlags.Const,
232+
),
233+
);
234+
}
235+
236+
export function createQueryKeyFnExport(
237+
queryKey: string,
238+
method: VariableDeclaration,
239+
type: "query" | "mutation" = "query",
240+
) {
241+
// Mutation keys don't require clientOptions
242+
const params = type === "query" ? getRequestParamFromMethod(method) : null;
243+
244+
// override key is used to allow the user to override the the queryKey values
245+
const overrideKey = ts.factory.createParameterDeclaration(
246+
undefined,
247+
undefined,
248+
ts.factory.createIdentifier(type === "query" ? "queryKey" : "mutationKey"),
249+
QuestionToken,
250+
ts.factory.createTypeReferenceNode("Array<unknown>", []),
251+
);
252+
253+
return ts.factory.createVariableStatement(
254+
[ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)],
255+
ts.factory.createVariableDeclarationList(
256+
[
257+
ts.factory.createVariableDeclaration(
258+
ts.factory.createIdentifier(getQueryKeyFnName(queryKey)),
259+
undefined,
260+
undefined,
261+
ts.factory.createArrowFunction(
262+
undefined,
263+
undefined,
264+
params ? [params, overrideKey] : [overrideKey],
265+
undefined,
266+
EqualsOrGreaterThanToken,
267+
type === "query"
268+
? queryKeyFn(queryKey, method)
269+
: mutationKeyFn(queryKey),
270+
),
271+
),
272+
],
273+
ts.NodeFlags.Const,
274+
),
275+
);
276+
}
277+
278+
function queryKeyFn(
279+
queryKey: string,
280+
method: VariableDeclaration,
281+
): ts.Expression {
282+
return ts.factory.createArrayLiteralExpression(
283+
[
284+
ts.factory.createIdentifier(queryKey),
285+
ts.factory.createSpreadElement(
286+
ts.factory.createParenthesizedExpression(
287+
ts.factory.createBinaryExpression(
288+
ts.factory.createIdentifier("queryKey"),
289+
ts.factory.createToken(ts.SyntaxKind.QuestionQuestionToken),
290+
getVariableArrowFunctionParameters(method)
291+
? // [...clientOptions]
292+
ts.factory.createArrayLiteralExpression([
293+
ts.factory.createIdentifier("clientOptions"),
294+
])
295+
: // []
296+
ts.factory.createArrayLiteralExpression(),
297+
),
298+
),
299+
),
300+
],
301+
false,
302+
);
303+
}
304+
305+
function mutationKeyFn(mutationKey: string): ts.Expression {
306+
return ts.factory.createArrayLiteralExpression(
307+
[
308+
ts.factory.createIdentifier(mutationKey),
309+
ts.factory.createSpreadElement(
310+
ts.factory.createParenthesizedExpression(
311+
ts.factory.createBinaryExpression(
312+
ts.factory.createIdentifier("mutationKey"),
313+
ts.factory.createToken(ts.SyntaxKind.QuestionQuestionToken),
314+
ts.factory.createArrayLiteralExpression(),
315+
),
316+
),
317+
),
318+
],
319+
false,
320+
);
321+
}
322+
323+
export function getRequestParamFromMethod(
324+
method: VariableDeclaration,
325+
pageParam?: string,
326+
modelNames: string[] = [],
327+
) {
328+
if (!getVariableArrowFunctionParameters(method).length) {
329+
return null;
330+
}
331+
const methodName = getNameFromVariable(method);
332+
333+
const params = getVariableArrowFunctionParameters(method).flatMap((param) => {
334+
const paramNodes = extractPropertiesFromObjectParam(param);
335+
336+
return paramNodes
337+
.filter((p) => p.name !== pageParam)
338+
.map((refParam) => ({
339+
name: refParam.name,
340+
// TODO: Client<Request, Response, unknown, RequestOptions> -> Client<Request, Response, unknown>
341+
typeName: getShortType(refParam.type?.getText() ?? ""),
342+
optional: refParam.optional,
343+
}));
344+
});
345+
346+
const areAllPropertiesOptional = params.every((param) => param.optional);
347+
348+
return ts.factory.createParameterDeclaration(
349+
undefined,
350+
undefined,
351+
ts.factory.createIdentifier("clientOptions"),
352+
undefined,
353+
ts.factory.createTypeReferenceNode(ts.factory.createIdentifier("Options"), [
354+
ts.factory.createTypeReferenceNode(
355+
modelNames.includes(`${capitalizeFirstLetter(methodName)}Data`)
356+
? `${capitalizeFirstLetter(methodName)}Data`
357+
: "unknown",
358+
),
359+
ts.factory.createTypeReferenceNode(ts.factory.createIdentifier("true")),
360+
]),
361+
// if all params are optional, we create an empty object literal
362+
// so the hook can be called without any parameters
363+
areAllPropertiesOptional
364+
? ts.factory.createObjectLiteralExpression()
365+
: undefined,
366+
);
367+
}

0 commit comments

Comments
 (0)