TypeDynamo is an ORM for your Typescript projects running in Node.js environment. Its goal is to help you develop backend applications that uses DynamoDB by abstracting most of the Dynamo boilerplate and letting you focus on what really matters: querying and writing your data!
TypeDynamo is completely agnostic to your server structure, so it supports both serverless and serverfull projects (see more in the Demo section).
This library is heavily inspired by other famous ORMs and ODMs, like TypeORM, Sequelize and Mongoose.
Some of TypeDynamo features:
- Easy declaration for your tables and indexes;
- Very simple CRUD methods with promise-like and chaining style;
- Type-safe database operations: all TypeDynamo methods have it's signature based on your table/index declaration, so you're allways type-safe;
- Pagination out of the box;
- Expression resolvers: never write complicated expressions with attribute names and values again!
...and more!
Table of Contents
Instalation
yarn
yarn add type-dynamo
npm
npm install --save type-dynamo
Getting started
Dynamo setup
In order to use DynamoDB in your projects, you must have an AWS access key and secret key. If you don't have it, refer to this link.
Now, you just have to create a TypeDynamo instance by passing your configuration:
// dynamo.config.ts
Note: It's a bad practice to put your keys hardcoded like that. In real projects you should set your keys as Node environment variables and access them in your code:
// dynamo.config.ts
As an option, you could define your keys at ~/.aws/credentials file. If you don't know how to do that, refer to this link. After that, you can instantiate TypeDynamo with no arguments:
// dynamo.config.ts
Defining your Schema
In DynamoDB, you must allways declare an attribute as a partition key and optionally another attribute as a sort key for your Table. The choice of your key is very important since Dynamo will index your table based on the provided keys, which means that you'll be able to access your items immediately through this keys. For more information, checkout this link.
With that in mind, TypeDynamo makes your schema declaration very easy, since your table model is just a regular Typescript class. Let's say you have the following User class:
All you have to do is call your typeDynamo instance define method, passing your table configuration:
// User.ts
... and that's all! You're ready to start querying and writing data to Dynamo!
Note: DynamoDB requires the partitionKey and sortKey attributes to be of type string or number. So if you declare an boolean attribute as your partitionKey, for example, DynamoDB will throw an error at execution time. Although, TypeDynamo cannot prevent this error in compile time due to a TypeScript limitation.
Database operations
TypeDynamo provides 4 high level functions to help you querying and writing data: find(), save(), update() and delete(). Let's dive into it!
Querying data
TypeDynamo makes easier to retrieve data from Dynamo by exposing find(), a high level function for reading the data. Let's see some examples based on the User schema declared in the early section:
- Getting a specific user by id
- Getting many specific users by ids
- Getting all users in the table
- Getting a paginated list of users with fewer attributes
- Getting a user list applying a filter expression
To support every use case of reading data from Dynamo, the find() method has 4 overload signatures:
find // makes a Dynamo Scan request behind the scenes findkeys: Array<Key> // makes a Dynamo BatchGetItem behind the scenes findkey: Key // makes a Dynamo GetItem behind the scenes findpartitionKey: PartitionKey // makes either a GetItem or Query, depending whether the schema has declared a sortKey.
This way, TypeDynamo will allways make the Dynamo request that best fits to your use case.
PS: The Key type is actually a generic type depending on your schema declaration. In the provided User schema example, you would have type PartitionKey = { id: string }
and type Key = { id: string }
as well. Notice that since this table has only a partition key, TypeDynamo will never make a query request because it doesn't make sense: you can get any item with the partition key already. But if you have a schema declaration with a composite key like this:
// UserOrder.ts
...then you have type PartitionKey = { userId: string }
, type SortKey = { orderId: string }
and type Key = { userId: string, orderId: string }
. This way, TypeDynamo can know that when you call find() like UserOrderRepo.find({ userId: '1', orderId: 'abc'})
it must make a GetItem request, since you are getting a specific item from the table. But if you're calling find() like UserOrderRepo.find({ userId: '1'})
you're actually making a query, because there could be more than one item in the table with this userId. So it will look for every item in the table with this userId and return the matched results.
A great thing about find() is that it comes with a built-in workaround for DynamoDB limitations in the result size for BatchGetItem, Scan and Query methods, so you don't have to worry about that.
Also, find() method is strongly typed so if you try to pass invalid arguments TypeScript will complain about it. In our User example, all of these calls would cause a compiler error:
UserRepo.find.execute // Compiler error, because user id is of type string and not boolean UserRepo.find.withAttributes.execute // Compiler error bacause attribute 'lastName' does not belong to User UserRepo.find.execute // Compiler error because 'email' does not belong to User Key
Writing new data
Many times you're going to need not only to query data from the database, but also write new data into it. TypeDynamo provides the high level save() method for that. Let's get into some examples with the User schema:
- Saving a new user
- Saving many new users
- Writing a new user only if not already exists
Like find(), the save() method has overload signature to support both single and batch write operations:
saveitem: Item // makes a Dynamo PutItem request behind the scenes saveitems: Item // makes a Dynamo BatchWrite behind the scenes
It also handles Dynamo limitations for BatchWrite out of the box, so you don't have to worry if you want to write more than 25 items at once, for example.
Note: By default, save() method has the same behavior of Dynamo SDK when writing an item, which means that it will overwrite any existing item unless you add a .withCondition(attributeNotExists('TABLE_KEY')) clause. Also, remember that Dynamo does not allow you to add such condition when calling BatchWriteItem, which means that you're allways subject to overwriting items when calling a save() with multiple items.
Updating data
For updating, use the update() method. TypeDynamo allows you to call update() in two different ways. A couple of examples:
- Updating a new user with two arguments - the key and the update item
- Updating a new user with just one argument - the update item
- Updating a new user under a specific condition
If you notice well, when you call update() method with just one argument, the input must contain the item key along with the attributes you want to update. Otherwise, DynamoDB can not know which item you're trying to udpate. But don't worry: this is really well typed in TypeDynamo, so you won't be able to make any mistakes.
Note: TypeDynamo update() does not currently support batch update due to DynamoDB limitations.
Deleting data
TypeDynamo exposes the high level function delete() for deleting your items. Examples:
- Deleting a single user
- Deleting a single user under a specific condition
- Deleting multiple users
To support both single and multiple delete operations, TypeDynamo delete() method has 2 signatures:
delete
Just like find() and save(), the delete() method has a workaround for DynamoDB limitations, so you don't have to worry about deleting more items than DynamoDB actually supports.
Note: When deleting many items at once, TypeDynamo can't return the deleted items from the table, since DynamoDB doesn't support it. Also, DynamoDB only supports specifying conditions to single delete operations, so when you call TypeDynamo delete() method passing more than one item, you can't specify a delete condition.
Indexes
TypeDynamo also supports Dynamo Indexes. You can declare indexes very straightforward:
// User.ts
Now, you can make operations upon indexes just like that:
UserRepo.onIndex.emailIndex.find.execute // TypeDynamo will turn this into a Query operation behind the scenes UserRepo.onIndex.emailIndex.find.allResults.execute // TypeDynamo will turn this into a Scan operation behind the scenes
Remember that Dynamo only allows Scan and Query operations on indexes.
If you have multiple indexes, you can declare them just by chaining your declaration (but don't forget that Dynamo let's you declare up to 5 indexes per table).
// both works fineUserRepo.onIndex.emailIndex.find.executeUserRepo.onIndex.nameIndex.find.execute
Dynamo requires a projection type on every index you declare. TypeDynamo supports all 3 types of projection (KEYS_ONLY, ALL and INCLUDE) and adjust the index type according to your projection.
Example:
console.loguser.id, user.name, user.email, user.age // compiles ok console.loguser.id, user.name, user.email, user.age // causes a compile error since nameIndex has projection type KEYS_ONLY
PS: When declaring projection_type = INCLUDE, you must specify the 'attributes' option:
IMPORTANT: Index names must be camel case in order to TypeDynamo preserve types.
- Explanation: It is not possible to do something like that
UserRepo.onIndex['email-index']
without losing types due to a TypeScript limitation.
In many use cases, your indexes will have both partition key and sort key, and you will want query your data by specifying the partition key and applying some condition on the sort key. This is totally possible on TypeDynamo:
Examples
- Serverless
- GraphQL Yoga - coming soon
- Express - coming soon