Configure Lambda resolvers
@function
The @function
directive allows you to quickly & easily configure AWS Lambda resolvers within your AWS AppSync API.
Definition
directive @function(name: String!, region: String) on FIELD_DEFINITION
Usage
The @function
directive allows you to quickly connect lambda resolvers to an AppSync API. You may deploy the AWS Lambda functions via the Amplify CLI, AWS Lambda console, or any other tool. To connect an AWS Lambda resolver, add the @function
directive to a field in your schema.graphql
.
Let's assume you have deployed an echo function with the following contents:
exports.handler = async function (event, context) { return event.arguments.msg;};
If you deployed your function using the function
category
The Amplify CLI provides support for maintaining multiple environments out of the box. When you deploy a function via amplify add function
, it will automatically add the environment suffix to your Lambda function name. For example if you create a function named echofunction using amplify add function
in the dev environment, the deployed function will be named echofunction-dev. The @function
directive allows you to use ${env}
to reference the current Amplify CLI environment.
type Query { echo(msg: String): String @function(name: "echofunction-${env}")}
If you deployed your function without amplify
If you deployed your API without amplify then you must provide the full Lambda function name. If you deployed the same function with the name echofunction then you would have:
type Query { echo(msg: String): String @function(name: "echofunction")}
Example: Return custom data and run custom logic
You can use the @function
directive to write custom business logic in an AWS Lambda function. To get started, use amplify add function
, the AWS Lambda console, or other tool to deploy an AWS Lambda function with the following contents.
For example purposes assume the function is named GraphQLResolverFunction
:
const POSTS = [ { id: 1, title: 'AWS Lambda: How To Guide.' }, { id: 2, title: 'AWS Amplify Launches @function and @key directives.' }, { id: 3, title: 'Serverless 101' }];const COMMENTS = [ { postId: 1, content: 'Great guide!' }, { postId: 1, content: 'Thanks for sharing!' }, { postId: 2, content: "Can't wait to try them out!" }];
// Get all posts. Write your own logic that reads from any data source.function getPosts() { return POSTS;}
// Get the comments for a single post.function getCommentsForPost(postId) { return COMMENTS.filter((comment) => comment.postId === postId);}
/** * Using this as the entry point, you can use a single function to handle many resolvers. */const resolvers = { Query: { posts: (ctx) => { return getPosts(); } }, Post: { comments: (ctx) => { return getCommentsForPost(ctx.source.id); } }};
// event// {// "typeName": "Query", /* Filled dynamically based on @function usage location */// "fieldName": "me", /* Filled dynamically based on @function usage location */// "arguments": { /* GraphQL field arguments via $ctx.arguments */ },// "identity": { /* AppSync identity object via $ctx.identity */ },// "source": { /* The object returned by the parent resolver. E.G. if resolving field 'Post.comments', the source is the Post object. */ },// "request": { /* AppSync request object. Contains things like headers. */ },// "prev": { /* If using the built-in pipeline resolver support, this contains the object returned by the previous function. */ },// }exports.handler = async (event) => { const typeHandler = resolvers[event.typeName]; if (typeHandler) { const resolver = typeHandler[event.fieldName]; if (resolver) { return await resolver(event); } } throw new Error('Resolver not found.');};
Example: Get the logged in user from Amazon Cognito User Pools
When building applications, it is often useful to fetch information for the current user. You can use the @function
directive to quickly add a resolver that uses AppSync identity information to fetch a user from Amazon Cognito User Pools. First make sure you have added Amazon Cognito User Pools enabled via amplify add auth
and a GraphQL API via amplify add api
to an amplify project. Once you have created the user pool, get the UserPoolId from amplify-meta.json in the backend/ directory of your amplify project. You will provide this value as an environment variable in a moment. Next, using the Amplify function category, AWS console, or some other tool, deploy an AWS Lambda function with the following contents.
For example purposes assume the function is named GraphQLResolverFunction
:
/* Amplify Params - DO NOT EDITYou can access the following resource attributes as environment variables from your Lambda functionvar environment = process.env.ENVvar region = process.env.REGIONvar authMyResourceNameUserPoolId = process.env.AUTH_MYRESOURCENAME_USERPOOLID
Amplify Params - DO NOT EDIT */
const { CognitoIdentityServiceProvider } = require('aws-sdk');const cognitoIdentityServiceProvider = new CognitoIdentityServiceProvider();
/** * Get user pool information from environment variables. */const COGNITO_USERPOOL_ID = process.env.AUTH_MYRESOURCENAME_USERPOOLID;if (!COGNITO_USERPOOL_ID) { throw new Error( `Function requires environment variable: 'COGNITO_USERPOOL_ID'` );}const COGNITO_USERNAME_CLAIM_KEY = 'cognito:username';
/** * Using this as the entry point, you can use a single function to handle many resolvers. */const resolvers = { Query: { echo: (ctx) => { return ctx.arguments.msg; }, me: async (ctx) => { var params = { UserPoolId: COGNITO_USERPOOL_ID /* required */, Username: ctx.identity.claims[COGNITO_USERNAME_CLAIM_KEY] /* required */ }; try { // Read more: https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/CognitoIdentityServiceProvider.html#adminGetUser-property return await cognitoIdentityServiceProvider .adminGetUser(params) .promise(); } catch (e) { throw new Error(`NOT FOUND`); } } }};
// event// {// "typeName": "Query", /* Filled dynamically based on @function usage location */// "fieldName": "me", /* Filled dynamically based on @function usage location */// "arguments": { /* GraphQL field arguments via $ctx.arguments */ },// "identity": { /* AppSync identity object via $ctx.identity */ },// "source": { /* The object returned by the parent resolver. E.G. if resolving field 'Post.comments', the source is the Post object. */ },// "request": { /* AppSync request object. Contains things like headers. */ },// "prev": { /* If using the built-in pipeline resolver support, this contains the object returned by the previous function. */ },// }exports.handler = async (event) => { const typeHandler = resolvers[event.typeName]; if (typeHandler) { const resolver = typeHandler[event.fieldName]; if (resolver) { return await resolver(event); } } throw new Error('Resolver not found.');};
/* Amplify Params - DO NOT EDITYou can access the following resource attributes as environment variables from your Lambda functionvar environment = process.env.ENVvar region = process.env.REGIONvar authMyResourceNameUserPoolId = process.env.AUTH_MYRESOURCENAME_USERPOOLID
Amplify Params - DO NOT EDIT */
const { CognitoIdentityProviderClient, AdminGetUserCommand } = require('@aws-sdk/client-cognito-identity-provider');const cognitoIdentityProviderClient = new CognitoIdentityProviderClient({ region: process.env.REGION });
/** * Get user pool information from environment variables. */const COGNITO_USERPOOL_ID = process.env.AUTH_MYRESOURCENAME_USERPOOLID;if (!COGNITO_USERPOOL_ID) { throw new Error( `Function requires environment variable: 'COGNITO_USERPOOL_ID'` );}const COGNITO_USERNAME_CLAIM_KEY = 'cognito:username';
/** * Using this as the entry point, you can use a single function to handle many resolvers. */const resolvers = { Query: { echo: (ctx) => { return ctx.arguments.msg; }, me: async (ctx) => { const params = { UserPoolId: COGNITO_USERPOOL_ID, Username: ctx.identity.claims[COGNITO_USERNAME_CLAIM_KEY] }; try { const response = await cognitoIdentityProviderClient.send(new AdminGetUserCommand(params)); return response.UserAttributes; } catch (e) { throw new Error(`NOT FOUND`); } } }};
// event// {// "typeName": "Query", /* Filled dynamically based on @function usage location */// "fieldName": "me", /* Filled dynamically based on @function usage location */// "arguments": { /* GraphQL field arguments via $ctx.arguments */ },// "identity": { /* AppSync identity object via $ctx.identity */ },// "source": { /* The object returned by the parent resolver. E.G. if resolving field 'Post.comments', the source is the Post object. */ },// "request": { /* AppSync request object. Contains things like headers. */ },// "prev": { /* If using the built-in pipeline resolver support, this contains the object returned by the previous function. */ },// }exports.handler = async (event) => { const typeHandler = resolvers[event.typeName]; if (typeHandler) { const resolver = typeHandler[event.fieldName]; if (resolver) { return await resolver(event); } } throw new Error('Resolver not found.');};
You can connect this function to your AppSync API deployed via Amplify using this schema:
type Query { posts: [Post] @function(name: "GraphQLResolverFunction")}type Post { id: ID! title: String! comments: [Comment] @function(name: "GraphQLResolverFunction")}type Comment { postId: ID! content: String}
This simple lambda function shows how you can write your own custom logic using a language of your choosing. Try enhancing the example with your own data and logic.
When deploying the function, make sure your function has access to the auth resource. You can run the
amplify update function
command for the CLI to automatically supply an environment variable namedAUTH_<RESOURCE_NAME>_USERPOOLID
to the function and associate corresponding CRUD policies to the execution role of the function.
After deploying your function, you can connect it to AppSync by defining some types and using the @function directive. Add this to your schema, to connect the Query.echo
and Query.me
resolvers to your new function.
type Query { me: User @function(name: "ResolverFunction") echo(msg: String): String @function(name: "ResolverFunction")}# These types derived from https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/CognitoIdentityServiceProvider.html#adminGetUser-propertytype User { Username: String! UserAttributes: [Value] UserCreateDate: String UserLastModifiedDate: String Enabled: Boolean UserStatus: UserStatus MFAOptions: [MFAOption] PreferredMfaSetting: String UserMFASettingList: String}type Value { Name: String! Value: String}type MFAOption { DeliveryMedium: String AttributeName: String}enum UserStatus { UNCONFIRMED CONFIRMED ARCHIVED COMPROMISED UNKNOWN RESET_REQUIRED FORCE_CHANGE_PASSWORD}
Next run amplify push
and wait as your project finishes deploying. To test that everything is working as expected run amplify api console
to open the GraphiQL editor for your API. You are going to need to open the Amazon Cognito User Pools console to create a user if you do not yet have any. Once you have created a user go back to the AppSync console's query page and click "Login with User Pools". You can find the ClientId in amplify-meta.json under the key AppClientIDWeb. Paste that value into the modal and login using your username and password. You can now run this query:
query { me { Username UserStatus UserCreateDate UserAttributes { Name Value } MFAOptions { AttributeName DeliveryMedium } Enabled PreferredMfaSetting UserMFASettingList UserLastModifiedDate }}
which will return user information related to the current user directly from your user pool.
Structure of the function event
When writing lambda functions that are connected via the @function
directive, you can expect the following structure for the AWS Lambda event object.
Key | Description |
---|---|
typeName | The name of the parent object type of the field being resolver. |
fieldName | The name of the field being resolved. |
arguments | A map containing the arguments passed to the field being resolved. |
identity | A map containing identity information for the request. Contains a nested key 'claims' that will contains the JWT claims if they exist. |
source | When resolving a nested field in a query, the source contains parent value at runtime. For example when resolving Post.comments , the source will be the Post object. |
request | The AppSync request object. Contains header information. |
prev | When using pipeline resolvers, this contains the object returned by the previous function. You can return the previous value for auditing use cases. |
Your function should follow the lambda handler guidelines for your specific language. See the developer guides from the AWS Lambda documentation for your chosen language. If you choose to use structured types, your type should serialize the AWS Lambda event object outlined above. For example, if using Golang, you should define a struct with the above fields.
Calling functions in different regions
By default, you expect the function to be in the same region as the amplify project. If you need to call a function in a different (or static) region, you can provide the region argument.
type Query { echo(msg: String): String @function(name: "echofunction", region: "us-east-1")}
Calling functions in different AWS accounts is not supported via the @function directive but is supported by AWS AppSync.
Chaining functions
The @function directive supports AWS AppSync pipeline resolvers. That means, you can chain together multiple functions such that they are invoked in series when your field's resolver is invoked. To create a pipeline resolver that calls out to multiple AWS Lambda functions in series, use multiple @function
directives on the field.
type Mutation { doSomeWork(msg: String): String @function(name: "worker-function") @function(name: "audit-function")}
In the example above when you run a mutation that calls the Mutation.doSomeWork
field, the worker-function will be invoked first then the audit-function will be invoked with an event that contains the results of the worker-function under the event.prev.result key. The audit-function would need to return event.prev.result if you want the result of worker-function to be returned for the field. Under the hood, Amplify creates an AppSync::FunctionConfiguration
for each unique instance of @function
in a document and a pipeline resolver containing a pointer to a function for each @function
on a given field.
Generates
The @function
directive generates these resources as necessary:
- An AWS IAM role that has permission to invoke the function as well as a trust policy with AWS AppSync.
- An AWS AppSync data source that registers the new role and existing function with your AppSync API.
- An AWS AppSync pipeline function that prepares the lambda event and invokes the new data source.
- An AWS AppSync resolver that attaches to the GraphQL field and invokes the new pipeline functions.