[Node.JS, TypeScript] Кастомные декораторы для NestJS: от простого к сложному
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
оригинал
Введение
NestJS — стремительно набирающий популярность фрeймворк, построенный на идеях IoC/DI, модульного дизайна и декораторов. Благодаря последним, Nest имеет лаконичный и выразительный синтаксис, что повышает удобство разработки.
Декораторы или аннотации — наследники аспектов, которые позволяют декларативно описывать логику, модифицировать поведение классов, их свойств, аргументов и методов.
Технически декораторы — это просто функции, но их вызовом полностью управляет компилятор.
Важная особенность заключается в том, что в зависимости от контекста, сигнатуры аргументов будут различаться. Материалов на эту тему существует довольно много, однако мы сосредоточимся на специфике, связанной непосредственно с 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 адаптируются к широкому спектру прикладных задач. В них легко переносится утилитная и бизнесовая логика. Их несложно расширять, композировать, совмещая несколько сценариев. И в этом, без сомнения, одна из сильных сторон фреймворка.
Однако важно помнить, что синтаксис декораторов сегодня все еще является экспериментальным, а их чрезмерное использование может дать обратный эффект, и сделать ваш код более запутанным.
===========
Источник:
habr.com
===========
Похожие новости:
- [Разработка веб-сайтов, JavaScript, Node.JS] Установка и обновление зависимостей в JavaScript
- [Node.JS] Пишем телеграм бота на node.js
- [JavaScript, ReactJS, TypeScript] Todolist на React Hooks + TypeScript: от сборки до тестирования
- [Разработка веб-сайтов, JavaScript, Node.JS] Управление зависимостями JavaScript
- [Google Chrome, JavaScript, Node.JS] Как обойти запрет доступа к страницам с помощью Chrome в headless-режиме (перевод)
- [JavaScript, Node.JS, ReactJS] Redux store vs React state
- [JavaScript, TypeScript, Отладка] Source Maps: быстро и понятно
- [API, Node.JS, TypeScript] NodeJS Бот для Телеграм с обработкой математических выражений
- [Angular, JavaScript, Open source, TypeScript] Лабаем на MIDI клавиатуре в Angular
- [Децентрализованные сети, JavaScript, Node.JS, Распределённые системы] Spreadable — вариант децентрализованной сети
Теги для поиска: #_node.js, #_typescript, #_typescript, #_decorator, #_metadata, #_nestjs, #_blog_kompanii_qiwi (
Блог компании QIWI
), #_node.js, #_typescript
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 23:41
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
оригинал Введение NestJS — стремительно набирающий популярность фрeймворк, построенный на идеях IoC/DI, модульного дизайна и декораторов. Благодаря последним, Nest имеет лаконичный и выразительный синтаксис, что повышает удобство разработки. Декораторы или аннотации — наследники аспектов, которые позволяют декларативно описывать логику, модифицировать поведение классов, их свойств, аргументов и методов. Технически декораторы — это просто функции, но их вызовом полностью управляет компилятор. Важная особенность заключается в том, что в зависимости от контекста, сигнатуры аргументов будут различаться. Материалов на эту тему существует довольно много, однако мы сосредоточимся на специфике, связанной непосредственно с 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 адаптируются к широкому спектру прикладных задач. В них легко переносится утилитная и бизнесовая логика. Их несложно расширять, композировать, совмещая несколько сценариев. И в этом, без сомнения, одна из сильных сторон фреймворка. Однако важно помнить, что синтаксис декораторов сегодня все еще является экспериментальным, а их чрезмерное использование может дать обратный эффект, и сделать ваш код более запутанным. =========== Источник: habr.com =========== Похожие новости:
Блог компании QIWI ), #_node.js, #_typescript |
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 23:41
Часовой пояс: UTC + 5