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 から使うには、以下のように Skypack(Pika 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 } }