How to Create a Sequence of Queries

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

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

Let's create at an example of how this works. We'll use two APIs, IP API and OpenWeatherMap, to get the weather based on the latitude and longitude we obtained by using the IP address.

This requires executing a sequence of steps:

  1. Get the location given an IP address.
  2. Get weather from the latitude and longitude derived from the location.

Creating the Query

Let's start by writing the GraphQL code that will create the schema for our StepZen endpoint.

Create the file weather.graphql in your working directory that contains two types:

  1. Weather contains properties about the weather.
  2. Coord contains properties about the location (i.e. longitude and latitude).

We've also defined two queries.

  1. location gets a longitude and latitude using IPAPI based upon a provided IP address.
  2. weatherReport gets the current weather from OpenWeatherMap based upon a provided latitude and longitude.
type Weather {
  temp: Float!
  feelsLike: Float!
  description: String!
  units: String!
}
type Coord {
  latitude: Float!
  longitude: Float!
  city: String!
}
type Query {
  location(ip: String!): Coord
    @connector(
      type: "__ipapi_location_ip_connector__"
      configuration: "ipapi_default"
    )
  weatherReport(latitude: Float!, longitude: Float!): Weather
    @connector(
      type: "__openweathermap_weather_location_connector__"
      configuration: "owm_default"
    )
}

At this point, we have two types and two queries that each have a single purpose, but they aren't connected in any way to achieve the result we're looking for. In order to do that, we'll create a new query that is a combination of location and weatherReport executed in a sequence.

weather(ip: String!): Weather
    @sequence(
        steps: [
            { query: "location" }
            { query: "weatherReport" }
            ]
    )

This query is composed of an array of steps that will execute in order (i.e. location then weatherReport) to get our intended result. In this case, that's all that is needed because the location query returns the properties that weatherReport expects as arguments. StepZen recognizes this and automatially passes the values into the subsequent query.

To test this out, create an index.graphql with the following content to tell StepZen how to assemble our schema.

schema @sdl(
  files: [
    "weather.graphql"
  ]
) {
  query: Query
}

Deploy the schema using the stepzen start command and issue the following query in the StepZen Schema explorer:

{
  weather(ip: "72.188.196.163") {
    temp
    feelsLike
    description
  }
}

The response in the browser will be:

{
  "data": {
    "weather": {
      "description": "scattered clouds",
      "feelsLike": 31.14,
      "temp": 30.28
    }
  }
}

Let's see how this works in more detail.

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. So, in this fragment:

weather(ip: String!): Weather
  @sequence(
    steps: [
      { query: "location" }
      { query: "weatherReport" }
    ]
  )

@sequence tells StepZen to:

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

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

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

So the second query, weatherReport, requires latitude and longitude as its parameters. As you may recall, the location query returns a type of Coord that has latitude and longitude fields, so that is the query that provides the fields to weatherReport.

type Coord {
  latitude: Float!
  longitude: Float!
}

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

If any step in the sequence returns an array of results (ex. [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. Therefore, since the weatherReport query in our last step returns a type of Weather, our sequenced weather query must do the same. Later on in this tutorial, we'll see you how you can assemble the final response composed of more than just the results from the final step.

A good practice is to make sure that the types that each step in the sequence return contain only those fields. This prevents nullability errors. You will see an example in the next section.

Building Complex Sequences

While we can now get weather, what if we also wanted the city, so that we might greet our visitors: "Hello John, it is 63F in San Jose!". We would use the following query:

{
  weatherAndCity(ip: "72.188.196.163") {
    temp
    feelsLike
    description
    city
  }
}

However, the Coord type had the city value, not the Weather type that is returned by the weatherReport query. How would change our code to get that?

First, let's create a new type WeatherAndCity in our weather.graphql file.

Note: We are putting all the code into a single file for example purposes only.

type WeatherAndCity {
  temp: Float!
  feelsLike: Float!
  description: String!
  units: String!
  city: String!
}

Next, change the return type of the weather query to be WeatherAndCity. Then add a third step to the sequence using a new query (collect) that we'll define in a moment.

weather(ip: String!): WeatherAndCity
  @sequence(
    steps: [
      { query: "location" }
      { query: "weatherReport" }
      { query: "collect"}
    ]
  )

Let's create the collect query:

collect (
  temp: Float!,
  feelsLike: Float!,
  description: String!,
  units: String!,
  city: String!
): WeatherAndCity
  @connector (type: "echo")

The parameters of the collect query (temp, feelsLike, description, units, and city) are picked up from each prior step in the sequence. The first four parameters are picked up from the immediately preceding step (weatherReport), and the last parameter is picked up from the first step (location).

All of the parameters are corralled into our new WeatherAndCity type using the special StepZen connector echo. This connector effectively echos back the results from the prior steps.

Your overall code should look like:

type Weather {
  temp: Float!
  feelsLike: Float!
  description: String!
  units: String!
}
type Coord {
  latitude: Float!
  longitude: Float!
  city: String!
}
type WeatherAndCity {
  temp: Float!
  feelsLike: Float!
  description: String!
  units: String!
  city: String!
}
type Query {
  location(ip: String!): Coord
    @connector(
      type: "__ipapi_location_ip_connector__"
      configuration: "ipapi_default"
    )
  weatherReport(latitude: Float!, longitude: Float!): Weather
    @connector(
      type: "__openweathermap_weather_location_connector__"
      configuration: "owm_default"
    )
  collect(
    temp: Float!
    feelsLike: Float!
    description: String!
    units: String!
    city: String!
  ): WeatherAndCity @connector(type: "echo")
  weather(ip: String!): WeatherAndCity
    @sequence(
      steps: [
        { query: "location" }
        { query: "weatherReport" }
        { query: "collect" }
      ]
    )
}

And now issue the query:

{
  weather(ip: "72.188.196.163") {
    temp
    feelsLike
    description
    city
  }
}

You should get a response like:

{
  "data": {
    "weather": {
      "city": "Orlando",
      "description": "overcast clouds",
      "feelsLike": 33.02,
      "temp": 30.1
    }
  }
}

Rename outputs

There is one more piece of the puzzle that we should mention here. Sometimes the output of a previous step might not have the fields named correctly for a subsequent step.

So for example, if you wanted to change the output to be locale as opposed to city, we start by changing WeatherAndCity to return the field locale instead of the field city.

type WeatherAndCity {
  temp: Float!
  feelsLike: Float!
  description: String!
  units: String!
  locale: String!
}

We also need to change the collect query to accept locale instead of city, since the echo connector basically takes all the parameters and builds it into the return type, we need to make sure that its parameter is locale and not city.

collect (
  temp: Float!,
  feelsLike: Float!,
  description: String!,
  units: String!,
  locale: String!
): WeatherAndCity
  @connector (type: "echo")

The query location returns city, so we'll pass in arguments to this step in the sequence:

{
  "query": "collect",
  "arguments": [
    {
      "name": "locale",
      "field": "city"
    }
  ]
}

The arguments lists values we'd like 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.

Here's the complete code to accomplish this:

type WeatherReport {
  temp: Float!
  feelsLike: Float!
  description: String!
  units: String!
}
type Coord {
  latitude: Float!
  longitude: Float!
  city: String!
}
type WeatherAndCity {
  temp: Float!
  feelsLike: Float!
  description: String!
  units: String!
  locale: String!
}
type Query {
  weather(ip: String!): WeatherAndCity
    @sequence(
      steps: [
        { query: "location" }
        { query: "weatherReport" }
        { query: "collect", arguments: [{ name: "locale", field: "city" }] }
      ]
    )
  location(ip: String!): Coord
    @connector(
      type: "__ipapi_location_ip_connector__"
      configuration: "ipapi_default"
    )
  weatherReport(latitude: Float!, longitude: Float!): WeatherReport
    @connector(
      type: "__openweathermap_weather_location_connector__"
      configuration: "owm_default"
    )
  collect(
    temp: Float!
    feelsLike: Float!
    description: String!
    units: String!
    locale: String!
  ): WeatherAndCity @connector(type: "echo")
}

Testing

A good way to test the sequence is to test individual bits, executing each query one by one. For example, in the StepZen API Explorer that we spin up for you as part of stepzen start, you can do it using the "Explorer" tab. Once each step does the right thing, the whole sequence will.

For example, say we have this sequence:

weather(ip: String!): WeatherAndCity
  @sequence(
    steps: [
      { query: "location" }
      { query: "weatherReport" }
      { query: "collect", arguments: [{ name: "locale", field: "city" }]}
    ]
)

and this query:

query MyQuery {
  weather(ip: "72.188.196.163") {
    description
    feelsLike
    locale
    temp
    units
  }
}

and we're getting this error back:

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

We can take each step of the query one at a time to see where it's being thrown.

First, let's see location:

query MyQuery {
  location(ip: "72.188.196.163") {
    city
    latitude
    longitude
  }
}

It's working:

{
  "data": {
    "location": {
      "city": "Orlando",
      "latitude": 28.53,
      "longitude": -81.4057
    }
  }
}

So we know the problem's not there. How about weatherReport?

query MyQuery {
  weatherReport(latitude: 1.5, longitude: 1.5) {
    description
    feelsLike
    temp
    units
  }
}
{
  "data": {
    "weatherReport": {
      "description": "moderate rain",
      "feelsLike": 26.62,
      "temp": 26.62,
      "units": "degree Celsius"
    }
  }
}

That one seems good, too!

How about collect?

query MyQuery {
  collect(description: "", feelsLike: 1.5, locale: "", temp: 1.5, units: "") {
    description
    feelsLike
    locale
    temp
    units
  }
}

Aha!

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

Let's take a look at collect in our weather.graphql file:

collect (
  temp: Float!,
  feelsLike: Float!,
  description: String!,
  units: String!,
  locale: String!
): WeatherAndCity
  @connector (type: "echo")

We can see that the @connector type is missing an 'o' on the end. Add it, and our original query returns the correct data!

    "weather": {
      "description": "broken clouds",
      "feelsLike": 28.95,
      "locale": "Council Bluffs",
      "temp": 27.27,
      "units": "degree Celsius"
    }
  }
}

This site uses cookies: By using this website, you consent to our use of cookies in accordance with our Website Terms of Use and Cookie Policy.