diff --git a/README.md b/README.md index c14a1c82..14033a00 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,7 @@ You can use routing-controllers with [express.js][1] or [koa.js][2]. - [Throw HTTP errors](#throw-http-errors) - [Enable CORS](#enable-cors) - [Default settings](#default-settings) + - [Selectively disabling request/response transform](#selectively-disable-requestresponse-transforming) * [Using middlewares](#using-middlewares) + [Use exist middleware](#use-exist-middleware) + [Creating your own express middleware](#creating-your-own-express-middleware) @@ -694,7 +695,7 @@ There are set of prepared errors you can use: * UnauthorizedError -You can also create and use your own errors by extending `HttpError` class. +You can also create and use your own errors by extending `HttpError` class. To define the data returned to the client, you could define a toJSON method in your error. ```typescript @@ -716,7 +717,7 @@ class DbError extends HttpError { } } } -``` +``` #### Enable CORS @@ -757,7 +758,7 @@ app.listen(3000); #### Default settings -You can override default status code in routing-controllers options. +You can override default status code in routing-controllers options. ```typescript import "reflect-metadata"; @@ -770,9 +771,9 @@ const app = createExpressServer({ //with this option, null will return 404 by default nullResultCode: 404, - //with this option, void or Promise will return 204 by default + //with this option, void or Promise will return 204 by default undefinedResultCode: 204, - + paramOptions: { //with this option, argument will be required by default required: true @@ -783,6 +784,20 @@ const app = createExpressServer({ app.listen(3000); ``` +#### Selectively disable request/response transform + +To disable `class-transformer` on a per-controller or per-route basis, use the `transformRequest` and `transformResponse` options on your controller and route decorators: + +```typescript +@Controller("/users", {transformRequest: false, transformResponse: false}) +export class UserController { + + @Get("/", {transformResponse: true}) { + // route option overrides controller option + } +} +``` + ## Using middlewares You can use any exist express / koa middleware, or create your own. @@ -1171,7 +1186,7 @@ If its a class - then instance of this class will be created. This technique works not only with `@Body`, but also with `@Param`, `@QueryParam`, `@BodyParam` and other decorators. Learn more about class-transformer and how to handle more complex object constructions [here][4]. This behaviour is enabled by default. -If you want to disable it simply pass `classTransformer: false` to createExpressServer method. +If you want to disable it simply pass `classTransformer: false` to createExpressServer method. Alternatively you can disable transforming for [individual controllers or routes](#selectively-disable-requestresponse-transforming). ## Auto validating action params diff --git a/src/ActionParameterHandler.ts b/src/ActionParameterHandler.ts index c3ce73d6..71830d41 100644 --- a/src/ActionParameterHandler.ts +++ b/src/ActionParameterHandler.ts @@ -162,6 +162,7 @@ export class ActionParameterHandler { */ protected transformValue(value: any, paramMetadata: ParamMetadata): any { if (this.driver.useClassTransformer && + paramMetadata.actionMetadata.options.transformRequest !== false && paramMetadata.targetType && paramMetadata.targetType !== Object && !(value instanceof paramMetadata.targetType)) { diff --git a/src/RoutingControllersOptions.ts b/src/RoutingControllersOptions.ts index 82ba2194..f1381e6b 100644 --- a/src/RoutingControllersOptions.ts +++ b/src/RoutingControllersOptions.ts @@ -85,7 +85,7 @@ export interface RoutingControllersOptions { * Special function used to get currently authorized user. */ currentUserChecker?: CurrentUserChecker; - + /** * Default settings */ @@ -110,4 +110,4 @@ export interface RoutingControllersOptions { required?: boolean; }; }; -} \ No newline at end of file +} diff --git a/src/decorator-options/ControllerOptions.ts b/src/decorator-options/ControllerOptions.ts new file mode 100644 index 00000000..4fe86b6c --- /dev/null +++ b/src/decorator-options/ControllerOptions.ts @@ -0,0 +1,14 @@ +/** + * Extra options that apply to each controller action. + */ +export interface ControllerOptions { + /** + * If set to false, class-transformer won't be used to perform request serialization. + */ + transformRequest?: boolean; + + /** + * If set to false, class-transformer won't be used to perform response serialization. + */ + transformResponse?: boolean; +} diff --git a/src/decorator-options/HandlerOptions.ts b/src/decorator-options/HandlerOptions.ts new file mode 100644 index 00000000..869ef0a4 --- /dev/null +++ b/src/decorator-options/HandlerOptions.ts @@ -0,0 +1,14 @@ +/** + * Extra handler-specific options. + */ +export interface HandlerOptions { + /** + * If set to false, class-transformer won't be used to perform request serialization. + */ + transformRequest?: boolean; + + /** + * If set to false, class-transformer won't be used to perform response serialization. + */ + transformResponse?: boolean; +} diff --git a/src/decorator/Controller.ts b/src/decorator/Controller.ts index 59857a97..e57f2aa9 100644 --- a/src/decorator/Controller.ts +++ b/src/decorator/Controller.ts @@ -1,4 +1,5 @@ import {getMetadataArgsStorage} from "../index"; +import {ControllerOptions} from "../decorator-options/ControllerOptions"; /** * Defines a class as a controller. @@ -6,13 +7,15 @@ import {getMetadataArgsStorage} from "../index"; * Controller actions are executed when request come. * * @param baseRoute Extra path you can apply as a base route to all controller actions + * @param options Extra options that apply to all controller actions */ -export function Controller(baseRoute?: string): Function { +export function Controller(baseRoute?: string, options?: ControllerOptions): Function { return function (object: Function) { getMetadataArgsStorage().controllers.push({ type: "default", target: object, - route: baseRoute + route: baseRoute, + options }); }; -} \ No newline at end of file +} diff --git a/src/decorator/Delete.ts b/src/decorator/Delete.ts index f0f0fcd4..3df85c24 100644 --- a/src/decorator/Delete.ts +++ b/src/decorator/Delete.ts @@ -1,28 +1,30 @@ +import {HandlerOptions} from "../decorator-options/HandlerOptions"; import {getMetadataArgsStorage} from "../index"; /** * Registers a controller method to be executed when DELETE request comes on a given route. * Must be applied on a controller action. */ -export function Delete(route?: RegExp): Function; +export function Delete(route?: RegExp, options?: HandlerOptions): Function; /** * Registers a controller method to be executed when DELETE request comes on a given route. * Must be applied on a controller action. */ -export function Delete(route?: string): Function; +export function Delete(route?: string, options?: HandlerOptions): Function; /** * Registers a controller method to be executed when DELETE request comes on a given route. * Must be applied on a controller action. */ -export function Delete(route?: string|RegExp): Function { +export function Delete(route?: string|RegExp, options?: HandlerOptions): Function { return function (object: Object, methodName: string) { getMetadataArgsStorage().actions.push({ type: "delete", target: object.constructor, method: methodName, - route: route + route: route, + options }); }; } diff --git a/src/decorator/Get.ts b/src/decorator/Get.ts index 36b8ffe8..caccb01e 100644 --- a/src/decorator/Get.ts +++ b/src/decorator/Get.ts @@ -1,28 +1,30 @@ +import {HandlerOptions} from "../decorator-options/HandlerOptions"; import {getMetadataArgsStorage} from "../index"; /** * Registers an action to be executed when GET request comes on a given route. * Must be applied on a controller action. */ -export function Get(route?: RegExp): Function; +export function Get(route?: RegExp, options?: HandlerOptions): Function; /** * Registers an action to be executed when GET request comes on a given route. * Must be applied on a controller action. */ -export function Get(route?: string): Function; +export function Get(route?: string, options?: HandlerOptions): Function; /** * Registers an action to be executed when GET request comes on a given route. * Must be applied on a controller action. */ -export function Get(route?: string|RegExp): Function { +export function Get(route?: string|RegExp, options?: HandlerOptions): Function { return function (object: Object, methodName: string) { getMetadataArgsStorage().actions.push({ type: "get", target: object.constructor, method: methodName, - route: route + options, + route }); }; -} \ No newline at end of file +} diff --git a/src/decorator/Head.ts b/src/decorator/Head.ts index 4a2e96c8..eacd5d47 100644 --- a/src/decorator/Head.ts +++ b/src/decorator/Head.ts @@ -1,28 +1,30 @@ +import {HandlerOptions} from "../decorator-options/HandlerOptions"; import {getMetadataArgsStorage} from "../index"; /** * Registers an action to be executed when HEAD request comes on a given route. * Must be applied on a controller action. */ -export function Head(route?: RegExp): Function; +export function Head(route?: RegExp, options?: HandlerOptions): Function; /** * Registers an action to be executed when HEAD request comes on a given route. * Must be applied on a controller action. */ -export function Head(route?: string): Function; +export function Head(route?: string, options?: HandlerOptions): Function; /** * Registers an action to be executed when HEAD request comes on a given route. * Must be applied on a controller action. */ -export function Head(route?: string|RegExp): Function { +export function Head(route?: string|RegExp, options?: HandlerOptions): Function { return function (object: Object, methodName: string) { getMetadataArgsStorage().actions.push({ type: "head", target: object.constructor, method: methodName, - route: route + options, + route }); }; -} \ No newline at end of file +} diff --git a/src/decorator/JsonController.ts b/src/decorator/JsonController.ts index 826c1d4b..dc8d6252 100644 --- a/src/decorator/JsonController.ts +++ b/src/decorator/JsonController.ts @@ -1,17 +1,20 @@ import {getMetadataArgsStorage} from "../index"; +import {ControllerOptions} from "../decorator-options/ControllerOptions"; /** * Defines a class as a JSON controller. If JSON controller is used, then all controller actions will return * a serialized json data, and its response content-type always will be application/json. * * @param baseRoute Extra path you can apply as a base route to all controller actions + * @param options Extra options that apply to all controller actions */ -export function JsonController(baseRoute?: string) { +export function JsonController(baseRoute?: string, options?: ControllerOptions) { return function (object: Function) { getMetadataArgsStorage().controllers.push({ type: "json", target: object, - route: baseRoute + route: baseRoute, + options }); }; } diff --git a/src/decorator/Method.ts b/src/decorator/Method.ts index cb9f8bbd..dfa46ecd 100644 --- a/src/decorator/Method.ts +++ b/src/decorator/Method.ts @@ -1,3 +1,4 @@ +import {HandlerOptions} from "../decorator-options/HandlerOptions"; import {getMetadataArgsStorage} from "../index"; import {ActionType} from "../metadata/types/ActionType"; @@ -5,25 +6,26 @@ import {ActionType} from "../metadata/types/ActionType"; * Registers an action to be executed when request with specified method comes on a given route. * Must be applied on a controller action. */ -export function Method(method: ActionType, route?: RegExp): Function; +export function Method(method: ActionType, route?: RegExp, options?: HandlerOptions): Function; /** * Registers an action to be executed when request with specified method comes on a given route. * Must be applied on a controller action. */ -export function Method(method: ActionType, route?: string): Function; +export function Method(method: ActionType, route?: string, options?: HandlerOptions): Function; /** * Registers an action to be executed when request with specified method comes on a given route. * Must be applied on a controller action. */ -export function Method(method: ActionType, route?: string|RegExp): Function { +export function Method(method: ActionType, route?: string|RegExp, options?: HandlerOptions): Function { return function (object: Object, methodName: string) { getMetadataArgsStorage().actions.push({ type: method, target: object.constructor, method: methodName, - route: route + options, + route }); }; } diff --git a/src/decorator/Patch.ts b/src/decorator/Patch.ts index 2246e5ed..1cf8177f 100644 --- a/src/decorator/Patch.ts +++ b/src/decorator/Patch.ts @@ -1,28 +1,30 @@ +import {HandlerOptions} from "../decorator-options/HandlerOptions"; import {getMetadataArgsStorage} from "../index"; /** * Registers an action to be executed when PATCH request comes on a given route. * Must be applied on a controller action. */ -export function Patch(route?: RegExp): Function; +export function Patch(route?: RegExp, options?: HandlerOptions): Function; /** * Registers an action to be executed when PATCH request comes on a given route. * Must be applied on a controller action. */ -export function Patch(route?: string): Function; +export function Patch(route?: string, options?: HandlerOptions): Function; /** * Registers an action to be executed when PATCH request comes on a given route. * Must be applied on a controller action. */ -export function Patch(route?: string|RegExp): Function { +export function Patch(route?: string|RegExp, options?: HandlerOptions): Function { return function (object: Object, methodName: string) { getMetadataArgsStorage().actions.push({ type: "patch", target: object.constructor, method: methodName, - route: route + route: route, + options }); }; -} \ No newline at end of file +} diff --git a/src/decorator/Post.ts b/src/decorator/Post.ts index 87b774bb..dc2e6fda 100644 --- a/src/decorator/Post.ts +++ b/src/decorator/Post.ts @@ -1,28 +1,30 @@ +import {HandlerOptions} from "../decorator-options/HandlerOptions"; import {getMetadataArgsStorage} from "../index"; /** * Registers an action to be executed when POST request comes on a given route. * Must be applied on a controller action. */ -export function Post(route?: RegExp): Function; +export function Post(route?: RegExp, options?: HandlerOptions): Function; /** * Registers an action to be executed when POST request comes on a given route. * Must be applied on a controller action. */ -export function Post(route?: string): Function; +export function Post(route?: string, options?: HandlerOptions): Function; /** * Registers an action to be executed when POST request comes on a given route. * Must be applied on a controller action. */ -export function Post(route?: string|RegExp): Function { +export function Post(route?: string|RegExp, options?: HandlerOptions): Function { return function (object: Object, methodName: string) { getMetadataArgsStorage().actions.push({ type: "post", target: object.constructor, method: methodName, - route: route + options, + route }); }; } diff --git a/src/decorator/Put.ts b/src/decorator/Put.ts index 5bbd19d4..ff1bbf8c 100644 --- a/src/decorator/Put.ts +++ b/src/decorator/Put.ts @@ -1,28 +1,30 @@ +import {HandlerOptions} from "../decorator-options/HandlerOptions"; import {getMetadataArgsStorage} from "../index"; /** * Registers an action to be executed when PUT request comes on a given route. * Must be applied on a controller action. */ -export function Put(route?: RegExp): Function; +export function Put(route?: RegExp, options?: HandlerOptions): Function; /** * Registers an action to be executed when POST request comes on a given route. * Must be applied on a controller action. */ -export function Put(route?: string): Function; +export function Put(route?: string, options?: HandlerOptions): Function; /** * Registers an action to be executed when POST request comes on a given route. * Must be applied on a controller action. */ -export function Put(route?: string|RegExp): Function { +export function Put(route?: string|RegExp, options?: HandlerOptions): Function { return function (object: Object, methodName: string) { getMetadataArgsStorage().actions.push({ type: "put", target: object.constructor, method: methodName, - route: route + route: route, + options }); }; } diff --git a/src/driver/BaseDriver.ts b/src/driver/BaseDriver.ts index 26c298db..07ebabc2 100644 --- a/src/driver/BaseDriver.ts +++ b/src/driver/BaseDriver.ts @@ -44,7 +44,7 @@ export abstract class BaseDriver { * Global class-validator options passed during validate operation. */ validationOptions: ValidatorOptions; - + /** * Global class transformer options passed to class-transformer during plainToClass operation. * This operation is being executed when parsing user parameters. @@ -92,7 +92,7 @@ export abstract class BaseDriver { * Initializes the things driver needs before routes and middleware registration. */ abstract initialize(): void; - + /** * Registers given middleware. */ @@ -129,14 +129,15 @@ export abstract class BaseDriver { protected transformResult(result: any, action: ActionMetadata, options: Action): any { // check if we need to transform result - const shouldTransform = (this.useClassTransformer && result != null) // transform only if enabled and value exist + const shouldTransform = this.useClassTransformer // transform only if class-transformer is enabled + && action.options.transformResponse !== false // don't transform if action response transform is disabled && result instanceof Object // don't transform primitive types (string/number/boolean) && !( result instanceof Uint8Array // don't transform binary data || result.pipe instanceof Function // don't transform streams ); - + // transform result if needed if (shouldTransform) { const options = action.responseClassTransformOptions || this.classToPlainTransformOptions; @@ -152,7 +153,7 @@ export abstract class BaseDriver { if (typeof error.toJSON === "function") return error.toJSON(); - + let processedError: any = {}; if (error instanceof Error) { const name = error.name && error.name !== "Error" ? error.name : error.constructor.name; diff --git a/src/metadata-builder/MetadataBuilder.ts b/src/metadata-builder/MetadataBuilder.ts index 25414fb8..e96e4b93 100644 --- a/src/metadata-builder/MetadataBuilder.ts +++ b/src/metadata-builder/MetadataBuilder.ts @@ -73,6 +73,7 @@ export class MetadataBuilder { return controllers.map(controllerArgs => { const controller = new ControllerMetadata(controllerArgs); controller.build(this.createControllerResponseHandlers(controller)); + controller.options = controllerArgs.options; controller.actions = this.createActions(controller); controller.uses = this.createControllerUses(controller); controller.interceptors = this.createControllerInterceptorUses(controller); @@ -88,6 +89,7 @@ export class MetadataBuilder { .filterActionsWithTarget(controller.target) .map(actionArgs => { const action = new ActionMetadata(controller, actionArgs, this.options); + action.options = {...controller.options, ...actionArgs.options}; action.params = this.createParams(action); action.uses = this.createActionUses(action); action.interceptors = this.createActionInterceptorUses(action); @@ -112,7 +114,7 @@ export class MetadataBuilder { let options = this.options.defaults && this.options.defaults.paramOptions; if (!options) return paramArgs; - + if (paramArgs.required === undefined) paramArgs.required = options.required || false; diff --git a/src/metadata/ActionMetadata.ts b/src/metadata/ActionMetadata.ts index ab96861c..99afc807 100644 --- a/src/metadata/ActionMetadata.ts +++ b/src/metadata/ActionMetadata.ts @@ -6,7 +6,8 @@ import {ControllerMetadata} from "./ControllerMetadata"; import {InterceptorMetadata} from "./InterceptorMetadata"; import {ParamMetadata} from "./ParamMetadata"; import {ResponseHandlerMetadata} from "./ResponseHandleMetadata"; -import { RoutingControllersOptions } from "../RoutingControllersOptions"; +import {HandlerOptions} from "../decorator-options/HandlerOptions"; +import {RoutingControllersOptions} from "../RoutingControllersOptions"; import {UseMetadata} from "./UseMetadata"; /** @@ -48,6 +49,11 @@ export class ActionMetadata { */ method: string; + /** + * Action-specific options. + */ + options: HandlerOptions; + /** * Action type represents http method used for the registered route. Can be one of the value defined in ActionTypes * class. @@ -148,11 +154,12 @@ export class ActionMetadata { // Constructor // ------------------------------------------------------------------------- - constructor(controllerMetadata: ControllerMetadata, args: ActionMetadataArgs, private options: RoutingControllersOptions) { + constructor(controllerMetadata: ControllerMetadata, args: ActionMetadataArgs, private globalOptions: RoutingControllersOptions) { this.controllerMetadata = controllerMetadata; this.route = args.route; this.target = args.target; this.method = args.method; + this.options = args.options; this.type = args.type; this.appendParams = args.appendParams; this.methodOverride = args.methodOverride; @@ -179,15 +186,15 @@ export class ActionMetadata { if (classTransformerResponseHandler) this.responseClassTransformOptions = classTransformerResponseHandler.value; - + this.undefinedResultCode = undefinedResultHandler ? undefinedResultHandler.value - : this.options.defaults && this.options.defaults.undefinedResultCode; - + : this.globalOptions.defaults && this.globalOptions.defaults.undefinedResultCode; + this.nullResultCode = nullResultHandler ? nullResultHandler.value - : this.options.defaults && this.options.defaults.nullResultCode; - + : this.globalOptions.defaults && this.globalOptions.defaults.nullResultCode; + if (successCodeHandler) this.successHttpCode = successCodeHandler.value; if (redirectHandler) @@ -199,7 +206,7 @@ export class ActionMetadata { this.isBodyUsed = !!this.params.find(param => param.type === "body" || param.type === "body-param"); this.isFilesUsed = !!this.params.find(param => param.type === "files"); this.isFileUsed = !!this.params.find(param => param.type === "file"); - this.isJsonTyped = (contentTypeHandler !== undefined + this.isJsonTyped = (contentTypeHandler !== undefined ? /json/.test(contentTypeHandler.value) : this.controllerMetadata.type === "json" ); @@ -280,8 +287,8 @@ export class ActionMetadata { if (!baseRoute || baseRoute === "") return route; const fullPath = `^${prefix}${route.toString().substr(1)}?$`; - + return new RegExp(fullPath, route.flags); } -} \ No newline at end of file +} diff --git a/src/metadata/ControllerMetadata.ts b/src/metadata/ControllerMetadata.ts index 3ecd2b3a..a53b89b3 100644 --- a/src/metadata/ControllerMetadata.ts +++ b/src/metadata/ControllerMetadata.ts @@ -2,6 +2,7 @@ import {ActionMetadata} from "./ActionMetadata"; import {ControllerMetadataArgs} from "./args/ControllerMetadataArgs"; import {UseMetadata} from "./UseMetadata"; import {getFromContainer} from "../container"; +import {ControllerOptions} from "../decorator-options/ControllerOptions"; import {ResponseHandlerMetadata} from "./ResponseHandleMetadata"; import {InterceptorMetadata} from "./InterceptorMetadata"; @@ -34,6 +35,11 @@ export class ControllerMetadata { */ type: "default"|"json"; + /** + * Options that apply to all controller actions. + */ + options: ControllerOptions; + /** * Middleware "use"-s applied to a whole controller. */ @@ -57,11 +63,12 @@ export class ControllerMetadata { // ------------------------------------------------------------------------- // Constructor // ------------------------------------------------------------------------- - + constructor(args: ControllerMetadataArgs) { this.target = args.target; this.route = args.route; this.type = args.type; + this.options = args.options; } // ------------------------------------------------------------------------- @@ -89,4 +96,4 @@ export class ControllerMetadata { this.authorizedRoles = [].concat((authorizedHandler && authorizedHandler.value) || []); } -} \ No newline at end of file +} diff --git a/src/metadata/args/ActionMetadataArgs.ts b/src/metadata/args/ActionMetadataArgs.ts index 28e8829f..88ce1dac 100644 --- a/src/metadata/args/ActionMetadataArgs.ts +++ b/src/metadata/args/ActionMetadataArgs.ts @@ -1,6 +1,7 @@ import {ActionType} from "../types/ActionType"; import {Action} from "../../Action"; import {ActionMetadata} from "../ActionMetadata"; +import {HandlerOptions} from "../../decorator-options/HandlerOptions"; /** * Action metadata used to storage information about registered action. @@ -16,12 +17,17 @@ export interface ActionMetadataArgs { * Class on which's method this action is attached. */ target: Function; - + /** * Object's method that will be executed on this action. */ method: string; + /** + * Action-specific options. + */ + options: HandlerOptions; + /** * Action type represents http method used for the registered route. Can be one of the value defined in ActionTypes * class. @@ -37,5 +43,5 @@ export interface ActionMetadataArgs { * Special function that will be called instead of orignal method of the target. */ methodOverride?: (actionMetadata: ActionMetadata, action: Action, params: any[]) => Promise|any; - -} \ No newline at end of file + +} diff --git a/src/metadata/args/ControllerMetadataArgs.ts b/src/metadata/args/ControllerMetadataArgs.ts index 2ee42303..424f332d 100644 --- a/src/metadata/args/ControllerMetadataArgs.ts +++ b/src/metadata/args/ControllerMetadataArgs.ts @@ -1,3 +1,5 @@ +import {ControllerOptions} from "../../decorator-options/ControllerOptions"; + /** * Controller metadata used to storage information about registered controller. */ @@ -17,5 +19,10 @@ export interface ControllerMetadataArgs { * Controller type. Can be default or json-typed. Json-typed controllers operate with json requests and responses. */ type: "default"|"json"; - -} \ No newline at end of file + + /** + * Options that apply to all controller actions. + */ + options: ControllerOptions; + +} diff --git a/test/functional/action-options.spec.ts b/test/functional/action-options.spec.ts new file mode 100644 index 00000000..4a7faf2b --- /dev/null +++ b/test/functional/action-options.spec.ts @@ -0,0 +1,97 @@ +import "reflect-metadata"; +import {Exclude, Expose} from "class-transformer"; +import {defaultMetadataStorage} from "class-transformer/storage"; +import {JsonController} from "../../src/decorator/JsonController"; +import {Post} from "../../src/decorator/Post"; +import {Body} from "../../src/decorator/Body"; +import {createExpressServer, createKoaServer, getMetadataArgsStorage} from "../../src/index"; +import {assertRequest} from "./test-utils"; +const expect = require("chakram").expect; + +describe("action options", () => { + + let initializedUser: any; + let User: any; + + after(() => { + defaultMetadataStorage.clear(); + }); + + beforeEach(() => { + initializedUser = undefined; + }); + + before(() => { + + // reset metadata args storage + getMetadataArgsStorage().reset(); + + @Exclude() + class UserModel { + @Expose() + firstName: string; + + lastName: string; + } + User = UserModel; + + function handler(user: UserModel) { + initializedUser = user; + const ret = new User(); + ret.firstName = user.firstName; + ret.lastName = user.lastName || "default"; + return ret; + } + + @JsonController("", {transformResponse: false}) + class NoTransformResponseController { + @Post("/default") + default(@Body() user: UserModel) { + return handler(user); + } + + @Post("/transformRequestOnly", {transformRequest: true, transformResponse: false}) + transformRequestOnly(@Body() user: UserModel) { + return handler(user); + } + + @Post("/transformResponseOnly", {transformRequest: false, transformResponse: true}) + transformResponseOnly(@Body() user: UserModel) { + return handler(user); + } + } + }); + + let expressApp: any, koaApp: any; + before(done => expressApp = createExpressServer().listen(3001, done)); + after(done => expressApp.close(done)); + before(done => koaApp = createKoaServer().listen(3002, done)); + after(done => koaApp.close(done)); + + it("should use controller options when action transform options are not set", () => { + assertRequest([3001, 3002], "post", "default", { firstName: "Umed", lastName: "Khudoiberdiev" }, response => { + expect(initializedUser).to.be.instanceOf(User); + expect(initializedUser.lastName).to.be.undefined; + expect(response).to.have.status(200); + expect(response.body.lastName).to.equal("default"); + }); + }); + + it("should override controller options with action transformRequest option", () => { + assertRequest([3001, 3002], "post", "transformRequestOnly", { firstName: "Umed", lastName: "Khudoiberdiev" }, response => { + expect(initializedUser).to.be.instanceOf(User); + expect(initializedUser.lastName).to.be.undefined; + expect(response).to.have.status(200); + expect(response.body.lastName).to.equal("default"); + }); + }); + + it("should override controller options with action transformResponse option", () => { + assertRequest([3001, 3002], "post", "transformResponseOnly", { firstName: "Umed", lastName: "Khudoiberdiev" }, response => { + expect(initializedUser).not.to.be.instanceOf(User); + expect(initializedUser.lastName).to.exist; + expect(response).to.have.status(200); + expect(response.body.lastName).to.be.undefined; + }); + }); +}); diff --git a/test/functional/class-transformer-options.ts b/test/functional/class-transformer-options.ts index 40e01dce..3d7f45e9 100644 --- a/test/functional/class-transformer-options.ts +++ b/test/functional/class-transformer-options.ts @@ -17,16 +17,20 @@ describe("class transformer options", () => { keyword: string; } - class UserModel { - id: number; - _firstName: string; - _lastName: string; - - @Expose() - get name(): string { - return this._firstName + " " + this._lastName; + let UserModel: any; + before(() => { + class User { + id: number; + _firstName: string; + _lastName: string; + + @Expose() + get name(): string { + return this._firstName + " " + this._lastName; + } } - } + UserModel = User; + }); after(() => { defaultMetadataStorage.clear(); @@ -180,4 +184,4 @@ describe("class transformer options", () => { }); }); -}); \ No newline at end of file +}); diff --git a/test/functional/controller-options.spec.ts b/test/functional/controller-options.spec.ts new file mode 100644 index 00000000..4f205306 --- /dev/null +++ b/test/functional/controller-options.spec.ts @@ -0,0 +1,99 @@ +import "reflect-metadata"; +import {Exclude, Expose} from "class-transformer"; +import {defaultMetadataStorage} from "class-transformer/storage"; +import {JsonController} from "../../src/decorator/JsonController"; +import {Post} from "../../src/decorator/Post"; +import {Body} from "../../src/decorator/Body"; +import {createExpressServer, createKoaServer, getMetadataArgsStorage} from "../../src/index"; +import {assertRequest} from "./test-utils"; +const expect = require("chakram").expect; + +describe("controller options", () => { + + let initializedUser: any; + let User: any; + + after(() => { + defaultMetadataStorage.clear(); + }); + + beforeEach(() => { + initializedUser = undefined; + }); + + before(() => { + + // reset metadata args storage + getMetadataArgsStorage().reset(); + + @Exclude() + class UserModel { + @Expose() + firstName: string; + + lastName: string; + } + User = UserModel; + + function handler(user: UserModel) { + initializedUser = user; + const ret = new User(); + ret.firstName = user.firstName; + ret.lastName = user.lastName; + return ret; + } + + @JsonController("/default") + class DefaultController { + @Post("/") + postUsers(@Body() user: UserModel) { return handler(user); } + } + + @JsonController("/transform", {transformRequest: true, transformResponse: true}) + class TransformController { + @Post("/") + postUsers(@Body() user: UserModel) { return handler(user); } + } + + @JsonController("/noTransform", {transformRequest: false, transformResponse: false}) + class NoTransformController { + @Post("/") + postUsers(@Body() user: UserModel) { return handler(user); } + } + + }); + + let expressApp: any, koaApp: any; + before(done => expressApp = createExpressServer().listen(3001, done)); + after(done => expressApp.close(done)); + before(done => koaApp = createKoaServer().listen(3002, done)); + after(done => koaApp.close(done)); + + describe("controller transform is enabled by default", () => { + assertRequest([3001, 3002], "post", "default", { firstName: "Umed", lastName: "Khudoiberdiev" }, response => { + expect(initializedUser).to.be.instanceOf(User); + expect(initializedUser.lastName).to.be.undefined; + expect(response).to.have.status(200); + expect(response.body.lastName).to.be.undefined; + }); + }); + + describe("when controller transform is enabled", () => { + assertRequest([3001, 3002], "post", "transform", { firstName: "Umed", lastName: "Khudoiberdiev" }, response => { + expect(initializedUser).to.be.instanceOf(User); + expect(initializedUser.lastName).to.be.undefined; + expect(response).to.have.status(200); + expect(response.body.lastName).to.be.undefined; + }); + }); + + describe("when controller transform is disabled", () => { + assertRequest([3001, 3002], "post", "noTransform", { firstName: "Umed", lastName: "Khudoiberdiev" }, response => { + expect(initializedUser).not.to.be.instanceOf(User); + expect(initializedUser.lastName).to.exist; + expect(response).to.have.status(200); + expect(response.body.lastName).to.exist; + }); + }); + +}); diff --git a/test/functional/global-options.spec.ts b/test/functional/global-options.spec.ts index 381348c8..adf5bae8 100644 --- a/test/functional/global-options.spec.ts +++ b/test/functional/global-options.spec.ts @@ -1,23 +1,21 @@ import "reflect-metadata"; +import {Exclude, Expose} from "class-transformer"; +import {defaultMetadataStorage} from "class-transformer/storage"; import {JsonController} from "../../src/decorator/JsonController"; import {Post} from "../../src/decorator/Post"; import {Body} from "../../src/decorator/Body"; import {createExpressServer, createKoaServer, getMetadataArgsStorage} from "../../src/index"; import {assertRequest} from "./test-utils"; -const chakram = require("chakram"); -const expect = chakram.expect; - -export class User { - firstName: string; - lastName: string; - getName(): string { - return this.firstName + " " + this.lastName; - } -} +const expect = require("chakram").expect; describe("routing-controllers global options", () => { - let initializedUser: User; + let initializedUser: any; + let User: any; + + after(() => { + defaultMetadataStorage.clear(); + }); beforeEach(() => { initializedUser = undefined; @@ -28,21 +26,33 @@ describe("routing-controllers global options", () => { // reset metadata args storage getMetadataArgsStorage().reset(); + @Exclude() + class UserModel { + @Expose() + firstName: string; + + lastName: string; + } + User = UserModel; + @JsonController() class TestUserController { @Post("/users") - postUsers(@Body() user: User) { + postUsers(@Body() user: UserModel) { initializedUser = user; - return ""; + const ret = new User(); + ret.firstName = user.firstName; + ret.lastName = user.lastName; + return ret; } - + @Post(new RegExp("/(prefix|regex)/users")) - postUsersWithRegex(@Body() user: User) { + postUsersWithRegex(@Body() user: UserModel) { initializedUser = user; return ""; } - + } }); @@ -56,7 +66,9 @@ describe("routing-controllers global options", () => { assertRequest([3001, 3002], "post", "users", { firstName: "Umed", lastName: "Khudoiberdiev" }, response => { expect(initializedUser).to.be.instanceOf(User); + expect(initializedUser.lastName).to.be.undefined; expect(response).to.have.status(200); + expect(response.body.lastName).to.be.undefined; }); }); @@ -70,33 +82,37 @@ describe("routing-controllers global options", () => { assertRequest([3001, 3002], "post", "users", { firstName: "Umed", lastName: "Khudoiberdiev" }, response => { expect(initializedUser).to.be.instanceOf(User); + expect(initializedUser.lastName).to.be.undefined; expect(response).to.have.status(200); + expect(response.body.lastName).to.be.undefined; }); }); - describe("when useClassTransformer is not set", () => { + describe("when useClassTransformer is set to false", () => { let expressApp: any, koaApp: any; before(done => expressApp = createExpressServer({ classTransformer: false }).listen(3001, done)); after(done => expressApp.close(done)); before(done => koaApp = createKoaServer({ classTransformer: false }).listen(3002, done)); after(done => koaApp.close(done)); - + assertRequest([3001, 3002], "post", "users", { firstName: "Umed", lastName: "Khudoiberdiev" }, response => { expect(initializedUser).not.to.be.instanceOf(User); + expect(initializedUser.lastName).to.exist; expect(response).to.have.status(200); + expect(response.body.lastName).to.exist; }); }); describe("when routePrefix is used all controller routes should be appended by it", () => { - + let apps: any[] = []; before(done => apps.push(createExpressServer({ routePrefix: "/api" }).listen(3001, done))); before(done => apps.push(createExpressServer({ routePrefix: "api" }).listen(3002, done))); before(done => apps.push(createKoaServer({ routePrefix: "/api" }).listen(3003, done))); before(done => apps.push(createKoaServer({ routePrefix: "api" }).listen(3004, done))); after(done => { apps.forEach(app => app.close()); done(); }); - + assertRequest([3001, 3002, 3003, 3004], "post", "api/users", { firstName: "Umed", lastName: "Khudoiberdiev" }, response => { expect(initializedUser).to.be.instanceOf(User); expect(response).to.have.status(200); @@ -108,4 +124,4 @@ describe("routing-controllers global options", () => { }); }); -}); \ No newline at end of file +});