gql.tadaでコード生成なしでGraphQLのクエリとクエリ結果を補完する
GraphQLのクエリとクエリ結果を補完に graphql.tada を使ってみました。とりあえず手元で動かしたい型はこちらのリポジトリを確認ください。
https://github.com/RyukyuInteractive/blog-2024-08-23-gql-tada
これはTypeScriptの型推論を用いてコード生成をせずにGraphQLのクエリ文字列とクエリ結果を補完するライブラリです。
このように graphql
関数の引数の文字列がクエリとして補完されます。
このライブラリはASTNodeを出力するのでApolloやGraphQLのライブラリを用いて結果も補完できます。
ちなみにPokeAPIのGraphQLを使用しておりレスポンスはこのようになります。
https://pokeapi.co/docs/graphql
準備
基本的にこの説明に従って設定します。
https://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-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>
}