Skip to content
VibORM
Esc
navigateopen⌘Jpreview
On this page

Nested Writes

Create, update, and manage related records in a single operation

Nested writes let you create, connect, update, and delete related records in the same call as their parent. The whole operation is atomic — if any part fails, nothing is written — and generated IDs flow automatically from a parent to its children.

Create Operations

create

Create nested records:

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

// Create user with multiple posts
const user = await client.user.create({
  data: {
    email: "bob@example.com",
    posts: {
      create: [
        { title: "Post 1" },
        { title: "Post 2" },
      ],
    },
  },
  include: { posts: true },
});

connect

Connect to existing records:

// Connect existing category
const post = await client.post.create({
  data: {
    title: "My Post",
    category: {
      connect: { id: "cat_123" },
    },
  },
});

// Connect multiple tags
const post = await client.post.create({
  data: {
    title: "Tagged Post",
    tags: {
      connect: [
        { id: "tag_1" },
        { name: "featured" },
      ],
    },
  },
});

connectOrCreate

Connect if exists, create if not:

const post = await client.post.create({
  data: {
    title: "My Post",
    category: {
      connectOrCreate: {
        where: { name: "Tech" },
        create: { name: "Tech" },
      },
    },
    tags: {
      connectOrCreate: [
        {
          where: { name: "tutorial" },
          create: { name: "tutorial" },
        },
        {
          where: { name: "beginner" },
          create: { name: "beginner" },
        },
      ],
    },
  },
});

Update Operations

update (nested)

A to-one nested update targets the currently related row. If there’s no related row yet, the operation throws and nothing is written:

const user = await client.user.update({
  where: { id: "user_123" },
  data: {
    profile: {
      update: { bio: "Updated bio" },
    },
  },
});

A to-one nested update also accepts a { where, data } envelope. Unlike the to-many form below, this where is an ordinary (non-unique) filter — a to-one has exactly one related row, so the filter is a precondition on that row rather than a way to pick between candidates. If the related row exists but fails the filter, the whole operation is rejected and nothing is written:

const user = await client.user.update({
  where: { id: "user_123" },
  data: {
    profile: {
      update: {
        where: { verified: true },
        data: { bio: "Updated bio" },
      },
    },
  },
});

The filter may reach through relations (where: { badges: { some: { active: true } } }) and use AND / OR / NOT.

A to-many nested update targets a specific related row. The where must identify a row that belongs to the parent you’re updating — targeting another record’s row is rejected:

const user = await client.user.update({
  where: { id: "user_123" },
  data: {
    posts: {
      update: {
        where: { id: "post_123" },
        data: { published: true },
      },
    },
  },
});

The to-many form also accepts an array of targeted updates:

const user = await client.user.update({
  where: { id: "user_123" },
  data: {
    posts: {
      update: [
        {
          where: { id: "post_123" },
          data: { published: true },
        },
        {
          where: { slug: "draft-notes" },
          data: { archived: true },
        },
      ],
    },
  },
});

updateMany (nested)

Nested updateMany is supported for to-many relations only. It combines the child filter with parent correlation. Matching zero rows is allowed and does not throw:

const user = await client.user.update({
  where: { id: "user_123" },
  data: {
    posts: {
      updateMany: {
        where: { published: false },
        data: { archived: true },
      },
    },
  },
});

updateMany applies to to-many relations only.

upsert (nested)

A to-one nested upsert updates the currently related row when one exists; otherwise it creates and connects a new related row:

const user = await client.user.update({
  where: { id: "user_123" },
  data: {
    profile: {
      upsert: {
        create: { bio: "New bio" },
        update: { bio: "Updated bio" },
      },
    },
  },
});

A to-many nested upsert requires where, create, and update. If where matches a row already correlated to the parent, VibORM updates that row. If where matches no row, VibORM creates a new row correlated to the parent. If where matches a row owned by another parent, VibORM rejects instead of mutating it or creating a duplicate:

const user = await client.user.update({
  where: { id: "user_123" },
  data: {
    posts: {
      upsert: {
        where: { slug: "launch-plan" },
        update: { title: "Launch plan v2" },
        create: {
          slug: "launch-plan",
          title: "Launch plan",
        },
      },
    },
  },
});

The to-many form also accepts an array of { where, create, update } envelopes.

Connection Management

disconnect

Disconnect related records (many-to-many):

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

// Disconnect to-one (set FK to null)
const post = await client.post.update({
  where: { id: "post_123" },
  data: {
    category: { disconnect: true },
  },
});

set

Replace all connections:

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

// Clear all tags
const post = await client.post.update({
  where: { id: "post_123" },
  data: {
    tags: { set: [] },
  },
});

Delete Operations

delete (nested)

Delete related records. A nested delete throws when the target isn’t found, including when it matches a row that belongs to another parent:

const user = await client.user.update({
  where: { id: "user_123" },
  data: {
    posts: {
      delete: { id: "post_123" },
    },
  },
});

// Delete multiple
const user = await client.user.update({
  where: { id: "user_123" },
  data: {
    posts: {
      delete: [{ id: "post_1" }, { id: "post_2" }],
    },
  },
});

deleteMany (nested)

Nested deleteMany is supported for to-many relations only. It combines the filter with parent correlation. Matching zero rows is allowed and does not throw:

const user = await client.user.update({
  where: { id: "user_123" },
  data: {
    posts: {
      deleteMany: {
        published: false,
        createdAt: { lt: new Date("2023-01-01") },
      },
    },
  },
});

The to-many form also accepts an array of filters:

const user = await client.user.update({
  where: { id: "user_123" },
  data: {
    posts: {
      deleteMany: [
        { published: false },
        { archived: true },
      ],
    },
  },
});

deleteMany applies to to-many relations only. For a to-one relation, use delete: true.

What’s not allowed

Required relations

When a relation’s foreign key is required, you can’t detach or orphan its records — these are rejected and nothing is written:

// post.authorId is required, so the author cannot be disconnected.
await client.post.update({
  where: { id: "post_123" },
  data: {
    author: {
      disconnect: true,
    },
  },
});
// post.authorId is required, so clearing the user's posts would orphan rows.
await client.user.update({
  where: { id: "user_123" },
  data: {
    posts: {
      set: [],
    },
  },
});
// Required foreign keys cannot be nulled through nested updateMany data.
await client.user.update({
  where: { id: "user_123" },
  data: {
    posts: {
      updateMany: {
        where: { archived: false },
        data: { authorId: null },
      },
    },
  },
});

Update and delete inside a create

You can’t update or delete related records inside a create (or the create branch of an upsert) — there’s no existing related record yet:

await client.user.create({
  data: {
    email: "new@example.com",
    profile: {
      update: { bio: "No existing profile under a new user" },
    },
  },
});
await client.user.upsert({
  where: { email: "new@example.com" },
  create: {
    email: "new@example.com",
    posts: {
      deleteMany: { archived: true },
    },
  },
  update: {
    name: "Existing user",
  },
});

Examples

Complete User Registration

async function registerUser(data: {
  email: string;
  name: string;
  bio?: string;
  defaultTags?: string[];
}) {
  return client.user.create({
    data: {
      email: data.email,
      name: data.name,
      profile: {
        create: { bio: data.bio || "" },
      },
      settings: {
        create: {
          theme: "system",
          notifications: true,
        },
      },
      ...(data.defaultTags && {
        followedTags: {
          connectOrCreate: data.defaultTags.map(name => ({
            where: { name },
            create: { name },
          })),
        },
      }),
    },
    include: {
      profile: true,
      settings: true,
      followedTags: true,
    },
  });
}

Update Post with Tags

async function updatePostTags(postId: string, tagNames: string[]) {
  return client.post.update({
    where: { id: postId },
    data: {
      tags: {
        set: [],  // Clear existing
        connectOrCreate: tagNames.map(name => ({
          where: { name },
          create: { name },
        })),
      },
    },
    include: { tags: true },
  });
}

Was this page helpful?