Collection methods
What each method on a collection client does, what it sends over the wire, and what comes back.
You have the method list. This page is what each one actually does, and the two or three places where the shape is not what you would guess.
Reading
Three reads. find gives you a page, findOne gives you one row or null,
count gives you a number.
const result = await client.collections.posts.find({
where: { published: true, author: { is: { role: "editor" } } },
with: { author: true },
orderBy: { createdAt: "desc" },
limit: 20,
offset: 0,
});
const post = await client.collections.posts.findOne({ where: { id } });
const total = await client.collections.posts.count({
where: { published: true },
});find takes where, with, orderBy, limit, offset, search,
groupBy, includeDeleted, locale, localeFallback and stage. It all
goes into the query string. findOne takes the same list minus limit,
offset and groupBy. count takes where and includeDeleted only.
`columns` does not reach a collection route
The type accepts it and narrows the row you get back. The route never reads
it, so the server sends every field anyway. Project server-side instead,
through app.collections.<name>.find(). A global's get() does read it.
`find()` hands back a page, not an array
It resolves to { docs, totalDocs, totalPages, page, limit, pagingCounter, hasPrevPage, hasNextPage, prevPage, nextPage }. Your rows are on docs.
count() is the exception and unwraps to a plain number for you.
Pass groupBy and the shape changes. You get groups, an array of
{ key, value, count, docs }, plus groupBy and totalGroups. In that mode
limit and offset paginate the groups, not the rows. Both shapes are
inferred from the options you passed, so TypeScript already knows which one
you are holding.
The findOne shortcut
findOne calls GET /:collection/:id when where has exactly one key and
that key is id. Otherwise it runs a find at limit: 1 and hands you
docs[0] or null.
The shortcut puts your `id` straight in the URL
where: { id } is fine. where: { id: { in: [a, b] } } is not, because the
single-key check still passes and the operator object lands in the path. Add
a second condition, or use find.
Writing one row
const created = await client.collections.posts.create({
title: "Hello",
slug: "hello",
author: authorId, // belongsTo takes a raw id
tags: { create: [{ name: "intro" }] },
});
const updated = await client.collections.posts.updateById({
id: created.id,
data: { published: true },
});
await client.collections.posts.deleteById({ id: created.id });create takes the data first and the locale options second. updateById takes
{ id, data }. Nested relation writes accept connect, create,
connectOrCreate and set, and a belongsTo field also takes the raw id. See
Relations.
deleteById resolves to { success, data }, where data is the row as it
stood. It soft-deletes when the collection has soft delete on. Then
restoreById({ id }) brings the row back and purgeById({ id }) removes it
for good. Purge has its own access rule, so delete rights do not grant it, and
it rejects a row that is still active.
Writing many rows
// One `data`, applied to every row that matches. Returns the rows it wrote.
const written = await client.collections.posts.updateMany({
where: { published: false },
data: { published: true },
});
// Different data per row, all in one transaction.
await client.collections.posts.updateBatch({
updates: [
{ id: a, data: { title: "A" } },
{ id: b, data: { title: "B" } },
],
});
const { count } = await client.collections.posts.deleteMany({
where: { published: false },
});`updateMany` and `deleteMany` are claim-checked
Both lock the matching rows and re-run where inside the transaction.
updateMany returns only the rows it actually wrote, so an empty array means
nothing matched at write time. It is not an error. deleteMany's count is
the rows that still matched at delete time.
deleteMany goes to POST /:collection/delete-many rather than a DELETE,
because it carries a body.
Versions and workflow
These need versioning on the collection, and transitionStage needs a
workflow too. See Options.
const versions = await client.collections.posts.findVersions({ id, limit: 10 });
// each row is the row itself plus versionId, versionNumber, sourceRevision,
// versionOperation, versionUserId and versionCreatedAt
await client.collections.posts.revertToVersion({ id, version: 3 });
await client.collections.posts.transitionStage({ id, stage: "published" });Pass either version or versionId to revert. A Date in scheduledAt is
sent as an ISO string. Leave it out and the transition happens now.
Turn on optimisticConcurrency and rows carry a revision. Every by-id
mutation then requires expectedRevision. updateMany and deleteMany take
an expectedRevisions array. updateBatch puts one expectedRevision on each
entry instead. A stale value comes back as a conflict. See
Optimistic concurrency.
Introspection
const meta = await client.collections.posts.meta();
const schema = await client.collections.posts.schema();meta() is the light one: field names, the title field, timestamps, soft
delete, relation names, and which fields are localized or virtual. schema()
is the full picture. Its relations carry their type and target collection, not
just a name. It also adds a validation JSON Schema for insert and update, and
access evaluated for the current session. The admin panel builds its forms
and tables from schema().
Live reads
live() and liveIter() take the same query and push you find()-shaped
snapshots as rows change. They carry where, with, limit, offset,
orderBy and locale only. Anything else you pass is dropped rather than
applied. See Realtime.
Next
Uploads covers upload and uploadMany, which
take a different code path from everything here.