@reiwa

gql.tadaでコード生成なしでGraphQLのクエリとクエリ結果を補完する

typescript
graphql
apollo

GraphQLのクエリとクエリ結果を補完に graphql.tada を使ってみました。とりあえず手元で動かしたい型はこちらのリポジトリを確認ください。

https://github.com/RyukyuInteractive/blog-2024-08-23-gql-tada

これはTypeScriptの型推論を用いてコード生成をせずにGraphQLのクエリ文字列とクエリ結果を補完するライブラリです。

このように graphql 関数の引数の文字列がクエリとして補完されます。

img

このライブラリはASTNodeを出力するのでApolloやGraphQLのライブラリを用いて結果も補完できます。

img

ちなみにPokeAPIのGraphQLを使用しておりレスポンスはこのようになります。

https://pokeapi.co/docs/graphqlhttps://pokeapi.co/docs/graphql

img

準備

基本的にこの説明に従って設定します。

https://gql-tada.0no.co/get-started/installationhttps://gql-tada.0no.co/get-started/installation

$ bun i gql.tada

このように schema にURLを書いても機能します。

{
  "compilerOptions": {
    "strict": true,
    "plugins": [
      {
        "name": "gql.tada/ts-plugin",
        "schema": "https://beta.pokeapi.co/graphql/v1beta",
        "tadaOutputLocation": "graphql-env.d.ts"
      }
    ]
  }
}

ここで graphql-env.d.ts というスキーマの型を生成しておく必要があります。これはスキーマが変わるたびに生成が必要です。

$ bunx gql.tada generate-schema

VSCodeの場合はこのように設定します。

https://gql-tada.0no.co/get-started/installation#vscode-setuphttps://gql-tada.0no.co/get-started/installation#vscode-setup

{
  "typescript.tsdk": "node_modules/typescript/lib",
  "typescript.enablePromptUseWorkspaceTsdk": true
}

Fetchと組み合わせる

Fetchを用いた例はこちらです。

import { type ResultOf, graphql } from "gql.tada"
import { print } from "graphql"

const query = graphql(
  `query Query {
    pokemon_v2_pokemon(limit: 16) {
      id
      name
      weight
    }
  }`,
)

const resp = await fetch("https://beta.pokeapi.co/graphql/v1beta", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ query: print(query) }),
})

const json = await resp.json()

const result = json.data as ResultOf<typeof query>

console.table(result.pokemon_v2_pokemon)

gql.tadaのgraphql関数はASTNodeを返すので、それをgraphqlのprint関数を用いて文字列に変換します。

JSON.stringify({ query: print(query) })

BunやWorkersなど環境に依存しますが、Fetchのレスポンスの型がAnyである場合は ResultOf で型を定義します。

const result = json.data as ResultOf<typeof query>

Apolloと組み合わせる

Apolloの場合はASTNodeをそのまま使うことができます。

import { ApolloClient, InMemoryCache, createHttpLink } from "@apollo/client"
import { graphql } from "gql.tada"

const query = graphql(
  `query Query {
    pokemon_v2_pokemon(limit: 16) {
      id
      name
      weight
    }
  }`,
)

const client = new ApolloClient({
  cache: new InMemoryCache({}),
  link: createHttpLink({ uri: "https://beta.pokeapi.co/graphql/v1beta" }),
})

const result = await client.query({ query: query })

console.table(result.data.pokemon_v2_pokemon)

クエリ結果も補完されます。

const result = await client.query({ query: query })

useQuery

ApolloのuseQueryでもそのまま使えます。

import { useMutation, useSuspenseQuery } from "@apollo/client/index"

const LoaderQuery = graphql(
  `query Query {
    pokemon_v2_pokemon(limit: 16) {
      id
      name
      weight
    }
  }`,
)

const result = useSuspenseQuery(LoaderQuery, {})

既にライブラリがある場合

既にライブラリがある場合はTadaDocumentNodeを受け取りResultOfを返すような関数を作成します。

例えばShopifyのHydrogenのようなライブラリが考えられます。

const { storefront } = createStorefrontClient({
  cache,
  waitUntil,
  i18n: getLocaleFromRequest(request),
  publicStorefrontToken: env.PUBLIC_STOREFRONT_API_TOKEN,
  privateStorefrontToken: env.PRIVATE_STOREFRONT_API_TOKEN,
  storeDomain: env.PUBLIC_STORE_DOMAIN,
  storefrontId: env.PUBLIC_STOREFRONT_ID,
  storefrontHeaders: getStorefrontHeaders(request),
})

例えば、このような関数enhanceStorefrontClientを定義して引数と返り値を補完します。

import { I18nBase, Storefront } from "@shopify/hydrogen"
import { print } from "graphql"
import { ResultOf, TadaDocumentNode } from "gql.tada"

export type TadaClient = <Result, Variables>(
  node: TadaDocumentNode<Result, Variables, void>,
) => Promise<ResultOf<typeof node>>

export type EnhancedStorefront<TI18n extends I18nBase = I18nBase> =
  Storefront<TI18n> & {
    tada: TadaClient
  }

export function enhanceStorefrontClient<TI18n extends I18nBase = I18nBase>(
  client: Storefront<TI18n>,
): EnhancedStorefront<TI18n> {
  const tada: TadaClient = (node) => {
    return client.query(print(node))
  }

  return Object.assign(client, { tada: tada })
}

このような感じで使えます。

const handleRequest = createRequestHandler({
  getLoadContext() {
    return {
      storefront: enhanceStorefrontClient(storefront),
    }
  }
})

型定義も忘れないように。

export interface AppLoadContext {
  storefront: EnhancedStorefront<I18nLocale>
}

その他のタグ

関連する記事