From 1f8866bb6cdc16eeb372aca7951e6f8ee6bfd50f Mon Sep 17 00:00:00 2001 From: W3D Date: Thu, 13 Nov 2025 18:52:34 +0200 Subject: [PATCH 1/3] feat(codegen): support explicit tag overrides without altering defaults --- .../rtk-query-codegen-openapi/src/codegen.ts | 32 ++++++- .../rtk-query-codegen-openapi/src/generate.ts | 11 ++- .../rtk-query-codegen-openapi/src/types.ts | 2 + .../test/generateEndpoints.test.ts | 92 +++++++++++++++++++ 4 files changed, 132 insertions(+), 5 deletions(-) diff --git a/packages/rtk-query-codegen-openapi/src/codegen.ts b/packages/rtk-query-codegen-openapi/src/codegen.ts index 9ac4bd2274..6ea573807a 100644 --- a/packages/rtk-query-codegen-openapi/src/codegen.ts +++ b/packages/rtk-query-codegen-openapi/src/codegen.ts @@ -119,6 +119,7 @@ export function generateEndpointDefinition({ endpointBuilder = defaultEndpointBuilder, extraEndpointsProps, tags, + tagOverrides, }: { operationName: string; type: 'query' | 'mutation'; @@ -127,14 +128,37 @@ export function generateEndpointDefinition({ queryFn: ts.Expression; endpointBuilder?: ts.Identifier; extraEndpointsProps: ObjectPropertyDefinitions; - tags: string[]; + tags?: string[]; + tagOverrides?: { providesTags?: string[]; invalidatesTags?: string[] }; }) { const objectProperties = generateObjectProperties({ query: queryFn, ...extraEndpointsProps }); - if (tags.length > 0) { + const providesTags = + tagOverrides && 'providesTags' in tagOverrides + ? tagOverrides.providesTags + : type === 'query' + ? tags + : undefined; + const invalidatesTags = + tagOverrides && 'invalidatesTags' in tagOverrides + ? tagOverrides.invalidatesTags + : type === 'mutation' + ? tags + : undefined; + + if (providesTags !== undefined) { + objectProperties.push( + factory.createPropertyAssignment( + factory.createIdentifier('providesTags'), + factory.createArrayLiteralExpression(providesTags.map((tag) => factory.createStringLiteral(tag)), false) + ) + ); + } + + if (invalidatesTags !== undefined) { objectProperties.push( factory.createPropertyAssignment( - factory.createIdentifier(type === 'query' ? 'providesTags' : 'invalidatesTags'), - factory.createArrayLiteralExpression(tags.map((tag) => factory.createStringLiteral(tag), false)) + factory.createIdentifier('invalidatesTags'), + factory.createArrayLiteralExpression(invalidatesTags.map((tag) => factory.createStringLiteral(tag)), false) ) ); } diff --git a/packages/rtk-query-codegen-openapi/src/generate.ts b/packages/rtk-query-codegen-openapi/src/generate.ts index d6f125964a..0cf52b6130 100644 --- a/packages/rtk-query-codegen-openapi/src/generate.ts +++ b/packages/rtk-query-codegen-openapi/src/generate.ts @@ -315,7 +315,7 @@ export async function generateApi( operation: { responses, requestBody }, } = operationDefinition; const operationName = getOperationName({ verb, path, operation }); - const tags = tag ? getTags({ verb, pathItem }) : []; + const tags = tag ? getTags({ verb, pathItem }) : undefined; const isQuery = testIsQuery(verb, overrides); const returnsJson = apiGen.getResponseType(responses) === 'json'; @@ -470,6 +470,14 @@ export async function generateApi( ).name ); + const tagOverrides = + overrides && (overrides.providesTags !== undefined || overrides.invalidatesTags !== undefined) + ? { + ...(overrides.providesTags !== undefined ? { providesTags: overrides.providesTags } : {}), + ...(overrides.invalidatesTags !== undefined ? { invalidatesTags: overrides.invalidatesTags } : {}), + } + : undefined; + return generateEndpointDefinition({ operationName: operationNameSuffix ? capitalize(operationName + operationNameSuffix) : operationName, type: isQuery ? 'query' : 'mutation', @@ -487,6 +495,7 @@ export async function generateApi( ? generateQueryEndpointProps({ operationDefinition }) : generateMutationEndpointProps({ operationDefinition }), tags, + tagOverrides, }); } diff --git a/packages/rtk-query-codegen-openapi/src/types.ts b/packages/rtk-query-codegen-openapi/src/types.ts index b86375cdfb..f753883f4e 100644 --- a/packages/rtk-query-codegen-openapi/src/types.ts +++ b/packages/rtk-query-codegen-openapi/src/types.ts @@ -159,6 +159,8 @@ export type EndpointOverrides = { } & AtLeastOneKey<{ type: 'mutation' | 'query'; parameterFilter: ParameterMatcher; + providesTags: string[]; + invalidatesTags: string[]; }>; export type ConfigFile = diff --git a/packages/rtk-query-codegen-openapi/test/generateEndpoints.test.ts b/packages/rtk-query-codegen-openapi/test/generateEndpoints.test.ts index c3cd943850..5dbca3f1f8 100644 --- a/packages/rtk-query-codegen-openapi/test/generateEndpoints.test.ts +++ b/packages/rtk-query-codegen-openapi/test/generateEndpoints.test.ts @@ -172,6 +172,98 @@ describe('endpoint overrides', () => { expect(api).not.toMatch(/headers: {/); expect(api).toMatchSnapshot('should remove all parameters except for findPetsByStatus'); }); + + it('should override generated tags', async () => { + const api = await generateEndpoints({ + unionUndefined: true, + tag: true, + apiFile: './fixtures/emptyApi.ts', + schemaFile: resolve(__dirname, 'fixtures/petstore.json'), + filterEndpoints: ['getPetById', 'deletePet'], + endpointOverrides: [ + { + pattern: 'getPetById', + providesTags: ['CustomQueryTag'], + }, + { + pattern: 'deletePet', + invalidatesTags: [], + }, + ], + }); + + expect(api).toMatch(/getPetById: build\.query[\s\S]*providesTags: \["CustomQueryTag"\]/); + expect(api).not.toMatch(/getPetById: build\.query[\s\S]*providesTags: \["pet"\]/); + expect(api).toMatch(/deletePet: build\.mutation[\s\S]*invalidatesTags: \[\]/); + expect(api).not.toMatch(/deletePet: build\.mutation[\s\S]*invalidatesTags: \["pet"\]/); + }); + + it('should allow tag overrides when tag generation is disabled', async () => { + const api = await generateEndpoints({ + unionUndefined: true, + apiFile: './fixtures/emptyApi.ts', + schemaFile: resolve(__dirname, 'fixtures/petstore.json'), + filterEndpoints: ['getPetById', 'deletePet'], + endpointOverrides: [ + { + pattern: 'getPetById', + providesTags: ['ManualProvides'], + }, + { + pattern: 'deletePet', + invalidatesTags: ['ManualInvalidates'], + }, + ], + }); + + expect(api).toMatch(/getPetById: build\.query[\s\S]*providesTags: \["ManualProvides"\]/); + expect(api).toMatch(/deletePet: build\.mutation[\s\S]*invalidatesTags: \["ManualInvalidates"\]/); + expect(api).not.toMatch(/providesTags: \[\]/); + expect(api).not.toMatch(/invalidatesTags: \[\]/); + }); + + it('allows overriding tags regardless of inferred endpoint type', async () => { + const api = await generateEndpoints({ + unionUndefined: true, + apiFile: './fixtures/emptyApi.ts', + schemaFile: resolve(__dirname, 'fixtures/petstore.json'), + filterEndpoints: 'loginUser', + endpointOverrides: [ + { + pattern: 'loginUser', + type: 'mutation', + providesTags: ['LoginStatus'], + }, + ], + }); + + expect(api).toMatch(/loginUser: build\.mutation/); + expect(api).toMatch(/providesTags: \["LoginStatus"\]/); + expect(api).not.toMatch(/invalidatesTags:/); + }); + + it('allows overriding both providesTags and invalidatesTags simultaneously', async () => { + const api = await generateEndpoints({ + unionUndefined: true, + tag: true, + apiFile: './fixtures/emptyApi.ts', + schemaFile: resolve(__dirname, 'fixtures/petstore.json'), + filterEndpoints: 'findPetsByStatus', + endpointOverrides: [ + { + pattern: 'findPetsByStatus', + providesTags: ['CustomProvide'], + invalidatesTags: ['CustomInvalidate'], + }, + ], + }); + + expect(api).toMatch(/findPetsByStatus: build\.query/); + expect(api).toMatch(/providesTags: \["CustomProvide"\]/); + expect(api).toMatch(/invalidatesTags: \["CustomInvalidate"\]/); + expect(api).not.toMatch(/providesTags: \["pet"\]/); + expect(api).not.toMatch(/invalidatesTags: \["pet"\]/); + }); }); describe('option encodePathParams', () => { From 5120199936fbf4b91c5ae70358d636e478d33f59 Mon Sep 17 00:00:00 2001 From: Mark Erikson Date: Sun, 7 Dec 2025 18:15:47 -0500 Subject: [PATCH 2/3] Add docs --- docs/rtk-query/usage/code-generation.mdx | 64 ++++++++++++++++++- .../rtk-query-codegen-openapi/src/types.ts | 21 ++++++ 2 files changed, 83 insertions(+), 2 deletions(-) diff --git a/docs/rtk-query/usage/code-generation.mdx b/docs/rtk-query/usage/code-generation.mdx index cfa798a757..f307339267 100644 --- a/docs/rtk-query/usage/code-generation.mdx +++ b/docs/rtk-query/usage/code-generation.mdx @@ -67,9 +67,12 @@ npx @rtk-query/codegen-openapi openapi-config.ts If your OpenAPI specification uses [tags](https://swagger.io/docs/specification/grouping-operations-with-tags/), you can specify the `tag` option to the codegen. That will result in all generated endpoints having `providesTags`/`invalidatesTags` declarations for the `tags` of their respective operation definition. -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. +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. -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`. +In that case you have two options: + +1. Use [`endpointOverrides`](#overriding-tags) to customize tags for specific endpoints during code generation +2. Use [`enhanceEndpoints`](../api/created-api/code-splitting.mdx) after generation to manually add more specific `providesTags`/`invalidatesTags` with IDs ### Programmatic usage @@ -120,6 +123,16 @@ interface SimpleUsage { | Array endpointOverrides?: EndpointOverrides[] flattenArg?: boolean +} + +export type EndpointOverrides = { + pattern: EndpointMatcher +} & AtLeastOneOf<{ + type: 'mutation' | 'query' + parameterFilter: ParameterMatcher + providesTags: string[] + invalidatesTags: string[] +}> useEnumType?: boolean outputRegexConstants?: boolean httpResolverOptions?: SwaggerParser.HTTPResolverOptions @@ -189,6 +202,53 @@ const withOverride: ConfigFile = { } ``` +#### Overriding tags + +You can override the `providesTags` and `invalidatesTags` generated for any endpoint, regardless of whether the global `tag` option is enabled: + +```ts no-transpile title="openapi-config.ts" +const withTagOverrides: ConfigFile = { + // ... + tag: true, // or false - overrides work either way + endpointOverrides: [ + { + // Override the tags for a specific query + pattern: 'getPetById', + providesTags: ['SinglePet', 'PetDetails'], + }, + { + // Remove auto-generated tags by providing an empty array + pattern: 'deletePet', + invalidatesTags: [], + }, + { + // Add both providesTags AND invalidatesTags to any endpoint + pattern: 'updatePet', + providesTags: ['LastUpdatedPet'], + invalidatesTags: ['Pet', 'PetList'], + }, + ], +} +``` + +**Key behaviors:** + +- Tag overrides take precedence over auto-generated tags from the OpenAPI `tags` field +- You can use an empty array (`[]`) to explicitly remove tags from an endpoint +- Both `providesTags` and `invalidatesTags` can be set on any endpoint type (query or mutation) +- Overrides work regardless of whether the global `tag: true` option is set + +This is useful when: + +- The OpenAPI tags don't match your caching strategy +- You need more specific cache invalidation than the default tag generation provides +- A mutation should also provide tags (e.g., login returning user data) +- A query should also invalidate tags (e.g., polling that triggers cache updates) + +:::note +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. +::: + #### Generating hooks 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 }`. diff --git a/packages/rtk-query-codegen-openapi/src/types.ts b/packages/rtk-query-codegen-openapi/src/types.ts index f753883f4e..bdff603282 100644 --- a/packages/rtk-query-codegen-openapi/src/types.ts +++ b/packages/rtk-query-codegen-openapi/src/types.ts @@ -154,12 +154,33 @@ export interface OutputFileOptions extends Partial { useEnumType?: boolean; } +/** + * Configuration for overriding specific endpoint behaviors during code generation. + * At least one override option (besides `pattern`) must be specified. + */ export type EndpointOverrides = { + /** Pattern to match endpoint names. Can be a string, RegExp, or matcher function. */ pattern: EndpointMatcher; } & AtLeastOneKey<{ + /** Override the endpoint type (query vs mutation) when the inferred type is incorrect. */ type: 'mutation' | 'query'; + /** Filter which parameters are included in the generated endpoint. Path parameters cannot be filtered. */ parameterFilter: ParameterMatcher; + /** + * Override providesTags for this endpoint. + * Takes precedence over auto-generated tags from OpenAPI spec. + * Use an empty array to explicitly omit providesTags. + * Works regardless of the global `tag` setting and endpoint type. + * @example ['Pet', 'SinglePet'] + */ providesTags: string[]; + /** + * Override invalidatesTags for this endpoint. + * Takes precedence over auto-generated tags from OpenAPI spec. + * Use an empty array to explicitly omit invalidatesTags. + * Works regardless of the global `tag` setting and endpoint type. + * @example ['Pet', 'PetList'] + */ invalidatesTags: string[]; }>; From 201be308ae6107a44171f17a11030864e6065021 Mon Sep 17 00:00:00 2001 From: Mark Erikson Date: Sun, 7 Dec 2025 18:21:28 -0500 Subject: [PATCH 3/3] Add additional test --- .../test/generateEndpoints.test.ts | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/packages/rtk-query-codegen-openapi/test/generateEndpoints.test.ts b/packages/rtk-query-codegen-openapi/test/generateEndpoints.test.ts index 5dbca3f1f8..04a942e911 100644 --- a/packages/rtk-query-codegen-openapi/test/generateEndpoints.test.ts +++ b/packages/rtk-query-codegen-openapi/test/generateEndpoints.test.ts @@ -264,6 +264,26 @@ describe('endpoint overrides', () => { expect(api).not.toMatch(/providesTags: \["pet"\]/); expect(api).not.toMatch(/invalidatesTags: \["pet"\]/); }); + + it('does not add override tags to addTagTypes when tag generation is disabled', async () => { + const api = await generateEndpoints({ + unionUndefined: true, + apiFile: './fixtures/emptyApi.ts', + schemaFile: resolve(__dirname, 'fixtures/petstore.json'), + filterEndpoints: 'getPetById', + endpointOverrides: [ + { + pattern: 'getPetById', + providesTags: ['CustomTag'], + }, + ], + }); + + // The providesTags override should be present in the generated code + expect(api).toMatch(/providesTags: \["CustomTag"\]/); + // But addTagTypes should not be generated when tag: false (default) + expect(api).not.toContain('addTagTypes'); + }); }); describe('option encodePathParams', () => {