YouBothAgent▾
You — Business rules and flows you own. Read these yourself.
Both — Know the idea; your agent follows the details.
Agent — Conventions and references your agent follows. Look up as needed.
General▾
Interface▾
Observability▾
Performance▾
Native▾
Development▾

Mutating Data

You need to bump a view counter, archive a batch of rows, or edit the one record a user opened. There are two ways to write, and the choice decides whether your hooks run.
Document Path
Loads the document, changes it and saves it, so hooks run and a removal takes its cascade.
await this.updatePost(id, data);
Query Write
Sends one atomic SQL statement and loads nothing. Fast and safe under races, but no hook runs.
await this.Post.updateOne(filter, change);
Which one runs what
If a hook or a cascade must run for each document, take a document path. The table uses three words:
schema.preschema.post
Schema hooks, registered in _onSchema. They run whenever a document is saved or removed.
_preCreate_postCreate_preUpdate_postUpdate_preRemove_postRemove
Service hooks. Only the service's create<Model>, update<Model> and remove<Model> run them.
cascade
A removal declared on a field: documents linked to the removed one go with it.
Method
Schema hooks
Service hooks
cascade
Document path: load, change, save
update<Model>(id, data)
✓
✓
Generated on the service. The default choice for one record.
remove<Model>(id)
✓
✓
✓
Generated on the service. Soft-removes the document, then runs the cascade.
pickAndWrite(id, data)
✓
On the model: pick, set, save. pickOneAndWrite(query, data) picks by query.
doc.set(data).save()
✓
The same thing, spelled out, when you already hold the document.
Query write: one SQL statement, nothing loaded
updateOne · updateMany
Change the newest match, or every match.
removeOne · removeMany
Soft-remove the newest match, or every match.
updateById · removeById
The same query writes, narrowed to one id.
update<Filter>(…).set(…)
Generated per filter, like remove<Filter>, updateOne<Filter> and removeOne<Filter>.
bulkWrite(operations)
A list of updateOne operations, run one after another.
✓RunsDoes not run

Two Write Styles

Every query write takes a filter first, then the change. Write the change as a plain object or as a builder function:
FormWhenExample
ObjectYou only assign values. A bare value means set.{ status: "published", pinned: true }
BuilderYou need inc, addToSet or another operator.({ inc }) => ({ viewNum: inc(1) })
In a model class, the two look like this:
apps/myapp/lib/post/post.document.ts
  • The builder's argument holds every operator, the way q holds the conditions in a filter. Destructure only what you use; there is nothing to import.
  • Keys are field paths. "profile.city" writes inside an object field, and a key that names no declared field throws.
  • The four base columns take only set and unset. id, createdAt, updatedAt and removedAt refuse inc and every other operator.

Counters And Sets

Numeric and array operators run inside the database. Two requests that bump the same counter at once both count, and neither overwrites the other:
apps/myapp/lib/post/post.document.ts
  • Numbers: inc, mul, min, max. A missing field counts as 0 for inc and mul; min and max just write the value. inc() alone adds 1.
  • Arrays: push, addToSet, pull. A missing array starts empty. push always appends, addToSet appends only when the value is absent, and pull removes every equal element.
  • updateMany reports how many rows it touched in modifiedCount, all in one statement.

Upsert

An upsert changes the match, or inserts a new row when nothing matches. Pass { upsert: true } as the third argument:
apps/myapp/lib/stat/stat.document.ts
When nothing matches, each part of the call ends up here:
Part of the callIn the new row
{ key: "daily-visits" }Plain values in the filter are copied in. Conditions such as q.oneOf() are not.
total: inc(1)Operators apply to an empty value, so total starts at 1.
status: setOnInsert("active")Written on this insert only. An update that finds a match ignores it.
resultupsertedId holds the new id, and matchedCount is 0.
  • Only updateOne, updateById and bulkWrite upsert. updateMany takes no options.
  • The insert runs the "create" schema hooks only. The "save" hooks and service hooks such as _postCreate do not run.

How It Becomes SQL

Every operator folds into one nested JSON expression on the _doc column, and updatedAt is stamped on every write. The database applies the whole update as one statement.
plain value
Sets the field. The short form of set.
set(value)
Sets the field.
unset()
Removes the field.
inc(by = 1)
Adds by. A missing field counts as 0.
mul(by)
Multiplies by by. A missing field counts as 0.
min(value)
Keeps the smaller of the stored value and value.
max(value)
Keeps the larger of the stored value and value.
push(value)
Appends to the array. A missing array starts empty.
addToSet(value)
Appends only when no equal element is there yet.
pull(value)
Removes every element equal to value.
setOnInsert(value)
Sets the field only when an upsert inserts a new row.
nested path
A dotted key writes inside an object field.
combined
Several operators nest into one expression.
  • Simplified, in the SQLite dialect. Postgres uses the matching jsonb functions.
  • Every operator reads the document as it was before the update, so all changes in one call see the same original values.

Tips And Pitfalls

  • Query writes for counters and bulk state changes, on a model with no removal side effect. Take a document path whenever hooks, a cascade or domain logic must run.
  • Put atomic writes on the model class in <model>.document.ts, and return !!modifiedCount so the service gets a plain yes or no.
  • Use the builder form instead of importing update helpers at module scope.
  • q.search() cannot filter a write. A query write whose filter searches throws; find the ids first, then write by id.
  • bulkWrite runs its operations one by one, each as its own statement, and adds up the counts.
What a write returns
Every query write resolves to the same result. Check modifiedCount when the write must have hit a row:
acknowledgedboolean
true once the statement ran.
matchedCountnumber
Rows the filter matched. 0 when an upsert inserted instead.
modifiedCountnumber
Rows the write changed, counting an upsert's insert.
upsertedIdstring | nulloptional
The new row's id when an upsert inserted; otherwise null or absent.
Read next

Released under the MIT License

Connect your AI to these docs

MCPhttps://akanjs.com/mcp
Copyright © 2026 Akan.js All rights reserved.System managed bybassman