UNPKG

typescript-swagger

Version:

Generate Swagger files from a decorator library like typescript-rest or a @decorators/express.

630 lines 30.6 kB
"use strict"; var __assign = (this && this.__assign) || function () { __assign = Object.assign || function(t) { for (var s, i = 1, n = arguments.length; i < n; i++) { s = arguments[i]; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; } return t; }; return __assign.apply(this, arguments); }; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __generator = (this && this.__generator) || function (thisArg, body) { var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; function verb(n) { return function (v) { return step([n, v]); }; } function step(op) { if (f) throw new TypeError("Generator is already executing."); while (_) try { if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; if (y = 0, t) op = [op[0] & 2, t.value]; switch (op[0]) { case 0: case 1: t = op; break; case 4: _.label++; return { value: op[1], done: false }; case 5: _.label++; y = op[1]; op = [0]; continue; case 7: op = _.ops.pop(); _.trys.pop(); continue; default: if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } if (t[2]) _.ops.pop(); _.trys.pop(); continue; } op = body.call(thisArg, _); } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; } }; var __spreadArray = (this && this.__spreadArray) || function (to, from) { for (var i = 0, il = from.length, j = to.length; i < il; i++, j++) to[j] = from[i]; return to; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.SpecGenerator = exports.generateDocumentation = void 0; var fs_1 = require("fs"); var lodash_1 = require("lodash"); var path_1 = require("path"); var yamljs_1 = require("yamljs"); var config_1 = require("../config"); var debug_1 = require("../debug"); var metadataGenerator_1 = require("../metadata/metadataGenerator"); var type_1 = require("../metadata/resolver/type"); function generateDocumentation(swaggerConfig, tsConfig) { return __awaiter(this, void 0, void 0, function () { var metadata; return __generator(this, function (_a) { switch (_a.label) { case 0: metadata = new metadataGenerator_1.MetadataGenerator(swaggerConfig.entryFile, tsConfig, swaggerConfig.ignore, swaggerConfig.decoratorConfig).generate(); return [4 /*yield*/, new SpecGenerator(metadata, swaggerConfig).generate()]; case 1: _a.sent(); return [2 /*return*/, Array.isArray(swaggerConfig.outputDirectory) ? swaggerConfig.outputDirectory.join('/') : swaggerConfig.outputDirectory]; } }); }); } exports.generateDocumentation = generateDocumentation; var SpecGenerator = /** @class */ (function () { function SpecGenerator(metadata, config) { this.metadata = metadata; this.config = config; this.debugger = debug_1.useDebugger(); } SpecGenerator.prototype.generate = function () { return __awaiter(this, void 0, void 0, function () { var spec; var _this = this; return __generator(this, function (_a) { switch (_a.label) { case 0: this.debugger('Generating swagger files.'); this.debugger('Swagger Config: %j', this.config); this.debugger('Services Metadata: %j', this.metadata); spec = this.getSwaggerSpec(); if (!(this.config.outputFormat === config_1.Specification.OpenApi_3)) return [3 /*break*/, 2]; return [4 /*yield*/, this.convertToOpenApiSpec(spec)]; case 1: spec = _a.sent(); _a.label = 2; case 2: return [2 /*return*/, new Promise(function (resolve, reject) { var swaggerDirs = lodash_1.castArray(_this.config.outputDirectory); _this.debugger('Saving specs to folders: %j', swaggerDirs); swaggerDirs.forEach(function (swaggerDir) { fs_1.promises.mkdir(swaggerDir, { recursive: true }).then(function () { _this.debugger('Saving specs json file to folder: %j', swaggerDir); fs_1.writeFile(swaggerDir + "/swagger.json", JSON.stringify(spec, null, '\t'), function (err) { if (err) { return reject(err); } if (_this.config.yaml) { _this.debugger('Saving specs yaml file to folder: %j', swaggerDir); fs_1.writeFile(swaggerDir + "/swagger.yaml", yamljs_1.stringify(spec, 1000), function (errYaml) { if (errYaml) { return reject(errYaml); } _this.debugger('Generated files saved to folder: %j', swaggerDir); resolve(); }); } else { _this.debugger('Generated files saved to folder: %j', swaggerDir); resolve(); } }); }).catch(reject); }); })]; } }); }); }; SpecGenerator.prototype.getMetaData = function () { return this.metadata; }; SpecGenerator.prototype.getSwaggerSpec = function () { var spec = { basePath: this.config.basePath, definitions: this.buildDefinitions(), info: {}, paths: this.buildPaths(), swagger: '2.0' }; spec.securityDefinitions = this.config.securityDefinitions ? this.config.securityDefinitions : {}; if (this.config.consumes) { spec.consumes = this.config.consumes; } if (this.config.produces) { spec.produces = this.config.produces; } if (this.config.description) { spec.info.description = this.config.description; } if (this.config.license) { spec.info.license = { name: this.config.license }; } if (this.config.name) { spec.info.title = this.config.name; } if (this.config.version) { spec.info.version = this.config.version; } if (this.config.host) { var url = new URL(this.config.host); var host = (url.host + url.pathname).replace(/([^:]\/)\/+/g, "$1"); host = host.substr(-1, 1) === '/' ? host.substr(0, host.length - 1) : host; spec.host = host; } if (this.config.spec) { spec = require('merge').recursive(spec, this.config.spec); } this.debugger('Generated specs: %j', spec); return spec; }; SpecGenerator.prototype.getOpenApiSpec = function () { return __awaiter(this, void 0, void 0, function () { return __generator(this, function (_a) { switch (_a.label) { case 0: return [4 /*yield*/, this.convertToOpenApiSpec(this.getSwaggerSpec())]; case 1: return [2 /*return*/, _a.sent()]; } }); }); }; SpecGenerator.prototype.convertToOpenApiSpec = function (spec) { return __awaiter(this, void 0, void 0, function () { var converter, options, openapi; return __generator(this, function (_a) { switch (_a.label) { case 0: this.debugger('Converting specs to openapi 3.0'); converter = require('swagger2openapi'); options = { patch: true, warnOnly: true }; return [4 /*yield*/, converter.convertObj(spec, options)]; case 1: openapi = _a.sent(); this.debugger('Converted to openapi 3.0: %j', openapi); return [2 /*return*/, openapi.openapi]; } }); }); }; SpecGenerator.prototype.buildDefinitions = function () { var _this = this; var definitions = {}; Object.keys(this.metadata.referenceTypes).map(function (typeName) { var referenceType = _this.metadata.referenceTypes[typeName]; // const key : string = referenceType.typeName.replace('_', ''); if (type_1.Resolver.isRefObjectType(referenceType)) { var required = referenceType.properties.filter(function (p) { return p.required; }).map(function (p) { return p.name; }); definitions[referenceType.refName] = { description: referenceType.description, properties: _this.buildProperties(referenceType.properties), required: required && required.length > 0 ? Array.from(new Set(required)) : undefined, type: 'object', }; if (referenceType.additionalProperties) { definitions[referenceType.refName].additionalProperties = true; } else { // Since additionalProperties was not explicitly set in the TypeScript interface for this model // ...we need to make a decision definitions[referenceType.refName].additionalProperties = true; } if (referenceType.example) { // @ts-ignore definitions[referenceType.refName].example = referenceType.example; } } else if (type_1.Resolver.isRefEnumType(referenceType)) { definitions[referenceType.refName] = { description: referenceType.description, enum: referenceType.members, type: _this.decideEnumType(referenceType.members, referenceType.refName), }; if (referenceType.memberNames !== undefined && referenceType.members.length === referenceType.memberNames.length) { // @ts-ignore definitions[referenceType.refName]['x-enum-varnames'] = referenceType.memberNames; } } else if (type_1.Resolver.isRefAliasType(referenceType)) { var swaggerType = _this.getSwaggerType(referenceType.type); var format = referenceType.format; var validators = Object.keys(referenceType.validators) .filter(function (key) { return !key.startsWith('is') && key !== 'minDate' && key !== 'maxDate'; }) .reduce(function (acc, key) { var _a; return __assign(__assign({}, acc), (_a = {}, _a[key] = referenceType.validators[key].value, _a)); }, {}); definitions[referenceType.refName] = __assign(__assign(__assign({}, swaggerType), { default: referenceType.default || swaggerType.default, example: referenceType.example, format: format || swaggerType.format, description: referenceType.description }), validators); } else { console.log(referenceType); } }); return definitions; }; SpecGenerator.prototype.buildPaths = function () { var _this = this; var paths = {}; this.debugger('Generating paths declarations'); this.metadata.controllers.forEach(function (controller) { _this.debugger('Generating paths for controller: %s', controller.name); controller.methods.forEach(function (method) { _this.debugger('Generating paths for method: %s', method.name); var path = path_1.posix.join('/', (controller.path ? controller.path : ''), method.path); paths[path] = paths[path] || {}; method.consumes = lodash_1.union(controller.consumes, method.consumes); method.produces = lodash_1.union(controller.produces, method.produces); method.tags = lodash_1.union(controller.tags, method.tags); method.security = method.security || controller.security; method.responses = lodash_1.union(controller.responses, method.responses); var pathObject = paths[path]; pathObject[method.method] = _this.buildPathMethod(controller.name, method); _this.debugger('Generated path for method %s: %j', method.name, pathObject[method.method]); }); }); return paths; }; SpecGenerator.prototype.buildPathMethod = function (controllerName, method) { var _this = this; var pathMethod = this.buildOperation(controllerName, method); pathMethod.description = method.description; if (method.summary) { pathMethod.summary = method.summary; } if (method.deprecated) { pathMethod.deprecated = method.deprecated; } if (method.tags.length) { pathMethod.tags = method.tags; } if (method.security) { pathMethod.security = method.security.map(function (s) { var _a; return (_a = {}, _a[s.name] = s.scopes || [], _a); }); } this.handleMethodConsumes(method, pathMethod); pathMethod.parameters = method.parameters .filter(function (p) { return (p.in !== 'param'); }) .map(function (p) { return _this.buildParameter(p); }); method.parameters .filter(function (p) { return (p.in === 'param'); }) .forEach(function (p) { pathMethod.parameters.push(_this.buildParameter({ description: p.description, in: 'query', name: p.name, parameterName: p.parameterName, required: false, type: p.type })); pathMethod.parameters.push(_this.buildParameter({ description: p.description, in: 'formData', name: p.name, parameterName: p.parameterName, required: false, type: p.type })); }); if (pathMethod.parameters.filter(function (p) { return p.in === 'body'; }).length > 1) { throw new Error('Only one body parameter allowed per controller method.'); } return pathMethod; }; SpecGenerator.prototype.handleMethodConsumes = function (method, pathMethod) { if (method.consumes.length) { pathMethod.consumes = method.consumes; } if ((!pathMethod.consumes || !pathMethod.consumes.length)) { if (method.parameters.some(function (p) { return (p.in === 'formData' && p.type.typeName === 'file'); })) { pathMethod.consumes = pathMethod.consumes || []; pathMethod.consumes.push('multipart/form-data'); } else if (this.hasFormParams(method)) { pathMethod.consumes = pathMethod.consumes || []; pathMethod.consumes.push('application/x-www-form-urlencoded'); } else if (this.supportsBodyParameters(method.method)) { pathMethod.consumes = pathMethod.consumes || []; pathMethod.consumes.push('application/json'); } } }; SpecGenerator.prototype.hasFormParams = function (method) { return method.parameters.find(function (p) { return (p.in === 'formData'); }); }; SpecGenerator.prototype.supportsBodyParameters = function (method) { return ['post', 'put', 'patch'].some(function (m) { return m === method; }); }; SpecGenerator.prototype.buildParameter = function (parameter) { var swaggerParameter = { description: parameter.description, in: parameter.in, name: parameter.name, required: parameter.required }; var parameterType = this.getSwaggerType(parameter.type); if (parameterType.$ref || parameter.in === 'body') { swaggerParameter.schema = parameterType; } else { swaggerParameter.type = parameterType.type; if (parameterType.items) { swaggerParameter.items = parameterType.items; if (parameter.collectionFormat || this.config.collectionFormat) { swaggerParameter.collectionFormat = parameter.collectionFormat || this.config.collectionFormat; } } } if (parameterType.format) { swaggerParameter.format = parameterType.format; } if (parameter.default !== undefined) { swaggerParameter.default = parameter.default; } if (parameterType.enum) { swaggerParameter.enum = parameterType.enum; } return swaggerParameter; }; SpecGenerator.prototype.buildProperties = function (properties) { var _this = this; var swaggerProperties = {}; properties.forEach(function (property) { var swaggerType = _this.getSwaggerType(property.type); if (!swaggerType.$ref) { swaggerType.description = property.description; } swaggerProperties[property.name] = swaggerType; }); return swaggerProperties; }; SpecGenerator.prototype.decideEnumType = function (anEnum, nameOfEnum) { var typesUsedInEnum = this.determineTypesUsedInEnum(anEnum); var badEnumErrorMessage = function () { var valuesDelimited = Array.from(typesUsedInEnum).join(','); return "Enums can only have string or number values, but enum " + nameOfEnum + " had " + valuesDelimited; }; var enumTypeForSwagger = 'string'; if (typesUsedInEnum.has('string') && typesUsedInEnum.size === 1) { enumTypeForSwagger = 'string'; } else if (typesUsedInEnum.has('number') && typesUsedInEnum.size === 1) { enumTypeForSwagger = 'number'; } else if (typesUsedInEnum.size === 2 && typesUsedInEnum.has('number') && typesUsedInEnum.has('string')) { enumTypeForSwagger = 'string'; } else { throw new Error(badEnumErrorMessage()); } return enumTypeForSwagger; }; SpecGenerator.prototype.buildOperation = function (controllerName, method) { var _this = this; var operation = { operationId: this.getOperationId(controllerName, method.name), produces: [], responses: {} }; var methodReturnTypes = new Set(); method.responses.forEach(function (res) { operation.responses[res.status] = { description: res.description }; if (res.schema) { var swaggerType = _this.getSwaggerType(res.schema); if (swaggerType.type !== 'void') { operation.responses[res.status]['schema'] = swaggerType; } methodReturnTypes.add(_this.getMimeType(swaggerType)); } if (res.examples) { operation.responses[res.status]['examples'] = { 'application/json': res.examples }; } }); this.handleMethodProduces(method, operation, methodReturnTypes); return operation; }; SpecGenerator.prototype.getMimeType = function (swaggerType) { if (swaggerType.$ref || swaggerType.type === 'array' || swaggerType.type === 'object') { return 'application/json'; } else if (swaggerType.type === 'string' && swaggerType.format === 'binary') { return 'application/octet-stream'; } else { return 'text/html'; } }; SpecGenerator.prototype.handleMethodProduces = function (method, operation, methodReturnTypes) { if (method.produces.length) { operation.produces = method.produces; } else if (methodReturnTypes && methodReturnTypes.size > 0) { operation.produces = Array.from(methodReturnTypes); } }; SpecGenerator.prototype.getOperationId = function (controllerName, methodName) { var controllerNameWithoutSuffix = controllerName.replace(new RegExp('Controller$'), ''); return "" + controllerNameWithoutSuffix + (methodName.charAt(0).toUpperCase() + methodName.substr(1)); }; SpecGenerator.prototype.getSwaggerType = function (type) { if (type_1.Resolver.isVoidType(type)) { return {}; } else if (type_1.Resolver.isReferenceType(type)) { return this.getSwaggerTypeForReferenceType(type); } else if (type.typeName === 'any' || type.typeName === 'binary' || type.typeName === 'boolean' || type.typeName === 'buffer' || type.typeName === 'byte' || type.typeName === 'date' || type.typeName === 'datetime' || type.typeName === 'double' || type.typeName === 'float' || type.typeName === 'file' || type.typeName === 'integer' || type.typeName === 'long' || type.typeName === 'object' || type.typeName === 'string') { return this.getSwaggerTypeForPrimitiveType(type.typeName); } else if (type_1.Resolver.isArrayType(type)) { return this.getSwaggerTypeForArrayType(type); } else if (type_1.Resolver.isEnumType(type)) { return this.getSwaggerTypeForEnumType(type); } else if (type_1.Resolver.isUnionType(type)) { return this.getSwaggerTypeForUnionType(type); } else if (type_1.Resolver.isIntersectionType(type)) { return this.getSwaggerTypeForIntersectionType(type); } else if (type_1.Resolver.isNestedObjectLiteralType(type)) { return this.getSwaggerTypeForObjectLiteral(type); } else { console.log(type); } return {}; }; SpecGenerator.prototype.isNull = function (type) { return type_1.Resolver.isEnumType(type) && type.members.length === 1 && type.members[0] === null; }; SpecGenerator.prototype.getSwaggerTypeForUnionType = function (type) { if (type.members.every(function (subType) { return subType.typeName === 'enum'; })) { var mergedEnum_1 = { typeName: 'enum', members: [] }; type.members.forEach(function (t) { mergedEnum_1.members = __spreadArray(__spreadArray([], mergedEnum_1.members), t.members); }); return this.getSwaggerTypeForEnumType(mergedEnum_1); } else if (type.members.length === 2 && type.members.find(function (typeInUnion) { return typeInUnion.typeName === 'enum' && typeInUnion.members.includes(null); })) { // Backwards compatible representation of dataType or null, $ref does not allow any sibling attributes, so we have to bail out var nullEnumIndex = type.members.findIndex(function (a) { return type_1.Resolver.isEnumType(a) && a.members.includes(null); }); var typeIndex = nullEnumIndex === 1 ? 0 : 1; var swaggerType = this.getSwaggerType(type.members[typeIndex]); var isRef = !!swaggerType.$ref; if (isRef) { return { type: 'object' }; } else { // @ts-ignore swaggerType['x-nullable'] = true; return swaggerType; } } if (type.members.length === 2) { var index = type.members.findIndex(function (member) { return type_1.Resolver.isArrayType(member); }); if (index !== -1) { var otherIndex = index === 0 ? 1 : 0; if (type.members[index].elementType.typeName === type.members[otherIndex].typeName) { return this.getSwaggerType(type.members[otherIndex]); } } index = type.members.findIndex(function (member) { return type_1.Resolver.isAnyType(member); }); if (index !== -1) { var otherIndex = index === 0 ? 1 : 0; if (type_1.Resolver.isAnyType(type.members[index])) { return this.getSwaggerType(type.members[otherIndex]); } } } return { type: 'object' }; }; SpecGenerator.prototype.getSwaggerTypeForPrimitiveType = function (type) { var map = { any: { // While the any type is discouraged, it does explicitly allows anything, so it should always allow additionalProperties additionalProperties: true, }, binary: { type: 'string', format: 'binary' }, boolean: { type: 'boolean' }, buffer: { type: 'string', format: 'byte' }, byte: { type: 'string', format: 'byte' }, date: { type: 'string', format: 'date' }, datetime: { type: 'string', format: 'date-time' }, double: { type: 'number', format: 'double' }, file: { type: 'file' }, float: { type: 'number', format: 'float' }, integer: { type: 'integer', format: 'int32' }, long: { type: 'integer', format: 'int64' }, object: { additionalProperties: true, type: 'object', }, string: { type: 'string' }, }; return map[type]; }; SpecGenerator.prototype.getSwaggerTypeForArrayType = function (arrayType) { return { type: 'array', items: this.getSwaggerType(arrayType.elementType) }; }; SpecGenerator.prototype.getSwaggerTypeForIntersectionType = function (type) { var _this = this; return { allOf: type.members.map(function (x) { return _this.getSwaggerType(x); }) }; }; SpecGenerator.prototype.getSwaggerTypeForEnumType = function (enumType) { var types = this.determineTypesUsedInEnum(enumType.members); if (types.size === 1) { var type = types.values().next().value; var nullable = !!enumType.members.includes(null); return { type: type, enum: enumType.members.map(function (member) { return (member === null ? null : String(member)); }), nullable: nullable }; } else { var valuesDelimited = Array.from(types).join(','); throw new Error("Enums can only have string or number values, but enum had " + valuesDelimited); } }; SpecGenerator.prototype.getSwaggerTypeForObjectLiteral = function (objectLiteral) { var properties = this.buildProperties(objectLiteral.properties); var additionalProperties = objectLiteral.additionalProperties && this.getSwaggerType(objectLiteral.additionalProperties); var required = objectLiteral.properties.filter(function (prop) { return prop.required; }).map(function (prop) { return prop.name; }); // An empty list required: [] is not valid. // If all properties are optional, do not specify the required keyword. return __assign(__assign(__assign({ properties: properties }, (additionalProperties && { additionalProperties: additionalProperties })), (required && required.length && { required: required })), { type: 'object' }); }; SpecGenerator.prototype.getSwaggerTypeForReferenceType = function (referenceType) { return { $ref: "#/definitions/" + referenceType.refName }; }; SpecGenerator.prototype.determineTypesUsedInEnum = function (anEnum) { return anEnum.reduce(function (theSet, curr) { var typeUsed = curr === null ? 'number' : typeof curr; theSet.add(typeUsed); return theSet; }, new Set()); }; return SpecGenerator; }()); exports.SpecGenerator = SpecGenerator; //# sourceMappingURL=generator.js.map