-
Notifications
You must be signed in to change notification settings - Fork 0
Expressions: Update builder
buildUpdate constructs a DynamoDB UpdateExpression with attribute-name aliasing, attribute-value placeholders, and atomic array operations.
See also (AWS JS SDK v3):
UpdateCommand· Update expressions · Expression attribute names · Expression attribute values. Vocabulary: Concepts.
A patch is to writes what projection is to reads. On the read side, projection asks DynamoDB to return only the attributes you actually need; on the write side, a patch asks DynamoDB to modify only the attributes that actually changed. Same philosophy, opposite direction — and the same three wins: less capacity consumed, less network bandwidth used, less work for DynamoDB to do.
Concretely: when a caller holds a projection of an item, mutates a handful of fields on the client, and wants to persist those changes, do not serialize the whole (partial) item into a PutItem — that would overwrite the attributes you didn't read with whatever your client happens to have (or didn't have). Instead, build an UpdateExpression that SETs the changed fields, REMOVEs the ones you explicitly want cleared, and leaves everything else alone. The resulting wire payload is proportional to the change, not to the item size.
Patches also enable DB-side invariant checks with no round-trip. A partial update that touches a subset of fields leaves open the question "is the resulting item still valid?" Instead of reading the item back to the client to verify, attach a ConditionExpression — or register a checkConsistency hook — and DynamoDB rejects the update atomically if the invariant is violated. See Adapter: Transaction auto-upgrade for the pattern.
Every builder in dynamodb-toolkit/expressions mutates a params object in place and returns the same object. The design is deliberately additive: a caller builds up a full DynamoDB Command input by running one builder after another against the same params — each call adds its own expression field plus the supporting name/value aliases.
-
Simple case — pass
{}(or nothing), use the returned object directly. -
Typical case — start with the mandatory SDK fields (
TableName,Key,Item,IndexName, …), pass through each builder you need, callcleanParamsat the end, hand the result to the SDK.
buildUpdate fits in the middle of that pipeline:
import {buildUpdate, buildCondition, cleanParams} from 'dynamodb-toolkit/expressions';
import {UpdateCommand} from '@aws-sdk/lib-dynamodb';
let params = {TableName: 'planets', Key: {name: 'Hoth'}}; // mandatory SDK fields
params = buildUpdate({climate: 'cold'}, {}, params); // adds UpdateExpression, names, values
params = buildCondition( // AND-merges into ConditionExpression
[{path: 'revision', op: '=', value: 3}], params
);
params = cleanParams(params); // strips any unused name/value
await docClient.send(new UpdateCommand(params));Each builder picks up the running #/: counters from whatever is already on params — calling buildUpdate then buildCondition produces a coherent, collision-free expression regardless of order. See Concepts → Additive params for the full picture.
This is not a fluent chain (.a().b()) — it's a passed-through mutable bag. There is no step-based "chain" to interrupt; you can call builders zero, one, or many times on the same params, and they all append.
import {buildUpdate} from 'dynamodb-toolkit/expressions';
const params = buildUpdate(
{climate: 'cold', diameter: 7200},
{delete: ['old_field'], separator: '.', arrayOps: [{op: 'append', path: 'tags', values: ['frozen']}]}
);
// params.UpdateExpression: "SET #upk0 = :upv0, #upk1 = :upv1, #upk2 = list_append(if_not_exists(#upk2, :upv3), :upv4) REMOVE #upk5"
// params.ExpressionAttributeNames / ExpressionAttributeValues populatedfunction buildUpdate<T extends object>(
patch: Record<string, unknown>,
options?: UpdateOptions,
params?: T
): T & {UpdateExpression: string};
interface UpdateOptions {
delete?: string[]; // paths to REMOVE
separator?: string; // path separator, default '.'
arrayOps?: ArrayOp[]; // atomic array operations
}buildUpdate mutates and returns params (or creates {} when omitted). Existing ExpressionAttributeNames / ExpressionAttributeValues are preserved — new aliases get unique counters (#upk0, #upk1, …, :upv0, :upv1, …) continuing from whatever is already in the map.
Pure-digit segments become array indices:
buildUpdate({'config.thresholds.0': 100, 'tags.2': 'archived'});
// SET #upk0.#upk1[0] = :upv0, #upk2[2] = :upv1Use a non-default separator if your keys contain .:
buildUpdate({'a.b.c': 'flat'}, {separator: '/'});
// SET #upk0 = :upv0 (treats the whole string as one segment)op |
DDB expression | Notes |
|---|---|---|
append |
SET path = list_append(if_not_exists(path, :empty), :values) |
Add to tail; values is an array |
prepend |
SET path = list_append(:values, if_not_exists(path, :empty)) |
Add to head |
setAtIndex |
SET path[i] = :value |
Absolute-index write |
removeAtIndex |
REMOVE path[i] |
DDB leaves a hole — no shift |
add |
ADD path :value |
Atomic numeric increment, or Set add |
buildUpdate({}, {arrayOps: [
{op: 'append', path: 'moons', values: ['A', 'B']},
{op: 'setAtIndex', path: 'moons', index: 5, value: 'F'},
{op: 'removeAtIndex', path: 'tags', index: 2},
{op: 'add', path: 'visit_count', value: 1}
]});-
splice(remove + shift),reorder(swap),insertAtIndex(shift up). DynamoDB has no atomic primitive for these; they would require a read-modify-write loop (concurrency-unsafe) or aTransactWriteItemsread+write pair with preconditions. Build it in your own consistency hook if you need it.
The builders allocate every name/value placeholder a rich input might need. Conditional paths (an empty patch, a condition builder with no clauses, a field that was if_not_exists-guarded on a path that never fired) can leave entries in ExpressionAttributeNames / ExpressionAttributeValues that don't appear in any expression field. DynamoDB rejects unused entries at request time.
cleanParams(params) scans the five expression fields (KeyConditionExpression, ConditionExpression, UpdateExpression, ProjectionExpression, FilterExpression) and drops every name/value alias that isn't referenced. It's idempotent; safe to call on any params.
import {buildUpdate, cleanParams} from 'dynamodb-toolkit/expressions';
let params = buildUpdate({climate: 'cold'}, {}, {TableName: 'planets', Key: {name: 'Hoth'}});
params = cleanParams(params);
// Unused aliases (if any) removed; params ready for UpdateCommandThe Adapter calls cleanParams automatically inside every make* / CRUD / mass method (see src/adapter/adapter.js). Call it yourself at the end of any builder pipeline you assemble by hand.
Full example — build a conditional update from a plain patch, ship it via the SDK, no Adapter:
import {UpdateCommand} from '@aws-sdk/lib-dynamodb';
import {buildUpdate, buildCondition, cleanParams} from 'dynamodb-toolkit/expressions';
// 1. Mandatory SDK fields (TableName, Key)
let params = {TableName: 'planets', Key: {name: 'Hoth'}};
// 2. Build the mutation — SET climate, remove old_field
params = buildUpdate({climate: 'cold'}, {delete: ['old_field']}, params);
// 3. Guard: only apply the update if revision is still 3 (optimistic lock)
params = buildCondition(
[{path: 'revision', op: '=', value: 3}],
params
);
// 4. Bump revision as part of the same atomic update
params = buildUpdate({}, {arrayOps: [{op: 'add', path: 'revision', value: 1}]}, params);
// 5. Drop any unused aliases and ship
params = cleanParams(params);
await docClient.send(new UpdateCommand(params));Steps 2–4 could happen in any order; builders only read the running counters and append. This is the "additive builders" contract — params is an accumulator, not a fluent chain.
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