wheatandcatの開発ブログ

React Nativeで開発しているペペロミア & memoirの技術系記事を投稿してます

gqlgenとgraphql-codegenでGraphQLのtypeを自動生成してフロントエンドのコード作成する

バックエンドをgqlgenで実装して

gqlgen.com

graphql-codegenでスキーマ情報からTypeScriptのtypeを自動生成する構成で実装したました。

graphql-code-generator.com

実装

gqlgenでバックエンドを実装

■ Pull Request https://github.com/wheatandcat/PeperomiaBackend/pull/23/files

まずは、以下のチュートリアルから、そのまま実装

gqlgen.com

$ go get github.com/99designs/gqlgen
$ go run github.com/99designs/gqlgen init

これで初期ファイルが生成されるので、まずは以下を参考にgin側のHandlerを追加

Using Gin to setup HTTP handlers — gqlgen

次に、実装に合わせてschema.graphqlsを以下の通りに修正

■ graph/schema.graphqls

type Item {
  id: ID!
  title: String!
  kind: String!
  itemDetails: [ItemDetail]!
  calendar: Calendar!
}

type Calendar {
  id: ID!
  itemId: String!
  date: String!
}

type ItemDetail {
  id: ID!
  title: String!
  itemId: String!
  kind: String!
  moveMinutes: Int!
  place: String!
  url: String!
  memo: String!
  priority: Int!
}

type Query {
  item(id: ID!): Item!
}

今回は更新は無いのでMutationは無しで実装していきます

schema.graphqlsを書き換えたら以下のコマンドを実行して再生成

$ go run github.com/99designs/gqlgen generate

これでスキーマの情報にあせてtypeとResolverを自動生成してくれます

■ graph/model/models_gen.go

package model

type Calendar struct {
    ID     string `json:"id"`
    ItemID string `json:"itemId"`
    Date   string `json:"date"`
}

type Item struct {
    ID          string        `json:"id"`
    Title       string        `json:"title"`
    Kind        string        `json:"kind"`
    ItemDetails []*ItemDetail `json:"itemDetails"`
    Calendar    *Calendar     `json:"calendar"`
}

type ItemDetail struct {
    ID          string `json:"id"`
    Title       string `json:"title"`
    ItemID      string `json:"itemId"`
    Kind        string `json:"kind"`
    MoveMinutes int    `json:"moveMinutes"`
    Place       string `json:"place"`
    URL         string `json:"url"`
    Memo        string `json:"memo"`
    Priority    int    `json:"priority"`
}

あとはResolverにFirestoreからデータを取得するコードを追加して完成

■ graph/schema.resolvers.go

package graph

import (
    "context"

    "github.com/wheatandcat/PeperomiaBackend/graph/generated"
    "github.com/wheatandcat/PeperomiaBackend/graph/model"
)

func (r *queryResolver) Item(ctx context.Context, id string) (*model.Item, error) {
    h := r.Handler
    item := &model.Item{}

    i, err := h.App.ItemRepository.FindByPublicAndID(ctx, h.FirestoreClient, id)
    if err != nil {
        return item, err
    }

    c, _ := h.App.CalendarRepository.FindByItemID(ctx, h.FirestoreClient, id)

    ids, _ := h.App.ItemDetailRepository.FindByItemID(ctx, h.FirestoreClient, id)

    item = i.ToModel()
    item.Calendar = c.ToModel()

    for _, id := range ids {
        item.ItemDetails = append(item.ItemDetails, id.ToModel())
    }

    return item, nil
}

// Query returns generated.QueryResolver implementation.
func (r *Resolver) Query() generated.QueryResolver { return &queryResolver{r} }

type queryResolver struct{ *Resolver }

これで実際にAPIを叩くと以下みたいな感じで取得できるようになります

f:id:wheatandcat:20200713200109p:plain

graphql-codegenでtypeを自動生成

先程、作成したschema.graphqlsの元にgraphql-codegeでtypeファイルを生成します。

■ Pull Request

https://github.com/wheatandcat/PeperomiaWeb/pull/47

まずは以下のインストールする

$ yarn add -D @graphql-codegen/cli @graphql-codegen/typescript @graphql-codegen/typescript-operations

まず、以下のコマンドをpackage.jsonに追加

■ package.json

  "scripts": {
    ...
    "download:schema.graphqls": "curl -L -O https://raw.githubusercontent.com/wheatandcat/PeperomiaBackend/master/graph/schema.graphqls",
    "codegen": "npm run download:schema.graphqls && graphql-codegen",
    "codegen:lint:fix": "eslint --fix ./queries/types/index.d.ts"
  },

以下の設定ファイルを作成

■ codegen.yml

overwrite: true
schema:
  - ./schema.graphqls
documents:
  - "./queries/**/*.gql"
generates:
  ./queries/types/index.d.ts:
    hooks:
      afterOneFileWrite:
        - yarn codegen:lint:fix
    plugins:
      - typescript
      - typescript-operations
    config:
      skipTypename: true
      preResolveTypes: true

そして、Web画面で使用するGraphQLファイルを設置

■ queries/shareItem.gql

query ShareItem($itemId: ID!) {
  item(id: $itemId) {
    id
    title
    kind
    itemDetails {
      id
      title
      kind
      moveMinutes
      place
      url
      memo
      priority
    }
    calendar {
      id
      date
    }
  }
}

ここまで準備完了です。あとは以下のコマンドを実行

$ yarn codegen

すると、スキーマ情報とgqlファイルから以下のtypeファイルが生成されます

■ queries/types/index.d.ts

export type Maybe<T> = T | null
export type Exact<T extends { [key: string]: any }> = { [K in keyof T]: T[K] }
/** 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 Calendar = {
  id: Scalars['ID']
  itemId: Scalars['String']
  date: Scalars['String']
}

export type Item = {
  id: Scalars['ID']
  title: Scalars['String']
  kind: Scalars['String']
  itemDetails: Array<Maybe<ItemDetail>>
  calendar: Calendar
}

export type ItemDetail = {
  id: Scalars['ID']
  title: Scalars['String']
  itemId: Scalars['String']
  kind: Scalars['String']
  moveMinutes: Scalars['Int']
  place: Scalars['String']
  url: Scalars['String']
  memo: Scalars['String']
  priority: Scalars['Int']
}

export type Query = {
  item: Item
}

export type QueryItemArgs = {
  id: Scalars['ID']
}

export type ShareItemQueryVariables = Exact<{
  itemId: Scalars['ID']
}>

export type ShareItemQuery = {
  item: {
    id: string
    title: string
    kind: string
    itemDetails: Array<
      Maybe<{
        id: string
        title: string
        kind: string
        moveMinutes: number
        place: string
        url: string
        memo: string
        priority: number
      }>
    >
    calendar: { id: string; date: string }
  }
}

@nuxtjs/apolloでWeb側を実装

上記で作成したtypeを使用してnuxtjs/apolloで実装

github.com

nuxt.config.jsに以下を追加

■ nuxt.config.js

  modules: ['@nuxt/http', '@nuxtjs/firebase', '@nuxtjs/apollo'],
  ...
  apollo: {
    tokenName: 'yourApolloTokenName',
    cookieAttributes: {
      expires: 7,
    },
    includeNodeModules: true,
    clientConfigs: {
      default: '~/plugins/apollo-client.ts',
    },
  },

これnuxtjs/apolloが使用できるので、composition apiを使用してAPI取得部分を作成

■ use/useShareItem.ts

import { reactive, SetupContext, toRefs } from '@vue/composition-api'
import shareItemQuery from '~/queries/shareItem.gql'
import { ShareItemQuery, ShareItemQueryVariables } from '~/queries/types'

type UseFetchShareItemState = {
  item: ShareItemQuery['item'] | null
  loading: boolean
}

const useFetchShareItem = (ctx: SetupContext) => {
  const state = reactive<UseFetchShareItemState>({
    item: null,
    loading: true,
  })

  const fetchShareItem = async (itemId: string) => {
    state.loading = true
    const res = await ctx.root.$apollo.query<
      ShareItemQuery,
      ShareItemQueryVariables
    >({
      query: shareItemQuery,
      fetchPolicy: 'network-only',
      variables: { itemId },
    })

    state.item = res.data.item
    state.loading = false
  }

  return {
    ...toRefs(state),
    fetchShareItem,
  }
}

export default useFetchShareItem

graphql-codegenで自動生成したtypeは以下のように指定すればOKです

import { ShareItemQuery, ShareItemQueryVariables } from '~/queries/types'


...

    const res = await ctx.root.$apollo.query<
      ShareItemQuery,
      ShareItemQueryVariables
    >({
      query: shareItemQuery,
      fetchPolicy: 'network-only',
      variables: { itemId },
    })

これでGraphQLの型を自動生成しつつフロントエンドのコード作成が行えるようになりました。

上記を利用して、以下のwebページを作成しました。

https://app.peperomia.info/share/1601cad6-a3f9-4df3-8de9-7a08fea6f35f