VibORM
Relations

Many-to-Many

Define many-to-many relationships between models

Many-to-Many Relation

A many-to-many relation connects multiple records on both sides. VibORM manages the junction table automatically.

Basic Example

import { s } from "viborm";

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

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

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

Config Methods (before .manyToMany())

.through(tableName)

Specify a custom junction table name:

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

.A(fieldName)

Specify the junction table FK column for the source model:

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

.B(fieldName)

Specify the junction table FK column for the target model:

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

Auto-Generated Junction Table

Without explicit configuration, VibORM generates:

// For: post.tags = s.relation.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.relation
    .through("post_tags")
    .A("post_id")
    .B("tag_id")
    .manyToMany(() => tag),
}).map("posts");

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

Complete Example

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

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

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

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.relation
    .fields("studentId")
    .references("id")
    .manyToOne(() => student),
  course: s.relation
    .fields("courseId")
    .references("id")
    .manyToOne(() => course),
})
  .map("enrollments")
  .unique(["studentId", "courseId"]);

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

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

Self-Referential Many-to-Many

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

On this page