StepZen is now part of IBM. For the most recent product information and updates go to
https://www.ibm.com/products/stepzen

How to Create a Sequence of Queries

Sequence a series of queries into a single result using @sequence

StepZen's custom @sequence directive executes multiple queries in a sequence, one after the other. This enables you to create complex queries without manually orchestrating API calls or writing lots of logic in server-side code.

This tutorial shows an example where IP API and OpenWeatherMap are used to get the weather based on the location (latitude and longitude) corresponding to an IP address.

The following subsections describe how to build, work with, and test sequences of queries:

Create the Query

Follow the steps below to implement the GraphQL code that creates the schema for your StepZen endpoint:

  1. Create a file index.graphql in your working directory that contains the following types:
type Openweather_Weather {
  description: String
  icon: String
  id: Int
  main: String
}

type Openweather_Current {
  clouds: Int
  dew_point: Float
  dt: Int
  feels_like: Float
  humidity: Int
  pressure: Int
  sunrise: Int
  sunset: Int
  temp: Float
  uvi: Float
  visibility: Int
  weather: [Openweather_Weather]
  wind_deg: Int
  wind_speed: Float
}

type Openweather_WeatherForecast {
  lat: Float
  lon: Float
  clouds: Int
  temp: Float
}

type IpApi_Location {
  country: String
  city: String
  zip: String
  lat: Float!
  lon: Float!
  ip: String
}
  1. Define two queries inside index.graphql:

    • location: Gets a longitude and latitude using IPAPI based on an IP address.
    • weatherReport: Gets the current weather from OpenWeatherMap based on a latitude and longitude, as well as an openweather_appid argument. You can get a key by opening an OpenWeather account.
    type Query {

      location(ip: String!, lang: String! = "en"): IpApi_Location
            @rest(
                endpoint: "http://ip-api.com/json/$ip?fields=64745471&lang=$lang"
                setters: [{ field: "ip", path: "query" }]
                )

      weatherReport(openweather_appid: Secret!
        lang: String! = "en"
        lat: Float!
        lon: Float!):  Openweather_WeatherForecast
           @rest(
                endpoint: "https://api.openweathermap.org/data/2.5/onecall?appid=$openweather_appid&lang=$lang&lat=$lat&lon=$lon&exclude=minutely%2Chourly"
                setters: [{ field: "clouds", path: "current.clouds" },{ field: "temp", path: "current.temp" } ]
                )
    }

You can see the custom GraphQL directive @rest being used to specify the endpoints, as well as the JSON paths with setters. You can read more about the @rest connector in the Connecting Backends portion of our docs. There's documentation available on setters as well.

At this point, you have two types and two queries that each have a single purpose, but they aren't connected in any way to achieve the desired result. This will be resolved in the next step.

  1. Create a new query that combines location and weatherReport, executed in a sequence:
    weather(ip: String! openweather_appid: Secret! lang: String! = "en"): Openweather_WeatherForecast
        @sequence(
            steps: [
                { query: "location"}
                { query: "weatherReport", arguments: [{name: "openweather_appid", argument: "openweather_appid"}] }
                ]
                
    )

This query is composed of an array of steps that will execute in order (i.e., location then weatherReport) to get the intended result. StepZen automatically passes the values into the subsequent query. In this case, we add openweather_appid to authenticate the Open Weather query.

Follow the steps below to test this out:

  1. Deploy the schema using the stepzen start command and issue the following query in the explorer on the StepZen dashboard:
query MyQuery {
  weather(
    ip: "72.188.196.163"
    openweather_appid: "b4548ecd778518b766619e797744de85"
  ) {
      clouds
      temp
    }
}

The response in the browser will look similar to the following:

{
  "data": {
    "weather": {
      "clouds": 40,
      "temp": 303.5
    }
  }
}

If the temperature seems high, note that the OpenWeather API provides the temperatures in Kelvin units as a default.

Let's see how this works in more detail.

Learn how @sequence Works

A sequence is a set of steps, and in StepZen's @sequence directive, each step is either a query or a mutation. In the following example:

    weather(ip: String! openweather_appid: Secret!): Openweather_WeatherForecast
        @sequence(
            steps: [
                { query: "location" }
                { query: "weatherReport", arguments: [{name: "openweather_appid", argument: "openweather_appid"}] }
                ]
        )

@sequence tells StepZen to:

  1. Execute the query location.
  2. Execute the query weatherReport.

Each query in the sequence takes some arguments and returns a type. For example, location (ip: String!): IpApi_Location takes ip and returns type IpApi_Location. For the first query in the sequence, the input arguments come from the input arguments of the overall query. For example, weather(ip: String!), provides the argument to the location query as well.

For the following steps, the arguments can come from either the overall query or from any of the previous steps. If there is a naming conflict, then StepZen picks the step that is closest to the current one being executed.

The second query, weatherReport, requires lat and lon as its arguments. As you may recall, the location query returns a type of IpApi_Location that has lat and lon fields, so that is the query that provides the fields to weatherReport.

type IpApi_Location {
  country: String
  city: String
  zip: String
  lat: Float
  lon: Float
  ip: String
}

StepZen automatically populates the query arguments in weatherReport with the values returned by location.

If any step in the sequence returns an array of results (e.g. [Coord]), then the next step is called once for every entry in that list. You can think of it like executing a for loop on the array of results.

The final response returned by the query is the output of the last step. Since the weatherReport query in the last step returns a type of Weather, the sequenced weather query must do the same. Later on in this tutorial, you'll assemble the final response composed of more than just the results from the final step.

Build Complex Sequences

While you can now get weather, what if you also wanted the city, so as to perhaps greet our visitors like: "Hello John, it is 63F in San Jose!". For this, you must support a query like:

{
  weatherAndCity(ip: "72.188.196.163") {
    temp
    clouds
    city
  }
}

However, the IpApi_Location type has the city value, not the Weather type that is returned by the weatherReport query.

Follow the steps below to fix that:

  1. Create a new type WeatherAndCity in index.graphql:
type WeatherAndCity {
  temp: Float
  clouds: Int
  city: String
}

Note: All code is placed into a single file for example purposes only.

  1. Change the return type of the weather query to be WeatherAndCity, rename the query to match, and add a third step to the sequence using a new query, collect, that you'll define in the next step:
  weatherAndCity(ip: String! openweather_appid: Secret! lang: String! = "en"): WeatherAndCity
      @sequence(
          steps: [
              { query: "location"}
              { query: "weatherReport", arguments: [{name: "openweather_appid", argument: "openweather_appid"}] }
               { query: "collect"}
              ]
              
  )
  1. Create the collect query:

    collect (
     clouds: Int
     temp: Float
     city: String
    ): WeatherAndCity
      @connector (type: "echo")

    The arguments of the collect query (clouds, temp, and city) are picked up from each prior step in the sequence. The first two arguments are picked up from the preceding step (weatherReport), and the last argument is picked up from the first step (location).

    All of the arguments are corralled into our new WeatherAndCity type using the special StepZen connector echo. This connector effectively echoes back whatever arguments are passed into it.

    Your overall code should look as follows:

type Openweather_Weather {
  description: String
  icon: String
  id: Int
  main: String
}

type Openweather_Current {
  clouds: Int
  dew_point: Float
  dt: Int
  feels_like: Float
  humidity: Int
  pressure: Int
  sunrise: Int
  sunset: Int
  temp: Float
  uvi: Float
  visibility: Int
  weather: [Openweather_Weather]
  wind_deg: Int
  wind_speed: Float
}

type Openweather_WeatherForecast {
  lat: Float
  lon: Float
  clouds: Int
  temp: Float
}

type IpApi_Location {
  country: String
  city: String
  zip: String
  lat: Float!
  lon: Float!
  ip: String
}

type WeatherAndCity {
  temp: Float
  clouds: Int
  city: String
}

type Query {

      location(ip: String!, lang: String! = "en"): IpApi_Location
            @rest(
                endpoint: "http://ip-api.com/json/$ip?fields=64745471&lang=$lang"
                setters: [{ field: "ip", path: "query" }]
                )

      weatherReport(openweather_appid: Secret!
        lang: String! = "en"
        lat: Float!
        lon: Float!):  Openweather_WeatherForecast
           @rest(
                endpoint: "https://api.openweathermap.org/data/2.5/onecall?appid=$openweather_appid&lang=$lang&lat=$lat&lon=$lon&exclude=minutely%2Chourly"
                   setters: [{ field: "clouds", path: "current.clouds" },{ field: "temp", path: "current.temp" } ]
                )
             

        collect (
            city: String
            clouds: Int
            temp: Float
          ): WeatherAndCity
            @connector (type: "echo")


    weatherAndCity(ip: String! openweather_appid: Secret! lang: String! = "en"): WeatherAndCity
        @sequence(
            steps: [
                { query: "location"}
                { query: "weatherReport", arguments: [{name: "openweather_appid", argument: "openweather_appid"}] }
                 { query: "collect"}
                ]
                
    )
}
        
  1. Issue the query:
query MyQuery {
  weatherAndCity(
    ip: "72.188.196.163"
    openweather_appid: {{YOUR_ID_HERE}}
  ) {
    clouds
    city
    temp
  }
}

The response is similar to the following:

  {
  "data": {
    "weatherAndCity": {
      "clouds": 55,
      "city": "Orlando",
      "temp": 302.81
    }
  }
}

Rename Outputs

Sometimes, the output of a previous step might not have the fields named correctly for a subsequent step (e.g. if you want to change the output to be locale as opposed to city).

Follow the steps below to make this change:

  1. Change WeatherAndCity to return the field locale instead of the field city:
type WeatherAndCity {
  temp: Float
  clouds: Int
  locale: String
}
  1. Change the collect query to accept locale instead of city. Since the echo connector takes all of the arguments and builds it into the return type, you must ensure that its argument is locale and not city:
   collect (
      locale: String
      clouds: Int
      temp: Float
    ): WeatherAndCity
      @connector (type: "echo")
  1. Pass in arguments to this step in the sequence, since the query location returns city:
    {
      "query": "collect",
      "arguments": [
        {
          "name": "locale",
          "field": "city"
        }
      ]
    } 

arguments lists the values to map. In this case, it takes the value returned in the field city returned from the location query and converts it to the value for the field locale passed to the collect query. You'll need to reflect the change in the @sequence directive as well:

     { query: "collect",  arguments:  [{ name: "locale", field: "city" }] }
Here's the complete code to accomplish this:
type Openweather_Weather {
  description: String
  icon: String
  id: Int
  main: String
}

type Openweather_Current {
  clouds: Int
  dew_point: Float
  dt: Int
  feels_like: Float
  humidity: Int
  pressure: Int
  sunrise: Int
  sunset: Int
  temp: Float
  uvi: Float
  visibility: Int
  weather: [Openweather_Weather]
  wind_deg: Int
  wind_speed: Float
}

type Openweather_WeatherForecast {
  lat: Float
  lon: Float
  clouds: Int
  temp: Float
}

type IpApi_Location {
  country: String
  city: String
  zip: String
  lat: Float!
  lon: Float!
  ip: String
}

type WeatherAndCity {
  temp: Float
  clouds: Int
  city: String
}

type Query {

      location(ip: String!, lang: String! = "en"): IpApi_Location
            @rest(
                endpoint: "http://ip-api.com/json/$ip?fields=64745471&lang=$lang"
                setters: [{ field: "ip", path: "query" }]
                )

      weatherReport(openweather_appid: Secret!
        lang: String! = "en"
        lat: Float!
        lon: Float!):  Openweather_WeatherForecast
           @rest(
                endpoint: "https://api.openweathermap.org/data/2.5/onecall?appid=$openweather_appid&lang=$lang&lat=$lat&lon=$lon&exclude=minutely%2Chourly"
                setters: [{ field: "clouds", path: "current.clouds" },{ field: "temp", path: "current.temp" } ]
                )        

   collect (
      locale: String
     clouds: Int
      temp: Float
    ): WeatherAndCity
      @connector (type: "echo")


    weatherAndCity(ip: String! openweather_appid: Secret! lang: String! = "en"): WeatherAndCity
        @sequence(
            steps: [
                { query: "location"}
                { query: "weatherReport", arguments: [{name: "openweather_appid", argument: "openweather_appid"}] }
                 { query: "collect",  arguments:  [{ name: "locale", field: "city" }] }
                ]
                
    )
}

Test the Sequence

A good way to test the sequence is to test individual bits, executing each query one by one. For example, you can do it using the Explorer tab in the GraphiQL explorer made available at your localhost as part of stepzen start. Once each step performs correctly, the whole sequence will work.

For example, the following sequence:

    weatherAndCity(ip: String! openweather_appid: Secret! lang: String! = "en"): WeatherAndCity
        @sequence(
            steps: [
                { query: "location"}
                { query: "weatherReport", arguments: [{name: "openweather_appid", argument: "openweather_appid"}] }
                 { query: "collect", arguments: [{ name: "locale", field: "city" }] }
                ]
                
    )

And this query:

query MyQuery {
  weatherAndCity(
    ip: "72.188.196.163"
    openweather_appid: {{YOUR_ID_HERE}}
    lang: "en"
  ) {
    clouds
    locale
    temp
  }
}

Results in this error:

{
  "data": {
    "weatherAndCity": null
  },
  "errors": [
    {
      "message": "Factory for connector type ech does not exist",
      "locations": [
        {
          "line": 2,
          "column": 3
        }
      ],
      "path": [
        "weatherAndCity"
      ]
    }
  ]
}

You can take each step of the query, one at a time, to see where the error is being thrown.

First, test location:

query MyQuery {
  location(ip: "72.188.196.163", lang: "en") {
    city
    lat
    lon
  }
}

The response shows that it's working:

{
  "data": {
    "location": {
      "city": "Orlando",
      "lat": 28.5436,
      "lon": -81.3738
    }
  }
}

So, the problem is not there. How about weatherReport?

query MyQuery {
  weatherReport(lat: 1.5, lon: 1.5, openweather_appid: {{YOUR_ID_HERE}}) {
    temp
  }
}
{
  "data": {
    "weatherReport": {
      "temp": 298.45
    }
  }
}

That one seems good, too!

How about collect?

query MyQuery {
  collect(clouds: 10, locale: "Orlando", temp: 1.5) {
    locale
    clouds
    temp
  }
}

The response contains an error:

{
  "data": {
    "collect": null
  },
  "errors": [
    {
      "message": "Factory for connector type ech does not exist",
      "locations": [
        {
          "line": 3,
          "column": 3
        }
      ],
      "path": [
        "collect"
      ]
    }
  ]
}

Take a look at collect in your index.graphql file:

   collect (
     locale: String
     clouds: Int
     temp: Float
    ): WeatherAndCity
      @connector (type: "ech")

It appears that the @connector type is missing an 'o' on the end. Add it, save the change, and the original query returns the correct data!

{
  "data": {
    "weatherAndCity": {
      "clouds": 40,
      "locale": "Orlando",
      "temp": 302.85
    }
  }
}