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.

@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:

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 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 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 the resultroot property to user.
  • 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 }.

  • 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 to mysql, postgresql, and mssql.

  • query defines the query that will be executed against the backend defined in type. 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 named type 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 of SELECT id, name, email FROM Customers WHERE id = ?. Specifying table requires that the name of the table's columns matches the name of the concrete type's fields. Only one of query or table can be set.

  • configuration defines the connection details for the backend via a reference to the name in config.yaml.

  • dml specifies the type of a mutation on the database. Valid values are INSERT and DELETE to enable adding or removing records respectively. The following is an example of a type Mutation specified in an SDL file that enables adding a record with dml: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.
  • 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 a name 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 called orders to type Customer. We can achieve the same purpose by editing the customer.graphql file. but in general, using extend keeps the concerns of the supergraph separate from those of the subgraphs.
  • The field orders - must be of the type returned by the query (or mutation) referenced in the @materializer clause. This enables compile time checking.
  • The @materializer clause calls a query or a mutation. It has two arguments.
    • The name of the queryy or mutation.
    • 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 or mutation. So here, the query: "orders" gets the customerId argument from the field id of Customer.

@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.

This site uses cookies: By using this website, you consent to our use of cookies in accordance with our Website Terms of Use and Cookie Policy.