Input Object 'oneOf'
Input object oneOf — это особый тип input object, в котором ровно одно из полей ввода должно быть предоставлено как input, иначе сервер возвращает ошибку валидации. Такое поведение вводит полиморфизм для inputs в GraphQL, позволяя проектировать более чистые схемы.
Например, получение пользователя в нашем приложении может быть выполнено по различным свойствам, таким как ID пользователя или email. Для этого нам обычно пришлось бы создать отдельное поле для каждого свойства:
type Query {
userByID(id: ID!): User
userByEmail(email: String!): User
}Благодаря input object oneOf мы можем вместо этого иметь единственное поле user, принимающее все свойства через input object oneOf UserByInput, зная, что только одно из свойств (либо ID, либо email) может и должно быть предоставлено:
type Query {
user(by: UserByInput!): User
}
input UserByInput @oneOf {
id: ID
email: String
}(Обратите внимание, что синтаксис @oneOf выше используется исключительно в документационных целях в контексте Gato GraphQL, поскольку нам не нужно использовать SDL —Schema Definition Language— для генерации схемы; плагин уже генерирует схему через PHP-код, используя inputs из Конфигурации схемы.)
В запросе мы предоставляем значение ввода ровно для одного из свойств:
{
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 использует input objects oneOf
Рассмотрим несколько ситуаций, в которых плагин использует эту возможность и которые мы также можем применять для расширения наших GraphQL-схем.
Выборка единственной сущности по различным свойствам
Это общий случай для продемонстрированного выше запроса, касающийся input UserByInput в поле user.
Когда нам нужно получить единственную сущность (единственный User, Post, PostTag и т.д.), которая может быть однозначно идентифицирована более чем одним свойством (например, по ID или email, ID или slug и т.д.), мы можем определить все различные свойства в input object oneOf и свести все разные поля для получения этой сущности к одному полю.
Приём различных наборов данных в мутациях
При выполнении мутации мы можем принимать различные наборы данных в качестве inputs. Вместо того чтобы выставлять разные поля мутаций для каждого набора данных, используя input object oneOf, одно поле мутации может охватить все возможности.
Например, мутация loginUser может поддерживать вход пользователей с помощью ряда различных методов: имя пользователя/пароль, JWT-токен, application passwords или другие. Вот почему эта мутация принимает Input Object oneOf LoginUserByInput, который на данный момент поддерживает стандартную валидацию WordPress по имени пользователя/паролю, но также может быть расширен до других методов:
type Mutation {
loginUser(by: LoginUserByInput!): RootLoginUserMutationPayload!
}
input LoginUserByInput @oneOf {
credentials: LoginCredentialsInput
}
input LoginCredentialsInput {
usernameOrEmail: String!
password: String!
}Запрос meta values
Запрос meta values в 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 может быть строкой или массивом, и в зависимости от этого значения compare может принимать один набор значений или другой (например, IN только для массивов, LIKE только для строк). Кроме того, value является обязательным, но только если compare не получает EXISTS, — в этом случае value не нужен вовсе.
Анализируя различные наборы inputs, мы обнаружим, что существует 4 возможных комбинации в зависимости от типа сравнения, применяемого к ключу или значению, и типа значения:
keynumericValuestringValuearrayValue
Input object oneOf MetaQueryCompareByInput обрабатывает эти 4 inputs при помощи различных Enum, определяющих возможные операторы для каждого input. Таким образом, фильтруя по 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
}Таким образом, выбирая, какой input использовать в compareBy, корректность всего набора входных данных будет проверена GraphQL. Теперь при фильтрации записей, где существует некий meta key, мы не можем указывать 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
}
}Интроспекция: определение того, является ли тип Input Object «oneOf»
Мы можем узнать, является ли тип Input Object «oneOf», с помощью поля интроспекции isOneOf:
query IsOneOfInputObject {
__schema {
types {
name
isOneOf
}
}
}