beer-swagger 1.0.4 → 4.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.
Files changed (2) hide show
  1. package/generate-apis.js +152 -60
  2. package/package.json +1 -1
package/generate-apis.js CHANGED
@@ -5,7 +5,6 @@ import path from 'path';
5
5
  import { createInterface } from 'readline';
6
6
 
7
7
  const ENV_PATH = path.join(process.cwd(), '.env');
8
- const CLIENT_TYPES = new Set(['WEB', 'WeChatMiniProgram']);
9
8
  const SKIPPED_SERVICES = new Set([
10
9
  'billbear-common-web-gateway',
11
10
  'billbear-common-data-panel'
@@ -117,7 +116,7 @@ function appendParamSuffix(baseName, paramSuffix) {
117
116
  }
118
117
 
119
118
  // Add a verb when the HTTP method should be reflected in the name.
120
- function addVerbForSingleSegment(name, method, hasPathParams = false) {
119
+ function addVerbForSingleSegment(name, method) {
121
120
  const lower = name.toLowerCase();
122
121
  if (KEEP_METHOD_NAMES.has(lower)) {
123
122
  return name;
@@ -126,7 +125,7 @@ function addVerbForSingleSegment(name, method, hasPathParams = false) {
126
125
  const suffix = name[0].toUpperCase() + name.slice(1);
127
126
  return `removeBy${suffix}`;
128
127
  }
129
- if ((method === 'post' || method === 'put') && !hasPathParams) {
128
+ if (method === 'post' || method === 'put') {
130
129
  return name;
131
130
  }
132
131
  const verbMap = {
@@ -186,7 +185,7 @@ function getBodyTypeName(requestBody, schemas) {
186
185
  // Map a Swagger schema to a runtime return type.
187
186
  function mapSchemaToType(schema, schemas, useTypes) {
188
187
  if (!schema) {
189
- return '{}';
188
+ return 'object';
190
189
  }
191
190
  const {
192
191
  $ref,
@@ -202,18 +201,18 @@ function mapSchemaToType(schema, schemas, useTypes) {
202
201
  if (responseSchema && responseSchema.properties && responseSchema.properties.data) {
203
202
  return mapSchemaToType(responseSchema.properties.data, schemas, useTypes);
204
203
  }
205
- return '{}';
204
+ return 'object';
206
205
  }
207
206
  const refName = normalizeDtoName(rawRefName);
208
207
  const refSchema = schemas[rawRefName] || schemas[refName];
209
208
  if (refSchema && refSchema.properties && refSchema.properties.data) {
210
209
  return mapSchemaToType(refSchema.properties.data, schemas, useTypes);
211
210
  }
212
- return useTypes ? `Types.${refName}` : '{}';
211
+ return useTypes ? `Types.${refName}` : 'object';
213
212
  }
214
213
  if (!type && items) {
215
214
  const itemType = mapSchemaToType(items, schemas, useTypes);
216
- if (itemType === '{}' || itemType === '[]') {
215
+ if (itemType === '[]') {
217
216
  return '[]';
218
217
  }
219
218
  return `${itemType}[]`;
@@ -235,7 +234,7 @@ function mapSchemaToType(schema, schemas, useTypes) {
235
234
  }
236
235
  if (type === 'array') {
237
236
  const itemType = mapSchemaToType(items, schemas, useTypes);
238
- if (itemType === '{}' || itemType === '[]') {
237
+ if (itemType === '[]') {
239
238
  return '[]';
240
239
  }
241
240
  return `${itemType}[]`;
@@ -247,9 +246,9 @@ function mapSchemaToType(schema, schemas, useTypes) {
247
246
  }
248
247
 
249
248
  // Map parameter schema to a type for method signatures.
250
- function mapParamSchemaToType(schema, schemas, useTypes) {
249
+ function mapParamSchemaToType(schema, schemas, useTypes, isQuery = false) {
251
250
  if (!schema) {
252
- return '{}';
251
+ return isQuery ? 'Record<string, string | undefined>' : 'object';
253
252
  }
254
253
  const {
255
254
  $ref,
@@ -260,7 +259,7 @@ function mapParamSchemaToType(schema, schemas, useTypes) {
260
259
  if ($ref) {
261
260
  const refName = normalizeDtoName($ref.split('/')
262
261
  .pop());
263
- return useTypes ? `Types.${refName}` : '{}';
262
+ return useTypes ? `Types.${refName}` : 'object';
264
263
  }
265
264
  if (!type && items) {
266
265
  return '[]';
@@ -284,9 +283,29 @@ function mapParamSchemaToType(schema, schemas, useTypes) {
284
283
  return '[]';
285
284
  }
286
285
  if (type === 'object') {
287
- return 'object';
286
+ return isQuery ? 'Record<string, string | undefined>' : 'object';
288
287
  }
289
- return 'object';
288
+ return isQuery ? 'Record<string, string | undefined>' : 'object';
289
+ }
290
+
291
+ // Resolve parameter type for a parameter definition.
292
+ function resolveParamType(param, schemas) {
293
+ const schemaToUse = param.schema || (param.type ? { type: param.type, format: param.format, items: param.items } : null);
294
+ const isQuery = param.in === 'query';
295
+ return mapParamSchemaToType(schemaToUse, schemas, true, isQuery);
296
+ }
297
+
298
+ // Detect event streams in successful or default responses.
299
+ function isEventStreamResponse(op) {
300
+ return Object.entries(op.responses || {}).some(([status, response]) => {
301
+ if (!/^2(?:\d{2}|XX)$/i.test(status) && status !== 'default') {
302
+ return false;
303
+ }
304
+ return Object.keys(response.content || {}).some((mediaType) => {
305
+ const type = mediaType.split(';')[0].trim().toLowerCase();
306
+ return type === 'text/event-stream' || type === 'event-stream';
307
+ });
308
+ });
290
309
  }
291
310
 
292
311
  // Resolve default response type from Swagger responses.
@@ -296,7 +315,7 @@ function getDefaultReturnType(op, schemas, useTypes) {
296
315
  const { content = {} } = res;
297
316
  const firstContent = content['application/json'] || content['application/*+json'] || Object.values(content)[0];
298
317
  if (!firstContent) {
299
- return '{}';
318
+ return 'object';
300
319
  }
301
320
  return mapSchemaToType(firstContent.schema, schemas, useTypes);
302
321
  }
@@ -403,7 +422,7 @@ function toPascal(value) {
403
422
  }
404
423
 
405
424
  // Generate API file content for a service.
406
- function generateForSpec(spec, serviceName, fileBase, clientType) {
425
+ function generateForSpec(spec, serviceName, fileBase, isDefaultFetch) {
407
426
  const {
408
427
  components = {},
409
428
  tags: specTags = [],
@@ -456,8 +475,8 @@ function generateForSpec(spec, serviceName, fileBase, clientType) {
456
475
  const pathSegments = p.split('/')
457
476
  .filter(Boolean)
458
477
  .filter((part) => !part.startsWith('{'));
459
- const shouldAddVerb = pathSegments.length === 1 || ((method === 'post' || method === 'put') && pathParams.length > 0);
460
- name = shouldAddVerb ? addVerbForSingleSegment(inferred, method, pathParams.length > 0) : inferred;
478
+ const shouldAddVerb = pathSegments.length === 1;
479
+ name = shouldAddVerb ? addVerbForSingleSegment(inferred, method) : inferred;
461
480
  }
462
481
  if (!KEEP_METHOD_NAMES.has(name.toLowerCase())) {
463
482
  name = fixVerbCase(name);
@@ -499,6 +518,11 @@ function generateForSpec(spec, serviceName, fileBase, clientType) {
499
518
  const className = toClassName(tag);
500
519
  const facadePrefix = toFacadePrefix(className);
501
520
  const sortedEntries = [...entries].sort((a, b) => {
521
+ const aDynamic = a.params.some((p) => p.in === 'path') || a.path.includes('{') ? 1 : 0;
522
+ const bDynamic = b.params.some((p) => p.in === 'path') || b.path.includes('{') ? 1 : 0;
523
+ if (aDynamic !== bDynamic) {
524
+ return aDynamic - bDynamic;
525
+ }
502
526
  const aScore = (a.hasBody ? 1 : 0) + (a.params.length ? 1 : 0);
503
527
  const bScore = (b.hasBody ? 1 : 0) + (b.params.length ? 1 : 0);
504
528
  return aScore - bScore;
@@ -519,21 +543,28 @@ function generateForSpec(spec, serviceName, fileBase, clientType) {
519
543
  if (!KEEP_METHOD_NAMES.has(name.toLowerCase()) && (name === 'get' || name === 'delete') && facadePrefix) {
520
544
  name = `${name}${facadePrefix}`;
521
545
  }
546
+ const isDynamicPath = entryParams.some((p) => p.in === 'path') || entryPath.includes('{');
547
+ if (seen.has(name) && isDynamicPath && (entryMethod === 'post' || entryMethod === 'put')) {
548
+ const hasSavePrefix = name.startsWith('save') && (name.length === 4 || /^[A-Z]/.test(name.slice(4)));
549
+ const saveName = hasSavePrefix ? name : `save${toPascal(name)}`;
550
+ name = saveName;
551
+ }
552
+ const baseName = name;
522
553
  if (seen.has(name)) {
523
554
  const bodyTypeName = entryHasBody ? getBodyTypeName(entryOp.requestBody, schemas) : '';
524
555
  if (bodyTypeName) {
525
- name = `${entryName}By${toPascal(bodyTypeName)}`;
556
+ name = `${baseName}By${toPascal(bodyTypeName)}`;
526
557
  }
527
558
  }
528
559
  if (seen.has(name)) {
529
560
  const paramSuffix = toParamSuffix(entryParams, paramNameMap);
530
561
  if (paramSuffix) {
531
- name = appendParamSuffix(entryName, paramSuffix);
562
+ name = appendParamSuffix(baseName, paramSuffix);
532
563
  }
533
564
  }
534
565
  if (seen.has(name)) {
535
566
  const methodSuffix = entryMethod[0].toUpperCase() + entryMethod.slice(1);
536
- name = `${entryName}${methodSuffix}`;
567
+ name = `${baseName}${methodSuffix}`;
537
568
  }
538
569
  seen.add(name);
539
570
  const defaultType = entryDefaultReturnType;
@@ -544,17 +575,16 @@ function generateForSpec(spec, serviceName, fileBase, clientType) {
544
575
 
545
576
  const paramArgs = orderedParams.map((p) => {
546
577
  const {
547
- schema: paramSchema,
548
578
  required,
549
579
  name: paramName
550
580
  } = p;
551
581
  const mappedName = paramNameMap.get(paramName) || paramName;
552
- const paramType = mapParamSchemaToType(paramSchema, schemas, true);
582
+ const paramType = resolveParamType(p, schemas);
553
583
  const optional = required ? '' : '?';
554
584
  return `${mappedName}${optional}: ${paramType}`;
555
585
  });
556
586
 
557
- let bodyType = '{}';
587
+ let bodyType = 'object';
558
588
  const { requestBody: opRequestBody } = entryOp;
559
589
  if (entryHasBody && opRequestBody && opRequestBody.content) {
560
590
  const { content } = opRequestBody;
@@ -606,17 +636,43 @@ function generateForSpec(spec, serviceName, fileBase, clientType) {
606
636
  methods.push(' */');
607
637
  }
608
638
 
609
- const queryParamNames = entryParams.filter((p) => p.in === 'query')
610
- .map((p) => {
639
+ const queryParams = entryParams.filter((p) => p.in === 'query');
640
+ let paramsObject = 'undefined';
641
+ if (queryParams.length === 1) {
642
+ const p = queryParams[0];
643
+ const paramType = resolveParamType(p, schemas);
644
+ const mappedName = paramNameMap.get(p.name) || p.name;
645
+ if (paramType === 'Record<string, string | undefined>') {
646
+ paramsObject = mappedName;
647
+ } else {
648
+ const key = formatObjectKey(p.name);
649
+ const prop = mappedName === p.name ? key : `${key}: ${mappedName}`;
650
+ paramsObject = `{ ${prop} }`;
651
+ }
652
+ } else if (queryParams.length > 1) {
653
+ const entries = queryParams.map((p) => {
654
+ const paramType = resolveParamType(p, schemas);
611
655
  const mappedName = paramNameMap.get(p.name) || p.name;
656
+ if (paramType === 'Record<string, string | undefined>') {
657
+ return `...${mappedName}`;
658
+ }
612
659
  const key = formatObjectKey(p.name);
613
660
  return mappedName === p.name ? key : `${key}: ${mappedName}`;
614
661
  });
615
- const paramsObject = queryParamNames.length ? `{ ${queryParamNames.join(', ')} }` : 'undefined';
662
+ paramsObject = `{ ${entries.join(', ')} }`;
663
+ }
616
664
 
617
665
  const pathTemplate = withPathParams(entryPath, entryParams, paramNameMap);
618
666
  const pathLiteral = pathTemplate.includes('${') ? `\`${pathTemplate}\`` : `'${pathTemplate}'`;
619
667
 
668
+ if (isEventStreamResponse(entryOp)) {
669
+ methods.push(` public ${name}(${sigParts}): EventSource {`);
670
+ methods.push(` return this.sse(${pathLiteral}, ${paramsObject});`);
671
+ methods.push(' }');
672
+ methods.push('');
673
+ continue;
674
+ }
675
+
620
676
  if (entryMethod === 'get') {
621
677
  methods.push(` public async ${name}<T = ${defaultType}>(${sigParts}): Promise<JsonResponse<T>> {`);
622
678
  methods.push(` return await this.get<T>(${pathLiteral}, ${paramsObject});`);
@@ -702,7 +758,7 @@ function generateForSpec(spec, serviceName, fileBase, clientType) {
702
758
  lines.push(' * This file is auto-generated. Do not edit manually.');
703
759
  lines.push(' * To update, run the generate-apis.js script.');
704
760
  lines.push(' */');
705
- const fetchImport = clientType === 'WeChatMiniProgram' ? './fetch' : 'beer-network/api';
761
+ const fetchImport = isDefaultFetch ? 'beer-network/api' : './fetch';
706
762
  lines.push(`import type { JsonResponse } from '${fetchImport}';`);
707
763
  lines.push(`import Fetch from '${fetchImport}';`);
708
764
  if (fileBase) {
@@ -734,7 +790,7 @@ function generateForSpec(spec, serviceName, fileBase, clientType) {
734
790
  // Map Swagger schema to a TS type for DTO output.
735
791
  function mapSchemaToTsType(schema, schemas) {
736
792
  if (!schema) {
737
- return '{}';
793
+ return 'object';
738
794
  }
739
795
  const {
740
796
  $ref,
@@ -752,7 +808,7 @@ function mapSchemaToTsType(schema, schemas) {
752
808
  if (refSchema && refSchema.properties && refSchema.properties.data) {
753
809
  return mapSchemaToTsType(refSchema.properties.data, schemas);
754
810
  }
755
- return '{}';
811
+ return 'object';
756
812
  }
757
813
  return normalizeDtoName(refName);
758
814
  }
@@ -764,6 +820,9 @@ function mapSchemaToTsType(schema, schemas) {
764
820
  return schemaEnum.map((v) => (typeof v === 'string' ? `'${v}'` : String(v)))
765
821
  .join(' | ');
766
822
  }
823
+ if (type === 'any') {
824
+ return 'any';
825
+ }
767
826
  if (type === 'string') {
768
827
  return 'string';
769
828
  }
@@ -785,11 +844,38 @@ function mapSchemaToTsType(schema, schemas) {
785
844
  }
786
845
  if (type === 'object') {
787
846
  if (additionalProperties) {
788
- return 'Record<string, {}>';
847
+ return 'Record<string, object>';
789
848
  }
790
- return '{}';
849
+ return 'object';
791
850
  }
792
- return '{}';
851
+ return 'object';
852
+ }
853
+
854
+ // Swagger treats schemas without a type constraint as any.
855
+ function isAnySchema(schema) {
856
+ if (schema === true || schema === null || schema === undefined) {
857
+ return true;
858
+ }
859
+ if (typeof schema !== 'object' || Array.isArray(schema)) {
860
+ return false;
861
+ }
862
+ if (schema.type === 'any') {
863
+ return true;
864
+ }
865
+ const typeConstraints = [
866
+ '$ref',
867
+ 'type',
868
+ 'items',
869
+ 'enum',
870
+ 'const',
871
+ 'properties',
872
+ 'additionalProperties',
873
+ 'allOf',
874
+ 'oneOf',
875
+ 'anyOf',
876
+ 'not'
877
+ ];
878
+ return !typeConstraints.some((key) => schema[key] !== undefined);
793
879
  }
794
880
 
795
881
  // Generate DTO/type definitions file content.
@@ -830,7 +916,14 @@ function generateTypes(spec) {
830
916
  const def = typeDefs.get(normalizedName);
831
917
  const requiredSet = new Set(required || []);
832
918
  for (const [prop, propSchema] of Object.entries(properties)) {
833
- def.props[prop] = mapSchemaToTsType(propSchema, schemas);
919
+ if (isAnySchema(propSchema)) {
920
+ continue;
921
+ }
922
+ const propType = mapSchemaToTsType(propSchema, schemas);
923
+ if (propType === 'any') {
924
+ continue;
925
+ }
926
+ def.props[prop] = propType;
834
927
  def.required[prop] = requiredSet.has(prop);
835
928
  const { description } = propSchema || {};
836
929
  const desc = description
@@ -918,22 +1011,23 @@ function writeEnvFile(nextEnv) {
918
1011
  fs.writeFileSync(ENV_PATH, `${lines.join('\n')}\n`, 'utf8');
919
1012
  }
920
1013
 
921
- function normalizeClientType(value) {
922
- if (!value) {
923
- return '';
1014
+ function normalizeBoolean(value) {
1015
+ if (value === undefined || value === null || value === '') {
1016
+ return undefined;
924
1017
  }
925
- if (CLIENT_TYPES.has(value)) {
1018
+ if (typeof value === 'boolean') {
926
1019
  return value;
927
1020
  }
928
- if (value.toUpperCase() === 'WEB') {
929
- return 'WEB';
930
- }
931
- const normalized = value.replace(/[-_\s]/g, '')
1021
+ const normalized = String(value)
1022
+ .trim()
932
1023
  .toLowerCase();
933
- if (normalized === 'wechatminiprogram') {
934
- return 'WeChatMiniProgram';
1024
+ if (['true', '1', 'yes', 'y'].includes(normalized)) {
1025
+ return true;
1026
+ }
1027
+ if (['false', '0', 'no', 'n'].includes(normalized)) {
1028
+ return false;
935
1029
  }
936
- return '';
1030
+ return undefined;
937
1031
  }
938
1032
 
939
1033
  // Ask user for missing values via CLI.
@@ -953,13 +1047,12 @@ async function promptForMissing(env) {
953
1047
  if (!result.SWAGGER_OUT_DIRECTORY) {
954
1048
  result.SWAGGER_OUT_DIRECTORY = (await ask('DIRECTORY: ')).trim();
955
1049
  }
956
- if (!result.CLIENT_TYPE || !CLIENT_TYPES.has(result.CLIENT_TYPE)) {
957
- let selected = '';
958
- while (!CLIENT_TYPES.has(selected)) {
959
- selected = normalizeClientType((await ask('CLIENT_TYPE (WEB/WeChatMiniProgram): ')).trim());
960
- }
961
- result.CLIENT_TYPE = selected;
1050
+ let isDefaultFetch = normalizeBoolean(result.IS_DEFAULT_FETCH);
1051
+ while (isDefaultFetch === undefined) {
1052
+ const selected = (await ask('IS_DEFAULT_FETCH (true/false): ')).trim();
1053
+ isDefaultFetch = normalizeBoolean(selected);
962
1054
  }
1055
+ result.IS_DEFAULT_FETCH = String(isDefaultFetch);
963
1056
  rl.close();
964
1057
  return result;
965
1058
  }
@@ -970,23 +1063,23 @@ async function main() {
970
1063
  let {
971
1064
  SWAGGER_BASE_URL: baseUrl,
972
1065
  SWAGGER_OUT_DIRECTORY: directory,
973
- CLIENT_TYPE: clientType
1066
+ IS_DEFAULT_FETCH: isDefaultFetch
974
1067
  } = { ...process.env, ...envFile };
975
- clientType = normalizeClientType(clientType);
1068
+ isDefaultFetch = normalizeBoolean(isDefaultFetch);
976
1069
 
977
- if (!baseUrl || !directory || !clientType) {
1070
+ if (!baseUrl || !directory || isDefaultFetch === undefined) {
978
1071
  const filled = await promptForMissing({
979
1072
  SWAGGER_BASE_URL: baseUrl || '',
980
1073
  SWAGGER_OUT_DIRECTORY: directory || '',
981
- CLIENT_TYPE: clientType || ''
1074
+ IS_DEFAULT_FETCH: isDefaultFetch === undefined ? '' : String(isDefaultFetch)
982
1075
  });
983
1076
  baseUrl = filled.SWAGGER_BASE_URL;
984
1077
  directory = filled.SWAGGER_OUT_DIRECTORY;
985
- clientType = normalizeClientType(filled.CLIENT_TYPE);
1078
+ isDefaultFetch = normalizeBoolean(filled.IS_DEFAULT_FETCH);
986
1079
  writeEnvFile({
987
1080
  SWAGGER_BASE_URL: baseUrl,
988
1081
  SWAGGER_OUT_DIRECTORY: directory,
989
- CLIENT_TYPE: clientType
1082
+ IS_DEFAULT_FETCH: String(isDefaultFetch)
990
1083
  });
991
1084
  }
992
1085
 
@@ -996,10 +1089,9 @@ async function main() {
996
1089
  if (!directory) {
997
1090
  throw new Error('SWAGGER_OUT_DIRECTORY is required.');
998
1091
  }
999
- if (!clientType) {
1000
- throw new Error('CLIENT_TYPE is required.');
1092
+ if (isDefaultFetch === undefined) {
1093
+ throw new Error('IS_DEFAULT_FETCH is required.');
1001
1094
  }
1002
-
1003
1095
  const configUrl = `${baseUrl}/v3/api-docs/swagger-config`;
1004
1096
  const outputDirectory = path.isAbsolute(directory) ? directory : path.join(process.cwd(), directory);
1005
1097
  const typesDirectory = path.join(outputDirectory, 'types');
@@ -1037,7 +1129,7 @@ async function main() {
1037
1129
  const specUrl = `${baseUrl}${url}`;
1038
1130
  const spec = await fetchJson(specUrl);
1039
1131
  const fileBase = toFileBase(serviceName);
1040
- const content = generateForSpec(spec, serviceName, fileBase, clientType);
1132
+ const content = generateForSpec(spec, serviceName, fileBase, isDefaultFetch);
1041
1133
  const typesContent = generateTypes(spec);
1042
1134
 
1043
1135
  const outPath = path.join(outputDirectory, `${fileBase}.ts`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "beer-swagger",
3
- "version": "1.0.4",
3
+ "version": "4.0.1",
4
4
  "bin": {
5
5
  "beer-swagger": "./generate-apis.js"
6
6
  },