かもメモ

自分の落ちた落とし穴に何度も落ちる人のメモ帳

Hasura を GraphQL サーバーに使って code generator で TypeScript の型生成をするまでのメモ

今参加しているプロジェクトでは React (TypeScript) をフロントエンドに Hasura を GraphQL サーバーにした構成になっています。
Hasura を GraphQL サーバーにして code generator で TypeScript の型生成を行ったメモをまとめたエントリーです

経緯のメモ

構成

  • GraphQL サーバー
  • Frontend
    • React 18.2.0
    • typescript 5.0.4
    • Vite 4.3.9
    • @tanstack/react-query 4.29.3
    • graphql-request 6.0.0
    • graphql 16.6.0

Frontend の vite で構築した React から react-query + graphql-request で Hasura にアクセスする構成にしました

1. GraphQL codegen の下準備

GraphQL 関連のパッケージをインストール

$ npm i graphql
$ npm i -D @graphql-codegen/cli

codegen config の作成

npx graphql-codegen init で設定ファイルが作成できる

$ npx graphql-codegen init
? What type of application are you building?: Application built with React
? Where is your schema?: http://localhost:4000/graphql
? Where are your operations and fragments?: src/**/*.graphql
? Where to write the output: src/gql/
? Do you want to generate an introspection file?: Yes
? How to name the config file?: codegen.ts
? What script in package.json should run the codegen?: codegen
etching latest versions of selected plugins...
    Config file generated at codegen.ts
      $ npm install
    To install the plugins.
      $ npm run codegen
    To run GraphQL Code Generator.
# 必要なパッケージをインストール
$ npm install

設定ファイル codegen.ts が作成され codegen を実行する npm script が追加される

📝 Where to write the output? は最後が / で終わるパスを指定しておかないと codegen 時にエラーになる
=> ✖ [client-preset] target output should be a directory, ex: "src/gql/"

codegen.ts

import type { CodegenConfig } from '@graphql-codegen/cli';

const config: CodegenConfig = {
  overwrite: true,
  schema: "http://localhost:4000/graphql",
  documents: "src/**/*.graphql",
  generates: {
    "src/gql/": {
      preset: "client",
      plugins: []
    },
    "./graphql.schema.json": {
      plugins: ["introspection"]
    }
  }
};
export default config;

2. Hasura にアクセスして codegen できるようにする

codegen.ts の schema に Hasura の GraphQL のエンドポイントの URL を指定し、全てにアクセスできるよう headers に { "x-hasura-admin-secret": ADMIN_SECRET } を渡せば良い

cf. Admin Access | Hasura GraphQL Docs

codegen.ts

const config: CodegenConfig = {
  overwrite: true,
- schema: "http://localhost:4000/graphql",
+ schema: [
+   {
+     'https://hasura.XXXXX.dev/v1/graphql': {
+       headers: {
+         'x-hasura-admin-secret': 'YOUR_ADMIN_SECRET'
+       }
+     }
+   }
+ ], 
  documents: "src/**/*.graphql",
  // …
}

Hasura のエンドポイント と secret key を環境変数にする

secret key が直接設定ファイルに書かれているのは良くないので .env 経由で読み込ませるように変更する

.env

HASURA_GRAPHQL_URL=https://hasura.XXXXX.dev/v1/graphql
HASURA_GRAPHQL_ADMIN_SECRET=your_secret

📝 VITE_ 接頭語を使うと React から環境変数にアクセス可能になるが、build されたファイルに直接値が書き込まれフロントエンドから丸見えの状態になってしまうので注意

codegen.ts

const config: CodegenConfig = {
  overwrite: true,
  schema: [
    {
-     'https://hasura.XXXXX.dev/v1/graphql': {
+     [process.env.HASURA_GRAPHQL_URL ?? "http://localhost:4000/v1/graphql"]: {
        headers: {
-         'x-hasura-admin-secret': 'admin_secret'
+         'x-hasura-admin-secret': process.env.HASURA_GRAPHQL_ADMIN_SECRET ?? '',
        },
      },
    },
  ],
  documents: 'src/**/*.graphql',
  // …
}

環境変数 .env を使って codegen できるように npm script を修正する

codegen コマンドに --require dotenv/config を付けることで .env を読み込めるようになる
📝 vite + react 環境なら dotenv が含まれているので別途 npm install dotenv をする必要はない

package.json

"scripts": {
- "codegen": "graphql-codegen --config codegen.ts"
+ "codegen": "graphql-codegen --require dotenv/config --config codegen.ts"
},

cf. require field – GraphQL Code Generator

3. client-preset を使って React-Query + Graphql-Request で使える型を生成する

以前は Apollo や React-Query といったクライアントごとにプラグインを使ってクライアントに合わせた型を生成していたが、graphql-codegen v3 以降では client-preset (preset: "client") を使うことでクライアントを問わず利用できる型を出力できるようになった

@graphql-codegen/cli v3 以降では npx graphql-codegen init で出力される設定がデフォルトで client-preset を使うものになっているので特に変更すべき点はない

codegen.ts

import type { CodegenConfig } from '@graphql-codegen/cli';

const config: CodegenConfig = {
  overwrite: true,
  schema: [
    {
      [process.env.HASURA_GRAPHQL_URL ?? "http://localhost:4000/v1/graphql"]: {
        headers: {
          'X-Hasura-Admin-Secret':
            process.env.HASURA_GRAPHQL_ADMIN_SECRET ?? '',
        },
      },
    },
  ],
  documents: "src/**/*.graphql",
  generates: {
    // client-preset を使った型を `src/gql/` に出力する
    "src/gql/": {
      preset: "client",
      plugins: []
    },
    "./graphql.schema.json": {
      plugins: ["introspection"]
    }
  }
};
export default config;

適当な .graphql ファイルを作成して codegen で型生成をする

何かしら src/**/*.graphql が存在しなければ graphql.schema.json も生成できないので適当な query ファイルを作成する
今回は Hasura を使っているのでコンソールから適当なクエリを作ってファイルにコピペすればOK

./src/gql/queries/getUser.graphql

query GetUser($email: String!) {
  users(email: $mail) {
    id,
    email,
    displayName
  }
}

GraphQL codgen

$ npm run codegen
> graphql-codegen@0.0.0 codegen
> graphql-codegen --require dotenv/config --config codegen.ts

✔ Parse Configuration
✔ Generate outputs

graphql.schema.jsonsrc/gql/ ディレクトリ内に型ファイルが生成される

|- graphql.schema.json
|- /src/gql/
    |- fragment-masking.ts
    |- graphql.ts
    |- gql.ts
    |- index.ts

graphql.ts にフロントで使う TypeScript の型が書かれている

4. React-Query + GraphQL-Request を使って Hasura に GraphQL リクエストを送信する

$ npm i @tanstack/react-query graphql-request

4-1. react-query の プロバイダーを設定

<QueryClientProvider> で全体を囲う

/src/App.tsx

import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
const queryClient = new QueryClient();

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <h1>My App</h1>
      <MyComponent />
    </QueryClientProvider>
  );
}
export { App };

4-2. GraphQLClient を作成

Hasura にフロントエンドからアクセスするには JWT token を header に含める必要があるので、トークンをセットしたクライアントを返す関数を作成する

cf. Authentication Using JWTs | Hasura GraphQL Docs

/src/graphqlClient.ts

import { GraphQLClient } from 'graphql-request';
const endpoint = import.meta.env.VITE_HASURA_URL ?? '';

export const buildGraphQLClient = (token?: string) => {
  const headers = token ? { authorization: `Bearer ${token} } : undefined;
  const client = new GraphQLClient(endpoint, {
    headers,
  });

  return client;
};

4-3. React-Query + GraphQL Client (graphql-request) を使って Hasura にリクエストする Hooks を作成する

'/src/hooks/useGetUser.ts'

import { useQuery } from '@tanstack/react-query';
// codegen で生成された {QueryName}Document を使用する
import { GetUserDocument, GetUserQueryVariables } from '../gql/graphql';
import { getGraphQLClient } from '../graphqlClient';
// token を取得する hook があるものとする
import { useAuth } from './useAuth';

export const useGetUser = ({ email }: GetUserQueryVariables ) => {
  const { token } = useAuth();
  return useQuery(
    ['graphl', 'get', 'user', email],
    ({ queryKey }) => {
      const client = getGraphQLClient(token);
      return client.request(GetUserDocument, { email: queryKey[3] ?? '' });
    }
  );
};

4-4. アプリから User 情報の取得を行う

先ほど作成した useGetUser を使って Hasura に GraphQL リクエストを送ってユーザー情報を取得する

/src/MyComponent.ts

import { useGetUser } from './hooks/useGetUser';

function MyComponent() {
  const const { data, isLoading, error } = useGetUser(email);
  if ( isLoading ) { return <div>Loading…</div> }
  if { error } { throw error; return null; }
  return (
    <div>User: { data ? <UserInfo {…data} /> : <span>No User</span> }</div>
  );
}

これで TypeScript の型補完の恩恵を受けながら Hasura に GraphQL リクエストを送れるようになりました!
mutation だったり React-Query のオプションなどは長くなるのでまた別のエントリーにしようと思います。

おわり ₍ ᐢ. ̫ .ᐢ ₎


[参考]