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
42 changes: 42 additions & 0 deletions docs/rtk-query/usage/code-generation.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ interface SimpleUsage {
endpointOverrides?: EndpointOverrides[]
flattenArg?: boolean
useEnumType?: boolean
outputRegexConstants?: boolean
httpResolverOptions?: SwaggerParser.HTTPResolverOptions
}

Expand Down Expand Up @@ -192,6 +193,47 @@ const withOverride: ConfigFile = {

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 }`.

#### Generating regex constants for schema patterns

If your OpenAPI schema uses the [`pattern` keyword](https://swagger.io/docs/specification/data-models/data-types/#pattern) to specify regex validation on string properties, you can export these patterns as JavaScript regex constants by setting `outputRegexConstants: true`.

```ts no-transpile title="openapi-config.ts"
const config: ConfigFile = {
schemaFile: 'https://petstore3.swagger.io/api/v3/openapi.json',
apiFile: './src/store/emptyApi.ts',
outputFile: './src/store/petApi.ts',
outputRegexConstants: true,
}
```

For a schema with pattern-validated properties like:

```yaml
User:
type: object
properties:
email:
type: string
pattern: '^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
phone:
type: string
pattern: '^\+?[1-9]\d{1,14}$'
```

The codegen will generate:

```ts no-transpile title="Generated output"
export const userEmailPattern =
/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/
export const userPhonePattern = /^\+?[1-9]\d{1,14}$/
```

These constants can be used for client-side validation to ensure consistency with API expectations.

:::note
Only string-type properties with non-empty `pattern` values will generate constants. The constant name follows the format `{typeName}{propertyName}Pattern` in camelCase.
:::

#### Multiple output files

```ts no-transpile title="openapi-config.ts"
Expand Down
70 changes: 66 additions & 4 deletions packages/rtk-query-codegen-openapi/src/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@ import camelCase from 'lodash.camelcase';
import path from 'node:path';
import ApiGenerator, {
getOperationName as _getOperationName,
getReferenceName,
isReference,
supportDeepObjects,
createPropertyAssignment,
createQuestionToken,
getReferenceName,
isReference,
isValidIdentifier,
keywordType,
supportDeepObjects,
} from 'oazapfts/generate';
import type { OpenAPIV3 } from 'openapi-types';
import ts from 'typescript';
Expand Down Expand Up @@ -87,6 +87,56 @@ function withQueryComment<T extends ts.Node>(node: T, def: QueryArgDefinition, h
return node;
}

function getPatternFromProperty(
property: OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject,
apiGen: ApiGenerator
): string | null {
const resolved = apiGen.resolve(property);
if (!resolved || typeof resolved !== 'object' || !('pattern' in resolved)) return null;
if (resolved.type !== 'string') return null;
const pattern = resolved.pattern;
return typeof pattern === 'string' && pattern.length > 0 ? pattern : null;
}

function generateRegexConstantsForType(
typeName: string,
schema: OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject,
apiGen: ApiGenerator
): ts.VariableStatement[] {
const resolvedSchema = apiGen.resolve(schema);
if (!resolvedSchema || !('properties' in resolvedSchema) || !resolvedSchema.properties) return [];

const constants: ts.VariableStatement[] = [];

for (const [propertyName, property] of Object.entries(resolvedSchema.properties)) {
const pattern = getPatternFromProperty(property, apiGen);
if (!pattern) continue;

const constantName = camelCase(`${typeName} ${propertyName} Pattern`);
const escapedPattern = pattern.replaceAll('/', String.raw`\/`);
const regexLiteral = factory.createRegularExpressionLiteral(`/${escapedPattern}/`);

constants.push(
factory.createVariableStatement(
[factory.createModifier(ts.SyntaxKind.ExportKeyword)],
factory.createVariableDeclarationList(
[
factory.createVariableDeclaration(
factory.createIdentifier(constantName),
undefined,
undefined,
regexLiteral
),
],
ts.NodeFlags.Const
)
)
);
}

return constants;
}

export function getOverrides(
operation: OperationDefinition,
endpointOverrides?: EndpointOverrides[]
Expand Down Expand Up @@ -119,6 +169,7 @@ export async function generateApi(
httpResolverOptions,
useUnknown = false,
esmExtensions = false,
outputRegexConstants = false,
}: GenerationOptions
) {
const v3Doc = (v3DocCache[spec] ??= await getV3Doc(spec, httpResolverOptions));
Expand Down Expand Up @@ -206,7 +257,18 @@ export async function generateApi(
undefined
),
...Object.values(interfaces),
...apiGen.aliases,
...(outputRegexConstants
? apiGen.aliases.flatMap((alias) => {
if (!ts.isInterfaceDeclaration(alias) && !ts.isTypeAliasDeclaration(alias)) return [alias];

const typeName = alias.name.escapedText.toString();
const schema = v3Doc.components?.schemas?.[typeName];
if (!schema) return [alias];

const regexConstants = generateRegexConstantsForType(typeName, schema, apiGen);
return regexConstants.length > 0 ? [alias, ...regexConstants] : [alias];
})
: apiGen.aliases),
...apiGen.enumAliases,
...(hooks
? [
Expand Down
5 changes: 5 additions & 0 deletions packages/rtk-query-codegen-openapi/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,11 @@ export interface CommonOptions {
* 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;
}

export type TextMatcher = string | RegExp | (string | RegExp)[];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,7 @@ export type User = {
email?: string;
password?: string;
phone?: string;
website?: string;
/** User Status */
userStatus?: number;
};
Expand Down Expand Up @@ -488,6 +489,7 @@ export type User = {
email?: string;
password?: string;
phone?: string;
website?: string;
/** User Status */
userStatus?: number;
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,7 @@ export type User = {
email?: string;
password?: string;
phone?: string;
website?: string;
/** User Status */
userStatus?: number;
};
Expand Down Expand Up @@ -488,6 +489,7 @@ export type User = {
email?: string;
password?: string;
phone?: string;
website?: string;
/** User Status */
userStatus?: number;
};
Expand Down Expand Up @@ -791,6 +793,7 @@ export type User = {
email?: string | undefined;
password?: string | undefined;
phone?: string | undefined;
website?: string | undefined;
/** User Status */
userStatus?: number | undefined;
};
Expand Down Expand Up @@ -1196,6 +1199,7 @@ export type User = {
email?: string | undefined;
password?: string | undefined;
phone?: string | undefined;
website?: string | undefined;
/** User Status */
userStatus?: number | undefined;
};
Expand Down Expand Up @@ -1477,6 +1481,7 @@ export type User = {
email?: string | undefined;
password?: string | undefined;
phone?: string | undefined;
website?: string | undefined;
/** User Status */
userStatus?: number | undefined;
};
Expand Down Expand Up @@ -1776,6 +1781,7 @@ export type User = {
email?: string | undefined;
password?: string | undefined;
phone?: string | undefined;
website?: string | undefined;
/** User Status */
userStatus?: number | undefined;
};
Expand Down Expand Up @@ -2057,6 +2063,7 @@ export type User = {
email?: string | undefined;
password?: string | undefined;
phone?: string | undefined;
website?: string | undefined;
/** User Status */
userStatus?: number | undefined;
};
Expand Down Expand Up @@ -2329,6 +2336,7 @@ export type User = {
email?: string | undefined;
password?: string | undefined;
phone?: string | undefined;
website?: string | undefined;
/** User Status */
userStatus?: number | undefined;
};
Expand Down Expand Up @@ -2886,6 +2894,7 @@ export type User = {
email?: string | undefined;
password?: string | undefined;
phone?: string | undefined;
website?: string | undefined;
/** User Status */
userStatus?: number | undefined;
};
Expand Down Expand Up @@ -3513,6 +3522,7 @@ export type User = {
email?: string | undefined;
password?: string | undefined;
phone?: string | undefined;
website?: string | undefined;
/** User Status */
userStatus?: number | undefined;
};
Expand Down
14 changes: 12 additions & 2 deletions packages/rtk-query-codegen-openapi/test/fixtures/petstore.json
Original file line number Diff line number Diff line change
Expand Up @@ -1015,18 +1015,27 @@
},
"email": {
"type": "string",
"pattern": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$",
"example": "john@email.com"
},
"password": {
"type": "string",
"example": "12345"
"example": "12345",
"pattern": ""
},
"phone": {
"type": "string",
"pattern": "^\\+?[1-9]\\d{1,14}$",
"example": "12345"
},
"website": {
"type": "string",
"pattern": "^https?://[^\\s]+$",
"example": "https://example.com"
},
"userStatus": {
"type": "integer",
"pattern": "^[1-9]\\d{0,2}$",
"description": "User Status",
"format": "int32",
"example": 1
Expand All @@ -1044,7 +1053,8 @@
"format": "int64"
},
"name": {
"type": "string"
"type": "string",
"pattern": "^\\S+$"
}
},
"xml": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -695,15 +695,23 @@ components:
example: James
email:
type: string
pattern: '^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
example: john@email.com
password:
type: string
pattern: ''
example: '12345'
phone:
type: string
pattern: '^\+?[1-9]\d{1,14}$'
example: '12345'
website:
type: string
pattern: '^https?://[^\s]+$'
example: 'https://example.com'
userStatus:
type: integer
pattern: '^[1-9]\d{0,2}$'
description: User Status
format: int32
example: 1
Expand All @@ -717,6 +725,7 @@ components:
format: int64
name:
type: string
pattern: '^\S+$'
xml:
name: tag
Pet:
Expand Down
Loading