Skip to main content

Schema Spec Reference

Every project has a schema that defines its functionality. Users can configure most of a project's functionality in its JSON schema file. Configurable features include:

  • What the project's GraphQL API is capable of, including which API services it uses and what queries and mutations it exposes. (Read more in our Shapes guide)
  • Which workflows should be used when a project's content is edited. (Read more in our workflows guide)
  • What the admin UI interface for editing a project's content should look like.

But the JSON schema file does not contain the entire project's schema. Instead, a project's schema composes the editable JSON schema with other hidden configuration information.

There are hidden objects which are part of the project's schema, though they are not visible or configurable in the JSON schema file. These hidden objects are outlined at the end of this specification, and should be ignored in most cases.

There is also a service schema, which is hidden from the editable JSON schema file. The service schema is automatically updated when a new API service is added to a project, and cannot be directly viewed or configured.

Learn more about the Service Schema:

This specification details all configurable functionality in a project's JSON schema file.

Root objects

Root-level objects in a project's schema define the project's high-level functionality.

Projects use queries, mutations and shapes objects to generate a GraphQL API every time the project is deployed. The services object configures API service connections. The forms object configures the interface for creating content. The workflows object configures content creation workflows. The locales and defaultLocale objects configure a project's language localization features.

The following sections will explain the functionality and configuration of all of a schema's root-level objects.

shapes

A Shape is a schema object for structuring and storing data from one or more sources, including a project's data store, and connected services like Shopify.

Shapes are analogous to GraphQL types. Learn more in our shapes guide.

Usage

All shapes exist inside the root-level schema object shapes. The key for the shape should be the shape's name. For example, a shape to represent products should be called Product. A shape to represent reviews should be called Review.

example shapes object
"shapes": {
"Review": {
"id": "Review",
"name": "Review",
"title": "A custom Review shape",
"schema": {
...
}
}
}

Shape properties

Because shapes represent data a project needs to manipulate, they have several properties to identify them for reference in other areas of the schema and project. Those properties include:

id (required)

The shape's ID, which will be used to reference it in other places in the schema. The value of the id key is a used in the shapeIds property of an @relationship and available as _shapeId on an item.

Changing a shape's ID orphans its data

When using built-in database to store shape data, the id value is used in the database as a stable "table name," and changing it will result in orphaning that data. Changing the ID back will make the data accessible again.

name (required)

The name of the shape. Must be in pascal-case. This value must be the same as the shape's object key value. For example, a Review shape must have a name property of "Review"

The name property is important.

Projects use the name property's value in multiple areas, including:

  • When @ref and @mapping annotations are needed.
  • During the automatic generation of queries and mutations for models, like getShape and getShapeList
  • In the _shapeName property on an item.

title (required)

The shape's title. The value of the title property will appear as the shape's title in the admin UI.

workflow

Takes the name of the workflow this shape should be scoped to as a string.

description

A description of the shape that will appear in the admin UI. This description appears in the Docs Panel.

model

The data model used to store instances of this shape. Valid properties include multiple, single, and taxonomy.

schema

Because shapes function like GraphQL types, queries and mutations can return instances of them. The fields available to such queries must be configured in the schema property.

The schema object contains three configuration properties:

Shape schema config

type

The value of this property should always be "object". It indicates the GraphQL type of the shape, which is always an object.

required

An array of strings, which match the keys of the properties that are required when creating an instance of this shape.

For example, if you have a Product shape with properties price, name and id, and all three are required when creating a Product instance, your required array will look like this:

"required":["price", "name", "id"]
properties

This object can have an arbitrary number of nested objects within it, each of which will be fields for the shape. If you have a Product shape that has a price field, you might configure the properties like so:

"Product": {
"id": "jadxIUkZV",
"name": "Product",
"title": "Product",
"schema": {
"type": "object",
"properties": {
"price": {
"type": "number",
"description": "The product's price",
"title": "Product price"
}
}
}
}

Shape fields

When creating a field in the properties object, the only required property is type, which is the GraphQL type of the data the property represents, like string, boolean, integer, etc. But there are many other possible properties, as demonstrated in the example below.

properties example
"properties": {
"customId": {
"type": "integer",
"description": "A custom ID property for this shape.",
"title": "This shape's custom ID.",
"minimum": 0
},
"handle": {
"type":"string"
},
"myResolvedProperty": {
"description": "A custom property that uses @resolver.",
"title": "My Resolved Property.",
"@ref": "shopify-storefront:Product",
"@resolver": {
"name": "graphql:query",
"service": "shopify-storefront",
"if": "$source.handle",
"fieldName": "productByHandle",
"args": {
"ops": [{"path": "handle", "mapping": "$source.handle"}]
}
}
}
}

The list of fields possible for a property include:

type (required)

The GraphQL type of the property. Possible values:

  • array
  • boolean
  • integer
  • number
  • string
  • object
type isn't always required.

Using the @ref annotation indicates that a field is an instance of a shape defined elsewhere in the schema. That means you shouldn't use type if you use @ref. Otherwise, type is required.

Configuring object fields

If the field's type is object, then properties will also be required. Within this nested properties object, you can add the properties of the object field. For example:

Shape with an object property example
"Product": {
"id": "Product",
"name": "Product",
"title": "My Custom Product",
"schema": {
"type": "object",
"properties": {
"variants": {
"type": "object",
"properties": {
"variantId":{
"type":"integer",
"description": "The ID for this variant"
},
"variantTitle":{
"type":"string",
"title": "Variant Title"
}
}
},
"id": {"type": "integer"},
"price": {"type":"number"}
}
}
}

When querying for the above Product, the properties will be accessible as fields:

Product query example
getProduct(id:123){
id
price
variant {
variantId
variantTitle
}
}

Objects can be nested arbitrarily deeply, and fields defined within objects can have the same annotations as top-level fields, like @resolver, @input, etc.

Configuring array fields

Array fields require the items object, which allows you to specify the data type of the items in the array. The items object can be configured the same as a standard field, meaning it requires either an @ref or a type, and can have any of the other properties specified for fields in this section, including annotations.

For example, here is a ShoppingCart shape that contains an array of Product instances, and an array of Coupon instances.

Array field example
"ShoppingCart": {
"id": "PShoppingCartroduct",
"name": "ShoppingCart",
"title": "My Custom Shopping Cart",
"schema": {
"type": "object",
"properties": {
"products": {
"type": "array",
"items": {"@ref":"local:Product"}
},
"coupons": {
"type": "array",
"items": {
"type": "object",
"properties": {
"couponCode": {"type":"string"},
"discountValue": {"type":"number"}
}
}
}
}
}
},
"Product": {
"id": "Product",
"name": "Product",
"title": "My Custom Product",
"schema": {
"properties": {
"title": {"type":"string"},
"price": {"type":"integer"}
}
}
}
You can use @ref in nested fields.

In the example above, ShoppingCart has two array fields. The first, products, is an array with items that are instances of the Product shape, which is declared later in the project schema. The second, coupons, is defined within the ShoppingCart shape.

The benefit of declaring a shape in the schema is that you can re-use it everywhere with the @ref annotation. But if a field has a simple configuration, and won't be used anywhere else, it makes sense to define it within the shape that uses it.

description

A description of the property that will appear in the admin UI. For example, you will see a property's description in the docs panel of the API Explorer when you hover over it.

title

The title of the field that will appear in the admin UI.

minLength

An integer minimum length for the value of the property.

minimum

The minimum value possible for a property whose type is set to integer.

Shape annotations {shape-annotations}

Annotations are properties on fields that enable extended functionality. Similar to GraphQL directives, annotations enable defining a field as an instance of a shape defined in the schema, attaching a query resolver to a field so that its data can be fetched from remote API services, and more.

No annotation is ever required, but @ref can be used in place of the type property on a field.

The following a list of all valid annotations.

@args

This annotation lets you define the schema of the arguments that can be passed to a field that uses an @resolver. Because the @resolver can query remote API services, @args should be used for defining the arguments that will be used when querying a service.

For example, here's what an @args annotation looks like on a field with a resolver that fetches a Shopify product:

@args example
"Product": {
"id": "Product",
"name": "Product",
"title": "Product",
"schema": {
"type": "object",
"properties": {
"id": {"type": "integer"},
"shopifyProduct": {
"title": "Shopify Product",
"description": "A field that returns a shopify product",
"@ref": "shopify:Product}",
"@args": {
"type": "object",
"properties": {
"handle": {"type":"string"},
"id": {"type":"string"}
}
},
"@resolver": {
"if": "!isEmpty($args.handle) || !isEmpty($args.handle)",
"name": "graphql:query",
"service": "shopify",
"fieldName": "product",
"args": {
"ops":[
{"path": "handle", "mapping": "$args.handle"},
{"path": "id", "mapping": "$args.id"}
]
}
}
}
}
}
}

When querying for the above product, the id or handle can be passed to the shopifyProduct field.

getProduct(id:123){
shopifyProduct(handle:"example-product"){
title
id
handle
}
}

The fields available on shopifyProduct are defined in the Shopify GraphQL API spec, since shopifyProduct is an instance of shopify:Product. To learn more about using @ref this way, read our section on using @ref below.

@ref

This annotation takes the scoped name of a shape that exists in a project's schema. It's used when a field is an instance of a shape. It's useful when a field has many nested subfields, or when a field is an instance of a shape defined elsewhere in the schema.

For example, if you have a User shape that you want to associate Stripe customer data with, you might define a stripeCustomerData field on the User shape as shown below:

@ref example
"User": {
"id": "User",
"name": "User",
"title": "User",
"schema": {
"type": "object",
"properties": {
"stripeCustomerData": {
"@ref": "stripe:Customer",
"description": "The stripe customer data associated with this user."
},
"username": {
"type": "string",
"description": "The username associated with this user."
}
}
}
}

The above example creates a stripeCustomerData field that can return Stripe customer data, but an @resolver annotation is required to define how that data can be fetched from stripe. read our section on @resolver below ot learn more.

Referencing scoped shapes with @ref

To define a field as an instance of a shape, that shape must fulfill one of three conditions:

  • It must be defined in the JSON schema, either manually or automatically via the visual editor.

  • It must be a hidden schema object, all of which are listed at the end of this spec.

  • It must be defined in the hidden service schema.

The service schema is neither visible nor directly editable, but all projects have them. They contain configuration information for connected API services, like Stripe or ShipEngine. Projects automatically update the service schema whenever a service is added or deleted.

To create a field that is an instance of a shape from a connected service, you must scope the shape with the service name and a colon, like so:

"@ref":"shopify:Product"

The format is service:ShapeName.

Service schema errors can happen

When a service is connected with a generic REST provider, or a generic GraphQL provider, there is a chance its API configuration will to be added to the service schema.

In such cases, shapes from the service must be manually defined in the JSON schema file. Trying to reference them from the service schema will fail.

Defining a field as an instance of a shape defined in the JSON schema file requires the use of the local scope, like so:

"@ref":"local:Product"

The format is local:ShapeName.

@resolver

An annotation that enables fields to have return data from remote services when queried.

For example, a User shape with a stripeCustomerData field should have customer data from stripe when that field is queried. To ensure that data is there, an @resolver annotation must be used to trigger a query to stripe's customers endpoint.

Example query for stripeCustomerData
{
getUser(id: 123){
username
# Including a below field will cause the query
# in @resolver to be executed before a response
# is returned.
stripeCustomerData(stripeCustomerId: "cus_ABC123pxLtTYUM"){
email
phone
}
}
}

Here's how the User shape might be defined:

@resolver example
"User": {
"id": "User",
"name": "User",
"title": "User",
"schema": {
"type": "object",
"properties": {
"stripeCustomerData": {
"@ref": "stripe:Customer",
"description": "The stripe customer data associated with this user.",
"@resolver": {
"name": "rest:get",
"service": "stripe",
"options": {"ignoreErrors": true},
"path": {
"ops": [
{"path": "customerId", "mapping": "$args.stripeCustomerId"}
],
"serialize": {"template": "/v1/customers/{customerId}"}
}
},
"@args": {
"type": "object",
"properties": {
"stripeCustomerId": {
"type": "string"
}
}
}
}
},
"username": {
"type": "string",
"description": "The username associated with this user."
}
}
}

@mapping

This annotation is usually generated for shapes created in the visual editor. Editing this value can cause a loss of data. It maps instances of the shape to a table in a project's database. Learn more in our @mapping section below.

@input

This annotation enables overriding the input type for the field, which can be useful when setting up fields that store foreign keys and resolver data from other services. The @input properties will overwrite the field's original properties, with the exception of @mapping, which is preserved.

You cannot rely on any property inheritance from the original field when using an @input annotation.

For example, this ProductPage Shape accepts a string as input for its product field but returns a product from Shopify when that field is queried.

{
"ProductPage": {
"id": "ProductPage",
"name": "ProductPage",
"title": "ProductPage",
"schema": {
"type": "object",
"properties": {
"title": {
"type": "string"
},
"product": {
"@ref": "shopify:Product",
"@resolver": {
"name": "graphql:query",
"service": "shopify",
"fieldName": "product",
"args": {
"ops": [{"path": "id", "mapping": "$source.product"}]
}
},
"@input": {
"type": "string"
}
}
}
}
}
}
Keep your shapes as flat and composed as possible

Rather than deeply nesting and copy-pasting objects to create fields, complex shapes are best composed with the @ref and @relationship annotations.

Hidden properties

Shape schemas also have a number of hidden properties that are automatically managed, such as _created and _updated, which record the dates shape items are created and edited. Though you cannot manipulate them, remember that their names are reserved, and fields with those names cannot be created.

Shape joins

{
"Shopify_Product": {
"name": "Shopify_Product",
"id": "Shopify_Product",
"title": "Shopify_Product",
"joins": {
"sanity:Product": {
"resolver": {
"name": "delegate",
"to": "sanity:Query.allProduct",
"args": {
"ops": [
{
"path": "where.shopifyProductId.eq",
"mapping": "$source.id"
}
]
},
"results": {
"ops": [{"path": "$", "mapping": "$finalResolver[0]"}]
}
},
"dependencies": "{id}"
}
},
"schema": {
"extends": [
{"@ref": "shopify:Product"},
{
"type": "object",
"properties": {
"customDescription": {
"type": "string",
"@mapping": "sanity:Product.customDescription"
},
"seo": {
"@ref": "sanity:Seo",
"@mapping": "sanity:Product.seo"
}
}
}
]
}
}
}

Shape joins allow you to mesh the properties of multiple shapes into one. In this example we have our product shape from Shopify combined with a custom product shape stored in Sanity.io. This is done by specifying the join in the joins property:

{
"joins": {
"sanity:Product": {
"resolver": {
"name": "delegate",
"to": "sanity:Query.allProduct",
"args": {
"ops": [
{
"path": "where.shopifyProductId.eq",
"mapping": "$source.id"
}
]
},
"results": {
"ops": [{"path": "$", "mapping": "$finalResolver[0]"}]
}
},
"dependencies": "{id}"
}
}
}
  • joins is an object keyed by the ref to the shape we want to join, in this case sanity:Product.
  • Join objects contain the following props
    • resolver (required) - A delegate resolver OR shapedb resolver
    • dependencies - A GraphQL-style selection of properties from the same shape that this join depends on. This can also include properties from other joined shapes.

Once a join config has been added you can use @mapping to specify the source of each property:

{
"customDescription": {
"type": "string",
"@mapping": "sanity:Product.description"
},
"seo": {
"@ref": "sanity:Seo",
"@mapping": "sanity:Product.seo"
}
}

Shape caching/indexing

To enable caching with indexing for a given shape add the cache and loaders configuration. For a detailed walkthrough of configuring cache/loaders in your project, check out our API Indexing guide.

{
"Shopify_Product": {
"name": "Shopify_Product",
"id": "Shopify_Product",
"title": "Shopify_Product",
"schema": {"extends": [{"@ref": "shopify:Product"}]},
"cache": {
"enabled": true,
"fragment": {
"maxDepth": 2
},
"triggers": [{"type": "schedule", "query": "list", "interval": 1440}]
},
"loaders": {
"list": {
"query": "shopify.Query.products"
},
"get": {
"query": "shopify.Query.product"
}
}
}
}

loaders (required)

This object contains the queries that the API Indexer will use to index remote API data. Valid properties are list and get. See the section on valid list and get properties below for more information.

Valid list and get properties

Within your list or get queries, you can also configure how indexing finds the information in the result of the query.

name (required)

The string name of the query to run.

Example: "Stripe_listCustomers"

pagination

Valid only for list queries. A configuration object that allows you to specify how the indexing function fetches paginated data. See the section below on valid pagination properties for more information.

Example:

"pagination": {
"type": "cursor",
"cursorPath": "data[(@.length-1)].id",
"itemsPath": "data",
"hasMorePath": "has_more",
"cursorArg": "starting_after"
}

Valid pagination properties

Every pagination configuration has a type property, which specifies the type of pagination. Below are the properties that are valid for all pagination types:

NOTE

All path property values are parsed with jsonPath syntax.

type (required)

The type of pagination, which can be one of cursor, offset, or page.

Example:

"pagination": {
"type": "cursor",
"cursorPath": "edges[(@.length-1)].cursor",
"itemsPath": "edges",
"itemPath": "node",
"hasMorePath": "has_more",
}

Below are other properties categorized by which pagination type they're appropriate for.

cursor pagination
  • cursorArg (required) — The string name of the query argument that accepts the cursor value for the start of the next page.

  • cursorPath (required) — The string path to the cursor returned in the query's response object.

  • hasMorePath (required) — The string path to the property in the query's response that indicates whether there are more pages to return.

  • itemsPath (required) — The path to the array of items to index in the query's response.

  • pageSize (optional) — The number of items to fetch in each page.

  • pageSizeArg (required) — The string name of the of the argument passed to the pagination query to indicate the size of the desired result.

Example: "first"

offset pagination
  • offsetArg (required) — The string name of the offset argument to use when querying the API.

  • itemsPath (required) — The path to the array of items to index in the query's response.

  • itemTotalPath (required) — The path to the property which contains the number representing the total items.

page pagination
  • pageArg (required) — The string argument passed to the pagination query to indicate where the pagination slicing should begin or end.

  • pageTotalPath (required) — The path to the total number of pages in the response data.

  • itemsPath (required) — The path to the array of items to index in the query's response.

cache (required)

fragment

This object contains settings that determine the query fragment that is used in indexing

maxDepth

This number dictates how deep of an indexing query we generate and in-turn how deep we index. This number accounts for known pagination schemes so wrapper objects will not count as extra depth. For example:

{
Shopify_products {
nodes {
id # depth 1
title
featuredImage {
url # depth 2
}
variants {
edges {
node {
id # depth 2 accounting for relay style pagination
title
price
}
}
}
}
}
}

Setting maxDepth will automatically index all fields to the specified depth. This value defaults to 2 unless selectionSet is specified.

selectionSet

This setting enables full control over the indexing query's fragment. This is used when you want to index deeply or specify custom args.

For example when indexing Character from https://rickandmortyapi.com/graphql our selectionSet is set to:

{
id
name
episode {
name
created
air_date
characters {
id
name
}
episode
}
}

With this configuration we can search characters that appear in the same episode.

{
search(shapeNames: ["Rick_Character"] where: {episode: {characters: {name: {eq: "Pickle Rick"}}}}) {
results {
...on Rick_Character {
id
name
}
}
}
}

maxDepth can also be used in combination with selectionSet. In this example, we want to index all fields at depth 1 (id, name, etc) and also deeply index episode on top of that. If we just used a custom selectionSet by itself, we’d have to specify all the depth 1 fields in the selectionSet and update it if the schema changes. Instead, we can get the best of both worlds by specifying "maxDepth": 1 along with the following selectionSet:

{
episode {
name
created
air_date
characters {
id
name
}
episode
}
}

This results in an indexing query:

{
Rick_characters {
info {
pages
}
results {
id
name
status
species
type
gender
image
created
episode {
name
created
air_date
characters {
id
name
}
episode
}
}
}
}
ignoreFields

The array of string names of fields to ignore when using the maxDepth setting.

triggers (required)

This array contains objects that define the conditions under which the indexing function will trigger its queries. See the section on valid triggers properties below for more information.

Example:

  "triggers": [
{
"type": "schedule",
"query": "list",
"interval": 1
},
{
"type": "webhook",
"query": "get",
"service": "yourServiceIdHere",
"events": ["names", "of", "events", "here"]
}
]

searchSummaryField

This property takes a string that will be the name of the field in the indexed shape that you want to use as the searchSummary. Useful for customizing the results returned for this shape when running the search query against your project's API.

Learn more about using the search query in our search recipe.

idField

This property takes a string that is the name of the property in the indexed shape that will return the shape's ID. Useful for customizing the results for this shape when running the search query on your project's API.

NOTE

If the shape being indexed doesn't have a field called id, this should be set to whichever field is the actual unique identifier.

Valid triggers properties

type** (required)

A string defining the trigger's type. Valid values are schedule and webhook.

query

A string specifying which query the trigger corresponds to. Valid values are get and list.

interval** (required for schedule)

Valid only for schedule triggers. The integer number of minutes between each time the indexing function will run.

service (required for webhook)

Valid only for webhook triggers. The string ID of the service in your schema that has the webhooks to trigger this query.

events (required for webhook)

Valid only for webhook triggers. The array of string names of webhook events that can trigger this query.

Example:

  "triggers": [
{
"type": "schedule",
"query": "list",
"interval": 1
},
{
"type": "webhook",
"query": "get",
"service": "bigcommerce",
"events": ["store/cart/created"]
}
]

Shape Cache Example

This example shows how you configure a Stripe_Customer shape for indexing:

{
"Stripe_Customer": {
"name": "Stripe_Customer",
"id": "Stripe_Customer",
"title": "Stripe_Customer",
"schema": {"extends": [{"@ref": "stripe:Customer"}]},
"cache": {
"enabled": true,
"fragment": {
"maxDepth": 2
},
"triggers": [{"type": "schedule", "query": "list", "interval": 1440}]
},
"loaders": {
"list": {
"query": "Stripe_listCustomers",
"pagination": {
"type": "cursor",
"cursorPath": "data[(@.length-1)].id",
"itemsPath": "data",
"hasMorePath": "has_more",
"cursorArg": "starting_after",
"pageSizeArg": "limit"
}
}
}
}
}

pagination

To understand the pagination configuration refer to this example response from Stripe_listCustomers:

Consider this response object from the listCustomers query supplied by our Stripe integration

{
"object": "list",
"url": "/v1/customers",
"has_more": false,
"data": [
{
"id": "cus_KmAUtL4NbulKr4",
"object": "customer",
"address": null,
"balance": 0,
"created": 1639492731,
"currency": "usd",
"default_source": null,
"delinquent": false,
"description": null,
"discount": null,
"email": null,
"invoice_prefix": "F5DABA5",
"invoice_settings": {
"custom_fields": null,
"default_payment_method": null,
"footer": null
},
"livemode": false,
"metadata": {},
"name": null,
"next_invoice_sequence": 1,
"phone": null,
"preferred_locales": [],
"shipping": null,
"tax_exempt": "none"
}
]
}

type — Stripe's API uses cursor-based pagination, so the type in the pagination object should be cursor.

cursorPath — Because Stripe's API returns no specific cursor value, we'll specify the path to the cursor as a jsonPath string. This string will point to the id of the last object in the data array in the response object.

itemsPath — This is a jsonPath string specifying the path to the array of items expected in the response object. In this case, that array is called data.

hasMorePath — Stripe's API provides a has_more field at the root level of the response object, so you should set this value to "has_more".

cursorArg — You can check Stripe's documentation for the possible cursor arguments.

pageSizeArg — As Stripe's documentation shows, list queries take a limit argument to indicate the size of each result.

Learn more in our shapes guide.

services

By connecting new Services to your project schema, you can add shapes, queries, and mutations from 3rd party sources to your API. This allows you to mesh together exactly the API you need to interact with all of the services your app or business depends upon.

Services use an encrypted configuration to keep auth tokens secret. For this reason, services can only be added to a schema through the web client or through the Admin API.

Guides

Adding GraphQL services, shapes, and queries

Adding REST services, shapes, and queries

queries & mutations

Queries and Mutations in your schema map directly to queries and mutations in your project's GraphQL API. In the schema you can define behaviors for these queries and mutations that span one or more services.

Though they describe different functionality, query and mutation objects both have the same set of properties:

shape (required)

This string property specifies which shape is returned by the query or mutation. The shape can refer to a shape in the schema's shapes object or a shape that's available on a service schema.

resolver (required)

This configuration object describes the resolver for a query or mutation.

description

This string property provides more detail about what the query or mutation is for. This will be displayed in the automatically-generated GraphQL API docs.

args

The args field takes the name of one of the shapes you have defined in your schema as a string. This shape will be used to define the arguments your query or mutation can take as inputs. If your query or mutation takes no arguments, you can omit this string.

Guides

Working with Queries & Mutations

forms

Forms define a visual interface for editing Shapes. In most cases, users do not need to edit the forms object directly. The forms object is populated with new data when a user creates shapes in the visual editor.

Example

"Sneakers": {
"default": {
"order": ["sneakerShopId", "sneakerShop"],
"properties": {
"sneakerShopId": {
"instructions": "Format: gid://shopify/Product/11111111",
"label": "product ID",
"widget": "serviceObjectId",
"provider": "shopify",
"serviceObjectType": "product",
"service": "sneaker-shop"
},
"sneakerShop": {
"properties": {
"title": {
"widget": "serviceObjectProperty",
"provider": "shopify"
}
},
"widget": "shopify",
"wrapper": "shopifyServiceWrapper",
"order": ["title"]
}
}
}
}

workflows

Workflows describe the status of a shape item. Every schema starts with a default workflow with two steps: Disabled and Enabled.

Each step can be configured to have a custom name, title, and color. Steps have a live value to indicate whether items in the state should be returned in list queries.

Developer plans are limited to the default workflow. Professional projects can add new workflows and specify them on a per-shape basis.

Example

This is the default workflow that is applied to every project. It has two states: Disabled and Enabled.

"default": {
"name": "default",
"title": "Default",
"steps": [
{
"name": "disabled",
"title": "Disabled",
"key": "r1uCfi4ZL",
"color": "#bdbdbd",
"live": false
},
{
"name": "enabled",
"title": "Enabled",
"key": "rkhRGs4WL",
"color": "#5cd79b",
"live": true
}
]
}

locales

Locales are an array of IETF language tags your project supports for internationalization. By default, projects have a single en locale which is also the default locale.

Developer projects have a limited number of locales. Professional projects have access to additional locales.

Metadata properties

Also at the root of the schema are a number of properties that are mainly used by the web client and API. As a schema author, you should almost never need to edit these fields.

version

This is the revision number of your schema. Every time your project schema is updated, this value should be incremented. The version is used to identify versions in your schema's history.

projectId

The ID of the project this schema belongs to.

defaultLocale

The locale that should be preferred when creating new Shape items. This must be an entry in the **locales** array.

author

The ID of the user who created the schema.

created

The date the schema was created

updated

The date the schema was last updated

schemaVersion

The version of the schema format your project is using. We increase the version as we make breaking changes to the schema format. The current version is 3.

apiVersion

The version of the API your project is using. We increase the version as we make breaking changes to the API endpoints.The current version is 2.

Resolvers

A resolver is a function that executes when a query or mutation is called, or a shape field is accessed. A resolver is required on queries and mutations, and may be used in a shape property.

Below are the valid properties for a resolver.

name (required)

The name of the resolver your query or mutation should use. There are three types of resolvers:

  1. GraphQL resolvers
  2. REST resolvers
  3. ShapeDB resolvers

service (required)

Specifies the endpoint and authentication configuration the resolver should use, identified by a connected service's slug. For example:

  • rick-andmorty
  • open-weather
  • shapedb

options

Each type of resolver—GraphQL, REST, or ShapeDB—takes its own set of options. These are specified in the Built-in resolvers section below.

Mapping arguments

Because a resolver can be used to fetch data from remote APIs, available values can be mapped in a query's context to the inputs of a remote API's query.

That is to say, if a resolver is executing a query that requires an input, you can map data to that input.

There are four objects that can be used to map data to a query's input:

  • args for graphql queries
  • json for queries that expect json
  • form for queries that expect a form
  • body for queries that expect a string
argsMapping is deprecated

We previously recommended using the argsMapping object instead of the above options, but that method has been deprecated.

Instead, use one of the above objects.

To learn more about argsMapping, check out our deprecated page.

In general, you map values to query inputs using parameter ops, which means the input object has an ops property with an array of objects in them. These objects will contain mappings. Here's a generic example that maps arguments from a query to JSON:

"resolver": {
"name": "rest:post",
"service": "service-name-here",
"path": "/service-endpoint-here",
"json": {
"ops": [
{"path": "customer.source_id", "mapping": "$args.email"},
{"path": "status", "mapping": "$args.status"},
{"path": "amount", "mapping": "$args.amount"},
{"path": "items", "mapping": "$args.items"},
{"path": "items[*].id", "op": "remove"},
{"path": "items[*].name", "op": "remove"}
]
}
}

You can figure out what inputs your service requires by reading their docs. Once you know that, it's a matter of mapping data available to your query to the service's query's inputs.

The following sections will give examples of the different ways you can map inputs.

args

Necessary for mapping values to graphql query arguments. Shopify Admin, Shopify Storefront, and BigCommerce Storefront are popular examples of graphql APIs you might use.

For example, let's say you have a getMyCustomer query that fetched a Shopify customer. Fetching a customer from Shopify Storefront's API requires a customerAccessToken. Assuming you're passing an access token to the getMyCustomer query, you would map that argument to the Shopify query in the resolver like this:

"getMyCustomer": {
"resolver": {
"name": "graphql:query",
"service": "shopify-storefront",
"args": {
"type": "object",
"properties": {
"customer_access_token": {
"type": "string",
"description": "The customer access token necessary to fetch this customer from shopify."
}
},
"required": ["customer_access_token"]
},
"fieldName": "customer",
"args": {
"ops": [
{"path": "customerAccessToken", "mapping": "$args.customer_access_token"}
]
}
},
"shape": "ShopifyStorefront_Customer"
},

json

Necessary for mapping values to the input of a query that expects JSON.

For example, if you had a createMyCustomer query that created a customer in Shopify's REST Admin API using their /customers.json endpoint, your resolver might look like this:

"resolver": {
"name": "rest:post",
"service": "shopify",
"path": "/admin/api/2022-04/customers.json",
"options": {
"endpoint": "https://deluxe-sample-project.myshopify.com/"
},
"json": {
"ops": [
{
"path": "customer.first_name",
"mapping": "$args.input.firstName"
},
{
"path": "customer.last_name",
"mapping": "$args.input.lastName"
},
{"path": "customer.email", "mapping": "$args.input.email"},
{
"path": "customer.password",
"mapping": "$args.input.password"
},
{
"path": "customer.password_confirmation",
"mapping": "$args.input.password"
},
{"path": "customer.phone", "mapping": "$args.input.phone"},
{"path": "customer.send_email_welcome", "value": false},
{"path": "customer.verified_email", "value": true}
]
}
}

form

Necessary for mapping values to the input of a query that expects form data.

To pass url-encoded form data, you could use the $ character to map it directly to the input rather than to any specific arguments or paths. Your resolver might look like this:

"resolver": {
"name": "rest:post",
"service": "shopify",
"path": "/admin/api/2022-04/customers.json",
"options": {
"endpoint": "https://deluxe-sample-project.myshopify.com/"
},
"form": {
"ops": [
{"path": "$", "mapping": "$args.input"}
]
}
}

body

Necessary for mapping values to the input of a query that expects an object. Can also be used to map values to the input a query that expects a string.

For example, if you wanted to make a query that added a newsletter member to a Klaviyo email list, you would have to use their /members endpoint, which expects an object.

Here's the example curl request from their docs:

curl --request POST \
--url 'https://a.klaviyo.com/api/v2/list/LIST_ID/members?api_key=API_KEY' \
--header 'Accept: application/json' \
--header 'Content-Type: application/json' \
--data '
{
"profiles": [
{
"email": "george.washington@klaviyo.com"
},
{
"phone_number": "+13239169023"
}
]
}
'

Here's what the resolver for a subscribeMyEmailToNewsletter query that adds users to a mailing list might look like:

"subscribeMyEmailToNewsletter": {
"shape": "Klaviyo_AddMembersResponse",
"resolver": {
"name": "rest:post",
"service": "klaviyo",
"body": {
"ops": [{"path": "profiles[0].email", "mapping": "$claims.email"}],
"serialize": {"content": {"contentType": "application/json"}}
},
"path": {
"ops": [{"path": "list_id", "mapping": "$args.list_id"}],
"serialize": {
"template": "/v2/list/{list_id}/members",
"paths": {"list_id": {"style": "simple"}}
}
},
"headers": {
"ops": [{"path": "content-type", "value": "application/json"}]
},
"results": {"ops": [{"path": "items", "mapping": "$resolvers[0]"}]}
},
"args": {
"type": "object",
"properties": {"list_id": {"type": "string"}},
"required": ["list_id"]
}
},

In depth input mapping

It's important to remember that "input" must first be defined as a property on your args shape. Also, it does NOT have to be called "input". It could be anything. But it has to be defined in your args shape, and then must be referenced by the same name in your args/json/body/form.

Your args can take the name of a shape in your schema, as described in this section above, or a JSON object literal that defines the shape you want. Your args mapping will then depend on the properties of the shape you've assigned to the args property.

As an example, here is a shape you could use as args for a query:

  "ExampleArgs": {
"id": "your-custom-id",
"name": "ExampleArgs",
"title": "ExampleArgs",
"schema": {
"type": "object",
"properties": {
"input": {"type": "object"},
}
}

And if you wanted to make a mutation with arguments defined in the above shape, then you'd add your above shape to your mutation's args property:

  "exampleMutation": {
"description": "Makes an example mutation",
"shape": "ShapeOfResponse",
"args": "ExampleArgs",
"resolver": {
"name": "rest:post",
"service": "your-service-slug",
"options": {"path": "your-endpoint-path"},
"form": {
"ops": [
{"path":"$", "mapping":"args.input"}
]
}
}
}

Alternatively, if you don't want to define the shape of your args separately, your mutation shape could look like this:

"exampleMutation": {
"description": "Makes an example mutation",
"shape": "ShapeOfResponse",
"args": {
"type": "object",
"properties": {
"input": {"type": "object"},
},
"resolver": {
"name": "rest:post",
"service": "your-service-slug",
"options": {"path": "your-endpoint-path"},
"form": {
"ops": [
{"path":"$", "mapping":"args.input"}
]
}
}
}
NOTE

If you're using the body property to map a string to an input, you have to set the type of your input argument to "string":

  "ExampleArgs": {
"id": "your-custom-id",
"name": "ExampleArgs",
"title": "ExampleArgs",
"schema": {
"type": "object",
"properties": {
"input": {"type": "string"},
}
}

Or:

  "args": {
"type": "object",
"properties": {
"input": {"type": "string"}
}
}

You can learn more about mapping GraphQL query inputs to REST parameters in our doc on the subject.

Appending inputs to your query's path

Sometimes you may want to take a user's input and append it to the path of the endpoint that the resolver will hit when executing the query.

For example, you may want to take an input, which the user might specify as "myCustomPath", and append it to the resolver's path so the query's request goes to https://your-custom-endpoint.com/your-endpoint-path/myCustomPath.

To do this, add a path field to your query shape's resolver object with your preferred path set as the value, as shown below:

"resolver": {
"name": "rest:get",
"service": "service-name-here",
"path": "/your-endpoint-path",
...
}

The above will make the resolver add /your-endpoint-path to the service's endpoint when making the request. To splice the user's query input into the endpoint path, use the same parameter ops method that applies to args.

You would map the query's input to a variable name of your choosing, for example id, then use the serialize property of the resolver's path object to create a string template for the path the resolver should use. Wrap the variable name in brackets {id}, and insert it into the path where appropriate.

The example below demonstrates this:

"resolver": {
"name": "rest:get",
"service": "service-name-here",
"path": {
"ops": [
{"path": "id", "mapping": "$args.id"}
],
"serialize": {"template": "/your-endpoint-path/{id}"}
}
...
}

If you want to assign a value without using an input to the query, you can use value instead of mapping in the parameter ops.

Here's what the resolver looks like if you do that:

"resolver": {
"name": "rest:get",
"service": "service-name-here",
"path": {
"ops": [
{"path": "id", "value": 123}
],
"serialize": {"template": "/your-endpoint-path/{id}"}
}
...
}

Your resolver would hit this path: /endpoint/your-endpoint-path/123.

Adding inputs as query parameters

You can also map input args to query parameters on the resolver's endpoint path.

This requires using a searchParams object with parameter ops to map the input values to the query parameter name.

For example:

"resolver": {
"name": "rest:get",
"service": "recharge",
"path": "/customers",
"searchParams": {
"ops": [{"path": "email", "mapping": "$args.email"}]
}
},

In the above example, the email arg's value is mapped to the email query parameter, which means the endpoint the resolver hits will look like this:

/customers?email=example@email.com

As with setting values for the resolver path, you can use value to assign data to a query parameter directly.

"resolver": {
"name": "rest:get",
"service": "recharge",
"path": "/customers",
"searchParams": {
"ops": [{"path": "email", "value": "example@email.com"}]
}
},

Here's a full example of a query that gets subscription information from Recharge by composing two resolvers that use searchParams:

"getMySubscriptions": {
"shape": "Recharge_SubscriptionList",
"resolver": {
"compose": [
{
"id": "customerByEmail",
"name": "rest:get",
"service": "recharge",
"path": "/customers",
"searchParams": {
"ops": [{"path": "email", "mapping": "$claims.email"}]
}
},
{
"id": "subscriptions",
"name": "rest:get",
"service": "recharge",
"path": "/subscriptions",
"searchParams": {
"ops": [
{
"path": "shopify_customer_id",
"mapping": "$resolvers.customerByEmail.customers[0].shopify_customer_id"
}
]
}
}
]
"results": {
"ops": [
{
"path": "$",
"mapping": "get", {"path": "$resolvers.subscriptions"}
}
]
},
},
"description": "Get my subscriptions"
},

Mapping args to your query's headers

Custom headers can be added to a resolver with the headers object. Again, use parameter ops to map the header values.

For example, here's how you set the "content-type" header to "application/json":

"resolver": {
"name": "rest:post",
"service": "service-name-here",
"headers": {
"ops": [{"path": "content-type", "value": "application/json"}]
}
},

You can also map input arguments to headers:

"resolver": {
"name": "rest:post",
"service": "service-name-here",
"headers": {
"ops": [{"path": "Authorization", "mapping": "$args.apiKey"}]
}
},

Mapping resolver results

You can also map a service endpoint's response results to the expected shape of the query's response.

Here's an example of a mutation that creates a checkout in Shopify by composing multiple resolvers, then mapping the results to the shape that the query expects:

"createMyCheckoutSession": {
"shape": "shopify-storefront:Cart",
"resolver": {
"results": {
"ops": [{"path": "$", "mapping": "$resolvers.getShopifyCart"}]
},
"compose": [
{
"id": "createShopifyCart",
"name": "graphql:mutation",
"service": "shopify-storefront",
"fieldName": "cartCreate",
"options": {"selectionSet": "{cart{id}}"},
"args": {
"ops": [
{"path": "input.lines", "mapping": "$args.lines"},
{
"path": "input.buyerIdentity.email",
"mapping": "$claims.email"
}
]
}
},
{
"id": "getShopifyCart",
"name": "graphql:query",
"service": "shopify-storefront",
"fieldName": "cart",
"options": {"selectionSet": "{checkoutUrl}"},
"args": {
"ops": [
{
"path": "id",
"mapping": "$resolvers.createShopifyCart.cart.id"
}
]
}
}
]
},
"description": "Create a Shopify storefront checkout session.",
"args": {
"type": "object",
"properties": {
"lines": {
"type": "array",
"items": {
"type": "object",
"properties": {
"quantity": {"type": "integer"},
"merchandiseId": {"type": "string"},
"sellingPlanId": {"type": "string"}
}
}
}
},
"required": ["lines"]
}
}

Resolver types

A library of built-in resolvers are available for writing your own queries and mutations.

GraphQL resolvers

When writing queries and mutations that use a GraphQL service, you'll have access to two resolvers that execute queries and mutations on the connected service.

graphql:query
graphql:mutation
Options:
fieldName

The name of the query or mutation on the GraphQL service's schema that the resolver will use

selectionSet

Useful when doing mutation composition, this allows the schema author to override or modify the selectionSet of fieldName

ttl

The number of seconds to cache query results for. This setting only applies to graphl:query resolvers.

timeout

The number of milliseconds to allowed before timing out. The default is no timeout.

REST resolvers

When writing queries and mutations that use a REST service, you'll have access to resolvers that execute many of the standard HTTP request methods on the connected service.

rest:get
rest:head
rest:post
rest:put
rest:patch
rest:delete
Options:
path

The resource path your resolver is accessing on the REST service. This is appended to the service's base path when making requests.

The path can use variable values, like /character/{id}. This variable would be set using parameters ops and the serialize property of the path object. For example:

"path": {
"ops": [
{
"path": "productId",
"mapping": "$args.product_id"
}
],
"serialize": {"template": "/products/{productId}"}
},
ttl

The number of seconds to cache request results for. This setting only applies to rest:get resolvers.

timeout

The number of milliseconds to allowed before timing out. The default is no timeout.

retry

A number or object that is passed through to the got retry config.

If a retry configuration is specified in both the resolver options and the service configuration, the retry configurations are merged with the resolver taking precedence.

ShapeDB resolvers

There are also ShapeDB-specific resolvers that allow you to execute common CRUD actions on model-annotated shapes.

shapedb:get
shapedb:list
shapedb:create
shapedb:update
shapedb:delete
shapedb:duplicate
Options:
model

The name of the model this resolver should act upon.

Resolver composition

A resolver can compose multiple resolvers in a pipeline of resolution $resolvers.

Each resolver in the compose array is executed serially and referable by its id property.

id property

On any resolver step you can provide an id property which gives you a stable, easy-to-use identifier for use in your expressions and directives.

Example

The id below will allow you to refer to this step elsewhere in the resolver as $resolvers.shapedbUpdate.

{
"if": "!isEmpty(args.input._id)",
"id": "shapedbUpdate",
"name": "shapedb:update",
"service": "local",
"options": {
"model": "Product"
}
}

currentQuery context

The context of a query is available for use in directives and expressions. The context is comprised of:

  • source The return value of this field's parent resolver.
  • args The args provided to the parent query, essentially, your input.
  • $resolvers The results of each step in the compose pipeline. These are available as array index values, $resolvers[0] and optionally with ids you supply in the step config, for example, $resolvers.my-id.foo.
  • results Deprecated, an alias of $resolvers
  • previousStep The output of the previous step or directive, if there was one.

if expressions

if expressions control the execution of a step in the resolver pipeline. If the expression evaluates as true, the step runs. A step without an if will always run.

JavaScript-style logical operations and a limited number of functions from [lodash/fp](https://gist.github.com/jfmengels/6b973b69c491375117dc) are built-in to our expression engine.

A simple boolean evaluation looks like this:

"if": "args.input._id == 123"

In this next example, we're accessing a property in the args array (which may or may not exist), and utilizing the isEmpty function. isEmpty returns a boolean, so this expression can evaluate on its own:

"if": "isEmpty(args.input._id)"

Groupings, negations and more complicated expressions are also possible:

"if": "!isEmpty($resolvers.graphQLAPIUpdate.result.shopifyProductId) || !isEmpty($resolvers.graphQLAPICreate.result.shopifyProductId)"
Available lodash/fp functions

Documentation for these functions is on GitHub.

  • at
  • add
  • camelCase
  • capitalize
  • ceil
  • chunk
  • clamp
  • cloneWith
  • compact
  • concat
  • cond
  • conforms
  • conformsTo
  • constant
  • countBy
  • divide
  • each
  • eachRight
  • endsWith
  • entries
  • entriesIn
  • eq
  • escape
  • every
  • filter
  • find
  • first
  • flatMap
  • floor
  • flow
  • forEach
  • forEachRight
  • forIn
  • forInRight
  • forOwn
  • forOwnRight
  • fromPairs
  • get
  • groupBy
  • gt
  • gte
  • has
  • hasIn
  • inRange
  • includes
  • indexOf
  • intersection
  • invert
  • isArguments
  • isArray
  • isArrayBuffer
  • isArrayLike
  • isArrayLikeObject
  • isBoolean
  • isBuffer
  • isDate
  • isElement
  • isEmpty
  • isEqual
  • isEqualWith
  • isError
  • isFinite
  • isFunction
  • isInteger
  • isLength
  • isMap
  • isMatch
  • isMatchWith
  • isNaN
  • isNative
  • isNil
  • isNull
  • isNumber
  • isObject
  • isObjectLike
  • isPlainObject
  • isRegExp
  • isSafeInteger
  • isSet
  • isString
  • isSymbol
  • isTypedArray
  • isUndefined
  • isWeakMap
  • isWeakSet
  • join
  • kebabCase
  • keyBy
  • keys
  • keysIn
  • last
  • lastIndexOf
  • lowerCase
  • lowerFirst
  • lt
  • lte
  • map
  • matches
  • matchesProperty
  • max
  • maxBy
  • mean
  • meanBy
  • min
  • minBy
  • multiply
  • now
  • nth
  • omit
  • orderBy
  • partition
  • pick
  • pickBy
  • property
  • propertyOf
  • pull
  • pullAt
  • range
  • rangeRight
  • reject
  • remove
  • replace
  • result
  • reverse
  • round
  • set
  • size
  • slice
  • snakeCase
  • some
  • sortBy
  • sortedIndex
  • sortedIndexBy
  • sortedIndexOf
  • sortedLastIndex
  • sortedLastIndexBy
  • sortedLastIndexOf
  • sortedUniq
  • sortedUniqBy
  • split
  • startCase
  • startsWith
  • subtract
  • sum
  • sumBy
  • tail
  • take
  • takeRight
  • template
  • times
  • toArray
  • toFinite
  • toInteger
  • toLength
  • toLower
  • toNumber
  • toPairs
  • toPairsIn
  • toPath
  • toPlainObject
  • toSafeInteger
  • toString
  • toUpper
  • trim
  • trimEnd
  • trimStart
  • truncate
  • unescape
  • union
  • unionBy
  • uniq
  • uniqBy
  • uniqueId
  • upperCase
  • upperFirst
  • values
  • words
  • xor
Resolver composition example

This mutation composes several resolvers together in a pipeline to add a new mutation, upsertMyCustomer, that will update a customer and in the connected Shopify service. This example uses many resolver concepts described above.

"upsertMyCustomer": {
"shape": "Profile",
"resolver": {
"results": {
"ops": [
{"path": "$", "mapping": "$resolvers.updatedProfile"},
{"path": "$", "mapping": "$resolvers.existingProfile"}
]
},
"compose": [
{
"if": "!isEmpty($claims.sub)",
"id": "existingProfile",
"name": "shapedb:find",
"service": "shapedb",
"shapeName": "Profile",
"args": {"ops": [{"path": "where.id.eq", "mapping": "$claims.sub"}]}
},
{
"if": "!isEmpty($claims.sub) && !isEmpty($claims.email) && isEmpty($resolvers.existingProfile.shopifyCustomerId)",
"id": "existingCustomerByEmail",
"name": "graphql:query",
"service": "shopify-admin",
"fieldName": "customers",
"options": {"selectionSet": "{edges{node{id}}}"},
"args": {
"ops": [
{"path": "first", "value": 1},
{
"path": "query",
"mapping": [
["get", {"path": "$source.email"}],
["prepend", {"text": "email:"}]
]
}
]
}
},
{
"if": "!isEmpty($claims.sub) && !isEmpty($claims.email) && !isEmpty($resolvers.existingProfile.shopifyCustomerId)",
"id": "existingCustomerById",
"name": "graphql:query",
"service": "shopify-admin",
"fieldName": "customer",
"options": {
"selectionSet": "{id firstName lastName defaultAddress{address1 address2 firstName lastName city province provinceCode zip country}}"
},
"args": {
"ops": [
{
"path": "id",
"mapping": [
[
"get",
{"path": "$resolvers.existingProfile.shopifyCustomerId"}
]
]
}
]
}
},
{
"if": "!isEmpty($claims.sub) && !isEmpty($claims.email) && (!isEmpty($resolvers.existingProfile.shopifyCustomerId) || !isEmpty($resolvers.existingCustomerByEmail.edges[0].node.id) || !isEmpty($resolvers.existingCustomerById.id))",
"id": "updatedShopifyCustomer",
"name": "graphql:mutation",
"service": "shopify-admin",
"fieldName": "customerUpdate",
"options": {
"selectionSet": "{customer{id firstName lastName defaultAddress{address1 address2 firstName lastName city province provinceCode zip country}} userErrors{field message}}"
},
"args": {
"ops": [
{"path": "input.email", "mapping": "$claims.email"},
{
"path": "input.id",
"mapping": [
[
"get",
{"path": "$resolvers.existingProfile.shopifyCustomerId"}
],
[
"get",
{"path": "$resolvers.createShopifyCustomer.customer.id"}
],
[
"get",
{
"path": "$resolvers.existingCustomerByEmail.edges[0].node.id"
}
],
["get", {"path": "$resolvers.existingCustomerById.id"}],
["get", {"path": "$args.id"}]
]
},
{"path": "input.firstName", "mapping": "$args.firstName"},
{"path": "input.lastName", "mapping": "$args.lastName"},
{"path": "input.note", "mapping": "$args.description"},
{
"path": "input.addresses.firstName",
"mapping": "$args.firstName"
},
{
"path": "input.addresses.lastName",
"mapping": "$args.lastName"
},
{
"path": "input.addresses.address1",
"mapping": "$args.defaultAddress.address1"
},
{
"path": "input.addresses.address2",
"mapping": "$args.defaultAddress.address2"
},
{
"path": "input.addresses.city",
"mapping": "$args.defaultAddress.city"
},
{
"path": "input.addresses.country",
"mapping": "$args.defaultAddress.country"
},
{
"path": "input.addresses.zip",
"mapping": "$args.defaultAddress.zip"
},
{
"path": "input.addresses.provinceCode",
"mapping": "$args.defaultAddress.provinceCode"
}
]
}
},
{
"if": "!isEmpty($claims.sub) && !isEmpty($claims.email) && !isEmpty($resolvers.existingProfile)",
"id": "updatedProfile",
"name": "shapedb:update",
"service": "shapedb",
"shapeName": "Profile",
"args": {
"ops": [
{
"path": "input._id",
"mapping": "$resolvers.existingProfile._id"
},
{
"path": "input.shopifyCustomerId",
"mapping": [
[
"get",
{"path": "$resolvers.updatedShopifyCustomer.customer.id"}
],
[
"get",
{
"path": "$resolvers.existingCustomerByEmail.edges[0].node.id"
}
],
[
"get",
{"path": "$resolvers.existingProfile.shopifyCustomerId"}
]
]
},
{"path": "input.firstName", "mapping": "$args.firstName"},
{"path": "input.lastName", "mapping": "$args.lastName"}
]
}
}
]
},
"description": "Upsert my customer.",
"args": {
"type": "object",
"properties": {
"id": {"type": "string"},
"sub": {"type": "string"},
"email": {"type": "string"},
"firstName": {"type": "string"},
"lastName": {"type": "string"},
"description": {"type": "string"},
"defaultAddress": {
"type": "object",
"properties": {
"address1": {"type": "string"},
"address2": {"type": "string"},
"city": {"type": "string"},
"country": {"type": "string"},
"zip": {"type": "string"},
"provinceCode": {"type": "string"},
"province": {"type": "string"}
}
}
}
}
},

Here's a step-by-step walkthrough of what each resolver is doing:

"existingProfile"

Checks if there's an existing profile in the project's database. Uses $claims, which are sent in the query's headers by auth0. Read more about $claims in our auth0 provider guide.

"existingCustomerByEmail"

If the existingProfile resolver above failed to retrieve a profile with a Shopify customer ID, try to find a customer with the Shopify Admin API, and get the customer's ID.

"existingCustomerById"

If the existingProfile resolver successfully retrieved a profile with a Shopify customer ID, fetch the customer from Shopify.

"existingCustomerById"

If the existingProfile, existingCustomerByEmail, or existingCustomerById resolvers successfully retrieved a profile and the corresponding Shopify customer information, update that customer in Shopify with the input args provided to the query.

"existingCustomerById"

If the existingProfile resolver successfully retrieved a profile, update that profile with the input args provided to the query.

Mappings

Mappings describe the location of a property's data in shapeDB or in a connected service. A shape property with a @mapping reads and writes data to the provided location. Custom shape properties require a @mapping.

The value of a mapping follows the format serviceId:shapeName.key.

For shapeDB data, the key is randomly generated by the client when a field is added. This is done to allow Shape field names to be renamed without performing migrations. (shortId is used to generate database keys)

For connected services, the key is the name of a property on a remote shape.

A @mapping can also be an array of locations. In this case, mappings that produce values are preferred in order.

Query mappings

In queries, the mapping is used to determine where to get the value of the current property.

In the following example, the @mapping is pointing to the local shapeDB database table Book and using the K66gvuh1h column of the row to select the value:

{
"title": {
"type": "string",
"@mapping": "shapedb:Book.K66gvuh1h"
}
}

In this next example, @mapping is pointing to the Product type in the Shopify GraphQL API and using the descriptionHTML property to fetch the value of html:

{
"html": {
"type": "string",
"@mapping": "shopify:my-store:Product.descriptionHtml"
}
}

Mutation mappings

In mutations, the mapping is used to map data of the given shape back to its original type in GraphQL.

Models

A Model is a shape with data that is viewable and editable in the admin UI and through your project's GraphQL API. Models can reference other shapes in their schemas as objects or repeaters, and they can have database relationships with other Models. Models can also be remotely accessed from connected services.

Shapes can defined by as models by providing the model key with the properties:

  • type: ('single' | 'multiple' | 'taxonomy')

Any Shape that's a model will also have queries and mutations automatically created for common operations like Get, List, Create, Update, and Delete.

Relationships

One-to-one, many-to-one, and many-to-many relationships can also be created between Models. Relationships are configured by setting the @relationship on a shape's property.

Back references

@backreference controls whether a reference will automatically be created from the referred-to type shape back to the referring shape in a relationship.

@backreference can be either a booleantrue create a dynamic reference that uses the shape name to generate a reference. For example, "@backreference": true on the shape Post which is related to the shape Author would create a field on Author types called postSet.

If you provide @backreference as an object, you can specify an explicit name for your back reference.

Examples

boolean @backreference
{
"shapes": {
"Post": {
"properties": {
"author": {
"$ref": "#/shapes/TSRelationship/schema",
"@relationship": {
"shapeIds": ["Sk6yWoljg"],
"type": "single"
},
"@backreference": true
}
}
}
}
}
object @backreference
{
"shapes": {
"Post": {
"properties": {
"author": {
"$ref": "#/shapes/TSRelationship/schema",
"@relationship": {
"shapeIds": ["Sk6yWoljg"],
"type": "single"
},
"@backreference": {
"name": "postReference"
}
}
}
}
}
}

References

By design, the schema is flat. Rather than using deeply nested and potentially repeated objects, we create references using the @ref property. References link to other objects in the schema file or in a connected service's schema.

Objects in the schema are referenced using the local:ShapeName syntax, while remote objects are referenced using the service-key:TypeName syntax.

References can be used in queries and mutations in place of a shape's name in the args and shape keys. They can also be set in a shape's inner schema properties using the @ref key.

oneOf

The oneOf property can be used to create union fields.

Examples

  • An array of @ref schemas
oneOf example schema fragment created by adding a shape array widget
{
"type": "array",
"items": {"oneOf": [{"@ref": "local:Dog"}, {"@ref": "local:Cat"}]}
}
  • An object with @ref schemas
oneOf example schema fragment as an object
"featuredPet": {"oneOf": [{"@ref": "local:Dog"}, {"@ref": "local:Cat"}]}

extends

The extends keyword can be used in a shape's schema to extend another shape. This is most useful when working with remote shapes where you can't modify the original shape, but want to add specific properties.

Examples

This example shows a how to extend a Shopify Product with a Note that is stored in ShapeDB.

{
"Shopify_Product": {
"id": "Shopify_Product",
"name": "Shopify_Product",
"title": "Shopify_Product",
"schema": {
"extends": [
{"@ref": "my-shopify-store:Product"},
{
"type": "object",
"properties": {
"note": {
"@ref": "local:Note",
"@resolver": {
"name": "shapedb:find",
"service": "shapedb",
"shapeName": "Note",
"args": {
"ops": [{"path": "where.productId.eq", "mapping": "$source.id"}]
}
},
"title": "Note"
}
}
}
]
}
},
"Note": {
"id": "a_fjI9J54",
"name": "Note",
"title": "Note",
"workflow": "default",
"model": {"type": "multiple"},
"schema": {
"type": "object",
"properties": {
"productId": {
"@l10n": false,
"type": "string",
"title": "Product Id",
"@mapping": "shapedb:Note.vzxZ2Ld93"
},
"body": {
"minLength": 1,
"type": "string",
"@tag": "mdx",
"title": "Body",
"@mapping": "shapedb:Note.gOv6xZKgg"
}
},
"required": ["productId", "body"]
}
}
}

Built-in objects

The schema also contains some "built-in" objects that are required on every project.

The names of built-in objects are reserved.

We hide those objects when displaying or exporting a schema, and we automatically add them when a schema is created or imported.

Built-in shapes

Asset, JSON, TSRelationship, TSColorHsl, TSColorHsv, TSColorRgb, TSColor, TSStaticSite, TSStaticSiteEnvironmentVariables, TSStaticSiteTriggers

Built-in forms

Asset, TSStaticSite