diff --git a/packages/cli/src/routeGeneration/routeGenerator.ts b/packages/cli/src/routeGeneration/routeGenerator.ts index e88171bfe..5eb470c27 100644 --- a/packages/cli/src/routeGeneration/routeGenerator.ts +++ b/packages/cli/src/routeGeneration/routeGenerator.ts @@ -94,7 +94,7 @@ export abstract class AbstractRouteGenerator parameter.type.dataType === 'file'); + const uploadFilesWithDifferentFieldParameter = method.parameters.filter(parameter => parameter.type.dataType === 'file'); const uploadFilesParameter = method.parameters.find(parameter => parameter.type.dataType === 'array' && parameter.type.elementType.dataType === 'file'); return { fullPath: normalisedFullPath, @@ -102,8 +102,10 @@ export abstract class AbstractRouteGenerator 0, + uploadFileName: uploadFilesWithDifferentFieldParameter.map((parameter) => ({ + 'name': parameter.name, + })), uploadFiles: !!uploadFilesParameter, uploadFilesName: uploadFilesParameter?.name, security: method.security, diff --git a/packages/cli/src/routeGeneration/templates/express/express.hbs b/packages/cli/src/routeGeneration/templates/express/express.hbs index 52e93c724..b1709b7f5 100644 --- a/packages/cli/src/routeGeneration/templates/express/express.hbs +++ b/packages/cli/src/routeGeneration/templates/express/express.hbs @@ -67,7 +67,7 @@ export function RegisterRoutes(app: Router) { authenticateMiddleware({{json security}}), {{/if}} {{#if uploadFile}} - upload.single('{{uploadFileName}}'), + upload.fields({{json uploadFileName}}), {{/if}} {{#if uploadFiles}} upload.array('{{uploadFilesName}}'), diff --git a/packages/cli/src/routeGeneration/templates/express/expressTemplateService.ts b/packages/cli/src/routeGeneration/templates/express/expressTemplateService.ts index d6b123149..915991032 100644 --- a/packages/cli/src/routeGeneration/templates/express/expressTemplateService.ts +++ b/packages/cli/src/routeGeneration/templates/express/expressTemplateService.ts @@ -1,5 +1,5 @@ import { Request as ExRequest, Response as ExResponse } from 'express'; -import { FieldErrors, HttpStatusCodeLiteral, TsoaResponse, ValidateError, ValidationService } from "@tsoa/runtime"; +import { FieldErrors, HttpStatusCodeLiteral, TsoaResponse, ValidateError, ValidationService } from '@tsoa/runtime'; import { TemplateService, isController } from '../templateService'; @@ -25,70 +25,74 @@ export class ExpressTemplateService implements TemplateService next(error)); } returnHandler(response: any, headers: any = {}, statusCode?: number, data?: any) { if (response.headersSent) { - return; + return; } Object.keys(headers).forEach((name: string) => { - response.set(name, headers[name]); + response.set(name, headers[name]); }); if (data && typeof data.pipe === 'function' && data.readable && typeof data._read === 'function') { - response.status(statusCode || 200) - data.pipe(response); + response.status(statusCode || 200); + data.pipe(response); } else if (data !== null && data !== undefined) { - response.status(statusCode || 200).json(data); + response.status(statusCode || 200).json(data); } else { - response.status(statusCode || 204).end(); + response.status(statusCode || 204).end(); } } - responder(response: any): TsoaResponse { + responder(response: any): TsoaResponse { return (status, data, headers) => { - this.returnHandler(response, headers, status, data); + this.returnHandler(response, headers, status, data); }; } getValidatedArgs(args: any, request: ExRequest, response: ExResponse): any[] { - const fieldErrors: FieldErrors = {}; - const values = Object.keys(args).map((key) => { - const name = args[key].name; - switch (args[key].in) { - case 'request': - return request; - case 'request-prop': - return this.validationService.ValidateParam(args[key], (request as any)[name], name, fieldErrors, undefined, this.minimalSwaggerConfig); - case 'query': - return this.validationService.ValidateParam(args[key], request.query[name], name, fieldErrors, undefined, this.minimalSwaggerConfig); - case 'queries': - return this.validationService.ValidateParam(args[key], request.query, name, fieldErrors, undefined, this.minimalSwaggerConfig); - case 'path': - return this.validationService.ValidateParam(args[key], request.params[name], name, fieldErrors, undefined, this.minimalSwaggerConfig); - case 'header': - return this.validationService.ValidateParam(args[key], request.header(name), name, fieldErrors, undefined, this.minimalSwaggerConfig); - case 'body': - return this.validationService.ValidateParam(args[key], request.body, name, fieldErrors, undefined, this.minimalSwaggerConfig); - case 'body-prop': - return this.validationService.ValidateParam(args[key], request.body[name], name, fieldErrors, 'body.', this.minimalSwaggerConfig); - case 'formData': - if (args[key].dataType === 'file') { - return this.validationService.ValidateParam(args[key], request.file, name, fieldErrors, undefined, this.minimalSwaggerConfig); - } else if (args[key].dataType === 'array' && args[key].array.dataType === 'file') { - return this.validationService.ValidateParam(args[key], request.files, name, fieldErrors, undefined, this.minimalSwaggerConfig); - } else { - return this.validationService.ValidateParam(args[key], request.body[name], name, fieldErrors, undefined, this.minimalSwaggerConfig); - } - case 'res': - return this.responder(response); + const fieldErrors: FieldErrors = {}; + const values = Object.keys(args).map(key => { + const name = args[key].name; + switch (args[key].in) { + case 'request': + return request; + case 'request-prop': + return this.validationService.ValidateParam(args[key], (request as any)[name], name, fieldErrors, undefined, this.minimalSwaggerConfig); + case 'query': + return this.validationService.ValidateParam(args[key], request.query[name], name, fieldErrors, undefined, this.minimalSwaggerConfig); + case 'queries': + return this.validationService.ValidateParam(args[key], request.query, name, fieldErrors, undefined, this.minimalSwaggerConfig); + case 'path': + return this.validationService.ValidateParam(args[key], request.params[name], name, fieldErrors, undefined, this.minimalSwaggerConfig); + case 'header': + return this.validationService.ValidateParam(args[key], request.header(name), name, fieldErrors, undefined, this.minimalSwaggerConfig); + case 'body': + return this.validationService.ValidateParam(args[key], request.body, name, fieldErrors, undefined, this.minimalSwaggerConfig); + case 'body-prop': + return this.validationService.ValidateParam(args[key], request.body[name], name, fieldErrors, 'body.', this.minimalSwaggerConfig); + case 'formData': { + const formFiles = Object.keys(args).filter(argKey => args[argKey].dataType === 'file'); + if (formFiles.length > 0) { + const requestFiles = request.files as { [fileName: string]: Express.Multer.File[] }; + const fileArgs = this.validationService.ValidateParam(args[key], requestFiles[name], name, fieldErrors, undefined, this.minimalSwaggerConfig); + return fileArgs.length === 1 ? fileArgs[0] : fileArgs; + } else if (args[key].dataType === 'array' && args[key].array.dataType === 'file') { + return this.validationService.ValidateParam(args[key], request.files, name, fieldErrors, undefined, this.minimalSwaggerConfig); + } else { + return this.validationService.ValidateParam(args[key], request.body[name], name, fieldErrors, undefined, this.minimalSwaggerConfig); + } } + case 'res': + return this.responder(response); + } }); if (Object.keys(fieldErrors).length > 0) { - throw new ValidateError(fieldErrors, ''); + throw new ValidateError(fieldErrors, ''); } return values; } diff --git a/packages/cli/src/routeGeneration/templates/hapi/hapi.hbs b/packages/cli/src/routeGeneration/templates/hapi/hapi.hbs index 097b16004..010898bb3 100644 --- a/packages/cli/src/routeGeneration/templates/hapi/hapi.hbs +++ b/packages/cli/src/routeGeneration/templates/hapi/hapi.hbs @@ -66,9 +66,11 @@ export function RegisterRoutes(server: any) { }, {{/if}} {{#if uploadFile}} + {{#each uploadFileName}} { - method: fileUploadMiddleware('{{uploadFileName}}', false) + method: fileUploadMiddleware('{{name}}', false) }, + {{/each}} {{/if}} {{#if uploadFiles}} { diff --git a/packages/cli/src/routeGeneration/templates/koa/koa.hbs b/packages/cli/src/routeGeneration/templates/koa/koa.hbs index 0d7ba7502..f7eb68b10 100644 --- a/packages/cli/src/routeGeneration/templates/koa/koa.hbs +++ b/packages/cli/src/routeGeneration/templates/koa/koa.hbs @@ -68,7 +68,7 @@ export function RegisterRoutes(router: KoaRouter) { authenticateMiddleware({{json security}}), {{/if}} {{#if uploadFile}} - upload.single('{{uploadFileName}}'), + upload.fields({{json uploadFileName}}), {{/if}} {{#if uploadFiles}} upload.array('{{uploadFilesName}}'), diff --git a/packages/cli/src/routeGeneration/templates/koa/koaTemplateService.ts b/packages/cli/src/routeGeneration/templates/koa/koaTemplateService.ts index 49509f548..66cd9002c 100644 --- a/packages/cli/src/routeGeneration/templates/koa/koaTemplateService.ts +++ b/packages/cli/src/routeGeneration/templates/koa/koaTemplateService.ts @@ -1,6 +1,6 @@ import type { Context } from 'koa'; -import { TsoaResponse, HttpStatusCodeLiteral, FieldErrors, ValidationService, ValidateError } from "@tsoa/runtime"; -import { TemplateService, isController } from "../templateService"; +import { TsoaResponse, HttpStatusCodeLiteral, FieldErrors, ValidationService, ValidateError } from '@tsoa/runtime'; +import { TemplateService, isController } from '../templateService'; export class KoaTemplateService implements TemplateService { private readonly validationService: ValidationService; @@ -19,8 +19,8 @@ export class KoaTemplateService implements TemplateService { let headers; if (isController(controllerObj)) { - headers = controllerObj.getHeaders(); - statusCode = controllerObj.getStatus() || statusCode; + headers = controllerObj.getHeaders(); + statusCode = controllerObj.getStatus() || statusCode; } return this.returnHandler(context, headers, statusCode, data, next); }) @@ -33,14 +33,14 @@ export class KoaTemplateService implements TemplateService { returnHandler(context: Context, headers: any, statusCode?: number | undefined, data?: any, next?: any) { if (!context.headerSent && !(context.response as any).__tsoaResponded) { if (data !== null && data !== undefined) { - context.body = data; - context.status = 200; + context.body = data; + context.status = 200; } else { - context.status = 204; + context.status = 204; } if (statusCode) { - context.status = statusCode; + context.status = statusCode; } context.set(headers); @@ -49,48 +49,52 @@ export class KoaTemplateService implements TemplateService { } } - responder(context: any, next?: any): TsoaResponse { + responder(context: any, next?: any): TsoaResponse { return (status, data, headers) => { - this.returnHandler(context, headers, status, data, next); + this.returnHandler(context, headers, status, data, next); }; } getValidatedArgs(args: any, request: any, context: Context, next: () => any): any[] { const errorFields: FieldErrors = {}; const values = Object.keys(args).map(key => { - const name = args[key].name; - switch (args[key].in) { + const name = args[key].name; + switch (args[key].in) { case 'request': return context.request; case 'request-prop': return this.validationService.ValidateParam(args[key], (context.request as any)[name], name, errorFields, undefined, this.minimalSwaggerConfig); case 'query': - return this.validationService.ValidateParam(args[key], context.request.query[name], name, errorFields, undefined, this.minimalSwaggerConfig); + return this.validationService.ValidateParam(args[key], context.request.query[name], name, errorFields, undefined, this.minimalSwaggerConfig); case 'queries': - return this.validationService.ValidateParam(args[key], context.request.query, name, errorFields, undefined, this.minimalSwaggerConfig); + return this.validationService.ValidateParam(args[key], context.request.query, name, errorFields, undefined, this.minimalSwaggerConfig); case 'path': - return this.validationService.ValidateParam(args[key], context.params[name], name, errorFields, undefined, this.minimalSwaggerConfig); + return this.validationService.ValidateParam(args[key], context.params[name], name, errorFields, undefined, this.minimalSwaggerConfig); case 'header': - return this.validationService.ValidateParam(args[key], context.request.headers[name], name, errorFields, undefined, this.minimalSwaggerConfig); + return this.validationService.ValidateParam(args[key], context.request.headers[name], name, errorFields, undefined, this.minimalSwaggerConfig); case 'body': - return this.validationService.ValidateParam(args[key], (context.request as any).body, name, errorFields, undefined, this.minimalSwaggerConfig); + return this.validationService.ValidateParam(args[key], (context.request as any).body, name, errorFields, undefined, this.minimalSwaggerConfig); case 'body-prop': - return this.validationService.ValidateParam(args[key], (context.request as any).body[name], name, errorFields, 'body.', this.minimalSwaggerConfig); - case 'formData': - if (args[key].dataType === 'file') { - return this.validationService.ValidateParam(args[key], (context.request as any).file, name, errorFields, undefined, this.minimalSwaggerConfig); - } else if (args[key].dataType === 'array' && args[key].array.dataType === 'file') { - return this.validationService.ValidateParam(args[key], (context.request as any).files, name, errorFields, undefined, this.minimalSwaggerConfig); - } else { - return this.validationService.ValidateParam(args[key], (context.request as any).body[name], name, errorFields, undefined, this.minimalSwaggerConfig); - } - case 'res': - return this.responder(context, next); + return this.validationService.ValidateParam(args[key], (context.request as any).body[name], name, errorFields, 'body.', this.minimalSwaggerConfig); + case 'formData': { + const files = Object.keys(args).filter(argKey => args[argKey].dataType === 'file'); + const contextRequest = context.request as any; + if (files.length > 0) { + const fileArgs = this.validationService.ValidateParam(args[key], contextRequest.files[name], name, errorFields, undefined, this.minimalSwaggerConfig); + return fileArgs.length === 1 ? fileArgs[0] : fileArgs; + } else if (args[key].dataType === 'array' && args[key].array.dataType === 'file') { + return this.validationService.ValidateParam(args[key], contextRequest.files, name, errorFields, undefined, this.minimalSwaggerConfig); + } else { + return this.validationService.ValidateParam(args[key], contextRequest.body[name], name, errorFields, undefined, this.minimalSwaggerConfig); + } } + case 'res': + return this.responder(context, next); + } }); if (Object.keys(errorFields).length > 0) { - throw new ValidateError(errorFields, ''); + throw new ValidateError(errorFields, ''); } return values; -} + } } diff --git a/tests/fixtures/controllers/postController.ts b/tests/fixtures/controllers/postController.ts index 5fd7e2c48..11e645607 100644 --- a/tests/fixtures/controllers/postController.ts +++ b/tests/fixtures/controllers/postController.ts @@ -67,6 +67,14 @@ export class PostTestController { return files; } + @Post('ManyFilesInDifferentFields') + public async postWithDifferentFields( + @UploadedFile('file_a') fileA: File, + @UploadedFile('file_b') fileB: File, + ): Promise { + return [fileA, fileB]; + } + /** * * @param aFile File description of multipart diff --git a/tests/integration/express-server.spec.ts b/tests/integration/express-server.spec.ts index 58865d809..7215947a9 100644 --- a/tests/integration/express-server.spec.ts +++ b/tests/integration/express-server.spec.ts @@ -1524,6 +1524,25 @@ describe('Express Server', () => { }); }); + 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); + } + }); + }); + function verifyFileUploadRequest( path: string, formData: any, diff --git a/tests/integration/hapi-server.spec.ts b/tests/integration/hapi-server.spec.ts index 238f9a07b..8e75c5e1e 100644 --- a/tests/integration/hapi-server.spec.ts +++ b/tests/integration/hapi-server.spec.ts @@ -1423,6 +1423,25 @@ describe('Hapi Server', () => { }); }); + 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); + } + }); + }); + function verifyFileUploadRequest( path: string, formData: any, diff --git a/tests/integration/koa-server.spec.ts b/tests/integration/koa-server.spec.ts index 7654ca1c5..d9e8e752c 100644 --- a/tests/integration/koa-server.spec.ts +++ b/tests/integration/koa-server.spec.ts @@ -1387,6 +1387,25 @@ describe('Koa Server', () => { }); }); + 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); + } + }); + }); + function verifyFileUploadRequest( path: string, formData: any,