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:
- 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 }
-
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 anopenweather_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.
- Create a new query that combines
location
andweatherReport
, 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:
- 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:
- Execute the query
location
. - 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:
- Create a new type
WeatherAndCity
inindex.graphql
:
type WeatherAndCity { temp: Float clouds: Int city: String }
Note: All code is placed into a single file for example purposes only.
- Change the return type of the
weather
query to beWeatherAndCity
, 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"} ] )
-
Create the
collect
query:collect ( clouds: Int temp: Float city: String ): WeatherAndCity @connector (type: "echo")
The arguments of the
collect
query (clouds
,temp
, andcity
) 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 connectorecho
. 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"} ] ) }
- 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:
- Change
WeatherAndCity
to return the fieldlocale
instead of the fieldcity
:
type WeatherAndCity { temp: Float clouds: Int locale: String }
- Change the
collect
query to acceptlocale
instead ofcity
. Since theecho
connector takes all of the arguments and builds it into the return type, you must ensure that its argument islocale
and notcity
:
collect ( locale: String clouds: Int temp: Float ): WeatherAndCity @connector (type: "echo")
- Pass in arguments to this step in the sequence, since the query
location
returnscity
:
{ "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 } } }