VibORM
Relations

One-to-One

Define one-to-one relationships between models

One-to-One Relation

A one-to-one relation connects a single record to exactly one other record.

Basic Example

import { s } from "viborm";

// User has one profile
const user = s.model({
  id: s.string().id().ulid(),
  email: s.string().unique(),
  profile: s.relation.optional().oneToOne(() => profile),
}).map("users");

// Profile belongs to one user
const profile = s.model({
  id: s.string().id().ulid(),
  bio: s.string(),
  userId: s.string().unique(),  // FK field, unique for 1:1
  user: s.relation
    .fields("userId")
    .references("id")
    .oneToOne(() => user),
}).map("profiles");

Which Side Owns the FK?

In a one-to-one relation, one side must have the foreign key:

SideHas FKConfiguration
Owner (Profile)Yes.fields("userId").references("id") before .oneToOne()
Non-owner (User)NoJust .optional() before .oneToOne() or nothing

Config Methods (before .oneToOne())

.fields(...fieldNames)

Specifies the FK field(s) on the current model:

s.relation.fields("userId").oneToOne(() => user)

.references(...fieldNames)

Specifies the referenced field(s) on the target model:

s.relation
  .fields("userId")
  .references("id")
  .oneToOne(() => user)

.optional()

Makes the relation optional (can be null):

// User may or may not have a profile
profile: s.relation.optional().oneToOne(() => profile)

.onDelete(action)

Referential action when the related record is deleted:

s.relation
  .fields("userId")
  .references("id")
  .onDelete("cascade")  // Delete profile when user is deleted
  .oneToOne(() => user)

.onUpdate(action)

Referential action when the referenced field is updated:

s.relation
  .fields("userId")
  .references("id")
  .onUpdate("cascade")
  .oneToOne(() => user)

Complete Example

const user = s.model({
  id: s.string().id().ulid(),
  email: s.string().unique(),
  // Non-owning side - no FK, can be optional
  profile: s.relation.optional().oneToOne(() => profile),
}).map("users");

const profile = s.model({
  id: s.string().id().ulid(),
  bio: s.string().nullable(),
  avatar: s.string().nullable(),
  // FK field - unique for 1:1
  userId: s.string().unique(),
  // Owning side - has FK configuration
  user: s.relation
    .fields("userId")
    .references("id")
    .onDelete("cascade")
    .oneToOne(() => user),
}).map("profiles");

Querying One-to-One

// Include profile when fetching user
const user = await client.user.findUnique({
  where: { id: "user_123" },
  include: { profile: true },
});
// user.profile: Profile | null

// Filter users by profile data
const users = await client.user.findMany({
  where: {
    profile: {
      is: { bio: { contains: "developer" } }
    }
  }
});

// Create user with profile
const user = await client.user.create({
  data: {
    email: "alice@example.com",
    profile: {
      create: { bio: "Software developer" }
    }
  },
  include: { profile: true },
});

Common Patterns

Required vs Optional

// Required: Every user MUST have a profile
// (Create profile in same transaction)
profile: s.relation.oneToOne(() => profile)

// Optional: User MAY have a profile
profile: s.relation.optional().oneToOne(() => profile)

Self-Referential

const employee = s.model({
  id: s.string().id().ulid(),
  name: s.string(),
  managerId: s.string().nullable(),
  manager: s.relation
    .fields("managerId")
    .references("id")
    .optional()
    .oneToOne(() => employee),
}).map("employees");

On this page