@burojs/field-types
v0.6.0
Published
Buro: default field types (string, number, ref, ...)
Readme
@burojs/field-types
The default field-type set for buro — the f.* builder you use inside defineResource.
Install
pnpm add @burojs/field-types @burojs/coreUsage
import { defineResource } from '@burojs/core';
import { f } from '@burojs/field-types';
import { users } from './users.js';
defineResource(
'document',
{
fields: {
id: f.number({ system: true }),
title: f.string({ texts: 'Title', required: true }),
status: f.enum(['draft', 'published'], { texts: 'Status' }),
author: f.relation({ to: users, texts: 'Author' }),
tags: f.relationMany({ to: 'tags', texts: 'Tags' }),
published_at: f.dateTime({ texts: 'Published' }),
config: f.code({ language: 'yaml' }),
is_active: f.boolean({ texts: 'Active' }),
},
},
);Each field carries its own label, validation and UI hints, so the list, show and form surfaces are all derived from one declaration.
Relations
f.relation (one key) and f.relationMany (an array of keys) name their target once, in to:
- a resource handle —
f.relation({ to: users }): typed, so aviahop past it and abyare checked against the target's fields; - a thunk —
f.relation({ to: () => createOrderResource() }): for a resource declared later in the module, in another module that imports this one, or a factory. The thunk is called when the app first reads the resource's declaration (app.use), never insidedefineResource, so two modules may relate to each other. A self-relation's thunk takes a return type,to: (): ResourceHandle => users, and the hop past it is then untyped in TypeScript (still checked at publish); between two mutually importing modules, annotate one side's thunk the same way. An error in the thunk is a rejectedapp.usenaming the resource and the field; - a resource name —
f.relation({ to: 'articles' }): legal for a target you cannot import, and untyped past the hop.
product: f.relation({ to: products, by: 'gtin', resolve: 'lazy' }), // stores the gtin; shows it, no lookup
owner: f.relation({ to: users }), // eager by default: shows the title
tags: f.relationMany({ to: tags }),bynames the target field the key holds when it is not the id. It must be a declared, scalar, non-sensitivetarget field that admitseqandin, in the target's record DTO, and not its id field (data.idField ?? 'id'); otherwise it isrelation.by-invalidat publish (and a TS error with a typed target). Every reader follows it:viahops, displays (one batched<by> in [...]read per column), the picker (it writesrow[by]) and the list filter. Two target rows with the same key are never resolved to either: the display shows the key muted and logs a development error.resolvegoverns displays only:'eager'(the default) reads the target's title through the batched aggregator;'lazy'reads nothing and shows the key — a link to the target's show page when the key is the id, plain text withby. A picker always resolves its selected value.
f.ref, f.refMany, f.children and f.relation's second argument are removed:
| Removed | Write instead |
| --- | --- |
| f.ref(target, opts) | f.relation({ to: target, ...opts }) |
| f.refMany(target, opts) | f.relationMany({ to: target, ...opts }) |
| f.children(target, { inline, fields, ... }) | no field: place relatedList(target) with where: [{ field: '<fk>', op: 'eq', value: l.ref('record', 'id') }] on the parent's show tree |
| f.relation(opts, target), ui.to | f.relation({ to: target, ...opts }) |
Many-to-many links
A link keeps its keys in a junction resource instead of a column of the record. Declare it with
through:
reservedCodes: f.relationMany({
to: codes,
through: {
resource: documentCodes, // the junction: a handle, a thunk or a name
sourceKey: 'document_id', // the junction field holding this record's key
targetKey: 'code_id', // the junction field holding the target's key
where: [{ field: 'type', op: 'eq', value: 'reserved' }], // optional, literals only
},
preview: 'chips',
providerOptions: { relationship: 'document_codes' }, // optional provider hint
}),through— with a typed junction,sourceKey,targetKeyand everywhere.fieldare checked against its fields. Becausef.relationManyhas two overloads (a column, a link), an invalid link reports its error on the call expression (TS2769, "No overload matches this call") rather than on the bad key.whereholds literal values in thewherevocabulary and applies to every read and filter of the link. At publish the junction must be a resource of the application whosesourceKeyandtargetKeyare to-one relations to the owner and the target; see the JSON form for the checker codes. A link takes noby,nullableordefault.- Separate (the default) or
inline: true. A separate link is read apart from the record: place a panel of it withrelatedList({ link: 'reservedCodes' })(see the screens guide), and it is not shown in show or list modes on its own.preview: 'chips'also shows its keys as chips in show and list.inline: trueshows them as chips with the record. Chips read the junction in batches for the visible rows, with any data provider. A record with more than 1000 junction rows shows "More than 1000 links" in place of its chips, and keys whose target the user may not see collapse into one "N not visible" chip. - Filtering. A link filters with
has,hasAny,hasAllandhasNone(see the lists guide).
Limits of this version. Links are read-only. A link field is not a form input (inline links
included), is never part of a create or update payload or of the record DTO, and is not part of the
inferred record type (InferRecord). A separate link cannot be required
(relation.through-required); required on an inline link is accepted and not yet enforced.
through: 'provider' is refused.
Main exports
f/FieldBuilder/extendFieldBuilder— the field builder; build a resource'sfieldswith it and pass them to@burojs/core'sdefineResource. An extended builder writesimport { f as base } from '@burojs/field-types'andextendFieldBuilder(base, …). Includesstring,number,boolean,date,dateTime,enum,relation,relationMany,code,richText,jsonand friends; every field takesin(the modes it appears in: planned and checked from P1a, honoured by forms and show from P1b).fieldTypesPlugin— registers the set with adefineAppinstance.fromSchema/FromSchemaFieldDef— derive fields from an existing schema instead of hand-writing them.isStandardSchemaandStandardSchemaV1— re-exported from@burojs/core: the guard detects a Standard Schema v1 validator (an object or, like an ArkType type, a function), so Zod/Valibot/ArkType can be passed to a field'svalidatewith no runtime dependency on any of them.
License
MIT
