Multi-picker for a many-to-many relation.
| Builder | F.belongsToMany(attribute: string): BelongsToMany |
| Wire type key | belongs-to-many |
| Class chain | BelongsToMany → RelationField → Field |
| Detail projection | relation-entry |
| Column counterpart | — (not tabulatable) |
Example
import { F } from '@adonia/core'
F.belongsToMany('tags')
.resource('tags')
.optionLabel('name')
.searchable()
.creatable()
// With pivot columns: state elements become `{ id, sort_order }`
F.belongsToMany('tags').pivotFields([F.number('sort_order').label('Order')])Props
Beyond the shared field DSL:
| Prop | Declared on | Behaviour |
|---|---|---|
pivotFields(schema: readonly SchemaComponent[]): this |
BelongsToMany |
Declares a sub-form over the PIVOT columns (§7.3 pivotFields(schema)). |
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 |
|---|---|---|
detailProps(): JsonObject |
BelongsToMany |
Chrome, the relation props, and multiple: true (protocol §8). |
nestedSchema(): readonly SchemaComponent[] | undefined |
BelongsToMany |
The pivot sub-form, emitted as the node’s children (protocol §2). |
boundaryKeys(): readonly string[] |
BelongsToMany |
PIVOT_KEY — the item’s identity, and the one key of a pivot item that is NOT a pivot field (a pivot schema may not declare id). |
get hasPivotFields(): boolean |
BelongsToMany |
Whether the state elements carry pivot attributes. |
persistRelation(record: object, value: unknown, _ctx: HttpContextLike): Promise<void> |
BelongsToMany |
Reconciles the pivot table with the submitted selection, inside the write transaction (see the module TSDoc for why a rebuild is wrong). |
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. |
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.array(id) with a BATCHED exists — one query for the whole
array (the batched relation-check contract; a per-element rule is prohibited, it is N+1 by
construction). With pivot fields the member is the item object, built by
the same buildFieldObjectSchema the resource validator uses, so every
§11.2 rule applies inside a pivot row too.
Contributed by BelongsToMany.vineSchema() in packages/core/src/schema/fields/belongs_to_many.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_many.ts—BelongsToManypackages/core/src/schema/fields/relation.ts—RelationField