Skip to content

Commit cdff16f

Browse files
w3dimarkerikson
andauthored
feat(codegen): support explicit tag overrides without altering defaults (#5135)
* feat(codegen): support explicit tag overrides without altering defaults * Add docs * Add additional test --------- Co-authored-by: Mark Erikson <[email protected]>
1 parent 53d02cc commit cdff16f

File tree

5 files changed

+235
-7
lines changed

5 files changed

+235
-7
lines changed

docs/rtk-query/usage/code-generation.mdx

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,9 +67,12 @@ npx @rtk-query/codegen-openapi openapi-config.ts
6767
If your OpenAPI specification uses [tags](https://swagger.io/docs/specification/grouping-operations-with-tags/), you can specify the `tag` option to the codegen.
6868
That will result in all generated endpoints having `providesTags`/`invalidatesTags` declarations for the `tags` of their respective operation definition.
6969

70-
Note that this will only result in string tags with no ids, so it might lead to scenarios where too much is invalidated and unneccessary requests are made on mutation.
70+
Note that this will only result in string tags with no ids, so it might lead to scenarios where too much is invalidated and unnecessary requests are made on mutation.
7171

72-
In that case it is still recommended to manually specify tags by using [`enhanceEndpoints`](../api/created-api/code-splitting.mdx) on top of the generated api and manually declare `providesTags`/`invalidatesTags`.
72+
In that case you have two options:
73+
74+
1. Use [`endpointOverrides`](#overriding-tags) to customize tags for specific endpoints during code generation
75+
2. Use [`enhanceEndpoints`](../api/created-api/code-splitting.mdx) after generation to manually add more specific `providesTags`/`invalidatesTags` with IDs
7376

7477
### Programmatic usage
7578

@@ -120,6 +123,16 @@ interface SimpleUsage {
120123
| Array<string | RegExp | EndpointMatcherFunction>
121124
endpointOverrides?: EndpointOverrides[]
122125
flattenArg?: boolean
126+
}
127+
128+
export type EndpointOverrides = {
129+
pattern: EndpointMatcher
130+
} & AtLeastOneOf<{
131+
type: 'mutation' | 'query'
132+
parameterFilter: ParameterMatcher
133+
providesTags: string[]
134+
invalidatesTags: string[]
135+
}>
123136
useEnumType?: boolean
124137
outputRegexConstants?: boolean
125138
httpResolverOptions?: SwaggerParser.HTTPResolverOptions
@@ -189,6 +202,53 @@ const withOverride: ConfigFile = {
189202
}
190203
```
191204

205+
#### Overriding tags
206+
207+
You can override the `providesTags` and `invalidatesTags` generated for any endpoint, regardless of whether the global `tag` option is enabled:
208+
209+
```ts no-transpile title="openapi-config.ts"
210+
const withTagOverrides: ConfigFile = {
211+
// ...
212+
tag: true, // or false - overrides work either way
213+
endpointOverrides: [
214+
{
215+
// Override the tags for a specific query
216+
pattern: 'getPetById',
217+
providesTags: ['SinglePet', 'PetDetails'],
218+
},
219+
{
220+
// Remove auto-generated tags by providing an empty array
221+
pattern: 'deletePet',
222+
invalidatesTags: [],
223+
},
224+
{
225+
// Add both providesTags AND invalidatesTags to any endpoint
226+
pattern: 'updatePet',
227+
providesTags: ['LastUpdatedPet'],
228+
invalidatesTags: ['Pet', 'PetList'],
229+
},
230+
],
231+
}
232+
```
233+
234+
**Key behaviors:**
235+
236+
- Tag overrides take precedence over auto-generated tags from the OpenAPI `tags` field
237+
- You can use an empty array (`[]`) to explicitly remove tags from an endpoint
238+
- Both `providesTags` and `invalidatesTags` can be set on any endpoint type (query or mutation)
239+
- Overrides work regardless of whether the global `tag: true` option is set
240+
241+
This is useful when:
242+
243+
- The OpenAPI tags don't match your caching strategy
244+
- You need more specific cache invalidation than the default tag generation provides
245+
- A mutation should also provide tags (e.g., login returning user data)
246+
- A query should also invalidate tags (e.g., polling that triggers cache updates)
247+
248+
:::note
249+
When using tag overrides with `tag: false`, the overridden tags will be emitted in the generated code, but they won't be automatically added to `addTagTypes`. You may need to manually add your custom tags to the base API's `tagTypes` array.
250+
:::
251+
192252
#### Generating hooks
193253

194254
Setting `hooks: true` will generate `useQuery` and `useMutation` hook exports. If you also want `useLazyQuery` hooks generated or more granular control, you can also pass an object in the shape of: `{ queries: boolean; lazyQueries: boolean; mutations: boolean }`.

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

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ export function generateEndpointDefinition({
119119
endpointBuilder = defaultEndpointBuilder,
120120
extraEndpointsProps,
121121
tags,
122+
tagOverrides,
122123
}: {
123124
operationName: string;
124125
type: 'query' | 'mutation';
@@ -127,14 +128,37 @@ export function generateEndpointDefinition({
127128
queryFn: ts.Expression;
128129
endpointBuilder?: ts.Identifier;
129130
extraEndpointsProps: ObjectPropertyDefinitions;
130-
tags: string[];
131+
tags?: string[];
132+
tagOverrides?: { providesTags?: string[]; invalidatesTags?: string[] };
131133
}) {
132134
const objectProperties = generateObjectProperties({ query: queryFn, ...extraEndpointsProps });
133-
if (tags.length > 0) {
135+
const providesTags =
136+
tagOverrides && 'providesTags' in tagOverrides
137+
? tagOverrides.providesTags
138+
: type === 'query'
139+
? tags
140+
: undefined;
141+
const invalidatesTags =
142+
tagOverrides && 'invalidatesTags' in tagOverrides
143+
? tagOverrides.invalidatesTags
144+
: type === 'mutation'
145+
? tags
146+
: undefined;
147+
148+
if (providesTags !== undefined) {
149+
objectProperties.push(
150+
factory.createPropertyAssignment(
151+
factory.createIdentifier('providesTags'),
152+
factory.createArrayLiteralExpression(providesTags.map((tag) => factory.createStringLiteral(tag)), false)
153+
)
154+
);
155+
}
156+
157+
if (invalidatesTags !== undefined) {
134158
objectProperties.push(
135159
factory.createPropertyAssignment(
136-
factory.createIdentifier(type === 'query' ? 'providesTags' : 'invalidatesTags'),
137-
factory.createArrayLiteralExpression(tags.map((tag) => factory.createStringLiteral(tag), false))
160+
factory.createIdentifier('invalidatesTags'),
161+
factory.createArrayLiteralExpression(invalidatesTags.map((tag) => factory.createStringLiteral(tag)), false)
138162
)
139163
);
140164
}

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

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -315,7 +315,7 @@ export async function generateApi(
315315
operation: { responses, requestBody },
316316
} = operationDefinition;
317317
const operationName = getOperationName({ verb, path, operation });
318-
const tags = tag ? getTags({ verb, pathItem }) : [];
318+
const tags = tag ? getTags({ verb, pathItem }) : undefined;
319319
const isQuery = testIsQuery(verb, overrides);
320320

321321
const returnsJson = apiGen.getResponseType(responses) === 'json';
@@ -470,6 +470,14 @@ export async function generateApi(
470470
).name
471471
);
472472

473+
const tagOverrides =
474+
overrides && (overrides.providesTags !== undefined || overrides.invalidatesTags !== undefined)
475+
? {
476+
...(overrides.providesTags !== undefined ? { providesTags: overrides.providesTags } : {}),
477+
...(overrides.invalidatesTags !== undefined ? { invalidatesTags: overrides.invalidatesTags } : {}),
478+
}
479+
: undefined;
480+
473481
return generateEndpointDefinition({
474482
operationName: operationNameSuffix ? capitalize(operationName + operationNameSuffix) : operationName,
475483
type: isQuery ? 'query' : 'mutation',
@@ -487,6 +495,7 @@ export async function generateApi(
487495
? generateQueryEndpointProps({ operationDefinition })
488496
: generateMutationEndpointProps({ operationDefinition }),
489497
tags,
498+
tagOverrides,
490499
});
491500
}
492501

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

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,11 +154,34 @@ export interface OutputFileOptions extends Partial<CommonOptions> {
154154
useEnumType?: boolean;
155155
}
156156

157+
/**
158+
* Configuration for overriding specific endpoint behaviors during code generation.
159+
* At least one override option (besides `pattern`) must be specified.
160+
*/
157161
export type EndpointOverrides = {
162+
/** Pattern to match endpoint names. Can be a string, RegExp, or matcher function. */
158163
pattern: EndpointMatcher;
159164
} & AtLeastOneKey<{
165+
/** Override the endpoint type (query vs mutation) when the inferred type is incorrect. */
160166
type: 'mutation' | 'query';
167+
/** Filter which parameters are included in the generated endpoint. Path parameters cannot be filtered. */
161168
parameterFilter: ParameterMatcher;
169+
/**
170+
* Override providesTags for this endpoint.
171+
* Takes precedence over auto-generated tags from OpenAPI spec.
172+
* Use an empty array to explicitly omit providesTags.
173+
* Works regardless of the global `tag` setting and endpoint type.
174+
* @example ['Pet', 'SinglePet']
175+
*/
176+
providesTags: string[];
177+
/**
178+
* Override invalidatesTags for this endpoint.
179+
* Takes precedence over auto-generated tags from OpenAPI spec.
180+
* Use an empty array to explicitly omit invalidatesTags.
181+
* Works regardless of the global `tag` setting and endpoint type.
182+
* @example ['Pet', 'PetList']
183+
*/
184+
invalidatesTags: string[];
162185
}>;
163186

164187
export type ConfigFile =

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

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,118 @@ describe('endpoint overrides', () => {
172172
expect(api).not.toMatch(/headers: {/);
173173
expect(api).toMatchSnapshot('should remove all parameters except for findPetsByStatus');
174174
});
175+
176+
it('should override generated tags', async () => {
177+
const api = await generateEndpoints({
178+
unionUndefined: true,
179+
tag: true,
180+
apiFile: './fixtures/emptyApi.ts',
181+
schemaFile: resolve(__dirname, 'fixtures/petstore.json'),
182+
filterEndpoints: ['getPetById', 'deletePet'],
183+
endpointOverrides: [
184+
{
185+
pattern: 'getPetById',
186+
providesTags: ['CustomQueryTag'],
187+
},
188+
{
189+
pattern: 'deletePet',
190+
invalidatesTags: [],
191+
},
192+
],
193+
});
194+
195+
expect(api).toMatch(/getPetById: build\.query[\s\S]*providesTags: \["CustomQueryTag"\]/);
196+
expect(api).not.toMatch(/getPetById: build\.query[\s\S]*providesTags: \["pet"\]/);
197+
expect(api).toMatch(/deletePet: build\.mutation[\s\S]*invalidatesTags: \[\]/);
198+
expect(api).not.toMatch(/deletePet: build\.mutation[\s\S]*invalidatesTags: \["pet"\]/);
199+
});
200+
201+
it('should allow tag overrides when tag generation is disabled', async () => {
202+
const api = await generateEndpoints({
203+
unionUndefined: true,
204+
apiFile: './fixtures/emptyApi.ts',
205+
schemaFile: resolve(__dirname, 'fixtures/petstore.json'),
206+
filterEndpoints: ['getPetById', 'deletePet'],
207+
endpointOverrides: [
208+
{
209+
pattern: 'getPetById',
210+
providesTags: ['ManualProvides'],
211+
},
212+
{
213+
pattern: 'deletePet',
214+
invalidatesTags: ['ManualInvalidates'],
215+
},
216+
],
217+
});
218+
219+
expect(api).toMatch(/getPetById: build\.query[\s\S]*providesTags: \["ManualProvides"\]/);
220+
expect(api).toMatch(/deletePet: build\.mutation[\s\S]*invalidatesTags: \["ManualInvalidates"\]/);
221+
expect(api).not.toMatch(/providesTags: \[\]/);
222+
expect(api).not.toMatch(/invalidatesTags: \[\]/);
223+
});
224+
225+
it('allows overriding tags regardless of inferred endpoint type', async () => {
226+
const api = await generateEndpoints({
227+
unionUndefined: true,
228+
apiFile: './fixtures/emptyApi.ts',
229+
schemaFile: resolve(__dirname, 'fixtures/petstore.json'),
230+
filterEndpoints: 'loginUser',
231+
endpointOverrides: [
232+
{
233+
pattern: 'loginUser',
234+
type: 'mutation',
235+
providesTags: ['LoginStatus'],
236+
},
237+
],
238+
});
239+
240+
expect(api).toMatch(/loginUser: build\.mutation/);
241+
expect(api).toMatch(/providesTags: \["LoginStatus"\]/);
242+
expect(api).not.toMatch(/invalidatesTags:/);
243+
});
244+
245+
it('allows overriding both providesTags and invalidatesTags simultaneously', async () => {
246+
const api = await generateEndpoints({
247+
unionUndefined: true,
248+
tag: true,
249+
apiFile: './fixtures/emptyApi.ts',
250+
schemaFile: resolve(__dirname, 'fixtures/petstore.json'),
251+
filterEndpoints: 'findPetsByStatus',
252+
endpointOverrides: [
253+
{
254+
pattern: 'findPetsByStatus',
255+
providesTags: ['CustomProvide'],
256+
invalidatesTags: ['CustomInvalidate'],
257+
},
258+
],
259+
});
260+
261+
expect(api).toMatch(/findPetsByStatus: build\.query/);
262+
expect(api).toMatch(/providesTags: \["CustomProvide"\]/);
263+
expect(api).toMatch(/invalidatesTags: \["CustomInvalidate"\]/);
264+
expect(api).not.toMatch(/providesTags: \["pet"\]/);
265+
expect(api).not.toMatch(/invalidatesTags: \["pet"\]/);
266+
});
267+
268+
it('does not add override tags to addTagTypes when tag generation is disabled', async () => {
269+
const api = await generateEndpoints({
270+
unionUndefined: true,
271+
apiFile: './fixtures/emptyApi.ts',
272+
schemaFile: resolve(__dirname, 'fixtures/petstore.json'),
273+
filterEndpoints: 'getPetById',
274+
endpointOverrides: [
275+
{
276+
pattern: 'getPetById',
277+
providesTags: ['CustomTag'],
278+
},
279+
],
280+
});
281+
282+
// The providesTags override should be present in the generated code
283+
expect(api).toMatch(/providesTags: \["CustomTag"\]/);
284+
// But addTagTypes should not be generated when tag: false (default)
285+
expect(api).not.toContain('addTagTypes');
286+
});
175287
});
176288

177289
describe('option encodePathParams', () => {

0 commit comments

Comments
 (0)