UNPKG

@namecheap/tsoa-cli

Version:

Build swagger-compliant REST APIs using TypeScript and Node

381 lines 19.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ParameterGenerator = void 0; const ts = require("typescript"); const decoratorUtils_1 = require("./../utils/decoratorUtils"); const jsDocUtils_1 = require("./../utils/jsDocUtils"); const validatorUtils_1 = require("./../utils/validatorUtils"); const exceptions_1 = require("./exceptions"); const initializer_value_1 = require("./initializer-value"); const typeResolver_1 = require("./typeResolver"); const headerTypeHelpers_1 = require("../utils/headerTypeHelpers"); class ParameterGenerator { constructor(parameter, method, path, current) { this.parameter = parameter; this.method = method; this.path = path; this.current = current; } Generate() { const decoratorName = (0, decoratorUtils_1.getNodeFirstDecoratorName)(this.parameter, identifier => this.supportParameterDecorator(identifier.text)); switch (decoratorName) { case 'Request': return [this.getRequestParameter(this.parameter)]; case 'Body': return [this.getBodyParameter(this.parameter)]; case 'BodyProp': return [this.getBodyPropParameter(this.parameter)]; case 'FormField': return [this.getFormFieldParameter(this.parameter)]; case 'Header': return [this.getHeaderParameter(this.parameter)]; case 'Query': return this.getQueryParameters(this.parameter); case 'Path': return [this.getPathParameter(this.parameter)]; case 'Res': return this.getResParameters(this.parameter); case 'Inject': return []; case 'UploadedFile': return [this.getUploadedFileParameter(this.parameter)]; case 'UploadedFiles': return [this.getUploadedFileParameter(this.parameter, true)]; default: return [this.getPathParameter(this.parameter)]; } } getRequestParameter(parameter) { const parameterName = parameter.name.text; return { description: this.getParameterDescription(parameter), in: 'request', name: parameterName, parameterName, required: !parameter.questionToken && !parameter.initializer, type: { dataType: 'object' }, validators: (0, validatorUtils_1.getParameterValidators)(this.parameter, parameterName), deprecated: this.getParameterDeprecation(parameter), }; } getResParameters(parameter) { const parameterName = parameter.name.text; const decorator = (0, decoratorUtils_1.getNodeFirstDecoratorValue)(this.parameter, this.current.typeChecker, ident => ident.text === 'Res') || parameterName; if (!decorator) { throw new exceptions_1.GenerateMetadataError('Could not find Decorator', parameter); } const typeNode = parameter.type; if (!typeNode || !ts.isTypeReferenceNode(typeNode) || typeNode.typeName.getText() !== 'TsoaResponse') { throw new exceptions_1.GenerateMetadataError('@Res() requires the type to be TsoaResponse<HTTPStatusCode, ResBody>', parameter); } if (!typeNode.typeArguments || !typeNode.typeArguments[0]) { throw new exceptions_1.GenerateMetadataError('@Res() requires the type to be TsoaResponse<HTTPStatusCode, ResBody>', parameter); } const statusArgument = typeNode.typeArguments[0]; const bodyArgument = typeNode.typeArguments[1]; // support a union of status codes, all with the same response body const statusArguments = ts.isUnionTypeNode(statusArgument) ? [...statusArgument.types] : [statusArgument]; const statusArgumentTypes = statusArguments.map(a => this.current.typeChecker.getTypeAtLocation(a)); const isNumberLiteralType = (tsType) => { // eslint-disable-next-line no-bitwise return (tsType.getFlags() & ts.TypeFlags.NumberLiteral) !== 0; }; const headers = (0, headerTypeHelpers_1.getHeaderType)(typeNode.typeArguments, 2, this.current); return statusArgumentTypes.map(statusArgumentType => { if (!isNumberLiteralType(statusArgumentType)) { throw new exceptions_1.GenerateMetadataError('@Res() requires the type to be TsoaResponse<HTTPStatusCode, ResBody>', parameter); } const status = String(statusArgumentType.value); const type = new typeResolver_1.TypeResolver(bodyArgument, this.current, typeNode).resolve(); const { examples, exampleLabels } = this.getParameterExample(parameter, parameterName); return { description: this.getParameterDescription(parameter) || '', in: 'res', name: status, produces: headers ? this.getProducesFromResHeaders(headers) : undefined, parameterName, examples, required: true, type, exampleLabels, schema: type, validators: {}, headers, deprecated: this.getParameterDeprecation(parameter), }; }); } getProducesFromResHeaders(headers) { const { properties } = headers; const [contentTypeProp] = (properties || []).filter(p => p.name.toLowerCase() === 'content-type' && p.type.dataType === 'enum'); if (contentTypeProp) { const type = contentTypeProp.type; return type.enums; } return; } getBodyPropParameter(parameter) { const parameterName = parameter.name.text; const type = this.getValidatedType(parameter); if (!this.supportBodyMethod(this.method)) { throw new exceptions_1.GenerateMetadataError(`@BodyProp('${parameterName}') Can't support in ${this.method.toUpperCase()} method.`); } const { examples: example, exampleLabels } = this.getParameterExample(parameter, parameterName); return { default: (0, initializer_value_1.getInitializerValue)(parameter.initializer, this.current.typeChecker, type), description: this.getParameterDescription(parameter), example, exampleLabels, in: 'body-prop', name: (0, decoratorUtils_1.getNodeFirstDecoratorValue)(this.parameter, this.current.typeChecker, ident => ident.text === 'BodyProp') || parameterName, parameterName, required: !parameter.questionToken && !parameter.initializer, type, validators: (0, validatorUtils_1.getParameterValidators)(this.parameter, parameterName), deprecated: this.getParameterDeprecation(parameter), }; } getBodyParameter(parameter) { const parameterName = parameter.name.text; const type = this.getValidatedType(parameter); if (!this.supportBodyMethod(this.method)) { throw new exceptions_1.GenerateMetadataError(`@Body('${parameterName}') Can't support in ${this.method.toUpperCase()} method.`); } const { examples: example, exampleLabels } = this.getParameterExample(parameter, parameterName); return { description: this.getParameterDescription(parameter), in: 'body', name: parameterName, example, exampleLabels, parameterName, required: !parameter.questionToken && !parameter.initializer, type, validators: (0, validatorUtils_1.getParameterValidators)(this.parameter, parameterName), deprecated: this.getParameterDeprecation(parameter), }; } getHeaderParameter(parameter) { const parameterName = parameter.name.text; const type = this.getValidatedType(parameter); if (!this.supportPathDataType(type)) { throw new exceptions_1.GenerateMetadataError(`@Header('${parameterName}') Can't support '${type.dataType}' type.`); } const { examples: example, exampleLabels } = this.getParameterExample(parameter, parameterName); return { default: (0, initializer_value_1.getInitializerValue)(parameter.initializer, this.current.typeChecker, type), description: this.getParameterDescription(parameter), example, exampleLabels, in: 'header', name: (0, decoratorUtils_1.getNodeFirstDecoratorValue)(this.parameter, this.current.typeChecker, ident => ident.text === 'Header') || parameterName, parameterName, required: !parameter.questionToken && !parameter.initializer, type, validators: (0, validatorUtils_1.getParameterValidators)(this.parameter, parameterName), deprecated: this.getParameterDeprecation(parameter), }; } getUploadedFileParameter(parameter, isArray) { var _a; const parameterName = parameter.name.text; const elementType = { dataType: 'file' }; let type; if (isArray) { type = { dataType: 'array', elementType }; } else { type = elementType; } if (!this.supportPathDataType(elementType)) { throw new exceptions_1.GenerateMetadataError(`Parameter '${parameterName}:${type.dataType}' can't be passed as an uploaded file(s) parameter in '${this.method.toUpperCase()}'.`, parameter); } return { description: this.getParameterDescription(parameter), in: 'formData', name: (_a = (0, decoratorUtils_1.getNodeFirstDecoratorValue)(this.parameter, this.current.typeChecker, ident => { if (isArray) { return ident.text === 'UploadedFiles'; } return ident.text === 'UploadedFile'; })) !== null && _a !== void 0 ? _a : parameterName, required: !parameter.questionToken && !parameter.initializer, type, parameterName, validators: (0, validatorUtils_1.getParameterValidators)(this.parameter, parameterName), deprecated: this.getParameterDeprecation(parameter), }; } getFormFieldParameter(parameter) { var _a; const parameterName = parameter.name.text; const type = { dataType: 'string' }; if (!this.supportPathDataType(type)) { throw new exceptions_1.GenerateMetadataError(`Parameter '${parameterName}:${type.dataType}' can't be passed as form field parameter in '${this.method.toUpperCase()}'.`, parameter); } return { description: this.getParameterDescription(parameter), in: 'formData', name: (_a = (0, decoratorUtils_1.getNodeFirstDecoratorValue)(this.parameter, this.current.typeChecker, ident => ident.text === 'FormField')) !== null && _a !== void 0 ? _a : parameterName, required: !parameter.questionToken && !parameter.initializer, type, parameterName, validators: (0, validatorUtils_1.getParameterValidators)(this.parameter, parameterName), deprecated: this.getParameterDeprecation(parameter), }; } getQueryParameters(parameter) { const parameterName = parameter.name.text; const type = this.getValidatedType(parameter); const { examples: example, exampleLabels } = this.getParameterExample(parameter, parameterName); const commonProperties = { default: (0, initializer_value_1.getInitializerValue)(parameter.initializer, this.current.typeChecker, type), description: this.getParameterDescription(parameter), example, exampleLabels, in: 'query', name: (0, decoratorUtils_1.getNodeFirstDecoratorValue)(this.parameter, this.current.typeChecker, ident => ident.text === 'Query') || parameterName, parameterName, required: !parameter.questionToken && !parameter.initializer, validators: (0, validatorUtils_1.getParameterValidators)(this.parameter, parameterName), deprecated: this.getParameterDeprecation(parameter), }; if (this.getQueryParameterIsHidden(parameter)) { if (commonProperties.required) { throw new exceptions_1.GenerateMetadataError(`@Query('${parameterName}') Can't support @Hidden because it is required (does not allow undefined and does not have a default value).`); } return []; } if (type.dataType === 'array') { const arrayType = type; if (!this.supportPathDataType(arrayType.elementType)) { throw new exceptions_1.GenerateMetadataError(`@Query('${parameterName}') Can't support array '${arrayType.elementType.dataType}' type.`); } return [ { ...commonProperties, collectionFormat: 'multi', type: arrayType, }, ]; } if (!this.supportPathDataType(type)) { throw new exceptions_1.GenerateMetadataError(`@Query('${parameterName}') Can't support '${type.dataType}' type.`); } return [ { ...commonProperties, type, }, ]; } getPathParameter(parameter) { const parameterName = parameter.name.text; const type = this.getValidatedType(parameter); const pathName = String((0, decoratorUtils_1.getNodeFirstDecoratorValue)(this.parameter, this.current.typeChecker, ident => ident.text === 'Path') || parameterName); if (!this.supportPathDataType(type)) { throw new exceptions_1.GenerateMetadataError(`@Path('${parameterName}') Can't support '${type.dataType}' type.`); } if (!this.path.includes(`{${pathName}}`) && !this.path.includes(`:${pathName}`)) { throw new exceptions_1.GenerateMetadataError(`@Path('${parameterName}') Can't match in URL: '${this.path}'.`); } const { examples, exampleLabels } = this.getParameterExample(parameter, parameterName); return { default: (0, initializer_value_1.getInitializerValue)(parameter.initializer, this.current.typeChecker, type), description: this.getParameterDescription(parameter), example: examples, exampleLabels, in: 'path', name: pathName, parameterName, required: true, type, validators: (0, validatorUtils_1.getParameterValidators)(this.parameter, parameterName), deprecated: this.getParameterDeprecation(parameter), }; } getParameterDescription(node) { const symbol = this.current.typeChecker.getSymbolAtLocation(node.name); if (!symbol) { return undefined; } const comments = symbol.getDocumentationComment(this.current.typeChecker); if (comments.length) { return ts.displayPartsToString(comments); } return undefined; } getParameterDeprecation(node) { return (0, jsDocUtils_1.isExistJSDocTag)(node, tag => tag.tagName.text === 'deprecated') || (0, decoratorUtils_1.isDecorator)(node, identifier => identifier.text === 'Deprecated'); } getParameterExample(node, parameterName) { const exampleLabels = []; const examples = (0, jsDocUtils_1.getJSDocTags)(node.parent, tag => { const comment = (0, jsDocUtils_1.commentToString)(tag.comment); const isExample = (tag.tagName.text === 'example' || tag.tagName.escapedText === 'example') && !!tag.comment && (comment === null || comment === void 0 ? void 0 : comment.startsWith(parameterName)); const hasExampleLabel = ((comment === null || comment === void 0 ? void 0 : comment.indexOf('.')) || -1) > 0; if (isExample) { // custom example label is delimited by first '.' and the rest will all be included as example label exampleLabels.push(hasExampleLabel ? comment === null || comment === void 0 ? void 0 : comment.split(' ')[0].split('.').slice(1).join('.') : undefined); } return isExample !== null && isExample !== void 0 ? isExample : false; }).map(tag => { var _a; return ((0, jsDocUtils_1.commentToString)(tag.comment) || '').replace(`${((_a = (0, jsDocUtils_1.commentToString)(tag.comment)) === null || _a === void 0 ? void 0 : _a.split(' ')[0]) || ''}`, '').replace(/\r/g, ''); }); if (examples.length === 0) { return { examples: undefined, exampleLabels: undefined, }; } else { try { return { examples: examples.map(example => JSON.parse(example)), exampleLabels, }; } catch (e) { throw new exceptions_1.GenerateMetadataError(`JSON format is incorrect: ${String(e.message)}`); } } } supportBodyMethod(method) { return ['post', 'put', 'patch', 'delete'].some(m => m === method.toLowerCase()); } supportParameterDecorator(decoratorName) { return ['header', 'query', 'path', 'body', 'bodyprop', 'request', 'res', 'inject', 'uploadedfile', 'uploadedfiles', 'formfield'].some(d => d === decoratorName.toLocaleLowerCase()); } supportPathDataType(parameterType) { const supportedPathDataTypes = ['string', 'integer', 'long', 'float', 'double', 'date', 'datetime', 'buffer', 'boolean', 'enum', 'refEnum', 'file', 'any']; if (supportedPathDataTypes.find(t => t === parameterType.dataType)) { return true; } if (parameterType.dataType === 'refAlias') { return this.supportPathDataType(parameterType.type); } if (parameterType.dataType === 'union') { return !parameterType.types.map(t => this.supportPathDataType(t)).some(t => t === false); } return false; } getValidatedType(parameter) { let typeNode = parameter.type; if (!typeNode) { const type = this.current.typeChecker.getTypeAtLocation(parameter); typeNode = this.current.typeChecker.typeToTypeNode(type, undefined, ts.NodeBuilderFlags.NoTruncation); } return new typeResolver_1.TypeResolver(typeNode, this.current, parameter).resolve(); } getQueryParameterIsHidden(parameter) { const hiddenDecorators = (0, decoratorUtils_1.getDecorators)(parameter, identifier => identifier.text === 'Hidden'); if (!hiddenDecorators || !hiddenDecorators.length) { return false; } if (hiddenDecorators.length > 1) { const parameterName = parameter.name.text; throw new exceptions_1.GenerateMetadataError(`Only one Hidden decorator allowed on @Query('${parameterName}').`); } return true; } } exports.ParameterGenerator = ParameterGenerator; //# sourceMappingURL=parameterGenerator.js.map