Resolver(スキーマファースト)

スキーマファーストでGraphQLのResolverを作成する時、SDLでスキーマの型を手動で定義することから始めます。以下のSDL型の定義について詳細に考えてみましょう。
メモ
このセクションでは便宜上、すべてのSDLを一箇所に集約しています。(例えば、以下のように.gqlファイルを作成します)実際には、コードをモジュール形式で整理するのが適切でしょう。たとえば、各ドメインエンティティを表す型定義と関連サービス、ResolverコードやNestモジュール定義クラスを含む個別のSDLファイルを、そのエンティティ専用のディレクトリに作成すると便利なことがあります。NestJSでは、実行時に個々のスキーマの型定義をすべて集約します。
type Author {
id: Int!
firstName: String
lastName: String
posts: [Post]
}
type Post {
id: Int!
title: String!
votes: Int
}
type Query {
author(id: Int!): Author
}

スキーマファーストのResolver

上記のスキーマは、author(id: Int!)という1つのクエリを公開します。
メモ
GraphQLのクエリに関しては、こちらをご確認ください。
それではAuthorsResolverクラスを作成します。(authorデータへのアクセスを行う)
authors/authors.resolver.ts
@Resolver('Author')
export class AuthorsResolver {
constructor(
private authorsService: AuthorsService,
private postsService: PostsService,
) {}
@Query()
async author(@Args('id') id: number) {
return this.authorsService.findOneById(id);
}
@ResolveField()
async posts(@Parent() author) {
const { id } = author;
return this.postsService.findAll({ authorId: id });
}
}
メモ
@Resolver@ResolveField@Args@nestjs/graphqlパッケージからインポートできます。
注意
AuthorsServiceおよびPostsServiceクラス内のロジックは、必要なだけシンプルにすることも高度にすることも両方できます。この例の主なポイントは、リゾルバの構築方法と、それらが他のプロバイダとどのように相互作用できるかを示すことです。
@Resolver()デコレータは必須です。オプションの文字列引数にはクラスを指定します。このクラス名は、クラスが@ResolveField()デコレータを含む場合は常に必要で、NestJSにデコレートされたメソッドが親の型(今回の例ではAuthor型)に関連していることを知らせます。また、クラスの先頭で@Resolver()を設定する代わりに、各メソッドに対して設定することも可能です。
@Resolver('Author')
@ResolveField()
async posts(@Parent() author) {
const { id } = author;
return this.postsService.findAll({ authorId: id });
}
この場合(メソッドレベルでの@Resolver()デコレータ)、クラス内に複数の@ResolveField()デコレータがあると、それらすべてに@Resolver()を追加しなければなりません。これはあまり推奨されていません。
メモ
@Resolver()に渡されたクラス名の引数は、クエリ(@Query()デコレータ)やミューテーション(@Mutation()デコレータ)には影響しません。
注意
メソッドレベルでの@Resolver()デコレータはコードファーストのアプローチではサポートされていません。
上記の例では、@Query()@ResolveField()デコレータは、メソッド名に基づいてGraphQLスキーマの方と関連付けられています。例えば、上記の例から以下のような構成を考えてみましょう。
@Query()
async author(@Args('id') id: number) {
return this.authorsService.findOneById(id);
}
これにより、スキーマのauthorクエリに次のようなエントリが生成されます。
type Query {
author(id: Int!): Author
}
逆に、これらを分離してResolverのメソッドにgetAuthor()getPosts()というような名前を使うのが一般的でした。これを行うには、以下のようにマッピング名をデコレータの引数に渡します。
@Resolver('Author')
export class AuthorsResolver {
constructor(
private authorsService: AuthorsService,
private postsService: PostsService,
) {}
@Query('author')
async getAuthor(@Args('id') id: number) {
return this.authorsService.findOneById(id);
}
@ResolveField('posts')
async getPosts(@Parent() author) {
const { id } = author;
return this.postsService.findAll({ authorId: id });
}
}
メモ
Nest CLIはすべての定型的なコードを自動的に生成するジェネレータ(回路図)を提供し、このようなことをしなくても済むようにし、シンプルな開発体験を提供しています。
この機能の詳細についてはこちらをご確認ください。

型の生成

スキーマファーストのアプローチを活用し、型付け生成機能を有効にした場合、アプリケーションを実行すると以下のファイルが生成されます。(GraphQLModule.forRoot()メソッドで指定した場所にあります)。例えば、src/graphql.tsの場合は以下のようになります。
graphql.ts
export class Author {
id: number;
firstName?: string;
lastName?: string;
posts?: Post[];
}
export class Post {
id: number;
title: string;
votes?: number;
}
export abstract class IQuery {
abstract author(id: number): Author | Promise<Author>;
}
クラスを生成する(インターフェイスを生成するデフォルトの手法の代わりに)ことで、宣言型の検証デコレータをスキーマファーストのアプローチと組み合わせて使うことができます。
例えば、以下のように生成されたCreatePostInputクラスにクラス検証デコレータを追加すると、titleフィールドに最小及び最大の文字列長を強制できます。
import { MinLength, MaxLength } from 'class-validator';
export class CreatePostInput {
@MinLength(3)
@MaxLength(50)
title: string;
}
注意
入力(およびパラメータ)の自動検証を有効化するには、ValidationPipeを活用します。
しかしながら、自動生成されたファイルに直接デコレータを追加すると、ファイルが生成されるたびに上書きされてしまいます。かわりに、別のファイルを作成して生成されたクラスを単純に拡張してください。
import { MinLength, MaxLength } from 'class-validator';
import { Post } from '../../graphql.ts';
export class CreatePostInput extends Post {
@MinLength(3)
@MaxLength(50)
title: string;
}

GraphQLの引数デコレータ

専用のデコレータを使用して、標準的なGraphQLリゾルバの引数にアクセスできます。以下は、Nestデコレータとそれらが表現するプレーンなApolloパラメータの比較です。
@Root()/@Parent()
root/parent
@Context(param?: string)
context/context[param]
@Info(param?: string)
info/info[param]
@Args(param?: string)
args/args[param]
これらの引数の意味は以下の通りです。
  • root:親フィールドのリゾルバから返された結果、あるいはトップレベルのQueryフィールドの場合はサーバから渡されたrootValueを含むオブジェクト。
  • context:特定のクエリにおいて、すべてのリゾルバに共有されるオブジェクトで、通常、リクエストごとの状態を含むために使用される。
  • info:クエリの実行状態に関する情報を含むオブジェクト。
  • args:クエリ内のフィールドに渡された引数を持つオブジェクト

モジュール

上記の手順が終わると、GraphQLModuleがリゾルバマップを生成するために必要なすべての情報を宣言的に指定したことになります。GraphQLModuleはリフレクションを使ってデコレーター経由で提供されたメタデータをイントロスペクトし、クラスを自動的に正しいリゾルバーマップに変換します。
あとはリゾルバクラス(AuthorsResolver)を提供し(つまり何らかのモジュールにプロバイダとしてリストし)、そのモジュール(AuthorsModule)をどこかにインポートして、Nestがそれを利用できるようにするだけでよいのです。
例えば、AuthorsModuleの中でこれを行うことができ、この文脈で必要な他のサービスも提供することができます。AuthorsModuleは必ずどこか(ルートモジュールや、ルートモジュールからインポートされた他のモジュールなど)でインポートしてください。
authors/authors.module.ts
@Module({
imports: [PostsModule],
providers: [AuthorsService, AuthorsResolver],
})
export class AuthorsModule {}
メモ
いわゆるドメインモデルによってコードを整理すると便利です(REST APIのエントリポイントを整理する方法と同様です)。このアプローチでは、モデル(ObjectTypeクラス)、リゾルバ、サービスを、ドメインモデルを表すNestモジュール内にまとめておきます。これらのコンポーネントはすべて、モジュールごとに1つのフォルダーに収めます。このようにして、Nest CLIを使用して各要素を生成すると、Nestがこれらの部品をすべて自動的に配線します(適切なフォルダーにファイルを配置し、プロバイダーとインポート配列にエントリを生成するなど)。