"How to GraphQL" チュートリアルからのメモ

# まとめ
- GraphQLについて言及されている場面に遭遇したので、調べてみたところ以下のページを見つけたので、チュートリアルに沿って動かしてみた。
- 言語はpythonを選択。今のところ読み解けるのがpythonだけだというのと、知りたいのはサーバ側の挙動でクライアント側ではなかったから、というのが理由。

https://www.howtographql.com/

# 次やってみること
- 同じ作業をEC2でやってみよう。

# チュートリアルからのメモ

## 1. ローカル環境の設定
pythonは3.6を使うこと、と明示されていたので、初めてvirtualenvを使用してみる。`pyenv local`だけでうまくいくのかも知れなかったけど、以前にEdukitで指定のパッケージがインストールできず、あれやこれやあくせくした経験があったので、チュートリアル用の環境をvirtualenvで分けることにした。

```Shell:Python環境設定
> pyenv install 3.6.15 # 指定されているpython3.6をインストール
> pyenv virtualenv 3.6.15 graphql3.6.15 # 環境作成
> cd workdir/python
> mkdir graphql
> cd graphql/
> pyenv local graphql3.6.15 # フォルダに環境を紐付け
```

## 2. DjangoとGrapheneのインストール
> - Djangoは、Pythonで実装されたWebアプリケーションフレームワーク。
> - Graphene is a library that provides tools to implement a GraphQL API in Python using a code-first approach.

サーバの立ち上げはめちゃくちゃ簡単。お決まりのHello World!的な。素人としては、この拍子抜けする感じにはまだ慣れない。ともあれ、以下のコマンドを叩けば、ローカル環境にサーバが立ち上がる。

```Shell:Djangoサーバセットアップ
> pip install django==2.1.4 graphene-django==2.2.0 django-filter==2.0.0 django-graphql-jwt==0.1.5
> django-admin startproject hackernews
> cd hackernews
> python manage.py migrate
> python manage.py runserver
```
ブラウザで`http://localhost:8000`にアクセスすると、サーバが立ち上がっている。`django-admin startproject hackernews`で立ち上がったサーバの構成は以下の通り。全く関係ないけど、treeパッケージをインストールしたのが最近で、楽しくてやたらと`tree`と打っている。

```Shell:Tree(一部省略)
.
└── hackernews
    ├── db.sqlite3
    ├── hackernews
    │   ├── __init__.py
    │   ├── __pycache__ # 下にファイルあり
    │   ├── settings.py
    │   ├── urls.py
    │   └── wsgi.py
    └── manage.py
```
## 3. Graphene Djangoのコンフィグ
正直、このセクションは呪文。。。とりあえずやったことを列挙しておく。

- settings.pyの中の`INSTALLED_APPS`変数に`'graphene-dgango'`を追加
- settings.pyの中で新たな変数`GRAPHENE`を設定

```python:GRAPHENE
GRAPHENE = {
    'SCHEMA': 'hackernews.schema.schema',
}
```

## 4. Linksアプリ立ち上げ

次にLinksというアプリケーションを立ち上げる。コマンドは`python manage.py startapp links`。Djangoはプロジェクトをアプリケーションごとに分けると説明されていたが、その意味はよく分からない。いずれ分かるんだろう。。。その他にいくつかコンフィグ設定。

- links/models.pyにLinkのモデルを追加する。
- hackernews/settings.pyの`INSTALLED_APPS`に`'links'`を追加
- 新たなDBテーブルを作成する。コマンドは以下の２行
    - `python manage.py makemigrations`
    - `python manage.py migrate'
- テーブルの中身をShellから入力する

```Shell:Step4終了時
.
├── db.sqlite3
├── hackernews           # 変化なしなので下のフォルダ省略
├── links                # python manage.py startapp linksで追加
│   ├── __init__.py
│   ├── admin.py
│   ├── apps.py
│   ├── migrations
│   │   └── __init__.py
│   ├── models.py
│   ├── tests.py
│   └── views.py
└── manage.py
```
## 5. TypeとSchemaの作成
> - Type: いくつかのFieldを含むオブジェクト (例: type Post { title: String! text: String! datePublished: DateTime! ...})
> - Schema: Typeの集合
> - Resolver: Fieldの値を返すメソッド

ここでは、hackernews/schema.pyとlinks/schema.pyを新たに作成してスキーマを作成する。と書いているけれど、何が設定されているのかは分からずにとりあえず進んでいる。

```shell:schema.py追加後 
.
├── db.sqlite3
├── hackernews
│   ├── __init__.py
│   ├── __pycache__
│   ├── schema.py # 新たに作成
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
├── links
│   ├── __init__.py
│   ├── __pycache__
│   ├── admin.py
│   ├── apps.py
│   ├── migrations
│   ├── models.py
│   ├── schema.py # 新たに作成
│   ├── tests.py
│   └── views.py
└── manage.py
```

## 6. GraphiQLの導入
> GraphiQL is a graphical interactive in-browser GraphQL IDE

hackernews/urls.pyにGraphiQLを読み込むラインを追加することで、ブラウザからのクエリが可能になる。

```python:追加ライン
from django.views.decorators.csrf import csrf_exempt
from graphene_django.views import GraphQLView

urlpatterns = [
    path('admin/', admin.site.urls),
    path('graphql/', csrf_exempt(GraphQLView.as_view(graphiql=True))),
]
```

## 7. Mutation
> The process of sending data to server is called mutation. Defining it is pretty similar on how you’ve defined the query.

ここでは、links/schema.pyで定義して、hackernews/schema.pyで呼び出す状態にする。これらのコードをアップデートした後からは、`http://localhost:8000/graphql/`からMutationの操作が可能になる。以下は追加例

```graphql:Mutation
mutation {
  createLink (
    url: "http://github.com",
    description: "Lots of code!"
  ) {
    id
    url
    description
  }
}
```
## 8. Userの作成
ここでは`python manage.py startapp <APP_NAME>`ではなくて、userという名前のフォルダを新たに作って、そこにschema.pyを作る、というステップを踏んでいる。ただその後はlinksの時と同様、user/schema.pyにMutationを実行するクラスを作成して、hackernews/schema.py (root schema) の方で、そのクラスを呼び出す命令を追加すると、Localhost上から`createUser`が実行できるようになる。同じ操作をQueryについても実行することで、userにクエリを走らせることも可能になる。

```graphql:createUser
mutation {
  createUser(username: "shiro", email: "shiro@example.com", password: "12345678") {
    user {
      id
      username
      email
    }
  }
}
```

## 9. User Authentication
このチュートリアルでは、JWT (JSON Web Tokens)を実装する方法を紹介している。Authenticationについてはとりあえず深追いしないで先に行く。JWTに関する情報は色々あるみたい。久々登場のhackernews/setting.pyにMIDDLEWAREの変数を追加する。

```python:hackernews/setting.py
MIDDLEWARE = [
    # all the middlewares
    'django.contrib.auth.middleware.AuthenticationMiddleware',
]
```
さらに、変数GRAPHENEにMIDDLEWARESを追記し、新たな環境変数AUTHENTICATION_BACKENDSも追加する。

```python:hackernews/setting.py
GRAPHENE = {
    'SCHEMA': 'hackernews.schema.schema',
    'MIDDLEWARES': [
        'graphql_jwt.middleware.JSONWebTokenMiddleware',
    ]
}

AUTHENTICATION_BACKENDS = [
    'graphql_jwt.backends.JSONWebTokenBackend',
    'django.contrib.auth.backends.ModelBackend',
]
```

コードをアップデートしてさて、Localhostにアクセスしようと思ったら、エラーが出て立ち上がらない。エラーメッセージから調べたら、変数名に注意！というやりとりを発見。ありがたいありがたい。

https://stackoverflow.com/questions/62271614/what-does-typeerror-init-missing-1-required-positional-argument-get-res

もう一個引っかかりポイントが。無事に`http://localhost:8000/graphql/`にアクセスできるようになった、と思ったら今度はToken発行がうまくいかない。で、これも調べるとStackOverflow様様で解決策発見。`pip install PyJWT==1.7.0`で無事に動きましたとさ。こういう解決策を探してくるところまでチュートリアルの一部だと考えると、私みたいな初心者には逆になかなか良いな、このチュートリアル。（まあ、多分直してないだけだと思うけど）

https://stackoverflow.com/questions/65704023/django-graphql-jwt-tokenauth-mutation-returns-str-object-has-no-attribute-deco

で、ここで発行されたトークンをDebuggerに入れたら、ちゃんとちゃんとちゃんと元の情報が出てくるのね。なんか感動してしまった。

https://jwt.io/

この後、Insomniaを使って、あれやらこれやら確認する、なんてステップに入っていったので割愛。適当にわからないことは飛ばしながら進みます。

## 10. Search
Searchは、links/schema.py内にあるQueryのClassにSearch機能を追記することで追加する。

```python:links/schema.py
# ..code
# After the imports, add
from django.db.models import Q

# ...code
class Query(graphene.ObjectType):
    # Add the search parameter inside our links field
    links = graphene.List(LinkType, search=graphene.String())

    # Change the resolver
    def resolve_links(self, info, search=None, **kwargs):
        # The value sent with the search parameter will be in the args variable
        if search:
            filter = (
                Q(url__icontains=search) |
                Q(description__icontains=search)
            )
            return Link.objects.filter(filter)

        return Link.objects.all()
```

## 最後に
途中のステップをいくつか飛ばしたけれど、DBについて何も知らなかったところから始めたので、基本的な考え方も透けて見えてきてかなり勉強になった。誰のためにもならない記事になってしまったけれど、これを描きながらチュートリアルを進めることで、曖昧な理解にとどめなかった箇所がいくつかあるので個人的にはとても満足。