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 theresultroot
. In this example, this makes the data undermetadata
andsys
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 above 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 above, the value of user.name
cannot be automatically mapped to a field in the GraphQL type. You have two options:
- Create another type for
User
that has the corresponding fields and then use theresultroot
property touser
. - Flatten the values and add them to the
Article
type 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:
endpoint
sets the endpoint of the API.configuration
references the configuration file by its name value.setters
specifies 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 quotes above, 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 above 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 example below 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
Query
the default is{ strategy: DEFAULT }
and:- When
method=GET
the default is{ strategy: ON }
. - When
method=PATCH
the default is{ strategy: OFF }
- When
method=POST
the default is{ strategy: OFF }
. - When
method=PUT
the default is{ strategy: OFF }
- When
method=DELETE
the default is{ strategy: OFF }
If applied to a
Mutation
field, 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:
term
matches 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 theeditor
should be applied at the root.
Available editors are listed below. 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
transforms
argument below will properly transcribe an XML response to JSON.transforms: [{pathpattern: [], editor: "xml2json"}]
The following rules apply to the
xml2json
editor:- if an
xml2json
editor 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
transform
argument, and thexml2json
editor must be the first input object. - the only valid
pathpattern
that can accompany anxml2json
editor 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
jq
formula against the entire tree using any of thejq
online tools (such as jqplay) and then copy thejq
formula directly into your StepZen schema, often without any changes. Makepathpattern:[]
in this case. While this approach often works, sometimesjq
treatment ofarrays
etc. 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 JSON response below.
[ { "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
objectToArray
editor 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 description onpathpattern
wildcard syntax above).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
name
field to be present. How would you achieve it against the same customer API? You could declare two types,type Anonymous
which does not have the name field andtype Customer
which does, and then have each query above return those two types respectively. But an easier answer is to just prunename
away 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
$id
field add thetransforms
argument below. Identify the location of the field in thepathpattern
and 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 example below
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, anytransforms
), 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 example below:
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.
-
type
is currently restricted tomysql
,postgresql
,mssql
, andsnowflake
. -
query
defines the SQL statement that will be executed against the backend defined intype
. The query is parameterized by the arguments to the GraphQL query that@dbquery
is 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 namedtype
field names, and the types must be convertible to the declared GraphQL types. Cannot be set whiledml
ortable
are set. -
table
defines 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 = ?
. Specifyingtable
requires that the name of the table's columns matches the name of the concrete type's fields. Only one ofquery
ortable
can be set. -
configuration
defines the connection details for the backend via a reference to thename
inconfig.yaml
. -
schema
is the name of the database schema and is currently restricted topostgresql
andmssql
. -
dml
specifies the type of a mutation on the database. Valid values areINSERT
andDELETE
to enable adding or removing records respectively. Cannot be used in conjunction withquery
. The following is an example of atype Mutation
specified 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
@graphql
directive. - 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 thevalue
key and theincludeRootOperations
boolean indicates whether root operation fields inQuery
andMutation
are 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 thequery
argument). Each element of arguments consists of aname
and one of the following:field: "<field_name>"
use<field_name>
as the query argumentname
argument: "<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
extend
clause 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.graphql
file, but usingextend
keeps 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
@materializer
directive specifies a field selection that is used to resolve the annotated field. It has twoarguments
:- A field selection, specified by the
query
argument. 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: String
field 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
arguments
argument. 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 thecustomerId
argument from the fieldid
ofCustomer
.
- 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 } } }