Kosaku Kurino

Kosaku Kurino

【【GraphQL】GraphQL実装で押さえておくべき勘所

まえがき

GraphQLを触ってみて、考えないといけない点が見えてきたのでまとめます。 勘所をまとめたものになるので、コードはあまり出てきません。

前半はGraphQLあまり知らなくても理解できますが、後半の認証・認可あたりは内容が難しくなるので、GraphQL少し勉強してから読んだ方がいいかもしれません。

GraphQLの特徴

GraphQLの特徴ぐらい知ってるよって人はここ読み飛ばして下さい。

  1. エンドポイントは一つだけ
  2. 参照用クエリのqueryと、追加・更新用クエリのmutationの2種類が存在する
  3. クエリで送信するデータの型(スキーマ)を元に、必要なデータのみを型(スキーマ)通りに1回のリクエストで取得可能

メリット

この特徴から得られるメリットは、深く考えなくてもシンプルなAPIが完成する。今主流の「REST API」で起こりがちな不必要な回帰的リクエストがおこらない設計を簡単に実現できる。実装後回帰的リクエストが必要になった場合、GraphQLでは破壊的な変更を加えずすぐに拡張できるという意味。

つまり、必要な情報を1回のリクエストで取得できるAPIを拡張性・シンプルさを持ち合わせた上で簡単に作れる。

デメリット

デメリットとして、追加・更新用クエリmutationは「REST API」と同じで処理はこちら側で規定しなくてはならない。具体的にいうと、このmutaitonクエリはホゲホゲという値をどのテーブルのどのレコードに登録するか決めてあげなければならないということ。次に、認証・認可を簡単に導入できない、導入する方法があまり定まっていない。最後に、特定のクエリのみ実行させる特別対応処理を実行させる場所をどうするのか定まっていないし、そもそもGraqhQLに特別対応処理は入れるべきではない。特別対応処理とは、画像データをAWSのS3に保存して、そのURLをDBに格納させるなど。

つまり、mutationクエリの作成、認証・認可の実装、特別対応処理を考えるとGraphQLの拡張性・シンプルさが失われていく。

設計方針

メリットを残しつつ、できる限りデメリットを排除してく設計がGraphQL設計では重要になってくる。

mutationに特別処理はできる限り含めない、できる限りではなく含まなくてもいいように設計しよう。ただし、認証・認可は切ってもきれないものなので、GraphQLの機能として追加する。

特別処理は別のAPI(REST API?)を用意し、データベースの格納に関係するデータのみをGraphQLのクエリで流し追加・更新することで、GraphQLのシンプルさを保とう。2回リクエストを投げる必要が出てくるがGraqhQLのデメリットを考えたとき、やはり切り分けた方が良いとなった。

graphql_01.png

ここで残る、懸念点は認証・認可はどこにどう実装するのかである。

認証・認可の実装方法

https://slides.com/ryanchenkie/graphql-auth#/

この記事がかなり参考になる、ここで説明する内容は、基本的にここのまとめみたいなものです。

全てのクエリに認証をつける

全てのクエリに認証をつけるのは簡単です。 GraphQLを実装するサーバーにミドルウェアを追加して、ミドルウェア内に認証処理を追加すればいいだけです。ミドルウェア内で認証が失敗すれば、クエリを実行させるず、エラーを返せばできます。しかしこれではクエリごとに認証を追加できません。

クエリごとに認証・認可をつける

1. リソルバーで認証・認可のチェックを行う

リソルバーとは、クエリの型(スキーマ)をもとに、返すデータを作成する場所です。 リソルバー内でデータを返す箇所を認証・認可用関数でラップする。

リソルバーはクエリの型(スキーマ)をもとに、返すデータを作成する場所なので、認証・認可処理をここに書くのは「んー」っと思います。おすすめではありません。

認可の例

const checkScopeAndResolve = (context, expectedScope, controller) => {
  const token = context.headers.authorization;
  
  try {
    // 認証・認可処理
    // ...
     
     return controller.apply(this);
  } catch (err) {
    // 認証・認可処理失敗時の処理
  }
}

const controller = model.getArticles(context.user.id);

const resolvers = {
  Query: {
    articlesByAuthor: (_, args, context) 
      => checkScopeAndResolve(
        context.user.scope,
        ['read:articles'],
        controller
      );
  }
}
2. ディレクティブで認証・認可のチェックを行う

ディレクティブはデータの型(スキーマ)やクエリにメタデータを付与できるようなものです。 クエリに認証・認可ディレクティブを付与することで、このクエリは認証・認可が必要という風に宣言できます。 そして、クエリを実行する前に独自で作成した認証・認可ディレクティブ処理を走らせることができます。

ディレクティブの実装は少し難しいのが辛いですが、リソルバーも汚さないのでこれが一番おすすめです。

認証の例

import { SchemaDirectiveVisitor } from "graphql-tools"
import { DirectiveLocation, GraphQLDirective, GraphQLList, GraphQLString } from "graphql"
import { AuthorizationError } from '../errors'

export class IsAuthenticatedDirective extends SchemaDirectiveVisitor {
  static getDirectiveDeclaration(directiveName, schema) {
    return new GraphQLDirective({
      name: "isAuthenticated",
      locations: [DirectiveLocation.FIELD_DEFINITION]
    });
  }

  visitFieldDefinition(field) {
    const { resolve = defaultFieldResolver } = field
    field.resolve = async (...args) => {
      const ctx = args[2]
      if (!ctx || !ctx.req || !ctx.req.headers || 
 !ctx.req.headers.authorization) {
        throw new AuthorizationError({ message: "No authorization token." })
      }
      const token = ctx.req.headers.authorization

      try {
        const id_token = token.replace("Bearer ", "")

        // authorization process
        // ...

        return resolve.apply(this, args)
      }
      catch (e) {
        throw new AuthorizationError({ message: "You are not authorized." })
      }
    }
  }


}

あとがき

GraphQLはできる限り薄く、シンプルに実装した方が扱いやすくなるので、認証・認可以外は含めないよう設計しましょう。