diff --git a/packages/cli/src/metadataGeneration/parameterGenerator.ts b/packages/cli/src/metadataGeneration/parameterGenerator.ts index dc6c6164b..a4c33744d 100644 --- a/packages/cli/src/metadataGeneration/parameterGenerator.ts +++ b/packages/cli/src/metadataGeneration/parameterGenerator.ts @@ -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)); @@ -414,7 +419,7 @@ export class ParameterGenerator { const exampleLabels: Array = []; 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; @@ -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 { diff --git a/packages/cli/src/metadataGeneration/transformer/referenceTransformer.ts b/packages/cli/src/metadataGeneration/transformer/referenceTransformer.ts index 6caa0f798..a434f7e87 100644 --- a/packages/cli/src/metadataGeneration/transformer/referenceTransformer.ts +++ b/packages/cli/src/metadataGeneration/transformer/referenceTransformer.ts @@ -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[]); } diff --git a/packages/cli/src/metadataGeneration/typeResolver.ts b/packages/cli/src/metadataGeneration/typeResolver.ts index 8b34f20cd..2e25d0cfa 100644 --- a/packages/cli/src/metadataGeneration/typeResolver.ts +++ b/packages/cli/src/metadataGeneration/typeResolver.ts @@ -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(); } @@ -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; @@ -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 @@ -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(); @@ -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', @@ -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(); } @@ -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, ); } @@ -504,19 +475,14 @@ 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': @@ -524,7 +490,7 @@ export class TypeResolver { return { dataType: 'buffer' }; case 'Array': if (typeArguments && typeArguments.length === 1) { - return { + return { dataType: 'array', elementType: new TypeResolver(typeArguments[0], current, parentNode, context).resolve(), }; @@ -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; } } @@ -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; } @@ -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; @@ -729,8 +687,7 @@ 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)) { @@ -738,10 +695,7 @@ export class TypeResolver { 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( @@ -749,10 +703,7 @@ export class TypeResolver { 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; @@ -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'); @@ -995,12 +943,9 @@ 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; } @@ -1008,10 +953,7 @@ export class TypeResolver { 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' @@ -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(); } diff --git a/packages/cli/src/routeGeneration/templates/express.hbs b/packages/cli/src/routeGeneration/templates/express.hbs index b9174534d..ef9388215 100644 --- a/packages/cli/src/routeGeneration/templates/express.hbs +++ b/packages/cli/src/routeGeneration/templates/express.hbs @@ -21,7 +21,7 @@ import multer from 'multer'; {{else}} const multer = require('multer'); {{/if}} -const upload = multer({{{json multerOpts}}}); + {{/if}} {{#if authenticationModule}} @@ -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}) { +{{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}}', @@ -73,6 +87,9 @@ export function RegisterRoutes(app: Router) { {{#if uploadFile}} upload.fields({{json uploadFileName}}), {{/if}} + {{#if uploadFiles}} + upload.array('{{uploadFilesName}}'), + {{/if}} ...(fetchMiddlewares({{../name}})), ...(fetchMiddlewares({{../name}}.prototype.{{name}})), diff --git a/packages/cli/src/routeGeneration/templates/koa.hbs b/packages/cli/src/routeGeneration/templates/koa.hbs index 0ad925181..176666c6a 100644 --- a/packages/cli/src/routeGeneration/templates/koa.hbs +++ b/packages/cli/src/routeGeneration/templates/koa.hbs @@ -22,7 +22,6 @@ import multer from '@koa/multer'; {{else}} const multer = require('@koa/multer'); {{/if}} -const upload = multer({{{json multerOpts}}}); {{/if}} {{#if authenticationModule}} const koaAuthenticationRecasted = koaAuthentication as (req: KRequest, securityName: string, scopes?: string[], res?: KResponse) => Promise; @@ -59,11 +58,22 @@ const templateService = new KoaTemplateService(models, {{{ json minimalSwaggerCo // 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(router: KoaRouter,opts?:{multer?:ReturnType}) { +{{else}} export function RegisterRoutes(router: KoaRouter) { +{{/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}} router.{{method}}('{{fullPath}}', @@ -73,6 +83,9 @@ export function RegisterRoutes(router: KoaRouter) { {{#if uploadFile}} upload.fields({{json uploadFileName}}), {{/if}} + {{#if uploadFiles}} + upload.array('{{uploadFilesName}}'), + {{/if}} ...(fetchMiddlewares({{../name}})), ...(fetchMiddlewares({{../name}}.prototype.{{name}})), diff --git a/packages/cli/src/swagger/specGenerator2.ts b/packages/cli/src/swagger/specGenerator2.ts index 252336d5b..34a833d57 100644 --- a/packages/cli/src/swagger/specGenerator2.ts +++ b/packages/cli/src/swagger/specGenerator2.ts @@ -438,6 +438,7 @@ export class SpecGenerator2 extends SpecGenerator { if (typesWithoutUndefined.every(subType => subType.dataType === 'enum')) { const mergedEnum: Tsoa.EnumType = { dataType: 'enum', enums: [] }; typesWithoutUndefined.forEach(t => { + /* eslint-disable @typescript-eslint/no-unnecessary-type-assertion */ mergedEnum.enums = [...mergedEnum.enums, ...(t as Tsoa.EnumType).enums]; }); return this.getSwaggerTypeForEnumType(mergedEnum); diff --git a/packages/cli/src/swagger/specGenerator3.ts b/packages/cli/src/swagger/specGenerator3.ts index 65059adec..cc17ee02d 100644 --- a/packages/cli/src/swagger/specGenerator3.ts +++ b/packages/cli/src/swagger/specGenerator3.ts @@ -102,6 +102,7 @@ export class SpecGenerator3 extends SpecGenerator { type: 'http', } as Swagger.BasicSecurity3; } else if (definitions[key].type === 'oauth2') { + /* eslint-disable @typescript-eslint/no-unnecessary-type-assertion */ const definition = definitions[key] as | Swagger.OAuth2PasswordSecurity | Swagger.OAuth2ApplicationSecurity diff --git a/packages/cli/src/utils/jsDocUtils.ts b/packages/cli/src/utils/jsDocUtils.ts index 3957cd80a..dc85658ad 100644 --- a/packages/cli/src/utils/jsDocUtils.ts +++ b/packages/cli/src/utils/jsDocUtils.ts @@ -20,7 +20,7 @@ export function getJSDocComment(node: ts.Node, tagName: string) { } export function getJSDocComments(node: ts.Node, tagName: string) { - const tags = getJSDocTags(node, tag => tag.tagName.text === tagName || tag.tagName.escapedText === tagName); + const tags = getJSDocTags(node, tag => tag.tagName.text === tagName || (tag.tagName.escapedText as string) === tagName); if (tags.length === 0) { return; } diff --git a/packages/runtime/src/interfaces/controller.ts b/packages/runtime/src/interfaces/controller.ts index d2835fd65..605811220 100644 --- a/packages/runtime/src/interfaces/controller.ts +++ b/packages/runtime/src/interfaces/controller.ts @@ -1,4 +1,4 @@ -import type { OutgoingHttpHeaders } from "node:http"; +import type { OutgoingHttpHeaders } from 'node:http'; type HeaderNames = keyof OutgoingHttpHeaders; type HeaderValue = OutgoingHttpHeaders[H]; @@ -16,7 +16,7 @@ export class Controller { } public setHeader(name: H, value?: HeaderValue): void; - public setHeader(name: string, value?: string | string[]): void + public setHeader(name: string, value?: string | string[]): void; public setHeader(name: string, value?: string | string[]) { this.headers[name] = value; diff --git a/tests/fixtures/express-router-with-custom-multer/authentication.ts b/tests/fixtures/express-router-with-custom-multer/authentication.ts new file mode 100644 index 000000000..4721b1b7b --- /dev/null +++ b/tests/fixtures/express-router-with-custom-multer/authentication.ts @@ -0,0 +1,32 @@ +import * as express from 'express'; + +export function expressAuthentication(req: express.Request, name: string, _scopes: string[] | undefined, res: express.Response): Promise { + if (name === 'api_key') { + let token; + if (req.query && req.query.access_token) { + token = req.query.access_token; + } else { + return Promise.reject({}); + } + + if (token === 'abc123456') { + return Promise.resolve({ + id: 1, + name: 'Ironman', + }); + } else if (token === 'xyz123456') { + return Promise.resolve({ + id: 2, + name: 'Thor', + }); + } else { + return Promise.reject({}); + } + } else { + if (req.query && req.query.tsoa && req.query.tsoa === 'abc123456') { + return Promise.resolve({}); + } else { + return Promise.reject({}); + } + } +} diff --git a/tests/fixtures/express-router-with-custom-multer/server.ts b/tests/fixtures/express-router-with-custom-multer/server.ts new file mode 100644 index 000000000..c2f3bc2c9 --- /dev/null +++ b/tests/fixtures/express-router-with-custom-multer/server.ts @@ -0,0 +1,41 @@ +import * as bodyParser from 'body-parser'; +import * as express from 'express'; +import * as methodOverride from 'method-override'; +import '../controllers/rootController'; + +import { RegisterRoutes } from './routes'; + +export const app: express.Express = express(); +export const router = express.Router(); +app.use('/v1', router); +router.use(bodyParser.urlencoded({ extended: true })); +router.use(bodyParser.json()); +router.use(methodOverride()); +router.use((req: any, res: any, next: any) => { + req.stringValue = 'fancyStringForContext'; + next(); +}); + +import multer = require('multer'); + +RegisterRoutes(router, { + multer: multer({ + limits: { + fieldNameSize: 120, + }, + }), +}); + +// It's important that this come after the main routes are registered +app.use((err: any, req: express.Request, res: express.Response, next: express.NextFunction) => { + const status = err.status || 500; + const body: any = { + fields: err.fields || undefined, + message: err.message || 'An error occurred during the request.', + name: err.name, + status, + }; + res.status(status).json(body); +}); + +app.listen(); diff --git a/tests/fixtures/express-router/server.ts b/tests/fixtures/express-router/server.ts index 09520928a..b613b7057 100644 --- a/tests/fixtures/express-router/server.ts +++ b/tests/fixtures/express-router/server.ts @@ -15,6 +15,7 @@ router.use((req: any, res: any, next: any) => { req.stringValue = 'fancyStringForContext'; next(); }); + RegisterRoutes(router); // It's important that this come after the main routes are registered diff --git a/tests/integration/express-server-custom-multer.spec.ts b/tests/integration/express-server-custom-multer.spec.ts new file mode 100644 index 000000000..1f0843223 --- /dev/null +++ b/tests/integration/express-server-custom-multer.spec.ts @@ -0,0 +1,205 @@ +import { File } from '@tsoa/runtime'; +import { expect } from 'chai'; +import { readFileSync } from 'fs'; +import 'mocha'; +import { resolve } from 'path'; +import * as request from 'supertest'; +import { app } from '../fixtures/express/server'; + +const basePath = '/v1'; + +describe('Express Server With custom multer', () => { + describe('file upload With custom multer instance', () => { + it('can post a file', () => { + const formData = { someFile: '@../package.json' }; + return verifyFileUploadRequest(basePath + '/PostTest/File', formData, (_err, res) => { + const packageJsonBuffer = readFileSync(resolve(__dirname, '../package.json')); + const returnedBuffer = Buffer.from(res.body.buffer); + expect(res.body).to.not.be.undefined; + expect(res.body.fieldname).to.equal('someFile'); + expect(res.body.originalname).to.equal('package.json'); + expect(res.body.encoding).to.be.not.undefined; + expect(res.body.mimetype).to.equal('application/json'); + expect(Buffer.compare(returnedBuffer, packageJsonBuffer)).to.equal(0); + }); + }); + + it('can post a file without name', () => { + const formData = { aFile: '@../package.json' }; + return verifyFileUploadRequest(basePath + '/PostTest/FileWithoutName', formData, (_err, res) => { + expect(res.body).to.not.be.undefined; + expect(res.body.fieldname).to.equal('aFile'); + }); + }); + + it('cannot post a file with wrong attribute name', async () => { + const formData = { wrongAttributeName: '@../package.json' }; + verifyFileUploadRequest(basePath + '/PostTest/File', formData, (_err, res) => { + expect(res.status).to.equal(500); + expect(res.text).to.equal('{"message":"Unexpected field","name":"MulterError","status":500}'); + }); + }); + + it('can post multiple files with other form fields', () => { + const formData = { + a: 'b', + c: 'd', + someFiles: ['@../package.json', '@../tsconfig.json'], + }; + + return verifyFileUploadRequest(basePath + '/PostTest/ManyFilesAndFormFields', formData, (_err, res) => { + for (const file of res.body as File[]) { + const packageJsonBuffer = readFileSync(resolve(__dirname, `../${file.originalname}`)); + const returnedBuffer = Buffer.from(file.buffer); + expect(file).to.not.be.undefined; + expect(file.fieldname).to.be.not.undefined; + expect(file.originalname).to.be.not.undefined; + expect(file.encoding).to.be.not.undefined; + expect(file.mimetype).to.equal('application/json'); + expect(Buffer.compare(returnedBuffer, packageJsonBuffer)).to.equal(0); + } + }); + }); + + it('can post single file to multi file field', () => { + const formData = { + a: 'b', + c: 'd', + someFiles: ['@../package.json'], + }; + + return verifyFileUploadRequest(basePath + '/PostTest/ManyFilesAndFormFields', formData, (_err, res) => { + expect(res.body).to.be.length(1); + }); + }); + + it('can post multiple files with different field', () => { + const formData = { + file_a: '@../package.json', + file_b: '@../tsconfig.json', + }; + return verifyFileUploadRequest(`${basePath}/PostTest/ManyFilesInDifferentFields`, formData, (_err, res) => { + for (const file of res.body as File[]) { + const packageJsonBuffer = readFileSync(resolve(__dirname, `../${file.originalname}`)); + const returnedBuffer = Buffer.from(file.buffer); + expect(file).to.not.be.undefined; + expect(file.fieldname).to.be.not.undefined; + expect(file.originalname).to.be.not.undefined; + expect(file.encoding).to.be.not.undefined; + expect(file.mimetype).to.equal('application/json'); + expect(Buffer.compare(returnedBuffer, packageJsonBuffer)).to.equal(0); + } + }); + }); + + it('can post multiple files with different array fields', () => { + const formData = { + files_a: ['@../package.json', '@../tsconfig.json'], + file_b: '@../tsoa.json', + files_c: ['@../tsconfig.json', '@../package.json'], + }; + return verifyFileUploadRequest(`${basePath}/PostTest/ManyFilesInDifferentArrayFields`, formData, (_err, res) => { + for (const fileList of res.body as File[][]) { + for (const file of fileList) { + const packageJsonBuffer = readFileSync(resolve(__dirname, `../${file.originalname}`)); + const returnedBuffer = Buffer.from(file.buffer); + expect(file).to.not.be.undefined; + expect(file.fieldname).to.be.not.undefined; + expect(file.originalname).to.be.not.undefined; + expect(file.encoding).to.be.not.undefined; + expect(file.mimetype).to.equal('application/json'); + expect(Buffer.compare(returnedBuffer, packageJsonBuffer)).to.equal(0); + } + } + }); + }); + + it('can post mixed form data content with file and not providing optional file', () => { + const formData = { + username: 'test', + avatar: '@../tsconfig.json', + }; + return verifyFileUploadRequest(`${basePath}/PostTest/MixedFormDataWithFilesContainsOptionalFile`, formData, (_err, res) => { + const file = res.body.avatar; + const packageJsonBuffer = readFileSync(resolve(__dirname, `../${file.originalname}`)); + const returnedBuffer = Buffer.from(file.buffer); + expect(res.body.username).to.equal(formData.username); + expect(res.body.optionalAvatar).to.undefined; + expect(file).to.not.be.undefined; + expect(file.fieldname).to.be.not.undefined; + expect(file.originalname).to.be.not.undefined; + expect(file.encoding).to.be.not.undefined; + expect(file.mimetype).to.equal('application/json'); + expect(Buffer.compare(returnedBuffer, packageJsonBuffer)).to.equal(0); + }); + }); + + it('can post mixed form data content with file and provides optional file', () => { + const formData = { + username: 'test', + avatar: '@../tsconfig.json', + optionalAvatar: '@../package.json', + }; + return verifyFileUploadRequest(`${basePath}/PostTest/MixedFormDataWithFilesContainsOptionalFile`, formData, (_err, res) => { + expect(res.body.username).to.equal(formData.username); + for (const fieldName of ['avatar', 'optionalAvatar']) { + const file = res.body[fieldName]; + const packageJsonBuffer = readFileSync(resolve(__dirname, `../${file.originalname}`)); + const returnedBuffer = Buffer.from(file.buffer); + expect(file).to.not.be.undefined; + expect(file.fieldname).to.be.not.undefined; + expect(file.originalname).to.be.not.undefined; + expect(file.encoding).to.be.not.undefined; + expect(file.mimetype).to.equal('application/json'); + expect(Buffer.compare(returnedBuffer, packageJsonBuffer)).to.equal(0); + } + }); + }); + + function verifyFileUploadRequest( + path: string, + formData: any, + verifyResponse: (err: any, res: request.Response) => any = () => { + /**/ + }, + expectedStatus?: number, + ) { + return verifyRequest( + verifyResponse, + request => + Object.keys(formData).reduce((req, key) => { + const values = [].concat(formData[key]); + values.forEach((v: any) => (v.startsWith('@') ? req.attach(key, resolve(__dirname, v.slice(1))) : req.field(key, v))); + return req; + }, request.post(path)), + expectedStatus, + ); + } + }); + + function verifyRequest(verifyResponse: (err: any, res: request.Response) => any, methodOperation: (request: request.SuperTest) => request.Test, expectedStatus = 200) { + return new Promise((resolve, reject) => { + methodOperation(request(app)) + .expect(expectedStatus) + .end((err: any, res: any) => { + let parsedError: any; + try { + parsedError = JSON.parse(res.error); + } catch (err) { + parsedError = res?.error; + } + + if (err) { + reject({ + error: err, + response: parsedError, + }); + return; + } + + verifyResponse(parsedError, res); + resolve(); + }); + }); + } +}); diff --git a/tests/prepare.ts b/tests/prepare.ts index 3102bcd26..3c39f0dd8 100644 --- a/tests/prepare.ts +++ b/tests/prepare.ts @@ -56,6 +56,22 @@ const log = async (label: string, fn: () => Promise) => { metadata, ), ), + log('Express Router Route Generation With custom multer instance', () => + generateRoutes( + { + noImplicitAdditionalProperties: 'silently-remove-extras', + bodyCoercion: true, + authenticationModule: './fixtures/express-router-with-custom-multer/authentication.ts', + entryFile: './fixtures/express-router-with-custom-multer/server.ts', + middleware: 'express', + routesDir: './fixtures/express-router-with-custom-multer', + }, + undefined, + undefined, + metadata, + ), + ), + log('Express Route Generation, OpenAPI3, noImplicitAdditionalProperties', () => generateRoutes( {