VibORM

Pagination

Paginate query results with offset or cursor

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

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

take and skip

OptionDescription
takeNumber of records to return (positive = forward, negative = backward)
skipNumber of records to skip
cursorStart position for cursor pagination

Examples

Simple Paginator

async function paginate<T>(
  findMany: (args: { take: number; skip: number }) => Promise<T[]>,
  page: number,
  pageSize: number
) {
  return findMany({
    take: pageSize,
    skip: (page - 1) * pageSize,
  });
}

// Usage
const users = await paginate(
  (args) => client.user.findMany({ ...args, orderBy: { name: "asc" } }),
  1,
  20
);

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 take = first ?? -(last ?? 10);
  const cursor = after ?? before;
  
  const items = await client.user.findMany({
    take: Math.abs(take) + 1,
    ...(cursor && { skip: 1, cursor: { id: cursor } }),
    orderBy: { id: "asc" },
  });
  
  const hasMore = items.length > Math.abs(take);
  const nodes = hasMore ? items.slice(0, -1) : items;
  
  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

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

When to Use Offset

  • Small datasets (<10k records)
  • Need random page access
  • Simple admin interfaces

When to Use Cursor

  • Large datasets
  • Infinite scroll
  • Real-time feeds
  • Mobile apps

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

On this page