Skip to content

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.

const results = await store
.query()
.from("Person", "p") // Start from Person nodes, alias as "p"
.select((ctx) => ctx.p)
.execute();
.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)

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.

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 use

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 subclasses
const 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 kind field in results reflects the actual node kind
  • All properties common to the parent kind are accessible

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.

from() returns a QueryBuilder that provides access to all query methods:

  • Filter - Reduce results with whereNode()
  • Traverse - Navigate to related nodes
  • Shape - Define output with select()