Query: filter, sort, group and total Blocks
A query (DataQuery, @skaile/forge-editor/core) asks the Block index for the Blocks of one or several Block types: which Documents, which Properties, in which order, grouped how, totalled how. A query is data. It is a JSON object that a view stores in a Typed block's meta, that the client sends over HTTP, and that the document chat builds in a tool call. One evaluator, evaluateQuery, answers all of them.
| Piece | What it does | Port (capability) |
|---|---|---|
DataQuery, DataResult, QueryIssue | the query and its answer (@skaile/forge-editor/core) | none |
validateQuery(defs, query) | the issues of a query: unknown field, operator the type does not take, wrong value | none |
evaluateQuery(rows, defs, query, env) | filters, searches, sorts, groups, totals and pages the rows | none |
BlockIndex.query(query, scope, env, definitions?) | answers a query over the Documents in scope | index |
POST /api/editor/query | the same over HTTP | index |
EditorBackend['index'].query | the client call | index |
find_blocks with blockType and filter | the document chat lists Blocks by their Properties | index |
The query
{
"v": 1,
"source": { "kind": "blocks", "blockType": "task", "scope": { "documentIds": ["plan"] } },
"filter": { "and": [
{ "field": "status", "op": "eq", "value": "done" },
{ "field": "points", "op": "gte", "value": 3 }
] },
"search": { "q": "angebot" },
"sorts": [{ "field": "due", "dir": "asc", "nulls": "last" }],
"group": { "field": "$basket", "hideEmpty": true },
"aggregates": [{ "fn": "sum", "field": "points", "as": "total" }],
"select": ["status", "points", "due"],
"page": { "limit": 50 }
}
source.blockTypeis one Block type id or a list. A Typed block's type is itsblockType; any other Block has its node type (paragraph,heading, …).source.scopenarrows further:documentIds,tags(a row needs one of them),basket.subtreeOfis not applied: the index does not know the nesting, and the answer says so with a warning.filteris a condition{ field, op, value? }or{ and: [...] }/{ or: [...] }, nested as deep as needed. An emptyandholds, an emptyordoes not.search.quses the words rule of the workspace search: every word must match, as a whole word or as the start of one. Without asortsentry that decides, the best match comes first.sorts: the first entry that tells two rows apart wins. A row without a value sorts last;nulls: 'first'turns that around.grouppartitions the page intogroups. The groups follow the order of the property's options (astatusgrouptodo,doing,doneas declared), else the order of first appearance; the group without a value (key: null) is last unlesshideEmpty.aggregates(count,sum,avg,min,max) are computed over all matching rows, not just the page, and answered underas.selectlists the property keys and pseudo-fields to put inDataRow.values. Absent: every property of the Block.page.limitis at mostMAX_QUERY_ROWS(1000).nextCursorof an answer goes intopage.cursorof the next request. The cursor is the base64 of the row offset, so a page is not stable if Blocks change between two calls.
Fields
A field is a property key of the Block types in the source, or a pseudo-field:
| Pseudo-field | Value |
|---|---|
$title | the title of a Typed block, the text of a heading |
$tags | the tag names in the Block |
$type | the Block type id or node type |
$id | the block ID |
$doc | the Document ID |
$basket | the colour basket of a Typed block |
Property keys must not start with $; propertyDefinitionSchema refuses them.
Operators per property kind
Which operators, sort key, group key and aggregates apply is the property kind's entry in PROPERTY_KINDS (core/property-kinds.ts); the query does not branch on the type itself.
| Kind | Operators | Aggregates |
|---|---|---|
text, textarea, url | eq ne contains startsWith isEmpty isNotEmpty | count |
number | eq ne lt lte gt gte isEmpty isNotEmpty | count sum avg min max |
date | eq ne before after on isEmpty isNotEmpty | count min max |
select, status, reference | eq ne hasAny hasNone isEmpty isNotEmpty | count |
multiselect, reference-list | hasAny hasAll hasNone isEmpty isNotEmpty | count |
checkbox | eq ne | count |
bibliography | contains isEmpty isNotEmpty | count |
derivation | eq ne contains isEmpty isNotEmpty | count |
source-anchor | eq ne lt lte gt gte contains isEmpty isNotEmpty | count min max (the page; contains reads the quoted text) |
Dates: on compares the calendar day in env.timeZone (a plain YYYY-MM-DD is that day whatever the time zone), before and after compare the instant (a plain day counts as its start in UTC). Text sorts by the collation of env.locale, numerically where it holds numbers (item 2 before item 10).
env (QueryEnv) carries locale, timeZone, now and me. The evaluator is pure: the same rows, query and env give the same answer.
Which Property definitions apply
evaluateQuery(rows, defs, …) types a field by defs. On the server, defs are the properties capability of the host's Block types (EditorServerOptions.blockTypes) that the query's source names, merged, the first definition of a key winning (queryDefinitionsOf in @skaile/forge-editor/server). A property that no Block type declares is not an error: the evaluator infers a kind from the values of the rows (a boolean is a checkbox, a number a number, a date-like text a date, other text text, a list of addresses a reference-list, a list of text a multiselect; the first value of a key decides). A key that no row has is an unknown-field. So a host with no blockTypes configured still answers queries over task Blocks, just without option order or status groups.
An error issue means no rows: the answer has rows: [], total: 0 and the issues. A warning (a select entry nobody has, subtreeOf) is answered along with the rows.
The port, the route and the client
BlockIndex.query(query, scope, env, definitions?) answers a query over the Blocks of the Documents in scope. source.scope.documentIds narrows the scope and never widens it (narrowScope). The memory adapter runs evaluateQuery over its rows. The drizzle adapter loads the rows of the source's Block types per Document with one SQL statement and evaluates in JS; a database-specific adapter may push parts of the filter into SQL as long as it gives the answer evaluateQuery gives (the BlockIndex contract in test/support/block-index-contract.ts checks this). A custom BlockIndex must implement query; the quickest way is evaluateQuery over byDocument.
POST /api/editor/query takes { query, env? } (QueryRequestBody; env has locale and timeZone, defaulting to the first Accept-Language tag Intl knows and UTC). It answers a DataResult for the Documents the user may read (readableDocumentsOf). 401 without a user, 400 for a body that is no query, 422 when the query has an error issue (the error's data lists the issues), 501 without the index port.
const result = await backend.index.query({
v: 1,
source: { kind: 'blocks', blockType: 'task' },
filter: { field: 'status', op: 'eq', value: 'done' },
group: { field: '$basket' },
})
for (const group of result.groups ?? []) console.log(group.key, group.rows.length)
EditorBackend.index.query?(query, { env?, signal? }) is optional: a backend without it leaves data views out. It rejects with BackendError (422, data = the issues) for an invalid query. The HTTP backend sends the browser's time zone unless env says otherwise.
The document chat
find_blocks (createWorkspaceTools()) gains two inputs:
blockTypelists the Blocks of that type (all of them without afilter);filteris aFilterNodeover their Properties, and needsblockType.
With query as well, only the hits of the search come back. Each hit then carries its values. A filter that does not fit the type comes back to the model as a sentence (Ungültige Abfrage: … naming the field and the operators the type takes), so it can correct the call. Without blockType the tool works as before: a search by words.
AI proposals and the export check
Show unchecked AI results as „KI-Vorschlag“ in the Freigabestatus, list what to look at before a Typed block is exported, and register your own check source.
Edges: backlinks and authored connections
Which Blocks point at a Block: the Edges the Block index projects from references, Derivations, embeds and tags, the ones a person adds in the inspector, the edges route and the drizzle table.