How I feel about VTL on any given day
Haha okay, okay. I'm just kiddin' (mostly).
AWS recently announced Direct Lambda Resolvers support, which means you can write AppSync resolvers in your favorite Lambda runtimes without relying on the Velocity Template Language.
I wanted to take advantage of this, as I had some complicated logic that that I didn't know how to write with VTL and it was much easier to do with javascript. I'll share with you how I added Direct Lambda Resolvers to an Amplify project with a little magic from the @function directive.
I'm gonna assume you've already created a GraphQL API for
your Amplify project. If you haven't, follow the steps here on how to Create a GraphQL API
Step 1: Create a Lambda with Amplify cli
amplify add function
? Provide a friendly name for your resource to be used as a label for this category in the project: DirectLambdaResolver
? Provide the AWS Lambda function name: DirectLambdaResolver
? Choose the function runtime that you want to use: NodeJS
? Choose the function template that you want to use: Hello World
? Do you want to access other resources created in this project from your Lambda function? No
? Do you want to invoke this function on a recurring schedule? No
? Do you want to edit the local lambda function now? Yes
Go to amplify/backend/function/DirectLambdaResolver/src/index.js
and update it with the following:
let response;
exports.handler = async (event, context) => {
console.log({event, context})
try {
response = "this lambda is super direct"
} catch (err) {
console.log({err})
console.log(err);
return err;
}
return response
};
Alright, save the file and press enter in the terminal to continue.
Step 2: Auto-generate code with @function
and copy code to CustomResources.json
Now we're gonna go to amplify/backend/api/[your-api-name]/schema.graphql
and take advantage of the @function
directive
### schema.graphql
type Todo @model {
id: ID
task: String @function(name: "DirectLambdaResolver-${env}")
}
then
$: amplify api gql-compile
This will create all the files inside amplify/backend/api/[your-api-name]/build/
. What we're looking for is the file FunctionDirectiveStack.json
. Open it up and you'll see the following:
{
"AWSTemplateFormatVersion": "2010-09-09",
"Description": "An auto-generated nested stack.",
"Metadata": {},
"Parameters": {
...
},
"Resources": {
// IAM Role
"DirectLambdaResolverLambdaDataSourceRole": {
...
},
// DataSource
"DirectLambdaResolverLambdaDataSource": {
...
},
// Direct Lambda Resolver
"InvokeDirectLambdaResolverLambdaDataSource": {
...
},
// Pipeline Resolver
"TodotaskResolver": {
...
}
}
When amplify auto-generates with @function, it creates a Lambda DataSource, an IAM Role for that DataSource, the Direct Lambda Resolver and a Pipeline Resolver.
We're going to copy everything from the Resources
object and paste it in our CustomResources.json
file located at amplify/backend/api/[your-api-name]/stacks/
. It should now look like the following:
{
"AWSTemplateFormatVersion": "2010-09-09",
"Description": "An auto-generated nested stack.",
"Metadata": {},
"Parameters": {
...
},
"Resources":
"DirectLambdaResolverLambdaDataSourceRole": {
...
},
"DirectLambdaResolverLambdaDataSource": {
...
},
"InvokeDirectLambdaResolverLambdaDataSource": {
...
},
"TodotaskResolver": {
...
}
},
"Conditions": {
...
},
"Outputs": {
...
}
}
We also need to update our DirectLambdaResolverLambdaDataSourceRole
to replace GetAttGraphQLAPIApiId
with AppSyncApiId
"DirectLambdaResolverLambdaDataSourceRole": {
"Type": "AWS::IAM::Role",
"Properties": {
"RoleName": {
"Fn::If": [
"HasEnvironmentParameter",
{
"Fn::Join": [
"-",
[
"DirectLambdaResolver774c",
{
"Ref": "AppSyncApiId" // Used to be GetAttGraphQLAPIApiId
},
{
"Ref": "env"
}
]
]
},
{
"Fn::Join": [
"-",
[
"DirectLambdaResolver774c",
{
"Ref": "AppSyncApiId" // Used to be GetAttGraphQLAPIApiId
}
]
]
}
]
},
"AssumeRolePolicyDocument": {
...
},
"Policies": [
...
]
}
},
Next up, in order to make our new Lambda a Direct Lambda Resolver with AppSync, we'll need to remove the request and response mapping templates from our function InvokeDirectLambdaResolverLambdaDataSource
. They're nested inside the Properties
object
"InvokeDirectLambdaResolverLambdaDataSource": {
"Type": "AWS::AppSync::FunctionConfiguration",
"Properties": {
"ApiId": {
"Ref": "AppSyncApiId"
},
"Name": "InvokeDirectLambdaResolverLambdaDataSource",
"DataSourceName": "DirectLambdaResolverLambdaDataSource",
"FunctionVersion": "2018-05-29",
// REMOVE ME
"RequestMappingTemplateS3Location": {
...
},
// REMOVE ME
"ResponseMappingTemplateS3Location": {
...
}
},
"DependsOn": "DirectLambdaResolverLambdaDataSource"
}
We'll also need to copy Todo.task.req.vtl
and Todo.task.res.vtl
from amplify/backend/api/[your-api-name]/build/resolvers
to amplify/backend/api/[your-api-name]/resolvers
.
Wait a minute. I thought we said no more VTL? What gives?
TodotaskResolver
is a Pipeline Resolver.InvokeDirectLambdaResolverLambdaDataSource
is our Direct Lambda Resolver.
Don't wan't a Pipeline resolver? Don't worry, I got you covered towards the end 😉
Step 3: Remove autogenerated code
Remember how we added @function(name: "DirectLambdaResolver-${env}
directive in our task: String
field within our schema.graphql? We need to remove all the generated code, including FunctionDirectiveStack.json
.
Luckily that's pretty easy. Just remove the @function
and run:
$: amplify api gql-compile
If you look inside amplify/backend/api/[your-api-name]/build/
, you'll see our updated CustomResources.json file and that the FunctionDirectiveStack.json has been removed.
Puh-push it real good
$: amplify push
If you go to your AppSync console, click on schema and search Todo
inside the Resolvers search field, and scroll down to task
and it should say Pipeline
. Click on it and it will take you to the Todo.task Pipeline Resolver with your DirectLambdaResolver listed. Notice the mapping templates disabled?
Yeaaa buddy, no more VTL (kind of) 😆 😆 😆
Bonus Round: Remove Pipeline Resolver
As I promised earlier, if you want to get rid of the Todo.task Pipeline Resolver, we need to make two changes inside amplify/backend/api/[your-api-name]/stacks/CustomResources.json
.
First, delete TodotaskResolver
.
Second, replace your InvokeDirectLambdaResolverLambdaDataSource
with the following:
"TodotaskDirectLambdaResolver": {
"Type": "AWS::AppSync::Resolver",
"Properties": {
"ApiId": {
"Ref": "AppSyncApiId"
},
"DataSourceName": "DirectLambdaResolverLambdaDataSource",
"TypeName": "Todo",
"FieldName": "task"
},
"DependsOn": "DirectLambdaResolverLambdaDataSource"
}
$: amplify api push
Now, go forth and banish VTL templates at will!
I hope you enjoyed this post and if you did, follow me on twitter for all things AWS Amplify, AppSync, and React-Native 🙌 🙌 🙌
https://twitter.com/andthensumm
Resources:
Add a custom resolver that targets an aws lambda
How to add pipelineResolvers - Github Issue
Top comments (15)
Thank you Shawn for catching this, it's a treat just to have you actually use this code!
not at all lol, i barely know what i'm doing and was following along just to try to learn about this new feature 😅
i think the struggle i had was how to use direct lambda resolvers in mutations not just queries. i couldnt figure out how to do it. maybe worth a followup post?
Pfffh, you got it. I'll whip something up for this week.
Did you guys manage to figure this out?
@nxtra , in order to do this for a mutation, you'd set the
typeName: Mutation
andfieldName: yourMutationName
. Query, Mutation and Subscription are really just aType
likeTodo
.The one gotcha is that you can't create a resolver on a mutation that already has one. Example being
createToDo
. Amplify has already created a resolver for it inside CloudFormation. You'll need to either to add@model(mutations: null)
so you can override it or create a custom mutation. I prefer the custom mutation because then I still have access to the autogenerated vtl from Amplify. There are times where the authorization rules that are generated in the response.vtl can still be useful.Thanks for the info! I didn't realize yet that indeed a resolver with that name already exists if you name it like that.
Until now I've been making custom Resolvers with lambda functions as pipeline resolvers. I'll try to convert one to a "no-more-vtl" version using your guide.
I'm confused, I thought the whole point of the Amplify cli was so I wasn't modifying cft's directly?
I'd also say the intent of Amplify is to offer you a core set of services that includes authentication, database, file storage, analytics, managed hosting, deployments, with more and counting.
The services are supposed to make it more accessible and approachable for backend and front-end devs, which on the whole, they absolutely do. They're still growing and figuring things out though and in the meantime, they've given the option of customizing your CFTs when you need to do something off the beaten bath.
It's almost inevitable that you will modify CFTs in the future. Amplify offers a lot out of the box but there are several solutions that they don't offer from the cli.
I followed the above, but now the lambda is no longer invoked and the field that previously had the @function now comes back with a null value; any suggestions as to what may be wrong? Thanks!!
Hey Rich, I’d have to know more about your project and see your CustomResources.json file
Is there any risk in breaking this when you regenerate code or add other resources with Amplify? Or once you've done this it's stable?
As long as you keep the code in the CustomResources file (or another custom stack) and you make sure to remove the @directive, you’re good.
These days I mostly use amplify for its api/auth/appsync integration and the cli for generating as much code as possible because I’m lazy ;)
Awesome, thanks!