diff --git a/server/README.md b/server/README.md index 440cb11..e01b57f 100644 --- a/server/README.md +++ b/server/README.md @@ -141,11 +141,11 @@ module.set('views', join(__dirname, '/views')); ``` ### Parameter -* `@Body(paramName?: string)` - Request body object or single body param -* `@Cookies(paramName?: string)` - Request cookies or single cookies param -* `@Headers(paramName?: string)` - Request headers object or single headers param -* `@Params(paramName?: string)` - Request params object or single param -* `@Query(paramName?: string)` - Request query object or single query param +* `@Body(paramName?: string, paramValidator?: Validator)` - Request body object or single body param +* `@Cookies(paramName?: string, paramValidator?: Validator)` - Request cookies or single cookies param +* `@Headers(paramName?: string, paramValidator?: Validator)` - Request headers object or single headers param +* `@Params(paramName?: string, paramValidator?: Validator)` - Request params object or single param +* `@Query(paramName?: string, paramValidator?: Validator)` - Request query object or single query param * `@Request(paramName?: string)` - Returns request object or any other object available in req object itself * `@Response(paramName?: string)` - Returns response object or any other object available in response object itself diff --git a/server/example/modules/posts/decorators/access-param.ts b/server/example/modules/posts/decorators/access-param.ts index 6684261..4469191 100644 --- a/server/example/modules/posts/decorators/access-param.ts +++ b/server/example/modules/posts/decorators/access-param.ts @@ -6,8 +6,6 @@ export function AccessParam() { return createParamDecorator((context: HttpContext) => { const req = context.getRequest(); - req['user'] = { id: 1, name: 'John Doe' }; - - return req['user']; + return req.query.access; }); } diff --git a/server/example/modules/posts/posts.controller.ts b/server/example/modules/posts/posts.controller.ts index 6c8b4dc..92b5f04 100644 --- a/server/example/modules/posts/posts.controller.ts +++ b/server/example/modules/posts/posts.controller.ts @@ -11,11 +11,6 @@ class PostType { title: string; } -class User { - id: number; - name: string; -} - @Controller() export class PostsController { constructor(private postsService: PostsService) { } @@ -31,7 +26,7 @@ export class PostsController { @Pipe(AccessPipe) @Get(':id', 200) @Render('post') - post(@Params('id') id: string, @AccessParam() access: User) { + post(@Params('id', Number) id: number, @AccessParam() access: string) { return { access, id }; } } diff --git a/server/integration/http/params/src/app.controller.ts b/server/integration/http/params/src/app.controller.ts index 32f7337..c1c7579 100644 --- a/server/integration/http/params/src/app.controller.ts +++ b/server/integration/http/params/src/app.controller.ts @@ -66,9 +66,9 @@ export class AppController { return body; } - @Post('with-simple-validator/:example') - withSimpleValidator( - @Params('example') param: string, + @Post('with-custom-validator') + withCustomValidator( + @Body('example', (arg: unknown) => typeof arg === 'string') param: string, ) { return param; } diff --git a/server/integration/http/params/test/express.spec.ts b/server/integration/http/params/test/express.spec.ts index f58fea8..6502810 100644 --- a/server/integration/http/params/test/express.spec.ts +++ b/server/integration/http/params/test/express.spec.ts @@ -95,17 +95,21 @@ describe('Express Params', () => { }); }); - describe('with simple validator', () => { + describe('with custom validator', () => { it('passes validation', () => { return request(module.getHttpServer()) - .post('/with-simple-validator/example') - .expect('example'); + .post('/with-custom-validator') + .send({ example: 'param' }) + .expect('param'); }); it('fails validation', () => { return request(module.getHttpServer()) - .post('/with-simple-validator/123') - .expect(({ body }) => expect(body.message).toBeDefined()); + .post('/with-custom-validator') + .send({ example: 100 }) + .expect(({ body }) => { + expect(body.message).toBeDefined(); + }); }); }); }); diff --git a/server/package-lock.json b/server/package-lock.json index fbf7211..f9d40d8 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -1,12 +1,12 @@ { "name": "@decorators/server", - "version": "1.0.0-beta.7", + "version": "1.0.0-beta.8", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@decorators/server", - "version": "1.0.0-beta.7", + "version": "1.0.0-beta.8", "license": "MIT", "devDependencies": { "@decorators/di": "../di", diff --git a/server/package.json b/server/package.json index f72e380..493329d 100644 --- a/server/package.json +++ b/server/package.json @@ -69,5 +69,5 @@ ] } }, - "version": "1.0.0-beta.7" + "version": "1.0.0-beta.8" } diff --git a/server/src/core/helpers/decorators.ts b/server/src/core/helpers/decorators.ts index e0b0195..fa2ab4f 100644 --- a/server/src/core/helpers/decorators.ts +++ b/server/src/core/helpers/decorators.ts @@ -5,10 +5,10 @@ import { Context } from './context'; export function paramDecoratorFactory(metadata: object) { return function (target: InstanceType, methodName: string, index: number) { const params = Reflect.getMetadata(PARAMS_METADATA, target.constructor) ?? []; - const validator = Reflect.getMetadata(PARAM_TYPE_METADATA, target, methodName)[index]; + const argType = Reflect.getMetadata(PARAM_TYPE_METADATA, target, methodName)[index]; const argName = extractParamNames(target[methodName])[index]; - params.push({ argName, index, methodName, validator, ...metadata }); + params.push({ argName, argType, index, methodName, ...metadata }); Reflect.defineMetadata(PARAMS_METADATA, params, target.constructor); }; @@ -45,7 +45,7 @@ export function methodDecoratorFactory(metadata: object) { * authorize(@AccessParam() access: string) * ... */ -export function createParamDecorator(factory: (context: Context) => unknown) { +export function createParamDecorator(factory: (context: Context) => Promise | any) { return paramDecoratorFactory({ factory }); } diff --git a/server/src/core/helpers/param-validator.ts b/server/src/core/helpers/param-validator.ts index 9749ff6..b123033 100644 --- a/server/src/core/helpers/param-validator.ts +++ b/server/src/core/helpers/param-validator.ts @@ -3,41 +3,23 @@ import { plainToInstance } from 'class-transformer'; import { getMetadataStorage, validate } from 'class-validator'; import { ClassConstructor, Handler, ParamMetadata } from '../types'; +import { isClass, isFunction } from '../utils'; import { BadRequestError } from './errors'; @Injectable() export class ParamValidator { async validate(params: ParamMetadata[], args: any[]) { for (const [i, arg] of args.entries()) { - const type = params[i].validator; + const validator = params[i].paramValidator; + const type = isClass(validator) ? validator : params[i].argType; - if (!type) { - continue; + if (isFunction(validator)) { + await this.useFunctionValidator(params[i], arg); } - if (this.validateSimple(arg, type as Handler)) { - continue; - } - - const instance = plainToInstance(type as ClassConstructor, arg); - if (this.hasDecorators(type)) { - const errors = await validate(instance, { validationError: { target: false } }); - - if (errors.length) { - throw new BadRequestError(`Invalid param “${params[i].argName}”`, errors); - } - - continue; - } - - if (instance instanceof type) { - continue; + await this.useMetadataValidator(type as ClassConstructor, params[i], arg); } - - throw new BadRequestError( - `Invalid param “${params[i].argName}”. “${params[i].validator.name}” expected, “${typeof arg}” received`, - ); } } @@ -48,11 +30,25 @@ export class ParamValidator { return metadatas.length > 0; } - private validateSimple(arg: unknown, Type: Handler) { - try { - return typeof arg === typeof Type(); - } catch { - return false; + private async useFunctionValidator(meta: ParamMetadata, arg: any) { + if (await (meta.paramValidator as Handler)(arg)) { + return; + } + + throw new BadRequestError( + `Invalid param "${meta.argName}". "${typeof arg}" received`, + ); + } + + private async useMetadataValidator(type: ClassConstructor, meta: ParamMetadata, arg: any) { + const instance = plainToInstance(type, arg); + + if (this.hasDecorators(type)) { + const errors = await validate(instance, { validationError: { target: false } }); + + if (errors.length) { + throw new BadRequestError(`Invalid param "${meta.argName}".`, errors); + } } } } diff --git a/server/src/core/types.ts b/server/src/core/types.ts index f7f3429..ad75756 100644 --- a/server/src/core/types.ts +++ b/server/src/core/types.ts @@ -26,13 +26,17 @@ export interface MethodMetadata { url: string; } +export type ValidatorFn = (arg: any) => Promise | boolean; +export type Validator = Handler | ClassConstructor | ValidatorFn; + export interface ParamMetadata { // argument name defined in the function argName?: string; - factory?: (context: any) => Promise | unknown; + argType?: Handler | ClassConstructor; + factory?: (context: any) => Promise | any; index: number; methodName: string; paramName: string; paramType: string; - validator?: Handler | ClassConstructor; + paramValidator?: Validator; } diff --git a/server/src/core/utils.ts b/server/src/core/utils.ts index 297bbcb..917108d 100644 --- a/server/src/core/utils.ts +++ b/server/src/core/utils.ts @@ -1,4 +1,4 @@ -import { Handler } from './types'; +import { ClassConstructor, Handler } from './types'; export function addLeadingSlash(url: string): string { if (url.startsWith('/')) { @@ -24,8 +24,8 @@ export function toStandardType(param: unknown) { return param === 'true'; } - if (!isNaN(Number(param))) { - return Number(param); + if (!isNaN(Number(param)) && !isNaN(parseFloat(param as string))) { + return parseFloat(param as string); } return param; @@ -37,3 +37,11 @@ export function extractParamNames(handler: Handler) { .split(',') .map(key => key.trim()); } + +export function isClass(type: Handler | ClassConstructor) { + return typeof type === 'function' && type.toString().startsWith('class'); +} + +export function isFunction(type: Handler | ClassConstructor) { + return typeof type === 'function' && !isClass(type); +} diff --git a/server/src/platforms/http/decorators/params.ts b/server/src/platforms/http/decorators/params.ts index cbfec9b..3909378 100644 --- a/server/src/platforms/http/decorators/params.ts +++ b/server/src/platforms/http/decorators/params.ts @@ -1,30 +1,56 @@ -import { paramDecoratorFactory } from '../../../core'; +import { paramDecoratorFactory, Validator } from '../../../core'; import { ParameterType } from '../helpers'; -export function Body(paramName?: string) { - return paramDecoratorFactory({ paramName, paramType: ParameterType.BODY }); +export function Body(paramName?: string, paramValidator?: Validator) { + return paramDecoratorFactory({ + paramName, + paramType: ParameterType.BODY, + paramValidator, + }); } -export function Cookies(paramName?: string) { - return paramDecoratorFactory({ paramName, paramType: ParameterType.COOKIE }); +export function Cookies(paramName?: string, paramValidator?: Validator) { + return paramDecoratorFactory({ + paramName, + paramType: ParameterType.COOKIE, + paramValidator, + }); } -export function Headers(paramName?: string) { - return paramDecoratorFactory({ paramName, paramType: ParameterType.HEADER }); +export function Headers(paramName?: string, paramValidator?: Validator) { + return paramDecoratorFactory({ + paramName, + paramType: ParameterType.HEADER, + paramValidator, + }); } -export function Params(paramName?: string) { - return paramDecoratorFactory({ paramName, paramType: ParameterType.PARAM }); +export function Params(paramName?: string, paramValidator?: Validator) { + return paramDecoratorFactory({ + paramName, + paramType: ParameterType.PARAM, + paramValidator, + }); } -export function Query(paramName?: string) { - return paramDecoratorFactory({ paramName, paramType: ParameterType.QUERY }); +export function Query(paramName?: string, paramValidator?: Validator) { + return paramDecoratorFactory({ + paramName, + paramType: ParameterType.QUERY, + paramValidator, + }); } export function Request(paramName?: string) { - return paramDecoratorFactory({ paramName, paramType: ParameterType.REQUEST }); + return paramDecoratorFactory({ + paramName, + paramType: ParameterType.REQUEST, + }); } export function Response(paramName?: string) { - return paramDecoratorFactory({ paramName, paramType: ParameterType.RESPONSE }); + return paramDecoratorFactory({ + paramName, + paramType: ParameterType.RESPONSE, + }); }