Skip to content

release: 0.8.x #315

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 27 commits into from
Sep 30, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
e8cbf59
Fix missing @QueryParams options
MichalLytek Aug 31, 2017
3e71052
Add normalizing feature to queries object
MichalLytek Aug 31, 2017
1c97969
Create test cases for @QueryParams and its options
MichalLytek Aug 31, 2017
6353ca1
Add check if property type is not undefined (no type annotation)
MichalLytek Aug 31, 2017
4eac44c
Add info in docs about @QueryParams() features
MichalLytek Aug 31, 2017
1ca0c3c
Add optional query params test case
MichalLytek Sep 3, 2017
5aa5087
Merge branch 'master' into fix/params-decorator-options
MichalLytek Sep 8, 2017
0087878
Fix readme enum type example
MichalLytek Sep 11, 2017
4df235e
Add ParamNormalization error and change normalization handler flow
MichalLytek Sep 11, 2017
3c13d39
Add support for normalizing objects (query, path, etc.)
MichalLytek Sep 11, 2017
316d62d
Merge branch 'master' into params-fixes-n-features
MichalLytek Sep 11, 2017
fb18b54
Add @Params decorator options
MichalLytek Sep 11, 2017
dd4f710
Rename @Param(s) decorators to @PathParam(s)
MichalLytek Sep 11, 2017
f91d392
Update changelog
MichalLytek Sep 11, 2017
d7fe496
Fix typo and change .indexOf to .some
MichalLytek Sep 16, 2017
d4ce9ca
Merge pull request #289 from 19majkel94/params-fixes-n-features
19majkel94 Sep 18, 2017
ccc2e54
Bump version to premajor
MichalLytek Sep 18, 2017
bac90a1
Revert `@PathParam` decorator name change (#289)
MichalLytek Oct 21, 2017
673dda9
Merge branch 'master' into next
MichalLytek Oct 21, 2017
34ac8ac
Change `ParamNormalizationError` to `InvalidParamError`
MichalLytek Oct 21, 2017
0e91408
Refactor part of `@Session` decorator to `@SessionParam`
MichalLytek Oct 21, 2017
63d10e4
merge: merge master into next
NoNameProvided Nov 11, 2017
6d7ce5f
chore: add breaking changes to this version in the CHANGELOG
NoNameProvided Nov 11, 2017
823155c
Fix types in `normalizeParamValue` method
MichalLytek Nov 11, 2017
34cb2e4
Merge branch 'next' of https://github.com/pleerock/routing-controller…
MichalLytek Nov 11, 2017
6f64c86
Add nested query params json support
MichalLytek Nov 28, 2017
888c790
Merge branch 'master' into next
jotamorais Sep 30, 2019
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

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,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmm, false is the default value? Doesn't we default to using class-transformer?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For @Session validate is default to false because there's no need to validate the session object as it's not the user/client input but the express-session internal object from session storage.

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