@namecheap/tsoa-cli
Version:
Build swagger-compliant REST APIs using TypeScript and Node
487 lines • 24.3 kB
JavaScript
;
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.ParameterGenerator = void 0;
const ts = __importStar(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 'RequestProp':
return [this.getRequestPropParameter(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 'Queries':
return [this.getQueriesParameters(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),
};
}
getRequestPropParameter(parameter) {
const parameterName = parameter.name.text;
const type = this.getValidatedType(parameter);
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: 'request-prop',
name: (0, decoratorUtils_1.getNodeFirstDecoratorValue)(this.parameter, this.current.typeChecker, ident => ident.text === 'ParameterProp') || parameterName,
parameterName,
required: !parameter.questionToken && !parameter.initializer,
type,
validators: (0, validatorUtils_1.getParameterValidators)(this.parameter, parameterName),
deprecated: this.getParameterDeprecation(parameter),
};
}
extractTsoaResponse(typeNode) {
if (!typeNode || !ts.isTypeReferenceNode(typeNode)) {
return undefined;
}
if (typeNode.typeName.getText() === 'TsoaResponse') {
return typeNode;
}
const symbol = this.current.typeChecker.getTypeAtLocation(typeNode).aliasSymbol;
if (!symbol || !symbol.declarations) {
return undefined;
}
const declaration = symbol.declarations[0];
if (!ts.isTypeAliasDeclaration(declaration) || !ts.isTypeReferenceNode(declaration.type)) {
return undefined;
}
return declaration.type.typeName.getText() === 'TsoaResponse' ? declaration.type : undefined;
}
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 = this.extractTsoaResponse(parameter.type);
if (!typeNode) {
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) {
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: (0, decoratorUtils_1.getNodeFirstDecoratorValue)(this.parameter, this.current.typeChecker, ident => {
if (isArray) {
return ident.text === 'UploadedFiles';
}
return ident.text === 'UploadedFile';
}) ?? parameterName,
required: !parameter.questionToken && !parameter.initializer,
type,
parameterName,
validators: (0, validatorUtils_1.getParameterValidators)(this.parameter, parameterName),
deprecated: this.getParameterDeprecation(parameter),
};
}
getFormFieldParameter(parameter) {
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: (0, decoratorUtils_1.getNodeFirstDecoratorValue)(this.parameter, this.current.typeChecker, ident => ident.text === 'FormField') ?? parameterName,
required: !parameter.questionToken && !parameter.initializer,
type,
parameterName,
validators: (0, validatorUtils_1.getParameterValidators)(this.parameter, parameterName),
deprecated: this.getParameterDeprecation(parameter),
};
}
getQueriesParameters(parameter) {
const parameterName = parameter.name.text;
const type = this.getValidatedType(parameter);
if (type.dataType !== 'refObject' && type.dataType !== 'nestedObjectLiteral') {
throw new exceptions_1.GenerateMetadataError(`@Queries('${parameterName}') only support 'refObject' or 'nestedObjectLiteral' types. If you want only one query parameter, please use the '@Query' decorator.`);
}
for (const property of type.properties) {
this.validateQueriesProperties(property, parameterName);
}
const { examples: example, exampleLabels } = this.getParameterExample(parameter, parameterName);
return {
description: this.getParameterDescription(parameter),
in: 'queries',
name: parameterName,
example,
exampleLabels,
parameterName,
required: !parameter.questionToken && !parameter.initializer,
type,
validators: (0, validatorUtils_1.getParameterValidators)(this.parameter, parameterName),
deprecated: this.getParameterDeprecation(parameter),
};
}
validateQueriesProperties(property, parentName) {
if (property.type.dataType === 'array') {
const arrayType = property.type;
if (!this.supportPathDataType(arrayType.elementType)) {
throw new exceptions_1.GenerateMetadataError(`@Queries('${parentName}') property '${property.name}' can't support array '${arrayType.elementType.dataType}' type.`);
}
}
else if (!this.supportPathDataType(property.type)) {
throw new exceptions_1.GenerateMetadataError(`@Queries('${parentName}') nested property '${property.name}' Can't support '${property.type.dataType}' type.`);
}
}
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?.startsWith(parameterName);
if (isExample) {
const hasExampleLabel = (comment?.split(' ')[0].indexOf('.') || -1) > 0;
// custom example label is delimited by first '.' and the rest will all be included as example label
exampleLabels.push(hasExampleLabel ? comment?.split(' ')[0].split('.').slice(1).join('.') : undefined);
}
return isExample ?? false;
}).map(tag => ((0, jsDocUtils_1.commentToString)(tag.comment) || '').replace(`${(0, jsDocUtils_1.commentToString)(tag.comment)?.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) {
const message = e instanceof Error ? e.message : String(e);
throw new exceptions_1.GenerateMetadataError(`JSON format is incorrect: ${message}`);
}
}
}
supportBodyMethod(method) {
return ['post', 'put', 'patch', 'delete'].some(m => m === method.toLowerCase());
}
supportParameterDecorator(decoratorName) {
return ['header', 'query', 'queries', 'path', 'body', 'bodyprop', 'request', 'requestprop', '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') {
// skip undefined inside unions
return !parameterType.types.map(t => t.dataType === 'undefined' || 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