Пошук уроків, статей та іншого контенту
Зрозумієте, як зберігати й читати метадані класів і методів для побудови декларативної логіки NestJS.
Метадані — це додаткова інформація, яку можна прив’язати до класу, методу, властивості або параметра без зміни їхньої основної логіки.
Наприклад, для методу контролера можна зберегти інформацію про ролі, які мають право його викликати:
roles: ['admin', 'manager']Сам метод не перевіряє ці ролі безпосередньо. Він лише містить метадані, а guard або інший компонент NestJS читає їх і виконує потрібну логіку.
Такий підхід називають
@Roles('admin')
@Get('reports')
getReports() {
// Логіка методу не містить перевірки ролей
}Замість імперативного коду:
@Get('reports')
getReports(@Req() request: Request) {
if (request.user.role !== 'admin') {
throw new ForbiddenException();
}
// Логіка методу
}reflect-metadataДля роботи з метаданими в TypeScript використовується пакет reflect-metadata.
Він додає методи до глобального об’єкта Reflect, зокрема:
Reflect.defineMetadata() — записує метадані;
Reflect.getMetadata() — читає метадані;
Reflect.getOwnMetadata() — читає метадані лише безпосередньо з об’єкта;
Reflect.hasMetadata() — перевіряє наявність метаданих;
Reflect.deleteMetadata() — видаляє метадані.
У NestJS пакет зазвичай імпортують на початку файлу main.ts:
import 'reflect-metadata';Пакет має бути встановлений у проєкті:
npm install reflect-metadataМетадані можна записати на клас:
import 'reflect-metadata';
class ReportsController {}
Reflect.defineMetadata(
'resource',
'reports',
ReportsController,
);У цьому прикладі:
'resource' — ключ метаданих;
'reports' — значення;
ReportsController — об’єкт, до якого прикріплено метадані.
Прочитати значення можна так:
const resource = Reflect.getMetadata(
'resource',
ReportsController,
);
console.log(resource); // reportsКлюч метаданих зазвичай зберігають у константі, щоб уникати помилок у рядках:
const RESOURCE_METADATA_KEY = 'resource';
Reflect.defineMetadata(
RESOURCE_METADATA_KEY,
'reports',
ReportsController,
);
const resource = Reflect.getMetadata(
RESOURCE_METADATA_KEY,
ReportsController,
);Метадані можна прикріпити не тільки до класу, а й до окремого методу.
import 'reflect-metadata';
class ReportsController {
getPublicReports() {
return [];
}
getPrivateReports() {
return [];
}
}
Reflect.defineMetadata(
'access',
'public',
ReportsController.prototype,
'getPublicReports',
);
Reflect.defineMetadata(
'access',
'private',
ReportsController.prototype,
'getPrivateReports',
);
const publicAccess = Reflect.getMetadata(
'access',
ReportsController.prototype,
'getPublicReports',
);
const privateAccess = Reflect.getMetadata(
'access',
ReportsController.prototype,
'getPrivateReports',
);
console.log(publicAccess); // public
console.log(privateAccess); // privateДля методу потрібно передати:
ключ метаданих;
значення;
об’єкт, якому належить метод;
назву методу.
Методи класу зберігаються на його prototype, тому для ручної роботи з метаданими методу використовується саме:
ReportsController.prototypeа не екземпляр:
const controller = new ReportsController();Ручний виклик Reflect.defineMetadata() працює, але в NestJS зручніше приховати його за власним декоратором.
Декоратор класу може виглядати так:
import 'reflect-metadata';
const RESOURCE_METADATA_KEY = 'resource';
function Resource(name: string): ClassDecorator {
return (target) => {
Reflect.defineMetadata(
RESOURCE_METADATA_KEY,
name,
target,
);
};
}
@Resource('reports')
class ReportsController {}
const resource = Reflect.getMetadata(
RESOURCE_METADATA_KEY,
ReportsController,
);
console.log(resource); // reportsТепер клас декларативно описує свій ресурс:
@Resource('reports')
class ReportsController {}Декоратор методу має іншу сигнатуру, оскільки отримує об’єкт і назву методу:
import 'reflect-metadata';
const ACCESS_METADATA_KEY = 'access';
function Access(level: string): MethodDecorator {
return (target, propertyKey) => {
Reflect.defineMetadata(
ACCESS_METADATA_KEY,
level,
target,
propertyKey,
);
};
}
class ReportsController {
@Access('public')
getPublicReports() {
return [];
}
}
const access = Reflect.getMetadata(
ACCESS_METADATA_KEY,
ReportsController.prototype,
'getPublicReports',
);
console.log(access); // publicSetMetadata у NestJSNestJS надає готовий декоратор SetMetadata, який записує метадані через механізм Reflect Metadata.
import { SetMetadata } from '@nestjs/common';
export const Roles = (...roles: string[]) =>
SetMetadata('roles', roles);Тепер цей декоратор можна застосовувати до методів контролера:
import { Controller, Get } from '@nestjs/common';
import { Roles } from './roles.decorator';
@Controller('reports')
export class ReportsController {
@Get()
@Roles('admin', 'manager')
findAll() {
return [];
}
}Виклик:
@Roles('admin', 'manager')прикріплює до методу метадані приблизно такого вигляду:
{
roles: ['admin', 'manager']
}У цьому й полягає користь метаданих: контролер описує вимоги декларативно, а окремий guard використовує ці вимоги.
ReflectorУ NestJS для читання метаданих зазвичай використовують клас Reflector з пакета @nestjs/core.
Guard отримує ExecutionContext, з якого можна дістати:
клас контролера;
метод обробника поточного запиту;
HTTP-запит.
import {
CanActivate,
ExecutionContext,
Injectable,
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private readonly reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const roles = this.reflector.get<string[]>(
'roles',
context.getHandler(),
);
if (!roles) {
return true;
}
const request = context.switchToHttp().getRequest();
const user = request.user;
return Boolean(user && roles.includes(user.role));
}
}У цьому коді:
context.getHandler()повертає метод контролера, який обробляє поточний запит.
Тому guard читає метадані, встановлені на методі:
@Roles('admin', 'manager')
findAll() {
return [];
}Якщо метаданих roles немає, guard повертає true. Це дає змогу застосовувати guard до всього контролера, але обмежувати лише окремі методи.
Метадані можна встановити як на весь контролер, так і на конкретний метод:
@Roles('user')
@Controller('reports')
export class ReportsController {
@Get()
findAll() {
return [];
}
@Get('private')
@Roles('admin')
findPrivate() {
return [];
}
}Тут:
усі методи контролера мають вимагати роль user;
метод findPrivate() має власне значення admin.
Для роботи з таким сценарієм використовується getAllAndOverride():
const roles = this.reflector.getAllAndOverride<string[]>(
'roles',
[
context.getHandler(),
context.getClass(),
],
);NestJS перевіряє об’єкти в заданому порядку:
метадані методу;
метадані класу.
Якщо метадані є на методі, вони замінюють значення класу.
Повний приклад guard і декораторів:
import {
CanActivate,
Controller,
ExecutionContext,
Get,
Injectable,
SetMetadata,
UseGuards,
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
export const Roles = (...roles: string[]) =>
SetMetadata('roles', roles);
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private readonly reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const roles = this.reflector.getAllAndOverride<string[]>(
'roles',
[
context.getHandler(),
context.getClass(),
],
);
// Якщо обмеження ролей не задано, доступ дозволено.
if (!roles) {
return true;
}
const request = context.switchToHttp().getRequest<{
user?: { role: string };
}>();
const user = request.user;
// Користувач без ролі не проходить перевірку.
if (!user) {
return false;
}
return roles.includes(user.role);
}
}
@Controller('reports')
@UseGuards(RolesGuard)
@Roles('user')
export class ReportsController {
@Get()
findAll() {
return ['public report'];
}
@Get('private')
@Roles('admin')
findPrivate() {
return ['private report'];
}
}У цьому прикладі:
RolesGuard застосовується до всього контролера;
контролер за замовчуванням вимагає роль user;
findPrivate() перевизначає це правило і вимагає роль admin;
getAllAndOverride() вибирає найбільш конкретне значення.
Щоб приклад працював у NestJS-модулі, RolesGuard потрібно зареєструвати як provider:
import { Module } from '@nestjs/common';
@Module({
controllers: [ReportsController],
providers: [RolesGuard],
})
export class ReportsModule {}ReflectorgetЧитає метадані з одного об’єкта:
const roles = this.reflector.get<string[]>(
'roles',
context.getHandler(),
);Цей варіант підходить, коли метадані встановлюються лише на методі або лише на класі.
getAllAndOverrideПовертає перше знайдене значення з переданого списку:
const roles = this.reflector.getAllAndOverride<string[]>(
'roles',
[
context.getHandler(),
context.getClass(),
],
);Цей варіант зручний для правил, де метод може перевизначати налаштування класу.
getAllAndMergeОб’єднує значення метаданих з усіх переданих об’єктів:
const permissions =
this.reflector.getAllAndMerge<string[]>(
'permissions',
[
context.getHandler(),
context.getClass(),
],
);Якщо клас містить:
@Permissions('reports:read')а метод:
@Permissions('reports:export')результатом буде об’єднаний список:
['reports:export', 'reports:read']getAllAndMerge() доречний, коли правила класу та методу мають накопичуватися, а не замінювати одне одного.
getMetadata і getOwnMetadataReflect.getMetadata() може шукати метадані в ланцюжку прототипів.
import 'reflect-metadata';
class BaseController {}
Reflect.defineMetadata(
'scope',
'base',
BaseController,
);
class ReportsController extends BaseController {}
const scope = Reflect.getMetadata(
'scope',
ReportsController,
);
console.log(scope); // baseReportsController не має власного метаданого scope, але успадковує його від BaseController.
Reflect.getOwnMetadata() перевіряє лише сам об’єкт:
const scope = Reflect.getOwnMetadata(
'scope',
ReportsController,
);
console.log(scope); // undefinedУ NestJS для більшості типових задач варто користуватися Reflector, оскільки він краще відповідає структурі контролерів, методів і ExecutionContext.
SetMetadataКраще не використовувати ключі метаданих безпосередньо в кожному контролері:
SetMetadata('roles', ['admin']);Замість цього створюють власний декларативний декоратор:
export const Roles = (...roles: string[]) =>
SetMetadata('roles', roles);Тоді код контролера стає зрозумілішим:
@Roles('admin')
@Get('settings')
getSettings() {
return {};
}А ключ 'roles' залишається в одному місці — у файлі декоратора або спільних констант.
Це також зменшує ризик помилки, коли в одному місці випадково використовується 'role', а в іншому — 'roles'.
emitDecoratorMetadataTypeScript може генерувати метадані про типи параметрів і властивостей за допомогою опції:
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}Для роботи декораторів також потрібна опція:
{
"compilerOptions": {
"experimentalDecorators": true
}
}NestJS використовує таку інформацію, зокрема, для аналізу залежностей у конструкторах:
@Injectable()
export class ReportsService {
constructor(
private readonly repository: ReportsRepository,
) {}
}Важливо розрізняти:
experimentalDecorators дозволяє використовувати декоратори;
emitDecoratorMetadata додає метадані про типи;
reflect-metadata надає механізм збереження та читання метаданих під час виконання.
Метадані типів не замінюють явні власні метадані на кшталт:
@Roles('admin')Це різні механізми з різним призначенням.
reflect-metadataЯкщо метадані читаються або записуються до ініціалізації потрібного polyfill, можуть виникати помилки під час виконання.
Додайте імпорт на початку точки входу застосунку:
import 'reflect-metadata';Метадані методу потрібно читати з самого методу:
Reflect.getMetadata(
'roles',
context.getHandler(),
);А метадані класу — з класу:
Reflect.getMetadata(
'roles',
context.getClass(),
);Якщо передати клас замість методу або навпаки, значення може бути undefined.
Ці ключі є різними:
SetMetadata('roles', ['admin']);
Reflect.getMetadata('role', target);Значення не буде знайдено, оскільки ключі не збігаються.
undefinedМетадані можуть бути не встановлені:
const roles = this.reflector.get<string[]>(
'roles',
context.getHandler(),
);Тому потрібно обробляти випадок, коли roles дорівнює undefined:
if (!roles) {
return true;
}Якщо метод має повністю замінювати правило класу, використовуйте:
getAllAndOverride()Якщо правила класу та методу потрібно об’єднати, використовуйте:
getAllAndMerge()Метадані — це додаткова інформація, прив’язана до класу або методу.
reflect-metadata надає API для запису й читання метаданих.
Reflect.defineMetadata() записує значення, а Reflect.getMetadata() його читає.
У NestJS для роботи з метаданими зазвичай використовують SetMetadata і Reflector.
Власні декоратори на кшталт @Roles() роблять код контролерів декларативним.
context.getHandler() повертає метод обробника, а context.getClass() — клас контролера.
getAllAndOverride() використовують для перевизначення метаданих методу над метаданими класу.
getAllAndMerge() використовують для об’єднання метаданих класу та методу.
Метадані самі по собі не виконують логіку — їх читають guards, interceptors, pipes або інші компоненти NestJS.