Resolver(コードファースト)

ResolverとはGraphQLの操作(クエリ、ミューテーション、サブスクリプション)をデータに変換するための命令を提供します。スキーマで指定したものと同じ形のデータを、同期的に、あるいはその形の結果に解決されるPromiseとして返します。Resolverのマップは手動で作成します。
一方で、@nestjs/qraphqlパッケージではクラスのアノテーションに使われるデコレータが提供するメタデータを使ってResolverのマップを自動的に生成します。パッケージの機能を使ってGraphQL APIを作成するプロセスを示すために簡単なAPIを作成します。
本セクションではコードファーストのアプローチを中心に説明していきます。スキーマファーストのアプローチについては書きませんのでご了承ください。
コードファーストでのアプローチは、GraphQL SDLを手書きしてGraphQLスキーマを作成するという典型的なプロセスは示しません。代わりとして、TypeScriptのクラス定義からSDLを生成するためにTypeScriptのデコレータを使用します。@nestjs/graphqlパッケージはデコレータを通して定義されたメタデータを受け取り、スキーマを自動的に生成してくれます。

オブジェクトタイプ

GraphQLスキーマの定義の大半は、オブジェクトタイプです。定義する各オブジェクトタイプは、アプリケーションクライアントが対話する必要のあるドメインオブジェクトを表す必要があります。例えば、私たちのサンプルAPIは著者とその投稿のリストを取得する必要があるため、この機能をサポートするためにAuthorタイプとPostタイプを定義する必要があります。
スキーマファーストのアプローチを使用する場合は、 SDL でこのようなスキーマを定義することになります。
type Author {
id: Int!
firstName: String
lastName: String
posts: [Post!]!
}
この場合、コードファーストのアプローチでは、TypeScriptのクラスを使ってスキーマを定義し、そのクラスのフィールドにTypeScriptのデコレータを使ってアノテーションをつけます。以下のようにしてSDLを実装してください。
authors/models/author.model.ts
import { Field, Int, ObjectType } from '@nestjs/graphql';
import { Post } from './post';
@ObjectType()
export class Author {
@Field(type => Int)
id: number;
@Field({ nullable: true })
firstName?: string;
@Field({ nullable: true })
lastName?: string;
@Field(type => [Post])
posts: Post[];
}
メモ
TypeScriptのメタデータ反映システムにはいくつかの制限があり、たとえばクラスがどのようなプロパティから構成されているかを判断したり、与えられたプロパティがオプションか必須かを認識したりすることは不可能です。これらの制限のため、スキーマ定義クラスで@Field()デコレーターを明示的に使用して各フィールドのGraphQL型とオプション性に関するメタデータを提供するか、CLIプラグインを使用してこれらを生成する必要があります。
Authorオブジェクトタイプは、他のクラスと同様に、フィールドのコレクションで構成されており、各フィールドはタイプを宣言しています。フィールドの型は、GraphQLの型に対応します。フィールドのGraphQLタイプは、他のオブジェクトタイプかスカラータイプになります。GraphQLのスカラー型は、単一の値に解決されるプリミティブ(IDStringBooleanIntなど)です。
メモ
GraphQLの組み込みスカラー型に加えて、カスタムのスカラー型を定義できます。
上記のAuthorオブジェクトタイプの定義に加えて、NestJSは上記で示したSDLを生成します。
type Author {
id: Int!
firstName: String
lastName: String
posts: [Post!]!
}
@Field()デコレータは、オプションで型関数 (例: type => Int) とオプションでオプション・オブジェクトを受け取ります。
TypeScriptの型システムとGraphQLの型システムの間で曖昧さが生じる可能性がある場合、型関数が必要になります。具体的には、文字列と真偽値型には必要ありませんが、数値(GraphQLのIntまたはFloatにマッピングされなければなりません)には必要です。型関数は単純に目的のGraphQL型を返すべきです(これらの章のさまざまな例で示されているように)。
オプションオブジェクトは、以下のキーと値のペアのいずれかを持つことができます。
  • nullable:フィールドがnullableであるかどうかを指定します (SDL では、デフォルトで各フィールドは non-nullable になっています)。
  • description: フィールドの説明を設定します。
  • deprecationReason: フィールドを非推奨とする理由を書きます。
例えば以下のようにします。
@Field({ description: `Book title`, deprecationReason: 'Not useful in v2 schema' })
title: string;
メモ
また、オブジェクトタイプ全体に説明を追加したり、非推奨にしたりすることもできます。@ObjectType({ description: 'Author model' })
フィールドが配列の場合、以下のように@Field()デコレータのtype関数で配列の型を手動で指定する必要があります。
@Field(type => [Post])
posts: Post[];
メモ
配列のブラケット表記([ ])を用いると、配列の深さを表すことができます。例えば、[[Int]]を使えば、整数行列を表すことができます。
配列のアイテム(配列そのものではない)をnullableにすることを宣言するには、以下のようにnullableプロパティを'items'に設定します。
@Field(type => [Post], { nullable: 'items' })
posts: Post[];
メモ
配列とその項目の両方がnullableな場合は、代わりに'itemsAndList'nullableに設定します。
Authorオブジェクトタイプが作成されたので、Postオブジェクトタイプを定義してみましょう。
posts/models/post.model.ts
import { Field, Int, ObjectType } from '@nestjs/graphql';
@ObjectType()
export class Post {
@Field(type => Int)
id: number;
@Field()
title: string;
@Field(type => Int, { nullable: true })
votes?: number;
}
Postオブジェクトタイプは、SDLのGraphQLスキーマの以下の部分を生成することになります。
type Post {
id: Int!
title: String!
votes: Int
}

コードファーストのResolver

この時点で、データグラフに存在しうるオブジェクト(型定義)を定義しましたが、 クライアントはまだそれらのオブジェクトと対話する手段を持っていません。これに対処するために、リゾルバクラスを作成する必要がある。コード最初のメソッドでは、リゾルバクラスはリゾルバ関数の定義と Query 型の生成の両方を行う。このことは、以下の例で作業していくうちに明らかになるでしょう。
authors/authors.resolver.ts
@Resolver(of => Author)
export class AuthorsResolver {
constructor(
private authorsService: AuthorsService,
private postsService: PostsService,
) {}
@Query(returns => Author)
async author(@Args('id', { type: () => Int }) id: number) {
return this.authorsService.findOneById(id);
}
@ResolveField()
async posts(@Parent() author: Author) {
const { id } = author;
return this.postsService.findAll({ authorId: id });
}
}
メモ
すべてのデコレータ(@Resolver, @ResolveField, @Argsなど)は、 @nestjs/graphqlパッケージからエクスポートされます。
複数のリゾルバクラスを定義することができます。Nestは実行時にこれらを結合します。コードの構成については、以下のモジュールのセクションを参照してください。
注意
AuthorsServiceおよびPostsServiceクラス内のロジックは、必要なだけシンプルにすることも、高度にすることもできます。この例の主なポイントは、リゾルバの構築方法と、それらが他のプロバイダとどのように相互作用できるかを示すことです。
上の例では、クエリリゾルバー関数とフィールドリゾルバー関数をひとつずつ定義した AuthorsResolverを作成しました。リゾルバを作成するには、リゾルバ関数をメソッドとして持つクラスを作成し、 @Resolver()デコレータでそのクラスにアノテーションを付けます。
この例では、リクエストで送信されたidに基づいてauthorオブジェクトを取得するクエリハンドラを定義しています。このメソッドがクエリハンドラであることを指定するには、 @Query()デコレータを使用します。
@Resolver()デコレータに渡す引数はオプションですが、グラフがトリビアルでなくなったときに登場します。これは、フィールドリゾルバ関数がオブジェクトグラフを走査する際に使用する親オブジェクトを指定するために使用します。
この例では、このクラスにフィールド解決関数 (Authorオブジェクトタイプのpostsプロパティ) が含まれているので、 @Resolver()デコレータに値を与えて、 このクラス内で定義されているすべてのフィールド解決関数に対して どのクラスが親タイプ (ObjectType クラス名) であるのかを示す必要があります。
この例から明らかなように、フィールドリゾルバの関数を書く際には 親オブジェクト (解決しようとしているフィールドが所属しているオブジェクト) にアクセスする必要があります。この例では、著者の投稿の配列にフィールドリゾルバを投入し、 引数として著者のIDを受け取るサービスをコールしています。そのため、@Resolver()デコレータで親オブジェクトを特定する必要があります。これに対応する@Parent()メソッドパラメータ・デコレータを使用して、 フィールドリゾルバでその親オブジェクトへの参照を抽出していることに注意しましょう。
複数の@Query()リゾルバ関数を定義することができます (このクラス内でも、他のリゾルバクラスでも可能です)。 そしてそれらは、生成される SDL においてひとつの Query 型定義にまとめられ、 リゾルバマップの適切な項目として扱われます。これにより、使用するモデルやサービスの近くでクエリを定義し、 モジュールの中でうまくまとめることができるようになります。
メモ
Nest CLIは、すべての定型的なコードを自動的に生成するジェネレータ(回路図)を提供し、このようなことをしなくても済むようにし、開発者の体験をよりシンプルにすることを目的としています。この機能の詳細については、こちらをご覧ください。

クエリ型の名前

上記の例では、@Query()デコレーターがメソッド名に基づいてGraphQLスキーマのクエリー型名を生成しています。例えば、上記の例から次のような構成を考えてみましょう。
@Query(returns => Author)
async author(@Args('id', { type: () => Int }) id: number) {
return this.authorsService.findOneById(id);
}
これにより、スキーマのauthorクエリに次のようなエントリが生成されます (クエリタイプはメソッド名と同じ名前を使用します)。
type Query {
author(id: Int!): Author
}
メモ
GraphQLのクエリに関してはこちらをご確認ください。
たとえば、クエリハンドラメソッドにはgetAuthor()のような名前をつけておき、 クエリの型名には authorを使用します。同じことが、フィールドリゾルバにも当てはまります。以下のように、@Query()@ResolveField()デコレータの引数にマッピング名を渡すことで、簡単にこれを行うことができます。
authors/authors.resolver.ts
@Resolver(of => Author)
export class AuthorsResolver {
constructor(
private authorsService: AuthorsService,
private postsService: PostsService,
) {}
@Query(returns => Author, { name: 'author' })
async getAuthor(@Args('id', { type: () => Int }) id: number) {
return this.authorsService.findOneById(id);
}
@ResolveField('posts', returns => [Post])
async getPosts(@Parent() author: Author) {
const { id } = author;
return this.postsService.findAll({ authorId: id });
}
}
上記のgetAuthorハンドラメソッドにより、SDLでGraphQLスキーマの以下の部分が生成されます。
type Query {
author(id: Int!): Author
}

クエリのデコレータオプション

@Query()デコレータのオプションオブジェクト(上記の例では{name: 'author'})には、いくつかのキーと値のペアを指定できます。
  • name:クエリの名前。データ型はstring
  • description:GraphQLスキーマドキュメントの生成に使用される説明(例:GraphQL Playground)。データ型はstring
  • deprecationReason:クエリのメタデータを設定し、クエリが非推奨だということを示す。データ型はstring
  • nullable:クエリがnullデータ応答を返すかどうかを示す。データ型はboolean’items’あるいは'itemsAndList'

引数デコレータオプション

メソッドハンドラで使用する引数をリクエストから取り出すには、@Args()デコレータを使用します。
通常、@Args()デコレータはシンプルで上記のgetAuthor()メソッドのようなオブジェクト引数は必要ありません。たとえば、識別子の型が文字列の場合、以下の構文で十分です。単に受信したGraphQLリクエストから名前の付いたフィールドを取り出し、メソッド引数として使用します。
@Args('id') id: string
getAuthor()の場合、number型が使用されていて、これが課題となっています。TypeScriptにおけるnumberは、期待されるGraphQL表現について十分な情報を与えてくれません。したがって、明示的に型参照を明記する必要があります。そのためには、@Args()デコレータに第2引数を渡して以下のように引数オプションを指定します。
@Query(returns => Author, { name: 'author' })
async getAuthor(@Args('id', { type: () => Int }) id: number) {
return this.authorsService.findOneById(id);
}
optionsオブジェクトでは、次のオプションのキーと値のペアを指定できます。
  • type:GraphQLの型を返す関数
  • defaultValue:デフォルト値。任意で設定できる。データ型はany
  • description:説明のメタデータ。データ型はstring
  • deprecationReason:フィール祖を非推奨とし、その理由を記述したメタデータ。データ型はstring
  • nullable:フィールドがnull可能かどうか
クエリのハンドラメソッドは、複数の引数を取得できます。例えば、firstNamelastNameに基づいてAuthorを取得する場合を考えてみましょう。この場合、@Args()を2回呼び出せます。
getAuthor(
@Args('firstName', { nullable: true }) firstName?: string,
@Args('lastName', { defaultValue: '' }) lastName?: string,
) {}

専用引数クラス

インラインの@Args()呼び出しでは、上記の例のようなコードが肥大化します。その代用として、以下のように専用のGetAuthorArgs引数クラスを作成し、ハンドラメソッド内でアクセス可能です。
@Args() args: GetAuthorArgs
以下のように、@ArgsType()を作成してGetAuthorArgsクラスを作成します。
authors/dto/get-author.args.ts
import { MinLength } from 'class-validator';
import { Field, ArgsType } from '@nestjs/graphql';
@ArgsType()
class GetAuthorArgs {
@Field({ nullable: true })
firstName?: string;
@Field({ defaultValue: '' })
@MinLength(3)
lastName: string;
}
メモ
ここでもTypeScriptのメタデータ反映システムの制限により、@Field()デコレータを使用して手動で型やオプションを指定するか、CLIプラグインを使用する必要があります。
これによって、SDLでGraphQLスキーマの以下の部分が生成されることになります。
type Query {
author(firstName: String, lastName: String = ''): Author
}
メモ
GetAuthorArgsのような引数クラスは、ValidationPipeと非常に相性が良いことに注意してください。

クラスの継承

標準的なTypeScriptのクラス継承を利用して、拡張可能な汎用的なユーティリティ型の機能(フィールドやフィールドプロパティ、バリデーションなど)を持つベースクラスを作成できます。
例えば、ページネーションに関連する引数のセットで、標準的なoffsetlimitフィールドを常に含み、さらにタイプ固有の他のindexフィールドを含むものがあるとします。以下のように、クラス階層を設定することができます。
@ArgsType()クラスは以下のようになります。
@ArgsType()
class PaginationArgs {
@Field((type) => Int)
offset: number = 0;
@Field((type) => Int)
limit: number = 10;
}
基本クラス@ArgsType()の型固有のサブクラスは以下のようになります。
@ArgsType()
class GetAuthorArgs extends PaginationArgs {
@Field({ nullable: true })
firstName?: string;
@Field({ defaultValue: '' })
@MinLength(3)
lastName: string;
}
同じアプローチは、@ObjectType()オブジェクトでも可能です。ベースクラスでジェネリックプロパティを定義します。
@ObjectType()
class Character {
@Field((type) => Int)
id: number;
@Field()
name: string;
}
サブクラスに型固有のプロパティを追加します。
@ObjectType()
class Warrior extends Character {
@Field()
level: number;
}
Resolverでも継承を利用できます。継承とTypeScriptのジェネリックスを組み合わせることで型安全性を担保できます。例えば、ジェネリックなfindAllクエリを持つベースクラスを作るには以下のような構文にします。
function BaseResolver<T extends Type<unknown>>(classRef: T): any {
@Resolver({ isAbstract: true })
abstract class BaseResolverHost {
@Query((type) => [classRef], { name: `findAll${classRef.name}` })
async findAll(): Promise<T[]> {
return [];
}
}
return BaseResolverHost;
}
以下のことに注意してください。
  • 明示的な戻り値の型(上記のany)が必要です。そうしないと、TypeScriptはプライベートなクラス定義が使われていると認識します。anyを使う代わりにinterfaceを定義しましょう。
  • 型は@nestjs/commonパッケージからインポートされています。
  • isAbstracttrueの場合、このクラスに対してSDLを生成しないことを命令します。他の型にもこのプロパティを設定することで、SDLの生成をコントロールできます。
以下は、BaseResolverの具体的なサブクラスを生成する方法です。
@Resolver((of) => Recipe)
export class RecipesResolver extends BaseResolver(Recipe) {
constructor(private recipesService: RecipesService) {
super();
}
}
上記の構成では、以下のようなSDLが生成されます。
type Query {
findAllRecipe: [Recipe!]!
}

ジェネリックス

上記でジェネリックスの使い方の一つを示しました。この強力なTypeScriptの機能は、有効なクラス抽象化を実装するために使えます。例えば、GraphQL公式ドキュメントをベースにしたカーソルベースのページネーション実装のサンプルを以下に示します。
import { Field, ObjectType, Int } from '@nestjs/graphql';
import { Type } from '@nestjs/common';
interface IEdgeType<T> {
cursor: string;
node: T;
}
export interface IPaginatedType<T> {
edges: IEdgeType<T>[];
nodes: T[];
totalCount: number;
hasNextPage: boolean;
}
export function Paginated<T>(classRef: Type<T>): Type<IPaginatedType<T>> {
@ObjectType(`${classRef.name}Edge`)
abstract class EdgeType {
@Field((type) => String)
cursor: string;
@Field((type) => classRef)
node: T;
}
@ObjectType({ isAbstract: true })
abstract class PaginatedType implements IPaginatedType<T> {
@Field((type) => [EdgeType], { nullable: true })
edges: EdgeType[];
@Field((type) => [classRef], { nullable: true })
nodes: T[];
@Field((type) => Int)
totalCount: number;
@Field()
hasNextPage: boolean;
}
return PaginatedType as Type<IPaginatedType<T>>;
}
上記の基本クラスが定義されていれば、この動作を継承した特殊な型を簡単に作成できます。
@ObjectType()
class PaginatedAuthor extends Paginated(Author) {}