typescript-swagger
Version:
Generate Swagger files from a decorator library like typescript-rest or a @decorators/express.
630 lines • 30.6 kB
JavaScript
"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