きみはねこみたいなにゃんにゃんなまほう

ねこもスクリプトをかくなり

Go の struct と interface で Embedding

Effective Go の Embedding の内容を試してみます。

Go では embedding を利用して継承のようなことができますが、struct と interface の違いが今ひとつ理解できていなかったため、実際にコードを書いてコンパイラに怒られながら、どういう違いがあるのか試していきたいと思います。

確認方法

こんな感じで embedding を利用して BaseMethodSuperMethod を実装するパターンを色々見ていきます。

type baseInterface interface {
    BaseMethod() string
}

type superInterface interface {
    baseInterface
    SuperMethod() string
}

実装の確認用兼、interface を満たしているかの確認用の関数を用意します。

func printSuper(name string, v superInterface) {
    fmt.Println(name+".BaseMethod ->", v.BaseMethod())
    fmt.Println(name+".SuperMethod ->", v.SuperMethod())
}

interface を interface に埋め込む

type baseInterface interface {
    BaseMethod() string
}

// 一応 baseInterface の方の実装も作っておく
type implOfBaseInterface struct{}

func (implOfBaseInterface) BaseMethod() string {
    return "implOfBaseInterface.BaseMethod"
}

type superInterface interface {
    baseInterface
    SuperMethod() string
}

type implOfSuperInterface struct{}

// interface を interface に埋め込んだ場合 BaseMethod の定義は必要
func (i implOfSuperInterface) BaseMethod() string {
    return "implOfSuperInterface.BaseMethod"
}

func (implOfSuperInterface) SuperMethod() string {
    return "implOfSuperInterface.SuperMethod"
}

impleOfSuperInterface 側でも BaseMethod の実装が必要になります。 実装を参照する先も無いので当たり前と言えば当たり前ですね。

func main() {
    printSuper("implOfSuperInterface", implOfSuperInterface{})
}

してみると

implOfSuperInterface.BaseMethod -> implOfSuperInterface.BaseMethod
implOfSuperInterface.SuperMethod -> implOfSuperInterface.SuperMethod

これも impleOfSuperInterface に実装したものがそのまま呼ばれて、当たり前の結果ですね。 このパターンの埋め込みをするメリットとしては、interface 側のメソッドの定義を省略できることくらいでしょうか。

struct を struct に埋め込む

type baseStruct struct{}

func (b baseStruct) BaseMethod() string {
    return "baseStruct.BaseMethod"
}

type superStruct struct {
    baseStruct
}

// この場合 BaseMethod は省略できるが実装してもいい
// func (s superStruct) BaseMethod() string {
//     // 明示的に baseStruct.BaseMethod を呼ぶこともできるし(あまり意味なさそう)
//     return s.baseStruct.BaseMethod()
//     // 他の処理を定義してもいい
//     return "superStruct.BaseMethod"
// }

func (s superStruct) SuperMethod() string {
    return "superStruct.SuperMethod"
}

struct を struct に埋め込んだ場合は superStruct.BaseMethod の実装を省略できるみたいです。

func main() {
    printSuper("superStruct", superStruct{})
}

してみると

superStruct.BaseMethod -> baseStruct.BaseMethod
superStruct.SuperMethod -> superStruct.SuperMethod

superStruct.BaseMethodbaseStruct.BaseMethod が呼ばれていますね。

struct を interface に埋め込む(不可)

type structEmbeddedInterface interface {
    baseStruct
    SuperMethod()
}

type implOfStructEmbeddedInterface struct{}

func (implOfStructEmbeddedInterface) SuperMethod() {
    fmt.Println("structEmbeddedInterface.SuperMethod")
}

これは不可能なパターンで、コンパイル時に interface contains embedded non-interface baseStruct と怒られます。

interface を struct に埋め込む

type interfaceEmbeddedStruct struct {
    baseInterface
}

func (interfaceEmbeddedStruct) SuperMethod() string {
    return "interfaceEmbeddedStruct.SuperMethod"
}

この場合は interfaceEmbeddedStruct.BaseMethod の実装をしなくてもコンパイルは通ります。しかし、ランタイムで panic: runtime error: invalid memory address or nil pointer dereference というエラーになります。一番タチが悪いですね。

このパターンの活用事例は悩ましいですが、考察されているページがあったので時間のある時に読みたいと思います。 https://horizoon.jp/post/2019/03/16/go_embedded_interface/

今回触って改めて、このGo言語における embedding は手を動かさないと理解できないなと思いました。単純にパターンが多いので網羅しづらいのと、網羅されても今度は長大になるため読み解く時間がないので、手元で試した方が速いなと…

dataloader を使ってリクエストを取りまとめる React デモを作る

以前 GraphQL を利用していたときに、サーバサイドのSQLリクエストを減らすために使おうとしていたパッケージに dataloader というものがあります。

現在は GraphQL ではなく gRPC-Web を利用したアプリを書いているのですが、dataloader は別に GraphQL に限ったものではなく、id 単体を指定するような複数のリクエストを、複数の id を指定する単一のリクエストにまとめて実行するということをやってくれます*1。つまり dataloader

get****ById(id1) x n → get****ByIds(id1, id2, ...)

という処理を行ってくれる中間層として機能してくれるものになります。

デフォルトではレスポンスに含まれる id を利用して、リクエストの取りまとめや重複するキーへのリクエストの dedupe 処理を行ってくれます。 GraphQL のサーバサイドではスキーマの patching を行っていたのですが、GraphQL はレスポンスに id を含めるかどうかが叩く側次第なためこの辺りの処理ががうまくいかずに頓挫しました。

話を戻して、今回は GraphQL ではなく、一般化したリモートリクエストをWebから行うようなAPIクライアントを対象にして、dataloader の動作を以下のような単純なアプリを作って試していきたいと思います。

f:id:lightbulbcat:20190322010347p:plain

動作の詳しい仕組みは理解していませんが、とりあえず使ってみるところから始めます。

この記事で説明しているコードの、実際に動作するプロジェクトは以下の til リポジトリに置いてあります。

下準備

API クライアントのモックを定義する

今回デモで利用するAPIは以下のようなものです。

// api.ts

// Data Types
// ----------

export type IdType = number

export interface User {
  id: IdType
  name: string
  favoriteThingIds: IdType[]
}

export interface Thing {
  id: IdType
  name: string
}

// Data Mock
// ---------

const thingList: Thing[] = [
  { id: 1, name: "Raindrops" },
  { id: 2, name: "Kittens" },
  { id: 3, name: "Kettles" },
  { id: 4, name: "Mittens" },
  { id: 5, name: "Packages" }
]

const userList: User[] = [
  { id: 1, name: "Alice", favoriteThingIds: [1, 2, 3, 4, 5] },
  { id: 2, name: "Bob", favoriteThingIds: [2, 4] },
  { id: 3, name: "Carol", favoriteThingIds: [1, 3] }
]

// API Mock
// --------

const emulateNetworkDelay = <V>(value: V): Promise<V> => {
  return new Promise(resolve => setTimeout(() => resolve(value), 1000))
}

export default {
  getUserById(id: IdType) {
    console.log(`API called: getUserById(${id})`)
    const user = userList.find(u => u.id === id)
    return emulateNetworkDelay(user)
  },
  getUsersByIds(ids: IdType[]) {
    console.log(`API called: getUsersByIds(${ids})`)
    const users = ids.map(id => userList.find(u => u.id === id))
    return emulateNetworkDelay(users)
  },
  getThingById(id: IdType) {
    console.log(`API called: getThingById(${id})`)
    const thing = thingList.find(t => t.id === id)
    return emulateNetworkDelay(thing)
  },
  getThingsByIds(ids: IdType[]) {
    console.log(`API called: getThingsByIds(${ids})`)
    const things = ids.map(id => thingList.find(t => t.id === id))
    return emulateNetworkDelay(things)
  }
}

データ型としては UserThing への参照を id のみで持っており、Thing の内容を見たい場合は更にAPIを叩く必要がある、というAPI設計です。

ByIds のクエリにより id 複数指定でデータを取得することができるという想定です。dataloader の利用にはこういったAPIが存在することが前提となります。

しかしこの ByIds 系のクエリですが、実際に React アプリを書いた経験から言うと、意外と活かしづらいです。私が React を使う場合、コンポーネントがマウントされたタイミングでそのコンポーネントが必要とするリクエストを行う、という方針でコンポーネントを切り分け・実装することが多いです。なのでリクエストの効率化のために ByIds でデータ取得の取りまとめを行おうとすると、ちょっと面倒くさく感じる場面が多いですね。

この辺の処理を React のデータフローだけで組もうとすると、実際にデータが必要なコンポーネントのより親階層の方で子コンポーネントに必要なリクエストを取りまとめて行う必要がありそうですが、それをやろうとすると描画コンポーネントと、データを取得するコンテナコンポーネントの間の仕様的な結合が強くなり、使い回しをしづらいコンポーネントになってしまうのではないかと思っています。

という事情もあり ByIds 的なAPI側が用意されているにも関わらず、実際には React での組みやすさによる制約が働いて「複数 id 指定使ってないです、すみません...」となることが今までよくありました。

APIクライアントに対して dataloader を噛ませたラッパーを用意する

さて、前置きが長くなりましたが、上記のAPIクライアントに対して、以下のような dataloader を定義してAPIクライアントのラッパーを定義します。

// api-with-dataloader.ts

import DataLoader from "dataloader"
import api, { IdType } from "./api"

// DataLoaders
// -----------

const userLoader = new DataLoader((keys: IdType[]) => api.getUsersByIds(keys))
const thingLoader = new DataLoader((keys: IdType[]) => api.getThingsByIds(keys))

// Batching API Mock
// -----------------

export default {
  getUserById(id: IdType) {
    return userLoader.load(id)
  },
  getThingById(id: IdType) {
    return thingLoader.load(id)
  }
}

UserThing のそれぞれに dataloader を仕込んだAPIクライアントのラッパーを定義します。ByIds 分の定義も loadMany を利用すればできそうですが、今回のデモでは利用しないので省略します。

React コンポーネントを定義する

これらのAPIクライアントを利用するコンポーネントを以下のように組みます。

import React, { Component, useEffect, useState } from "react"
import logo from "./logo.svg"
import "./App.css"

import api, { IdType, User, Thing } from "./api"
import batchApi from "./api-with-dataloader"

// ThingSummary
// ------------

interface ThingSummaryProps {
  id: IdType
  batch?: boolean
}

function ThingSummary(props: ThingSummaryProps) {
  const { id, batch } = props
  const [thing, setThing] = useState<Thing | undefined>(undefined)

  useEffect(() => {
    if (batch) {
      batchApi.getThingById(id).then(setThing)
    } else {
      api.getThingById(id).then(setThing)
    }
  }, [])

  if (!thing) return <div>loading...</div>

  return (
    <div>
      #{thing.id}: {thing.name}
      {batch && " (batched!)"}
    </div>
  )
}

// UserSummary
// -----------

interface UserSummaryProps {
  id: IdType
  batch?: boolean
}

function UserSummary(props: UserSummaryProps) {
  const { id, batch } = props
  const [user, setUser] = useState<User | undefined>(undefined)

  useEffect(() => {
    if (batch) {
      batchApi.getUserById(id).then(setUser)
    } else {
      api.getUserById(id).then(setUser)
    }
  }, [])

  if (!user) return <div>loading...</div>

  return (
    <div>
      <h3>
        #{user.id}: {user.name}'s Favorite Things
        {batch && " (batched!)"}
      </h3>
      <ul>
        {user.favoriteThingIds.map(id => (
          <ThingSummary key={id} id={id} batch={batch} />
        ))}
      </ul>
    </div>
  )
}

// Main Component
// --------------

export default class App extends Component {
  render() {
    return (
      <div className="App">
        <header className="App-header">
          <img src={logo} className="App-logo" alt="logo" />
          <div>
            <UserSummary id={1} batch />
            <UserSummary id={2} batch />
            <UserSummary id={3} />
          </div>
        </header>
      </div>
    )
  }
}

UserThing それぞれに Summary コンポーネントを定義して、それぞれのコンポーネントは必要なデータを自身がマウントされた際にリクエストを飛ばして取得する、という設計です。こういう処理が簡単に書けるので React Hook 便利ですね。

それぞれの Summary コンポーネントには batch オプションをプロパティとして定義しています。有効化された場合には素の api の代わりに dataloader を仕込んだ batchApi が利用されます。batch が指定された UserSummary は自身のリクエストの他に子コンポーネントThingSummarybatch オプションも有効化するようにしています。

全体としては、id12 のユーザ対しては batch リクエストとして dataloader を利用したAPIクライアントの方を利用し、3 のユーザに対しては元のAPIクライアントを利用する、という描画内容になっています。

実行する

実行結果をイメージするために、改めてデータの中身と描画部分のコードを載せておきます。

const thingList: Thing[] = [
  { id: 1, name: "Raindrops" },
  { id: 2, name: "Kittens" },
  { id: 3, name: "Kettles" },
  { id: 4, name: "Mittens" },
  { id: 5, name: "Packages" }
]

const userList: User[] = [
  { id: 1, name: "Alice", favoriteThingIds: [1, 2, 3, 4, 5] },
  { id: 2, name: "Bob", favoriteThingIds: [2, 4] },
  { id: 3, name: "Carol", favoriteThingIds: [1, 3] }
]
            <UserSummary id={1} batch />
            <UserSummary id={2} batch />
            <UserSummary id={3} />

もしこれらのデータが全て ById によって取得された場合、叩かれるAPIの回数としては以下のようになりますね。

  • getUserById: 3 リクエス
  • getThingById: 9 リクエス

実際にアプリを実行してみると冒頭に貼った画像のように、以下のように描画されます。 「batched!」と描画されている箇所が batch オプションを指定した箇所です。 問題なくデータは指定した id の通りに取得されているようですね。

コンソールに出力されているのは dataloader をかませていない、素のAPIクライアントの各メソッドが呼ばれたログです。

f:id:lightbulbcat:20190322010347p:plain

コンソールの行数だけ見ても、明らかにリクエスト数は減っていますね。実際、APIクライアントが叩かれた回数としては

  • getUserById: 1 リクエスト( id: 3)
  • getUsersByIds: 1 リクエスト(id: 1, 2)
    • 直接コンポーネントからは呼んでいませんが dataloader 経由で呼ばれていますね
  • getThingById: 2 リクエスト(id: 1, 3)
  • getThingsByIds: 1 リクエスト(id: 1, 2, 3, 4, 5)
    • こちらも同様です

となりました。確かに ById リクエストが ByIds リクエストにまとめられていることがわかります。 実際に動作することがつかめたので、使い慣れたあたりで、次は dataloader の動作原理にも踏み込んでみたいですね。

今回は以上です。

*1:まとめられる単位はソースを軽く眺めたところ実行コンテキスト単位(?)な雰囲気を感じましたが、詳しくはまだ理解できていません

Rubyist じゃないけど Vagrantifile を読みたい

(=˘ ꒳ ˘=) kubernetes を試そうと Vagrant のリハビリしようとしたら Ruby の表記を思い出すのに手間取ったの巻...

Multi-Machine - Vagrant by HashiCorpVagrantfile を例に、初見で読み方に困ったところを思い出しながら書き留めておきます。

Vagrant.configure("2") do |config|
  config.vm.provision "shell", inline: "echo Hello"

  config.vm.define "web" do |web|
    web.vm.box = "apache"
  end

  config.vm.define "db" do |db|
    db.vm.box = "mysql"
  end
end

ブロック(do..end)

Vagrant.configure("2") do |config|
  # ...
end

は以下の表現と等価ですではありませんでした。

Vagrant.configure("2") { |config|
  # ...
}

どちらの表記も見かけるので迷いますね。複数行の場合は do..end を使う...とかでしょうか。

引数のカッコを省略

  config.vm.provision "shell", inline: "echo Hello"

は以下の表現と等価です。

  config.vm.provision("shell", { inline: "echo Hello" })

他の言語から来ると悪魔のようにも見える表記ですが、慣れるとだんだん括弧が見えてくるようになりますね。 この表記のおかげで、私の中では「Rubyは設定ファイル言語」という先入観があります。

DSL-like 表記の面目躍如といった所ですね。

参考

Bundler の bundler/setup と bundle exec

(=˘ ꒳ ˘=) Ruby のコードちんぷんかんぷんで読みづらい... import やら require やら load やら色々読み込みの構文があるのに加えて、どのライブラリから来た定義なのかが追いづらかったり...

...という愚痴は置いておいて、Ruby のモジュールシステムを別の方面からややこしくしている Bundler の振る舞いについて整理したいと思います。ややこしいと書きましたが、プロジェクト下に依存ライブラリを置けるという、Node.js から来た身からすると勝手知ったるベンダリング手法が取れるありがたいツールです。

Bundler がインストールしたパッケージを使うためには bundle exec したり require bundler/setup したりと、いくつかお作法が必要そうなのですが、それぞれどう作用するかを実際に動かしながら整理していきます。

公式の記述は How to use Bundler with Ruby にあります。しかし情報がパラパラしていて追いづらかったので実際に試してみることにしたのでした。

試してみる

├── Gemfile
├── bin
│   ├── with-bundler-setup.rb
│   └── without-bundler-setup.rb
├── lib
│   └── using-bundled-packages.rb
└── vendor

というプロジェクトを用意します。 https://github.com/asa-taka/try-module-on-bundler に実際のファイルを置いておきました。

フロントのスクリプトをふた通り用意します。

# bin/with-bundler-setup.rb
require 'rubygems'
require 'bundler/setup'
require './lib/using-bundled-packages'
# bin/without-bundler-setup.rb
require './lib/using-bundled-packages'

require の振る舞いを見たかったので一段 lib/using-bundled-packages.rb というスクリプトを挟んでいます。

# lib/using-bundled-packages.rb
require 'awesome_print'

として awesome_print を呼んでいます。vendor 下にインストールされた awesome_print には print 'awesome_print by bundler loaded' と仕込んで、Bundler によりインストールされたパッケージが実行された場合にテキストがプリントされるようにしました。

試した結果

bundle exec ruby ./bin/with-bundler-setup のように各組み合わせを試したところ以下のような結果になりました。

実行コマンド bundler/setup なし bundler/setup あり
ruby .... global のパッケージ Bundler によるパッケージ
bundle exec ruby ... Bundler によるパッケージ Bundler によるパッケージ

つまり bundle exec を使う、もしくはエントリポイントで require bundler/setup をすると孫 require でも Bundler によるパッケージが使われることになりますね。

毎回 bundle exec するのも面倒なので require bundler/setup の方が楽そうですね。

どういう処理でこうなっているのかはまた掘り下げてみたいところです。

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 というものになっていたらしいです。

簡単に GraphQLSchemaprintSchema して確認してみました。

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 の処理をいい感じに定義する方法はあるのでしょうか...