Continuing with building our GraphQL CRUD API, we now need a way to update an item. We'll do this by using a mutation.
DocumentClient update expressions are cumbersome, instead of writing them ourselves, we are going to install the dynoexpr library that will generate them for us.
But, in order to be able to bundle the third-party package into our lambda function, we will use the Amazon Lambda Node.js Library
Tomasz Lakomy: [0:00] Currently, our API allows us to get a book by its ID. We can see the result of that over here. This book, in particular, has a rating of 11, which doesn't seem right. We should be able to fix that.
[0:11] As such, we're going to implement the updateBook mutation. First, let's go back to the schema. I'm going to add a mutation, updateBook, which is going to take a UpdateBookInput as its argument, and it's going to return a book or null. I'm going to add this UpdateBookInput, and I'm going to add those fields.
[0:31] In order to update a book, we need to provide the ID. That is why there's an exclamation mark here. We can either update the title, mark the book as completed or not completed, or change the rating of a book, or any combination of the above.
[0:45] Next, we need to generate new TypeScript types. To do that, open up the terminal and run, npm run codegen. Next up, let us take a look at AWS documentation. The link to that page is in the description of this video.
[0:57] We can see that, in order to update an item, we need to provide an UpdateExpression which is somewhat complicated. In order to do that properly, we need to also specify ExpressionAttributeNames, ExpressionAttributeValues in order to replace this value over here with this "Sum", and so on.
[1:14] Honestly, it gets complicated rather quickly. Luckily, we don't have to do this by hand. We can use a third-party dependency. Let's use dynoexpr. This is an expression builder for AWS DocumentClient. We can see an example over here, that this dynoexpr takes in an object.
[1:30] It's going to generate a DynamoDB expression for us so we can focus on getting our problem solved and not necessarily writing those objects by hand. First of all, let us install it from npm. I'm going to copy the install command from over here and install it in our project.
[1:45] If you recall, all of our Lambda functions were not using any external dependencies. As such, we need to figure out, how do we bundle an external third-party dependency in our Lambda function? There are multiple ways of achieving that. In our case, we're going to use another construct provided by CDK team.
[2:02] This is aws-lambda-nodejs module. This is a module specifically designed for Node.js Lambda functions. It is using esbuild under the hood in order to compile and bundle all of our dependencies together. There's also additional benefit of using this construct, that your Lambda packages are smaller because they only contain the code and dependencies needed to run a particular function.
[2:26] Next, let us install more stuff. I'm going to install @aws-cdk/aws-lambda-nodejs and esbuild, so npm install esbuild. Notice that this is a development dependency.
[2:37] Next, we're going to go to our import section and import two things. I'm going to import nodeJsLambda from "@aws-cdk/aws-lambda-nodejs", as well as path, which is a built-in Node.js module. Next, let's add a new Lambda function to our stack. I'm going to do, const updateBookLambda. That is going to be equal to, new nodeJsLambda.NodejsFunction.
[3:00] This follows the same structure as other constructs. The first argument is this, the second argument is updateBookHandler, and the third argument, as you know, are props. I'm going to spread all the common Lambda props that we have defined for our other Lambda functions. Unlike Lambda.function, Node.js function takes in an entry as an argument.
[3:19] I'm going to do, path.join, and I'm going to get the current directory name, and get the path to functions/updateBook.ts, which we are going to create in just a minute. Next, similar to createBookLambda, we need to specify proper permissions for DynamoDB, create a data source, and so on.
[3:36] I'm going to scroll up and just steal all of that, copy and paste it over here. Instead of create, I'm going to simply do update. This has to be updated to createResolver. That's it.
[3:50] Let us move on to implementing the Lambda function. Go to functions and create updateBook.ts. This updateBook function is going to be quite similar to createBook, in terms of checking whether the books table exists, catching errors and so on.
[4:05] I'm going to copy and paste some boilerplate code. Let me close that. This is where we have to implement the body of this function. You may notice that I am using this updateBook arguments type, which was generated for us from our GraphQL schema.
[4:19] First up, to implement this function, I'm going to do, await documentClient, which is the DynamoDB document client. I'm going to run the update function, and I'm going to pass in some params -- which we are going to create in just a second -- and I'm going to return a promise.
[4:33] How do we create those params? Now is the time to use this Dynamo Expression module. I'm going to import it, so, import dynoexpr from "@tuplo/dynoexpr", and I'm going to use it in order to create an object. This object needs to be of a certain type. Luckily, this dynoexpr is a generic function, so I can pass in the type that I expect to be returned.
[4:53] In fact, our type is going to be, DynamoDB.DocumentClient.UpdateItemInput, because I'm trying to update an item in a DynamoDB table. We need to pass in a bunch of arguments to this dynamoClient update function.
[5:08] The first two arguments are going to be the table name. In our case, is the BOOKS_TABLE, and the key, that is, which book I want to update. Update call in DynamoDB can return a different set of return values.
[5:20] In our case, what we want to get is the ALL_NEW values. What that means is that, as soon as the update is going to be finished, we're going to get an entire book object, including all the values that have just been updated.
[5:33] Lastly, we need to specify what we want to update. As mentioned earlier, we would like to be able to update the title, completed, or rating, or any combination of the above. Let's implement that.
[5:44] I'm going to do update. Update takes in an object containing the fields that are supposed to be updated. I'm going to use a handle trick. This is a conditional spread. The idea is, if the book.title is defined, then I would like to spread an object containing, title equals book.title. Otherwise, I want to spread an empty object. I'm going to do the same for rating, as well as completed.
[6:10] This will ensure that the update object will contain only the values that have been actually passed in as argument to our update function. All that remains is to actually return a value from the function. I'm going to do, const result. This is going to be equal to the result of the update function.
[6:27] I'm going to return result.Attributes, which are the actual attributes from DynamoDB that were returned. I'm going to assign them type Book, because again, TypeScript doesn't know that it is, in fact, a book. With all of that in place, let us deploy it. I'm going to run cdk deploy again.
[6:44] After a successful deployment, let's give it a shot. I'm going to navigate to my GraphQL client, and we want to fix the rating of this book. I'm going to paste in a mutation. This mutation is going to get exactly the same book and update its rating to 10.
[7:01] Let me run it, and voila! It seems that the rating of this book has been, in fact, updated to 10. Let me run the getBookById query again to double-check. Awesome, the rating is set to 10.
[7:11] What if I wanted to update the rating, but also, mark the book as not complete because I was not able to finish this book, it was so bad? I'm going to update the book once again and run the query. There we go. We can see that I am able to update both values at the same time, and I can also change the title. I could change the title to, "This book is bad." Let me run it again. There you go.