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のスカラー型は、単一の値に解決されるプリミティブ(ID
、String
、Boolean
、Int
など)です。メモ
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
}
この時点で、データグラフに存在しうるオブジェクト(型定義)を定義しましたが、 クライアントはまだそれらのオブジェクトと対話する手段を持っていません。これに対処するために、リゾルバクラスを作成する必要がある。コード最初のメソッドでは、リゾルバクラスはリゾルバ関数の定義と 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
パッケージからエクスポートされます。注意
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
}
メモ
たとえば、クエリハンドラメソッドには
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
可能かどうか
クエリのハンドラメソッドは、複数の引数を取得できます。例えば、
firstName
とlastName
に基づいて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のクラス継承を利用して、拡張可能な汎用的なユーティリティ型の機能(フィールドやフィールドプロパティ、バリデーションなど)を持つベースクラスを作成できます。
例えば、ページネーションに関連する引数のセットで、標準的な
offset
とlimit
フィールドを常に含み、さらにタイプ固有の他の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
パッケージからインポートされています。 isAbstract
:true
の場合、このクラスに対して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) {}
Last modified 10mo ago