Back to skills
SKILL.md
Electrodb
ASecurityUse 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.
- 3 stars
- 0 votes
- 0 copies
- 2 views
- Added September 6, 2026
Works with
Security analysis
96/100- Installs packages at runtime which could introduce malicious dependencies
Pro scans all 3 files and shows the line behind each finding
npx -y skills add whimzyLive/nightshift-ai --skill electrodb --agent claude-codeAre you the author of Electrodb?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/whimzylive-electrodb)---
name: electrodb
description: 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.
---
# ElectroDB
TypeScript-first DynamoDB ORM. Manages composite key composition, enforces attribute types, and generates safe DocumentClient params. No raw `DynamoDBClient` calls.
## Installation
```bash
npm install electrodb @aws-sdk/client-dynamodb @aws-sdk/lib-dynamodb
```
## Entity Definition
```typescript
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
| 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` |
### model fields
| 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 |
## Index Design
```typescript
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)
```typescript
const { data } = await Task.put({
taskId: "t-001",
projectId: "proj-x",
employeeId: "emp-42",
status: "pending",
}).go();
```
### create (fails if item exists)
```typescript
await Task.create({ taskId, projectId, employeeId, status: "pending" }).go();
```
### get
```typescript
const { data } = await Task.get({ taskId: "t-001", projectId: "proj-x" }).go();
// data is null if not found
```
### delete
```typescript
await Task.delete({ taskId: "t-001", projectId: "proj-x" }).go();
```
### update
```typescript
// 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)
```typescript
await Task.patch({ taskId, projectId }).set({ status: "done" }).go();
```
## Queries
```typescript
// 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
| 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` |
### Filter (post-query)
```typescript
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
```typescript
// 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
```typescript
// 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.
```typescript
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:
```typescript
import { Service } from "electrodb";
const AppService = new Service({ Task, Employee });
// AppService.collections.<collectionName>(...).go()
```
## Common Mistakes
| 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` |
## 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.
Files in this skill
- SKILL.md
- evals/evals.json
- scripts/new-entity.sh
Attribution
Comments
Loading comments…
Newsletter
Get the weekly list of safe new skills, and the ones we pulled.