export const meta = { title: `Overwrite & customize resolvers`, description: `GraphQL resolvers connect the fields in a type’s schema to a data source. Resolvers are the mechanism by which requests are fulfilled. Learn how to overwrite or add custom resolvers with Amplify.`, }; ## Overwriting Resolvers Let's say you have a simple *schema.graphql*... ```graphql type Todo @model { id: ID! name: String! description: String } ``` and you want to change the behavior of request mapping template for the *Query.getTodo* resolver that will be generated when the project compiles. To do this you would create a file named `Query.getTodo.req.vtl` in the *resolvers* directory of your API project. The next time you run `amplify push` or `amplify api gql-compile`, your resolver template will be used instead of the auto-generated template. You may similarly create a `Query.getTodo.res.vtl` file to change the behavior of the resolver's response mapping template. ## Custom Resolvers You can add custom `Query`, `Mutation` and `Subscription` when the generated ones do not cover your use case. 1. Add the required `Query`, `Mutation` or `Subscription` type to your schema. 2. Create resolvers for newly created `Query`, `Mutation` or `Subscription` by creating request and response template in `/amplify/backend/api//resolvers` folder. Graphql Transformer follows `...vtl` as convention to name the resolvers. So if you're adding a custom query name `myCustomQuery` the resolvers would be name `Query.myCustomQuery.req.vtl` and `Query.myCustomQuery.res.vtl`. 3. Add resolvers resource by creating a custom stack inside `/amplify/backend/api//stacks` directory of your API. To add the custom fields, add the following to your schema: ```graphql # amplify/backend/api//schema.graphql type Query { # Add all the custom queries here } type Mutation { # Add all the custom mutations here } type Subscription { # Add all the custom subscription here } ``` The GraphQL Transformer by default creates a file called `CustomResources.json` inside `/amplify/backend/api//stacks`, which can be used to add the custom resolvers for newly added `Query`, `Mutation` or `Subscription`. The custom stack gets the following arguments passed to it, allowing you to get details about API:
| Parameter | Type | Possible values | Description | | :--------------------------------- | :----- | ---------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | AppSyncApiId | String | | The id of the AppSync API associated with this project | | AppSyncApiName | String | | The name of the AppSync API | | env | String | | Environment name | | S3DeploymentBucket | String | | The S3 bucket containing all deployment assets for the project | | S3DeploymentRootKey | String | | An S3 key relative to the S3DeploymentBucket that points to the root of the deployment directory. | | DynamoDBEnableServerSideEncryption | String | `true` or `false` | Enable server side encryption powered by KMS. | | AuthCognitoUserPoolId | String | | The id of an existing User Pool to connect | | DynamoDBModelTableReadIOPS | Number | | The number of read IOPS the table should support. | | DynamoDBModelTableWriteIOPS | Number | | The number of write IOPS the table should support | | DynamoDBBillingMode | String | `PAY_PER_REQUEST` or `PROVISIONED` | Configure @model types to create DynamoDB tables with PAY_PER_REQUEST or PROVISIONED billing modes | | DynamoDBEnablePointInTimeRecovery | String | `true` or `false` | Whether to enable Point in Time Recovery on the table | | APIKeyExpirationEpoch | Number | | he epoch time in seconds when the API Key should expire | | CreateAPIKey | Number | `0` or `1` | The boolean value to control if an API Key will be created or not. The value of the property is automatically set by the CLI. If the value is set to 0 no API Key will be created |
Any additional values added Custom Stacks will be exposed as parameter in the root stack, and value can be set by adding the value for it in `/amplify/backend/api//parameters.json` file. To add a custom resolver, add the following in the resource section of `CustomResource.json` ```json { "Resources": { "CustomQuery1": { "Type": "AWS::AppSync::Resolver", "Properties": { "ApiId": { "Ref": "AppSyncApiId" }, "DataSourceName": "CommentTable", "TypeName": "Query", "FieldName": "myCustomQuery", "RequestMappingTemplateS3Location": { "Fn::Sub": [ "s3://${S3DeploymentBucket}/${S3DeploymentRootKey}/resolvers/Query.myCustomQuery.req.vtl", { "S3DeploymentBucket": { "Ref": "S3DeploymentBucket" }, "S3DeploymentRootKey": { "Ref": "S3DeploymentRootKey" } } ] }, "ResponseMappingTemplateS3Location": { "Fn::Sub": [ "s3://${S3DeploymentBucket}/${S3DeploymentRootKey}/resolvers/Query.myCustomQuery.res.vtl", { "S3DeploymentBucket": { "Ref": "S3DeploymentBucket" }, "S3DeploymentRootKey": { "Ref": "S3DeploymentRootKey" } } ] } } } } } ``` The request and response template should be placed inside `/amplify/backend/api//resolvers` folder. Resolver templates are written in the [Apache Velocity Template Language](https://velocity.apache.org/engine/1.7/user-guide.html), commonly referred to as VTL. `Query.myCustomQuery.req.vtl` is a request mapping template, which receives an incoming AppSync request and transforms it into a JSON document that is subsequently passed to the GraphQL resolver. Similarly, `Query.myCustomQuery.res.vtl` is a response mapping template. These templates receive the GraphQL resolver's response and transform the data before returning it to the user. Several example VTL files are discussed later in this documentation. For more detailed information on VTL, including how it can be used in the context of GraphQL resolvers, see the official [AppSync Resolver Mapping Template Reference](https://docs.aws.amazon.com/appsync/latest/devguide/resolver-mapping-template-reference.html). ### Add a custom resolver that targets a DynamoDB table from @model This is useful if you want to write a more specific query against a DynamoDB table that was created by *@model*. For example, assume you had this schema with two *@model* types and a pair of *@connection* directives. ```graphql type Todo @model { id: ID! name: String! description: String comments: [Comment] @connection(name: "TodoComments") } type Comment @model { id: ID! content: String todo: Todo @connection(name: "TodoComments") } ``` This schema will generate resolvers for *Query.getTodo*, *Query.listTodos*, *Query.getComment*, and *Query.listComments* at the top level as well as for *Todo.comments*, and *Comment.todo* to implement the *@connection*. Under the hood, the transform will create a global secondary index on the Comment table in DynamoDB but it will not generate a top level query field that queries the GSI because you can fetch the comments for a given todo object via the *Query.getTodo.comments* query path. If you want to fetch all comments for a todo object via a top level query field i.e. *Query.commentsForTodo* then do the following: - Add the desired field to your *schema.graphql*. ```graphql # ... Todo and Comment types from above type CommentConnection { items: [Comment] nextToken: String } type Query { commentsForTodo(todoId: ID!, limit: Int, nextToken: String): CommentConnection } ``` - Add a resolver resource to a stack in the *stacks/* directory. The `DataSourceName` is auto-generated. In most cases, it'll look like `{MODEL_NAME}Table`. To confirm the data source name, you can verify it from within the **AppSync Console** (`amplify console api`) and clicking on the **Data Sources** tab. ```json { // ... The rest of the template "Resources": { "QueryCommentsForTodoResolver": { "Type": "AWS::AppSync::Resolver", "Properties": { "ApiId": { "Ref": "AppSyncApiId" }, "DataSourceName": "CommentTable", "TypeName": "Query", "FieldName": "commentsForTodo", "RequestMappingTemplateS3Location": { "Fn::Sub": [ "s3://${S3DeploymentBucket}/${S3DeploymentRootKey}/resolvers/Query.commentsForTodo.req.vtl", { "S3DeploymentBucket": { "Ref": "S3DeploymentBucket" }, "S3DeploymentRootKey": { "Ref": "S3DeploymentRootKey" } } ] }, "ResponseMappingTemplateS3Location": { "Fn::Sub": [ "s3://${S3DeploymentBucket}/${S3DeploymentRootKey}/resolvers/Query.commentsForTodo.res.vtl", { "S3DeploymentBucket": { "Ref": "S3DeploymentBucket" }, "S3DeploymentRootKey": { "Ref": "S3DeploymentRootKey" } } ] } } } } } ``` - Write the resolver templates. ```text ## Query.commentsForTodo.req.vtl ** #set( $limit = $util.defaultIfNull($context.args.limit, 10) ) { "version": "2017-02-28", "operation": "Query", "query": { "expression": "#connectionAttribute = :connectionAttribute", "expressionNames": { "#connectionAttribute": "commentTodoId" }, "expressionValues": { ":connectionAttribute": { "S": "$context.args.todoId" } } }, "scanIndexForward": true, "limit": $limit, "nextToken": #if( $context.args.nextToken ) "$context.args.nextToken" #else null #end, "index": "gsi-TodoComments" } ``` ```text ## Query.commentsForTodo.res.vtl ** $util.toJson($ctx.result) ``` ### Add a custom resolver that targets an AWS Lambda function Velocity is useful as a fast, secure environment to run arbitrary code but when it comes to writing complex business logic you can just as easily call out to an AWS lambda function. Here is how: - First create a function by running `amplify add function`. The rest of the example assumes you created a function named "echofunction" via the `amplify add function` command. If you already have a function then you may skip this step. - Add a field to your schema.graphql that will invoke the AWS Lambda function. ```graphql type Query { echo(msg: String): String } ``` - Add the function as an AppSync data source in the stack's *Resources* block. ```json "EchoLambdaDataSource": { "Type": "AWS::AppSync::DataSource", "Properties": { "ApiId": { "Ref": "AppSyncApiId" }, "Name": "EchoFunction", "Type": "AWS_LAMBDA", "ServiceRoleArn": { "Fn::GetAtt": [ "EchoLambdaDataSourceRole", "Arn" ] }, "LambdaConfig": { "LambdaFunctionArn": { "Fn::Sub": [ "arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:echofunction-${env}", { "env": { "Ref": "env" } } ] } } } } ``` - Create an AWS IAM role that allows AppSync to invoke the lambda function on your behalf to the stack's *Resources* block. ```json "EchoLambdaDataSourceRole": { "Type": "AWS::IAM::Role", "Properties": { "RoleName": { "Fn::Sub": [ "EchoLambdaDataSourceRole-${env}", { "env": { "Ref": "env" } } ] }, "AssumeRolePolicyDocument": { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": { "Service": "appsync.amazonaws.com" }, "Action": "sts:AssumeRole" } ] }, "Policies": [ { "PolicyName": "InvokeLambdaFunction", "PolicyDocument": { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "lambda:invokeFunction" ], "Resource": [ { "Fn::Sub": [ "arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:echofunction-${env}", { "env": { "Ref": "env" } } ] } ] } ] } } ] } } ``` - Create an AppSync resolver in the stack's *Resources* block. ```json "QueryEchoResolver": { "Type": "AWS::AppSync::Resolver", "Properties": { "ApiId": { "Ref": "AppSyncApiId" }, "DataSourceName": { "Fn::GetAtt": [ "EchoLambdaDataSource", "Name" ] }, "TypeName": "Query", "FieldName": "echo", "RequestMappingTemplateS3Location": { "Fn::Sub": [ "s3://${S3DeploymentBucket}/${S3DeploymentRootKey}/resolvers/Query.echo.req.vtl", { "S3DeploymentBucket": { "Ref": "S3DeploymentBucket" }, "S3DeploymentRootKey": { "Ref": "S3DeploymentRootKey" } } ] }, "ResponseMappingTemplateS3Location": { "Fn::Sub": [ "s3://${S3DeploymentBucket}/${S3DeploymentRootKey}/resolvers/Query.echo.res.vtl", { "S3DeploymentBucket": { "Ref": "S3DeploymentBucket" }, "S3DeploymentRootKey": { "Ref": "S3DeploymentRootKey" } } ] } } } ``` - Create the resolver templates in the project's *resolvers* directory. **resolvers/Query.echo.req.vtl** ```text { "version": "2017-02-28", "operation": "Invoke", "payload": { "type": "Query", "field": "echo", "arguments": $utils.toJson($context.arguments), "identity": $utils.toJson($context.identity), "source": $utils.toJson($context.source) } } ``` **resolvers/Query.echo.res.vtl** ``` $util.toJson($ctx.result) ``` After running `amplify push` open the AppSync console with `amplify api console` and test your API with this simple query: ```graphql query { echo(msg:"Hello, world!") } ``` ### Add a custom geolocation search resolver that targets an OpenSearch domain created by @searchable To add a geolocation search capabilities to an API add the *@searchable* directive to an *@model* type. ```graphql type Todo @model @searchable { id: ID! name: String! description: String comments: [Comment] @connection(name: "TodoComments") } ``` The next time you run `amplify push`, an Amazon OpenSearch domain will be created and configured such that data automatically streams from DynamoDB into OpenSearch. The *@searchable* directive on the Todo type will generate a *Query.searchTodos* query field and resolver but it is not uncommon to want more specific search capabilities. You can write a custom search resolver by following these steps: - Add the relevant location and search fields to the schema. ```graphql type Comment @model { id: ID! content: String todo: Todo @connection(name: "TodoComments") } type Location { lat: Float lon: Float } type Todo @model @searchable { id: ID! name: String! description: String comments: [Comment] @connection(name: "TodoComments") location: Location } type TodoConnection { items: [Todo] nextToken: String } input LocationInput { lat: Float lon: Float } type Query { nearbyTodos(location: LocationInput!, km: Int): TodoConnection } ``` - Create the resolver record in the stack's *Resources* block. ```json "QueryNearbyTodos": { "Type": "AWS::AppSync::Resolver", "Properties": { "ApiId": { "Ref": "AppSyncApiId" }, "DataSourceName": "ElasticSearchDomain", "TypeName": "Query", "FieldName": "nearbyTodos", "RequestMappingTemplateS3Location": { "Fn::Sub": [ "s3://${S3DeploymentBucket}/${S3DeploymentRootKey}/resolvers/Query.nearbyTodos.req.vtl", { "S3DeploymentBucket": { "Ref": "S3DeploymentBucket" }, "S3DeploymentRootKey": { "Ref": "S3DeploymentRootKey" } } ] }, "ResponseMappingTemplateS3Location": { "Fn::Sub": [ "s3://${S3DeploymentBucket}/${S3DeploymentRootKey}/resolvers/Query.nearbyTodos.res.vtl", { "S3DeploymentBucket": { "Ref": "S3DeploymentBucket" }, "S3DeploymentRootKey": { "Ref": "S3DeploymentRootKey" } } ] } } } ``` - Write the resolver templates. ``` ## Query.nearbyTodos.req.vtl ## Objects of type Todo will be stored in the /todo index #set( $indexPath = "/todo/doc/_search" ) #set( $distance = $util.defaultIfNull($ctx.args.km, 200) ) { "version": "2017-02-28", "operation": "GET", "path": "$indexPath.toLowerCase()", "params": { "body": { "query": { "bool": { "must": { "match_all": {} }, "filter": { "geo_distance": { "distance": "${distance}km", "location": $util.toJson($ctx.args.location) } } } } } } } ``` ``` ## Query.nearbyTodos.res.vtl #set( $items = [] ) #foreach( $entry in $context.result.hits.hits ) #if( !$foreach.hasNext ) #set( $nextToken = "$entry.sort.get(0)" ) #end $util.qr($items.add($entry.get("_source"))) #end $util.toJson({ "items": $items, "total": $ctx.result.hits.total, "nextToken": $nextToken }) ``` - Run `amplify push` Amazon OpenSearch domains can take a while to deploy. Take this time to read up on OpenSearch to see what capabilities you are about to unlock. [Getting Started with OpenSearch](https://opensearch.org/docs/opensearch/index/) - After the update is complete but before creating any objects, update your OpenSearch index mapping. An index mapping tells OpenSearch how it should treat the data that you are trying to store. By default, if you create an object with field `"location": { "lat": 40, "lon": -40 }`, OpenSearch will treat that data as an *object* type when in reality you want it to be treated as a *geo_point*. You use the mapping APIs to tell OpenSearch how to do this. Make sure you tell OpenSearch that your location field is a *geo_point* before creating objects in the index because otherwise you will need delete the index and try again. Go to the [Amazon OpenSearch Console](https://console.aws.amazon.com/aos/home) and find the OpenSearch domain that contains this environment's GraphQL API ID. Click on it and open the OpenSearch Dashboard link. To get the OpenSearch Dashboard to show up you need to install a browser extension such as [AWS Agent](https://addons.mozilla.org/en-US/firefox/addon/aws-agent/) and configure it with your AWS profile's public key and secret so the browser can sign your requests to the OpenSearch Dashboard for security reasons. Once you have the OpenSearch Dashboard open, click the "Dev Tools" tab on the left and run the commands below using the in browser console. ```text # Create the /todo index if it does not exist PUT /todo # Tell OpenSearch that the location field is a geo_point PUT /todo/_mapping/doc { "properties": { "location": { "type": "geo_point" } } } ``` - Use your API to create objects and immediately search them. After updating the OpenSearch index mapping, open the AWS AppSync console with `amplify api console` and try out these queries. ```graphql mutation CreateTodo { createTodo(input:{ name: "Todo 1", description: "The first thing to do", location: { lat:43.476446, lon:-110.767786 } }) { id name location { lat lon } description } } query NearbyTodos { nearbyTodos(location: { lat: 43.476546, lon: -110.768786 }, km: 200) { items { id name location { lat lon } } } } ``` When you run *Mutation.createTodo*, the data will automatically be streamed via AWS Lambda into OpenSearch such that it nearly immediately available via *Query.nearbyTodos*.