UNPKG

@namecheap/tsoa-cli

Version:

Build swagger-compliant REST APIs using TypeScript and Node

381 lines 18.3 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.MethodGenerator = void 0; const ts = __importStar(require("typescript")); const path = __importStar(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 !== undefined && this.path !== undefined; } 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); const methodMetadata = { 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, }; this.processCustomDecorators(methodMetadata); return methodMetadata; } buildParameters() { if (!this.IsValid()) { throw new exceptions_1.GenerateMetadataError("This isn't a valid a controller method."); } const fullPath = path.join(this.parentPath || '', this.path); const method = this.method; const parameters = this.node.parameters .map(p => { try { return new parameterGenerator_1.ParameterGenerator(p, method, fullPath, this.current).Generate(); } catch (e) { const methodId = this.node.name; const controllerId = this.node.parent.name; const message = e instanceof Error ? e.message : String(e); throw new exceptions_1.GenerateMetadataError(`${message} \n in '${controllerId.text}.${methodId.text}'`); } }) .reduce((flattened, params) => [...flattened, ...params], []); this.validateBodyParameters(parameters); this.validateQueryParameters(parameters); return parameters; } validateBodyParameters(parameters) { 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 @Body or @BodyProp in '${this.getCurrentLocation()}' method.`); } if (hasBodyParameter && hasFormDataParameters) { throw new Error(`@Body or @BodyProp cannot be used with @FormField, @UploadedFile, or @UploadedFiles in '${this.getCurrentLocation()}' method.`); } } validateQueryParameters(parameters) { const queryParameters = parameters.filter(p => p.in === 'query'); const queriesParameters = parameters.filter(p => p.in === 'queries'); if (queriesParameters.length > 1) { throw new exceptions_1.GenerateMetadataError(`Only one queries parameter allowed in '${this.getCurrentLocation()}' method.`); } if (queriesParameters.length > 0 && queryParameters.length > 0) { throw new exceptions_1.GenerateMetadataError(`Choose either during @Query or @Queries in '${this.getCurrentLocation()}' method.`); } } 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 responseExamplesByName = {}; 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); if (example !== undefined) { responseExamplesByName[name] = responseExamplesByName[name] ? [...responseExamplesByName[name], example] : [example]; } return { description: description || '', examples: responseExamplesByName[name] || undefined, 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?.map(ex => ex.example), exampleLabels: 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?.map(ex => ex.example), exampleLabels: 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) { if (!ts.isCallExpression(expression) || !expression.typeArguments?.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?.length > 1) { throw new exceptions_1.GenerateMetadataError(`Only one NoSecurity decorator allowed in '${this.getCurrentLocation()}' method.`); } if (noSecurityDecorators?.length && securityDecorators?.length) { throw new exceptions_1.GenerateMetadataError(`NoSecurity decorator cannot be used in conjunction with Security decorator in '${this.getCurrentLocation()}' method.`); } if (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; } processCustomDecorators(methodMetadata) { if (!this.current.customDecoratorProcessors) { return; } for (const [decoratorName, processor] of Object.entries(this.current.customDecoratorProcessors)) { const decorators = (0, decoratorUtils_1.getDecorators)(this.node, identifier => identifier.text === decoratorName); for (const decorator of decorators) { try { const context = { methodObject: methodMetadata, decoratorArguments: (0, decoratorUtils_1.getDecoratorValues)(decorator, this.current.typeChecker), }; processor(context); } catch (error) { throw new exceptions_1.GenerateMetadataError(`Error in custom decorator processor for '${decoratorName}'`, this.node); } } } } } exports.MethodGenerator = MethodGenerator; //# sourceMappingURL=methodGenerator.js.map