diff --git a/CHANGELOG.md b/CHANGELOG.md index 471a34d1..568f2587 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog and release notes +### 0.8.0 + +- rename `@Param` and `@Params` decorators to `@PathParam` and `@PathParams` (#289) +- restore/introduce `@QueryParams()` and `@PathParams()` missing decorators options (they are needed for validation purposes) (#289) +- normalize param object properties (for "queries", "headers", "params" and "cookies") - now you can easily validate query/path params using `class-validator` (#289) +- enhance params normalization - converting from string to primitive types is now more strict and can throw ParamNormalizationError, +e.g. when number is expected but the invalid string (NaN) has been received (#289) + ### 0.7.2 - FIXED: Using `@Authorization` decorator with Koa caused 404 responses (ref [#240](https://github.com/pleerock/routing-controllers/pull/240)) diff --git a/README.md b/README.md index 8a979b6b..884bae31 100644 --- a/README.md +++ b/README.md @@ -128,7 +128,7 @@ You can use routing-controllers with [express.js][1] or [koa.js][2]. } @Get("/users/:id") - getOne(@Param("id") id: number) { + getOne(@PathParam("id") id: number) { return "This action returns user #" + id; } @@ -138,12 +138,12 @@ You can use routing-controllers with [express.js][1] or [koa.js][2]. } @Put("/users/:id") - put(@Param("id") id: number, @Body() user: any) { + put(@PathParam("id") id: number, @Body() user: any) { return "Updating a user..."; } @Delete("/users/:id") - remove(@Param("id") id: number) { + remove(@PathParam("id") id: number) { return "Removing user..."; } @@ -195,7 +195,7 @@ export class UserController { } @Get("/users/:id") - getOne(@Param("id") id: number) { + getOne(@PathParam("id") id: number) { return userRepository.findById(id); } @@ -222,7 +222,7 @@ export class UserController { } @Get("/users/:id") - getOne(@Param("id") id: number) { + getOne(@PathParam("id") id: number) { return userRepository.findById(id); } @@ -232,12 +232,12 @@ export class UserController { } @Put("/users/:id") - put(@Param("id") id: number, @Body() user: User) { + put(@PathParam("id") id: number, @Body() user: User) { return userRepository.updateById(id, user); } @Delete("/users/:id") - remove(@Param("id") id: number) { + remove(@PathParam("id") id: number) { return userRepository.removeById(id); } @@ -346,15 +346,15 @@ export class UserController { #### Inject routing parameters -You can use `@Param` decorator to inject parameters in your controller actions: +You can use `@PathParam` decorator to inject parameters in your controller actions: ```typescript @Get("/users/:id") -getOne(@Param("id") id: number) { // id will be automatically casted to "number" because it has type number +getOne(@PathParam("id") id: number) { // id will be automatically casted to "number" because it has type number } ``` -If you want to inject all parameters use `@Params()` decorator. +If you want to inject all parameters use `@PathParams()` decorator. #### Inject query parameters @@ -367,6 +367,37 @@ getUsers(@QueryParam("limit") limit: number) { ``` If you want to inject all query parameters use `@QueryParams()` decorator. +The bigest benefit of this approach is that you can perform validation of the params. + +```typescript +enum Roles { + Admin = "admin", + User = "user", + Guest = "guest", +} + +class GetUsersQuery { + + @IsPositive() + limit: number; + + @IsAlpha() + city: string; + + @IsEnum(Roles) + role: Roles; + + @IsBoolean() + isActive: boolean; + +} + +@Get("/users") +getUsers(@QueryParams() query: GetUsersQuery) { + // here you can access query.role, query.limit + // and others valid query parameters +} +``` #### Inject request body @@ -584,7 +615,7 @@ To prevent this if you need to specify what status code you want to return using ```typescript @Delete("/users/:id") @OnUndefined(204) -async remove(@Param("id") id: number): Promise { +async remove(@PathParam("id") id: number): Promise { return userRepository.removeById(id); } ``` @@ -596,7 +627,7 @@ This action will return 404 in the case if user was not found, and regular 200 i ```typescript @Get("/users/:id") @OnUndefined(404) -getOne(@Param("id") id: number) { +getOne(@PathParam("id") id: number) { return userRepository.findOneById(id); } ``` @@ -616,7 +647,7 @@ export class UserNotFoundError extends HttpError { ```typescript @Get("/users/:id") @OnUndefined(UserNotFoundError) -saveUser(@Param("id") id: number) { +saveUser(@PathParam("id") id: number) { return userRepository.findOneById(id); } ``` @@ -630,7 +661,7 @@ You can set any custom header in a response: ```typescript @Get("/users/:id") @Header("Cache-Control", "none") -getOne(@Param("id") id: number) { +getOne(@PathParam("id") id: number) { // ... } ``` @@ -660,7 +691,7 @@ If you want to return errors with specific error codes, there is an easy way: ```typescript @Get("/users/:id") -getOne(@Param("id") id: number) { +getOne(@PathParam("id") id: number) { const user = this.userRepository.findOneById(id); if (!user) @@ -779,7 +810,7 @@ For example, lets try to use [compression](https://github.com/expressjs/compress @Get("/users/:id") @UseBefore(compression()) - getOne(@Param("id") id: number) { + getOne(@PathParam("id") id: number) { // ... } ``` @@ -871,7 +902,7 @@ Here is example of creating middleware for express.js: @Get("/users/:id") @UseBefore(MyMiddleware) @UseAfter(loggingMiddleware) - getOne(@Param("id") id: number) { + getOne(@PathParam("id") id: number) { // ... } ``` @@ -938,7 +969,7 @@ Here is example of creating middleware for koa.js: @Get("/users/:id") @UseBefore(MyMiddleware) @UseAfter(loggingMiddleware) - getOne(@Param("id") id: number) { + getOne(@PathParam("id") id: number) { // ... } ``` @@ -1044,7 +1075,7 @@ import {Get, Param, UseInterceptor} from "routing-controllers"; // in it and return a replaced result. replaced result will be returned to the user return content.replace(/Mike/gi, "Michael"); }) -getOne(@Param("id") id: number) { +getOne(@PathParam("id") id: number) { return "Hello, I am Mike!"; // client will get a "Hello, I am Michael!" response. } ``` @@ -1078,7 +1109,7 @@ import {NameCorrectionInterceptor} from "./NameCorrectionInterceptor"; @Get("/users") @UseInterceptor(NameCorrectionInterceptor) -getOne(@Param("id") id: number) { +getOne(@PathParam("id") id: number) { return "Hello, I am Mike!"; // client will get a "Hello, I am Michael!" response. } ``` @@ -1142,7 +1173,7 @@ export class UserController { If `User` is an interface - then simple literal object will be created. 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. +This technique works not only with `@Body`, but also with `@PathParam`, `@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. @@ -1203,7 +1234,7 @@ an error will be thrown and captured by routing-controller, so the client will r If you need special options for validation (groups, skipping missing properties, etc.) or transforming (groups, excluding prefixes, versions, etc.), you can pass them as global config as `validation ` in createExpressServer method or as a local `validate` setting for method parameter - `@Body({ validate: localOptions })`. -This technique works not only with `@Body` but also with `@Param`, `@QueryParam`, `@BodyParam` and other decorators. +This technique works not only with `@Body` but also with `@PathParam`, `@QueryParam`, `@BodyParam` and other decorators. ## Using authorization features @@ -1397,8 +1428,8 @@ export class QuestionController { | `@Req()` | `getAll(@Req() request: Request)` | Injects a Request object. | `function (request, response)` | | `@Res()` | `getAll(@Res() response: Response)` | Injects a Response object. | `function (request, response)` | | `@Ctx()` | `getAll(@Ctx() context: Context)` | Injects a Context object (koa-specific) | `function (ctx)` (koa-analogue) | -| `@Param(name: string, options?: ParamOptions)` | `get(@Param("id") id: number)` | Injects a router parameter. | `request.params.id` | -| `@Params()` | `get(@Params() params: any)` | Injects all request parameters. | `request.params` | +| `@PathParam(name: string, options?: ParamOptions)` | `get(@PathParam("id") id: number)` | Injects a router parameter. | `request.params.id` | +| `@PathParams()` | `get(@PathParams() params: any)` | Injects all request parameters. | `request.params` | | `@QueryParam(name: string, options?: ParamOptions)` | `get(@QueryParam("id") id: number)` | Injects a query string parameter. | `request.query.id` | | `@QueryParams()` | `get(@QueryParams() params: any)` | Injects all query parameters. | `request.query` | | `@HeaderParam(name: string, options?: ParamOptions)` | `get(@HeaderParam("token") token: string)` | Injects a specific request headers. | `request.headers.token` | diff --git a/sample/sample11-complete-sample-express/modules/question/controllers/QuestionController.ts b/sample/sample11-complete-sample-express/modules/question/controllers/QuestionController.ts index e54e5efa..c616898d 100644 --- a/sample/sample11-complete-sample-express/modules/question/controllers/QuestionController.ts +++ b/sample/sample11-complete-sample-express/modules/question/controllers/QuestionController.ts @@ -1,7 +1,7 @@ import {Request} from "express"; import {JsonController} from "../../../../../src/decorator/JsonController"; import {Get} from "../../../../../src/decorator/Get"; -import {Param} from "../../../../../src/decorator/Param"; +import {PathParam} from "../../../../../src/decorator/PathParam"; import {Post} from "../../../../../src/decorator/Post"; import {Req} from "../../../../../src/decorator/Req"; import {Put} from "../../../../../src/decorator/Put"; @@ -20,7 +20,7 @@ export class QuestionController { } @Get("/questions/:id") - getOne(@Param("id") id: number) { + getOne(@PathParam("id") id: number) { if (!id) return Promise.reject(new Error("No id is specified")); diff --git a/sample/sample12-session-support/UserController.ts b/sample/sample12-session-support/UserController.ts index f3720469..66e7df4c 100644 --- a/sample/sample12-session-support/UserController.ts +++ b/sample/sample12-session-support/UserController.ts @@ -7,7 +7,7 @@ import {Post} from "../../src/decorator/Post"; import {Put} from "../../src/decorator/Put"; import {Patch} from "../../src/decorator/Patch"; import {Delete} from "../../src/decorator/Delete"; -import {Param} from "../../src/decorator/Param"; +import {PathParam} from "../../src/decorator/PathParam"; import {Session} from "../../src/decorator/Session"; import {ContentType} from "../../src/decorator/ContentType"; @@ -36,7 +36,7 @@ export class UserController { } @Put("/users/:id") - put(@Param("id") id: number, @Session() session: Express.Session) { + put(@PathParam("id") id: number, @Session() session: Express.Session) { (session as any).user = { name: "test", number: id }; return "User has been putted!"; } diff --git a/sample/sample4-extra-parameters/BlogController.ts b/sample/sample4-extra-parameters/BlogController.ts index 213bda64..2fd4d35f 100644 --- a/sample/sample4-extra-parameters/BlogController.ts +++ b/sample/sample4-extra-parameters/BlogController.ts @@ -5,7 +5,7 @@ import {Put} from "../../src/decorator/Put"; import {Patch} from "../../src/decorator/Patch"; import {Delete} from "../../src/decorator/Delete"; import {QueryParam} from "../../src/decorator/QueryParam"; -import {Param} from "../../src/decorator/Param"; +import {PathParam} from "../../src/decorator/PathParam"; import {Body} from "../../src/decorator/Body"; export interface BlogFilter { @@ -26,7 +26,7 @@ export class BlogController { } @Get("/blogs/:id") - getOne(@Param("id") id: number, @QueryParam("name") name: string) { + getOne(@PathParam("id") id: number, @QueryParam("name") name: string) { return { id: id, name: name }; } @@ -36,17 +36,17 @@ export class BlogController { } @Put("/blogs/:id") - put(@Param("id") id: number) { + put(@PathParam("id") id: number) { return "Blog #" + id + " has been putted!"; } @Patch("/blogs/:id") - patch(@Param("id") id: number) { + patch(@PathParam("id") id: number) { return "Blog #" + id + " has been patched!"; } @Delete("/blogs/:id") - remove(@Param("id") id: number) { + remove(@PathParam("id") id: number) { return "Blog #" + id + " has been removed!"; } diff --git a/sample/sample6-global-middlewares/BlogController.ts b/sample/sample6-global-middlewares/BlogController.ts index 63f383e8..cd336528 100644 --- a/sample/sample6-global-middlewares/BlogController.ts +++ b/sample/sample6-global-middlewares/BlogController.ts @@ -1,7 +1,7 @@ import {ForbiddenError} from "../../src/http-error/ForbiddenError"; import {Controller} from "../../src/decorator/Controller"; import {Get} from "../../src/decorator/Get"; -import {Param} from "../../src/decorator/Param"; +import {PathParam} from "../../src/decorator/PathParam"; import {ContentType} from "../../src/decorator/ContentType"; @Controller() @@ -19,7 +19,7 @@ export class BlogController { @Get("/blogs/:id") @ContentType("application/json") - getOne(@Param("id") id: number) { + getOne(@PathParam("id") id: number) { if (!id) throw new ForbiddenError(); diff --git a/sample/sample9-use-and-middlewares/BlogController.ts b/sample/sample9-use-and-middlewares/BlogController.ts index bbb19666..42d33f1b 100644 --- a/sample/sample9-use-and-middlewares/BlogController.ts +++ b/sample/sample9-use-and-middlewares/BlogController.ts @@ -1,6 +1,6 @@ import {JsonController} from "../../src/decorator/JsonController"; import {Get} from "../../src/decorator/Get"; -import {Param} from "../../src/decorator/Param"; +import {PathParam} from "../../src/decorator/PathParam"; import {CompressionMiddleware} from "./CompressionMiddleware"; import {AllControllerActionsMiddleware} from "./AllControllerActionsMiddleware"; import {UseBefore} from "../../src/decorator/UseBefore"; @@ -24,7 +24,7 @@ export class BlogController { } @Get("/blogs/:id") - getOne(@Param("id") id: number) { + getOne(@PathParam("id") id: number) { return { id: id, firstName: "First", secondName: "blog" }; } diff --git a/src/ActionParameterHandler.ts b/src/ActionParameterHandler.ts index 1c5d364e..d6dde47c 100644 --- a/src/ActionParameterHandler.ts +++ b/src/ActionParameterHandler.ts @@ -9,6 +9,7 @@ import {ParamRequiredError} from "./error/ParamRequiredError"; import {AuthorizationRequiredError} from "./error/AuthorizationRequiredError"; import {CurrentUserCheckerNotDefinedError} from "./error/CurrentUserCheckerNotDefinedError"; import {isPromiseLike} from "./util/isPromiseLike"; +import { ParamNormalizationError } from "./error/ParamNormalizationError"; /** * Handles action parameter. @@ -42,6 +43,7 @@ export class ActionParameterHandler { // get parameter value from request and normalize it const value = this.normalizeParamValue(this.driver.getParamFromRequest(action, param), param); + if (isPromiseLike(value)) return value.then(value => this.handleValue(value, action, param)); @@ -72,7 +74,7 @@ export class ActionParameterHandler { // check cases when parameter is required but its empty and throw errors in this case if (param.required) { const isValueEmpty = value === null || value === undefined || value === ""; - const isValueEmptyObject = value instanceof Object && Object.keys(value).length === 0; + const isValueEmptyObject = typeof value === "object" && Object.keys(value).length === 0; if (param.type === "body" && !param.name && (isValueEmpty || isValueEmptyObject)) { // body has a special check and error message return Promise.reject(new ParamRequiredError(action, param)); @@ -103,43 +105,83 @@ export class ActionParameterHandler { /** * Normalizes parameter value. */ - protected normalizeParamValue(value: any, param: ParamMetadata): Promise|any { + protected async normalizeParamValue(value: any, param: ParamMetadata): Promise { if (value === null || value === undefined) return value; - switch (param.targetName) { + // if param value is an object and param type match, normalize its string properties + if (typeof value === "object" && ["queries", "headers", "path-params", "cookies"].some(paramType => paramType === param.type)) { + Object.keys(value).map(key => { + const keyValue = (value as any)[key]; + if (typeof keyValue === "string") { + const ParamType = Reflect.getMetadata("design:type", param.targetType.prototype, key); + if (ParamType) { + const typeString = ParamType.name.toLowerCase(); // reflected type is always constructor-like (?) + (value as any)[key] = this.normalizeStringValue(keyValue, param.name, typeString); + } + } + }); + } + // if value is a string, normalize it to demanded type + else if (typeof value === "string") { + switch (param.targetName) { + case "number": + case "string": + case "boolean": + case "date": + return this.normalizeStringValue(value, param.name, param.targetName); + } + } + + // if target type is not primitive, transform and validate it + if ((["number", "string", "boolean"].indexOf(param.targetName) === -1) + && (param.parse || param.isTargetObject) + ) { + value = this.parseValue(value, param); + value = this.transformValue(value, param); + value = this.validateValue(value, param); // note this one can return promise + } + + return value; + } + + /** + * Normalizes string value to number or boolean. + */ + protected normalizeStringValue(value: string, parameterName: string, parameterType: string) { + switch (parameterType) { case "number": - if (value === "") return undefined; - return +value; + if (value === "") { + throw new ParamNormalizationError(value, parameterName, parameterType); + } - case "string": - return value; + const valueNumber = +value; + if (valueNumber === NaN) { + throw new ParamNormalizationError(value, parameterName, parameterType); + } + + return valueNumber; case "boolean": - if (value === "true" || value === "1") { + if (value === "true" || value === "1" || value === "") { return true; - } else if (value === "false" || value === "0") { return false; + } else { + throw new ParamNormalizationError(value, parameterName, parameterType); } - - return !!value; - + case "date": const parsedDate = new Date(value); - if (isNaN(parsedDate.getTime())) { - return Promise.reject(new BadRequestError(`${param.name} is invalid! It can't be parsed to date.`)); + if (Number.isNaN(parsedDate.getTime())) { + throw new ParamNormalizationError(value, parameterName, parameterType); } return parsedDate; - + + case "string": default: - if (value && (param.parse || param.isTargetObject)) { - value = this.parseValue(value, param); - value = this.transformValue(value, param); - value = this.validateValue(value, param); // note this one can return promise - } + return value; } - return value; } /** diff --git a/src/decorator/Params.ts b/src/decorator/Params.ts deleted file mode 100644 index 63aa6866..00000000 --- a/src/decorator/Params.ts +++ /dev/null @@ -1,19 +0,0 @@ -import {getMetadataArgsStorage} from "../index"; - -/** - * Injects all request's route parameters to the controller action parameter. - * Must be applied on a controller action parameter. - */ -export function Params(): Function { - return function (object: Object, methodName: string, index: number) { - getMetadataArgsStorage().params.push({ - type: "params", - object: object, - method: methodName, - index: index, - parse: false, // it does not make sense for Param to be parsed - required: false, - classTransform: undefined - }); - }; -} \ No newline at end of file diff --git a/src/decorator/Param.ts b/src/decorator/PathParam.ts similarity index 88% rename from src/decorator/Param.ts rename to src/decorator/PathParam.ts index cc176888..7ef9d7f2 100644 --- a/src/decorator/Param.ts +++ b/src/decorator/PathParam.ts @@ -4,10 +4,10 @@ import {getMetadataArgsStorage} from "../index"; * Injects a request's route parameter value to the controller action parameter. * Must be applied on a controller action parameter. */ -export function Param(name: string): Function { +export function PathParam(name: string): Function { return function (object: Object, methodName: string, index: number) { getMetadataArgsStorage().params.push({ - type: "param", + type: "path-param", object: object, method: methodName, index: index, diff --git a/src/decorator/PathParams.ts b/src/decorator/PathParams.ts new file mode 100644 index 00000000..5a512aff --- /dev/null +++ b/src/decorator/PathParams.ts @@ -0,0 +1,22 @@ +import {ParamOptions} from "../decorator-options/ParamOptions"; +import {getMetadataArgsStorage} from "../index"; + +/** + * Injects all request's route parameters to the controller action parameter. + * Must be applied on a controller action parameter. + */ +export function PathParams(options?: ParamOptions): Function { + return function (object: Object, methodName: string, index: number) { + getMetadataArgsStorage().params.push({ + type: "path-params", + object: object, + method: methodName, + index: index, + parse: options ? options.parse : false, + required: options ? options.required : undefined, + classTransform: options ? options.transform : undefined, + explicitType: options ? options.type : undefined, + validate: options ? options.validate : undefined, + }); + }; +} diff --git a/src/decorator/QueryParams.ts b/src/decorator/QueryParams.ts index f24e1ea5..bfc481ad 100644 --- a/src/decorator/QueryParams.ts +++ b/src/decorator/QueryParams.ts @@ -1,18 +1,22 @@ +import {ParamOptions} from "../decorator-options/ParamOptions"; import {getMetadataArgsStorage} from "../index"; /** * Injects all request's query parameters to the controller action parameter. * Must be applied on a controller action parameter. */ -export function QueryParams(): Function { +export function QueryParams(options?: ParamOptions): Function { return function (object: Object, methodName: string, index: number) { getMetadataArgsStorage().params.push({ type: "queries", object: object, method: methodName, index: index, - parse: false, - required: false + parse: options ? options.parse : false, + required: options ? options.required : undefined, + classTransform: options ? options.transform : undefined, + explicitType: options ? options.type : undefined, + validate: options ? options.validate : undefined, }); }; } \ No newline at end of file diff --git a/src/driver/express/ExpressDriver.ts b/src/driver/express/ExpressDriver.ts index 162e486a..9e74cfad 100644 --- a/src/driver/express/ExpressDriver.ts +++ b/src/driver/express/ExpressDriver.ts @@ -181,10 +181,10 @@ export class ExpressDriver extends BaseDriver { case "body-param": return request.body[param.name]; - case "param": + case "path-param": return request.params[param.name]; - case "params": + case "path-params": return request.params; case "session": diff --git a/src/driver/koa/KoaDriver.ts b/src/driver/koa/KoaDriver.ts index 1cbbc696..14bc7a59 100644 --- a/src/driver/koa/KoaDriver.ts +++ b/src/driver/koa/KoaDriver.ts @@ -163,10 +163,10 @@ export class KoaDriver extends BaseDriver { case "body-param": return request.body[param.name]; - case "param": + case "path-param": return context.params[param.name]; - case "params": + case "path-params": return context.params; case "session": diff --git a/src/error/ParamNormalizationError.ts b/src/error/ParamNormalizationError.ts new file mode 100644 index 00000000..b7f81fe4 --- /dev/null +++ b/src/error/ParamNormalizationError.ts @@ -0,0 +1,15 @@ +import {BadRequestError} from "../http-error/BadRequestError"; + +/** + * Caused when user query parameter is invalid (cannot be parsed into selected type). + */ +export class ParamNormalizationError extends BadRequestError { + name = "ParamNormalizationError"; + + constructor(value: any, parameterName: string, parameterType: string) { + super(`Given parameter ${parameterName} is invalid. Value (${JSON.stringify(value)}) cannot be parsed into ${parameterType}.`); + + Object.setPrototypeOf(this, ParamNormalizationError.prototype); + } + +} diff --git a/src/error/ParamRequiredError.ts b/src/error/ParamRequiredError.ts index bb134097..f37b5089 100644 --- a/src/error/ParamRequiredError.ts +++ b/src/error/ParamRequiredError.ts @@ -15,8 +15,8 @@ export class ParamRequiredError extends BadRequestError { let paramName: string; switch (param.type) { - case "param": - paramName = `Parameter "${param.name}" is`; + case "path-param": + paramName = `Path parameter "${param.name}" is`; break; case "body": diff --git a/src/index.ts b/src/index.ts index e143ab93..0eccd852 100644 --- a/src/index.ts +++ b/src/index.ts @@ -37,8 +37,8 @@ export * from "./decorator/Method"; export * from "./decorator/Middleware"; export * from "./decorator/OnNull"; export * from "./decorator/OnUndefined"; -export * from "./decorator/Param"; -export * from "./decorator/Params"; +export * from "./decorator/PathParam"; +export * from "./decorator/PathParams"; export * from "./decorator/Patch"; export * from "./decorator/Post"; export * from "./decorator/Put"; diff --git a/src/metadata/types/ParamType.ts b/src/metadata/types/ParamType.ts index 6b90f35f..53ec38c0 100644 --- a/src/metadata/types/ParamType.ts +++ b/src/metadata/types/ParamType.ts @@ -9,8 +9,8 @@ export type ParamType = "body" |"headers" |"file" |"files" - |"param" - |"params" + |"path-param" + |"path-params" |"session" |"state" |"cookie" diff --git a/test/functional/action-params.spec.ts b/test/functional/action-params.spec.ts index e7a79fea..b5f91593 100644 --- a/test/functional/action-params.spec.ts +++ b/test/functional/action-params.spec.ts @@ -1,19 +1,20 @@ import "reflect-metadata"; -import {createExpressServer, createKoaServer, getMetadataArgsStorage} from "../../src/index"; - +import {IsString, IsBoolean, Min, MaxLength} from "class-validator"; +import {getMetadataArgsStorage, createExpressServer, createKoaServer} from "../../src/index"; import {assertRequest} from "./test-utils"; import {User} from "../fakes/global-options/User"; import {Controller} from "../../src/decorator/Controller"; import {Get} from "../../src/decorator/Get"; import {Req} from "../../src/decorator/Req"; import {Res} from "../../src/decorator/Res"; -import {Param} from "../../src/decorator/Param"; +import {PathParam} from "../../src/decorator/PathParam"; import {Post} from "../../src/decorator/Post"; import {UseBefore} from "../../src/decorator/UseBefore"; import {Session} from "../../src/decorator/Session"; import {State} from "../../src/decorator/State"; import {QueryParam} from "../../src/decorator/QueryParam"; +import {QueryParams} from "../../src/decorator/QueryParams"; import {HeaderParam} from "../../src/decorator/HeaderParam"; import {CookieParam} from "../../src/decorator/CookieParam"; import {Body} from "../../src/decorator/Body"; @@ -31,6 +32,7 @@ describe("action parameters", () => { let paramUserId: number, paramFirstId: number, paramSecondId: number; let sessionTestElement: string; let queryParamSortBy: string, queryParamCount: string, queryParamLimit: number, queryParamShowAll: boolean, queryParamFilter: any; + let queryParams1: {[key: string]: any}, queryParams2: {[key: string]: any}, queryParams3: {[key: string]: any}; let headerParamToken: string, headerParamCount: number, headerParamLimit: number, headerParamShowAll: boolean, headerParamFilter: any; let cookieParamToken: string, cookieParamCount: number, cookieParamLimit: number, cookieParamShowAll: boolean, cookieParamFilter: any; let body: string; @@ -50,6 +52,9 @@ describe("action parameters", () => { queryParamLimit = undefined; queryParamShowAll = undefined; queryParamFilter = undefined; + queryParams1 = undefined; + queryParams2 = undefined; + queryParams3 = undefined; headerParamToken = undefined; headerParamCount = undefined; headerParamShowAll = undefined; @@ -78,6 +83,20 @@ describe("action parameters", () => { const {SetStateMiddleware} = require("../fakes/global-options/koa-middlewares/SetStateMiddleware"); const {SessionMiddleware} = require("../fakes/global-options/SessionMiddleware"); + class QueryClass { + @MaxLength(5) + sortBy?: string; + + @IsString() + count?: string; + + @Min(5) + limit?: number; + + @IsBoolean() + showAll: boolean = true; + } + @Controller() class UserActionParamsController { @@ -89,14 +108,14 @@ describe("action parameters", () => { } @Get("/users/:userId") - getUser(@Param("userId") userId: number) { + getUser(@PathParam("userId") userId: number) { paramUserId = userId; return `${userId}`; } @Get("/users/:firstId/photos/:secondId") - getUserPhoto(@Param("firstId") firstId: number, - @Param("secondId") secondId: number) { + getUserPhoto(@PathParam("firstId") firstId: number, + @PathParam("secondId") secondId: number) { paramFirstId = firstId; paramSecondId = secondId; return `${firstId},${secondId}`; @@ -166,6 +185,24 @@ describe("action parameters", () => { return `hello`; } + @Get("/photos-params") + getPhotosWithQuery(@QueryParams() query: QueryClass) { + queryParams1 = query; + return `hello`; + } + + @Get("/photos-params-no-validate") + getPhotosWithQueryAndNoValidation(@QueryParams({ validate: false }) query: QueryClass) { + queryParams2 = query; + return `hello`; + } + + @Get("/photos-params-optional") + getPhotosWithOptionalQuery(@QueryParams({ validate: { skipMissingProperties: true } }) query: QueryClass) { + queryParams3 = query; + return `hello`; + } + @Get("/photos-with-required") getPhotosWithIdRequired(@QueryParam("limit", { required: true }) limit: number) { queryParamLimit = limit; @@ -346,7 +383,7 @@ describe("action parameters", () => { }); }); - describe("@Param should give a param from route", () => { + describe("@PathParam should give a param from route", () => { assertRequest([3001, 3002], "get", "users/1", response => { expect(paramUserId).to.be.equal(1); expect(response).to.be.status(200); @@ -355,7 +392,7 @@ describe("action parameters", () => { }); }); - describe("multiple @Param should give a proper values from route", () => { + describe("multiple @PathParam should give a proper values from route", () => { assertRequest([3001, 3002], "get", "users/23/photos/32", response => { expect(paramFirstId).to.be.equal(23); expect(paramSecondId).to.be.equal(32); @@ -422,6 +459,40 @@ describe("action parameters", () => { }); }); + // todo: enable koa test when #227 fixed + describe("@QueryParams should give a proper values from request query parameters", () => { + assertRequest([3001, /*3002*/], "get", "photos-params?sortBy=name&count=2&limit=10&showAll", response => { + expect(response).to.be.status(200); + expect(response).to.have.header("content-type", "text/html; charset=utf-8"); + expect(queryParams1.sortBy).to.be.equal("name"); + expect(queryParams1.count).to.be.equal("2"); + expect(queryParams1.limit).to.be.equal(10); + expect(queryParams1.showAll).to.be.equal(true); + }); + }); + + describe("@QueryParams should not validate request query parameters when it's turned off in validator options", () => { + assertRequest([3001, 3002], "get", "photos-params-no-validate?sortBy=verylongtext&count=2&limit=1&showAll=true", response => { + expect(response).to.be.status(200); + expect(response).to.have.header("content-type", "text/html; charset=utf-8"); + expect(queryParams2.sortBy).to.be.equal("verylongtext"); + expect(queryParams2.count).to.be.equal("2"); + expect(queryParams2.limit).to.be.equal(1); + expect(queryParams2.showAll).to.be.equal(true); + }); + }); + + // todo: enable koa test when #227 fixed + describe("@QueryParams should give a proper values from request query parameters", () => { + assertRequest([3001, /*3002*/], "get", "photos-params-optional?sortBy=name&limit=10", response => { + expect(queryParams3.sortBy).to.be.equal("name"); + expect(queryParams3.count).to.be.equal(undefined); + expect(queryParams3.limit).to.be.equal(10); + expect(queryParams3.showAll).to.be.equal(true); + expect(response).to.be.status(200); + expect(response).to.have.header("content-type", "text/html; charset=utf-8"); + }); + }); describe("@QueryParam should give a proper values from request query parameters", () => { assertRequest([3001, 3002], "get", "photos?sortBy=name&count=2&limit=10&showAll=true", response => { @@ -459,8 +530,7 @@ describe("action parameters", () => { describe("for @QueryParam when the type is Date and it is invalid then the response should be a BadRequest error", () => { assertRequest([3001, 3002], "get", "posts-after/?from=InvalidDate", response => { expect(response).to.be.status(400); - expect(response.body.name).to.be.equals("BadRequestError"); - expect(response.body.message).to.be.equals("from is invalid! It can't be parsed to date."); + expect(response.body.name).to.be.equals("ParamNormalizationError"); }); }); diff --git a/test/functional/other-controller-decorators.spec.ts b/test/functional/other-controller-decorators.spec.ts index 2923700d..42c598bc 100644 --- a/test/functional/other-controller-decorators.spec.ts +++ b/test/functional/other-controller-decorators.spec.ts @@ -1,7 +1,7 @@ import "reflect-metadata"; import {Controller} from "../../src/decorator/Controller"; import {Get} from "../../src/decorator/Get"; -import {Param} from "../../src/decorator/Param"; +import {PathParam} from "../../src/decorator/PathParam"; import {Post} from "../../src/decorator/Post"; import {createExpressServer, createKoaServer, getMetadataArgsStorage, OnNull} from "../../src/index"; import {assertRequest} from "./test-utils"; @@ -49,7 +49,7 @@ describe("other controller decorators", () => { @Get("/posts/:id") @OnNull(404) - getPost(@Param("id") id: number) { + getPost(@PathParam("id") id: number) { return new Promise((ok, fail) => { if (id === 1) { ok("Post"); @@ -68,7 +68,7 @@ describe("other controller decorators", () => { @Get("/photos/:id") @OnUndefined(201) - getPhoto(@Param("id") id: number) { + getPhoto(@PathParam("id") id: number) { if (id === 4) { return undefined; } @@ -127,7 +127,7 @@ describe("other controller decorators", () => { @Get("/questions/:id") @OnUndefined(QuestionNotFoundError) - getPosts(@Param("id") id: number) { + getPosts(@PathParam("id") id: number) { return new Promise((ok, fail) => { if (id === 1) { ok("Question"); diff --git a/test/functional/redirect-decorator.spec.ts b/test/functional/redirect-decorator.spec.ts index 3209a039..63379f88 100644 --- a/test/functional/redirect-decorator.spec.ts +++ b/test/functional/redirect-decorator.spec.ts @@ -4,7 +4,7 @@ import {createExpressServer, createKoaServer, getMetadataArgsStorage} from "../. import {assertRequest} from "./test-utils"; import {Redirect} from "../../src/decorator/Redirect"; import {JsonController} from "../../src/decorator/JsonController"; -import {Param} from "../../src/decorator/Param"; +import {PathParam} from "../../src/decorator/PathParam"; const chakram = require("chakram"); const expect = chakram.expect; @@ -19,7 +19,7 @@ describe("dynamic redirect", function () { class TestController { @Get("/:id") - async getOne(@Param("id") id: string) { + async getOne(@PathParam("id") id: string) { return { login: id };