Querying
Standard CRUD operations, field projection, filtering, search, sorting, pagination, batch operations, and nested collections.
Standard CRUD operations
Every ModelService exposes the same set of operations. Module-specific docs list only the fields and examples; the semantics below are universal.
| Operation | Description |
|---|---|
find |
List records with filtering, sorting, and pagination |
findOneById |
Fetch a single record by primary key |
create |
Insert a new record |
updateOneById |
Partial update (PATCH semantics) — only specified fields change |
replaceOneById |
Full replace (PUT semantics) — entire document is replaced |
update |
Batch partial update in a single transaction (all-or-nothing) |
deleteOneById |
Delete one record, returns its final state |
delete |
Batch delete in a single transaction (all-or-nothing) |
HTTP mapping
On services that publish an HTTP transport, the operations map to REST verbs:
| Operation | Method | Path |
|---|---|---|
find |
GET |
/v1/<module>/<service> |
findOneById |
GET |
/v1/<module>/<service>/:id |
create |
POST |
/v1/<module>/<service> |
updateOneById |
PATCH |
/v1/<module>/<service>/:id |
replaceOneById |
PUT |
/v1/<module>/<service>/:id |
update (batch) |
PATCH |
/v1/<module>/<service> |
deleteOneById |
DELETE |
/v1/<module>/<service>/:id |
delete (batch) |
DELETE |
/v1/<module>/<service> |
Common parameters
fields — Field projection
Available on all operations. Restricts which fields appear in the response. When omitted, the server returns all readable fields.
{
"fields": ["id", "created_at", "enc_title"]
}
Nested fields use dot notation:
{
"fields": ["id", "task_user_role.user_id", "task_user_role.wrapped_content_key"]
}
filter — Record filtering
Available on find. Object of { "field": { "$op": value } }. Multiple operators on the same field are AND-combined. Multiple fields are AND-combined.
Comparison operators
| Operator | Description | Example |
|---|---|---|
$eq |
Equal | {"completed": {"$eq": true}} |
$neq |
Not equal | {"priority": {"$neq": 0}} |
$gt |
Greater than | {"due_at": {"$gt": "2026-01-01"}} |
$gte |
Greater than or equal | {"priority": {"$gte": 2}} |
$lt |
Less than | {"due_at": {"$lt": "2026-06-01"}} |
$lte |
Less than or equal | {"priority": {"$lte": 1}} |
Set operators
| Operator | Description | Example |
|---|---|---|
$in |
Value in set | {"priority": {"$in": [2, 3]}} |
$nin |
Value not in set | {"priority": {"$nin": [0]}} |
Existence operator
| Operator | Description | Example |
|---|---|---|
$ex |
Field is not null (true) or is null (false) |
{"due_at": {"$ex": true}} |
Text operators (string fields only)
| Operator | Description | Example |
|---|---|---|
$ct |
Contains (case-insensitive substring) | {"name": {"$ct": "work"}} |
$nct |
Does not contain | {"name": {"$nct": "test"}} |
$sw |
Starts with | {"name": {"$sw": "Project"}} |
$nsw |
Does not start with | {"name": {"$nsw": "tmp"}} |
$re |
Matches regular expression | {"name": {"$re": "^[A-Z]"}} |
$nre |
Does not match regular expression | {"name": {"$nre": "^temp"}} |
Array operators (array fields only)
| Operator | Description | Example |
|---|---|---|
$has |
Array contains element | {"tags": {"$has": "important"}} |
$hasall |
Array contains all elements | {"tags": {"$hasall": ["a","b"]}} |
$hasany |
Array contains any of the elements | {"tags": {"$hasany": ["a","b"]}} |
search — Full-text search
Available on find. Case-insensitive substring match across all string fields on the model.
{
"search": "quarterly report"
}
sort — Result ordering
Available on find. Array of { "field": "...", "dir": "asc" | "desc" }. Direction defaults to "asc" if omitted.
{
"sort": [
{"field": "due_at", "dir": "asc"},
{"field": "priority", "dir": "desc"}
]
}
offset / limit — Pagination
Available on find.
| Parameter | Type | Default | Max | Description |
|---|---|---|---|---|
offset |
integer |
0 |
— | Number of records to skip |
limit |
integer |
20 |
100 |
Page size |
The response always includes total — the count of records matching the filter before pagination:
{
"result": {
"items": [ ... ],
"total": 247
}
}
To fetch all records, iterate with increasing offset until offset >= total.
Batch operations
Batch update
{
"method": "<module>.<service>.update",
"params": {
"items": [
{"id": "1001", "document": {"field": "new_value"}},
{"id": "1002", "document": {"field": "new_value"}}
]
}
}
Response
{
"result": {
"affected": 2,
"items": [
{"id": "1001", "field": "new_value"},
{"id": "1002", "field": "new_value"}
]
}
}
All items are updated in a single transaction. If any record fails (not found, validation error), the entire batch rolls back.
Batch delete
{
"method": "<module>.<service>.delete",
"params": {
"items": [
{"id": "1001"},
{"id": "1002"}
]
}
}
Response
{
"result": {
"affected": 2,
"items": [
{"id": "1001"},
{"id": "1002"}
]
}
}
Same transactional guarantee — all-or-nothing.
Nested collections
Some fields contain nested collections (subtasks, attachments, access roles, tags, etc.). These are returned as arrays inside the parent record and can be modified through the parent’s create / updateOneById operations.
Reading nested collections
Request specific nested fields via dot notation in fields:
{
"fields": [
"id", "enc_title",
"task_subtask.id", "task_subtask.completed", "task_subtask.enc_title"
]
}
Response
{
"id": "1001",
"enc_title": "<base64>",
"task_subtask": [
{"id": "2001", "completed": false, "enc_title": "<base64>"},
{"id": "2002", "completed": true, "enc_title": "<base64>"}
]
}
Writing nested collections
Pass the full desired state of the array. The server compares it against the current state:
{
"method": "<module>.<service>.updateOneById",
"params": {
"id": "1001",
"document": {
"task_subtask": [
{"id": "2001", "completed": true, "enc_title": "<base64>"},
{"completed": false, "enc_title": "<base64>"}
]
}
}
}
- Item with
idpresent → updated - Item without
id→ created - Item missing from array → deleted
M2M tag arrays
Pure M2M junction fields like task_tag, note_tag, secret_tag, photo_tag, and file_tag use flat ID arrays:
{
"task_tag": ["3001", "3002"]
}
The array represents the complete desired state — IDs present are assigned, IDs absent are removed.
Junction inlines with extra fields
Some junction tables carry additional data (e.g. wrapped keys for sharing). These use object arrays instead of flat IDs:
{
"contact_group": [
{"id": "5001", "group_id": "3001", "group_wrapped_key": null},
{"group_id": "3002"}
]
}
Items with id are updated, items without id are created, items missing from the array are deleted — same semantics as regular nested collections. photo_album follows the same pattern with album_id and album_wrapped_key.