Skip to content

Commit

Permalink
feat(oak): query trans params
Browse files Browse the repository at this point in the history
  • Loading branch information
jiawei397 committed Mar 2, 2022
1 parent 91aa9b1 commit 7f55a09
Show file tree
Hide file tree
Showing 5 changed files with 186 additions and 27 deletions.
2 changes: 1 addition & 1 deletion example/user/controllers/role.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ export class RoleController {
}

@Get("/info")
async getInfo(@Res() res: Response, @Query() params: any) {
async getInfo(@Res() res: Response, @Query() params: RoleInfoDto) {
console.log("params is ", params);
res.body = "role get info " + JSON.stringify(params) + " - " +
await this.roleService.info() + "-\n" + this.asyncService.info();
Expand Down
6 changes: 6 additions & 0 deletions example/user/dtos/role.dto.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
import { Property } from "../../../mod.ts";
import { Max, Min } from "../../deps.ts";

export class RoleInfoDto {
@Max(2)
@Min(1)
@Property()
pageNum!: number;

@Max(5)
@Min(1)
@Property()
pageCount!: number;

@Property()
sex!: boolean;
}
94 changes: 69 additions & 25 deletions src/decorators/oak.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,45 @@ import {
createParamDecoratorWithLowLevel,
} from "../params.ts";
import { parseSearch } from "../utils.ts";
import { Constructor } from "../interfaces/type.interface.ts";

const typePreKey = "oaktype:";

export function Property(): PropertyDecorator {
return (target: any, propertyKey: any) => {
const type = Reflect.getMetadata("design:type", target, propertyKey);
Reflect.defineMetadata(typePreKey + propertyKey, type, target);
};
}

// deno-lint-ignore ban-types
async function validateParams(Cls: Constructor, value: object) {
if (!Cls || Cls === Object) { // if no class validation, we can skip this
return;
}
const post = new Cls();
Object.assign(post, value);
try {
await validateOrReject(post);
} catch (errors) {
// console.debug(errors);
const msgs: string[] = [];
errors.forEach((err: ValidationError) => {
if (err.constraints) {
Object.values(err.constraints).forEach((element) => {
msgs.push(element);
});
}
});
assert(
msgs.length > 0,
`the msgs must be not empty and the validationErrors are ${
JSON.stringify(errors)
}`,
);
throw new BodyParamValidationException(msgs.join(","));
}
}

export const Body = createParamDecorator(
async (ctx: Context, target: any, methodName: string, index: number) => {
Expand All @@ -24,30 +63,7 @@ export const Body = createParamDecorator(
target,
methodName,
);
if (providers?.[index] && providers[index] !== Object) { // if no class validation, we can skip this
const post = new providers[index]();
Object.assign(post, value);
try {
await validateOrReject(post);
} catch (errors) {
// console.debug(errors);
const msgs: string[] = [];
errors.forEach((err: ValidationError) => {
if (err.constraints) {
Object.values(err.constraints).forEach((element) => {
msgs.push(element);
});
}
});
assert(
msgs.length > 0,
`the msgs must be not empty and the validationErrors are ${
JSON.stringify(errors)
}`,
);
throw new BodyParamValidationException(msgs.join(","));
}
}
await validateParams(providers?.[index], value);
return value;
}
},
Expand Down Expand Up @@ -84,10 +100,38 @@ function parseNumOrBool(
*/
export function Query(key?: string) {
return createParamDecoratorWithLowLevel(
(ctx: Context, target: any, methodName: string, index: number) => {
async (ctx: Context, target: any, methodName: string, index: number) => {
const { search } = ctx.request.url;
const map = parseSearch(search);
if (!key) {
const providers = Reflect.getMetadata( // get the params providers
"design:paramtypes",
target,
methodName,
);
if (!providers || !providers[index] || providers[index] === Object) {
return map;
}
const cls = providers[index];
const keys = Reflect.getMetadataKeys(cls.prototype);
let isNeedValidate = false;
keys.forEach((key) => {
if (!key.startsWith(typePreKey)) {
return;
}
isNeedValidate = true;
const type = Reflect.getMetadata(key, cls.prototype);
const realKey = key.replace(typePreKey, "");
// console.log(key, type);
if (type === Boolean) {
map[realKey] = map[realKey] === "true";
} else if (type === Number) {
map[realKey] = Number(map[realKey]);
}
});
if (isNeedValidate) { // if not use Property to translate the params, then we can skip this
await validateParams(cls, map);
}
return map;
}
return parseNumOrBool(map[key], target, methodName, index);
Expand Down
110 changes: 109 additions & 1 deletion src/decorators/oak_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
Headers,
MethodName,
Params,
Property,
Query,
Req,
Res,
Expand Down Expand Up @@ -182,8 +183,49 @@ Deno.test("Query", async () => {
c: "4",
f: "false",
g: "true",
i: "dd",
j: "5",
};
const mockPath = "/a?a=b&c=4&f=false&g=true";
const mockPath = "/a?a=b&c=4&f=false&g=true&i=dd&j=5";
const mockErrorPath = "/d?a=b&c=30";
const mockErrorQuery = {
a: "b",
c: 30,
};
const mockErrorButNotValidatePath = "/e?a=b&d=30";
const mockErrorButNotValidatePathQuery = {
a: "b",
d: "30",
};

// deno-lint-ignore no-unused-vars
class QueryDto {
@Property()
a!: string;

@Property()
@Max(20)
c!: number;

@Property()
f!: boolean;

@Property()
g!: boolean;

@Property()
i!: boolean;

j!: number;
}

// deno-lint-ignore no-unused-vars
class QueryNotValidateDto {
a!: string;

@Max(20)
d!: number;
}

@Controller("")
class A {
Expand All @@ -197,6 +239,7 @@ Deno.test("Query", async () => {
@Query("f") f: boolean,
@Query("g") g: boolean,
@Query("h") h: boolean,
@Query() query2: QueryDto,
) {
callStack.push(1);
assertEquals(query, mockQuery);
Expand All @@ -212,6 +255,15 @@ Deno.test("Query", async () => {
undefined,
"if no parsed, should be undefined instead of false",
);
// query2 is translated
assert(typeof query2.c === "number");
assertEquals(query2.c, Number(mockQuery.c));
assertEquals(query2.a, mockQuery.a);
assert(query2.f === false);
assert(query2.g === true);
assert(query2.i === false);
assertEquals(query2.j, mockQuery.j);
assert(typeof query2.j === "string", "not transferred");
}

@Post("a")
Expand All @@ -235,6 +287,31 @@ Deno.test("Query", async () => {
callStack.push(3);
assertEquals(query, {});
}

@Get("d")
testErrorQuery(@Query() query: QueryDto) {
callStack.push(4);
assertEquals(query.a, mockErrorQuery.a);
assertEquals(
query.c,
mockErrorQuery.c,
);
assert(typeof query.c === "number");
}

@Get("e")
testErrorButNotValidateQuery(@Query() query: QueryNotValidateDto) {
callStack.push(5);
assertEquals(query.a, mockErrorButNotValidatePathQuery.a);
assertEquals(
query.d,
mockErrorButNotValidatePathQuery.d,
);
assert(
typeof query.d === "string",
"not set Property, so should be string type",
);
}
}

const router = new Router();
Expand Down Expand Up @@ -281,6 +358,37 @@ Deno.test("Query", async () => {
assertEquals(callStack, [3]);
callStack.length = 0;
}

{
const ctx = testing.createMockContext({
path: mockErrorPath,
method: "GET",
});
const mw = router.routes();
const next = testing.createMockNext();

try {
await mw(ctx, next);
} catch (error) {
// console.log(error);
assertEquals(error.message, "c must not be greater than 20");
callStack.push(5);
}
assertEquals(callStack, [5]);
callStack.length = 0;
}

{
const ctx = testing.createMockContext({
path: mockErrorButNotValidatePath,
method: "GET",
});
const mw = router.routes();
const next = testing.createMockNext();
await mw(ctx, next);
assertEquals(callStack, [5]);
callStack.length = 0;
}
});

Deno.test("Params", async () => {
Expand Down
1 change: 1 addition & 0 deletions test_deps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export type {
} from "https://deno.land/x/[email protected]/mod.ts";

export {
IsOptional,
IsString,
Max,
Min,
Expand Down

0 comments on commit 7f55a09

Please sign in to comment.