Skip to content

Instance graphs (aboxGraph) Runtime

jt.aboxGraph(quads) builds an in-memory typed graph view over a set of ABox quads and exposes a fluent cursor API for navigating it. The mental model is an ORM for graphs: entities are typed by their registered schemas, associations derive from the TBox that those schemas already emit (rdfs:domain, rdfs:range, owl:InverseFunctionalProperty), and navigation is left-to-right dot-chaining over a lazy cursor with no query language, no nested calls, and no path strings to parse.

The example below runs for real in your browser. It is the verbatim source of the gate-verified examples/docs/advanced/106-abox-graph.ts. Edit it and press Run to execute it against the actual library and see the true output.

/**
 * Advanced Example 106 — typed ABox graph traversal (aboxGraph)
 *
 * Demonstrates the ORM-for-graphs cursor surface: build an in-memory
 * typed graph from ABox quads and navigate it via fluent dot-chaining —
 * forward (objects), inverse (subjects), schema (predicate/class), and
 * closure (subClassOf transitive).
 *
 * The bookstore scenario: customer Bastian Balthazar Bux places an order.
 * The Order carries customerId as a scalar foreign key — the same UUID that
 * inverseFunctionally identifies the Customer. The cursor resolves that FK
 * to the typed Customer without any explicit join. A Review carries bookIsbn —
 * a differently-named foreign key whose Isbn range backs Book's inverse-
 * functional identity — and resolves to the (subclassed) Book all the same.
 */

import {
  aboxFixtures,
  bookstoreEntities,
  CustomerSchema,
  OrderSchema,
  RareBookSchema,
  ReviewSchema
} from '../bookstore/index.js';
import type { QuadInterface } from '../../../src/interfaces/QuadInterface.js';

// ---------------------------------------------------------------------------
// Build the ABox quad set from the customer + order fixtures.
// The order carries customerId as a scalar FK ($ref to the CustomerId identity
// primitive). Connectivity to the customer depends on the inverse-functional
// identity resolution the cursor derives from the TBox — no schema change
// is required.
// ---------------------------------------------------------------------------

const ABOX_GRAPH_IRI = 'https://bookstore.example/graph/abox';

function bookstoreAboxQuads(): QuadInterface[] {
  const quads: QuadInterface[] = [];

  quads.push(...bookstoreEntities.toQuads(
    CustomerSchema,
    bookstoreEntities.instantiate(CustomerSchema, aboxFixtures.customer),
    { 'graphIri': ABOX_GRAPH_IRI }
  ));
  quads.push(...bookstoreEntities.toQuads(
    OrderSchema,
    bookstoreEntities.instantiate(OrderSchema, aboxFixtures.order),
    { 'graphIri': ABOX_GRAPH_IRI }
  ));
  // A RareBook (subclass of Book) and a Review that references it via bookIsbn —
  // a differently-named FK that resolves through Book's inverse-functional isbn.
  quads.push(...bookstoreEntities.toQuads(
    RareBookSchema,
    bookstoreEntities.instantiate(RareBookSchema, aboxFixtures.rareBook),
    { 'graphIri': ABOX_GRAPH_IRI }
  ));
  quads.push(...bookstoreEntities.toQuads(
    ReviewSchema,
    bookstoreEntities.instantiate(ReviewSchema, aboxFixtures.review),
    { 'graphIri': ABOX_GRAPH_IRI }
  ));

  return quads;
}

// Safe cast for lifted unknown instances — avoids any, stays strict.
function record(value: unknown): Record<string, unknown> {
  return value as Record<string, unknown>;
}

const quads = bookstoreAboxQuads();
const graph = bookstoreEntities.aboxGraph(quads);

// ---------------------------------------------------------------------------
// 1. Forward FK resolution: Order → Customer via customerId.
//    objects('customerId') resolves the scalar UUID foreign key to the typed
//    Customer instance through the inverse-functional identity index.
//    first() returns the first typed result or undefined if the cursor is empty.
// ---------------------------------------------------------------------------

const orderIris = graph.instances(OrderSchema.$id)
  .iris();
const orderIri = orderIris.at(0) ?? '';

const customer = record(graph.resource(orderIri).objects('customerId')
  .first());

console.log('FK resolved — customer name:', customer.name);
console.log('FK resolved — customer id  :', customer.customerId);

// ---------------------------------------------------------------------------
// 2. Inverse FK: Customer → all resources that reference it via customerId.
//    subjects('customerId') is the ^ (inverse predicate) direction.
// ---------------------------------------------------------------------------

const customerIris = graph.instances(CustomerSchema.$id)
  .iris();
const customerIri = customerIris.at(0) ?? '';

const referrerCount = graph.resource(customerIri).subjects('customerId')
  .count();

console.log('Inverse FK — referrer count:', referrerCount);

// ---------------------------------------------------------------------------
// 3. Object-property traversal: Order → its shippingAddress.
//    shippingAddress is a direct $ref object property (no FK resolution needed).
// ---------------------------------------------------------------------------

const address = record(graph.resource(orderIri).objects('shippingAddress')
  .first());

console.log('Object property — shipping city   :', address.city);
console.log('Object property — shipping country:', address.country);

// ---------------------------------------------------------------------------
// 4. Schema cursor — predicate range.
//    g.predicate('shippingAddress').range().one() reads the TBox to find the
//    rdfs:range class. The result is the typed Address schema ($id field).
//    Note: array-property ranges yield rdf:List (the RDF collection type);
//    scalar object properties yield the target class directly.
// ---------------------------------------------------------------------------

const rangeSchema = record(graph.predicate('shippingAddress').range()
  .one());

console.log('Schema cursor — range $id:', rangeSchema.$id);

// ---------------------------------------------------------------------------
// 5. Differently-named FK across a shared identity range: Review → Book.
//    Review.bookIsbn is NOT named `isbn`, but its Isbn range is the same
//    primitive that backs Book's inverse-functional `isbn` identity — so the
//    cursor resolves bookIsbn to the Book it identifies, with no join and no
//    matching key name. The resolved Book is actually a RareBook (a subclass);
//    subclass instances inherit the parent's identity, so resolution still hits.
// ---------------------------------------------------------------------------

const reviewIri = graph.instances(ReviewSchema.$id)
  .iris()
  .at(0) ?? '';

const book = record(graph.resource(reviewIri).objects('bookIsbn')
  .first());

console.log('Differently-named FK — review→book title:', book.title);
console.log('Differently-named FK — review→book isbn :', book.isbn);

// ---------------------------------------------------------------------------
// 6. Schema cursor — transitive subClassOf.
//    RareBook → PrintBook → Book (two hops). The transitive walk bubbles up
//    through the entire superclass chain.
// ---------------------------------------------------------------------------

const transitiveSupers = graph.class('urn:bookstore:RareBook')
  .subClassOf({ 'transitive': true })
  .iris();

console.log('Transitive subClassOf includes Book:', transitiveSupers.includes('urn:bookstore:Book'));
console.log('Transitive superclasses:', transitiveSupers.join(', '));
Output
Press Execute to run this example against the real library.

Concepts

Quads-in, typed graph out

Pass any QuadInterface[] to jt.aboxGraph(quads). The quads come from jt.toQuads(), from an external n3 parser, from EYE-reasoner output, or from any RDF/JS-compatible source. The graph is indexed once at construction time; subsequent cursor calls are in-memory set operations.

The same TBox the registry already holds is unioned into the graph view automatically. Schema associations (rdfs:domain, rdfs:range, rdfs:subClassOf, inverse-functional identity) are read from there; no separate configuration is required.

The cursor

Every entry point returns a Cursor, a lazy, typed selection of resources. Cursors compose via dot-chaining. Terminals (one, first, all, iris, count, some, none) materialize the selection.

g.resource(iri)           → Cursor over { iri }
g.instances(classIri)     → Cursor over all resources of rdf:type classIri

Navigation chains (Cursor → Cursor):

MethodDirectionNotes
.objects(predicate)Forward: each resource's objects via predicateFK-resolved via inverse-functional identity
.subjects(predicate)Inverse: resources that point at each resourceThe ^predicate direction
.ofType(classIri)Filter by rdf:type
.where(fn)Filter by typed JS predicate over the lifted instance
.having(predicate, value)Match a specific value
.closure(predicate)Transitive hop (p+/p* bounded BFS)
.subgraph(depth)Bounded N-hop neighbourhood expansion

Set operations and modifiers (Cursor → Cursor):

Method
.union(cursor)Set union
.intersect(cursor)Set intersection
.distinct()Deduplicate
.orderBy(compareFn)Sort by typed instances
.limit(n)Bound the result set

Terminals (Cursor → values):

MethodReturns
.one()Single typed instance; throws CURSOR_CARDINALITY if 0 or >1
.first()First typed instance, or undefined if empty
.all()Typed instance array (alias: .resources())
.iris()The underlying IRI strings
.count()Number of selected resources
.some()true if count > 0
.none()true if count === 0

Schema cursors

The same cursor vocabulary exposes the TBox. Schema cursors operate over classes and predicates rather than instances.

g.predicate(name).domain()                     → Cursor over rdfs:domain class(es)
g.predicate(name).range()                      → Cursor over rdfs:range class / datatype
g.class(classIri).subClassOf()                 → Direct superclasses
g.class(classIri).subClassOf({ transitive: true })  → Full transitive closure upward
g.class(classIri).properties()                 → Predicate IRIs whose domain is this class

Predicates are addressable by authored property name ('shippingAddress'), by IRI, or by CURIE. PredicateResolver maps names to their flat predicate IRI automatically.

Foreign-key resolution

Order.customerId carries the same UUID type ($ref: CustomerId) that Customer.customerId is declared as owl:InverseFunctionalProperty. The cursor reads that TBox declaration and resolves the scalar FK to the typed Customer at traversal time, with no extra schema authoring and no toQuads change.

.objects('customerId') follows the FK forward to the Customer. .subjects('customerId') is the inverse: all resources that reference a given Customer via that predicate.

A foreign key resolves whenever its range primitive backs an owl:InverseFunctionalProperty identity on a target class, even when the key is named differently from the identity property. Review.bookIsbn and OrderLine.bookIsbn (range Isbn) resolve to the Book identified by its inverse-functional isbn (also range Isbn); subclass-typed instances (a RareBook) inherit their parent's identity. A property whose range backs no declared identity stays a literal value. (One constraint: array-property range() yields the RDF collection type rdf:List, since the item type is in the array encoding rather than a single rdfs:range triple; use a scalar object property for schema-cursor range demos.)

Multi-hop navigation

Multi-hop is fluent dot-chaining, read left to right:

ts
// Order → its OrderLines → the Books they reference (bookIsbn FK resolved), no DSL:
g.resource(orderIri).objects('orderLines').objects('bookIsbn').all();

// Everything that references this Customer:
g.resource(customerIri).subjects('customerId').all();

Result typing

Terminals call fromQuads internally on the resolved IRI set and return unknown. Cast via a Record<string, unknown> helper to stay strict (no any):

ts
function record(value: unknown): Record<string, unknown> {
  return value as Record<string, unknown>;
}

const name = record(cursor.one())['name'];

Scope

aboxGraph is an in-memory read-only cursor over projected quads. It is not a persistent store, not a SPARQL engine, and not a reasoner. For multi-variable joins, GROUP-BY, OPTIONAL, or full SPARQL property paths, pass the standard RDF/JS quads to a dedicated engine (n3.js, Comunica). The cursor is the direct path for typed instance navigation where the schemas already carry the associations.

Released under the MIT License.