Deno で GraphQL

GraphQL を Deno で試してみました。

https://deno.land/x に Deno 用の GraphQL モジュールがいくつかありましたが(基本的には GraphQL.js のポーティング)、ここでは GraphQL.js を直接使う事にします。

今回のサンプルコードは http://github.com/fits/try_samples/tree/master/blog/20200817/

1. GraphQL.js の使用

GraphQL.js を Deno から使うには、以下のように SkypackPika CDN の後継)という CDN から import するのが簡単そうです。

import { ・・・ } from 'https://cdn.skypack.dev/graphql'

2. 単独実行

GraphQL.js を使って GraphQL の Query・Mutation・Subscription を一通り実行してみます。

まずは、buildSchema 関数に GraphQL の型定義を渡して GraphQLSchema を取得します。

これをクエリや GraphQL 処理の実装と共に graphql 関数や subscribe 関数へ渡す事で処理結果を得ます。

graphql 関数の方は第二引数に文字列を使えましたが(型は Source | string)、subscribe 関数の方は使えなかったので(型は DocumentNode)、こちらは parse した結果を渡すようにしています。

GraphQL 処理の実装(下記サンプルの root 変数の箇所)では、Query・Mutation・Subscription の処理に応じた関数を用意し、型定義に応じた値を返すように実装します。

Subscription の場合は、([Symbol.asyncIterator] プロパティで AsyncIterator を返す)AsyncIterable を戻り値としなければならないようなので、下記では MessageBox クラスとして実装しています。

sample1.js
import {
    graphql, buildSchema, subscribe, parse
} from 'https://cdn.skypack.dev/graphql'

import { v4 } from 'https://deno.land/std/uuid/mod.ts'

// GraphQL 型定義
const schema = buildSchema(`
    enum Category {
        Standard
        Extra
    }

    input CreateItem {
        category: Category!
        value: Int!
    }

    type Item {
        id: ID!
        category: Category!
        value: Int!
    }

    type Mutation {
        create(input: CreateItem!): Item
    }

    type Query {
        find(id: ID!): Item
    }

    type Subscription {
        created: Item
    }
`)

// Subscription 用の AsyncIterable
class MessageBox {
    #messages = []
    #resolves = []

    publish(value) {
        const resolve = this.#resolves.shift()

        if (resolve) {
            resolve({ value })
            // 以下でも可
            // resolve({ value, done: false })
        }
        else {
            this.#messages.push(value)
        }
    }

    [Symbol.asyncIterator]() {
        return {
            next: () => {
                console.log('** asyncIterator next')

                return new Promise(resolve => {
                    const value = this.#messages.shift()

                    if (value) {
                        resolve({ value })
                        // 以下でも可
                        // resolve({ value, done: false })
                    }
                    else {
                        this.#resolves.push(resolve)
                    }
                })
            }
        }
    }
}

const store = {}
const box = new MessageBox()

// GraphQL 処理の実装
const root = {
    create: ({ input: { category, value } }) => {
        console.log(`* call create: category = ${category}, value = ${value}`)

        const id = `item-${v4.generate()}`
        const item = { id, category, value }

        store[id] = item
        box.publish({ created: item })

        return item
    },
    find: ({ id }) => {
        console.log(`* call find: ${id}`)
        return store[id]
    },
    // Subscription の実装では戻り値を AsyncIterable とする
    created: () => box
}

const run = async () => {
    const m1 = `
        mutation {
            create(input: { category: Standard, value: 10 }) {
                id
            }
        }
    `
    // Mutation の実行1
    const mr1 = await graphql(schema, m1, root)
    console.log(mr1)

    const m2 = `
        mutation Create($p: CreateItem!) {
            create(input: $p) {
                id
            }
        }
    `

    const vars = {
        p: {
            category: 'Extra',
            value: 123
        }
    }
    // Mutation の実行2(変数利用)
    const mr2 = await graphql(schema, m2, root, null, vars)
    console.log(mr2)

    const q = `
        {
            find(id: "${mr2.data.create.id}") {
                id
                category
                value
            }
        }
    `
    // Query の実行
    const qr = await graphql(schema, q, root)
    console.log(qr)

    const s = parse(`
        subscription {
            created {
                id
                category
            }
        }
    `)
    // Subscription 処理
    const subsc = await subscribe(schema, s, root)

    for await (const r of subsc) {
        console.log('*** subscribe')
        console.log(r)
    }
}

run().catch(err => console.error(err))

実行結果は以下の通りです。

実行結果
> deno run sample1.js

・・・
* call create: category = Standard, value = 10
{ data: { create: { id: "item-11ca8326-832b-4e13-9b47-d2c70c1e95e9" } } }
* call create: category = Extra, value = 123
{ data: { create: { id: "item-29f1851b-4cbb-4f23-872f-11856f1c0bf7" } } }
* call find: item-29f1851b-4cbb-4f23-872f-11856f1c0bf7
{
  data: {
    find: { id: "item-29f1851b-4cbb-4f23-872f-11856f1c0bf7", category: "Extra", value: 123 }
  }
}
** asyncIterator next
*** subscribe
{
  data: {
    created: { id: "item-11ca8326-832b-4e13-9b47-d2c70c1e95e9", category: "Standard" }
  }
}
** asyncIterator next
*** subscribe
{
  data: {
    created: { id: "item-29f1851b-4cbb-4f23-872f-11856f1c0bf7", category: "Extra" }
  }
}
** asyncIterator next

3. Web サーバー化

Deno の http を使って、上記処理を Web サーバー化してみました。

sample2.js
import {
    graphql, buildSchema, subscribe, parse
} from 'https://cdn.skypack.dev/graphql'

import { v4 } from 'https://deno.land/std/uuid/mod.ts'
import { serve } from 'https://deno.land/std@0.65.0/http/server.ts'

const schema = buildSchema(`
    enum Category {
        Standard
        Extra
    }

    ・・・
`)

class MessageBox {
    ・・・
}

const store = {}
const box = new MessageBox()

const root = {
    create: ({ input: { category, value } }) => {
        const id = `item-${v4.generate()}`
        const item = { id, category, value }

        store[id] = item
        box.publish({ created: item })

        return item
    },
    find: ({ id }) => {
        return store[id]
    },
    created: () => box
}

// Web サーバー処理
const run = async () => {
    const server = serve({ port: 8080 })

    // リクエスト毎の処理
    for await (const req of server) {
        const buf = await Deno.readAll(req.body)
        const query = new TextDecoder().decode(buf)

        console.log(`* query: ${query}`)

        const res = await graphql(schema, query, root)

        req.respond({ body: JSON.stringify(res) })
    }
}

const runSubscribe = async () => {
    const s = parse(`
        subscription {
            created {
                id
                category
                value
            }
        }
    `)

    const subsc = await subscribe(schema, s, root)

    for await (const r of subsc) {
        console.log(`*** subscribe: ${JSON.stringify(r)}`)
    }
}

Promise.all([
    run(),
    runSubscribe()
]).catch(err => console.error(err))

実行

deno run で Web サーバーを起動します。 --allow-net でネットワーク処理を許可する必要があります。

Web サーバー起動
> deno run --allow-net sample2.js

create を実行した結果です。

Mutation の実行
$ curl -s http://localhost:8080 -d 'mutation { create(input: { category: Extra, value: 5 }) { id } }'

{"data":{"create":{"id":"item-7fc5e3bb-286f-48fe-b027-9ca34dcc6451"}}}

create で返された id を指定して find した結果です。

Query の実行1
$ curl -s http://localhost:8080 -d '{ find(id: "item-7fc5e3bb-286f-48fe-b027-9ca34dcc6451") { id category value } }'

{"data":{"find":{"id":"item-7fc5e3bb-286f-48fe-b027-9ca34dcc6451","category":"Extra","value":5}}}

存在しない id を指定して find した結果です。

Query の実行2
$ curl -s http://localhost:8080 -d '{ find(id: "item-invalid") { id category value } }'

{"data":{"find":null}}

サーバー側の出力結果は以下の通りです。

Web サーバー出力結果
> deno run --allow-net sample2.js

・・・
** asyncIterator next
* query: mutation { create(input: { category: Extra, value: 5 }) { id } }
*** subscribe: {"data":{"created":{"id":"item-7fc5e3bb-286f-48fe-b027-9ca34dcc6451","category":"Extra","value":5}}}
** asyncIterator next
* query: { find(id: "item-7fc5e3bb-286f-48fe-b027-9ca34dcc6451") { id category value } }
* query: { find(id: "item-invalid") { id category value } }