車輪の二度漬け

railsを触っていましたが、最近はreactに興味あります。

Next.js + Vercel + TypeScript + Firebaseでアプリを作る。GraphQL事前準備編

0. 今回やること

前回の記事はこちら
今回は、GraphQLを使うための前準備をする。
やることとしては、Cloud Firestoreに入っているデータをGraphQLを使って返す、と言う感じ。

1. 事前準備

データの設定

まずはFirestoreの設定から。
と言っても、今回はまだデプロイしたりはしないので、どちらかと言うと開発用のデータの設定からやっていく。
今回使うデータは以下:

userProfiles: [
  {
    id: 1,
   bio: "hogehoge"
  },
  {
    id: 2,
   bio: "fugafuga"
  }
]

このデータを、Dockerコンテナの中で firebase emulators:start をして立ち上がるローカル用コンソール画面 http://localhost:4000Firestore の項目から入力。
その後、一度コンテナ内で

$ firebase emulators:export ./seed

をして、プロジェクトルートディレクトリ直下の /seedというディレクトリにこのデータをエクスポートしておく。
その後、package.jsonスクリプトにあるserve

"serve": "npm run build && firebase emulators:start --only functions"

から

"serve": "firebase emulators:start --import=../seed",

にしておく。
こうすることで、エミュレータ立ち上げ時に、エクスポートしたデータがインポートされる(npm run buildを無くした理由は後述)。
ただ、これだと手動でエクスポートする羽目になるので、前回の記事にも書いたこの方法をしたほうがいいかも。

<2020/11/26 追記>
Firebase Firestoreのエミュレーターを使ってローカルで開発する←こちらを読ませてもらったところ、--export-on-exitというすごく便利なoptionがあることがわかった。

/hogehoge # firebase emulators:start -h
Usage: firebase emulators:start [options]

start the local Firebase emulators

Options:

  ...中略...

  --import [dir]              import emulator data from a previous export (see emulators:export)
  --export-on-exit [dir]      automatically export emulator data (emulators:export) when the emulators make a clean exit (SIGINT), when no dir is provided the
                              location of --import [dir] is used
  -h, --help                  output usage information

とのことらしいので、このオプションもつけておけばよさそう。
感謝。
公式を見てもエミュレータの細かい使い方がいまいちわからなかったんだけど、-hやるのは大事だね。

Cloud Functionsで使うTypeScriptファイルの自動再コンパイル設定

やることとしては、

tsc --watch

を追加するだけなのだが、package.jsonスクリプト

"serve": "tsc --watch && firebase emulators:start --import=../seed"

という風にしても、tsc --watch と firebaseエミュレータの両方でターミナルを使うため、うまく行かない。
なので 、

"watch": "tsc --watch",
"serve": "firebase emulators:start --import=../seed",

という風にして、watchエミュレータを異なるターミナルで実行できるようにした。
ターミナルのタブを2つ使うのがめんどくさいけど、もっと良い方法があるのかな。

これで事前準備は終わり。

2. Apollo Serverの設定

次はApollo Serverの設定をしていく。
Apollo ServerはGraphQLを用いてのデータのやり取りをするためのサーバー。
Cloud Functions向けのライブラリも出てるので、これを使うことにした。
と言っても、今の段階では
Cloud Functions for Firebaseを使ってApollo Serverを構築する - 雑食日誌
Apollo ServerからFireStoreのデータを取得する - 雑食日誌
こちらの記事を参考にさせてもらった感じ。
まずは必要なライブラリのインストールから。

# npm i apollo-server-cloud-functions 

で、現時点でのfunctions/src/index.ts

const { ApolloServer, gql } = require('apollo-server-cloud-functions');

const typeDefs = gql`
  type UserProfile {
    id: Int!
    bio: String!
  }

  type Query {
    userProfiles: [UserProfile]!
  }
`;

const resolvers = {
  Query: {
    userProfiles: async () => {
      const snapshot = await db.collection('userProfiles').get()
      return snapshot.docs.map((doc) => doc.data())
    },
  },
}

const server = new ApolloServer({
  typeDefs,
  resolvers,
})

export const helloWorld = functions.https.onRequest(server.createHandler())

という感じ。
resolversは別ファイルに分けてもいいかも。
これでコンパイルが通ったら、http://localhost:5001/<エミュレータ立ち上げ時に出るpath>/helloWorldにPostmanなどを使ってPOST

query {
    userProfiles {
        id,
        bio
    }
}

のようなリクエストを投げて、設定したデータが返ってきたらOK。
この記事を書いてて気づいたけど、resolversは別ファイルにしたほうがよさそう。

3. GraphQLの設定

↑の例だと、gqlに渡すテンプレートリテラルの中でGraphQLのスキーマを設定しているので他のファイルに外出ししたい。
ちなみに、Apolloのサンプルコードも大体スキーマはテンプレートリテラルに直書きとなってることが多い。
ただ、これだとスキーマが増えたりした時に可読性が下がって萎えるので、今のうちからファイルを分けておくことにした。
色々ググってみたところ、GraphQL Toolsこの記事が役に立ちそうだったので、これでやってみる。

まずは必要なライブラリのインストールから。

# npm i graphql-tools

で、ディレクトリ構成はこんな感じにした:

.
└── /project root/
    └── /functions/
        ├── /src/
        │   └── index.ts // Cloud Functions用のファイル
        ├── /graphql/
        │   ├── /schemas/
        │   │   └── userProfile.gql
        │   └── /queries/
        │       └── userProfiles.gql
        └── /lib // index.tsのコンパイル後のファイルの格納先

各gqlファイルは以下:

# functions/graphql/schemas/userProfile.gql

type UserProfile {
  id: Int!
  bio: String!
}
# functions/graphql/queries/userProfiles.gql

type Query {
  userProfiles: [UserProfile]!
}

その後、gqlファイルを読み込むために functions/src/index.tsを以下のように変更:

# functions/src/index.ts

import * as functions from 'firebase-functions'
import * as admin from 'firebase-admin'
import { ApolloServer } from 'apollo-server-cloud-functions'
import path from 'path'
import { mergeTypeDefs, loadFilesSync } from 'graphql-tools'

const typesArray = loadFilesSync(path.resolve(__dirname, '../graphql'), {
  recursive: true,
})

const typeDefs = mergeTypeDefs(typesArray)

admin.initializeApp()

const db = admin.firestore()

const resolvers = {
  Query: {
    userProfiles: async () => {
      const snapshot = await db.collection('userProfiles').get()
      return snapshot.docs.map((doc) => doc.data())
    },
  },
}

const server = new ApolloServer({
  typeDefs,
  resolvers,
})

export const helloWorld = functions.https.onRequest(server.createHandler())

これでテンプレートリテラルスキーマを書かなくても良くなった。

4. VSCodeの設定

最後に、VSCodeスキーマの定義をしやすくするために、ファイルをまたいでの補完ができるようにしておく。
GraphQLのスキーマ定義に役立つVSCode拡張機能をざっと見た限り、 https://marketplace.visualstudio.com/items?itemName=GraphQL.vscode-graphql:title
https://marketplace.visualstudio.com/items?itemName=kumar-harsh.graphql-for-vscode:title
の2つが主だった拡張機能の様子。
ただ、設定方法を見ると、graphql-for-vscodevscode-graphqlに比べて、watchman@playlyfe/gqlと言ったライブラリの事前インストールが必要というところが面倒に感じたので、vscode-graphqlを使うことにした。
なので、まずVSCodeでこの拡張機能をインストール後、ルートディレクトリ直下に.graphqlrc.ymlという設定ファイルを作成し、設定手順にあるとおりに以下の内容を記述する:

# .graphqlrc.yml

schema: ./functions/graphql/**/*.gql
documents: null

ちなみに、この設定ファイルは、JSON形式やjsファイルでも書けるようになっている。
内部でGraphQL Configを使っているようで、GraphQL Configのドキュメントにより設定ファイルの書き方の詳細が載っている。
その後、functions/graphql/queries/userProfiles.gqlUserProfileと入力して補完されればOK...となるはずだが、現時点(v0.3.12)では補完が効かない。
色々調べてみると、こういうissueが上がっており、要はconfigファイルschemaの項目にglobを使うとダメらしい。
なので、今はこの書き方をしても補完はされない。
リポジトリのREADMEには

Note: The primary maintainer @acao is on hiatus until December 2020

とあるので、解決まではもう少し待つことになりそう。
一応回避策として、

scehma:
  - functions/graphql/queries/userProfiles.gql
  - functions/graphql/schemas/userProfile.gql

というように列挙すれば補完が効くようになるが、これはすごーくめんどくさいので待った方がよさそう。
それかgraphql-for-vscodeを使うか。

5. 所感

流石にサンプルコードレベルじゃ勉強の意味が薄いなと思ってファイル分割とか開発の利便性あたりまで踏み込んでみたけど、ここまで手こずるとは思わなかった。
特にVSCodeの拡張の謎の不具合は本当に罠だったー。
でもまあ最終的に原因が分かったからよかった。
また、今回はリクエストが来るたびに全てのスキーマファイルを読み込むようにしてるけど、ここにあるように、本番では事前にファイルに書き出したスキーマをつかうようにしてもいいのかもなあ、と思った。
まあ、この辺は追々やって行こう。

次はいよいよGraphQLの踏み込んだ使い方とFirestoreのデータ設計に入って行こうと思う。

6. その他参考にしたサイト等

GraphQL | A query language for your API
Documentation Home - Apollo Basics - Apollo GraphQL Docs
Welcome | GraphQL Tools
Firestore エミュレータのデータをローカル環境で import/export する - Qiita