In Leonardo Losoviz's article, Manipulating Data in GraphQL Part 1 - Dynamic Variables, he showed a solution for getting data from one query and then passing that data as a variable to a subsequent query. The solution used a couple of non-standard GraphQL directives and some very creative schema code to work around some of the limitations that currently exist in GraphQL.

In this post, I want to explore an alternative for accomplishing the same task but with far less code and complexity. In doing so, we're going to take advantage of some new (and in a couple cases, undocumented) features of StepZen. As you might expect with undocumented features, there are a few rough edges that we are working to improve, but I think it also still illustrates the potential that StepZen offers and how it can make some complicated tasks easy.

The example

I'm going to borrow from Leo's example, other than to switch the source of the blog posts from Wordpress to MySQL (it's worth noting, however, that StepZen can connect to Wordpress as well using our REST connector).

The goal of the schema is to do all of the following in a single client-server interaction – in a single query:

  • Get the language/locale preference for user from a list of email subscribers to a newsletter.
  • Get the latest post from the blog.
  • Use the locale preference to translate the latest post into the user's preferred language and return that as the result of the query.

Normally, this sort of task would require quite a bit of server-side code to manage passing data from one API call to the next. Leo showed how this could be accomplished in a single query request using some unofficial GraphQL directives and some advanced GraphQL techniques and a good amount of schema code. In my example, we'll do the same using some of StepZen's directives and a fairly trivial amount of schema code.

Retrieving the newsletter list

In Leo's example, he used getJSON directive in combination with an export directive to get data from a REST API and export a variable that can be used in subsequent requests from that response. Neither of these is a standardized GraphQL feature, though some GraphQL server software supports them.

StepZen supports an @rest directive that functions similarly to getJSON but has many more options. Also, we don't need the @export directive as we'll ultimately use StepZen's @sequence directive to accomplish the same result of passing data from one GraphQL query to the next in a sequence.

Thus, Leo's example was:

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

The StepZen equivalent would be:

newsletterList: [Subscriber]
    @rest(
      endpoint: "https://newapi.getpop.org/wp-json/newsletter/v1/subscriptions"
    )

Iterating the list of entries

So far, the code looks very similar – swapping @getJSON for @rest – but here's where things start to get exciting. Leo walks through a creative but also somewhat complex solution involving multiple @exports with extract configurations to iterate through the list of emails and get the single record that he was looking for. His code looked like this:

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

Note: There is a slightly shorter version of this schema code in his post as well that works on some GraphQL servers that don’t execute each step asynchronously.

Alternatively, StepZen's @rest directive supports a filter configuration that accomplishes the same task in a single line of configuration. Here's the StepZen alternative that will return the single record for the email we want:

newsletterList: [Subscriber]
@rest(
    endpoint: "https://newapi.getpop.org/wp-json/newsletter/v1/subscriptions"
    filter: "email==\"quezarapadon@quebrulacha.net\""
)

It's worth noting that the filter parameter is a new feature that was recently released and, as of yet, doesn't support passing a variable as the filter value, but that is a feature we currently have on our roadmap. This would allow us to ultimately pass the email as a parameter in our query and filter based upon the passed value.

The remainder of Leo's code to extract the user's language from the result isn't necessary when using StepZen, as we'll see in the next step.

Translating the blog post's content

Ultimately, Leo uses a custom @translate directive that uses the Google Translate API to translate the text of a blog post to a supplied language. Let's recreate that and then put all the steps together.

Connecting MySQL

First, we need to get the content of a post. In our case, we'll use StepZen's @dquery directive to connect my GraphQL API to a MySQL database that contains my post's content. Here's how to configure that query.

getPostByID(id: ID!): Post
  @dbquery(type: "mysql", table: "blogposts", configuration: "mysql_config")

We can then query it:

{
	getPostByID(id:1) {
    title
    body
  }
}

It'll return my simple sample post content:

{
  "getPostByID": {
    "body": "This is my very first post. What do you think?",
    "title": "Hello World!"
  }
}

Connect the Google Translate API

We'll use the @rest directive again to connect to the Google Translate API (note, you'll need an API key to connect to this and place that in your config.yaml file). This will use an undocumented feature of StepZen that can pass data to a REST API via a POST request body. In this case, we are passing the text and target values in the POST body (yes, the syntax isn't pretty at the moment – this is also on our roadmap to improve).

translatedText(text: String!, target: String!): TranslatedText
@rest(
    endpoint: "https://translation.googleapis.com/language/translate/v2?key=$key"
    postbody: """
    {
        "q": "{{ .Get "text" }}",
        "target": "{{ .Get "target" }}"
    }
    """
    configuration: "GoogleTranslate"
    method: POST
    resultroot: "data.translations[]"
)

We can now call this query and get back the language that Google thinks the original text is written in and the translated version.

{
  translatedText(text: "Hello World!", target: "fr") {
    detectedSourceLanguage
    translatedText
  }
}

And we'll get:

{
  "data": {
    "translatedText": {
      "detectedSourceLanguage": "en",
      "translatedText": "Bonjour le monde!"
    }
  }
}

Putting the pieces together

Now we have all the pieces we need:

  • newsletterList gets the language preference of the user using @rest with the filter parameter to get the single record we want.
  • getPostByID gets the post contents that we want to translate.
  • translatedText will translate any text that we send it into whatever language we specify.

Let's put these pieces together using StepZen's @sequence directive to sequence all these together into a single request. Before I can do that I need to add one more query. The purpose of this query is to collect all of the data from each of these steps into a single response.

Keep in mind getPostByID returns a Post object and translatedText returns a TranslatedText object. What we want to get back is a type that combines these two into a single type that has the original text and the translated text (a type named TranslatedPost). To do this, we'll use a special echo connector in StepZen that can echo the data from prior steps in a sequence to allow us to collect it (i.e. combine the data from the prior requests into a combined type).

collect(
id: ID!
title: String!
body: String!
published: String!
translatedText: String!
): TranslatedPost @connector(type: "echo")

Now we can sequence the pieces together, using that collect query as the last step. We'll need to pass the body field from getPostByID and the lang field from newsletterList to translatedText as the text and target parameters respectiively. We do that using the arguments property.

getTranslatedPostByID(id: ID!): TranslatedPost
@sequence(
    steps: [
    { query: "getPostByID" }
    { query: "newsletterList" }
    {
        query: "translatedText"
        arguments: [
        { name: "text", field: "body" }
        { name: "target", field: "lang" }
        ]
    }
    { query: "collect" }
    ]
)

The complete query

Ok, let's compare the final results. Leo's final schema code for his query looked like this:

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 }

For comparison's sake, I'll note that this version used the shorter option of his OperateToFindUserLanguage query that will only work on servers that aren't executing each step asynchronously. It didn't need to implement the code to get a post since that was built into the WordPress GraphQL implementation it was added to. It also doesn't include the implementation of the Google Translate API as that is built into the server in his case, but that you'd still have to implement yourself somehow.

Here's the StepZen alternative.

type Query {
  newsletterList: [Subscriber]
    @rest(
      endpoint: "https://newapi.getpop.org/wp-json/newsletter/v1/subscriptions"
      filter: "email==\"quezarapadon@quebrulacha.net\""
    )
  translatedText(text: String!, target: String!): TranslatedText
    @rest(
      endpoint: "https://translation.googleapis.com/language/translate/v2?key=$key"
      postbody: """
      {
          "q": "{{ .Get "text" }}",
          "target": "{{ .Get "target" }}"
      }
      """
      configuration: "GoogleTranslate"
      method: POST
      resultroot: "data.translations[]"
    )
  getPostByID(id: ID!): Post
    @dbquery(type: "mysql", table: "blogposts", configuration: "mysql_config")
  collect(
    id: ID!
    title: String!
    body: String!
    published: String!
    translatedText: String!
  ): TranslatedPost @connector(type: "echo")
  getTranslatedPostByID(id: ID!): TranslatedPost
    @sequence(
      steps: [
        { query: "getPostByID" }
        { query: "newsletterList" }
        {
          query: "translatedText"
          arguments: [
            { name: "text", field: "body" }
            { name: "target", field: "lang" }
          ]
        }
        { query: "collect" }
      ]
    )
}

With comments removed and using the shorter alternative, Leo's version is 27 lines of code. The equivalent StepZen alternative is 46 lines, but, to make a fair comparison, if you remove the Google Translate API implementation and the get post implementation, we're down to 29 lines. So, still slightly longer but not sunstantively different.

Conclusion

Where I see the strength of the StepZen alternative is not so much in the code length but in the complexity of the code.

  • There's no need to implement a @getJSON, @export or @translate directive on your server. You can use StepZen's built-in @rest and @sequence directives to accomplish the same task. This is a critical benefit as you don't need to build or maintain your own implementations of these – these features and more all come "out of the box" with StepZen.
  • The pieces are self-contained and reusable. There's no need for a task like OperateToFindUserLanguage that isn't usable independent of the process.
  • In my opinion, the code is easier to read and comprehend. There are no complicated processes to decipher. The @sequence is easy to understand. As we improve feautres and syntax like the filter and the postbody parameters for @rest, this will only improve.

To be clear, this is by no means mean to be a criticism of Leo's post. I loved reading about his inventive and creative solution to features that he needs but GraphQL doesn't yet support. But, it struck me as I read it how many of these problems StepZen already helps you solve today! So, why not give it a shot? It's free to try.