StepZen Directives Reference
GraphQL directives are declarative constructs that simplify how you build GraphQL APIs, and assemble graphs of graphs.
The schema that defines your GraphQL APIs incorporates directives that allow you to assemble GraphQL declaratively in StepZen and control how your schemas are executed. This section describes StepZen directives and their application when building and running GraphQL APIs.
Next to the built-in directives, StepZen also provides a set of custom directives that allow you to connect to external data sources, such as REST APIs, databases, and other GraphQL APIs. Or to alter the data returned from an operation.
In GraphQL there are two types of directives: type system directives and executable directives. Type system directives are used to annotate types and fields in a GraphQL schema using SDL. Executable directives are used to control the execution of a GraphQL operation in a GraphQL document when requesting or mutating data.
Type System Directives
Type system directives are used to annotate types and fields in a GraphQL schema using SDL. The following type system directives are supported by StepZen:
@rest
@rest (endpoint: String!, configuration: String, resultroot: String, setters: [{field: String, path: String}], headers: [{name: String, value: String}])
@rest enables you to connect any REST API as a data source for a GraphQL schema. The directive can be applied to a GraphQL query, so the query's result is populated with data returned from REST API, often mapped through a series
of transformations to the query's type.
For REST backends, execution of the query results in an HTTP call (defaults to GET) built from the specified endpoint enhanced by values in the @rest arguments. The arguments for the @rest directive are:
endpoint(required)configuration(optional)method(optional)resultroot(optional)setters(optional)filter(optional)headers(optional)forwardheaders(optional)postbody(optional)cachepolicy(optional)transforms(optional)ecmascript(optional)
For examples of how to use the @rest directive arguments, see Connect a REST Service Using @rest
endpoint
This value is required. The endpoint argument specifies the URL for the HTTP request. Endpoint is a string value that can contain variables preceded by a $, which are replaced by StepZen. These variables can match
query arguments or variables in a corresponding configuration of the same name. For example, $username in the endpoint string will be replaced by the value of a username query argument.
By default, StepZen automatically appends all query arguments to the endpoint URL query string (except when using POST endpoints with a default postbody). All argument values
are automatically URL-encoded so that when querying your StepZen GraphQL endpoint you can safely pass any argument "as is".
For example, in the following schema the actual endpoint URL is https://httpbin.org/anything?number=$number&message=$message.
type Query {
echo(number: Int, message: String): EchoResponse
@rest(endpoint: "https://httpbin.org/anything")
}
If you want to disable this default and take full control over the endpoint URL, add a configuration to your @rest directive that sets the stepzen.queryextensionguard attribute to true (in config.yaml). For example, with in the following schema only the message argument is appended to the endpoint URL:
type Query {
echo(number: Int, message: String): EchoResponse
@rest(
endpoint: "https://httpbin.org/anything?message=$message"
configuration: "your_config_name"
)
}
configurationset:
- configuration:
name: "your_config_name"
stepzen.queryextensionguard: true
There is a special value for endpoint stepzen:empty that can be used in conjunction with the ecmascript argument. stepzen:empty configures the StepZen server to make no http
request, but still process the ecmascript argument, allowing you to create responses entirely from ecmascript.
configuration
This value is optional. The connection details to pass down into headers (e.g. authorization) - specifying to StepZen which configuration to use for this endpoint. StepZen configurations are stored in a config.yaml file and
are assigned names.
For example, a named configuration within config.yaml called github_config will be referenced by a configuration property of @rest as configuration: github_config. A configuration
can contain things like API keys needed to connect to a REST API or other configuration values that may be needed to construct the endpoint URL.
method
This value is optional. The default value for method is GET, other values can be DELETE, PATCH, POST, or PUT. If postbody is set on @rest, then the default changes to POST. The selection of method affects the configuration properties you can use.
type Mutation {
sendPostRequest(to: String!, from: String!, template_id: String!): Email
@rest(method: POST, endpoint: "https://api.sendgrid.com/v3/mail/send")
}
resultroot
This value is optional. In cases where the data to populate the GraphQL type is not in the root object of the result from a REST API, use resultroot to specify the path that StepZen is to use as the root.
Let's look at an example. This is the structure of a response from the Contentful Delivery API:
{
"fields": {
"title": {
"en-US": "Hello, World!"
},
"body": {
"en-US": "Bacon is healthy!"
}
},
"metadata": {
// ...
},
"sys": {
// ...
}
}
In this example, fields contains all the data to populate a type representing this content object. Therefore, resultroot is set to fields as shown here:
contentfulPost(id: ID!): Post
@rest(
endpoint: "https://cdn.contentful.com/spaces/$spaceid/entries/$id"
resultroot: "fields"
configuration: "contentful_config"
)
Important: The value of setters is mapped from the resultroot. In this example, this makes the data under metadata and sys inaccessible.
In some cases, the data to populate the GraphQL type is located inside of an array of items of the result from a REST API. Therefore, you must set the resultroot inside of an array of items:
{
"sys": { "type": "Array" },
"skip": 0,
"limit": 100,
"total": 1256,
"items": [
{
"fields": {
/* list of fields for this entry */
}
}
]
}
In the preceding example, Contentful returns all the entries and we need to set the value of the root to fields inside the array of items.
You can do this by adding an empty array notation in the resultroot as in the following example:
contentfulBlogs: [Blog]
@rest(
endpoint: "https://cdn.contentful.com/spaces/$spaceid/entries"
resultroot: "items[].fields"
configuration: "contentful_config"
)
setters
This value is optional. Sometimes the name or structure of the content returned by a REST API doesn't exactly match the GraphQL type that the query will populate. In these cases, you can use setters to map the values returned by a REST API result to the appropriate fields within the returned GraphQL type. (Only fields that need to be remapped need to be specified, otherwise StepZen makes good default assumptions.)
setters takes an array of objects each containing a field and path. The field is the property in the GraphQL type returned by the query that the value of field should be set to. The path is the path to the value in the endpoint's JSON result.
To illustrate this concept, let's look at the following example JSON response:
{
"id": 194541,
"title": "The Title",
"slug": "the-url-2kgk",
"published_at": "2019-10-24T13:52:17Z",
"user": {
"name": "Brian Rinaldi",
"username": "remotesynth"
}
}
If the corresponding Article GraphQL type has a field of published but not published_at, StepZen will not be able to automatically map the value returned by the REST API to the value in the GraphQL
type.
To resolve this, add a setter with the following values:
{ field: "published", path: "published_at" }
Setters are also useful for mapping values in nested objects, returned by a REST API.
In the example preceding, the value of user.name cannot be automatically mapped to a field in the GraphQL type. You have two options:
- Create another type for
Userthat has the corresponding fields and then use theresultrootproperty touser. - Flatten the values and add them to the
Articletype using the following setters:
{ field: "author", path: "user.name" }
{ field: "username", path: "user.username" }
Here's an example of a @rest directive in a file called article.graphql:
type Article {
id: ID!
title: String!
description: String
cover_image: String
username: String!
github_username: String!
}
type Query {
myArticles(username: String!): [Article]
@rest(
endpoint: "https://dev.to/api/articles?username=$username"
configuration: "dev_config"
setters: [
{ field: "username", path: "user.username" }
{ field: "github_username", path: "user.github_username" }
]
)
}
}
Let's pull out the @rest directive to take a closer look:
@rest(
endpoint: "https://dev.to/api/articles?username=$username"
configuration: "dev_config"
setters: [
{ field: "username", path: "user.username" }
{ field: "github_username", path: "user.github_username" }
]
)
endpoint, configuration, and setters are all arguments to the @rest directive. That is, they give StepZen the information it needs to connect to the REST API:
endpointsets the endpoint of the API.configurationreferences the configuration file by its name value.settersspecifies the names of the fields that will be used and their values from their paths.
Character that are not numbers, letters, or underscores
If the value for a path in setters contains any character that is not a number, letter, or underscore, you must wrap the value with back-ticks. For example:
The path value in: { field: "terminal", path: "terminal-name" }
must be wrapped in back ticks so the line reads as: { field: "terminal", path: "`terminal-name`" }
Note that the value accepts template literals, but the field will not, for example, { field: "`terminal`", path: "`terminal-name`" } will not work.
filter
This value is optional. The filter argument will filter results returned from the REST API based on a condition so that the query will only returned the filtered results. It accepts any boolean operator (e.g. ==,
!=, >. <, >=, <=).
For example, the following filter returns only the record for the email that matches the string:
newsletterList: [Subscriber]
@rest(
endpoint: "https://newapi.getpop.org/wp-json/newsletter/v1/subscriptions"
filter: "email==\"quezarapadon@quebrulacha.net\""
)
Note: Special characters, like the preceding quotes, need to be escaped with \. Using variables within a filter condition are not supported at this time.
headers
This value is optional. The headers argument specifies headers as name and $variable. Variables are passed into header fields from the configuration or arguments in the
query
@rest(
headers: [
{ name: "Authorization" value: "Bearer $bearerToken" }
]
In this example, the $bearerToken variable can come from a configuration or a query argument.
forwardheaders
This value is optional. This defines the list of headers forwarded from the incoming GraphQL HTTP request to the HTTP call made to endpoint. Nonexistent headers will forward as an empty header. Headers rendered via configuration entries take priority.
@rest(
forwardheaders: ["Authorization", "User-Agent"]
In this example, the HTTP call made to the endpoint will include the Authorization and User-Agent headers from the incoming GraphQL HTTP request.
postbody
This value is optional. You set postbody when you need customize the automatically generated body for a PATCH, POST, or PUT request. Let's look at some examples to help explain how this argument can be used.
StepZen automatically generates the request body using all query or mutation arguments. For example, consider a query defined as follows:
type Query {
formPost(firstName: String! lastName: String!): JSON
@rest(
method: POST
endpoint: "https://httpbin.org/post"
)
}
This query accepts two string values and returns a JSON scalar. The contenttype has not been set, so defaults to application/json. The postbody has not been set, so StepZen automatically generates a
simple JSON body using the query argument names and values:
{
"firstName":"value",
"lastName":"value"
}
If we change the contenttype to application/x-www-form-urlencoded, StepZen generates a form-encoded request body request body.
type Query {
formPost(firstName: String! lastName: String!): JSON
@rest(
method: POST
contenttype: "application/x-www-form-urlencoded"
endpoint: "https://httpbin.org/post"
)
}
The preceding schema code results in a body of:
firstName=value&lastName=value
If you need to change the field names in the automatically generated bodies, you can do that using the arguments attribute for the directive. In the following example we have changed the names from camelCase to snake_case:
type Query {
formPost(firstName: String! lastName: String!): JSON
@rest(
method: POST
endpoint: "https://httpbin.org/post"
arguments: [
{argument:"firstName", name:"first_name"},
{argument:"lastName", name:"last_name"}
]
)
}
This works for both application/json and x-www-form-urlencoded bodies.
We only need to use postbody when we need to customize the request body more than simply renaming fields. For example, if we need to add fields, or generate a more complex structure. When performing variable substitution in
the postbody, we need to use the Go language template syntax.
For example, consider a query that uses the Client Credentials flow to obtain an OAuth token. Since the grant_type argument is not included in the query definition, we need to add it to the request body.
type Query {
token(client_id: String! client_secret: String!): Token
@rest(
endpoint: "<oauth token endpoint"
method: POST
contenttype: "x-www-form-urlencoded"
postbody: """
grant_type=client_credentials&client_id={{ .Get "client_id" }}&client_secret={{ .Get "client_secret"}}
"""
)
}
Consider a JSON payload from our earlier example, but our JSON payload needs to be more complex:
{"user":{
"firstName": "first name"
"lastName": "last name"
}
}
type Query {
logUser(firstName: String! lastName: String!): JSON
@rest(
endpoint: "https://httbin.org/post"
postbody: """
{
"user": {
"firstName": "{{ .Get "firstName" }}",
"lastName": "{{ .Get "lastName" }}"
}
}
"""
)
}
As we are not automatically generating the body of the request, the GraphQL query arguments are automatically appended to the request URL as as parameters in a query string, e.g. https://httpbin.org/post?firstName=<value>&lastName=<value>.
We can tell StepZen not to append GraphQL query arguments as query string parameters by setting the stepzen.queryextensionguard attribute to true in the config.yaml file as follows:
configurationset:
- configuration:
name: "config"
stepzen.queryextensionguard: true
Our final query schema is:
type Query {
logUser(firstName: String! lastName: String!): JSON
@rest(
endpoint: "https://httbin.org/post"
postbody: """
{
"user": {
"firstName": "{{ .Get "firstName" }}",
"lastName": "{{ .Get "lastName" }}"
}
}
"""
configuration: "config"
)
}
PATCH with autopostbody works for the simplest form of patch using nullable arguments.
type Mutation {
updateUser_Optionals(first_name: String last_name: String): JSON
@rest(
endpoint: "https://httpbin.org/patch"
method: PATCH
)
}
Query:
mutation stepzenPatch {
updateUser_Optionals(first_name: "John", last_name: "Doe")
}
To pass JSON as an argument in the GraphQL Mutation, the argument JSONPatch must be added as a variable to the postbody argument:
type Mutation {
updateUser_PatchString(JSONPatch: String): JSON
@rest(
endpoint: "https://httpbin.org/patch"
method: PATCH
postbody: """{{ .Get "JSONPatch" }}"""
)
}
Query:
mutation MyMutation {
updateUser_PatchString(JSONPatch: "{\"name\":\"John\"}")
}
An example of when updateUser_PatchString would be used rather than updateUser_Optionals is when an application or operation, such as @sequence,
passes JSON as an argument.
The JSONPatch can also be built using the JSON scalar type in which case, the following works:
type Mutation {
updateUser_Patch(JSONPatch: JSON): JSON
@rest(
endpoint: "https://httpbin.org/patch"
method: PATCH
postbody: """{{ .GetJSON "JSONPatch" }}"""
)
DELETE does automatic query string extension, passing all GraphQL arguments by default. postbody may be specified if needed; there is no postbody autogeneration.
type Mutation {
deleteUser(userID: ID!): JSON
@rest(
endpoint: "https://httpbin.org/delete"
method: DELETE
)
}
The query would result in the following URL being called with DELETE https://httpbin.org/delete?userID=1
mutation MyMutation {
deleteUser(userID: "1")
}
cachepolicy
This value is optional. The cachepolicy property overrides the default cache behavior.
In StepZen, caching is automatic. The system knows when the backend call is a REST call and sets up a cache with a default time-to-live (TTL) of one minute. Because StepZen uses multiple servers to handle traffic, these caches are available within the execution of a query and across queries.
cachepolicy can be set to the following values:
-
DEFAULT: Default cache strategy. The DEFAULT strategy varies according to field and method.If applied to a field of
Querythe default is{ strategy: DEFAULT }and:- When
method=GETthe default is{ strategy: ON }. - When
method=PATCHthe default is{ strategy: OFF } - When
method=POSTthe default is{ strategy: OFF }. - When
method=PUTthe default is{ strategy: OFF } - When
method=DELETEthe default is{ strategy: OFF }
If applied to a
Mutationfield, the default is{ strategy: OFF }. - When
-
ON: Use cache. -
OFF: Ignore cache. -
FORCE: The request is forced and the result is placed in the cache.
@rest(
endpoint: "yourendpoint.com",
cachepolicy: { strategy: ON }
)
transforms
This value is optional. The transforms argument "transforms" the @rest JSON response. The transforms argument takes the following form:
transforms: [{pathpattern: "...", editor: "..."},
{pathpattern: "...", editor: "..."},
...]
The transforms argument of the @rest directive takes a list of objects of the form {pathpattern: "...", editor: "..."}. This list must contain at least one object. If there is more
than one object, they are processed in the order in which they appear, the output of one providing the input for the next.
The pathpattern as the name implies specifies what objects are included in the transform processing by specifying the allowed paths. Here are the ways to select a JSON path:
termmatches a specific object key, likepathpattern:["records"].[num]matches a specific array index, likepathpattern:["records",[2]].<>matches exactly one object key.[]matches exactly one array index.*matches either exactly one key, or a exactly one index.<>*matches a sequence of zero or more keys, likepathpattern:["records", "<>*"]would matchrecords,records.name, and including those nested likerecords[1].name,records.artist.name, etc.[]*matches a sequence of zero or more indices.**matches a sequence of zero or more anything, keys or indices.pathpattern:[](an emptypathpattern) indicates that theeditorshould be applied at the root.
Available editors are listed in the following section. All of the editors except xml2json expect JSON input. xml2json expects XML input. The transforms are applied in the order they appear in the transforms argument in the schema.
-
xml2json: Transform XML to JSON.SOAP backends commonly return XML. The following
transformsargument will properly transcribe an XML response to JSON.transforms: [{pathpattern: [], editor: "xml2json"}]The following rules apply to the
xml2jsoneditor:- if an
xml2jsoneditor is present, it must be specified in the first or only input object. - if the REST response (or the output of any preceding ecmascript transform) produces an XML value, then there must be a
transformargument, and thexml2jsoneditor must be the first input object. - the only valid
pathpatternthat can accompany anxml2jsoneditor is[](root) since the input is not JSON
- if an
-
jq:<jq formula>: Apply a jq formula to the JSON.Tip: You can can also test out your
jqformula against the entire tree using any of thejqonline tools (such as jqplay) and then copy thejqformula directly into your StepZen schema, often without any changes. Makepathpattern:[]in this case. While this approach often works, sometimesjqtreatment ofarraysetc. differs subtly, so you might have to modify the formula that works outside StepZen a bit for it to work inside StepZen.To manipulate the following JSON response.
[ { "city": "Miami", "name": "John Doe" } ]This transforms argument manipulates the JSON response, applying the jq formula
.[]|{name,address:{city:.city}}.transforms: [{pathpattern:[],editor:"jq:.[]|{name,address:{city:.city}}"}]To place the field,
city, as a field in the new nested object,address.{ "data": { "customers": [ { "name": "John Doe", "address": { "city": "Miami" } } ] } } -
jsonata:<jsonata expression>: Apply a jsonata expression to the JSON.Similar to the jq editor, the jsonata editor allows you to manipulate the JSON response. You can learn more about jsonata at https://jsonata.org/, and there is an online jsonata playground that you can use to test your jsonata expressions.
-
objectToArray: Transform a list of key-values pairs to an array.Many backends return a list of key-values pairs with field names representing the key, as opposed to an array. So one might get a response back that looks like:
{ "data": { "2333": { "name": "john doe", "age": 23 }, "3333": { "name": "jane smith", "age": 21 } } }The classic GraphQL conversion of this will generate a new type for each entry which is not viable. The
objectToArrayeditor addresses this issue.@rest (endpoint: "...", transforms: [ {pathpattern: ["data","<>*"], editor: "objectToArray"} ])This configures StepZen to take an occurrence of zero or more keys at the path
"data"and apply the transform (the expression"<>*"is the expression that you will use almost always, though other possibilities exist — see the preceding description onpathpatternwildcard syntax).This will produce:
{ "data": [ { "name": "2333", "value": { "age": 23, "name": "john doe" } }, { "name": "3333", "value": { "age": 21, "name": "jane smith" } } ] }Now you can treat it as an array of objects that can be processed by downstream @rest directive arguments.
-
drop: prunes the JSON tree.Supposing you had two queries:
type Query { anonymous: [Customer] known: [Customer] }where in the anonymous query, you did not want the
namefield to be present. How would you achieve it against the same customer API? You could declare two types,type Anonymouswhich does not have the name field andtype Customerwhich does, and then have each preceding query return those two types respectively. But an easier answer is to just prunenameaway from the JSON response in the first query, and as a result, there is no way it will get populated.type Query { anonymous: [Customer] @rest (endpoint: "https://introspection.apis.stepzen.com/customers", transforms: [ {pathpattern: ["[]","name"], editor: "drop"}]) known: [Customer] @rest (endpoint: "https://json2api-customers-zlwadjbovq-uc.a.run.app/customers") } -
rename:<old-name>,<new-name>: Rename a field.{ "item": { "$id": 23 } }To rename the
$idfield add the followingtransformsargument. Identify the location of the field in thepathpatternand rename the field in theeditor.transforms: [{pathpattern: ["item"], editor: "rename:$id,id"}]The transformed JSON will be:
{ "item": { "id": 23 } }
ecmascript
This argument enables you to run ECMAScript 5.1 scripts to manipulate the HTTP request and response bodies.
There are two function signatures you can implement:
- bodyPOST(s) : manipulates the HTTP request body
- transformREST(s) manipulates the HTTP response body, s.
Each function signature has a built in function get which allows retrieving arguments. Refer to the following example:
type Query {
scriptExample(message: String!): JSON
@rest(
endpoint: "https://httpbin.org/anything"
method: POST
ecmascript: """
function bodyPOST(s) {
let body = JSON.parse(s);
body.ExtraMessage = get("message");
return JSON.stringify(body);
}
function transformREST(s) {
let out = JSON.parse(s);
out.CustomMessage = get("message");
return JSON.stringify(out);
}
"""
)
}
In this schema, we are sending an HTTP POST request to https://httpbin.org/anything. The payload of the request is modified using the bodyPOST function, adding an "ExtraMessage" field that contains the contents of the "message" argument. The response is modified using the transformREST function, adding a "CustomMessage" field that also contains the contents of the "message" argument.
Tip: When using ecmascript (or for that matter, any transforms), it is insightful to initially set the return type of the query to be JSON, which allows you to test and fix errors. Then you can
set the return type to be a specific schema type.
Bonus Tip: You can use ecmascript to simulate a REST API response.
You can use ecmascript combined with the endpoint stepzen:empty to simulate a REST API response. See the following example:
type Query {
customers: JSON
@rest (
endpoint: "stepzen:empty",
ecmascript: """
function transformREST(s) {
return (JSON.stringify({
"records": [
{"name": "John Doe", "countryRegion": "US"},
{"name": "Jane Smith", "countryRegion": "UK"}
]
}))
}
"""
)
}
@dbquery
@dbquery (type: String!, query: String, dml: enum, table: String, configuration: String!, schema: String)
@dbquery enables you to connect a MySQL, PostgreSQL, or MSSQL database. The directive can be applied to a GraphQL query or mutation, to populate its result with data returned from a database query.
-
typeis currently restricted tomysql,postgresql,mssql, andsnowflake. -
querydefines the SQL statement that will be executed against the backend defined intype. The query is parameterized by the arguments to the GraphQL query that@dbqueryis annotating. With a SQL query, the order of the parameter markers?matches the order of arguments in the GraphQL query declaration. The result of the query must have column names that match the declared returned namedtypefield names, and the types must be convertible to the declared GraphQL types. Cannot be set whiledmlortableare set. -
tabledefines the backend table that a StepZen-generated query will be executed against. For example, with a SQL database,table:"Customers"can be specified instead of having to specify the equivalent query ofSELECT id, name, email FROM Customers WHERE id = ?. Specifyingtablerequires that the name of the table's columns matches the name of the concrete type's fields. Only one ofqueryortablecan be set. -
configurationdefines the connection details for the backend via a reference to thenameinconfig.yaml. -
schemais the name of the database schema and is currently restricted topostgresqlandmssql. -
dmlspecifies the type of a mutation on the database. Valid values areINSERTandDELETEto enable adding or removing records respectively. Cannot be used in conjunction withquery. The following is an example of atype Mutationspecified in an GraphQL Schema Definition Language (SDL) file that enables adding a record withdml:INSERT:
type Mutation {
addCustomerById(id: ID!, name: String!, email: String!): Customer
@dbquery(
type: "mysql"
table: "customer"
dml: INSERT
configuration: "mysql_config"
schema: "schema_name"
)
}
For examples of how to use @dbquery to connect a database and add/remove database records, see Connect a Database Using @dbquery.
@graphql
@graphql (endpoint: String!, configuration: String, headers: [{name: String, value: String}], prefix: { value: String, includeRootOperations: Boolean })
@graphql connects to a GraphQL backend. It performs an HTTP POST request when the query is executed for GraphQL backends. The directive specifies the endpoint and passes all query arguments to the backend as parameters
in a URL query string. Typical arguments in an @graphql call are:
endpoint: Endpoint URL to be called. The variables available for constructing the URL include:- All arguments of the query attached to the
@graphqldirective. - All parameters in the corresponding
configuration.
- All arguments of the query attached to the
headers(optional): Header fields. Variables pass into header fields from the configuration or arguments in the query.prefix(optional): Prefix to prepend to the schema type and queries. Applicable when the GraphQL generator is used to introspect the GraphQL schema. The value is indicated by thevaluekey and theincludeRootOperationsboolean indicates whether root operation fields inQueryandMutationare prefixed with the value.configuration: Connection details to pass into headers (e.g.authorization).
For examples of how to use the @graphql arguments, see How to Connect to a GraphQL API.
@materializer
@materializer (query: String!, arguments: [{name: "name", field: "field"}])
@materializer is used to link types (including when federating two subgraphs into a supergraph). Data from one subgraph is used to provide arguments to the same or a different subgraph, which is then used to resolve a field in
the originating subgraph through a series of transformations. (For more, see Link Types: @materializer.)
@materializer defines that the annotated field is resolved by executing a field selection. The annotated field's arguments and enclosing type can be used to provide the values for arguments of the selection.
query: Specifies the field selection that will be used to resolve the annotated fieldarguments: defines mappings from the annotated field's arguments or enclosing type's fields to one or more arguments of the materializer's selection (specified in thequeryargument). Each element of arguments consists of anameand one of the following:field: "<field_name>"use<field_name>as the query argumentnameargument: "<field_argument_name>"use<field_argument_name>as the query argumentname
If an argument of the field selection is not specified, then it is mapped from a field of the enclosing type with the same name if one exists. Validation requires that all required arguments of the field selection specified in thequery argument of @materializer must be mapped using either explicit arguments via arguments or through the default of a matching field name in the enclosing type. Any optional arguments that are not mapped either explicitly or through
the default will be set to their default value.
Let's take an example of two schemas that we want to combine into one. We have a Customer schema (data from a REST backend), and anOrder schema with data from a MySQL database:
customer.graphql:
type Customer {
name: String
id: ID
email: String
}
type Query {
customer(id: ID): Customer @rest(endpoint: "https://api.acme/com/customers/$id")
customerByEmail(email: String): Customer
@rest(endpoint: "https://api.acme.com/customers?email=$email")
}
order.graphql:
type Order {
createOn: Date
amount: Float
}
type Query {
orders(customerId: ID): [Order] @dbquery(type: "mysql", table: "orders")
}
A new schema file that combines these two might look like this:
customer-orders.graphql:
extend type Customer {
orders: [Order]
@materializer (query: "orders", arguments: [{name: "customerId", field: "id"}]
}
where:
- The
extendclause extends a subgraph with new fields. In this case, we are adding a new field totype Customer. We can achieve the same purpose by editing thecustomer.graphqlfile, but usingextendkeeps the concerns of the supergraph separate from those of the subgraphs, allowing each to evolve and be maintained with minimal impact to the other. - The new field declaration defines the name of the new field (
orders), its type ([Order]), and a @materializer directive. - The
@materializerdirective specifies a field selection that is used to resolve the annotated field. It has twoarguments:- A field selection, specified by the
queryargument. This field selection must have the same type as the the new field. In this case, both have the same type[Orders]. The field selection need not be a top-level field selection. It can be a nested field selection, as long as the type of the field selection matches the type of the new field. For example, you could add acustomerName: Stringfield to a type, and populate it with a materializer like this:customerName: String @materializer(query: "customerByEmail { name }") - A list of argument mappings, specified by the
argumentsargument. Basically, you are telling StepZen how to map the arguments and fields of the annotated field and its enclosing type, respectively, to the arguments of the field selection specified byquery. So here, thequery: "orders"gets thecustomerIdargument from the fieldidofCustomer.
- A field selection, specified by the
If you need to pass an additional argument to the query not present in the enclosing type of the annotated field, eg. ordersWeight(customerId: ID, overweight: Int!): [Order], then we would have:
extend type Customer {
orders(vendorOverweight: Int!): [Order]
@materializer (query: "ordersWeight",
arguments: [{name: "customerId", field: "id"},
{name: "overweight", argument: "vendorOverweight"}]
}
where argument maps the field argument vendorOverweight to the field selection argument overweight. There is no default mapping from the annotated field's arguments to the field selection's field arguments.
@mock
@mock mocks results type by type, so you can create mock queries in your schema. @mock ensures that any query against a type, returns mocked data based on the fields specified. This directive can
be used on any type and/or interface.
@mock can be helpful for creating stubs, because you can be sure that the returned value you test will be the same. For example:
type User @mock {
id: Int
name: String!
description: String
}
Next, you could make this query:
{
users {
id
name
description
}
}
Mock data similar to the following will be returned:
{
"data": {
"users": [
{
"id": 318,
"description": "Morbi in ipsum sit amet pede facilisis laoreet",
"name": "Praesent mauris"
}
]
}
}
@mockfn
@mockfn can be used in conjunction with @mock to set a particular mock value.
type company {
ceo: String @mockfn(name: "LastName")
company_mottoes: String @mockfn(name: "List", values: [JSON])
}
In the name field, place the name of the type of data you want to mock and in the values field, place the desired values.
Here is a list of types of data you can use with @mockfn.
- `FutureDate` - select a Date up to N days into the future, where N is the first and only value in the list.
- `List` - select from the list of values. The values are converted from
String values into the field's type.
- `NumberRange` - select a value between the first (lower) and second (upper)
values in the list.
- `PastDate` - select a Date up to N days into the past, where N is the first and only value in the list.
- `Email` - a mocked email address
- `FirstName` - a first name
- `LastName` - a last name
- `Phone` - a phone number
- `SSN` - a mocked US social security number
- `City` - a mocked city
- `Country` - a mocked country
- `CountryCode` - a mocked country code (ISO 3166-1 alpha-2)
- `Latitude` - a mocked latitude
- `Longitude` - a mocked longitude
- `Zip` - a mocked US five digit zip code
- `UUID` - a mocked UUID
- `DomainName` - a mocked domain name
- `DomainSuffix` - a mocked domain suffix, e.g. `org`, `com`
- `IPv4Address` - a mocked IPv4 address as a string, e.g. `140.186.32.250`
- `IPv6Address` - a mocked IPv6 address as a string, e.g. `2d84:26ad:91c9:b832:42b7:55e7:bf22:e737`
- `URL` - a mocked URL
- `Username` - a mocked username
- `CreditCard` - a mocked credit card number, e.g. `2229798696491323`.
- `Currency` - a mocked currency name, e.g. `Bahamas Dollar`
- `CurrencyCode` - a mocked currency code (ISO 4217)
@sdl
@sdl(files: [#list of schemas with relative paths])
The @sdl directive is used in the index.graphql file at the root of your StepZen project. It specifies to StepZen all the .graphql files you want to assemble into a unified GraphQL schema (with relative
paths). Here is an example of an index.graphql file with a list of three schemas to assemble:
schema
@sdl(
files: [
"algolia/algolia.graphql"
"contentful/blog.graphql"
"contentful/person.graphql"
]
) {
query: Query
}
@supplies
@supplies (query: String!)
@supplies connects a query on a concrete type to an interface query.
For example, you can use interface queries when you want to run the same query against more than one backend:
interface Delivery {
status: String!
statusDate: Date!
}
type DeliveryFedEx implements Delivery {
status: String!
statusDate: Date!
}
type DeliveryUPS implements Delivery {
status: String!
statusDate: Date!
}
type Query {
delivery(carrier: String!, trackingId: String!): Delivery
deliveryFedEx(carrier: String!, trackingId: String!): DeliveryFedEx
@supplies(query: "delivery")
deliveryUPS(carrier: String!, trackingId: String!): DeliveryUPS
@supplies(query: "delivery")
}
The result of invoking the interface query delivery is the result of executing deliveryFedEx with the same arguments. This returns the data where there exists a match between UPS and FedEx IDs.
@sequence
@sequence(
steps:
[
{query: "step1"}
{query: "step2"}
]
)
@sequence executes multiple queries in a sequence, one after the other.
steps is an array of queries that are executed in sequence. Each object in the array must contain the name of the query that the step will call.
In the following example, @sequence is applied to the query weather. This in turn, executes the query location followed by weatherReport:
type WeatherReport {
temp: Float!
feelsLike: Float!
description: String!
units: String!
}
type Coord {
latitude: Float!
longitude: Float!
city: String!
}
type Query {
weather(ip: String!): WeatherReport
@sequence(
steps: [
{ query: "location" }
{ query: "weatherReport" }
]
)
location(ip: String!): Coord
@connector(
type: "__ipapi_location_ip_connector__"
)
weatherReport(latitude: Float!, longitude: Float!): WeatherReport
@connector(
type: "__openweathermap_weather_location_connector__"
)
}
The following example shows the execution and response of the weather query:
{
weather(ip:"8.8.8.8") {
temp
feelsLike
}
}
{
"data": {
"weather": {
"feelsLike": 19.4,
"temp": 18.85
}
}
}
For additional information see Execute Multiple Queries in Sequence and How to Create a Sequence of Queries.
Executable Directives
Executable directives are used to control the execution of a GraphQL operation in a GraphQL document when requesting or mutating data. The following executable directives are supported by StepZen:
@sort
@sort
@sort is used to sort a field selection in ascending order.
Leaf fields
When applied to a field selection that is a list of a scalar or enum type (for example [String]) then @sort sorts the value of the list.
For example the result of this query:
query {
products {
tags # ['c', 'b', 'a', null]
}
}
Will be transformed to this:
query {
products {
tags @sort # [null, 'a', 'b', 'c']
}
}
The sort order is ascending, with null values sorted first.
Object fields
When applied to a field selection that is a list of an object or interface type then @sort sorts the list of objects.
The sort order is derived from the direct sub-selection of the @sort field selection.
For example this query will sort customers by lastName first, then firstName and then email in ascending order for each field, with null values sorted first.
query {
customers @sort {
lastName
firstName
email
}
}
Fields can be selected but omitted from the sort order by selecting them in a fragment. This query will only sort by lastName, since firstName and email are in an inline fragment, and thus are not the
direct sub-selection of customers.
query {
customers @sort {
lastName
... {
firstName
email
}
}
}