@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: How to configure a query within the directive.
- Handling Nested JSON Results: How to map nested REST API results to GraphQL types.
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 theweather
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 werecover(coverSize: Int): Image
above, then you must add{ name: "size" argument: "size"}
Note: As always, we loosely use the term
query
to refer tofield
s in root typeQuery
.
- 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 forresults[].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.