Skip to main content

Spec Reference

Root Objects#

At the root of the schema, you'll find the core objects that power TakeShape projects.

shapes#

A Shape is a schema object for structuring and storing data from one or more sources, including the TakeShape data store and connected services.

All shapes exist inside the root-level schema object shapes. Each shape is defined by an object key, an id field, and a name field.

  • The value of the object key¬†and the name field should always be the same. This value is used in¬†@ref¬†and¬†@mapping¬†annotations; during the automatic generation of queries and mutations for models, like getShape and getShapeList; and is available as¬†_shapeName¬†on an item.
  • The¬†id¬†is a used in the¬†shapeIds¬†property of a¬†@relationship¬†and available as¬†_shapeId¬†on an item.

The additional field title is used for presenting the shape in UIs.

Shapes contain an inner schema that configures their properties. Like the rest of the schema, this is designed to be a flattened object. Instead of deeply nested and potentially repeated objects, complex structures are achieved through linking with the @refargs and @relationship annotations.

Shape schemas also have a number of underscore prefixed properties that are automatically managed by TakeShape, such as _created and _updated to record the dates shape items are created and edited.

Shapes can also be extended by use of the allOf keyword. This is most useful when working with remote shapes where you can't modify the original shape, but want to add specific properties. See the example below.

Guides#

Working with Shapes

Examples#

This example shows a Sneaker shape that saves an ID in TakeShape, but fetches the rest of its data from a connected Shopify service.

Sneaker
"Sneakers": {
"id": "content-type-id",
"name": "Sneakers",
"title": "Sneakers",
"model": {
"type": "multiple"
},
"schema": {
"type": "object",
"properties": {
"sneakerShopId": {
"type": "string",
"@mapping": "takeshape:local:Sneakers.service-id",
"title": "Product ID",
"minLength": 1,
"pattern": "^gid://shopify/Product/\\d+$"
},
"sneakerShop": {
"$ref": "#/shapes/TSShopify_Product/schema",
"@resolver": {
"name": "graphql-query",
"service": "shopify:sneaker-shop",
"options": {
"fieldName": "product"
},
"argsMapping": {
"id": [["jsonPath", { "path": "$.source.sneakerShopId" }]]
}
}
}
},
"required": ["sneakerShopId"]
}
},
"ExtendedSneakers": {
"id": "content-type-id",
"name": "ExtendedSneakers",
"title": "ExtendedSneakers",
"model": {
"type": "multiple"
},
"schema": {
"allOf": [{
"@ref": "local:Sneakers"
}, {
"type": "object",
"properties": {
"sneakerColor": {
"type": "string",
"@mapping": "takeshape:local:Sneakers.service-id",
"title": "Color",
}
}
}
}]
}
},

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: string (required) 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 (we provide an example of a remote shape reference below).
  • resolver: object (required) Configures the resolver for your query or mutation.
  • description: string (optional) Provides more detail about what the query or mutation is for. This will be displayed in the automatically-generated GraphQL API docs (optional)
  • args: string (optional) The args field specifies the name of a Shape for the input arguments required by your resolver. If your query does not need any input, you can omit this.

Guides

Working with Queries & Mutations

forms#

Forms define a visual interface for editing Shapes.

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 & mutations, and may be used in a shape property.

export interface BasicResolver {
name: string;
service: string;
argsMapping?: DirectiveMap | DirectiveConfig[];
resultsMapping?: DirectiveMap | DirectiveConfig[];
options?: {
model?: string;
[k: string]: any;
};
}
  • name The name of the built-in resolver your query or mutation should use. There's three types of built-in resolvers:

    1. [GraphQL resolvers](https://www.notion.so/Core-Concepts-a1933e712b64428d9e72927678a8c724)
    2. [REST resolvers](https://www.notion.so/Core-Concepts-a1933e712b64428d9e72927678a8c724)
    3. [TakeShape resolvers](https://www.notion.so/Core-Concepts-a1933e712b64428d9e72927678a8c724)
  • service

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

    • graphql:rick-andmorty
    • rest:open-weather
    • takeshape:local
  • options (optional)

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

  • argsMapping (optional)

    Maps a query's input args to the input expected by the service's endpoint.

  • resultsMapping (optional)

    Maps a service endpoint's response results to the expected shape of the query's response.

Argument and results mappings are defined using directives, which are a composable pipelines of function. Each directive is a two-element array, which combines an operation like "get" or "set" with a configuration.

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 (optional)

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

  • ttl (optional)

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

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 the argsMapping key pathParams.id

  • ttl (optional)

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

TakeShape Resolvers#

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

takeshape:get
takeshape:list
takeshape:create
takeshape:update
takeshape:delete
takeshape: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 steps.

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 steps.takeshapeUpdate.

    {
    "if": "!isEmpty(args.input._id)",
    "id": "takeshapeUpdate",
    "argsMapping": [["get", {"path": "args"}]],
    "name": "takeshape: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.
  • steps The results of each step in the compose pipeline. These are available as array index values, steps[0] and optionally with ids you supply in the step config, e.g., steps.my-id.foo.
  • results Deprecated, an alias of steps
  • 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(steps.takeshapeUpdate.result.shopifyProductId) || !isEmpty(steps.takeshapeCreate.result.shopifyProductId)"
  • Available lodash/fp functions

    Documentation for these functions is on GitHub.

    at, chunk, compact, concat, cond, conforms, constant, countBy, filter, flatMap, flow, fromPairs, groupBy, intersection, invert, keyBy, keys, keysIn, map, matches, matchesProperty, omit, orderBy, partition, pick, pickBy, property, propertyOf, pull, pullAt, range, rangeRight, reject, remove, reverse, set, slice, sortBy, sortedUniq, sortedUniqBy, split, tail, take, takeRight, toArray, toPairs, toPairsIn, toPath, toPlainObject, union, unionBy, uniq, uniqBy, values, words, xor, entries, entriesIn, add, camelCase, capitalize, ceil, clamp, cloneWith, conformsTo, divide, endsWith, eq, every, find, floor, forEach, forEachRight, forIn, forInRight, forOwn, forOwnRight, get, gt, gte, has, hasIn, includes, indexOf, inRange, 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, last, lastIndexOf, lowerCase, lowerFirst, lt, lte, max, maxBy, mean, meanBy, min, minBy, multiply, nth, now, escape, replace, result, round, size, snakeCase, some, sortedIndex, sortedIndexBy, sortedIndexOf, sortedLastIndex, sortedLastIndexBy, sortedLastIndexOf, startCase, startsWith, subtract, sum, sumBy, template, times, toFinite, toInteger, toLength, toLower, toNumber, toSafeInteger, toString, toUpper, trim, trimEnd, trimStart, truncate, unescape, uniqueId, upperCase, upperFirst, each, eachRight, first

  • Resolver composition example

    This mutation composes several resolvers together in a pipeline to add a new mutation, upsertProduct, that will create or update a Product in TakeShape and in the connected Shopify service. This example uses all the resolver concepts described above.

    "upsertProduct": {
    "description": "Update Product",
    "shape": "UpdateResult<Product>",
    "args": "UpdateArgs<Product>"
    "resolver": {
    "resultsMapping": {
    "result": [["get", { "path": "steps.finalTakeshapeResults" }]]
    },
    "compose": [
    {
    "id": "takeshapeUpdate",
    "name": "takeshape:update",
    "service": "local",
    "if": "!isEmpty(args.input._id)",
    "argsMapping": [["get", { "path": "args" }]],
    "options": {
    "model": "Product"
    }
    },
    {
    "id": "takeshapeCreate",
    "name": "takeshape:create",
    "service": "local",
    "if": "isEmpty(args.input._id)",
    "argsMapping": [["get", { "path": "args" }]],
    "options": {
    "model": "Product"
    }
    },
    {
    "id": "shopifyProductUpdate",
    "name": "graphql:mutation",
    "service": "my-shopify-store",
    "if": "!isEmpty(steps.takeshapeUpdate.result.shopifyProductId) || !isEmpty(steps.takeshapeCreate.result.shopifyProductId)",
    "argsMapping": {
    "input.id": [
    ["get", { "path": "steps.takeshapeUpdate.result.shopifyProductId" }],
    ["get", { "path": "steps.takeshapeCreate.result.shopifyProductId" }]
    ],
    "input.title": [
    ["get", { "path": "steps.takeshapeUpdate.result.name" }],
    ["get", { "path": "steps.takeshapeCreate.result.name" }]
    ]
    },
    "options": {
    "fieldName": "productUpdate",
    "selectionSet": "{ product { id } }"
    }
    },
    {
    "id": "shopifyProductCreate",
    "name": "graphql:mutation",
    "service": "my-shopify-store",
    "if": "isEmpty(steps.takeshapeCreate.result.shopifyProductId) && isEmpty(steps.takeshapeUpdate.result.shopifyProductId)",
    "argsMapping": {
    "input.title": [
    ["get", { "path": "steps.takeshapeUpdate.result.name" }],
    ["get", { "path": "steps.takeshapeCreate.result.name" }]
    ]
    },
    "options": {
    "fieldName": "productCreate",
    "selectionSet": "{ product { id } }"
    }
    },
    {
    "name": "takeshape:update",
    "service": "local",
    "if": "!isEmpty(steps.shopifyProductCreate.product.id)",
    "argsMapping": {
    "input._id": [
    ["get", { "path": "steps.takeshapeCreate.result._id" }],
    ["get", { "path": "steps.takeshapeUpdate.result._id" }]
    ],
    "input.shopifyProductId": [
    ["get", { "path": "steps.shopifyProductCreate.product.id" }]
    ]
    },
    "options": {
    "model": "Product"
    }
    },
    {
    "id": "finalTakeshapeResults",
    "name": "takeshape:get",
    "service": "local",
    "argsMapping": {
    "_id": [
    ["get", { "path": "steps.takeshapeUpdate.result._id" }],
    ["get", { "path": "steps.takeshapeCreate.result._id" }]
    ]
    },
    "options": {
    "model": "Product"
    }
    }
    ]
    },
    }

    Here's a translation for what each step is doing:

    1. Updates an existing Product shape item if an _id argument is provided. It passes through the mutation's args.
    2. Creates a new Product shape item if no _id argument is provided. It passes through the mutation's args.
    3. This step tries to update a Shopify Product that's connected to the TakeShape Product we either updated or created. If the shape's shopifyProductId field is not empty, we'll update its title. Notice how we're using fallback directives in the argsMapping: since only either takeshapeUpdate or takeshapeCreate steps ran, we try to use the values from each.
    4. If no Shopify Product is connected to the TakeShape Product we created or updated, we'll instead run this step to create a new product in Shopify.
    5. If we created a new Shopify project, this step saves its _id to the shape's shopifyProductId field.
    6. Finally, we return the Product shape item we either created or updated.

Directives#

Directives are small functions for accessing and manipulating data, composable in pipelines to achieve more complex transformations. Their primary use case is for mapping arguments and results in resolver configurations.

They have a minimal set of options and can access the values in the currentQuery context.

type Directive = [
function: string,
{
[key: string]: string
}
]

Find all the directives in [api/src/lib/graphql-v3/directives.ts](https://github.com/takeshape/takeshape/blob/71cb27d61fc1f3f19fc15eced195da40a0c17937/packages/api/src/lib/graphql-v3/directives.ts)

set#

This simple directive returns the provided value as a string. It's great for directly setting a literal value, or for starting off a directive pipeline with a value available in the currentQuery syntax.

Options

  • value required

    The string that should be set.

get#

Gets the value at path. If the resolved value is undefined, the defaultValue is returned in its place.

**get** is one of the most useful directives. While using get, you have access to the currentQuery context.

Options

  • **path** required This is property path to get from the context.

  • **defaultValue** A value to use if the path returns undefined

  • **passThroughOnUndefined** If **path** returns undefined, and no **defaultValue** is set, return the previous value. Defaults to true.

  • Example

    In the example below, input.title would be set to the value of either the takeshapeUpdate or takeshapeCreate step, if one is undefined and the other has a value. If both have a value, input.title will be the value of takeshapeCreate since that step is run later in the pipeline.

    If we disabled passThroughOnUndefined and the takeshapeCreate step returned undefined our result would be undefined, not the value of takeshapeUpdate.

    "argsMapping": {
    "input.title": [
    ["get", { "path": "steps.takeshapeUpdate.result.name" }],
    ["get", { "path": "steps.takeshapeCreate.result.name" }]
    ],
    "input.vendor": [["get", {
    "path": "steps.takeshapeCreate.result.vendor",
    "defaultValue": "Kirkland"
    }]
    ]
    }

trim#

Removes matching leading and trailing characters from a string.

Options

  • **chars** Defines the leading and trailing characters to remove from a string. For example, if you want to remove quotes from a string. Defaults to remove whitespace characters.

toUpper#

Converts the string, as a whole, to upper case.

prepend#

Prepends the text provided in options to the string.

Options

  • **text** required The text you'd like to prepend to the string

expressionEval#

Use the expression-eval module and our own custom context.

Options

  • expression ****required

Context

  • The same currentQuery context from above, plus
  • A number of callable functions
    • ~180 functions exported from lodash/fp, for the full list view api/src/lib/graphqlv3/expressions-functions
    • format exported from util
    • mapDeep a custom function for deep mappings, e.g., all [foo.name](http://foo.name) object paths at any depth in an array of objects
    • newObject provides a new object, given the limitations of the expression evaluation approach
    • getImageUrl from @takeshape/routing

getMultiple#

Allows you to get multiple paths, returning an array.

Options

  • paths required

zipAndMerge#

zipWith from lodash, using merge also from lodash to deeply merge objects at the same indexes in multiple arrays.

This will operate on whatever was output from the directive at the previous step, but is a noop if that value is not an array.

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](https://github.com/dylang/shortid#readme) to generate database keys)

For connected services, the key is the name of an existing 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 the row to select the value:

{
"title": {
"type": "string",
"@mapping": "takeshape:local: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.

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 an existing 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"}]}

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, TSRelationship, TSColorHsl, TSColorHsv, TSColorRgb, TSColor, TsStaticSite, TsStaticSiteEnvironmentVariables, TsStaticSiteTriggers

Built-in forms#

Asset, TsStaticSite