Wednesday, 11 March 2020

Building Serverless CRUD services in Go with DynamoDB - Part 3

So far we've created ``createHandler.go`` and ``listHandler.go``. In part 3, we will learn how to build ``updateHandler.go`` # Getting started First, let's add the config under functions in serverless.yml ``` update: handler: bin/handlers/updateHandler package: include: - ./bin/handlers/updateHandler events: - http: path: iam/{id} method: patch cors: true ``` Create a file updateHandler.go under src/handlers Similarly, we have the below structure. ``` package main import ( "context" "encoding/json" "fmt" "github.com/aws/aws-lambda-go/events" "github.com/aws/aws-lambda-go/lambda" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/dynamodb" "github.com/aws/aws-sdk-go/service/dynamodb/expression" "gopkg.in/go-playground/validator.v9" "os" "reflect" "strings" "time" ) var svc *dynamodb.DynamoDB func init() { region := os.Getenv("AWS_REGION") // Initialize a session if session, err := session.NewSession(&aws.Config{ Region: ®ion, }); err != nil { fmt.Println(fmt.Sprintf("Failed to initialize a session to AWS: %s", err.Error())) } else { // Create DynamoDB client svc = dynamodb.New(session) } } type User struct { ID *string `json:"id,omitempty"` UserName *string `json:"user_name,omitempty" validate:"omitempty,min=4,max=20"` FirstName *string `json:"first_name,omitempty"` LastName *string `json:"last_name,omitempty"` Age *int `json:"age,omitempty"` Phone *string `json:"phone,omitempty"` Email *string `json:"email,omitempty" validate:"omitempty,email"` Role *string `json:"role,omitempty" validate:"omitempty,min=4,max=20"` IsActive *bool `json:"is_active,omitempty"` CreatedAt *string `json:"created_at,omitempty"` ModifiedAt string `json:"modified_at,omitempty"` DeactivatedAt *string `json:"deactivated_at,omitempty"` } func Update(ctx context.Context, request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { var ( tableName = aws.String(os.Getenv("IAM_TABLE_NAME")) id = aws.String(request.PathParameters["id"]) ) // TODO: Add Update logic } func main() { lambda.Start(Update) } ``` When we update a record, we need to update the column ``ModifiedAt``. ``` user := &User{ ModifiedAt: time.Now().String(), } ``` Similar to ``createHandler.go``, we parse the request body and perform validation. ``` // Parse request body json.Unmarshal([]byte(request.Body), user) // Validate user struct var validate *validator.Validate validate = validator.New() err := validate.Struct(user) if err != nil { // Status Bad Request return events.APIGatewayProxyResponse{ Body: err.Error(), StatusCode: 400, }, nil } ``` We need to create a ``dynamodb.UpdateItemInput`` for DynamoDB service to update the item. You may see that some people use the following code. ``` input := &dynamodb.UpdateItemInput{ Key: map[string]*dynamodb.AttributeValue{ "id": { S: aws.String(id), }, UpdateExpression: aws.String("set #a = :a, #b = :b, #c = :c"), ExpressionAttributeNames: map[string]*string{ "#a": &a, "#b": &b, "#c": &c, }, ExpressionAttributeValues: map[string]*dynamodb.AttributeValue{ ":a": { BOOL: aws.Bool(true), }, ":b": { BOOL: aws.Bool(true), }, ":c": { BOOL: aws.Bool(true), }, }, ReturnValues: aws.String("UPDATED_NEW"), TableName: "tableName", } ``` The above example uses ``set`` to update attribute ``a``, ``b``, and ``c`` with mapped attribute values provided in ``ExpressionAttributeValues``. With such approach, the expression cannot be dynamic as we allow users to update some specific attributes only. To do that, we use reflect to get the input struct and get the json name without a corresponding tag. Then we append each json field name and its value to UpdateBuilder by using ``UpdateBuilder.Set``. ``` u := reflect.ValueOf(user).Elem() t := u.Type() for i := 0; i < u.NumField(); i++ { f := u.Field(i) // check if it is empty if !reflect.DeepEqual(f.Interface(), reflect.Zero(f.Type()).Interface()) { jsonFieldName := t.Field(i).Name // get json field name if jsonTag := t.Field(i).Tag.Get("json"); jsonTag != "" && jsonTag != "-" { if commaIdx := strings.Index(jsonTag, ","); commaIdx > 0 { jsonFieldName = jsonTag[:commaIdx] } } // construct update update = update.Set(expression.Name(jsonFieldName), expression.Value(f.Interface())) } } ``` Create a new Builder with Update ``` builder := expression.NewBuilder().WithUpdate(update) ``` Call ``Build()`` to get the expression and error ``` expression, err := builder.Build() ``` Verify if there is an error ``` if err != nil { // Status Bad Request return events.APIGatewayProxyResponse{ Body: err.Error(), StatusCode: 400, }, nil } ``` Create ``dynamodb.UpdateItemInput`` ``` // Update a record by id input := &dynamodb.UpdateItemInput{ Key: map[string]*dynamodb.AttributeValue{ "id": { S: id, }, }, ExpressionAttributeNames: expression.Names(), ExpressionAttributeValues: expression.Values(), UpdateExpression: expression.Update(), ReturnValues: aws.String("UPDATED_NEW"), TableName: tableName, } ``` Feed it into ``UpdateItem`` ``` _, err = svc.UpdateItem(input) ``` Check if it can be updated or not ``` if err != nil { fmt.Println("Got error calling UpdateItem:") fmt.Println(err.Error()) // Status Internal Server Error return events.APIGatewayProxyResponse{ Body: err.Error(), StatusCode: 500, }, nil } // Status OK return events.APIGatewayProxyResponse{ Body: request.Body, StatusCode: 200, }, nil ``` Run the below command to deploy our code ``` ./scripts/deploy.sh ``` # Testing If you go to AWS Lambda Console, you will see there is a function called ``serverless-iam-dynamodb-dev-update`` ![image](https://user-images.githubusercontent.com/35857179/76139123-5371b100-6088-11ea-9e74-97ce9aeaeb6d.png) Go to API Gateway Console to test it, this time we need to set an id. ```json { "email": "wingkwong@gmail.com" } ``` ![image](https://user-images.githubusercontent.com/35857179/76139149-aa778600-6088-11ea-8045-26697e65c1ed.png) If the update returns 200, then go to DynamoDB to verify the result. ![image](https://user-images.githubusercontent.com/35857179/76139160-c24f0a00-6088-11ea-8300-7177bc0647e7.png) We should see that only the email has been updated. That's it for part 3. In part 4, we will create ``deleteHandler.go``.

No comments:

Post a Comment

A Fun Problem - Math

# Problem Statement JATC's math teacher always gives the class some interesting math problems so that they don't get bored. Today t...