VibORM
Relations

Many-to-One

Define many-to-one relationships between models

Many-to-One Relation

A many-to-one relation connects multiple records to a single related record. This is the "many" side that owns the foreign key.

Basic Example

import { s } from "viborm";

const post = s.model({
  id: s.string().id().ulid(),
  title: s.string(),
  authorId: s.string(),  // FK field
  author: s.relation
    .fields("authorId")
    .references("id")
    .manyToOne(() => user),
}).map("posts");

const user = s.model({
  id: s.string().id().ulid(),
  email: s.string().unique(),
  posts: s.relation.oneToMany(() => post),  // Inverse side
}).map("users");

Characteristics

AspectValue
ReturnsSingle object or null
FK locationOn this model
Can be nullYes, with .optional()
Required by defaultYes

Config Methods (before .manyToOne())

.fields(...fieldNames)

Required. Specifies the FK field(s) on this model:

s.relation.fields("authorId").manyToOne(() => user)

// Composite FK
s.relation.fields("orgId", "teamId").manyToOne(() => organization)

.references(...fieldNames)

Required. Specifies the referenced field(s) on the target:

s.relation
  .fields("authorId")
  .references("id")
  .manyToOne(() => user)

// Composite reference
s.relation
  .fields("orgId", "teamId")
  .references("id", "teamId")
  .manyToOne(() => organization)

.optional()

Makes the relation optional (FK can be null):

// Post may or may not have an author
author: s.relation
  .fields("authorId")
  .references("id")
  .optional()
  .manyToOne(() => user)

When using .optional(), the FK field should also be nullable:

authorId: s.string().nullable(),
author: s.relation
  .fields("authorId")
  .references("id")
  .optional()
  .manyToOne(() => user)

.onDelete(action)

Referential action when the related record is deleted:

s.relation
  .fields("authorId")
  .references("id")
  .onDelete("cascade")  // Delete posts when user is deleted
  .manyToOne(() => user)
ActionEffect
cascadeDelete this record too
setNullSet FK to null (requires .optional())
restrictPrevent deletion
noActionDatabase default

.onUpdate(action)

Referential action when the referenced field changes:

s.relation
  .fields("authorId")
  .references("id")
  .onUpdate("cascade")  // Update FK when user ID changes
  .manyToOne(() => user)

Complete Example

const post = s.model({
  id: s.string().id().ulid(),
  title: s.string(),
  content: s.string(),
  published: s.boolean().default(false),
  
  // Required author
  authorId: s.string(),
  author: s.relation
    .fields("authorId")
    .references("id")
    .onDelete("cascade")
    .manyToOne(() => user),
  
  // Optional category
  categoryId: s.string().nullable(),
  category: s.relation
    .fields("categoryId")
    .references("id")
    .optional()
    .onDelete("setNull")
    .manyToOne(() => category),
}).map("posts");

Querying Many-to-One

// Include author when fetching post
const post = await client.post.findUnique({
  where: { id: "post_123" },
  include: { author: true },
});
// post.author: User

// Filter posts by author
const posts = await client.post.findMany({
  where: {
    author: {
      is: { role: "ADMIN" }
    }
  }
});

// Filter posts without author (optional relation)
const orphanedPosts = await client.post.findMany({
  where: {
    author: {
      is: null
    }
  }
});

// Select specific author fields
const posts = await client.post.findMany({
  select: {
    title: true,
    author: {
      select: { name: true, email: true }
    }
  }
});

Creating with Relations

// Connect to existing user
const post = await client.post.create({
  data: {
    title: "My Post",
    content: "Hello!",
    author: {
      connect: { id: "user_123" }
    }
  }
});

// Or use FK directly
const post = await client.post.create({
  data: {
    title: "My Post",
    content: "Hello!",
    authorId: "user_123"
  }
});

// Create and connect in one operation
const post = await client.post.create({
  data: {
    title: "My Post",
    content: "Hello!",
    author: {
      connectOrCreate: {
        where: { email: "alice@example.com" },
        create: { email: "alice@example.com", name: "Alice" }
      }
    }
  }
});

Common Patterns

Required vs Optional

// Required: Every post MUST have an author
authorId: s.string(),
author: s.relation
  .fields("authorId")
  .references("id")
  .manyToOne(() => user)

// Optional: Post MAY have a category
categoryId: s.string().nullable(),
category: s.relation
  .fields("categoryId")
  .references("id")
  .optional()
  .manyToOne(() => category)

Self-Referential (Tree Structure)

const comment = s.model({
  id: s.string().id().ulid(),
  content: s.string(),
  parentId: s.string().nullable(),
  parent: s.relation
    .fields("parentId")
    .references("id")
    .optional()
    .manyToOne(() => comment),
  replies: s.relation.oneToMany(() => comment),
}).map("comments");

On this page