GraphQL Clientの使い方


# はじめに

- GraphQLの勉強のため、以下のサイトのfull-stack tutorialをやってみた。
- https://www.apollographql.com/docs/tutorial/introduction/
- サーバー側はなんとか理解したが、クライアント側がReactベースで全く分からない。
- 代わりにPythonからGraphQL Clientを使用してみた。

## GraphQLサーバーの起動

```
cd fullstack-tutorial\final\server
npm run start
http://127.0.0.1:4000/
```

## スキーマ定義

- LaunchConnection.cursorは、打ち上げデータに付与されているlaunch_date_unixに対応。
- Mission.missionPatchは、打ち上げに対応するアイコン画像情報。大と小があり、デフォルトは大。
- User.tokenは、ここではemailをbase64エンコーディングしたもの。

```graphql
type Query {
  launches(
    """
    The number of results to show. Must be >= 1. Default = 20
    """
    pageSize: Int
    """
    If you add a cursor here, it will only return results _after_ this cursor
    """
    after: String
  ): LaunchConnection!
  launch(id: ID!): Launch
  me: User
}

type Mutation {
  # if false, signup failed -- check errors
  bookTrips(launchIds: [ID]!): TripUpdateResponse!

  # if false, cancellation failed -- check errors
  cancelTrip(launchId: ID!): TripUpdateResponse!

  login(email: String): User
}

type TripUpdateResponse {
  success: Boolean!
  message: String
  launches: [Launch]
}

"""
Simple wrapper around our list of launches that contains a cursor to the
last item in the list. Pass this cursor to the launches query to fetch results
after these.
"""
type LaunchConnection {
  cursor: String!
  hasMore: Boolean!
  launches: [Launch]!
}

type Launch {
  id: ID!
  site: String
  mission: Mission
  rocket: Rocket
  isBooked: Boolean!
}

type Rocket {
  id: ID!
  name: String
  type: String
}

type User {
  id: ID!
  email: String!
  profileImage: String
  trips: [Launch]!
  token: String
}

type Mission {
  name: String
  missionPatch(size: PatchSize): String
}

enum PatchSize {
  SMALL
  LARGE
}
```

# PythonからGraphQL Clientを使用

- 以下のライブラリを使用。
- https://github.com/graphql-python/gql
- pip install --pre gql[all]

```python
import json

from gql import gql, Client
from gql.transport.aiohttp import AIOHTTPTransport

transport = AIOHTTPTransport(url="http://127.0.0.1:4000/")
client = Client(transport=transport)

# 打ち上げ予定の一覧を取得 (名前をlaunchesからpageに変更)
query = gql("""
    query GetLaunches {
      page: launches(pageSize: 3) {
        cursor
        hasMore
        launches {
          id
          mission {
            name
            missionPatch(size: SMALL)
          }
        }
      }
    }
""")

result = client.execute(query)
print(json.dumps(result, indent=2))
```

```json
{
  "page": {
    "cursor": "1605486420",
    "hasMore": true,
    "launches": [
      {
        "id": "109",
        "mission": {
          "name": "Starlink-15 (v1.0)",
          "missionPatch": "https://images2.imgbox.com/9a/96/nLppz9HW_o.png"
        }
      },
      {
        "id": "108",
        "mission": {
          "name": "Sentinel-6 Michael Freilich",
          "missionPatch": null
        }
      },
      {
        "id": "107",
        "mission": {
          "name": "Crew-1",
          "missionPatch": "https://i.imgur.com/BzaSAnx.png"
        }
      }
    ]
  }
}
```

```python
# ログインしてトークンを取得
query = gql("""
    mutation LoginUser {
      login(email: "daisy@apollographql.com") {
        token
      }
    }
""")

result = client.execute(query)
print(json.dumps(result, indent=2))
```

```json
{
  "login": {
    "token": "ZGFpc3lAYXBvbGxvZ3JhcGhxbC5jb20="
  }
}
```

```python
# ヘッダーを設定したclientを作成
headers = {'authorization': 'ZGFpc3lAYXBvbGxvZ3JhcGhxbC5jb20='}
transport = AIOHTTPTransport(url="http://127.0.0.1:4000/", headers=headers)
client = Client(transport=transport)

# 打ち上げの予約
query = gql("""
    mutation BookTrips {
      bookTrips(launchIds: [67, 68, 69]) {
        success
        message
        launches {
          id
        }
      }
    }
""")

result = client.execute(query)
print(json.dumps(result, indent=2))
```

```json
{
  "bookTrips": {
    "success": true,
    "message": "trips booked successfully",
    "launches": [
      {
        "id": "67"
      },
      {
        "id": "68"
      },
      {
        "id": "69"
      }
    ]
  }
}
```

```python
# 打ち上げのキャンセル
query = gql("""
    mutation BookTrips {
      cancelTrip(launchId: 68) {
        success
        message
        launches {
          id
        }
      }
    }
""")

result = client.execute(query)
print(json.dumps(result, indent=2))
```

```json
{
  "cancelTrip": {
    "success": true,
    "message": "trip cancelled",
    "launches": [
      {
        "id": "68"
      }
    ]
  }
}
```

```python
# ユーザーの情報と予約している打ち上げを取得
query = gql("""
    query {
      me {
        email
        trips {
          id
          mission {
            name
            missionPatch
          }
        }
      }
    }
""")

result = client.execute(query)
print(json.dumps(result, indent=2))
```

```json
{
  "me": {
    "email": "daisy@apollographql.com",
    "trips": [
      {
        "id": "67",
        "mission": {
          "name": "Merah Putih",
          "missionPatch": "https://images2.imgbox.com/a8/f5/ZgdsrbqW_o.png"
        }
      },
        "id": "69",
        "mission": {
          "name": "SAOCOM 1A",
          "missionPatch": "https://images2.imgbox.com/66/d2/oVB1ofaZ_o.png"
        }
      }
    ]
  }
}
```

# その他

- SQLite3では、INTEGER型のカラムに対してPRIMARY KEY制約を設定すると、自動インクリメントとなる。
- Sequelize ORMを使用すると、テーブルのカラムにcreatedAtとupdatedAtが追加される。
- データベースの更新処理は非同期で実行される。