Subscription
クエリによるデータの取得、Mutationによるデータの変更に加えてGraphQL仕様では
subscription
と呼ばれる3つ目の操作Typeがサポートされています。GraphQLにおけるSubscriptionとは、サーバからのリアルタイムメッセージを受け取ることを選択したクライアントに対して、サーバからデータをプッシュするための方法です。Subscriptionは、クライアントに配信するフィールドのセットを指定するという点ではクエリーと似ていますが、すぐに単一の回答を返すのではなく、サーバーで特定のイベントが発生するたびにチャンネルが開かれ、結果がクライアントに送信されるようになっています。
Subscriptionを可能にするためには、
installSubscriptionHandlers
プロパティをtrue
に設定してください。GraphQLModule.forRoot<ApolloDriverConfig>({
driver: ApolloDriver,
installSubscriptionHandlers: true,
}),
注意
installSubscriptionHandlers
設定オプションはApolloサーバーの最新版から削除され、このパッケージでもまもなく非推奨となります。デフォルトでは、installSubscriptionHandlers
はsubscriptions-transport-ws
を使うようにフォールバックしますが、代わりにgraphql-ws
ライブラリを使うことを強く推奨します。代わりに
graphql-ws
パッケージを使うならば、以下の設定を使用します。GraphQLModule.forRoot<ApolloDriverConfig>({
driver: ApolloDriver,
subscriptions: {
'graphql-ws': true
},
}),
メモ
また、後方互換性を考慮して両方のパッケージ(
subscription-transport-ws
とgraphql-ws
)を同時に使用できます。コードファーストのアプローチでサブスクリプションを作成するには、
@Subscription()
デコレーター (@nestjs/graphql
パッケージからエクスポート) と、シンプルな publish/subscribe API を提供するgraphql-subscriptions
パッケージのPubSub
クラスを使用します。以下のサブスクリプションハンドラは、
PubSub#asyncIterator
を呼び出すことで、イベントの購読を引き受けています。このメソッドは、イベントトピックの名前に対応するtriggerName
という単一の引数を取ります。const pubSub = new PubSub();
@Resolver((of) => Author)
export class AuthorResolver {
// ...
@Subscription((returns) => Comment)
commentAdded() {
return pubSub.asyncIterator('commentAdded');
}
}
メモ
すべてのデコレータは
@nestjs/graphql
パッケージから、PubSub
クラスは graphql-subscriptions
パッケージからエクスポートされています。注意
PubSub
は、シンプルなPublish and Subscribe APIを公開するクラスです。詳しくはこちらをご覧ください。Apolloのドキュメントでは、デフォルトの実装は実運用に適さないと警告していることに注意してください。運用アプリケーションでは、外部ストアに支えられたPubSub
の実装を使用する必要があります。これにより、SDLでGraphQLスキーマの以下の部分が作成されます。
type Subscription {
commentAdded(): Comment!
}
Subscriptionは、定義上Subscriptionの名前をキーとする単一のトップレベルプロパティを持つオブジェクトを返すことに注意しましょう。この名前は、サブスクリプションハンドラメソッドの名前から継承されるか (すなわち、上記の
commentAdded
) 、あるいは以下に示すように@Subscription()
デコレーターの第二引数としてキー名を持つオプションを渡すことによって明示的に提供されます。@Subscription(returns => Comment, {
name: 'commentAdded',
})
subscribeToCommentAdded() {
return pubSub.asyncIterator('commentAdded');
}
こちらの構文は、前のコードサンプルと同じSDLを作成しますが、メソッド名とSubscriptionを切り離すことができます。
さて、イベントを発行するには
PubSub#publish
メソッドを使用します。これは、オブジェクトグラフの一部が変更されたときにクライアント側の更新をトリガーするために、変異の中でよく使用されます。posts/post.service.ts
@Mutation(returns => Post)
async addComment(
@Args('postId', { type: () => Int }) postId: number,
@Args('comment', { type: () => Comment }) comment: CommentInput,
) {
const newComment = this.commentsService.addComment({ id: postId, comment });
pubSub.publish('commentAdded', { commentAdded: newComment });
return newComment;
}
PubSub#publish
メソッドは、最初のパラメータとして triggerName
(繰り返しますが、これはイベントトピック名と考えてください)、2番目のパラメータとしてイベントペイロードを受け取ります。前述のように、サブスクリプションは定義上、値を返し、その値は形状を持っています。commentAdded
サブスクリプションの生成された SDL をもう一度見てみましょう。type Subscription {
commentAdded(): Comment!
}
これは、サブスクリプションが
commentAdded
というトップレベルのプロパティ名 を持つオブジェクトを返さなければならず、その値がComment
オブジェクトである ことを教えています。注意すべき重要な点は、PubSub#publish
メソッドによって発行されるイベントペイロードの形は、サブスクリプションから戻ると期待される値の形に対応していなければならないということです。したがって、上記の例では、
pubSub.publish('commentAdded', { commentAdded: newComment })
文は、適切な形のペイロードを持つ commentAdded
イベントを発行しています。これらの形状が一致しない場合、サブスクリプションはGraphQL
検証フェーズで失敗します。特定のイベントをフィルタリングするには、
filter
プロパティにフィルタリング関数を設定します。この関数は、配列filter
に渡される関数と同じように動作します。この関数は 2 つの引数を取ります: payload
はイベントのペイロード (イベントパブリッシャーから送られたもの) を含み、variables
はサブスクリプション要求の間に渡された任意の引数を取ります。それは、このイベントがクライアントのリスナーに公開されるべきかどうかを決定する boolean
を返します。@Subscription(returns => Comment, {
filter: (payload, variables) =>
payload.commentAdded.title === variables.title,
})
commentAdded(@Args('title') title: string) {
return pubSub.asyncIterator('commentAdded');
}
公開されたイベントペイロードを変更するには、
resolve
プロパティを関数に設定します。この関数はイベントペイロードを受け取り(イベントパブリッシャーから送信される)、適切な値を返します。@Subscription(returns => Comment, {
resolve: value => value,
})
commentAdded() {
return pubSub.asyncIterator('commentAdded');
}
メモ
resolve
オプションを使用する場合は、ラップされていないペイロードを返す必要があります (たとえば、この例では{ commentAdded: newComment }
オブジェクトではなく、newComment
オブジェクトを直接返します)。注入されたプロバイダにアクセスする必要がある場合(例えば、データを検証するために外部サービスを使用する)、以下の構造を使用します。
@Subscription(returns => Comment, {
resolve(this: AuthorResolver, value) {
// "this" refers to an instance of "AuthorResolver"
return value;
}
})
commentAdded() {
return pubSub.asyncIterator('commentAdded');
}
フィルターも同じ構造で実装できます。
@Subscription(returns => Comment, {
filter(this: AuthorResolver, payload, variables) {
// "this" refers to an instance of "AuthorResolver"
return payload.commentAdded.title === variables.title;
}
})
commentAdded() {
return pubSub.asyncIterator('commentAdded');
}
NestJSで同等のSubscriptionを作成するには、
@Subscription()
デコレータを活用します。const pubSub = new PubSub();
@Resolver('Author')
export class AuthorResolver {
// ...
@Subscription()
commentAdded() {
return pubSub.asyncIterator('commentAdded');
}
}
コンテキストと引数に基づいて特定のイベントをフィルタリングするときには、
filter
プロパティを設定します。@Subscription('commentAdded', {
filter: (payload, variables) =>
payload.commentAdded.title === variables.title,
})
commentAdded() {
return pubSub.asyncIterator('commentAdded');
}
発行されたペイロードを変異させるには、
resolve
関数が使えます。@Subscription('commentAdded', {
resolve: value => value,
})
commentAdded() {
return pubSub.asyncIterator('commentAdded');
}
注入されたプロバイダにアクセスする必要がある場合(例えば、データを検証するために外部サービスを使用する)、以下の構造を使用します。
@Subscription('commentAdded', {
resolve(this: AuthorResolver, value) {
// "this" refers to an instance of "AuthorResolver"
return value;
}
})
commentAdded() {
return pubSub.asyncIterator('commentAdded');
}
フィルターも同じような構造で実装します。
@Subscription('commentAdded', {
filter(this: AuthorResolver, payload, variables) {
// "this" refers to an instance of "AuthorResolver"
return payload.commentAdded.title === variables.title;
}
})
commentAdded() {
return pubSub.asyncIterator('commentAdded');
}
最後に、型定義ファイルの更新を行います。(GraphQLファイル側で)
type Author {
id: Int!
firstName: String
lastName: String
posts: [Post]
}
type Post {
id: Int!
title: String
votes: Int
}
type Query {
author(id: Int!): Author
}
type Comment {
id: String
content: String
}
type Subscription {
commentAdded(title: String!): Comment
}
上記では、ローカルの
PubSub
インスタンスをインスタンス化しました。望ましい方法は、PubSub
をプロバイダとして定義し、コンストラクタを通して (@Inject()
デコレータを使用して) それを注入することです。これにより、アプリケーション全体でインスタンスを再利用することができます。例えば、以下のようにプロバイダを定義し、必要な場所に'PUB_SUB'
をインジェクトします。{
provide: 'PUB_SUB',
useValue: new PubSub(),
}
購読サーバーをカスタマイズする(パスを変更するなど)には、
subscription
オプションのプロパティを使用します。GraphQLModule.forRoot<ApolloDriverConfig>({
driver: ApolloDriver,
subscriptions: {
'subscriptions-transport-ws': {
path: '/graphql'
},
}
}),
Subscriptionに
graphql-ws
パッケージを活用している場合、以下のようにsubscriptions-transport-ws
キーをgraphql-ws
に置き換えてください。GraphQLModule.forRoot<ApolloDriverConfig>({
driver: ApolloDriver,
subscriptions: {
'graphql-ws': {
path: '/graphql'
},
}
}),
ユーザが認証されているかどうかのチェックは、Subscriptionのオプションで指定できる
onConnect
コールバック関数の内部で実装できます。onConnect
は、SubscriptionClient
に渡されたconnectionParams
を第一引数として受け取れます。GraphQLModule.forRoot<ApolloDriverConfig>({
driver: ApolloDriver,
subscriptions: {
'subscriptions-transport-ws': {
onConnect: (connectionParams) => {
const authToken = connectionParams.authToken;
if (!isValid(authToken)) {
throw new Error('Token is not valid');
}
// extract user information from token
const user = parseToken(authToken);
// return user info to add them to the context later
return { user };
},
}
},
context: ({ connection }) => {
// connection.context will be equal to what was returned by the "onConnect" callback
},
}),
上記のプログラムにおける
authToken
は、接続が最初に確立されたときにクライアントから一度だけ送信されます。この接続で作成されたすべてのサブスクリプションは、同じauthToken
を持ち、したがって同じユーザー情報を持つことになります。注意
subscriptions-transport-ws
には、コネクションがonConnect
フェーズをスキップできるバグがあります。ユーザーがサブスクリプションを開始したときに onConnect
が呼び出されたと仮定せず、常にコンテキストが投入されていることをチェックする必要があります.graphql-ws
パッケージを使用している場合、onConnect
コールバックのシグネチャが若干異なります。GraphQLModule.forRoot<ApolloDriverConfig>({
driver: ApolloDriver,
subscriptions: {
'graphql-ws': {
onConnect: (context: Context<any>) => {
const { connectionParams, extra } = context;
// user validation will remain the same as in the example above
// when using with graphql-ws, additional context value should be stored in the extra field
extra.user = { user: {} };
},
},
},
context: ({ extra }) => {
// you can now access your additional context value through the extra field
},
});
Subscriptionを有効化するには、
subscription
プロパティをtrue
に設定します。GraphQLModule.forRoot<MercuriusDriverConfig>({
driver: MercuriusDriver,
subscription: true,
}),
メモ
また、設定のオブジェクトを渡すことで、カスタムエミッターの設定や、着信接続の検証などを行うことができます。
コードファーストのアプローチでSubscriptionを作成するには、
@Subscription()
デコレータ(@nestjs/graphql
パッケージからインポート)と、シンプルなAPIを提供するmercurius
パッケージのPubSub
クラスを使います。以下のSubscriptionハンドラは、
PubSub#asyncIterator
を呼び出すことで、イベントへのSubscriptionの処理を行います。このメソッドは、イベントトピックの名前に対応するtriggerName
という単一の引数を取ります。@Resolver((of) => Author)
export class AuthorResolver {
// ...
@Subscription((returns) => Comment)
commentAdded(@Context('pubsub') pubSub: PubSub) {
return pubSub.subscribe('commentAdded');
}
}
メモ
上記の例で使われているデコレータはすべて
@nestjs/graphql
パッケージから、PubSub
クラスはmercurius
パッケージからエクスポートされています。注意
PubSub
は、シンプルな発行と登録を実装するAPIを公開するクラスです。これによって、SDLでGraphQLスキーマの以下の部分が作成されます。
type Subscription {
commentAdded(): Comment!
}
Subscriptionは定義上、Subscriptionの名前をキーとする単一のトップレベルプロパティを持つオブジェクトを返すことに注意しましょう。この名前は、サブスクリプションハンドラメソッドの名前から継承されるか (すなわち、上記の
commentAdded
) 、あるいは以下に示すように@Subscription()
デコレーターの第二引数としてキー名を持つオプションを渡すことによって明示的に提供されます。@Subscription(returns => Comment, {
name: 'commentAdded',
})
subscribeToCommentAdded(@Context('pubsub') pubSub: PubSub) {
return pubSub.subscribe('commentAdded');
}
上記の構文は、前のコードサンプルと同じSDLを作成しますが、メソッド名とSubscriptionを分離できます。
さて、 イベントを発行するためには
PubSub#publish
メソッドを使用します。これは、オブジェクトグラフの一部が変更されたときにクライアント側の更新をトリガーするために、変異の中でよく使用されます。posts/post.resolver.ts
@Mutation(returns => Post)
async addComment(
@Args('postId', { type: () => Int }) postId: number,
@Args('comment', { type: () => Comment }) comment: CommentInput,
@Context('pubsub') pubSub: PubSub,
) {
const newComment = this.commentsService.addComment({ id: postId, comment });
await pubSub.publish({
topic: 'commentAdded',
payload: {
commentAdded: newComment
}
});
return newComment;
}
前述のように、Subscriptionは定義上値を返し、その値は型を持っています。
commentAdded
Subscriptionが生成したSDLを再度確認してみましょう。type Subscription {
commentAdded(): Comment!
}
これは、サブスクリプションが
commentAdded
というトップレベルのプロパティ名 を持つオブジェクトを返さなければならず、その値がComment
オブジェクトである ことを教えています。注意すべき重要な点は、PubSub#publish
メソッドによって発行されるイベントペイロードの形は、サブスクリプションから戻ると期待される値の形に対応していなければならないということです。したがって、上記の例では、
pubSub.publish({ topic: 'commentAdded', payload: { commentAdded: newComment })
ステートメントは、適切な形のペイロードでcommentAdded
イベントをパブリッシュします。これらの形状が一致しない場合、サブスクリプションは GraphQL 検証フェーズの間に失敗します。特定のイベントをフィルタリングするには、
filter
プロパティにフィルタリング関数を設定します。この関数は、配列フィルターに渡される関数と同じように動作します。この関数は 2 つの引数を取ります。言い換えれば、payload
はイベントのペイロード (イベントパブリッシャーから送られたもの) を含み、variables
はサブスクリプション要求の間に渡された任意の引数を取ります。それは、このイベントがクライアントのリスナーに公開されるべきかどうかを決定する真偽値を返します。@Subscription(returns => Comment, {
filter: (payload, variables) =>
payload.commentAdded.title === variables.title,
})
commentAdded(@Args('title') title: string, @Context('pubsub') pubSub: PubSub) {
return pubSub.subscribe('commentAdded');
}
注入されたプロバイダにアクセスする必要がある場合(例えば、データを検証するために外部サービスを活用するなど)、以下の構造を使用します。
@Subscription(returns => Comment, {
filter(this: AuthorResolver, payload, variables) {
// "this" refers to an instance of "AuthorResolver"
return payload.commentAdded.title === variables.title;
}
})
commentAdded(@Args('title') title: string, @Context('pubsub') pubSub: PubSub) {
return pubSub.subscribe('commentAdded');
}
NestJSで同等のSubscriptionを作成するために、
@Subscription()
デコレータを使います。const pubSub = new PubSub();
@Resolver('Author')
export class AuthorResolver {
// ...
@Subscription()
commentAdded(@Context('pubsub') pubSub: PubSub) {
return pubSub.subscribe('commentAdded');
}
}
コンテキストと引数に基づいて特定のイベントをフィルタリングするには、
filter
プロパティを設定します。@Subscription('commentAdded', {
filter: (payload, variables) =>
payload.commentAdded.title === variables.title,
})
commentAdded(@Context('pubsub') pubSub: PubSub) {
return pubSub.subscribe('commentAdded');
}
注入されたプロバイダにアクセスする必要がある場合(例えば、データを検証するために外部サービスを活用するなど)、以下の構造を使用します。
@Subscription('commentAdded', {
filter(this: AuthorResolver, payload, variables) {
// "this" refers to an instance of "AuthorResolver"
return payload.commentAdded.title === variables.title;
}
})
commentAdded(@Context('pubsub') pubSub: PubSub) {
return pubSub.subscribe('commentAdded');
}
最後に、GraphQLの型定義ファイルの更新を行います。
type Author {
id: Int!
firstName: String
lastName: String
posts: [Post]
}
type Post {
id: Int!
title: String
votes: Int
}
type Query {
author(id: Int!): Author
}
type Comment {
id: String
content: String
}
type Subscription {
commentAdded(title: String!): Comment
}
これで
commentAdded(title: String!): Comment
Subscriptionの実装は完了です。上記の例では、デフォルトのPubSubエミッターを使いました。あるいは、カスタムのPubSub実装を提供できます。
GraphQLModule.forRoot<MercuriusDriverConfig>({
driver: MercuriusDriver,
subscription: {
emitter: require('mqemitter-redis')({
port: 6579,
host: '127.0.0.1',
}),
},
});
ユーザーが認証されているかどうかのチェックは、サブスクリプションのオプションで指定できる
verifyClient
コールバック関数の内部で行うことができます。verifyClient
は第一引数としてinfo
オブジェクトを受け取り、それを使ってリクエストのヘッダを取得します。GraphQLModule.forRoot<MercuriusDriverConfig>({
driver: MercuriusDriver,
subscription: {
verifyClient: (info, next) => {
const authorization = info.req.headers?.authorization as string;
if (!authorization?.startsWith('Bearer ')) {
return next(false);
}
next(true);
},
}
}),
Last modified 10mo ago