Skip to content

Commit 2771c65

Browse files
authored
Merge pull request #315 from typestack/next
New major release (0.8.x)
2 parents 4a56d17 + 888c790 commit 2771c65

File tree

14 files changed

+291
-77
lines changed

14 files changed

+291
-77
lines changed

CHANGELOG.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
# Changelog and release notes
22

3-
### 0.7.8
3+
### 0.8.0 [BREAKING CHANGES]
44

55
#### Features
66

7-
- updated `class-transformer` and `class-validator` to latest.
7+
- extract generic `@Session()` deocorator into `@SessionParam()` and `@Session()` (ref [#335][#348][#407])
8+
- restore/introduce `@QueryParams()` and `@Params()` missing decorators options (ref [#289][#289])
9+
- normalize param object properties (for "queries", "headers", "params" and "cookies"), with this change you can easily validate query/path params using `class-validator` (ref [#289][#289])
10+
- improved params normalization, converting to primitive types is now more strict and can throw ParamNormalizationError (e.g. when number is expected but an invalid string (NaN) has been received) (ref [#289][#289])
811

9-
### 0.7.7
12+
### 0.7.7 (to be released)
1013

1114
#### Features
1215

README.md

Lines changed: 43 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -370,6 +370,37 @@ getUsers(@QueryParam("limit") limit: number) {
370370
```
371371

372372
If you want to inject all query parameters use `@QueryParams()` decorator.
373+
The bigest benefit of this approach is that you can perform validation of the params.
374+
375+
```typescript
376+
enum Roles {
377+
Admin = "admin",
378+
User = "user",
379+
Guest = "guest",
380+
}
381+
382+
class GetUsersQuery {
383+
384+
@IsPositive()
385+
limit: number;
386+
387+
@IsAlpha()
388+
city: string;
389+
390+
@IsEnum(Roles)
391+
role: Roles;
392+
393+
@IsBoolean()
394+
isActive: boolean;
395+
396+
}
397+
398+
@Get("/users")
399+
getUsers(@QueryParams() query: GetUsersQuery) {
400+
// here you can access query.role, query.limit
401+
// and others valid query parameters
402+
}
403+
```
373404

374405
#### Inject request body
375406

@@ -421,18 +452,20 @@ If you want to inject all header parameters use `@CookieParams()` decorator.
421452

422453
#### Inject session object
423454

424-
To inject a session value, use `@Session` decorator:
455+
To inject a session value, use `@SessionParam` decorator:
425456

426457
```typescript
427-
@Get("/login/")
428-
savePost(@Session("user") user: User, @Body() post: Post) {
429-
}
458+
@Get("/login")
459+
savePost(@SessionParam("user") user: User, @Body() post: Post) {}
430460
```
431461
If you want to inject the main session object, use `@Session()` without any parameters.
432-
462+
```typescript
463+
@Get("/login")
464+
savePost(@Session() session: any, @Body() post: Post) {}
465+
```
433466
The parameter marked with `@Session` decorator is required by default. If your action param is optional, you have to mark it as not required:
434467
```ts
435-
action(@Session("user", { required: false }) user: User)
468+
action(@Session("user", { required: false }) user: User) {}
436469
```
437470

438471
Express uses [express-session][5] / Koa uses [koa-session][6] or [koa-generic-session][7] to handle session, so firstly you have to install it manually to use `@Session` decorator.
@@ -1424,14 +1457,15 @@ export class QuestionController {
14241457
| `@Res()` | `getAll(@Res() response: Response)` | Injects a Response object. | `function (request, response)` |
14251458
| `@Ctx()` | `getAll(@Ctx() context: Context)` | Injects a Context object (koa-specific) | `function (ctx)` (koa-analogue) |
14261459
| `@Param(name: string, options?: ParamOptions)` | `get(@Param("id") id: number)` | Injects a router parameter. | `request.params.id` |
1427-
| `@Params()` | `get(@Params() params: any)` | Injects all request parameters. | `request.params` |
1460+
| `@Params()` | `get(@Params() params: any)` | Injects all router parameters. | `request.params` |
14281461
| `@QueryParam(name: string, options?: ParamOptions)` | `get(@QueryParam("id") id: number)` | Injects a query string parameter. | `request.query.id` |
14291462
| `@QueryParams()` | `get(@QueryParams() params: any)` | Injects all query parameters. | `request.query` |
14301463
| `@HeaderParam(name: string, options?: ParamOptions)` | `get(@HeaderParam("token") token: string)` | Injects a specific request headers. | `request.headers.token` |
14311464
| `@HeaderParams()` | `get(@HeaderParams() params: any)` | Injects all request headers. | `request.headers` |
14321465
| `@CookieParam(name: string, options?: ParamOptions)` | `get(@CookieParam("username") username: string)` | Injects a cookie parameter. | `request.cookie("username")` |
1433-
| `@CookieParams()` | `get(@CookieParams() params: any)` | Injects all cookies. | `request.cookies |
1434-
| `@Session(name?: string)` | `get(@Session("user") user: User)` | Injects an object from session (or the whole session). | `request.session.user` |
1466+
| `@CookieParams()` | `get(@CookieParams() params: any)` | Injects all cookies. | `request.cookies` |
1467+
| `@Session()` | `get(@Session() session: any)` | Injects the whole session object. | `request.session` |
1468+
| `@SessionParam(name: string)` | `get(@SessionParam("user") user: User)` | Injects an object from session property. | `request.session.user` |
14351469
| `@State(name?: string)` | `get(@State() session: StateType)` | Injects an object from the state (or the whole state). | `ctx.state` (koa-analogue) |
14361470
| `@Body(options?: BodyOptions)` | `post(@Body() body: any)` | Injects a body. In parameter options you can specify body parser middleware options. | `request.body` |
14371471
| `@BodyParam(name: string, options?: ParamOptions)` | `post(@BodyParam("name") name: string)` | Injects a body parameter. | `request.body.name` |

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "routing-controllers",
33
"private": true,
4-
"version": "0.7.8",
4+
"version": "0.8.0",
55
"description": "Create structured, declarative and beautifully organized class-based controllers with heavy decorators usage for Express / Koa using TypeScript.",
66
"license": "MIT",
77
"readmeFilename": "README.md",

sample/sample12-session-support/UserController.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {Patch} from "../../src/decorator/Patch";
99
import {Delete} from "../../src/decorator/Delete";
1010
import {Param} from "../../src/decorator/Param";
1111
import {Session} from "../../src/decorator/Session";
12+
import {SessionParam} from "../../src/decorator/SessionParam";
1213
import {ContentType} from "../../src/decorator/ContentType";
1314

1415
@Controller()
@@ -25,7 +26,7 @@ export class UserController {
2526

2627
@Get("/users/:id")
2728
@ContentType("application/json")
28-
getOne(@Session("user") user: any) {
29+
getOne(@SessionParam("user") user: any) {
2930
return user;
3031
}
3132

src/ActionParameterHandler.ts

Lines changed: 68 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {ParamRequiredError} from "./error/ParamRequiredError";
99
import {AuthorizationRequiredError} from "./error/AuthorizationRequiredError";
1010
import {CurrentUserCheckerNotDefinedError} from "./error/CurrentUserCheckerNotDefinedError";
1111
import {isPromiseLike} from "./util/isPromiseLike";
12+
import { InvalidParamError } from "./error/ParamNormalizationError";
1213

1314
/**
1415
* Handles action parameter.
@@ -42,6 +43,7 @@ export class ActionParameterHandler<T extends BaseDriver> {
4243

4344
// get parameter value from request and normalize it
4445
const value = this.normalizeParamValue(this.driver.getParamFromRequest(action, param), param);
46+
4547
if (isPromiseLike(value))
4648
return value.then(value => this.handleValue(value, action, param));
4749

@@ -72,7 +74,7 @@ export class ActionParameterHandler<T extends BaseDriver> {
7274
// check cases when parameter is required but its empty and throw errors in this case
7375
if (param.required) {
7476
const isValueEmpty = value === null || value === undefined || value === "";
75-
const isValueEmptyObject = value instanceof Object && Object.keys(value).length === 0;
77+
const isValueEmptyObject = typeof value === "object" && Object.keys(value).length === 0;
7678

7779
if (param.type === "body" && !param.name && (isValueEmpty || isValueEmptyObject)) { // body has a special check and error message
7880
return Promise.reject(new ParamRequiredError(action, param));
@@ -103,43 +105,88 @@ export class ActionParameterHandler<T extends BaseDriver> {
103105
/**
104106
* Normalizes parameter value.
105107
*/
106-
protected normalizeParamValue(value: any, param: ParamMetadata): Promise<any>|any {
108+
protected async normalizeParamValue(value: any, param: ParamMetadata): Promise<any> {
107109
if (value === null || value === undefined)
108110
return value;
109111

110-
switch (param.targetName) {
112+
// if param value is an object and param type match, normalize its string properties
113+
if (typeof value === "object" && ["queries", "headers", "params", "cookies"].some(paramType => paramType === param.type)) {
114+
await Promise.all(Object.keys(value).map(async key => {
115+
const keyValue = value[key];
116+
if (typeof keyValue === "string") {
117+
const ParamType: Function|undefined = Reflect.getMetadata("design:type", param.targetType.prototype, key);
118+
if (ParamType) {
119+
const typeString = ParamType.name.toLowerCase();
120+
value[key] = await this.normalizeParamValue(keyValue, {
121+
...param,
122+
name: key,
123+
targetType: ParamType,
124+
targetName: typeString,
125+
});
126+
}
127+
}
128+
}));
129+
}
130+
// if value is a string, normalize it to demanded type
131+
else if (typeof value === "string") {
132+
switch (param.targetName) {
133+
case "number":
134+
case "string":
135+
case "boolean":
136+
case "date":
137+
return this.normalizeStringValue(value, param.name, param.targetName);
138+
}
139+
}
140+
141+
// if target type is not primitive, transform and validate it
142+
if ((["number", "string", "boolean"].indexOf(param.targetName) === -1)
143+
&& (param.parse || param.isTargetObject)
144+
) {
145+
value = this.parseValue(value, param);
146+
value = this.transformValue(value, param);
147+
value = await this.validateValue(value, param);
148+
}
149+
150+
return value;
151+
}
152+
153+
/**
154+
* Normalizes string value to number or boolean.
155+
*/
156+
protected normalizeStringValue(value: string, parameterName: string, parameterType: string) {
157+
switch (parameterType) {
111158
case "number":
112-
if (value === "") return undefined;
113-
return +value;
159+
if (value === "") {
160+
throw new InvalidParamError(value, parameterName, parameterType);
161+
}
114162

115-
case "string":
116-
return value;
163+
const valueNumber = +value;
164+
if (valueNumber === NaN) {
165+
throw new InvalidParamError(value, parameterName, parameterType);
166+
}
167+
168+
return valueNumber;
117169

118170
case "boolean":
119-
if (value === "true" || value === "1") {
171+
if (value === "true" || value === "1" || value === "") {
120172
return true;
121-
122173
} else if (value === "false" || value === "0") {
123174
return false;
175+
} else {
176+
throw new InvalidParamError(value, parameterName, parameterType);
124177
}
125-
126-
return !!value;
127-
178+
128179
case "date":
129180
const parsedDate = new Date(value);
130-
if (isNaN(parsedDate.getTime())) {
131-
return Promise.reject(new BadRequestError(`${param.name} is invalid! It can't be parsed to date.`));
181+
if (Number.isNaN(parsedDate.getTime())) {
182+
throw new InvalidParamError(value, parameterName, parameterType);
132183
}
133184
return parsedDate;
134-
185+
186+
case "string":
135187
default:
136-
if (value && (param.parse || param.isTargetObject)) {
137-
value = this.parseValue(value, param);
138-
value = this.transformValue(value, param);
139-
value = this.validateValue(value, param); // note this one can return promise
140-
}
188+
return value;
141189
}
142-
return value;
143190
}
144191

145192
/**

src/decorator/Params.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,22 @@
1+
import {ParamOptions} from "../decorator-options/ParamOptions";
12
import {getMetadataArgsStorage} from "../index";
23

34
/**
45
* Injects all request's route parameters to the controller action parameter.
56
* Must be applied on a controller action parameter.
67
*/
7-
export function Params(): Function {
8+
export function Params(options?: ParamOptions): Function {
89
return function (object: Object, methodName: string, index: number) {
910
getMetadataArgsStorage().params.push({
1011
type: "params",
1112
object: object,
1213
method: methodName,
1314
index: index,
14-
parse: false, // it does not make sense for Param to be parsed
15-
required: false,
16-
classTransform: undefined
15+
parse: options ? options.parse : false,
16+
required: options ? options.required : undefined,
17+
classTransform: options ? options.transform : undefined,
18+
explicitType: options ? options.type : undefined,
19+
validate: options ? options.validate : undefined,
1720
});
1821
};
19-
}
22+
}

src/decorator/QueryParams.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,23 @@
1+
import {ParamOptions} from "../decorator-options/ParamOptions";
12
import {getMetadataArgsStorage} from "../index";
23

34
/**
45
* Injects all request's query parameters to the controller action parameter.
56
* Must be applied on a controller action parameter.
67
*/
7-
export function QueryParams(): Function {
8+
export function QueryParams(options?: ParamOptions): Function {
89
return function (object: Object, methodName: string, index: number) {
910
getMetadataArgsStorage().params.push({
1011
type: "queries",
1112
object: object,
1213
method: methodName,
1314
index: index,
14-
parse: false,
15-
required: false
15+
name: "",
16+
parse: options ? options.parse : false,
17+
required: options ? options.required : undefined,
18+
classTransform: options ? options.transform : undefined,
19+
explicitType: options ? options.type : undefined,
20+
validate: options ? options.validate : undefined,
1621
});
1722
};
1823
}

src/decorator/Session.ts

Lines changed: 4 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,34 +5,17 @@ import { getMetadataArgsStorage } from "../index";
55
* Injects a Session object to the controller action parameter.
66
* Must be applied on a controller action parameter.
77
*/
8-
export function Session(options?: ParamOptions): ParameterDecorator;
9-
/**
10-
* Injects a Session object to the controller action parameter.
11-
* Must be applied on a controller action parameter.
12-
*/
13-
export function Session(propertyName: string, options?: ParamOptions): ParameterDecorator;
14-
15-
export function Session(optionsOrObjectName?: ParamOptions|string, paramOptions?: ParamOptions): ParameterDecorator {
16-
let propertyName: string|undefined;
17-
let options: ParamOptions|undefined;
18-
if (typeof optionsOrObjectName === "string") {
19-
propertyName = optionsOrObjectName;
20-
options = paramOptions || {};
21-
} else {
22-
options = optionsOrObjectName || {};
23-
}
24-
8+
export function Session(options?: ParamOptions): ParameterDecorator {
259
return function (object: Object, methodName: string, index: number) {
2610
getMetadataArgsStorage().params.push({
2711
type: "session",
2812
object: object,
2913
method: methodName,
3014
index: index,
31-
name: propertyName,
3215
parse: false, // it makes no sense for Session object to be parsed as json
33-
required: options.required !== undefined ? options.required : true,
34-
classTransform: options.transform,
35-
validate: options.validate !== undefined ? options.validate : false,
16+
required: options && options.required !== undefined ? options.required : true,
17+
classTransform: options && options.transform,
18+
validate: options && options.validate !== undefined ? options.validate : false,
3619
});
3720
};
3821
}

src/decorator/SessionParam.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { ParamOptions } from "../decorator-options/ParamOptions";
2+
import { getMetadataArgsStorage } from "../index";
3+
4+
/**
5+
* Injects a Session object property to the controller action parameter.
6+
* Must be applied on a controller action parameter.
7+
*/
8+
export function SessionParam(propertyName: string, options?: ParamOptions): ParameterDecorator {
9+
return function (object: Object, methodName: string, index: number) {
10+
getMetadataArgsStorage().params.push({
11+
type: "session-param",
12+
object: object,
13+
method: methodName,
14+
index: index,
15+
name: propertyName,
16+
parse: false, // it makes no sense for Session object to be parsed as json
17+
required: options && options.required !== undefined ? options.required : false,
18+
classTransform: options && options.transform,
19+
validate: options && options.validate !== undefined ? options.validate : false,
20+
});
21+
};
22+
}

0 commit comments

Comments
 (0)