Source
Every query starts with from(), which specifies the node kind or explicit list of kinds to query
and assigns an alias for referencing it throughout the query.
Basic Usage
Section titled “Basic Usage”const results = await store .query() .from("Person", "p") // Start from Person nodes, alias as "p" .select((ctx) => ctx.p) .execute();Parameters
Section titled “Parameters”.from(kind, alias, options?)| Parameter | Type | Description |
|---|---|---|
kind |
string |
The node kind to query (must exist in your graph definition) |
alias |
string |
A unique identifier for referencing this node in the query |
options.includeSubClasses |
boolean |
Include nodes of subclass kinds (default: false) |
Multiple kinds in one query
Section titled “Multiple kinds in one query”Pass a nonempty list to scan several registered kinds in one SQL query:
// Person and Company both declare a string name property.const page = await store .query() .from(["Person", "Company"], "entity") .whereNode("entity", (entity) => entity.name.startsWith("A")) .orderBy("entity", "name") .select((ctx) => ctx.entity) .paginate({ first: 20 });The source scans exactly these kinds. Repeated kinds are normalized, an empty list is rejected,
and unknown kinds throw KindNotFoundError. List sources do not accept an options bag or expand
subclasses. For a reusable list, preserve its nonempty tuple type with as const.
Predicates and database expressions expose compatible properties shared by every selected kind,
plus system fields such as id and kind. A property present on only one kind, or declared with
incompatible types across kinds, cannot be used as a shared query field. Full-node selections
retain each kind’s properties as a discriminated union; check the returned node’s kind before
reading kind-specific properties.
Multi-kind sources use the existing graph scope, visibility, temporal, identity, traversal,
aggregation, and prepared-query paths. They also compose with batchOnce() like single-kind
sources. Use them for a single ordered stream, such as a directory of people and companies.
For separate result lists with independent filters and limits, use
one-statement batches instead.
Node identity across kinds is (kind, id). Cursor pagination and streaming append missing
kind ASC and id ASC keys to make page boundaries deterministic, including when two kinds
contain the same ID. For queries that fan out through traversals, also order by the traversed row
identities when one source node can produce multiple rows. The input list’s order does not order results:
specify orderBy().
Restart saved multi-kind cursors from older versions if they lack the kind identity column.
This is one source scan, but it does not guarantee an index-only plan or eliminate a database sort; verify the execution plan for your selected kinds, filters, and ordering.
Aliases
Section titled “Aliases”The alias is used throughout the query to reference the node:
const results = await store .query() .from("Person", "person") .whereNode("person", (p) => p.status.eq("active")) // Reference in filter .orderBy("person", "name", "asc") // Reference in ordering .select((ctx) => ({ name: ctx.person.name, // Reference in selection email: ctx.person.email, })) .execute();Aliases must be unique within a query. TypeScript enforces this at compile time:
store .query() .from("Person", "p") .traverse("worksAt", "e") .to("Company", "p") // TypeScript error: alias "p" already in useSubclass Expansion
Section titled “Subclass Expansion”If your ontology defines subclass relationships, you can query a parent kind and include all subclasses:
// Graph definition with subclass relationships:// subClassOf(Podcast, Media)// subClassOf(Article, Media)// subClassOf(Video, Media)
// Query only exact Media nodes (default behavior)const exactMedia = await store .query() .from("Media", "m") .select((ctx) => ctx.m) .execute();
// Query Media and all subclassesconst allMedia = await store .query() .from("Media", "m", { includeSubClasses: true }) .select((ctx) => ({ kind: ctx.m.kind, // "Media" | "Podcast" | "Article" | "Video" title: ctx.m.title, })) .execute();When includeSubClasses: true:
- Results include nodes of the specified kind AND all subclass kinds
- The
kindfield in results reflects the actual node kind - All properties common to the parent kind are accessible
Runtime-declared kinds
Section titled “Runtime-declared kinds”from() requires kind to be a compile-time literal in your graph
definition. For kinds added at runtime via graph
extensions, use fromDynamic():
// "Paper" was added by store.evolve(extension), so it isn't in the// compile-time graph type. fromDynamic accepts arbitrary string kinds.const recent = await store .query() .fromDynamic("Paper", "p") .whereNode("p", (p) => p.field("year").number().gte(2020)) .select((ctx) => ctx.p) .execute();The kind is validated against the registry — typos throw
KindNotFoundError instead of silently producing an empty query.
The alias’s predicate accessor is DynamicNodeAccessor, which exposes
schema properties through a .field(name) discriminator —
.field("year").number().gte(2020) for type-narrow predicates,
.field("year").eq(...) directly for BaseFieldAccessor methods. See
Traverse ▸ The .field()
discriminator for the full
surface.
fromDynamic mixes freely with typed traverse / to and the dynamic
siblings. See graph extensions ▸ Querying extension
kinds for the full story.
Passing Store-issued runtime-kind evidence instead of a string narrows the alias to the graph-extension definition, so ordinary typed property access is available. String inputs retain the discriminator-based dynamic surface.
Return Type
Section titled “Return Type”from() returns a QueryBuilder that provides access to all query methods:
- Filter -
whereNode(),whereEdge() - Traverse -
traverse(),optionalTraverse() - Shape -
select(),aggregate() - Order -
orderBy(),limit(),offset() - Aggregate -
groupBy(),groupByNode() - Temporal -
temporal() - Compose -
pipe()