Queries
store.query(type) starts a query over a type and its subtypes, and store.queryAll() one over all
nodes. The builder is the C# one and writes the same query text. Nothing is sent until the query
runs. Queries are immutable, so you can keep a base query and extend it:
const inStock = store.query(Product).where(n => n.InStock.isTrue());
const cheap = await inStock.where(n => n.Price.lt(1000)).orderBy(n => n.Price).execute();
const total = await inStock.count();Conditions
where() takes a function of n, which has a handle for each property. Which methods a handle has
depends on the property's type, so n.Price.startsWith('1') does not compile:
| Property | Methods |
|---|---|
string |
eq, ne, oneOf, gt, gte, lt, lte, between, startsWith, contains, matchesSearch |
| numbers | eq, ne, oneOf, gt, gte, lt, lte, between |
Date |
the same, and inRange(from, to); a date may also be ISO text |
boolean |
eq, ne, isTrue(), isFalse() |
| arrays | contains(element) |
GeoCoordinate |
isWithin(center, meters) |
One<T> |
is(id): the end points to this node |
Many<T> |
has(id): this node is among the related ones |
Ref<T>, Refs<T> |
eq(id), ne(id), oneOf(ids); contains(id) |
$createdUtc, $changedUtc |
as a Date, on every type |
startsWith and contains compare ordinally, so case matters. matchesSearch needs
IndexedByWords on the property.
Combine conditions with db.and, db.or and db.not, or with the same methods on a condition.
Chained where() calls are combined with AND:
const { and, or, not } = db;
const found = await store.query(Product)
.where(n => or(n.Price.lt(500), and(n.InStock.isTrue(), n.Name.startsWith('Rain'))))
.where(n => not(n.Sku.startsWith('OLD-')))
.where(n => n.$createdUtc.gt(new Date('2026-01-01T00:00:00Z')))
.execute();Values never go into the query text. Each is a parameter typed by its property, so a decimal is
compared with a decimal and a date with a DateTime. eq(null) compares with null.
db.raw(text) writes a condition in the query language itself, with n as the lambda variable. Its
values are literals, which cannot express every type: a decimal compared with 100 there is compared
with 0. Keep values in the builder's conditions, or use rawQuery (below).
Relations, types, ids and search
| Method | Keeps the rows that |
|---|---|
whereIds(ids) |
have these ids |
whereIn(n => n.Prop, values) |
have one of the values; cheaper than oneOf for a long list |
whereTypes(types, includeDescendants = true) |
are of these types |
whereRelates(n => n.End, id), whereNotRelates(…) |
are, or are not, related to the node through the end |
whereRelatesAny(n => n.End, ids) |
are related to any of the nodes |
whereSearch(text, options?) |
match a text search over the node's combined text index |
const news = await store.query(Section).where(n => n.Name.eq('News')).first();
const inNews = await store.query(Article).whereRelates(n => n.Parent, news!.$id).execute();
const pages = await store.query(Page).whereTypes([Article, Product]).count();The search options are semanticRatio (0 for keywords only, 1 for meaning only, or a blend),
minimumVectorSimilarity, orSearch (any of the words rather than all) and maxWordsEvaluated.
Search needs the text index that the model sets up, as the Relatude.DB manual explains.
Includes and traversal
include() loads a relation end's related nodes with each row, and adds them to the row's type:
Node<X> | null for a "one" end, Node<X>[] for a "many" end.
const products = await store.query(Product)
.include(n => n.Parent) // p.Parent: Node<IPage> | null
.execute();
const sections = await store.query(Section)
.include(n => n.Children, {
top: 5, // at most five per section
where: c => c.ShowInMenu.isTrue(), // filters the children, not the sections
then: c => c.include(x => x.Children), // and the children's own children
})
.execute();traverse() walks a relation breadth-first from the rows. The query becomes one over the nodes it
reaches, from minLevel (1 by default) to maxLevel steps away. It also takes direction
('default', 'reverse' or 'both') and maxVisited.
const below = await store.query(Section).whereIds(shopId)
.traverse(n => n.Children, { maxLevel: 2 }) // children and grandchildren
.where(n => n.ShowInMenu.isTrue()) // now a query over IPage
.execute();Order and pages
orderBy(n => n.Prop, 'asc' | 'desc') sorts, and orderByDescending(n => n.Prop) is short for
'desc'. Chain them for tie-breakers. page(index, size) returns one page, and totalCount still
counts every row. take and skip work too. The server refuses more rows at once than
CmsApiOptions.MaxPageSize, which is 10 000 by default.
const norwegian = store.withContext({ culture: 'nb-NO', cultureFallback: true });
const editing = store.withContext({ edit: true }); // unpublished nodes tooReadContext |
Means |
|---|---|
culture |
read and write in this culture; the database's default when left out |
cultureFallback |
fall back to another culture for nodes with no version in this one |
edit |
edit view: unpublished nodes too, with edit-view access applied as well as read access |
hidden |
nodes of hidden types too |
revisions |
{ nodeId: revisionId }: which revision to read and write of a node with several |
collection |
the site that new nodes are inserted in |
links |
'stored': HTML and Markdown with their links as stored # tokens, which contentLinks() resolves |
A single query can do the same with culture(code), cultureFallback() and hidden().
Results
What a query returns depends on the method it ends with:
| Ends with | Returns |
|---|---|
execute() |
items, totalCount, count, pageIndex, pageSize, isLastPage, durationMs |
first() |
the first row, or null |
count() |
how many rows match, counted in the indexes |
sum(n => n.Prop) |
the sum of a number property over the rows |
selectId().execute() |
the ids, without reading the nodes |
select(n => ({ … })).execute() |
plain objects with the properties named, typed from the handles |
search(text, options?).execute() |
ranked hits, each with its node, score and a text sample |
facets() … .execute() |
the rows, and facets for a filter sidebar |
pivot() … .execute(), groupBy(…) … .execute() |
groups on rows and columns, with measures in every cell |
shortestPath(n => n.End, from, to) |
{ found, ids, items } |
const rows = await store.query(Product).select(n => ({ name: n.Name, price: n.Price })).execute();
// rows.items: { name: string; price: number }[]
const ranked = await store.query(Page).search('rain jacket', { semanticRatio: 0.5 }).page(0, 10).execute();
for (const hit of ranked.hits) console.log(hit.score, hit.node.Name, hit.sample?.fragments);
const sidebar = await store.query(Product).facets()
.addValueFacet(n => n.InStock)
.addRangeFacet(n => n.Price)
.setFacetValue(n => n.InStock, true) // the visitor's choice
.page(0, 20)
.execute();
// sidebar.items: the rows; sidebar.facets: each with values { value, displayName, count, selected }
const byStock = await store.query(Product).groupBy(n => n.InStock).addCount().addAverage(n => n.Price, 'avg').execute();A facet value is a token: to select it, pass its value back unchanged, with value2 for a range.
A pivot takes rows and columns with addRow… and addColumn… methods, a date by interval
(addColumn(n => n.PublishedUtc, 'Month')), and measures with addCount, addCountDistinct,
addSum, addAverage, addMin and addMax.
What the builder writes
toRequest(schema) shows the query text and its parameters without running the query:
const schema = await store.schema();
const request = store.query(Product).where(n => n.Price.lte(500)).orderBy(n => n.Name).toRequest(schema);
// { query: 'IProduct.Where(n => (n.Price <= @P0)).OrderBy(n => n.Name)',
// parameters: [{ name: '@P0', type: 'decimal', value: 500 }] }rawQuery() runs a query written in the query language, with typed parameters. It returns the
server's kind and result, as chapter 17 lists them:
const r = await store.rawQuery('IProduct.Where(n => n.Price < @P0).Count()', [{ name: '@P0', type: 'decimal', value: 1000 }]);
// { kind: 'value', value: 2 }