VibORM

L4 - Relations

How models connect via one-to-many, many-to-one, many-to-many, and one-to-one relationships

Location: src/schema/relation/

Why This Layer Exists

ORMs need to express database relationships in a type-safe way. The challenge: models reference each other circularly.

const user = s.model({
  id: s.string().id(),
  posts: s.oneToMany(() => post),  // User references Post
});

const post = s.model({
  id: s.string().id(),
  authorId: s.string(),
  author: s.manyToOne(() => user)  // Post references User
    .fields("authorId")
    .references("id"),
});

JavaScript can't reference a variable before it's declared. We solve this with thunks - functions that defer evaluation until the model is used.

Relation Types

TypeMeaningForeign Key Location
oneToManyOne record has many relatedOn the "many" side
manyToOneMany records belong to oneOn this side
oneToOneOne-to-one relationshipConfigurable
manyToManyMany-to-many via join tableJoin table

Chainable API

Relations use a chainable API where the thunk comes first:

const post = s.model({
  authorId: s.string(),  // Foreign key column
  author: s.manyToOne(() => user)
    .fields("authorId")      // Local FK column
    .references("id"),       // Remote PK column
});

Available Methods by Type

RelationMethods
oneToOne.fields(), .references(), .optional(), .onDelete(), .onUpdate(), .name()
manyToOne.fields(), .references(), .optional(), .onDelete(), .onUpdate(), .name()
oneToMany.name() only (FK is on other side)
manyToMany.through(), .A(), .B(), .onDelete(), .onUpdate(), .name()

Bidirectional Relations

Both sides of a relation can be defined:

// On User - the "one" side (doesn't own FK)
const user = s.model({
  id: s.string().id(),
  posts: s.oneToMany(() => post),  // "I have many posts"
});

// On Post - the "many" side (owns FK)
const post = s.model({
  id: s.string().id(),
  authorId: s.string(),
  author: s.manyToOne(() => user)
    .fields("authorId")
    .references("id"),             // "I belong to one user"
});

VibORM uses these definitions to:

  • Generate correct JOINs
  • Enable nested queries (where: { author: { name: "..." } })
  • Support nested writes (create: { author: { connect: {...} } })

Many-to-Many Relations

Many-to-many uses an implicit junction table:

const post = s.model({
  id: s.string().id(),
  title: s.string(),
  tags: s.manyToMany(() => tag),  // Auto-creates post_tag table
});

const tag = s.model({
  id: s.string().id(),
  name: s.string(),
  posts: s.manyToMany(() => post),
});

With explicit junction table configuration:

tags: s.manyToMany(() => tag)
  .through("post_tags")   // Custom table name
  .A("postId")            // Source FK column
  .B("tagId"),            // Target FK column

Why Thunks?

The () => model pattern is essential for two reasons:

1. Circular References

Without thunks, this would fail:

const user = s.model({
  posts: s.oneToMany(post),  // ❌ ReferenceError: post is not defined
});
const post = s.model({ ... });

2. TypeScript Inference

Thunks let TypeScript infer the return type before the variable is initialized:

// TypeScript can infer () => typeof post
// even though post isn't assigned yet
posts: s.oneToMany(() => post)

Connection to Other Layers

  • L3 (Query Schemas): Relations generate nested schemas for include/select
  • L6 (Query Engine): Relations determine JOIN generation
  • L10 (Migrations): Relations inform foreign key constraints

On this page