Schema Spec Reference
Every TakeShape 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 GUI 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 TakeShape 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 TakeShape project, and cannot be directly viewed or configured.
Learn more about the Service Schema:
This specification details all configurable functionality in a TakeShape project's JSON schema file.
Root objects
Root-level objects in a project's schema define the project's high-level functionality.
TakeShape uses the 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 TakeShape's language localization features.
The following sections will explain the functionality and configuration of all of a TakeShape schema's root-level objects.
shapes
A Shape is a schema object for structuring and storing data from one or more sources, including a TakeShape 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
.
"shapes": {
"Review": {
"id": "Review",
"name": "Review",
"title": "A custom Review shape",
"schema": {
...
}
}
}
Shape properties
Because shapes represent data a TakeShape 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.
When using TakeShape's 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"
name
property is important.TakeShape uses 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
andgetShapeList
- 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 TakeShape web client's 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 TakeShape's GUI. This description appears in the Docs Panel.
model
The data model TakeShape should use 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": {
"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:
"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:
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.
"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"}
}
}
}
@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 TakeShape GUI. 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 TakeShape GUI.
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:
"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:
"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 TakeShape projects have them. They contain configuration information for connected API services, like Stripe or ShipEngine. TakeShape 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
.
When a service is connected with a generic REST provider, or a generic GraphQL provider, there is a chance TakeShape will fail to add its API configuration 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.
{
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:
"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 by TakeShape 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 TakeShape 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 inheritence 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"
}
}
}
}
}
}
Rather than deeply nesting and copy-pasting objects to create fields, complex shapes are best composed with the @ref
and @relationship
annotations.
Shape schemas also have a number of hidden properties that are automatically managed by TakeShape, like _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 casesanity:Product
.- Join objects contain the following props
resolver
(required) - A delegate resolver OR shapedb resolverdependencies
- 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 TakeShape's indexing function can find 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 TakeShape's 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:
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.
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 in TakeShape. 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. TakeShape populates the forms
object 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. Learn more about professional plans
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 are limited to 2 locales. Professional projects can have up to 5 locales. Learn more about professional plans
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 TakeShape 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 TakeShape 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 TakeShape 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:
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 TakeShape—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, TakeShape allows you to map values available 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 queriesjson
for queries that expect jsonform
for queries that expect a formbody
for queries that expect a string
argsMapping
is deprecatedWe 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 TakeShape query to the service's query's inputs.
The following sections will give examples of the different ways you can map inputs with TakeShape.
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"}
]
}
}
}
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 paramter 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 aslo 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
TakeShape provides a library of built-in resolvers when you're 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.
TakeShape resolvers
There are also TakeShape-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 TakeShape 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 withid
s 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.takeshapeUpdate.result.shopifyProductId) || !isEmpty($resolvers.takeshapeCreate.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 in TakeShape 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 TakeShape 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 in TakeShape with the input args provided to the query.
Mappings
Mappings describe the location of a property's data in TakeShape 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 TakeShape 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. (TakeShape uses shortId 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 takeshape 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 TakeShape web client 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 TakeShape will automatically create a reference for you from the referred-to type shape back to the referring shape in a relationship.
@backreference
can be either a boolean
— true
tells TakeShape to 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
{
"shapes": {
"Post": {
"properties": {
"author": {
"$ref": "#/shapes/TSRelationship/schema",
"@relationship": {
"shapeIds": ["Sk6yWoljg"],
"type": "single"
},
"@backreference": true
}
}
}
}
}
{
"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
{
"type": "array",
"items": {"oneOf": [{"@ref": "local:Dog"}, {"@ref": "local:Cat"}]}
}
- An object with
@ref
schemas
"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 TakeShape's 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 TakeShape 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