MongoDB Cursor Pagination With Mongoose

Paginate large MongoDB collections with stable cursor queries on indexed compound sort keys, avoiding skipped documents on offset paging.

JavascriptNodeData Fetching

Code

snippet
import mongoose from 'mongoose';

const EventSchema = new mongoose.Schema(
  { userId: Number, kind: String, at: Date },
  { timestamps: false },
);
EventSchema.index({ userId: 1, at: -1, _id: -1 });

const Event = mongoose.model('Event', EventSchema);

async function page(userId, cursor = null, limit = 20) {
  const filter = { userId };
  if (cursor) {
    const [at, id] = cursor.split('|');
    filter.$or = [
      { at: { $lt: new Date(at) } },
      { at: new Date(at), _id: { $lt: new mongoose.Types.ObjectId(id) } },
    ];
  }

  const docs = await Event.find(filter)
    .sort({ at: -1, _id: -1 })
    .limit(limit + 1);

  const hasMore = docs.length > limit;
  const items = hasMore ? docs.slice(0, limit) : docs;
  const last = items[items.length - 1];

  return {
    items,
    nextCursor: hasMore && last ? `${last.at.toISOString()}|${last._id}` : null,
  };
}

const first = await page(42);
const second = await page(42, first.nextCursor);
console.log('page sizes:', first.items.length, second.items.length);