beer-swagger 1.1.1 → 4.0.2
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.
- package/generate-apis.js +231 -52
- package/package.json +1 -1
package/generate-apis.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import fs from 'fs';
|
|
4
4
|
import path from 'path';
|
|
5
|
-
import {
|
|
5
|
+
import {createInterface} from 'readline';
|
|
6
6
|
|
|
7
7
|
const ENV_PATH = path.join(process.cwd(), '.env');
|
|
8
8
|
const SKIPPED_SERVICES = new Set([
|
|
@@ -116,7 +116,7 @@ function appendParamSuffix(baseName, paramSuffix) {
|
|
|
116
116
|
}
|
|
117
117
|
|
|
118
118
|
// Add a verb when the HTTP method should be reflected in the name.
|
|
119
|
-
function addVerbForSingleSegment(name, method
|
|
119
|
+
function addVerbForSingleSegment(name, method) {
|
|
120
120
|
const lower = name.toLowerCase();
|
|
121
121
|
if (KEEP_METHOD_NAMES.has(lower)) {
|
|
122
122
|
return name;
|
|
@@ -125,7 +125,7 @@ function addVerbForSingleSegment(name, method, hasPathParams = false) {
|
|
|
125
125
|
const suffix = name[0].toUpperCase() + name.slice(1);
|
|
126
126
|
return `removeBy${suffix}`;
|
|
127
127
|
}
|
|
128
|
-
if (
|
|
128
|
+
if (method === 'post' || method === 'put') {
|
|
129
129
|
return name;
|
|
130
130
|
}
|
|
131
131
|
const verbMap = {
|
|
@@ -166,12 +166,12 @@ function getBodyTypeName(requestBody, schemas) {
|
|
|
166
166
|
if (!requestBody || !requestBody.content) {
|
|
167
167
|
return '';
|
|
168
168
|
}
|
|
169
|
-
const {
|
|
169
|
+
const {content} = requestBody;
|
|
170
170
|
const firstContent = content['application/json'] || content['application/*+json'] || Object.values(content)[0];
|
|
171
171
|
if (!firstContent || !firstContent.schema) {
|
|
172
172
|
return '';
|
|
173
173
|
}
|
|
174
|
-
const {
|
|
174
|
+
const {schema} = firstContent;
|
|
175
175
|
if (!schema.$ref) {
|
|
176
176
|
return '';
|
|
177
177
|
}
|
|
@@ -182,10 +182,53 @@ function getBodyTypeName(requestBody, schemas) {
|
|
|
182
182
|
return stripped || normalized;
|
|
183
183
|
}
|
|
184
184
|
|
|
185
|
+
// Return the path segment immediately above the operation segment.
|
|
186
|
+
function getParentPathSegment(pathStr) {
|
|
187
|
+
const segments = pathStr.split('/')
|
|
188
|
+
.filter(Boolean)
|
|
189
|
+
.filter((part) => !part.startsWith('{'));
|
|
190
|
+
return segments.length > 1 ? segments[segments.length - 2] : '';
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Compare two names using normalized Levenshtein similarity.
|
|
194
|
+
function nameSimilarity(left, right) {
|
|
195
|
+
const a = String(left || '').replace(/[^a-zA-Z0-9]/g, '').toLowerCase();
|
|
196
|
+
const b = String(right || '').replace(/[^a-zA-Z0-9]/g, '').toLowerCase();
|
|
197
|
+
if (!a || !b) {
|
|
198
|
+
return 0;
|
|
199
|
+
}
|
|
200
|
+
if (a === b) {
|
|
201
|
+
return 1;
|
|
202
|
+
}
|
|
203
|
+
const previous = Array.from({length: b.length + 1}, (_, index) => index);
|
|
204
|
+
for (let i = 1; i <= a.length; i += 1) {
|
|
205
|
+
let diagonal = previous[0];
|
|
206
|
+
previous[0] = i;
|
|
207
|
+
for (let j = 1; j <= b.length; j += 1) {
|
|
208
|
+
const above = previous[j];
|
|
209
|
+
previous[j] = a[i - 1] === b[j - 1]
|
|
210
|
+
? diagonal
|
|
211
|
+
: Math.min(previous[j] + 1, previous[j - 1] + 1, diagonal + 1);
|
|
212
|
+
diagonal = above;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return 1 - previous[b.length] / Math.max(a.length, b.length);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// A POST whose parent resource matches its defined request entity is a save.
|
|
219
|
+
function shouldSaveByPathEntity(pathStr, method, requestBody, schemas) {
|
|
220
|
+
if (method !== 'post') {
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
223
|
+
const parentPath = getParentPathSegment(pathStr);
|
|
224
|
+
const bodyTypeName = getBodyTypeName(requestBody, schemas);
|
|
225
|
+
return !!parentPath && !!bodyTypeName && nameSimilarity(parentPath, bodyTypeName) >= 0.8;
|
|
226
|
+
}
|
|
227
|
+
|
|
185
228
|
// Map a Swagger schema to a runtime return type.
|
|
186
229
|
function mapSchemaToType(schema, schemas, useTypes) {
|
|
187
230
|
if (!schema) {
|
|
188
|
-
return '
|
|
231
|
+
return 'object';
|
|
189
232
|
}
|
|
190
233
|
const {
|
|
191
234
|
$ref,
|
|
@@ -201,18 +244,18 @@ function mapSchemaToType(schema, schemas, useTypes) {
|
|
|
201
244
|
if (responseSchema && responseSchema.properties && responseSchema.properties.data) {
|
|
202
245
|
return mapSchemaToType(responseSchema.properties.data, schemas, useTypes);
|
|
203
246
|
}
|
|
204
|
-
return '
|
|
247
|
+
return 'object';
|
|
205
248
|
}
|
|
206
249
|
const refName = normalizeDtoName(rawRefName);
|
|
207
250
|
const refSchema = schemas[rawRefName] || schemas[refName];
|
|
208
251
|
if (refSchema && refSchema.properties && refSchema.properties.data) {
|
|
209
252
|
return mapSchemaToType(refSchema.properties.data, schemas, useTypes);
|
|
210
253
|
}
|
|
211
|
-
return useTypes ? `Types.${refName}` : '
|
|
254
|
+
return useTypes ? `Types.${refName}` : 'object';
|
|
212
255
|
}
|
|
213
256
|
if (!type && items) {
|
|
214
257
|
const itemType = mapSchemaToType(items, schemas, useTypes);
|
|
215
|
-
if (itemType === '
|
|
258
|
+
if (itemType === '[]') {
|
|
216
259
|
return '[]';
|
|
217
260
|
}
|
|
218
261
|
return `${itemType}[]`;
|
|
@@ -234,7 +277,7 @@ function mapSchemaToType(schema, schemas, useTypes) {
|
|
|
234
277
|
}
|
|
235
278
|
if (type === 'array') {
|
|
236
279
|
const itemType = mapSchemaToType(items, schemas, useTypes);
|
|
237
|
-
if (itemType === '
|
|
280
|
+
if (itemType === '[]') {
|
|
238
281
|
return '[]';
|
|
239
282
|
}
|
|
240
283
|
return `${itemType}[]`;
|
|
@@ -246,20 +289,20 @@ function mapSchemaToType(schema, schemas, useTypes) {
|
|
|
246
289
|
}
|
|
247
290
|
|
|
248
291
|
// Map parameter schema to a type for method signatures.
|
|
249
|
-
function mapParamSchemaToType(schema, schemas, useTypes) {
|
|
292
|
+
function mapParamSchemaToType(schema, schemas, useTypes, isQuery = false) {
|
|
250
293
|
if (!schema) {
|
|
251
|
-
return '
|
|
294
|
+
return isQuery ? 'Record<string, string | undefined>' : 'object';
|
|
252
295
|
}
|
|
253
296
|
const {
|
|
254
297
|
$ref,
|
|
255
298
|
type,
|
|
256
299
|
format
|
|
257
300
|
} = schema;
|
|
258
|
-
const {
|
|
301
|
+
const {items} = schema;
|
|
259
302
|
if ($ref) {
|
|
260
303
|
const refName = normalizeDtoName($ref.split('/')
|
|
261
304
|
.pop());
|
|
262
|
-
return useTypes ? `Types.${refName}` : '
|
|
305
|
+
return useTypes ? `Types.${refName}` : 'object';
|
|
263
306
|
}
|
|
264
307
|
if (!type && items) {
|
|
265
308
|
return '[]';
|
|
@@ -283,26 +326,50 @@ function mapParamSchemaToType(schema, schemas, useTypes) {
|
|
|
283
326
|
return '[]';
|
|
284
327
|
}
|
|
285
328
|
if (type === 'object') {
|
|
286
|
-
return 'object';
|
|
329
|
+
return isQuery ? 'Record<string, string | undefined>' : 'object';
|
|
287
330
|
}
|
|
288
|
-
return 'object';
|
|
331
|
+
return isQuery ? 'Record<string, string | undefined>' : 'object';
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Resolve parameter type for a parameter definition.
|
|
335
|
+
function resolveParamType(param, schemas) {
|
|
336
|
+
const schemaToUse = param.schema || (param.type ? {
|
|
337
|
+
type: param.type,
|
|
338
|
+
format: param.format,
|
|
339
|
+
items: param.items
|
|
340
|
+
} : null);
|
|
341
|
+
const isQuery = param.in === 'query';
|
|
342
|
+
return mapParamSchemaToType(schemaToUse, schemas, true, isQuery);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Detect event streams in successful or default responses.
|
|
346
|
+
function isEventStreamResponse(op) {
|
|
347
|
+
return Object.entries(op.responses || {}).some(([status, response]) => {
|
|
348
|
+
if (!/^2(?:\d{2}|XX)$/i.test(status) && status !== 'default') {
|
|
349
|
+
return false;
|
|
350
|
+
}
|
|
351
|
+
return Object.keys(response.content || {}).some((mediaType) => {
|
|
352
|
+
const type = mediaType.split(';')[0].trim().toLowerCase();
|
|
353
|
+
return type === 'text/event-stream' || type === 'event-stream';
|
|
354
|
+
});
|
|
355
|
+
});
|
|
289
356
|
}
|
|
290
357
|
|
|
291
358
|
// Resolve default response type from Swagger responses.
|
|
292
359
|
function getDefaultReturnType(op, schemas, useTypes) {
|
|
293
|
-
const {
|
|
360
|
+
const {responses = {}} = op;
|
|
294
361
|
const res = responses['200'] || responses['201'] || responses.default || {};
|
|
295
|
-
const {
|
|
362
|
+
const {content = {}} = res;
|
|
296
363
|
const firstContent = content['application/json'] || content['application/*+json'] || Object.values(content)[0];
|
|
297
364
|
if (!firstContent) {
|
|
298
|
-
return '
|
|
365
|
+
return 'object';
|
|
299
366
|
}
|
|
300
367
|
return mapSchemaToType(firstContent.schema, schemas, useTypes);
|
|
301
368
|
}
|
|
302
369
|
|
|
303
370
|
// Collect query/path parameters.
|
|
304
371
|
function getParams(op) {
|
|
305
|
-
const {
|
|
372
|
+
const {parameters = []} = op;
|
|
306
373
|
const params = Array.isArray(parameters) ? parameters : [];
|
|
307
374
|
return params.filter((p) => p.in === 'query' || p.in === 'path');
|
|
308
375
|
}
|
|
@@ -401,15 +468,28 @@ function toPascal(value) {
|
|
|
401
468
|
return value[0].toUpperCase() + value.slice(1);
|
|
402
469
|
}
|
|
403
470
|
|
|
471
|
+
// Join path prefixes while keeping a single separator at their boundary.
|
|
472
|
+
function joinPathPrefixes(...prefixes) {
|
|
473
|
+
return prefixes
|
|
474
|
+
.map((prefix) => typeof prefix === 'string' ? prefix.trim() : '')
|
|
475
|
+
.filter(Boolean)
|
|
476
|
+
.reduce((result, prefix) => {
|
|
477
|
+
if (!result) {
|
|
478
|
+
return prefix;
|
|
479
|
+
}
|
|
480
|
+
return `${result.replace(/\/+$/, '')}/${prefix.replace(/^\/+/, '')}`;
|
|
481
|
+
}, '');
|
|
482
|
+
}
|
|
483
|
+
|
|
404
484
|
// Generate API file content for a service.
|
|
405
|
-
function generateForSpec(spec, serviceName, fileBase, isDefaultFetch) {
|
|
485
|
+
function generateForSpec(spec, serviceName, fileBase, isDefaultFetch, responsePathPrefix = '') {
|
|
406
486
|
const {
|
|
407
487
|
components = {},
|
|
408
488
|
tags: specTags = [],
|
|
409
489
|
paths = {}
|
|
410
490
|
} = spec;
|
|
411
|
-
const defaultPathPrefix =
|
|
412
|
-
const {
|
|
491
|
+
const defaultPathPrefix = joinPathPrefixes(responsePathPrefix, spec.pathPrefix);
|
|
492
|
+
const {schemas = {}} = components;
|
|
413
493
|
const tagDescriptions = {};
|
|
414
494
|
if (Array.isArray(specTags)) {
|
|
415
495
|
for (const tag of specTags) {
|
|
@@ -455,8 +535,8 @@ function generateForSpec(spec, serviceName, fileBase, isDefaultFetch) {
|
|
|
455
535
|
const pathSegments = p.split('/')
|
|
456
536
|
.filter(Boolean)
|
|
457
537
|
.filter((part) => !part.startsWith('{'));
|
|
458
|
-
const shouldAddVerb = pathSegments.length === 1
|
|
459
|
-
name = shouldAddVerb ? addVerbForSingleSegment(inferred, method
|
|
538
|
+
const shouldAddVerb = pathSegments.length === 1;
|
|
539
|
+
name = shouldAddVerb ? addVerbForSingleSegment(inferred, method) : inferred;
|
|
460
540
|
}
|
|
461
541
|
if (!KEEP_METHOD_NAMES.has(name.toLowerCase())) {
|
|
462
542
|
name = fixVerbCase(name);
|
|
@@ -498,6 +578,11 @@ function generateForSpec(spec, serviceName, fileBase, isDefaultFetch) {
|
|
|
498
578
|
const className = toClassName(tag);
|
|
499
579
|
const facadePrefix = toFacadePrefix(className);
|
|
500
580
|
const sortedEntries = [...entries].sort((a, b) => {
|
|
581
|
+
const aDynamic = a.params.some((p) => p.in === 'path') || a.path.includes('{') ? 1 : 0;
|
|
582
|
+
const bDynamic = b.params.some((p) => p.in === 'path') || b.path.includes('{') ? 1 : 0;
|
|
583
|
+
if (aDynamic !== bDynamic) {
|
|
584
|
+
return aDynamic - bDynamic;
|
|
585
|
+
}
|
|
501
586
|
const aScore = (a.hasBody ? 1 : 0) + (a.params.length ? 1 : 0);
|
|
502
587
|
const bScore = (b.hasBody ? 1 : 0) + (b.params.length ? 1 : 0);
|
|
503
588
|
return aScore - bScore;
|
|
@@ -518,21 +603,33 @@ function generateForSpec(spec, serviceName, fileBase, isDefaultFetch) {
|
|
|
518
603
|
if (!KEEP_METHOD_NAMES.has(name.toLowerCase()) && (name === 'get' || name === 'delete') && facadePrefix) {
|
|
519
604
|
name = `${name}${facadePrefix}`;
|
|
520
605
|
}
|
|
606
|
+
const isDynamicPath = entryParams.some((p) => p.in === 'path') || entryPath.includes('{');
|
|
607
|
+
const saveByPathEntity = shouldSaveByPathEntity(
|
|
608
|
+
entryPath,
|
|
609
|
+
entryMethod,
|
|
610
|
+
entryHasBody ? entryOp.requestBody : null,
|
|
611
|
+
schemas
|
|
612
|
+
);
|
|
613
|
+
if (saveByPathEntity || (seen.has(name) && isDynamicPath && (entryMethod === 'post' || entryMethod === 'put'))) {
|
|
614
|
+
const hasSavePrefix = name.startsWith('save') && (name.length === 4 || /^[A-Z]/.test(name.slice(4)));
|
|
615
|
+
name = hasSavePrefix ? name : `save${toPascal(name)}`;
|
|
616
|
+
}
|
|
617
|
+
const baseName = name;
|
|
521
618
|
if (seen.has(name)) {
|
|
522
619
|
const bodyTypeName = entryHasBody ? getBodyTypeName(entryOp.requestBody, schemas) : '';
|
|
523
620
|
if (bodyTypeName) {
|
|
524
|
-
name = `${
|
|
621
|
+
name = `${baseName}By${toPascal(bodyTypeName)}`;
|
|
525
622
|
}
|
|
526
623
|
}
|
|
527
624
|
if (seen.has(name)) {
|
|
528
625
|
const paramSuffix = toParamSuffix(entryParams, paramNameMap);
|
|
529
626
|
if (paramSuffix) {
|
|
530
|
-
name = appendParamSuffix(
|
|
627
|
+
name = appendParamSuffix(baseName, paramSuffix);
|
|
531
628
|
}
|
|
532
629
|
}
|
|
533
630
|
if (seen.has(name)) {
|
|
534
631
|
const methodSuffix = entryMethod[0].toUpperCase() + entryMethod.slice(1);
|
|
535
|
-
name = `${
|
|
632
|
+
name = `${baseName}${methodSuffix}`;
|
|
536
633
|
}
|
|
537
634
|
seen.add(name);
|
|
538
635
|
const defaultType = entryDefaultReturnType;
|
|
@@ -543,20 +640,19 @@ function generateForSpec(spec, serviceName, fileBase, isDefaultFetch) {
|
|
|
543
640
|
|
|
544
641
|
const paramArgs = orderedParams.map((p) => {
|
|
545
642
|
const {
|
|
546
|
-
schema: paramSchema,
|
|
547
643
|
required,
|
|
548
644
|
name: paramName
|
|
549
645
|
} = p;
|
|
550
646
|
const mappedName = paramNameMap.get(paramName) || paramName;
|
|
551
|
-
const paramType =
|
|
647
|
+
const paramType = resolveParamType(p, schemas);
|
|
552
648
|
const optional = required ? '' : '?';
|
|
553
649
|
return `${mappedName}${optional}: ${paramType}`;
|
|
554
650
|
});
|
|
555
651
|
|
|
556
|
-
let bodyType = '
|
|
557
|
-
const {
|
|
652
|
+
let bodyType = 'object';
|
|
653
|
+
const {requestBody: opRequestBody} = entryOp;
|
|
558
654
|
if (entryHasBody && opRequestBody && opRequestBody.content) {
|
|
559
|
-
const {
|
|
655
|
+
const {content} = opRequestBody;
|
|
560
656
|
const firstContent = content['application/json'] || content['application/*+json'] || Object.values(content)[0];
|
|
561
657
|
if (firstContent && firstContent.schema) {
|
|
562
658
|
bodyType = mapSchemaToType(firstContent.schema, schemas, true);
|
|
@@ -605,17 +701,43 @@ function generateForSpec(spec, serviceName, fileBase, isDefaultFetch) {
|
|
|
605
701
|
methods.push(' */');
|
|
606
702
|
}
|
|
607
703
|
|
|
608
|
-
const
|
|
609
|
-
|
|
704
|
+
const queryParams = entryParams.filter((p) => p.in === 'query');
|
|
705
|
+
let paramsObject = 'undefined';
|
|
706
|
+
if (queryParams.length === 1) {
|
|
707
|
+
const p = queryParams[0];
|
|
708
|
+
const paramType = resolveParamType(p, schemas);
|
|
709
|
+
const mappedName = paramNameMap.get(p.name) || p.name;
|
|
710
|
+
if (paramType === 'Record<string, string | undefined>') {
|
|
711
|
+
paramsObject = mappedName;
|
|
712
|
+
} else {
|
|
713
|
+
const key = formatObjectKey(p.name);
|
|
714
|
+
const prop = mappedName === p.name ? key : `${key}: ${mappedName}`;
|
|
715
|
+
paramsObject = `{ ${prop} }`;
|
|
716
|
+
}
|
|
717
|
+
} else if (queryParams.length > 1) {
|
|
718
|
+
const entries = queryParams.map((p) => {
|
|
719
|
+
const paramType = resolveParamType(p, schemas);
|
|
610
720
|
const mappedName = paramNameMap.get(p.name) || p.name;
|
|
721
|
+
if (paramType === 'Record<string, string | undefined>') {
|
|
722
|
+
return `...${mappedName}`;
|
|
723
|
+
}
|
|
611
724
|
const key = formatObjectKey(p.name);
|
|
612
725
|
return mappedName === p.name ? key : `${key}: ${mappedName}`;
|
|
613
726
|
});
|
|
614
|
-
|
|
727
|
+
paramsObject = `{ ${entries.join(', ')} }`;
|
|
728
|
+
}
|
|
615
729
|
|
|
616
730
|
const pathTemplate = withPathParams(entryPath, entryParams, paramNameMap);
|
|
617
731
|
const pathLiteral = pathTemplate.includes('${') ? `\`${pathTemplate}\`` : `'${pathTemplate}'`;
|
|
618
732
|
|
|
733
|
+
if (isEventStreamResponse(entryOp)) {
|
|
734
|
+
methods.push(` public ${name}(${sigParts}): EventSource {`);
|
|
735
|
+
methods.push(` return this.sse(${pathLiteral}, ${paramsObject});`);
|
|
736
|
+
methods.push(' }');
|
|
737
|
+
methods.push('');
|
|
738
|
+
continue;
|
|
739
|
+
}
|
|
740
|
+
|
|
619
741
|
if (entryMethod === 'get') {
|
|
620
742
|
methods.push(` public async ${name}<T = ${defaultType}>(${sigParts}): Promise<JsonResponse<T>> {`);
|
|
621
743
|
methods.push(` return await this.get<T>(${pathLiteral}, ${paramsObject});`);
|
|
@@ -733,7 +855,7 @@ function generateForSpec(spec, serviceName, fileBase, isDefaultFetch) {
|
|
|
733
855
|
// Map Swagger schema to a TS type for DTO output.
|
|
734
856
|
function mapSchemaToTsType(schema, schemas) {
|
|
735
857
|
if (!schema) {
|
|
736
|
-
return '
|
|
858
|
+
return 'object';
|
|
737
859
|
}
|
|
738
860
|
const {
|
|
739
861
|
$ref,
|
|
@@ -751,7 +873,7 @@ function mapSchemaToTsType(schema, schemas) {
|
|
|
751
873
|
if (refSchema && refSchema.properties && refSchema.properties.data) {
|
|
752
874
|
return mapSchemaToTsType(refSchema.properties.data, schemas);
|
|
753
875
|
}
|
|
754
|
-
return '
|
|
876
|
+
return 'object';
|
|
755
877
|
}
|
|
756
878
|
return normalizeDtoName(refName);
|
|
757
879
|
}
|
|
@@ -763,6 +885,9 @@ function mapSchemaToTsType(schema, schemas) {
|
|
|
763
885
|
return schemaEnum.map((v) => (typeof v === 'string' ? `'${v}'` : String(v)))
|
|
764
886
|
.join(' | ');
|
|
765
887
|
}
|
|
888
|
+
if (type === 'any') {
|
|
889
|
+
return 'any';
|
|
890
|
+
}
|
|
766
891
|
if (type === 'string') {
|
|
767
892
|
return 'string';
|
|
768
893
|
}
|
|
@@ -784,17 +909,44 @@ function mapSchemaToTsType(schema, schemas) {
|
|
|
784
909
|
}
|
|
785
910
|
if (type === 'object') {
|
|
786
911
|
if (additionalProperties) {
|
|
787
|
-
return 'Record<string,
|
|
912
|
+
return 'Record<string, object>';
|
|
788
913
|
}
|
|
789
|
-
return '
|
|
914
|
+
return 'object';
|
|
790
915
|
}
|
|
791
|
-
return '
|
|
916
|
+
return 'object';
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
// Swagger treats schemas without a type constraint as any.
|
|
920
|
+
function isAnySchema(schema) {
|
|
921
|
+
if (schema === true || schema === null || schema === undefined) {
|
|
922
|
+
return true;
|
|
923
|
+
}
|
|
924
|
+
if (typeof schema !== 'object' || Array.isArray(schema)) {
|
|
925
|
+
return false;
|
|
926
|
+
}
|
|
927
|
+
if (schema.type === 'any') {
|
|
928
|
+
return true;
|
|
929
|
+
}
|
|
930
|
+
const typeConstraints = [
|
|
931
|
+
'$ref',
|
|
932
|
+
'type',
|
|
933
|
+
'items',
|
|
934
|
+
'enum',
|
|
935
|
+
'const',
|
|
936
|
+
'properties',
|
|
937
|
+
'additionalProperties',
|
|
938
|
+
'allOf',
|
|
939
|
+
'oneOf',
|
|
940
|
+
'anyOf',
|
|
941
|
+
'not'
|
|
942
|
+
];
|
|
943
|
+
return !typeConstraints.some((key) => schema[key] !== undefined);
|
|
792
944
|
}
|
|
793
945
|
|
|
794
946
|
// Generate DTO/type definitions file content.
|
|
795
947
|
function generateTypes(spec) {
|
|
796
|
-
const {
|
|
797
|
-
const {
|
|
948
|
+
const {components = {}} = spec;
|
|
949
|
+
const {schemas = {}} = components;
|
|
798
950
|
const lines = [];
|
|
799
951
|
lines.push('/*');
|
|
800
952
|
lines.push(' * This file is auto-generated. Do not edit manually.');
|
|
@@ -829,9 +981,16 @@ function generateTypes(spec) {
|
|
|
829
981
|
const def = typeDefs.get(normalizedName);
|
|
830
982
|
const requiredSet = new Set(required || []);
|
|
831
983
|
for (const [prop, propSchema] of Object.entries(properties)) {
|
|
832
|
-
|
|
984
|
+
if (isAnySchema(propSchema)) {
|
|
985
|
+
continue;
|
|
986
|
+
}
|
|
987
|
+
const propType = mapSchemaToTsType(propSchema, schemas);
|
|
988
|
+
if (propType === 'any') {
|
|
989
|
+
continue;
|
|
990
|
+
}
|
|
991
|
+
def.props[prop] = propType;
|
|
833
992
|
def.required[prop] = requiredSet.has(prop);
|
|
834
|
-
const {
|
|
993
|
+
const {description} = propSchema || {};
|
|
835
994
|
const desc = description
|
|
836
995
|
? description.replace(/\n/g, ' ')
|
|
837
996
|
.trim()
|
|
@@ -883,6 +1042,26 @@ async function fetchJson(url) {
|
|
|
883
1042
|
return response.json();
|
|
884
1043
|
}
|
|
885
1044
|
|
|
1045
|
+
// Fetch JSON together with response headers needed by API generation.
|
|
1046
|
+
async function fetchJsonWithHeaders(url) {
|
|
1047
|
+
console.log(`Fetching ${url}`);
|
|
1048
|
+
const response = await fetch(url);
|
|
1049
|
+
if (!response.ok) {
|
|
1050
|
+
throw new Error(`Failed to fetch ${url}: ${response.status}`);
|
|
1051
|
+
}
|
|
1052
|
+
const responseText = await response.text();
|
|
1053
|
+
if (responseText === '') {
|
|
1054
|
+
return {
|
|
1055
|
+
data: {},
|
|
1056
|
+
pathPrefix: response.headers.get('x-path-prefix') || ''
|
|
1057
|
+
};
|
|
1058
|
+
}
|
|
1059
|
+
return {
|
|
1060
|
+
data: JSON.parse(responseText),
|
|
1061
|
+
pathPrefix: response.headers.get('x-path-prefix') || ''
|
|
1062
|
+
};
|
|
1063
|
+
}
|
|
1064
|
+
|
|
886
1065
|
// Read key/value pairs from .env without extra dependencies.
|
|
887
1066
|
function readEnvFile() {
|
|
888
1067
|
if (!fs.existsSync(ENV_PATH)) {
|
|
@@ -911,7 +1090,7 @@ function readEnvFile() {
|
|
|
911
1090
|
// Persist values into .env (overwrite or append keys).
|
|
912
1091
|
function writeEnvFile(nextEnv) {
|
|
913
1092
|
const existing = readEnvFile();
|
|
914
|
-
const merged = {
|
|
1093
|
+
const merged = {...existing, ...nextEnv};
|
|
915
1094
|
const lines = Object.entries(merged)
|
|
916
1095
|
.map(([key, value]) => `${key}=${value}`);
|
|
917
1096
|
fs.writeFileSync(ENV_PATH, `${lines.join('\n')}\n`, 'utf8');
|
|
@@ -946,7 +1125,7 @@ async function promptForMissing(env) {
|
|
|
946
1125
|
rl.question(q, resolve);
|
|
947
1126
|
});
|
|
948
1127
|
|
|
949
|
-
const result = {
|
|
1128
|
+
const result = {...env};
|
|
950
1129
|
if (!result.SWAGGER_BASE_URL) {
|
|
951
1130
|
result.SWAGGER_BASE_URL = (await ask('BASE_URL: ')).trim();
|
|
952
1131
|
}
|
|
@@ -970,7 +1149,7 @@ async function main() {
|
|
|
970
1149
|
SWAGGER_BASE_URL: baseUrl,
|
|
971
1150
|
SWAGGER_OUT_DIRECTORY: directory,
|
|
972
1151
|
IS_DEFAULT_FETCH: isDefaultFetch
|
|
973
|
-
} = {
|
|
1152
|
+
} = {...process.env, ...envFile};
|
|
974
1153
|
isDefaultFetch = normalizeBoolean(isDefaultFetch);
|
|
975
1154
|
|
|
976
1155
|
if (!baseUrl || !directory || isDefaultFetch === undefined) {
|
|
@@ -1015,10 +1194,10 @@ async function main() {
|
|
|
1015
1194
|
}
|
|
1016
1195
|
|
|
1017
1196
|
if (!fs.existsSync(outputDirectory)) {
|
|
1018
|
-
fs.mkdirSync(outputDirectory, {
|
|
1197
|
+
fs.mkdirSync(outputDirectory, {recursive: true});
|
|
1019
1198
|
}
|
|
1020
1199
|
if (!fs.existsSync(typesDirectory)) {
|
|
1021
|
-
fs.mkdirSync(typesDirectory, {
|
|
1200
|
+
fs.mkdirSync(typesDirectory, {recursive: true});
|
|
1022
1201
|
}
|
|
1023
1202
|
|
|
1024
1203
|
for (const service of services) {
|
|
@@ -1033,9 +1212,9 @@ async function main() {
|
|
|
1033
1212
|
continue;
|
|
1034
1213
|
}
|
|
1035
1214
|
const specUrl = `${baseUrl}${url}`;
|
|
1036
|
-
const spec = await
|
|
1215
|
+
const {data: spec, pathPrefix} = await fetchJsonWithHeaders(specUrl);
|
|
1037
1216
|
const fileBase = toFileBase(serviceName);
|
|
1038
|
-
const content = generateForSpec(spec, serviceName, fileBase, isDefaultFetch);
|
|
1217
|
+
const content = generateForSpec(spec, serviceName, fileBase, isDefaultFetch, pathPrefix);
|
|
1039
1218
|
const typesContent = generateTypes(spec);
|
|
1040
1219
|
|
|
1041
1220
|
const outPath = path.join(outputDirectory, `${fileBase}.ts`);
|