GraphQL Schema Language 中の description がコメントから block string に変わった
graphql-tools の makeExecutableSchema
のコードを読んでいて commentDescription
なる項目が気になり、さらに
utilities/buildASTSchema.js
を見ていて
export type BuildSchemaOptions = { ...GraphQLSchemaValidationOptions, /** * Descriptions are defined as preceding string literals, however an older * experimental version of the SDL supported preceding comments as * descriptions. Set to true to enable this deprecated behavior. * * Default: false */ commentDescriptions?: boolean, };
と、description の書き方が変わったらしいことに気がつきました。
https://github.com/graphql/graphql-js/issues/1245 によると graphql@0.12.0 から GraphQL Schema Language 中の description の書き方がコメントから block string というものになっていたらしいです。
簡単に GraphQLSchema
を printSchema
して確認してみました。
import { GraphQLSchema, GraphQLObjectType, GraphQLString, printSchema } from 'graphql' const schema = new GraphQLSchema({ query: new GraphQLObjectType({ name: 'rootQuery', description: 'This is Root Query', fields: { hello: { type: GraphQLString, description: 'Greeting', }, }, }), })
というスキーマを用意して
console.log(printSchema(schema))
とすると
schema { query: rootQuery } """This is Root Query""" type rootQuery { """Greeting""" hello: String }
という """
で囲まれた block string による description が得られます。
今まで通りのコメントによる description を得るには commentDescription
という後方互換性のためのオプションが用意されているので
console.log(printSchema(schema, { commentDescriptions: true }))
とすると
schema { query: rootQuery } # This is Root Query type rootQuery { # Greeting hello: String }
と今までどおりの形式で得られます。
graphql-tools の makeExecutableSchema で Directive を定義して簡易認可を実装してみる
Schema directives | GraphQL Tools によると
graphql-tools の makeExecutableSchema
で Directive を実装できるようなので
簡易的な認可ロジックを実装してみます。
あくまで Directive のハンズオンなので認可ロジックはとても簡単なものです。
認証情報は context
経由で処理の部分に渡すことにします。
「タイトルは自由に取得できるけど本文は許された人にしか閲覧できないドキュメント」を取得するためのクエリを定義していきます。
import { graphql } from 'graphql' import { makeExecutableSchema } from 'graphql-tools' const typeDefs = ` directive @auth(permitted: [String!]) on FIELD type Document { title: String! content: String! @auth(permitted: ["asa-taka"]) } type Query { document: Document! } ` // Directive の処理を定義する const directiveResolvers = { // @auth Directive の処理を定義 auth(next, src, args, ctx, info) { return next().then(res => { // コンテクスの認証ユーザが `permitted` に含まれていたら値を素通し if (args.permitted.includes(ctx.auth.name)) return res // 認証エラーを返してあげる (*˘꒳˘*) やさしい // info の中身は複雑なので適当に調べながら... const path = info.path throw new Error(`User not permitted: ${path.prev.key}.${path.key}`) }) }, } const schema = makeExecutableSchema({ typeDefs, directiveResolvers }) const query = `{ document { title content } }` // データソースの準備 const rootValue = { document: { title: 'My Document', content: 'Awesome contents...' }, } // コンテクスト経由で認証情報を渡してやる const context = { auth: { name: 'asa-taka' } } graphql(schema, query, rootValue, context).then(console.log, console.error)
これを実行すると
{ data: { document: { title: 'My Document', content: 'Awesome contents...' } } }
が得られます。ここから認証情報を
const context = { auth: { name: 'some-other-user' } }
と変更すると
{ errors: [ { Error: User not permitted: document.content
とエラーが返されます。ここから更に認証フィールドである content
をクエリから除外して
const query = `{ document { title } }`
とすると
{ data: { document: { title: 'My Document' } } }
エラーなくデータが得られます。
ディレクティブの定義
基本的な使い方は Schema directives | GraphQL Tools に書いてある通りです。
まず GraphQLSchema
内で利用する Directive を定義します。
GraphQL Schema Language では以下のように Custom Directive を定義できます
directive @auth(permitted: [String!]) on FIELD
次に定義した Directive についての処理の実装を定義します。
graphql-tools の makeExecutableSchema
では directiveResolver
プロパティでディレクティブの処理を定義できます。
const directiveResolvers = { auth(next, src, args, ctx, info) { return next().then(res => { if (args.permitted.includes(ctx.auth.name)) return res const path = info.path throw new Error(`User not permitted: ${path.prev.key}.${path.key}`) }) }, } const schema = makeExecutableSchema({ typeDefs, directiveResolvers })
GraphQL の Directive も Apollo のおかげで意外と簡単に実装できますね。
これまで
...と、Directive 実装に苦労してきた経緯があるのですが、makeExecutableSchema
はどう実装されているのか気になりますね。
GraphQL Schema Language で Directive を定義する
(=˘ ꒳ ˘=) GraphQL Schema Language 内で Directive を定義する方法を探していたのですが、公式にドキュメントが見つからなかったのでメモしておきます...
directive @myDirective(age: Int) on FIELD
のように Directive を定義できるようです。
import { graphql, buildSchema } from 'graphql' const schema = buildSchema(` directive @myDirective(age: Int) on FIELD type Query { hello: String! } `) const query = `{ hello @myDirective(age: 12) }` const rootValue = { hello: 'world' } graphql(schema, query, rootValue).then(console.log, console.error)
のように実行できます。
見つけた背景
GraphQLDirective
を使って定義した後にそれを printSchema
するとどうなるか興味本位で試していて見つけました。
import { graphql, GraphQLSchema, GraphQLDirective, GraphQLObjectType, GraphQLString, printSchema, } from 'graphql' const myDirective = new GraphQLDirective({ name: 'myDirective', locations: ['FIELD'], args: { age: { type: GraphQLString }, }, }) const schema = new GraphQLSchema({ query: new GraphQLObjectType({ name: 'rootQuery', fields: { hello: { type: GraphQLString }, }, }), directives: [myDirective], }) console.log(printSchema(schema))
これを実行すると
schema { query: rootQuery } directive @myDirective(age: String) on FIELD type rootQuery { hello: String }
と表示されます。Directives はてっきり GraphQL Schema Language の対象外かと思っていたのですが定義できたのですね。
私が見つけられていないだけで、公式の記述はどこかにあるのでしょうか。
そして定義ができることと処理を実装できることはまた別なのですよね... Directive の処理をいい感じに定義する方法はあるのでしょうか...
GraphQL の Custom Directive について
(=˘ ꒳ ˘=) GraphQL のラストフロンティア Custom Directive にもそろそろ手を出してみたい...
ここまで趣味で GraphQL のいろいろな要素を見てきました。
実行時のパラメータとして GraphQLSchema、queryString、rootValue、context、variables、 スキーマを構成する要素として Query、Mutation、Subscription、Custom Scalar と見てきて最後に残った Custom Directive について見ていきたいと思います。 色々できそうで楽しそうですよね。
しかしこの Custom Directive、意外と魔窟なのでした...
- GraphQL の Directive について
- Directive の処理はどこにあるのか
- GraphQLDirective で定義してみる
- フィールドに付加したディレクティブの情報はどこで取得できるのか
- graphql-custom-directive
- 追記
GraphQL の Directive について
とりあえず定義から眺めていきます
- GraphQL.org
- Working Draft – October 2016
と、さてここから型情報を頼りに実装をしてみようかと思ったのですが、意外と難航して...
Directive の処理はどこにあるのか
てっきり GraphQLObjectType
のフィールド定義の resolve
のようなものがあるのかと思っていたのですが見つかりません。
GraphQLDirective
のコンストラクタ引数を見てみると以下のようになっています。
export interface GraphQLDirectiveConfig { name: string; description?: string; locations: DirectiveLocationEnum[]; args?: GraphQLFieldConfigArgumentMap; astNode?: DirectiveDefinitionNode; }
他の箇所も探してみたのですが、フィルタ処理や変換処理の実装を記述する箇所が見つかりません。
GraphQL.js の @skip
ディレクティブの定義を見てみると
export const GraphQLSkipDirective = new GraphQLDirective({ name: 'skip', description: 'Directs the executor to skip this field or fragment when the `if` ' + 'argument is true.', locations: [ DirectiveLocation.FIELD, DirectiveLocation.FRAGMENT_SPREAD, DirectiveLocation.INLINE_FRAGMENT, ], args: { if: { type: GraphQLNonNull(GraphQLBoolean), description: 'Skipped when true.', }, }, });
https://github.com/graphql/graphql-js/blob/master/src/type/directives.js#L111-L127
と、どこにも処理の内容が書かれていません。ならばと思い execution/execution.js のコードを覗いてみると shouldIncludeNode として フィールドをフィルタリングする処理が定義されていました。
つまり Directive の処理はスキーマではなく、それを解釈する実行関数側に寄せられて実装されているのですね。
これを素直に受け取れば Custom Directives を実装したければ、実行関数である graphql
のカスタム実装を自前で書け、という事になります。
それはちょっと常人にはつらい道ですね。
GraphQLDirective で定義してみる
とはいえ一度自分で行けるところまで実装してみようと GraphQLDirective
を使ってコードをこねくり回してみました。
import { graphql, GraphQLSchema, GraphQLDirective, GraphQLObjectType, GraphQLString, } from 'graphql' // @myDirective というディレクティブを定義する const myDirective = new GraphQLDirective({ name: 'myDirective', // どこで使うディレクティブかを指定 // FIELD、FRAGMENT_DEFINITION、FRAGMENT_SPREAD...など色々指定できる locations: ['FIELD'], // ディレクティブの引数を定義 args: { name: { type: GraphQLString, }, }, }) const schema = new GraphQLSchema({ query: new GraphQLObjectType({ name: 'rootQuery', fields: { hello: { type: GraphQLString, }, }, }), directives: [myDirective], // スキーマに @myDirective を組み込む }) // クエリ内で @myDirective を使ってもエラーにならない const query = `{ hello @myDirective(name: "asa-taka") }` const rootValue = { hello: 'world', } graphql(schema, query, rootValue).then(console.log, console.error)
これで一応 @myDirective
という Custom Directive がクエリ内で利用できるようになりました。
正確には「クエリ内で利用してもエラーが出ない」ようにはなりました。
しかし肝心の処理がまだ定義できていません。
値の変換やフィルタ処理など、いろいろ定義したいですよね。
フィールドに付加したディレクティブの情報はどこで取得できるのか
resolve
を定義して console.log
で探し回った結論だけ書いておきますと、第4引数 info
の fieldNodes
の directives
でフィールドに付加したディレクティブの情報にアクセスできるようです。
hello: { type: GraphQLString, resolve(src, args, ctx, info) { console.log(info.fieldNodes[0].directives[0]) } }
しかしこれ、ここまでやってディレクティブの name
と args
しか取得できず、ここから更にそのディレクティブについての処理を書かなければなりません。
このディレクティブが存在すれば resolve
の処理をこう変える、という処理ですね。
更にこれだと特定のフィールドに対してのみの実装ででしかない、ということも忘れてはなりません。
ディレクティブは複数フィールドで使いたい場合がほとんどだと思いますが、その分の実装を複数フィールドに行うのはとても現実的ではありませんね。
graphql-custom-directive
graphql
の再実装なんてできるわけなく、フィールド毎の resolve
のディレクティブ実装も現実的ではない、ということで
graphql-custom-directive
という便利ライブラリがあります。
使い方はREADMEに書いてある通りで、パブリックなAPIは以下のふたつです。
GraphQLCustomDirective
resolve
が追加されたGraphQLDirective
を定義できる- オリジナルには存在しない
GraphQLDirective.resolve
を Directive の処理実装置き場としてしれっと追加している
applySchemaCustomDirectives
GraphQLSchema
にDirective
の処理を追加するGraphQLSchema
には予めdirectives
にGraphQLCustomDirectives
を列挙しておく必要がある
内部処理の大枠としては以下の通りです。
wrapFieldsWithMiddleware
GraphQLSchema
の各フィールドにresolveMiddlewareWrapper
を適用する
resolveMiddlewareWrapper
- フィールドの
resolve
に Directive のresolve
を絡めるラッパー info
からディレクティブの情報を読み取っている
- フィールドの
resolveWithDirective(resolve, source, directive, context, info)
- 通常のフィールドの
resolve
に対して、第1引数に Directive のresolve
が拡張されたもの
- 通常のフィールドの
コードを見ればわかるように _queryType
へのアクセスなど GraphQL.js のプライベートインタフェースにかなり依存した実装となっており、バージョンアップに対してかなり無防備な実装になっています。とは言え現状 GraphQLSchema
クラスの仕様上、一旦作成したスキーマを変更する手段が他にないため仕方がないともいえますね。
つまりここまで拡張しなければ Custom Directives は定義できないということで、ひとまず納得しました。
追記
そしてこの graphql-custom-directive を使ってみたところうまく動きませんでした。実は graphql-custom-directives の READMEに書いてあったところから、この s がつかない graphql-custom-directive を見つけたのですが、graphql-custom-directives の方でも同様の applySchemaCustomDirectives
の実装があり、更に内部でアクセスしている GraphQLSchema
のプライベートインタフェースが異なるという、どうも GraphQL.js の内部実装の変更に追従できていない様子でした。
GraphQL の Directive はあまり一利用者が実装するような段階のものではないのかもしれません...
GraphQL の色々なスキーマ表現について - GraphQL Schema Language や schema.json
(=˘ ꒳ ˘=) GraphQL のスキーマ表現いろいろ多すぎ...
で Introspection について眺めたら GraphQL のスキーマ表現について整理できてきたのでまとめてみます。
GraphQL のスキーマ表現
GraphQL Schema Language
type Query { hello: String! }
のようなDSLのことを指します。GraphQL SDL(Schema Definition Language) とも呼ばれています。
人が一番読みやすい形式といったらこれですね。
GraphQL.js の buildSchema
に渡されたり graphql-tools の makeExecutableSchema
に typeDefs
として渡されます。
GraphQL.js の GraphQLSchema
import { GraphQLSchema, GraphQLObjectType } from 'graphql' const schema = new GraphQLSchema({ query: new GraphQLObjectType({ name: 'rootQuery', fields: { hello: { type: GraphQLString, }, }, }),
のような GraphQLSchema
インスタンスを直接コンストラクトする表現。
resolve
定義など何でもできますが、手作業では書きたくないですね。
ある程度 GraphQL に慣れていてもそう思いますし、そうでないならなおさらだと思います。
Introspection Result
apollo-codegen のようなツールが schema.json のように出力するファイルの中身がこれにあたります。 実態はリソースとなるAPIに対する Introspection の結果が書き出されたものです。 この Introspection に用いるクエリはintrospectionQuery として定義されています。 定義内容はとても長いので手作業では書きたくないですね。
簡単なコードでこのクエリを実行してみます。
import { graphql, buildSchema, introspectionQuery } from 'graphql' const schema = buildSchema(` type Query { hello: String! } `) graphql(schema, introspectionQuery).then(console.log)
これを実行すると schema.json
の中身としてよく見かける
{ data: { __schema: { queryType: [Object], mutationType: null, subscriptionType: null, types: [Array], directives: [Array] } } }
という値が得られます。
AST Schema
GraphQL Schema Language をパースしたAST(抽象構文木)です。
ライブラリ利用者としてはあまり利用する機会はありませんが、何気なく console.log
した結果によく出てくる気がします。
import { parse, buildASTSchema } from 'graphql' const typeDefs = ` type Query { hello: String! } ` const ast = parse(typeDefs)
を実行すると
{ kind: 'Document', definitions: [ { kind: 'ObjectTypeDefinition', description: undefined, name: [Object], interfaces: [], directives: [], fields: [Array], loc: [Object] } ], loc: { start: 0, end: 33 } }
という AST Schema が得られます。
スキーマ表現間の変換
これらのスキーマ表現は graphql と graphql-tools の各種ユーティリティである程度の変換が可能となっています。
- graphql
buildClientSchema
: Introspection Result →GraphQLSchema
buildSchema
: GraphQL Schema Language →GraphQLSchema
printSchema
:GraphQLSchema
→ GraphQL Schema Languageparse
: GraphQL Schema Language → AST SchemabuildASTSchema
: AST Schema →GraphQLSchema
- graphql-tools
makeExecutableSchema
: GraphQL Schema Language + resolve/subscribe 実装 →GraphQLSchema
- この
GraphQLSchema
にはresolve/subscribe
が含まれる
- この
なので resolve
など実行時用の定義を除けば、これらの表現は相互変換可能、ということになります。
早見表
output \ source | Schema Language | Introspection Result | GraphQLSchema Instance | AST |
---|---|---|---|---|
Schema Language | printSchema |
|||
Introspection Result | (*) | |||
GraphQLSchema Instance | buildSchema , makeExecutableSchema |
buildClientSchema |
buildASTSchema |
|
AST | parse |
(*) graphql(schema, introspectionQuery)
で取得可能
こうしてみると GraphQLSchema
インスタンスへの変換が一番充実していることがわかりますね。直接変換できなくても、例えば
printSchema(buildClientSchema(introspectionResult))
のようにすると Introspection Result から GraphQL Schema Language が得られるように、複数組み合わせれば大体の場合は相互変換ができそうですね。
GraphQL.js で Custom Scalars を定義する - まずは serialize から
(=˘ ꒳ ˘=) GraphQL.js の TypeScript の定義を眺めながらなんとなくな雰囲気で Custom Scalar を定義してみる
...GraphQL.js の GraphQLScalarType
を使います。コンストラクタの引数は以下のようになっています。
export interface GraphQLScalarTypeConfig<TInternal, TExternal> { name: string; description?: string; astNode?: ScalarTypeDefinitionNode; serialize(value: any): TExternal | null | undefined; parseValue?(value: any): TInternal | null | undefined; parseLiteral?(valueNode: ValueNode): TInternal | null | undefined; }
とりあえず name
と serialize
が最低限必要みたいですね。
シリアライズで何もしない
現在時刻の Unix 秒をソースにして graphql を実行する処理を書いてみましょう。まずはシリアライズで何もしないようにします。
import { graphql, GraphQLScalarType, GraphQLObjectType, GraphQLSchema, } from 'graphql' const GraphQLDate = new GraphQLScalarType({ name: 'Date', serialize(value) { // 何もしない (*˘꒳˘*) ただ返すだけ... return value }, }) const schema = new GraphQLSchema({ query: new GraphQLObjectType({ name: 'rootQuery', fields: { now: { type: GraphQLDate, }, }, }), }) const query = `{ now }` const rootValue = { // 現在時刻をUnix秒で返す now: () => Date.now(), } graphql(schema, query, rootValue).then(console.log, console.error)
これを実行すると
{ data: { now: 1518864848888 } }
と、Unix 秒がそのまま取得されるのがわかります。
シリアライズで変換処理
次にシリアライズを Unix 秒からISOの時刻文字列に変換する処理に変更してみます。
const GraphQLDate = new GraphQLScalarType({ name: 'Date', serialize(value) { // もう何もしないぼくじゃない (*˘꒳˘*) ただ返すだけとか無能 return new Date(value).toISOString() }, })
とすると
{ data: { now: '2018-02-17T10:56:22.882Z' } }
ISOの時刻文字列に変換された値が取得できます。
という感じで GraphQL.js で Custom Scalars を使う方法がなんとなくわかりましたね。
GraphQL で時刻を扱う
GraphQL で時刻を扱う場合、現在メジャーなパッケージは graphql-iso-date になるでしょうか。
参考までに覗いてみると型定義は
src/dateTime/index.js
に存在します。基本的な定義は大体読めるようになりましたが、今回触れなかった parseLiteral
や parseValue
といった定義が serialize
の他にも行われていますね。
また調べていきたいところです。
余談
graphql
を実行している以下の部分
const query = `{ now }` const rootValue = { now: () => Date.now(), } graphql(schema, query, rootValue).then(console.log, console.error)
サンプルコードとしてわかりやすく書きたかったので今回は丁寧に手前で変数を定義して実行していますが、もうすこし簡単に書けてしまいます。
rootValue
の定義をよく見て見ると now
で Date.now
を呼んでいるだけなので Date
をそのまま使っても同様のインタフェースを提供できますね。なので
graphql(schema, `{ now }`, Date).then(console.log, console.error)
のようにワンライナーで実行できてしまいます。
GraphQL の Introspection について - schema.json って何だろう
(=˘ ꒳ ˘=) GraphQL を使っているとよく schema.json などと名付けられた JSON 形式のファイルを利用している例に突き当たる...
この schema.json ってなんだろうというお話。
schema.json - Apollo の場合
例えば apollo-codegen の使い方を見てみると
// schema.json 生成 apollo-codegen introspect-schema http://localhost:8080/graphql --output schema.json // schema.json から型定義を生成 apollo-codegen generate **/*.graphql --schema schema.json --output API.swift
のように introspection で得られる値が schema.json ということらしい。
GraphQL の Introspection
Introspection について Working Draft を見ると
__schema: __Schema! __type(name: String!): __Type
で root query から得られるものとある。__Schema
定義は以下の通り。
type __Schema { types: [__Type!]! queryType: __Type! mutationType: __Type directives: [__Directive!]! }
__Type
の中身は GraphQL.js の TypeScript 等の型情報を眺めたことのある人なら「なるほど」と読めるような内容で定義されている。
Standard Introspection Query
apollo-codegen は introspection に
import { introspectionQuery } from 'graphql/utilities'
を利用している。このutilities/introspectionQuery.js には __schema
で得られる introspection 情報を完全に得るためのフィールド定義が延々と書かれている。初めて見た時は目を疑った。
Mocking | GraphQL Tools ではこのクエリは Standard Introspection Query と呼ばれている。しかし Working Draft には見つからなかった。
試しに graphql
で introspectionQuery
を実行してみる。
import { graphql, buildSchema, introspectionQuery } from 'graphql' const schema = buildSchema(` type Query { hello: String! } `) graphql(schema, introspectionQuery).then(console.log)
とすると schema.json でよく見る形式のデータが帰ってくる。
{ data: { __schema: { queryType: [Object], mutationType: null, subscriptionType: null, types: [Array], directives: [Array] } } }
これをさらに buildClientSchema で GraphQLSchema
オブジェクトに戻すことができる。
graphql(schema, introspectionQuery).then(res => { const clientSchema = buildClientSchema(res.data) console.log(clientSchema) })
ドキュメントによると
Build a GraphQLSchema for use by client tools. Given the result of a client running the introspection query, creates and returns a GraphQLSchema instance which can be then used with all GraphQL.js tools, but cannot be used to execute a query, as introspection does not represent the "resolver", "parse" or "serialize" functions or any other server-internal mechanisms.
つまり introspection query の結果を渡すことでクライアントやツール類が利用するための GraphQLSchema
オブジェクトを構成するユーティリティメソッドらしい。Introspection に resolver
などの関数の情報が含まれていないのでこのインスタンスはサーバ側で値を返すために利用することはできない。
GraphQL スキーマの表現形式の比較
ここまで GraphQL 周辺を触ってきて、いろいろなスキーマの表現形式を見かけてきた。
GraphQL Schema Language の DSL を利用したスキーマ表現。JSON形式のスキーマ表現。これは introspection result とも dump と呼ばれることも多い気がする。それから GraphQLSchema
オブジェクトで表される JavaScript コード中の表現。折角なので比較してみよう。
GraphQL Schema Language (.graphql
)
description
やカスタムディレクティブ定義など一部の表現をサポートしていない- 人が読み書きしやすい
GraphQL.js の GraphQLSchema
Introspection Result (.json
)
- Introspection の結果をそのまま JSON として吐き出したもの
- 雰囲気としてはGraphQLSchema から JSON として表現できない処理を除いたあらゆる情報が含まれている形式
- 情報としては完全なものに近いが、しかし人が読み書きするものではない
どれも一長一短だが、ワークフロー中のスキーマ取得手法として「APIサーバからの型情報取得」という手を採用するのなら現状 GraphQL Schema Language では表現しきれない部分も多いため Introspection Result を型情報のソースとしておいておくのは妥当に思える。
Swagger や同様、スキーマというものはそれ単体では機能しない。必ず「ワークフロー」と呼ばれるスキーマ共有・更新の共有を行うための仕組みが必要となる。GraphQL の場合は Introspection の仕組みがそれに該当するが、Custom Scalar をサーバ・フロントでどう共有するかなど、まだ掴みきれていない部分も多い。ライブラリとしてリポジトリ管理し両サイドで共有するのがいいか、それとも他に何か良い方法があるのか...