การเสริมขยาย Schema
การเสริมขยาย Schema'oneOf' Input Object

'oneOf' Input Object

oneOf input object คือ input object ชนิดพิเศษ ซึ่งต้องระบุค่าให้กับ input field เพียงหนึ่งฟิลด์เท่านั้น มิฉะนั้นเซิร์ฟเวอร์จะคืนค่าข้อผิดพลาดการตรวจสอบ พฤติกรรมนี้นำ polymorphism มาสู่ inputs ใน GraphQL ช่วยให้เราออกแบบ schema ที่สะอาดกว่าได้

ตัวอย่างเช่น การดึงข้อมูลผู้ใช้ในแอปพลิเคชันของเราสามารถทำได้ผ่านหลายคุณสมบัติ เช่น ID ของผู้ใช้หรืออีเมล ในการทำเช่นนี้ โดยปกติเราจะต้องสร้างฟิลด์แยกสำหรับแต่ละคุณสมบัติ:

type Query {
  userByID(id: ID!): User
  userByEmail(email: String!): User
}

ด้วย oneOf input object เราสามารถมีฟิลด์เดียว user ที่รับทุกคุณสมบัติผ่าน UserByInput oneOf input object แทนได้ โดยรับประกันว่าจะต้องระบุคุณสมบัติเพียงหนึ่งอย่างเท่านั้น (ไม่ว่าจะเป็น ID หรืออีเมล):

type Query {
  user(by: UserByInput!): User
}
 
input UserByInput @oneOf {
  id: ID
  email: String
}

(โปรดทราบว่าไวยากรณ์ @oneOf ข้างต้นใช้เพื่อจุดประสงค์ด้านเอกสารภายในบริบทของ Gato GraphQL เท่านั้น เนื่องจากเราไม่จำเป็นต้องใช้ SDL — Schema Definition Language — เพื่อสร้าง schema ปลั๊กอินสร้าง schema ผ่านโค้ด PHP โดยใช้ inputs จาก Schema Configuration)

ในการ query เราระบุค่า input สำหรับคุณสมบัติเพียงหนึ่งอย่างเท่านั้น:

{
  tom: user(by: {
    id: 1
  }) {
    name
  }
 
  jerry: user(by: {
    email: "jerry@warnerbros.com"
  }) {
    name
  }
}

หากเราระบุสองค่า (หรือมากกว่า) ให้กับ input:

{
  user(by: {
    id: 1
    email: "jerry@warnerbros.com"
  }) {
    name
  }
}

... เซิร์ฟเวอร์จะคืนค่าข้อผิดพลาด:

{
  "errors": [
    {
      "message": "The oneOf input object 'UserByInput' must be provided exactly one value, but 2 have been provided",
      "extensions": {
        "type": "Query",
        "field": "user(by:{id:1,email:\"jerry@warnerbros.com\"})",
        "argument": "by"
      }
    }
  ],
  "data": {
    "user": null
  }
}

Gato GraphQL ใช้ oneOf input objects อย่างไร

มาดูสถานการณ์บางอย่างที่ปลั๊กอินใช้คุณสมบัตินี้ และที่เราสามารถนำมาใช้เพื่อขยาย GraphQL schemas ของเราเองได้

การเลือก entity เดียวด้วยคุณสมบัติที่แตกต่างกัน

นี่คือกรณีทั่วไปสำหรับ query ที่แสดงไว้ข้างต้น เกี่ยวกับ input UserByInput ในฟิลด์ user

เมื่อใดก็ตามที่เราต้องการดึงข้อมูล entity เดียว (เช่น User, Post, PostTag เป็นต้น) ที่สามารถระบุตัวตนได้โดยไม่ซ้ำกันด้วยคุณสมบัติมากกว่าหนึ่งอย่าง (เช่น ตาม ID หรืออีเมล, ID หรือ slug เป็นต้น) เราสามารถกำหนดคุณสมบัติที่แตกต่างกันทั้งหมดลงใน oneOf input object และรวมฟิลด์ต่างๆ ทั้งหมดที่ใช้ดึงข้อมูล entity นั้นให้เหลือฟิลด์เดียว

การรับชุดข้อมูลที่แตกต่างกันใน mutations

เมื่อทำ mutation เราอาจรับชุดข้อมูลที่แตกต่างกันเป็น inputs แทนที่จะเปิดเผย mutation fields ที่แตกต่างกันสำหรับแต่ละชุดข้อมูล การใช้ oneOf input object ช่วยให้ mutation field เดียวสามารถรองรับทุกความเป็นไปได้

ตัวอย่างเช่น mutation loginUser สามารถรองรับการล็อกอินผู้ใช้ด้วยวิธีต่างๆ หลายวิธี เช่น username/password, JWT token, application passwords หรืออื่นๆ นั่นคือเหตุผลที่ mutation นี้รับ oneOf Input Object LoginUserByInput ซึ่งปัจจุบันรองรับการตรวจสอบ username/password มาตรฐานของ WordPress แต่ยังสามารถขยายไปยังวิธีอื่นๆ ได้:

type Mutation {
  loginUser(by: LoginUserByInput!): RootLoginUserMutationPayload!
}
 
input LoginUserByInput @oneOf {
  credentials: LoginCredentialsInput
}
 
input LoginCredentialsInput {
  usernameOrEmail: String!
  password: String!
}

การ query ค่า meta

การ query ค่า meta ใน WordPress อาจมีความซับซ้อน โดยมีการผสมผสาน inputs ที่อาจขัดแย้งกัน ตามที่อธิบายไว้ในเอกสาร:

The following arguments can be passed in a key=>value paired array.

  • meta_query (array) – Contains one or more arrays with the following keys:
    • key (string) – Custom field key.
    • value (string|array) – Custom field value. It can be an array only when compare is 'IN', 'NOT IN', 'BETWEEN', or 'NOT BETWEEN'. You don't have to specify a value when using the 'EXISTS' or 'NOT EXISTS' comparisons in WordPress 3.9 and up. (Note: Due to bug #23268, value was required for NOT EXISTS comparisons to work correctly prior to 3.9. You had to supply some string for the value parameter. An empty string or NULL will NOT work. However, any other string will do the trick and will NOT show up in your SQL when using NOT EXISTS. Need inspiration? How about 'bug #23268'.)
    • compare (string) – Operator to test. Possible values are '=', '!=', '>', '>=', '<', '<=', 'LIKE', 'NOT LIKE', 'IN', 'NOT IN', 'BETWEEN', 'NOT BETWEEN', 'EXISTS' (only in WP >= 3.5), and 'NOT EXISTS' (also only in WP >= 3.5). Values 'REGEXP', 'NOT REGEXP' and 'RLIKE' were added in WordPress 3.7. Default value is '='.

เอกสารอธิบายว่า value สามารถเป็น string หรือ array ได้ และขึ้นอยู่กับค่านี้ compare สามารถรับชุดค่าหนึ่งหรืออีกชุดหนึ่งได้ (เช่น IN สำหรับ array เท่านั้น, LIKE สำหรับ string เท่านั้น) นอกจากนี้ value เป็นฟิลด์บังคับ แต่เฉพาะในกรณีที่ compare ไม่รับค่า EXISTS เท่านั้น ซึ่งในกรณีนั้น value ไม่จำเป็นเลย

เมื่อวิเคราะห์ชุด inputs ที่แตกต่างกัน เราจะพบว่ามี 4 combinations ที่เป็นไปได้ ขึ้นอยู่กับการเปรียบเทียบที่ใช้กับ key หรือ value และประเภทของค่า:

  • key
  • numericValue
  • stringValue
  • arrayValue

oneOf input object MetaQueryCompareByInput จัดการกับ 4 inputs เหล่านี้ โดยได้รับความช่วยเหลือจาก Enums ต่างๆ ที่กำหนด operators ที่แต่ละ input สามารถใช้ได้ เมื่อกรองด้วย numericValue เราสามารถใช้ operator GREATER_THAN ได้ เมื่อกรองด้วย arrayValue เราสามารถใช้ operator IN ได้ และเมื่อกรองด้วย key เราสามารถใช้ operator EXISTS ได้ (และไม่จำเป็นต้องระบุ value)

GraphQL schema ที่ได้ (โดยใช้ SDL) มีดังนี้:

type Query {
  posts(filter: PostsFilterInput): [Post!]!
}
 
input PostsFilterInput {
  metaQuery: [PostMetaQueryInput!] 
}
 
input PostMetaQueryInput {
  compareBy: MetaQueryCompareByInput!
  key: String!
}
 
type MetaQueryCompareByInput @oneOf {
  """
  Compare against the meta key
  """
  key: MetaQueryCompareByKeyInput
 
  """
  Compare against an array meta value
  """
  array: ValueMetaQueryCompareByArrayValueInput
 
  """
  Compare against a numeric meta value
  """
  numeric: ValueMetaQueryCompareByNumericValueInput
 
  """
  Compare against a string meta value
  """
  string: ValueMetaQueryCompareByStringValueInput
}
 
input MetaQueryCompareByKeyInput {
  operator: MetaQueryCompareByKeyOperatorEnum!
}
 
enum MetaQueryCompareByKeyOperatorEnum {
  EXISTS
  NOT_EXISTS
}
 
input ValueMetaQueryCompareByArrayValueInput {
  operator: MetaQueryCompareByArrayValueOperatorEnum!
  value: [AnyBuiltInScalar!]!
}
 
# AnyBuiltInScalar: Int, Float, String or Bool
scalar AnyBuiltInScalar
 
enum MetaQueryCompareByArrayValueOperatorEnum {
  BETWEEN
  IN
  NOT_BETWEEN
  NOT_IN
}
 
input ValueMetaQueryCompareByNumericValueInput {
  operator: MetaQueryCompareByNumericValueOperatorEnum!
  value: Numeric!
}
 
enum MetaQueryCompareByNumericValueOperatorEnum {
  EQUALS
  GREATER_THAN
  GREATER_THAN_OR_EQUAL
  LESS_THAN
  LESS_THAN_OR_EQUAL
  NOT_EQUALS
}
 
# Numeric: Float or Int
scalar Numeric
 
input ValueMetaQueryCompareByStringValueInput {
  operator: MetaQueryCompareByStringValueOperatorEnum!
  value: String!
}
 
enum MetaQueryCompareByStringValueOperatorEnum {
  EQUALS
  LIKE
  NOT_EQUALS
  NOT_LIKE
  NOT_REGEXP
  REGEXP
  RLIKE
}

ด้วยวิธีนี้ การเลือก input ที่จะใช้ภายใต้ compareBy ความถูกต้องของชุดข้อมูล input โดยรวมจะได้รับการตรวจสอบโดย GraphQL ขณะนี้เมื่อกรองโพสต์ที่มี meta key อยู่ เราไม่สามารถระบุ value ได้:

{
  posts(filter: {
    metaQuery: {
      key: "_thumbnail_id",
      compareBy:{
        key: {
          operator: EXISTS
        }
      }
    }
  }) {
    id
    title
    metaValue(key: "_thumbnail_id")
  }
}

เพื่อกรองโพสต์ที่ผู้ใช้คนหนึ่ง "ถูกใจ" เราใช้ input arrayValue และเลือก operator IN:

query FilterPostsLikedByUser($userID: ID!) {
  posts(filter: {
    metaQuery: {
      key: "liked_by_users",
      compareBy:{
        arrayValue: {
          value: $userID
          operator: IN
        }
      }
    }
  }) {
    id
    title
  }
}

Introspection: การตรวจสอบว่า type เป็น "oneOf" Input Object หรือไม่

เราสามารถตรวจสอบว่า type เป็น "oneOf" Input Object หรือไม่ผ่าน introspection field isOneOf:

query IsOneOfInputObject {
  __schema {
    types {
      name
      isOneOf
    }
  }
}