Skip to content
VibORM
Esc
navigateopen⌘Jpreview
On this page

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

Type Meaning Foreign Key Location
oneToMany One record has many related On the “many” side
manyToOne Many records belong to one On this side
oneToOne One-to-one relationship Configurable
manyToMany Many-to-many via join table Join 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

Relation Methods
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
  • L12 (Migrations): Relations inform foreign key constraints

Was this page helpful?