2歩戻ったら2.5歩進みたい

関東で働くweb developerのブログ

GraphQLのいろいろな読み物を読んだ時のメモ

Introduction to GraphQL

  • graphqlの仕様をまずはちゃんと読みましょう
  • ざっと流し読んで知らないところだけメモる感じで
  • fragment辺りの理解が怪しい

Query

{
  human(id: "1000") {
    name
    height(unit: FOOT)
  }
}
  • このクエリ書けるのもしかして俺知らなかった?
    • typeのfieldになんか渡すやつ出来るのか
    • ルートのtypeは知ってたけど
{
  leftComparison: hero(episode: EMPIRE) {
    ...comparisonFields
  }
  rightComparison: hero(episode: JEDI) {
    ...comparisonFields
  }
}

fragment comparisonFields on Character {
  name
  appearsIn
  friends {
    name
  }
}
  • aliasもここに出てくるのか
    • ほんでaliasすると同じ項目をまとめたいからfragmentが便利ですと。よく出来たドキュメントだ。
  • operationNameの有用性について
    • but its use is encouraged because it is very helpful for debugging and server-side logging. When something goes wrong (you see errors either in your network logs, or in the logs of your GraphQL server) it is easier to identify a query in your codebase by name instead of trying to decipher the contents.

Variables

  • variableは動的なクエリストリングの書き換えよりも楽ちんなのでメリットが有るよねという話。言われてみればそうか...

Directives

  • directiveも動的なクエリのための機構で、fragmentやfieldに対して付与することが出来る。

Mutation

Inline Fragment

query HeroForEpisode($ep: Episode!) {
  hero(episode: $ep) {
    name
    ... on Droid {
      primaryFunction
    }
    ... on Human {
      height
    }
  }
}

Schemas and Types - GraphQL

公式ドキュメントの次の章。

GraphQL Best Practices - GraphQL

公式ガイド、これより前のセクションはだいたい知ってたのでこっち読んだ方が有用そうだったので読んでいく

  • 認証とかネットワークとかは手に負えないからGraphqlのドキュメントに載ってないのではなくて、単純にGraphQLそのものというよりはプラクティスの類になるから、だそう
    • あくまでプラクティスなので、ケースによっては意図的に無視したりしてもOK
  • HTTP
    • RESTとは違ってエンドポイントは1つ。複数持ってもいいけどしんどいのでおすすめしません
  • JSON
    • レスポンスはJSONだけど、これは別に仕様ではない。Gzipとの相性がいいのでproductionではクライアントのリクエストヘッダにAccept-Encoding: gzipを付けさせたりすると圧縮されていい感じになる
  • Versioning
    • GraphQLのバージョニングは止められてないけどいろんなツールがあるから避けることをおすすめするよ
    • 破壊的な変更が起こるとバージョニングが必要になる。GraphQLは要求されたものしか返さないので破壊的な変更を起こさずに新しい型を追加していくだけで良い。やったね!
      • いやそう言っても色々あるでしょうに....
  • Nullability
    • 多くのシステムでnullは明示的に宣言する必要があるけど、GraphQLはデフォルトがnullだよ
    • なぜならGraphQLの背後にはDBや非同期処理などが存在するため、外部要因でデータが不完全になるケースが多く存在するから、という思想に基づいているよ
    • GraphQLのサーバーを作る時はあらゆる問題が起こり得ることを想定すると同時に失敗したフィールドに対してはnullが適切であることを覚えておくといいよ
  • Pagenation
    • Connectionとかね
  • Server-side Batching & Caching
    • common problemなのでDataLoaderとかで解決できるよ

Thinking in Graphs

考え方みたいなのいっぱい載ってる章。

Working with Legacy Data

  • DBのスキーマに負債が溜まってたりような状況の時はそのままGraphQLのスキーマに反映させずにクライアント側に使ってほしい形で書くのが良い。
    • Build your GraphQL schema to express "how" rather than "what". Then you can improve your implementation details without breaking the interface with older clients.

    • ここだけ言ってることが分からん

Serving over HTTP

HTTPとGraphQLの付き合い方について

Web Request Pipeline

殆どのウェブアプリのフレームワークはpipeline model(リクエストはたくさんのmiddlewareを通り抜けて処理されることを期待するモデルのこと)だけど、GraphQLは全ての認証middlewareの後ろに存在しているべき。そうすることで他のHTTPエンドポイントとセッションを共有できる。

Authorization

var postType = new GraphQLObjectType({
  name: ‘Post’,
  fields: {
    body: {
      type: GraphQLString,
      resolve: (post, args, context, { rootValue }) => {
        // return the post body only if the user is the post's author
        if (context.user && (context.user.id === post.authorId)) {
          return post.body;
        }
        return null;
      }
    }
  }
});
  • こういう型を作ると型の数だけ認可ロジックを作らないといけないので当然漏れる。
    • (やってるなぁ........)
  • ↓のが望ましい。
//Authorization logic lives inside postRepository
var postRepository = require('postRepository');

var postType = new GraphQLObjectType({
  name: ‘Post’,
  fields: {
    body: {
      type: GraphQLString,
      resolve: (post, args, context, { rootValue }) => {
        return postRepository.getBody(context.user, post);
      }
    }
  }
});
  • 改善したいンゴ〜

Pagination

  • カーソルベースのページネーションをする時、フォーマットに依存しないようにbase64するのがいいよ
  • カーソルを得るためにConnectionとnodeの間にedgeという層を挟むよ
    • Connection has many edges, edge has one node and cursorになる
    • このedgeがnodeに関するメタ情報を持ってくれるのでGoodだよ
  • さらに、あとどれぐらいページネーションできるかなどのConnection自体の情報を持つためにConnectinoの子にpageInfoを持つよ
    • pageInfoはendCursor(レスポンスとして返したリストの最後のカーソル)やhasNextPageなどを返すよ
  • おっ公式でfriendsConnectionって名前つけてるな

Global Object Identification

GraphQL - Authorization - graphql-ruby

graphql-rubyの認可プラクティス。

  • typeやargumentはauthorized?のメソッドをデフォで持ってるのでこれを活用していく。
    • そもそもここで間違ってるんだよな...
  • だいたいこんな感じで認可する。BaseObjectの場合はresolveの後に呼ばれて、falseを返すと結果を返さなくなる。
class Types::Friendship < Types::BaseObject
  # You can only see the details on a `Friendship`
  # if you're one of the people involved in it.
  def self.authorized?(object, context)
    super && (object.to_friend == context[:viewer] || object.from_friend == context[:viewer])
  end
end
  • Fieldだとこんな感じ。↓ Fieldのauthorized?はresolveの前に呼ばれる。
class Types::BaseField < GraphQL::Field
  # Pass `field ..., require_admin: true` to reject non-admin users from a given field
  def initialize(*args, **kwargs, require_admin: false, &block)
    @require_admin = require_admin
    super(*args, **kwargs, &block)
  end

  def authorized?(obj, args, ctx)
    # if `require_admin:` was given, then require the current user to be an admin
    super && (@require_admin ? ctx[:viewer]&.admin? : true)
  end
end
  • こういうのをquery_typeに書いてたりするので暇があったらドンドコ移していったほうが良いな
    • profile周りどうすればいいかな.........
    def wish_lists(user_id:)
      return [] unless request_from_owner?(user_id: user_id.to_i) || User.find(user_id)&.profile&.info_public?

      UserWishList.where(user_id: user_id)
    end

    private

    def request_from_owner?(user_id:)
      context[:current_user]&.id == user_id.to_i
    end

GraphQL - Directives - graphql-ruby

directiveで認可の設計できるんだっけ?ということを知りたい

  • graphqlのdirectiveには2種類ある。 runtime directives クライアント側から投げつける@includeとか@skipとかのdirective。クライアント側でクエリの形を動的に変えたい時に使うやつ。 schema directives スキーマに対してアノテートするディレクティブ。こういうの↓
type User {
  firstName @deprecated(reason: "Use `name` instead")
  lastName @deprecated(reason: "Use `name` instead")
  name
}

GraphQL Advent Calendar 2020 - Qiita

なんか見っけた。

GraphQL - Lookahead - graphql-ruby

GraphQL-Rubyのv1.9からlookaheadという、クエリが要求したフィールドをType側で先読みできる機能ができたらしい。

field :files, [Types::File], null: false, extras: [:lookahead]

def files(lookahead:)
  if lookahead.selects?(:full_path)
    # This is a query like `files { fullPath ... }`
  else
    # This query doesn't have `fullPath`
  end
end
  • よっぽど重い処理やってないと使わないと思うけど...

初めて Apollo Client を使うことになったらキャッシュについて知るべきこと - WASD TECH BLOG

取得するフィールドに id は必ず含める 更新処理のときは Mutation のレスポンスでオブジェクトのキャッシュを更新する 作成、削除処理のときは refetchQueries などを使い配列のキャッシュを更新する 画面表示のたびに最新のデータを表示したければ fetchPolicy: "cache-and-network" を使う

やっぱapolloとかシュッとしたGraphQL client使ってるとこの辺がスタンダードなんだろうな

Shopify/graphql-design-tutorial - GitHub.com

takadaくんがtimesに貼ってたやつ。

  • 中間テーブルがDBに存在しても、ビジネスのドメインで意味がないんだったらスキーマには含めないほうが良い。
  • 特定可能な主要なビジネスオブジェクト(例えば商品やコレクションなど)はNodeインターフェースを実装するべきです。

type Collection implements Node {
  id: ID!
}

Protip: リストのように、Booleanもほとんどいつも非Nullです。 NullableなBooleanを使う場合は、本当に3状態(Null/false/true)を区別する必要があるのかという点と、設計上のより大きな問題を招かないことを確認ましょう。* 手動コレクションの場合にこのフィールドは一体どの値をもつべきでしょうか。 trueとfalseのいずれもミスリーディングのように感じます。 かといってNullableにしたところで3状態を表現するフラグは、自動コレクションの場合に違和感を覚えます。 ルール #6: 密接に関連する複数のフィールドはサブオブジェクトにまとめること。

  • bodyHtmlは詳細設計なのでdescriptionに変えた上でHtml typeを作るのが良いって言ってるなー
    • たしかにこのケースだとコンテキスト表すのに型作ったほうが良いか

      2つ目は、こちらのほうが大きな問題です、クライアントに実装を要求するという点です。 これは設計哲学における最も重要なポイントのひとつで、つねにサーバーこそが唯一の正しいビジネスロジックの情報源であるべきです。 ほとんどの場合でAPIは複数のクライアントに対して提供されます。 もしそれぞれのクライアントが同じロジックを実装しなければならないとしたら、それはコードの重複を生み、不必要なタスクとエラーが発生させる余地を伴います。 ルール #12: APIはデータだけではなくビジネスロジックを提供すること。複雑な計算はサーバーで為されるべきで、複数のクライアントではない。

Mutationについて

  • 肥大化するupdateにどう立ち向かうのか?
    • 別々のmutation(addProductやremoveProductなど)に分ける方法。柔軟、効率的で、最もうまくいく。

    • 急にポジション取っててウケる
    • ルール #15: 関連に対する操作は複雑で、ひとつの便利な指針で語ることはできない。 ルール #19: フォーマットが明確であり、クライアント側の検証が複雑な場合には、入力に対して弱い型付け(EmailではなくString)を行うこと。これによりサーバーは一度にすべての検証を実行し、単一のフォーマットでエラーを報告することになり、結果としてクライアントが非常にシンプルになる。

  • 分かりみがある
  • updateとcreateのIFの話
    • 共通にしちゃった
type Mutation {
  # ...
  collectionCreate(collection: CollectionInput!)
  collectionUpdate(collectionId: ID!, collection: CollectionInput!)
}

input CollectionInput {
  title: String
  ruleSet: CollectionRuleSetInput
  image: ImageInput
  description: String
}

ルール #21: たとえいくつかのフィールドの必須制約を緩和する必要があっても、重複を減らすためにmutationの入力を構造化すること。

  • これは意外だなぁ...

    各mutationはその他の必要な情報に加えてユーザエラーフィールドを含む"payload"型を定義すべきです。 createのmutationはおそらく以下のようになるでしょう。

type CollectionCreatePayload {
  userErrors: [UserError!]!
  collection: Collection
}

type UserError {
  message: String!

  # Path to input field which caused the error.
  field: [String!]
}
  • data: {"CollectionCreatePayload": {"collection": {...}, userErrors: [...]}を許容するということかー
    • うーんあんまり賛成できないな
    • GraphQLの仕様にerrorsとdataがあるのにそれ以上のことをdataで行うのはどうなんだろ
    • extensionsならまだ分かる
      • ルール #22: mutationはビジネスロジックレベルのエラーをuserErrorsフィールドに入れて返すこと。トップレベルのエラーフィールドはクライアントおよびサーバーレベルのエラーのために残しておくこと。

    • あートップレベルはクライアント&サーバーレベルに残すってそういうことか

Designing GraphQL Mutations - apollo blog

後方互換性を保ちながらmutationを設計するためのtips。

Naming

  • #{動詞}#{名詞}で構成するのが良い
    • Shopifyは操作対象のデータモデルでアルファベット順に並べたいので#{名詞}#{動詞}になってるけど、多くのアプリケーションでは操作されるデータとアクション名が対になることが少ない。
    • 例えばsendPasswordResetEmailというmutationは操作対象というよりはRPC(Remote Procedure Call)的な発想なので動詞が先に来て欲しい。
      • Shopifyよりもこっちのほうが賛成できるな〜〜〜〜

Specificity

  • mutationは汎化させて引数でどうこうするよりも特定のケースごとに作ったほうが良い。
    • 例えばsendEmail(type: PASSWORD_RESET)みたいなmutationは未来の機能拡張によっては引数や返り値が複雑になりうるのでsendPasswordResetEmailとして言い切ってしまったほうが良い。
      • Specific mutations that correspond to semantic user actions are more powerful than general mutations

      • とあるのでめっちゃ強い思いを持ってるっぽい

Nesting

input object

  • inputは必ず1つのnon-nullかつmutationごとにユニークなinput objectで渡されるべき。要はこういうこと↓
# Good
updatePost(input: { id: 4, newText: "..." }) { ... }

# Not Good
updatePost(id: 4, newText: "...") { ... }
  • 小さい違いだけれど、引数の数が多くなった時にGraphQLのmutation stringがかなり違ってくる↓
mutation MyMutation($input: UpdatePostInput!) {
  updatePost(input: $input) { ... }
}

# vs.

mutation MyMutation($id: ID!, $newText: String, ...) {
  updatePost(id: $id, newText: $newText, ...) { ... }
}
  • input objectは可能な限りネストしたほうがいい。
    • ネストすると将来的に不必要になったフィールドだけdeprecatedを付けたりして別のフィールドを置いたり出来る。
mutation {
  createPerson(input: {
    # By nesting we have room at the top level of `input`
    # to add fields like `password`, or metadata fields like
    # `clientMutationId` for Relay. We could also deprecate
    # `person` in the future to use another top level field
    # like `partialPerson`.
    password: "qwerty"
    person: {
      id: 4
      name: "Budd Deey"
    }
  }) { ... }
  updatePerson(input: {
    # The `id` field represents who we want to update.
    id: 4
    # The `patch` field represents what we want to update.
    patch: {
      name: "Budd Deey"
    }
  }) { ... }
}
  • そうなのか〜〜〜
    • mutation stringの長さが短くなるのはそうなんだけど、結局mutation stringの中でinputから取り出すのは同じだからそこまで短くなるんだっけ?とは思ったけどまぁdeprecatedにしやすいってのは納得した

mutation payload

  • mutation payloadに関してもinputと同じことが言えて、ネストするのが望ましい。

mutation { createPerson(input: { ... }) { # You could add other fields now to your mutation payload. # Like `clientMutationId` or `userErrors`. person { id name } } updatePerson(input: { ... }) { person { id name } } } - 要するにこういう風に書いて、いきなりnameとかidとかのプリミティブな型を置くなという話 - >Preemptively removing design space is not something you want to do when designing a versionless GraphQL API. - 金言だなぁ