Skip to content

belongs-to-many

Multi-picker for a many-to-many relation.

Builder F.belongsToMany(attribute: string): BelongsToMany
Wire type key belongs-to-many
Class chain BelongsToManyRelationFieldField
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 — authorIdauthor — 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.tsBelongsToMany
  • packages/core/src/schema/fields/relation.tsRelationField
Navigation

Type to search…

↑↓ navigate↵ selectEsc close