2021/10/22 Kaigi on Rails 2021
株式会社マネーフォワード 玉井あゆみ @ayumitamai97
2021/10/22 Kaigi on Rails 2021
株式会社マネーフォワード 玉井あゆみ @ayumitamai97
APIに問い合わせ可能なあらゆるデータ型を定義する
特定の実装(言語、フレームワーク)に依存しない
型の種類
query Query( $login: String!, $privacy: RepositoryPrivacy, $repoLast: Int, $languagesFirst: Int ) { user(login: $login) { name bio repositories(privacy: $privacy, last: $repoLast) { nodes { name languages(first: $languagesFirst) { nodes { name } } } } } }
query Query( $login: String!, $privacy: RepositoryPrivacy, $repoLast: Int, $languagesFirst: Int ) { user(login: $login) { name bio repositories(privacy: $privacy, last: $repoLast) { nodes { name languages(first: $languagesFirst) { nodes { name } } } } } }
{ "login": "ayumitamai97", "privacy": "PUBLIC", "repoLast": 5, "languagesFirst": 1 }
{ "login": "ayumitamai97", "privacy": "PUBLIC", "repoLast": 5, "languagesFirst": 1 }
{ "data": { "user": { "name": "Ayumi Tamai", "bio": "・x・", "repositories": { "nodes": [ { "name": "cont-codegen-sandbox-api", "languages": { "nodes": [ { "name": "Ruby" } ] } }, { "name": "cont-codegen-sandbox-client", "languages": { "nodes": [ { "name": "HTML" } ] } }, { "name": "constellation-theme", "languages": { "nodes": [ { "name": "Vim script" } ] } }, { "name": "k8s-config", "languages": { "nodes": [] } }, { "name": "eks-example", "languages": { "nodes": [ { "name": "Dockerfile" } ] } } ] } } } }
{ "data": { "user": { "name": "Ayumi Tamai", "bio": "・x・", "repositories": { "nodes": [ { "name": "cont-codegen-sandbox-api", "languages": { "nodes": [ { "name": "Ruby" } ] } }, { "name": "cont-codegen-sandbox-client", "languages": { "nodes": [ { "name": "HTML" } ] } }, { "name": "constellation-theme", "languages": { "nodes": [ { "name": "Vim script" } ] } }, { "name": "k8s-config", "languages": { "nodes": [] } }, { "name": "eks-example", "languages": { "nodes": [ { "name": "Dockerfile" } ] } } ] } } } }
gem 'graphql'
Ruby における GraphQL 実装
GraphQL のリクエストを受け付ける GraphQLController
を定義する
GraphQL Spec 本体には定められていない発展的な機能も含まれる
TypeScript の型を GraphQL API に合わせて定義したい
GraphQL Schema の型と TypeScript の型とを二重管理するのは辛い
GraphQL の型システムを説明するために使われる Interface Definition Language
.graphql
API – フロントエンド間の共通言語として GraphQL Schema IDL を利用する
※ 以下の前提で進みます
GraphQL::RakeTask
GraphQL::RakeTask
を利用して、*.graphql
を生成するpre-commit
などに bin/rake graphql:schema:idl
を設定しておくと便利# lib/tasks/graphql.rake # Documentation: https://graphql-ruby.org/api-doc/1.9.12/GraphQL/RakeTask require "graphql/rake_task" GraphQL::RakeTask.new( schema_name: 'MySchema', idl_outfile: 'schema.graphql', # Schema Introspection に認証・認可などが必要な場合は # よしなに context を渡す load_context: lambda do |_task| { current_user: User.new(role: 'full_access') } end )
# lib/tasks/graphql.rake # Documentation: https://graphql-ruby.org/api-doc/1.9.12/GraphQL/RakeTask require "graphql/rake_task" GraphQL::RakeTask.new( schema_name: 'MySchema', idl_outfile: 'schema.graphql', # Schema Introspection に認証・認可などが必要な場合は # よしなに context を渡す load_context: lambda do |_task| { current_user: User.new(role: 'full_access') } end )
# schema.graphql schema { query: Query } type Query { user(id: ID!): User } type User { id: ID! username: String! }
# schema.graphql schema { query: Query } type Query { user(id: ID!): User } type User { id: ID! username: String! }
graphql-codegen
graphql-codegen
というパッケージを利用して、 GraphQL IDL から TypeScript の型を自動で生成する# schema.graphql schema { query: Query } type Query { user(id: ID!): User } type User { id: ID! username: String! }
# schema.graphql schema { query: Query } type Query { user(id: ID!): User } type User { id: ID! username: String! }
# codegen.yml # Documentation: https://www.graphql-code-generator.com/docs/getting-started/codegen-config overwrite: true schema: - github:{OWNER}/{API_REPO}#{BRANCH}:schema.graphql: token: ${GITHUB_PAT} # 指定したリポジトリのREAD権限を持つ generates: ./types/graphql.ts: plugins: - typescript config: # GraphQL Client として apollo-client を使用する場合 apolloClientVersion: 2
# codegen.yml # Documentation: https://www.graphql-code-generator.com/docs/getting-started/codegen-config overwrite: true schema: - github:{OWNER}/{API_REPO}#{BRANCH}:schema.graphql: token: ${GITHUB_PAT} # 指定したリポジトリのREAD権限を持つ generates: ./types/graphql.ts: plugins: - typescript config: # GraphQL Client として apollo-client を使用する場合 apolloClientVersion: 2
※ 紙幅の都合上、改行を多々省いています
// types/graphql.ts export type Maybe<T> = T | null; export type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] }; export type MakeOptional<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]?: Maybe<T[SubKey]> }; export type MakeMaybe<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]: Maybe<T[SubKey]> }; /** All built-in and custom scalars, mapped to their actual values */ export type Scalars = { ID: string; String: string; Boolean: boolean; Int: number; Float: number; }; export type Query = { __typename?: 'Query'; user?: Maybe<User>; }; export type QueryUserArgs = { id: Scalars['ID']; }; export type User = { __typename?: 'User'; id: Scalars['ID']; username: Scalars['String']; };
// types/graphql.ts export type Maybe<T> = T | null; export type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] }; export type MakeOptional<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]?: Maybe<T[SubKey]> }; export type MakeMaybe<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]: Maybe<T[SubKey]> }; /** All built-in and custom scalars, mapped to their actual values */ export type Scalars = { ID: string; String: string; Boolean: boolean; Int: number; Float: number; }; export type Query = { __typename?: 'Query'; user?: Maybe<User>; }; export type QueryUserArgs = { id: Scalars['ID']; }; export type User = { __typename?: 'User'; id: Scalars['ID']; username: Scalars['String']; };
APIのプロジェクトとフロントエンドのプロジェクト間で型定義の同期をとる方法
git commit
すると差分が競合するgraphql-codegen
を実行する graphql-codegen
を実行する自動化には GitHub Actions を利用する
name: GraphQL Schema IDL ファイルの更新をフロントエンドのリポジトリに通知する on: push: branches: [ main ] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - uses: technote-space/get-diff-action@v4.0.1 with: PATTERNS: schema.graphql # このファイルに差分があった場合は env.GIT_DIFF にファイル名が格納される - name: フロントエンドのリポジトリに repository_dispatch イベントを通知する uses: peter-evans/repository-dispatch@v1 with: repository: {OWNER}/{FRONTEND_REPO} token: ${{ secrets.GITHUB_PAT }} event-type: graphql-schema-updated if: env.GIT_DIFF
name: GraphQL Schema IDL ファイルの更新をフロントエンドのリポジトリに通知する on: push: branches: [ main ] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - uses: technote-space/get-diff-action@v4.0.1 with: PATTERNS: schema.graphql # このファイルに差分があった場合は env.GIT_DIFF にファイル名が格納される - name: フロントエンドのリポジトリに repository_dispatch イベントを通知する uses: peter-evans/repository-dispatch@v1 with: repository: {OWNER}/{FRONTEND_REPO} token: ${{ secrets.GITHUB_PAT }} event-type: graphql-schema-updated if: env.GIT_DIFF
name: APIのリポジトリの GraphQL Schema IDL を読んで TypeScript の型を生成し、PRを作成する on: repository_dispatch: types: [ graphql-schema-updated ] jobs: update-graphql-schema-ts: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - run: yarn install - name: TypeScript 型定義を生成 run: GITHUB_PAT=${{ secrets.GITHUB_PAT }} yarn graphql-codegen - name: 現在のブランチの状態をコミットし、PRを作成する uses: peter-evans/create-pull-request@v3 with: token: ${{ secrets.GITHUB_PAT }} # automerge-action を使わない場合は `GITHUB_TOKEN` で十分 branch: feature/update-graphql-schema title: '[bot] Update types/graphql-schema.ts' labels: automerge # automerge-action を使う場合 # ...
name: APIのリポジトリの GraphQL Schema IDL を読んで TypeScript の型を生成し、PRを作成する on: repository_dispatch: types: [ graphql-schema-updated ] jobs: update-graphql-schema-ts: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - run: yarn install - name: TypeScript 型定義を生成 run: GITHUB_PAT=${{ secrets.GITHUB_PAT }} yarn graphql-codegen - name: 現在のブランチの状態をコミットし、PRを作成する uses: peter-evans/create-pull-request@v3 with: token: ${{ secrets.GITHUB_PAT }} # automerge-action を使わない場合は `GITHUB_TOKEN` で十分 branch: feature/update-graphql-schema title: '[bot] Update types/graphql-schema.ts' labels: automerge # automerge-action を使う場合 # ...
name: automerge ラベルがつけられたPRを自動で approve & merge する on: pull_request: types: - labeled jobs: wait-for-checks-and-auto-merge: runs-on: ubuntu-latest steps: - name: Checks がパスするまで待つ uses: fountainhead/action-wait-for-check@v1.0.0 id: wait-for-build-and-test with: token: ${{ secrets.GITHUB_TOKEN }} ref: ${{ github.event.pull_request.head.sha || github.sha }} checkName: build_and_test # 実行完了を待ちたい Checks の名前を指定する intervalSeconds: 60 timeoutSeconds: 1200 # 次ページへ続く
name: automerge ラベルがつけられたPRを自動で approve & merge する on: pull_request: types: - labeled jobs: wait-for-checks-and-auto-merge: runs-on: ubuntu-latest steps: - name: Checks がパスするまで待つ uses: fountainhead/action-wait-for-check@v1.0.0 id: wait-for-build-and-test with: token: ${{ secrets.GITHUB_TOKEN }} ref: ${{ github.event.pull_request.head.sha || github.sha }} checkName: build_and_test # 実行完了を待ちたい Checks の名前を指定する intervalSeconds: 60 timeoutSeconds: 1200 # 次ページへ続く
# 前のページの続き # jobs.wait-for-checks-and-auto-merge.steps - name: CIがパスしていたらPRをapproveする uses: hmarr/auto-approve-action@v2.0.0 if: | steps.wait-for-build-and-test.outputs.conclusion == 'success' && startsWith(github.head_ref, 'feature/update-graphql-schema') == true with: github-token: ${{ secrets.GITHUB_TOKEN }} - name: CIがパスしていたらPRを自動マージする uses: pascalgn/automerge-action@v0.13.0 if: steps.wait-for-build-and-test.outputs.conclusion == 'success' env: UPDATE_RETRIES: 10 UPDATE_RETRY_SLEEP: 10000 MERGE_DELETE_BRANCH: true
# 前のページの続き # jobs.wait-for-checks-and-auto-merge.steps - name: CIがパスしていたらPRをapproveする uses: hmarr/auto-approve-action@v2.0.0 if: | steps.wait-for-build-and-test.outputs.conclusion == 'success' && startsWith(github.head_ref, 'feature/update-graphql-schema') == true with: github-token: ${{ secrets.GITHUB_TOKEN }} - name: CIがパスしていたらPRを自動マージする uses: pascalgn/automerge-action@v0.13.0 if: steps.wait-for-build-and-test.outputs.conclusion == 'success' env: UPDATE_RETRIES: 10 UPDATE_RETRY_SLEEP: 10000 MERGE_DELETE_BRANCH: true