コンセプト、アイデア、戦略
コンセプト、アイデア、戦略メタディレクティブによるスクリプティング機能

メタディレクティブによるスクリプティング機能

クエリ内のフィールドに適用できるディレクティブ @strTitleCase があるとします。このディレクティブはフィールドの値を "hello world!" から "Hello World!" に変換するため、String 型のフィールドにのみ適用するのが合理的です。

次のクエリを実行すると:

{
  post(by: { id: 1 }) {
    title @strTitleCase
  }
}

...次の結果が得られます:

{
  "data": {
    "post": {
      "title": "Hello World!"
    }
  }
}

では、フィールドの型が [String](または [String!])である場合、たとえば次のようなケースはどうでしょうか:

type Post {
  categoryNames: [String!]
}

このクエリを実行したとき、フィールド categoryNames にディレクティブ @strTitleCase を適用するとどうなるべきでしょうか?

{
  post(by: { id: 1 }) {
    categoryNames @strTitleCase
  }
}

理想的には、配列内のすべての String 値が変換された結果が返されるべきです:

{
  "data": {
    "post": {
      "categoryNames": [
        "Software",
        "Web Development",
        "Mobile App"
      ]
    }
  }
}

これを実現するには、@strTitleCase のディレクティブリゾルバが入力が配列かどうかを確認し、それに応じて処理する必要があります(この PHP コードは例であり、プラグインの実際のメソッドとは異なります):

function applyDirective(mixed $value, array $schemaDef): mixed
{
  // Convert each item in an array to title case
  if ($schemaDef['isArray']) {
    return array_map(ucwords(...), $value);
  }
 
  // Convert the String value to title case
  return ucwords($value);
}

これはそれほど難しくありません。しかし、フィールドが String の配列の配列、つまり [[String]] だった場合はどうなるでしょうか?少し難しくなりますが、ディレクティブはこれも処理できます:

function applyDirective(mixed $value, array $schemaDef): mixed
{
  // Convert each item in an array of arrays to title case
  if ($schemaDef['isArrayOfArrays']) {
    return array_map(
      fn (array $array) => array_map(ucwords(...), $array),
      $value
    );
  }
 
  // Convert each item in an array to title case
  if ($schemaDef['isArray']) {
    return array_map(ucwords(...), $value);
  }
 
  // Convert the String value to title case
  return ucwords($value);
}

では、[[[String]]][[[[String]]]] だった場合はどうでしょうか?実装がどんどん難しくなっていきます。

さらに悪いことに、このような追加のロジックのボイラープレートは、配列に適用される可能性があるすべてのディレクティブに実装する必要があります。たとえば、ディレクティブ @strUpperCase を実装する場合も、この追加ロジックが必要になります:

function applyDirective(mixed $value, array $schemaDef): mixed
{
  // Convert each item in an array of arrays to uppercase
  if ($schemaDef['isArrayOfArrays']) {
    return array_map(
      fn (array $array) => array_map(strtoupper(...), $array),
      $value
    );
  }
 
  // Convert each item in an array to uppercase
  if ($schemaDef['isArray']) {
    return array_map(strtoupper(...), $value);
  }
 
  // Convert the String value to uppercase
  return strtoupper($value);
}

あまりきれいではありませんね。

解決策:別のディレクティブを通じてディレクティブへの入力を変更する

ここで、あるディレクティブを適用して別のディレクティブの動作を変更することが有用になります。

フィールドの配列の深さ(つまり String[String][[String]][[[String]]] など)に対してすべて対応する代わりに、@strTitleCase は基本ケースである String だけを処理するようにできます:

function applyDirective(mixed $value, array $schemaDef): mixed
{
  // The input will always be `String`
  // Convert the String value to title case
  return ucwords($value);
}

そして、別のディレクティブ @underEachArrayItem がその動作を次のように変更します:

  1. [String] 型の単一の入力を String 型の入力の配列に変換する
  2. この配列のアイテムを反復し、それぞれに対して下流のディレクティブ(@strTitleCase)を呼び出して適用する。このとき String 型の入力を受け取る
  3. String 値の配列を再び単一の [String] 値に変換する

これにより、次のクエリを実行できます:

{
  post(by: { id: 1 }) {
    categoryNames @underEachArrayItem @strTitleCase
  }
}

この gif は @underEachArrayItem の動作を示しています:

Adding @underEachArrayItem to modify another directive

この解決策の優れた点は、配列の深さとディレクティブの実装を切り離せることです。入力の型が [[String]] であれば、追加の @underEachArrayItem を加えるだけでよく、それが意図したディレクティブを変更する @underEachArrayItem を変更します:

{
  customerAllNames @underEachArrayItem @underEachArrayItem @strTitleCase
}

...次の結果を生成します:

{
  "data": {
    "customerAllNames": [
      [
        "John",
        "Edward",
        "Stevenson"
      ],
      [
        "Samantha",
        "Perkins"
      ],
      [
        "Michael",
        "Edward",
        "Higgs"
      ]
    ]
  }
}

このように、あるディレクティブが別のディレクティブを変更するということは、ディレクティブのパイプラインでも発生し得ます。その場合、1つのディレクティブが下流のディレクティブに影響を与え、かつ自分自身も上流のディレクティブによって変更されます。

私たちは @underEachArrayItem を「メタディレクティブ」と呼んでいます:別のディレクティブの動作を変更するディレクティブです。これにより、開発者は GraphQL クエリ内にプログラミングロジックを追加する「メタスクリプティング」機能を得ることができます。

GraphQL クエリのフォーマット

空白文字はセマンティックな値を持たないため、クエリと SDL をフォーマットしてネストをより分かりやすく表現できます:

{
  customerAllNames
    @underEachArrayItem
      @underEachArrayItem
        @strTitleCase
}

ネストされたディレクティブのパイプラインを定義する

@underEachArrayItem はどのようにして @strTitleCase の動作を変更すべきだと判断するのでしょうか?前の例では、直前に配置されていたからでした。しかし、その後にさらに別のディレクティブがある場合はどうなるでしょうか?

たとえば、このクエリでは:

{
  post(by: { id: 1 }) {
    categoryNames
      @underEachArrayItem
        @strTitleCase
        @strTranslate(to: "es")
  }
}

...@underEachArrayItem はディレクティブ @strTranslate の動作も変更すべきです。このディレクティブも String に適用する必要があるため、次の応答が生成されます:

{
  "data": {
    "post": {
      "categoryNames": [
        "Software",
        "Desarrollo web",
        "Aplicación movil"
      ]
    }
  }
}

しかし、後に配置されたディレクティブが個別の String 値ではなく配列に対して適用される必要がある場合もあります。たとえば、以下のディレクティブ @arrayPad は配列に不足しているエントリをデフォルト値で補うため、@underEachArrayItem の影響を受けるべきではありません:

{
  post(by: { id: 1 }) {
    categoryNames
      @underEachArrayItem
        @strTitleCase
      @arrayPad(length: 5, value: "undefined")
  }
}

...次の応答が生成されます:

{
  "data": {
    "post": {
      "categoryNames": [
        "Software",
        "Web Development",
        "Mobile App",
        "undefined",
        "undefined"
      ]
    }
  }
}

この2つの状況を区別するために、@underEachArrayItem に引数 affectDirectivesUnderPos を導入します。これは影響を受けるべきディレクティブの相対位置を Int の配列として定義します。

以下のクエリでは、@underEachArrayItem@strTitleCase@strTranslate に適用すべきことを認識しています。これらは自身から相対位置 12 に配置されているためです:

{
  post(by: { id: 1 }) {
    categoryNames
      @underEachArrayItem(affectDirectivesUnderPos: [1, 2])
        @strTitleCase
        @strTranslate(to: "es")
  }
}

この別のクエリでは、@underEachArrayItem@strTitleCase(相対位置 1)にのみ適用され、@arrayPad には適用されません:

{
  post(by: { id: 1 }) {
    categoryNames
      @underEachArrayItem(affectDirectivesUnderPos: [1])
        @strTitleCase
      @arrayPad(length: 5, value: "undefined")
  }
}

affectDirectivesUnderPos のデフォルト値は [1] に設定されているため、指定しない場合、ディレクティブは常に直後のディレクティブに適用されます。上のクエリはこれと同等です:

{
  post(by: { id: 1 }) {
    categoryNames
      @underEachArrayItem
        @strTitleCase
      @arrayPad(length: 5, value: "undefined")
  }
}

メタディレクティブの影響を受けるディレクティブと受けないディレクティブの任意の組み合わせを定義できます:

{
  post(by: { id: 1 }) {
    categoryNames
      @underEachArrayItem(affectDirectivesUnderPos: [1, 2])
        @strTitleCase
        @strTranslate(to: "es")
      @arrayPad(length: 5, value: "undefined")
  }
}