diff --git a/src/decorators/parameter.ts b/src/decorators/parameter.ts index 0c04f762f..c45f7884b 100644 --- a/src/decorators/parameter.ts +++ b/src/decorators/parameter.ts @@ -66,3 +66,12 @@ export function UploadedFile(name?: string): any { export function UploadedFiles(name?: string): any { return () => { return; }; }; + +/** + * Inject uploaded files + * + * @param {string} [name] The name of the uploaded files parameter + */ +export function FormField(name?: string): any { + return () => { return; }; +}; diff --git a/src/metadataGeneration/metadataGenerator.ts b/src/metadataGeneration/metadataGenerator.ts index bec3609cd..d89423b7c 100644 --- a/src/metadataGeneration/metadataGenerator.ts +++ b/src/metadataGeneration/metadataGenerator.ts @@ -101,7 +101,7 @@ export interface Security { export interface Type { typeName: string; -}; +} export interface EnumerateType extends Type { enumMembers: string[]; diff --git a/src/metadataGeneration/methodGenerator.ts b/src/metadataGeneration/methodGenerator.ts index 09b78cc3c..3d44c69bc 100644 --- a/src/metadataGeneration/methodGenerator.ts +++ b/src/metadataGeneration/methodGenerator.ts @@ -56,12 +56,19 @@ export class MethodGenerator { const bodyParameters = parameters.filter(p => p.in === 'body'); const bodyProps = parameters.filter(p => p.in === 'body-prop'); + const hasFormDataParameters = parameters.some(p => p.in === 'formData'); + const hasBodyParameter = bodyProps.length + bodyParameters.length > 0; + if (bodyParameters.length > 1) { throw new Error(`Only one body parameter allowed in '${this.getCurrentLocation()}' method.`); } if (bodyParameters.length > 0 && bodyProps.length > 0) { throw new Error(`Choose either during @Body or @BodyProp in '${this.getCurrentLocation()}' method.`); } + if (hasBodyParameter && hasFormDataParameters) { + throw new Error(`@Body or @BodyProp cannot be used with @FormField, @UploadedFile, or @UploadedFiles in '${this.getCurrentLocation()}' method.`); + } + return parameters; } diff --git a/src/metadataGeneration/parameterGenerator.ts b/src/metadataGeneration/parameterGenerator.ts index 32f6e0e79..3d263ed28 100644 --- a/src/metadataGeneration/parameterGenerator.ts +++ b/src/metadataGeneration/parameterGenerator.ts @@ -3,6 +3,11 @@ import { ResolveType } from './resolveType'; import { getDecoratorName, getDecoratorTextValue } from './../utils/decoratorUtils'; import * as ts from 'typescript'; +const METHODS_SUPPORTING_BODY = ['post', 'put', 'patch']; +const PARAMETER_DECORATORS = ['header', 'query', 'path', 'body', 'bodyprop', 'request', 'uploadedfile', 'uploadedfiles', 'formfield']; +const SUPPORTED_PARAMETER_TYPES = ['string', 'integer', 'long', 'float', 'double', 'date', 'datetime', 'buffer', + 'boolean', 'enum', 'file', 'file[]']; + export class ParameterGenerator { constructor( private readonly parameter: ts.ParameterDeclaration, @@ -30,6 +35,8 @@ export class ParameterGenerator { return this.getUploadedFileParameter(this.parameter); case 'UploadedFiles': return this.getUploadedFilesParameter(this.parameter); + case 'FormField': + return this.getFormFieldParameter(this.parameter); default: return this.getPathParameter(this.parameter); } @@ -93,7 +100,7 @@ export class ParameterGenerator { const parameterName = (parameter.name as ts.Identifier).text; const type = this.getValidatedType(parameter); - if (!this.supportPathDataType(type)) { + if (!this.supportParameterType(type)) { throw new InvalidParameterException(`Parameter '${parameterName}' can't be passed as a header parameter in '${this.getCurrentLocation()}'.`); } @@ -111,7 +118,7 @@ export class ParameterGenerator { const parameterName = (parameter.name as ts.Identifier).text; const type = this.getValidatedType(parameter); - if (!this.supportPathDataType(type)) { + if (!this.supportParameterType(type)) { throw new InvalidParameterException(`Parameter '${parameterName}' can't be passed as a query parameter in '${this.getCurrentLocation()}'.`); } @@ -130,11 +137,11 @@ export class ParameterGenerator { const type = this.getValidatedType(parameter); const pathName = getDecoratorTextValue(this.parameter, ident => ident.text === 'Path') || parameterName; - if (!this.supportPathDataType(type)) { + if (!this.supportParameterType(type)) { throw new InvalidParameterException(`Parameter '${parameterName}:${type}' can't be passed as a path parameter in '${this.getCurrentLocation()}'.`); } if (!this.path.includes(`{${pathName}}`)) { - throw new Error(`Parameter '${parameterName}' can't macth in path: '${this.path}'`); + throw new Error(`Parameter '${parameterName}' can't match in path: '${this.path}'`); } return { @@ -151,7 +158,7 @@ export class ParameterGenerator { const parameterName = (parameter.name as ts.Identifier).text; const type = {typeName: 'file'}; - if (!this.supportPathDataType(type)) { + if (!this.supportParameterType(type)) { throw new InvalidParameterException(`Parameter '${parameterName}:${type}' can't be passed as an uploaded file parameter in '${this.getCurrentLocation()}'.`); } @@ -169,7 +176,7 @@ export class ParameterGenerator { const parameterName = (parameter.name as ts.Identifier).text; const type = {typeName: 'file[]'}; - if (!this.supportPathDataType(type)) { + if (!this.supportParameterType(type)) { throw new InvalidParameterException(`Parameter '${parameterName}:${type}' can't be passed as an uploaded files parameter in '${this.getCurrentLocation()}'.`); } @@ -183,6 +190,24 @@ export class ParameterGenerator { }; } + private getFormFieldParameter(parameter: ts.ParameterDeclaration): Parameter { + const parameterName = (parameter.name as ts.Identifier).text; + const type = {typeName: 'string'}; + + if (!this.supportParameterType(type)) { + throw new InvalidParameterException(`Parameter '${parameterName}:${type}' can't be passed as form field parameter in '${this.getCurrentLocation()}'.`); + } + + return { + description: this.getParameterDescription(parameter), + in: 'formData', + name: getDecoratorTextValue(this.parameter, ident => ident.text === 'FormField') || parameterName, + required: true, + type, + parameterName + }; + } + private getParameterDescription(node: ts.ParameterDeclaration) { const symbol = MetadataGenerator.current.typeChecker.getSymbolAtLocation(node.name); @@ -193,15 +218,15 @@ export class ParameterGenerator { } private supportsBodyParameters(method: string) { - return ['post', 'put', 'patch'].some(m => m === method.toLowerCase()); + return METHODS_SUPPORTING_BODY.some(m => m === method.toLowerCase()); } private supportParameterDecorator(decoratorName: string) { - return ['header', 'query', 'parem', 'body', 'bodyprop', 'request', 'uploadedfile', 'uploadedfiles'].some(d => d === decoratorName.toLocaleLowerCase()); + return PARAMETER_DECORATORS.some(d => d === decoratorName.toLocaleLowerCase()); } - private supportPathDataType(parameterType: Type) { - return ['string', 'integer', 'long', 'float', 'double', 'date', 'datetime', 'buffer', 'boolean', 'enum', 'file', 'file[]'].find(t => t === parameterType.typeName); + private supportParameterType(parameterType: Type) { + return SUPPORTED_PARAMETER_TYPES.find(t => t === parameterType.typeName); } private getValidatedType(parameter: ts.ParameterDeclaration) { diff --git a/src/routeGeneration/templates/koa.ts b/src/routeGeneration/templates/koa.ts index dd9b69a47..dee6672af 100644 --- a/src/routeGeneration/templates/koa.ts +++ b/src/routeGeneration/templates/koa.ts @@ -145,15 +145,15 @@ export function RegisterRoutes(router: KoaRouter) { case 'body': return ValidateParam(args[key], context.request.body, models, name); case 'body-prop': - // When https://github.com/koa-modules/multer/pull/15 gets merged in, the conditional can be removed - return ValidateParam(args[key], context.request.body[name] || context.req.body[name], models, name); + return ValidateParam(args[key], context.request.body[name], models, name); case 'formData': if (args[key].typeName === 'file') { return ValidateParam(args[key], context.req.file, models, name); } else if (args[key].typeName === 'file[]') { return ValidateParam(args[key], context.req.files, models, name); } else { - return ValidateParam(args[key], context.body[name], models, name); + // When https://github.com/koa-modules/multer/pull/15 gets merged in, the conditional can be removed + return ValidateParam(args[key], context.request.body[name] || context.req.body[name], models, name); } } }); diff --git a/src/swagger/specGenerator.ts b/src/swagger/specGenerator.ts index 4483f93cc..ab6b043aa 100644 --- a/src/swagger/specGenerator.ts +++ b/src/swagger/specGenerator.ts @@ -207,11 +207,19 @@ export class SpecGenerator { } }); - return { + const operation: any = { operationId: this.getOperationId(controllerName, method.name), produces: ['application/json'], responses: responses }; + + const hasFormData = method.parameters.some(p => p.in === 'formData'); + + if (hasFormData) { + operation.consumes = 'multipart/form-data'; + } + + return operation; } private getOperationId(controllerName: string, methodName: string) { diff --git a/tests/fixtures/controllers/postController.ts b/tests/fixtures/controllers/postController.ts index 7868c08d9..77fcceee7 100644 --- a/tests/fixtures/controllers/postController.ts +++ b/tests/fixtures/controllers/postController.ts @@ -1,5 +1,5 @@ import { Route } from '../../../src/decorators/route'; -import { Body, BodyProp, Query, UploadedFile, UploadedFiles } from '../../../src/decorators/parameter'; +import { Body, Query, UploadedFile, UploadedFiles, FormField } from '../../../src/decorators/parameter'; import { Post, Patch } from '../../../src/decorators/methods'; import { GenericRequest, TestModel, TestClassModel } from '../testModel'; import { ModelService } from '../services/modelService'; @@ -62,7 +62,7 @@ export class PostTestController { @Post('ManyFilesAndFormFields') public async postWithFiles(@UploadedFiles('someFiles') files: File[], - @BodyProp('a') a: string, @BodyProp('c') c: string): Promise { + @FormField('a') a: string, @FormField('c') c: string): Promise { return new ModelService().getModel(); } } diff --git a/tests/fixtures/custom/routes.ts b/tests/fixtures/custom/routes.ts index cfac99fe1..562998338 100644 --- a/tests/fixtures/custom/routes.ts +++ b/tests/fixtures/custom/routes.ts @@ -444,8 +444,8 @@ export function RegisterRoutes(app: any) { function(request: any, response: any, next: any) { const args = { files: { "in": "formData", "name": "someFiles", "required": true, "typeName": "file[]" }, - a: { "in": "body-prop", "name": "a", "required": true, "typeName": "string" }, - c: { "in": "body-prop", "name": "c", "required": true, "typeName": "string" }, + a: { "in": "formData", "name": "a", "required": true, "typeName": "string" }, + c: { "in": "formData", "name": "c", "required": true, "typeName": "string" }, }; let validatedArgs: any[] = []; diff --git a/tests/fixtures/express/routes.ts b/tests/fixtures/express/routes.ts index 7e1047757..54968cbef 100644 --- a/tests/fixtures/express/routes.ts +++ b/tests/fixtures/express/routes.ts @@ -449,8 +449,8 @@ export function RegisterRoutes(app: any) { function(request: any, response: any, next: any) { const args = { files: { "in": "formData", "name": "someFiles", "required": true, "typeName": "file[]" }, - a: { "in": "body-prop", "name": "a", "required": true, "typeName": "string" }, - c: { "in": "body-prop", "name": "c", "required": true, "typeName": "string" }, + a: { "in": "formData", "name": "a", "required": true, "typeName": "string" }, + c: { "in": "formData", "name": "c", "required": true, "typeName": "string" }, }; let validatedArgs: any[] = []; diff --git a/tests/fixtures/hapi/routes.ts b/tests/fixtures/hapi/routes.ts index fe2f89c5d..ac7537056 100644 --- a/tests/fixtures/hapi/routes.ts +++ b/tests/fixtures/hapi/routes.ts @@ -586,8 +586,8 @@ export function RegisterRoutes(server: hapi.Server) { handler: (request: any, reply: hapi.IReply) => { const args: Args = { files: { "in": "formData", "name": "someFiles", "required": true, "typeName": "file[]" }, - a: { "in": "body-prop", "name": "a", "required": true, "typeName": "string" }, - c: { "in": "body-prop", "name": "c", "required": true, "typeName": "string" }, + a: { "in": "formData", "name": "a", "required": true, "typeName": "string" }, + c: { "in": "formData", "name": "c", "required": true, "typeName": "string" }, }; let validatedArgs: any[] = []; @@ -2007,7 +2007,7 @@ export function RegisterRoutes(server: hapi.Server) { contentTransferEncoding[0] && contentTransferEncoding[0].toLowerCase() || '7bit'; const mimetype = headers['content-type'] || 'text/plain'; - const destination = './upload2'; + const destination = './uploads'; const filename = crypto.pseudoRandomBytes(16).toString('hex'); const filePath = path.join(destination, filename); return mkdirp(destination, err => { diff --git a/tests/fixtures/koa/routes.ts b/tests/fixtures/koa/routes.ts index 8ceb79ad5..fdf1b32a0 100644 --- a/tests/fixtures/koa/routes.ts +++ b/tests/fixtures/koa/routes.ts @@ -488,8 +488,8 @@ export function RegisterRoutes(router: KoaRouter) { async (context, next) => { const args = { files: { "in": "formData", "name": "someFiles", "required": true, "typeName": "file[]" }, - a: { "in": "body-prop", "name": "a", "required": true, "typeName": "string" }, - c: { "in": "body-prop", "name": "c", "required": true, "typeName": "string" }, + a: { "in": "formData", "name": "a", "required": true, "typeName": "string" }, + c: { "in": "formData", "name": "c", "required": true, "typeName": "string" }, }; let validatedArgs: any[] = []; @@ -1699,15 +1699,15 @@ export function RegisterRoutes(router: KoaRouter) { case 'body': return ValidateParam(args[key], context.request.body, models, name); case 'body-prop': - // When https://github.com/koa-modules/multer/pull/15 gets merged in, the conditional can be removed - return ValidateParam(args[key], context.request.body[name] || context.req.body[name], models, name); + return ValidateParam(args[key], context.request.body[name], models, name); case 'formData': if (args[key].typeName === 'file') { return ValidateParam(args[key], context.req.file, models, name); } else if (args[key].typeName === 'file[]') { return ValidateParam(args[key], context.req.files, models, name); } else { - return ValidateParam(args[key], context.body[name], models, name); + // When https://github.com/koa-modules/multer/pull/15 gets merged in, the conditional can be removed + return ValidateParam(args[key], context.request.body[name] || context.req.body[name], models, name); } } });