Picker for a to-one relation.
| Builder | F.belongsTo(attribute: string): BelongsTo |
| Wire type key | belongs-to |
| Class chain | BelongsTo → RelationField → Field |
| Detail projection | relation-entry |
| Column counterpart | — (not tabulatable) |
Example
import { F, when } from '@adonia/core'
F.belongsTo('authorId') // relation inferred: `author`
.resource('users') // target inferred when omitted
.optionLabel('fullName')
.searchable() // urls.options, no inline options
.creatable() // inline-create modal (D2-5)
.required()
F.belongsTo('categoryId').preload(50).optionLabel('name').nullable()Props
Beyond the shared field DSL:
| Prop | Declared on | Behaviour |
|---|---|---|
resource(slug: string): this |
RelationField |
Names the target resource explicitly (§7.3 { resource }). Without it the target is inferred from the Lucid relation, which is right whenever exactly one resource manages the related model. |
relation(name: string): this |
RelationField |
Lucid relation this field reads and writes. Defaults to the attribute with a Id/_id suffix stripped — authorId → author — which is the Lucid convention for a belongsTo foreign key. |
optionLabel(attribute: string): this |
RelationField |
Target attribute rendered as the option label, overriding the target’s recordTitle (§6.1). |
searchable(enabled = true): this |
RelationField |
Renders the picker as an async combobox backed by the urls.options capability URL (protocol §4) instead of a static list. NO options ride the descriptor — that is the entire point: a resource with 200 000 rows must not serialize them into a form. |
preload(limit: number = RELATION_OPTIONS_PAGE_SIZE): this |
RelationField |
Ships the first limit options inline as props.options while STILL emitting urls.options (protocol §4): the list renders without a round-trip, and typing past the preloaded page searches the rest. |
creatable(enabled = true): this |
RelationField |
Lets the user create a target record from inside this form (§7.3), in a modal built from the TARGET resource’s own descriptor. |
keyType(type: RelationKeyType): this |
RelationField |
Primary-key shape of the target model, driving the §11.1 base (vine.number() vs vine.string()). Defaults to 'number': Lucid carries no SQL type metadata on $columnsDefinitions, so the shape cannot be introspected, and increments('id') is the overwhelming convention. UUID-keyed targets declare .keyType('string'). |
Introspection
Read-only members this type adds — used by the compiler, the table derivation and tests rather than by a schema author.
| Member | Declared on | Behaviour |
|---|---|---|
get targetSlug(): string | undefined |
RelationField |
The declared target slug, when .resource() was called. |
get relationName(): string |
RelationField |
The Lucid relation name (declared, or inferred from the attribute). |
get optionLabelAttribute(): string | undefined |
RelationField |
The declared option-label attribute, when any. |
get keyShape(): RelationKeyType |
RelationField |
Primary-key shape of the target (§11.1). |
get preloadLimit(): number | undefined |
RelationField |
Inline-options page size, or undefined when preload() was not called. |
get isSearchable(): boolean |
RelationField |
Whether the field offers server-side search (§7.3 searchable()). |
get capabilities(): readonly string[] |
RelationField |
Protocol §4: options, unconditionally. |
detailProps(): JsonObject |
RelationField |
Chrome plus the three props a relation-entry is defined to read (protocol §8). |
get columnType(): string | undefined |
RelationField |
Derives to a relation cell only when a label attribute is declared: a table column showing raw foreign keys is noise, and the dotted key author.fullName is also what registers the relation for preloading (§18). A per-instance decision, like a choice field’s badge/text split. |
get columnKey(): string |
RelationField |
author.fullName — the dotted path the derived relation cell reads (§7.5). |
contributeHints(hints: EagerHints): void |
RelationField |
§18: register the relation so a detail/edit page hydrating through it reads materialized data. Without this a 40-field form with three relation fields costs three extra queries per render, and belongs-to-many hydration costs one per record. |
get optionsResolver(): OptionsResolver | undefined |
RelationField |
The built-in options resolver (protocol §4), unless the author declared their own through dependentOptions() — an explicit resolver is a deliberate override of the target’s row set and must win. |
resolveTarget(ctx: HttpContextLike | undefined): Promise<RelationTarget | undefined> |
RelationField |
Resolves the target once per field instance (a resource instance is per-request, so this is a per-request memo, not a global cache). |
preloadedOptions(ctx: HttpContextLike): Promise<FieldOption[]> |
RelationField |
The first page of options, for preload() — the same resolution the endpoint uses, so inline and searched options are the same rows in the same order. |
Validation
Vine base: vine.string(), vine.number().
§11.1: vine.number() / vine.string() matching the target’s primary
key, plus the exists rule against the target’s scoped table.
Contributed by BelongsTo.vineSchema() in packages/core/src/schema/fields/belongs_to.ts. The shared modifiers (required, nullable, requiredWhen, unique, optionality) are applied on top by Field.buildValidation, and a rules() override replaces the base entirely — the validator compiler never branches on the field class.
Declared in
packages/core/src/schema/fields/belongs_to.ts—BelongsTopackages/core/src/schema/fields/relation.ts—RelationField