Skip to content
Open
7 changes: 7 additions & 0 deletions docs/rtk-query/usage/code-generation.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,13 @@ const filteredConfig: ConfigFile = {
}
```

:::note
You can avoid transforming endpoint names to camel case by passing `exactOperationIds: true` to `generateEndpoints`. However, you will have to ensure the following:
- All endpoints in use have an operation ID
- You have no duplicate operation IDs
- None of your operation IDs include whitespace
:::

#### Endpoint overrides

If an endpoint is generated as a mutation instead of a query or the other way round, you can override that:
Expand Down
41 changes: 34 additions & 7 deletions packages/rtk-query-codegen-openapi/src/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,17 @@ function patternMatches(pattern?: TextMatcher) {
};
}

function operationMatches(pattern?: EndpointMatcher) {
function operationMatches(pattern: EndpointMatcher | undefined, exactOperationIds: boolean) {
const checkMatch = typeof pattern === 'function' ? pattern : patternMatches(pattern);
return function matcher(operationDefinition: OperationDefinition) {
if (!pattern) return true;
const operationName = getOperationName(operationDefinition);
if (exactOperationIds && operationDefinition.operation.operationId === undefined) {
// TODO: More descriptive error message with traceable information
throw new Error('exactOperationIds specified, but found operation missing operationId');
}
const operationName = exactOperationIds
? operationDefinition.operation.operationId!
: getOperationName(operationDefinition);
return checkMatch(operationName, operationDefinition);
};
}
Expand Down Expand Up @@ -139,9 +145,10 @@ function generateRegexConstantsForType(

export function getOverrides(
operation: OperationDefinition,
endpointOverrides?: EndpointOverrides[]
endpointOverrides?: EndpointOverrides[],
exactOperationIds: boolean = false
): EndpointOverrides | undefined {
return endpointOverrides?.find((override) => operationMatches(override.pattern)(operation));
return endpointOverrides?.find((override) => operationMatches(override.pattern, exactOperationIds)(operation));
}

export async function generateApi(
Expand Down Expand Up @@ -170,6 +177,7 @@ export async function generateApi(
useUnknown = false,
esmExtensions = false,
outputRegexConstants = false,
exactOperationIds = false,
}: GenerationOptions
) {
const v3Doc = (v3DocCache[spec] ??= await getV3Doc(spec, httpResolverOptions));
Expand All @@ -186,7 +194,20 @@ export async function generateApi(
apiGen.preprocessComponents(apiGen.spec.components.schemas);
}

const operationDefinitions = getOperationDefinitions(v3Doc).filter(operationMatches(filterEndpoints));
const operationDefinitions = getOperationDefinitions(v3Doc).filter(
operationMatches(filterEndpoints, exactOperationIds)
);

if (exactOperationIds) {
const allOperationIds = operationDefinitions.map((o) => o.operation.operationId!);
const duplicateOperationIds = allOperationIds.filter(
(operationId, index) => allOperationIds.findIndex((id) => id === operationId) !== index
);
if (duplicateOperationIds.length > 0) {
// TODO: More descriptive error message with traceable information
throw new Error('Duplicate operation IDs not allowed when using exactOperationIds');
}
}

const resultFile = ts.createSourceFile(
'someFileName.ts',
Expand Down Expand Up @@ -239,7 +260,8 @@ export async function generateApi(
operationDefinitions.map((operationDefinition) =>
generateEndpoint({
operationDefinition,
overrides: getOverrides(operationDefinition, endpointOverrides),
overrides: getOverrides(operationDefinition, endpointOverrides, exactOperationIds),
exactOperationIds,
})
),
true
Expand Down Expand Up @@ -278,6 +300,7 @@ export async function generateApi(
endpointOverrides,
config: hooks,
operationNameSuffix,
exactOperationIds,
}),
]
: []),
Expand All @@ -303,9 +326,11 @@ export async function generateApi(
function generateEndpoint({
operationDefinition,
overrides,
exactOperationIds,
}: {
operationDefinition: OperationDefinition;
overrides?: EndpointOverrides;
exactOperationIds: boolean;
}) {
const {
verb,
Expand All @@ -314,7 +339,9 @@ export async function generateApi(
operation,
operation: { responses, requestBody },
} = operationDefinition;
const operationName = getOperationName({ verb, path, operation });
const operationName = exactOperationIds
? operation.operationId! // This is a safe operation when using exactOperationIds, as all operation IDs have already been confirmed to exist
: getOperationName({ verb, path, operation });
const tags = tag ? getTags({ verb, pathItem }) : undefined;
const isQuery = testIsQuery(verb, overrides);

Expand Down
28 changes: 24 additions & 4 deletions packages/rtk-query-codegen-openapi/src/generators/react-hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,39 +12,49 @@ type GetReactHookNameParams = {
endpointOverrides: EndpointOverrides[] | undefined;
config: HooksConfigOptions;
operationNameSuffix?: string;
exactOperationIds: boolean;
};

type CreateBindingParams = {
operationDefinition: OperationDefinition;
overrides?: EndpointOverrides;
isLazy?: boolean;
operationNameSuffix?: string;
exactOperationIds: boolean;
};

const createBinding = ({
operationDefinition: { verb, path, operation },
overrides,
isLazy = false,
operationNameSuffix,
exactOperationIds,
}: CreateBindingParams) =>
factory.createBindingElement(
undefined,
undefined,
factory.createIdentifier(
`use${isLazy ? 'Lazy' : ''}${capitalize(getOperationName(verb, path, operation.operationId))}${operationNameSuffix ?? ''}${
`use${isLazy ? 'Lazy' : ''}${capitalize(exactOperationIds ? operation.operationId! : getOperationName(verb, path, operation.operationId))}${operationNameSuffix ?? ''}${
isQuery(verb, overrides) ? 'Query' : 'Mutation'
}`
),
undefined
);

const getReactHookName = ({ operationDefinition, endpointOverrides, config, operationNameSuffix }: GetReactHookNameParams) => {
const overrides = getOverrides(operationDefinition, endpointOverrides);
const getReactHookName = ({
operationDefinition,
endpointOverrides,
config,
operationNameSuffix,
exactOperationIds,
}: GetReactHookNameParams) => {
const overrides = getOverrides(operationDefinition, endpointOverrides, exactOperationIds);

const baseParams = {
operationDefinition,
overrides,
operationNameSuffix,
exactOperationIds,
};

const _isQuery = isQuery(operationDefinition.verb, overrides);
Expand All @@ -71,13 +81,15 @@ type GenerateReactHooksParams = {
endpointOverrides: EndpointOverrides[] | undefined;
config: HooksConfigOptions;
operationNameSuffix?: string;
exactOperationIds: boolean;
};
export const generateReactHooks = ({
exportName,
operationDefinitions,
endpointOverrides,
config,
operationNameSuffix,
exactOperationIds,
}: GenerateReactHooksParams) =>
factory.createVariableStatement(
[factory.createModifier(ts.SyntaxKind.ExportKeyword)],
Expand All @@ -86,7 +98,15 @@ export const generateReactHooks = ({
factory.createVariableDeclaration(
factory.createObjectBindingPattern(
operationDefinitions
.map((operationDefinition) => getReactHookName({ operationDefinition, endpointOverrides, config, operationNameSuffix }))
.map((operationDefinition) =>
getReactHookName({
operationDefinition,
endpointOverrides,
config,
operationNameSuffix,
exactOperationIds,
})
)
.flat()
),
undefined,
Expand Down
2 changes: 2 additions & 0 deletions packages/rtk-query-codegen-openapi/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ export type { ConfigFile } from './types';

const require = createRequire(__filename);

export async function generateEndpoints(options: GenerationOptions & { outputFile: string }): Promise<void>;
export async function generateEndpoints(options: GenerationOptions & { outputFile?: never }): Promise<string>;
Comment on lines +11 to +12
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm also including this slightly off-topic change here. This allows the return type of the function (string or void) to be inferred based on whether outputFile is passed. Without this, I have to do await generateEndpoints(...) as string in my codegen setup.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting. So outputFilevoid, and no outputFilestring, correct? If that's expected behavior, the types should reflect it more cleanly. I'll open a small follow-up PR for this. Thanks for flagging!

export async function generateEndpoints(options: GenerationOptions): Promise<string | void> {
const schemaLocation = options.schemaFile;

Expand Down
9 changes: 9 additions & 0 deletions packages/rtk-query-codegen-openapi/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,16 +121,25 @@ export interface CommonOptions {
* @since 2.1.0
*/
useUnknown?: boolean;

/**
* @default false
* Will generate imports with file extension matching the expected compiled output of the api file
*/
esmExtensions?: boolean;

/**
* @default false
* Will generate regex constants for pattern keywords in the schema
*/
outputRegexConstants?: boolean;

/**
* @default false
* Will use the original operation IDs from the schema instead of converting them to camel case.
* May cause issues if you have duplicate/missing operation IDs
*/
exactOperationIds?: boolean;
}

export type TextMatcher = string | RegExp | (string | RegExp)[];
Expand Down
Loading