Relatude Help

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).

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.

Culture, edit view and hidden types

A store reads in a context: a culture, and whether it sees what an editor sees. withContext() returns the same store in another context, sharing the connection and the schema:

const norwegian = store.withContext({ culture: 'nb-NO', cultureFallback: true });
const editing = store.withContext({ edit: true });         // unpublished nodes too
ReadContext 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 }