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

AreaLegacy0.9.50
Model rootdefineDataModel()createSchema() or createFormSchemaOrchestrator()
CollectiondefineObject() / object-form defineField()collection(name, options)
Fieldobject-form defineField() / defineCollection()bd.text(), bd.number(), etc.
EnumsHand-authored optionsEnums() + bd.enum() / bd.multiEnum()
Layoutlayout.*layoutContainers.*
Orchestrator input{ collections, layout, options }(nodes[], options?)
Orchestrator outputschema + layout in legacy shapedatabaseSchema, 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 stateRuntime state
Fields and input intentCurrent values
Steps and orderActive step
Conditional rulesCurrent visibility
Repeater shape and limitsRepeater instances
Validation metadataCurrent errors
Branch definitionsSelected 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 uiFormTree separately,
  • secret API keys remain server-only.

Select v0.9.35 in the docs navbar to return to the supported legacy documentation set.