Dyna-Record is a strongly typed ORM (Object-Relational Mapping) tool designed for modeling and interacting with data stored in DynamoDB in a structured and type-safe manner. It simplifies the process of defining data models (entities), performing CRUD operations, and handling complex queries. To support relational data, dyna-record implements a flavor of the single-table design pattern and the adjacency list design pattern. All operations are ACID compliant transactions*. To enforce data integrity beyond the type system, schema validation is performed at runtime.
Note: ACID compliant according to DynamoDB limitations
To install Dyna-Record, use npm or yarn:
npm install dyna-record
or
yarn add dyna-record
Entities in Dyna-Record represent your DynamoDB table structure and relationships. Think of each entity as a table in a relational database, even though they will be represented on a single table.
Create a table class that extends DynaRecord base class and is decorated with the Table decorator. At a minimum, the table class must define the PartitionKeyAttribute and SortKeyAttribute.
import DynaRecord, {
Table,
PartitionKeyAttribute,
SortKeyAttribute,
PartitionKey,
SortKey
} from "dyna-record";
@Table({ name: "my-table", delimiter: "#" })
abstract class MyTable extends DynaRecord {
@PartitionKeyAttribute({ alias: "PK" })
public readonly pk: PartitionKey;
@SortKeyAttribute({ alias: "SK" })
public readonly sk: SortKey;
}
import DynaRecord, {
Table,
PartitionKeyAttribute,
SortKeyAttribute,
PartitionKey,
SortKey
} from "dyna-record";
@Table({
name: "mock-table",
delimiter: "#",
defaultFields: {
id: { alias: "Id" },
type: { alias: "Type" },
createdAt: { alias: "CreatedAt" },
updatedAt: { alias: "UpdatedAt" },
foreignKey: { alias: "ForeignKey" },
foreignEntityType: { alias: "ForeignEntityType" }
}
})
abstract class MyTable extends DynaRecord {
@PartitionKeyAttribute({ alias: "PK" })
public readonly pk: PartitionKey;
@SortKeyAttribute({ alias: "SK" })
public readonly sk: SortKey;
}
Each entity must extend the Table class. To support single table design patterns, they must extend the same tables class.
By default, each entity will have default attributes
import { Entity } from "dyna-record";
@Entity
class Student extends MyTable {
// ...
}
@Entity
class Course extends MyTable {
/// ...
}
Use the attribute decorators below to define attributes on a model. The decorator maps class properties to DynamoDB table attributes.
Attribute decorators
The alias option allows you to specify the attribute name as it appears in the DynamoDB table, different from your class property name.
Set nullable attributes as optional for optimal type safety
Attempting to remove a non-nullable attribute will result in a NullConstrainViolationError
import { Entity, Attribute } from "dyna-record";
@Entity
class Student extends MyTable {
@StringAttribute({ alias: "Username" }) // Sets alias if field in Dynamo is different then on the model
public username: string;
@StringAttribute() // Dynamo field and entity field are the same
public email: string;
@NumberAttribute({ nullable: true })
public someAttribute?: number; // Mark as optional
}
Define foreign keys in order to support @BelongsTo relationships. A foreign key is required for @HasOne and @HasMany relationships.
import {
Entity,
ForeignKeyAttribute,
ForeignKey,
NullableForeignKey,
BelongsTo
} from "dyna-record";
@Entity
class Assignment extends MyTable {
@ForeignKeyAttribute()
public readonly courseId: ForeignKey;
@BelongsTo(() => Course, { foreignKey: "courseId" })
public readonly course: Course;
}
@Entity
class Course extends MyTable {
@ForeignKeyAttribute({ nullable: true })
public readonly teacherId?: NullableForeignKey; // Set as optional
@BelongsTo(() => Teacher, { foreignKey: "teacherId" })
public readonly teacher?: Teacher; // Set as optional because its linked through NullableForeignKey
}
Dyna-Record supports defining relationships between entities such as @HasOne, @HasMany, @BelongsTo and @HasAndBelongsToMany. It does this by de-normalizing BelongsToLinks according the each pattern to support relational querying.
A relationship can be defined as nullable or non-nullable. Non-nullable relationships will be enforced via transactions and violations will result in NullConstraintViolationError
import {
Entity,
ForeignKeyAttribute,
ForeignKey,
BelongsTo,
HasOne
} from "dyna-record";
@Entity
class Assignment extends MyTable {
// 'assignmentId' must be defined on associated model
@HasOne(() => Grade, { foreignKey: "assignmentId" })
public readonly grade: Grade;
}
@Entity
class Grade extends MyTable {
@ForeignKeyAttribute()
public readonly assignmentId: ForeignKey;
// 'assignmentId' Must be defined on self as ForeignKey or NullableForeignKey
@BelongsTo(() => Assignment, { foreignKey: "assignmentId" })
public readonly assignment: Assignment;
}
import { Entity, NullableForeignKey, BelongsTo, HasMany } from "dyna-record";
@Entity
class Teacher extends MyTable {
// 'teacherId' must be defined on associated model
@HasMany(() => Course, { foreignKey: "teacherId" })
public readonly courses: Course[];
}
@Entity
class Course extends MyTable {
@ForeignKeyAttribute({ nullable: true })
public readonly teacherId?: NullableForeignKey; // Mark as optional
// 'teacherId' Must be defined on self as ForeignKey or NullableForeignKey
@BelongsTo(() => Teacher, { foreignKey: "teacherId" })
public readonly teacher?: Teacher;
}
HasAndBelongsToMany relationships require a JoinTable class. This represents a virtual table to support the relationship
import {
Entity,
JoinTable,
ForeignKey,
HasAndBelongsToMany
} from "dyna-record";
class StudentCourse extends JoinTable<Student, Course> {
public readonly studentId: ForeignKey;
public readonly courseId: ForeignKey;
}
@Entity
class Course extends MyTable {
@HasAndBelongsToMany(() => Student, {
targetKey: "courses",
through: () => ({ joinTable: StudentCourse, foreignKey: "courseId" })
})
public readonly students: Student[];
}
@Entity
class Student extends OtherTable {
@HasAndBelongsToMany(() => Course, {
targetKey: "students",
through: () => ({ joinTable: StudentCourse, foreignKey: "studentId" })
})
public readonly courses: Course[];
}
The create method is used to insert a new record into a DynamoDB table. This method automatically handles key generation (using UUIDs or custom id field if IdAttribute is set), timestamps for createdAt and updatedAt fields, and the management of relationships between entities. It leverages AWS SDK's TransactWriteCommand for transactional integrity, ensuring either complete success or rollback in case of any failure. The method handles conditional checks to ensure data integrity and consistency during creation. If a foreignKey is set on create, dyna-record will de-normalize the data required in order to support the relationship
To use the create method, call it on the model class you wish to create a new record for. Pass the properties of the new record as an object argument to the method. Only attributes defined on the model can be configured, and will be enforced via types and runtime schema validation.
const myModel: MyModel = await MyModel.create({
someAttr: "123",
otherAttr: 456,
someDate: new Date("2024-01-01")
});
const grade: Grade = await Grade.create({
gradeValue: "A+",
assignmentId: "123",
studentId: "456"
});
The method is designed to throw errors under various conditions, such as transaction cancellation due to failed conditional checks. For instance, if you attempt to create a Grade
for an Assignment
that already has one, the method throws a TransactionWriteFailedError.
Retrieve a single record by its primary key.
findById performs a direct lookup for an entity based on its primary key. It utilizes the GetCommand from AWS SDK's lib-dynamodb to execute a consistent read by default, ensuring the most recent data is fetched. Moreover, it supports eagerly loading related entities through the include option, making it easier to work with complex data relationships. findById
provides strong typing for both the fetched entity and any included associations, aiding in development-time checks and editor autocompletion.
To retrieve an entity, simply call findById on the model class with the ID of the record you wish to find.
If no record is found matching the provided ID, findById returns undefined. This behavior is consistent across all usages, whether or not related entities are included in the fetch.
const course = await Course.findById("123");
// user.id; - ok for any attribute
// user.teacher; - Error! teacher relationship was not included in query
// user.assignments; - Error! assignments relationship was not included in query
const course = await Course.findById("123", {
include: [{ association: "teacher" }, { association: "assignments" }]
});
// user.id; - ok for any attribute
// user.teacher - ok because teacher is in include
// user.assignments - ok because assignments is in include
The query method is a versatile tool for querying data from DynamoDB tables using primary key conditions and various optional filters. This method enables fetching multiple items that match specific criteria, making it ideal for situations where more than one item needs to be retrieved based on attributes of the primary key (partition key and sort key).
There are two main patterns; query by id and query by primary key
To query items using the id, simply pass the partition key value as the first parameter. This fetches all items that share the same partition key value.
The result will be an array of the entity and BelongsToLinks
Querying using the id will abstract away setting up the partition key conditions.
const customers = await Customer.query("123");
Query by partition key and sort key
const result = await Customer.query("123", {
skCondition: "Order"
});
To be more precise to the underlying data, you can specify the partition key and sort key directly. The keys here will be the partition and sort keys defined on the table class.
const orderLinks = await Customer.query({
pk: "Customer#123",
sk: { $beginsWith: "Order" }
});
The query method supports advanced filtering using the filter option. This allows for more complex queries, such as filtering items by attributes other than the primary key.
const result = await Course.query(
{
myPk: "Course|123"
},
{
filter: {
type: ["BelongsToLink", "Brewery"],
createdAt: { $beginsWith: "202" },
$or: [
{
foreignKey: "111",
updatedAt: { $beginsWith: "2023-02-15" }
},
{
foreignKey: ["222", "333"],
createdAt: { $beginsWith: "2021-09-15T" },
foreignEntityType: "Assignment"
},
{
id: "123"
}
]
}
}
);
For querying based on secondary indexes, you can specify the index name in the options.
const result = await Customer.query(
{
pk: "Customer#123",
sk: { $beginsWith: "Order" }
},
{ indexName: "myIndex" }
);
The update method enables modifications to existing items in a DynamoDB table. It supports updating simple attributes, handling nullable fields, and managing relationships between entities, including updating and removing foreign keys. Only attributes defined on the model can be updated, and will be enforced via types and runtime schema validation.
await Customer.update("123", {
name: "New Name",
address: "New Address"
});
Note: Attempting to remove a non nullable attribute will result in a NullConstraintViolationError
await ContactInformation.update("123", {
email: "new@example.com",
phone: null
});
To update the foreign key reference of an entity to point to a different entity, simply pass the new foreign key value
await PaymentMethod.update("123", {
customerId: "456"
});
Nullable foreign key references can be removed by setting them to null
Note: Attempting to remove a non nullable foreign key will result in a NullConstraintViolationError
await Pet.update("123", {
ownerId: null
});
There is an instance update
method that has the same rules above, but returns the full updated instance.
const updatedInstance = await petInstance.update({
ownerId: null
});
The delete method is used to remove an entity from a DynamoDB table, along with handling the deletion of associated items in relationships (like HasMany, HasOne, BelongsTo) to maintain the integrity of the database schema.
await User.delete("user-id");
When deleting entities involved in HasMany or HasOne relationships:
If a Pet belongs to an Owner (HasMany relationship), deleting the Pet will remove its BelongsToLink from the Owner's partition. If a Home belongs to a Person (HasOne relationship), deleting the Home will remove its BelongsToLink from the Person's partition.
await Home.delete("123");
This deletes the Home entity and its BelongsToLink with a Person.
For entities part of a HasAndBelongsToMany relationship, deleting one entity will remove the association links (join table entries) with the related entities.
If a Book has and belongs to many authors:
await Book.delete("123");
This deletes a Book entity and its association links with Author entities.
If deleting an entity or its relationships fails due to database constraints or errors during transaction execution, a TransactionWriteFailedError is thrown, possibly with details such as ConditionalCheckFailedError or NullConstraintViolationError for more specific issues related to relationship constraints or nullability violations.
Dyna-Record integrates type safety into your DynamoDB interactions, reducing runtime errors and enhancing code quality.
To enable debug logging set process.env.DYNA_RECORD_LOGGING_ENABLED
to "true"
. When enabled, dyna-record will log to console the dynamo operations it is performing.
Generated using TypeDoc