@namecheap/tsoa-cli
Version:
Build swagger-compliant REST APIs using TypeScript and Node
225 lines • 11.9 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.MetadataGenerator = void 0;
const minimatch_1 = require("minimatch");
const typescript_1 = require("typescript");
const decoratorUtils_1 = require("../utils/decoratorUtils");
const importClassesFromDirectories_1 = require("../utils/importClassesFromDirectories");
const controllerGenerator_1 = require("./controllerGenerator");
const exceptions_1 = require("./exceptions");
const typeResolver_1 = require("./typeResolver");
class MetadataGenerator {
constructor(entryFile, compilerOptions, ignorePaths, controllers, rootSecurity = [], defaultNumberType = 'double', esm = false, generatorOptions) {
this.compilerOptions = compilerOptions;
this.ignorePaths = ignorePaths;
this.rootSecurity = rootSecurity;
this.defaultNumberType = defaultNumberType;
this.controllerNodes = new Array();
this.referenceTypeMap = {};
this.modelDefinitionPosMap = {};
this.expressionOrigNameMap = {};
this.checkForMethodSignatureDuplicates = (controllers) => {
const map = {};
controllers.forEach(controller => {
controller.methods.forEach(method => {
const signature = method.path ? `@${method.method}(${controller.path}/${method.path})` : `@${method.method}(${controller.path})`;
const methodDescription = `${controller.name}#${method.name}`;
if (map[signature]) {
map[signature].push(methodDescription);
}
else {
map[signature] = [methodDescription];
}
});
});
let message = '';
Object.keys(map).forEach(signature => {
const controllers = map[signature];
if (controllers.length > 1) {
message += `Duplicate method signature ${signature} found in controllers: ${controllers.join(', ')}\n`;
}
});
if (message) {
throw new exceptions_1.GenerateMetadataError(message);
}
};
this.checkForPathParamSignatureDuplicates = (controllers) => {
const paramRegExp = new RegExp('{(\\w*)}|:(\\w+)', 'g');
let PathDuplicationType;
(function (PathDuplicationType) {
PathDuplicationType[PathDuplicationType["FULL"] = 0] = "FULL";
PathDuplicationType[PathDuplicationType["PARTIAL"] = 1] = "PARTIAL";
})(PathDuplicationType || (PathDuplicationType = {}));
const collisions = [];
function addCollision(type, method, controller, collidesWith) {
let existingCollision = collisions.find(collision => collision.type === type && collision.method === method && collision.controller === controller);
if (!existingCollision) {
existingCollision = {
type,
method,
controller,
collidesWith: [],
};
collisions.push(existingCollision);
}
existingCollision.collidesWith.push(collidesWith);
}
controllers.forEach(controller => {
const methodRouteGroup = {};
// Group all ts methods with HTTP method decorator into same object in same controller.
controller.methods.forEach(method => {
if (methodRouteGroup[method.method] === undefined) {
methodRouteGroup[method.method] = [];
}
const params = method.path.match(paramRegExp);
methodRouteGroup[method.method].push({
method, // method.name + ": " + method.path) as any,
path: params?.reduce((s, a) => {
// replace all params with {} placeholder for comparison
return s.replace(a, '{}');
}, method.path) || method.path,
});
});
Object.keys(methodRouteGroup).forEach((key) => {
const methodRoutes = methodRouteGroup[key];
// check each route with the routes that are defined before it
for (let i = 0; i < methodRoutes.length; i += 1) {
for (let j = 0; j < i; j += 1) {
if (methodRoutes[i].path === methodRoutes[j].path) {
// full match
addCollision(PathDuplicationType.FULL, methodRoutes[i].method, controller, methodRoutes[j].method);
}
else if (methodRoutes[i].path.split('/').length === methodRoutes[j].path.split('/').length &&
methodRoutes[j].path
.substr(methodRoutes[j].path.lastIndexOf('/')) // compare only the "last" part of the path
.split('/')
.some(v => !!v) && // ensure the comparison path has a value
methodRoutes[i].path.split('/').every((v, index) => {
const comparisonPathPart = methodRoutes[j].path.split('/')[index];
// if no params, compare values
if (!v.includes('{}')) {
return v === comparisonPathPart;
}
// otherwise check if route starts with comparison route
return v.startsWith(methodRoutes[j].path.split('/')[index]);
})) {
// partial match - reorder routes!
addCollision(PathDuplicationType.PARTIAL, methodRoutes[i].method, controller, methodRoutes[j].method);
}
}
}
});
});
// print warnings for each collision (grouped by route)
collisions.forEach(collision => {
let message = '';
if (collision.type === PathDuplicationType.FULL) {
message = `Duplicate path parameter definition signature found in controller `;
}
else if (collision.type === PathDuplicationType.PARTIAL) {
message = `Overlapping path parameter definition signature found in controller `;
}
message += collision.controller.name;
message += ` [ method ${collision.method.method.toUpperCase()} ${collision.method.name} route: ${collision.method.path} ] collides with `;
message += collision.collidesWith
.map((method) => {
return `[ method ${method.method.toUpperCase()} ${method.name} route: ${method.path} ]`;
})
.join(', ');
message += '\n';
console.warn(message);
});
};
typeResolver_1.TypeResolver.clearCache();
this.program = controllers ? this.setProgramToDynamicControllersFiles(controllers, esm) : (0, typescript_1.createProgram)([entryFile], this.resolveCompilerOptions(compilerOptions));
this.typeChecker = this.program.getTypeChecker();
this.securityGenerator = generatorOptions?.securityGenerator;
this.customDecoratorProcessors = generatorOptions?.customDecoratorProcessors;
}
Generate() {
this.extractNodeFromProgramSourceFiles();
const controllers = this.buildControllers();
this.checkForMethodSignatureDuplicates(controllers);
this.checkForPathParamSignatureDuplicates(controllers);
return {
controllers,
referenceTypeMap: this.referenceTypeMap,
};
}
setProgramToDynamicControllersFiles(controllers, esm) {
const allGlobFiles = (0, importClassesFromDirectories_1.importClassesFromDirectories)(controllers, esm ? ['.mts', '.ts', '.cts'] : ['.ts']);
if (allGlobFiles.length === 0) {
throw new exceptions_1.GenerateMetadataError(`[${controllers.join(', ')}] globs found 0 controllers.`);
}
return (0, typescript_1.createProgram)(allGlobFiles, this.resolveCompilerOptions(this.compilerOptions));
}
// [namecheap] Callers (e.g. host app DefaultGenerator) pass compilerOptions as raw JSON
// (string values like target:'ES2020'). TypeScript's createProgram requires numeric enum
// values, so we normalise via convertCompilerOptionsFromJson before every createProgram call.
resolveCompilerOptions(options) {
if (!options)
return {};
const { options: converted } = (0, typescript_1.convertCompilerOptionsFromJson)(options, process.cwd());
return converted;
}
extractNodeFromProgramSourceFiles() {
this.program.getSourceFiles().forEach(sf => {
if (this.ignorePaths && this.ignorePaths.length) {
for (const path of this.ignorePaths) {
if ((0, minimatch_1.minimatch)(sf.fileName, path)) {
return;
}
}
}
(0, typescript_1.forEachChild)(sf, node => {
if ((0, typescript_1.isClassDeclaration)(node) && (0, decoratorUtils_1.getDecorators)(node, identifier => identifier.text === 'Route').length) {
this.controllerNodes.push(node);
}
});
});
}
TypeChecker() {
return this.typeChecker;
}
AddReferenceType(referenceType) {
if (!referenceType.refName) {
throw new Error('no reference type name found');
}
this.referenceTypeMap[referenceType.refName] = referenceType;
}
GetReferenceType(refName) {
return this.referenceTypeMap[refName];
}
CheckModelUnicity(refName, positions) {
if (!this.modelDefinitionPosMap[refName]) {
this.modelDefinitionPosMap[refName] = positions;
}
else {
const origPositions = this.modelDefinitionPosMap[refName];
if (!(origPositions.length === positions.length && positions.every(pos => origPositions.find(origPos => pos.pos === origPos.pos && pos.fileName === origPos.fileName)))) {
throw new Error(`Found 2 different model definitions for model ${refName}: orig: ${JSON.stringify(origPositions)}, act: ${JSON.stringify(positions)}`);
}
}
}
CheckExpressionUnicity(formattedRefName, refName) {
if (!this.expressionOrigNameMap[formattedRefName]) {
this.expressionOrigNameMap[formattedRefName] = refName;
}
else {
if (this.expressionOrigNameMap[formattedRefName] !== refName) {
throw new Error(`Found 2 different type expressions for formatted name "${formattedRefName}": orig: "${this.expressionOrigNameMap[formattedRefName]}", act: "${refName}"`);
}
}
}
buildControllers() {
if (this.controllerNodes.length === 0) {
throw new Error('no controllers found, check tsoa configuration');
}
return this.controllerNodes
.map(classDeclaration => new controllerGenerator_1.ControllerGenerator(classDeclaration, this, this.rootSecurity))
.filter(generator => generator.IsValid())
.map(generator => generator.Generate());
}
}
exports.MetadataGenerator = MetadataGenerator;
//# sourceMappingURL=metadataGenerator.js.map