CtrlK
BlogDocsLog inGet started
Tessl Logo

electrodb

Use when defining DynamoDB entities, modeling indexes, or writing queries/mutations with the ElectroDB ORM. Covers entity schema, access patterns, CRUD operations, update expressions, and single-table design patterns.

SKILL.md
Quality
Evals
Security

ElectroDB

TypeScript-first DynamoDB ORM. Manages composite key composition, enforces attribute types, and generates safe DocumentClient params. No raw DynamoDBClient calls.

Installation

npm install electrodb @aws-sdk/client-dynamodb @aws-sdk/lib-dynamodb

Entity Definition

import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { DynamoDBDocumentClient } from "@aws-sdk/lib-dynamodb";
import { Entity } from "electrodb";

const client = DynamoDBDocumentClient.from(new DynamoDBClient({}));

const Task = new Entity(
  {
    model: {
      entity: "task",      // item type namespace
      service: "taskapp",  // application namespace
      version: "1",
    },
    attributes: {
      taskId:      { type: "string", required: true, default: () => crypto.randomUUID() },
      projectId:   { type: "string", required: true },
      employeeId:  { type: "string", required: true },
      status:      { type: ["pending", "active", "done"] as const, required: true },
      description: { type: "string" },
      ttl:         { type: "number" },  // epoch seconds for DynamoDB TTL
      createdAt:   { type: "string", readOnly: true, default: () => new Date().toISOString() },
      updatedAt:   { type: "string", watch: "*", set: () => new Date().toISOString() },
    },
    indexes: {
      // Table index (no `index` property = primary table index)
      primary: {
        pk: { field: "pk", composite: ["taskId"] },
        sk: { field: "sk", composite: ["projectId"] },
      },
      // GSI — reference by logical name, not DynamoDB index name
      byProject: {
        index: "gsi1pk-gsi1sk-index",
        pk: { field: "gsi1pk", composite: ["projectId"] },
        sk: { field: "gsi1sk", composite: ["employeeId", "taskId"] },
      },
    },
  },
  { table: process.env.TABLE_NAME!, client },
);

Schema Quick Reference

Attribute options

OptionDescription
type"string" "number" "boolean" "list" "map" "set" or enum array ["a","b"]
requiredThrow on put/create if missing
defaultStatic value or () => value factory
readOnlyCannot be changed after creation
watch: "*"Re-run set on every update (useful for updatedAt)
validateRegex or (val) => boolean

model fields

FieldPurpose
model.entityNamespaces items on the table; prevents cross-entity key collisions
model.serviceGroups entities into a Service for cross-entity queries
model.versionBumping changes key format — migrate existing items first

Index Design

indexes: {
  // Primary (table index) — no `index` property
  primary: {
    pk: { field: "pk", composite: ["tenantId"] },
    sk: { field: "sk", composite: ["entityId", "createdAt"] },
  },
  // GSI
  byStatus: {
    index: "gsi1pk-gsi1sk-index",
    pk: { field: "gsi1pk", composite: ["tenantId", "status"] },
    sk: { field: "gsi1sk", composite: ["createdAt"] },
  },
  // LSI (same pk field as table index)
  byDate: {
    index: "lsi1-index",
    pk: { field: "pk", composite: ["tenantId"] },
    sk: { field: "lsi1sk", composite: ["createdAt"] },
  },
}

Key composition rule: composite array order is hierarchical — you can query on a prefix but not skip attributes. ["a","b","c"] supports queries on a or a+b or a+b+c, never b alone.

CRUD Operations

All operations are lazy — chain methods build the expression, then call .go() to execute or .params() to inspect the raw DocumentClient params.

put (full overwrite)

const { data } = await Task.put({
  taskId: "t-001",
  projectId: "proj-x",
  employeeId: "emp-42",
  status: "pending",
}).go();

create (fails if item exists)

await Task.create({ taskId, projectId, employeeId, status: "pending" }).go();

get

const { data } = await Task.get({ taskId: "t-001", projectId: "proj-x" }).go();
// data is null if not found

delete

await Task.delete({ taskId: "t-001", projectId: "proj-x" }).go();

update

// Chain operations
await Task.update({ taskId, projectId })
  .set({ status: "active", description: "started" })
  .go();

// data() for type-safe complex updates
await Task.update({ taskId, projectId })
  .data((a, o) => {
    o.set(a.status, "done");
    o.remove(a.description);
    o.add(a.retryCount, 1);           // increment number
    o.append(a.log, ["step complete"]); // append to list
  })
  .go();

patch (update + fails if item missing)

await Task.patch({ taskId, projectId }).set({ status: "done" }).go();

Queries

// By primary index — pk composite only
const { data } = await Task.query.primary({ taskId: "t-001" }).go();

// By GSI — filter sk with begins/between/gte/lte
const { data } = await Task.query.byProject({ projectId: "proj-x" })
  .begins({ employeeId: "emp-" })
  .go();

// Sort descending, limit, paginate
const { data, cursor } = await Task.query.byProject({ projectId: "proj-x" })
  .go({ order: "desc", limit: 20 });

// Next page
const { data: page2 } = await Task.query.byProject({ projectId: "proj-x" })
  .go({ cursor });

Sort key operations

MethodDynamoDB equivalent
.begins({ sk: "val" })begins_with
.between({ sk: "a" }, { sk: "z" })BETWEEN
.gte({ sk: "val" })>= val
.gt({ sk: "val" })> val
.lte({ sk: "val" })<= val
.lt({ sk: "val" })< val

Filter (post-query)

const { data } = await Task.query.byProject({ projectId })
  .where((attr, op) => op.eq(attr.status, "pending"))
  .go();

// Multiple conditions
  .where((attr, op) => `${op.eq(attr.status, "pending")} AND ${op.gt(attr.ttl, 0)}`)

Conditional Writes

// Conditional update — fail if condition not met
await Task.update({ taskId, projectId })
  .set({ status: "done" })
  .where((attr, op) => op.eq(attr.status, "active"))
  .go();

Execution Options

// Inspect params without executing
const params = Task.get({ taskId, projectId }).params();

// Override table at runtime
await Task.get({ taskId, projectId }).go({ table: "other-table" });

// Include pk/sk in response (stripped by default)
await Task.get({ taskId, projectId }).go({ data: "includeKeys" });

// Consistent read
await Task.get({ taskId, projectId }).go({ consistent: true });

// Project specific attributes
await Task.get({ taskId, projectId }).go({ attributes: ["status", "createdAt"] });

Single-Table Design Patterns

One table, generic key names — ElectroDB namespaces items by model.entity automatically. Items from different entities on the same table never collide.

TTL — declare as { type: "number" } attribute, enable TTL on the field in DynamoDB console/CDK. Set epoch-seconds value at write time.

const ttl = Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 7; // 7 days
await Task.put({ ...attrs, ttl }).go();

Service — group multiple Entity objects for cross-entity queries:

import { Service } from "electrodb";
const AppService = new Service({ Task, Employee });
// AppService.collections.<collectionName>(...).go()

Common Mistakes

MistakeFix
Querying a partial composite pkAll pk composites required; sk composites are optional prefix
Using update on non-existent itemUse patch to get ConditionalCheckFailed or upsert
Forgetting .go()Expressions are lazy — nothing executes without .go()
Raw DynamoDBClient for DynamoDB opsAlways use entity methods; raw client bypasses type safety
Bumping model.version without migrationChanges key format — old items become invisible
set on a readOnly attributeThrows at runtime; set defaults only via default

Additional resources

  • scripts/new-entity.sh <EntityName> — scaffolds src/entities/<entity-name>.entity.ts with a ready-to-edit new Entity({...}) skeleton (model namespace, createdAt/updatedAt/ttl attributes, a primary pk/sk composite index, and one GSI). Derives a kebab-case filename and a PascalCase export from the argument, and never overwrites an existing file. Use it to start a new entity instead of hand-copying the boilerplate above.
Repository
whimzyLive/nightshift-ai
Last updated
First committed

Is this your skill?

If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.