Mutations (atomic operators)
.mutate() opens the MutationBuilder — a sub-chain for atomic,
field-level operations that map to MongoDB's update operators
($set, $inc, $push, etc.). Use mutations when you want to
change specific fields without rewriting the whole document, or when
you want atomic semantics for things like counters and array
merges.
Mutations belong to a write terminal. On the MutationBuilder itself
the terminals are .updateOne(), .updateMany(), .patchOne(),
.patchMany() to execute immediately, and .asUpdateOne(),
.asUpdateMany(), .asPatchOne(), .asPatchMany() when you're
queueing the operation into a bulk.
Shape at a glance
await bob.flowQuery('finance/bill')
.docId('B00000048')
.mutate()
.set({ 'data.amount': 5000, 'data.currency': 'USD' })
.inc({ 'data.lineCount': 1 })
.push({ 'data.tags': 'urgent' })
.status('posted')
.updateOne()
The chain accumulates operators into the IR's mutation payload;
the terminal compiles and executes.
Field-level operators
.set(dotPaths) — $set
.set({ 'data.amount': 5000, 'data.currency': 'USD' })
.setData(fields, opts?) — $set with auto data. prefix
Convenience: keys are automatically prefixed with data..
.setData({ amount: 5000, currency: 'USD' })
// same as .set({ 'data.amount': 5000, 'data.currency': 'USD' })
.setFlat(nested) — flatten nested → dot-notation
Useful when you have a nested object and want a precise $set that
only touches leaf fields (not clobber a parent object):
.setFlat({ data: { name: 'John', address: { line1: '123 Main St' } } })
// produces $set: { 'data.name': 'John', 'data.address.line1': '123 Main St' }
.unset(...fields) — $unset
Removes a field entirely:
.unset('data.tempField')
.unset('data.a', 'data.b', 'data.c')
.inc(dotPaths) — $inc
Atomic numeric increment. Negative values decrement.
.inc({ 'data.lineCount': 1, 'data.totalAmount': -50 })
.multiply(dotPaths) — $mul
Atomic numeric multiply. 1.10 applies a 10% increase.
.multiply({ 'data.price': 1.10 })
.min(dotPaths) / .max(dotPaths) — $min / $max
Update only if the new value is smaller/larger than the existing value.
.min({ 'data.lowestPrice': 99.99 })
.max({ 'data.highestPrice': 500 })
.rename(mapping) — $rename
Rename fields in place.
.rename({ 'data.oldField': 'data.newField' })
Array operators
.push(dotPaths) — $push
Append to an array (no deduplication).
.push({ 'data.tags': 'urgent' })
.pull(dotPaths) — $pull
Remove matching elements from an array.
.pull({ 'data.tags': 'draft' })
.pullAll(dotPaths) — $pullAll
Remove every occurrence of every listed value.
.pullAll({ 'data.tags': ['draft', 'pending'] })
.addToSet(dotPaths) — $addToSet
Append only if the element isn't already present (set semantics).
.addToSet({ 'data.categories': 'reviewed' })
.pop(field, direction) — $pop
Pop first (-1) or last (1) element.
.pop('data.queue', 1) // pop the last element
.slice(field, maxLength) — bounded push
Trim an array to a maximum length (implemented via $push +
$slice). Use for bounded queues, recent-items lists, etc.
.slice('data.recentNotes', 10)
.updateArrayElement(arrayField, data, filter) — positional update
Update a specific element inside an array by a filter.
.updateArrayElement(
'data.lineItems',
{ quantity: 5 },
{ sku: 'ABC-123' },
)
Upsert-only operator
.setOnInsert(dotPaths) — $setOnInsert
Applied only when the write triggers an insert (relevant for upsert patterns). Ignored on a plain update.
.setOnInsert({ 'data.createdBy': userId })
Status and data-type transitions
A mutation can bundle a status or data-type transition alongside field ops:
.mutate()
.set({ 'data.approvedAt': new Date() })
.status('posted')
.updateOne()
.mutate()
.setData({ paidAmount: 1000 })
.dataType('bill-payment-reconciliation-status', 'paid')
.patchOne()
These run through the same transition guards as setStatus and
setDataType.
Bypassing immutability
.mutate()
.set({ 'data.amount': 5000 })
.modifyImmutable() // explicit opt-in — required when the current status is immutable
.updateOne()
Same semantics as updateOne(payload, { modifyImmutable: true }).
Terminals
The mutation chain ends with one of:
| Terminal | Executes immediately | Semantics |
|---|---|---|
.updateOne() | yes | Full-replace semantics on one doc |
.updateMany() | yes | Full-replace semantics on all matching |
.patchOne() | yes | Merge semantics on one doc |
.patchMany() | yes | Merge semantics on all matching |
.asUpdateOne() | no | Configure for bulk |
.asUpdateMany() | no | Configure for bulk |
.asPatchOne() | no | Configure for bulk |
.asPatchMany() | no | Configure for bulk |
The as* terminals return the parent FlowQueryBuilder, ready to be
handed to bulk.add(...). See Bulk.
Realistic example
await bob.flowQuery('finance/journalItem')
.docId(docId)
.mutate()
.setData({ approvedBy: bob.flowUser.userId })
.inc({ 'data.revisionCount': 1 })
.addToSet({ 'data.history': { event: 'approved', at: new Date() } })
.unset('data.pendingApprovalToken')
.status(bob.flowQuery('finance/journalItem').statusRef().value('posted'))
.updateOne()
Every field change is atomic, validated against the schema (unless
.skipValidation() is used), and participates in the current debe
session.
Next: Bulk →