Typed JavaScript abstraction for AWS AppSync resolver templates, supporting AWS CDK and Pulumi.
import { PulumiResolver, sendAppSyncRequest, operations, context } from 'appsync-resolverscript'
new PulumiResolver('getUserResolver', {
apiId: horselingApi.id,
dataSource: databaseDataSource.name,
type: 'Query',
field: 'getUser',
template: sendAppSyncRequest(operations.dynamodb.getItem({ id: context.args.id }))
})
- Declare AWS AppSync resolver Velocity templates in JavaScript or TypeScript.
- Use type-checked JavaScript rather than VTL.
- Publish patterns as NPM packages and re-use them.
- Works with AWS CDK and Pulumi.
import { sendAppSyncRequest, operations } from 'appsync-resolverscript'
const templateBuilder = operations.dynamodb.getItem(({ context }) => ({ id: context.args.id }))
const { requestTemplate, responseTemplate } = templateBuilder
...is equivalent to...
import { sendAppSyncRequest, vtl } from 'appsync-resolverscript'
const templateBuilder = sendAppSyncRequest(({ context, util }) => ({
operation: 'GetItem',
version: '2017-02-28',
key: {
id: util.dynamodb.toDynamoDBJson(context.args.id)
}
})).then(({ context, util }) => util.toJson(context.result))
const { requestTemplate, responseTemplate } = templateBuilder
...is equivalent to...
import { sendAppSyncRequest, vtl } from 'appsync-resolverscript'
const templateBuilder = sendAppSyncRequest({
operation: 'GetItem',
version: '2017-02-28',
key: {
id: vtl`$util.dynamodb.toDynamoDBJson($context.args.id)`
}
}).then(vtl`$util.toJson($context.result)`)
const { requestTemplate, responseTemplate } = templateBuilder
...is equivalent to...
const requestTemplate = `{
"operation": "GetItem",
"version": "2017-02-28",
"key": {
"id": $util.dynamodb.toDynamoDBJson($context.args.id)
}
}`
const responseTemplate = '$util.toJson($context.result)'
With Yarn:
$ yarn add --dev appsync-resolverscript
Or NPM:
$ npm install appsync-resolverscript --save-dev
Use the sendAppSyncRequest(request)
function to define the request template. E.g.
// Defines: {}
const templateBuilder = sendAppSyncRequest({})
request
can be a primitive or object that will be stringified, a Velocity fragment, or a
function that returns any of the above. Multiple arguments are concatenated. It returns a
builder object that can be used to chain the response template.
Note that if you return a raw string as your template definition, it will be stringified to JSON. E.g.
// Defines: "#return"
sendAppSyncRequest('#return')
From the builder returned by the request, use the then(response)
function to define the response
template, in the same way as the request. Again, the builder is returned for further function
chaining. E.g.
// Defines: {}
templateBuilder.then({})
Defining a response is optional, as it defaults to:
$util.toJson($context.result)
For any value in the request or response definition, you can suspend JSON stringification and
provide raw VTL markup by using the vtl
template literal. E.g.
// Defines: #return
sendAppSyncRequest(vtl`#return`)
Alternatively, use an instance of VelocityFragment
.
You can jump back into JSON by embedding the stringify()
method, but make sure you use the one
from this package - it handles fragments, functions and variables correctly. E.g.
// Defines: #set( $person = { "name": "Bob" })
sendAppSyncRequest(vtl`#set( $person = ${stringify({ name: 'Bob' })})`)
The request or response templates can be defined using a function that returns the template structure. This function gets passed the Velocity context as a parameter, providing access to variables and functions. You can implement any logic you like in the function, but remember, any JavaScript conditional logic or loops are executed at deploy time, not when the template is executed - the template definition is the value returned by the function. E.g.
// If useKey === true, defines : $context.args.key
// If useKey === false, defines : { "id": $context.args.id }
sendAppSyncRequest(() => {
const useKey = // ... get from somewhere.
if (useKey) {
return vtl`$context.args.key`
} else {
return {
id: vtl`$context.args.id`
}
}
})
All of the standard AppSync functions are available via the Velocity context passed to function templates (* this is still WIP). Parameters passed to AppSync functions are stringified to JSON. E.g.
// Defines: $context.util.toJson(1, "two")
sendAppSyncRequest(velocityContext => velocityContext.util.toJson(1, 'two'))
You may want to use object-destructuring on the velocityContext
parameter to make this a little
less verbose, especially if you are calling functions in many places:
// Defines: '$context.util.toJson(1, "two")'
sendAppSyncRequest(({ util }) => util.toJson(1, 'two'))
AppSync functions can also be imported at module scope, which allows you avoid the boilerplate of defining your request or response as a function:
import { sendAppSyncRequest, util } from 'appsync-resolverscript'
// Defines: '$context.util.toJson(1, "two")'
sendAppSyncRequest(util.toJson(1, 'two'))
The standard AppSync context object is available as a context
property on the Velocity context passed
to function templates (* this is still WIP). Sorry, overloading the term context is a bit confusing. E.g.
// Defines: { "id": $context.args.id }
sendAppSyncRequest(velocityContext => {
id: velocityContext.context.args.id
})
or
// Defines: { "id": $context.args.id }
sendAppSyncRequest(({ context }) => ({ id: context.args.id }))
Once you get to an args value or result, you can navigate through to any sub-properties (although the sub-properties are not type-checked, as the TypeScript doesn't know the shape of your args or results). E.g.
// Defines: { "id": $context.args.id }
then(({ context, util }) => util.toJson(context.result.items))
Note that the ctx
abbreviation is not supported.
The AppSync context object can also be imported at module scope, The downside of this approach is
that the context object is a superset of request, response and pipeline function contexts, and so not
all properties are appropriate for all mapping types (e.g. context.result
could be mis-used in a
request). The advantage is that it allows you avoid the boilerplate of defining your request
or response as a function. E.g.
import { sendAppSyncRequest, context } from 'appsync-resolverscript'
// Defines: { "id": $context.args.id }
sendAppSyncRequest({ id: context.args.id })
If you are working in Typescript, AppSync function parameters and return values are typed. So too are the non-dynamic properties from the Velocity context. E.g.
// Typescript errors:
// - defaultIfNullOrEmpty(string, string)
// - $context.identity.claims is an object
// - false is a boolean
sendAppSyncRequest(util.defaultIfNullOrEmpty(context.identity.claims, false))
The default type of a Velocity fragment is AnyType
, which matches any other type. This means that
you don't have to worry about the type of any fragments, although you can choose to type them if
you want to enable the type-checking. E.g.
// Typescript errors:
// - defaultIfNullOrEmpty(string, string)
// - $context.identity.claims is an object
sendAppSyncRequest(util.defaultIfNullOrEmpty(vtl<object>`$context.identity.claims`, '[]'))
The builder returned by sendAppSyncRequest(request)
has requestTemplate
and responseTemplate
properties to get the declared request and response mapping template as Velocity Template Language
markup. E.g.
const templateBuilder = sendAppSyncRequest('myRequest').then('myResponse')
assert.deepEqual('"myRequest"', templateBuilder.requestTemplate)
assert.deepEqual('"myResponse"', templateBuilder.responseTemplate)
Note: you don't generally need to use these properties if you use the CDK or Pulumi-specific classes.
Higher-level abstractions of the AppSync operations are available under operations
. They all
accept functions and VTL markup, and return a mapping definition that can be used in
sendAppSyncRequest()
.
GetItem:
const templateBuilder = sentAppSyncRequest(operations.dynamodb.getItem({ id: context.args.id }))
Pulumi is supported via the PulumiResolver
class. It has the same usage as aws.appsync.Resolver
, but the
requestTemplate
and responseTemplate
properties are replaced with template: ResolverTemplateBuilder
, and
can be used as follows:
new PulumiResolver('getUserResolver', {
apiId: horselingApi.id,
dataSource: databaseDataSource.name,
type: 'Query',
field: 'getUser',
template: sendAppSyncRequest(operations.dynamodb.getItem({ id: context.args.id }))
})
- Add typing to fragments and
util
functions. - Add typing where possible to the appsync context.
- Pre-populate Velocity variables for Unit and Pipeline templates.
- Add ability to set Velocity variables.
- Complete mapping of all core
util
functions. - Complete mapping of all
dynamodb
functions. - Add higher-level abstractions for DynamoDB API.
- Support
sendRequest().catch()
. - Support
map()
andfilter()
on variables. - Add explicit support for pipeline resolvers.
- Review namespacing of modules - don't import everything at root.
- Add explicit support for AWS CDK.
- Explore using JSX to build more complex templates from components.
- Add examples.
Contributions with unit tests are welcome, but please raise an issue and get feedback before spending any significant effort.