StepZen's custom directive @sequence allows for multiple queries to be executed in a sequence, one after the other to return a single result. Each step in the sequence passes data to the next, allowing data from step 1 in a sequence to be used as arguments to step 2 in a sequence and so on.

This can allow you to create complex queries without having to manually orchestrate APIs, write lots of server-side logic, or handle asynchronous calls and database queries. In this example we will see how to use @sequence to query three APIs to get the sunrise based on the users's location (latitude and longitude) which we get using their IP address.

You can view the code for this example over on our StepZen Github collection.

The steps argument

The only required argument to create a sequence is the steps that make up that sequence. This is an array of objects that the query steps through. Each object in the array must contain at a minimum the name of the query that the step will call.

@sequence(
  steps: [
    { query: "step1" }
    { query: "step2" }
  ]
)

The value of query must be a query that is defined on the schema. The result of the sequence will be the same as the result of the last step, step2 in this case.

Collecting results

By default, the result of the final step is the result of the sequence. However, in some scenarios, you may need data from prior steps as part of the result of the entire sequence.

For example, imagine step1 returns user information including their name but step2 returns location information including city. If you wanted the full sequence to return both name and city, you can use an extra step in the sequence that calls a query utilizing a special echo connector.

@sequence(
  steps: 
    [
      { query: "step1" }
      { query: "step2" }
      { query: "collect" }
    ]
)

The collect query in step 3 of the sequence would then use the echo connector:

collect (
  name: String!,
  city: String!
): UserWithLocation
  @connector (
    type: "echo"
  )

Sunrise API Example

We'll use three APIs to get the sunrise based on the location (latitude and longitude) which we get using the IP address:

This requires executing a sequence of steps:

  1. Get the location given an IP address.
  2. Get sunrise 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 a file sunrise.graphql in your working directory that contains the following types:

  1. SunriseSunset contains properties about the sunrise and sunset
type SunriseSunset {
  sunrise: String!
  sunset: String!
  solar_noon: String!
  day_length: String!
  civil_twilight_begin: String!
  civil_twilight_end: String!
  nautical_twilight_begin: String!
  nautical_twilight_end: String!
  astronomical_twilight_begin: String!
  astronomical_twilight_end: String!
  sunrise_unix: Int
  sunset_unix: Int
  abbreviation: String
}
  1. Geolocation contains properties about the location (i.e. longitude and latitude)
type Geolocation {
  ip_address: String!
  city: String
  city_geoname_id: Int
  region: String
  region_iso_code: String
  region_geoname_id: Int
  postal_code: String
  country: String
  country_code: String
  country_geoname_id: Int
  country_is_eu: Boolean
  continent: String
  continent_code: String
  continent_geoname_id: Int
  longitude: Float
  latitude: Float
  abbreviation: String
}
  1. Timezone contains properties about the timezone
type Timezone {
  countryCode: String
  countryName: String
  zoneName: String!
  abbreviation: String
  gmtOffset: Int!
  dst: Int
  zoneStart: Int
  zoneEnd: Int
  nextAbbreviation: String
  timestamp: Int
  timestring: String
  formatted: Date
}

We've also defined four queries.

  1. getLocationByIpAddress gets a location using IP Geolocation API based upon a provided IP address
type Query {
  getLocationByIpAddress(
    ip_address: String!
  ): Geolocation
    @rest(
      endpoint: "https://ipgeolocation.abstractapi.com/v1/?api_key=$api_key&ip_address=$ip_address"
      configuration: "abstractapi_config"
    )
}
  1. getTimezoneByLatLong gets the timezone from TimeZoneDB based upon a provided latitude and longitude
type Query {
  getTimezoneByLatLong(
    latitude: Float!,
    longitude: Float!
  ): Timezone
    @rest(
      endpoint: "http://api.timezonedb.com/v2.1/get-time-zone?format=json&key=$api_key&by=position&lat=$latitude&lng=$longitude"
      configuration: "timezone_config"
    )
}
  1. getSunriseByLatLong gets the sunrise from the Sunrise Sunset API based upon a provided latitude and longitude
type Query {
  getSunriseByLatLong(latitude: Float!, longitude: Float!): SunriseSunsetFoo
    @rest(
      endpoint: "https://api.sunrise-sunset.org/json?lat=$latitude&lng=$longitude"
      resultroot: "results"
      cel: """
      function transformREST(json)
      { ... }
      """
    )
}

We are using a custom transformation that StepZen allows within the cel property. This is an undocumented beta capability of StepZen that allows you to write custom code to process the results of a query.

4. collectSunrise uses the echo connector to return the final result

type Query {
  collectSunrise(
    sunrise: String!
    sunset: String!
    solar_noon: String!
    day_length: String!
    civil_twilight_begin: String!
    civil_twilight_end: String!
    nautical_twilight_begin: String!
    nautical_twilight_end: String!
    astronomical_twilight_begin: String!
    astronomical_twilight_end: String!
    sunrise_unix: Int
    sunrise_local: String
    sunset_unix: Int
    sunset_local: String
    abbreviation: String
  ): SunriseSunset
    @connector(type: "echo")
}

getSunriseByIp sequence

Now if we look at our complete getSunriseByIp sequence we will see an array of steps that will execute in order to get our intended result.

getSunriseByIp(ip_address: String!): SunriseSunset
  @sequence(
    steps: [
      { query: "getLocationByIpAddress" }
      { query: "getTimezoneByLatLong" }
      { query: "getSunriseByLatLong" }
      { query: "collectSunrise" }
    ]
  )

index.graphl

To test this out, create an index.graphql with the following content to tell StepZen how to assemble our schema. The index.graphql file ties the schema files together so that StepZen can deploy them to one endpoint.

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

The entire project is consolidated down to one file to simplify the example, but in practice it is good to break up your files into specific concerns such as Sunrise, Geolocation, and Timezone.

Deploy the schema with stepzen start

Deploy the schema using the stepzen start command and issue the following query in the StepZen Schema explorer. You can find your own IP address by Googling, "get my IP address."

query MyQuery {
  getSunriseByIp(ip_address: "2601:643:8500:abc0:3d25:c4aa:6445:d586") {
    abbreviation
    sunrise
    sunset
  }
}

The response in the browser will be:

{
  "data": {
    "getSunriseByIp": {
      "abbreviation": "PDT",
      "sunrise": "6:04:56 AM",
      "sunset": "8:25:52 PM"
    }
  }
}

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:

getSunriseByIp(ip_address: String!): SunriseSunset
  @sequence(
    steps: [
      { query: "getLocationByIpAddress" }
      { query: "getTimezoneByLatLong" }
      { query: "getSunriseByLatLong" }
      { query: "collectSunrise" }
    ]
  )

@sequence tells StepZen to execute the following queries in order:

  1. getLocationByIpAddress
  2. getTimezoneByLatLong
  3. getSunriseByLatLong
  4. collectSunrise

Each query in the sequence takes some parameters and returns a type. For example, getLocationByIpAddress(ip_address: String!): Geolocation takes ip_address and returns type Geolocation. For the first query in the sequence, the input parameters come from the input parameters of the overall query.

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.

The second query, getTimezoneByLatLong, requires latitude and longitude as its parameters. The getLocationByIpAddress query returns a type of Geolocation that has latitude and longitude fields, so that is the query that provides the fields to getTimezoneByLatLong.

StepZen automatically populates the query parameters in getTimezoneByLatLong with the values returned by getLocationByIpAddress. If any step in the sequence returns an array of results 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 collectSunrise query in our last step returns a type of SunriseSunset, our sequenced getSunriseByIp query must do the same.

Conclusion

We've now seen how we can easily and quickly combine multiple queries together with StepZen's @sequence directive. If you want to explore more example repositories you can visit the StepZen GitHub.