npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

dyna-record

v0.6.1

Published

Typescript Data Modeler and ORM for Dynamo

Readme

Dyna-Record

API Documentation

Medium Article

Dyna-Record is a strongly typed Data Modeler and 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

Table of Contents

Getting Started

Installation

To install Dyna-Record, use npm or yarn:

npm install dyna-record

or

yarn add dyna-record

Defining Entities

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.

Table

Docs

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.

Basic usage

import DynaRecord, {
  Table,
  PartitionKeyAttribute,
  SortKeyAttribute,
  PartitionKey,
  SortKey
} from "dyna-record";

@Table({ name: "my-table" })
abstract class MyTable extends DynaRecord {
  @PartitionKeyAttribute({ alias: "PK" })
  public readonly pk: PartitionKey;

  @SortKeyAttribute({ alias: "SK" })
  public readonly sk: SortKey;
}

Customizing the default field table aliases or delimiter

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" }
  }
})
abstract class MyTable extends DynaRecord {
  @PartitionKeyAttribute({ alias: "PK" })
  public readonly pk: PartitionKey;

  @SortKeyAttribute({ alias: "SK" })
  public readonly sk: SortKey;
}

Entity

Docs

Each entity must extend the Table class. To support single table design patterns, they must extend the same tables class.

Each entity must declare its type property as a string literal matching the class name. This enables compile-time type safety for query filters and return types. Omitting this declaration will produce a compile error at the @Entity decorator.

By default, each entity will have default attributes

  • The partition key defined on the table class
  • The sort key defined on the table class
  • id - The id for the model. This will be an autogenerated uuid unless IdAttribute is set on a non-nullable entity attribute.
  • type - The type of the entity. Value is the entity class name. Must be declared as a string literal via declare readonly type: "ClassName".
  • createdAt - The timestamp of when the entity was created
  • updatedAt - Timestamp of when the entity was updated last
import { Entity } from "dyna-record";

@Entity
class Student extends MyTable {
  declare readonly type: "Student";
  // ...
}

@Entity
class Course extends MyTable {
  declare readonly type: "Course";
  // ...
}

Note: declare readonly type is a pure TypeScript type annotation with zero runtime impact. The ORM sets type to the class name automatically. The declaration simply tells TypeScript the exact literal type, enabling typed query filters and return type narrowing.

Attributes

Use the attribute decorators below to define attributes on a model. The decorator maps class properties to DynamoDB table attributes.

import { Entity, Attribute } from "dyna-record";

@Entity
class Student extends MyTable {
  declare readonly type: "Student";

  @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
}

@ObjectAttribute

Use @ObjectAttribute to define structured, typed object attributes on an entity. Objects are validated at runtime and stored as native DynamoDB Map types.

Define the shape using an ObjectSchema and derive the TypeScript type with InferObjectSchema:

import { Entity, ObjectAttribute } from "dyna-record";
import type { ObjectSchema, InferObjectSchema } from "dyna-record";

const addressSchema = {
  street: { type: "string" },
  city: { type: "string" },
  zip: { type: "number", nullable: true },
  tags: { type: "array", items: { type: "string" } },
  category: { type: "enum", values: ["home", "work", "other"] },
  createdDate: { type: "date" },
  geo: {
    type: "object",
    fields: {
      lat: { type: "number" },
      lng: { type: "number" }
    }
  }
} as const satisfies ObjectSchema;

@Entity
class Store extends MyTable {
  declare readonly type: "Store";

  @ObjectAttribute({ alias: "Address", schema: addressSchema })
  public readonly address: InferObjectSchema<typeof addressSchema>;
}
  • Supported field types: "string", "number", "boolean", "date" (stored as ISO strings, exposed as Date objects), "enum" (via values), nested "object" (via fields), and "array" (via items)
  • Nullable fields: Set nullable: true on individual non-object fields within the schema to make them optional
  • Object attributes are never nullable: DynamoDB cannot update nested document paths (e.g., address.geo.lat) if the parent object does not exist. To prevent this, @ObjectAttribute fields always exist as at least an empty object {}. Nested object fields within the schema are also never nullable. Non-object fields (primitives, enums, dates, arrays) can still be nullable.
  • Alias support: Use the alias option to map to a different DynamoDB attribute name
  • Storage: Objects are stored as native DynamoDB Map types
  • Partial updates: Updates are partial — only the fields you provide are modified. Omitted fields are preserved. Nested objects are recursively merged. See Updating Object Attributes
  • Filtering: Object attributes support dot-path filtering in queries — see Filtering on Object Attributes
Enum fields

Use { type: "enum", values: [...] } to define a field that only accepts specific string values. The TypeScript type is inferred as a union of the provided values, and invalid values are rejected at runtime via Zod validation.

Enum fields can appear at any nesting level — top-level, inside nested objects, or as array items:

const schema = {
  // Top-level enum: inferred as "active" | "inactive"
  status: { type: "enum", values: ["active", "inactive"] },

  // Nullable enum: inferred as "home" | "work" | "other" | undefined
  category: { type: "enum", values: ["home", "work", "other"], nullable: true },

  // Enum inside a nested object
  geo: {
    type: "object",
    fields: {
      accuracy: { type: "enum", values: ["precise", "approximate"] }
    }
  },

  // Array of enum values: inferred as ("admin" | "user")[]
  roles: { type: "array", items: { type: "enum", values: ["admin", "user"] } }
} as const satisfies ObjectSchema;

The schema must be declared with as const satisfies ObjectSchema so TypeScript preserves the literal string values for type inference. At runtime, providing an invalid value (e.g., status: "unknown") throws a ValidationError.

Foreign Keys

Define foreign keys in order to support @BelongsTo relationships. A foreign key is required for @HasOne and @HasMany relationships.

  • 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 foreign key attributes as optional for optimal type safety
  • Attempting to remove an entity from a non-nullable foreign key will result in a NullConstrainViolationError
  • Always provide the referenced entity class to @ForeignKeyAttribute (for example @ForeignKeyAttribute(() => Customer)); this allows DynaRecord to enforce referential integrity even when no relationship decorator is defined.
  • Create and Update automatically add DynamoDB condition checks for standalone foreign keys (those without a relationship decorator) to ensure the referenced entity exists, enabling referential integrity even when no denormalised access pattern is required.
import {
  Entity,
  ForeignKeyAttribute,
  ForeignKey,
  NullableForeignKey,
  BelongsTo
} from "dyna-record";

@Entity
class Assignment extends MyTable {
  declare readonly type: "Assignment";

  @ForeignKeyAttribute(() => Course)
  public readonly courseId: ForeignKey<Course>;

  @BelongsTo(() => Course, { foreignKey: "courseId" })
  public readonly course: Course;
}

@Entity
class Course extends MyTable {
  declare readonly type: "Course";

  @ForeignKeyAttribute(() => Teacher, { nullable: true })
  public readonly teacherId?: NullableForeignKey<Teacher>; // Set as optional

  @BelongsTo(() => Teacher, { foreignKey: "teacherId" })
  public readonly teacher?: Teacher; // Set as optional because its linked through NullableForeignKey
}

Relationships

Dyna-Record supports defining relationships between entities such as @HasOne, @HasMany, @BelongsTo and @HasAndBelongsToMany. It does this by de-normalizing records to each of its related entities partitions.

A relationship can be defined as nullable or non-nullable. Non-nullable relationships will be enforced via transactions and violations will result in NullConstraintViolationError

HasOne

Docs

import {
  Entity,
  ForeignKeyAttribute,
  ForeignKey,
  BelongsTo,
  HasOne
} from "dyna-record";

@Entity
class Assignment extends MyTable {
  declare readonly type: "Assignment";

  // 'assignmentId' must be defined on associated model
  @HasOne(() => Grade, { foreignKey: "assignmentId" })
  public readonly grade: Grade;
}

@Entity
class Grade extends MyTable {
  declare readonly type: "Grade";

  @ForeignKeyAttribute(() => Assignment)
  public readonly assignmentId: ForeignKey<Assignment>;

  // 'assignmentId' Must be defined on self as ForeignKey or NullableForeignKey
  @BelongsTo(() => Assignment, { foreignKey: "assignmentId" })
  public readonly assignment: Assignment;
}

HasMany

Docs

import { Entity, NullableForeignKey, BelongsTo, HasMany } from "dyna-record";

@Entity
class Teacher extends MyTable {
  declare readonly type: "Teacher";

  // 'teacherId' must be defined on associated model
  @HasMany(() => Course, { foreignKey: "teacherId" })
  public readonly courses: Course[];
}

@Entity
class Course extends MyTable {
  declare readonly type: "Course";

  @ForeignKeyAttribute(() => Teacher, { nullable: true })
  public readonly teacherId?: NullableForeignKey<Teacher>; // Mark as optional

  // 'teacherId' Must be defined on self as ForeignKey or NullableForeignKey
  @BelongsTo(() => Teacher, { foreignKey: "teacherId" })
  public readonly teacher?: Teacher;
}

By default, a HasMany relationship is bi-directional—records are denormalized into both the parent and child entity partitions. This setup supports access patterns that allow each entity to retrieve its associated records. However, updating a HasMany entity requires updating its denormalized record in every associated partition, which can lead to issues given DynamoDB's 100-item transaction limit.

To mitigate this, you can specify uniDirectional in the HasMany decorator and remove the BelongsTo relationship from the child entity. With this configuration, only the parent-to-child access pattern is supported.

import { Entity, NullableForeignKey, BelongsTo, HasMany } from "dyna-record";

@Entity
class Teacher extends MyTable {
  declare readonly type: "Teacher";

  // 'teacherId' must be defined on associated model
  @HasMany(() => Course, { foreignKey: "teacherId", uniDirectional: true })
  public readonly courses: Course[];
}

@Entity
class Course extends MyTable {
  declare readonly type: "Course";

  @ForeignKeyAttribute(() => Teacher, { nullable: true })
  public readonly teacherId?: NullableForeignKey<Teacher>; // Mark as optional
}

HasAndBelongsToMany

Docs

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 {
  declare readonly type: "Course";

  @HasAndBelongsToMany(() => Student, {
    targetKey: "courses",
    through: () => ({ joinTable: StudentCourse, foreignKey: "courseId" })
  })
  public readonly students: Student[];
}

@Entity
class Student extends OtherTable {
  declare readonly type: "Student";

  @HasAndBelongsToMany(() => Course, {
    targetKey: "students",
    through: () => ({ joinTable: StudentCourse, foreignKey: "studentId" })
  })
  public readonly courses: Course[];
}

CRUD Operations

Create

Docs

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.

Basic Usage

const myModel: MyModel = await MyModel.create({
  someAttr: "123",
  otherAttr: 456,
  someDate: new Date("2024-01-01")
});

Example: Creating an Entity with Relationships

const grade: Grade = await Grade.create({
  gradeValue: "A+",
  assignmentId: "123",
  studentId: "456"
});

Skipping Referential Integrity Checks

By default, when creating entities with foreign key references, dyna-record performs condition checks to ensure that referenced entities exist. This prevents creating entities with invalid foreign key references. However, in high-contention or high-throughput systems where the same foreign key may be referenced in parallel operations, these condition checks can fail due to transaction conflicts. In scenarios such as bulk imports or when you've already verified the references, you may want to skip these checks to prevent such failures.

To skip referential integrity checks, pass an options object as the second parameter with referentialIntegrityCheck: false:

const grade: Grade = await Grade.create(
  {
    gradeValue: "A+",
    assignmentId: "123",
    studentId: "456"
  },
  { referentialIntegrityCheck: false }
);

Note: When referentialIntegrityCheck is set to false, the condition checks that verify foreign key references exist are skipped. This means you can create entities even if the referenced entities don't exist, which may lead to data integrity issues. Use this option with caution.

Error handling

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.

Notes

  • Automatic Timestamp Management: The createdAt and updatedAt fields are managed automatically and reflect the time of creation and the last update, respectively.
  • Automatic ID Generation: Each entity created gets a unique id generated by the uuidv4 method.
    • This can be customized IdAttribute to support custom id attributes
  • Relationship Management: The ORM manages entity relationships through DynamoDB's single-table design patterns, creating and maintaining the necessary links between related entities.
  • Conditional Checks: To ensure data integrity, the create method performs various conditional checks, such as verifying the existence of entities that new records relate to.
  • Error Handling: Errors during the creation process are handled gracefully, with specific errors thrown for different failure scenarios, such as conditional check failures or transaction cancellations.

FindById

Docs

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.

Find an entity by id
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

Including related entities

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

Query

Docs

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

Basic usage

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 or related entities that match the filters

Query by id

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"
});
Query by primary key

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. The sk value is typed to only accept valid entity names from the partition.

const orders = await Customer.query({
  pk: "Customer#123",
  sk: { $beginsWith: "Order" }
});

Advanced usage

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: ["Assignment", "Teacher"],
      createdAt: { $beginsWith: "202" },
      $or: [
        {
          name: "Potions",
          updatedAt: { $beginsWith: "2023-02-15" }
        },
        {
          type: "Assignment",
          createdAt: { $beginsWith: "2021-09-15T" }
        },
        {
          id: "123"
        }
      ]
    }
  }
);

Filtering on Object Attributes

When using @ObjectAttribute, you can filter on nested Map fields using dot-path notation and check List membership using the $contains operator.

Dot-path filtering on nested fields

Use dot notation to filter on fields within an @ObjectAttribute. All standard filter operators work with dot-paths: equality, $beginsWith, and IN (array of values).

// Equality on a nested field
const result = await Store.query("123", {
  filter: { "address.city": "Springfield" }
});

// $beginsWith on a nested field
const result = await Store.query("123", {
  filter: { "address.street": { $beginsWith: "123" } }
});

// IN on a nested field
const result = await Store.query("123", {
  filter: { "address.city": ["Springfield", "Shelbyville"] }
});

// Deeply nested fields
const result = await Store.query("123", {
  filter: { "address.geo.lat": 40 }
});
$contains operator

Use $contains to check if a List attribute contains a specific element, or if a string attribute contains a substring. Works on both top-level attributes and nested fields via dot-path.

// Check if a List contains an element
const result = await Store.query("123", {
  filter: { "address.tags": { $contains: "home" } }
});

// Check if a top-level string contains a substring
const result = await Store.query("123", {
  filter: { name: { $contains: "john" } }
});
Combining dot-path and $contains with AND/OR

Dot-path filters and $contains work with all existing AND/OR filter combinations.

const result = await Store.query("123", {
  filter: {
    "address.city": "Springfield",
    "address.geo.lat": 40,
    $or: [
      { "address.tags": { $contains: "home" } },
      { name: { $beginsWith: "Main" } }
    ]
  }
});

Typed Query Filters

Query filters are strongly typed based on the entities in the queried partition. A partition includes the entity itself plus all entities reachable through its declared relationships (@HasMany, @HasOne, @BelongsTo, @HasAndBelongsToMany). For example, if Customer has @HasMany(() => Order) and @HasOne(() => ContactInformation), then Customer's partition entities are Customer, Order, and ContactInformation.

The type system validates:

  • Filter attribute keys: Only attributes that exist on the entity or its related entities are accepted. Relationship property names, partition keys, and sort keys are excluded.
  • type field values: The type field only accepts entity names from the partition — the entity itself and its declared relationships. Entities from other tables or unrelated entities on the same table are rejected.
  • Sort key values: Both skCondition and the sk property in key conditions only accept entity names from the partition. This matches dyna-record's single-table design where sort key values always start with an entity class name.
  • type narrowing in $or: Each $or element is independently narrowed. When an $or block specifies type: "Order", only Order's attributes are allowed in that block.
  • Dot-path keys: Nested @ObjectAttribute fields are available as typed filter keys using dot notation (e.g., "address.city").
Filter key validation
// Valid: 'name' exists on Customer, 'lastFour' on PaymentMethod
await Customer.query("123", {
  filter: { name: "John", lastFour: "1234" }
});

// Error: 'nonExistent' is not an attribute on any entity in Customer's partition
await Customer.query("123", {
  filter: { nonExistent: "value" } // Compile error
});

// Error: 'orders' is a relationship property, not a filterable attribute
await Customer.query("123", {
  filter: { orders: "value" } // Compile error
});
Type field narrowing
// Valid entity names only
await Customer.query("123", {
  filter: { type: "Order" } // OK: "Order" is in Customer's partition
});

await Customer.query("123", {
  filter: { type: "NonExistent" } // Compile error
});

// Array form (IN operator) accepts valid entity names
await Customer.query("123", {
  filter: { type: ["Order", "PaymentMethod"] }
});
$or element narrowing

Each $or element narrows independently based on its own type value:

await Customer.query("123", {
  filter: {
    $or: [
      { type: "Order", orderDate: "2023" }, // OK: orderDate is on Order
      { type: "PaymentMethod", lastFour: "1234" } // OK: lastFour is on PaymentMethod
    ]
  }
});

// Error in $or: lastFour is not an attribute on Order
await Customer.query("123", {
  filter: {
    $or: [
      { type: "Order", lastFour: "1234" } // Compile error
    ]
  }
});
Return type narrowing

When querying a partition with no filter or sort key condition, the return type is a union of the entity itself and all its related entities:

// Return type: Array<EntityAttributesInstance<Customer> | EntityAttributesInstance<Order>
//   | EntityAttributesInstance<PaymentMethod> | EntityAttributesInstance<ContactInformation>>
const results = await Customer.query("123");

When the filter specifies a type value, the return type automatically narrows to only the matching entities:

// Return type: Array<EntityAttributesInstance<Order>>
const orders = await Customer.query("123", {
  filter: { type: "Order" }
});

orders[0]?.orderDate; // OK: orderDate is accessible

// Return type: Array<EntityAttributesInstance<Order> | EntityAttributesInstance<PaymentMethod>>
const mixed = await Customer.query("123", {
  filter: { type: ["Order", "PaymentMethod"] }
});
Sort key validation and narrowing

Sort key values are typed to only accept valid entity names from the partition, matching dyna-record's single-table design where SK values always start with an entity class name. This applies to both the skCondition option and the sk property in key conditions:

// Both forms validate sort key values against partition entity names

// skCondition option (string form)
await Customer.query("123", { skCondition: "Order" }); // OK
await Customer.query("123", { skCondition: "Order#123" }); // OK
await Customer.query("123", { skCondition: { $beginsWith: "Order" } }); // OK
await Customer.query("123", { skCondition: "NonExistent" }); // Compile error

// sk property in key conditions (object form)
await Customer.query({ pk: "Customer#123", sk: "Order" }); // OK
await Customer.query({ pk: "Customer#123", sk: "Order#001" }); // OK
await Customer.query({ pk: "Customer#123", sk: { $beginsWith: "Order" } }); // OK
await Customer.query({ pk: "Customer#123", sk: "NonExistent" }); // Compile error

Return type narrowing works with skCondition when the value is an exact entity name or $beginsWith with an entity name:

// skCondition narrows the return type
const orders = await Customer.query("123", { skCondition: "Order" });
// orders is Array<EntityAttributesInstance<Order>>

const orders2 = await Customer.query("123", {
  skCondition: { $beginsWith: "Order" }
});
// orders2 is Array<EntityAttributesInstance<Order>>

// Suffix prevents narrowing (delimiter is configurable)
const specific = await Customer.query("123", { skCondition: "Order#123" });
// specific is QueryResults<Customer> (full union)

When using the object key form ({ pk: "...", sk: "..." }), sort key values are validated but the return type is not narrowed. Use filter: { type: "Order" } alongside key conditions for return type narrowing:

// sk is validated but does NOT narrow the return type
const results = await Customer.query({ pk: "Customer#123", sk: "Order" });
// results is QueryResults<Customer> (full union)

// Combine with filter type for return type narrowing
const orders = await Customer.query(
  { pk: "Customer#123", sk: { $beginsWith: "Order" } },
  { filter: { type: "Order" } }
);
// orders is Array<EntityAttributesInstance<Order>>

Note: Return type narrowing applies to the top-level type filter field, type values within $or elements, and to the skCondition option. When $or elements specify type values, the return type narrows to the union of those entity types. The sk property in key conditions validates values but does not narrow return types. Index queries ({ indexName: "..." }) use untyped filters.

Filter key narrowing: When no type is specified, the return type automatically narrows based on which entities have the filtered attributes. For example, filter: { orderDate: "2023" } narrows to Order if only Order has orderDate. In $or blocks, each element narrows independently — by type if present, or by filter keys otherwise — and the return type is the union across all blocks.

AND intersection: Since DynamoDB ANDs top-level filter conditions with $or blocks, the return type reflects this. When both top-level conditions and $or blocks independently narrow to specific entity sets, the return type is their intersection. If no entity satisfies both (e.g., { orderDate: "2023", $or: [{ lastFour: "1234" }] } where orderDate is on Order and lastFour is on PaymentMethod), the return type is never[] — correctly indicating that no records can match.

Querying on an index

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" }
);

Update

Docs

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.

Updating simple attributes

await Customer.update("123", {
  name: "New Name",
  address: "New Address"
});

Removing attributes

Note: Attempting to remove a non nullable attribute will result in a NullConstraintViolationError

await ContactInformation.update("123", {
  email: "[email protected]",
  phone: null
});

Updating Foreign Key References

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"
});

Removing Foreign Key References

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
});

Updating Object Attributes

Object attribute updates are partial — only the fields you provide are modified, and omitted fields are preserved. This uses DynamoDB document path expressions under the hood (e.g., SET #address.#street = :address_street) for efficient field-level updates.

// Only updates street — city, zip, geo, etc. are preserved
await Store.update("123", {
  address: { street: "456 New St" }
});

Nested objects are recursively merged:

// Only updates lat — lng and accuracy are preserved
await Store.update("123", {
  address: {
    geo: { lat: 42 }
  }
});

Nullable fields within the object can be removed by setting them to null:

// Removes zip, preserves all other fields
await Store.update("123", {
  address: { zip: null }
});

Arrays within objects are full replacement (not merged):

// Replaces the entire tags array
await Store.update("123", {
  address: { tags: ["new-tag-1", "new-tag-2"] }
});

The instance update method returns a deep-merged result, preserving existing fields:

const updated = await storeInstance.update({
  address: { street: "New Street" }
});
// updated.address.city → still has the original value

Instance Method

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
});

Skipping Referential Integrity Checks

By default, when updating entities with foreign key references, dyna-record performs condition checks to ensure that referenced entities exist. This prevents updating entities with invalid foreign key references. However, in high-contention or high-throughput systems where the same foreign key may be referenced in parallel operations, these condition checks can fail due to transaction conflicts. In scenarios such as bulk updates or when you've already verified the references, you may want to skip these checks to prevent such failures.

To skip referential integrity checks, pass an options object as the third parameter with referentialIntegrityCheck: false:

await PaymentMethod.update(
  "123",
  { customerId: "456" },
  { referentialIntegrityCheck: false }
);

For instance methods:

const updatedInstance = await paymentMethodInstance.update(
  { customerId: "456" },
  { referentialIntegrityCheck: false }
);

Note: When referentialIntegrityCheck is set to false, the condition checks that verify foreign key references exist are skipped. This means you can update entities even if the referenced entities don't exist, which may lead to data integrity issues. Use this option with caution.

Delete

Docs

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");

Handling HasMany and HasOne Relationships

When deleting entities involved in HasMany or HasOne relationships:

If a Pet belongs to an Owner (HasMany relationship), deleting the Pet will remove its denormalized records from the Owner's partition. If a Home belongs to a Person (HasOne relationship), deleting the Home will remove its denormalized records from the Person's partition.

await Home.delete("123");

This deletes the Home entity and its denormalized record with a Person.

Deleting Entities from HasAndBelongsToMany Relationships

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.

Error Handling

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.

Type Safety Features

Dyna-Record integrates type safety into your DynamoDB interactions, reducing runtime errors and enhancing code quality.

  • Entity Type Declaration: The @Entity decorator enforces that each entity declares readonly type as a string literal matching the class name (declare readonly type: "MyEntity"). This is required for compile-time query type safety.
  • Attribute Type Enforcement: Ensures that the data types of attributes match their definitions in your entities.
  • Method Parameter Checking: Validates method parameters against entity definitions, preventing invalid operations.
  • Relationship Integrity: Automatically manages the consistency of relationships between entities, ensuring data integrity.
  • Typed Query Filters: Query filter keys are validated against the attributes of entities in the partition. Invalid keys, relationship property names, and non-existent attributes produce compile errors. The type field only accepts valid entity class names.
  • Return Type Narrowing: When a query filter specifies a type value, the return type is automatically narrowed to only the matching entity types instead of the full partition union.
  • $or Element Narrowing: Each element in a $or filter array is independently type-checked based on its own type field, preventing attribute mismatches.

Best Practices

  • Define Clear Entity Relationships: Clearly define how your entities relate to each other for easier data retrieval and manipulation.
  • Use Type Aliases for Foreign Keys: Utilize TypeScript's type aliases for foreign keys to enhance code readability and maintainability.
  • Leverage Type Safety: Take advantage of Dyna-Record's type safety features to catch errors early in development.
  • Define Access Patterns: Dynamo is not as flexible as a relational database. Try to define all access patterns up front.

Debug logging

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.