Skip to content
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

Fix missing @QueryParams decorator options #270

Closed
Closed
Show file tree
Hide file tree
Changes from 3 commits
Commits
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
63 changes: 47 additions & 16 deletions src/ActionParameterHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,32 +103,30 @@ export class ActionParameterHandler {
/**
* 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;

// map @QueryParams object properties from string to basic types (normalize)
if (param.type === "queries" && typeof value === "object") {
Object.keys(value).map(key => {
const type = Reflect.getMetadata("design:type", param.targetType.prototype, key);
const typeString = typeof type();
Copy link
Contributor Author

@MichalLytek MichalLytek Aug 31, 2017

Choose a reason for hiding this comment

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

TBH, I don't like this part, I did this as a quick shortcut.
It may introduce some weird behavior when the reflected type is not constructor-like (is it possible in TypeScript?) especially when it's undefined, so I think I should check if the type is undefined for case like no property type provided in class definition.

value[key] = this.normalizeValue(value[key], typeString);
});
}

switch (param.targetName) {
case "number":
if (value === "") return undefined;
return +value;

case "number":
case "string":
return value;

case "boolean":
if (value === "true" || value === "1") {
return true;

} else if (value === "false" || value === "0") {
return false;
}

return !!value;
return this.normalizeValue(value, param.targetName);

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.`));
throw new BadRequestError(`${param.name} is invalid! It can't be parsed to date.`);
}
return parsedDate;

Expand All @@ -138,8 +136,41 @@ export class ActionParameterHandler {
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 normalizeValue(value: any, type: string) {
switch (type) {
case "number":
if (value === "")
return undefined;
Copy link
Member

Choose a reason for hiding this comment

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

I think if a parameter is passed in the query with no value it should be casted to null instead of undefined.

I will add a normal comment to detail this.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I've just extracted pleerock's code to separate method, I haven't changed nothing in normalizing logic.
https://github.com/pleerock/routing-controllers/blob/master/src/ActionParameterHandler.ts#L112

I agree that we should convert "" to true for boolean, but for number we should use undefined if param value not exist or NaN if it's not a number string. However I would like to don't mix different things in one PR, as we first need to discuss the edge cases we should merge this @QueryParams PR and start a new one with better normalizing.

const valueNumber = Number(value);
// tslint:disable-next-line:triple-equals
if (valueNumber == value)
return valueNumber;
else
throw new BadRequestError(`${value} can't be parsed to number.`);

case "string":
return value;

case "boolean":
if (value === "true" || value === "1") {
return true;

} else if (value === "false" || value === "0") {
return false;
}

return Boolean(value);

default:
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think we should also cover the date case and also the array feature which was requested in #123 - it is possible with class-transform @Type decorator to work with arrays.

return value;
}
return value;
}

/**
Expand Down
10 changes: 7 additions & 3 deletions src/decorator/QueryParams.ts
Original file line number Diff line number Diff line change
@@ -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,
});
};
}
56 changes: 55 additions & 1 deletion test/functional/action-params.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import "reflect-metadata";

import {createExpressServer, createKoaServer, getMetadataArgsStorage} from "../../src/index";
import { createExpressServer, createKoaServer, getMetadataArgsStorage, QueryParams } from "../../src/index";

import {assertRequest} from "./test-utils";
import {User} from "../fakes/global-options/User";
Expand All @@ -22,6 +22,7 @@ import {UploadedFile} from "../../src/decorator/UploadedFile";
import {UploadedFiles} from "../../src/decorator/UploadedFiles";
import {ContentType} from "../../src/decorator/ContentType";
import {JsonController} from "../../src/decorator/JsonController";
import { IsString, IsBoolean, Min, MaxLength } from "class-validator";

const chakram = require("chakram");
const expect = chakram.expect;
Expand All @@ -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};
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;
Expand All @@ -50,6 +52,8 @@ describe("action parameters", () => {
queryParamLimit = undefined;
queryParamShowAll = undefined;
queryParamFilter = undefined;
queryParams1 = undefined;
queryParams2 = undefined;
headerParamToken = undefined;
headerParamCount = undefined;
headerParamShowAll = undefined;
Expand Down Expand Up @@ -78,6 +82,22 @@ 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;

}

@Controller()
class UserActionParamsController {

Expand Down Expand Up @@ -166,6 +186,18 @@ describe("action parameters", () => {
return `<html><body>hello</body></html>`;
}

@Get("/photos-params")
getPhotosWithQuery(@QueryParams() query: QueryClass) {
queryParams1 = query;
return `<html><body>hello</body></html>`;
}

@Get("/photos-params-no-validate")
getPhotosWithQueryAndNoValidation(@QueryParams({ validate: false }) query: QueryClass) {
queryParams2 = query;
return `<html><body>hello</body></html>`;
}

@Get("/photos-with-required")
getPhotosWithIdRequired(@QueryParam("limit", { required: true }) limit: number) {
queryParamLimit = limit;
Expand Down Expand Up @@ -420,6 +452,28 @@ describe("action parameters", () => {
});
});

describe("@QueryParams should give a proper values from request query parameters", () => {
assertRequest([3001, /*3002*/], "get", "photos-params?sortBy=name&count=2&limit=10&showAll=true", 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);
});
});

describe("for @QueryParam when required is params must be provided and they should not be empty", () => {
assertRequest([3001, 3002], "get", "photos-with-required/?limit=0", response => {
expect(queryParamLimit).to.be.equal(0);
Expand Down