Chapter 18

fundamentals-executioncontext

kisihara.c
kisihara.c
2021.02.18に更新

実行コンテキスト

Nestは複数のアプリケーションコンテキスト(例:NestHTTPサーバーベースの、マイクロサービスやWebSocketsのアプリケーションコンテキスト)にまたがって動くアプリケーションを、簡単に作成する為に役立つ、いくつかのユーティリティクラスを提供している。多くのコントローラ、メソッド、および実行テキストを超えて動作する汎用的なガードフィルタインターセプターの構築に役に立つはずだ。

この章ではArgumentsHostクラスとExecutionContextクラスを取り上げる。

ArgumentsHostクラス

ArgumentHostクラスはハンドラに渡された引数を取得するためのメソッドを提供する。結果、引数を取得するための適切なコンテキスト(例えばHTTP、RPC(マイクロサービス)、WebSockets)を選択できる。フレームワークは貴方のアクセスしようとしていた場所でArgumentsHostのインスタンスを提供する。典型的にはhostパラメータとして引用されるものだ。例えば例外フィルタcatch()メソッドはArgumentsHostインスタンスと一緒に(前置詞with、「~を使って」かも…)呼び出される。

ArgumentsHostはシンプルにハンドラの引数を抽象化したものとして機能する。例えばHTTPサーバーアプリケーション(@nestjs/platform-expressが使用されている時)に対しては、hostオブジェクトはExpressの[request, response, next]配列をカプセル化する。requestはリクエストオブジェクトとなり、responseはレスポンスオブジェクトとなり、nextはアプリケーションのrequest-responseサイクルをコントロールする関数となる。一方、GraphQLアプリケーションの場合、ホストオブジェクトには[root, args, context, info]配列が含まれる。

現在のアプリケーションのコンテキスト

複数のアプリケーションコンテキストで動作する汎用のガードフィルタインターセプターを構築する際、メソッドが現在動作しているアプリケーションの型を決定する方法が必要だ。これはArgumentsHostgetType()メソッドで行う。

if (host.getType() === 'http') {
  // 通常のHTTPリクエスト(REST)のコンテキストでのみ行いたい動作をやる
} else if (host.getType() === 'rpc') {
  // マイクロサービスのリクエストのコンテキストでのみ行いたい動作をやる
} else if (host.getType<GqlContextType>() === 'graphql') {
  // GlaphQLリクエストのコンテキストでのみ行いたい動作をやる
}

HINT
GqlContest@nestjs/graphqlパッケージからインポートしている。

アプリケーションの型が利用できるようになったので、以下のようにもっと汎用的なコンポーネントを書ける。

ホストハンドラの引数

ハンドラに渡される引数の配列を取得する為には、ホストオブジェクトのgetArgs()メソッドを使うアプローチが一つある。

const [req, res, next] = host.getArgs();

getArgByIndex()メソッドを使用して、インデックスを使って特定の引数をpluckできる。

const request = host.getArgByIndex(0);
const response = host.getArgByIndex(1);

以上の例の上ではリクエストとレスポンスのオブジェクトをインデックスから取得しているが、これはアプリケーションを特定の実行コンテキストと結びつける為、一般的にはオススメできない。代わりにhostオブジェクトのユーティリティメソッドを利用し、状況に適したアプリケーションコンテキストに切り替える事で、コードをより堅牢かつ再利用しやすいものにできる。コンテキストを切り替えるユーティリティメソッドは以下の通り。

/**
 * コンテキストをRPCに切り替える。
 */
switchToRpc(): RpcArgumentsHost;
/**
 * コンテキストをHTTPに切り替える。
 */
switchToHttp(): HttpArgumentsHost;
/**
 * コンテキストをWebSocketsに切り替える。
 */
switchToWs(): WsArgumentsHost;

先程の例をswitchToHttp()メソッドを使って書き換えてみる。host.switchToHttp()ヘルパーを呼び出すと、HTTPアプリケーションのコンテキストに適したHttpArgumentsHostオブジェクトを返す。このオブジェクトには目的のオブジェクトを抽出する為に使用可能な便利なメソッドが2つある。ネイティブなExpress型のオブジェクトを返す場合は、Expressの型アサーションも使おう。

const ctx = host.switchToHttp();
const request = ctx.getRequest<Request>();
const response = ctx.getResponse<Response>();

同様に、WsArgumentsHostRpcArgumentsHostには、マイクロサービスとWebSocketsのコンテキストで適切なオブジェクトを返すメソッドがある。

export interface WsArgumentsHost {
  /**
   * データオブジェクトを返す。
   */
  getData<T>(): T;
  /**
   * クライアントオブジェクトを返す。
   */
  getClient<T>(): T;
}
export interface RpcArgumentsHost {
  /**
   * データオブジェクトを返す。
   */
  getData<T>(): T;

  /**
   * クライアントオブジェクトを返す。
   */
  getContext<T>(): T;
}

ExecutionContextクラス

ExecutionContextArgumentsHostを拡張し、現在の実行プロセスに関するさらなる詳細を提供する。ArgmentsHostと同様に、NestはガードcanActivate()メソッドやインターセプターintercept()メソッドなど、必要に応じてExecutionContextのインスタンスを提供する。提供されるメソッドは以下等。

export interface ExecutionContext extends ArgumentsHost {
  /**
   * 現在のハンドラが属するコントローラクラスの型を返す
   */
  getClass<T>(): Type<T>;
  /**
   * リクエストパイプラインで次に呼び出される
   * ハンドラ(メソッド)への参照を返す
   */
  getHandler(): Function;
}

getHandler()メソッドは、呼び出されようとしているハンドラへの参照を返す。getClass()メソッドは、このハンドラが属するControllerクラスの型を返す。例えばHTTPのコンテキストの中、現在処理中のリクエストがPOSTリクエストで、CatsControllercreate()メソッドにバインドされている場合、getHandler()create()メソッドへの参照を返し、getClass()CatsControllerの(インスタンスではなく)を返す。

const methodKey = ctx.getHandler().name; // "create"
const className = ctx.getClass().name; // "CatsController"

currentなクラスとハンドラメソッドの両方へアクセスできる機能は、強い柔軟性を提供する。最も大事なのは、ガードやインターセプタ内から@SetMetadata()デコレータを通してメタデータの集合にアクセスする方法がある事だ。以下ではこのユースケースを取り上げる。

リフレクションとメタデータ

Nestは@SetMetadata()デコレータを使ってカスタムメタデータをルートハンドラにアタッチする機能を提供している。そうなればクラス内からこのメタデータにアクセスし、certain decisionsをmakeする事ができる(訳出できず)。

cats.controller.ts
@Post()
@SetMetadata('roles', ['admin'])
async create(@Body() createCatDto: CreateCatDto) {
  this.catsService.create(createCatDto);
}

HINT
@SetMetadata()デコレータは@nestjs/commonパッケージからインポートしている。

上記のコードではrolesメタデータ(rolesはメタデータのキー、['admin']は関連する変数)をcreate()メソッドにアタッチしている。これは動くが、ルートで直接@SetMetadata()を使うのはグッドプラクティスとはいえない。代わりに以下のように独自のデコレータを作成してほしい。

roles.decorator.ts
import { SetMetadata } from '@nestjs/common';

export const Roles = (...roles: string[]) => SetMetadata('roles', roles);

このアプローチのほうがずっとすっきりしていて読みやすく、強く型付けされている。カスタムの@Roles()デコレータができたので、create()メソッドを改めて装飾できる。

cats.controller.ts
@Post()
@Roles('admin')
async create(@Body() createCatDto: CreateCatDto) {
  this.catsService.create(createCatDto);
}

ルートのロール(role、roles。カスタムメタデータ)にアクセスするには、フレームワークから提供され@nestjs/coreパッケージで公開されているReflectorヘルパークラスを使用する。Reflectorは通常の方法でクラスにインジェクションできる。

roles.guard.ts
@Injectable()
export class RolesGuard {
  constructor(private reflector: Reflector) {}
}

HINT
Refletorクラスは@nestjs/coreパッケージからインポートしている。

ハンドラのメタデータを読むためには、get()メソッドを使用する。

const roles = this.reflector.get<string[]>('roles', context.getHandler());

Reflector#getメソッドを使うと、2つの引数(メタデータキーとメタデータを取得するコンテキスト(デコレータの対象))を取得する事でメタデータに簡単にアクセスできる。この例では、指定されたキーrolesだ(上記のroles.decorator.tsとその中のSetMetadata()の呼び出しを参照の事)。コンテキストがcontet.getHandler()の呼び出しによって提供され、現在処理中のルートハンドラのメタデータが抽出される。思い出してほしい、getHandler()はルートハンドラ関数への参照を提供している。

もしくは、コントローラレベルでメタデータを適用し、コントローラクラス内の全てのルートに適用することで、コントローラを整理する事もできる。

cats.controller.ts
@Roles('admin')
@Controller('cats')
export class CatsController {}
}

この場合、コントローラのメタデータを抽出するために、context.getHandlerの代わりに2つめの引数としてcontext.getClassを渡す。これは、メタデータ抽出の為のコンテキストとしてコントローラクラスを提供する為。

roles.guard.ts
const roles = this.reflector.get<string[]>('roles', context.getClass());

複数のレベルでメタデータを提供できる事を考えると、複数のコンテキストからメタデータを抽出してマージする必要があるかもしれない。Reflectorクラスにはこれを支援する為の2つのユーティリティメソッドが用意されている。これらのメソッドはコントローラとメソッドの両方のメタデータを一度に抽出し、別々の方法で結合する。

以下のシナリオでは、両方のレベルでrolesメタデータを用意している。

cats.controller.ts
@Roles('user')
@Controller('cats')
export class CatsController {
  @Post()
  @Roles('admin')
  async create(@Body() createCatDto: CreateCatDto) {
    this.catsService.create(createCatDto);
  }
}

デフォルトのロールとして'user'を指定し、特定のメソッドに対して選択的にオーバーライドしたい場合は、getAllAndOverride()を使う事がある。

const roles = this.reflector.getAllAndOverride<string[]>('roles', [
  context.getHandler(),
  context.getClass(),
]);

このコードを持つガードが、上記のメタデータを持つcreate()メソッドのコンテキストで実行されると、['admin']を含むロールになる。

両方のメタデータを取得してマージするには(このメソッドは配列とオブジェクトの両方をマージできる)getAllAndMerge()メソッドを使用する。

const roles = this.reflector.getAllAndMerge<string[]>('roles', [
  context.getHandler(),
  context.getClass(),
]);

これは['user', 'admin']を含むロールになる。

これらのマージメソッドの両方において、第一引数にメタデータキーを渡し、第二引数にメタデータターゲットのコンテキストの配列(すなわちgetHandler()メソッド及び/あるいはgetClass()メソッド)を渡す事になる。