Skip to content

Commit

Permalink
Merge pull request #1638 from midoelhawy/support-using-custom-multer-…
Browse files Browse the repository at this point in the history
…instance-in-register-routes

feat: support using custom multer instance in RegisterRoutes
  • Loading branch information
WoH committed Jul 12, 2024
2 parents c2bd263 + cfb3cda commit bc648c1
Show file tree
Hide file tree
Showing 14 changed files with 369 additions and 94 deletions.
13 changes: 10 additions & 3 deletions packages/cli/src/metadataGeneration/parameterGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,12 @@ import { TypeResolver } from './typeResolver';
import { getHeaderType } from '../utils/headerTypeHelpers';

export class ParameterGenerator {
constructor(private readonly parameter: ts.ParameterDeclaration, private readonly method: string, private readonly path: string, private readonly current: MetadataGenerator) {}
constructor(
private readonly parameter: ts.ParameterDeclaration,
private readonly method: string,
private readonly path: string,
private readonly current: MetadataGenerator,
) {}

public Generate(): Tsoa.Parameter[] {
const decoratorName = getNodeFirstDecoratorName(this.parameter, identifier => this.supportParameterDecorator(identifier.text));
Expand Down Expand Up @@ -414,7 +419,7 @@ export class ParameterGenerator {
const exampleLabels: Array<string | undefined> = [];
const examples = getJSDocTags(node.parent, tag => {
const comment = commentToString(tag.comment);
const isExample = (tag.tagName.text === 'example' || tag.tagName.escapedText === 'example') && !!tag.comment && comment?.startsWith(parameterName);
const isExample = (tag.tagName.text === 'example' || (tag.tagName.escapedText as string) === 'example') && !!tag.comment && comment?.startsWith(parameterName);

if (isExample) {
const hasExampleLabel = (comment?.split(' ')[0].indexOf('.') || -1) > 0;
Expand Down Expand Up @@ -447,7 +452,9 @@ export class ParameterGenerator {
}

private supportParameterDecorator(decoratorName: string) {
return ['header', 'query', 'queries', 'path', 'body', 'bodyprop', 'request', 'requestprop', 'res', 'inject', 'uploadedfile', 'uploadedfiles', 'formfield'].some(d => d === decoratorName.toLocaleLowerCase());
return ['header', 'query', 'queries', 'path', 'body', 'bodyprop', 'request', 'requestprop', 'res', 'inject', 'uploadedfile', 'uploadedfiles', 'formfield'].some(
d => d === decoratorName.toLocaleLowerCase(),
);
}

private supportPathDataType(parameterType: Tsoa.Type): boolean {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@ export class ReferenceTransformer extends Transformer {
}

if (referenceTypes.every(refType => refType.dataType === 'refEnum')) {
/* eslint-disable @typescript-eslint/no-unnecessary-type-assertion */
return EnumTransformer.mergeMany(referenceTypes as Tsoa.RefEnumType[]);
}

if (referenceTypes.every(refType => refType.dataType === 'refObject')) {
/* eslint-disable @typescript-eslint/no-unnecessary-type-assertion */
return this.mergeManyRefObj(referenceTypes as Tsoa.RefObjectType[]);
}

Expand Down
111 changes: 25 additions & 86 deletions packages/cli/src/metadataGeneration/typeResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,13 +141,11 @@ export class TypeResolver {
let additionalType: Tsoa.Type | undefined;

if (indexMember) {
/* eslint-disable @typescript-eslint/no-unnecessary-type-assertion */
const indexSignatureDeclaration = indexMember as ts.IndexSignatureDeclaration;
const indexType = new TypeResolver(indexSignatureDeclaration.parameters[0].type as ts.TypeNode, this.current, this.parentNode, this.context).resolve();

throwUnless(
indexType.dataType === 'string',
new GenerateMetadataError(`Only string indexers are supported.`, this.typeNode),
);
throwUnless(indexType.dataType === 'string', new GenerateMetadataError(`Only string indexers are supported.`, this.typeNode));

additionalType = new TypeResolver(indexSignatureDeclaration.type, this.current, this.parentNode, this.context).resolve();
}
Expand Down Expand Up @@ -216,7 +214,7 @@ export class TypeResolver {
const parent = getOneOrigDeclaration(property); //If there are more declarations, we need to get one of them, from where we want to recognize jsDoc
const type = new TypeResolver(typeNode, this.current, parent, this.context, propertyType).resolve();

const required = !(this.hasFlag(property, ts.SymbolFlags.Optional));
const required = !this.hasFlag(property, ts.SymbolFlags.Optional);

const comments = property.getDocumentationComment(this.current.typeChecker);
const description = comments.length ? ts.displayPartsToString(comments) : undefined;
Expand Down Expand Up @@ -319,22 +317,12 @@ export class TypeResolver {
return new TypeResolver(this.typeNode.type, this.current, this.typeNode, this.context, this.referencer).resolve();
}

throwUnless(
this.typeNode.kind === ts.SyntaxKind.TypeReference,
new GenerateMetadataError(`Unknown type: ${ts.SyntaxKind[this.typeNode.kind]}`, this.typeNode),
);
throwUnless(this.typeNode.kind === ts.SyntaxKind.TypeReference, new GenerateMetadataError(`Unknown type: ${ts.SyntaxKind[this.typeNode.kind]}`, this.typeNode));

return this.resolveTypeReferenceNode(this.typeNode as ts.TypeReferenceNode, this.current, this.context, this.parentNode);
}

private resolveTypeOperatorNode(
typeNode: ts.TypeOperatorNode,
typeChecker: ts.TypeChecker,
current: MetadataGenerator,
context: Context,
parentNode?: ts.Node,
referencer?: ts.Type,
): Tsoa.Type {
private resolveTypeOperatorNode(typeNode: ts.TypeOperatorNode, typeChecker: ts.TypeChecker, current: MetadataGenerator, context: Context, parentNode?: ts.Node, referencer?: ts.Type): Tsoa.Type {
switch (typeNode.operator) {
case ts.SyntaxKind.KeyOfKeyword: {
// keyof
Expand All @@ -344,10 +332,7 @@ export class TypeResolver {
const symbol = type.type.getSymbol();
if (symbol && symbol.getFlags() & ts.TypeFlags.TypeParameter) {
const typeName = symbol.getEscapedName();
throwUnless(
typeof typeName === 'string',
new GenerateMetadataError(`typeName is not string, but ${typeof typeName}`, typeNode),
);
throwUnless(typeof typeName === 'string', new GenerateMetadataError(`typeName is not string, but ${typeof typeName}`, typeNode));

if (context[typeName]) {
const subResult = new TypeResolver(context[typeName].type, current, parentNode, context).resolve();
Expand All @@ -358,10 +343,7 @@ export class TypeResolver {
};
}
const properties = (subResult as Tsoa.RefObjectType).properties?.map(v => v.name);
throwUnless(
properties,
new GenerateMetadataError(`TypeOperator 'keyof' on node which have no properties`, context[typeName].type),
);
throwUnless(properties, new GenerateMetadataError(`TypeOperator 'keyof' on node which have no properties`, context[typeName].type));

return {
dataType: 'enum',
Expand Down Expand Up @@ -459,22 +441,13 @@ export class TypeResolver {
const isNumberIndexType = indexType.kind === ts.SyntaxKind.NumberKeyword;
const typeOfObjectType = typeChecker.getTypeFromTypeNode(objectType);
const type = isNumberIndexType ? typeOfObjectType.getNumberIndexType() : typeOfObjectType.getStringIndexType();
throwUnless(
type,
new GenerateMetadataError(`Could not determine ${isNumberIndexType ? 'number' : 'string'} index on ${typeChecker.typeToString(typeOfObjectType)}`, typeNode),
);
throwUnless(type, new GenerateMetadataError(`Could not determine ${isNumberIndexType ? 'number' : 'string'} index on ${typeChecker.typeToString(typeOfObjectType)}`, typeNode));
return new TypeResolver(typeChecker.typeToTypeNode(type, objectType, ts.NodeBuilderFlags.NoTruncation)!, current, typeNode, context).resolve();
} else if (ts.isLiteralTypeNode(indexType) && (ts.isStringLiteral(indexType.literal) || ts.isNumericLiteral(indexType.literal))) {
// Indexed by literal
const hasType = (node: ts.Node | undefined): node is ts.HasType => node !== undefined && Object.prototype.hasOwnProperty.call(node, 'type');
const symbol = typeChecker.getPropertyOfType(typeChecker.getTypeFromTypeNode(objectType), indexType.literal.text);
throwUnless(
symbol,
new GenerateMetadataError(
`Could not determine the keys on ${typeChecker.typeToString(typeChecker.getTypeFromTypeNode(objectType))}`,
typeNode,
),
);
throwUnless(symbol, new GenerateMetadataError(`Could not determine the keys on ${typeChecker.typeToString(typeChecker.getTypeFromTypeNode(objectType))}`, typeNode));
if (hasType(symbol.valueDeclaration) && symbol.valueDeclaration.type) {
return new TypeResolver(symbol.valueDeclaration.type, current, typeNode, context).resolve();
}
Expand All @@ -483,9 +456,7 @@ export class TypeResolver {
return new TypeResolver(typeChecker.typeToTypeNode(declaration, objectType, ts.NodeBuilderFlags.NoTruncation)!, current, typeNode, context).resolve();
} catch {
throw new GenerateMetadataError(
`Could not determine the keys on ${typeChecker.typeToString(
typeChecker.getTypeFromTypeNode(typeChecker.typeToTypeNode(declaration, undefined, ts.NodeBuilderFlags.NoTruncation)!),
)}`,
`Could not determine the keys on ${typeChecker.typeToString(typeChecker.getTypeFromTypeNode(typeChecker.typeToTypeNode(declaration, undefined, ts.NodeBuilderFlags.NoTruncation)!))}`,
typeNode,
);
}
Expand All @@ -504,27 +475,22 @@ export class TypeResolver {
throw new GenerateMetadataError(`Unknown type: ${ts.SyntaxKind[typeNode.kind]}`, typeNode);
}

private resolveTypeReferenceNode(
typeNode: ts.TypeReferenceNode,
current: MetadataGenerator,
context: Context,
parentNode?: ts.Node,
): Tsoa.Type {
private resolveTypeReferenceNode(typeNode: ts.TypeReferenceNode, current: MetadataGenerator, context: Context, parentNode?: ts.Node): Tsoa.Type {
const { typeName, typeArguments } = typeNode;

if (typeName.kind !== ts.SyntaxKind.Identifier) {
return this.getReferenceType(typeNode);
}

switch(typeName.text) {
switch (typeName.text) {
case 'Date':
return new DateTransformer(this).transform(parentNode);
case 'Buffer':
case 'Readable':
return { dataType: 'buffer' };
case 'Array':
if (typeArguments && typeArguments.length === 1) {
return {
return {
dataType: 'array',
elementType: new TypeResolver(typeArguments[0], current, parentNode, context).resolve(),
};
Expand Down Expand Up @@ -559,10 +525,7 @@ export class TypeResolver {
case ts.SyntaxKind.NullKeyword:
return null;
default:
throwUnless(
Object.prototype.hasOwnProperty.call(typeNode.literal, 'text'),
new GenerateMetadataError(`Couldn't resolve literal node: ${typeNode.literal.getText()}`),
);
throwUnless(Object.prototype.hasOwnProperty.call(typeNode.literal, 'text'), new GenerateMetadataError(`Couldn't resolve literal node: ${typeNode.literal.getText()}`));
return (typeNode.literal as ts.LiteralExpression).text;
}
}
Expand All @@ -578,15 +541,13 @@ export class TypeResolver {
return nodes;
}

throwUnless(
designatedNodes.length === 1,
new GenerateMetadataError(`Multiple models for ${typeName} marked with '@tsoaModel'; '@tsoaModel' should only be applied to one model.`),
);
throwUnless(designatedNodes.length === 1, new GenerateMetadataError(`Multiple models for ${typeName} marked with '@tsoaModel'; '@tsoaModel' should only be applied to one model.`));

return designatedNodes;
}

private hasFlag(type: ts.Type | ts.Symbol | ts.Declaration, flag: ts.TypeFlags | ts.NodeFlags | ts.SymbolFlags) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison
return (type.flags & flag) === flag;
}

Expand Down Expand Up @@ -650,10 +611,7 @@ export class TypeResolver {

while (!ts.isSourceFile(actNode)) {
if (!(isFirst && ts.isEnumDeclaration(actNode)) && !ts.isModuleBlock(actNode)) {
throwUnless(
ts.isModuleDeclaration(actNode),
new GenerateMetadataError(`This node kind is unknown: ${actNode.kind}`, type),
);
throwUnless(ts.isModuleDeclaration(actNode), new GenerateMetadataError(`This node kind is unknown: ${actNode.kind}`, type));

if (!isGlobalDeclaration(actNode)) {
const moduleName = actNode.name.text;
Expand Down Expand Up @@ -729,30 +687,23 @@ export class TypeResolver {
return resolvedType;
}
if (ts.isTypeReferenceNode(arg) || ts.isExpressionWithTypeArguments(arg)) {
const [_, name] = this.calcTypeReferenceTypeName(arg);
return name;
return this.calcTypeReferenceTypeName(arg)[1];
} else if (ts.isTypeLiteralNode(arg)) {
const members = arg.members.map(member => {
if (ts.isPropertySignature(member)) {
const name = (member.name as ts.Identifier).text;
const typeText = this.calcTypeName(member.type as ts.TypeNode);
return `"${name}"${member.questionToken ? '?' : ''}${this.calcMemberJsDocProperties(member)}: ${typeText}`;
} else if (ts.isIndexSignatureDeclaration(member)) {
throwUnless(
member.parameters.length === 1,
new GenerateMetadataError(`Index signature parameters length != 1`, member),
);
throwUnless(member.parameters.length === 1, new GenerateMetadataError(`Index signature parameters length != 1`, member));

const indexType = member.parameters[0];
throwUnless(
// now we can't reach this part of code
ts.isParameter(indexType),
new GenerateMetadataError(`indexSignature declaration parameter kind is not SyntaxKind.Parameter`, indexType),
);
throwUnless(
!indexType.questionToken,
new GenerateMetadataError(`Question token has found for an indexSignature declaration`, indexType),
);
throwUnless(!indexType.questionToken, new GenerateMetadataError(`Question token has found for an indexSignature declaration`, indexType));

const typeText = this.calcTypeName(member.type);
const indexName = (indexType.name as ts.Identifier).text;
Expand Down Expand Up @@ -886,10 +837,7 @@ export class TypeResolver {
const deprecated = isExistJSDocTag(modelType, tag => tag.tagName.text === 'deprecated') || isDecorator(modelType, identifier => identifier.text === 'Deprecated');

// Handle toJSON methods
throwUnless(
modelType.name,
new GenerateMetadataError("Can't get Symbol from anonymous class", modelType),
);
throwUnless(modelType.name, new GenerateMetadataError("Can't get Symbol from anonymous class", modelType));

const type = this.current.typeChecker.getTypeAtLocation(modelType.name);
const toJSON = this.current.typeChecker.getPropertyOfType(type, 'toJSON');
Expand Down Expand Up @@ -995,23 +943,17 @@ export class TypeResolver {
}
const declarations = symbol?.getDeclarations();

throwUnless(
symbol && declarations,
new GenerateMetadataError(`No declarations found for referenced type ${typeName}.`),
);
throwUnless(symbol && declarations, new GenerateMetadataError(`No declarations found for referenced type ${typeName}.`));

if (symbol.escapedName !== typeName && symbol.escapedName !== 'default') {
if ((symbol.escapedName as string) !== typeName && (symbol.escapedName as string) !== 'default') {
typeName = symbol.escapedName as string;
}

let modelTypes = declarations.filter((node): node is UsableDeclarationWithoutPropertySignature => {
return this.nodeIsUsable(node) && node.name?.getText() === typeName;
});

throwUnless(
modelTypes.length,
new GenerateMetadataError(`No matching model found for referenced type ${typeName}.`),
);
throwUnless(modelTypes.length, new GenerateMetadataError(`No matching model found for referenced type ${typeName}.`));

if (modelTypes.length > 1) {
// remove types that are from typescript e.g. 'Account'
Expand Down Expand Up @@ -1041,10 +983,7 @@ export class TypeResolver {

const indexSignatureDeclaration = indexMember as ts.IndexSignatureDeclaration;
const indexType = new TypeResolver(indexSignatureDeclaration.parameters[0].type as ts.TypeNode, this.current, this.parentNode, this.context).resolve();
throwUnless(
indexType.dataType === 'string',
new GenerateMetadataError(`Only string indexers are supported.`, this.typeNode),
);
throwUnless(indexType.dataType === 'string', new GenerateMetadataError(`Only string indexers are supported.`, this.typeNode));

return new TypeResolver(indexSignatureDeclaration.type, this.current, this.parentNode, this.context).resolve();
}
Expand Down
19 changes: 18 additions & 1 deletion packages/cli/src/routeGeneration/templates/express.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import multer from 'multer';
{{else}}
const multer = require('multer');
{{/if}}
const upload = multer({{{json multerOpts}}});

{{/if}}

{{#if authenticationModule}}
Expand Down Expand Up @@ -59,11 +59,25 @@ const templateService = new ExpressTemplateService(models, {{{ json minimalSwagg

// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa




{{#if useFileUploads}}
export function RegisterRoutes(app: Router,opts?:{multer?:ReturnType<typeof multer>}) {
{{else}}
export function RegisterRoutes(app: Router) {
{{/if}}

// ###########################################################################################################
// NOTE: If you do not see routes for all of your controllers in this file, then you might not have informed tsoa of where to look
// Please look into the "controllerPathGlobs" config option described in the readme: https://github.com/lukeautry/tsoa
// ###########################################################################################################

{{#if useFileUploads}}
const upload = opts?.multer || multer({{{json multerOpts}}});
{{/if}}


{{#each controllers}}
{{#each actions}}
app.{{method}}('{{fullPath}}',
Expand All @@ -73,6 +87,9 @@ export function RegisterRoutes(app: Router) {
{{#if uploadFile}}
upload.fields({{json uploadFileName}}),
{{/if}}
{{#if uploadFiles}}
upload.array('{{uploadFilesName}}'),
{{/if}}
...(fetchMiddlewares<RequestHandler>({{../name}})),
...(fetchMiddlewares<RequestHandler>({{../name}}.prototype.{{name}})),

Expand Down
Loading

0 comments on commit bc648c1

Please sign in to comment.