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.
69
87%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
Passed
No findings from the security scan
TypeScript-first DynamoDB ORM. Manages composite key composition, enforces attribute types, and generates safe DocumentClient params. No raw DynamoDBClient calls.
npm install electrodb @aws-sdk/client-dynamodb @aws-sdk/lib-dynamodbimport { 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 },
);| Option | Description |
|---|---|
type | "string" "number" "boolean" "list" "map" "set" or enum array ["a","b"] |
required | Throw on put/create if missing |
default | Static value or () => value factory |
readOnly | Cannot be changed after creation |
watch: "*" | Re-run set on every update (useful for updatedAt) |
validate | Regex or (val) => boolean |
| Field | Purpose |
|---|---|
model.entity | Namespaces items on the table; prevents cross-entity key collisions |
model.service | Groups entities into a Service for cross-entity queries |
model.version | Bumping changes key format — migrate existing items first |
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.
All operations are lazy — chain methods build the expression, then call .go() to execute or .params() to inspect the raw DocumentClient params.
const { data } = await Task.put({
taskId: "t-001",
projectId: "proj-x",
employeeId: "emp-42",
status: "pending",
}).go();await Task.create({ taskId, projectId, employeeId, status: "pending" }).go();const { data } = await Task.get({ taskId: "t-001", projectId: "proj-x" }).go();
// data is null if not foundawait Task.delete({ taskId: "t-001", projectId: "proj-x" }).go();// 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();await Task.patch({ taskId, projectId }).set({ status: "done" }).go();// 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 });| Method | DynamoDB 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 |
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 update — fail if condition not met
await Task.update({ taskId, projectId })
.set({ status: "done" })
.where((attr, op) => op.eq(attr.status, "active"))
.go();// 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"] });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()| Mistake | Fix |
|---|---|
| Querying a partial composite pk | All pk composites required; sk composites are optional prefix |
Using update on non-existent item | Use patch to get ConditionalCheckFailed or upsert |
Forgetting .go() | Expressions are lazy — nothing executes without .go() |
Raw DynamoDBClient for DynamoDB ops | Always use entity methods; raw client bypasses type safety |
Bumping model.version without migration | Changes key format — old items become invisible |
set on a readOnly attribute | Throws at runtime; set defaults only via default |
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.whimzyLive/nightshift-ai
44c1caf
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.