@acrool/rtk-query-codegen-openapi 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,93 @@
1
+ import ts from 'typescript';
2
+ import { getOperationName } from 'oazapfts/generate';
3
+ import { capitalize, isQuery } from '../utils';
4
+ import type { OperationDefinition, EndpointOverrides, ConfigFile } from '../types';
5
+ import { getOverrides } from '../generate';
6
+ import { factory } from '../utils/factory';
7
+
8
+ type HooksConfigOptions = NonNullable<ConfigFile['hooks']>;
9
+
10
+ type GetReactHookNameParams = {
11
+ operationDefinition: OperationDefinition;
12
+ endpointOverrides: EndpointOverrides[] | undefined;
13
+ config: HooksConfigOptions;
14
+ };
15
+
16
+ type CreateBindingParams = {
17
+ operationDefinition: OperationDefinition;
18
+ overrides?: EndpointOverrides;
19
+ isLazy?: boolean;
20
+ };
21
+
22
+ const createBinding = ({
23
+ operationDefinition: { verb, path, operation },
24
+ overrides,
25
+ isLazy = false,
26
+ }: CreateBindingParams) =>
27
+ factory.createBindingElement(
28
+ undefined,
29
+ undefined,
30
+ factory.createIdentifier(
31
+ `use${isLazy ? 'Lazy' : ''}${capitalize(getOperationName(verb, path, operation.operationId))}${
32
+ isQuery(verb, overrides) ? 'Query' : 'Mutation'
33
+ }`
34
+ ),
35
+ undefined
36
+ );
37
+
38
+ const getReactHookName = ({ operationDefinition, endpointOverrides, config }: GetReactHookNameParams) => {
39
+ const overrides = getOverrides(operationDefinition, endpointOverrides);
40
+
41
+ const baseParams = {
42
+ operationDefinition,
43
+ overrides,
44
+ };
45
+
46
+ const _isQuery = isQuery(operationDefinition.verb, overrides);
47
+
48
+ // If `config` is true, just generate everything
49
+ if (typeof config === 'boolean') {
50
+ return createBinding(baseParams);
51
+ }
52
+
53
+ // `config` is an object and we need to check for the configuration of each property
54
+ if (_isQuery) {
55
+ return [
56
+ ...(config.queries ? [createBinding(baseParams)] : []),
57
+ ...(config.lazyQueries ? [createBinding({ ...baseParams, isLazy: true })] : []),
58
+ ];
59
+ }
60
+
61
+ return config.mutations ? createBinding(baseParams) : [];
62
+ };
63
+
64
+ type GenerateReactHooksParams = {
65
+ exportName: string;
66
+ operationDefinitions: OperationDefinition[];
67
+ endpointOverrides: EndpointOverrides[] | undefined;
68
+ config: HooksConfigOptions;
69
+ };
70
+ export const generateReactHooks = ({
71
+ exportName,
72
+ operationDefinitions,
73
+ endpointOverrides,
74
+ config,
75
+ }: GenerateReactHooksParams) =>
76
+ factory.createVariableStatement(
77
+ [factory.createModifier(ts.SyntaxKind.ExportKeyword)],
78
+ factory.createVariableDeclarationList(
79
+ [
80
+ factory.createVariableDeclaration(
81
+ factory.createObjectBindingPattern(
82
+ operationDefinitions
83
+ .map((operationDefinition) => getReactHookName({ operationDefinition, endpointOverrides, config }))
84
+ .flat()
85
+ ),
86
+ undefined,
87
+ undefined,
88
+ factory.createIdentifier(exportName)
89
+ ),
90
+ ],
91
+ ts.NodeFlags.Const
92
+ )
93
+ );
package/src/index.ts ADDED
@@ -0,0 +1,68 @@
1
+ import fs from 'node:fs';
2
+ import { createRequire } from 'node:module';
3
+ import path from 'node:path';
4
+ import { generateApi } from './generate';
5
+ import type { CommonOptions, ConfigFile, GenerationOptions, OutputFileOptions } from './types';
6
+ import { isValidUrl, prettify } from './utils';
7
+ export type { ConfigFile } from './types';
8
+
9
+ const require = createRequire(__filename);
10
+
11
+ export async function generateEndpoints(options: GenerationOptions): Promise<string | void> {
12
+ const schemaLocation = options.schemaFile;
13
+
14
+ const schemaAbsPath = isValidUrl(options.schemaFile)
15
+ ? options.schemaFile
16
+ : path.resolve(process.cwd(), schemaLocation);
17
+
18
+ const sourceCode = await enforceOazapftsTsVersion(async () => {
19
+ return generateApi(schemaAbsPath, options);
20
+ });
21
+ const { outputFile, prettierConfigFile } = options;
22
+ if (outputFile) {
23
+ fs.writeFileSync(
24
+ path.resolve(process.cwd(), outputFile),
25
+ await prettify(outputFile, sourceCode, prettierConfigFile)
26
+ );
27
+ } else {
28
+ return await prettify(null, sourceCode, prettierConfigFile);
29
+ }
30
+ }
31
+
32
+ export function parseConfig(fullConfig: ConfigFile) {
33
+ const outFiles: (CommonOptions & OutputFileOptions)[] = [];
34
+
35
+ if ('outputFiles' in fullConfig) {
36
+ const { outputFiles, ...commonConfig } = fullConfig;
37
+ for (const [outputFile, specificConfig] of Object.entries(outputFiles)) {
38
+ outFiles.push({
39
+ ...commonConfig,
40
+ ...specificConfig,
41
+ outputFile,
42
+ });
43
+ }
44
+ } else {
45
+ outFiles.push(fullConfig);
46
+ }
47
+ return outFiles;
48
+ }
49
+
50
+ /**
51
+ * Enforces `oazapfts` to use the same TypeScript version as this module itself uses.
52
+ * That should prevent enums from running out of sync if both libraries use different TS versions.
53
+ */
54
+ function enforceOazapftsTsVersion<T>(cb: () => T): T {
55
+ const ozTsPath = require.resolve('typescript', { paths: [require.resolve('oazapfts')] });
56
+ const tsPath = require.resolve('typescript');
57
+ const originalEntry = require.cache[ozTsPath];
58
+ try {
59
+ require.cache[ozTsPath] = require.cache[tsPath];
60
+ return cb();
61
+ } finally {
62
+ if (originalEntry) {
63
+ require.cache[ozTsPath] = originalEntry;
64
+ } else {
65
+ delete require.cache[ozTsPath];
66
+ }
67
+ }
68
+ }
package/src/types.ts ADDED
@@ -0,0 +1,150 @@
1
+ import type SwaggerParser from '@apidevtools/swagger-parser';
2
+ import type { OpenAPIV3 } from 'openapi-types';
3
+
4
+ export type OperationDefinition = {
5
+ path: string;
6
+ verb: (typeof operationKeys)[number];
7
+ pathItem: OpenAPIV3.PathItemObject;
8
+ operation: OpenAPIV3.OperationObject;
9
+ };
10
+
11
+ export type ParameterDefinition = OpenAPIV3.ParameterObject;
12
+
13
+ type Require<T, K extends keyof T> = { [k in K]-?: NonNullable<T[k]> } & Omit<T, K>;
14
+ type Optional<T, K extends keyof T> = { [k in K]?: NonNullable<T[k]> } & Omit<T, K>;
15
+ type Id<T> = { [K in keyof T]: T[K] } & {};
16
+ type AtLeastOneKey<T> = {
17
+ [K in keyof T]-?: Pick<T, K> & Partial<T>;
18
+ }[keyof T];
19
+
20
+ export const operationKeys = ['get', 'put', 'post', 'delete', 'options', 'head', 'patch', 'trace'] as const;
21
+
22
+ export type GenerationOptions = Id<
23
+ CommonOptions &
24
+ Optional<OutputFileOptions, 'outputFile'> & {
25
+ isDataResponse?(
26
+ code: string,
27
+ includeDefault: boolean,
28
+ response: OpenAPIV3.ResponseObject,
29
+ allResponses: OpenAPIV3.ResponsesObject
30
+ ): boolean;
31
+ }
32
+ >;
33
+
34
+ export interface CommonOptions {
35
+ apiFile: string;
36
+ /**
37
+ * filename or url
38
+ */
39
+ schemaFile: string;
40
+ /**
41
+ * defaults to "api"
42
+ */
43
+ apiImport?: string;
44
+ /**
45
+ * defaults to "enhancedApi"
46
+ */
47
+ exportName?: string;
48
+ /**
49
+ * defaults to "ApiArg"
50
+ */
51
+ argSuffix?: string;
52
+ /**
53
+ * defaults to "ApiResponse"
54
+ */
55
+ responseSuffix?: string;
56
+ /**
57
+ * defaults to empty
58
+ */
59
+ operationNameSuffix?: string;
60
+ /**
61
+ * defaults to `false`
62
+ * `true` will generate hooks for queries and mutations, but no lazyQueries
63
+ */
64
+ hooks?: boolean | { queries: boolean; lazyQueries: boolean; mutations: boolean };
65
+ /**
66
+ * defaults to false
67
+ * `true` will generate a union type for `undefined` properties like: `{ id?: string | undefined }` instead of `{ id?: string }`
68
+ */
69
+ unionUndefined?: boolean;
70
+ /**
71
+ * defaults to false
72
+ * `true` will result in all generated endpoints having `providesTags`/`invalidatesTags` declarations for the `tags` of their respective operation definition
73
+ * @see https://redux-toolkit.js.org/rtk-query/usage/code-generation for more information
74
+ */
75
+ tag?: boolean;
76
+ /**
77
+ * defaults to false
78
+ * `true` will add `encodeURIComponent` to the generated path parameters
79
+ */
80
+ encodePathParams?: boolean;
81
+ /**
82
+ * defaults to false
83
+ * `true` will add `encodeURIComponent` to the generated query parameters
84
+ */
85
+ encodeQueryParams?: boolean;
86
+ /**
87
+ * defaults to false
88
+ * `true` will "flatten" the arg so that you can do things like `useGetEntityById(1)` instead of `useGetEntityById({ entityId: 1 })`
89
+ */
90
+ flattenArg?: boolean;
91
+ /**
92
+ * default to false
93
+ * If set to `true`, the default response type will be included in the generated code for all endpoints.
94
+ * @see https://swagger.io/docs/specification/describing-responses/#default
95
+ */
96
+ includeDefault?: boolean;
97
+ /**
98
+ * default to false
99
+ * `true` will not generate separate types for read-only and write-only properties.
100
+ */
101
+ mergeReadWriteOnly?: boolean;
102
+ /**
103
+ *
104
+ * HTTPResolverOptions object that is passed to the SwaggerParser bundle function.
105
+ */
106
+ httpResolverOptions?: SwaggerParser.HTTPResolverOptions;
107
+
108
+ /**
109
+ * defaults to undefined
110
+ * If present the given file will be used as prettier config when formatting the generated code. If undefined the default prettier config
111
+ * resolution mechanism will be used.
112
+ */
113
+ prettierConfigFile?: string;
114
+ }
115
+
116
+ export type TextMatcher = string | RegExp | (string | RegExp)[];
117
+
118
+ export type EndpointMatcherFunction = (operationName: string, operationDefinition: OperationDefinition) => boolean;
119
+
120
+ export type EndpointMatcher = TextMatcher | EndpointMatcherFunction;
121
+
122
+ export type ParameterMatcherFunction = (parameterName: string, parameterDefinition: ParameterDefinition) => boolean;
123
+
124
+ export type ParameterMatcher = TextMatcher | ParameterMatcherFunction;
125
+
126
+ export interface OutputFileOptions extends Partial<CommonOptions> {
127
+ outputFile: string;
128
+ filterEndpoints?: EndpointMatcher;
129
+ endpointOverrides?: EndpointOverrides[];
130
+ /**
131
+ * defaults to false
132
+ * If passed as true it will generate TS enums instead of union of strings
133
+ */
134
+ useEnumType?: boolean;
135
+ }
136
+
137
+ export type EndpointOverrides = {
138
+ pattern: EndpointMatcher;
139
+ } & AtLeastOneKey<{
140
+ type: 'mutation' | 'query';
141
+ parameterFilter: ParameterMatcher;
142
+ }>;
143
+
144
+ export type ConfigFile =
145
+ | Id<Require<CommonOptions & OutputFileOptions, 'outputFile'>>
146
+ | Id<
147
+ Omit<CommonOptions, 'outputFile'> & {
148
+ outputFiles: { [outputFile: string]: Omit<OutputFileOptions, 'outputFile'> };
149
+ }
150
+ >;
@@ -0,0 +1,3 @@
1
+ export function capitalize(str: string) {
2
+ return str.replace(str[0], str[0].toUpperCase());
3
+ }
@@ -0,0 +1,29 @@
1
+ import ts from 'typescript';
2
+ import semver from 'semver';
3
+
4
+ const originalFactory = ts.factory;
5
+
6
+ function createImportSpecifier(propertyName: ts.Identifier | undefined, name: ts.Identifier): ts.ImportSpecifier {
7
+ if (semver.satisfies(ts.version, '>= 4.5'))
8
+ // @ts-ignore
9
+ return originalFactory.createImportSpecifier(false, propertyName, name);
10
+ // @ts-ignore
11
+ return originalFactory.createImportSpecifier(propertyName, name);
12
+ }
13
+
14
+ function createExportSpecifier(
15
+ propertyName: string | ts.Identifier | undefined,
16
+ name: string | ts.Identifier
17
+ ): ts.ExportSpecifier {
18
+ if (semver.satisfies(ts.version, '>= 4.5'))
19
+ // @ts-ignore
20
+ return originalFactory.createExportSpecifier(false, propertyName, name);
21
+ // @ts-ignore
22
+ return originalFactory.createExportSpecifier(propertyName, name);
23
+ }
24
+
25
+ export const factory = {
26
+ ...originalFactory,
27
+ createImportSpecifier,
28
+ createExportSpecifier,
29
+ };
@@ -0,0 +1,20 @@
1
+ import type { OpenAPIV3 } from 'openapi-types';
2
+ import type { OperationDefinition } from '../types';
3
+ import { operationKeys } from '../types';
4
+
5
+ export function getOperationDefinitions(v3Doc: OpenAPIV3.Document): OperationDefinition[] {
6
+ return Object.entries(v3Doc.paths).flatMap(([path, pathItem]) =>
7
+ !pathItem
8
+ ? []
9
+ : Object.entries(pathItem)
10
+ .filter((arg): arg is [(typeof operationKeys)[number], OpenAPIV3.OperationObject] =>
11
+ operationKeys.includes(arg[0] as any)
12
+ )
13
+ .map(([verb, operation]) => ({
14
+ path,
15
+ verb,
16
+ pathItem,
17
+ operation,
18
+ }))
19
+ );
20
+ }
@@ -0,0 +1,24 @@
1
+ import SwaggerParser from '@apidevtools/swagger-parser';
2
+ import type { OpenAPIV3 } from 'openapi-types';
3
+ // @ts-ignore
4
+ import converter from 'swagger2openapi';
5
+
6
+ export async function getV3Doc(
7
+ spec: string,
8
+ httpResolverOptions?: SwaggerParser.HTTPResolverOptions
9
+ ): Promise<OpenAPIV3.Document> {
10
+ const doc = await SwaggerParser.bundle(spec, {
11
+ resolve: {
12
+ http: httpResolverOptions,
13
+ },
14
+ });
15
+
16
+ const isOpenApiV3 = 'openapi' in doc && doc.openapi.startsWith('3');
17
+
18
+ if (isOpenApiV3) {
19
+ return doc as OpenAPIV3.Document;
20
+ } else {
21
+ const result = await converter.convertObj(doc, {});
22
+ return result.openapi as OpenAPIV3.Document;
23
+ }
24
+ }
@@ -0,0 +1,8 @@
1
+ export * from './capitalize';
2
+ export * from './getOperationDefinitions';
3
+ export * from './getV3Doc';
4
+ export * from './isQuery';
5
+ export * from './isValidUrl';
6
+ export * from './prettier';
7
+ export * from './messages';
8
+ export * from './removeUndefined';
@@ -0,0 +1,8 @@
1
+ import type { EndpointOverrides, operationKeys } from '../types';
2
+
3
+ export function isQuery(verb: (typeof operationKeys)[number], overrides: EndpointOverrides | undefined) {
4
+ if (overrides?.type) {
5
+ return overrides.type === 'query';
6
+ }
7
+ return verb === 'get';
8
+ }
@@ -0,0 +1,9 @@
1
+ export function isValidUrl(string: string) {
2
+ try {
3
+ new URL(string);
4
+ } catch (_) {
5
+ return false;
6
+ }
7
+
8
+ return true;
9
+ }
@@ -0,0 +1,7 @@
1
+ export const MESSAGES = {
2
+ NAMED_EXPORT_MISSING: `You specified a named export that does not exist or was empty.`,
3
+ DEFAULT_EXPORT_MISSING: `Specified file exists, but no default export was found for the --baseQuery`,
4
+ FILE_NOT_FOUND: `Unable to locate the specified file provided to --baseQuery`,
5
+ TSCONFIG_FILE_NOT_FOUND: `Unable to locate the specified file provided to -c, --config`,
6
+ BASE_URL_IGNORED: `The url provided to --baseUrl is ignored when using --baseQuery`,
7
+ };
@@ -0,0 +1,46 @@
1
+ import path from 'node:path';
2
+ import prettier from 'prettier';
3
+ import type { BuiltInParserName } from 'prettier';
4
+
5
+ const EXTENSION_TO_PARSER: Record<string, BuiltInParserName> = {
6
+ ts: 'typescript',
7
+ tsx: 'typescript',
8
+ js: 'babel',
9
+ jsx: 'babel',
10
+ 'js.flow': 'flow',
11
+ flow: 'flow',
12
+ gql: 'graphql',
13
+ graphql: 'graphql',
14
+ css: 'scss',
15
+ scss: 'scss',
16
+ less: 'scss',
17
+ stylus: 'scss',
18
+ markdown: 'markdown',
19
+ md: 'markdown',
20
+ json: 'json',
21
+ };
22
+
23
+ export async function prettify(filePath: string | null, content: string, prettierConfigFile?: string): Promise<string> {
24
+ let config = null;
25
+ let parser = 'typescript';
26
+
27
+ if (filePath) {
28
+ const fileExtension = path.extname(filePath).slice(1);
29
+ parser = EXTENSION_TO_PARSER[fileExtension];
30
+ config = await prettier.resolveConfig(process.cwd(), {
31
+ useCache: true,
32
+ editorconfig: !prettierConfigFile,
33
+ config: prettierConfigFile,
34
+ });
35
+ } else if (prettierConfigFile) {
36
+ config = await prettier.resolveConfig(process.cwd(), {
37
+ useCache: true,
38
+ config: prettierConfigFile,
39
+ });
40
+ }
41
+
42
+ return prettier.format(content, {
43
+ parser,
44
+ ...config,
45
+ });
46
+ }
@@ -0,0 +1,3 @@
1
+ export function removeUndefined<T>(t: T | undefined): t is T {
2
+ return typeof t !== 'undefined';
3
+ }