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:
- Core components of Amazon DynamoDB
- DynamoDB Guide
- The DynamoDB Book - Alex DeBrie
- DynamoDB Tutorials - Everything You Need To Master It
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 type | Dynamode Typescript equivalent | Notes |
---|---|---|
No value | undefined | DynamoDB non existent values are mapped to undefined . |
Null | null | Null represents an attribute with an unknown or undefined state. |
String | string | Partition and sort keys can't be empty strings. |
Number | number | DynamoDB does not support Infinite and NaN values. |
Binary | Uint8Array | Binary data is represented using the Uint8Array type. DynamoDB does not natively support other binary types such as File /Buffer |
Boolean | boolean | true or false . |
List | Array<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. |
Map | Map<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. |
Set | Set<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/A | Date | DynamoDB 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
.
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.
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.
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.
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);