QUESTPIE

Collection methods

What each method on a collection client does, what it sends over the wire, and what comes back.

View markdown

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.

On this page