# HOW TO GRAPHQLでGraphQLの勉強をはじめました その⑨
# Realtime GraphQL Subscriptions
GraphQLのサブスクリプションを使って新しいLinkが作られた時とupvotedされた時にリアルタイムにそれを受け取れるようにしていく感じです、と。
# What are GraphQL subscriptions?
そもそもGraphQLにおけるサブスクリプションって?という話ですが、大体WebSocketsで実装されることが多いそうで、サーバーとクライアントとの接続を維持することでリアルタイムに〜という形。イベントが発生した場合にサーバー側からサブスクライブしているクライアント側に通知する。
# Subscriptions with Prisma
Prismaにはout-of-the-boxにSubscriptionをサポートしていますよ、と。
Prismaのデータモデルにおけるイベントに対応
- A new model is created: 登録
- An existing model updated: 更新
- An existing model is deleted: 削除
Prismaクライアントで $subscribe メソッドを使う感じらしい。
# Subscribing to new Link elements
ということで、Linkが新しく作られた時の通知をしていきます。まずはスキーマ定義からです。Subscriptionタイプを追加していきます。
type Subscription {
newLink: Link
}
次にnewLinkフィールドに対するリゾルバを書いていきます。クエリやミューテーションとは少し異なるらしい。
- データを直接受け取るのではなく、AsyncIteratorがGraphQLサーバーからクライアントにプッシュされる
- Subscriptionリゾルバはsubscribeのバリューとしてオブジェクトの中にラップされる。resolveというフィールドを用意することでAsyncIteratorによって取得したデータを扱うことができる
んー、なんか少し難解ですが、コード書いて理解していきましょう。ということでまずはSubscriptionのリゾルバ用のjsファイルを作っていきます。
touch src/resolvers/Subscription.js
リゾルバファンクションは👇のような感じ、と。
function newLinkSubscribe(parent, args, context, info) {
return context.prisma.$subscribe.link({ mutation_in: ['CREATED'] }).node()
}
const newLink = {
subscribe: newLinkSubscribe,
resolve: payload => {
return payload
},
}
module.exports = {
newLink,
}
このコードそのものはPrismaの$subscribeを使うととてもシンプルで直感的。contextにPrismaがあるだけで便利やわ〜
そして、index.jsにSubscriptionを足してあげて、
const Query = require('./resolvers/Query')
const Mutation = require('./resolvers/Mutation')
const Subscription = require('./resolvers/Subscription')
const User = require('./resolvers/User')
const Link = require('./resolvers/Link')
const resolvers = {
Query,
Mutation,
Subscription,
User,
Link,
}
サーバーを立ち上げてテストしていきます。
SubscriptionのリクエストをPlaygroundでするとクルクル回ってて接続しっぱなし感が出てて良いですね 😃
ということで、別の窓で認証をカマしながら、Mutationのリクエストを投げてデータを登録すると、
👇のように www.graphqlweekly.com というリンクがAliceさんによって登録された旨が通知されています。
# Adding a voting feature
# Implementing a vote mutation
続いてupvoteな機能を追加していきます。
datamodel.prismaにVoteに関する定義を追加していきます。LinkがどれだけVoteされているか、Userが行ったVote、そしてVoteそのもの。
type Link {
id: ID! @id
createdAt: DateTime! @createdAt
description: String!
url: String!
postedBy: User
votes: [Vote!]
}
type User {
id: ID! @id
name: String!
email: String! @unique
password: String!
links: [Link!]!
votes: [Vote!]!
}
type Vote {
id: ID! @id
linke: Link!
user: User!
}
で、これをPrismaにデプロイしていきます。あー、タイポみつけちゃった。Voteの定義のlinkがlinkeになってる…。んま、ここ直して再度デプロイします。笑
$ prisma deploy
Deploying service `hackernews-node` to stage `dev` to server `prisma-us1` 1.6s
Changes:
Vote (Type)
+ Created type `Vote`
+ Created field `id` of type `ID!`
+ Created field `linke` of type `Link!`
+ Created field `user` of type `User!`
Link (Type)
+ Created field `votes` of type `[Vote!]!`
User (Type)
+ Created field `votes` of type `[Vote!]!`
UserToVote (Relation)
+ Created an inline relation between `User` and `Vote` in the column `user` of table `Vote`
LinkToVote (Relation)
+ Created an inline relation between `Link` and `Vote` in the column `linke` of table `Vote`
Applying changes 853ms
Generating schema 32ms
めでたしめでたし。
$ prisma deploy
Deploying service `hackernews-node` to stage `dev` to server `prisma-us1` 1.7s
Changes:
Vote (Type)
- Deleted field `linke`
+ Created field `link` of type `Link!`
LinkToVote (Relation)
Applying changes 940ms
Generating schema 29ms
post-deploy hookによって手動でprisma generateを再度行う必要はありません。
そして、スキーマ駆動開発ということで、schema.graphql。Mutationにvoteを追加します。
type Mutation {
post(url: String!, description: String!): Link!
signup(email: String!, password: String!, name: String!): AuthPayload
login(email: String!, password: String!): AuthPayload
vote(linkId: ID!): Vote
}
そして、schema.graphqlにVoteのtypeを追加します。
type Vote {
id: ID!
link: Link!
user: User!
}
そして、linkにある全てのvoteをクエリできるようにするためにLinkにもvotesを追加します。
type Link {
id: ID!
description: String!
url: String!
postedBy: User
votes: [Vote!]!
}
ということでschema.graphqlの定義の追加が終わり、Mutationのリゾルバの実装に入ります。
async function vote(parent, args, context, info) {
// postのリゾルバと同様にgetUserIdを使って。もしvalidなJWTでなければException
const userId = getUserId(context)
// prisma.$existsは初めて出てきたけど、where filterを使ってそのデータが存在するかを確認。at least one element in the databaseかどうかをみて、既に存在するのであれば今回はエラー
const voteExists = await context.prisma.$exists.vote({
user: { id: userId },
link: { id: args.linkId },
})
if (voteExists) {
throw new Error(`Already voted for link: ${args.linkId}`)
}
// まだリンクへのvoteがされていない場合はcreateVoteする。新しく作られるVoteはUserとLinkと関連付けされる
return context.prisma.createVote({
user: { connect: { id: userId } },
link: { connect: { id: args.linkId } },
})
}
moduleにおけるvoteリゾルバのステートメントへの追加を忘れずに。
module.exports = {
post,
signup,
login,
vote,
}
ということでGraphQLのスキーマに👇こんなのが必要になります。
- votes on Link
- user on Vote
- link on Vote
Link.jsを開いてvotesファンクションを追加します。module.exportsへの追加も忘れずに。
function postedBy(parent, args, context) {
return context.prisma.link({ id: parent.id }).postedBy()
}
function votes(parent, args, context) {
return context.prisma.link({ id: parent.id }).votes()
}
module.exports = {
postedBy,
votes,
}
Vote.jsを作ってから
touch src/resolvers/Vote.js
👇以下のような実装をしていきます。
function link(parent, args, context) {
return context.prisma.vote({ id: parent.id }).link()
}
function user(parent, args, context) {
return context.prisma.vote({ id: parent.id }).user()
}
module.exports = {
link,
user,
}
そして、index.jsでVote.jsをインポートして、リゾルバに含めます。
const Query = require('./resolvers/Query')
const Mutation = require('./resolvers/Mutation')
const Subscription = require('./resolvers/Subscription')
const User = require('./resolvers/User')
const Link = require('./resolvers/Link')
const Vote = require('./resolvers/Vote')
const resolvers = {
Query,
Mutation,
Subscription,
User,
Link,
Vote,
}
ってことで、voteのmutationが出来ました!
# Subscribing to new votes
続いてvoteに関するSubscriptionの実装をします。 Subscription.jsを開いて以下のように。なんというかLinkとまんまコピペというか。
function newLinkSubscribe(parent, args, context, info) {
return context.prisma.$subscribe.link({ mutation_in: ['CREATED'] }).node()
}
const newLink = {
subscribe: newLinkSubscribe,
resolve: payload => {
return payload
},
}
function newVoteSubscribe(parent, args, context, info) {
return context.prisma.$subscribe.vote({ mutation_in: ['CREATED'] }).node()
}
const newVote = {
subscribe: newVoteSubscribe,
resolve: payload => {
return payload
},
}
module.exports = {
newLink,
newVote,
}
ってことでVoteのSubscriptionもテストしていきます。 サーバーを再起動してから👇のようなsubscriptionリクエストを送ります。
subscription {
newVote {
id
link {
url
description
}
user {
name
email
}
}
}
と思ったらサーバー起動時にエラーが…w
$ node src/index.js
/Users/eijishinohara/hackernews-node/node_modules/graphql-tools/dist/generate/addResolveFunctionsToSchema.js:79
throw new _1.SchemaError(typeName + "." + fieldName + " defined in resolvers, but not in schema");
^
[Error: Subscription.newVote defined in resolvers, but not in schema]
あれ、リゾルバにはあるけどschema.graphqlにnewVoteが無いぞ、と。見逃してたっぽい、、、ってことで👇を足して再起動します。
type Subscription {
newLink: Link
newVote: Vote
}
あれ、、、今度はLink.votesがないぞ、と。。
$ node src/index.js
/Users/eijishinohara/hackernews-node/node_modules/graphql-tools/dist/generate/addResolveFunctionsToSchema.js:79
throw new _1.SchemaError(typeName + "." + fieldName + " defined in resolvers, but not in schema");
^
[Error: Link.votes defined in resolvers, but not in schema]
👇足して再起動します!
votes: [Vote!]!
で、voteのmutationを発行したら、、、。あれ、prisma deployしたのにまだlinkeが無いとか言われてる…。
Error: Variable '$data' expected value of type 'VoteCreateInput!' but got: {"user":{"connect":{"id":"ck7wsgfxg50jn0981qfwdhnqc"}},"link":{"connect":{"id":"ck7y6gqk2cjak0934q0q1sm31"}}}. Reason: 'linke' Expected non-null value, found null. (line 1, column 11):
mutation ($data: VoteCreateInput!) {
^
at BatchedGraphQLClient.<anonymous> (/Users/eijishinohara/hackernews-node/node_modules/http-link-dataloader/dist/src/BatchedGraphQLClient.js:77:35)
at step (/Users/eijishinohara/hackernews-node/node_modules/http-link-dataloader/dist/src/BatchedGraphQLClient.js:40:23)
at Object.next (/Users/eijishinohara/hackernews-node/node_modules/http-link-dataloader/dist/src/BatchedGraphQLClient.js:21:53)
at fulfilled (/Users/eijishinohara/hackernews-node/node_modules/http-link-dataloader/dist/src/BatchedGraphQLClient.js:12:58)
at processTicksAndRejections (internal/process/task_queues.js:93:5)
なんか👇のPrismaのForumみてたら同じような人いるのかな。。。 Prisma deploy does not deploy and does not show any errors https://www.prisma.io/forum/t/prisma-deploy-does-not-deploy-and-does-not-show-any-errors/6559/3
履歴みるとlinkeは消えてるけど、
クエリには残ってるしなぁ、、
Prismaのコンソール上でクエリ投げても👇のようにlinkeじゃないの?って言われるし。。。
[
{
"message": "Cannot query field 'link' on type 'Vote'. Did you mean 'linke'? (line 4, column 5):\n link {\n ^",
"locations": [
{
"line": 4,
"column": 5
}
]
}
]
ってことで、一回datamodel.prismaを元の上に戻してprisma deployしてから
a$ prisma deploy
Deploying service `hackernews-node` to stage `dev` to server `prisma-us1` 965ms
Changes:
Link (Type)
- Deleted field `votes`
User (Type)
- Deleted field `links`
- Deleted field `votes`
Vote (Type)
- Deleted type `Vote`
UserToVote (Relation)
- Deleted relation between undefined and undefined
LinkToVote (Relation)
- Deleted relation between undefined and undefined
Applying changes 7.1s
Generating schema 23ms
もう一度追加してデプロイしてみます。。。これプロダクション環境でこんな不具合起こったら冷や汗止まらないんだろうな。。。。
$ prisma deploy
Deploying service `hackernews-node` to stage `dev` to server `prisma-us1` 938ms
Changes:
Vote (Type)
+ Created type `Vote`
+ Created field `id` of type `ID!`
+ Created field `link` of type `Link!`
+ Created field `user` of type `User!`
Link (Type)
+ Created field `votes` of type `[Vote!]!`
User (Type)
+ Created field `links` of type `[Link!]!`
+ Created field `votes` of type `[Vote!]!`
UserToVote (Relation)
+ Created an inline relation between `User` and `Vote` in the column `user` of table `Vote`
LinkToVote (Relation)
+ Created an inline relation between `Link` and `Vote` in the column `link` of table `Vote`
Applying changes 843ms
Generating schema 29ms
そしてMutationを走らせると既にvoteされてるよエラーが。。。んーー。。。
Error: Already voted for link: ck7y6gqk2cjak0934q0q1sm31
at vote (/Users/eijishinohara/hackernews-node/src/resolvers/Mutation.js:63:11)
at processTicksAndRejections (internal/process/task_queues.js:93:5)
Error: Cannot return null for non-nullable field Vote.link.
at completeValue (/Users/eijishinohara/hackernews-node/node_modules/graphql/execution/execute.js:560:13)
at completeValueCatchingError (/Users/eijishinohara/hackernews-node/node_modules/graphql/execution/execute.js:495:19)
at resolveField (/Users/eijishinohara/hackernews-node/node_modules/graphql/execution/execute.js:435:10)
at executeFields (/Users/eijishinohara/hackernews-node/node_modules/graphql/execution/execute.js:275:18)
at collectAndExecuteSubfields (/Users/eijishinohara/hackernews-node/node_modules/graphql/execution/execute.js:713:10)
at completeObjectValue (/Users/eijishinohara/hackernews-node/node_modules/graphql/execution/execute.js:703:10)
at completeValue (/Users/eijishinohara/hackernews-node/node_modules/graphql/execution/execute.js:591:12)
at /Users/eijishinohara/hackernews-node/node_modules/graphql/execution/execute.js:492:16
at processTicksAndRejections (internal/process/task_queues.js:93:5)
at async Promise.all (index 0)
Error: Already voted for link: ck7wtmk2451wt0981jym4fc5x
at vote (/Users/eijishinohara/hackernews-node/src/resolvers/Mutation.js:63:11)
at processTicksAndRejections (internal/process/task_queues.js:93:5)
Prismaサーバーがなんか怪しいんだけど、再起動とか出来ないのかな。。。Servicesをクリックしてもクルクル回ってるだけで反応しないし。。。。
ってことで、Vote.jsでマズいとことか見つけたりして、なんだかんだでようやくSubscriptionしたvoteのMutationを受け取れるようになりました!
👇がMutation
👇が色々エラー出てるけど、下にあるのがSubscribeで受け取ったデータ。
んーー、、なんか今日はつまづいちゃったので結構時間かかったな。。。^^;