Skip to content

Commit 215a73d

Browse files
Codegen: create an option to "flatten" the queryArg (#2407)
* create option flattenArg * Remove async from describe block in generateEndpoints * Update snapshots * Update descriptions in types for codegen options * Remove tags from `generateQueryFn` Co-authored-by: Matt Sutkowski <msutkowski@gmail.com>
1 parent 7e94313 commit 215a73d

File tree

4 files changed

+150
-77
lines changed

4 files changed

+150
-77
lines changed

packages/rtk-query-codegen-openapi/src/generate.ts

Lines changed: 50 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,19 @@ function operationMatches(pattern?: EndpointMatcher) {
5656
};
5757
}
5858

59+
function withQueryComment<T extends ts.Node>(node: T, def: QueryArgDefinition, hasTrailingNewLine: boolean): T {
60+
const comment = def.origin === 'param' ? def.param.description : def.body.description;
61+
if (comment) {
62+
return ts.addSyntheticLeadingComment(
63+
node,
64+
ts.SyntaxKind.MultiLineCommentTrivia,
65+
`* ${comment} `,
66+
hasTrailingNewLine
67+
);
68+
}
69+
return node;
70+
}
71+
5972
export function getOverrides(
6073
operation: OperationDefinition,
6174
endpointOverrides?: EndpointOverrides[]
@@ -78,6 +91,7 @@ export async function generateApi(
7891
filterEndpoints,
7992
endpointOverrides,
8093
unionUndefined,
94+
flattenArg = false,
8195
}: GenerationOptions
8296
) {
8397
const v3Doc = await getV3Doc(spec);
@@ -290,6 +304,8 @@ export async function generateApi(
290304

291305
const queryArgValues = Object.values(queryArg);
292306

307+
const isFlatArg = flattenArg && queryArgValues.length === 1;
308+
293309
const QueryArg = factory.createTypeReferenceNode(
294310
registerInterface(
295311
factory.createTypeAliasDeclaration(
@@ -298,27 +314,22 @@ export async function generateApi(
298314
capitalize(operationName + argSuffix),
299315
undefined,
300316
queryArgValues.length > 0
301-
? factory.createTypeLiteralNode(
302-
queryArgValues.map((def) => {
303-
const comment = def.origin === 'param' ? def.param.description : def.body.description;
304-
const node = factory.createPropertySignature(
305-
undefined,
306-
propertyName(def.name),
307-
createQuestionToken(!def.required),
308-
def.type
309-
);
310-
311-
if (comment) {
312-
return ts.addSyntheticLeadingComment(
313-
node,
314-
ts.SyntaxKind.MultiLineCommentTrivia,
315-
`* ${comment} `,
317+
? isFlatArg
318+
? withQueryComment({ ...queryArgValues[0].type }, queryArgValues[0], false)
319+
: factory.createTypeLiteralNode(
320+
queryArgValues.map((def) =>
321+
withQueryComment(
322+
factory.createPropertySignature(
323+
undefined,
324+
propertyName(def.name),
325+
createQuestionToken(!def.required),
326+
def.type
327+
),
328+
def,
316329
true
317-
);
318-
}
319-
return node;
320-
})
321-
)
330+
)
331+
)
332+
)
322333
: factory.createKeywordTypeNode(ts.SyntaxKind.VoidKeyword)
323334
)
324335
).name
@@ -329,7 +340,7 @@ export async function generateApi(
329340
type: isQuery ? 'query' : 'mutation',
330341
Response: ResponseTypeName,
331342
QueryArg,
332-
queryFn: generateQueryFn({ operationDefinition, queryArg, isQuery, tags }),
343+
queryFn: generateQueryFn({ operationDefinition, queryArg, isQuery, isFlatArg }),
333344
extraEndpointsProps: isQuery
334345
? generateQueryEndpointProps({ operationDefinition })
335346
: generateMutationEndpointProps({ operationDefinition }),
@@ -340,13 +351,13 @@ export async function generateApi(
340351
function generateQueryFn({
341352
operationDefinition,
342353
queryArg,
354+
isFlatArg,
343355
isQuery,
344-
tags,
345356
}: {
346357
operationDefinition: OperationDefinition;
347358
queryArg: QueryArgDefinitions;
359+
isFlatArg: boolean;
348360
isQuery: boolean;
349-
tags: string[];
350361
}) {
351362
const { path, verb } = operationDefinition;
352363

@@ -365,7 +376,11 @@ export async function generateApi(
365376
factory.createIdentifier(propertyName),
366377
factory.createObjectLiteralExpression(
367378
parameters.map(
368-
(param) => createPropertyAssignment(param.originalName, accessProperty(rootObject, param.name)),
379+
(param) =>
380+
createPropertyAssignment(
381+
param.originalName,
382+
isFlatArg ? rootObject : accessProperty(rootObject, param.name)
383+
),
369384
true
370385
)
371386
)
@@ -395,7 +410,7 @@ export async function generateApi(
395410
[
396411
factory.createPropertyAssignment(
397412
factory.createIdentifier('url'),
398-
generatePathExpression(path, pickParams('path'), rootObject)
413+
generatePathExpression(path, pickParams('path'), rootObject, isFlatArg)
399414
),
400415
isQuery && verb.toUpperCase() === 'GET'
401416
? undefined
@@ -407,7 +422,9 @@ export async function generateApi(
407422
? undefined
408423
: factory.createPropertyAssignment(
409424
factory.createIdentifier('body'),
410-
factory.createPropertyAccessExpression(rootObject, factory.createIdentifier(bodyParameter.name))
425+
isFlatArg
426+
? rootObject
427+
: factory.createPropertyAccessExpression(rootObject, factory.createIdentifier(bodyParameter.name))
411428
),
412429
createObjectLiteralProperty(pickParams('cookie'), 'cookies'),
413430
createObjectLiteralProperty(pickParams('header'), 'headers'),
@@ -436,7 +453,12 @@ function accessProperty(rootObject: ts.Identifier, propertyName: string) {
436453
: factory.createElementAccessExpression(rootObject, factory.createStringLiteral(propertyName));
437454
}
438455

439-
function generatePathExpression(path: string, pathParameters: QueryArgDefinition[], rootObject: ts.Identifier) {
456+
function generatePathExpression(
457+
path: string,
458+
pathParameters: QueryArgDefinition[],
459+
rootObject: ts.Identifier,
460+
isFlatArg: boolean
461+
) {
440462
const expressions: Array<[string, string]> = [];
441463

442464
const head = path.replace(/\{(.*?)\}(.*?)(?=\{|$)/g, (_, expression, literal) => {
@@ -453,7 +475,7 @@ function generatePathExpression(path: string, pathParameters: QueryArgDefinition
453475
factory.createTemplateHead(head),
454476
expressions.map(([prop, literal], index) =>
455477
factory.createTemplateSpan(
456-
accessProperty(rootObject, prop),
478+
isFlatArg ? rootObject : accessProperty(rootObject, prop),
457479
index === expressions.length - 1
458480
? factory.createTemplateTail(literal)
459481
: factory.createTemplateMiddle(literal)

packages/rtk-query-codegen-openapi/src/types.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,12 +53,20 @@ export interface CommonOptions {
5353
hooks?: boolean | { queries: boolean; lazyQueries: boolean; mutations: boolean };
5454
/**
5555
* defaults to false
56+
* `true` will generate a union type for `undefined` properties like: `{ id?: string | undefined }` instead of `{ id?: string }`
5657
*/
5758
unionUndefined?: boolean;
5859
/**
5960
* defaults to false
61+
* `true` will result in all generated endpoints having `providesTags`/`invalidatesTags` declarations for the `tags` of their respective operation definition
62+
* @see https://redux-toolkit.js.org/rtk-query/usage/code-generation for more information
6063
*/
6164
tag?: boolean;
65+
/**
66+
* defaults to false
67+
* `true` will "flatten" the arg so that you can do things like `useGetEntityById(1)` instead of `useGetEntityById({ entityId: 1 })`
68+
*/
69+
flattenArg?: boolean;
6270
}
6371

6472
export type TextMatcher = string | RegExp | (string | RegExp)[];

packages/rtk-query-codegen-openapi/test/__snapshots__/generateEndpoints.test.ts.snap

Lines changed: 48 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -263,54 +263,6 @@ export type User = {
263263

264264
`;
265265

266-
exports[`default hooks generation: should generate an \`useGetPetByIdQuery\` query hook and an \`useAddPetMutation\` mutation hook 1`] = `
267-
import { api } from './fixtures/emptyApi';
268-
const injectedRtkApi = api.injectEndpoints({
269-
endpoints: (build) => ({
270-
addPet: build.mutation<AddPetApiResponse, AddPetApiArg>({
271-
query: (queryArg) => ({
272-
url: \`/pet\`,
273-
method: 'POST',
274-
body: queryArg.pet,
275-
}),
276-
}),
277-
getPetById: build.query<GetPetByIdApiResponse, GetPetByIdApiArg>({
278-
query: (queryArg) => ({ url: \`/pet/\${queryArg.petId}\` }),
279-
}),
280-
}),
281-
overrideExisting: false,
282-
});
283-
export { injectedRtkApi as enhancedApi };
284-
export type AddPetApiResponse = /** status 200 Successful operation */ Pet;
285-
export type AddPetApiArg = {
286-
/** Create a new pet in the store */
287-
pet: Pet;
288-
};
289-
export type GetPetByIdApiResponse = /** status 200 successful operation */ Pet;
290-
export type GetPetByIdApiArg = {
291-
/** ID of pet to return */
292-
petId: number;
293-
};
294-
export type Category = {
295-
id?: number | undefined;
296-
name?: string | undefined;
297-
};
298-
export type Tag = {
299-
id?: number | undefined;
300-
name?: string | undefined;
301-
};
302-
export type Pet = {
303-
id?: number | undefined;
304-
name: string;
305-
category?: Category | undefined;
306-
photoUrls: string[];
307-
tags?: Tag[] | undefined;
308-
status?: ('available' | 'pending' | 'sold') | undefined;
309-
};
310-
export const { useAddPetMutation, useGetPetByIdQuery } = injectedRtkApi;
311-
312-
`;
313-
314266
exports[`endpoint filtering: should only have endpoints loginUser, placeOrder, getOrderById, deleteOrder 1`] = `
315267
import { api } from './fixtures/emptyApi';
316268
const injectedRtkApi = api.injectEndpoints({
@@ -424,6 +376,54 @@ export const { useLoginUserMutation } = injectedRtkApi;
424376

425377
`;
426378

379+
exports[`hooks generation: should generate an \`useGetPetByIdQuery\` query hook and an \`useAddPetMutation\` mutation hook 1`] = `
380+
import { api } from './fixtures/emptyApi';
381+
const injectedRtkApi = api.injectEndpoints({
382+
endpoints: (build) => ({
383+
addPet: build.mutation<AddPetApiResponse, AddPetApiArg>({
384+
query: (queryArg) => ({
385+
url: \`/pet\`,
386+
method: 'POST',
387+
body: queryArg.pet,
388+
}),
389+
}),
390+
getPetById: build.query<GetPetByIdApiResponse, GetPetByIdApiArg>({
391+
query: (queryArg) => ({ url: \`/pet/\${queryArg.petId}\` }),
392+
}),
393+
}),
394+
overrideExisting: false,
395+
});
396+
export { injectedRtkApi as enhancedApi };
397+
export type AddPetApiResponse = /** status 200 Successful operation */ Pet;
398+
export type AddPetApiArg = {
399+
/** Create a new pet in the store */
400+
pet: Pet;
401+
};
402+
export type GetPetByIdApiResponse = /** status 200 successful operation */ Pet;
403+
export type GetPetByIdApiArg = {
404+
/** ID of pet to return */
405+
petId: number;
406+
};
407+
export type Category = {
408+
id?: number | undefined;
409+
name?: string | undefined;
410+
};
411+
export type Tag = {
412+
id?: number | undefined;
413+
name?: string | undefined;
414+
};
415+
export type Pet = {
416+
id?: number | undefined;
417+
name: string;
418+
category?: Category | undefined;
419+
photoUrls: string[];
420+
tags?: Tag[] | undefined;
421+
status?: ('available' | 'pending' | 'sold') | undefined;
422+
};
423+
export const { useAddPetMutation, useGetPetByIdQuery } = injectedRtkApi;
424+
425+
`;
426+
427427
exports[`should use brackets in a querystring urls arg, when the arg contains full stops 1`] = `
428428
import { api } from './fixtures/emptyApi';
429429
const injectedRtkApi = api.injectEndpoints({

packages/rtk-query-codegen-openapi/test/generateEndpoints.test.ts

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,50 @@ test('endpoint overrides', async () => {
7373
expect(api).toMatchSnapshot('loginUser should be a mutation');
7474
});
7575

76-
test('default hooks generation', async () => {
76+
describe('option flattenArg', () => {
77+
const config = {
78+
apiFile: './fixtures/emptyApi.ts',
79+
schemaFile: resolve(__dirname, 'fixtures/petstore.json'),
80+
flattenArg: true,
81+
};
82+
83+
it('should apply a queryArg directly in the path', async () => {
84+
const api = await generateEndpoints({
85+
...config,
86+
filterEndpoints: ['getOrderById'],
87+
});
88+
// eslint-disable-next-line no-template-curly-in-string
89+
expect(api).toContain('`/store/order/${queryArg}`');
90+
expect(api).toMatch(/export type GetOrderByIdApiArg =[\s/*]+ID of order that needs to be fetched[\s/*]+number;/);
91+
});
92+
93+
it('should apply a queryArg directly in the params', async () => {
94+
const api = await generateEndpoints({
95+
...config,
96+
filterEndpoints: ['findPetsByStatus'],
97+
});
98+
expect(api).toContain('params: { status: queryArg }');
99+
expect(api).not.toContain('export type FindPetsByStatusApiArg = {');
100+
});
101+
102+
it('should use the queryArg as the entire body', async () => {
103+
const api = await generateEndpoints({
104+
...config,
105+
filterEndpoints: ['addPet'],
106+
});
107+
expect(api).toMatch(/body: queryArg[^.]/);
108+
});
109+
110+
it('should not change anything if there are 2+ arguments.', async () => {
111+
const api = await generateEndpoints({
112+
...config,
113+
filterEndpoints: ['uploadFile'],
114+
});
115+
expect(api).toContain('queryArg.body');
116+
});
117+
});
118+
119+
test('hooks generation', async () => {
77120
const api = await generateEndpoints({
78121
unionUndefined: true,
79122
apiFile: './fixtures/emptyApi.ts',

0 commit comments

Comments
 (0)