Input Object 'oneOf'
El input object oneOf es un tipo particular de input object, donde exactamente uno de los campos de entrada debe proporcionarse como input, o de lo contrario el servidor devuelve un error de validación. Este comportamiento introduce polimorfismo para los inputs en GraphQL, permitiéndonos diseñar esquemas más limpios.
Por ejemplo, recuperar un usuario en nuestra aplicación podría hacerse por diferentes propiedades, como el ID de usuario o el email. Para hacer esto, normalmente necesitaríamos crear un campo separado para cada propiedad:
type Query {
userByID(id: ID!): User
userByEmail(email: String!): User
}Gracias al input object oneOf, podemos en su lugar tener un único campo user que acepte todas las propiedades vía un input object oneOf UserByInput, sabiendo que solo una de las propiedades (ya sea el ID o el email) puede y debe proporcionarse:
type Query {
user(by: UserByInput!): User
}
input UserByInput @oneOf {
id: ID
email: String
}(Ten en cuenta que la sintaxis @oneOf anterior es solo a efectos de documentación dentro del contexto de Gato GraphQL, ya que no necesitamos usar SDL —Schema Definition Language— para generar el esquema; el plugin ya genera el esquema vía código PHP, usando los inputs de la Configuración del Esquema.)
En la consulta, proporcionamos el valor de entrada para exactamente una de las propiedades:
{
tom: user(by: {
id: 1
}) {
name
}
jerry: user(by: {
email: "jerry@warnerbros.com"
}) {
name
}
}Si proporcionamos dos (o más) valores al input:
{
user(by: {
id: 1
email: "jerry@warnerbros.com"
}) {
name
}
}... entonces el servidor devolverá un error:
{
"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
}
}Cómo Gato GraphQL hace uso de los input objects oneOf
Veamos algunas situaciones en las que el plugin hace uso de esta característica, y que también podemos usar para extender nuestros esquemas GraphQL.
Seleccionar una única entidad por diferentes propiedades
Este es el caso general de la consulta demostrada arriba, relativa al input UserByInput en el campo user.
Siempre que necesitemos obtener una única entidad (un único User, Post, PostTag, etc.) que pueda identificarse de forma única por más de una propiedad (como por ID o email, ID o slug, etc.), podemos definir todas las propiedades distintas en un input object oneOf, y converger todos los distintos campos para recuperar esa entidad en un único campo.
Aceptar diferentes conjuntos de datos en mutaciones
Al hacer una mutación, podemos aceptar diferentes conjuntos de datos como inputs. En lugar de exponer diferentes campos de mutación para cada distinto conjunto de datos, usando un input object oneOf, un único campo de mutación puede abordar todas las posibilidades.
Por ejemplo, la mutación loginUser puede soportar el inicio de sesión de usuarios mediante varios métodos distintos: usuario/contraseña, token JWT, application passwords, u otros. Por eso esta mutación recibe el Input Object oneOf LoginUserByInput, que actualmente acepta la validación estándar usuario/contraseña de WordPress, pero también puede expandirse a otros métodos:
type Mutation {
loginUser(by: LoginUserByInput!): RootLoginUserMutationPayload!
}
input LoginUserByInput @oneOf {
credentials: LoginCredentialsInput
}
input LoginCredentialsInput {
usernameOrEmail: String!
password: String!
}Consultando meta values
Consultar meta values en WordPress puede ser complejo, con combinaciones de inputs que pueden entrar en conflicto entre sí, como se explica en su documentación:
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 ‘=’.
La documentación explica que value puede ser una cadena o un array, y dependiendo de este valor, entonces compare puede aceptar un conjunto de valores u otro (como IN solo para arrays, LIKE solo para cadenas). Además, value es obligatorio, pero solo si compare no recibe EXISTS, en cuyo caso value no se necesita en absoluto.
Analizando los diferentes conjuntos de inputs descubriremos que hay 4 combinaciones posibles, dependiendo de la comparación que se aplique sobre la clave o el valor, y el tipo de valor:
keynumericValuestringValuearrayValue
El input object oneOf MetaQueryCompareByInput aborda estos 4 inputs, ayudado por diferentes Enums que definen los posibles operadores que cada input puede usar. Entonces, filtrando por numericValue podemos usar el operador GREATER_THAN, por arrayValue podemos usar el operador IN, y por key podemos usar el operador EXISTS (y no hay necesidad de proporcionar un value).
El esquema GraphQL resultante (usando SDL) es este:
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
}De este modo, eligiendo qué input usar bajo compareBy, la correctitud del conjunto de datos de input global será validada por GraphQL. Ahora, al filtrar entradas donde alguna meta key existe, no podemos proporcionar un value:
{
posts(filter: {
metaQuery: {
key: "_thumbnail_id",
compareBy:{
key: {
operator: EXISTS
}
}
}
}) {
id
title
metaValue(key: "_thumbnail_id")
}
}Para filtrar entradas "marcadas como gustadas" por algún usuario, usamos el input arrayValue, y seleccionamos el operador IN:
query FilterPostsLikedByUser($userID: ID!) {
posts(filter: {
metaQuery: {
key: "liked_by_users",
compareBy:{
arrayValue: {
value: $userID
operator: IN
}
}
}
}) {
id
title
}
}Introspección: averiguando si un tipo es un Input Object "oneOf"
Podemos averiguar si un tipo es un Input Object "oneOf" mediante el campo de introspección isOneOf:
query IsOneOfInputObject {
__schema {
types {
name
isOneOf
}
}
}