Links
Links describe how objects relate: a project is delivered for a customer, led by one employee, and
staffed by many. Declare links on the source object type with link(...),
then write and traverse them through the typed API.
Use links for relationships your app needs to navigate, query, or display. For values that live on a single object, use properties instead.
Declare a link
Add a links array to defineObjectType. Each entry connects this object type (the source) to a
target object type.
import { defineObjectType, link, prop, stringEnum } from "@sixb/core/ontology"
import { Customer } from "./customer"
import { Employee } from "./employee"
export const Project = defineObjectType({
id: "Project",
name: "Project",
properties: [
prop("id", "string", { required: true, primary: true }),
prop("name", "string", { required: true }),
prop("status", stringEnum(["draft", "active", "paused", "completed", "cancelled"])),
],
links: [
link("customer", Customer, { cardinality: "one" }),
link("lead", Employee, { cardinality: "one" }),
link("members", Employee, { cardinality: "many" }),
],
})
Link forms
link(id, target, options?)
link.ref(id, targetTypeId, options?)
link.self(id, options?)
link.any(id, options?)
For links to object types you can import directly, use link(...):
link("customer", Customer)
link("relatedTo", [Project, Task])
For id references, self-links, or intentionally open links, use the explicit helpers:
link.ref("customer", "Customer")
link.self("parent", { cardinality: "one" })
link.any("relatedTo", { cardinality: "many" })
Use link.ref(...) when you want to reference another object type by id instead of importing it.
Sixb's generated ontology type manifest lets typed client queries resolve those ids.
Use link.self(...) for recursive relationships such as folders, org charts, or threaded
comments. The target id is filled in from the object type that declares the link.
Use link.any(...) only for wildcard relationships that can point to any object type. Prefer a
specific target for relationships your app understands.
Link parameters
| Parameter | Required | Expected |
|---|---|---|
id | Yes | A stable relationship key, unique within the source object type |
target | Depends on helper | The object type or object type id this link can point to |
options | No | Metadata and relationship behavior |
options accepts:
| Option | Type | Meaning |
|---|---|---|
name | string | Display name. Defaults to the link id. |
description | string | Human-readable context for the relationship. |
cardinality | "one" | "many" | Whether each source links to one or many targets. |
properties | Property[] | Metadata stored on each relationship instance. |
Target forms
link(...) accepts an object type or an array of object types. link.ref(...) accepts an object
type id string or an array of ids. link.self(...) points back to the declaring object type.
link.any(...) creates a wildcard.
| Form | Example | Points to |
|---|---|---|
| Object type | link("customer", Customer) | One specific type |
| Object type id | link.ref("customer", "Customer") | One specific type, by id |
| Array of types | link("relatedTo", [Project, Task]) | Any of the listed types |
| Array of ids | link.ref("relatedTo", ["Project", "Task"]) | Any of the listed types |
| Self-link | link.self("parent") | The declaring object type |
| Wildcard | link.any("anything") | Any object type |
| Wildcard with options | link.any("anything", { cardinality: "many" }) | Any object type |
Passing an object type extracts its .id at build time. Prefer the object-type form when the
target can be imported — it keeps the target type-checked and powers typed traversal.
Cardinality
cardinality controls how many targets each source object can link to under this link id.
| Value | Meaning |
|---|---|
"one" | Each source links to at most one target — a project has one lead. |
"many" | The source can link to multiple targets — a project has many members. |
link("lead", Employee, { cardinality: "one" })
link("members", Employee, { cardinality: "many" })
Link properties
A link can carry metadata about the relationship itself — not about either object. Declare these
with prop(...), exactly like object properties.
import { defineObjectType, link, prop } from "@sixb/core/ontology"
import { Employee } from "./employee"
export const Project = defineObjectType({
id: "Project",
name: "Project",
properties: [prop("id", "string", { required: true, primary: true })],
links: [
link("members", Employee, {
cardinality: "many",
properties: [
prop("role", "string"),
prop("allocatedAt", "timestamp"),
],
}),
],
})
Good link properties are facts about the connection: a member's role on the project, when they
were allocatedAt, or a confidence score. If a link does not declare properties, writing a
link with properties is rejected with an OntologyValidationError.
Create and remove links
Once a type is registered, write links through the typed API on a byId(...) handle. The link
token comes from the object type as Type.l.<linkId>.
const projects = sixb.objects(Project)
// Link to a target by object reference
await projects.byId("proj-001").link(Project.l.members, {
objectTypeId: "Employee",
primaryId: "emp-014",
})
// With link properties
await projects.byId("proj-001").link(
Project.l.members,
{ objectTypeId: "Employee", primaryId: "emp-014" },
{ properties: { role: "backend", allocatedAt: new Date() } }
)
// Remove a link
await projects.byId("proj-001").unlink(Project.l.members, {
objectTypeId: "Employee",
primaryId: "emp-014",
})
Each link(...) call writes the complete property set for that relationship: properties you leave out
are cleared, and required link properties must be present every time.
The target is an ObjectRef ({ objectTypeId, primaryId }). TypeScript checks it against the
link's declared target, so linking members to a Customer is a compile error.
Sixb distinguishes an exact link edge, identified by (source, linkId, target), from a single-valued link slot, identified by (source, linkId). Cardinality-many overrides own edges; cardinality-one overrides own slots and select at most one target.
For a cardinality: "one" link, link(...) is not a setter. Calling link(...) with the same
target updates that relationship's properties, but calling it with a different target while one is
already present is rejected. Reassign by removing the current target, then linking the new one:
const [currentLead] = await sixb.objects(Project).byId("proj-001").listLinks(Project.l.lead)
if (currentLead) {
await projects.byId("proj-001").unlink(Project.l.lead, {
objectTypeId: currentLead.targetTypeId,
primaryId: currentLead.targetId,
})
}
await projects.byId("proj-001").link(Project.l.lead, {
objectTypeId: "Employee",
primaryId: "emp-027",
})
For cardinality: "many", unlink(...) removes one exact relationship row. For cardinality: "one", it clears the whole (source, linkId) slot: the target remains required as
the operation anchor, but a later projected target stays hidden instead of appearing beside the managed decision. When you only know the linkId, read the current target first with
listLinks(Project.l.lead).
Inside an action's staged .edits(...) facade the same link(...) and unlink(...) shapes apply,
plus resetLink(...) to forget a relationship the action added so a projected one shows again. An
action reassigns a cardinality-one link the same way — unlink the current target and link the new one
— and both edits land together. See Actions.
Writing a new link emits link.created; changing an existing link emits link.updated.
Removing one emits link.deleted. See Events.
List links from an object
listLinks(...) returns the relationship rows for an object, optionally filtered to one link token.
// All links from this project
const all = await sixb.objects(Project).byId("proj-001").listLinks()
// Only members links
const members = await sixb
.objects(Project)
.byId("proj-001")
.listLinks(Project.l.members)
Traverse links in queries
traverse(...) follows a link inside an object query and changes the
result type to the type on the other end of the link.
// Outgoing: from a project to its members
const members = await sixb
.objects(Project)
.query()
.where((project) => project.p.id.eq("proj-001"))
.traverse(Project.l.members)
.list()
Pass { direction: "incoming" } to walk a link backwards — from the link's target type to its
source type:
// Incoming: from a customer to the active projects that link to it
const projects = await sixb
.objects(Customer)
.query()
.where((customer) => customer.p.id.eq("cust-001"))
.traverse(Project.l.customer, { direction: "incoming" })
.where((project) => project.p.status.eq("active"))
.list()
| Direction | Token comes from | Result type |
|---|---|---|
"outgoing" (default) | The source type's link | The link's target type |
"incoming" | The other type's link | The link's source type |
After traverse(...), the builder operates on the new result type, so where(...),
orderBy(...), and list(...) apply to the traversed objects.
Inherited links
When an object type uses extends, it inherits the parent's links along with its properties. A
Contract extends Document gains Document's project and author links without redeclaring
them. See object types for inheritance details.
Related
- Object types — declare the types links connect
- Properties — values that live on a single object
- Querying objects — filters, ordering, and
traverse - CRUD — upsert, link, and unlink from code
- Events —
link.created,link.updated, andlink.deleted