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.
hasMany — a related collection
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.