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.
@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 result is populated with JSON data returned from a REST API.
For REST backends, execution of the query results in an HTTP GET
call using the specified endpoint and the query arguments as URL query parameters. The typical parameters in an @rest
call are:
endpoint
(required)configuration
(optional)method
(optional)resultroot
(optional)setters
(optional)filter
(optional)headers
(optional)postbody
(optional)cachepolicy
(optional)
For examples of how to use the @rest
parameters, see Connect a REST Service Using @rest
endpoint
This value is required. The URL to be called.It determines the REST endpoint that will be used to populate the query result. 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.
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 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 parameters on 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 backticks. 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
parameter 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
parameter 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 parameter.
postbody
This value is optional. You set postbody
when you need customize the automatically generated body for a PUT or POST request. Let's look at some examples to help explain how this parameter 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 to retrieve a client credentials OAuth token. Since the grant_type parameter 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 query arguments are automatically appended to the request URL as query parameters, e.g. https://httpbin.org/post?firstName=<value>&lastName=<value>
. To prevent that we need to create a configuration file that instructs StepZen not to automatically append query parameters to the request. Do this by setting the attribute stepzen.queryextentionguard
to true. An example of that file follows:
configurationset:
- configuration:
name: "config"
stepzen.queryextentionguard: 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"
)
}
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=POST
the default is{ strategy: OFF }
. - When
method=PUT
the default is{ strategy: OFF }
and you must not override it.
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 }
)
The Query Responses
The query response must be a JSON array of objects if the query is declared to return a list [X]
– or a single object otherwise.
For example:
type Article {
id: ID!
title: String!
description: String
type Query {
myArticles(username: String!): [Article]
@rest(
endpoint: "https://dev.to/api/articles?username=$username&per_page=1000"
)
}
}
The following GraphQL query placed in your GraphiQL query editor results in an HTTP GET request to https://dev.to/api/articles?username=$username&per_page=1000
:
query MyQuery {
myArticles(username: "remotesynth") {
title
description
}
}
Response:
{
"data": {
"myArticles": [
{
"description": "Create a developer portfolio featuring content pulled from your DEV.to blog posts and GitHub profile and projects using Next.js and StepZen.",
"title": "Creating a Developer Portfolio using Next.js, GraphQL, DEV and GitHub"
},
{
"description": "Some tips and things to think about when choosing to run a virtual developer conference based upon my recent experiences.",
"title": "Tips for Running Your First Virtual Conference"
}
]
}
}
See our blog post on turning a REST API into GraphQL for more information about this example.
If the request returns no results, then the response can be any of the following:
- JSON
null
- Empty response
- 204 status code
- Empty list for a query returning a list
For your config.yaml, you must include an Authorization
header to avoid 404 errors.
An Authorization
header is added in the named configuration as shown here:
- configuration:
name: dev_config
Authorization: Bearer MY_PERSONAL_ACCESS_TOKEN
@dbquery
@dbquery (type: String!, query: String, dml: enum, table: String, configuration: String!)
@dbquery
enables you to connect a MySQL, PostgreSQL, or MSSQL database. The directive can be applied to a GraphQL query, to populate its result with data returned from a database query.
type
is currently restricted tomysql
,postgresql
, andmssql
.query
defines the query 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 an 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.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
.dml
specifies the type of a mutation on the database. Valid values areINSERT
andDELETE
to enable adding or removing records respectively. The following is an example of atype Mutation
specified in an 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"
)
}
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 sets the query arguments as URL query parameters. Typical parameters in an @graphql
call are:
endpoint
: Endpoint URL to be called. The parameters available for constructing the URL include:- All parameters of the query attached to the
@graphql
directive. - All parameters in the corresponding
configuration
.
- All parameters 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.configuration
: Connection details to pass into headers (e.g.authorization
).
For examples of how to use the @graphql
parameters, see How to Connect to a GraphQL API.
@materializer
@materializer (query: String!, arguments: [{name: "name", field: "field"}])
@materializer
is used to combine two schema into a single schema (or, two subgraphs into a supergraph). Data from one subgraph is used to call a query/mutation in the same subgraph, or a different subgraph, through a series of transformations.(For more, see Declaratively Build a Graph of Graphs.
The parameters for @materializer
are:
query
: Specifies which query will be used.arguments
: defines one or more arguments to pass to the query. Each argument consists of aname
and one of the following:argument: "<argument_value>"
sets the argument to the specified value.field: "<field_value>"
sets the field to the specified 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: "api.acme/com/customers/$id")
customerByEmail(email: String): Customer
@rest(endpoint: "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 calledorders
totype Customer
. We can achieve the same purpose by editing thecustomer.graphql
file. but in general, usingextend
keeps the concerns of the supergraph separate from those of the subgraphs. - The field
orders
- must be of the type returned by thequery
(ormutation
) referenced in the@materializer
clause. This enables compile time checking. - The
@materializer
clause calls aquery
or amutation
. It has two arguments.- The name of the
queryy
ormutation
. - A list of argument mappings. Basically, you are telling StepZen how to take the fields in the calling subgraph, and convert them into the arguments of the
query
ormutation
. So here, thequery: "orders"
gets thecustomerId
argument from the fieldid
ofCustomer
.
- The name of the
@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.