Skip to content
VibORM
Esc
navigateopen⌘Jpreview
On this page

Pagination

Paginate query results using offset-based or cursor-based pagination

Offset Pagination

Simple page-based pagination:

// Page 1 (first 10 records)
const page1 = await client.user.findMany({
  take: 10,
  skip: 0,
});

// Page 2
const page2 = await client.user.findMany({
  take: 10,
  skip: 10,
});

// Page N
const pageN = await client.user.findMany({
  take: 10,
  skip: (page - 1) * 10,
});

With Total Count

async function getPage(page: number, pageSize: number) {
  const [items, total] = await Promise.all([
    client.user.findMany({
      take: pageSize,
      skip: (page - 1) * pageSize,
      orderBy: { createdAt: "desc" },
    }),
    client.user.count(),
  ]);

  return {
    items,
    total,
    page,
    pageSize,
    totalPages: Math.ceil(total / pageSize),
  };
}

Cursor Pagination

More efficient for large datasets:

// First page
const page1 = await client.user.findMany({
  take: 10,
  orderBy: { id: "asc" },
});

// Next page (using last item's ID as cursor)
const page2 = await client.user.findMany({
  take: 10,
  skip: 1,  // Skip the cursor itself
  cursor: { id: lastId },
  orderBy: { id: "asc" },
});

Cursor queries are deterministic

  • Your orderBy keys apply in the order you declare them, and VibORM appends the primary key as a final ascending tie-breaker — so every cursor query has one deterministic order, and tied or null values never cause duplicated or skipped records between pages.
  • A bare ascending order places null last; a bare descending order places null first. Use { sort, nulls } to choose explicitly.
  • A cursor that doesn’t match an existing row returns an empty page.
  • A cursor takes unique discriminators only. Unlike the where of findUnique, it does not accept extra scalar filters — a cursor is an exact row address, not a lookup to narrow.
  • Cursor pagination works with plain scalar asc/desc ordering only — relation ordering and vector-distance ordering must be queried without a cursor.

Bi-directional Cursor

Navigate forward and backward:

// Forward (next)
const next = await client.user.findMany({
  take: 10,
  skip: 1,
  cursor: { id: cursorId },
  orderBy: { id: "asc" },
});

// Backward (previous)
const prev = await client.user.findMany({
  take: -10,  // Negative take = backward
  skip: 1,
  cursor: { id: cursorId },
  orderBy: { id: "asc" },
});

With a negative take, results still come back in the original order — don’t reverse the returned page yourself.

take and skip

Option Description
take Number of records to return (positive = forward, negative = backward)
skip Number of records to skip
cursor Start position for cursor pagination

Examples

Infinite Scroll

async function loadMore(cursor?: string, limit = 20) {
  const items = await client.post.findMany({
    take: limit + 1,  // Fetch one extra to check if more exist
    ...(cursor && {
      skip: 1,
      cursor: { id: cursor },
    }),
    orderBy: { createdAt: "desc" },
  });

  const hasMore = items.length > limit;
  const data = hasMore ? items.slice(0, -1) : items;

  return {
    data,
    hasMore,
    nextCursor: hasMore ? data[data.length - 1].id : undefined,
  };
}

Relay-Style Pagination

interface Connection<T> {
  edges: { node: T; cursor: string }[];
  pageInfo: {
    hasNextPage: boolean;
    hasPreviousPage: boolean;
    startCursor?: string;
    endCursor?: string;
  };
}

async function getConnection(
  first?: number,
  after?: string,
  last?: number,
  before?: string
): Promise<Connection<User>> {
  const pageSize = first ?? last ?? 10;
  const take = first !== undefined ? pageSize + 1 : -(pageSize + 1);
  const cursor = after ?? before;

  const items = await client.user.findMany({
    take,
    ...(cursor && { skip: 1, cursor: { id: cursor } }),
    orderBy: { id: "asc" },
  });

  const hasMore = items.length > pageSize;
  let nodes = items;
  if (hasMore) {
    nodes = first !== undefined ? items.slice(0, -1) : items.slice(1);
  }

  return {
    edges: nodes.map(node => ({
      node,
      cursor: node.id,
    })),
    pageInfo: {
      hasNextPage: first ? hasMore : false,
      hasPreviousPage: last ? hasMore : false,
      startCursor: nodes[0]?.id,
      endCursor: nodes[nodes.length - 1]?.id,
    },
  };
}

Offset vs Cursor

Aspect Offset Cursor
Simplicity ✅ Simple ⚠️ More complex
Performance ⚠️ Degrades on large offsets ✅ Consistent
Random access ✅ Any page ❌ Sequential only
Real-time data ⚠️ Can skip/duplicate ✅ Consistent

distinct

Return unique values:

// Distinct roles
const roles = await client.user.findMany({
  distinct: ["role"],
  select: { role: true },
});

// Distinct by multiple fields
const unique = await client.post.findMany({
  distinct: ["authorId", "categoryId"],
});

Was this page helpful?