Extension du schéma
Extension du schémaInput Object 'oneOf'

Input Object 'oneOf'

L'input object oneOf est un type particulier d'input object, dans lequel exactement un des champs d'entrée doit être fourni comme input, faute de quoi le serveur renvoie une erreur de validation. Ce comportement introduit le polymorphisme pour les inputs en GraphQL, nous permettant de concevoir des schémas plus élégants.

Par exemple, récupérer un utilisateur dans notre application pourrait se faire via différentes propriétés, comme l'ID utilisateur ou l'e-mail. Pour ce faire, nous aurions normalement besoin de créer un champ séparé pour chaque propriété :

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

Grâce à l'input object oneOf, nous pouvons à la place avoir un unique champ user qui accepte toutes les propriétés via un input object oneOf UserByInput, sachant qu'une seule des propriétés (soit l'ID, soit l'e-mail) peut et doit être fournie :

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

(Notez que la syntaxe @oneOf ci-dessus est uniquement à des fins de documentation dans le contexte de Gato GraphQL, car nous n'avons pas besoin d'utiliser SDL —Schema Definition Language— pour générer le schéma ; le plugin génère déjà le schéma via du code PHP, en utilisant les inputs de la Configuration du Schéma.)

Dans la requête, nous fournissons la valeur d'entrée pour exactement une des propriétés :

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

Si nous fournissons deux valeurs (ou plus) à l'input :

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

... alors le serveur renverra une erreur :

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

Comment Gato GraphQL utilise les input objects oneOf

Voyons quelques situations dans lesquelles le plugin fait usage de cette fonctionnalité, et que nous pouvons également utiliser pour étendre nos schémas GraphQL.

Sélectionner une entité unique par différentes propriétés

C'est le cas général de la requête démontrée ci-dessus, concernant l'input UserByInput dans le champ user.

Chaque fois que nous avons besoin de récupérer une entité unique (un unique User, Post, PostTag, etc.) qui peut être identifiée de manière unique par plus d'une propriété (comme par ID ou e-mail, ID ou slug, etc.), nous pouvons définir toutes les différentes propriétés dans un input object oneOf, et regrouper tous les différents champs permettant de récupérer cette entité en un seul champ.

Accepter différents ensembles de données dans les mutations

Lors d'une mutation, nous pouvons accepter différents ensembles de données comme inputs. Au lieu d'exposer différents champs de mutation pour chaque ensemble de données distinct, en utilisant un input object oneOf, un unique champ de mutation peut couvrir toutes les possibilités.

Par exemple, la mutation loginUser peut prendre en charge la connexion des utilisateurs via plusieurs méthodes différentes : identifiant/mot de passe, token JWT, application passwords, ou autres. C'est pourquoi cette mutation reçoit l'Input Object oneOf LoginUserByInput, qui accepte actuellement la validation standard identifiant/mot de passe de WordPress, mais peut également être étendu à d'autres méthodes :

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

Interroger les meta values

Interroger les meta values dans WordPress peut être complexe, avec des combinaisons d'inputs qui peuvent entrer en conflit les uns avec les autres, comme expliqué dans sa documentation :

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 documentation explique que value peut être une chaîne ou un tableau, et selon cette valeur, compare peut accepter un ensemble de valeurs ou un autre (comme IN uniquement pour les tableaux, LIKE uniquement pour les chaînes). De plus, value est obligatoire, mais seulement si compare ne reçoit pas EXISTS, auquel cas value n'est pas nécessaire du tout.

En analysant les différents ensembles d'inputs, nous découvrirons qu'il existe 4 combinaisons possibles, selon la comparaison appliquée sur la clé ou la valeur, et le type de valeur :

  • key
  • numericValue
  • stringValue
  • arrayValue

L'input object oneOf MetaQueryCompareByInput gère ces 4 inputs, aidé par différents Enums qui définissent les opérateurs possibles que chaque input peut utiliser. Ainsi, en filtrant par numericValue, nous pouvons utiliser l'opérateur GREATER_THAN, par arrayValue nous pouvons utiliser l'opérateur IN, et par key nous pouvons utiliser l'opérateur EXISTS (sans avoir besoin de fournir un value).

Le schéma GraphQL résultant (en utilisant SDL) est le suivant :

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
}

Ainsi, en choisissant quel input utiliser sous compareBy, la cohérence de l'ensemble des données d'input sera validée par GraphQL. Maintenant, lorsqu'on filtre les articles pour lesquels une meta key existe, nous ne pouvons pas fournir de value :

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

Pour filtrer les articles « aimés » par un utilisateur, nous utilisons l'input arrayValue et sélectionnons l'opérateur IN :

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

Introspection : déterminer si un type est un Input Object « oneOf »

Nous pouvons déterminer si un type est un Input Object « oneOf » via le champ d'introspection isOneOf :

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