Relationships and Extensions

Relationships connect records across collections. Declare the relationship in the schema first; record links are written separately after the model exists.

This page builds on Collections and CLI Changes.

One-to-many references

The short form:

const posts = collection('posts', {
  fields: {
    author: bd.reference('users', {
      onDelete: 'restrict',
    }),
  },
})

The fluent form:

const posts = collection('posts', {
  fields: {
    author: bd
      .relation()
      .references('users')
      .onDelete('RESTRICT')
      .required(),
  },
})

During createSchema(), a collection-name target is resolved to the target collection's universalIdentifier. Compilation fails when the target does not exist in the model.

Deletion behavior

.references(target, onDelete) accepts:

  • restrict
  • setNull
  • cascade

The chainable .onDelete() form accepts database-style names:

  • RESTRICT
  • SET NULL
  • CASCADE
  • NO ACTION

Choose the behavior based on what should happen to source records when the target record is deleted.

Many-to-many relationships

const posts = collection('posts', {
  fields: {
    tags: bd.relation().belongsToMany('tags', {
      joinCollectionName: 'post_tags',
    }),
  },
})

createSchema() generates the junction collection and its source/target relation fields. The relation builder is virtual on the source collection; the actual links live in the junction table.

Extend the junction collection

Add fields when the relationship itself carries data:

const projects = collection('projects', {
  fields: {
    members: bd
      .relation()
      .belongsToMany('users', {
        joinCollectionName: 'project_members',
        fields: {
          role: bd.enum([
            'owner',
            'editor',
            'viewer',
          ] as const).required(),
          sortOrder: bd.number().default(0),
          invitedAt: bd.dateTime().optional(),
        },
      })
      .onDelete('CASCADE'),
  },
})

These extension fields compile onto the generated junction collection. Use them for attributes that belong to the connection rather than either record.

Relationship values are not ordinary fields

A relation link does not live inside a record's values object. Seed records first, then use the separate relations channel:

{
  "object": "source-object-uid",
  "universalIdentifier": "source-row-uid",
  "values": {
    "title": "Launch plan"
  },
  "relations": {
    "author": "target-row-uid",
    "tags": ["tag-row-uid-1", "tag-row-uid-2"]
  }
}

sync-records writes records and resolves links in separate passes, so seed file order does not have to match relationship order.

For each relation field included in relations, the seed converges that field:

  • missing links are added,
  • undeclared existing links are removed,
  • an empty array clears the field,
  • omitted relation fields are left alone.

Relationship limits

  • Relation targets in seed files are row universalIdentifier values.
  • Rows created without a seed identity cannot be targeted by a convergent seed.
  • MORPH_RELATION is not supported by the 0.9.50 declaration builder.
  • Standard BusinessDash objects use their own APIs and business rules.

For the complete record-seeding and readback workflow, continue to Relationship Data.

Next: Unified Form Orchestration.