ElectroDB

repository·master·Indexed 22 days ago

https://github.com/tywalch/electrodb

A high-level DynamoDB modeling library designed to simplify Single-Table Design. It provides entity isolation, attribute schema enforcement, and intuitive APIs for querying and mutating data, abstracting the complexities of raw DynamoDB UpdateExpressions and ConditionExpressions. Key features include hierarchical access patterns, cross-entity queries via collections, TypeScript support, and a CLI for prototyping.

Tokens
86.6K
Snippets
201
Records
291
Agent score
75%

What's inside electrodb

  1. Introduction to ElectroDB

    master
    ElectroDB is a type-safe modeling library for Amazon DynamoDB. It abstracts the boilerplate associated with raw DocumentClient calls, such as manually constructing parameter objects and managing composite keys. Instead of writing low-level DynamoDB parameters, you define a single, type-safe model and express your intent (e.g., updating a specific field or applying a condition), and ElectroDB handles the resolution of composite keys and the construction of update expressions.
  2. What is ElectroDB?

    master
    ElectroDB is a DynamoDB library designed to simplify managing multiple entities and complex hierarchical relationships within a single DynamoDB table (Single-Table Design). It provides high-level abstractions for modeling, querying, and mutating data while handling the complexities of DynamoDB's low-level API.
  3. Key Features of ElectroDB

    master

    ElectroDB provides several core capabilities to simplify DynamoDB development:

    • Single-Table Entity Isolation: Prevents entity conflicts when using a single table.
    • Attribute Schema Enforcement: Defines schemas with validation, defaults, types, and aliases.
    • Hierarchical Access Patterns: Easily design and use hierarchical keys for indexes.
    • Simplified Querying: Provides easy-to-use interfaces for sort key conditions, filter composition, and automatic index selection via .find() or .match().
    • Simplified Mutations: Streamlines update expressions and condition composition without manual ExpressionAttributeNames or ExpressionAttributeValues management.
    • Cross-Entity Queries: Use 'collections' to query multiple entities in a single request.
    • Pagination: Generates URL-safe cursors and supports async iteration.
    • TypeScript Support: Strong type inference for Entities and Services.
    • CLI & Prototyping: Query entities via terminal or stand up a REST server for prototyping using electrocli.
  4. What is a Collection in ElectroDB?

    master

    A Collection is a grouping of multiple Entities that share the same Partition Key. It allows you to perform a single, efficient DynamoDB query to retrieve related data across different entity types, similar to how a SQL VIEW works with joined tables.

    Key Characteristics:

    • Single Query: Collections use one DynamoDB query to retrieve results for all associated Entities, supporting Single Table Design.
    • Index-Based: Collections are defined on an Index. To create a collection, multiple entities must point to the same index and use the same collection name.
    • Uniqueness: A collection name must be unique to a single common index across all entities within a Service.
    • Ordering Note: DynamoDB returns records in order of the Entity's sort key. In very large partitions, pagination might cause some entities to be missed; this can be mitigated using specific Index Types.
    // Example of defining a collection via an index in two different entities
    
    // Entity 1
    const Employee = new Entity({
      model: { entity: "employee", version: "1", service: "taskapp" },
      // ... attributes
      indexes: {
        employee: {
          collection: "assignments", // The collection name
          index: "gsi2",
          pk: { field: "gsi2pk", composite: ["employeeId"] },
          sk: { field: "gsi2sk", composite: [] },
        },
      },
    });
    
    // Entity 2
    const Task = new Entity({
      model: { entity: "tasks", version: "1", service: "taskapp" },
      // ... attributes
      indexes: {
        assigned: {
          collection: "assignments", // Must match the collection name above
          index: "gsi2",             // Must match the index name above
          pk: { field: "gsi2pk", composite: ["employeeId"] },
          sk: { field: "gsi2sk", composite: ["projectId"] },
        },
      },
    });
  5. What is a Service in ElectroDB

    master

    A Service in ElectroDB represents a collection of related Entity objects. While you can use Entities independently, you must use a Service if you intend to perform queries that "join" multiple Entities. Services allow multiple Entities to coexist on a single DynamoDB table without collision.

    When you define a Service, the property names you assign to the entities become the "aliases" used to reference them through the Service instance.

    import { Service } from "electrodb";
    
    // The property names 'employee' and 'task' become the aliases
    const TaskApp = new Service(
      {
        employee: Employee, // accessible via TaskApp.entities.employee
        task: Task,        // accessible via TaskApp.entities.task
      },
      { table, client },
    );
  6. Update complex types (maps, lists, sets)

    master

    You can update nested properties in complex DynamoDB types using dot notation or square brackets.

    Using Chain Methods

    To target a specific property in a map or an index in a list, use the JSON path as the attribute name:

    • Map property: Use dot notation (e.g., set({ 'address.city': 'New York' })).
    • List element: Use square brackets with the index (e.g., remove(['items[0]'])).

    Using the data() method

    The data() method provides TypeScript type safety for complex types. Instead of using string paths, you can drill into the attributes object directly:

    // Example: Updating a nested property in a map via data()
    entity.update({ id: '123' }).data(({ attributes, operations }) => {
      operations.set(attributes.metadata.location, 'London');
    }).go();
  7. Configure concurrency for Batch Delete

    master

    DynamoDB has a limit of 25 records per BatchWrite request. If you attempt to delete more than 25 records, ElectroDB automatically splits the operation into multiple requests and returns the results in a single array.

    By default, ElectroDB executes these requests in series (concurrent: 1). You can increase throughput by using the concurrent execution option to run multiple requests simultaneously.

    Warning: Increasing concurrency affects your table's throughput. Ensure your table can handle the increased load before setting a high value.

    Example Scenarios (75 records total):

    • concurrent: 1 (Default): Executes 3 requests of 25 records sequentially.
    • concurrent: 2: Executes the first 25-record request and the second 25-record request simultaneously, then executes the final 25-record request once the first two complete.
    // Example of setting concurrency
    await entity.delete(items, { concurrent: 2 });
  8. Filter on complex attributes (Maps and Lists) in `where()`

    master

    ElectroDB supports using the where() method with DynamoDB's complex attribute types like map and list. When using the injected attributes object, you can drill into the attribute to apply filters directly to nested elements.

    • Maps: Access nested keys directly via the attribute object.
    • Lists: Access specific elements or indices within the list.
    // Example: Filtering on a map attribute
    await MyEntity.get({ id: '1' })
      .where(({ metadata }) => eq(metadata.color, 'red'))
      .go();
    
    // Example: Filtering on a list element
    await MyEntity.get({ id: '1' })
      .where(({ tags }) => contains(tags, 'important'))
      .go();
  9. Execute query chains with .go() or .params()

    master

    All ElectroDB query chains must be terminated with either .go() or .params() to perform an action. Both methods accept an optional configuration object for execution options.

    .params()

    Terminates the chain synchronously and returns a formatted object ready to be passed directly to the DynamoDB docClient.

    .go()

    Terminates the chain asynchronously and executes the query against DynamoDB using the client provided in the model or via the execution options. It returns a Promise that resolves to an object containing the data and an optional cursor.

    Return shape for .go():

    {
      data: Array<T>,
      cursor: string | null
    }
  10. Structure of a Live Documentation Example

    master

    Live documentation examples in the ElectroDB docs are organized into self-contained directories. Each example follows a specific file structure to ensure type safety and modularity:

    • example.ts: The primary code shown in the documentation. It imports the Entity from ./entity.
    • entity.ts: Contains the Entity instantiation, importing the table name from ./table.
    • table.ts: Defines the DynamoDB table and exports the table name.

    This structure allows examples to modify their own schema (entity.ts) or table definition (table.ts) without affecting other documentation pages.

    src/examples/<example-name>/
      example.ts   # the code shown by default; imports the entity from ./entity
      entity.ts    # the Entity instantiation; imports the table name from ./table
      table.ts     # the DynamoDB table definition + exported table name
  11. Handle condition check failures without throwing

    master

    By default, if a condition expression fails during a mutation, ElectroDB throws an error. You can change this behavior using the returnOnConditionCheckFailure option:

    • Set to true: The method will return { rejected: true } instead of throwing.
    • Set to "all_old": The method will return { rejected: true } and also include the existing item under the data property.
    // Example: Returning a rejected status instead of throwing
    const result = await myEntity.put({ id: '123' }, { version: 1 }).go({
      returnOnConditionCheckFailure: true
    });
    
    if (result.rejected) {
      // Handle the fact that the condition failed
    }
  12. Use the patch method for safe DynamoDB updates

    master

    In DynamoDB, standard update operations create a new item by default if the target record does not exist. This can lead to partial items and incorrect typing.

    ElectroDB's patch method prevents this by dynamically utilizing the attribute_exists() parameter. This ensures that an update only occurs if the record already exists, making it a "safer" update operation.

    Key Difference: Unlike the update method, patch returns an EntityItem type rather than just identifiers, because it guarantees you are interacting with an existing, fully-typed record.

    // Note: The specific method call is not in this segment, but the concept is:
    // patch() ensures attribute_exists() is applied to prevent accidental creation.