Add Pipeline Resolvers in AWS Amplify
Tags: aws, graphql, react, serverless
Published: 2020-07-18 18:19:13

*[AWS AppSync](https://aws.amazon.com/appsync/) and [AWS Amplify](https://aws.amazon.com/amplify/) make it easy for anyone to build Web Application (or iOS/Android Applications) without having to manage the underlying infrastructure. Developers can focus on building their front-end web application while AppSync and Amplify handles the heavy-lifting on the backend infrastructure side.*

## What are pipeline resolvers?
> AWS AppSync executes resolvers on a GraphQL field. In some cases, applications require executing multiple operations to resolve a single GraphQL field. With pipeline resolvers, developers can now compose operations (called Functions or Pipeline Functions) and execute them in sequence. Pipeline resolvers are useful for applications that, for instance, require performing an authorization check before fetching data for a field.

## What are we building?
In this tutorial, you'll learn how to add a pipeline resolver in an AWS Amplify application. You'll build a new AWS Amplify backend with:
1. **A User model**.
2. **A signupUser mutation** that uses a pipeline resolver.
3. **A lambda function** that takes the password field from input data and replaces it with a hashed password for security. It will be part of the pipeline resolver for signupUser mutation.

### Prerequisites
To complete this tutorial, you will need:
* Node.js(>=10.x) and NPM(>=6.x) installed. Download from [here](https://nodejs.org/en/download/).
* An AWS account. If you don't have one, you can create [here](https://portal.aws.amazon.com/billing/signup?nc2=h_ct&src=default&redirect_url=https%3A%2F%2Faws.amazon.com%2Fregistration-confirmation#/start).
* [Amplify CLI installed and configured](https://docs.amplify.aws/cli/start/install).

### Step 1 - Create a new React App
```console
$ npx create-react-app amplify-pipeline
$ cd amplify-pipeline
```

### Step 2 - Initialize a new backend
```console
$ amplify init
```
You'll be prompted for some information about the app.

![amplify init output](https://dev-to-uploads.s3.amazonaws.com/i/youb1rdxo55uy581kw3g.png)

### Step 3 - Add GraphQL API
* Run the following command.
```console
$ amplify add api
```
* Select the following options:
  * Select GraphQL
  * When asked if you have a schema, say No
  * Select one of the default samples; you can change this later
  * Choose to edit the schema and it will open the new schema.graphql in your editor

* Update schema.graphql with the following code:
```graphql
type User @model {
  id: ID!
  name: String!
  email: AWSEmail!
  password: String
}
```
Save the file and hit enter in your terminal window. If no error messages are thrown this means the transformation was successful and schema is valid.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/dgkx1hir9u5d9l8rrbfh.png)

### Step 4 - Add a Lambda function to hash password
Create a lambda function that takes the password field from the input and returns hashed password in response.

* Run the following command.
```console
$ amplify add function
```
* Complete the relevant prompts.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/b7ep04tu4mrp590xmfec.png)

* Update the source code of the **amplifypipelineHashPassword** lambda function(i.e. index.js).
```js
const bcrypt = require('bcryptjs');

exports.handler = async (event) => {
  // get password field from input
  const { password } = event.arguments.input;
  // use bcrypt to hash password
  const hash = await bcrypt.hash(password, 10);
  // return the hashed password as response
  return {
    hash,
  };
};
```
* Add **bcryptjs** node library to the lambda function. Run this from the root directory of your React app.
```console
$ cd amplify/backend/function/amplifypipelineHashPassword/src
$ npm install bcryptjs
```
* Now you can deploy the API and lambda function to AWS.
```console
$ amplify push
```

### Step 5 - Add user signup mutation
You will add a custom mutation called **signupUser** that creates a new user.
For this, you will need to add a [custom resolver](https://docs.amplify.aws/cli/graphql-transformer/resolvers#add-a-custom-resolver-that-targets-a-dynamodb-table-from-model) that targets the User table.

Here instead of a custom resolver, you'll add a pipeline resolver and a pipeline function that targets the UserTable.

* Add **signupUser** mutation to schema.graphql.
```graphql
type Mutation {
  signupUser(input: SignupUserInput!): User
}

input SignupUserInput {
  id: ID
  name: String!
  email: AWSEmail!
  password: String
}
```
* Add a pipeline resolver and pipeline function resource to a stack(like CustomResources.json) 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
// stacks/CustomResources.json

{
// ... The rest of the template
"Resources": {
    "MutationCreateUserFunction": {
      "Type": "AWS::AppSync::FunctionConfiguration",
      "Properties": {
        "ApiId": {
          "Ref": "AppSyncApiId"
        },
        "Name": "MutationCreateUserFunction",
        "DataSourceName": "UserTable",
        "FunctionVersion": "2018-05-29",
        "RequestMappingTemplateS3Location": {
          "Fn::Sub": [
            "s3://${S3DeploymentBucket}/${S3DeploymentRootKey}/pipelineFunctions/${ResolverFileName}",
            {
              "S3DeploymentBucket": {
                "Ref": "S3DeploymentBucket"
              },
              "S3DeploymentRootKey": {
                "Ref": "S3DeploymentRootKey"
              },
              "ResolverFileName": {
                "Fn::Join": [".", ["MutationCreateUserFunction", "req", "vtl"]]
              }
            }
          ]
        },
        "ResponseMappingTemplateS3Location": {
          "Fn::Sub": [
            "s3://${S3DeploymentBucket}/${S3DeploymentRootKey}/pipelineFunctions/${ResolverFileName}",
            {
              "S3DeploymentBucket": {
                "Ref": "S3DeploymentBucket"
              },
              "S3DeploymentRootKey": {
                "Ref": "S3DeploymentRootKey"
              },
              "ResolverFileName": {
                "Fn::Join": [".", ["MutationCreateUserFunction", "res", "vtl"]]
              }
            }
          ]
        }
      }
    },
    "MutationSignupUserResolver": {
      "Type": "AWS::AppSync::Resolver",
      "Properties": {
        "ApiId": {
          "Ref": "AppSyncApiId"
        },
        "TypeName": "Mutation",
        "FieldName": "signupUser",
        "Kind": "PIPELINE",
        "PipelineConfig": {
          "Functions": [
            {
              "Fn::GetAtt": ["MutationCreateUserFunction", "FunctionId"]
            }
          ]
        },
        "RequestMappingTemplateS3Location": {
          "Fn::Sub": [
            "s3://${S3DeploymentBucket}/${S3DeploymentRootKey}/resolvers/${ResolverFileName}",
            {
              "S3DeploymentBucket": {
                "Ref": "S3DeploymentBucket"
              },
              "S3DeploymentRootKey": {
                "Ref": "S3DeploymentRootKey"
              },
              "ResolverFileName": {
                "Fn::Join": [".", ["Mutation", "signupUser", "req", "vtl"]]
              }
            }
          ]
        },
        "ResponseMappingTemplateS3Location": {
          "Fn::Sub": [
            "s3://${S3DeploymentBucket}/${S3DeploymentRootKey}/resolvers/${ResolverFileName}",
            {
              "S3DeploymentBucket": {
                "Ref": "S3DeploymentBucket"
              },
              "S3DeploymentRootKey": {
                "Ref": "S3DeploymentRootKey"
              },
              "ResolverFileName": {
                "Fn::Join": [".", ["Mutation", "signupUser", "res", "vtl"]]
              }
            }
          ]
        }
      },
      "DependsOn": ["MutationCreateUserFunction"]
    }
  }
}
```
* Write the pipeline resolver templates in the resolvers directory. These are the before and after mapping templates that are run in the beginning and the end of the pipeline respectively.
```vtl
## resolvers/Mutation.signupUser.req.vtl

## [Start] Stash resolver specific context.. **
$util.qr($ctx.stash.put("typeName", "Mutation"))
$util.qr($ctx.stash.put("fieldName", "signupUser"))
{}
## [End] Stash resolver specific context.. **
```
```vtl
## resolvers/Mutation.signupUser.res.vtl

$util.toJson($ctx.prev.result)
```
* Write the pipelineFunction templates for **MutationCreateUserFunction** resource. Create a pipelineFunctions folder if it doesn't exist in your API resource. From your app root, run the following command.
```console
$ cd amplify/backend/api/amplifypipeline
$ mkdir pipelineFunctions
```
In the req template, you access the previous function result to get the hashed password. You then replace the password field in the input with the hashed password. So the new user created will save the hashed password in the database instead.
*Note: In a pipeline function, you can access the results of the previous pipeline function as `$ctx.prev.result`.*
```vtl
## pipelineFunctions/MutationCreateUserFunction.req.vtl

## [Start] Replace password in input with hash
## Set hash from previous pipeline function result
#set( $hash = $ctx.prev.result.hash )
## Set the password field as hash if present
$util.qr($context.args.input.put("password", $util.defaultIfNull($hash, $context.args.input.password)))
## [End] Replace password in input with hash

## [Start] Set default values. **
$util.qr($context.args.input.put("id", $util.defaultIfNull($ctx.args.input.id, $util.autoId())))
#set( $createdAt = $util.time.nowISO8601() )
## Automatically set the createdAt timestamp. **
$util.qr($context.args.input.put("createdAt", $util.defaultIfNull($ctx.args.input.createdAt, $createdAt)))
## Automatically set the updatedAt timestamp. **
$util.qr($context.args.input.put("updatedAt", $util.defaultIfNull($ctx.args.input.updatedAt, $createdAt)))
## [End] Set default values. **
## [Start] Prepare DynamoDB PutItem Request. **
$util.qr($context.args.input.put("__typename", "User"))
#set( $condition = {
  "expression": "attribute_not_exists(#id)",
  "expressionNames": {
      "#id": "id"
  }
} )
#if( $context.args.condition )
  #set( $condition.expressionValues = {} )
  #set( $conditionFilterExpressions = $util.parseJson($util.transform.toDynamoDBConditionExpression($context.args.condition)) )
  $util.qr($condition.put("expression", "($condition.expression) AND $conditionFilterExpressions.expression"))
  $util.qr($condition.expressionNames.putAll($conditionFilterExpressions.expressionNames))
  $util.qr($condition.expressionValues.putAll($conditionFilterExpressions.expressionValues))
#end
#if( $condition.expressionValues && $condition.expressionValues.size() == 0 )
  #set( $condition = {
  "expression": $condition.expression,
  "expressionNames": $condition.expressionNames
} )
#end
{
  "version": "2017-02-28",
  "operation": "PutItem",
  "key": #if( $modelObjectKey ) $util.toJson($modelObjectKey) #else {
  "id":   $util.dynamodb.toDynamoDBJson($ctx.args.input.id)
} #end,
  "attributeValues": $util.dynamodb.toMapValuesJson($context.args.input),
  "condition": $util.toJson($condition)
}
## [End] Prepare DynamoDB PutItem Request. **
``` 
In the res template, convert to JSON and return the result.
```vtl
## pipelineFunctions/MutationCreateUserFunction.res.vtl

#if( $ctx.error )
  $util.error($ctx.error.message, $ctx.error.type)
#end
$util.toJson($ctx.result)
```

### Step 6 - Add hash password lambda to pipeline
To add a lambda function to an AppSync pipeline resolver, you need:
* A lambda function. You already created the hash password lambda function.
* An AppSync DataSource **HashPasswordLambdaDataSource** that targets the lambda function.
* An AWS IAM role **HashPasswordLambdaDataSourceRole** that allows AppSync to invoke the lambda function on your behalf to the stack’s Resources block.
* A pipeline function **InvokeHashPasswordLambdaDataSource** resource that invokes the **HashPasswordLambdaDataSource**.
* Update **MutationSignupUserResolver** resource block and add lambda function in the pipeline.
Here is the complete resources block in a stack(like CustomResources.json) in the stacks/ directory.
```json
// stacks/CustomResources.json

{
// ... The rest of the template
  "Resources": {
    "InvokeHashPasswordLambdaDataSource": {
      "Type": "AWS::AppSync::FunctionConfiguration",
      "Properties": {
        "ApiId": {
          "Ref": "AppSyncApiId"
        },
        "Name": "InvokeHashPasswordLambdaDataSource",
        "DataSourceName": "HashPasswordLambdaDataSource",
        "FunctionVersion": "2018-05-29",
        "RequestMappingTemplateS3Location": {
          "Fn::Sub": [
            "s3://${S3DeploymentBucket}/${S3DeploymentRootKey}/pipelineFunctions/${ResolverFileName}",
            {
              "S3DeploymentBucket": {
                "Ref": "S3DeploymentBucket"
              },
              "S3DeploymentRootKey": {
                "Ref": "S3DeploymentRootKey"
              },
              "ResolverFileName": {
                "Fn::Join": [".", ["InvokeHashPasswordLambdaDataSource", "req", "vtl"]]
              }
            }
          ]
        },
        "ResponseMappingTemplateS3Location": {
          "Fn::Sub": [
            "s3://${S3DeploymentBucket}/${S3DeploymentRootKey}/pipelineFunctions/${ResolverFileName}",
            {
              "S3DeploymentBucket": {
                "Ref": "S3DeploymentBucket"
              },
              "S3DeploymentRootKey": {
                "Ref": "S3DeploymentRootKey"
              },
              "ResolverFileName": {
                "Fn::Join": [".", ["InvokeHashPasswordLambdaDataSource", "res", "vtl"]]
              }
            }
          ]
        }
      },
      "DependsOn": "HashPasswordLambdaDataSource"
    },
    "HashPasswordLambdaDataSourceRole": {
      "Type": "AWS::IAM::Role",
      "Properties": {
        "RoleName": {
          "Fn::If": [
            "HasEnvironmentParameter",
            {
              "Fn::Join": [
                "-",
                [
                  "HashPasswordLambdabf85",
                  {
                    "Ref": "AppSyncApiId"
                  },
                  {
                    "Ref": "env"
                  }
                ]
              ]
            },
            {
              "Fn::Join": [
                "-",
                [
                  "HashPasswordLambdabf85",
                  {
                    "Ref": "AppSyncApiId"
                  }
                ]
              ]
            }
          ]
        },
        "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::If": [
                      "HasEnvironmentParameter",
                      {
                        "Fn::Sub": [
                          "arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:amplifypipelineHashPassword-${env}",
                          {
                            "env": {
                              "Ref": "env"
                            }
                          }
                        ]
                      },
                      {
                        "Fn::Sub": [
                          "arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:amplifypipelineHashPassword",
                          {}
                        ]
                      }
                    ]
                  }
                }
              ]
            }
          }
        ]
      }
    },
    "HashPasswordLambdaDataSource": {
      "Type": "AWS::AppSync::DataSource",
      "Properties": {
        "ApiId": {
          "Ref": "AppSyncApiId"
        },
        "Name": "HashPasswordLambdaDataSource",
        "Type": "AWS_LAMBDA",
        "ServiceRoleArn": {
          "Fn::GetAtt": ["HashPasswordLambdaDataSourceRole", "Arn"]
        },
        "LambdaConfig": {
          "LambdaFunctionArn": {
            "Fn::If": [
              "HasEnvironmentParameter",
              {
                "Fn::Sub": [
                  "arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:amplifypipelineHashPassword-${env}",
                  {
                    "env": {
                      "Ref": "env"
                    }
                  }
                ]
              },
              {
                "Fn::Sub": ["arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:amplifypipelineHashPassword", {}]
              }
            ]
          }
        }
      },
      "DependsOn": "HashPasswordLambdaDataSourceRole"
    },
    "MutationCreateUserFunction": {
      "Type": "AWS::AppSync::FunctionConfiguration",
      "Properties": {
        "ApiId": {
          "Ref": "AppSyncApiId"
        },
        "Name": "MutationCreateUserFunction",
        "DataSourceName": "UserTable",
        "FunctionVersion": "2018-05-29",
        "RequestMappingTemplateS3Location": {
          "Fn::Sub": [
            "s3://${S3DeploymentBucket}/${S3DeploymentRootKey}/pipelineFunctions/${ResolverFileName}",
            {
              "S3DeploymentBucket": {
                "Ref": "S3DeploymentBucket"
              },
              "S3DeploymentRootKey": {
                "Ref": "S3DeploymentRootKey"
              },
              "ResolverFileName": {
                "Fn::Join": [".", ["MutationCreateUserFunction", "req", "vtl"]]
              }
            }
          ]
        },
        "ResponseMappingTemplateS3Location": {
          "Fn::Sub": [
            "s3://${S3DeploymentBucket}/${S3DeploymentRootKey}/pipelineFunctions/${ResolverFileName}",
            {
              "S3DeploymentBucket": {
                "Ref": "S3DeploymentBucket"
              },
              "S3DeploymentRootKey": {
                "Ref": "S3DeploymentRootKey"
              },
              "ResolverFileName": {
                "Fn::Join": [".", ["MutationCreateUserFunction", "res", "vtl"]]
              }
            }
          ]
        }
      }
    },
    "MutationSignupUserResolver": {
      "Type": "AWS::AppSync::Resolver",
      "Properties": {
        "ApiId": {
          "Ref": "AppSyncApiId"
        },
        "TypeName": "Mutation",
        "FieldName": "signupUser",
        "Kind": "PIPELINE",
        "PipelineConfig": {
          "Functions": [
            {
              "Fn::GetAtt": ["InvokeHashPasswordLambdaDataSource", "FunctionId"]
            },
            {
              "Fn::GetAtt": ["MutationCreateUserFunction", "FunctionId"]
            }
          ]
        },
        "RequestMappingTemplateS3Location": {
          "Fn::Sub": [
            "s3://${S3DeploymentBucket}/${S3DeploymentRootKey}/resolvers/${ResolverFileName}",
            {
              "S3DeploymentBucket": {
                "Ref": "S3DeploymentBucket"
              },
              "S3DeploymentRootKey": {
                "Ref": "S3DeploymentRootKey"
              },
              "ResolverFileName": {
                "Fn::Join": [".", ["Mutation", "signupUser", "req", "vtl"]]
              }
            }
          ]
        },
        "ResponseMappingTemplateS3Location": {
          "Fn::Sub": [
            "s3://${S3DeploymentBucket}/${S3DeploymentRootKey}/resolvers/${ResolverFileName}",
            {
              "S3DeploymentBucket": {
                "Ref": "S3DeploymentBucket"
              },
              "S3DeploymentRootKey": {
                "Ref": "S3DeploymentRootKey"
              },
              "ResolverFileName": {
                "Fn::Join": [".", ["Mutation", "signupUser", "res", "vtl"]]
              }
            }
          ]
        }
      },
      "DependsOn": ["MutationCreateUserFunction", "InvokeHashPasswordLambdaDataSource"]
    }
  }
}
```
* Add templates for **InvokeHashPasswordLambdaDataSource** pipeline function.
```vtl
## pipelineFunctions/InvokeHashPasswordLambdaDataSource.req.vtl

## [Start] Invoke AWS Lambda data source: HashPasswordLambdaDataSource. **
{
  "version": "2018-05-29",
  "operation": "Invoke",
  "payload": {
      "typeName": "$ctx.stash.get("typeName")",
      "fieldName": "$ctx.stash.get("fieldName")",
      "arguments": $util.toJson($ctx.arguments),
      "identity": $util.toJson($ctx.identity),
      "source": $util.toJson($ctx.source),
      "request": $util.toJson($ctx.request),
      "prev": $util.toJson($ctx.prev)
  }
}
## [End] Invoke AWS Lambda data source: HashPasswordLambdaDataSource. **
```
```vtl
## pipelineFunctions/InvokeHashPasswordLambdaDataSource.res.vtl

## [Start] Handle error or return result. **
#if( $ctx.error )
  $util.error($ctx.error.message, $ctx.error.type)
#end
$util.toJson($ctx.result)
## [End] Handle error or return result. **
```
* Now deploy the latest code to AWS.
```console
$ amplify push
```

### Step 7 - Test your mutation
* Run the following command. It will open the AppSync console where you can run graphQL queries and mutation under Queries tab.
```console
$ amplify console api
```
* Create new user using the signupUser mutation.
```graphql
mutation {
  signupUser(
    input: {
      name: "Satvik Daga", 
      email: "test@example.com", 
      password: "test123"
    }) {
    id
    name
    email
    password
  }
}
```
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/30w7u7yulaunsg3q67xh.gif)

The password of the newly created user is hashed.
First, the password field is hashed by the HashPasword Lambda function, and then a user with hashed password is created by the pipeline resolver for **signupUser mutation**.

*Note: You can also test your app locally by running the following command `amplify mock`. Click  [here](https://docs.amplify.aws/cli/usage/mock#local-mocking) for more details.*

## Conclusion
*Congratulations!* You have successfully added a pipeline resolver to your AWS Amplify application. Now you can easily add a pipeline resolver for any graphQL operation.

You can find the complete source code for this tutorial on GitHub.
{% github dagasatvik10/amplify-pipeline-tutorial %}