@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,627 @@
1
+ import camelCase from 'lodash.camelcase';
2
+ import path from 'node:path';
3
+ import ApiGenerator, {
4
+ getOperationName as _getOperationName,
5
+ getReferenceName,
6
+ isReference,
7
+ supportDeepObjects,
8
+ createPropertyAssignment,
9
+ createQuestionToken,
10
+ isValidIdentifier,
11
+ keywordType,
12
+ } from 'oazapfts/generate';
13
+ import type { OpenAPIV3 } from 'openapi-types';
14
+ import ts from 'typescript';
15
+ import type { ObjectPropertyDefinitions } from './codegen';
16
+ import { generateCreateApiCall, generateEndpointDefinition, generateImportNode, generateTagTypes } from './codegen';
17
+ import { generateReactHooks } from './generators/react-hooks';
18
+ import type {
19
+ EndpointMatcher,
20
+ EndpointOverrides,
21
+ GenerationOptions,
22
+ OperationDefinition,
23
+ ParameterDefinition,
24
+ ParameterMatcher,
25
+ TextMatcher,
26
+ } from './types';
27
+ import { capitalize, getOperationDefinitions, getV3Doc, removeUndefined, isQuery as testIsQuery } from './utils';
28
+ import { factory } from './utils/factory';
29
+
30
+ const generatedApiName = 'injectedRtkApi';
31
+ const v3DocCache: Record<string, OpenAPIV3.Document> = {};
32
+
33
+ // Add IUseFetcherArgs import
34
+ const useFetcherImport = generateImportNode('@acrool/react-fetcher', {
35
+ IUseFetcherArgs: 'IUseFetcherArgs',
36
+ });
37
+
38
+ function defaultIsDataResponse(
39
+ code: string,
40
+ includeDefault: boolean,
41
+ response: OpenAPIV3.ResponseObject,
42
+ allResponses: OpenAPIV3.ResponsesObject
43
+ ) {
44
+ if (includeDefault && code === 'default') {
45
+ return true;
46
+ }
47
+ const parsedCode = Number(code);
48
+ return !Number.isNaN(parsedCode) && parsedCode >= 200 && parsedCode < 300;
49
+ }
50
+
51
+ function getOperationName({ verb, path, operation }: Pick<OperationDefinition, 'verb' | 'path' | 'operation'>) {
52
+ return _getOperationName(verb, path, operation.operationId);
53
+ }
54
+
55
+ function getTags({ verb, pathItem }: Pick<OperationDefinition, 'verb' | 'pathItem'>): string[] {
56
+ const tags = verb && pathItem[verb]?.tags ? pathItem[verb].tags : [];
57
+ return tags.map((tag) => tag.toString());
58
+ }
59
+
60
+ function patternMatches(pattern?: TextMatcher) {
61
+ const filters = Array.isArray(pattern) ? pattern : [pattern];
62
+ return function matcher(operationName: string) {
63
+ if (!pattern) return true;
64
+ return filters.some((filter) =>
65
+ typeof filter === 'string' ? filter === operationName : filter?.test(operationName)
66
+ );
67
+ };
68
+ }
69
+
70
+ function operationMatches(pattern?: EndpointMatcher) {
71
+ const checkMatch = typeof pattern === 'function' ? pattern : patternMatches(pattern);
72
+ return function matcher(operationDefinition: OperationDefinition) {
73
+ if (!pattern) return true;
74
+ const operationName = getOperationName(operationDefinition);
75
+ return checkMatch(operationName, operationDefinition);
76
+ };
77
+ }
78
+
79
+ function argumentMatches(pattern?: ParameterMatcher) {
80
+ const checkMatch = typeof pattern === 'function' ? pattern : patternMatches(pattern);
81
+ return function matcher(argumentDefinition: ParameterDefinition) {
82
+ if (!pattern || argumentDefinition.in === 'path') return true;
83
+ const argumentName = argumentDefinition.name;
84
+ return checkMatch(argumentName, argumentDefinition);
85
+ };
86
+ }
87
+
88
+ function withQueryComment<T extends ts.Node>(node: T, def: QueryArgDefinition, hasTrailingNewLine: boolean): T {
89
+ const comment = def.origin === 'param' ? def.param.description : def.body.description;
90
+ if (comment) {
91
+ return ts.addSyntheticLeadingComment(
92
+ node,
93
+ ts.SyntaxKind.MultiLineCommentTrivia,
94
+ `* ${comment} `,
95
+ hasTrailingNewLine
96
+ );
97
+ }
98
+ return node;
99
+ }
100
+
101
+ export function getOverrides(
102
+ operation: OperationDefinition,
103
+ endpointOverrides?: EndpointOverrides[]
104
+ ): EndpointOverrides | undefined {
105
+ return endpointOverrides?.find((override) => operationMatches(override.pattern)(operation));
106
+ }
107
+
108
+ export async function generateApi(
109
+ spec: string,
110
+ {
111
+ apiFile,
112
+ apiImport = 'api',
113
+ exportName = 'enhancedApi',
114
+ argSuffix = 'ApiArg',
115
+ responseSuffix = 'ApiResponse',
116
+ operationNameSuffix = '',
117
+ hooks = false,
118
+ tag = false,
119
+ outputFile,
120
+ isDataResponse = defaultIsDataResponse,
121
+ filterEndpoints,
122
+ endpointOverrides,
123
+ unionUndefined,
124
+ encodePathParams = false,
125
+ encodeQueryParams = false,
126
+ flattenArg = false,
127
+ includeDefault = false,
128
+ useEnumType = false,
129
+ mergeReadWriteOnly = false,
130
+ httpResolverOptions,
131
+ }: GenerationOptions
132
+ ) {
133
+ const v3Doc = (v3DocCache[spec] ??= await getV3Doc(spec, httpResolverOptions));
134
+
135
+ const apiGen = new ApiGenerator(v3Doc, {
136
+ unionUndefined,
137
+ useEnumType,
138
+ mergeReadWriteOnly,
139
+ });
140
+
141
+ // temporary workaround for https://github.com/oazapfts/oazapfts/issues/491
142
+ if (apiGen.spec.components?.schemas) {
143
+ apiGen.preprocessComponents(apiGen.spec.components.schemas);
144
+ }
145
+
146
+ const operationDefinitions = getOperationDefinitions(v3Doc).filter(operationMatches(filterEndpoints));
147
+
148
+ const resultFile = ts.createSourceFile(
149
+ 'someFileName.ts',
150
+ '',
151
+ ts.ScriptTarget.Latest,
152
+ /*setParentNodes*/ false,
153
+ ts.ScriptKind.TS
154
+ );
155
+ const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });
156
+
157
+ const interfaces: Record<string, ts.InterfaceDeclaration | ts.TypeAliasDeclaration> = {};
158
+ function registerInterface(declaration: ts.InterfaceDeclaration | ts.TypeAliasDeclaration) {
159
+ const name = declaration.name.escapedText.toString();
160
+ if (name in interfaces) {
161
+ throw new Error(`interface/type alias ${name} already registered`);
162
+ }
163
+ interfaces[name] = declaration;
164
+ return declaration;
165
+ }
166
+
167
+ if (outputFile) {
168
+ outputFile = path.resolve(process.cwd(), outputFile);
169
+ if (apiFile.startsWith('.')) {
170
+ apiFile = path.relative(path.dirname(outputFile), apiFile);
171
+ apiFile = apiFile.replace(/\\/g, '/');
172
+ if (!apiFile.startsWith('.')) apiFile = `./${apiFile}`;
173
+ }
174
+ }
175
+ apiFile = apiFile.replace(/\.[jt]sx?$/, '');
176
+
177
+ return printer.printNode(
178
+ ts.EmitHint.Unspecified,
179
+ factory.createSourceFile(
180
+ [
181
+ generateImportNode(apiFile, { [apiImport]: 'api' }),
182
+ ...(tag ? [generateTagTypes({ addTagTypes: extractAllTagTypes({ operationDefinitions }) })] : []),
183
+ generateCreateApiCall({
184
+ tag,
185
+ endpointDefinitions: factory.createObjectLiteralExpression(
186
+ operationDefinitions.map((operationDefinition) =>
187
+ generateEndpoint({
188
+ operationDefinition,
189
+ overrides: getOverrides(operationDefinition, endpointOverrides),
190
+ })
191
+ ),
192
+ true
193
+ ),
194
+ }),
195
+ factory.createExportDeclaration(
196
+ undefined,
197
+ false,
198
+ factory.createNamedExports([
199
+ factory.createExportSpecifier(
200
+ factory.createIdentifier(generatedApiName),
201
+ factory.createIdentifier(exportName)
202
+ ),
203
+ ]),
204
+ undefined
205
+ ),
206
+ ...Object.values(interfaces),
207
+ ...apiGen.aliases,
208
+ ...apiGen.enumAliases,
209
+ ...(hooks
210
+ ? [
211
+ generateReactHooks({
212
+ exportName: generatedApiName,
213
+ operationDefinitions,
214
+ endpointOverrides,
215
+ config: hooks,
216
+ }),
217
+ ]
218
+ : []),
219
+ ],
220
+ factory.createToken(ts.SyntaxKind.EndOfFileToken),
221
+ ts.NodeFlags.None
222
+ ),
223
+ resultFile
224
+ );
225
+
226
+ function extractAllTagTypes({ operationDefinitions }: { operationDefinitions: OperationDefinition[] }) {
227
+ const allTagTypes = new Set<string>();
228
+
229
+ for (const operationDefinition of operationDefinitions) {
230
+ const { verb, pathItem } = operationDefinition;
231
+ for (const tag of getTags({ verb, pathItem })) {
232
+ allTagTypes.add(tag);
233
+ }
234
+ }
235
+ return [...allTagTypes];
236
+ }
237
+
238
+ function generateQueryArgType(operationDefinition: OperationDefinition): ts.TypeNode {
239
+ const {
240
+ operation: { parameters = [], requestBody },
241
+ } = operationDefinition;
242
+
243
+ const properties: ts.PropertySignature[] = [];
244
+
245
+ // Add body property if needed
246
+ if (requestBody && !isReference(requestBody)) {
247
+ const bodySchema = requestBody.content?.['application/json']?.schema;
248
+ if (bodySchema) {
249
+ properties.push(
250
+ factory.createPropertySignature(
251
+ undefined,
252
+ factory.createIdentifier('body'),
253
+ undefined,
254
+ factory.createTypeReferenceNode(factory.createIdentifier('any'), undefined)
255
+ )
256
+ );
257
+ }
258
+ }
259
+
260
+ // Add params property if needed
261
+ const queryParams = parameters.filter((p): p is OpenAPIV3.ParameterObject => !isReference(p) && p.in === 'query');
262
+ if (queryParams.length > 0) {
263
+ properties.push(
264
+ factory.createPropertySignature(
265
+ undefined,
266
+ factory.createIdentifier('params'),
267
+ undefined,
268
+ factory.createTypeLiteralNode(
269
+ queryParams.map((param) =>
270
+ factory.createPropertySignature(
271
+ undefined,
272
+ factory.createIdentifier(param.name),
273
+ factory.createToken(ts.SyntaxKind.QuestionToken),
274
+ factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword)
275
+ )
276
+ )
277
+ )
278
+ )
279
+ );
280
+ }
281
+
282
+ // Add headers property if needed
283
+ const headerParams = parameters.filter((p): p is OpenAPIV3.ParameterObject => !isReference(p) && p.in === 'header');
284
+ if (headerParams.length > 0) {
285
+ properties.push(
286
+ factory.createPropertySignature(
287
+ undefined,
288
+ factory.createIdentifier('headers'),
289
+ undefined,
290
+ factory.createTypeLiteralNode(
291
+ headerParams.map((param) =>
292
+ factory.createPropertySignature(
293
+ undefined,
294
+ factory.createIdentifier(param.name),
295
+ factory.createToken(ts.SyntaxKind.QuestionToken),
296
+ factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword)
297
+ )
298
+ )
299
+ )
300
+ )
301
+ );
302
+ }
303
+
304
+ return factory.createTypeLiteralNode([
305
+ factory.createPropertySignature(
306
+ undefined,
307
+ factory.createIdentifier('variables'),
308
+ undefined,
309
+ factory.createTypeLiteralNode(properties)
310
+ ),
311
+ factory.createPropertySignature(
312
+ undefined,
313
+ factory.createIdentifier('fetchOptions'),
314
+ factory.createToken(ts.SyntaxKind.QuestionToken),
315
+ factory.createTypeReferenceNode(factory.createIdentifier('any'), undefined)
316
+ ),
317
+ ]);
318
+ }
319
+
320
+ function generateQueryArgDefinitions(operationDefinition: OperationDefinition): QueryArgDefinitions {
321
+ const {
322
+ operation: { parameters = [], requestBody },
323
+ } = operationDefinition;
324
+
325
+ const queryArg: QueryArgDefinitions = {};
326
+
327
+ // Add body definition if needed
328
+ if (requestBody && !isReference(requestBody)) {
329
+ const bodySchema = requestBody.content?.['application/json']?.schema;
330
+ if (bodySchema) {
331
+ queryArg['body'] = {
332
+ name: 'body',
333
+ originalName: 'body',
334
+ type: factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword),
335
+ required: true,
336
+ origin: 'body',
337
+ body: requestBody,
338
+ };
339
+ }
340
+ }
341
+
342
+ // Add parameter definitions
343
+ parameters
344
+ .filter((p): p is OpenAPIV3.ParameterObject => !isReference(p))
345
+ .forEach((param) => {
346
+ queryArg[param.name] = {
347
+ name: param.name,
348
+ originalName: param.name,
349
+ type: factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword),
350
+ required: param.required || false,
351
+ origin: 'param',
352
+ param,
353
+ };
354
+ });
355
+
356
+ return queryArg;
357
+ }
358
+
359
+ function generateEndpoint({
360
+ operationDefinition,
361
+ overrides,
362
+ }: {
363
+ operationDefinition: OperationDefinition;
364
+ overrides?: EndpointOverrides;
365
+ }) {
366
+ const {
367
+ verb,
368
+ path,
369
+ pathItem,
370
+ operation,
371
+ operation: { responses, requestBody },
372
+ } = operationDefinition;
373
+ const operationName = getOperationName({ verb, path, operation });
374
+ const tags = tag ? getTags({ verb, pathItem }) : [];
375
+ const isQuery = testIsQuery(verb, overrides);
376
+
377
+ const returnsJson = apiGen.getResponseType(responses) === 'json';
378
+ let ResponseType: ts.TypeNode = factory.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword);
379
+ if (returnsJson) {
380
+ const returnTypes = Object.entries(responses || {})
381
+ .map(([code, response]) =>
382
+ isDataResponse(code, includeDefault, response as OpenAPIV3.ResponseObject, responses as OpenAPIV3.ResponsesObject) && response
383
+ ? apiGen.getTypeFromResponse(response as OpenAPIV3.ResponseObject)
384
+ : undefined
385
+ )
386
+ .filter(removeUndefined);
387
+
388
+ if (returnTypes.length === 1) {
389
+ ResponseType = returnTypes[0];
390
+ } else if (returnTypes.length > 1) {
391
+ ResponseType = factory.createUnionTypeNode(returnTypes);
392
+ }
393
+ }
394
+
395
+ const QueryArg = generateQueryArgType(operationDefinition);
396
+ const wrappedQueryArg = factory.createTypeReferenceNode(factory.createIdentifier('IUseFetcherArgs'), [QueryArg]);
397
+
398
+ const endpointBuilder = factory.createIdentifier('build');
399
+
400
+ const Response = factory.createTypeReferenceNode(
401
+ factory.createIdentifier(`${capitalize(operationName)}${responseSuffix}`),
402
+ undefined
403
+ );
404
+
405
+ const queryArgDefinitions = generateQueryArgDefinitions(operationDefinition);
406
+
407
+ const extraEndpointsProps = isQuery
408
+ ? generateQueryEndpointProps({ operationDefinition })
409
+ : generateMutationEndpointProps({ operationDefinition });
410
+
411
+ return generateEndpointDefinition({
412
+ operationName,
413
+ type: isQuery ? 'query' : 'mutation',
414
+ Response,
415
+ QueryArg: wrappedQueryArg,
416
+ queryFn: generateQueryFn({
417
+ operationDefinition,
418
+ queryArg: queryArgDefinitions,
419
+ isFlatArg: flattenArg,
420
+ isQuery,
421
+ encodePathParams,
422
+ encodeQueryParams,
423
+ }),
424
+ extraEndpointsProps,
425
+ tags,
426
+ endpointBuilder,
427
+ });
428
+ }
429
+
430
+ function generateQueryFn({
431
+ operationDefinition,
432
+ queryArg,
433
+ isFlatArg,
434
+ isQuery,
435
+ encodePathParams,
436
+ encodeQueryParams,
437
+ }: {
438
+ operationDefinition: OperationDefinition;
439
+ queryArg: QueryArgDefinitions;
440
+ isFlatArg: boolean;
441
+ isQuery: boolean;
442
+ encodePathParams: boolean;
443
+ encodeQueryParams: boolean;
444
+ }) {
445
+ const {
446
+ verb,
447
+ path,
448
+ operation: { parameters = [], requestBody },
449
+ } = operationDefinition;
450
+
451
+ const bodyParameter =
452
+ requestBody && !isReference(requestBody) ? requestBody.content?.['application/json']?.schema : undefined;
453
+ const bodyArg = bodyParameter ? queryArg['body'] : undefined;
454
+
455
+ const pathParameters = parameters
456
+ .filter((p): p is OpenAPIV3.ParameterObject => !isReference(p) && p.in === 'path')
457
+ .map((param) => ({
458
+ name: param.name,
459
+ originalName: param.name,
460
+ type: factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword),
461
+ required: param.required,
462
+ param,
463
+ origin: 'param' as const,
464
+ }));
465
+
466
+ const queryParameters = parameters
467
+ .filter((p): p is OpenAPIV3.ParameterObject => !isReference(p) && p.in === 'query')
468
+ .map((param) => ({
469
+ name: param.name,
470
+ originalName: param.name,
471
+ type: factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword),
472
+ required: param.required,
473
+ param,
474
+ origin: 'param' as const,
475
+ }));
476
+
477
+ const headerParameters = parameters
478
+ .filter((p): p is OpenAPIV3.ParameterObject => !isReference(p) && p.in === 'header')
479
+ .map((param) => ({
480
+ name: param.name,
481
+ originalName: param.name,
482
+ type: factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword),
483
+ required: param.required,
484
+ param,
485
+ origin: 'param' as const,
486
+ }));
487
+
488
+ const rootObject = factory.createIdentifier('queryArg');
489
+
490
+ const objectProperties: ts.ObjectLiteralElementLike[] = [
491
+ factory.createPropertyAssignment(
492
+ 'url',
493
+ generatePathExpression(path, pathParameters, rootObject, isFlatArg, encodePathParams)
494
+ ),
495
+ factory.createPropertyAssignment('method', factory.createStringLiteral(verb.toUpperCase())),
496
+ ];
497
+
498
+ if (bodyArg) {
499
+ objectProperties.push(
500
+ factory.createPropertyAssignment(
501
+ 'body',
502
+ factory.createPropertyAccessExpression(
503
+ factory.createPropertyAccessExpression(rootObject, factory.createIdentifier('variables')),
504
+ factory.createIdentifier('body')
505
+ )
506
+ )
507
+ );
508
+ }
509
+
510
+ if (queryParameters.length) {
511
+ objectProperties.push(
512
+ factory.createPropertyAssignment(
513
+ 'params',
514
+ factory.createPropertyAccessExpression(
515
+ factory.createPropertyAccessExpression(rootObject, factory.createIdentifier('variables')),
516
+ factory.createIdentifier('params')
517
+ )
518
+ )
519
+ );
520
+ }
521
+
522
+ if (headerParameters.length) {
523
+ objectProperties.push(
524
+ factory.createPropertyAssignment(
525
+ 'headers',
526
+ factory.createPropertyAccessExpression(
527
+ factory.createPropertyAccessExpression(rootObject, factory.createIdentifier('variables')),
528
+ factory.createIdentifier('headers')
529
+ )
530
+ )
531
+ );
532
+ }
533
+
534
+ // Add fetchOptions
535
+ objectProperties.push(
536
+ factory.createPropertyAssignment(
537
+ 'fetchOptions',
538
+ factory.createPropertyAccessExpression(rootObject, factory.createIdentifier('fetchOptions'))
539
+ )
540
+ );
541
+
542
+ return factory.createArrowFunction(
543
+ undefined,
544
+ undefined,
545
+ [factory.createParameterDeclaration(undefined, undefined, rootObject)],
546
+ undefined,
547
+ factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken),
548
+ factory.createParenthesizedExpression(factory.createObjectLiteralExpression(objectProperties, true))
549
+ );
550
+ }
551
+
552
+ // eslint-disable-next-line no-empty-pattern
553
+ function generateQueryEndpointProps({}: { operationDefinition: OperationDefinition }): ObjectPropertyDefinitions {
554
+ return {}; /* TODO needs implementation - skip for now */
555
+ }
556
+
557
+ // eslint-disable-next-line no-empty-pattern
558
+ function generateMutationEndpointProps({}: { operationDefinition: OperationDefinition }): ObjectPropertyDefinitions {
559
+ return {}; /* TODO needs implementation - skip for now */
560
+ }
561
+ }
562
+
563
+ function accessProperty(rootObject: ts.Identifier, propertyName: string) {
564
+ return isValidIdentifier(propertyName)
565
+ ? factory.createPropertyAccessExpression(rootObject, factory.createIdentifier(propertyName))
566
+ : factory.createElementAccessExpression(rootObject, factory.createStringLiteral(propertyName));
567
+ }
568
+
569
+ function generatePathExpression(
570
+ path: string,
571
+ pathParameters: QueryArgDefinition[],
572
+ rootObject: ts.Identifier,
573
+ isFlatArg: boolean,
574
+ encodePathParams: boolean
575
+ ): ts.Expression {
576
+ const expressions: Array<[string, string]> = [];
577
+
578
+ const head = path.replace(/\{(.*?)}(.*?)(?=\{|$)/g, (_, expression, literal) => {
579
+ const param = pathParameters.find((p) => p.originalName === expression);
580
+ if (!param) {
581
+ throw new Error(`path parameter ${expression} does not seem to be defined in '${path}'!`);
582
+ }
583
+ expressions.push([param.name, literal]);
584
+ return '';
585
+ });
586
+
587
+ return expressions.length
588
+ ? factory.createTemplateExpression(
589
+ factory.createTemplateHead(head, head),
590
+ expressions.map(([prop, literal], index) => {
591
+ const value = factory.createPropertyAccessExpression(
592
+ factory.createPropertyAccessExpression(rootObject, factory.createIdentifier('variables')),
593
+ factory.createIdentifier(prop)
594
+ );
595
+ const encodedValue = encodePathParams
596
+ ? factory.createCallExpression(factory.createIdentifier('encodeURIComponent'), undefined, [
597
+ factory.createCallExpression(factory.createIdentifier('String'), undefined, [value]),
598
+ ])
599
+ : value;
600
+ return factory.createTemplateSpan(
601
+ encodedValue,
602
+ index === expressions.length - 1
603
+ ? factory.createTemplateTail(literal, literal)
604
+ : factory.createTemplateMiddle(literal, literal)
605
+ );
606
+ })
607
+ )
608
+ : factory.createStringLiteral(head);
609
+ }
610
+
611
+ type QueryArgDefinition = {
612
+ name: string;
613
+ originalName: string;
614
+ type: ts.TypeNode;
615
+ required?: boolean;
616
+ param?: OpenAPIV3.ParameterObject;
617
+ } & (
618
+ | {
619
+ origin: 'param';
620
+ param: OpenAPIV3.ParameterObject;
621
+ }
622
+ | {
623
+ origin: 'body';
624
+ body: OpenAPIV3.RequestBodyObject;
625
+ }
626
+ );
627
+ type QueryArgDefinitions = Record<string, QueryArgDefinition>;