Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 62 additions & 2 deletions docs/rtk-query/usage/code-generation.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -120,6 +123,16 @@ interface SimpleUsage {
| Array<string | RegExp | EndpointMatcherFunction>
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
Expand Down Expand Up @@ -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 }`.
Expand Down
32 changes: 28 additions & 4 deletions packages/rtk-query-codegen-openapi/src/codegen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ export function generateEndpointDefinition({
endpointBuilder = defaultEndpointBuilder,
extraEndpointsProps,
tags,
tagOverrides,
}: {
operationName: string;
type: 'query' | 'mutation';
Expand All @@ -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)
)
);
}
Expand Down
11 changes: 10 additions & 1 deletion packages/rtk-query-codegen-openapi/src/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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',
Expand All @@ -487,6 +495,7 @@ export async function generateApi(
? generateQueryEndpointProps({ operationDefinition })
: generateMutationEndpointProps({ operationDefinition }),
tags,
tagOverrides,
});
}

Expand Down
23 changes: 23 additions & 0 deletions packages/rtk-query-codegen-openapi/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,11 +154,34 @@ export interface OutputFileOptions extends Partial<CommonOptions> {
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[];
}>;

export type ConfigFile =
Expand Down
112 changes: 112 additions & 0 deletions packages/rtk-query-codegen-openapi/test/generateEndpoints.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,118 @@ 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"\]/);
});

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', () => {
Expand Down