Version 6.x now supports both CommonJS and ESM
DynamoDB + TypeScript made simple
Table of contents
Get Started
Examples
Using npm:
$ npm i -S @hexlabs/dynamo-ts
Create a definition for your table
This can be stored and used for type information and generation in CloudFormation for example.
type MyTableType = { identifier: string; sort: string; abc: { xyz: number } };
export const myTableDefinition = TableDefinition.ofType<MyTableType>()
.withPartitionKey('identifier') // <- type checked to be a key in your type
.withSortKey('sort') // <- optional, aso type checked
.withGlobalSecondaryIndex('my-index', 'sort')
.withNoSortKey(); // Global or Local index
Build a client from the definition above
import { DynamoDBDocument } from '@aws-sdk/lib-dynamodb';
import { DynamoDB } from '@aws-sdk/client-dynamodb';
import { myTableDefinition } from './define-table';
const dynamoConfig: DynamoConfig = {
client: DynamoDBDocument.from(new DynamoDB({})),
tableName: 'my-table',
logStatements: true, // Logs all interactions with Dynamo
};
const myTableClient = TableClient.build(myTableDefinition, dynamoConfig);
This client can now be used to interact with DynamoDb
// PUT ITEM
await myTable.put({ identifier: 'id', text: 'some text' }); // This object must match the definition above
// GET ITEM
const result = await myTable.get({ identifier: 'id'});
// typeof result.item is {identifier: string; text: string}
All examples can be found in the examples
directory in this repository.
An example table for using these examples can be found in examples/example-table.ts
// Scan Table
const {member, next} = await tableClient.scan();
// typeof member = {identifier: string; make: string; model: string; year: number; colour: string}[]
// use next to paginate by passing in as argument to scan
// Filter results
// Get all cars in the year 2000
await tableClient.scan({filter: compare => compare().year.eq(2000)});
// Get Item (Partition Key and Sort Key)
await tableClient.get({make: 'Tesla', identifier: '<identifier>'});
// Get Projected Item
const result = await tableClient.get({identifier: '1234', make: 'Tesla'}, {projection: projector => projector.project('model')});
// typeof result = {model: string} | undefined;
// Put Item
await tableClient.put({identifier: '1234', make: 'Tesla', model: 'Model S', year: 2022, colour: 'white'});
//Put Item Return overwritten item
const result = await tableClient.put(
{identifier: '1234', make: 'Tesla', model: 'Model S', year: 2022, colour: 'white'},
{returnOldValues: true}
);
// typeof result.item = {identifier: string; make: string; model: string; year: number; colour: string}
// Conditionally Put Item if doesn't already exist (throws ConditionError)
await tableClient.put(
{identifier: '1234', make: 'Tesla', model: 'Model S', year: 2022, colour: 'white'},
{condition: compare => compare().notExists('identifier')}
)
//Delete (requires Partition Key and Sort Key)
await tableClient.delete({identifier: '1234', make: 'Tesla'})
// Simple Query against Partition
// Get all Cars with make 'Tesla'
await tableClient.query({make: 'Tesla'});
// Query and Filter
// Get all Nissan Cars in the year 2006
await tableClient.query({make: 'Nissan', filter: compare => compare().year.eq(2006)});
// Query an Index using KeyConditionExpression
// Get all Nissan Cars with a model beginning with '3' and order backwards
await tableClient.index('model-index').query({
make: 'Nissan',
model: sortKey => sortKey.beginsWith('3'),
dynamo: { ScanIndexForward: false }
});
// Filter with between
// Get all Nissan Cars between 2006 and 2022
await tableClient.query({make: 'Nissan', filter: compare => compare().year.between(2006, 2022)});
// Combining Filter Comparisons (and / or)
// Get all Nissan Cars between 2006 & 2007 AND with colour 'Metallic Black'
await tableClient.query({
make: 'Nissan',
filter: compare => compare().year.between(2006, 2007).and(compare().colour.eq('Metallic Black'))
});
// Projection
// Get only model and year
const result = await tableClient.query({make: 'Tesla', projection: projector => projector.project('model').project('year')});
// typeof result.member = {model: string; year: string}
//Update Model S Tesla by setting the year to 2022 and deleting the colour (undefined means delete)
await tableClient.update({key: {identifier: '1234', make: 'Tesla'}, updates: {year: 2022, colour: undefined}});
//Atomic Addition
//Update by incrementing the year by 1 atomically, if it doesn't exist set it to 2020, also set model to 'Another Model'
await tableClient.update({key: {identifier: '1234', make: 'Tesla'}, updates: {year: 1, model: 'Another Model'}, increments: [{key: 'year', start: 2020}]});
//Return Old Values
const result = await tableClient.update({key: {identifier: '1234', make: 'Tesla'}, updates: {year: 2022, colour: undefined}, return: 'ALL_OLD'});
// typeof result.item = {identifier: string; make: string; model: string; year: number; colour: string}
const result = await testTable
.batchGet([
{ identifier: '0' },
{ identifier: '3' },
{ identifier: '4' },
])
//Use and() to combine other operations against other tables
.and(
testTable2.batchGet(
[
{ identifier: '10000', sort: '0' },
{ identifier: '10008', sort: '8' },
],
{ projection: (projector) => projector.project('sort') },
),
)
.execute();
const result = await testTable
//Choose batchPut or Delete to begin the operation agains an initial table
.batchDelete({ identifier: 'id1' })
//Then, use and() to combine other operations against other tables
.and(testTable.batchPut([{ identifier: 'id2', text: 'text' }]))
.and(testTable2.batchPut([{ identifier: 'id3', text: 'text' }]))
.execute();
const result = await transactionTable
.transaction
.put({
item: { identifier: '777', count: 1, description: 'some description' },
condition: compare => compare().description.notExists
})
.then(
transactionTable.transaction.update({
key: { identifier: '777-000' },
increments: [{key: 'count', start: 0}],
updates: { count: 5 }
})
)
.execute();
const result = await transactionTable
.transaction.get([{identifier: '0'}])
.and(
testTable2.transaction.get([{identifier: '10000', sort: '0'}])
).execute()
See the Single Table Design section on Medium for a detailed explanation
Testing is no different than how you would have tested dynamo before. We use @shelf/jest-dynamodb to run a local version of dynamodb when we test. If you would like us to generate table definitions that can be used in this testing library, do the following:
- Create a file called jest-setup.ts
import {table1, table2} from './test/tables';
import {writeJestDynamoConfig} from "./src/dynamo-jest-setup";
(async () => writeJestDynamoConfig({testTable: table1, 'ThisIsTheTableNameForTable2': table2}, 'jest-dynamodb-config.js',{port: 5001}))();
- Then, in package.json, Update your scripts to include a pretest command which executes the setup file. Note that you may need to install ts-node as a dev dependency.
This will create a file named jest-dynamodb-config.js
at the root of the project which is the config file searched for by the testing library to build tables.
"scripts": {
"pretest": "ts-node ./jest-setup.ts",
...
}
- At the top of the test file you want to use dynamo in add the following to get a document client:
import { DynamoDB } from "@aws-sdk/client-dynamodb";
import { DynamoDBDocument } from "@aws-sdk/lib-dynamodb";
const dynamo = new DynamoDB({
endpoint: { hostname: 'localhost', port: 5001, protocol: 'http:', path: '/' },
region: 'local-env',
credentials: { accessKeyId: 'x', secretAccessKey: 'x' }
});
const dynamoClient = DynamoDBDocument.from(dynamo);
- Inject the client wherever you use dynamo, and you will have tables that match your dynamo definitions.
Thanks to everyone who has contributed so far!