Migrating to 0.9.50
0.9.50 introduces a fluent schema builder and a single expression tree for data, validation, and form structure. The migration is conceptual as well as syntactic: database fields become the source of form input and validation metadata.
Before and after
| Area | Legacy | 0.9.50 |
|---|---|---|
| Model root | defineDataModel() | createSchema() or createFormSchemaOrchestrator() |
| Collection | defineObject() / object-form defineField() | collection(name, options) |
| Field | object-form defineField() / defineCollection() | bd.text(), bd.number(), etc. |
| Enums | Hand-authored options | Enums() + bd.enum() / bd.multiEnum() |
| Layout | layout.* | layoutContainers.* |
| Orchestrator input | { collections, layout, options } | (nodes[], options?) |
| Orchestrator output | schema + layout in legacy shape | databaseSchema, uiFormTree, formControlSettings, featureTags |
The legacy helpers still export deprecation warnings so existing models can be migrated deliberately.
1. Convert collections
Legacy:
const permits = defineObject({
universalIdentifier: '...',
nameSingular: 'permit',
namePlural: 'permits',
labelSingular: 'Permit',
labelPlural: 'Permits',
labelIdentifierFieldName: 'title',
fields: [
defineField({
universalIdentifier: '...',
name: 'title',
label: 'Title',
type: 'TEXT',
isNullable: false,
}),
],
})
0.9.50:
const permits = collection('permits', {
labelIdentifierFieldName: 'title',
fields: {
title: bd.text().required(),
},
})
collection() derives names and labels and assigns field positions. It also
allows createSchema() to inject the primary key when one is missing.
Identity matters to a Live model. Do not replace stable identifiers blindly and assume a migration will be recognized as a rename. Inspect the dry-run plan and preserve identities when adopting existing data.
2. Move validation onto builders
Legacy field objects spread nullability, validation, and UI hints across properties.
0.9.50 keeps them on one chain:
const price = bd
.number()
.required()
.formElement('currency')
.validate({
greaterThan: 0,
lessThan: 100000,
round: 2,
})
.helper('Amount before tax.')
This chain defines:
- storage type,
- required/null state,
- preferred input,
- structural validation,
- helper copy.
3. Replace enum option arrays
const Status = Enums([
'planned',
'inProgress',
{ value: 'blocked', label: 'Blocked by dependency' },
] as const)
status: bd.enum(Status).default('planned')
Enums() preserves literal types and owns the human label mapping.
4. Convert relationships
owner: bd.relation()
.references('users')
.onDelete('RESTRICT')
tags: bd.relation().belongsToMany('tags', {
joinCollectionName: 'project_tags',
})
The many-to-many builder generates its junction collection when the schema is compiled.
5. Replace layout.*
Legacy:
layout.step('Project', [
layout.collection('project'),
layout.conditional(
'project.kind',
'equals',
['hardware'],
undefined,
[layout.collection('hardwareNode')],
),
])
0.9.50:
FormStep(
'Project',
{ showTitle: true },
projectCollection,
ConditionalBlock(
{
matchStrategy: 'AND',
rules: [
{
fieldRef: 'project.kind',
op: 'equals',
values: ['hardware'],
},
],
},
hardwareCollection,
),
)
New containers take collection declarations as children. The compiler extracts them into the database schema and leaves render references in the UI tree.
6. Change the orchestrator signature
Legacy:
const result = createFormSchemaOrchestrator({
collections: [projects, hardwareNodes],
layout: [
layout.step('Project', [
layout.collection('project'),
]),
],
options: {
consecutive: true,
},
})
0.9.50:
const result = createFormSchemaOrchestrator(
[
FormStep(
'Project',
{},
projects,
hardwareNodes,
),
],
{
consecutive: true,
},
)
The options object is now the second argument.
7. Export the correct CLI value
export default result.databaseSchema
export const formTree = result.uiFormTree
The default export is a data-model CLI contract. Exporting the whole result
causes the CLI to report that toJSONSchema() is missing.
8. Separate definition state from runtime state
The new uiFormTree defines the form's immutable structure. It is not the
createFormController() snapshot and it does not contain values or errors.
| Definition state | Runtime state |
|---|---|
| Fields and input intent | Current values |
| Steps and order | Active step |
| Conditional rules | Current visibility |
| Repeater shape and limits | Repeater instances |
| Validation metadata | Current errors |
| Branch definitions | Selected branch |
Write or adopt a renderer/store for uiFormTree. Continue to use
createFormController() only with the published BusinessDash FormSchema contract.
Verification checklist
pnpm typecheck
npx bd print-data-model
npx bd sync-data-model --dry-run
Before posting:
- every conditional path uses the normalized singular collection id,
- every relation target is in the model,
- the default export is
databaseSchema, - the dry-run contains no accidental drops,
- the application renderer consumes
uiFormTreeseparately, - secret API keys remain server-only.
Select v0.9.35 in the docs navbar to return to the supported legacy documentation set.