스키마 확장
스키마 확장'oneOf' Input Object

'oneOf' Input Object

oneOf input object는 특정 종류의 input object로, 입력 필드 중 정확히 하나만 입력으로 제공해야 하며, 그렇지 않으면 서버가 유효성 검사 오류를 반환합니다. 이 동작은 GraphQL의 입력에 다형성을 도입하여 더 깔끔한 스키마를 설계할 수 있게 해줍니다.

예를 들어, 애플리케이션에서 사용자를 조회하려면 사용자 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 Configuration의 입력을 사용하는 PHP 코드를 통해 스키마를 생성합니다.)

쿼리에서는 속성 중 정확히 하나의 입력 값을 제공합니다.

{
  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 object 활용 방법

플러그인이 이 기능을 사용하는 몇 가지 상황과, GraphQL 스키마를 확장하기 위해 활용할 수 있는 방법을 살펴보겠습니다.

다양한 속성으로 단일 엔티티 선택하기

이것은 필드 user의 input UserByInput에 관한 위의 쿼리에서 보여준 일반적인 경우입니다.

여러 속성(ID 또는 이메일, ID 또는 슬러그 등)으로 고유하게 식별할 수 있는 단일 엔티티(단일 User, Post, PostTag 등)를 가져와야 할 때, 모든 다양한 속성을 oneOf input object에 정의하고, 해당 엔티티를 가져오기 위한 서로 다른 필드를 단일 필드로 통합할 수 있습니다.

뮤테이션에서 다양한 데이터 세트 받기

뮤테이션을 수행할 때, 입력으로 다양한 데이터 세트를 받을 수 있습니다. 각 다른 데이터 세트에 대해 별도의 뮤테이션 필드를 노출하는 대신, oneOf input object를 사용하면 단일 뮤테이션 필드가 모든 가능성을 처리할 수 있습니다.

예를 들어, 뮤테이션 loginUser는 사용자 이름/비밀번호, JWT 토큰, 애플리케이션 비밀번호 등 다양한 방법으로 사용자 로그인을 지원할 수 있습니다. 그래서 이 뮤테이션은 oneOf Input Object LoginUserByInput을 받습니다. 현재 WordPress의 표준 사용자 이름/비밀번호 인증을 받아들이지만, 다른 방법으로도 확장할 수 있습니다.

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

메타 값 쿼리하기

WordPress에서 메타 값을 쿼리하는 것은 복잡할 수 있으며, 서로 충돌할 수 있는 입력 조합이 존재합니다. 문서에서 설명된 바와 같이:

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가 문자열 또는 배열이 될 수 있으며, 이 값에 따라 compare가 받아들일 수 있는 값의 집합이 달라진다고 설명합니다(IN은 배열에만, LIKE는 문자열에만 사용 가능 등). 또한 value는 필수이지만, compareEXISTS를 받는 경우에는 value가 필요하지 않습니다.

다양한 입력 세트를 분석하면, 키 또는 값에 적용되는 비교 방식과 값의 유형에 따라 4가지 가능한 조합이 있음을 알 수 있습니다.

  • key
  • numericValue
  • stringValue
  • arrayValue

oneOf input object MetaQueryCompareByInput은 각 input이 사용할 수 있는 연산자를 정의하는 다양한 Enum의 도움을 받아 이 4가지 입력을 처리합니다. numericValue로 필터링할 때는 연산자 GREATER_THAN을, arrayValue로 필터링할 때는 연산자 IN을, key로 필터링할 때는 연산자 EXISTS를 사용할 수 있습니다(이 경우 value를 제공할 필요가 없습니다).

결과적인 GraphQL 스키마(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
}

이렇게 compareBy에서 사용할 input을 선택함으로써, 전체 입력 데이터 세트의 정확성이 GraphQL에 의해 검증됩니다. 이제 메타 키가 존재하는 게시물을 필터링할 때 value를 제공할 수 없습니다.

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

특정 사용자가 「좋아요」를 누른 게시물을 필터링하려면 input arrayValue를 사용하고 연산자 IN을 선택합니다.

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

인트로스펙션: 타입이 "oneOf" Input Object인지 확인하기

인트로스펙션 필드 isOneOf를 사용하여 타입이 "oneOf" Input Object인지 확인할 수 있습니다.

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