API Reference
Module-by-module reference for all public exports of effect-dynamodb.
DynamoModel
Section titled “DynamoModel”Provides annotations, date schemas, storage modifiers, and model configuration for effect-dynamodb.
import { DynamoModel } from "effect-dynamodb"Annotations
Section titled “Annotations”| Export | Type | Description |
|---|---|---|
Hidden | <S>(schema: S) => S | Marks a field as hidden from asModel/asRecord decode. Still stored in DynamoDB, visible in asItem/asNative |
isHidden | (schema: Schema.Top) => boolean | Check if a schema field has the Hidden annotation |
identifier | <S>(schema: S) => S | Marks the primary business identifier on a model. Exactly one per entity. Required for entities referenced via ref |
isIdentifier | (schema: Schema.Top) => boolean | Check if a schema field has the identifier annotation |
getIdentifierField | (model: Schema.Top) => { name, schema } | undefined | Find the identifier field in a model’s .fields |
ref | <S>(schema: S) => S | Marks a field as a denormalized reference to another entity. Transforms types in Entity (Input: ID, Record: full object, Update: optional ID) |
isRef | (schema: Schema.Top) => boolean | Check if a schema field has the ref annotation |
getRefAnnotation | (schema: Schema.Top) => RefAnnotation | undefined | Get the full ref annotation metadata |
Date Schemas
Section titled “Date Schemas”All date schemas carry a DynamoEncoding annotation that controls how the field is stored in DynamoDB.
| Export | Wire Type | Domain Type | DynamoDB Storage |
|---|---|---|---|
DateString | ISO 8601 string | DateTime.Utc | String (S) |
DateEpochMs | epoch milliseconds | DateTime.Utc | Number (N) |
DateEpochSeconds | epoch seconds | DateTime.Utc | Number (N) |
DateEpoch(opts) | auto-detect ms/seconds | DateTime.Utc | configurable |
DateTimeZoned | ISO+offset+zone string | DateTime.Zoned | String (S) |
UnsafeDateString | ISO 8601 string | native Date | String (S) |
UnsafeDateEpochMs | epoch milliseconds | native Date | Number (N) |
UnsafeDateEpochSeconds | epoch seconds | native Date | Number (N) |
TTL | epoch seconds | DateTime.Utc | Number (N) — alias for DateEpochSeconds |
Storage Modifier
Section titled “Storage Modifier”| Export | Type | Description |
|---|---|---|
storedAs | <A>(storageSchema: Schema<A>) => (fieldSchema: Schema<A>) => Schema<A> | Override DynamoDB storage format. Type-safe: domain types must match |
Model Configuration
Section titled “Model Configuration”| Export | Type | Description |
|---|---|---|
configure | (model, attributes) => ConfiguredModel<M> | Wrap a model with per-field DynamoDB overrides (field renaming, storage encoding, immutable) |
isConfiguredModel | (value) => boolean | Check if a value is a ConfiguredModel |
Encoding Utilities
Section titled “Encoding Utilities”| Export | Type | Description |
|---|---|---|
DynamoEncodingKey | symbol | Annotation key for reading DynamoEncoding from schema AST |
getEncoding | (schema) => DynamoEncoding | undefined | Read the DynamoEncoding annotation from a schema |
| Type | Description |
|---|---|
DynamoEncoding | { storage: "string" | "epochMs" | "epochSeconds", domain: "DateTime.Utc" | "DateTime.Zoned" | "Date" } |
ConfiguredModel<M> | Wrapper carrying original model + per-field attribute overrides |
RefAnnotation | { _tag: "Ref", refSchemaId?: string } — annotation metadata for ref fields |
// Annotationsclass Employee extends Schema.Class<Employee>("Employee")({ employeeId: Schema.String, createdBy: Schema.String, internalId: Schema.String.pipe(DynamoModel.Hidden),}) {}
// configure — immutable fields + field overridesconst EmployeeModel = DynamoModel.configure(Employee, { createdBy: { immutable: true },})
// Date schemasclass Event extends Schema.Class<Event>("Event")({ eventId: Schema.String, startedAt: DynamoModel.DateString, // ISO string ↔ DateTime.Utc expiresAt: DynamoModel.DateEpochSeconds, // epoch seconds (for TTL) scheduledAt: DynamoModel.DateTimeZoned, // with timezone}) {}
// storedAs — override storage format on a SELF schema (not a transform).// `Schema.DateTimeUtc` is the self schema; the annotation drives storage.class Order extends Schema.Class<Order>("Order")({ orderId: Schema.String, // Domain: DateTime.Utc, stored as epoch seconds (e.g. for DynamoDB TTL). // Applying `storedAs` to a transform schema (e.g. DateString or // DateTimeUtcFromString) is rejected at Entity.make() time. expiresAt: Schema.DateTimeUtc.pipe(DynamoModel.storedAs(DynamoModel.DateEpochSeconds)),}) {}
// configure — field renaming + storage overrides (self-schema only for storedAs)const OrderModel = DynamoModel.configure(Order, { expiresAt: { field: "ttl", storedAs: DynamoModel.DateEpochSeconds },})
// identifier — marks primary business ID for ref resolutionclass Team extends Schema.Class<Team>("Team")({ id: Schema.String.pipe(DynamoModel.identifier), name: Schema.String,}) {}
// ref — denormalized reference to another entityclass Selection extends Schema.Class<Selection>("Selection")({ team: Team.pipe(DynamoModel.ref), // Input: teamId, Record: Team player: Player.pipe(DynamoModel.ref), // Input: playerId, Record: Player}) {}DynamoSchema
Section titled “DynamoSchema”Application namespace for key prefixing and versioning.
import { DynamoSchema } from "effect-dynamodb"| Export | Type | Description |
|---|---|---|
make | (config) => DynamoSchema | Create a schema with name, version, and optional casing |
prefix | (schema) => string | Build schema prefix: $name#vN |
applyCasing | (schema, value) => string | Apply casing rules to a structural key part |
composeKey | (schema, entityType, composites) => string | Compose entity key: $schema#vN#entity_type#composites |
composeCollectionKey | (schema, collection, composites) => string | Compose collection PK |
composeClusteredSortKey | (schema, collection, entity, composites) => string | Compose clustered SK |
composeIsolatedSortKey | (schema, entity, composites) => string | Compose isolated SK |
composeUniqueKey | (schema, entity, constraint, values) => { pk, sk } | Compose unique constraint sentinel keys |
composeVersionKey | (schema, entity, version) => string | Compose version snapshot SK |
composeDeletedKey | (schema, entity, timestamp) => string | Compose soft-deleted item SK |
composeVersionKeyPrefix | (schema, entity) => string | Prefix for version queries |
composeDeletedKeyPrefix | (schema, entity) => string | Prefix for deleted queries |
| Type | Description |
|---|---|
DynamoSchema | Interface: { name: string, version: number, casing: Casing } |
Casing | "lowercase" | "uppercase" | "preserve" |
const AppSchema = DynamoSchema.make({ name: "myapp", version: 1 })// Keys generated as: $myapp#v1#entity_type#compositesSee Modeling for details.
Minimal table definition with Layer-based name injection.
import { Table } from "effect-dynamodb"| Export | Type | Description |
|---|---|---|
make | (config: { schema, entities?, aggregates? }) => Table | Create a table definition with entities and aggregates |
definition | (table) => TableDefinition | Derive CreateTableCommandInput fields from entity index declarations |
| Type | Description |
|---|---|
Table | Interface with schema, entities, aggregates, layer(), layerConfig(), and Tag for DI |
TableConfig | { name: string } — runtime table configuration |
TableDefinition | { KeySchema, AttributeDefinitions, GlobalSecondaryIndexes? } |
const MainTable = Table.make({ schema: AppSchema, entities: { UserEntity, TaskEntity } })
// Runtime name injection via LayerMainTable.layer({ name: "MyTable" })
// Typed client auto-derives table schema for creationconst db = yield* DynamoClient.make({ entities: { UserEntity, TaskEntity }, tables: { MainTable },})yield* db.tables.MainTable.create()See Modeling for details.
Entity
Section titled “Entity”The core module — binds models to tables, provides CRUD operations, query accessors, lifecycle management, and 7 derived types. This is the largest module.
import { Entity } from "effect-dynamodb"Construction
Section titled “Construction”| Export | Type | Description |
|---|---|---|
make | (config) => Entity | Create an entity with model, table, indexes, and optional system fields |
make config:
| Property | Type | Required | Description |
|---|---|---|---|
model | Schema.Class | Schema.Struct | ConfiguredModel | Yes | Domain model schema (or configured model with field overrides) |
entityType | string | Yes | Discriminator stored as __edd_e__ |
primaryKey | { pk: KeyDef, sk: KeyDef } | Yes | Primary key composition rules |
indexes | Record<string, GsiIndexDef> | No | GSI index definitions with name, pk, sk, optional collection |
timestamps | boolean | { created?, updated? } | No | Auto-managed createdAt/updatedAt. Each slot takes a field name, a DynamoModel date schema (storage only), or { field?, schema? }. A schema without a DynamoEncoding annotation throws EDD-9044 at make() time |
versioned | boolean | { retain?, field?, ttl? } | No | Auto-increment version with optional snapshot retention |
softDelete | boolean | { ttl?, preserveUnique? } | No | Soft delete with optional TTL |
unique | Record<string, string[]> | No | Unique constraint definitions |
refs | Record<string, Entity> | No | Map ref field names to their source entities for ref hydration |
Operations
Section titled “Operations”These are methods on the entity definition returned by Entity.make(). They return operation descriptors used by Transaction, Batch, and advanced pipeable workflows. For day-to-day CRUD, use DynamoClient.make(table) to get a typed client with executable methods (see below).
| Operation | Signature | Description |
|---|---|---|
entity.put(input) | EntityPut | Insert or overwrite an item (descriptor) |
entity.create(input) | EntityPut | Insert only — fails with ConditionalCheckFailed if exists (descriptor) |
entity.upsert(input) | EntityPut | Create or update — uses if_not_exists() for immutable fields, createdAt, version (descriptor) |
entity.get(key) | EntityGet | Get a single item by primary key (descriptor) |
entity.update(key) | EntityUpdate | Start an update operation (compose with set, add, etc.) (descriptor) |
entity.patch(key) | EntityUpdate | Update with attribute_exists — fails with ConditionalCheckFailed if not exists (descriptor) |
entity.delete(key) | EntityDelete | Delete an item (descriptor) |
entity.deleteIfExists(key) | EntityDelete | Delete with attribute_exists — fails with ConditionalCheckFailed if not exists (descriptor) |
entity.query.<indexName>(pk) | Query<Record> | Query a specific index by partition key (descriptor) |
entity.scan() | Query<Record> | Full table scan filtered by entity type (descriptor) |
entity.versions(key) | Query<Record> | Query version history for an item (requires versioned: { retain: true }) (descriptor) |
entity.deleted.list(key) | Query<Record> | List soft-deleted items for a key (requires softDelete) (descriptor) |
entity.batchGet(keys) | See Batch module | Batch get items |
entity.batchPut(items) | See Batch module | Batch put items |
entity.batchDelete(keys) | See Batch module | Batch delete items |
Client Gateway Pattern
Section titled “Client Gateway Pattern”DynamoClient.make({ entities, aggregates?, tables? }) resolves dependencies and returns a typed client with executable operations for all listed entities and aggregates, namespaced under entities, aggregates, collections, and tables:
const MainTable = Table.make({ schema: AppSchema, entities: { UserEntity } })const db = yield* DynamoClient.make({ entities: { UserEntity }, tables: { MainTable },})const users = db.entities.UserEntityBound entity methods:
| Method | Description |
|---|---|
bound.get(key) | Get a single item |
bound.put(input, ...combinators) | Insert or overwrite. Optional combinators (e.g. condition(...)) |
bound.create(input, ...combinators) | Insert only — fails with ConditionalCheckFailed if exists |
bound.upsert(input, ...combinators) | Create or update — if_not_exists() for immutable fields, createdAt, version |
bound.update(key, ...combinators) | Update an item — compose with Entity.set(...), Entity.expectedVersion(...), etc. |
bound.patch(key, ...combinators) | Update with attribute_exists — fails if item does not exist |
bound.delete(key, ...combinators) | Delete an item. Optional combinators (e.g. condition(...)) |
bound.deleteIfExists(key, ...combinators) | Delete with attribute_exists — fails if item does not exist |
bound.paginate(query, ...combinators) | Execute a query and return a lazy Stream<A> of items, automatically paginating |
bound.collect(query, ...combinators) | Execute a query and collect all pages into Effect<Array<A>> |
Bound lifecycle methods (require matching entity config):
| Method | Requires | Returns | Description |
|---|---|---|---|
bound.getVersion(key, version) | versioned: { retain: true } | Effect | Get a specific version snapshot |
bound.versions(key) | versioned: { retain: true } | BoundQuery | List all version snapshots as a fluent BoundQuery — supports .collect(), .fetch(), .paginate(), .count(), .limit(), .reverse(), .startFrom(), .filter(), .select() |
bound.restore(key) | softDelete | Effect | Restore a soft-deleted item |
bound.purge(key) | Any | Effect | Permanently remove item + all versions and sentinels |
bound.deleted.get(key) | softDelete | Effect | Get a specific soft-deleted item |
bound.deleted.list(key) | softDelete | BoundQuery | List all soft-deleted tombstones in the partition as a fluent BoundQuery |
Entity definition lifecycle operations (require matching config — listed in Operations table above for query descriptors):
| Operation | Requires | Description |
|---|---|---|
entity.getVersion(key, version) | versioned: { retain: true } | Get a specific version snapshot (descriptor) |
entity.deleted.get(key) | softDelete | Get a soft-deleted item (descriptor) |
entity.restore(key) | softDelete | Restore a soft-deleted item (descriptor) |
entity.purge(key) | Any | Delete all items in the partition (main + versions + deleted) (descriptor) |
Operation Combinators
Section titled “Operation Combinators”These functions transform entity operations via pipe:
| Export | Works On | Description |
|---|---|---|
set(updates) | EntityUpdate | Set fields to new values (dual API) |
expectedVersion(n) | EntityUpdate | Optimistic lock — fail if version doesn’t match |
consistentRead | EntityGet | Enable strongly consistent reads |
condition(input) | EntityPut, EntityUpdate, EntityDelete | Add a ConditionExpression |
remove(fields) | EntityUpdate | REMOVE attributes from the item |
add(values) | EntityUpdate | Atomically ADD to numeric attributes |
subtract(values) | EntityUpdate | Subtract from numeric attributes (SET #f = #f - :v) |
append(values) | EntityUpdate | Append to list attributes (list_append) |
deleteFromSet(values) | EntityUpdate | DELETE elements from Set attributes |
returnValues(mode) | EntityUpdate, EntityDelete | Control DynamoDB ReturnValues: "none" | "allOld" | "allNew" | "updatedOld" | "updatedNew" |
cascade(config) | EntityUpdate | Propagate source entity changes to target entities that embed it via DynamoModel.ref. Config: { targets, filter?, mode? } (dual API) |
Decode Mode Selectors
Section titled “Decode Mode Selectors”Control what type an operation returns:
| Export | Returns | Description |
|---|---|---|
asModel | Entity.Model<E> | Pure domain object (default for yield*) |
asRecord | Entity.Record<E> | Domain + system fields (version, timestamps) |
asItem | Entity.Item<E> | Full DynamoDB item (all keys + __edd_e__) |
asNative | Entity.Marshalled<E> | Raw DynamoDB AttributeValue format |
Type Extractors
Section titled “Type Extractors”| Type | Description |
|---|---|
Entity.Model<E> | Pure domain object fields |
Entity.Record<E> | Model + system metadata (version, timestamps) |
Entity.Input<E> | Creation input (model fields, no system fields) |
Entity.Update<E> | Mutable fields only (keys and immutable excluded) |
Entity.Key<E> | Primary key attributes only |
Entity.Item<E> | Full DynamoDB item (model + system + keys + __edd_e__) |
Entity.Marshalled<E> | DynamoDB AttributeValue format |
Schema & Attribute Accessors
Section titled “Schema & Attribute Accessors”| Export | Description |
|---|---|
keyAttributes(entity) | List all key attribute names (primary + GSI) |
keyFieldNames(entity) | List physical field names for all keys |
compositeAttributes(entity) | List all composite attribute names across indexes |
itemSchema(entity) | Get the item-level decode schema |
decodeMarshalledItem(entity, item) | Decode a marshalled DynamoDB item through entity schema |
const UserEntity = Entity.make({ model: User, entityType: "User", primaryKey: { pk: { field: "pk", composite: ["userId"] }, sk: { field: "sk", composite: [] }, }, indexes: { byEmail: { name: "gsi1", pk: { field: "gsi1pk", composite: ["email"] }, sk: { field: "gsi1sk", composite: [] }, }, }, timestamps: true, versioned: true, unique: { email: ["email"] },})
// Get typed client with executable operationsconst db = yield* DynamoClient.make({ entities: { UserEntity }, tables: { MainTable },})const users = db.entities.UserEntity
// CRUDconst user = yield* users.put({ userId: "u-1", email: "a@b.com", ... })const found = yield* users.get({ userId: "u-1" })
// Update with combinatorsyield* users.update({ userId: "u-1" }, Entity.set({ email: "new@b.com" }))
// Update with multiple combinatorsyield* users.update( { userId: "u-1" }, Entity.set({ email: "new@b.com" }), Entity.expectedVersion(1),)
yield* users.delete({ userId: "u-1" })
// Query execution via bound entityconst allUsers = yield* users.collect(UserEntity.query.byEmail({ email: "a@b.com" }))const stream = users.paginate(UserEntity.scan(), Query.pageSize(100))
// Or with v2 entity-centric pattern (BoundQuery fluent API):// const db = yield* DynamoClient.make({ entities: { UserEntity } })// const allUsers = yield* db.entities.UserEntity.byEmail({ email: "a@b.com" }).collect()See Getting Started and Modeling for details.
Pipeable Query<A> data type — a lazy, immutable description of a DynamoDB query or scan.
import { Query } from "effect-dynamodb"Combinators
Section titled “Combinators”| Export | Type | Description |
|---|---|---|
where(conditions) | Dual | Add sort key conditions (KeyConditionExpression) |
limit(n) | Dual | Return at most n items (a contract on results — accumulates across requests, works under a filter) |
pageSize(n) | Dual | Fetch in batches of n rows — sets DynamoDB’s Limit (a contract on round trips) |
maxPages(n) | Dual | Limit total number of pages fetched |
reverse | Combinator | Reverse sort order (descending) |
consistentRead | Combinator | Enable strongly consistent reads |
ignoreOwnership | Combinator | Skip __edd_e__ entity type filter — for mixed-table scenarios |
startFrom(cursor) | Dual | Resume pagination from a previous cursor |
select(attrs) | Dual | Project specific attributes (returns Query<Record<string, unknown>>) |
filterExpr(expr) | Dual | Add an Expr ADT filter (from callback API) |
selectPaths(paths) | Dual | Project path segments (from callback API) |
Terminals
Section titled “Terminals”| Export | Returns | Description |
|---|---|---|
execute | Effect<Page<A>> | Execute and return a page: { items: Array<A>, cursor: string | null } |
collect | Effect<Array<A>> | Execute, fetch all pages, and flatten into a single array |
paginate | Effect<Stream<Array<A>>> | Execute and return a Stream of pages for lazy pagination |
count | Effect<number> | Execute with SELECT COUNT — returns total matching items (respects maxPages) |
asParams | Effect<Record<string, unknown>> | Return built DynamoDB command input without executing — useful for debugging |
Filter Operators
Section titled “Filter Operators”Used in entity-level filter():
| Operator | Example | Description |
|---|---|---|
| Equality | { status: "active" } | Exact match |
ne | { status: { ne: "deleted" } } | Not equal |
gt | { price: { gt: 30 } } | Greater than |
gte | { price: { gte: 30 } } | Greater than or equal |
lt | { price: { lt: 100 } } | Less than |
lte | { price: { lte: 100 } } | Less than or equal |
between | { price: { between: [10, 50] } } | Inclusive range |
beginsWith | { name: { beginsWith: "A" } } | String prefix |
contains | { name: { contains: "widget" } } | Substring match |
exists | { email: { exists: true } } | Attribute exists |
notExists | { email: { notExists: true } } | Attribute does not exist |
Utilities
Section titled “Utilities”| Export | Description |
|---|---|
isQuery(value) | Type guard for Query<A> |
const db = yield* DynamoClient.make({ entities: { TaskEntity }, tables: { MainTable },})const tasks = db.entities.TaskEntity
const items = yield* tasks.collect( TaskEntity.query.byProject({ projectId: "p-1" }), Query.where({ beginsWith: activePrefix }), TaskEntity.filter({ priority: { gt: 3 } }), Query.reverse, Query.limit(25),)See Queries for details.
BoundQuery
Section titled “BoundQuery”The fluent query builder returned by every entity query accessor on a BoundEntity (e.g. db.entities.Tasks.byProject({...})) and by db.entities.Tasks.scan(). BoundQuery<Model, SkRemaining, A> wraps an internal Query<A> with pre-resolved services so all terminals return Effect<..., ..., never>.
import type { BoundQuery } from "effect-dynamodb"Combinators are immutable — each call returns a new BoundQuery. Terminals execute the query.
Combinators
Section titled “Combinators”| Method | Description |
|---|---|
.where((t, ops) => …) | Type-safe sort key condition on remaining SK composites. Only available when SK composites have not all been consumed. Consumes SkRemaining (cannot be called twice). Operators: eq, lt, lte, gt, gte, between, beginsWith. |
.filter((t, ops) => …) | Post-read filter expression via callback. Type-safe attribute paths via t, condition operators via ops. |
.filter(shorthand) | Post-read filter via shorthand object (e.g. { status: "active" } or { gt: { price: 30 } }). |
.select((t) => […paths]) | Projection expression via callback. Returns BoundQuery<…, Record<string, unknown>>. |
.select(["field", …]) | Projection via attribute name array. |
.limit(n) | Return at most n items. A contract on results: the query accumulates across as many requests as it takes, which is what makes it work under .filter(). |
.pageSize(n) | Fetch in batches of n rows — sets DynamoDB’s Limit. A contract on round trips, not on what comes back. |
.maxPages(n) | Maximum number of DynamoDB pages to fetch. |
.reverse() | Reverse sort order (ScanIndexForward = false). |
.startFrom(cursor) | Resume pagination from an opaque cursor returned by .fetch(). |
.consistentRead() | Enable strongly consistent reads. |
.ignoreOwnership() | Skip the __edd_e__ entity-type filter. Use only when querying a polymorphic GSI shared across entity types and you want every item back. |
Terminals
Section titled “Terminals”| Method | Returns |
|---|---|
.collect() | Effect<Array<A>, DynamoClientError | ValidationError, never> — drain all pages into a single array. |
.fetch() | Effect<Page<A>, DynamoClientError | ValidationError, never> — single page + opaque cursor. Page<A> is { items: Array<A>, cursor: string | undefined }. |
.paginate() | Stream<A, DynamoClientError | ValidationError, never> — lazy stream that paginates automatically. |
.count() | Effect<number, DynamoClientError, never> — count-only query (Select: COUNT); items are not returned. |
where typing
Section titled “where typing”.where() is conditionally available based on the SkRemaining type parameter. When the index’s SK composites have already been fully consumed (e.g., the entity has no SK composites, or all of them were supplied to the query accessor), .where() is not present on the type — calling it is a compile error. Once called, .where() consumes SkRemaining and the resulting BoundQuery no longer exposes .where().
const db = yield* DynamoClient.make({ entities: { Tasks } })
// Sort key condition + filter + limitconst recent = yield* db.entities.Tasks .byProject({ projectId: "p-1" }) .where((t, { beginsWith }) => beginsWith(t.createdAt, "2026")) .filter((t, { eq }) => eq(t.status, "active")) .limit(50) .collect()
// Single page + cursorconst page = yield* db.entities.Tasks .byProject({ projectId: "p-1" }) .fetch()
// Lazy streamyield* db.entities.Tasks .byProject({ projectId: "p-1" }) .paginate() .pipe(Stream.runForEach((task) => Console.log(task.title)))
// Count onlyconst total = yield* db.entities.Tasks.byProject({ projectId: "p-1" }).count()See Queries and Expressions for details.
Collections
Section titled “Collections”Multi-entity queries across a shared index. Collections are auto-discovered from entity indexes that share the same collection property.
Auto-Discovery
Section titled “Auto-Discovery”When multiple entities define the same collection name on the same GSI, they are automatically grouped into a collection accessible via db.collections.<name>():
// Define entities with shared collectionconst Employees = Entity.make({ // ... indexes: { byTenant: { collection: "tenantMembers", name: "gsi1", pk: { field: "gsi1pk", composite: ["tenantId"] }, sk: { field: "gsi1sk", composite: ["name"] }, }, },})
const Tasks = Entity.make({ // ... indexes: { byTenant: { collection: "tenantMembers", name: "gsi1", pk: { field: "gsi1pk", composite: ["tenantId"] }, sk: { field: "gsi1sk", composite: ["priority"] }, }, },})
// Auto-discovered on the typed clientconst db = yield* DynamoClient.make({ entities: { Employees, Tasks } })const result = yield* db.collections.tenantMembers({ tenantId: "t-1" }).collect()// result: { Employees: Employee[], Tasks: Task[] }Explicit Collections
Section titled “Explicit Collections”For advanced use, Collection.make() creates collections explicitly:
import { Collection } from "effect-dynamodb"| Export | Type | Description |
|---|---|---|
make | (name, entities) => Collection | Create a collection from entities sharing an index |
See Indexes & Collections for details.
Transaction
Section titled “Transaction”Atomic multi-item operations.
import { Transaction } from "effect-dynamodb"| Export | Type | Description |
|---|---|---|
transactGet | (items) => Effect<Tuple> | Atomically get up to 100 items — returns a typed tuple. Accepts unbound Entity.get intermediates AND the BoundGet returned by db.entities.*.get(...) |
transactWrite | (ops) => Effect<void> | Atomically write up to 100 items (puts, deletes, condition checks). Accepts unbound Entity.put/Entity.delete intermediates AND the bound builders returned by db.entities.*; conditions attached to either (including create()’s implicit attribute_not_exists) are compiled into the transact item. upsert, update and patch are rejected with a ValidationError — see below |
check | (get, condition) => ConditionCheckOp | Create a condition-check operation for transactWrite (dual API). Takes either get spelling |
transactGet
Section titled “transactGet”const [user, order] = yield* Transaction.transactGet([ UserEntity.get({ userId: "u-1" }), db.entities.Orders.get({ orderId: "o-1" }),])db.entities.X.get(key) returns a BoundGet: it is an Effect<Model, …, never> (yield it, pipe it, Effect.catchTag it) and it is also a read descriptor, so the same value works in transactGet, Batch.get and check. That matters most for entities authored with the pure, AWS-free @effect-dynamodb/schema package — a pure definition carries no operations, so the bound client is the only surface its author holds. Both spellings unwrap through one protocol and can be mixed in a single array; anything else fails with a ValidationError carrying EDD-9052.
transactWrite
Section titled “transactWrite”yield* Transaction.transactWrite( UserEntity.put({ userId: "u-1", ... }), OrderEntity.delete({ orderId: "o-old" }), Transaction.check( UserEntity.get({ userId: "u-1" }), { attributeExists: "email" }, ),)Ops transactWrite will not compile
Section titled “Ops transactWrite will not compile”transactWrite emits exactly one Put / Delete / ConditionCheck per op. Anything whose entity contract needs a different verb, extra items, or a service the compile step does not hold is refused with a ValidationError rather than written with different semantics:
| Op / entity feature | Why it is refused |
|---|---|
upsert() | An UpdateItem using if_not_exists for createdAt, immutable fields and the version counter. A Put would reset all three |
update() / patch() | No Update support in the shared transact builder yet |
refs | Write-time ref hydration reads the referenced entity; the compile step cannot, so the ref attribute would be written empty |
generatedId | Id generation needs the Crypto service |
vectorIndexes | Computing the embedding needs the Embedder service; the item would drop out of the index |
delete() of an entity with unique, versioned: { retain: true } or softDelete | EDD-9048. The extra items a delete must write come from the stored row — the sentinel to release is keyed by its unique values, a retain snapshot copies it, and a soft-delete tombstone is that row relocated to a new sort key. This path never reads. Use the entity’s own delete, which reads first |
Multi-item puts expand
Section titled “Multi-item puts expand”A put through transactWrite (or EventStore.append({ additionalItems })) of an entity with unique constraints or versioned: { retain: true } emits more than one item: the row, one attribute_not_exists-guarded sentinel per satisfiable constraint, and the v1 snapshot. All of it derives from the payload, so it needs no read and stays atomic with the rest of the transaction.
Consequences worth knowing:
- The uniqueness constraint is enforced. A duplicate fails with
UniqueConstraintViolation(naming the constraint and the values), not a bareTransactionCancelled. - The 100-item cap counts the expanded total. Ten
unique+retainputs occupy 30 slots. The error message reports both numbers. softDeletedoes not affect a put — only a delete — so puts of soft-deletable entities are unaffected.
| Type | Description |
|---|---|
ConditionCheckOp | A condition-check operation for inclusion in transactWrite |
Batch get and write with auto-chunking and unprocessed item retry.
import { Batch } from "effect-dynamodb"| Export | Type | Description |
|---|---|---|
get | (...gets) => Effect<Tuple> | Batch-get up to 100 items with typed tuple return. Accepts unbound Entity.get intermediates AND the BoundGet returned by db.entities.*.get(...) |
write | (...ops) => Effect<void> | Batch-write any number of items (auto-chunks at 25). Accepts unbound Entity.put/Entity.delete intermediates AND the bound builders returned by db.entities.*; conditional ops and upsert/update/patch are rejected with a ValidationError — see below |
// Batch get — typed positional results; unbound and bound gets may be mixedconst [user1, user2] = yield* Batch.get([ UserEntity.get({ userId: "u-1" }), db.entities.Users.get({ userId: "u-2" }),])
// Batch write — mixed puts and deletesyield* Batch.write( UserEntity.put({ userId: "u-1", ... }), UserEntity.put({ userId: "u-2", ... }), OrderEntity.delete({ orderId: "o-old" }),)Auto-chunking: get chunks at 100 items, write chunks at 25 items. Both retry unprocessed items automatically.
BatchWriteItem has no ConditionExpression, so Batch.write rejects any op carrying a condition — .condition(...), create() (implicit attribute_not_exists) and deleteIfExists() (implicit attribute_exists) all fail with a ValidationError rather than silently writing unconditionally. Use Transaction.transactWrite for conditional writes.
It also rejects (EDD-9049) any write of an entity configured with unique or versioned: { retain: true }, and any delete of a softDelete entity. BatchWriteItem has no ConditionExpression (which is the entirety of a uniqueness sentinel’s correctness), no UpdateRequest, and no atomicity across its 25-item chunks — a sentinel that lands without its row, or a row without its sentinel, is a corrupt partition. A put of a softDelete entity is unaffected, because softDelete only changes the delete path.
Expression
Section titled “Expression”Condition, filter, and update expression builders.
import { Expression } from "effect-dynamodb"| Export | Type | Description |
|---|---|---|
condition | (input: ConditionInput) => ExpressionResult | Build ConditionExpression |
filter | (input: ConditionInput) => ExpressionResult | Build FilterExpression |
update | (input: UpdateInput) => ExpressionResult | Build UpdateExpression |
ConditionInput Operators
Section titled “ConditionInput Operators”| Operator | Example | Description |
|---|---|---|
eq | { eq: { status: "active" } } | Equality |
ne | { ne: { status: "deleted" } } | Not equal |
gt, gte, lt, lte | { gt: { price: 0 } } | Comparisons |
between | { between: { price: [10, 50] } } | Inclusive range |
beginsWith | { beginsWith: { name: "A" } } | String prefix |
attributeExists | { attributeExists: "email" } | Attribute exists |
attributeNotExists | { attributeNotExists: "pk" } | Attribute does not exist |
UpdateInput
Section titled “UpdateInput”| Property | Example | Description |
|---|---|---|
set | { set: { name: "New" } } | SET attribute values |
remove | { remove: ["oldField"] } | REMOVE attributes |
add | { add: { count: 1 } } | ADD to numeric/set attributes |
delete | { delete: { tags: new Set(["old"]) } } | DELETE from set attributes |
| Type | Description |
|---|---|
ExpressionResult | { expression: string, names: Record, values: Record } |
ConditionInput | Declarative condition expression input |
UpdateInput | Declarative update expression input |
const cond = Expression.condition({ eq: { status: "active" }, gt: { stock: 0 },})// cond.expression: "#status = :v0 AND #stock > :v1"See Expressions Guide for comprehensive reference.
Expr ADT (Callback API)
Section titled “Expr ADT (Callback API)”Type-safe expression building with PathBuilder and ConditionOps.
import { compileExpr, createConditionOps, createPathBuilder, isExpr, parseShorthand, parseSimpleShorthand, type Expr, type ConditionOps, type Path, type PathBuilder,} from "effect-dynamodb"| Export | Type | Description |
|---|---|---|
createPathBuilder<M>() | () => PathBuilder<M, M> | Create a path proxy for type-safe attribute access |
createConditionOps<M>() | () => ConditionOps<M> | Create comparison and logical operators |
compileExpr(expr, resolveDbName?) | (Expr) => CompileResult | Compile Expr to DynamoDB expression string |
isExpr(u) | (unknown) => u is Expr | Type guard for Expr nodes |
parseShorthand(input) | (ConditionInput) => Expr | Convert ConditionInput to Expr |
parseSimpleShorthand(input) | (Record) => Expr | Convert { key: value } to Expr |
Entity-Level Combinators:
| Combinator | Description |
|---|---|
Entity.condition(cb | shorthand) | Callback or object → condition combinator for put/update/delete |
Entity.filter(cb | shorthand) | Callback or object → filter combinator for query/scan |
Entity.select(cb | attrs) | Callback or string array → projection combinator |
Path-Based Update Combinators:
| Combinator | Description |
|---|---|
Entity.pathSet(op) | SET nested path or attribute-to-attribute copy |
Entity.pathRemove(segments) | REMOVE nested path or array element |
Entity.pathAdd(op) | ADD to nested numeric/set |
Entity.pathSubtract(op) | Subtract from nested numeric |
Entity.pathAppend(op) | Append to nested list |
Entity.pathPrepend(op) | Prepend to nested list |
Entity.pathIfNotExists(op) | Set only if attribute doesn’t exist |
Entity.pathDelete(op) | DELETE from nested set |
Types:
| Type | Description |
|---|---|
Expr | Discriminated union of 16 expression node types |
ConditionOps<Model> | Typed comparison/logical operators for callbacks |
PathBuilder<Root, Model> | Recursive proxy type for attribute path access |
Path<Root, Value, Keys> | Resolved attribute path with phantom types |
SizeOperand<Root> | size() operand for path-based size comparisons |
CompileResult | { expression: string, names: Record, values: Record } |
DeepPick<T, Paths> | Type utility for projection return type narrowing |
Projection
Section titled “Projection”ProjectionExpression builder for selecting specific attributes.
import { Projection } from "effect-dynamodb"| Export | Type | Description |
|---|---|---|
projection | (attrs: string[]) => ProjectionResult | Build ProjectionExpression from attribute names |
| Type | Description |
|---|---|
ProjectionResult | { expression: string, names: Record<string, string> } |
const proj = Projection.projection(["name", "email", "status"])// proj.expression: "#proj_name, #proj_email, #proj_status"// proj.names: { "#proj_name": "name", "#proj_email": "email", "#proj_status": "status" }KeyComposer
Section titled “KeyComposer”Composite key composition from index definitions. Used internally by Entity, also available for advanced use cases.
import { KeyComposer } from "effect-dynamodb"| Export | Type | Description |
|---|---|---|
composePk | (schema, entity, index, record) => string | Compose partition key value |
composeSk | (schema, entity, index, record) => string | Compose sort key value |
composeIndexKeys | (schema, entity, index, record) => Record | Compose all key attributes for an index |
tryComposeIndexKeys | (schema, entity, index, record) => Record | undefined | Non-throwing variant for sparse GSIs |
composeAllKeys | (schema, entity, indexes, record) => Record | Compose keys for all indexes |
composeGsiKeysForUpdatePolicyAware | (schema, entity, indexes, updates, keyRecord, { removedSet? }) => { sets, removes } | Policy-aware GSI key composition for update/append. Implements v3 per-half structural composition + policy-aware hole detection (see indexPolicy guide). |
composeSortKeyPrefix | (schema, entity, index, composites) => string | Partial SK prefix for begins_with queries |
extractComposites | (keyPart, record) => string[] | Extract composite attribute values from a record |
tryExtractComposites | (keyPart, record) => string[] | undefined | Non-throwing variant |
serializeValue | (value) => string | Serialize a value for key composition |
| Type | Description |
|---|---|
KeyPart | { field: string, composite: string[] } |
IndexDefinition | { pk: KeyPart, sk: KeyPart, index?, collection?, type?, casing?, indexPolicy? } |
IndexPolicyKey | "sparse" | "preserve" |
IndexPolicy | { pk?: IndexPolicyKey, sk?: IndexPolicyKey } (each key defaults to "preserve" when omitted) |
GsiUpdateResult | { sets: Record<string, string>, removes: ReadonlyArray<string> } |
Marshaller
Section titled “Marshaller”Thin wrapper around @aws-sdk/util-dynamodb.
import { Marshaller } from "effect-dynamodb"| Export | Type | Description |
|---|---|---|
toAttributeMap | (record) => Record<string, AttributeValue> | Marshall JS object to DynamoDB format |
fromAttributeMap | (item) => Record<string, unknown> | Unmarshall DynamoDB format to JS object |
toAttributeValue | (value) => AttributeValue | Marshall a single value |
fromAttributeValue | (av) => unknown | Unmarshall a single value |
DynamoClient
Section titled “DynamoClient”Effect service wrapping AWS SDK DynamoDBClient.
import { DynamoClient } from "effect-dynamodb"Service Construction
Section titled “Service Construction”| Export | Type | Description |
|---|---|---|
DynamoClient | Context.Service | Effect service class |
DynamoClient.layer(config) | Layer<DynamoClient> | Create live layer with region + optional endpoint/credentials |
DynamoClient.layerConfig(config) | Layer<DynamoClient, ConfigError> | Create live layer from Effect Config providers. Accepts { region, endpoint?, credentials? } as Config.Config<...> values |
Service Methods
Section titled “Service Methods”| Method | Description |
|---|---|
createTable(input) | Create a DynamoDB table |
deleteTable(input) | Delete a DynamoDB table |
putItem(input) | Put a single item |
getItem(input) | Get a single item |
deleteItem(input) | Delete a single item |
updateItem(input) | Update an item with expression |
query(input) | Query a table or index |
scan(input) | Scan a table or index |
batchGetItem(input) | Batch-get up to 100 items |
batchWriteItem(input) | Batch-write up to 25 items |
transactGetItems(input) | Transact-get up to 100 items |
transactWriteItems(input) | Transact-write up to 100 items |
// Standard layerDynamoClient.layer({ region: "us-east-1" })
// DynamoDB LocalDynamoClient.layer({ region: "us-east-1", endpoint: "http://localhost:8000", credentials: { accessKeyId: "local", secretAccessKey: "local" },})
// Config-based (reads from Effect Config — e.g., env vars)DynamoClient.layerConfig({ region: Config.string("AWS_REGION"), endpoint: Config.option(Config.string("DYNAMO_ENDPOINT")),})Aggregate
Section titled “Aggregate”Graph-based composite domain model for DynamoDB. Binds a Schema.Class hierarchy to a DAG of underlying entity types sharing a partition key.
import { Aggregate } from "effect-dynamodb"Construction
Section titled “Construction”| Export | Type | Description |
|---|---|---|
make | Overloaded | Create a sub-aggregate or top-level aggregate (see below) |
one | (name, { entityType }) => OneEdge | Create a one-to-one edge descriptor |
many | (name, config) => ManyEdge | Create a one-to-many edge descriptor |
ref | (entity) => RefEdge | Create a ref edge (inline hydration, no separate DynamoDB item) |
Sub-aggregate form — Aggregate.make(Schema, { root, edges }):
Returns a SubAggregate<TSchema> with a .with(config) method for discriminator binding.
const TeamSheetAggregate = Aggregate.make(TeamSheet, { root: { entityType: "MatchTeam" }, edges: { coach: Aggregate.one("coach", { entityType: "MatchCoach" }), players: Aggregate.many("players", { entityType: "MatchPlayer" }), },})Top-level form — Aggregate.make(Schema, { table, schema, pk, collection, root, refs?, edges }):
Returns an Aggregate<TSchema, TKey> with get, create, update, delete operations.
const MatchAggregate = Aggregate.make(Match, { table: MainTable, schema: CricketSchema, pk: { field: "pk", composite: ["id"] }, collection: { name: "match" }, root: { entityType: "MatchItem" }, refs: { Team: Teams, Player: Players, Coach: Coaches, Venue: Venues }, edges: { venue: Aggregate.one("venue", { entityType: "MatchVenue" }), team1: TeamSheetAggregate.with({ discriminator: { teamNumber: 1 } }), team2: TeamSheetAggregate.with({ discriminator: { teamNumber: 2 } }), },})Operations
Section titled “Operations”| Operation | Signature | Description |
|---|---|---|
aggregate.get(key) | Effect<Domain, AggregateAssemblyError | DynamoError | ValidationError> | Fetch and assemble by partition key |
aggregate.create(input) | Effect<Domain, AggregateWriteError> | Create from input (ref IDs hydrated, sub-aggregate transactions) |
aggregate.update(key, fn) | Effect<Domain, AggregateWriteError> | Fetch → mutate → diff → write changed groups. fn receives UpdateContext with { state, cursor, optic, current } |
aggregate.delete(key) | Effect<void, AggregateAssemblyError | DynamoError> | Remove all items in the partition |
aggregate.list(key, options?) | Effect<ListResult<Domain>, AggregateAssemblyError | DynamoError | ValidationError> | Page aggregates off the list index. options: limit (at most n aggregates), pageSize (rows examined per request), filter (server-side FilterExpression, callback or shorthand), reverse, cursor. Returns { data, cursor }; cursor is null only when exhausted, and is always null on a sharded (cardinality) list, which rejects a passed cursor with EDD-9051 |
Edge Types
Section titled “Edge Types”| Type | Description |
|---|---|
OneEdge | One-to-one edge: { _tag: "OneEdge", name, entityType } |
ManyEdge | One-to-many edge: { _tag: "ManyEdge", name, entityType, edgeAttributes?, sk? } |
RefEdge | Inline ref edge: { _tag: "RefEdge", entity } — no separate DynamoDB item |
AggregateEdge | Union: OneEdge | ManyEdge | RefEdge |
Interfaces
Section titled “Interfaces”| Type | Description |
|---|---|
SubAggregate<TSchema> | Composable sub-aggregate with .with(config) for discriminator binding |
BoundSubAggregate<TSchema> | Discriminator-bound sub-aggregate, ready to embed in a parent |
Aggregate<TSchema, TKey> | Top-level aggregate with CRUD operations |
UpdateContext<TIso, TClass> | Context provided to update mutation: { state: TIso, cursor: Cursor<TIso>, optic: Optic.Iso<TIso, TIso>, current: TClass } |
Type Extractors
Section titled “Type Extractors”| Type | Description |
|---|---|
Aggregate.Type<A> | Assembled domain type (e.g., Match) |
Aggregate.Key<A> | Partition key type |
Type Guards
Section titled “Type Guards”| Export | Description |
|---|---|
isOneEdge(edge) | Check if edge is OneEdge |
isManyEdge(edge) | Check if edge is ManyEdge |
See Aggregates & Refs for details.
EventStore
Section titled “EventStore”Typed, Effect-native event sourcing on DynamoDB. Provides a Decider model for command-event-state aggregates, an EventStream repository per stream type, optimistic concurrency via stream versioning, and a commandHandler combinator for the read-decide-append cycle.
import { EventStore } from "effect-dynamodb"Construction
Section titled “Construction”| Export | Description |
|---|---|
EventStore.makeStream({ table, streamName, events, streamId, metadata? }) | Create an EventStream<TEvent, TStreamIdFields, TMetadata> bound to a Table. events is an array of Schema.Class event types, streamId.composite lists the stream-id composite fields, optional metadata is a Schema. |
EventStore.bind(stream) | Resolve DynamoClient and TableConfig from context and return a BoundEventStream whose operations have R = never. Use inside Context.Service make effects. |
Decider
Section titled “Decider”A Decider<State, Command, Event, E = never> encodes one aggregate’s command-event-state triad:
| Field | Type |
|---|---|
decide | (command: Command, state: State) => Effect<ReadonlyArray<Event>, E> |
evolve | (state: State, event: Event) => State (pure) |
initialState | State |
Operations
Section titled “Operations”EventStream and BoundEventStream expose the same operations. The only difference is the R parameter — EventStream ops require DynamoClient | TableConfig, BoundEventStream ops have R = never.
| Method | Returns |
|---|---|
append(streamId, events, expectedVersion, options?) | Effect<AppendResult<TEvent>, VersionConflict | DynamoClientError | ValidationError | TransactionCancelled, R> — atomically append events with optimistic concurrency. Each event becomes a Put with attribute_not_exists(pk) inside a single TransactWriteItems. Optional options.metadata is validated against the stream’s metadata schema. |
read(streamId) | Effect<ReadonlyArray<StreamEvent<TEvent>>, DynamoClientError | ValidationError, R> — read all events for a stream in version order. |
readFrom(streamId, afterVersion) | Effect<ReadonlyArray<StreamEvent<TEvent>>, DynamoClientError | ValidationError, R> — read events strictly after a given version. |
currentVersion(streamId) | Effect<number, DynamoClientError | ValidationError, R> — current head version (0 if the stream is empty). |
query.events(streamId) | Query<StreamEvent<TEvent>> — raw query handle for use with Query combinators. |
BoundEventStream additionally exposes provide(effect) as an escape hatch to push services into an arbitrary effect.
Command handler
Section titled “Command handler”| Export | Description |
|---|---|
EventStore.commandHandler(decider, stream) | Build a (streamId, command, options?) => Effect<CommandHandlerResult, …, R> that reads, folds, decides, and appends atomically. Dual API: commandHandler(decider, stream) (data-first) or stream.pipe(EventStore.commandHandler(decider)) (data-last). Works with both EventStream (R = DynamoClient | TableConfig) and BoundEventStream (R = never). On a no-op command (decide returns []) the handler returns the current state and version without writing. |
Fold helpers
Section titled “Fold helpers”| Export | Description |
|---|---|
EventStore.fold(decider, events) | Pure: fold a list of StreamEvent<TEvent> through decider.evolve starting from decider.initialState. Dual API. |
EventStore.foldFrom(decider, startState, events) | Pure: fold from a supplied starting state (e.g. snapshot + delta events). Dual API. |
| Type | Shape |
|---|---|
StreamEvent<A> | { streamId, version, eventType, data: A, metadata, timestamp } |
AppendResult<A> | { version: number, events: ReadonlyArray<A> } |
CommandHandlerResult<State, Event> | AppendResult<Event> & { state: State } |
EventStream<TEvent, TStreamIdFields, TMetadata> | Repository interface returned by makeStream |
BoundEventStream<TEvent, TStreamIdFields, TMetadata> | Same shape as EventStream with R = never on every operation |
import { EventStore } from "effect-dynamodb"import { Effect, Schema, Context } from "effect"
class MatchStarted extends Schema.Class<MatchStarted>("MatchStarted")({ venue: Schema.String,}) {}
class InningsCompleted extends Schema.Class<InningsCompleted>("InningsCompleted")({ innings: Schema.Number, runs: Schema.Number,}) {}
const MatchEvents = EventStore.makeStream({ table: EventsTable, streamName: "Match", events: [MatchStarted, InningsCompleted], streamId: { composite: ["matchId"] },})
const decider = { initialState: { innings: 0, totalRuns: 0 }, decide: (cmd: { _tag: "Start"; venue: string }, _state) => Effect.succeed([new MatchStarted({ venue: cmd.venue })]), evolve: (state, event) => { if (event._tag === "MatchStarted") return state return { innings: event.innings, totalRuns: state.totalRuns + event.runs } },}
class MatchEventService extends Context.Service<MatchEventService>()( "@app/MatchEventService", { make: Effect.gen(function* () { const stream = yield* EventStore.bind(MatchEvents) const handle = EventStore.commandHandler(decider, stream) return { start: (matchId: string, venue: string) => handle({ matchId }, { _tag: "Start", venue }), history: (matchId: string) => stream.read({ matchId }), } }), },) {}See Event Sourcing for a complete tutorial.
Errors
Section titled “Errors”Tagged error types for precise error handling with catchTag.
import { DynamoError, ThrottlingError, DynamoValidationError, InternalServerError, ResourceNotFoundError, ItemNotFound, ConditionalCheckFailed, ValidationError, TransactionCancelled, TransactionOverflow, UniqueConstraintViolation, OptimisticLockError, ItemDeleted, ItemNotDeleted, CompositeKeyHoleError, CompositeNullableError, RefNotFound, AggregateAssemblyError, AggregateDecompositionError, AggregateTransactionOverflow, CascadePartialFailure, VersionConflict, AppendTooLarge, AdditionalItemConditionFailed, DuplicateCommand,} from "effect-dynamodb"| Error | Tag | Description |
|---|---|---|
DynamoError | "DynamoError" | AWS SDK error wrapper (includes operation and cause) |
ThrottlingError | "ThrottlingError" | AWS SDK throttling — request rate exceeded |
DynamoValidationError | "DynamoValidationError" | Malformed DynamoDB request |
InternalServerError | "InternalServerError" | Transient DynamoDB failure |
ResourceNotFoundError | "ResourceNotFoundError" | Table or index does not exist |
ItemNotFound | "ItemNotFound" | getItem returned no item |
ConditionalCheckFailed | "ConditionalCheckFailed" | Condition expression not met (put, update, delete, create) |
ValidationError | "ValidationError" | Schema decode/encode failed |
TransactionCancelled | "TransactionCancelled" | Transaction rejected (includes cancellation reasons) |
TransactionOverflow | "TransactionOverflow" | Transaction exceeds 100-item DynamoDB limit |
AppendTooLarge | "AppendTooLarge" | EventStore.append exceeds the 100-item transaction limit (streamName, streamId, count, limit) |
UniqueConstraintViolation | "UniqueConstraintViolation" | Unique constraint violated on put/create |
OptimisticLockError | "OptimisticLockError" | Version mismatch on expectedVersion() |
ItemDeleted | "ItemDeleted" | Item is soft-deleted (get returns this instead of the item) |
ItemNotDeleted | "ItemNotDeleted" | Restore called on an item that isn’t soft-deleted |
CompositeKeyHoleError | "CompositeKeyHoleError" | EDD-9024 — deprecated in v1.7.1, no longer thrown at runtime. Class export retained for back-compat with consumers who type-imported it for Effect.catchTag handlers. Hole patterns now collapse into the unified per-half can’t-compose rule (drop under sparse, noop-or-cascade-override under preserve). |
CompositeNullableError | "CompositeNullableError" | EDD-9025 — composite attribute Schema includes null. Raised at Entity.make() time. Carries entityType, surface (e.g. "primaryKey", "index:...", "unique:..."), compositeAttribute, schemaPath. |
RefNotFound | "RefNotFound" | Referenced entity not found during ref hydration (entity, field, refEntity, refId) |
AggregateAssemblyError | "AggregateAssemblyError" | Aggregate read path failed — missing items, structural violations, or decode errors (aggregate, reason, key) |
AggregateDecompositionError | "AggregateDecompositionError" | Aggregate write path failed — schema validation or structural error (aggregate, member, reason) |
AggregateTransactionOverflow | "AggregateTransactionOverflow" | Sub-aggregate exceeds 100-item transaction limit (aggregate, subgraph, itemCount, limit) |
CascadePartialFailure | "CascadePartialFailure" | Cascade update partially failed in eventual mode (sourceEntity, sourceId, succeeded, failed, errors) |
VersionConflict | "VersionConflict" | Event store version mismatch |
AdditionalItemConditionFailed | "AdditionalItemConditionFailed" | A caller-supplied EventStore.append({ additionalItems }) condition failed — NOT a version conflict (streamName, streamId, indices, reasons) |
DuplicateCommand | "DuplicateCommand" | A commandId was already applied to this stream (streamName, streamId, commandId) |
const db = yield* DynamoClient.make({ entities: { UserEntity }, tables: { MainTable },})const users = db.entities.UserEntity
const user = yield* users.get({ userId: "u-1" }).pipe( Effect.catchTag("ItemNotFound", () => Effect.succeed(null)), Effect.catchTag("DynamoError", (e) => Effect.die(`DynamoDB ${e.operation} failed: ${e.cause}`) ),)