Relations & expand

The SDK infers relationships from the spec (foreign-key fields and nested list endpoints) and generates navigation for both directions.

belongsTo — follow a foreign key

A record that carries a *Id field can follow it to the related record:

space = bs.records.ref(42).space()      # Record.spaceId → Space (or None)

Costs 1 API call (0 if the record is already cached, +1 to load it if not). Returns the related record or null/None when the FK is empty.

A nested list endpoint (/record/{id}/label) becomes a relation-collection on the handle — itself a paginator, so it iterates lazily:

for label in bs.records.ref(42).labels():   # streams the record's labels
    ...
bs.records.ref(42).labels().all()            # materialise

Where the API exposes a verified link/unlink pair for a relation, that collection additionally gains attach(idOrModel) / detach(idOrModel) (reading "record 42's labels: attach 7"). These verbs appear only on relations backed by such a pair; the read-mostly public relations above expose iterate / all / page only.

expand — eager-load several relations

expand fetches named belongsTo relations up front and returns a typed composite, so you avoid a round-trip per relation in your own code:

c = bs.records.ref(42).expand(["space", "created_by"])
c.space           # already loaded

Honest limits

The API has no server-side expand/include — expand issues one request per relation (concurrently where the language allows: TS Promise.all, async-Python asyncio.gather; sequential elsewhere). So expand is convenience, not a way around N+1. hasMany collections stay lazy for the same reason. Active mutation through a model is deliberately not offered — models are pure data.