概要
GraphQL には Subscription
という操作が存在し、Mutation
によりデータソースの操作をした際に、クライアント側でサーバーから発行された変更情報を受け取ることができます。ユースケースとしてはチャットアプリ等が考えられます。
今回は Apollo サーバーを利用して、Subscription の動作を確認していきたいと思います。
Apollo とは
Apollo はデータグラフを構築するためのプラットフォームであり、アプリケーションクライアントをバックエンドにシームレスに接続する GraphQL のオープンソースです。Apollo プラットフォームは、既存のアーキテクチャに組み込むことも可能で、システム全体 (サーバー/クライアント) で GraphQL をスケールするのに役立つような本番環境用のツールも備えています。
環境構築
新規でプロジェクトディレクトリを作成し、必要なディレクトリ・ファイル群を作成、最後に npm 経由で apollo-server をインストールします。
$ mkdir subscription
$ cd subscription/
$ mkdir resolver
$ touch index.js db.js schema.js resolver/{Query.js,Mutation.js,Subscription.js}
$ npm init
$ npm install apollo-server@2.19.0 graphql@15.4.0
最終的に以下のようなディレクトリ構成となります (node_modules
は除いています)。
$ tree -L 2 -I node_modules
.
├── db.js
├── index.js
├── package-lock.json
├── package.json
├── resolver
│ ├── Mutation.js
│ ├── Query.js
│ └── Subscription.js
└── schema.js
それでは実装に入っていきます。
スキーマの定義
まずはスキーマの定義です。以下のように schema.js
内にスキーマを定義していきます。なお gql
というタグ付きテンプレートリテラルを利用し、GraphQL に関連する定義を JavaScript の文字列として表現しています。
const {gql} = require("apollo-server");
const typeDefs = gql`
type Query {
posts(query: String): [Post!]!
}
type Mutation {
createPost(data: CreatePostInput!): Post!
deletePost(id: ID!): Post!
updatePost(id: ID!, data: UpdatePostInput!): Post!
}
type Subscription {
post: PostSubscriptionPayload!
}
input CreatePostInput {
title: String!
author: String!
}
input UpdatePostInput {
title: String
author: String!
}
type Post {
id: ID!
title: String!
author: String!
}
enum MutationType {
CREATED
UPDATED
DELETED
}
type PostSubscriptionPayload {
mutation: MutationType!
data: Post!
}
`
module.exports = typeDefs;
テストデータの用意
次に、動作確認のためのテストデータを用意します。今回は動作確認用なので、実際の RDBMS や NoSQL といったデータベースサーバーを用意するのではなく、JavaScript のファイルを利用します。
const posts = [
{
id: "1",
title: "Do the right things",
author: "Milan"
},
{
id: "2",
title: "Dealing with ambiguity",
author: "Katie"
},
{
id: "3",
title: "Moving forward",
author: "Issac"
}
];
const db = {
posts
};
module.exports = db;
リゾルバの実装
テストデータの用意ができたら、次はリゾルバの実装に取り掛かります。なお、リゾルバ関数には固定の役割を持つ以下の 4 つの引数があり、よく利用されます。
引数 | 説明 |
---|---|
parent | 親リゾルバからの戻り値 |
args | フィールドに提供された全ての GraphQL 引数を含むオブジェクト |
context | 特定の操作に対して実行される全てのリゾルバ間で共有される。これを利用して認証情報やデータソースへのアクセスなどの操作ごとの状態を共有できる |
info | フィールド名、ルートからフィールドへのパスなど、操作の実行状態に関する情報 |
Query リゾルバ
Query のリゾルバ関数では引数の有無で条件分岐をします。発行したクエリに引数がなかった時は存在するデータを全て返し、引数があった場合は title もしくは author の値と一致したもののみ返却されます。
const Query = {
posts(parent, args, { db }, info) {
// List posts if there is no arg
if (!args.query) {
return db.posts;
} else {
return db.posts.filter((post) => {
const isTitleMatch = post.title.toLowerCase().includes(args.query.toLowerCase());
const isAuthorMatch = post.author.toLowerCase().includes(args.query.toLowerCase());
return isTitleMatch || isAuthorMatch;
});
}
}
}
module.exports = Query;
Mutation リゾルバ
Mutation のリゾルバ関数ではデータベースの更新 (createPost/updatePost/deletePost) 及び Subscription チャネルへの Publish をおこなっています。
const Mutation = {
createPost(parent, args, {db, pubsub}, info) {
const postNumTotal = String(db.posts.length + 1);
const post = {
id: postNumTotal,
...args.data
}
db.posts.push(post);
pubsub.publish("post", {
post: {
mutation: "CREATED",
data: post
}
});
return post;
},
updatePost(parent, args, {db, pubsub}, info) {
const {id, data} = args;
const post = db.post.find((post) => post.id === id);
if (!post) {
throw new Error("Post not found")
}
if (typeof data.title === "string" && typeof data.author === "string") {
post.title = data.title;
post.author = data.author;
pubsub.publish("post", {
post: {
mutation: "UPDATED",
data: post
}
});
}
return post;
},
deletePost(parent, args, {db, pubsub}, info) {
const post = db.posts.find((post) => post.id === args.id);
const postIndex = db.posts.findIndex((post) => post.id === args.id);
if (postIndex === -1) {
throw new Error("Post not found");
}
db.posts.splice(postIndex, 1);
pubsub.publish("post", {
post: {
mutation: "DELETED",
data: post
}
});
return post;
}
}
Subscription リゾルバ
Subscription のリゾルバ関数では Subscription のイベントを非同期でリッスンする AsyncIterator
を戻り値として指定する必要があります。引数には Mutation リゾルバ関数内の pubsub.publish()
で指定したトピック名である post
が入ります。
const Subscription = {
post: {
subscribe(parent, args, {pubsub}, info) {
return pubsub.asyncIterator("post");
}
}
}
module.exports = Subscription;
Apollo サーバーの起動
ここまでできたら後は Apollo サーバーを起動するための処理を書くだけです。今回は Subscription を利用するため、PubSub
のインスタンスを作成します。
const {ApolloServer, PubSub} = require("apollo-server");
const db = require("./db");
const Query = require("./resolver/Query");
const Mutation = require("./resolver/Mutation");
const Subscription = require("./resolver/Subscription");
const typeDefs = require("./schema");
//Create PubSub instance to subscribe
const pubsub = new PubSub();
//Create ApolloServer instance
const server = new ApolloServer({
typeDefs: typeDefs,
resolvers: {
Query,
Mutation,
Subscription
},
context: {
db,
pubsub
}
});
//Launch the server
server.listen().then(({ url, subscriptionsUrl }) => {
console.log(`Server ready at ${url}`);
console.log(`Subscriptions ready at ${subscriptionsUrl}`);
});
動作の確認
それでは実際に動作確認をします。まずはサーバーを起動します。
$ node index.js
Server ready at http://localhost:4000/
Subscriptions ready at ws://localhost:4000/graphql
次にブラウザから http://localhost:4000/ にアクセスしますが、今回は Apollo サーバーを利用しているため、Prisma によって作成される GraphQL Playground を使用する形となります。基本的な使い方は GraphQL IDE と同じです。
まず、Query でテストデータの読み込みが行えるか確認しましょう。
query {
posts {
id
title
author
}
}
{
"data": {
"posts": [
{
"id": "1",
"title": "Do the right things",
"author": "Milan"
},
{
"id": "2",
"title": "Dealing with ambiguity",
"author": "Katie"
},
{
"id": "3",
"title": "Moving forward",
"author": "Issac"
}
]
}
}
用意したテストデータが返ってきていますね。
それでは本題の Subscription です。以下のクエリを実行すると Subscription が開始されます。
subscription {
post {
mutation
data {
id
title
author
}
}
}
実際に WebSocket で通信を行なっていることも確認できます。
次に、別のタブを開いて以下のように新規でデータを挿入してみます。
mutation {
createPost(data: {
title: "A new world"
author: "Loyd"
}) {
id
title
author
}
}
{
"data": {
"createPost": {
"id": "4",
"title": "A new world",
"author": "Loyd"
}
}
}
すると、Subscription 側にメッセージが Publish されていることが確認できます。
同じように更新や削除もやっていきます。
更新
mutation {
updatePost(id: 4, data: {
title: "Hello world"
author: "Loyd"
}) {
id
title
author
}
}
{
"data": {
"updatePost": {
"id": "4",
"title": "Hello world",
"author": "Loyd"
}
}
}
削除
mutation {
deletePost(id: 4) {
id
title
author
}
}
{
"data": {
"deletePost": {
"id": "4",
"title": "Hello world",
"author": "Loyd"
}
}
}
すると Subscription 側ではそれらに対応する内容が Publish されていることがわかります。