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.