AWS Developer Tools Blog

Introducing the Amazon DynamoDB DataMapper for JavaScript – Developer Preview

We’re happy to announce that the @aws/dynamodb-data-mapper package is now in Developer Preview and available for you to try via npm.

The Amazon DynamoDB DataMapper for JavaScript is a high-level client for writing and reading structured data to and from DynamoDB, built on top of the AWS SDK for JavaScript.

Getting started

You can install the @aws/dynamodb-data-mapper package using npm or Yarn:


$ npm install --save @aws/dynamodb-data-mapper
$ yarn add @aws/dynamodb-data-mapper

In this blog post, we will also use the @aws/dynamodb-data-mapper-annotations package, which makes it easier to define your models in TypeScript. Using this package requires an additional installation step (yarn add @aws/dynamodb-data-mapper-annotations) and requires that your project be compiled with the experimentalDecorators and emitDecoratorMetadata options enabled. For details on these options, see TypeScript’s handbook on decorators.

If your application doesn’t use TypeScript or you don’t to enable decorator processing in your application, you can use the mapper without the annotations package. We provide a full example in Defining a model without TypeScript in this blog post.

Defining a model

You can map any JavaScript class to a DynamoDB table by using the decorators supplied by the @aws/dynamodb-data-mapper-annotations package:

import {
    autoGeneratedHashKey,
    rangeKey,
    table,
} from '@aws/dynamodb-data-mapper-annotations';

@table('posts')
class Forum {
    @autoGeneratedHashKey()
    id: string;
    
    @rangeKey()
    createdAt: Date;
}

The attribute, hashKey, and rangeKey decorators attempt to use typing data emitted by the TypeScript compiler to infer the correct DynamoDB data type for a given property:

import {
    attribute,
    autoGeneratedHashKey,
    rangeKey,
    table,
} from '@aws/dynamodb-data-mapper-annotations';

@table('posts')
class Post {
    @autoGeneratedHashKey()
    id: string;
    
    @rangeKey()
    createdAt: Date;
    
    @attribute()
    authorUsername: string;
    
    @attribute()
    title: string;
}

You can also define embedded documents by omitting the table annotation:

import {
    attribute,
    autoGeneratedHashKey,
    rangeKey,
    table,
} from '@aws/dynamodb-data-mapper-annotations';

class PostMetadata {
    @attribute()
    draft: boolean;
    
    @attribute({memberType: 'String'})
    tags: Set<string>;
}

@table('posts')
class Post {
    // Attributes as defined in the previous example
    
    @attribute()
    metadata: PostMetadata;
}

Or you can include untyped or loosely typed attributes:

import {attribute} from '@aws/dynamodb-data-mapper-annotations';

class MyClass {
        @autoGeneratedHashKey()
        key: string;
        
        @attribute()
        untyped: any;
        
        @attribute()
        untypedList: Array<any>;
}

Check out the annotation package’s README for more examples.

Defining a model without TypeScript

You can also define a model in JavaScript by attaching a table name and schema using the DynamoDbTable and DynamoDbSchema symbols. These symbols are exported by the @aws/dynamodb-data-mapper package. The Post model defined previously could be defined without annotations as follows:

const {
    DynamoDbSchema,
    DynamoDbTable,
    embed,
} = require('@aws/dynamodb-data-mapper');
const v4 = require('uuid/v4');

class Post {
    // Declare methods and properties as usual
}

class PostMetadata {
    // Methods and properties
}

Object.defineProperty(PostMetadata.prototype, DynamoDbSchema, {
    value: {
        draft: {type: 'Boolean'},
        tags: {
            type: 'Set',
            memberType: 'String'
        }
    }
});

Object.defineProperties(Post.prototype, {
    [DynamoDbTable]: {
        value: 'Posts'
    },
    [DynamoDbSchema]: {
        value: {
            id: {
                type: 'String',
                keyType: 'HASH',
                defaultProvider: v4,
            },
            createdAt: {
	              type: 'Date',
	              keyType: 'RANGE'
            },
            authorUsername: {type: 'String'},
            title: {type: 'String'},
            metadata: embed(PostMetadata)
        },
    },
});

For more information about the supported field types, see the README.

Operations with DynamoDB Items

With a model defined and its corresponding table created, you can create, read, update, and delete objects from the table. Let’s create a post using the model defined previously:

import {Post} from './Post';
import {DataMapper} from '@aws/dynamo-data-mapper';
import DynamoDB = require('aws-sdk/clients/dynamodb');

const client = new DynamoDB({region: 'us-west-2'});
const mapper = new DataMapper({client});

const post = new Post();
post.createdAt = new Date();
post.authorUsername = 'User1';
post.title = 'Hello, DataMapper';
post.metadata = Object.assign(new PostMetadata(), {
    draft: true,
    tags: new Set(['greeting', 'introduction', 'en-US'])
});

mapper.put({item: post}).then(() => {
    // The post has been created!
    console.log(post.id);
});

With the post’s ID, you can retrieve the record from DynamoDB:

const toFetch = new Post();
toFetch.id = postId;
const fetched = await mapper.get({item: toFetch})

Or you can modify its contents:

fetched.metadata.draft = false;
await mapper.put({item: fetched});

or delete it from the table:

await mapper.delete({item: fetched});

Querying and scanning

You can use the schema and table name defined in your model classes to perform query and scan operations against the table they represent. Simply provide the constructor for the class that represents the records within a table, and the mapper can return instances of that class for each item retrieved:

import {Post} from './Post';
import {DataMapper} from '@aws/dynamo-data-mapper';
import DynamoDB = require('aws-sdk/clients/dynamodb');

const client = new DynamoDB({region: 'us-west-2'});
const mapper = new DataMapper({client});

for await (const post of mapper.scan({valueConstructor: Post})) {
    // Each post is an instance of the Post class
}

The scan and query methods of the mapper return asynchronous iterators and automatically continue fetching new pages of results until you break out of the loop. Asynchronous iterators are currently a stage 3 ECMAScript proposal and may not be natively supported in all environments. It can be used in TypeScript 2.3 or later or with Babel using the @babel/plugin-transform-async-generator-functions plugin package.

To query a table, you also need to provide a keyCondition that targets a single value for the hash key and optionally expresses assertions about the range key:

import {MyDomainClass} from './MyDomainClass';
import {DataMapper} from '@aws/dynamo-data-mapper';
import {between} from '@aws/dynamodb-expressions';
import DynamoDB = require('aws-sdk/clients/dynamodb');

const client = new DynamoDB({region: 'us-west-2'});
const mapper = new DataMapper({client});

const iterator = mapper.query({
    valueConstructor: MyDomainClass,
    keyCondition: {
        hashKey: 'foo',
        rangeKey: between(10, 99)
    }
});

for await (const item of iterator) {
    // Each post is an instance of MyDomainClass
}

With both query and scan, you can limit the results returned to you by applying a filter:

import {Post} from './Post';
import {DataMapper} from '@aws/dynamo-data-mapper';
import {equals} from '@aws/dynamodb-expressions';
import DynamoDB = require('aws-sdk/clients/dynamodb');

const client = new DynamoDB({region: 'us-west-2'});
const mapper = new DataMapper({client});
const iterator = mapper.query({
    valueConstructor: Post,
    filter: {
        ...equals('User1'),
        subject: 'authorUsername'
    }
});

for await (const post of iterator) {
    // Each post is an instance of the Post class
    // written by 'User1'
}

You can execute both queries and scans against the base table or against an index. To execute one of these operations against an index, supply an indexName parameter when creating the query or scan iterator:

const iterator = mapper.scan({
    valueConstructor: Post,
    indexName: 'myIndex'
});

Get involved!

Please install the package, try it out, and let us know what you think. The data mapper is a work in progress, so we welcome feature requests, bug reports, and information about the kinds of problems you’d like to solve by using this package.

You can find the project on GitHub at https://github.com/awslabs/dynamodb-data-mapper-js.