แนวคิด ไอเดีย และกลยุทธ์
แนวคิด ไอเดีย และกลยุทธ์ความสามารถด้านการเขียนสคริปต์ผ่านเมตาไดเรกทีฟ

ความสามารถด้านการเขียนสคริปต์ผ่านเมตาไดเรกทีฟ

สมมติว่าเรามีไดเรกทีฟ @strTitleCase ที่สามารถใช้กับฟิลด์ในคิวรีได้ โดยแปลงค่าจาก "hello world!" เป็น "Hello World!" ดังนั้นจึงสมเหตุสมผลที่จะใช้กับฟิลด์ประเภท String เท่านั้น

เมื่อรันคิวรีนี้:

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

...จะได้ผลลัพธ์:

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

ทีนี้ สมมติว่าประเภทของฟิลด์คือ [String] (หรือ [String!]) ดังในกรณีนี้:

type Post {
  categoryNames: [String!]
}

ควรจะเกิดอะไรขึ้นเมื่อนำไดเรกทีฟ @strTitleCase มาใช้กับฟิลด์ categoryNames เมื่อรันคิวรีนี้?

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

ในอุดมคติ ผลลัพธ์ควรเป็นการแปลงค่า String ทุกค่าภายในอาร์เรย์:

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

เพื่อให้เป็นเช่นนั้น ไดเรกทีฟ resolver ของ @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]]]] ล่ะ? มันเริ่มยากในการนำไปใช้งาน

ยิ่งไปกว่านั้น โค้ดโครงสร้าง (boilerplate) ของลอจิกเพิ่มเติมนี้จะต้องถูกนำไปใช้ในทุกไดเรกทีฟที่อาจใช้กับอาร์เรย์ ตัวอย่างเช่น การนำไดเรกทีฟ @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. วนซ้ำผ่านรายการในอาร์เรย์นี้ และสำหรับแต่ละรายการ เรียกและนำไดเรกทีฟ downstream (@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"
      ]
    ]
  }
}

ดังนั้น ดังที่เราเห็น การที่ไดเรกทีฟปรับเปลี่ยนไดเรกทีฟอื่นสามารถเกิดขึ้นใน pipeline ของไดเรกทีฟได้เช่นกัน โดยหนึ่งในนั้นส่งผลต่อไดเรกทีฟ downstream และตัวเองก็ถูกปรับเปลี่ยนโดยไดเรกทีฟ upstream

เราเรียก @underEachArrayItem ว่า "เมตาไดเรกทีฟ": ไดเรกทีฟที่ปรับเปลี่ยนพฤติกรรมของไดเรกทีฟอื่น ด้วยการทำเช่นนี้ มันมอบความสามารถ "เมตา-สคริปติ้ง" ให้กับนักพัฒนา เพื่อเพิ่มลอจิกการโปรแกรมภายใน GraphQL queries

การจัดรูปแบบ GraphQL queries

เนื่องจากช่องว่างไม่มีค่าทางความหมาย เราสามารถจัดรูปแบบคิวรีและ SDL เพื่อแสดงการซ้อนกันให้ชัดเจนขึ้น:

{
  customerAllNames
    @underEachArrayItem
      @underEachArrayItem
        @strTitleCase
}

การกำหนด pipeline ของไดเรกทีฟที่ซ้อนกัน

@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"
      ]
    }
  }
}

เพื่อแยกความแตกต่างระหว่างสองสถานการณ์นี้ เราแนะนำอาร์กิวเมนต์ affectDirectivesUnderPos ให้กับ @underEachArrayItem ซึ่งกำหนดตำแหน่งสัมพัทธ์ของไดเรกทีฟที่ต้องได้รับผลกระทบ เป็นอาร์เรย์ของ Int

ในคิวรีด้านล่าง @underEachArrayItem รู้ว่าต้องนำไปใช้กับ @strTitleCase และ @strTranslate เนื่องจากอยู่ในตำแหน่งสัมพัทธ์ 1 และ 2 จากตัวมันเอง:

{
  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")
  }
}