O’Reilly Japan - 初めてのGraphQLの3 ~ 5章までのメモです。

実装したGraphqlサーバはこちらにあります。

なお、MoonHighway/learning-graphql: The code samples for Learning GraphQL by Eve Porcello and Alex Banks, published by O’Reilly Mediaにもサンプルコードがあるがnpmのライブラリのバージョンが古くてうまく動かなかったので参考になるかもしれません。

3章

GETクエリサンプル

curl 'http://snowtooth.herokuapp.com' \
-H 'Content-Type: application/json' \
--data '{"query": "{ allLifts { name}}"}'

{"data":
  {"allLifts":
    [
      {"name":"Astra Express"},
      {"name":"Jazz Cat"},
      {"name":"Jolly Roger"},
      {"name":"Neptune Rope"},
      {"name":"Panorama"},
      {"name":"Prickly Peak"},
      {"name":"Snowtooth Express"},
      {"name":"Summit"},
      {"name":"Wally's"},
      {"name":"Western States"},
      {"name":"Whirlybird"}
    ]
  }
}

変更(mutation)クエリサンプル

curl 'http://snowtooth.herokuapp.com' \
-H 'Content-Type: application/json' \
--data '{"query": "mutation {setLiftStatus(id: \"panorama\" status: OPEN) {name status}}"}'

{"data":{"setLiftStatus":{"name":"Panorama","status":"OPEN"}}}

試してみよう

GraphiQL

https://graphql.org/swapi-graphql

# query sample
{
  person(personID: 1) {
    name
    birthYear
    created
    filmConnection {
      films {
        title
        director
        releaseDate
      }
    }
  }
}

GraphQL Playground

https://snowtooth.moonhighway.com/

# シンプルなquery
query {
  allLifts {
    	name
    	status
  }
}

# 複数のリソースをまとめて取得するquery
query liftsAndTrails {
  liftCount(status: OPEN)
  allLifts {
    name
    status
  }
  allTrails {
    name
    difficulty
  }
}

スカラー型とオブジェクト型の話

fragmentを使ったクエリ

fragment … 選択セットのフィールドを定義しておくと使わ回すことができるためメンテナビリティが高まる

fragment liftInfo on Lift {
  name
  status
  capacity
  night
  elevationGain
}

query {
  Lift(id: "jazz-cat") {
    ...liftInfo
    trailAccess {
      name
      difficulty
    }
  }
  Trail(id: "river-run") {
    name
    difficulty
    accessedByLifts {
      ...liftInfo
    }
  }
}

ユニオン型

複数の型のオブジェクトを含むリストがほしい

下記のagendaクエリのレスポンスには2つのオブジェクトが配列として返却される

# Inline fragmentを使い複数の方に対してそれぞれにフィールドを指定する

query schedule {
  agenda {
    ...on Workout {
      name
      repos
    }
    ...on StudyGroup {
      name
      subject
      students
    }
  }
}

# 名前付きfragmenを使うこともできる
query today {
  agenda {
    ...workout
    ...study
  }
}

fragment workout on Workout {
  name
  repos
}

fragment study on StudyGroup {
  name
  subject
  students
}

インターフェース

複数のオブジェクト型を使うための手法 下の例だと findEventsAtVenue の内の各オブジェクトで選択するフィールドを定義している(id, name, minAgeRestrinction, startsAt)

on *** でフラグメントを定義して、特定のオブジェクトでのみ追加するフィールドを定義することができる

query {
  findEventsAtVenue(venueId: "Madison Square Garden") {
    id
    name
    minAgeRestriction
    startsAt

    ... on Festival {
      performers
    }

    ... on Concert {
      performingBand
    }

    ... on Conference {
      speakers
      workshops
    }
  }
}
{
  "data": {
    "findEventsAtVenue": [
      {
        "id": "Festival-2",
        "name": "Festival 2",
        "minAgeRestriction": 21,
        "startsAt": "2018-10-05T14:48:00.000Z",
        "performers": [
          "The Singers",
          "The Screamers"
        ]
      },
      {
        "id": "Concert-3",
        "name": "Concert 3",
        "minAgeRestriction": 18,
        "startsAt": "2018-10-07T14:48:00.000Z",
        "performingBand": "The Jumpers"
      },
      {
        "id": "Conference-4",
        "name": "Conference 4",
        "minAgeRestriction": null,
        "startsAt": "2018-10-09T14:48:00.000Z",
        "speakers": [
          "The Storytellers"
        ],
        "workshops": [
          "Writing",
          "Reading"
        ]
      }
    ]
  }
}

ミューテーション

ミューテーションは書き込み操作を行う

  • 危険なミューテーション
    # dangelous mutation
    mutation burnItDown {
      deleteAllData
    }
    
  • Createサンプル
    mutation createSong {
      addSong(title: "No Scrubs", numberOne: true, performerName: "TLC") {
        id
        title
        numberOne
      }
    }
    
    # Response
    {
      "data": {
        "addSong": {
          "id": "hogehogehgoe",
          "title": "No Scrubs",
          "nuberOne": true
        }
      }
    }
    
  • Updateサンプル
    mutation closeLift {
      setLiftStatus(id: "jass-cat" status: CLOSED) {
        name
        status
      }
    }
    

クエリ変数

動的に値を変えて実行できるようになる

mutation createSong ($title:String! $numberOne:Int $by:String!) {
  addSong(title:$title, numberOne:$numberOne, performerName:$by) {
    id
    title
    numberOne
  }
}

サブスクリプション

GraphQLサーバからリアルタイムにデータの更新情報を受け取ることができる

いいね情報をリアルタイムに更新したいという要求から生まれた

下記のsubscriptionを設定するとリフトの状態が変化したことの通知をWebSocketを通じて受け取ることができる

https://snowtooth.moonhighway.com/ で試せます。

subscription {
  liftStatusChange {
    name
    capacity
    status
  }
}

subscriptionを実行した後下記のMutationを実行する

mutation closeLift {
  setLiftStatus(id: "astra-express" status: HOLD) {
    name
    status
  }
}

subscriptionは停止するまで監視し続ける

イントロスペクション

APIスキーマの詳細を取得できる機能

  • APIで属できるすべての方の情報が取得できる
    query {
      __schema {
        types {
          name
          description
        }
      }
    }
    
    {
      "data": {
        "__schema": {
          "types": [
            {
              "name": "Lift",
              "description": "A `Lift` is a chairlift, gondola, tram, funicular, pulley, rope tow, or other means of ascending a mountain."
            },
            {
              "name": "ID",
              "description": "The `ID` scalar type represents a unique identifier, often used to refetch an object or as key for a cache. The ID type appears in a JSON response as a String; however, it is not intended to be human-readable. When expected as an input type, any string (such as `\"4\"`) or integer (such as `4`) input value will be accepted as an ID."
            },
            {
              "name": "String",
              "description": "The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text."
            },
            {
              "name": "Int",
              "description": "The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1."
            },
            {
              "name": "Boolean",
              "description": "The `Boolean` scalar type represents `true` or `false`."
            },
            {
              "name": "Trail",
              "description": "A `Trail` is a run at a ski resort"
            },
            {
              "name": "LiftStatus",
              "description": "An enum describing the options for `LiftStatus`: `OPEN`, `CLOSED`, `HOLD`"
            },
            {
              "name": "TrailStatus",
              "description": "An enum describing the options for `TrailStatus`: `OPEN`, `CLOSED`"
            },
            {
              "name": "SearchResult",
              "description": "This union type returns one of two types: a `Lift` or a `Trail`. When we search for a letter, we'll return a list of either `Lift` or `Trail` objects."
            },
            {
              "name": "Query",
              "description": null
            },
            {
              "name": "Mutation",
              "description": null
            },
            {
              "name": "Subscription",
              "description": null
            },
            {
              "name": "CacheControlScope",
              "description": null
            },
            {
              "name": "Upload",
              "description": "The `Upload` scalar type represents a file upload."
            },
            {
              "name": "__Schema",
              "description": "A GraphQL Schema defines the capabilities of a GraphQL server. It exposes all available types and directives on the server, as well as the entry points for query, mutation, and subscription operations."
            },
            {
              "name": "__Type",
              "description": "The fundamental unit of any GraphQL Schema is the type. There are many kinds of types in GraphQL as represented by the `__TypeKind` enum.\n\nDepending on the kind of a type, certain fields describe information about that type. Scalar types provide no information beyond a name, description and optional `specifiedByUrl`, while Enum types provide their values. Object and Interface types provide the fields they describe. Abstract types, Union and Interface, provide the Object types possible at runtime. List and NonNull types compose other types."
            },
            {
              "name": "__TypeKind",
              "description": "An enum describing what kind of type a given `__Type` is."
            },
            {
              "name": "__Field",
              "description": "Object and Interface types are described by a list of Fields, each of which has a name, potentially a list of arguments, and a return type."
            },
            {
              "name": "__InputValue",
              "description": "Arguments provided to Fields or Directives and the input fields of an InputObject are represented as Input Values which describe their type and optionally a default value."
            },
            {
              "name": "__EnumValue",
              "description": "One possible value for a given Enum. Enum values are unique values, not a placeholder for a string or numeric value. However an Enum value is returned in a JSON response as a string."
            },
            {
              "name": "__Directive",
              "description": "A Directive provides a way to describe alternate runtime execution and type validation behavior in a GraphQL document.\n\nIn some cases, you need to provide options to alter GraphQL's execution behavior in ways field arguments will not suffice, such as conditionally including or skipping a field. Directives provide this by describing additional information to the executor."
            },
            {
              "name": "__DirectiveLocation",
              "description": "A Directive can be adjacent to many parts of the GraphQL language, a __DirectiveLocation describes one such possible adjacencies."
            }
          ]
        }
      }
    }
    
  • 特定の型の詳細が知りたいとき
    query liftDetails {
      __type(name: "Lift") {
        name
        fields {
          name
          description
          type {
            name
          }
        }
      }
    }
    
    {
      "data": {
        "__type": {
          "name": "Lift",
          "fields": [
            {
              "name": "id",
              "description": "The unique identifier for a `Lift` (id: \"panorama\")",
              "type": {
                "name": null
              }
            },
            {
              "name": "name",
              "description": "The name of a `Lift`",
              "type": {
                "name": null
              }
            },
            {
              "name": "status",
              "description": "The current status for a `Lift`: `OPEN`, `CLOSED`, `HOLD`",
              "type": {
                "name": "LiftStatus"
              }
            },
            {
              "name": "capacity",
              "description": "The number of people that a `Lift` can hold",
              "type": {
                "name": null
              }
            },
            {
              "name": "night",
              "description": "A boolean describing whether a `Lift` is open for night skiing",
              "type": {
                "name": null
              }
            },
            {
              "name": "elevationGain",
              "description": "The number of feet in elevation that a `Lift` ascends",
              "type": {
                "name": null
              }
            },
            {
              "name": "trailAccess",
              "description": "A list of trails that this `Lift` serves",
              "type": {
                "name": null
              }
            }
          ]
        }
      }
    }
    
  • 新しいAPIを使う場合はルート型で指定できるフィールド確認スべし
    query roots {
      __schema {
        queryType {
        	...typeFields
        }
        mutationType {
          ...typeFields
        }
        subscriptionType {
          ...typeFields
        }
      }
    }
    
    fragment typeFields on __Type {
      name
      fields {
        name
      }
    }
    
    {
      "data": {
        "__schema": {
          "queryType": {
            "name": "Query",
            "fields": [
              {
                "name": "allLifts"
              },
              {
                "name": "allTrails"
              },
              {
                "name": "Lift"
              },
              {
                "name": "Trail"
              },
              {
                "name": "liftCount"
              },
              {
                "name": "trailCount"
              },
              {
                "name": "search"
              }
            ]
          },
          "mutationType": {
            "name": "Mutation",
            "fields": [
              {
                "name": "setLiftStatus"
              },
              {
                "name": "setTrailStatus"
              }
            ]
          },
          "subscriptionType": {
            "name": "Subscription",
            "fields": [
              {
                "name": "liftStatusChange"
              },
              {
                "name": "trailStatusChange"
              }
            ]
          }
        }
      }
    }
    

抽象構文木

参考: https://atmarkit.itmedia.co.jp/ait/articles/0707/11/news129.html

4章 スキーマの設計

RESTのようにエンドポイントの集合ではなく、型の集合として捉える = スキーマ

GraphQL SDL( Schema Definition Language )がある

[memo] これはAPIテストに使えそう

型やフィールドは開発チーム内で共通認識を作っていく

型定義

スキーマの核は型

写真共有アプリケーションで考えていく

  • GitHubアカウントでログイン
  • 写真投稿
  • ユーザがタグを付加できる

サンプルアプリケーションのスキーマ定義

  • 記述について
    • フィールド名: 型名
    • !はNot NULL値
    • 組み込みの方はスカラー型と呼ぶ
      • String: JSONの文字列として返却
      • ID: ユニークな値になるかをバリデーションする、JSON文字列を返却
      • Int
      • Float
      • Boolian

User型定義

type Photo {
  id: ID!
  name: String!
  url: String!
  description: String!
}

スカラー型

カスタムスカラー型を定義できる

  • DateTime: JSON文字列を返却。日時データとしてシリアライズできる正しいフォーマットかバリデーションされる ``` scalar DateTie

type Photo { id: ID! name: String! url: String! description: String! created: DateTime! }


### Enum ( 列挙型 )

予め定められた特定の文字列の一つを返すスカラー型

限られた選択肢のうち一つを返すようなフィールドを実装したいときに使う

例)

enum PhotCategory { SELFIE PORTRAIT ACTION LANDSCAPE GRAPHIC }

type Photo { id: ID! name: String! url: String! description: String! created: DateTime! category: PhotoCategory! }


## コネクションとリスト

特定の型のリストを定義できる

!の使い方が色々ある

[Int] : [1,2,3] or [null, 2, 3] or 配列自体がnull or [] [Int!] : [1,2,3] or 配列自体がnull or [] [Int]! : [1,2,3] or [null, 2, 3] or [] [Int!]! : [1,2,3] or []


基本的にはnull許可しない使い方をする

### 一対一の接続

PhotoはUserによって投稿されます。

つまりPhotoはUserとの間にエッジを持っているはずです。

`Photo --postedBy--> User`

スキーマ定義

type User { githubLogin: ID! name: String avatar: String }

type Photo { id: ID! name: String! url: String! description: String created: DateTime! category: PhotoCategory! postBy: User! }


### 一対多の接続

GraphQLのサービスは無向グラフにしておくといい

クライアントが自由度の高いクエリが作れるため

任意のノードを起点として接続していけるため

User型とPhoto型に双方向にエッジを作ると実現できる

Userは複数のPhotoを持つのでリストで定義する

スキーマ定義

type User { githubLogin: ID! name: String avatar: String postedPhotos: [Photo!]! }


User –postedBy–> Photo –postedBy–> Photo –postedBy–> Photo


一対多の接続はルート型でよく使う

PhotoやUserをクエリで使えるようにするため

Query型のフィールドに追加すると使用できるようになる

type Query { totalPhots: Int! allPhots: [Photo!]! totalUsers: Int! allUsers: [User!]! }

schema { query: Query }


PhotoとUserを問い合わせるクエリ例

query { totalPhotos allPhotos { name url } }


### 多対多の接続

写真に写っているユーザを写真にタグ付けする機能

type User { githubLogin: ID! name: String avatar: String postedPhotos: [Photo!]! inPhotos: [Photo!]! }

type Photo { id: ID! name: String! url: String! description: String created: DateTime! category: PhotoCategory! postBy: User! taggedUsers: [User!]! }


#### スルー型
多対多の関係の関係自体に意味合いを持たせたいときに使う

User同士の友人関係で考えてみる

Userがfriendsのリストを持っている

type User { friends: [User!]! }

知り合ってからの期間という関係性をもたせるにはどうするか

-> カスタムオブジェクト型で新しいエッジを構築する

UserとUserをつなぐためスルー型と言われる

type User { friends: [FriendShip!]! }

type FriendShip { friend_a: User! friend_b: User! howLong: Int! whereWeMet: Location }


同じ機会に複数の友人関係が構築される場合を考慮する

type FriendShip { friends: [User!]! howLong: Int! whereWeMet: Location }


### 異なる型のリスト

予定表を例にして考える

予定表の特徴
- 予定表は様々なイベントで構成されている
- それぞれのデータは異なるフィールドを持っている
  - 例. 勉強会とワークアウト
- 異なる型の用事リストと考えられる

`ユニオン型` と `インターフェース` がある

含まれている複数の方が全く異なるのであれば、`ユニオン型`

共通のフィールドがある場合は `インターフェース` を使う

#### ユニオン型

ユニオン型は複数の型のうちの一つを返す型

query schedule { agenda { …on Workout { name reps } …on StudyGroup { name subject students } } }


AgendaItemというユニオン型で定義した場合

好きなだけ多くの型を追加できる

パイプでつなぐだけ

union AgendaItem = StudyGroup | Workout

type StudyGroup { name: String! subject: String! students: [User!]! }

type Workout { name: String! reps: Int! }

type Query { agenda: [AgendaItem!]! }


#### インターフェース

インターフェースはオブジェクト型に実装できる抽象型

query schedule { agenda { name start end …on Workout { reps } } }


scalar DateTime

interface AgendaItem { name: String! start: DateTime! end: DateTime! }

type StudyGroup implements AgendaItem { name: String! start: DateTime! end: DateTime! participants: [User!]! topic: String! }

type Workout implements AgendaItem { name: String! start: DateTime! end: DateTime! reps: Int! }

type Query { agenda: [AgendaItem!]! }


## 引数

type Query { ….. User(githubLogin: ID!): User! Photo(id: ID!): Photo! }


query { User(githubLogin: “MoonTahoe”) { name avatar } }

query { Photo(id: “hogahogfugafuga”) { name description url } }



### データのフィルタリング

カテゴリを受け取って絞り込んだ結果を返却させるクエリ

type Query { allPhotos(category: PhotoCategory): [Photo!]! }


query { allPhotos(category: “SELFIE”) { name description url } }


#### データページング

データ量を指定して取得する処理


type Query { allUsers(first: Int=50 start: Int=0): [User!]! allPhotos(first: Int=25 start: Int=0): [Photo!]! }


query { allUsers(first: 10 start: 90) { name avatar } }


#### ソート


ソートのキーや順序ルールはenumで定義

enum SortDirection { ASCENDING DESCENDING }

enum SortablePhotoField { name description category created }

sort, sortByを引数に追加

Query { allPhotos( sort: SortDirection = DESCENDING sortBy: SortablePhotoField = created ): [Photo!]! }

呼び出し

query { allPhotos(sortBy: name) { name url } }


Query型以外のすべて型のフィールドに対して引数を設定することもできる

type User { postedPhotos( first: Int: 25 start: Int = 0 sort: SortDirection = DESCENDING sortBy: SortablePhotoField = created category: PhotoCategory ): [Photo!] }



## ミューテーション

アプリケーションでの動作(動詞)と対応付けるのが望ましい

ユーザが行う操作をすべて列挙する

写真アプリケーション
- GitHubアカウントでサインインする
- 写真を投稿する
- 写真にタグ付けする

ルート型のMutationに追加するとクライアントから利用できるようになる

スキーマ定義

type Mutation { postPhoto( name: String! description: String category: PhotoCategory=PORTRAIT ): Photo! }

schema { query: Query mutation: Mutation }

Mutation実行

mutation { postPhoto(name: “Sending the Palisades”) { id url created postedBy { name } } }


## 入力型
引数の数が増えてしまった場合に入力型を使う

input PostPhotoInput { name: String! description: String category: PhotoCategory=PORTRAIT }

type Mutation { postPhoto(input: PostPhotoInput!): Photo! }



## 返却型

## サブスクリプション

type Subscription { newPhoto: Photo! newUser: User! }

schema { query: Query mutation: Mutation subscription: Subscription }


フィルターができる定義

type Subscription { newPhoto(category: PhotoCategory): Photo! newUser: User! }

ACTIONカテゴリだけ通知

subscription { newPhoto(category: “ACTION”) { id name url postBy { name } } }


## スキーマのドキュメント化
`"`を使ってコメントを付加でき、イントロスペクションクエリで取得できる

type Mutation { “”” GitHubユーザで認可 “”” githubAuth( “ユーザの認可のために送信されるGitHubのユニークなコード” code: String! ): AuthPayload! }


# 5章 GraphQLサーバーの実装

環境
- JavaScript
- Apollo Server

## プロジェクトセットアップ

配布されているコードが動作しない(おそらくバージョンが古い)ため、Apolloの公式ドキュメントの手順で構築を進める

```bash
npm init --yes
npm install apollo-server graphql nodemon

nodemonの設定

ファイルの更新を監視し、サーバーを再起動するための設定

  "scripts": {
    "start": "nodemon -e js,json,graphql"
  },

バージョン1 (Query定義)

リゾルバ

リゾルバはデータを取得する役割。

特定のフィールドのデータを返す関数である。

非同期で処理ができ、REST API、DBなどからデータを取得したり更新したりできる。

index.jsの実装

const { ApolloServer, gql } = require('apollo-server');

// スキーマ定義
const typeDefs = gql`
    type Query {
        totalPhotos: Int!
    }
`;

// リゾルバ関数定義
const resolvers = {
    Query: {
        totalPhotos: () => 42
    },
};

// サーバーのインスタンス作成
const server = new ApolloServer({ typeDefs, resolvers });

// The `listen` method launches a web server.
server.listen().then(({ url }) => {
  console.log(`🚀  Server ready at ${url}`);
});

apollo server 起動

npm start

[nodemon] restarting due to changes...
[nodemon] starting `node index.js`
[nodemon] restarting due to changes...
[nodemon] starting `node index.js`
🚀  Server ready at http://localhost:4000/

バージョン2(Mutation型)

index.js

const typeDefs = gql`
    type Query {
        totalPhotos: Int!
    }

    type Mutation {
        postPhoto(name: String! description: String): Boolean!
    }
`;

var photos = []

const resolvers = {
    Query: {
        totalPhotos: () => photos.length
    },

    Mutation: {
        postPhoto(parent, args) {
            photos.push(args)
            return true
        }
    }
};

ミューテーション実行

mutation newPhoto {
  postPhoto(name: "sample photo")
}

// クエリ変数
mutation newPhoto ($name: String!, $description: String) {
  postPhoto(name: $name, description: $description)
}


// レスポンス
{
  "data": {
    "postPhoto": true
  }
}

バージョン3(型リゾルバ)

オブジェクトを返却するリゾルバ = 型リゾルバ

index.js

const typeDefs = gql`
    type Photo {
        id: ID!
        url: String!
        name: String!
        desription: String
    }

    type Query {
        totalPhotos: Int!
        allPhotos: [Photo!]!
    }

    type Mutation {
        postPhoto(name: String! description: String): Photo!
    }
`;

// 1. ユニークIDをインクリメントするための変数を定義
var _id = 0
var photos = []

const resolvers = {
    Query: {
        totalPhotos: () => photos.length,
        allPhotos: () => photos
    },

    Mutation: {
        postPhoto(parent, args) {

            // 2. 新しい写真を作成し、idを生成する
            var newPhoto = {
                id: _id++,
                ...args
            }
            photos.push(args)

            // 3. 新しい写真を返す
            return newPhoto
        }
    }
};

クエリ実行

// 写真投稿
// allPhotos前に何件か投稿する
mutation newPhoto ($name: String!, $description: String) {
  postPhoto(name: $name, description: $description) {
    id
    name
    desription
  }
}

// レスポンス
{
  "data": {
    "postPhoto": {
      "id": "7",
      "name": "sample",
      "desription": null
    }
  }
}

query listPhotos {
  allPhotos {
    id
    name
    desription
  }
}

// レスポンス
{
  "data": {
    "allPhotos": [
      {
        "id": "0",
        "name": "sample",
        "desription": null
      },
      {
        "id": "1",
        "name": "sample",
        "desription": null
      },
    ]
  }
}

// urlフィールド追加
query listPhotos {
  allPhotos {
    id
    name
    desription
    url
  }
}

// レスポンス
{
  "errors": [
    {
      "message": "Cannot return null for non-nullable field Photo.url.",

定義されていないフィールドがクエリにあると上記のエラーになる

Photoオブジェクトを追加しフィールド定義する

const resolvers = {
    Query: {...},
    Mutation: {...},
    Photo: {
        url: parent => `http://yoursite.com/img/${parent.id}.jpg`
    }
};
// 先程エラーになったクエリ
query listPhotos {
  allPhotos {
    id
    name
    desription
    url
  }
}

// レスポンス
{
  "data": {
    "allPhotos": [
      {
        "id": "0",
        "name": "sample",
        "desription": null,
        "url": "http://yoursite.com/img/0.jpg"
      },
      {
        "id": "1",
        "name": "sample",
        "desription": null,
        "url": "http://yoursite.com/img/1.jpg"
      },
    ]
  }
}

バージョン4(InputとEnum)

Input型を使うと再利用性が高まり

Enum(列挙)型を使うとバリデーションがかけられるようになる。

const typeDefs = gql`
    // カテゴリの選択肢を定義
    enum PhotoCategory {
        SELFIE
        PORTRAIT
        ACTION
        LANDSCAPE
        GRAPHIC
    }

    type Photo {
        ...
        category: PhotoCategory!
    }

    // 入力パラメータを定義
    input PostPhotoInput {
        name: String!
        // デフォルト値
        category: PhotoCategory=PORTRAIT
        description: String
    }

    type Mutation {
        // 引数に定義したinputを使う
        postPhoto(input: PostPhotoInput!): Photo!
    }
`;
# クエリ
mutation newPhoto($input: PostPhotoInput!) {
  postPhoto(input: $input) {
    id
    name
    url
    description
    category
  }
}

# 変数
{
  "input": {
    "name": "sample photo A",
    "description": "A Sample"
  }
}

# レスポンス
{
  "data": {
    "postPhoto": {
      "id": "0",
      "name": "sample photo A",
      "url": "http://yoursite.com/img/0.jpg",
      "description": null,
      "category": "PORTRAIT"
    }
  }
}

バージョン5(Eedgeと接続)

1対多の接続

const typeDefs = gql`
    // 略
    type Photo {
        id: ID!
        url: String!
        name: String!
        description: String
        category: PhotoCategory!
        postedBy: User! // 写真に対して1ユーザーが紐づく
    }

    type User {
        githubLogin: ID!
        name: String
        avatar: String
        postedPhotos: [Photo!]! // ユーザーは複数の写真を投稿できる
    }
`;

const resolvers = {
    // 略
    Photo: {
        url: parent => `http://yoursite.com/img/${parent.id}.jpg`,
        postedBy: parent => {
            // 写真に紐づく単一のユーザを返却
            return users.find(u => u.githubLogin === parent.githubUser)
        }
    },
    User: {
        postedPhotos: parent => {
            // ユーザに紐づく写真を配列で返却
            return photos.filter(p => p.githubUser === parent.githubLogin)
        }
    }
};
# クエリ
query photos {
  allPhotos {
    name
    url
    postedBy {
      name
    }
  }
}

# レスポンス
{
  "data": {
    "allPhotos": [
      {
        "name": "Dropping the Htert Chute",
        "url": "http://yoursite.com/img/1.jpg",
        "postedBy": {
          "name": "Glen Plake"
        }
      },
      {
        "name": "B",
        "url": "http://yoursite.com/img/2.jpg",
        "postedBy": {
          "name": "Scot Schmidt"
        }
      },
      {
        "name": "C",
        "url": "http://yoursite.com/img/3.jpg",
        "postedBy": {
          "name": "Scot Schmidt"
        }
      }
    ]
  }
}

多対多の接続

写真へのタグ付機能で試してみます

  • ユーザは複数の写真にタグが付ける
  • 写真には複数のユーザにタグが付けられる
const typeDefs = gql`
    type Photo {
        .....
        taggedUsers: [User!]!
    }

    type User {
        .....
        inPhotos: [Photo!]!
    }
};

const resolvers = {
    Photo: {
        .......
        taggedUsers: parent => tags
            // 対象の写真が関係するタグの配列を返す
            .filter(tag => tag.photoID === parent.id)
            // タグの配列をユーザーIDの配列に変換する
            .map(tag => tag.userID)
            // ユーザーIDの配列をユーザーオブジェクトに変換する
            .map(userID => users.find(u => u.githubLogin === userID))
    },
    User: {
        .......
        inPhotos: parent => tags
            // 対象のユーザが関係しているタグの配列を返す
            .filter(tag => tag.userID === parent.id)
            // タグの配列をPhoto IDの配列に変換する
            .map(tag => tag.photoID)
            // Photo IDの配列をPhotoオブジェクトに変換する
            .map(photoID => photos.find(p => p.id === photoID))
    }
};
// クエリ
query listPhotos {
  allPhotos {
    url
    taggedUsers {
      name
    }
  }
}

// レスポンス
{
  "data": {
    "allPhotos": [
      {
        "url": "http://yoursite.com/img/1.jpg",
        "taggedUsers": [
          {
            "name": "Glen Plake"
          }
        ]
      },
      {
        "url": "http://yoursite.com/img/2.jpg",
        "taggedUsers": [
          {
            "name": "Scot Schmidt"
          },
          {
            "name": "Mike Hattrup"
          },
          {
            "name": "Glen Plake"
          }
        ]
      },
      {
        "url": "http://yoursite.com/img/3.jpg",
        "taggedUsers": []
      }
    ]
  }
}

バージョン6(カスタムスカラー型)

Int, Floart, String,Boolean, ID で要件が満たせない場合に定義する。

今回はDateTime型を作成する

リゾルバに以下3つの関数を定義する

関数の処理内容は仕様による

(1) クエリの引数で受け取った値をDateオブジェクトに変換 parseValue: value => new Date(value)

(2) 問い合わせに対する返り値をISO日時フォーマットで返却する serialize: value => new Date(value).toISOString()

(3) クエリドキュメントに直接追加された場合にクエリからの抽象構文木でパースされた値を取得する処理

// クエリドキュメントに直接日付を追加されたクエリ
query {
  allPhotos(after: `4/18/2018`) {
    name
    url
  }
}

以下のようにastオブジェクトから値を取得する必要がある parseLiteral: ast => ast.value

apollo-server-express のセットアップ

Apollo Server Expressでは下記が可能になる

  • Apollo Severの最新機能の利用
  • 詳細設定
    • カスタムホームルート
    • プレイグラウウンドルート

これにより画像の保存ができるようになる!

# apollo-serverを削除
npm remove apollo-server
# apollo-server-expressをインストール
npm install apollo-server-express express
# playgroundをインストール
npm install graphql-playground-middleware-express

MongoDB のインストール

https://docs.mongodb.com/manual/tutorial/install-mongodb-on-os-x/

brew tap mongodb/brew
brew install mongodb-community
brew services start mongodb-community
brew services list
mongosh
npm install mongodb dotenv

meクエリの実装

https://github.com/naotospace/naotospace.github.io/pull/8/commits/fdd9652281dea2837644f7bc6b7237a97e3a3690

// 実装
const server = new ApolloServer({
    typeDefs,
    resolvers,
    context: async ({ req }) => {
        // Headerから認証トークン取得
        const githubToken = req.headers.authorization
        // トークンから現在のユーザー情報取得
        const currentUser = await db.collection('users').findOne({ githubToken })
        return { db, currentUser }
    }
});

// type定義
Query: {
    ...
    me: (parent, args, { currentUser }) => currentUser,


// リゾルバ
Query: {
  me: (parent, args, { currentUser }) => currentUser,
query currentUser {
  me {
    name
  }
}

postPhotoミューテーションの実装

// クエリ
mutation post($input: PostPhotoInput!) {
  postPhoto(input: $input) {
    id
    url
    postedBy {
      name
      avatar
    }
  }
}

// Variables
{
  "input": {
    "name": "image 1"
  }
}

// レスポンス
{
  "data": {
    "postPhoto": {
      "id": "61795dd4a78a604492eef66c",
      "url": "/img/photos/61795dd4a78a604492eef66c.jpg",
      "postedBy": {
        "name": "Naoto KISHINO",
        "avatar": "https://avatars.githubusercontent.com/u/2687752?v=4"
      }
    }
  }
}

フェイクユーザーミューテーション

スキップ

6章 クライアントの実装

graphqlサーバーが起動していると以下のようなcurlコマンドで問い合わせできる

curl -X POST \
-H "Content-Type:application/json" \
--data '{ "query": "{totalUsers, totalPhotos}" }' \
http://localhost:4000/graphql

{"data":{"totalUsers":1,"totalPhotos":1}}