Skip to Content

MetadataHandler

Возьмём пример из feeldown-backend (ссылка вне зоны доступа).

В проекте аутентификация построена на AuthGuard, который перед проверкой токена сначала проверяет метаданные эндпоинта — есть ли декораторы @Public() или @SkipAuthGuard(). Эта логика вынесена в отдельную цепочку обработчиков MetadataHandler, что делает код открытым для расширения и закрытым для изменения — идеальное соблюдение OCP.


Код

MetadataHandler — диспетчер цепочки

import type { MetadataHandlerType } from "@1/types"; import type { ExecutionContext } from "@nestjs/common"; import { Injectable } from "@nestjs/common"; @Injectable() export class MetadataHandler implements MetadataHandlerType { private readonly _handlers: MetadataHandlerType[] = []; public constructor() {} public apply(...handlers: MetadataHandlerType[]) { this._handlers.push(...handlers); return this; } public async execute(context: ExecutionContext) { for (const handler of this._handlers) { const validated = await handler.execute(context); if (validated) { return true; } } return false; } }

Обработчики метаданных

import type { ExecutionContext } from "@nestjs/common"; import { AbstractMetadataHandler } from "./abstract-metadata-handler"; import { Injectable } from "@nestjs/common"; import { Metadata } from "@/enums"; @Injectable() export class PublicHandler extends AbstractMetadataHandler { public async execute(context: ExecutionContext): Promise<boolean> { const isPublic = this.get<boolean>(context, Metadata.isPublic); return isPublic; } }
import { AbstractMetadataHandler } from "./abstract-metadata-handler"; import { ExecutionContext } from "@nestjs/common"; import { Metadata } from "@/enums"; export class SkipAuthGuardHandler extends AbstractMetadataHandler { public async execute(context: ExecutionContext) { const skipAuthGuard = this.get<boolean>(context, Metadata.skipAuthGuard); return skipAuthGuard; } }

AuthGuard — не меняется

@Injectable() export class AuthGuard implements CanActivate { public constructor( private readonly service: AuthGuardService, private readonly metadataHandler: MetadataHandler, private readonly publicHandler: PublicHandler, private readonly skipAuthGuardHandler: SkipAuthGuardHandler, ) {} public async canActivate(context: ExecutionContext): Promise<boolean> { const metadataValidated = await this.validateMetadata(context); if (metadataValidated) { return true; } const request = context.switchToHttp().getRequest<Request>(); const validate = () => this.service.validateRequest(request); const validated = tryCatchThrow(validate); return validated; } private async validateMetadata(context: ExecutionContext) { return this.metadataHandler .apply(this.publicHandler, this.skipAuthGuardHandler) .execute(context); } }

Что хорошо?

OCP в действии

  • AuthGuard вызывает metadataHandler.apply(...).execute().
  • В apply передаются конкретные обработчики.
  • Если появляется новый тип метаданных, мы создаём новый обработчик, но не меняем AuthGuard — он остаётся закрытым для изменений.

Пример расширения: добавляем @Roles()

Допустим, мы хотим добавить проверку ролей. Вместо того чтобы добавлять RolesHandler в AuthGuard, мы создаём отдельный Guard — RolesGuard. Это соблюдает SRP (аутентификация и авторизация — разные ответственности) и OCP (AuthGuard не меняется).

1. Создаём декоратор @Roles

import { SetMetadata } from "@nestjs/common"; import { Metadata } from "@/enums"; export const Roles = (...roles: string[]) => SetMetadata(Metadata.roles, roles);

2. Добавляем новое значение в Metadata enum

export const enum Metadata { isPublic = "isPublic", isOnlyMe = "isOnlyMe", skipAuthGuard = "skipAuthGuard", cacheDisabled = "cacheDisabled", roles = "roles", }

3. Создаём обработчик RolesHandler

import type { ExecutionContext } from "@nestjs/common"; import { AbstractMetadataHandler } from "./abstract-metadata-handler"; import { Injectable } from "@nestjs/common"; import { Metadata } from "@/enums"; import { ServerUserService } from "@1/services"; @Injectable() export class RolesHandler extends AbstractMetadataHandler { public constructor( protected readonly reflector: Reflector, private readonly serverUserService: ServerUserService, ) { super(reflector); } public async execute(context: ExecutionContext): Promise<boolean> { const requiredRoles = this.get<string[]>(context, Metadata.roles); if (!requiredRoles || requiredRoles.length === 0) { return false; } const request = context.switchToHttp().getRequest(); const user = await this.serverUserService.getByRequestOrThrow(request); // Предположим, что у пользователя есть поле `role` const hasRole = requiredRoles.includes(user.user.role); return hasRole; } }

4. Создаём отдельный RolesGuard, который использует MetadataHandler

import type { CanActivate, ExecutionContext } from "@nestjs/common"; import { Injectable } from "@nestjs/common"; import { MetadataHandler } from "@1/handlers"; import { RolesHandler } from "./roles.handler"; @Injectable() export class RolesGuard implements CanActivate { public constructor( private readonly metadataHandler: MetadataHandler, private readonly rolesHandler: RolesHandler, ) {} public async canActivate(context: ExecutionContext): Promise<boolean> { return this.metadataHandler.apply(this.rolesHandler).execute(context); } }

5. Используем оба Guard’а в контроллере

@UseGuards(AuthGuard, RolesGuard) @Delete('/users/:id') public deleteUser(@Param('id') id: string) { // сначала аутентификация (AuthGuard), потом авторизация (RolesGuard) }

Что мы получили?

  • AuthGuard остался нетронутым — он проверяет только аутентификацию.
  • RolesGuard отвечает только за проверку ролей.
  • MetadataHandler переиспользуется в обоих Guard’ах.
  • Новый функционал добавлен через новые классы, без изменения существующего кода.

Это идеальное соблюдение OCP и SRP.


Итог

MetadataHandler — это эталонный пример Open-Closed Principle:

  • Код открыт для расширения через добавление новых обработчиков.
  • Код закрыт для изменения — существующие классы не трогаются при добавлении новой функциональности.

Запомните: Если вы видите цепочки if/else по типу метаданных — это признак нарушения OCP. Вместо этого используйте цепочку обработчиков, как в MetadataHandler. И не забывайте разделять аутентификацию и авторизацию на разные Guard’ы — так вы соблюдёте и SRP, и OCP.


Исходный код доступен в репозитории feeldown-backend.

Last updated on