@namecheap/tsoa-cli
Version:
Build swagger-compliant REST APIs using TypeScript and Node
304 lines • 15.3 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.MethodGenerator = void 0;
const ts = require("typescript");
const path = require("path");
const isVoidType_1 = require("../utils/isVoidType");
const decoratorUtils_1 = require("./../utils/decoratorUtils");
const jsDocUtils_1 = require("./../utils/jsDocUtils");
const extension_1 = require("./extension");
const exceptions_1 = require("./exceptions");
const parameterGenerator_1 = require("./parameterGenerator");
const typeResolver_1 = require("./typeResolver");
const headerTypeHelpers_1 = require("../utils/headerTypeHelpers");
class MethodGenerator {
constructor(node, current, commonResponses, parentPath, parentTags, parentSecurity, isParentHidden) {
this.node = node;
this.current = current;
this.commonResponses = commonResponses;
this.parentPath = parentPath;
this.parentTags = parentTags;
this.parentSecurity = parentSecurity;
this.isParentHidden = isParentHidden;
this.processMethodDecorators();
}
IsValid() {
return !!this.method;
}
Generate() {
if (!this.IsValid()) {
throw new exceptions_1.GenerateMetadataError("This isn't a valid a controller method.");
}
let nodeType = this.node.type;
if (!nodeType) {
const typeChecker = this.current.typeChecker;
const signature = typeChecker.getSignatureFromDeclaration(this.node);
const implicitType = typeChecker.getReturnTypeOfSignature(signature);
nodeType = typeChecker.typeToTypeNode(implicitType, undefined, ts.NodeBuilderFlags.NoTruncation);
}
const type = new typeResolver_1.TypeResolver(nodeType, this.current).resolve();
const responses = this.commonResponses.concat(this.getMethodResponses());
const { response: successResponse, status: successStatus } = this.getMethodSuccessResponse(type);
responses.push(successResponse);
const parameters = this.buildParameters();
const additionalResponses = parameters.filter((p) => p.in === 'res');
responses.push(...additionalResponses);
return {
extensions: this.getExtensions(),
deprecated: this.getIsDeprecated(),
description: (0, jsDocUtils_1.getJSDocDescription)(this.node),
isHidden: this.getIsHidden(),
method: this.method,
name: this.node.name.text,
operationId: this.getOperationId(),
parameters,
path: this.path,
produces: this.produces,
consumes: this.consumes,
responses,
successStatus,
security: this.getSecurity(),
summary: (0, jsDocUtils_1.getJSDocComment)(this.node, 'summary'),
tags: this.getTags(),
type,
};
}
buildParameters() {
const fullPath = path.join(this.parentPath || '', this.path);
const parameters = this.node.parameters
.map(p => {
try {
return new parameterGenerator_1.ParameterGenerator(p, this.method, fullPath, this.current).Generate();
}
catch (e) {
const methodId = this.node.name;
const controllerId = this.node.parent.name;
throw new exceptions_1.GenerateMetadataError(`${String(e.message)} \n in '${controllerId.text}.${methodId.text}'`);
}
})
.reduce((flattened, params) => [...flattened, ...params], []);
const bodyParameters = parameters.filter(p => p.in === 'body');
const bodyProps = parameters.filter(p => p.in === 'body-prop');
const hasFormDataParameters = parameters.some(p => p.in === 'formData');
const hasBodyParameter = bodyProps.length + bodyParameters.length > 0;
if (bodyParameters.length > 1) {
throw new exceptions_1.GenerateMetadataError(`Only one body parameter allowed in '${this.getCurrentLocation()}' method.`);
}
if (bodyParameters.length > 0 && bodyProps.length > 0) {
throw new exceptions_1.GenerateMetadataError(`Choose either during or in '${this.getCurrentLocation()}' method.`);
}
if (hasBodyParameter && hasFormDataParameters) {
throw new Error(` or cannot be used with , , or in '${this.getCurrentLocation()}' method.`);
}
return parameters;
}
getExtensions() {
const extensionDecorators = this.getDecoratorsByIdentifier(this.node, 'Extension');
if (!extensionDecorators || !extensionDecorators.length) {
return [];
}
return (0, extension_1.getExtensions)(extensionDecorators, this.current);
}
getCurrentLocation() {
const methodId = this.node.name;
const controllerId = this.node.parent.name;
return `${controllerId.text}.${methodId.text}`;
}
processMethodDecorators() {
const pathDecorators = (0, decoratorUtils_1.getDecorators)(this.node, identifier => this.supportsPathMethod(identifier.text));
if (!pathDecorators || !pathDecorators.length) {
return;
}
if (pathDecorators.length > 1) {
throw new exceptions_1.GenerateMetadataError(`Only one path decorator in '${this.getCurrentLocation()}' method, Found: ${pathDecorators.map(d => d.text).join(', ')}`);
}
const decorator = pathDecorators[0];
this.method = decorator.text.toLowerCase();
// if you don't pass in a path to the method decorator, we'll just use the base route
// todo: what if someone has multiple no argument methods of the same type in a single controller?
// we need to throw an error there
this.path = (0, decoratorUtils_1.getPath)(decorator, this.current.typeChecker);
this.produces = this.getProduces();
this.consumes = this.getConsumes();
}
getProduces() {
const produces = (0, decoratorUtils_1.getProduces)(this.node, this.current.typeChecker);
return produces.length ? produces : undefined;
}
getConsumes() {
const consumesDecorators = this.getDecoratorsByIdentifier(this.node, 'Consumes');
if (!consumesDecorators || !consumesDecorators.length) {
return;
}
if (consumesDecorators.length > 1) {
throw new exceptions_1.GenerateMetadataError(`Only one Consumes decorator in '${this.getCurrentLocation()}' method, Found: ${consumesDecorators.map(d => d.text).join(', ')}`);
}
const [decorator] = consumesDecorators;
const [consumes] = (0, decoratorUtils_1.getDecoratorValues)(decorator, this.current.typeChecker);
return consumes;
}
getMethodResponses() {
const decorators = this.getDecoratorsByIdentifier(this.node, 'Response');
if (!decorators || !decorators.length) {
return [];
}
return decorators.map(decorator => {
const [name, description, example, produces] = (0, decoratorUtils_1.getDecoratorValues)(decorator, this.current.typeChecker);
return {
description: description || '',
examples: example === undefined ? undefined : [example],
name: name || '200',
produces: this.getProducesAdapter(produces),
schema: this.getSchemaFromDecorator(decorator, 0),
headers: this.getHeadersFromDecorator(decorator, 1),
};
});
}
getMethodSuccessResponse(type) {
const decorators = this.getDecoratorsByIdentifier(this.node, 'SuccessResponse');
const examplesWithLabels = this.getMethodSuccessExamples();
if (!decorators || !decorators.length) {
const returnsDescription = (0, jsDocUtils_1.getJSDocComment)(this.node, 'returns') || 'Ok';
return {
response: {
description: (0, isVoidType_1.isVoidType)(type) ? 'No content' : returnsDescription,
examples: examplesWithLabels === null || examplesWithLabels === void 0 ? void 0 : examplesWithLabels.map(ex => ex.example),
exampleLabels: examplesWithLabels === null || examplesWithLabels === void 0 ? void 0 : examplesWithLabels.map(ex => ex.label),
name: (0, isVoidType_1.isVoidType)(type) ? '204' : '200',
produces: this.produces,
schema: type,
},
};
}
if (decorators.length > 1) {
throw new exceptions_1.GenerateMetadataError(`Only one SuccessResponse decorator allowed in '${this.getCurrentLocation()}' method.`);
}
const [firstDecorator] = decorators;
const [name, description, produces] = (0, decoratorUtils_1.getDecoratorValues)(firstDecorator, this.current.typeChecker);
const headers = this.getHeadersFromDecorator(firstDecorator, 0);
return {
response: {
description: description || '',
examples: examplesWithLabels === null || examplesWithLabels === void 0 ? void 0 : examplesWithLabels.map(ex => ex.example),
exampleLabels: examplesWithLabels === null || examplesWithLabels === void 0 ? void 0 : examplesWithLabels.map(ex => ex.label),
name: name || '200',
produces: this.getProducesAdapter(produces),
schema: type,
headers,
},
status: name && /^\d+$/.test(name) ? parseInt(name, 10) : undefined,
};
}
getHeadersFromDecorator({ parent: expression }, headersIndex) {
if (!ts.isCallExpression(expression)) {
return undefined;
}
return (0, headerTypeHelpers_1.getHeaderType)(expression.typeArguments, headersIndex, this.current);
}
getSchemaFromDecorator({ parent: expression }, schemaIndex) {
var _a;
if (!ts.isCallExpression(expression) || !((_a = expression.typeArguments) === null || _a === void 0 ? void 0 : _a.length)) {
return undefined;
}
return new typeResolver_1.TypeResolver(expression.typeArguments[schemaIndex], this.current).resolve();
}
getMethodSuccessExamples() {
const exampleDecorators = this.getDecoratorsByIdentifier(this.node, 'Example');
if (!exampleDecorators || !exampleDecorators.length) {
return undefined;
}
const examples = exampleDecorators.map(exampleDecorator => {
const [example, label] = (0, decoratorUtils_1.getDecoratorValues)(exampleDecorator, this.current.typeChecker);
return { example, label };
});
return examples || undefined;
}
supportsPathMethod(method) {
return ['options', 'get', 'post', 'put', 'patch', 'delete', 'head'].some(m => m === method.toLowerCase());
}
getIsDeprecated() {
if ((0, jsDocUtils_1.isExistJSDocTag)(this.node, tag => tag.tagName.text === 'deprecated')) {
return true;
}
const depDecorators = this.getDecoratorsByIdentifier(this.node, 'Deprecated');
if (!depDecorators || !depDecorators.length) {
return false;
}
if (depDecorators.length > 1) {
throw new exceptions_1.GenerateMetadataError(`Only one Deprecated decorator allowed in '${this.getCurrentLocation()}' method.`);
}
return true;
}
getOperationId() {
const opDecorators = this.getDecoratorsByIdentifier(this.node, 'OperationId');
if (!opDecorators || !opDecorators.length) {
return undefined;
}
if (opDecorators.length > 1) {
throw new exceptions_1.GenerateMetadataError(`Only one OperationId decorator allowed in '${this.getCurrentLocation()}' method.`);
}
const values = (0, decoratorUtils_1.getDecoratorValues)(opDecorators[0], this.current.typeChecker);
return values && values[0];
}
getTags() {
const tagsDecorators = this.getDecoratorsByIdentifier(this.node, 'Tags');
if (!tagsDecorators || !tagsDecorators.length) {
return this.parentTags;
}
if (tagsDecorators.length > 1) {
throw new exceptions_1.GenerateMetadataError(`Only one Tags decorator allowed in '${this.getCurrentLocation()}' method.`);
}
const tags = (0, decoratorUtils_1.getDecoratorValues)(tagsDecorators[0], this.current.typeChecker);
if (tags && this.parentTags) {
tags.push(...this.parentTags);
}
return tags;
}
getSecurity() {
if (this.current.securityGenerator) {
return this.current.securityGenerator(this.node, this.current.typeChecker, this.parentSecurity);
}
const noSecurityDecorators = this.getDecoratorsByIdentifier(this.node, 'NoSecurity');
const securityDecorators = this.getDecoratorsByIdentifier(this.node, 'Security');
if ((noSecurityDecorators === null || noSecurityDecorators === void 0 ? void 0 : noSecurityDecorators.length) > 1) {
throw new exceptions_1.GenerateMetadataError(`Only one NoSecurity decorator allowed in '${this.getCurrentLocation()}' method.`);
}
if ((noSecurityDecorators === null || noSecurityDecorators === void 0 ? void 0 : noSecurityDecorators.length) && (securityDecorators === null || securityDecorators === void 0 ? void 0 : securityDecorators.length)) {
throw new exceptions_1.GenerateMetadataError(`NoSecurity decorator cannot be used in conjunction with Security decorator in '${this.getCurrentLocation()}' method.`);
}
if (noSecurityDecorators === null || noSecurityDecorators === void 0 ? void 0 : noSecurityDecorators.length) {
return [];
}
if (!securityDecorators || !securityDecorators.length) {
return this.parentSecurity || [];
}
return securityDecorators.map(d => (0, decoratorUtils_1.getSecurites)(d, this.current.typeChecker));
}
getIsHidden() {
const hiddenDecorators = this.getDecoratorsByIdentifier(this.node, 'Hidden');
if (!hiddenDecorators || !hiddenDecorators.length) {
return !!this.isParentHidden;
}
if (this.isParentHidden) {
throw new exceptions_1.GenerateMetadataError(`Hidden decorator cannot be set on '${this.getCurrentLocation()}' it is already defined on the controller`);
}
if (hiddenDecorators.length > 1) {
throw new exceptions_1.GenerateMetadataError(`Only one Hidden decorator allowed in '${this.getCurrentLocation()}' method.`);
}
return true;
}
getDecoratorsByIdentifier(node, id) {
return (0, decoratorUtils_1.getDecorators)(node, identifier => identifier.text === id);
}
getProducesAdapter(produces) {
if (Array.isArray(produces)) {
return produces;
}
else if (typeof produces === 'string') {
return [produces];
}
return;
}
}
exports.MethodGenerator = MethodGenerator;
//# sourceMappingURL=methodGenerator.js.map