Skip to content
Back to skills

Electrodb

ASecurity

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.

  • 3 stars
  • 0 votes
  • 0 copies
  • 2 views
  • Added September 6, 2026
ai-agentstypescriptgobashexpressaws

Works with

  • cursor
  • cli

Security analysis

A96/100
  • mediumInstalls packages at runtime which could introduce malicious dependencies

Pro scans all 3 files and shows the line behind each finding

Scanned September 6, 2026

npx -y skills add whimzyLive/nightshift-ai --skill electrodb --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Electrodb?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Electrodb
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/whimzylive-electrodb/badge)](https://www.skillsdirectory.com/skills/whimzylive-electrodb)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
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.md8.5 KB
  • evals/evals.json3 KB
  • scripts/new-entity.sh3.8 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.