What to build
The Relationship options read primitive plus the server-action op that exposes it — the bounded, projected fetch that replaces the unbounded findMany({}) currently used to populate relationship editors.
getRelationshipOptions(context, relatedListKey, { search?, take?, selectedIds? }): Promise<{ id: string; label: string }[]> in stack-ui:
- Builds a scalar-only fragment (
{ id: true, [labelField]: true }, label field from getLabelFieldName) and reads through the existing context.db.* fragment query path. Because the fragment selects no relations, buildIncludeWithAccessControl is bypassed — the depth-5 auto-include and its Field Visibility walk never happen.
where: contains on the label field when it is a text field; otherwise no filter (first-N).
orderBy: label field ascending.
take: bounded window.
- Always unions the currently-selected id(s) (
where: { id: { in: selectedIds } }) so the caller can render the current value even when it falls outside the search/take window.
- Maps rows through
getItemLabel. Operation-level query access on the related list still applies (denied → []).
Expose it through the generic action the host already wires: a { type: 'relationshipOptions', listKey, field, search, take, selectedIds } op on context.serverAction, so there is one wiring point and one access boundary. No UI consumers change in this slice — that is #2b.
This rides the existing fragment seam only; no change to the core read path, no ADR-0001 conflict, Field Visibility intact.
Acceptance criteria
Blocked by
What to build
The Relationship options read primitive plus the server-action op that exposes it — the bounded, projected fetch that replaces the unbounded
findMany({})currently used to populate relationship editors.getRelationshipOptions(context, relatedListKey, { search?, take?, selectedIds? }): Promise<{ id: string; label: string }[]>instack-ui:{ id: true, [labelField]: true }, label field fromgetLabelFieldName) and reads through the existingcontext.db.*fragmentquerypath. Because the fragment selects no relations,buildIncludeWithAccessControlis bypassed — the depth-5 auto-include and its Field Visibility walk never happen.where:containson the label field when it is a text field; otherwise no filter (first-N).orderBy: label field ascending.take: bounded window.where: { id: { in: selectedIds } }) so the caller can render the current value even when it falls outside the search/takewindow.getItemLabel. Operation-levelqueryaccess on the related list still applies (denied →[]).Expose it through the generic action the host already wires: a
{ type: 'relationshipOptions', listKey, field, search, take, selectedIds }op oncontext.serverAction, so there is one wiring point and one access boundary. No UI consumers change in this slice — that is #2b.This rides the existing fragment seam only; no change to the core read path, no ADR-0001 conflict, Field Visibility intact.
Acceptance criteria
getRelationshipOptionsreturns{ id, label }[]via a scalar-only fragmenttaketake/ not matchingsearchsearchfilters viacontainson the resolved label field; ordering is by that fieldqueryaccess denied → returns[]relationshipOptionsop available oncontext.serverAction, access-scoped through the same contextBlocked by