-
Notifications
You must be signed in to change notification settings - Fork 0
Adapter: Transaction auto upgrade
When hooks.checkConsistency(batch) returns BatchDescriptor[] (even an empty array), the Adapter upgrades a single CRUD op to a TransactWriteItems call covering the main op plus the consistency actions.
See also (AWS JS SDK v3):
TransactWriteCommand· TransactWriteItems API · Condition expressions. Vocabulary: Concepts.
Partial writes — especially patches — touch only a subset of an item's fields. That raises an obvious worry: does the resulting item still honor the invariants it was supposed to? Referential integrity (the parent row still exists), optimistic locking (the revision hasn't moved), quota enforcement (the tenant's row count is still under the cap), tenant scoping (the row actually belongs to the caller's tenant) — every one of these is a rule whose answer depends on other rows in the database.
The naive approach reads the other rows back to the client, evaluates the invariant in JavaScript, then writes. Two round-trips, and a race window between the read and the write where the invariant could silently become false.
Transaction auto-upgrade inverts the flow. checkConsistency(batch) returns ConditionCheck descriptors that DynamoDB evaluates server-side, atomically with the write, inside a single TransactWriteItems. All checks pass → the write commits. Any check fails → the whole transaction is rejected, the database is unchanged, and the caller sees a single TransactionCanceledException. No invariant data ever leaves the server. This is the canonical shape for invariant enforcement in DynamoDB apps that use the toolkit.
- CRUD method (
post/put/patch/delete) builds aBatchDescriptorvia the matchingmake*builder. -
await hooks.checkConsistency(batch)runs. - If the result is
nullorundefined, dispatch as a singlePutCommand/UpdateCommand/DeleteCommand. - If the result is an array, bundle the main op + checks into a single
TransactWriteCommand. - If the combined batch exceeds
TRANSACTION_LIMIT(100), throwTransactionLimitExceededError.
const adapter = new Adapter({
client: docClient,
table: 'planets',
keyFields: ['name'],
hooks: {
async checkConsistency(batch) {
// Only relevant for writes that include a parent reference
if (batch.action !== 'put' && batch.action !== 'patch') return null;
const item = batch.params.Item || {};
if (!item.parent) return null;
// Demand the parent exists before this write succeeds
return [{
action: 'check',
params: {
TableName: this.table,
Key: {name: item.parent},
ConditionExpression: 'attribute_exists(#k0)',
ExpressionAttributeNames: {'#k0': 'name'}
}
}];
}
}
});
// Auto-upgraded: dispatched as TransactWriteItems with [check, put]
await adapter.post({name: 'Mustafar', parent: 'Sol'});
// No parent → not upgraded; dispatched as plain Put
await adapter.post({name: 'Sol'});Why the ConditionExpression is a raw string here. The toolkit ships buildCondition for composing a ConditionExpression against a single params object — the builder walks a ConditionClause[] tree and mutates ConditionExpression + ExpressionAttributeNames / Values on that one object. Inside checkConsistency you are building a descriptor for a 'check' action that will become one of several TransactWriteItems, each with its own params. The raw-string form is the idiom here because you're populating a fresh params with exactly one condition; buildCondition carries the overhead of merging into an existing expression without an upside at this call site. If the condition gets complex (multiple clauses, path traversal), you can still call buildCondition — just pass {} as the params argument and copy the result into the descriptor.
checkConsistency: async () => []This still upgrades to TransactWriteItems, with just the main op as a single TransactItem. Useful when you want every write to have transactional semantics for observability or downstream consumers.
- DynamoDB caps
TransactWriteItemsat 100 actions per call. The Adapter throwsTransactionLimitExceededError(with.actionCount) when the combined main + checks would exceed it. - Consistency actions are by definition supposed to be atomic with the main op — silently splitting them across transactions would defeat the purpose.
import {TransactionLimitExceededError} from 'dynamodb-toolkit';
try {
await adapter.put(item);
} catch (e) {
if (e instanceof TransactionLimitExceededError) {
console.error(`Transaction would have ${e.actionCount} actions; cap is 100`);
}
throw e;
}For consumers building single-table apps where every write fans out into many Put/Update/Delete actions, the auto-upgrade pattern lets you keep the high-level CRUD surface while the consistency hook handles the fan-out. For graphs of related items, dynamodb-onetable and electrodb are dedicated tools.
Start here
- Getting started
- Concepts
- Key and field design
- Compatibility
- Migration: v2 to v3
- SDK v2 to v3 cheat sheet
Guides
- Hierarchical data walkthrough
- Key expression patterns
- Multi-type tables
- Pagination
- Mass operation semantics
- URL schema design
Adapter
- Adapter
- Constructor options
- CRUD methods
- Mass methods
- Batch builders
- Hooks
- Raw marker
- Indirect indices
- Transaction auto-upgrade
Expression builders
Batch / transactions / mass / paths
REST surface
Framework adapters
Recipes
- Recipes index
- List records of a tier
- Per-tier sparse GSI markers
- Tier within a partition
- Reservation with auto-release
- Keys-only GSI, runtime projection
- Cascade subtree operations
- Querying subtrees with buildKey
- Filter URL grammar
- Text search
- Provisioning workflow
- Resumable mass operations
History