VibORM
Relations

Many-to-Many

Define many-to-many relationships connecting multiple records on both sides

Basic Example

import { s } from "viborm";

const post = s.model({
  id: s.string().id().ulid(),
  title: s.string(),
  tags: s.manyToMany(() => tag),
});

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

VibORM automatically creates a junction table post_tag with postId and tagId columns.

Characteristics

AspectValue
ReturnsArray on both sides
Junction tableAuto-created or explicit
Can be emptyYes (empty array)
FK locationJunction table

Chainable Methods

.through(tableName)

Specify a custom junction table name:

s.manyToMany(() => tag).through("post_tags")

.A(fieldName)

Specify the junction table FK column for the source model:

s.manyToMany(() => tag)
  .through("post_tags")
  .A("post_id")  // FK to posts table

.B(fieldName)

Specify the junction table FK column for the target model:

s.manyToMany(() => tag)
  .through("post_tags")
  .A("post_id")
  .B("tag_id")  // FK to tags table

.onDelete(action)

Referential action for both FKs in the junction table:

s.manyToMany(() => tag)
  .through("post_tags")
  .onDelete("cascade")  // Remove junction rows when post/tag deleted

.onUpdate(action)

Referential action when referenced PKs change:

s.manyToMany(() => tag)
  .through("post_tags")
  .onUpdate("cascade")

.name(relationName)

Set a custom relation name:

s.manyToMany(() => tag).name("labels")

Auto-Generated Junction Table

Without explicit configuration, VibORM generates:

// For: post.tags = s.manyToMany(() => tag)
// Junction table: post_tag
// Columns: postId, tagId

Table name is derived from model names in alphabetical order: post + tagpost_tag.

Explicit Junction Table

For full control:

const post = s.model({
  id: s.string().id().ulid(),
  title: s.string(),
  tags: s.manyToMany(() => tag)
    .through("post_tags")
    .A("post_id")
    .B("tag_id"),
});

const tag = s.model({
  id: s.string().id().ulid(),
  name: s.string().unique(),
  posts: s.manyToMany(() => post)
    .through("post_tags")
    .A("tag_id")
    .B("post_id"),
});

Complete Example

// Users can follow other users
const user = s.model({
  id: s.string().id().ulid(),
  name: s.string(),
  following: s.manyToMany(() => user)
    .through("user_follows")
    .A("follower_id")
    .B("following_id"),
  followers: s.manyToMany(() => user)
    .through("user_follows")
    .A("following_id")
    .B("follower_id"),
});

// Products and categories
const product = s.model({
  id: s.string().id().ulid(),
  name: s.string(),
  categories: s.manyToMany(() => category),
});

const category = s.model({
  id: s.string().id().ulid(),
  name: s.string(),
  products: s.manyToMany(() => product),
});

Querying Many-to-Many

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

// Filter posts by tags
const posts = await client.post.findMany({
  where: {
    tags: {
      some: { name: "featured" }  // Has "featured" tag
    }
  }
});

// Posts with ALL specified tags
const posts = await client.post.findMany({
  where: {
    tags: {
      every: { name: { in: ["tech", "tutorial"] } }
    }
  }
});

// Posts with NONE of specified tags
const posts = await client.post.findMany({
  where: {
    tags: {
      none: { name: "draft" }
    }
  }
});

// Count tags per post
const posts = await client.post.findMany({
  include: {
    _count: { select: { tags: true } }
  }
});

Managing Relations

// Connect existing tags
await client.post.update({
  where: { id: "post_123" },
  data: {
    tags: {
      connect: [
        { id: "tag_1" },
        { id: "tag_2" },
      ]
    }
  }
});

// Disconnect tags
await client.post.update({
  where: { id: "post_123" },
  data: {
    tags: {
      disconnect: [{ id: "tag_1" }]
    }
  }
});

// Set tags (replace all)
await client.post.update({
  where: { id: "post_123" },
  data: {
    tags: {
      set: [{ id: "tag_2" }, { id: "tag_3" }]
    }
  }
});

// Create new tags and connect
await client.post.update({
  where: { id: "post_123" },
  data: {
    tags: {
      create: { name: "new-tag" },
      connect: { id: "existing_tag" },
    }
  }
});

// Connect or create
await client.post.update({
  where: { id: "post_123" },
  data: {
    tags: {
      connectOrCreate: {
        where: { name: "tech" },
        create: { name: "tech" }
      }
    }
  }
});

Common Patterns

Explicit Junction Model

When you need additional data on the relation:

// Junction model with extra fields
const enrollment = s.model({
  id: s.string().id().ulid(),
  studentId: s.string(),
  courseId: s.string(),
  enrolledAt: s.dateTime().now(),
  grade: s.string().nullable(),
  student: s.manyToOne(() => student)
    .fields("studentId")
    .references("id"),
  course: s.manyToOne(() => course)
    .fields("courseId")
    .references("id"),
})
  .map("enrollments")
  .unique(["studentId", "courseId"]);

const student = s.model({
  id: s.string().id().ulid(),
  name: s.string(),
  enrollments: s.oneToMany(() => enrollment),
});

const course = s.model({
  id: s.string().id().ulid(),
  title: s.string(),
  enrollments: s.oneToMany(() => enrollment),
});

Self-Referential Many-to-Many

const user = s.model({
  id: s.string().id().ulid(),
  name: s.string(),
  friends: s.manyToMany(() => user)
    .through("friendships")
    .A("user_id")
    .B("friend_id"),
});

On this page