Skip to content

An easier way to interact with DynamoDb using TypeScript

License

Notifications You must be signed in to change notification settings

hexlabsio/dynamo-ts

Repository files navigation

@hexlabs/dynamo-ts

Version

Version 6.x now supports both CommonJS and ESM

DynamoDB + TypeScript made simple

Typescript ESLint Prettier

Table of contents

Get Started
Examples

Installation

Using npm:

$ npm i -S @hexlabs/dynamo-ts

Get Started

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}

Examples

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

// 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

// 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

// 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 Item

//Delete (requires Partition Key and Sort Key)
await tableClient.delete({identifier: '1234', make: 'Tesla'})

Query Items

// 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 Items

//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}

Multi-Table Batch Gets (With Projections)

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();

Multi-Table Batch Writes

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();

Transactional Writes

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();

Transactional Gets

const result = await transactionTable
  .transaction.get([{identifier: '0'}])
    .and(
      testTable2.transaction.get([{identifier: '10000', sort: '0'}])
    ).execute()

Single Table Design

See the Single Table Design section on Medium for a detailed explanation

Testing

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:

  1. 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}))();
  1. 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",
  ...
}
  1. 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);
  1. Inject the client wherever you use dynamo, and you will have tables that match your dynamo definitions.

Contributors

Thanks to everyone who has contributed so far!

About

An easier way to interact with DynamoDb using TypeScript

Topics

Resources

License

Code of conduct

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published