Nest cung cấp sẵn một số utility class giúp bạn dễ dàng viết ứng dụng hoạt động được trên nhiều loại application context khác nhau (ví dụ: HTTP server, microservices và WebSockets). Các utility này cung cấp thông tin về execution context (ngữ cảnh thực thi) hiện tại, dùng để xây dựng các guard, filter, interceptor tổng quát, chạy được trên nhiều controller, method và execution context khác nhau.
Chương này đề cập tới hai class như vậy: ArgumentsHost và ExecutionContext.
Ghi chú người dịch Đừng nhầm “execution context” ở đây với context.Context trong Go — cả hai đều mang nghĩa “gói thông tin về một lượt xử lý đang diễn ra”, nhưng ở Nest nó là một object cụ thể (ArgumentsHost/ExecutionContext) được framework tạo ra và truyền vào guard/filter/interceptor, chứ không phải thứ bạn tự truyền qua các tầng gọi hàm như ở Go.
ArgumentsHost class
Class ArgumentsHost cung cấp các phương thức để lấy ra các argument đang được truyền cho một handler. Nó cho phép bạn chọn đúng context (ví dụ: HTTP, RPC (microservice), hoặc WebSockets) để lấy argument từ đó. Framework sẽ cung cấp sẵn một instance của ArgumentsHost, thường được tham chiếu bằng tham số tên host, ở những nơi bạn có thể cần dùng đến nó. Ví dụ, phương thức catch() của một exception filter được gọi với một instance ArgumentsHost.
ArgumentsHost đơn giản là một lớp trừu tượng (abstraction) bọc quanh các argument của handler. Ví dụ, với ứng dụng HTTP server (khi dùng @nestjs/platform-express), object host bọc lấy mảng [request, response, next] của Express, trong đó request là request object, response là response object, và next là hàm điều khiển request-response cycle của ứng dụng. Còn với ứng dụng GraphQL, object host chứa mảng [root, args, context, info].
Current application context (Ngữ cảnh ứng dụng hiện tại)
Khi xây dựng các guard, filter, interceptor tổng quát, nhằm chạy được trên nhiều application context khác nhau, ta cần một cách để xác định loại ứng dụng mà method của mình đang chạy trong đó. Dùng phương thức getType() của ArgumentsHost để làm việc này:
if (host.getType() === 'http') {
// do something that is only important in the context of regular HTTP requests (REST)
} else if (host.getType() === 'rpc') {
// do something that is only important in the context of Microservice requests
} else if (host.getType<GqlContextType>() === 'graphql') {
// do something that is only important in the context of GraphQL requests
}
Gợi ý GqlContextType được import từ package @nestjs/graphql.
Khi đã biết được loại ứng dụng, ta có thể viết các component tổng quát hơn, như ví dụ dưới đây.
Host handler arguments (Argument của handler thông qua host)
Để lấy ra mảng argument đang được truyền cho handler, một cách là dùng phương thức getArgs() của object host.
const [req, res, next] = host.getArgs();
Bạn có thể lấy một argument cụ thể theo index bằng phương thức getArgByIndex():
const request = host.getArgByIndex(0);
const response = host.getArgByIndex(1);
Trong các ví dụ trên, ta lấy request và response object bằng index, cách này thường không được khuyến khích vì nó làm ứng dụng bị “dính chặt” vào một execution context cụ thể. Thay vào đó, bạn có thể làm code robust và tái sử dụng tốt hơn bằng cách dùng một trong các utility method của object host để chuyển sang đúng application context mà ứng dụng bạn cần. Các phương thức chuyển context được liệt kê dưới đây.
/**
* Switch context to RPC.
*/
switchToRpc(): RpcArgumentsHost;
/**
* Switch context to HTTP.
*/
switchToHttp(): HttpArgumentsHost;
/**
* Switch context to WebSockets.
*/
switchToWs(): WsArgumentsHost;
Hãy viết lại ví dụ trước bằng phương thức switchToHttp(). Lời gọi host.switchToHttp() trả về một object HttpArgumentsHost phù hợp với HTTP application context. Object HttpArgumentsHost có hai phương thức hữu ích mà ta có thể dùng để lấy ra các object mong muốn. Ở đây ta cũng dùng type assertion của Express để trả về đúng kiểu Express gốc:
const ctx = host.switchToHttp();
const request = ctx.getRequest<Request>();
const response = ctx.getResponse<Response>();
Tương tự, WsArgumentsHost và RpcArgumentsHost cũng có các phương thức trả về object phù hợp trong microservices context và WebSockets context. Dưới đây là các phương thức của WsArgumentsHost:
export interface WsArgumentsHost {
/**
* Returns the data object.
*/
getData<T>(): T;
/**
* Returns the client object.
*/
getClient<T>(): T;
}
Và đây là các phương thức của RpcArgumentsHost:
export interface RpcArgumentsHost {
/**
* Returns the data object.
*/
getData<T>(): T;
/**
* Returns the context object.
*/
getContext<T>(): T;
}
ExecutionContext class
ExecutionContext kế thừa (extends) ArgumentsHost, cung cấp thêm thông tin chi tiết về quá trình thực thi hiện tại. Giống như ArgumentsHost, Nest sẽ cung cấp một instance của ExecutionContext ở những nơi bạn có thể cần, chẳng hạn trong phương thức canActivate() của một guard hay phương thức intercept() của một interceptor. Nó cung cấp các phương thức sau:
export interface ExecutionContext extends ArgumentsHost {
/**
* Returns the type of the controller class which the current handler belongs to.
*/
getClass<T>(): Type<T>;
/**
* Returns a reference to the handler (method) that will be invoked next in the
* request pipeline.
*/
getHandler(): Function;
}
Phương thức getHandler() trả về tham chiếu (reference) tới handler sắp được gọi. Phương thức getClass() trả về kiểu (type) của Controller class mà handler này thuộc về. Ví dụ, trong một HTTP context, nếu request đang xử lý là một request POST, được gắn (bind) với method create() trên CatsController, thì getHandler() sẽ trả về tham chiếu tới method create(), còn getClass() trả về class CatsController (không phải instance).
const methodKey = ctx.getHandler().name; // "create"
const className = ctx.getClass().name; // "CatsController"
Khả năng truy cập tham chiếu tới cả class và handler method hiện tại mang lại sự linh hoạt rất lớn. Quan trọng nhất, nó cho ta cơ hội truy cập metadata được gắn thông qua decorator tạo bằng Reflector#createDecorator hoặc decorator có sẵn @SetMetadata(). Ta sẽ tìm hiểu use case (trường hợp sử dụng) này ở phần dưới.
Reflection and metadata (Reflection và metadata)
Nest cho phép gắn metadata tùy chỉnh (custom metadata) vào route handler thông qua decorator được tạo bằng phương thức Reflector#createDecorator, cũng như decorator có sẵn @SetMetadata(). Trong phần này, hãy so sánh hai cách tiếp cận và xem cách truy cập metadata từ bên trong một guard hoặc interceptor.
Để tạo decorator có kiểu tường minh (strongly-typed) bằng Reflector#createDecorator, ta cần chỉ định type argument. Ví dụ, hãy tạo một decorator Roles nhận vào một mảng string làm argument.
import { Reflector } from '@nestjs/core';
export const Roles = Reflector.createDecorator<string[]>();
Decorator Roles ở đây là một hàm nhận vào một argument duy nhất kiểu string[].
Ghi chú người dịch Reflector.createDecorator giống với việc bạn định nghĩa một custom annotation kèm kiểu dữ liệu ở Java/Spring, hoặc tương tự việc gắn struct tag kèm giá trị ở Go rồi đọc lại bằng reflection — chỉ khác là ở TypeScript bạn có luôn type-safety khi đọc lại giá trị đó.
Giờ, để dùng decorator này, ta chỉ cần annotate handler với nó:
@Post()
@Roles(['admin'])
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}
@Post()
@Roles(['admin'])
@Bind(Body())
async create(createCatDto) {
this.catsService.create(createCatDto);
}
Ở đây ta đã gắn metadata của decorator Roles vào method create(), cho biết chỉ user có role admin mới được phép truy cập route này.
Để lấy role(s) (custom metadata) của route, ta lại dùng đến helper class Reflector. Reflector có thể được inject vào một class theo cách thông thường:
@Injectable()
export class RolesGuard {
constructor(private reflector: Reflector) {}
}
@Injectable()
@Dependencies(Reflector)
export class CatsService {
constructor(reflector) {
this.reflector = reflector;
}
}
Gợi ý Reflector class được import từ package @nestjs/core.
Giờ, để đọc metadata của handler, dùng phương thức get():
const roles = this.reflector.get(Roles, context.getHandler());
Phương thức Reflector#get cho phép ta dễ dàng lấy metadata bằng cách truyền vào hai argument: tham chiếu tới decorator, và một context (đối tượng đích mà decorator được gắn vào) để lấy metadata từ đó. Trong ví dụ này, decorator được chỉ định là Roles (xem lại file roles.decorator.ts ở trên). Context được cung cấp bởi lời gọi context.getHandler(), kết quả là lấy được metadata của route handler đang được xử lý. Nhớ rằng, getHandler() cho ta một tham chiếu (reference) tới hàm route handler.
Ngoài ra, ta cũng có thể tổ chức controller bằng cách gắn metadata ở cấp controller, áp dụng cho tất cả các route trong controller class đó.
@Roles(['admin'])
@Controller('cats')
export class CatsController {}
@Roles(['admin'])
@Controller('cats')
export class CatsController {}
Trong trường hợp này, để lấy metadata ở cấp controller, ta truyền context.getClass() làm argument thứ hai (cung cấp controller class làm context để lấy metadata) thay vì context.getHandler():
const roles = this.reflector.get(Roles, context.getClass());
Với khả năng cung cấp metadata ở nhiều cấp, đôi khi bạn cần lấy và gộp (merge) metadata từ nhiều context. Reflector class cung cấp hai utility method để hỗ trợ việc này. Hai phương thức này lấy metadata ở cả controller và method cùng lúc, rồi kết hợp chúng theo các cách khác nhau.
Xét kịch bản sau, khi bạn đã gắn metadata Roles ở cả hai cấp.
@Roles(['user'])
@Controller('cats')
export class CatsController {
@Post()
@Roles(['admin'])
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}
}
@Roles(['user'])
@Controller('cats')
export class CatsController {}
@Post()
@Roles(['admin'])
@Bind(Body())
async create(createCatDto) {
this.catsService.create(createCatDto);
}
}
Nếu ý định của bạn là đặt 'user' làm role mặc định, rồi override có chọn lọc cho một số method cụ thể, bạn nên dùng phương thức getAllAndOverride().
const roles = this.reflector.getAllAndOverride(Roles, [context.getHandler(), context.getClass()]);
Một guard chạy đoạn code này, trong context của method create(), với metadata như trên, sẽ khiến roles chứa giá trị ['admin'].
Để lấy metadata ở cả hai cấp rồi gộp lại (phương thức này gộp cả array lẫn object), dùng phương thức getAllAndMerge():
const roles = this.reflector.getAllAndMerge(Roles, [context.getHandler(), context.getClass()]);
Kết quả này sẽ khiến roles chứa ['user', 'admin'].
Ghi chú người dịch Có thể liên tưởng getAllAndOverride với việc CSS override style: rule ở cấp gần nhất (method) “thắng” rule ở cấp xa hơn (controller). Còn getAllAndMerge giống spread object trong React khi gộp default props với props truyền vào — cấp method chỉ bổ sung/nối thêm chứ không thay thế hoàn toàn cấp controller.
Với cả hai phương thức gộp này, bạn truyền metadata key làm argument đầu tiên, và một mảng các context đích của metadata (tức là các lời gọi getHandler() và/hoặc getClass()) làm argument thứ hai.
Low-level approach (Cách tiếp cận cấp thấp)
Như đã đề cập ở trên, thay vì dùng Reflector#createDecorator, bạn cũng có thể dùng decorator có sẵn @SetMetadata() để gắn metadata vào một handler.
@Post()
@SetMetadata('roles', ['admin'])
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}
@Post()
@SetMetadata('roles', ['admin'])
@Bind(Body())
async create(createCatDto) {
this.catsService.create(createCatDto);
}
Gợi ý @SetMetadata() decorator được import từ package @nestjs/common.
Với đoạn code trên, ta đã gắn metadata roles (roles là metadata key và ['admin'] là giá trị tương ứng) vào method create(). Cách này tuy hoạt động được, nhưng dùng trực tiếp @SetMetadata() trong route không phải là best practice (cách làm khuyến nghị). Thay vào đó, bạn nên tạo decorator riêng của mình, như dưới đây:
import { SetMetadata } from '@nestjs/common';
export const Roles = (...roles: string[]) => SetMetadata('roles', roles);
import { SetMetadata } from '@nestjs/common';
export const Roles = (...roles) => SetMetadata('roles', roles);
Cách này sạch và dễ đọc hơn nhiều, và phần nào giống với cách tiếp cận Reflector#createDecorator. Điểm khác biệt là với @SetMetadata bạn kiểm soát tốt hơn metadata key và value, đồng thời có thể tạo decorator nhận nhiều hơn một argument.
Giờ ta đã có decorator @Roles() tùy chỉnh, có thể dùng nó để decorate method create().
@Post()
@Roles('admin')
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}
@Post()
@Roles('admin')
@Bind(Body())
async create(createCatDto) {
this.catsService.create(createCatDto);
}
Để lấy role(s) (custom metadata) của route, ta lại dùng helper class Reflector:
@Injectable()
export class RolesGuard {
constructor(private reflector: Reflector) {}
}
@Injectable()
@Dependencies(Reflector)
export class CatsService {
constructor(reflector) {
this.reflector = reflector;
}
}
Gợi ý Reflector class được import từ package @nestjs/core.
Giờ, để đọc metadata của handler, dùng phương thức get().
const roles = this.reflector.get<string[]>('roles', context.getHandler());
Ở đây, thay vì truyền một tham chiếu decorator, ta truyền metadata key (trong trường hợp này là 'roles') làm argument đầu tiên. Mọi thứ còn lại giữ nguyên như ở ví dụ dùng Reflector#createDecorator.