Skip to main content

Docs for deprecated features

Unless otherwise stated, deprecated features will remain functional for projects that already incorporate them.

We highly recommend against using the instructions in this doc to configure new project schemas. However if you have a project schema that uses deprecated features, use this doc to continue working with them.

For the sake of supporting legacy customers, deprecated functionality will be documented here.

How can you know if a feature is deprecated?

If you can't find it in the schema spec reference, it is likely no longer recommended. A great example is the args property on resolvers, which replaced the argsMapping property documented here.

NOTE

Not all features described in this doc are deprecated, but the instructions and language are all outdated. For up-to-date information, always check the schema spec reference.

shapes

A Shape is a schema object for structuring and storing data from one or more sources, including the 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, like _created and _updated to record the dates shape items are created and edited.

Guides

Working with Shapes

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
  2. REST resolvers
  3. ShapeDB resolvers

service

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

Each type of resolver—GraphQL, REST, or ShapeDB—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 different property's of the connected service's endpoint, including the query params, headers, body, json, and form data. Does not work unless your query has an "args" property assigned to it.

This is typically necessary when your service is a REST API, or otherwise not GraphQL. You need to map the arguments of your GraphQL query to the data shape that your service's endpoint is expecting. For example, if you want to send url-encoded form data, your argsMapping would look something like this:

  "argsMapping": {
"form": "get", {"path": "args.input"}
}

If you instead were passing a json payload, it might look something like this:

  "argsMapping": {
"json": "get", {"path": "args.input"}
}

If your input is just a string, you can of course use the body property instead of form or json:

  "argsMapping": {
"body": "get", {"path": "args.input"}
}

In depth argsmapping

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

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"},
"argsMapping": {
"form": "get", {"path": "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"},
"argsMapping": {
"form": "get", {"path": "args.input"}
}
}
}
NOTE

If you're using the body property, 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 an options field to your query shape's resolver object. The options object must have a path property with your path set as the value, as shown below:

  "options": {
"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, you have to add the name of the data you'd like to add. Specify the data with curly brackets wrapped around it. For example, if you want to hit a resource with a particular ID, then you might add {id} to the path, as shown below:

  "options": {
"path": "/your-endpoint-path/{id}"
}

Now, in your argsMapping, add a pathParams object with a property of id. The value of that property will be added where {id} is in the path on your options object. For example:

  "argsMapping": {
"pathParams": {
"id": "get", {"path": "args.input"}
}
}

This will take the value of an arg called input, and set it to where {id} appears in your path.

Here's how the full example might look:

  "exampleQuery": {
"description":"Description here",
"shape": "ShapeOfResponse",
"args": "ExampleArgs",
"resolver": {
"name": "rest:get",
"service": "your-service-slug",
"options": {
"path": "/your-endpoint-path/{id}"
},
"argsMapping": {
"pathParams": {
"id": "get", {"path": "args.input"}
}
}
}
}

So if you pass 123 into the input arg of your query, your resolver will hit this path: /endpoint/your-endpoint-path/123.

You could also use the set directive instead of the get directive. This will allow you to customize your path without receiving data from an input arg. Your pathParams would look like this instead:

  "argsMapping": {
"pathParams": {
"id": "set", {"value": "123"}
}
}

And again, your resolver would hit this path: /endpoint/your-endpoint-path/123.

Adding inputs as query parameters

If you're interested in mapping your input args to query parameters on your endpoint path, you can check out our doc on that subject.

You'll add a searchParamsMapping array to your resolver object. You add your params to this array, and associate each param with a directive. If you use the get directive, you can assign the value of an arg to the parameter. If you use the set directive, you can manually set the parameter's value yourself.

Your searchParamsMapping might look like this:

  "searchParamsMapping": [
[
"param1",
"get", {"path": "args.customVal"}
],
[
"param2",
"set", {"value": "testVal"}
]
]

When your query is called, you can add a value to the customVal arg, and it will be set as the value of the of the param1 parameter. param2 will have the value of "testVal", because that's what we're setting it as.

For example, let's say you set customVal to 123 when making the query. Your resolver will hit your service's endpoint with the following string appended to the end:

?param1=123&param2=testVal

And here's a fuller example of what the whole query shape may look like:

  "exampleQuery": {
"description":"Description here",
"shape": "ShapeOfResponse",
"args": "ExampleArgs",
"resolver": {
"name": "rest:get",
"service": "your-service-slug",
"options": {"path": "/your-endpoint-path"},
"argsMapping": {
"pathParams": {
"id": "get", {"path": "args.input"}
}
},
"searchParamsMapping": [
[
"param1",
"get", {"path": "args.customVal"}
],
[
"param2",
"set", {"value": "testVal"}
]
]
}
}

Mapping args to your query's headers

Custom headers can be set by adding headers with square brackets around the name of the header. You can then either use a set directive to set the value of the header, or a get directive to get the value from your input args.

For example:

  "argsMapping": {
"headers['SetHeaderExample']": "set", {"value": "value-you-want-to-set"},

"headers['GetHeaderExample']": "get", {"path": "args.customHeaderVal"},
}

In the above example, you're getting the value of an input arg called customHeaderVal and setting it as the value of the GetHeaderExample header.

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

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.

  • timeout (optional)

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

  • ttl (optional)

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

  • timeout (optional)

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

  • retry (optional)

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

    {
    "if": "!isEmpty(args.input._id)",
    "id": "shapedbUpdate",
    "argsMapping": "get", {"path": "args"},
    "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.
  • 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, for example, 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.graphQLAPIUpdate.result.shopifyProductId) || !isEmpty(steps.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, upsertProduct, that will create or update a Product in ShapeDB 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.finalGraphQLAPIResults" }
    },
    "compose": [
    {
    "id": "graphQLAPIUpdate",
    "name": "graphQLAPI:update",
    "service": "local",
    "if": "!isEmpty(args.input._id)",
    "argsMapping": "get", { "path": "args" },
    "options": {
    "model": "Product"
    }
    },
    {
    "id": "graphQLAPICreate",
    "name": "shapedb:create",
    "service": "shapedb",
    "if": "isEmpty(args.input._id)",
    "argsMapping": "get", { "path": "args" },
    "options": {
    "model": "Product"
    }
    },
    {
    "id": "shopifyProductUpdate",
    "name": "graphql:mutation",
    "service": "my-shopify-store",
    "if": "!isEmpty(steps.graphQLAPIUpdate.result.shopifyProductId) || !isEmpty(steps.graphQLAPICreate.result.shopifyProductId)",
    "argsMapping": {
    "input.id": [
    ["get", { "path": "steps.graphQLAPIUpdate.result.shopifyProductId" }],
    ["get", { "path": "steps.graphQLAPICreate.result.shopifyProductId" }]
    ],
    "input.title": [
    ["get", { "path": "steps.graphQLAPIUpdate.result.name" }],
    ["get", { "path": "steps.graphQLAPICreate.result.name" }]
    ]
    },
    "options": {
    "fieldName": "productUpdate",
    "selectionSet": "{ product { id } }"
    }
    },
    {
    "id": "shopifyProductCreate",
    "name": "graphql:mutation",
    "service": "my-shopify-store",
    "if": "isEmpty(steps.graphQLAPICreate.result.shopifyProductId) && isEmpty(steps.graphQLAPIUpdate.result.shopifyProductId)",
    "argsMapping": {
    "input.title": [
    ["get", { "path": "steps.graphQLAPIUpdate.result.name" }],
    ["get", { "path": "steps.graphQLAPICreate.result.name" }]
    ]
    },
    "options": {
    "fieldName": "productCreate",
    "selectionSet": "{ product { id } }"
    }
    },
    {
    "name": "shapedb:update",
    "service": "shapedb",
    "if": "!isEmpty(steps.shopifyProductCreate.product.id)",
    "argsMapping": {
    "input._id": [
    ["get", { "path": "steps.graphQLAPICreate.result._id" }],
    ["get", { "path": "steps.graphQLAPIUpdate.result._id" }]
    ],
    "input.shopifyProductId": [
    ["get", { "path": "steps.shopifyProductCreate.product.id" }]
    ]
    },
    "options": {
    "model": "Product"
    }
    },
    {
    "id": "finalGraphQLAPIResults",
    "name": "shapedb:get",
    "service": "shapedb",
    "argsMapping": {
    "_id": [
    ["get", { "path": "steps.graphQLAPIUpdate.result._id" }],
    ["get", { "path": "steps.graphQLAPICreate.result._id" }]
    ]
    },
    "options": {
    "model": "Product"
    }
    }
    ]
    },
    }

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

    1. Updates a 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 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 graphQLAPIUpdate or graphQLAPICreate steps ran, we try to use the values from each.
    4. If no Shopify Product is connected to the 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.