Schema-uitbreiding
Schema-uitbreidingInput Object 'oneOf'

Input Object 'oneOf'

Het oneOf input object is een bijzonder type input object, waarbij precies één van de inputvelden als invoer moet worden opgegeven; anders geeft de server een validatiefout terug. Dit gedrag introduceert polymorfisme voor inputs in GraphQL, waardoor we neater schema's kunnen ontwerpen.

Zo kan het ophalen van een gebruiker in onze applicatie via verschillende eigenschappen gebeuren, zoals het gebruikers-ID of e-mailadres. Om dit te doen, zouden we normaal gesproken voor elke eigenschap een afzonderlijk veld moeten aanmaken:

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

Dankzij het oneOf input object kunnen we in plaats daarvan één enkel veld user hebben dat alle eigenschappen accepteert via een UserByInput oneOf input object, wetende dat slechts één van de eigenschappen (het ID of het e-mailadres) kan en moet worden opgegeven:

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

(Let op: de syntax @oneOf hierboven is uitsluitend bedoeld voor documentatiedoeleinden in de context van Gato GraphQL, omdat we geen SDL —Schema Definition Language— hoeven te gebruiken om het schema te genereren; de plugin genereert het schema al via PHP-code, met behulp van de inputs uit de Schemaconfiguratie.)

In de query geven we de inputwaarde op voor precies één van de eigenschappen:

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

Als we twee (of meer) waarden aan de input meegeven:

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

... dan geeft de server een fout terug:

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

Hoe Gato GraphQL gebruik maakt van oneOf input objects

Laten we een paar situaties bekijken waarin de plugin gebruik maakt van deze functie, en die we ook kunnen gebruiken om onze GraphQL-schema's uit te breiden.

Een enkele entiteit selecteren op basis van verschillende eigenschappen

Dit is het algemene geval voor de hierboven gedemonstreerde query, met betrekking tot input UserByInput in veld user.

Wanneer we een enkele entiteit (een enkele User, Post, PostTag, enz.) moeten ophalen die uniek geïdentificeerd kan worden door meer dan één eigenschap (zoals op ID of e-mailadres, ID of slug, enz.), kunnen we alle verschillende eigenschappen definiëren in een oneOf input object en alle verschillende velden voor het ophalen van die entiteit samenvoegen in één enkel veld.

Verschillende gegevenssets accepteren in mutaties

Bij het uitvoeren van een mutatie kunnen we verschillende gegevenssets als inputs accepteren. In plaats van voor elke afzonderlijke gegevensset aparte mutatievelden bloot te stellen, kan één mutatiepunt met behulp van een oneOf input object alle mogelijkheden verwerken.

De mutatie loginUser kan bijvoorbeeld het inloggen van gebruikers via een aantal verschillende methoden ondersteunen: gebruikersnaam/wachtwoord, JWT-token, applicatiewachtwoorden of andere. Daarom ontvangt deze mutatie het oneOf Input Object LoginUserByInput, dat momenteel de standaard WordPress-validatie voor gebruikersnaam/wachtwoord accepteert, maar ook uitgebreid kan worden met andere methoden:

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

Meta values opvragen

Het opvragen van meta values in WordPress kan complex zijn, met combinaties van inputs die met elkaar in conflict kunnen komen, zoals uitgelegd in de documentatie:

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 '='.

De documentatie legt uit dat value een string of een array kan zijn, en afhankelijk van deze waarde kan compare een bepaalde reeks waarden accepteren of een andere (zoals IN alleen voor arrays, LIKE alleen voor strings). Bovendien is value verplicht, maar alleen als compare niet de waarde EXISTS ontvangt; in dat geval is value helemaal niet nodig.

Door de verschillende inputsets te analyseren, ontdekken we dat er 4 mogelijke combinaties zijn, afhankelijk van de vergelijking die wordt toegepast op de sleutel of de waarde, en het type waarde:

  • key
  • numericValue
  • stringValue
  • arrayValue

Het oneOf input object MetaQueryCompareByInput verwerkt deze 4 inputs, ondersteund door verschillende Enums die de mogelijke operatoren definiëren die elke input kan gebruiken. Zo kunnen we bij het filteren op numericValue de operator GREATER_THAN gebruiken, bij arrayValue de operator IN, en bij key de operator EXISTS (en hoeven we geen value op te geven).

Het resulterende GraphQL-schema (met SDL) ziet er als volgt uit:

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
}

Op deze manier wordt de juistheid van de volledige invoergegevensset door GraphQL gevalideerd door te kiezen welke input je gebruikt onder compareBy. Nu kunnen we bij het filteren van posts waarbij een bepaalde meta key bestaat geen value opgeven:

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

Om posts te filteren die door een bepaalde gebruiker zijn "geliked", gebruiken we de input arrayValue en selecteren we de operator IN:

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

Introspectie: nagaan of een type een "oneOf" Input Object is

We kunnen nagaan of een type een "oneOf" Input Object is via het introspectiepunt isOneOf:

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