Query, sort, and bulk writes
List, bulk update, and bulk delete share the same filter encoding. The JavaScript, Dart, and Python SDKs build it for you.
Pagination
Section titled “Pagination”GET /api/p/:projectId/:table?page=1&pageSize=20
page defaults to 1 and pageSize defaults to 20. data is { "items", "total", "page", "pageSize" } (see REST conventions). SDK list() returns that data object.
Filters
Section titled “Filters”Chained where clauses combine with $and of per-field objects. Python uses the same operators in snake_case (is_equal_to, is_greater_than, and the rest of the table).
| SDK option | Mongo |
|---|---|
isEqualTo |
$eq |
isNotEqualTo |
$ne |
isGreaterThan |
$gt |
isGreaterThanOrEqualTo |
$gte |
isLessThan |
$lt |
isLessThanOrEqualTo |
$lte |
isIn |
$in |
isNotIn |
$nin |
matches |
$regex with $options: "i" |
matches is a case-insensitive regex. The SDKs do not escape the pattern; pass a regex or a literal. Escape user input before sending if you want contains-style matching. The server still rejects $regex values that are too long or use disallowed quantifiers. See Limits.
const published = await client .collection('posts') .where('status', { isEqualTo: 'published' }) .where('views', { isGreaterThan: 100 }) .orderBy('views', { descending: true }) .list({ page: 1, pageSize: 10 });Pass null explicitly when the filter must be JSON null. Omitting a key means “operator omitted”.
final published = await client .collection('posts') .where('status', isEqualTo: 'published') .where('views', isGreaterThan: 100) .orderBy('views', descending: true) .list(page: 1, pageSize: 10);Use Null.value when the filter must be JSON null (Dart null means omitted).
published = ( client.collection("posts") .where("status", is_equal_to="published") .where("views", is_greater_than=100) .order_by("views", descending=True) .list(page=1, page_size=10))Use Null from kataflax when the filter must be JSON null (Python None means omitted).
The same list over HTTP:
curl -sG "https://api.tefily.com/api/p/$PROJECT_ID/posts" \ -H "Authorization: Bearer $TOKEN" \ --data-urlencode 'page=1' \ --data-urlencode 'pageSize=10' \ --data-urlencode 'filter={"$and":[{"status":{"$eq":"published"}},{"views":{"$gt":100}}]}' \ --data-urlencode 'sort={"views":-1}' \ --data-urlencode 'fields=["title","status"]'orderBy('name') → {"name": 1}. orderBy('createdAt', { descending: true }) → {"createdAt": -1}. You can chain multiple orderBy calls. Python is order_by("views", descending=True).
On REST, pass sort as JSON (and filter as JSON) on the query string. Sort values are 1 or -1. When sort is omitted, the default is { "_createdAt": -1 }.
Field selection
Section titled “Field selection”select(['title', 'status']) sends fields=["title","status"]. _id is always returned. Unknown names are 400.
await client.collection('posts').select(['title', 'status']).list();await client.collection('posts').select(['title', 'status']).list();client.collection("posts").select(["title", "status"]).list()Use Query.get(id) (not Collection.get) when you also want select / join / expand on a single document. Collection.get(id) still returns the full document.
Joins and expand
Section titled “Joins and expand”Related documents are nested on the parent. At most 5 join/expand entries. Duplicate as is 400.
join always embeds an array (capped at 50 by default, max 100). Parent pageSize does not apply to nested arrays. If the array length equals limit, there may be more — query the related collection directly to load the rest. A scalar expand of a reference that is not an array unwraps to one object. join is always an array.
const posts = await client .collection('posts') .select(['title', 'authorId']) .join('authorId', { from: 'users', as: 'author', fields: ['name', 'email'] }) .join('_id', { from: 'comments', as: 'comments', foreignField: 'postId', limit: 10 }) .list({ page: 1, pageSize: 10 });final posts = await client .collection('posts') .select(['title', 'authorId']) .join('authorId', from: 'users', as: 'author', fields: ['name', 'email']) .join('_id', from: 'comments', as: 'comments', foreignField: 'postId', limit: 10) .list(page: 1, pageSize: 10);from and as are Python keywords, so the parameters are from_collection and as_name.
posts = ( client.collection("posts") .select(["title", "authorId"]) .join("authorId", from_collection="users", as_name="author", fields=["name", "email"]) .join("_id", from_collection="comments", as_name="comments", foreign_field="postId", limit=10) .list(page=1, page_size=10))expand('authorId') uses the reference field’s optional target collection. Scalar references become one object or null. Array references stay arrays (same cap). Without a stored target, use join or you get 400. Python is expand("authorId").
The server always applies the list rules of the joined collection. The client cannot skip them. If that list endpoint is disabled, the request is 404. If list is denied, 403. If list is allowed with a filter, unauthorized related documents are omitted.
REST: fields and expand are JSON query parameters on list and get.
Bulk writes
Section titled “Bulk writes”createMany (create_many in Python) POSTs a JSON array to the collection URL (a JSON object is still single create). At most 100 documents. Validation and create rules run on every item before any insert; one failure inserts nothing. It requires the create endpoint to be enabled.
const created = await client.collection('posts').createMany([ { title: 'Hello', status: 'draft' }, { title: 'World', status: 'draft' },]);final created = await client.collection('posts').createMany([ {'title': 'Hello', 'status': 'draft'}, {'title': 'World', 'status': 'draft'},]);created = client.collection("posts").create_many( [ {"title": "Hello", "status": "draft"}, {"title": "World", "status": "draft"}, ])updateMany / deleteMany (update_many / delete_many in Python) require at least one where. They map to PATCH / DELETE on the collection URL.
updateMany sends a partial patch. Fields you omit, including required ones, stay as they are. Values you send are type-checked. Unknown fields are rejected. If the update endpoint has an owner compare, that rule also sees this patch. A status-only patch can be denied. See Access rules.
await client .collection('posts') .where('status', { isEqualTo: 'draft' }) .updateMany({ status: 'archived' });
await client .collection('posts') .where('status', { isEqualTo: 'archived' }) .deleteMany();await client .collection('posts') .where('status', isEqualTo: 'draft') .updateMany({'status': 'archived'});
await client .collection('posts') .where('status', isEqualTo: 'archived') .deleteMany();client.collection("posts").where("status", is_equal_to="draft").update_many( {"status": "archived"})
client.collection("posts").where("status", is_equal_to="archived").delete_many()Limits
Section titled “Limits”The data API has no realtime subscription. Poll list or get.
- At most 5
expandentries. Error:Invalid "expand": at most 5 entries are allowed. - Join
limitdefaults to 50 and must be 1–100. Error:Invalid "expand" limit: must be between 1 and 100. - There is no
pageSizemaximum. $regexmust be a string, at most 64 characters, and must not contain+,*, or{. Errors:Filter operator $regex requires a string,Filter operator $regex is too long,Filter operator $regex contains disallowed quantifiers.$optionsis only legal beside$regex.matchessends$regexplus$options: "i"and does not enforce the length or quantifier rules; the server does.- Blocked operators return
400Filter operator $… is not allowed:$where,$expr,$jsonSchema,$text,$function,$accumulator,$let,$map,$reduce. Any other$key outside the allow-list is rejected too. - Allowed on the
filterquery parameter, beyond the SDK table: logical$and,$or,$not, and field operators$elemMatch,$exists,$size,$all,$type, plus the operators in the table above. - Unknown
fields/selectnames:400Unknown field <name>orUnknown fields: a, b._idis always projected. - The joined collection uses its list endpoint. Disabled →
404Endpoint not found or disabled. Denied →403with the rule reason.