Conditions Expressions
Building thoughtful indexes can make queries simple and performant. Sometimes you need to filter results down further or add conditions to an update/patch/upsert/put/create/delete/remove action.
ConditionExpressions
Below is the traditional way you would add a ConditionExpression to Dynamo’s DocumentClient directly alongside how you would accomplish the same using the where method.
Example
animals
.update({
animal: "blackbear",
name: "Isabelle",
})
// no longer pregnant because Ernesto was born!
.set({
isPregnant: false,
lastEvaluation: "2021-09-12",
lastEvaluationBy: "stephanie.adler",
})
// welcome to the world Ernesto!
.append({
offspring: [
{
name: "Ernesto",
birthday: "2021-09-12",
note: "healthy birth, mild pollen allergy",
},
],
})
// using the where clause can guard against making
// updates against stale data
.where(
({ isPregnant, lastEvaluation }, { lt, eq }) => `
${eq(isPregnant, true)} AND ${lt(lastEvaluation, "2021-09-12")}
`,
)
.go();
Equivalent Parameters
{
"UpdateExpression": "SET #isPregnant = :isPregnant_u0, #lastEvaluation = :lastEvaluation_u0, #lastEvaluationBy = :lastEvaluationBy_u0, #offspring = list_append(#offspring, :offspring_u0)",
"ExpressionAttributeNames": {
"#isPregnant": "isPregnant",
"#lastEvaluation": "lastEvaluation",
"#lastEvaluationBy": "lastEvaluationBy",
"#offspring": "offspring"
},
"ExpressionAttributeValues": {
":isPregnant0": true,
":lastEvaluation0": "2021-09-12",
":isPregnant_u0": false,
":lastEvaluation_u0": "2021-09-12",
":lastEvaluationBy_u0": "stephanie.adler",
":offspring_u0": [
{
"name": "Ernesto",
"birthday": "2021-09-12",
"note": "healthy birth, mild pollen allergy"
}
]
},
"TableName": "zoo_manifest",
"Key": {
"pk": "$zoo#animal_blackbear",
"sk": "$animals_1#name_isabelle"
},
"ConditionExpression": "#isPregnant = :isPregnant0 AND #lastEvaluation < :lastEvaluation0"
}
Where Clause
The where() method allow you to write a FilterExpression or ConditionExpression without having to worry about the complexities of expression attributes. To accomplish this, ElectroDB injects an object attributes as the first parameter to all Filter Functions, and an object operations, as the second parameter. Pass the properties from the attributes object to the methods found on the operations object, along with inline values to set filters and conditions.
Provided
wherecallbacks must return a string. All method on theoperationobject all return strings, so you can return the results of theoperationmethod or use template strings compose an expression.
Examples
A single filter operation
animals
.update({ habitat: "Africa", enclosure: "5b" })
.set({ keeper: "Joe Exotic" })
.where((attr, op) => op.eq(attr.dangerous, true))
.go();
A single filter operation with destructuring
animals
.update({ animal: "tiger", name: "janet" })
.set({ keeper: "Joe Exotic" })
.where(({ dangerous }, { eq }) => eq(dangerous, true))
.go();
Multiple conditions
animals
.update({ animal: "tiger", name: "janet" })
.set({ keeper: "Joe Exotic" })
.where(
(attr, op) => `
${op.eq(attr.dangerous, true)} AND ${op.notExists(attr.lastFed)}
`,
)
.go();
Chained usage (implicit AND)
animals
.update({ animal: "tiger", name: "janet" })
.set({ keeper: "Joe Exotic" })
.where(
(attr, op) => `
${op.eq(attr.dangerous, true)} OR ${op.notExists(attr.lastFed)}
`,
)
.where(({ birthday }, { between }) => {
const today = Date.now();
const lastMonth = today - 1000 * 60 * 60 * 24 * 30;
return between(birthday, lastMonth, today);
})
.go();
“dynamic” filtering
type GetAnimalOptions = {
habitat: string;
keepers: string[];
};
function getAnimals(options: GetAnimalOptions) {
const { habitat, excludeKeepers } = options;
const query = animals.query.exhibit({ habitat });
for (const name of excludeKeepers) {
query.where(({ keeper }, { ne }) => ne(keeper, name));
}
return query.go();
}
const { data, cursor } = await getAnimals({
habitat: "RainForest",
keepers: ["Joe Exotic", "Carol Baskin"],
});
Operations
The attributes object contains every Attribute defined in the Entity’s Model. The operations object contains the following methods:
| operator | example | result |
|---|---|---|
eq | eq(rent, maxRent) | #rent = :rent1 |
ne | eq(rent, maxRent) | #rent <> :rent1 |
gte | gte(rent, value) | #rent >= :rent1 |
gt | gt(rent, maxRent) | #rent > :rent1 |
lte | lte(rent, maxRent) | #rent <= :rent1 |
lt | lt(rent, maxRent) | #rent < :rent1 |
begins | begins(rent, maxRent) | begins_with(#rent, :rent1) |
exists | exists(rent) | attribute_exists(#rent) |
notExists | notExists(rent) | attribute_not_exists(#rent) |
contains | contains(rent, maxRent) | contains(#rent = :rent1) |
notContains | notContains(rent, maxRent) | not contains(#rent = :rent1) |
between | between(rent, minRent, maxRent) | (#rent between :rent1 and :rent2) |
name | name(rent) | #rent |
value | value(rent, maxRent) | :rent1 |
escape | escape(123), escape('abc') | #123, #abc |
field | field('field_name') | :field_name |
ElectroDB Functions
The filter functions available above all come from the list of functions supported by DynamoDB directly. ElectroDB offers a few functions that offer some additional convenience when creating Filter Expressions.
Note: If ElectroDB ever lags behind in implementing FilterExpression functions, you can use
value(),name(),escape(), and/orfield()to simply template out the implementation!
Name
The name() function allows you to create a reference to an attribute’s name, which can be useful to create filters referencing an attribute as it is currently stored. The function will add a ExpressionAttributeNames property for this record and return the partial expression for use in your filter.
This example demonstrates how you might find animals that were last fed by someone other than their “keeper”.
animals.query
.exhibit({ habitat: "Africa", enclosure: "5b" })
.where(
({ lastFedBy, keeper }, { name }) => `
${name(lastFedBy)} != ${name(keeper)}
`,
)
.go();
Value
The value() function can be paired with name() as convenience when building unique filters. The value() method is passed an attribute (used to enforce type), and a value for that attribute. ElectroDB will add a ExpressionAttributeValues property for this value, and return a reference to that value that can be used in your FilterExpression.ExpressionAttributeValues
This example shows how you can implement the eq operation without using the ElectroDB eq function
animals.query
.exhibit({ habitat: "Africa", enclosure: "5b" })
.where(
({ keeper }, { name, value, eq }) => `
${name(keeper)} = ${value(keeper, "Tiger King")}
`,
)
.go();
Escape
The escape() method allows you to provide any primitive to an expression without the need for a specific attribute. In many ways, this function is like a less restrictive version of value(). This can be useful when using static values and/or when creating custom FilterExpressions
This example shows how you might use escape to apply a filter against the size() of an attribute.
animals.query
.exhibit({ habitat: "Africa", enclosure: "5b" })
.where(
({ diet }, { size, escape }) => `
${size(diet)} > ${escape(2)}
`,
)
.go();
Field
The field() method allows you to provide and reference a field value that is not present in your model. This method is similar to escape() but is used for field names.
You can use both escape() and field() to template any filter that DynamoDB supports.
animals.query
.exhibit({ habitat: "Africa", enclosure: "5b" })
.where(
(_, { field, escape }) => `
contains(${field("gsi1sk")}, ${escape("value")})
`,
)
.go();
Handling Condition Check Failures
By default, when a condition expression fails (e.g. a create on an item that already exists), ElectroDB throws an error. The returnOnConditionCheckFailure execution option allows you to handle these failures gracefully by returning the existing item instead of throwing.
This maps to DynamoDB’s ReturnValuesOnConditionCheckFailure parameter and is supported on all write operations: put, create, update, patch, delete, remove, and upsert.
Note: This feature requires AWS SDK v3, or AWS SDK v2 >=
2.1408.0. Older v2 SDK versions do not includeReturnValuesOnConditionCheckFailurein their API model, so the parameter is silently stripped before reaching DynamoDB.
Response Format
When returnOnConditionCheckFailure is set to "all_old", the response shape changes to a discriminated union with a rejected property:
// Condition passed — operation succeeded
{ rejected: false, data: YourItem }
// Condition failed — existing item returned (or null if no item exists)
{ rejected: true, data: YourItem | null }
When this option is not provided, the response format is unchanged (
{ data: YourItem }).
Example: Safe Create
A common pattern is to attempt a create and gracefully handle the case where the item already exists:
const result = await StoreLocations
.create({
cityId: "Atlanta1",
storeId: "LatteLarrys",
mallId: "EastPointe",
buildingId: "BuildingA1",
unitId: "B47",
category: "food/coffee",
leaseEndDate: "2020-03-22",
rent: "4500.00",
})
.go({ returnOnConditionCheckFailure: "all_old" });
if (result.rejected) {
// The item already exists — result.data contains the existing item
console.log("Store already exists:", result.data);
} else {
// The item was created successfully
console.log("Store created:", result.data);
}
Example: Conditional Update
You can use returnOnConditionCheckFailure with .where() conditions to inspect the current state of an item when a condition fails:
const result = await StoreLocations
.patch({ cityId: "Atlanta1", mallId: "EastPointe", storeId: "LatteLarrys", buildingId: "BuildingA1" })
.set({ rent: "5000.00" })
.where(({ rent }, { lt }) => lt(rent, "4000.00"))
.go({ returnOnConditionCheckFailure: "all_old" });
if (result.rejected) {
// Condition failed — rent was not less than 4000.00
// result.data contains the current item so you can inspect it
console.log("Current rent:", result.data?.rent);
} else {
// Update succeeded
console.log("Rent updated successfully");
}
Null Data on Rejection
When the condition fails because the target item does not exist (e.g. a patch or remove on a non-existent item), result.data will be null:
const result = await StoreLocations
.remove({ cityId: "Atlanta1", mallId: "EastPointe", storeId: "LatteLarrys", buildingId: "BuildingA1" })
.go({ returnOnConditionCheckFailure: "all_old" });
if (result.rejected) {
// result.data is null — no item existed to return
console.log("Item did not exist:", result.data === null);
}
Combining with Other Options
The returnOnConditionCheckFailure option can be combined with other execution options:
response: On success, thedatafield respects theresponseoption (e.g."all_new"returns the updated item). On rejection,dataalways contains the existing (old) item.data: Works with"raw"and"includeKeys"— the rejected item is returned in the requested format.originalErr: When set totrue, the raw DynamoDB error is thrown even whenreturnOnConditionCheckFailureis set, bypassing the condition check handling.
Advanced Usage
Where with Complex Attributes
ElectroDB supports using the where() method with DynamoDB’s complex attribute types: map, list, and set. When using the injected attributes object, simply drill into the attribute itself to apply your update directly to the required object.
The following are examples on how to filter on complex attributes:
Filtering on a map attribute
animals
.update({ animal: "tiger", name: "janet" })
.set({ keeper: "Joe Exotic" })
.where(({ veterinarian }, { eq }) => eq(veterinarian.name, "Herb Peterson"))
.go();
Filtering on an element in a list attribute
animals
.update({ animal: "tiger", name: "janet" })
.set({ keeper: "Joe Exotic" })
.where(({ offspring }, { eq }) => eq(offspring[0].name, "Blitzen"))
.go();
Multiple Where Clauses
It is possible to include chain multiple where clauses. The resulting FilterExpressions (or ConditionExpressions) are concatenated with an implicit AND operator.
let MallStores = new Entity(model, { table: "StoreDirectory" });
let stores = await MallStores.query
.leases({ mallId: "EastPointe" })
.between({ leaseEndDate: "2020-04-01" }, { leaseEndDate: "2020-07-01" })
.where(
({ rent, discount }, { between, eq }) => `
${between(rent, "2000.00", "5000.00")} AND ${eq(discount, "1000.00")}
`,
)
.where(
({ category }, { eq }) => `
${eq(category, "food/coffee")}
`,
)
.go();
{
"TableName": "StoreDirectory",
"ExpressionAttributeNames": {
"#rent": "rent",
"#discount": "discount",
"#category": "category",
"#pk": "idx2pk",
"#sk1": "idx2sk"
},
"ExpressionAttributeValues": {
":rent1": "2000.00",
":rent2": "5000.00",
":discount1": "1000.00",
":category1": "food/coffee",
":pk": "$mallstoredirectory_1#mallid_eastpointe",
":sk1": "$mallstore#leaseenddate_2020-04-01#storeid_",
":sk2": "$mallstore#leaseenddate_2020-07-01#storeid_"
},
"KeyConditionExpression": "#pk = :pk and #sk1 BETWEEN :sk1 AND :sk2",
"IndexName": "idx2",
"FilterExpression": "(#rent between :rent1 and :rent2) AND (#discount = :discount1 AND #category = :category1)"
}