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"]}}

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 id present → 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.