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

Linking Multiple GraphQL Types

Connect multiple GraphQL types using the @materializer directive

@materializer is a custom StepZen directive for combining data returned from two types into one. It's useful for when you're making a call to two separate endpoints, separate backends, or assembling multiple GraphQL types from a nested JSON response.

This topic provides the following information about @materializer:

Configuration Properties

The available configuration properties are:

query

This value is required. query specifies the name of the query within the GraphQL schema that will be used to populate the data within your type.

The following example shows the GraphQL for a type Location with a field weather that will be populated with information from a (materialzing) query called weatherReport with the signature weatherReport(latitude: Float!, longitude: Float!): WeatherReport:

type Location {
  city: String
  country: String!
  ip: String!
  latitude: Float!
  longitude: Float!
  weather: WeatherReport @materializer(query: "weatherReport")
}

As expected, @materializer will naturally match fields to the arguments of the materializing query. So in this case, latitude and longtitude will be automatically be taken from Location. This default behavior makes using materializing queries straightforward for the most common usages.

Note: The type for the field (i.e., weather) needs to match the return type of the query (i.e., weatherReport). Note: The notion of materializing query is used here as a short hand.

  • If the materializing query (weatherReport in this case) returns an array, the type of the weather property can be set to [WeatherReport]
  • If the type of the property is a single type instance but the result of the query is an array, then only the first result will be returned.

What about the cases where the natural match doesn't work? For that, we have an arguments qualifier for @materializer

arguments

This value is optional and is used where the field names of the type do not exactly match the arguments. For example, the author query below expects an id as its argument (e.g. author(id: ID!): Author), but the author ID within the Book field is authorID. You can use arguments to map the queries argument name (i.e., id) to the field name on the type (i.e., authorID):

type Book {
  id: ID!
  name: String!
  originalPublishingDate: Date!
  authorID: ID!
  author: Author
  @materializer(
    query: "author"
    arguments: [{ name: "id" field: "authorID"}]
  )
}

Note: GraphQL allows you to write arguments: { name: "id" field: "authorID"}

There is a less common usage where you have:

type Book {
  id: ID!
  name: String!
  originalPublishingDate: Date!
  authorID: ID!
  cover(coverSize: Int): Image
  @materializer(
    query: "bookCover"
    arguments: [
     { name: "size" argument: "coverSize"},
     { name: "bookID" field: "id"}
    ]
  )
}

The bookCover signature is bookCover(bookID: ID!, size: Int):Image and argument inside of an arguments maps the field argument coverSize to the query argument size.

Note: argument is not mapped by default, so if it were cover(coverSize: Int): Image above, then you must add { name: "size" argument: "size"}

Note: As always, we loosely use the term query to refer to fields in root type Query.

  • If a query argument cannot be mapped using the default method or arguments, it will be null. For non-nullable values, this will result in an error.

Handling Nested JSON Results

The results from REST APIs are often returned as nested objects. You can use setters to map these to a flat result, but in many cases you may want to map the nested objects to separate GraphQL types. This can be done via @materializer.

See our docs for @rest for more details.

For example, a partial JSON result from the random user API may look like the following:

{
  "gender": "male",
  "name": {
      "first": "Brecht",
      "last": "Polfliet"
  },
  "location": {
      "city": "Ter Apelkanaal",
      "state": "Utrecht",
      "country": "Netherlands",
  },
  "email": "brecht.polfliet@example.com"
}

You can use setters to map the name fields to the main randomUser type, but you want to assign the results under location to a Location object. Use @materializer to fulfill the data, returning a Location type as follows:

type RandomUser {
  gender: String!
  firstName: String!
  lastName: String!
  email: String!
  location: Location @materializer(query: "userLocation")
}

type Location {
  city: String!
  state: String!
  country: String!
}

type Query {
  randomUser: RandomUser
    @rest(
      endpoint: "https://randomuser.me/api"
      resultroot: "results[]"
      setters: [
        { field: "firstName", path: "name.first" }
        { field: "lastName", path: "name.last" }
      ]
    )
  userLocation: Location
    @rest(
      endpoint: "https://randomuser.me/api"
      resultroot: "results[].location"
    )
}

Note: The root (i.e., resultroot) for location is the nested structure for results[].location.

But would this result in two different API calls? No. If the first and second URL match exactly, the second query will hit the cache and the second query will not be made. There are other factors that can affect this, but the goal is to offer the benefits of independence with the efficiency where possible.