React + Firebase + Hasuraで体験する快適なGraphQL生活
元々graphql-rubyとReactで生きていたんですが、どうやらHasuraなるものが良いらしいと知り、掲題のスタックで社内書籍管理サービスをつくってみました。
楽しかったのでちょっと書いてみます。
具体的には認証つきHasuraからusersを取得できるところまで。
Apolloは使わず、FetchでPOST投げちゃいます。
また、コード自体は本番運用に耐えうるものでは全くないので、
概念を理解する・触りを体験して全体像をつかむくらいの用途にとどめてください。
なお、Firebaseを利用する場合はCloudFunctionsを利用するので従量課金のBlazeプランにする必要があります。(といっても個人で遊ぶ分には無料枠で事足りる)
認証にAuth0を利用するのであればそこは考慮しなくてOKです。でもあれ真面目に使うとなると高いですよね(?)
TL;DR
- HasuraはRDB版Firestoreみたいな感覚
- 元々PostgreSQLのみ対応だったんだけど最近MySQLも対応したっぽい?
- Firebaseとか活用すると手軽に認証付きGraphQLサーバが手に入る
- めっちゃ便利なGraphiQL駆動でHasuraの命名規則に則ったクエリつくれるのが結構なDX
Hasura: https://hasura.io/
Hasuraとは
立ち位置的にはActiveRecord/graphql-rubyがやっていることを代替するような気持ちで利用していました。
GraphQLでリクエストを受ける → SQLを発行 → データ取得 → レスポンス返す
これを勝手にやってくれます。すごい。
Hasuraの規則に則ることで、よしなにSQLを発行してくれる形です。
例えばユーザーを追加するmutationであればこんな形。
mutation CreateUser($email: String = "") {
insert_users_one(object: {email: $email}) {
id
email
}
}
insert_テーブル名_なんやら という命名規則に則ってリクエストを送ると、Hasuraがうまいことやってくれます。
この命名規則も暗記する必要はなく、GraphiQLのサポート機能としてHasuraが提供してくれているので、ポチポチクリックするだけでクエリが構築できます。すごい楽。(GraphiQLの機能なのかHasuraの機能なのかは知らないです 教えてください)
Schemaももちろん落とせるので、codegenとかばっちりできます。最高。
HasuraとFirebaseのアカウント作る
まずはアカウント類を作ります。
Hasura
ここからアカウントを作成します。Try Hasuraから、Free Tierで進めます。
お好みの方法でアカウント作成を完了してください。
完了すると、ダッシュボードが表示されるはずです。
続けてプロジェクトを作成します。画面上部の new project
から進めます。
(アカウント作成の手順にプロジェクト作成が含まれていた気がします…が、下記の手順と同様の内容で進められると思います。)
最初の画面ではどのDBを使うか選べます。 Try with Heroku
でいきましょう。Herokuログインが求められたりするので、確認しつつ進めます。
完了するとプロジェクト詳細の画面に移ります。とりあえずやることはないので一旦放置。
Firebase
認証に利用するので、Firebaseのアカウントも作成します。
ガイドに従い、よしなに進めてください。というかこの記事読んでる方はすでにアカウント持ってそうな気もする。続けてプロジェクトも適当に作りましょう。
できたらAuthenticationでGoogle認証をonにしておきます。
OK!ではReact触っていきましょうか。
ReactからHasuraにリクエストを投げる
プロジェクトつくる
create-react-appでプロジェクトを作ります。
$ npx create-react-app --typescript hasura-demo
一応立ち上がることを確認。
$ yarn start
Hasuraでテーブル作成とクエリお試し
したらHasura側でテーブル作成と適当なデータ作成をします。DATAタブから、Add Table
しましょう。
テーブル定義はこんなところでOK。id
とemail
をtext
で入れておきます。
ページの最下部にAdd Table
ボタンがあるので、クリックして完了。ついでにテストデータも入れておきましょう。
Insert Row
タブから適当に登録しておきます。なお、ユーザー登録はあとでFirebaseのFunctionsに任せるようにします。
さて、ここまででステップ1完了です。実際にデータを取り出してみましょう。
GRAPHIQL
タブから、graphiql左のメニューをポチポチするとクエリが作れます。メニューはDBだかschemaだかを見て勝手にいい感じにしておいてくれます。すごい。
Reactからfetchする
これをReactから雑に取得してみます。
App.tsx
をこれにしてください。エンドポイントはHasuraのコンソールに書いてありますので置き換えましょう。
src/App.tsx
import React from 'react';
function App() {
const queryStr = "query MyQuery { users { id email } }"
const query = { query: queryStr }
const fetchUsers = () => {
fetch('https://<yours>.hasura.app/v1/graphql', {
method: 'POST',
body: JSON.stringify(query)
}).then(response => {
response.json().then(result => {
console.log(result.data)
})
})
}
return (
<div>
<button onClick={fetchUsers}>
fetch
</button>
</div>
);
}
export default App;
ボタンが一つだけある質素な画面になります。ボタンを押すとコンソールに結果が出ます。
良いですね。が、現状ではエンドポイントを知っていれば誰でも取り放題なので、これから認証設定をしていきます。
認証設定
Hasura側 環境変数設定
ADMIN_SECRETの設定
Hasuraは、プロジェクトの環境変数に適切な値を設定することで認証を動作させることができます。プロジェクト一覧から、プロジェクトの歯車ボタンをクリック。URLはこちらです。 https://cloud.hasura.io/projects
- メニューから
Env vars
を選択 New Env Var
- フォームに
admin
を入力してADMIN_SECRET
を選択 - 自身で硬い値を設定
しましょう。
この値をAuthorizationヘッダに含めると、admin権限でHasuraを利用できます。サーバからHasuraにアクセスする際に利用するものですかね。今回はFirebaseのFunctionsからHasuraにリクエストを投げる際に利用します。Reactからは使いません。
JWT_SECRETの設定
次に、Firebaseの方も環境変数に設定します。
このページでFirebaseのプロジェクトIDを入れるといい感じの設定を生成してくれます。
生成されたjsonは JWT_SECRET
に設定します。
これで認証設定はだいたいOKです。ヘッダのx-hasura-admin-secret
に正しいadmin_secretが設定されていればadmin権限でのリクエスト、AuthorizationヘッダにJWTが設定されていればそれによる認証を行うようになりました。この状態で先ほどのfetchボタンを押すと怒られるはずです。僕のコンソールのnetworkではこんな感じになります。
errors [ {…} ]
0 Object { message: "Missing Authorization header in JWT authentication mode", extensions: {…} }
extensions Object { path: "$", code: "invalid-headers" }
path "$"
code "invalid-headers"
message "Missing Authorization header in JWT authentication mode"
次はリクエスト時のヘッダ設定をしていきましょう。
Firebase側
Custom Claim設定
Functionsを利用して、ユーザー作成時にカスタムクレームを設定します。
Hasuraに「リクエストを行ったユーザーの権限」を伝えるための作業です。
なにはともあれFirebase initです。FirebaseのCLIが入ってない場合は入れておきましょう。https://firebase.google.com/docs/cli?hl=ja
また、FirebaseのプロジェクトはBlazeプラン にしておいてください。
$ firebase init
Functionsだけチェックしておけば大丈夫です。プロジェクトはさっき選んだやつにします。
あとはお好みでどうぞ。今回は特にいじる機会もそうないのでjsにしちゃいました。
必要なライブラリを入れておきます。といってもaxiosくらいです。Apollo使ってもいいですがやりたいことが非常に軽いので。
$ cd functions
$ yarn add axios
functions/index.js
はこれにします。axiosのurlとadmin_secretは自分のものに変更 しておいてください。
このあたりはこれらの記事を参考にさせていただいてます。
const functions = require('firebase-functions');
const admin = require('firebase-admin');
const axios = require('axios');
admin.initializeApp(functions.config().firebase);
const createUser = `
mutation createUser($id: String = "", $email: String = "") {
insert_users_one(object: {id: $id, email: $email}, on_conflict: {constraint: users_pkey, update_columns: []}) {
id
email
}
}
`
exports.processSignUp = functions.auth.user().onCreate(user => {
let customClaims;
customClaims = {
'https://hasura.io/jwt/claims': {
'x-hasura-default-role': 'user',
'x-hasura-allowed-roles': ['user'],
'x-hasura-user-id': user.uid
}
}
return admin.auth().setCustomUserClaims(user.uid, customClaims)
.then(() => {
let queryStr = {
"query": createUser,
"variables": {id: user.uid, email: user.email}
}
axios({
method: 'post',
url: '<url>',
data: queryStr,
headers: {
'x-hasura-admin-secret': "<secret>"
}
})
admin
.firestore()
.collection("user_meta")
.doc(user.uid)
.create({
refreshTime: admin.firestore.FieldValue.serverTimestamp()
});
})
.catch(error => {
console.log(error);
});
});
できたら$ firebase deploy --only functions
しましょう。
この際、jsの書き方やFirebaseのプランで怒られることが多いです。
ログイン
Firebase Authenticationを使ってログインします。特にあえて書くこともないので、このあたりを参考に実装しましょう。
また、ここで Firestoreを使うことになるのでコンソールから有効にしておきます。リージョンはお好みで。
ログインできるようになったら、IDトークン=JWTを取得してそれをヘッダに突っ込んじゃいましょう。
諸々設定し、ログイン/ログアウト・Hasura認証が通るようになったコードがこちらです。(まだusersは取得できません)
import React, { useState } from 'react';
import firebase from './firebaseConfig'
function App() {
const [idToken, setIdToken] = useState<string>('')
const queryStr = "query MyQuery { users { id email } }"
const query = { query: queryStr }
const login = () => {
const provider = new firebase.auth.GoogleAuthProvider()
firebase.auth().signInWithPopup(provider)
}
const logout = () => {
firebase.auth().signOut()
}
firebase.auth().onAuthStateChanged(user => {
if(user) {
user.getIdToken().then(token => {
setIdToken(token)
console.log(token)
})
}
})
const fetchUsers = () => {
fetch('<url>', {
method: 'POST',
headers: { Authorization: `Bearer ${idToken}` },
body: JSON.stringify(query)
}).then(response => {
response.json().then(result => {
console.dir(result.data)
})
})
}
return (
<div>
<button onClick={login}>
login
</button>
<button onClick={logout}>
logout
</button>
<button onClick={fetchUsers} disabled={!idToken.length}>
fetch
</button>
</div>
);
}
export default App;
firebaseConfig.ts
import * as firebase from "firebase/app"
import 'firebase/auth'
firebase.initializeApp(<config>)
export default firebase
一点注意なのですが、アカウント作成直後はidTokenにHasura用のカスタムクレームが入ってないことがあります。FirebaseのAuth側での反映に多少ラグがあるためです。
コンソールに表示されるidTokenを下記のページでdecodeし、こんな感じの内容が含まれているか確認してください。https://jwt.io/
"https://hasura.io/jwt/claims": {
"x-hasura-default-role": "user",
"x-hasura-allowed-roles": [
"user"
],
"x-hasura-user-id": "firebaseが付与するuid"
},
なければログアウトとログインを試してください。いずれこれが入ってくるはずです。
ここまで確認出来たら最後の仕上げです。
権限設定
さきほどのJWTでは、userのroleに"user"が入っていたり、uidが入っていたりします。Hasuraではこれをもとにアクセス権限の制御を行います。
例として、ユーザーが自分自身のレコードしか取得できないようにします。
- Hasuraのコンソールで、
DATA
タブからuserテーブルを選択 -
Permissions
タブを選択 - userロールを追加
-
select
のセルをクリック -
Row select permissions
をクリック - 画像の通りにセレクト
-
Column select permissions
をクリック - それぞれチェックを入れる
こんな感じにしたら完了です。保存しましょう。
これにより、ユーザーが自身のレコードしか取得できなくなります。
実際にReactからfetchボタンを押してみると、最初に手動で登録したレコードを含まない、自身の情報のみの配列が取得できるはずです。
なにかうまくいかない場合は、
- ヘッダがきちんと登録されているか
- カスタムクレームを含んでいるか
- Firebase上のuidとDBのidが一致しているか
等確認してみてください。(経験談)
この権限設定はかなり柔軟で、社内書籍管理サービスのusersはこんな設定にしていたりします。
自身と、自身が所属する企業のユーザーをselectできるようになっています。
以上
というわけで、React / Hasura / Firebaseのさわりでした。ちまちまと単純なCRUD書かなくて済むのが非常に楽です。権限管理もポチポチやるだけで済むのも良いですね。(初回いい感じにカスタムクレームを扱うあたり全然理解していないので教えてもらえると…)
次のステップとしてはmutationも同様に作ってみたり、schema落としてcodegenしたり、Apollo使って真面目に作ったりするとよきかと思います。
Discussion
とても参考になりました、ありがとうございます。
こちらについてですが、
の
getIdToken()
をgetIdToken(true)
にしたところ正常に動くようになりました!記事は未完成ですが参考にさせて頂きました。
ありがとうございます。
今からFirebaseの段階に入ろうかというところです。