# 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 メソッドを使う感じらしい。

ということで、Linkが新しく作られた時の通知をしていきます。まずはスキーマ定義からです。Subscriptionタイプを追加していきます。

type Subscription {
  newLink: Link
}

次にnewLinkフィールドに対するリゾルバを書いていきます。クエリやミューテーションとは少し異なるらしい。

  1. データを直接受け取るのではなく、AsyncIteratorがGraphQLサーバーからクライアントにプッシュされる
  2. 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でするとクルクル回ってて接続しっぱなし感が出てて良いですね 😃

Subscription

ということで、別の窓で認証をカマしながら、Mutationのリクエストを投げてデータを登録すると、

Mutation

👇のように www.graphqlweekly.com というリンクがAliceさんによって登録された旨が通知されています。

Push

# 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は消えてるけど、 linke

クエリには残ってるしなぁ、、 Query

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

Mutation

👇が色々エラー出てるけど、下にあるのがSubscribeで受け取ったデータ。

Subscribe


んーー、、なんか今日はつまづいちゃったので結構時間かかったな。。。^^;

このエントリーをはてなブックマークに追加

Algolia検索からの流入のみConversionボタン表示