Docs

Typed Object Queries & Hooks

@sixb/client/query gives browser code the same fluent object-query builder the server runtime uses, wired to the HTTP API. Reach for it when you query objects from a custom app or React frontend and want compile-time-checked property names and predicate values.

objects(Type) builds the identical query IR as the runtime and runs it through the generated SDK, so auth and CSRF come from your existing client config. React apps add TanStack Query hooks (@sixb/client/hooks) keyed on the normalized query IR.

For the full builder reference — filters, traversal, search, ordering — see /objects/querying. For SDK setup, see /client.

Builder over HTTP

objects(Type).query() returns the builder. Import your ontology types directly; there is no registration step. Property names and predicate values are type-checked at compile time, and the server validates every query against the registered ontology.

TS
import { objects } from "@sixb/client/query"
import { Project } from "../ontology/project"

const { objects: rows } = await objects(Project)
  .query()
  .where((project) => project.p.status.eq("active"))
  .orderBy(Project.p.deadline, "asc")
  .limit(20)
  .list()

Terminals run through the object-query routes via the generated SDK:

TerminalRouteReturns
.list()POST /api/objects/query{ objects, hasMore, total?, nextPageToken? }
.first()POST /api/objects/queryfirst row or null
.count()POST /api/objects/query/countnumber
.exists()POST /api/objects/query/existsboolean
.facets([…])POST /api/objects/query/facetsObjectQueryFacetResult[]

Each row carries primaryId, objectTypeId, properties, and Date-typed createdAt and updatedAt. Date predicate values survive the JSON wire format. Rows from an .expand(...)ed query also carry a typed links entry (with optional linkProperties) per expanded link — see Expanding links.

validate() and explain() need ontology access and are server-side only — they are not on the client builder.

Browser-safe ontology imports

Ontology files that browser code imports must define types via @sixb/core/ontology — the same defineObjectType, prop, link, and friends as the @sixb/core root, but without pulling the server runtime into the bundle.

TS
import { defineObjectType, prop } from "@sixb/core/ontology"

.expand(link) attaches an outgoing link's target objects to each row under .links, without changing the result type — the client counterpart to the runtime builder (see expanding links for the full API). A "one" link resolves to Target | null, a "many" link to Target[]; bound a "many" expansion with { limit, orderBy } and nest a callback for deeper hops.

TSX
const { objects: rows } = await objects(Invoice)
  .query()
  .where((invoice) => invoice.p.status.eq("overdue"))
  .expand(Invoice.l.customer, (customer) => customer.expand(Customer.l.region))
  .list()

const region = rows[0]?.links.customer?.links.region

Precise expand and link-target types depend on a generated type manifest. sixb typegen writes .sixb/types/ontology.d.ts, a module augmentation of SixbObjectTypeMap (from @sixb/core/ontology) that maps object-type ids like "Customer" to their exported type. sixb dev, sixb build, and sixb check regenerate it automatically; run sixb typegen before a bare tsc (the scaffold's typecheck script does exactly this). Without the manifest, string-target links and .expand() still work at runtime, but their row types degrade to a loose base shape.

React hooks

Hooks come from @sixb/client/hooks and take a built query directly — any query from the docs, the server runtime, or an event handler works unchanged. They key the cache on the normalized query IR, so identical queries share cache entries and inline builders are safe to construct on every render.

HookQuery terminalResult type
useObjectsQuerylist()ListResult<Row>
useObjectsInfinitepaged list()InfiniteData<…>
useObjectsCountcount()number
useObjectsExistsexists()boolean
useObjectsFacetsfacets()ObjectQueryFacetResult[]
TSX
import { useObjectsFacets, useObjectsQuery } from "@sixb/client/hooks"
import { objects } from "@sixb/client/query"
import { Project } from "../ontology/project"

function ProjectList() {
  const projects = useObjectsQuery(
    objects(Project)
      .query()
      .where((project) => project.p.status.eq("active"))
      .orderBy(Project.p.deadline, "asc")
      .limit(20)
  )

  // Group counts by status, for filter pills.
  const statusFacets = useObjectsFacets(objects(Project).query(), [
    { property: Project.p.status, limit: 10 },
  ])
  const buckets = statusFacets.data?.[0]?.buckets ?? []

  if (projects.isLoading) return <p>Loading…</p>
  if (projects.isError) return <p>Failed to load.</p>
  return <ul>{projects.data?.objects.map((p) => <li key={p.primaryId}>{p.properties.name}</li>)}</ul>
}

useObjectsFacets takes the query plus an array of { property, limit } requests; only properties with query.facet are facetable (Invoice.status, Project.status).

useObjectsInfinite threads nextPageToken automatically and requests pages with includeTotal: false:

TSX
import { useObjectsInfinite } from "@sixb/client/hooks"

const pages = useObjectsInfinite(objects(Project).query().search("dashboard"), {
  pageSize: 50,
})

Hook options

useObjectsQuery, useObjectsCount, useObjectsExists, and useObjectsFacets accept a final options argument with common TanStack passthroughs:

OptionTypeNotes
enabledbooleangate the query
staleTimenumberms before data is stale
gcTimenumberms before cache eviction
refetchIntervalnumber | falsepolling interval
refetchOnWindowFocusboolean
retryboolean | number

useObjectsInfinite takes { pageSize } plus enabled, staleTime, gcTime, and refetchInterval. For anything beyond these passthroughs (select, placeholderData, …), compose the option factories below with useQuery directly.

Shared queries

Queries are plain values, so define them once in a module and refine them at the call site. Each refinement (.where(…), .limit(…)) returns a new query value, and the cache key follows the resulting IR.

TSX
// queries/projects.ts
import { objects } from "@sixb/client/query"
import { Project } from "../ontology/project"

export const openProjects = objects(Project)
  .query()
  .where((project) => project.p.status.in(["active", "paused"]))
  .orderBy(Project.p.deadline, "asc")

// component
const { data } = useObjectsQuery(openProjects.limit(50))
const { data: openCount } = useObjectsCount(openProjects)

Option factories

For router loaders, prefetching, SSR, or full TanStack control, use the option factories instead of the hooks. Each returns a TanStack queryOptions/infiniteQueryOptions object keyed on the normalized IR, so it shares cache entries with the matching hook.

FactoryPairs withArguments
objectQueryOptionsuseObjectsQuery(query, options?)
objectQueryCountOptionsuseObjectsCount(query)
objectQueryExistsOptionsuseObjectsExists(query)
objectQueryFacetsOptionsuseObjectsFacets(query, facets)
objectQueryInfiniteOptionsuseObjectsInfinite(query, { pageSize })
TSX
import { objectQueryOptions } from "@sixb/client/hooks"
import { useQuery } from "@tanstack/react-query"
import { openProjects } from "../queries/projects"

// Prefetch in a loader
await queryClient.prefetchQuery(objectQueryOptions(openProjects.limit(50)))

// Full TanStack control in a component
const { data } = useQuery({
  ...objectQueryOptions(openProjects.limit(50)),
  placeholderData: (prev) => prev,
})

Query Keys and Invalidation

The hooks export stable object-query keys and exact invalidation helpers for apps that need manual cache control:

TSX
import { invalidateObjectQuery, objectQueryKeys } from "@sixb/client/hooks"

await queryClient.invalidateQueries({
  queryKey: objectQueryKeys.count(openProjects),
  exact: true,
})

await invalidateObjectQuery(queryClient, openProjects.limit(50))

For action buttons, prefer useActionRunMutation({ invalidateOnCommit: true }). It waits for the terminal action run and invalidates action-run caches plus object-query caches when the run commits edits.

Transport overrides

Hooks execute the query IR through the global SDK client (client, exported from @sixb/client). Wrap a subtree in SixbProvider to override the transport — base URL, auth, fetch — for every hook beneath it. SixbProvider takes a hey-api Client instance:

TSX
import { client } from "@sixb/client"
import { SixbProvider } from "@sixb/client/hooks"

function App() {
  return (
    <SixbProvider client={client}>
      <ProjectList />
    </SixbProvider>
  )
}

A per-query client passed to objects(Type, { client }) applies to imperative calls like .list() and .count(), not to hooks — hooks always bind to the nearest SixbProvider client (or the global client).

TS
const rows = await objects(Project, { client }).query().list()

Errors

Validation and planning failures throw SixbQueryError, carrying the structured issues array from the route (unknown properties, wrong value types, unsupported traversal shapes, provider capability limits, …). Inside hooks the error surfaces on query.error / query.isError.

TS
import { objects, SixbQueryError } from "@sixb/client/query"

try {
  await objects(Project).query().where((p) => p.p.unknown.eq("x")).list()
} catch (error) {
  if (error instanceof SixbQueryError) {
    console.error(error.message, error.issues)
  }
}
MemberTypeDescription
messagestring[SixbClient] … prefixed message
issuesreadonly ObjectQueryIssue[]structured validation issues

Search docs

Search the documentation