@jaypie/dynamodb
Prerequisites: npm install @jaypie/dynamodb
Status: Experimental - APIs may change
Overview
Section titled “Overview”@jaypie/dynamodb provides single-table DynamoDB utilities with model-keyed GSIs, hierarchical scoping, and soft delete.
Installation
Section titled “Installation”npm install @jaypie/dynamodbQuick Reference
Section titled “Quick Reference”Entity Operations
Section titled “Entity Operations”| Function | Purpose |
|---|---|
createEntity |
Create new entity (returns null if id exists) |
getEntity |
Read entity by ID |
updateEntity |
Update entity fields |
deleteEntity |
Soft delete (mark deleted) |
archiveEntity |
Archive (exclude from queries) |
destroyEntity |
Hard delete (permanent) |
Query Functions
Section titled “Query Functions”| Function | Purpose |
|---|---|
queryByScope |
Query by model, optionally narrowed by scope |
queryByAlias |
Query by alias |
queryByCategory |
Query by category |
queryByType |
Query by type |
queryByXid |
Query by external ID |
Seed and Export Functions
Section titled “Seed and Export Functions”| Function | Purpose |
|---|---|
seedEntityIfNotExists |
Seed single entity if not exists |
seedEntities |
Bulk seed with idempotency |
exportEntities |
Export entities by model/scope |
exportEntitiesToJson |
Export as JSON string |
Scan and Table Administration
Section titled “Scan and Table Administration”| Function | Purpose |
|---|---|
scanTable |
Async generator over every raw item (schema-agnostic Scan) |
countTable |
Total item count via paginated COUNT scan |
createTable |
Create a table with the registered-model GSI schema |
destroyTable |
Delete a table (tableName required) |
StorableEntity Interface
Section titled “StorableEntity Interface”All entities implement this interface:
interface StorableEntity { id: string; // Primary key (UUID) model: string; // Entity model name (e.g., "user", "order") scope: string; // APEX ("@") or "{parent.model}#{parent.id}"
name?: string; // Human-readable name alias?: string; // Human-friendly slug category?: string; // Category for filtering type?: string; // Type for filtering xid?: string; // External ID
createdAt?: string; // Backfilled by indexEntity updatedAt?: string; // Managed by indexEntity on every write archivedAt?: string; // Set by archiveEntity deletedAt?: string; // Set by deleteEntity
state?: Record<string, unknown>; [key: string]: unknown;}Table Structure
Section titled “Table Structure”Primary Key
Section titled “Primary Key”| Attribute | Type | Description |
|---|---|---|
id |
PK | UUID — sole primary key (no sort key) |
GSI Pattern
Section titled “GSI Pattern”GSIs are defined using fabricIndex() from @jaypie/fabric. All GSIs use a composite sort key of scope#updatedAt.
| GSI Name | PK Pattern | SK Attribute | Purpose |
|---|---|---|---|
indexModel |
{model} |
indexModelSk = {scope}#{updatedAt} |
List by model |
indexModelAlias |
{model}#{alias} (sparse) |
indexModelAliasSk |
Slug lookup |
indexModelCategory |
{model}#{category} (sparse) |
indexModelCategorySk |
Category filter |
indexModelType |
{model}#{type} (sparse) |
indexModelTypeSk |
Type filter |
indexModelXid |
{model}#{xid} (sparse) |
indexModelXidSk |
External ID lookup |
Entity Operations
Section titled “Entity Operations”Create Entity
Section titled “Create Entity”import { APEX, createEntity } from "@jaypie/dynamodb";
// indexEntity auto-populates GSI keys, createdAt, updatedAtconst user = await createEntity({ entity: { model: "user", id: crypto.randomUUID(), scope: APEX, name: "Alice", email: "alice@example.com", },});Read Entity
Section titled “Read Entity”import { getEntity } from "@jaypie/dynamodb";
// Primary key is id onlyconst user = await getEntity({ id: "user-123" });Update Entity
Section titled “Update Entity”import { updateEntity } from "@jaypie/dynamodb";
const updated = await updateEntity({ entity: { ...user, name: "Alice Smith" },});Soft Delete
Section titled “Soft Delete”import { deleteEntity } from "@jaypie/dynamodb";
await deleteEntity({ id: "user-123" });// Sets deletedAt, re-indexes with #deleted suffix on GSI pkHard Delete
Section titled “Hard Delete”import { destroyEntity } from "@jaypie/dynamodb";
await destroyEntity({ id: "user-123" });// Permanently removes entityQuery Patterns
Section titled “Query Patterns”Query by Model and Scope
Section titled “Query by Model and Scope”import { APEX, queryByScope } from "@jaypie/dynamodb";
// Get all entities of a model at root scopeconst { items } = await queryByScope({ model: "user", scope: APEX });
// Scope is optional — omit to query across all scopesconst { items: allUsers } = await queryByScope({ model: "user" });
// Get entities under a parentconst { items: childEntities } = await queryByScope({ model: "order", scope: "user#user-123",});Hierarchical Structure
Section titled “Hierarchical Structure”@ # Root scope (APEX)├── user#alice-123 # User's items│ ├── order#order-1│ └── order#order-2└── user#bob-456 # Another user's items └── order#order-3Query by Type
Section titled “Query by Type”import { queryByType } from "@jaypie/dynamodb";
// Requires fabricIndex("type") registered for the modelconst { items } = await queryByType({ model: "user", type: "premium",});Pagination
Section titled “Pagination”const { items, lastEvaluatedKey } = await queryByScope({ model: "user", scope: APEX, limit: 10,});
// Next pageconst nextPage = await queryByScope({ model: "user", scope: APEX, limit: 10, startKey: lastEvaluatedKey,});Index Entity
Section titled “Index Entity”Auto-populate GSI attributes and timestamps:
import { indexEntity } from "@jaypie/dynamodb";
const entity = { model: "user", id: "user-123", scope: "@", email: "alice@example.com", type: "premium",};
const indexed = indexEntity(entity);// Sets updatedAt, backfills createdAt, populates indexModel, indexModelSk, etc.CDK Integration
Section titled “CDK Integration”import { JaypieDynamoDb } from "@jaypie/constructs";import { fabricIndex } from "@jaypie/fabric";
// Basic table (no GSIs by default)new JaypieDynamoDb(this, "myApp");
// With indexesnew JaypieDynamoDb(this, "myApp", { indexes: [ fabricIndex(), // indexModel fabricIndex("alias"), // indexModelAlias (sparse) ],});Seed and Export
Section titled “Seed and Export”Utilities for bootstrapping and migrating data.
Seed Entities
Section titled “Seed Entities”import { APEX, seedEntities, seedEntityIfNotExists } from "@jaypie/dynamodb";
// Seed single entity if not existsconst created = await seedEntityIfNotExists({ alias: "config-main", model: "config", scope: APEX,});// Returns true if created, false if exists
// Bulk seed with idempotencyconst result = await seedEntities([ { alias: "en", model: "lang", scope: APEX }, { alias: "es", model: "lang", scope: APEX },]);// result.created: ["en", "es"]// result.skipped: [] (existing entities)// result.errors: []
// Optionsawait seedEntities(entities, { dryRun: true }); // Previewawait seedEntities(entities, { replace: true }); // OverwriteExport Entities
Section titled “Export Entities”import { APEX, exportEntities, exportEntitiesToJson } from "@jaypie/dynamodb";
// Export entitiesconst { entities, count } = await exportEntities("lang", APEX);
// With limitconst { entities: limited } = await exportEntities("lang", APEX, 10);
// Export as JSONconst json = await exportEntitiesToJson("lang", APEX); // Prettyconst compact = await exportEntitiesToJson("lang", APEX, false); // CompactRebuilding a Table
Section titled “Rebuilding a Table”The 0.5.0 primary-key change cannot be applied in place — DynamoDB cannot alter a primary key. Moving a populated 0.4.x table forward is a one-off, validated cutover to a new physical table. Run it once per environment (not on every deploy; that is JaypieMigration).
With a CDK-managed table name and a maintenance window:
- Deploy the new table alongside the old (CDK).
- Pause writes so the copy cannot go stale.
- Scan the old table, transform each record, write to the new table.
- Validate:
countTablematches on both; spot-check records. - Flip
DYNAMODB_TABLE_NAMEto the new table via CDK deploy; resume. destroyTablethe old table later, once confident — it is your rollback until then.
import { countTable, createTable, initClient, scanTable, updateEntity,} from "@jaypie/dynamodb";
initClient({ tableName: NEW }); // singleton → destinationawait createTable({ tableName: NEW }); // new schema, waits until ACTIVE
for await (const item of scanTable({ tableName: OLD })) { const { sequence, pk, sk, ...rest } = item; // 0.4 → 0.6 transform await updateEntity({ entity: rest }); // re-indexes + re-timestamps}
if ((await countTable({ tableName: OLD })) !== (await countTable())) { throw new Error("Count mismatch — do NOT destroy the old table");}// Validated. Flip CDK_ENV to NEW, deploy, then later: destroyTable({ tableName: OLD })scanTable issues a raw Scan, so it reads the old table despite its mismatched GSIs. The transform is application-specific; updateEntity recomputes every GSI key and timestamp on write.
MCP Tools
Section titled “MCP Tools”Local development tools via @jaypie/mcp:
dynamodb_query- Query entitiesdynamodb_get- Get single entitydynamodb_create- Create entitydynamodb_update- Update entitydynamodb_delete- Delete entity
Related
Section titled “Related”- CDK Infrastructure - DynamoDB construct
- @jaypie/constructs - JaypieDynamoDb