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 columnWhy 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