A schema-based object to document mapper for Amazon DynamoDB.
This project is a fork and a drop-in replacement of the original dynamodb-data-mapper-js. The goal of this project is to continue maintaining the project. One major step on this way is already done: this project is migrated to use AWS SDK v3. Read the migration guide here for more details.
- Nova ODM / Amazon DynamoDB DataMapper For JavaScript
npm install @nova-odm/mapper @nova-odm/annotations reflect-metadata
yarn add @nova-odm/mapper @nova-odm/annotations reflect-metadata
Import reflect-metadata
at the top level of your application:
import 'reflect-metadata'
Start by defining a model using decorators from the @nova-odm/annotations
package.
import {
attribute,
hashKey,
rangeKey,
table,
} from '@nova-odm/annotations';
@table('table_name')
class MyDomainObject {
public constructor(partial?: Partial<MyDomainObject>) {
Object.assign(this, partial)
}
@hashKey()
id: string;
@rangeKey({defaultProvider: () => new Date()})
createdAt: Date;
@attribute()
completed?: boolean;
}
We also highly recommend adding a partial constructor to your domain classes as shown above.
With domain classes defined, you can interact with records in DynamoDB via an
instance of DataMapper
:
import { DataMapper } from '@nova-odm/mapper';
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
const mapper = new DataMapper({
client: new DynamoDBClient({ region: 'us-west-2' }), // the SDK client used to execute operations
tableNamePrefix: 'dev_' // optionally, you can provide a table prefix to keep your dev and prod tables separate
});
Creates (or overwrites) an item in the table
const savedObject = await mapper.put(new MyDomainObject({id: 'foo'}));
Retrieves an item from DynamoDB
const item = await mapper.get(new MyDomainObject({ id: 'foo', createdAt: new Date(946684800000) }))
NB: The promise returned by the mapper will be rejected with an
ItemNotFoundException
if the item sought is not found.
Updates an item in the table
const item = await mapper.get(new MyDomainObject({ id: 'foo', createdAt: new Date(946684800000) }));
item.completed = true;
await mapper.update(item);
Also supports partial updates:
await this.mapper.update({
item: new MyDomainObject({
id,
completed: true,
}),
onMissing: 'skip',
})
This will not update a the createdAt
property of the item in this example.
Removes an item from the table
await mapper.delete(new MyDomainObject({ id: 'foo', createdAt: new Date(946684800000) }));
Lists the items in a table or index
for await (const item of mapper.scan(MyDomainObject)) {
// individual items will be yielded as the scan is performed
}
// Optionally, scan an index instead of the table:
for await (const item of mapper.scan(MyDomainObject, { indexName: 'myIndex' })) {
// individual items will be yielded as the scan is performed
}
You can also use the pages()
method to read items in pages and get the last evaluated key:
const paginator = mapper.scan(MyDomainObject, {
startKey: {
id: 'foo',
createdAt: new Date(946684800000),
},
}).pages()
const domainObjects = []
for await (const page of paginator) {
// page will be an array of items yielded from the scan
// Note: the last evaluated key is automatically passed to the next scan operation
domainObjects.push(...page)
// You can also access the last evaluated key as an object of the shape of your model:
console.log(paginator.lastEvaluatedKey)
}
Finds a specific item (or range of items) in a table or index
for await (const foo of mapper.query(MyDomainObject, { id: 'foo' })) {
// individual items with a hash key of "foo" will be yielded as the query is performed
}
You can also use the pages()
method to read items in pages and get the last evaluated key:
const paginator = mapper.query(MyDomainObject, {
type: 'Equals',
subject: 'id',
object: 'foo',
}, {
startKey: {
id: 'foo',
createdAt: new Date(946684800000),
},
}).pages()
const domainObjects = []
for await (const page of paginator) {
// page will be an array of items yielded from the scan
// Note: the last evaluated key is automatically passed to the next scan operation
domainObjects.push(...page)
// You can also access the last evaluated key as an object of the shape of your model:
console.log(paginator.lastEvaluatedKey)
}
The mapper also supports batch operations. Under the hood, the batch will
automatically be split into chunks that fall within DynamoDB's limits (25 for
batchPut
and batchDelete
, 100 for batchGet
). The items can belong to any
number of tables, and exponential backoff for unprocessed items is handled
automatically.
Creates (or overwrites) multiple items in the table
const toSave = [
new MyDomainObject({id: 'foo', completed: false}),
new MyDomainObject({id: 'bar', completed: false}),
];
for await (const persisted of mapper.batchPut(toSave)) {
// items will be yielded as they are successfully written
}
Fetches multiple items from the table
const toGet = [
new MyDomainObject({id: 'foo', createdAt: new Date(946684800000)}),
new MyDomainObject({id: 'bar', createdAt: new Date(946684800001)}),
];
for await (const found of mapper.batchGet(toGet)) {
// items will be yielded as they are successfully retrieved
}
NB: Only items that exist in the table will be retrieved. If a key is not found, it will be omitted from the result.
Removes multiple items from the table
const toRemove = [
new MyDomainObject({ id: 'foo', createdAt: new Date(946684800000) }),
new MyDomainObject({ id: 'bar', createdAt: new Date(946684800001) }),
];
for await (const found of mapper.batchDelete(toRemove)) {
// items will be yielded as they are successfully removed
}
The following example shows how to use the mapper with expressions. It will only insert a new record if the email is not already in use.
import {
AttributePath,
FunctionExpression,
UpdateExpression,
} from '@nova-odm/expressions';
// given the entity below
@table('tableName')
class MyRecord {
public constructor(partial?: Partial<MyRecord>) {
Object.assign(this, partial)
}
@hashKey()
email?: string;
@attribute()
passwordHash?: string;
@attribute()
passwordSalt?: string;
@attribute()
verified?: boolean;
@attribute()
verifyToken?: string;
}
// you make a mapper operation as follows
const aRecord = new MyRecord({
email,
passwordHash: password,
passwordSalt: salt,
verified: false,
verifyToken: token,
});
const result = await mapper.put(aRecord, {
condition: new FunctionExpression('attribute_not_exists', new AttributePath('email'))
})
Creates a table for the mapped class and waits for it to be initialized:
await mapper.createTable(MyDomainObject, {readCapacityUnits: 5, writeCapacityUnits: 5})
Like createTable
, but only creates the table if it doesn't already exist:
await mapper.ensureTableExists(MyDomainObject, {readCapacityUnits: 5, writeCapacityUnits: 5})
Deletes the table for the mapped class and waits for it to be removed:
await mapper.deleteTable(MyDomainObject)
Like deleteTable
, but only deletes the table if it exists:
await mapper.ensureTableNotExists(MyDomainObject)
The Nova ODM supports optimistic locking via the versionAttribute
decorator.
import { attribute, hashKey, rangeKey, table, versionAttribute } from '@nova-odm/annotations'
import { State, JourneyModel } from '../journey'
@table('posts')
export class Post {
public constructor(partial?: Partial<Post>) {
Object.assign(this, partial)
}
@hashKey()
public id: string
@attribute()
public title: string
@attribute()
public text: string
@versionAttribute()
public version: number
}
Every time an item is saved, the version attribute will be incremented. If the version attribute is not present on the item, it will be set to 1
. If the version attribute is present but does not match the version of the item in the table, the save will fail with a ConditionalCheckFailedException
.
You can also skip the version check/increment by passing skipVersionCheck: true
to the put
or update
methods.
await mapper.update({
item: new Post({
id: '123',
title: 'My Post',
}),
onMissing: 'skip',
skipVersionCheck: true,
})
Nova ODM is written in TypeScript and can be used with esbuild to bundle your application. However, esbuild does not emit decorator metadata, so we recommend using a esbuild-plugin-tsc
plugin.
First, install the plugin and typescript
:
npm install --save-dev esbuild-plugin-tsc typescript
or
yarn add -D esbuild-plugin-tsc typescript
Then, add the plugin to your esbuild configuration:
Javascript:
+const esbuildPluginTsc = require('esbuild-plugin-tsc');
...
esbuild.build({
...
plugins: [
+ esbuildPluginTsc(),
],
})
Typescript:
+import esbuildPluginTsc from 'esbuild-plugin-tsc';
...
esbuild.build({
...
plugins: [
+ esbuildPluginTsc(),
],
})
This project provides drop-in replacement packages for the original packages. Replace your dependencies / imports with the following respective packages:
dynamodb-data-mapper-js | Nova ODM |
---|---|
@aws/dynamodb-data-mapper | @nova-odm/mapper |
@aws/dynamodb-query-iterator | @nova-odm/query-iterator |
@aws/dynamodb-data-marshaller | @nova-odm/marshaller |
@aws/dynamodb-expressions | @nova-odm/expressions |
@aws/dynamodb-batch-iterator | @nova-odm/batch-iterator |
@aws/dynamodb-auto-marshaller | @nova-odm/auto-marshaller |
@aws/dynamodb-data-mapper-annotations | @nova-odm/annotations |
The Nova ODM is developed as a monorepo using lerna
.
More detailed documentation about the ODM's constituent packages is available
by viewing those packages directly.