GraphQL.js の buildSchema でカスタムScalar型を使う

GraphQL.jsbuildSchema でカスタム Scalar 型を利用してみました。

サンプルコードは こちら

はじめに

GraphQL.js では Scalar 型を GraphQLScalarTypeインスタンスとして実装するようになっており、ID (GraphQLID) や Int (GraphQLInt) 等の基本的な型が用意されています。(src/type/scalars.ts 参照)

用意されていない型は自前で定義する事ができ、例えば以下のような日付型のカスタム Scalar(名前は Date とする)は下記のように実装できます。

  • GraphQL 上は文字列として表現し、内部的に JavaScript の Date 型を使用
実装例 - カスタム Scalar の Date 型
const GraphQLDate = new GraphQLScalarType<Date, string>({
    name: 'Date',
    // 内部データを GraphQL 上の表現へ変換
    serialize: (outputValue) => {
        if (outputValue instanceof Date) {
            return outputValue.toISOString()
        }
        throw new GraphQLError('non date')
    },
    // GraphQL 上の値を内部データへ変換
    parseValue: (inputValue) => {    
        if (typeof inputValue === 'string') {
            const d = new Date(inputValue)

            if (isNaN(d.getTime())) {
                throw new GraphQLError('invalid date')
            }

            return d
        }
        throw new GraphQLError('non string value')
    },
    // GraphQL 上の表現を内部データへ変換
    parseLiteral: (valueNode) => {
        if (valueNode.kind === Kind.STRING) {
            const d = new Date(valueNode.value)

            if (isNaN(d.getTime())) {
                throw new GraphQLError('invalid date')
            }

            return d
        }
        throw new GraphQLError('non string value')
    }
})

buildSchema 利用時

GraphQL 上はカスタム Scalar 型を以下のように定義できます。

scalar 型名

これを buildSchema で処理すると、GraphQLScalarType のデフォルト実装が適用されてしまい、任意の処理へ差し替えたりする事はできないようでした。

そのため、現時点では buildSchema が構築した GraphQLScalarType を後から上書きするような対応が必要となりそうです。

サンプル作成

SampleDate というカスタム Scalar 型を定義し、それを用いた処理を Deno 用の TypeScript で実装してみました。

buildSchema の結果から SampleDate の型情報を取得して、下記の処理を差し替えています。

  • serialize(内部データを GraphQL 上の表現へ変換)
  • parseValue(GraphQL 上の値を内部データへ変換)
  • parseLiteral(GraphQL 上の表現を内部データへ変換)

なお、下記で parseLiteral へ variables 引数を付けているのは console.log して内容を確認するためです。

sample.ts
import { 
    graphql, buildSchema, GraphQLError, ValueNode, Kind 
} from 'https://cdn.skypack.dev/graphql?dts'

const schema = buildSchema(`
    scalar SampleDate

    type Query {
        now: SampleDate!
        nextDay(date: SampleDate!): SampleDate!
    }
`)

const toDate = (v: string) => {
    const d = new Date(v)

    if (isNaN(d.getTime())) {
        throw new GraphQLError('invalid date')
    }

    return d
}

type MaybeObjMap = { [key: string]: unknown } | null | undefined
// buildSchema が構築した SampleDate の処理内容を差し替え
Object.assign(schema.getTypeMap().SampleDate, {
    serialize: (outputValue: unknown) => {
        console.log(`*** called serialize: ${outputValue}`)

        if (outputValue instanceof Date) {
            return outputValue.toISOString()
        }
        throw new GraphQLError('non Date')
    },
    parseValue: (inputValue: unknown) => {
        console.log(`*** called parseValue: ${inputValue}`)
        
        if (typeof inputValue === 'string') {
            return toDate(inputValue)
        }
        throw new GraphQLError('non string value')
    },
    parseLiteral: (valueNode: ValueNode, variables?: MaybeObjMap) => {
        console.log(`*** called parseLiteral: ${JSON.stringify(valueNode)}, ${JSON.stringify(variables)}`)

        if (valueNode.kind === Kind.STRING) {
            return toDate(valueNode.value)
        }
        throw new GraphQLError('non string value')
    }
})

type DateInput = { date: Date }

const rootValue = {
    now: () => new Date(),
    nextDay: ({ date }: DateInput) => new Date(date.getTime() + 24 * 60 * 60 * 1000)
}

const r1 = await graphql({ schema, rootValue, source: '{ now }' })
console.log(r1)

console.log('-----')

const r2 = await graphql({
    schema, 
    rootValue, 
    source: '{ nextDay(date: "2022-10-21T13:00:00Z") }'
})
console.log(r2)

console.log('-----')

const r3 = await graphql({
    schema,
    rootValue, 
    source: `
        query ($d: SampleDate!) {
            nextDay(date: $d)
        }
    `,
    variableValues: { d: '2022-10-22T14:30:00Z' }
})
console.log(r3)

実行結果は下記のようになりました。

parseLiteral は variables 引数の値が undefined{} の場合の 2回呼び出されています。

また、変数(variableValues)を使った場合は parseValue が呼び出されています。

実行結果
% deno run sample.ts
*** called serialize: Fri Oct 21 2022 20:20:27 GMT+0900 (日本標準時)
{ data: { now: "2022-10-21T11:20:27.093Z" } }
-----
*** called parseLiteral: {"kind":"StringValue","value":"2022-10-21T13:00:00Z","block":false,"loc":{"start":16,"end":38}}, undefined
*** called parseLiteral: {"kind":"StringValue","value":"2022-10-21T13:00:00Z","block":false,"loc":{"start":16,"end":38}}, {}
*** called serialize: Sat Oct 22 2022 22:00:00 GMT+0900 (日本標準時)
{ data: { nextDay: "2022-10-22T13:00:00.000Z" } }
-----
*** called parseValue: 2022-10-22T14:30:00Z
*** called serialize: Sun Oct 23 2022 23:30:00 GMT+0900 (日本標準時)
{ data: { nextDay: "2022-10-23T14:30:00.000Z" } }