Skip to content

Commit bc648c1

Browse files
authored
Merge pull request #1638 from midoelhawy/support-using-custom-multer-instance-in-register-routes
feat: support using custom multer instance in RegisterRoutes
2 parents c2bd263 + cfb3cda commit bc648c1

File tree

14 files changed

+369
-94
lines changed

14 files changed

+369
-94
lines changed

packages/cli/src/metadataGeneration/parameterGenerator.ts

+10-3
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,12 @@ import { TypeResolver } from './typeResolver';
1010
import { getHeaderType } from '../utils/headerTypeHelpers';
1111

1212
export class ParameterGenerator {
13-
constructor(private readonly parameter: ts.ParameterDeclaration, private readonly method: string, private readonly path: string, private readonly current: MetadataGenerator) {}
13+
constructor(
14+
private readonly parameter: ts.ParameterDeclaration,
15+
private readonly method: string,
16+
private readonly path: string,
17+
private readonly current: MetadataGenerator,
18+
) {}
1419

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

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

449454
private supportParameterDecorator(decoratorName: string) {
450-
return ['header', 'query', 'queries', 'path', 'body', 'bodyprop', 'request', 'requestprop', 'res', 'inject', 'uploadedfile', 'uploadedfiles', 'formfield'].some(d => d === decoratorName.toLocaleLowerCase());
455+
return ['header', 'query', 'queries', 'path', 'body', 'bodyprop', 'request', 'requestprop', 'res', 'inject', 'uploadedfile', 'uploadedfiles', 'formfield'].some(
456+
d => d === decoratorName.toLocaleLowerCase(),
457+
);
451458
}
452459

453460
private supportPathDataType(parameterType: Tsoa.Type): boolean {

packages/cli/src/metadataGeneration/transformer/referenceTransformer.ts

+2
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,12 @@ export class ReferenceTransformer extends Transformer {
1414
}
1515

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

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

packages/cli/src/metadataGeneration/typeResolver.ts

+25-86
Original file line numberDiff line numberDiff line change
@@ -141,13 +141,11 @@ export class TypeResolver {
141141
let additionalType: Tsoa.Type | undefined;
142142

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

147-
throwUnless(
148-
indexType.dataType === 'string',
149-
new GenerateMetadataError(`Only string indexers are supported.`, this.typeNode),
150-
);
148+
throwUnless(indexType.dataType === 'string', new GenerateMetadataError(`Only string indexers are supported.`, this.typeNode));
151149

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

219-
const required = !(this.hasFlag(property, ts.SymbolFlags.Optional));
217+
const required = !this.hasFlag(property, ts.SymbolFlags.Optional);
220218

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

322-
throwUnless(
323-
this.typeNode.kind === ts.SyntaxKind.TypeReference,
324-
new GenerateMetadataError(`Unknown type: ${ts.SyntaxKind[this.typeNode.kind]}`, this.typeNode),
325-
);
320+
throwUnless(this.typeNode.kind === ts.SyntaxKind.TypeReference, new GenerateMetadataError(`Unknown type: ${ts.SyntaxKind[this.typeNode.kind]}`, this.typeNode));
326321

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

330-
private resolveTypeOperatorNode(
331-
typeNode: ts.TypeOperatorNode,
332-
typeChecker: ts.TypeChecker,
333-
current: MetadataGenerator,
334-
context: Context,
335-
parentNode?: ts.Node,
336-
referencer?: ts.Type,
337-
): Tsoa.Type {
325+
private resolveTypeOperatorNode(typeNode: ts.TypeOperatorNode, typeChecker: ts.TypeChecker, current: MetadataGenerator, context: Context, parentNode?: ts.Node, referencer?: ts.Type): Tsoa.Type {
338326
switch (typeNode.operator) {
339327
case ts.SyntaxKind.KeyOfKeyword: {
340328
// keyof
@@ -344,10 +332,7 @@ export class TypeResolver {
344332
const symbol = type.type.getSymbol();
345333
if (symbol && symbol.getFlags() & ts.TypeFlags.TypeParameter) {
346334
const typeName = symbol.getEscapedName();
347-
throwUnless(
348-
typeof typeName === 'string',
349-
new GenerateMetadataError(`typeName is not string, but ${typeof typeName}`, typeNode),
350-
);
335+
throwUnless(typeof typeName === 'string', new GenerateMetadataError(`typeName is not string, but ${typeof typeName}`, typeNode));
351336

352337
if (context[typeName]) {
353338
const subResult = new TypeResolver(context[typeName].type, current, parentNode, context).resolve();
@@ -358,10 +343,7 @@ export class TypeResolver {
358343
};
359344
}
360345
const properties = (subResult as Tsoa.RefObjectType).properties?.map(v => v.name);
361-
throwUnless(
362-
properties,
363-
new GenerateMetadataError(`TypeOperator 'keyof' on node which have no properties`, context[typeName].type),
364-
);
346+
throwUnless(properties, new GenerateMetadataError(`TypeOperator 'keyof' on node which have no properties`, context[typeName].type));
365347

366348
return {
367349
dataType: 'enum',
@@ -459,22 +441,13 @@ export class TypeResolver {
459441
const isNumberIndexType = indexType.kind === ts.SyntaxKind.NumberKeyword;
460442
const typeOfObjectType = typeChecker.getTypeFromTypeNode(objectType);
461443
const type = isNumberIndexType ? typeOfObjectType.getNumberIndexType() : typeOfObjectType.getStringIndexType();
462-
throwUnless(
463-
type,
464-
new GenerateMetadataError(`Could not determine ${isNumberIndexType ? 'number' : 'string'} index on ${typeChecker.typeToString(typeOfObjectType)}`, typeNode),
465-
);
444+
throwUnless(type, new GenerateMetadataError(`Could not determine ${isNumberIndexType ? 'number' : 'string'} index on ${typeChecker.typeToString(typeOfObjectType)}`, typeNode));
466445
return new TypeResolver(typeChecker.typeToTypeNode(type, objectType, ts.NodeBuilderFlags.NoTruncation)!, current, typeNode, context).resolve();
467446
} else if (ts.isLiteralTypeNode(indexType) && (ts.isStringLiteral(indexType.literal) || ts.isNumericLiteral(indexType.literal))) {
468447
// Indexed by literal
469448
const hasType = (node: ts.Node | undefined): node is ts.HasType => node !== undefined && Object.prototype.hasOwnProperty.call(node, 'type');
470449
const symbol = typeChecker.getPropertyOfType(typeChecker.getTypeFromTypeNode(objectType), indexType.literal.text);
471-
throwUnless(
472-
symbol,
473-
new GenerateMetadataError(
474-
`Could not determine the keys on ${typeChecker.typeToString(typeChecker.getTypeFromTypeNode(objectType))}`,
475-
typeNode,
476-
),
477-
);
450+
throwUnless(symbol, new GenerateMetadataError(`Could not determine the keys on ${typeChecker.typeToString(typeChecker.getTypeFromTypeNode(objectType))}`, typeNode));
478451
if (hasType(symbol.valueDeclaration) && symbol.valueDeclaration.type) {
479452
return new TypeResolver(symbol.valueDeclaration.type, current, typeNode, context).resolve();
480453
}
@@ -483,9 +456,7 @@ export class TypeResolver {
483456
return new TypeResolver(typeChecker.typeToTypeNode(declaration, objectType, ts.NodeBuilderFlags.NoTruncation)!, current, typeNode, context).resolve();
484457
} catch {
485458
throw new GenerateMetadataError(
486-
`Could not determine the keys on ${typeChecker.typeToString(
487-
typeChecker.getTypeFromTypeNode(typeChecker.typeToTypeNode(declaration, undefined, ts.NodeBuilderFlags.NoTruncation)!),
488-
)}`,
459+
`Could not determine the keys on ${typeChecker.typeToString(typeChecker.getTypeFromTypeNode(typeChecker.typeToTypeNode(declaration, undefined, ts.NodeBuilderFlags.NoTruncation)!))}`,
489460
typeNode,
490461
);
491462
}
@@ -504,27 +475,22 @@ export class TypeResolver {
504475
throw new GenerateMetadataError(`Unknown type: ${ts.SyntaxKind[typeNode.kind]}`, typeNode);
505476
}
506477

507-
private resolveTypeReferenceNode(
508-
typeNode: ts.TypeReferenceNode,
509-
current: MetadataGenerator,
510-
context: Context,
511-
parentNode?: ts.Node,
512-
): Tsoa.Type {
478+
private resolveTypeReferenceNode(typeNode: ts.TypeReferenceNode, current: MetadataGenerator, context: Context, parentNode?: ts.Node): Tsoa.Type {
513479
const { typeName, typeArguments } = typeNode;
514480

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

519-
switch(typeName.text) {
485+
switch (typeName.text) {
520486
case 'Date':
521487
return new DateTransformer(this).transform(parentNode);
522488
case 'Buffer':
523489
case 'Readable':
524490
return { dataType: 'buffer' };
525491
case 'Array':
526492
if (typeArguments && typeArguments.length === 1) {
527-
return {
493+
return {
528494
dataType: 'array',
529495
elementType: new TypeResolver(typeArguments[0], current, parentNode, context).resolve(),
530496
};
@@ -559,10 +525,7 @@ export class TypeResolver {
559525
case ts.SyntaxKind.NullKeyword:
560526
return null;
561527
default:
562-
throwUnless(
563-
Object.prototype.hasOwnProperty.call(typeNode.literal, 'text'),
564-
new GenerateMetadataError(`Couldn't resolve literal node: ${typeNode.literal.getText()}`),
565-
);
528+
throwUnless(Object.prototype.hasOwnProperty.call(typeNode.literal, 'text'), new GenerateMetadataError(`Couldn't resolve literal node: ${typeNode.literal.getText()}`));
566529
return (typeNode.literal as ts.LiteralExpression).text;
567530
}
568531
}
@@ -578,15 +541,13 @@ export class TypeResolver {
578541
return nodes;
579542
}
580543

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

586546
return designatedNodes;
587547
}
588548

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

@@ -650,10 +611,7 @@ export class TypeResolver {
650611

651612
while (!ts.isSourceFile(actNode)) {
652613
if (!(isFirst && ts.isEnumDeclaration(actNode)) && !ts.isModuleBlock(actNode)) {
653-
throwUnless(
654-
ts.isModuleDeclaration(actNode),
655-
new GenerateMetadataError(`This node kind is unknown: ${actNode.kind}`, type),
656-
);
614+
throwUnless(ts.isModuleDeclaration(actNode), new GenerateMetadataError(`This node kind is unknown: ${actNode.kind}`, type));
657615

658616
if (!isGlobalDeclaration(actNode)) {
659617
const moduleName = actNode.name.text;
@@ -729,30 +687,23 @@ export class TypeResolver {
729687
return resolvedType;
730688
}
731689
if (ts.isTypeReferenceNode(arg) || ts.isExpressionWithTypeArguments(arg)) {
732-
const [_, name] = this.calcTypeReferenceTypeName(arg);
733-
return name;
690+
return this.calcTypeReferenceTypeName(arg)[1];
734691
} else if (ts.isTypeLiteralNode(arg)) {
735692
const members = arg.members.map(member => {
736693
if (ts.isPropertySignature(member)) {
737694
const name = (member.name as ts.Identifier).text;
738695
const typeText = this.calcTypeName(member.type as ts.TypeNode);
739696
return `"${name}"${member.questionToken ? '?' : ''}${this.calcMemberJsDocProperties(member)}: ${typeText}`;
740697
} else if (ts.isIndexSignatureDeclaration(member)) {
741-
throwUnless(
742-
member.parameters.length === 1,
743-
new GenerateMetadataError(`Index signature parameters length != 1`, member),
744-
);
698+
throwUnless(member.parameters.length === 1, new GenerateMetadataError(`Index signature parameters length != 1`, member));
745699

746700
const indexType = member.parameters[0];
747701
throwUnless(
748702
// now we can't reach this part of code
749703
ts.isParameter(indexType),
750704
new GenerateMetadataError(`indexSignature declaration parameter kind is not SyntaxKind.Parameter`, indexType),
751705
);
752-
throwUnless(
753-
!indexType.questionToken,
754-
new GenerateMetadataError(`Question token has found for an indexSignature declaration`, indexType),
755-
);
706+
throwUnless(!indexType.questionToken, new GenerateMetadataError(`Question token has found for an indexSignature declaration`, indexType));
756707

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

888839
// Handle toJSON methods
889-
throwUnless(
890-
modelType.name,
891-
new GenerateMetadataError("Can't get Symbol from anonymous class", modelType),
892-
);
840+
throwUnless(modelType.name, new GenerateMetadataError("Can't get Symbol from anonymous class", modelType));
893841

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

998-
throwUnless(
999-
symbol && declarations,
1000-
new GenerateMetadataError(`No declarations found for referenced type ${typeName}.`),
1001-
);
946+
throwUnless(symbol && declarations, new GenerateMetadataError(`No declarations found for referenced type ${typeName}.`));
1002947

1003-
if (symbol.escapedName !== typeName && symbol.escapedName !== 'default') {
948+
if ((symbol.escapedName as string) !== typeName && (symbol.escapedName as string) !== 'default') {
1004949
typeName = symbol.escapedName as string;
1005950
}
1006951

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

1011-
throwUnless(
1012-
modelTypes.length,
1013-
new GenerateMetadataError(`No matching model found for referenced type ${typeName}.`),
1014-
);
956+
throwUnless(modelTypes.length, new GenerateMetadataError(`No matching model found for referenced type ${typeName}.`));
1015957

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

1042984
const indexSignatureDeclaration = indexMember as ts.IndexSignatureDeclaration;
1043985
const indexType = new TypeResolver(indexSignatureDeclaration.parameters[0].type as ts.TypeNode, this.current, this.parentNode, this.context).resolve();
1044-
throwUnless(
1045-
indexType.dataType === 'string',
1046-
new GenerateMetadataError(`Only string indexers are supported.`, this.typeNode),
1047-
);
986+
throwUnless(indexType.dataType === 'string', new GenerateMetadataError(`Only string indexers are supported.`, this.typeNode));
1048987

1049988
return new TypeResolver(indexSignatureDeclaration.type, this.current, this.parentNode, this.context).resolve();
1050989
}

packages/cli/src/routeGeneration/templates/express.hbs

+18-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import multer from 'multer';
2121
{{else}}
2222
const multer = require('multer');
2323
{{/if}}
24-
const upload = multer({{{json multerOpts}}});
24+
2525
{{/if}}
2626

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

6060
// 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
6161

62+
63+
64+
65+
{{#if useFileUploads}}
66+
export function RegisterRoutes(app: Router,opts?:{multer?:ReturnType<typeof multer>}) {
67+
{{else}}
6268
export function RegisterRoutes(app: Router) {
69+
{{/if}}
70+
6371
// ###########################################################################################################
6472
// 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
6573
// Please look into the "controllerPathGlobs" config option described in the readme: https://github.com/lukeautry/tsoa
6674
// ###########################################################################################################
75+
76+
{{#if useFileUploads}}
77+
const upload = opts?.multer || multer({{{json multerOpts}}});
78+
{{/if}}
79+
80+
6781
{{#each controllers}}
6882
{{#each actions}}
6983
app.{{method}}('{{fullPath}}',
@@ -73,6 +87,9 @@ export function RegisterRoutes(app: Router) {
7387
{{#if uploadFile}}
7488
upload.fields({{json uploadFileName}}),
7589
{{/if}}
90+
{{#if uploadFiles}}
91+
upload.array('{{uploadFilesName}}'),
92+
{{/if}}
7693
...(fetchMiddlewares<RequestHandler>({{../name}})),
7794
...(fetchMiddlewares<RequestHandler>({{../name}}.prototype.{{name}})),
7895

0 commit comments

Comments
 (0)