Skip to content

Commit

Permalink
Merge pull request #315 from typestack/next
Browse files Browse the repository at this point in the history
New major release (0.8.x)
  • Loading branch information
jotamorais authored Sep 30, 2019
2 parents 4a56d17 + 888c790 commit 2771c65
Show file tree
Hide file tree
Showing 14 changed files with 291 additions and 77 deletions.
9 changes: 6 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
# Changelog and release notes

### 0.7.8
### 0.8.0 [BREAKING CHANGES]

#### Features

- updated `class-transformer` and `class-validator` to latest.
- extract generic `@Session()` deocorator into `@SessionParam()` and `@Session()` (ref [#335][#348][#407])
- restore/introduce `@QueryParams()` and `@Params()` missing decorators options (ref [#289][#289])
- 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])
- 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])

### 0.7.7
### 0.7.7 (to be released)

#### Features

Expand Down
52 changes: 43 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,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

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

#### Inject session object

To inject a session value, use `@Session` decorator:
To inject a session value, use `@SessionParam` decorator:

```typescript
@Get("/login/")
savePost(@Session("user") user: User, @Body() post: Post) {
}
@Get("/login")
savePost(@SessionParam("user") user: User, @Body() post: Post) {}
```
If you want to inject the main session object, use `@Session()` without any parameters.

```typescript
@Get("/login")
savePost(@Session() session: any, @Body() post: Post) {}
```
The parameter marked with `@Session` decorator is required by default. If your action param is optional, you have to mark it as not required:
```ts
action(@Session("user", { required: false }) user: User)
action(@Session("user", { required: false }) user: User) {}
```

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.
Expand Down Expand Up @@ -1424,14 +1457,15 @@ export class QuestionController {
| `@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` |
| `@Params()` | `get(@Params() params: any)` | Injects all router 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` |
| `@HeaderParams()` | `get(@HeaderParams() params: any)` | Injects all request headers. | `request.headers` |
| `@CookieParam(name: string, options?: ParamOptions)` | `get(@CookieParam("username") username: string)` | Injects a cookie parameter. | `request.cookie("username")` |
| `@CookieParams()` | `get(@CookieParams() params: any)` | Injects all cookies. | `request.cookies |
| `@Session(name?: string)` | `get(@Session("user") user: User)` | Injects an object from session (or the whole session). | `request.session.user` |
| `@CookieParams()` | `get(@CookieParams() params: any)` | Injects all cookies. | `request.cookies` |
| `@Session()` | `get(@Session() session: any)` | Injects the whole session object. | `request.session` |
| `@SessionParam(name: string)` | `get(@SessionParam("user") user: User)` | Injects an object from session property. | `request.session.user` |
| `@State(name?: string)` | `get(@State() session: StateType)` | Injects an object from the state (or the whole state). | `ctx.state` (koa-analogue) |
| `@Body(options?: BodyOptions)` | `post(@Body() body: any)` | Injects a body. In parameter options you can specify body parser middleware options. | `request.body` |
| `@BodyParam(name: string, options?: ParamOptions)` | `post(@BodyParam("name") name: string)` | Injects a body parameter. | `request.body.name` |
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "routing-controllers",
"private": true,
"version": "0.7.8",
"version": "0.8.0",
"description": "Create structured, declarative and beautifully organized class-based controllers with heavy decorators usage for Express / Koa using TypeScript.",
"license": "MIT",
"readmeFilename": "README.md",
Expand Down
3 changes: 2 additions & 1 deletion sample/sample12-session-support/UserController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {Patch} from "../../src/decorator/Patch";
import {Delete} from "../../src/decorator/Delete";
import {Param} from "../../src/decorator/Param";
import {Session} from "../../src/decorator/Session";
import {SessionParam} from "../../src/decorator/SessionParam";
import {ContentType} from "../../src/decorator/ContentType";

@Controller()
Expand All @@ -25,7 +26,7 @@ export class UserController {

@Get("/users/:id")
@ContentType("application/json")
getOne(@Session("user") user: any) {
getOne(@SessionParam("user") user: any) {
return user;
}

Expand Down
89 changes: 68 additions & 21 deletions src/ActionParameterHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 { InvalidParamError } from "./error/ParamNormalizationError";

/**
* Handles action parameter.
Expand Down Expand Up @@ -42,6 +43,7 @@ export class ActionParameterHandler<T extends BaseDriver> {

// 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));

Expand Down Expand Up @@ -72,7 +74,7 @@ export class ActionParameterHandler<T extends BaseDriver> {
// 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));
Expand Down Expand Up @@ -103,43 +105,88 @@ export class ActionParameterHandler<T extends BaseDriver> {
/**
* Normalizes parameter value.
*/
protected normalizeParamValue(value: any, param: ParamMetadata): Promise<any>|any {
protected async normalizeParamValue(value: any, param: ParamMetadata): Promise<any> {
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", "params", "cookies"].some(paramType => paramType === param.type)) {
await Promise.all(Object.keys(value).map(async key => {
const keyValue = value[key];
if (typeof keyValue === "string") {
const ParamType: Function|undefined = Reflect.getMetadata("design:type", param.targetType.prototype, key);
if (ParamType) {
const typeString = ParamType.name.toLowerCase();
value[key] = await this.normalizeParamValue(keyValue, {
...param,
name: key,
targetType: ParamType,
targetName: 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 = await this.validateValue(value, param);
}

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 InvalidParamError(value, parameterName, parameterType);
}

case "string":
return value;
const valueNumber = +value;
if (valueNumber === NaN) {
throw new InvalidParamError(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 InvalidParamError(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 InvalidParamError(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;
}

/**
Expand Down
13 changes: 8 additions & 5 deletions src/decorator/Params.ts
Original file line number Diff line number Diff line change
@@ -1,19 +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 Params(): Function {
export function Params(options?: ParamOptions): 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
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,
});
};
}
}
11 changes: 8 additions & 3 deletions src/decorator/QueryParams.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,23 @@
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
name: "",
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,
});
};
}
25 changes: 4 additions & 21 deletions src/decorator/Session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,34 +5,17 @@ import { getMetadataArgsStorage } from "../index";
* Injects a Session object to the controller action parameter.
* Must be applied on a controller action parameter.
*/
export function Session(options?: ParamOptions): ParameterDecorator;
/**
* Injects a Session object to the controller action parameter.
* Must be applied on a controller action parameter.
*/
export function Session(propertyName: string, options?: ParamOptions): ParameterDecorator;

export function Session(optionsOrObjectName?: ParamOptions|string, paramOptions?: ParamOptions): ParameterDecorator {
let propertyName: string|undefined;
let options: ParamOptions|undefined;
if (typeof optionsOrObjectName === "string") {
propertyName = optionsOrObjectName;
options = paramOptions || {};
} else {
options = optionsOrObjectName || {};
}

export function Session(options?: ParamOptions): ParameterDecorator {
return function (object: Object, methodName: string, index: number) {
getMetadataArgsStorage().params.push({
type: "session",
object: object,
method: methodName,
index: index,
name: propertyName,
parse: false, // it makes no sense for Session object to be parsed as json
required: options.required !== undefined ? options.required : true,
classTransform: options.transform,
validate: options.validate !== undefined ? options.validate : false,
required: options && options.required !== undefined ? options.required : true,
classTransform: options && options.transform,
validate: options && options.validate !== undefined ? options.validate : false,
});
};
}
22 changes: 22 additions & 0 deletions src/decorator/SessionParam.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { ParamOptions } from "../decorator-options/ParamOptions";
import { getMetadataArgsStorage } from "../index";

/**
* Injects a Session object property to the controller action parameter.
* Must be applied on a controller action parameter.
*/
export function SessionParam(propertyName: string, options?: ParamOptions): ParameterDecorator {
return function (object: Object, methodName: string, index: number) {
getMetadataArgsStorage().params.push({
type: "session-param",
object: object,
method: methodName,
index: index,
name: propertyName,
parse: false, // it makes no sense for Session object to be parsed as json
required: options && options.required !== undefined ? options.required : false,
classTransform: options && options.transform,
validate: options && options.validate !== undefined ? options.validate : false,
});
};
}
Loading

0 comments on commit 2771c65

Please sign in to comment.