Skip to main content

Modeling

To better understand how to model with Dynamode it is recommended that you first read about DynamoDB. In comparison to relational databases, DynamoDB requires a different modeling approach. Here are some helpful documents:

Possible values

DynamoDB limits possible data types that can be saved in the database. To learn more: Supported data types and naming rules in Amazon DynamoDB.

Supported DynamoDB data types and its Dynamode equivalents:

DynamoDB typeDynamode Typescript equivalentNotes
No valueundefinedDynamoDB non existent values are mapped to undefined.
NullnullNull represents an attribute with an unknown or undefined state.
StringstringPartition and sort keys can't be empty strings.
NumbernumberDynamoDB does not support Infinite and NaN values.
BinaryUint8ArrayBinary data is represented using the Uint8Array type. DynamoDB does not natively support other binary types such as File/Buffer
Booleanbooleantrue or false.
ListArray<unknown>There are no restrictions on the data types that can be stored in an Array. Elements in an array do not have to be of the same type.
MapMap<string, unknown> / Record<string, unknown> / { [key: string]: unknown } / { [key]: unknown, ... }There are no restrictions on the data types that can be stored in a Map/object. Elements in a map do not have to be of the same type.
SetSet<string> / Set<number>Set can only represent sets of numbers or strings. All the elements within a set must be of the same type.
N/ADateDynamoDB does not natively support a Date data type. To store a Date Dynamode uses Unix epoch (number) or Iso 8601 format (string).

Modeling with Typescript

In order to start modeling you need to create a class that inherits Entity.

caution

Make sure that your classes names are unique within a table. Dynamode uses class names to identify entities.

import Entity from 'dynamode/entity';

class YourModel extends Entity {
...
}

Decorators

Only decorated properties will be saved in the database. Thanks to this you can add undecorated properties to your entity that won't be saved but can be useful for your application logic. Check out the list of decorators here.

caution

There are no limits to the number of attributes that you can add, but keep in mind the DynamoDB size limits.

import attribute from 'dynamode/decorators';

class YourModel extends Entity {
// These attributes will be saved in the database (decorated)
@attribute.partitionKey.string()
key: ...;

@attribute.sortKey.string()
key: ...;

@attribute.gsi.partitionKey.string({ indexName: '...' })
key: ...;

@attribute.lsi.sortKey.number({ indexName: '...' })
key: ...;

@attribute.date.string()
key: ...;

@attribute.date.number()
key: ...;

@attribute.string()
key: ...;

// Won't be saved in the database (undecorated)
key: ...;

...
}

dynamodeEntity property

Every entity has a dynamodeEntity property that is used to identify the entity in the database. It is a string that represents the class name of the entity.

danger

Each of your entities names must be unique within a table. Dynamode uses class names to identify entities.

import Entity from 'dynamode/entity';

class YourModel extends Entity {
...
}

const instance = YourModelManager.get({ key: '...' });
console.log(instance.dynamodeEntity); // YourModel
import Entity from 'dynamode/entity';
import {entity} from 'dynamode/decorators';

@entity.customName('CustomName')
class YourModel extends Entity {
...
}

const instance = YourModelManager.get({ key: '...' });
console.log(instance.dynamodeEntity); // CustomName

Additional methods

You can add non-static and static methods to your entities that you can call later. You can also add static properties that will be available in your class.

danger

Do not override Entity.dynamodeEntity property, unless you know what you are doing.

class YourModel extends Entity {
...

constructor(props: Props) {
super();
...
}

public method() {
...
}

public static staticMethod() {
...
}
}

Generic example

Basic setup of a YourModel class:

import Entity from 'dynamode/entity';
import { attribute } from 'dynamode/decorators';



type Props = {
key: string;
...
};


class YourModel extends Entity {
// Required
@attribute.partitionKey.string()
key: string;

// Optional
@attribute.prefix(...)
@attribute.suffix(...)
@attribute.sortKey.string()
@attribute.gsi.partitionKey.string(...)
@attribute.gsi.sortKey.number(...)
@attribute.lsi.sortKey.number(...)
@attribute.date.string()
@attribute.number()
@attribute.string()
@attribute.object()
@attribute.array()
@attribute.set()
@attribute.boolean()
name: any;

// Not saved in the database
otherName: any;

constructor(props: Props) {
super();

this.key = props.key;
// other initiations
...
}

// Custom methods that can be called later
public method() {
...
}

public static staticMethod() {
...
}
}

Examples

Here are some example models that shows how flexible Dynamode modeling is. Note that the entities in every example operate on a different table, primary keys and indexes. So you can model your entities based on your needs.

Key value

The simplest example with only key and value attributes. There is no sort key for the table, meaning it uses a simple primary key.

import Entity from 'dynamode/entity';
import attribute from 'dynamode/decorators';
import TableManager from 'dynamode/table';

type KeyValueProps = {
key: string;
value: Record<string, unknown>;
};

export class KeyValue extends Entity {
@attribute.partitionKey.string()
key: string;

@attribute.object()
value: Record<string, unknown>;

constructor(props: KeyValueProps) {
super();

this.key = props.key;
this.value = props.value;
}
}

const TABLE_NAME = 'key-value';

export const KeyValueTableManager = new TableManager(KeyValue, {
tableName: TABLE_NAME,
partitionKey: 'key',
});

export const KeyValueManager = KeyValueTableManager.entityManager();

User

Another model without indexes but this time with a composite primary key (with partition and sort keys).

import Entity from 'dynamode/entity';
import { attribute } from 'dynamode/decorators';
import TableManager from 'dynamode/table';

type UserProps = {
partitionKey: string;
sortKey: string;
username: string;
email: string;
age: number;
friends: string[];
config: {
isAdmin: boolean;
};
};

export class User extends Entity {
@attribute.partitionKey.string()
partitionKey: string;

@attribute.sortKey.string()
sortKey: string;

@attribute.string()
username: string;

@attribute.string()
email: string;

@attribute.number()
age: number;

@attribute.array()
friends: string[];

@attribute.object()
config: {
isAdmin: boolean;
};

constructor(props: UserProps) {
super();

// Primary key
this.partitionKey = props.partitionKey;
this.sortKey = props.sortKey;

// Other properties
this.username = props.username;
this.email = props.email;
this.age = props.age;
this.friends = props.friends;
this.config = props.config;
}
}

const USERS_TABLE = 'users';

export const UserTableManager = new TableManager(User, {
tableName: USERS_TABLE,
partitionKey: 'partitionKey',
sortKey: 'sortKey',
});

export const UserManager = UserTableManager.entityManager();

All possible properties

A model with all possible properties that are supported in Dynamode. This model has a composite primary key and two indexes.

This model also has two properties createdAt and updatedAt that represent the timestamps of when the model was created or last updated.

type AllPossiblePropertiesProps = {
partitionKey: string;
sortKey: string;
GSI_1_PK?: string;
GSI_1_SK?: number;
LSI_1_SK?: number;
createdAt?: Date;
updatedAt?: Date;

string: string;
object: {
optional?: string;
required: number;
};
array: string[];
map: Map<string, string>;
set: Set<string>;
number?: number;
boolean: boolean;
};

const PREFIX = 'prefix';

export class AllPossibleProperties extends Entity {
// Primary key
@attribute.prefix(PREFIX)
@attribute.partitionKey.string()
partitionKey: string;

@attribute.sortKey.string()
sortKey: string;

// Indexes
@attribute.gsi.partitionKey.string({ indexName: 'GSI_1_NAME' })
GSI_1_PK?: string;

@attribute.gsi.sortKey.number({ indexName: 'GSI_1_NAME' })
GSI_1_SK?: number;

@attribute.lsi.sortKey.number({ indexName: 'LSI_1_NAME' })
LSI_1_SK?: number;

// Timestamps
@attribute.date.string()
createdAt: Date;

@attribute.date.number()
updatedAt: Date;

@attribute.string()
string: string;

@attribute.object()
object: {
optional?: string;
required: number;
};

@attribute.array()
array?: string[];

@attribute.map()
map: Map<string, string>;

@attribute.set()
set: Set<string>;

@attribute.number()
number?: number;

@attribute.boolean()
boolean: boolean;

unsaved: string;

constructor(props: AllPossiblePropertiesProps) {
super();

// Primary key
this.partitionKey = props.partitionKey;
this.sortKey = props.sortKey;

// Indexes
this.GSI_1_PK = props.GSI_1_PK;
this.GSI_1_SK = props.GSI_1_SK;
this.LSI_1_SK = props.LSI_1_SK;

// Timestamps
this.createdAt = props.createdAt || new Date();
this.updatedAt = props.updatedAt || new Date();

this.string = props.string;
this.object = props.object;
this.array = props.array;
this.map = props.map;
this.set = props.set;
this.number = props.number;
this.boolean = props.boolean;
this.unsaved = 'unsaved';
}

public method() {
console.log('method');
}

public static staticMethod() {
console.log('staticMethod');
}
}

const TABLE_NAME = 'all-possible-properties';

const AllPossiblePropertiesTableManager = new TableManager(AllPossibleProperties, {
tableName: TABLE_NAME,
partitionKey: 'partitionKey',
sortKey: 'sortKey',
indexes: {
GSI_1_NAME: {
partitionKey: 'GSI_1_PK',
sortKey: 'GSI_1_SK',
},
LSI_1_NAME: {
sortKey: 'LSI_1_SK',
},
},
createdAt: 'createdAt',
updatedAt: 'updatedAt',
});

export const AllPossiblePropertiesManager = AllPossiblePropertiesTableManager.entityManager();

Inheritance

An important feature of Dynamode is the possibility to inherit entities multiple times. That means that models can share attributes, primary keys and indexes between each other (= less boilerplate).

Notice that Entity is only inherited once as all these models are bound to the same DynamoDB table. The idea is to have one base class that represent DynamoDB table (BaseTable). The rest of the models (EntityOne, EntityTwo and EntityThree) are representing individual entities.

type TableProps = {
propPk: string;
propSk: number;
index: string;
};

class BaseTable extends Entity {
@attribute.partitionKey.string()
propPk: string;

@attribute.sortKey.number()
propSk: number;

@attribute.lsi.sortKey.string({ indexName: 'LSI_NAME' })
index: string;

constructor(props: TableProps) {
super();

this.propPk = props.propPk;
this.propSk = props.propSk;
this.index = props.index;
}
}

type EntityOneProps = TableProps & {
one: { [k: string]: number };
};

export class EntityOne extends BaseTable {
@attribute.object()
one: { [k: string]: number };

constructor(props: EntityOneProps) {
super(props);

this.one = props.one;
}
}

type EntityTwoProps = EntityOneProps & {
two: { [k: string]: string };
};

export class EntityTwo extends EntityOne {
@attribute.object()
two: { [k: string]: string };

constructor(props: EntityTwoProps) {
super(props);

this.two = props.two;
}
}

type EntityThreeProps = TableProps & {
otherProperty: any;
};

export class EntityThree extends BaseTable {
@attribute.object()
otherProperty: { [k: string]: number };

constructor(props: EntityThreeProps) {
super(props);

this.otherProperty = props.otherProperty;
}
}

const TABLE_NAME = 'inheritance';

export const BaseTableManager = new TableManager(BaseTable, {
tableName: TABLE_NAME,
partitionKey: 'propPk',
sortKey: 'propSk',
indexes: {
LSI_NAME: {
sortKey: 'index',
},
},
});

export const EntityOneManager = BaseTableManager.entityManager(EntityOne);
export const EntityTwoManager = BaseTableManager.entityManager(EntityTwo);
export const EntityThreeManager = BaseTableManager.entityManager(EntityThree);