Diagram
100%

Drag to pan · Scroll to zoom · + / − to zoom · 0 to fit · Esc to close

Skip to content

4. Relationships and Projections ​

A reference field owns the stored value. A relationship owns graph meaning.

An area connects to its category; a projection selects the details a screen needs.

dart
abstract final class AreaFields {
  static const categoryId = ReferenceIdField(
    'area_category_id',
    title: 'Area Category',
    indexed: true,
    db: FieldDb(
      queryPatterns: [QueryPattern.filter, QueryPattern.aggregate],
    ),
    ui: FieldUI(
      widget: UIFieldControl.referencePicker,
      filterable: true,
    ),
    reference: UIReferenceHint(
      target: EntityRef(module: ModuleRef('ops'), name: 'area_category'),
    ),
  );
}

abstract final class AreaRels {
  static const category = Relationship(
    name: 'category',
    title: 'Area Category',
    target: EntityRef(module: ModuleRef('ops'), name: 'area_category'),
    cardinality: RelationshipCardinality.hasOne,
    kind: RelationshipKind.classifiedBy,
    field: AreaFields.categoryId,
    required: false,
    cascade: CascadeRule.restrict,
    embed: EmbedHint.lazy,
  );
}

Declare both on the owning facet:

dart
Facet(
  name: 'hierarchy',
  fields: const [AreaFields.categoryId],
  relationships: const [AreaRels.category],
)

Relationship cardinality ​

CardinalityMeaning
hasOneThis record points to one target. The declaring side holds the foreign key.
hasManyMultiple targets point back to this record. Usually derived as the reverse of hasOne.
manyToManyA set-to-set relationship without extra membership state.

Relationship kind ​

KindMeaning
mapsThis mapping record ties that end in.
locatedInThis occupies that place.
classifiedByThis instance is classified by that type.
partOfThis is a part of that.
participatesInThis takes part in that event.
appliesToThis quality or measurement applies to that bearer.
specifiesThis spec or plan specifies those events.
aboutThis information is about that thing.
realizesThis event carries out that function or spec.

If a many-to-many link has dates, status, role, scope, provenance, or actions, model the link as a mapping entity. A role assignment is data, not an anonymous join table.

Physical and logical edges ​

RelationshipStorage.foreignKey lowers to a Postgres foreign key. RelationshipStorage.logical participates in the graph without claiming a column or constraint. Use logical edges for:

  • remote or externally owned records;
  • polymorphic targets;
  • mapping-derived inverses;
  • cross-module links whose storage is owned elsewhere.

Polymorphic logical relationships may share an ID field and select targets through a discriminator field/value.

Cascade and embed are separate ​

CascadeRule answers what happens when the target disappears. EmbedHint answers how a read projection should load the target.

Never choose cascade behavior to obtain a UI result.

dart
RelatedField<String>(
  'area_category_name',
  via: AreaRels.category,
  source: AreaCategoryFields.name,
  ui: const FieldUI(
    filterable: true,
    sortable: true,
  ),
)

The DB/API projection can expose a category label without persisting a duplicate column on area. Related fields may compose through direct neighbors, but the chain must terminate in a stored field and must remain acyclic.

UI rendering follows semantics ​

  • Editor: reference picker, not UUID text input.
  • Summary: linked entity label, not raw ID.
  • Table: projected label when declared.
  • Relationship tab: collections for one-to-many or many-to-many edges.
  • Navigation: internal right arrow; reserve the up-right arrow for external links.

The UI does not infer a relationship from a field ending in _id. Declare the reference and the relationship explicitly.

Cross-module relationships ​

Use a structured target identity such as:

dart
const directory = ModuleRef('directory');
const user = EntityRef(module: directory, name: 'user');

The consuming module declares dependencies: [directory], and Directory exports EntityExport(entity: user). Wire strings such as directory.user are derived at protocol/storage boundaries. Module boundaries are enforced, not bypassed with authored dotted strings.

Model occurrences instead of shortcut genealogy edges ​

When a relationship itself represents an auditable business occurrence, make that occurrence an entity. A material transformation can use ordinary mapping entities for typed input, output, and resource participation, including quantities, equipment, actors, revision pins, actions, and evidence.

Blueprint does not define a special direct lot-to-lot edge or a second transaction engine. The occurrence remains the auditable fact.

Checkpoint ​

Model directory.user ↔ access.role through a rich role_assignment mapping entity. Include:

  • user and role references;
  • tenant/site scope;
  • effective-from/effective-until;
  • assignment status;
  • the actor who assigned it.

Then continue to Actions, rules, and lifecycles.