Skip to main content

Relationships

Relationships are the core differentiator of @biref/scanner. For every foreign key, the assembler emits two Relationship objects: one outbound on the FK holder and one inbound on the FK target.

Direction

type RelationshipDirection = 'outbound' | 'inbound';

interface Relationship {
direction: RelationshipDirection;
reference: Reference;
}
  • outbound: this entity holds the FK column(s) pointing to another entity
  • inbound: another entity's FK column(s) point to this entity

Reference

The underlying Reference describes the FK constraint:

interface Reference {
name: string; // constraint name
fromEntity: EntityRef; // { namespace, name } of the FK holder
fromFields: readonly string[]; // FK columns on the source
toEntity: EntityRef; // { namespace, name } of the FK target
toFields: readonly string[]; // referenced columns on the target
confidence: number; // 1 for declared FKs
onUpdate: ReferentialAction | null;
onDelete: ReferentialAction | null;
}

Querying relationships

// All relationships involving users (both directions)
model.relationshipsOf('public', 'users');

// Only outbound (FKs users declares)
model.outboundRelationshipsOf('public', 'users');

// Only inbound (other tables pointing to users)
const incoming = model.inboundRelationshipsOf('public', 'users');

for (const rel of incoming) {
const from = rel.reference.fromEntity;
console.log(`${from.namespace}.${from.name} -> users`);
}

Patterns

One-to-many

orders.user_id -> users.id produces:

  • outbound on orders (points to users)
  • inbound on users (from orders)

Self-referential

employees.manager_id -> employees.id produces:

  • outbound on employees (manager_id -> id)
  • inbound on employees (from itself)

Both directions appear on the same entity.

Many-to-many (join table)

product_tags(product_id, tag_id) with two FKs produces:

  • 2 outbound on product_tags (to products and to tags)
  • 1 inbound on products (from product_tags)
  • 1 inbound on tags (from product_tags)

Cross-name FK

customers.kyc_id -> kyc.id -- the FK column name (kyc_id) differs from the target table name (kyc). The scanner handles this correctly:

const ref = outbound[0].reference;
ref.fromFields // ['kyc_id']
ref.toEntity // { namespace: 'mydb', name: 'kyc' }
ref.toFields // ['id']

Composite FK

returns(order_id, variant_id) -> order_items(order_id, variant_id):

ref.fromFields // ['order_id', 'variant_id']
ref.toFields // ['order_id', 'variant_id']

Column order is preserved.