In my previous article, Unconventional Ways of Using GraphQL, I demonstrated some devious (but delightful) ways of using GraphQL. When describing how to implement a gateway in the query-side, I introduced the @export directive to produce a chain of queries, passing data forward among them.

I'd like to keep exploring this concept a bit more. What is interesting about this approach is that @export will be implicitly creating dynamic variables in the GraphQL server. Dynamic variables behave differently than the "static" variables documented in the GraphQL spec, whose inputs are provided to the query in advance, before it is parsed. Dynamic variables, instead, have their values calculated at runtime, during the execution of the query.

The implication is that we can conveniently use @export and dynamic variables as a way of storing temporary values, for instance to manipulate the data into a more complex form, relieving this task from the client.

In this article, I'll show an example on how we can use dynamic variables to manipulate data, before sending it in the response to the query.

StepZen provides another option that can be used to achieve similar results to the use of @export here. Their @sequence directive allows you to execute multiple queries in a single sequence, passing variables from one query to the next. You can read more about how the @sequence directive works in the documentation.

The example

The objective is to fetch a blog post's content already translated to the user's language. Unfortunately, the user's language is not stored in the website. Instead, we must retrieve it from the newsletter service, where the user has indicated his/her preferred language.

The newsletter service provides a REST endpoint, but truth be told, it's not great. We can't filter the results by user email. Instead, the single endpoint /subscriptions will produce a list of all the subscribed users, as a pair of email/language:

[
  {
    "email": "abracadabra@ganga.com",
    "lang": "de"
  },
  {
    "email": "longon@caramanon.com",
    "lang": "es"
  },
  {
    "email": "rancotanto@parabara.com",
    "lang": "en"
  },
  {
    "email": "quezarapadon@quebrulacha.net",
    "lang": "fr"
  },
  {
    "email": "test@test.com",
    "lang": "de"
  },
  {
    "email": "emilanga@pedrola.com",
    "lang": "fr"
  }
]

To retrieve the user's language, we must then retrieve the data from the newsletter service's REST endpoint, iterate all entries, and compare the entry's email with the provided email. Once we found it, we extract the language from the corresponding entry.

Finally, having the user's language, we can translate the blog post.

All this logic must be executed within a single client-server interaction. Hence, we will attempt to make it happen within a single GraphQL query, aided by @export and dynamic variables.

Retrieving the newsletter list

To fetch the newsletter entries from the external REST endpoint, we can implement the gateway in the query itself via field getJSON (as I explained in the previous article), and use @export to place the data with the entries under a dynamic variable $_newsletterList:

query FetchNewsletterList {
  newsletterList: getJSON(url: "https://newapi.getpop.org/wp-json/newsletter/v1/subscriptions") @export(as: "_newsletterList")
}

StepZen also provides an easy way to connect data from a REST API to your GraphQL API via its @rest directive. For more details, check the docs.

As a side note, are you wondering why the dynamic variable starts with $_? That's a hack I've had to add to add to my server, in order to tell dynamic variables, which must not be validated on query parsing since their value will be assigned on runtime, from static variables, which must be validated when parsing the query.

Iterating the list of entries

Here we arrive at the crux of the matter. Given a list of entries and an email provided as input, we want to find the corresponding language. Let's see how, dynamic variables, as provided by @export, will help us achieve that.

This query provides a solution: given static variable $email (provided by the user) and the dynamic variable $_newsletterList (calculated on runtime in the previous query), it will find the language and place it under a dynamic variable $_userLang:

query OperateToFindUserLanguage(
  $email: String!,
  $_newsletterList: [Object]! = [],
  $_newsletterEmailList: [String]! = [],
  $_pos: Int! = 0,
  $_userLangObj: Object! = {}
) {
  newsletterEmailList: extract(object: $_newsletterList, path: "email") @export(as: "_newsletterEmailList")
  pos: arraySearch(array: $_newsletterEmailList, element: $email) @export(as: "_pos")
  userLangObj: extract(object: $_newsletterList, path: $_pos) @export(as: "_userLangObj")
  userLang: extract(object: $_userLangObj, path: "lang") @export(as: "_userLang")
}

This query has several root fields only because, in this particular GraphQL server, the root fields are executed in the same order they were defined. Using graphql-js (and other servers), though, we can't make this assumption, because queries are resolved via promises, so that we can't know in what order will the fields be resolved.

To enforce the order of the fields, we must split them up into separate queries, and connect them via @export, like this (as it can be appreciated, the query became more verbose):

query ExtractEmails(
  $_newsletterList: [Object]! = []
) {
  newsletterEmailList: extract(object: $_newsletterList, path: "email") @export(as: "_newsletterEmailList")
}

query FindPosition(
  $email: String!,
  $_newsletterEmailList: [String]! = []
) {
  pos: arraySearch(array: $_newsletterEmailList, element: $email) @export(as: "_pos")
}

query ExtractEntry(
  $_newsletterList: [Object]! = [],
  $_pos: Int! = 0
) {
  userLangObj: extract(object: $_newsletterList, path: $_pos) @export(as: "_userLangObj")
}

query ExtractLanguage(
  $_userLangObj: Object! = {}
) {
  userLang: extract(object: $_userLangObj, path: "lang") @export(as: "_userLang")
}

Let's see how the query works. The first field, with alias newsletterEmailList, will extract the emails from the entries, and place them under the dynamic variable $_newsletterEmailList, which has type [String]:

{
  "data": {
    "newsletterEmailList": [
      "abracadabra@ganga.com",
      "longon@caramanon.com",
      "rancotanto@parabara.com",
      "quezarapadon@quebrulacha.net",
      "test@test.com",
      "emilanga@pedrola.com"
    ]
  }
}

The following field, with alias pos, will search for the position of the provided email in this array, and place the result under a dynamic variable $_pos, of type Int.

When providing this input as a static variable email:

{
  "email": "quezarapadon@quebrulacha.net"
}

...it will produce this response:

{
  "data": {
    "pos": 3
  }
}

The next field, with alias userLangObj, will extract the entry from the original newsletter list at that position, and place it under dynamic variable $_userLangObj. Its type is Object, which is a custom scalar representing a JSON object. The selected entry will be the one that contains both the email and language of the selected user:

{
  "data": {
    "userLangObj": {
      "email": "quezarapadon@quebrulacha.net",
      "lang": "fr"
    }
  }
}

Finally, the last field, with alias userLang, will extract the language from the entry, and place it under dynamic variable $_userLang of type String:

{
  "data": {
    "userLang": "fr"
  }
}

By continuously placing data on dynamic variables, and operating on them, we have been able (all within one query) to get the user's language: it's French, ooh la la!

Translating the blog post's content

We are almost there. Having placed the language under the dynamic variable $_userLang, we can reference it within the @translate directive:

query TranslatePostContent($postID: ID!, $_userLang: String! = "") {
  post: post(id: $postID) {
    titleEN: title
  	titleTranslated: title @translate(from: "en", to: $_userLang)
    contentEN: content
  	contentTranslated: content @translate(from: "en", to: $_userLang)
  }
}

We must also provide the blog post's ID as a static variable:

{
  "postID": 1
}

Querying it, will produce the desired outcome:

{
  "data": {
    "post": {
      "titleEN": "Hello world!",
      "titleTranslated": "Bonjour le monde!",
      "contentEN": "\n<p>Welcome to WordPress. This is your first post. Edit or delete it, then start writing!</p>\n\n\n\n<p>I&#8217;m demonstrating a Youtube video:</p>\n\n\n\n<figure class=\"wp-block-embed is-type-video is-provider-youtube wp-block-embed-youtube wp-embed-aspect-16-9 wp-has-aspect-ratio\"><div class=\"wp-block-embed__wrapper\">\n<iframe loading=\"lazy\" title=\"Introduction to the Component-based API by Leonardo Losoviz | JSConf.Asia 2019\" width=\"750\" height=\"422\" src=\"https://www.youtube.com/embed/9pT-q0SSYow?feature=oembed\" frameborder=\"0\" allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture\" allowfullscreen></iframe>\n</div><figcaption>This is my presentation in JSConf Asia 2019</figcaption></figure>\n",
      "contentTranslated": "\n<p>Bienvenue sur WordPress. Ceci est votre premier commentaire. Modifiez-le ou supprimez-le, puis commencez à écrire !</p>\n\n\n\n<p>Je fais la démonstration d'une vidéo Youtube :</p>\n\n\n\n<figure class=\"wp-block-embed is-type-video is-provider-youtube wp-block-embed-youtube wp-embed-aspect-16-9 wp-has-aspect-ratio\"><div class=\" wp-block-embed__wrapper\">\n<iframe loading=\"lazy\" title=\"Introduction à l'API basée sur les composants par Leonardo Losoviz | JSConf.Asia 2019\" width=\"750\" height=\"422\" src=\"https://www.youtube.com/embed /9pT-q0SSYow?feature=oembed\" frameborder=\"0\" allow=\"accelerometer; autoplay; clipboard-write; crypté-media; gyroscope; picture-in-picture\" allowfullscreen></iframe>\n</div><figcaption>Voici ma présentation à JSConf Asia 2019</figcaption></figure>\n"
    }
  }
}

The complete query

This is the complete query with the solution:

query FetchNewsletterList {
  newsletterList: getJSON(url: "https://newapi.getpop.org/wp-json/newsletter/v1/subscriptions") @export(as: "_newsletterList")
}

query OperateToFindUserLanguage(
  $email: String!,
  $_newsletterList: [Object]! = [],
  $_newsletterEmailList: [String]! = [],
  $_pos: Int! = 0,
  $_userLangObj: [String]! = []
) {
  newsletterEmailList: extract(object: $_newsletterList, path: "email") @export(as: "_newsletterEmailList")
  pos: arraySearch(array: $_newsletterEmailList, element: $email) @export(as: "_pos")
  userLangObj: extract(object: $_newsletterList, path: $_pos) @export(as: "_userLangObj")
  userLang: extract(object: $_userLangObj, path: "lang") @export(as: "_userLang")
}

query TranslatePostContent($postID: ID!, $_userLang: String! = "") {
  post: post(id: $postID) {
    titleEN: title
  	titleTranslated: title @translate(from: "en", to: $_userLang)
    contentEN: content
  	contentTranslated: content @translate(from: "en", to: $_userLang)
  }
}

# This is a hack to make GraphiQL execute several queries in a single request.
# Select operation "__ALL" from the dropdown when pressing on the "Run" button
query __ALL { id }

Conclusion

Dynamic variables are not part of the GraphQL spec. They have been requested, and the issue is in "Strawman (RFC 0)" stage, but it seems to have stagnated, having no inputs in over 1 year. I find this proposal to hold a great potential, so I hope it will eventually be approved for the spec.

As I've attempted to show in this article, dynamic variables allow us to execute a bit of logic in the query, as to avoid having to do it in the client and, as a consequence, reduce the latency from executing several queries.