supabase + GraphQL + Next.js + React18 で SSR 対応 Todo を作る
Vercelでの動作確認
https://supabase-test01.vercel.app/カスタム版supabase-cli
https://github.com/SoraKumo001/supabase-cli/releases
1.Supabase の GraphQL 対応
Firebase の代替として候補に挙がることの多い https://supabase.com/ が 2022/03/29 に GraphQL に対応しました。新規でプロジェクトを作るとエンドポイント/graphql/v1 から GraphQL の機能を利用可能となっています。
2.supabase-cli でローカル環境を整える
ということで早速使っていきたいと思います。まずはsupbase-cliを使って、ローカルに開発環境を作っていきましょう。
2.1 なんだと、動かん
supbase-cli をインストールし supabase start 実行後、拡張機能 pg_graphql を有効にしても、/graphql/v1のエンドポイントが正常に機能していません。issues を見ても、特にこれと言って何も見つかりませんでした。仕方が無いのでソースコードを fork して根本的な原因を調査しました。
2.2 色々バグってる
supabase/postgres のバージョンが低くて graphql.resolve が存在しない
kong のエンドポイントの設定で余計なスラッシュがついており、目的の場所へルーティングされない
graphql_public スキーマが存在しておらず、GraphQL の機能を利用するために必要な graphql_public.graphql ファンクションが無い
ぶっちゃけ動くわけが無いので、足りない機能を supbase-cli に追加しました
修正内容
https://github.com/SoraKumo001/supabase-cli/pull/1/files勝手に修正版リリース
https://github.com/SoraKumo001/supabase-cli/releases
2022/04/15時点で公式版で動くようになりました。
最終的にGraphQLのエンドポイントだけは修正されなかったので、こちらでプルリクを出して直してもらいました。
2.3 supabase-cli によるローカル環境の起動
2.3.1 supabase の起動
起動は以下のコマンドになります。
1supabase init2supabase start
以下の内容が表示され起動します
1 API URL: http://localhost:543212 DB URL: postgresql://postgres:postgres@localhost:54322/postgres3 Studio URL: http://localhost:543234 Inbucket URL: http://localhost:543245 anon key: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24ifQ.625_WdcF3KHqz5amU0x2X5WWHP-OEs_4qj0ssLNHzTs6service_role key: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6InNlcnZpY2Vfcm9sZSJ9.vI9obAHOGyVVKa3pD--kJlyxp-Z2zV9UUMAhKpNLAcU
ローカルではキーが固定値になっています。cli のソースコードに埋め込まれています。
2.3.2 GraphQL の動作確認
動作確認は ApolloStudio を使うと簡単です
https://studio.apollographql.com/sandbox/explorer
左上の設定をクリックして
1Endpoint http://localhost:54321/graphql/v12Default headers3 apiKey eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24ifQ.625_WdcF3KHqz5amU0x2X5WWHP-OEs_4qj0ssLNHzTs
を入力してください。これで特にエラーが出なければ接続成功です。左メニューのスキーマアイコンをクリックすると、デフォルトのデータ型などが確認出来ます。
2.3.3 Supabase Studio の動作確認
Studio URLに表示されているとおり http://localhost:54323 へアクセスします。
3.Todo アプリの作成
3.1 supabase の操作
3.1.1 テーブルの作成
Supabase Studio のテーブルエディターを開きます。テーブルエディタを使わず SQL で直接作ることも可能です。直接書く場合の注意点として、GraphQで使うにはプライマリキーが必須という部分です。
以下のようにカラムを追加してテーブルを作成します
3.1.2 Postgre 上に GraphQL スキーマの作成
SQL エディタで以下のクエリを実行します
1select graphql.rebuild_schema();
supabase-cli の改造版では start 時に実行するようにしています。
3.1.3 マイグレーションファイルの作成
1supabase db commit create_todo
以上のコマンドで supabase/migrations にマイグレーションファイルが作成されます
3.2 Next.js でフロント部分の作成
ようやくフロント開発です。
3.2.1 必要なパッケージのインストール
1yarn add @apollo/client graphql next react react-dom sass2yarn add -D @graphql-codegen/cli @graphql-codegen/introspection @graphql-codegen/typescript @graphql-codegen/typescript-operations @graphql-codegen/typescript-react-apollo @types/react @types/react-dom typescript
以上が今回の開発で必要なものです。
3.2.2 コードジェネレータの準備
GraphQL のスキーマーを TypeScript 用のコードに変換するための設定です
codegen.json
1{2 "schema": {3 "http://localhost:54321/graphql/v1": {4 "headers": {5 "apiKey": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24ifQ.625_WdcF3KHqz5amU0x2X5WWHP-OEs_4qj0ssLNHzTs"6 }7 }8 },9 "overwrite": true,10 "documents": "src/**/*.graphql",11 "generates": {12 "src/generated/graphql.tsx": {13 "plugins": [14 "typescript",15 "typescript-operations",16 "typescript-react-apollo"17 ]18 }19 }20}
3.2.3 GraphQL 操作用の定義を作成
以下のファイルを作成し変換をかけます
src/graphql/todo.graphql
1fragment todo on Todo {2 __typename3 id4 title5 description6 created_at7}8mutation insertTodo($value: TodoInsertInput!) {9 insertIntoTodoCollection(objects: [$value]) {10 records {11 ...todo12 }13 }14}15query queryTodo {16 todoCollection {17 edges {18 node {19 ...todo20 }21 }22 }23}24mutation deleteTodo($id: BigInt) {25 deleteFromTodoCollection(filter: { id: { eq: $id } }) {26 records {27 ...todo28 }29 }30}
変換
1graphql-codegen --config codegen.json
これで GraphQL の操作に必要なファイルは完成です
3.2.4 Next.js のコードを記述
.env.local
1NEXT_PUBLIC_SUPABASE_URL=http://localhost:54321/graphql/v12NEXT_PUBLIC_SUPABASE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24ifQ.625_WdcF3KHqz5amU0x2X5WWHP-OEs_4qj0ssLNHzTs
フロントからアクセスするための環境変数です
pages/_app.tsx
1import {2 ApolloClient,3 ApolloProvider,4 InMemoryCache,5 NormalizedCacheObject,6} from "@apollo/client";7import { getMarkupFromTree } from "@apollo/client/react/ssr";8import { AppContext, AppProps } from "next/app";9import React, { useMemo } from "react";10const { renderToReadableStream } = require("react-dom/server.browser");11const URI_ENDPOINT = process.env.NEXT_PUBLIC_SUPABASE_URL;12const ApiKey = process.env.NEXT_PUBLIC_SUPABASE_KEY;1314const App = (15 props: AppProps & {16 cache?: NormalizedCacheObject;17 memoryCache?: InMemoryCache;18 }19) => {20 const { Component, cache, memoryCache } = props;21 const client = useMemo(22 () =>23 new ApolloClient({24 uri: URI_ENDPOINT,25 cache: memoryCache || new InMemoryCache().restore(cache || {}),26 headers: { apiKey: ApiKey! },27 }),28 []29 );30 return (31 <ApolloProvider client={client}>32 <Component />33 </ApolloProvider>34 );35};3637App.getInitialProps = async ({ Component, router }: AppContext) => {38 if (typeof window !== "undefined") return {};39 const memoryCache = new InMemoryCache();40 await getMarkupFromTree({41 tree: (42 <App43 Component={Component}44 pageProps={undefined}45 router={router}46 cache={{}}47 memoryCache={memoryCache}48 />49 ),50 renderFunction: renderToReadableStream,51 }).catch(() => {});52 return { cache: memoryCache.extract() };53};54export default App;
Next.js で SSR を行うための設定です。SSR がいらない場合は getInitialProps を消すと CSR になります。apollo-client で普通に SSR をすると React18 がご乱心遊ばされるので、少し細工をしています。
src/pages/index.tsx
1import { TodoContainer } from "../components/TodoContainer";23const Page = () => {4 return <TodoContainer />;5};6export default Page;
src/components/TodoContener/TodoContainer.tsx
1import { useMemo } from "react";2import { TodoList } from "../TodoList";3import {4 useDeleteTodoMutation,5 useInsertTodoMutation,6 useQueryTodoQuery,7} from "../../generated/graphql";8import styled from "./index.module.scss";910export const TodoContainer = () => {11 const { data, refetch } = useQueryTodoQuery();12 const [insertTodo] = useInsertTodoMutation();13 const [deleteTodo] = useDeleteTodoMutation();14 const todoList = useMemo(() => {15 return data?.todoCollection?.edges16 .map((v) => v.node!)17 .sort((a, b) => (a.created_at > b.created_at ? -1 : 1));18 }, [data]);1920 const handleSubmit: React.FormEventHandler<HTMLFormElement> = (e) => {21 const form = e.target as HTMLFormElement & {22 title: HTMLInputElement;23 description: HTMLFormElement;24 };25 const title = form.title.value;26 const description = form.description.value;27 insertTodo({28 variables: { value: { title, description } },29 update: () => {30 (e.target as HTMLFormElement).reset();31 refetch();32 },33 });34 e.preventDefault();35 };36 const handleDelete = (id: number) => {37 deleteTodo({38 variables: { id },39 update: () => {40 refetch();41 },42 });43 };44 return (45 <div className={styled.root}>46 <form onSubmit={handleSubmit}>47 <button>Insert</button>48 <div>49 <input className={styled.title} id="title" placeholder="Title" />50 </div>51 <textarea52 className={styled.description}53 id="description"54 placeholder="Description"55 />56 </form>57 <TodoList todoList={todoList} onDelete={handleDelete} />58 </div>59 );60};
useQueryTodoQuery などはジェネレータで自動生成されているので、簡単にプログラムを組むことが出来ます。
4.まとめ
根本的な問題があるとすると、cli から GraphQL の機能がまともに使えないということです。それさえ解消すれば GraphQL の利点を生かして Firebase よりも遙かに高い開発効率が得られます。特に TypeScript では型の恩恵が大きいので、その効果は絶大です。
今回は supabase で GraphQL を使うことが主題なので、セキュリティや認証などは全く考慮していません。その辺りをきっちり実装していくといろいろな辛みは出てきそうですが、それもプログラミングの醍醐味です。