@znz blog

ZnZ の memo のようなもの

Shinosaka.rb #27 (GraphQL) に参加した

| Comments

Shinosaka.rb #27 に参加しました。 Shinosaka.rb 自体は初参加でした。

今回は GraphQL の解説と node と rails でのハンズオンでした。

以下、メモです。

メモ

動作確認環境

ソースコード

途中での graphiql での確認方法は下の作業メモの方に書いてあるので、あわせて参照してください。

感想

作業メモが長く続くので、先に感想を書いておきます。

node の方はエラーも json で帰ってきてブラウザーで見えて開発環境として使いやすそうな感じでしたが、 graphiql-rails の方はエラーの時に SyntaxError: Unexpected token < in JSON at position 0 とだけ出て、 詳細はサーバー側のログをみないといけないので、node に比べるとちょっと使いづらいかもしれない、と思いました。

GraphQL 自体は色々と利点も多そうだと思いましたが、サーバー側は結局 REST とは別に作り込まないといけなさそうで、 アクセス権限などを考えると、既存のアプリケーションで簡単に置き換えられるものでもなさそうかな、と思いました。

実際に使ってみる

step 1

index.js として以下の内容を作成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
'use strict'

const { graphql, buildSchema } = require('graphql')

const schema = buildSchema(`
type Query {
  foo: String
}

type Schema {
  query: Query
}
`)

const resolvers = {
  foo: () => 'bar',
}

const query = `
query myQuery {
  foo
}
`

graphql(schema, query, resolvers)
  .then(result => console.log(result))
  .catch(err => console.log(err))

実行結果:

1
2
% node.index.js
{ data: { foo: 'bar' } }

step 2

1
2
3
4
5
6
7
8
9
10
11
const schema = buildSchema(`
type Query {
  id: ID,
  title: String,
  watched: Boolean,
}

type Schema {
  query: Query
}
`)
1
2
3
4
5
const resolvers = {
  id: () => 1,
  title: () => 'bar',
  watched: () => true,
}
1
2
3
4
5
6
7
const query = `
query myQuery {
  id,
  title,
  watched,
}
`
1
2
% node index.js
{ data: { id: '1', title: 'bar', watched: true } }

query から watched を削ると { data: { id: '1', title: 'bar' } } になる。

step 3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const schema = buildSchema(`
type Video {
  id: ID,
  title: String,
  watched: Boolean,
}

type Query {
  video: Video
}

type Schema {
  query: Query
}
`)
1
2
3
4
5
6
7
const resolvers = {
  video: () =>({
    id: 1,
    title: 'bar',
    watched: true
  }),
}
1
2
3
4
5
6
7
8
9
const query = `
query myQuery {
  video {
    id,
    title,
    watched,
  }
}
`

step 4

videos 対応

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const schema = buildSchema(`
type Video {
  id: ID,
  title: String,
  watched: Boolean,
}

type Query {
  video: Video,
  videos: [Video],
}

type Schema {
  query: Query
}
`)
1
2
3
4
5
6
7
8
9
10
11
const videoA = {
  id: 1,
  title: 'title1',
  watched: true
}
const videoB = {
  id: 2,
  title: 'title2',
  watched: false
}
const videos = [videoA, videoB]
1
2
3
4
5
6
7
8
const resolvers = {
  video: () => ({
    id: 1,
    title: 'bar',
    watched: true,
  }),
  videos: () => videos,
}
1
2
3
4
5
6
7
8
9
const query = `
query myQuery {
  videos {
    id,
    title,
    watched,
  }
}
`
1
2
% node index.js
{ data: { videos: [ [Object], [Object] ] } }

step 5

yarn add express express-graphql or npm install express express-graphql

require('graphql') の行の上に追加:

1
2
const express = require('express')
const graphqlHTTP = require('express-graphql')

追加:

1
2
const PORT = process.env.PORT || 3000
const server = express()

末尾の graphql の呼び出しを置き換え:

1
2
3
4
5
6
7
8
9
server.use('/graphql', graphqlHTTP({
  schema,
  graphiql: true,
  rootValue: resolvers,
}))

server.listen(PORT, () => {
  console.log(`Listening on http://localhost:${PORT}`)
})

http://localhost:3000/graphql を開いて

1
2
3
4
5
6
7
{
  videos {
    id,
    title,
    watched,
  }
}

などを試す。

右上の Docs でスキーマも見える。

step 6

graphql の require のところを書き換え:

1
2
3
4
5
6
7
const {
  GraphQLSchema,
  GraphQLObjectType,
  GraphQLID,
  GraphQLString,
  GraphQLBoolean,
} = require('graphql')

buildSchema を書き換え:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const videoType = new GraphQLObjectType({
  name: 'Video',
  description: 'video',
  fields: {
    id: {
      type: GraphQLID,
      description: 'id of video',
    },
    title: {
      type: GraphQLString,
      description: 'title of video'
    },
    watched: {
      type: GraphQLBoolean,
      description: 'has watched'
    }
  }
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const queryType = new GraphQLObjectType({
  name: 'QueryType',
  description: 'root query',
  fields: {
    video: {
      type: videoType,
      resolve: () => new Promise(resolve => {
        resolve({
          id: 1,
          title: 'title1',
          watched: true,
        })
      })
    }
  }
})
1
2
3
const schema = new GraphQLSchema({
  query: queryType,
})

node index.js を再起動して http://localhost:3000/graphql

1
2
3
4
5
6
7
{
  video {
    id
    title
    watched
  }
}

などを試す。

休憩

id: 1 だけ欲しいときなど

videos を移動して data.js を作成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
'use strict'

const videoA = {
  id: 1,
  title: 'title1',
  watched: true
}
const videoB = {
  id: 2,
  title: 'title2',
  watched: false
}
const videos = [videoA, videoB]

const getVideoById = (id) => new Promise(resolve => {
  const [video] = videos.filter(v => (v.id + '') === id)
  resolve(video)
})

exports.getVideoById = getVideoById
1
const { getVideoById } = require('./data')
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const queryType = new GraphQLObjectType({
  name: 'QueryType',
  description: 'root query',
  fields: {
    video: {
      type: videoType,
      args: {
        id: {
          type: GraphQLID,
          description: 'id of video',
        },
      },
      resolve: (_, args) => getVideoById(args.id)
    }
  }
})

node index.js を再起動して http://localhost:3000/graphql

1
2
3
4
5
6
7
{
  video(id: 2) {
    id
    title
    watched
  }
}

などを試す。

id を必須にしたい

require('graphql') のところに GraphQLNonNull, を追加。

type: new GraphQLNonNull(GraphQLID), にする。

1
2
3
4
5
6
7
{
  "errors": [
    {
      "message": "Unknown operation named \"null\"."
    }
  ]
}

になってしまったが、 getVideos の追加の後、もう一度試したら動いたので謎。 謎のエラーが発生した時は Prettify を押すとエラーが起きなくなるみたい。

1
2
3
4
5
6
7
{
  video {
    id
    title
    watched
  }
}

などを試すと以下のように意図通りのエラーになる。

1
2
3
4
5
6
7
8
9
10
11
12
13
{
  "errors": [
    {
      "message": "Field \"video\" argument \"id\" of type \"ID!\" is required but not provided.",
      "locations": [
        {
          "line": 2,
          "column": 3
        }
      ]
    }
  ]
}

配列

GraphQLList を追加

data.js に追加:

1
const getVideos = () => new Promise(resolve => resolve(videos))
1
exports.getVideos = getVideos

index.js:

1
const { getVideoById, getVideos } = require('./data')
1
2
3
4
videos: {
  type: new GraphQLList(videoType),
  resolve: getVideos,
},

node index.js を再起動して http://localhost:3000/graphql

1
2
3
4
5
6
7
{
  videos {
    id
    title
    watched
  }
}

などを試す。

mutation

schema に mutation を追加:

1
2
3
4
const schema = new GraphQLSchema({
  query: queryType,
  mutation: mutationType,
})

schema の上に追加:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const mutationType = new GraphQLObjectType({
  name: 'Mutation',
  description: 'Mutation type',
  fields: {
    createVideo: {
      type: videoType,
      args: {
        title: {
          type: new GraphQLNonNull(GraphQLString),
          description: 'title of video',
        },
      },
      resolve: (_, args) => {
        return createVideo(args)
      }
    },
  },
})

data.js:

1
2
3
4
5
6
7
8
9
10
const createVideo = ({ title }) => {
  const maxId = Math.max.apply(null, videos.map(v => v.id))
  const watched = false
  const video = {
    id: maxId + 1,
    title,
    watched,
  }
  return video
}

(videos への push が抜けていた。)

1
exports.createVideo = createVideo

index.js:

1
const { getVideoById, getVideos, createVideo } = require('./data')

node index.js を再起動して http://localhost:3000/graphql

1
2
3
4
5
6
7
mutation M {
  createVideo(title: "hoge") {
    id
    title
    watched
  }
}

を試す。

この時点の index.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
'use strict'

const express = require('express')
const graphqlHTTP = require('express-graphql')
const {
  GraphQLSchema,
  GraphQLObjectType,
  GraphQLID,
  GraphQLString,
  GraphQLBoolean,
  GraphQLNonNull,
  GraphQLList,
} = require('graphql')
const { getVideoById, getVideos, createVideo } = require('./data')

const PORT = process.env.PORT || 3000
const server = express()

/*
video
  id
  title
  watched
*/

const videoType = new GraphQLObjectType({
  name: 'Video',
  description: 'video',
  fields: {
    id: {
      type: GraphQLID,
      description: 'id of video',
    },
    title: {
      type: GraphQLString,
      description: 'title of video'
    },
    watched: {
      type: GraphQLBoolean,
      description: 'has watched'
    }
  }
})

const queryType = new GraphQLObjectType({
  name: 'QueryType',
  description: 'root query',
  fields: {
    videos: {
      type: new GraphQLList(videoType),
      resolve: getVideos,
    },
    video: {
      type: videoType,
      args: {
        id: {
          type: new GraphQLNonNull(GraphQLID),
          description: 'id of video',
        }
      },
      resolve: (_, args) => getVideoById(args.id)
    }
  }
})

const mutationType = new GraphQLObjectType({
  name: 'Mutation',
  description: 'Mutation type',
  fields: {
    createVideo: {
      type: videoType,
      args: {
        title: {
          type: new GraphQLNonNull(GraphQLString),
          description: 'title of video',
        },
      },
      resolve: (_, args) => {
        return createVideo(args)
      }
    },
  },
})

const schema = new GraphQLSchema({
  query: queryType,
  mutation: mutationType,
})

server.use('/graphql', graphqlHTTP({
  schema,
  graphiql: true,
}))

server.listen(PORT, () => {
  console.log(`Listening on http://localhost:${PORT}`)
})

data.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
'use strict'

const videoA = {
  id: 1,
  title: 'title1',
  watched: true
}
const videoB = {
  id: 2,
  title: 'title2',
  watched: false
}
const videos = [videoA, videoB]

const getVideos = () => new Promise(resolve => resolve(videos))

const createVideo = ({ title }) => {
  const maxId = Math.max.apply(null, videos.map(v => v.id))
  const watched = false
  const video = {
    id: maxId + 1,
    title,
    watched,
  }
  videos.push(video)
  return video
}

const getVideoById = (id) => new Promise(resolve => {
  const [video] = videos.filter(v => (v.id + '') === id)
  resolve(video)
})

exports.getVideoById = getVideoById
exports.getVideos = getVideos
exports.createVideo = createVideo

createVideo の args を分離したい

require のところに GraphQLInputObjectType, を追加。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
const videoInputType = new GraphQLInputObjectType({
  name: 'VideoInputType',
  description: 'video input type',
  fields: {
    title: {
      type: new GraphQLNonNull(GraphQLString),
      description: 'title of video',
    },
  }
})

const mutationType = new GraphQLObjectType({
  name: 'Mutation',
  description: 'Mutation type',
  fields: {
    createVideo: {
      type: videoType,
      args: {
        video: {
          type: new GraphQLNonNull(videoInputType)
        },
      },
      resolve: (_, args) => {
        return createVideo(args.video)
      }
    },
  },
})

node index.js を再起動して http://localhost:3000/graphql

1
2
3
4
5
6
7
mutation M {
  createVideo(video: {title: "hoge"}) {
    id
    title
    watched
  }
}

を試す (video: で一段増えているので注意)

1
2
3
4
5
6
{
  videos {
    id
    title
  }
}

などを試す。

休憩

createVideo も Promise にするとどうか

createVideo の末尾を return Promise.resolve(video) にしても問題なく動いた。

ruby でどうか

  • rails new getting_started_graphql_ruby
  • http://graphql-ruby.org/getting_started
  • Gemfile に gem 'graphql' を追加
  • bundle install
  • rails g graphql:install
  • Gemfile に graphiql-rails が追加されているので bundle install

video 追加

  • rails g graphql:object Video id:Int title:String watched:Boolean
  • id は Int ではなく ID が正しいので rails d graphql:object Video id:Int title:String watched:Boolean で消してやり直し
  • rails g graphql:object Video id:ID title:String watched:Boolean
  • app/graphql/types/query_type.rb を変更
1
2
3
4
5
6
  field :video do
    type Types::VideoType
    argument :id, !types.ID
    description 'Find video by ID'
    resolve ->(obj, args, ctx) { Video.find(args["id"]) }
  end

rails s を起動して http://localhost:3000/graphiql (express-graphql での例と違って /graphql ではなく i が入る) で

1
2
3
4
5
6
{
  video(id: 1) {
    id
    title
  }
}

を試すと server 側で NameError (uninitialized constant Video): になるのを確認。

  • rails g model video title watched:boolean
  • rake db:migrate
  • rails cVideo.create(title: "Hoge", watched: false) などでレコードを作成しておく
  • graphiql で試す
1
2
3
4
5
6
7
{
  video(id: 1) {
    id
    title
    watched
  }
}

mutation

  • app/graphql/mutations/create_video.rb
1
2
3
4
5
6
7
8
9
10
11
12
# 動かない
Mutations::CreateVideo = GraphQL::Relay::Mutation.define do
  name "CreateVideo"

  return_field :video, Types::VideoType

  input_field :title, !types.String

  resolve ->(obj, args, ctx) {
    return Video.create(title: args["title"])
  }
end
  • app/graphql/getting_started_graphql_ruby_schema.rbmutation(Mutations::CreateVideo) を追加
  • GraphQL::Schema::InvalidTypeError (CreateVideo has an invalid type: must be an instance of GraphQL::BaseType, not GraphQL::Relay::Mutation になってうまくいかない
  • rails g graphql:mutation は relay mutation 用で違うらしい

クライアント

mutation の動くコード例

rito さんに動く例をみせてもらって修正。

app/graphql/mutations/video.rb:

1
2
3
4
5
6
7
8
9
10
11
12
Mutations::Video = GraphQL::ObjectType.define do
  name "mutation"

  field :video, Types::VideoType do
    description "Create a video"
    argument :title, !types.String

    resolve ->(obj, args, ctx) {
      Video.create(title: args["title"], watched: false)
    }
  end
end

(name "Video" にすると Duplicate type definition found for name 'Video' で動かなかった。)

app/graphql/getting_started_graphql_ruby_schema.rb:

1
2
3
4
GettingStartedGraphqlRubySchema = GraphQL::Schema.define do
  query(Types::QueryType)
  mutation(Mutations::Video)
end

http://localhost:3000/graphiql で以下を試す。

1
2
3
4
5
6
7
mutation M {
  video(title: "foo") {
    id
    title
    watched
  }
}
1
2
3
4
5
6
7
{
  video(id: 2) {
    id
    title
    watched
  }
}

追加されたのがみえたら OK

時間切れで試せなかったけど、 mutation を複数追加する場合はどうなるのかがわからなかった。

Comments