Skip to content

Commit a24d31b

Browse files
authored
fix!: Add camelize to queryKeyFn path parameters (#203)
* chore: Extract camelizedPathParams * fix: Wrap urls with camelizedPathParams
1 parent 138a808 commit a24d31b

File tree

7 files changed

+245
-14
lines changed

7 files changed

+245
-14
lines changed
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { camel } from "case";
2+
3+
/**
4+
* Transform url params case to camel.
5+
*
6+
* @example
7+
* `pet/{pet_id}` -> `pet/{petId}`
8+
*/
9+
export const camelizedPathParams = (url: string) =>
10+
url.replace(/\{[\w\d\-_.]*\}/g, (match) => `{${camel(match)}}`);

plugins/typescript/src/core/createOperationFetcherFnNodes.ts

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import { camel } from "case";
21
import { OperationObject } from "openapi3-ts";
32
import ts, { factory as f } from "typescript";
3+
import { camelizedPathParams } from "./camelizedPathParams";
44

55
/**
66
* Create the declaration of the fetcher function.
@@ -133,12 +133,3 @@ export const createOperationFetcherFnNodes = ({
133133
);
134134
return nodes;
135135
};
136-
137-
/**
138-
* Transform url params case to camel.
139-
*
140-
* @example
141-
* `pet/{pet_id}` -> `pet/{petId}`
142-
*/
143-
const camelizedPathParams = (url: string) =>
144-
url.replace(/\{[\w\d\-_.]*\}/g, (match) => `{${camel(match)}}`);

plugins/typescript/src/core/createOperationQueryFnNodes.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { camelCase } from "lodash";
22
import { OperationObject } from "openapi3-ts";
33
import ts, { factory as f } from "typescript";
4+
import { camelizedPathParams } from "./camelizedPathParams";
45

56
/**
67
* Create the declaration of the react-router queries.
@@ -156,7 +157,7 @@ export const createOperationQueryFnNodes = ({
156157
[
157158
f.createPropertyAssignment(
158159
f.createIdentifier("path"),
159-
f.createStringLiteral(url)
160+
f.createStringLiteral(camelizedPathParams(url))
160161
),
161162
f.createPropertyAssignment(
162163
f.createIdentifier("operationId"),

plugins/typescript/src/generators/generateReactQueryComponents.test.ts

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,112 @@ describe("generateReactQueryComponents", () => {
249249
`);
250250
});
251251

252+
it("should generate a useQuery wrapper (with pathParams)", async () => {
253+
const writeFile = jest.fn();
254+
const openAPIDocument: OpenAPIObject = {
255+
openapi: "3.0.0",
256+
info: {
257+
title: "petshop",
258+
version: "1.0.0",
259+
},
260+
paths: {
261+
"/pets/{pet_id}": {
262+
get: {
263+
operationId: "showPetById",
264+
description: "Info for a specific pet",
265+
parameters: [
266+
{
267+
in: "path",
268+
name: "pet_id",
269+
description: "The id of the pet to retrieve",
270+
required: true,
271+
schema: {
272+
type: "string",
273+
},
274+
},
275+
],
276+
responses: {
277+
"200": {
278+
description: "pet response",
279+
content: {
280+
"application/json": {
281+
schema: {
282+
type: "array",
283+
items: {
284+
$ref: "#/components/schemas/Pet",
285+
},
286+
},
287+
},
288+
},
289+
},
290+
},
291+
},
292+
},
293+
},
294+
};
295+
296+
await generateReactQueryComponents(
297+
{
298+
openAPIDocument,
299+
writeFile,
300+
existsFile: () => true,
301+
readFile: async () => "",
302+
},
303+
config
304+
);
305+
306+
expect(writeFile.mock.calls[0][0]).toBe("petstoreComponents.ts");
307+
expect(writeFile.mock.calls[0][1]).toMatchInlineSnapshot(`
308+
"/**
309+
* Generated by @openapi-codegen
310+
*
311+
* @version 1.0.0
312+
*/
313+
import * as reactQuery from \\"@tanstack/react-query\\";
314+
import { usePetstoreContext, PetstoreContext } from \\"./petstoreContext\\";
315+
import type * as Fetcher from \\"./petstoreFetcher\\";
316+
import { petstoreFetch } from \\"./petstoreFetcher\\";
317+
import type * as Schemas from \\"./petstoreSchemas\\";
318+
319+
export type ShowPetByIdPathParams = {
320+
/**
321+
* The id of the pet to retrieve
322+
*/
323+
petId: string;
324+
};
325+
326+
export type ShowPetByIdError = Fetcher.ErrorWrapper<undefined>;
327+
328+
export type ShowPetByIdResponse = Schemas.Pet[];
329+
330+
export type ShowPetByIdVariables = {
331+
pathParams: ShowPetByIdPathParams;
332+
} & PetstoreContext[\\"fetcherOptions\\"];
333+
334+
/**
335+
* Info for a specific pet
336+
*/
337+
export const fetchShowPetById = (variables: ShowPetByIdVariables, signal?: AbortSignal) => petstoreFetch<ShowPetByIdResponse, ShowPetByIdError, undefined, {}, {}, ShowPetByIdPathParams>({ url: \\"/pets/{petId}\\", method: \\"get\\", ...variables, signal });
338+
339+
/**
340+
* Info for a specific pet
341+
*/
342+
export const useShowPetById = <TData = ShowPetByIdResponse>(variables: ShowPetByIdVariables, options?: Omit<reactQuery.UseQueryOptions<ShowPetByIdResponse, ShowPetByIdError, TData>, \\"queryKey\\" | \\"queryFn\\" | \\"initialData\\">) => { const { fetcherOptions, queryOptions, queryKeyFn } = usePetstoreContext(options); return reactQuery.useQuery<ShowPetByIdResponse, ShowPetByIdError, TData>({
343+
queryKey: queryKeyFn({ path: \\"/pets/{petId}\\", operationId: \\"showPetById\\", variables }),
344+
queryFn: ({ signal }) => fetchShowPetById({ ...fetcherOptions, ...variables }, signal),
345+
...options,
346+
...queryOptions
347+
}); };
348+
349+
export type QueryOperation = {
350+
path: \\"/pets/{petId}\\";
351+
operationId: \\"showPetById\\";
352+
variables: ShowPetByIdVariables;
353+
};
354+
"
355+
`);
356+
});
357+
252358
it("should deal with injected headers (marked them as optional)", async () => {
253359
const writeFile = jest.fn();
254360
const openAPIDocument: OpenAPIObject = {

plugins/typescript/src/generators/generateReactQueryComponents.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { getFetcher } from "../templates/fetcher";
1717
import { getContext } from "../templates/context";
1818
import { getUtils } from "../templates/utils";
1919
import { createNamespaceImport } from "../core/createNamespaceImport";
20+
import { camelizedPathParams } from "../core/camelizedPathParams";
2021

2122
export type Config = ConfigBase & {
2223
/**
@@ -160,7 +161,9 @@ export const generateReactQueryComponents = async (
160161
undefined,
161162
f.createIdentifier("path"),
162163
undefined,
163-
f.createLiteralTypeNode(f.createStringLiteral(route))
164+
f.createLiteralTypeNode(
165+
f.createStringLiteral(camelizedPathParams(route))
166+
)
164167
),
165168
f.createPropertySignature(
166169
undefined,
@@ -561,7 +564,9 @@ const createQueryHook = ({
561564
f.createObjectLiteralExpression([
562565
f.createPropertyAssignment(
563566
"path",
564-
f.createStringLiteral(url)
567+
f.createStringLiteral(
568+
camelizedPathParams(url)
569+
)
565570
),
566571
f.createPropertyAssignment(
567572
"operationId",

plugins/typescript/src/generators/generateReactQueryFunctions.test.ts

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,121 @@ describe("generateReactQueryFunctions", () => {
267267
`);
268268
});
269269

270+
it("should generate a useQuery wrapper (with pathParams)", async () => {
271+
const writeFile = jest.fn();
272+
const openAPIDocument: OpenAPIObject = {
273+
openapi: "3.0.0",
274+
info: {
275+
title: "petshop",
276+
version: "1.0.0",
277+
},
278+
paths: {
279+
"/pets/{pet_id}": {
280+
get: {
281+
operationId: "showPetById",
282+
description: "Info for a specific pet",
283+
parameters: [
284+
{
285+
in: "path",
286+
name: "pet_id",
287+
description: "The id of the pet to retrieve",
288+
required: true,
289+
schema: {
290+
type: "string",
291+
},
292+
},
293+
],
294+
responses: {
295+
"200": {
296+
description: "pet response",
297+
content: {
298+
"application/json": {
299+
schema: {
300+
type: "array",
301+
items: {
302+
$ref: "#/components/schemas/Pet",
303+
},
304+
},
305+
},
306+
},
307+
},
308+
},
309+
},
310+
},
311+
},
312+
};
313+
314+
await generateReactQueryFunctions(
315+
{
316+
openAPIDocument,
317+
writeFile,
318+
existsFile: () => true,
319+
readFile: async () => "",
320+
},
321+
config
322+
);
323+
324+
expect(writeFile.mock.calls[0][0]).toBe("petstoreFunctions.ts");
325+
expect(writeFile.mock.calls[0][1]).toMatchInlineSnapshot(`
326+
"/**
327+
* Generated by @openapi-codegen
328+
*
329+
* @version 1.0.0
330+
*/
331+
import * as reactQuery from \\"@tanstack/react-query\\";
332+
import { PetstoreContext, queryKeyFn } from \\"./petstoreContext\\";
333+
import type * as Fetcher from \\"./petstoreFetcher\\";
334+
import { petstoreFetch } from \\"./petstoreFetcher\\";
335+
import type * as Schemas from \\"./petstoreSchemas\\";
336+
337+
export type ShowPetByIdPathParams = {
338+
/**
339+
* The id of the pet to retrieve
340+
*/
341+
petId: string;
342+
};
343+
344+
export type ShowPetByIdError = Fetcher.ErrorWrapper<undefined>;
345+
346+
export type ShowPetByIdResponse = Schemas.Pet[];
347+
348+
export type ShowPetByIdVariables = {
349+
pathParams: ShowPetByIdPathParams;
350+
} & PetstoreContext[\\"fetcherOptions\\"];
351+
352+
/**
353+
* Info for a specific pet
354+
*/
355+
export const fetchShowPetById = (variables: ShowPetByIdVariables, signal?: AbortSignal) => petstoreFetch<ShowPetByIdResponse, ShowPetByIdError, undefined, {}, {}, ShowPetByIdPathParams>({ url: \\"/pets/{petId}\\", method: \\"get\\", ...variables, signal });
356+
357+
/**
358+
* Info for a specific pet
359+
*/
360+
export const showPetByIdQuery = (variables: ShowPetByIdVariables): [
361+
reactQuery.QueryKey,
362+
({ signal }: {
363+
signal?: AbortSignal;
364+
}) => Promise<ShowPetByIdResponse>
365+
] => [
366+
queryKeyFn({
367+
path: \\"/pets/{petId}\\",
368+
operationId: \\"showPetById\\",
369+
variables
370+
}),
371+
async ({ signal }: {
372+
signal?: AbortSignal;
373+
}) => fetchShowPetById({ ...variables }, signal)
374+
];
375+
376+
export type QueryOperation = {
377+
path: \\"/pets/{petId}\\";
378+
operationId: \\"showPetById\\";
379+
variables: ShowPetByIdVariables;
380+
};
381+
"
382+
`);
383+
});
384+
270385
it("should deal with injected headers (marked them as optional)", async () => {
271386
const writeFile = jest.fn();
272387
const openAPIDocument: OpenAPIObject = {

plugins/typescript/src/generators/generateReactQueryFunctions.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { getFetcher } from "../templates/fetcher";
1919
import { getContext } from "../templates/context";
2020
import { getUtils } from "../templates/utils";
2121
import { createNamespaceImport } from "../core/createNamespaceImport";
22+
import { camelizedPathParams } from "../core/camelizedPathParams";
2223

2324
export type Config = ConfigBase & {
2425
/**
@@ -165,7 +166,9 @@ export const generateReactQueryFunctions = async (
165166
undefined,
166167
f.createIdentifier("path"),
167168
undefined,
168-
f.createLiteralTypeNode(f.createStringLiteral(route))
169+
f.createLiteralTypeNode(
170+
f.createStringLiteral(camelizedPathParams(route))
171+
)
169172
),
170173
f.createPropertySignature(
171174
undefined,

0 commit comments

Comments
 (0)