Pagination (Cursor & Relay) — GraphQL

Pagination di GraphQL List data butuh pagination. Dua pendekatan: offset-based (sederhana) dan cursor-based (scalable). Offset-based query { users(limit: 10…

Pagination di GraphQL

List data butuh pagination. Dua pendekatan: offset-based (sederhana) dan cursor-based (scalable).

Offset-based

query {
  users(limit: 10, offset: 20) {
    id
    name
  }
}

# Resolver
users: (_, { limit, offset }) => {
  return db.users.findMany({ take: limit, skip: offset });
}

# Masalah: jika data berubah antara page loads,
# items bisa terlewat atau muncul double

Cursor-based (Relay Spec)

# Schema
type UserConnection {
  edges: [UserEdge!]!
  pageInfo: PageInfo!
  totalCount: Int!
}

type UserEdge {
  node: User!
  cursor: String!  # opaque cursor (encoded ID/timestamp)
}

type PageInfo {
  hasNextPage: Boolean!
  hasPreviousPage: Boolean!
  startCursor: String
  endCursor: String
}

# Query
query {
  users(first: 10, after: "cursor_abc") {
    edges {
      node {
        id
        name
      }
      cursor
    }
    pageInfo {
      hasNextPage
      endCursor
    }
    totalCount
  }
}

Cursor Implementation

// Cursor = base64 encoded ID
const toCursor = (id) => Buffer.from(`cursor:${id}`).toString("base64");
const fromCursor = (cursor) => Buffer.from(cursor, "base64").toString().split(":")[1];

// Resolver
users: async (_, { first, after }) => {
  const afterId = after ? fromCursor(after) : null;

  const users = await db.users.findMany({
    take: first + 1, // fetch one extra to check hasNextPage
    ...(afterId && { cursor: { id: afterId }, skip: 1 }),
    orderBy: { id: "asc" },
  });

  const hasNextPage = users.length > first;
  const edges = users.slice(0, first).map(u => ({
    node: u,
    cursor: toCursor(u.id),
  }));

  return {
    edges,
    pageInfo: {
      hasNextPage,
      endCursor: edges[edges.length - 1]?.cursor,
    },
  };
}

Yang akan kamu pelajari