Décorateurs personnalisés pour NestJS: du simple au complexe

image



introduction



NestJS est un cadre de plus en plus populaire basé sur des idées d'IoC / DI, de conception modulaire et de décorateurs. Grâce à ce dernier, Nest dispose d'une syntaxe concise et expressive, ce qui améliore la convivialité du développement.



Les décorateurs ou annotations sont des héritiers d' aspects qui vous permettent de décrire de manière déclarative la logique, de modifier le comportement des classes, leurs propriétés, arguments et méthodes.



— , .

, , . , , Nest.





http-. , , . Nest .



Guard — , CanActivate @UseGuard.



@Injectable()
export class RoleGuard implements CanActivate {
  canActivate(
    context: ExecutionContext,
  ): boolean | Promise<boolean> | Observable<boolean> {
    const request = context.switchToHttp().getRequest();
    return getRole(request) === 'superuser'
  }
}

@Controller()
export class MyController {
  @Post('secure-path')
  @UseGuards(RoleGuard)
  async method() {
    return
  }
}


superuser — , .



Nest @SetMetadata. , — .



Reflector, reflect-metadata.



@Injectable()
export class RoleGuard implements CanActivate {
  constructor(private reflector: Reflector) {}
  canActivate(
    context: ExecutionContext,
  ): boolean | Promise<boolean> | Observable<boolean> {
    const role = this.reflector.get<string>('role', context.getHandler());
    const request = context.switchToHttp().getRequest();
    return getRole(request) === role
  }
}

@Controller()
export class MyController {
  @Post('secure-path')
  @SetMetadata('role', 'superuser')
  @UseGuards(RoleGuard)
  async test() {
    return
  }
}




.



- -. .



applyDecorators.



const Role = (role) => applyDecorators(UseGuards(RoleGuard), SetMetadata('role', role))


:



const Role = role => (proto, propName, descriptor) => {
  UseGuards(RoleGuard)(proto, propName, descriptor)
  SetMetadata('role', role)(proto, propName, descriptor)
}

@Controller()
export class MyController {
  @Post('secure-path')
  @Role('superuser')
  async test() {
    return
  }
}




, .



@Controller()
@UseGuards(RoleGuard)
export class MyController {
  @Post('secure-path')
  @Role('superuser')
  async test1() {
    return
  }

  @Post('almost-securest-path')
  @Role('superuser')
  async test2() {
    return
  }

  @Post('securest-path')
  @Role('superuser')
  async test3() {
    return
  }
}


, . , , -.



— — .



typescript , .



type ClassDecorator = <TFunction extends Function>(target: TFunction) => TFunction | void;
type MethodDecorator = <T>(target: Object, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor<T>) => TypedPropertyDescriptor<T> | void;
type ParameterDecorator = (target: Object, propertyKey: string | symbol, parameterIndex: number) => void;

const Role = (role: string): MethodDecorator | ClassDecorator => (...args) => {
  if (typeof args[0] === 'function') {
    //  
    const ctor = args[0]
    //  
    const proto = ctor.prototype
    //  
    const methods = Object
      .getOwnPropertyNames(proto)
      .filter(prop => prop !== 'constructor')

    //    
    methods.forEach((propName) => {
      RoleMethodDecorator(
        proto,
        propName,
        Object.getOwnPropertyDescriptor(proto, propName),
        role,
      )
    })
  } else {
    const [proto, propName, descriptor] = args
    RoleMethodDecorator(proto, propName, descriptor, role)
  }
}


, : lukehorvat/decorator-utils, qiwi/decorator-utils.

.



import { constructDecorator, CLASS, METHOD } from '@qiwi/decorator-utils'

const Role = constructDecorator(
  ({ targetType, descriptor, proto, propName, args: [role] }) => {
    if (targetType === METHOD) {
      RoleMethodDecorator(proto, propName, descriptor, role)
    }

    if (targetType === CLASS) {
      const methods = Object.getOwnPropertyNames(proto)
      methods.forEach((propName) => {
        RoleMethodDecorator(
          proto,
          propName,
          Object.getOwnPropertyDescriptor(proto, propName),
          role,
        )
      })
    }
  },
)


:

@DecForClass, @DecForMethood, @DecForParam @Dec.



, , - , @Role.



.

, createParamDecorator .



/ ( ParamsTokenFactory RouterExecutionContext).



//  
  if (typeof args[2] === 'number') {
    const [proto, propName, paramIndex] = args
    createParamDecorator((_data: unknown, ctx: ExecutionContext) => {
      return getRole(ctx.switchToHttp().getRequest())
    })()(proto, propName, paramIndex)
  }


, , , .



, , . ?



. , , .



class SomeController {
   @RequestSize(1000)
   @RequestSize(5000)
   @Post('foo')
   method(@Body() body) {
   }
}


: . , , , .



class SomeController {
   @Port(9092)
   @Port(8080)
   @Post('foo')
   method(@Body() body) {
   }
}


.



class SomeController {
  @Post('securest-path')
  @Role('superuser')
  @Role('usert')
  @Role('otheruser')
  method(@Role() role) {

  }
}


, reflect-metadata :



import { ExecutionContext, createParamDecorator } from '@nestjs/common'
import { constructDecorator, METHOD, PARAM } from '@qiwi/decorator-utils'

@Injectable()
export class RoleGuard implements CanActivate {
  canActivate(context: ExecutionContext): boolean | Promise<boolean> {
    const roleMetadata = Reflect.getMetadata(
      'roleMetadata',
      context.getClass().prototype,
    )
    const request = context.switchToHttp().getRequest()
    const role = getRole(request)
    return roleMetadata.find(({ value }) => value === role)
  }
}

const RoleMethodDecorator = (proto, propName, decsriptor, role) => {
  UseGuards(RoleGuard)(proto, propName, decsriptor)
  const meta = Reflect.getMetadata('roleMetadata', proto) || []

  Reflect.defineMetadata(
    'roleMetadata',
    [
      ...meta, {
        repeatable: true,
        value: role,
      },
    ],
    proto,
  )
}

export const Role = constructDecorator(
  ({ targetType, descriptor, proto, propName, paramIndex, args: [role] }) => {
    if (targetType === METHOD) {
      RoleMethodDecorator(proto, propName, descriptor, role)
    }

    if (targetType === PARAM) {
      createParamDecorator((_data: unknown, ctx: ExecutionContext) =>
        getRole(ctx.switchToHttp().getRequest()),
      )()(proto, propName, paramIndex)
    }
  },
)




Nest , . , , , . , @Controller «»

JSON-RPC.

, , : , Nest.



import {
  ControllerOptions,
  Controller,
  Post,
  Req,
  Res,
  HttpCode,
  HttpStatus,
} from '@nestjs/common'

import { Request, Response } from 'express'
import { Extender } from '@qiwi/json-rpc-common'
import { JsonRpcMiddleware } from 'expressjs-json-rpc'

export const JsonRpcController = (
  prefixOrOptions?: string | ControllerOptions,
): ClassDecorator => {
  return <TFunction extends Function>(target: TFunction) => {
    const extend: Extender = (base) => {
      @Controller(prefixOrOptions as any)
      @JsonRpcMiddleware()
      class Extended extends base {
        @Post('/')
        @HttpCode(HttpStatus.OK)
        rpc(@Req() req: Request, @Res() res: Response): any {
          return this.middleware(req, res)
        }
      }

      return Extended
    }

    return extend(target as any)
  }
}


@Req() rpc-method , , @JsonRpcMethod.



, :



import {
  JsonRpcController,
  JsonRpcMethod,
  IJsonRpcId,
  IJsonRpcParams,
} from 'nestjs-json-rpc'

@JsonRpcController('/jsonrpc/endpoint')
export class SomeJsonRpcController {
  @JsonRpcMethod('some-method')
  doSomething(
    @JsonRpcId() id: IJsonRpcId,
    @JsonRpcParams() params: IJsonRpcParams,
  ) {
    const { foo } = params

    if (foo === 'bar') {
      return new JsonRpcError(-100, '"foo" param should not be equal "bar"')
    }

    return 'ok'
  }
  @JsonRpcMethod('other-method')
  doElse(@JsonRpcId() id: IJsonRpcId) {
    return 'ok'
  }
}




Nest . . , , . , , .



, , , .




All Articles