Open1

GraphQL Code Generator メモ

yoshinoyoshino

https://the-guild.dev/graphql/codegen
https://github.com/dotansimha/graphql-code-generator

入門編

バックエンドからフロントエンドまで、GraphQL Code Generatorはその生成を自動化します。
GraphQL 操作の型付けを自動化および生成することは、開発者の体験とスタックの安定性の両方を向上させます。

フロントエンドでの使い方

GraphQLの操作タイプを手動で管理したり、タイプが完全にない場合は、多くの問題が発生する可能性があります。

  • outdated typing (regarding the current Schema)
  • typos
  • partial typing of data (not all Schema's fields has a corresponding type)
schema.graphql
type Author {
  id: Int!
  firstName: String!
  lastName: String!
  posts(findTitle: String): [Post]
}
 
type Post {
  id: Int!
  title: String!
  author: Author
}
 
type Query {
  posts: [Post]
}
codegen.ts(a simple configuration)
import type { CodegenConfig } from '@graphql-codegen/cli'
 
const config: CodegenConfig = {
   schema: 'https://localhost:4000/graphql',
   documents: ['src/**/*.tsx'],
   generates: {
      './src/gql/': {
        preset: 'client',
      }
   }
}
export default config

React Queryの場合

import { request } from 'graphql-request'
import { useQuery } from '@tanstack/react-query'
import { graphql } from './gql/gql'
 
// postsQueryDocument is now fully typed!
const postsQueryDocument = graphql(/* GraphQL */ `
  query Posts {
    posts {
      id
      title
      author {
        id
        firstName
        lastName
      }
    }
  }
`)
 
const Posts = () => {
  // React Query `useQuery()` knows how to work with typed graphql documents
  const { data } = useQuery<PostQuery>('posts', async () => {
    const { posts } = await request(endpoint, postsQueryDocument)
    return posts
  })
 
  // `data` is fully typed!
  // …
}
  • 常に最新な型
  • すべてのクエリ、ミューテーション、サブスクリプション変数、結果に対するオートコンプリート
  • Reactのフック生成のような完全なコード生成のおかげで、ボイラープレートも少なくて済む。

バックエンドでの使い方

ほとんどのGraphQL APIリゾルバは型付けされていない、または間違った型付けをしたままであり、これが複数の問題につながります。

  • リゾルバがスキーマの定義に準拠していない
  • リゾルバーの関数型シグネチャのタイプミス

このため、GraphQL Code Generatorでは、リゾルバのタイピングの生成を自動化するためのプラグインを複数用意しています。

以下は、GraphQL Code Generatorのリゾルバタイピングを活用したGraphQL APIの例です(上記のschema.graphqlをベースにしています)。

Apollo Serverの場合

import { readFileSync } from 'node:fs'
import { ApolloServer } from 'apollo-server'
import { Resolvers } from './resolvers-types'
 
const typeDefs = readFileSync('./schema.graphql', 'utf8')
 
const resolvers: Resolvers = {
  Query: {
    // typed resolvers!
  }
}
 
const server = new ApolloServer({ typeDefs, resolvers })
 
// The `listen` method launches a web server
server.listen().then(({ url }) => {
  console.log(`🚀  Server ready at ${url}`)
})

Guides

  • React
  • Appolo Server

React

以下のQueryを使ってシンプルなGraphQLフロントエンドアプリを構築し、スターウォーズの映画リストを取得します。

query allFilmsWithVariablesQuery($first: Int!) {
  allFilms(first: $first) {
    edges {
      node {
        ...FilmItem
      }
    }
  }
}

and its FilmItem Fragment definition:

fragment FilmItem on Film {
  id
  title
  releaseDate
  producers
}
codegen.ts
import { CodegenConfig } from '@graphql-codegen/cli'
 
const config: CodegenConfig = {
  schema: 'https://swapi-graphql.netlify.app/.netlify/functions/index',
  documents: ['src/**/*.tsx'],
  ignoreNoDocuments: true, // for better experience with the watcher
  generates: {
    './src/gql/': {
      preset: 'client'
    }
  }
}
 
export default config

Usage with react-query
https://the-guild.dev/graphql/codegen/docs/guides/react-vue#appendix-i-react-query-with-a-custom-fetcher-setup

Writing GraphQL Queries

React Queryの場合

src/App.tsx
import React from 'react'
import request from 'graphql-request'
import { useQuery } from '@tanstack/react-query'
 
import './App.css'
import Film from './Film'
import { graphql } from '../src/gql'
 
const allFilmsWithVariablesQueryDocument = graphql(/* GraphQL */ `
  query allFilmsWithVariablesQuery($first: Int!) {
    allFilms(first: $first) {
      edges {
        node {
          ...FilmItem
        }
      }
    }
  }
`)
 
function App() {
  // `data` is typed!
  const { data } = useQuery(['films'], async () =>
    request('https://swapi-graphql.netlify.app/.netlify/functions/index', allFilmsWithVariablesQueryDocument, {
      first: 10 // variables are typed too!
    })
  )
 
  return (
    <div className="App">
      {data && <ul>{data.allFilms?.edges?.map((e, i) => e?.node && <Film film={e?.node} key={`film-${i}`} />)}</ul>}
    </div>
  )
}
 
export default App

提供されているgraphql()関数(../src/gql/)を使用して、GraphQLクエリまたはMutationを定義するだけで、お気に入りのクライアントにGraphQLドキュメントを渡すだけですぐに型付き変数と結果を得ることができます!

Writing GraphQL Fragments

GraphQL フラグメントは、より優れた分離と再利用可能な UI コンポーネントの構築に役立ちます。
ReactでのFilmUIコンポーネントの実装を見てみましょう。

src/Film.tsx
import { FragmentType, useFragment } from './gql/fragment-masking'
import { graphql } from '../src/gql'
 
export const FilmFragment = graphql(/* GraphQL */ `
  fragment FilmItem on Film {
    id
    title
    releaseDate
    producers
  }
`)
 
const Film = (props: {
  /* `film` property has the correct type 🎉 */
  film: FragmentType<typeof FilmFragment>
}) => {
  const film = useFragment(FilmFragment, props.film)
  return (
    <div>
      <h3>{film.title}</h3>
      <p>{film.releaseDate}</p>
    </div>
  )
}
 
export default Film

<FilmItem>コンポーネントは、生成されたコード(../src/gql)から、FragmentType<T>型ヘルパーとuseFragment()関数という2つのインポートを利用していることがわかるでしょう。

  • FragmentType<typeof FilmFragment>を使って、対応するFragmentのTypeScript型を取得します。
  • その後、useFragment()を使ってfilmプロパティを取得する。

FragmentType<typeof FilmFragment>と useFragment()を活用することで、UIコンポーネントを分離し、親GraphQL Queryの型付けを継承しないようにします。

https://the-guild.dev/blog/unleash-the-power-of-fragments-with-graphql-codegen

おめでとうございます!これで、完全に型付けされたクエリーとミューテーションで、最高のGraphQLフロントエンド体験を手に入れました。

Appolo Server

https://the-guild.dev/blog/better-type-safety-for-resolvers-with-graphql-codegen

Config Reference

codegen.ts

GraphQL Code Generatorは、codegen.tsという設定ファイルに依存して、すべての可能なオプション、入力、および出力ドキュメント タイプを管理します。

schema field

  • schemaフィールドはGraphQLSchemaを指す必要があり、これを指定してGraphQLSchemaを読み込む方法は複数あります。
  • schemaには、スキーマを指す文字列か、マージされる複数のスキーマを指す文字列[]を指定することができます。

documents field

  • documentsフィールドは、GraphQLドキュメント(query、mutation、subscription、fragment)を指す必要があります。
  • documentsは、クライアントサイド用のコードを生成するプラグインを使用する場合のみ必要です。
  • 指定できるのは、自分の文書を指す文字列か、複数の文書を指す文字列[]のどちらかです。

plugin config

  • configフィールドは、プラグインに設定を渡すために使用されます。
  • configは、多くのレベルで指定することができます。
    • ルートレベル
    • 出力レベル
    • プラグインレベル

require field

requireフィールドは、事前にトランスパイルする必要なく、任意の外部ファイルを読み込むことができます。

plugin

クライアント

  • typescript
  • typescript-operations
  • typescript-graphql-request or typescript-react-query

サーバーサイド

  • @graphql-codegen/typescript
  • @graphql-codegen/typescript-resolvers

参考にするといいかも記事

https://techlife.cookpad.com/entry/2021/03/24/123214
https://qiita.com/kyntk/items/624f9b340e813844a292
https://zenn.dev/sky/articles/47b86d3387389d
https://zenn.dev/azuharu07/articles/fae5dec8f93080