@namecheap/tsoa-cli
Version:
Build swagger-compliant REST APIs using TypeScript and Node
237 lines • 11.7 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.RouteGenerator = void 0;
const fs = require("fs");
const handlebars = require("handlebars");
const path = require("path");
const tsoa_runtime_1 = require("@namecheap/tsoa-runtime");
const fs_1 = require("../utils/fs");
const internalTypeGuards_1 = require("../utils/internalTypeGuards");
const pathUtils_1 = require("./../utils/pathUtils");
class RouteGenerator {
constructor(metadata, options) {
this.metadata = metadata;
this.options = options;
}
async GenerateRoutes(middlewareTemplate, pathTransformer) {
if (!fs.lstatSync(this.options.routesDir).isDirectory()) {
throw new Error(`routesDir should be a directory`);
}
else if (this.options.routesFileName !== undefined && !this.options.routesFileName.endsWith('.ts')) {
throw new Error(`routesFileName should have a '.ts' extension`);
}
const fileName = `${this.options.routesDir}/${this.options.routesFileName || 'routes.ts'}`;
const content = this.buildContent(middlewareTemplate, pathTransformer);
if (this.options.noWriteIfUnchanged) {
if (await (0, fs_1.fsExists)(fileName)) {
const existingContent = (await (0, fs_1.fsReadFile)(fileName)).toString();
if (content === existingContent) {
return;
}
}
}
await (0, fs_1.fsWriteFile)(fileName, content);
}
async GenerateCustomRoutes(template, pathTransformer) {
const data = await (0, fs_1.fsReadFile)(path.join(template));
const file = data.toString();
return await this.GenerateRoutes(file, pathTransformer);
}
buildContent(middlewareTemplate, pathTransformer) {
handlebars.registerHelper('json', (context) => {
return JSON.stringify(context);
});
const additionalPropsHelper = (additionalProperties) => {
if (additionalProperties) {
// Then the model for this type explicitly allows additional properties and thus we should assign that
return JSON.stringify(additionalProperties);
}
else if (this.options.noImplicitAdditionalProperties === 'silently-remove-extras') {
return JSON.stringify(false);
}
else if (this.options.noImplicitAdditionalProperties === 'throw-on-extras') {
return JSON.stringify(false);
}
else if (this.options.noImplicitAdditionalProperties === 'ignore') {
return JSON.stringify(true);
}
else {
return (0, tsoa_runtime_1.assertNever)(this.options.noImplicitAdditionalProperties);
}
};
handlebars.registerHelper('additionalPropsHelper', additionalPropsHelper);
const routesTemplate = handlebars.compile(middlewareTemplate, { noEscape: true });
const authenticationModule = this.options.authenticationModule ? this.getRelativeImportPath(this.options.authenticationModule) : undefined;
const iocModule = this.options.iocModule ? this.getRelativeImportPath(this.options.iocModule) : undefined;
// Left in for backwards compatibility, previously if we're working locally then tsoa runtime code wasn't an importable module but now it is.
const canImportByAlias = true;
const normalisedBasePath = (0, pathUtils_1.normalisePath)(this.options.basePath, '/');
return routesTemplate({
authenticationModule,
basePath: normalisedBasePath,
canImportByAlias,
controllers: this.metadata.controllers.map(controller => {
const normalisedControllerPath = pathTransformer((0, pathUtils_1.normalisePath)(controller.path, '/'));
return {
actions: controller.methods.map(method => {
const parameterObjs = {};
method.parameters.forEach(parameter => {
parameterObjs[parameter.parameterName] = this.buildParameterSchema(parameter);
});
const normalisedMethodPath = pathTransformer((0, pathUtils_1.normalisePath)(method.path, '/'));
const normalisedFullPath = (0, pathUtils_1.normalisePath)(`${normalisedBasePath}${normalisedControllerPath}${normalisedMethodPath}`, '/', '', false);
const uploadFileParameter = method.parameters.find(parameter => parameter.type.dataType === 'file');
const uploadFilesParameter = method.parameters.find(parameter => parameter.type.dataType === 'array' && parameter.type.elementType.dataType === 'file');
return {
fullPath: normalisedFullPath,
method: method.method.toLowerCase(),
name: method.name,
parameters: parameterObjs,
path: normalisedMethodPath,
uploadFile: !!uploadFileParameter,
uploadFileName: uploadFileParameter === null || uploadFileParameter === void 0 ? void 0 : uploadFileParameter.name,
uploadFiles: !!uploadFilesParameter,
uploadFilesName: uploadFilesParameter === null || uploadFilesParameter === void 0 ? void 0 : uploadFilesParameter.name,
security: method.security,
successStatus: method.successStatus ? method.successStatus : 'undefined',
};
}),
modulePath: this.getRelativeImportPath(controller.location),
name: controller.name,
path: normalisedControllerPath,
};
}),
environment: process.env,
iocModule,
minimalSwaggerConfig: { noImplicitAdditionalProperties: this.options.noImplicitAdditionalProperties },
models: this.buildModels(),
useFileUploads: this.metadata.controllers.some(controller => controller.methods.some(method => !!method.parameters.find(parameter => {
if (parameter.type.dataType === 'file') {
return true;
}
else if (parameter.type.dataType === 'array' && parameter.type.elementType.dataType === 'file') {
return true;
}
return false;
}))),
multerOpts: this.options.multerOpts,
useSecurity: this.metadata.controllers.some(controller => controller.methods.some(method => !!method.security.length)),
esm: this.options.esm,
});
}
buildModels() {
const models = {};
Object.keys(this.metadata.referenceTypeMap).forEach(name => {
const referenceType = this.metadata.referenceTypeMap[name];
let model;
if (referenceType.dataType === 'refEnum') {
const refEnumModel = {
dataType: 'refEnum',
enums: referenceType.enums,
};
model = refEnumModel;
}
else if (referenceType.dataType === 'refObject') {
const propertySchemaDictionary = {};
referenceType.properties.forEach(property => {
propertySchemaDictionary[property.name] = this.buildPropertySchema(property);
});
const refObjModel = {
dataType: 'refObject',
properties: propertySchemaDictionary,
};
if (referenceType.additionalProperties) {
refObjModel.additionalProperties = this.buildProperty(referenceType.additionalProperties);
}
else if (this.options.noImplicitAdditionalProperties !== 'ignore') {
refObjModel.additionalProperties = false;
}
else {
// Since Swagger allows "excess properties" (to use a TypeScript term) by default
refObjModel.additionalProperties = true;
}
model = refObjModel;
}
else if (referenceType.dataType === 'refAlias') {
const refType = {
dataType: 'refAlias',
type: {
...this.buildProperty(referenceType.type),
validators: referenceType.validators,
default: referenceType.default,
},
};
model = refType;
}
else {
model = (0, tsoa_runtime_1.assertNever)(referenceType);
}
models[name] = model;
});
return models;
}
getRelativeImportPath(fileLocation) {
fileLocation = fileLocation.replace(/.ts$/, ''); // no ts extension in import
return `./${path.relative(this.options.routesDir, fileLocation).replace(/\\/g, '/')}${this.options.esm ? '.js' : ''}`;
}
buildPropertySchema(source) {
const propertySchema = this.buildProperty(source.type);
propertySchema.default = source.default;
propertySchema.required = source.required ? true : undefined;
if (Object.keys(source.validators).length > 0) {
propertySchema.validators = source.validators;
}
return propertySchema;
}
buildParameterSchema(source) {
const property = this.buildProperty(source.type);
const parameter = {
default: source.default,
in: source.in,
name: source.name,
required: source.required ? true : undefined,
};
const parameterSchema = Object.assign(parameter, property);
if (Object.keys(source.validators).length > 0) {
parameterSchema.validators = source.validators;
}
return parameterSchema;
}
buildProperty(type) {
const schema = {
dataType: type.dataType,
};
if ((0, internalTypeGuards_1.isRefType)(type)) {
schema.dataType = undefined;
schema.ref = type.refName;
}
if (type.dataType === 'array') {
const arrayType = type;
if ((0, internalTypeGuards_1.isRefType)(arrayType.elementType)) {
schema.array = {
dataType: arrayType.elementType.dataType,
ref: arrayType.elementType.refName,
};
}
else {
schema.array = this.buildProperty(arrayType.elementType);
}
}
if (type.dataType === 'enum') {
schema.enums = type.enums;
}
if (type.dataType === 'union' || type.dataType === 'intersection') {
schema.subSchemas = type.types.map(type => this.buildProperty(type));
}
if (type.dataType === 'nestedObjectLiteral') {
const objLiteral = type;
schema.nestedProperties = objLiteral.properties.reduce((acc, prop) => {
return { ...acc, [prop.name]: this.buildPropertySchema(prop) };
}, {});
schema.additionalProperties = objLiteral.additionalProperties && this.buildProperty(objLiteral.additionalProperties);
}
return schema;
}
}
exports.RouteGenerator = RouteGenerator;
//# sourceMappingURL=routeGenerator.js.map