Skip to content
VibORM
Esc
navigateopen⌘Jpreview
On this page

One-to-One

Define relations in which each side can reference at most one record

A one-to-one relationship is what two s.toOne slots spell. Neither factory name says “one-to-one”: the pair does.

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.toOne(() => profile),
});

// Profile belongs to one user
const profile = s.model({
  id: s.string().id().ulid(),
  bio: s.string(),
  userId: s.string(),   // FK field
  user: s.toOne(() => user)
    .fields("userId")
    .references("id"),
});

Which Side Owns the FK?

Exactly one side completes .fields(...).references(...); that side stores the foreign key. Because its partner is also a toOne, the referenced tuple is derived unique — VibORM emits the unique constraint for you.

Side Has FK Configuration
Owner (Profile) Yes .fields("userId").references("id")
Non-owner (User) No none

Declaring userId .unique() yourself is no longer needed to make the relation one-to-one. Declare it only when callers must address that column through whereUnique — that is a model-key decision, not a relation rule.

Neither endpoint may be left out: a toOne whose target model declares nothing back is refused with R002. Two owners are CM003; no owner is FK004.

Configuration

One-to-one relations use the shared singular methods — see the relation method reference for details and referential actions:

s.toOne(() => user)
  .name("owner")        // Pairing label, matched exactly on both endpoints
  .fields("userId")     // FK field(s) on this model (owning side only)
  .references("id")     // Referenced field(s) on the target
  .onDelete("cascade")  // Referential action on delete
  .onUpdate("cascade")  // Referential action on update

Complete Example

const user = s.model({
  id: s.string().id().ulid(),
  email: s.string().unique(),
  // Non-owning side - no FK, so zero related rows is valid
  profile: s.toOne(() => profile),
});

const profile = s.model({
  id: s.string().id().ulid(),
  bio: s.string().nullable(),
  avatar: s.string().nullable(),
  // FK field - the pair derives its unique constraint
  userId: s.string(),
  // Owning side - has FK configuration
  user: s.toOne(() => user)
    .fields("userId")
    .references("id")
    .onDelete("cascade"),
});

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" } }
    }
  }
});

See To-One Relation Filters for all operators and Nested Writes for creating related records.

Ownership and absence

// Non-owning: the related row may not exist, so this view is always nullable
profile: s.toOne(() => profile)

// Owning and required: this model stores a non-null foreign key
user: s.toOne(() => user)
  .fields("userId")
  .references("id")

There is no .optional() on a model-target relation, and none is needed.

A toOne without .fields(...) is the non-owning side. It has no local foreign key that can require another row to exist, so its public slot is derived zero-or-one. Nothing to declare, nothing to keep in sync.

An owning toOne follows its own scalars: required while its foreign-key fields are required, empty-able as soon as one of them is .nullable(). A compound foreign key mixing nullable and required members is nullable too — clearing its nullable members empties the membership while the required context members stay.

For nested updates through the non-owning side, delete is available because removing the child leaves the slot empty. disconnect is available only when the child’s owning foreign key is nullable: disconnect preserves the child and must therefore be able to clear its membership. Database setNull is stricter still — it needs every foreign-key member to be nullable.

Was this page helpful?