Extensions Reference
Extensions ReferenceMultiple Query Execution

Multiple Query Execution

Combine multiple queries into a single query, sharing state across them and executing them in the requested order.

Description

Multiple query execution combines the multiple queries into a single query, making sure they are executed in the same requested order. Operations can communicate state with each other via dynamic variables, which are computed only once but can be read multiple times throughout the document.

query SomeQuery {
  id @export(as: "rootID")
}
 
query AnotherQuery
  @depends(on: "SomeQuery")
{
  _echo(value: $rootID )
}

This feature offers several benefits:

  • It improves performance: Instead of executing a query against the GraphQL server, then wait for its response, and then use that result to execute another query, we can combine the queries together into one and execute them in a single request, thus avoiding the latency from the multiple HTTP connections.
  • It allows us to manage our GraphQL queries into atomic operations (or logical units) that depend on each other, and that can be conditionally executed based on the result from a previous operation.

Multiple query execution is different from query batching, in which the GraphQL server also executes multiple queries in a single request, but those queries are merely executed one after the other, independently from each other.

Enabled directives

When Multiple query execution is enabled, the following directives are made available in the GraphQL schema:

  • @depends (operation directive): To have an operation (whether a query or mutation) indicate what other operations must be executed before
  • @export (field directive): To export some field value from one query as a dynamic variable, to be input to some field or directive in another query
  • @deferredExport (field directive): Similar to @export but to be used with Multi-Field Directives

In addition, directives @include and @skip are also made available as operation directives (they are normally only field directives), and these can be used to conditionally execute an operation if it satisfies some condition.

@depends

When the GraphQL document contains multiple operations, we indicate to the server which one to execute via URL param ?operationName=...; otherwise, the last operation will be executed.

Starting from this initial operation, the server will collect all operations to execute, which are defined by adding directive depends(on: [...]), and execute them in the corresponding order respecting the dependencies.

Directive argument operations receives an array of operation names ([String]), or we can also provide a single operation name (String).

In this query, we pass ?operationName=Four, and the executed operations (whether query or mutation) will be ["One", "Two", "Three", "Four"]:

mutation One {
  # Do something ...
}
 
mutation Two {
  # Do something ...
}
 
query Three @depends(on: ["One", "Two"]) {
  # Do something ...
}
 
query Four @depends(on: "Three") {
  # Do something ...
}

@export

Directive @export exports the value of a field (or set of fields) into a dynamic variable, to be used as input in some field or query from another query.

For instance, in this query we export the logged-in user's name, and use this value to search for posts containing this string (please notice that variable $loggedInUserName, because it is dynamic, does not need be defined in operation FindPosts):

query GetLoggedInUserName {
  me {
    name @export(as: "loggedInUserName")
  }
}
 
query FindPosts @depends(on: "GetLoggedInUserName") {
  posts(filter: { search: $loggedInUserName }) {
    id
  }
}

@deferredExport

When the Multi-Field Directives feature is enabled and we export the value of multiple fields into a dictionary, use @deferredExport instead of @export to guarantee that all directives from each involved field have been executed before exporting the field's value.

For instance, in this query, the first field has directive @strUpperCase applied to it, and the second has @strTitleCase. When executing @deferredExport, the exported value will have these directives applied:

query One {
  id @strUpperCase # Will be exported as "ROOT"
  again: id @strTitleCase # Will be exported as "Root"
    @deferredExport(as: "props", affectAdditionalFieldsUnderPos: [1])
}
 
query Two @depends(on: "One") {
  mirrorProps: _echo(value: $props)
}

Producing:

{
  "data": {
    "id": "ROOT",
    "again": "Root",
    "mirrorProps": {
      "id": "ROOT",
      "again": "Root"
    }
  }
}

@skip and @include (in operations)

When Multiple Query Execution is enabled, directives @include and @skip are also available as operation directives, and these can be used to conditionally execute an operation if it satisfies some condition.

For instance, in this query, operation CheckIfPostExists exports a dynamic variable $postExists and, only if its value is true, will mutation ExecuteOnlyIfPostExists be executed:

query CheckIfPostExists($id: ID!) {
  # Initialize the dynamic variable to `false`
  postExists: _echo(value: false) @export(as: "postExists")
 
  post(by: { id: $id }) {
    # Found the Post => Set dynamic variable to `true`
    postExists: _echo(value: true) @export(as: "postExists")
  }
}
 
mutation ExecuteOnlyIfPostExists
  @depends(on: "CheckIfPostExists")
  @include(if: $postExists)
{
  # Do something...
}

Dynamic variable outputs

@export can produce 6 different outputs, based on a combination of:

  • The value of the type argument (either SINGLE, LIST or DICTIONARY)
  • If the directive is applied to a single field, or to multiple fields (via the Multi-Field Directives module)

The 6 possible outputs then are:

  1. SINGLE type:
    1. Single field
    2. Multi-field
  2. LIST type:
    1. Single field
    2. Multi-field
  3. DICTIONARY type:
    1. Single field
    2. Multi-field

SINGLE type / Single field

The output is a single value when passing param type: SINGLE (which is set as the default value).

In this query:

query {
  post(by: { id: 1 }) {
    title @export(as: "postTitle", type: SINGLE)
  }
}

...the dynamic variable $postTitle will have value:

"Hello world!"

Please notice that if SINGLE is applied over an array of entities, then the value for the last entity is the one that is exported.

In this query:

query {
  posts(filter: { ids: [1, 5] }) {
    title @export(as: "postTitle", type: SINGLE)
  }
}

...the dynamic variable $postTitle will have the value for post with ID 5:

"Everything good?"

SINGLE type / Multi-field

If @export is applied on several fields (by adding param affectAdditionalFieldsUnderPos provided by the Multi-Field Directives module), then the value that is set on the dynamic variable is a dictionary of { key: field alias, value: field value } (of type JSONObject).

This query:

query {
  post(by: { id: 1 }) {
    title
    content
      @export(
        as: "postData",
        type: SINGLE,
        affectAdditionalFieldsUnderPos: [1]
      )
  }
}

...exports dynamic variable $postData with value:

{
  "title": "Hello world!",
  "content": "Lorem ipsum."
}

LIST type / Single field

The dynamic variable will contain an array with the field value from all the queried entities (from the enclosing field), by passing param type: LIST.

When running this query (in which queried entities are posts with ID 1 and 5):

query {
  posts(filter: { ids: [1, 5] }) {
    title @export(as: "postTitles", type: LIST)
  }
}

...the dynamic variable $postTitles will have value:

[
  "Hello world!",
  "Everything good?"
]

LIST type / Multi-field

We obtain an array of dictionaries (of type JSONObject), each containing the values of the fields on which the directive is applied.

This query:

query {
  posts(filter: { ids: [1, 5] }) {
    title
    content
      @export(
        as: "postsData",
        type: LIST,
        affectAdditionalFieldsUnderPos: [1]
      )
  }
}

...exports dynamic variable $postsData with value:

[
  {
    "title": "Hello world!",
    "content": "Lorem ipsum."
  },
  {
    "title": "Everything good?",
    "content": "Quisque convallis libero in sapien pharetra tincidunt."
  }
]

DICTIONARY type / Single field

The dynamic variable will contain a dictionary (of type JSONObject) with the ID from the queries entity as key, and the field values as value, by passing param type: DICTIONARY.

This query:

query {
  posts(filter: { ids: [1, 5] }) {
    title @export(as: "postIDTitles", type: DICTIONARY)
  }
}

...exports dynamic variable $postIDTitles with value:

{
  "1": "Hello world!",
  "5": "Everything good?"
}

DICTIONARY type / Multi-field

In this combination, we export a dictionary of dictionaries: { key: entity ID, value: { key: field alias, value: field value } } (using a type JSONObject that will contain entries of type JSONObject).

This query:

query {
  posts(filter: { ids: [1, 5] }) {
    title
    content
      @export(
        as: "postsIDProperties",
        type: DICTIONARY,
        affectAdditionalFieldsUnderPos: [1]
      )
  }
}

...exports dynamic variable $postsIDProperties with value:

{
  "1":
  {
    "title": "Hello world!",
    "content": "Lorem ipsum."
  },
  "5":
  {
    "title": "Everything good?",
    "content": "Quisque convallis libero in sapien pharetra tincidunt."
  }
}

Exporting values when iterating an array or JSON object

@export respects the cardinality from any encompassing meta-directive.

In particular, whenever @export is nested below a meta-directive that iterates on array items or JSON object properties (i.e. @underEachArrayItem and @underEachJSONObjectProperty), then the exported value will be an array.

This query:

{
  post(by: { id: 19 }) {
    coreContentAttributeBlocks: blockFlattenedDataItems(
      filterBy: { include: "core/heading" }
    )
      @underEachArrayItem
        @underJSONObjectProperty(
          by: { path: "attributes.content" },
        )
          @export(
            as: "contentAttributes",
          )
  }
}

...produces $contentAttributes with value:

[
  "List Block",
  "Columns Block",
  "Columns inside Columns (nested inner blocks)",
  "Life is so rich",
  "Life is so dynamic"
]

In contrast, the same query that accesses a specific item in the array instead of iterating over all of them (by replacing @underEachArrayItem with @underArrayItem(index: 0)) will export a single value.

This query:

{
  post(by: { id: 19 }) {
    coreContentAttributeBlocks: blockFlattenedDataItems(
      filterBy: { include: "core/heading" }
    )
      @underArrayItem(index: 0)
        @underJSONObjectProperty(
          by: { path: "attributes.content" },
        )
          @export(
            as: "contentAttributes",
          )
  }
}

...produces $contentAttributes with value:

"List Block"

Directive execution order

If there are other directives before @export, the exported value will reflect the modifications by those previous directives.

For instance, in this query, depending on @export taking place before or after @strUpperCase, the result will be different:

query One {
  id
    # First export "root", only then will be converted to "ROOT"
    @export(as: "id")
    @strUpperCase
 
  again: id
    # First convert to "ROOT" and then export this value
    @strUpperCase
    @export(as: "again")
}
 
query Two @depends(on: "One") {
  mirrorID: _echo(value: $id)
  mirrorAgain: _echo(value: $again)
}

Producing:

{
  "data": {
    "id": "ROOT",
    "again": "ROOT",
    "mirrorID": "root",
    "mirrorAgain": "ROOT"
  }
}

Execution in Persisted Queries

When a GraphQL query contains multiple operations in a Persisted Query, we can invoke the corresponding endpoint passing URL param ?operationName=... with the name of the operation to execute; otherwise, the last operation will be executed.

For instance, to execute operation GetPostsContainingString in a Persisted Query with endpoint /graphql-query/posts-with-user-name/, we must invoke:

https://mysite.com/graphql-query/posts-with-user-name/?operationName=GetPostsContainingString

Examples

Import content from an external API endpoint:

query FetchDataFromExternalEndpoint
{
  _sendJSONObjectItemHTTPRequest(input: { url: "https://site.com/wp-json/wp/posts/1" } )
    @export(as: "externalData")
    @remove
}
 
query ManipulateDataIntoInput @depends(on: "FetchDataFromExternalEndpoint")
{
  title: _objectProperty(
    object: $externalData,
    by: {
      path: "title.rendered"
    }
  ) @export(as: "postTitle")
 
  excerpt: _objectProperty(
    object: $externalData,
    by: {
      key: "excerpt"
    }
  ) @export(as: "postExcerpt")
}
 
mutation CreatePost @depends(on: "ManipulateDataIntoInput")
{
  createPost(input: {
    title: $postTitle
    excerpt: $postExcerpt
  }) {
    id
  }
}

Retrieve the data for a post, transform it, and store it again:

query GetPostData(
  $postId: ID!
) {
  post(by: {id: $postId}) {
    id
    title @export(as: "postTitle")
    contentSource @export(as: "postContent")
  }
}
 
query AdaptPostData(
  $replaceFrom: String!,
  $replaceTo: String!
)
  @depends(on: "GetPostData")
{
  adaptedPostTitle: _strReplace(
    search: $replaceFrom
    replaceWith: $replaceTo
    in: $postTitle
  )
    @export(as: "adaptedPostTitle")
 
  adaptedPostContent: _strReplace(
    search: $replaceFrom
    replaceWith: $replaceTo
    in: $postContent
  )
    @export(as: "adaptedPostContent")
}
 
mutation StoreAdaptedPostData(
  $postId: ID!
)
  @depends(on: "AdaptPostData")
{
  updatePost(input: {
    id: $postId,
    title: $adaptedPostTitle,
    contentAs: { html: $adaptedPostContent },
  }) {
    status
    errors {
      __typename
      ...on ErrorPayload {
        message
      }
    }
    post {
      id
      title
      contentSource
    }
  }
}

Update a post if it exists, or show an error message otherwise:

query GetPost($id: ID!) {
  post(by:{id: $id}) {
    id
    title
  }
  _notNull(value: $__post) @export(as: "postExists")
}
 
query FailIfPostNotExists($id: ID!)
  @skip(if: $postExists)
  @depends(on: "GetPost")
{
  errorMessage: _sprintf(
    string: "There is no post with ID '%s'",
    values: [$id]
  ) @remove
  _fail(
    message: $__errorMessage
    data: {
      id: $id
    }
  ) @remove
}
 
mutation UpdatePost($id: ID!, $postTitle: String)
  @include(if: $postExists)
  @depends(on: "GetPost")
{
  updatePost(input: {
    id: $id,
    title: $postTitle,
  }) {
    status
    errors {
      __typename
      ...on ErrorPayload {
        message
      }
    }
    post {
      id
      title
      contentSource
    }
  }
}
 
query MaybeUpdatePost
  @depends(on: [
      "FailIfPostNotExists",
      "UpdatePost"
  ])
{
  id @remove
}

Log the user in before executing a mutation, and out immediately after:

mutation LogUserIn(
  $username: String!
  $password: String!
) {
  loginUser(by: {
    credentials: {
      usernameOrEmail: $username,
      password: $password
    }
  }) @remove {
    status
    user {
      id
      username
    }
  }
}
 
mutation AddComment(
  $customPostId: ID!
  $commentContent: HTML!
)
  @depends(on: "LogUserIn")
{
  addCommentToCustomPost(input: {
    customPostID: $customPostId,
    commentAs: { html: $commentContent }
  }) {
    status
    errors {
      __typename
      ...on ErrorPayload {
        message
      }
    }
    comment {
      id
      parent {
        id
      }
      content
      date
      author {
        name
        email
      }
    }
  }
}
 
mutation LogUserOut
  @depends(on: "AddComment")
{
  logoutUser @remove {
    status
    userID
  }
}
 
query ExecuteAllAddCommentOperations
  @depends(on: "LogUserOut")
{
  id @remove
}

Conditionally log the user in before executing a mutation, if provided:

query ExportUserLogin(
  $username: String
) {
  _notNull(value: $username)
    @export(as: "hasUsername")
    @remove
}
 
mutation MaybeLogUserIn(
  $username: String
  $password: String
)
  @depends(on: "ExportUserLogin")
  @include(if: $hasUsername)
{
  loginUser(by: {
    credentials: {
      usernameOrEmail: $username,
      password: $password
    }
  }) @remove {
    status
    user {
      id
      username
    }
  }
}
 
mutation AddComment(
  $customPostId: ID!
  $commentContent: HTML!
)
  @depends(on: "MaybeLogUserIn")
{
  addCommentToCustomPost(input: {
    customPostID: $customPostId,
    commentAs: { html: $commentContent }
  }) {
    status
    errors {
      __typename
      ...on ErrorPayload {
        message
      }
    }
    comment {
      id
      parent {
        id
      }
      content
      date
      author {
        name
        email
      }
    }
  }
}
 
mutation MaybeLogUserOut
  @depends(on: "AddComment")
  @include(if: $hasUsername)
{
  logoutUser @remove {
    status
    userID
  }
}
 
query ExecuteAllAddCommentOperations
  @depends(on: "MaybeLogUserOut")
{
  id @remove
}

GraphQL spec

This functionality is currently not part of the GraphQL spec, but it has been requested: