Code Monkey home page Code Monkey logo

mongo-cursor-pagination's Introduction

mongo-cursor-pagination

Build Status

This module aids in implementing "cursor-based" pagination using Mongo range queries or relevancy-based search results. This module is currently used in production for the Mixmax API to return millions of results a day.

New

Background

See this blog post for background on why this library was built.

API Pagination is typically implemented one of two different ways:

  1. Offset-based paging. This is traditional paging where skip and limit parameters are passed on the url (or some variation such as page_num and count). The API would return the results and some indication of whether there is a next page, such as has_more on the response. An issue with this approach is that it assumes a static data set; if collection changes while querying, then results in pages will shift and the response will be wrong.

  2. Cursor-based paging. An improved way of paging where an API passes back a "cursor" (an opaque string) to tell the caller where to query the next or previous pages. The cursor is usually passed using query parameters next and previous. It's implementation is typically more performant that skip/limit because it can jump to any page without traversing all the records. It also handles records being added or removed because it doesn't use fixed offsets.

This module helps in implementing #2 - cursor based paging - by providing a method that make it easy to query within a Mongo collection. It also helps by returning a url-safe string that you can return with your HTTP response (see example below).

Here are some examples of cursor-based APIs:

Install

npm install mongo-cursor-pagination --save

Usage

find()

Find will return ordered and paged results based on a field (paginatedField) that you pass in.

Call find() with the following parameters:

   Performs a find() query on a passed-in Mongo collection, using criteria you specify. The results
   are ordered by the paginatedField.

   @param {MongoCollection} collection A collection object returned from the MongoDB library's
      or the mongoist package's `db.collection(<collectionName>)` method.
   @param {Object} params
      -query {Object} The find query.
      -limit {Number} The page size. Must be between 1 and `config.MAX_LIMIT`.
      -fields {Object} Fields to query in the Mongo object format, e.g. {_id: 1, timestamp :1}.
        The default is to query all fields.
      -paginatedField {String} The field name to query the range for. The field must be:
          1. Orderable. We must sort by this value. If duplicate values for paginatedField field
            exist, the results will be secondarily ordered by the _id.
          2. Indexed. For large collections, this should be indexed for query performance.
          3. Immutable. If the value changes between paged queries, it could appear twice.
          4. Consistent. All values (except undefined and null values) must be of the same type.
        The default is to use the Mongo built-in '_id' field, which satisfies the above criteria.
        The only reason to NOT use the Mongo _id field is if you chose to implement your own ids.
      -sortAscending {Boolean} True to sort using paginatedField ascending (default is false - descending).
      -sortCaseInsensitive {boolean} Whether to ignore case when sorting, in which case `paginatedField`
        must be a string property.
      -next {String} The value to start querying the page.
      -previous {String} The value to start querying previous page.
   @param {Function} done Node errback style function.

Example:

const mongoist = require('mongoist');
const MongoPaging = require('mongo-cursor-pagination');

const db = mongoist('mongodb://localhost:27017/mydb');

async function findExample() {
  await db.collection('myobjects').insertMany([
    {
      counter: 1,
    },
    {
      counter: 2,
    },
    {
      counter: 3,
    },
    {
      counter: 4,
    },
  ]);

  // Query the first page.
  let result = await MongoPaging.find(db.collection('myobjects'), {
    limit: 2,
  });
  console.log(result);

  // Query next page.
  result = await MongoPaging.find(db.collection('myobjects'), {
    limit: 2,
    next: result.next, // This queries the next page
  });
  console.log(result);
}

findExample().catch(console.log);

Output:

page 1 { results:
   [ { _id: 580fd16aca2a6b271562d8bb, counter: 4 },
     { _id: 580fd16aca2a6b271562d8ba, counter: 3 } ],
  next: 'eyIkb2lkIjoiNTgwZmQxNmFjYTJhNmIyNzE1NjJkOGJhIn0',
  hasNext: true }
page 2 { results:
   [ { _id: 580fd16aca2a6b271562d8b9, counter: 2 },
     { _id: 580fd16aca2a6b271562d8b8, counter: 1 } ],
  previous: 'eyIkb2lkIjoiNTgwZmQxNmFjYTJhNmIyNzE1NjJkOGI5In0',
  next: 'eyIkb2lkIjoiNTgwZmQxNmFjYTJhNmIyNzE1NjJkOGI4In0',
  hasNext: false }

With Mongoose

Initialize Your Schema

const MongoPaging = require('mongo-cursor-pagination');
const mongoose = require('mongoose');
const counterSchema = new mongoose.Schema({ counter: Number });

Plug the mongoosePlugin.

// this will add paginate function.
counterSchema.plugin(MongoPaging.mongoosePlugin);

const counter = mongoose.model('counter', counterSchema);

// default function is "paginate"
counter.paginate({ limit: 10 }).then((result) => {
  console.log(result);
});

for paginate params refer the find section

const MongoPaging = require('mongo-cursor-pagination');
const mongoose = require('mongoose');

const counterSchema = new mongoose.Schema({ counter: Number });

// give custom function name

counterSchema.plugin(MongoPaging.mongoosePlugin, { name: 'paginateFN' });

const counter = mongoose.model('counter',
counterSchema);

// now you can call the custom named function

counter.paginateFN(params)
.then(....)
.catch(....);

You can also use the search function (as described below) like so;

// this will add paginate function.
counterSchema.plugin(MongoPaging.mongoosePlugin);

// give custom function name
// counterSchema.plugin(MongoPaging.mongoosePlugin, { searchFnName: 'searchFN' });

const counter = mongoose.model('counter', counterSchema);

// default function is "paginate"
counter.search('dog', { limit: 10 }).then((result) => {
  console.log(result);
});

search()

Search uses Mongo's text search feature and will return paged results ordered by search relevancy. As such, and unlike find(), it does not take a paginatedField parameter.

   Performs a search query on a Mongo collection and pages the results. This is different from
   find() in that the results are ordered by their relevancy, and as such, it does not take
   a paginatedField parameter. Note that this is less performant than find() because it must
   perform the full search on each call to this function. Also note that results might change

    @param {MongoCollection} collection A collection object returned from the MongoDB library's
       or the mongoist package's `db.collection(<collectionName>)` method. This MUST have a Mongo
       $text index on it.
      See https://docs.mongodb.com/manual/core/index-text/.
   @param {String} searchString String to search on.
   @param {Object} params
      -query {Object} The find query.
      -limit {Number} The page size. Must be between 1 and `config.MAX_LIMIT`.
      -fields {Object} Fields to query in the Mongo object format, e.g. {title :1}.
        The default is to query ONLY _id (note this is a difference from `find()`).
      -next {String} The value to start querying the page. Defaults to start at the beginning of
        the results.

Example:

const mongoist = require('mongoist');
const MongoPaging = require('mongo-cursor-pagination');

const db = mongoist('mongodb://localhost:27017/mydb');

async function searchExample() {
  await db.collection('myobjects').ensureIndex({
    mytext: 'text',
  });

  await db.collection('myobjects').insertMany([
    {
      mytext: 'dogs',
    },
    {
      mytext: 'dogs cats',
    },
    {
      mytext: 'dogs cats pigs',
    },
  ]);

  // Query the first page.
  let result = await MongoPaging.search(db.collection('myobjects'), 'dogs', {
    fields: {
      mytext: 1,
    },
    limit: 2,
  });
  console.log(result);

  // Query next page.
  result = await MongoPaging.search(db.collection('myobjects'), 'dogs', {
    limit: 2,
    next: result.next, // This queries the next page
  });
  console.log(result);
}

searchExample().catch(console.log);

Output:

page 1  { results:
   [ { _id: 581668318c11596af22a62de, mytext: 'dogs', score: 1 },
     { _id: 581668318c11596af22a62df, mytext: 'dogs cats', score: 0.75 } ],
  next: 'WzAuNzUseyIkb2lkIjoiNTgxNjY4MzE4YzExNTk2YWYyMmE2MmRmIn1d' }
page 2 { results:
   [ { _id: 581668318c11596af22a62e0, score: 0.6666666666666666 } ] }

Use with ExpressJS

A popular use of this module is with Express to implement a basic API. As a convenience for this use-case, this library exposes a findWithReq function that takes the request object from your Express middleware and returns results:

So this code using find():

router.get('/myobjects', async (req, res, next) => {
  try {
    const result = await MongoPaging.find(db.collection('myobjects'), {
      query: {
        userId: req.user._id
      },
      paginatedField: 'created',
      fields: { // Also need to read req.query.fields to use to filter these fields
        _id: 1,
        created: 1
      },
      limit: req.query.limit, // Also need to cap this to 25
      next: req.query.next,
      previous: req.query.previous,
    }
    res.json(result);
  } catch (err) {
    next(err);
  }
});

Is more elegant with findWithReq():

router.get('/myobjects', async (req, res, next) => {
  try {
    const result = await MongoPaging.findWithReq(req, db.collection('myobjects'), {
      query: {
        userId: req.user._id
      },
      paginatedField: 'created',
      fields: {
        _id: 1,
        created: 1
      },
      limit: 25 // Upper limit
    }
    res.json(result);
  } catch (err) {
    next(err);
  }
});

findWithReq() also handles basic security such as making sure the limit and fields requested on the URL are within the allowed values you specify in params.

Number of results

If the limit parameter isn't passed, then this library will default to returning 50 results. This can be overridden by setting mongoPaging.config.DEFAULT_LIMIT = <new default limit>;. Regardless of the limit passed in, a maximum of 300 documents will be returned. This can be overridden by setting mongoPaging.config.MAX_LIMIT = <new max limit>;.

Alphabetical sorting

The collation to use for alphabetical sorting, both with find and aggregate, can be selected by setting mongoPaging.config.COLLATION. If this parameter is not set, no collation will be provided for the aggregation/cursor, which means that MongoDB will use whatever collation was set for the collection.

(!) Important note regarding collation (!)

If using a global collation setting, or a query with collation argument, ensure that your collections' indexes (that index upon string fields) have been created with the same collation option; if this isn't the case, your queries will be unable to take advantage of any indexes.

See mongo documentation: https://docs.mongodb.com/manual/reference/collation/#collation-and-index-use

For instance, given the following index:

db.people.createIndex({ city: 1, _id: 1 });

When executing the query:

MongoPaging.find(db.people, {
  limit: 25,
  paginatedField: 'city'
  collation: { locale: 'en' },
});

The index won't be used for the query because it doesn't include the collation used. The index should added as such:

db.people.createIndex({ city: 1, _id: 1 }, { collation: { locale: 'en' } });

Indexes for sorting

mongo-cursor-pagination uses _id as a secondary sorting field when providing a paginatedField property. It is recommended that you have an index for optimal performance. Example:

MongoPaging.find(db.people, {
  query: {
    name: 'John'
  },
  paginatedField: 'city'
  limit: 25,
}).then((results) => {
  // handle results.
});

For the above query to be optimal, you should have an index like:

db.people.createIndex({
  name: 1,
  city: 1,
  _id: 1,
});

Running tests

To run tests, you first must start a Mongo server on port 27017 and then run npm test.

Future ideas

  • Add support to search() to query previous pages.

Publishing a new version

GH_TOKEN=xxx npx semantic-release --no-ci

mongo-cursor-pagination's People

Contributors

bergben avatar bradvogel avatar danielsinclair avatar dependabot-preview[bot] avatar gaastonsr avatar ghmeier avatar guilhermemj avatar gusth-sa avatar jhildensperger avatar jsalvata avatar kikajanovcik avatar kovalskyi avatar megantinleywilson avatar mericsson avatar pwiebe avatar raphaelbs avatar renovate[bot] avatar semantic-release-bot avatar skeggse avatar ttacon avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

mongo-cursor-pagination's Issues

Aggregation with Mongoose plugin?

I'm using this library with its plugin for pagination with Mongoose and it works well with normal queries. Now I want to paginate aggregation queries, but the plugin always makes paginate() to call find() instead of aggregate(). Do you have some info on how to proceed to use aggregate()?

If I change the fn const in mongoose.plugin.js to return aggregate() instead of find(), all goes well, the problem is that in deployment the original library will be downloaded and the workaround won't be there.

previous and next params

I am trying to do a pagination server side with mongo-cursor-pagination but the problem is that the pagination works only when i click on next button ( I send limit, next, previous) and not when i click on previous button. Also if go on page 3 from page 1 is not working because is showing the content from page 2. Please let me know how to make the call from frontend to get the content right from every page. This is my server side code:

router.get('/',(req, res, next) => {

var result = MongoPaging.find(db.collection('items'), {
  limit: parseInt(req.query.limit), // Also need to cap this to 25
  next: req.query.next,
  previous: req.query.previous,
});
result.then(function(data) {
  data.count = 100;
  res.status(200).json(data);
}).catch(function(err) {
  res.status(401).send({error: "Error at get items"});
});

});

Error: Semver version is `""`

When requiring this package, I am seeing this error message.
Screen Shot 2019-05-04 at 12 57 25 PM
When debugging, I'm able to see that the version is an empty string
Screen Shot 2019-05-04 at 12 58 49 PM

Are there any other dependencies needed to use your module?

Error in blog post code sample

The blog post linked in the Background section of the readme has an invalid code sample in it. The last code sample showing how to fetch the second page of documents sorted by launchDate is not using the proper sort criteria:

.sort({
   _id: -1
})

should in fact be

.sort({
   launchDate: -1,
  _id: -1
})

to retain the sort order of the original query. As written, the first page shows the latest documents sorted by launch date, but subsequent pages are no longer sorted correctly.

MAX_LIMIT can be bypassed with params.limit = NaN

Tested using the Mongoose Plugin.

The test: if (params.limit > config.MAX_LIMIT) params.limit = config.MAX_LIMIT; can be bypassed if a programming error or edge case exists resulting in params.limit = NaN.

An example is when parsing a query param using params.limit = parseInt(ctx.query.limit, 10).

In this case, params.limit will remain as NaN because NaN > 300 === false.

MongoDB will treat that as an infinite limit and fetch all documents, bypassing the limit value.

Document aggregation

It looks like the aggregate method isn't documented in readme.md. Not sure if this was intentional or not.

MongoDB Collation

I was wondering if there's an intended support for collation when doing find?

ref: https://docs.mongodb.com/manual/reference/collation/

I forked this repository and did something like this:

diff --git a/src/find.js b/src/find.js
index 886d310..84e15ac 100644
--- a/src/find.js
+++ b/src/find.js
@@ -40,6 +40,7 @@ module.exports = async function(collection, params) {
   const findMethod = collection.findAsCursor ? 'findAsCursor': 'find';

   const results = await collection[findMethod]({ $and: [cursorQuery, params.query] }, params.fields)
+    .collation({ strength: 1, locale: 'en' })
     .sort($sort)
     .limit(params.limit + 1) // Query one more element to see if there's another page.
     .toArray();

Is this a feature you guys are intending to support down the road?

Thanks!

I have a question.

first, thank you for the great open source.
we decided to use mixmax cursor as a way to fetch large amounts of data through mongodb.
It is also burdensome to request paging to db every time.
I'm trying to cache the paging itself with 'next' field key as the redis key value, but I would like to know if there is any problem.

Issue with aggregate and $group

Hi !

I am not deeply familiar with MongoDB aggregation mechanism but while trying to use the aggregate method implemented by mongo-cursor-pagination, I ran into troubles.

I could be wrong but it seems that the pagination mechanism works by getting one more element than the limit parameter passed by the user, and depending on the number of documents found, can guess if there is another page after. This particular mechanism is broken if you use a $group instruction inside your aggregation pipeline because the number of results is therefore changed.

Allow user to restrict fields to return when using mongoose

By default, queries in MongoDB return all fields in matching documents. To limit the amount of data that MongoDB sends to applications, you can include a projection document to specify or restrict fields to return.

Issue

using params.fields does nothing and all fields is returned in matching documents.

Expected

 it('returns data with expected fields', async () => {
    const data = await Post.paginate({ fields: { title: 1 }, limit: 1 });
    const [post] = data.results;
    expect(post.title).toBeTruthy();
    expect(post.date).toBeFalsy();
    expect(post.author).toBeFalsy();
    expect(post.body).toBeFalsy();
  });

The following test failed.

This PR seems to solve the issue :
#60

Please make a review if possible

find() fields straight up don't work

I've been trying to use the "fields" flag that's on the find() method, but it appears to return ALL indexes no matter what in the collection.

const tickets: IPaginateResult<ITicket> = await find(getDB().tickets, {
    limit: 50,
    fields: { _id: 1, assignee: 'github|16852656', status: TicketStatus.open },
    next: req.query.hasNext == 'true' ? true : false,
  });

This in turn returns this data:

mingo-chan_1                       | {
mingo-chan_1                       |   results: [
mingo-chan_1                       |     {
mingo-chan_1                       |       authorId: 'github|16852656',
mingo-chan_1                       |       author: 'Eddie Englund',
mingo-chan_1                       |       assignee: 'github|16852656',
mingo-chan_1                       |       status: 1,
mingo-chan_1                       |       createdAt: 2021-05-28T17:04:56.093Z,
mingo-chan_1                       |       isStarred: false,
mingo-chan_1                       |       tags: [],
mingo-chan_1                       |       labels: [],
mingo-chan_1                       |       isUpdated: true,
mingo-chan_1                       |       messages: [Array],
mingo-chan_1                       |       notes: [],
mingo-chan_1                       |       personnelView: [Array]
mingo-chan_1                       |     },
mingo-chan_1                       |     {
mingo-chan_1                       |       authorId: 'github|16852656',
mingo-chan_1                       |       author: 'Eddie Englund',
mingo-chan_1                       |       status: 1,
mingo-chan_1                       |       createdAt: 2021-05-28T17:04:54.317Z,
mingo-chan_1                       |       isStarred: false,
mingo-chan_1                       |       tags: [],
mingo-chan_1                       |       labels: [],
mingo-chan_1                       |       isUpdated: true,
mingo-chan_1                       |       messages: [Array],
mingo-chan_1                       |       notes: [],
mingo-chan_1                       |       personnelView: [Array]
mingo-chan_1                       |     }
mingo-chan_1                       |   ],
mingo-chan_1                       |   previous: 'eyIkb2lkIjoiNjBiMTIyYjgyYTAyNDMwMDFkZmE4NzI0In0',
mingo-chan_1                       |   hasPrevious: false,
mingo-chan_1                       |   next: 'eyIkb2lkIjoiNjBiMTIyYjYyYTAyNDMwMDFkZmE4NzIzIn0',
mingo-chan_1                       |   hasNext: false
mingo-chan_1                       | }

But clearly the above shouldn't be passed as A the assignee is null on the second user but also neither of them actually have _id of 1.

Note that I am using the native driver here.

The automated release is failing 🚨

🚨 The automated release from the master branch failed. 🚨

I recommend you give this issue a high priority, so other packages depending on you could benefit from your bug fixes and new features.

You can find below the list of errors reported by semantic-release. Each one of them has to be resolved in order to automatically publish your package. I’m sure you can resolve this πŸ’ͺ.

Errors are usually caused by a misconfiguration or an authentication problem. With each error reported below you will find explanation and guidance to help you to resolve it.

Once all the errors are resolved, semantic-release will release your package the next time you push a commit to the master branch. You can also manually restart the failed CI job that runs semantic-release.

If you are not sure how to resolve this, here is some links that can help you:

If those don’t help, or if this issue is reporting something you think isn’t right, you can always ask the humans behind semantic-release.


The commit message is not formatted correctly

Unfortunately this error doesn't have any additional information. Feel free to kindly ask the author of the @mixmaxhq/semantic-commitlint plugin to add more helpful information.


Good luck with your project ✨

Your semantic-release bot πŸ“¦πŸš€

Question: support for multiple sorters?

I was wondering if you have any advice on how to approach handling multiple sorters?
So far in the code I can only see support for 1 secondary sorter and I'm trying to wrap my brain around how to support multiple sorters, like, { price: -1, name: 1 } and so on.

Is it even possible? So far I can't seem to achieve this due to the sort field being used in the query $or, and so it filters our the records based on all sort field values.

Any advice is greatly appreciated! Cheers!

How to use Fields parameter with Mongoose paginate

Current Behaviour

The following fields parameter in my Mongoose query is not recognised by paginate function since the query simply returns the entire result including "bids.bid", which I am explicitly excluding. It appears any fields I include are disregarded.

Additionally, whenever I include a "fields" parameter in the paginate function, the _id is also never returned; however, removing the "fields" parameter entirely brings back the _id field as expected.

List.paginate({
	query: query,
	limit: paginationLimit,
	fields: {
		"bids.bid": 0
	}
}).then((result) => {
	sendJsonResponse(res, 200, result);
}).catch((err) => {
	sendJsonResponse(res, 404, err);
});

Expected Behaviour

The documentation for the paginate function refers to the find section so I was expecting the "fields" parameter to work as it does when not using Mongoose.

How should I exclude/include certain fields from my query using Mongoose and paginate?

[Feature Proposal] Compute cursor for each document

Feature request & proposal

Highlighting the relay specification for the cursor based pagination which specifies a cursor for each document. As things are today, this library returns pagination metadata as separate fields (next, previous, etc..) which points to the last document of the list.

I propose to add an additional field _cursor for each document which will allow the user to move in forward/backward direction from any node in a list.

Example -

{
 "results":
   [ 
      { 
        " _id" : "580fd16aca2a6b271562d8bb", 
        "counter": 4, 
        "_cursor": "eyIkb2lkIjoiNTgwZmQxNmFjYTJhNmIyNzE1NjJkOGJiIn0" },
      {
       " _id": "580fd16aca2a6b271562d8ba",
       "counter": 3, 
        "_cursor": "eyIkb2lkIjoiNTgwZmQxNmFjYTJhNmIyNzE1NjJkOGJhIn0" 
      } 
  ],
  "next": "eyIkb2lkIjoiNTgwZmQxNmFjYTJhNmIyNzE1NjJkOGJhIn0",
  "hasNext": "true" 
}

Add support for `first` and `last`

It would be nice if there was support for first and last arguments. As this lib already supports before and after. That would open the way to build relay compatible Connections with mongo-cursor-pagination.

The automated release is failing 🚨

🚨 The automated release from the master branch failed. 🚨

I recommend you give this issue a high priority, so other packages depending on you could benefit from your bug fixes and new features.

You can find below the list of errors reported by semantic-release. Each one of them has to be resolved in order to automatically publish your package. I’m sure you can resolve this πŸ’ͺ.

Errors are usually caused by a misconfiguration or an authentication problem. With each error reported below you will find explanation and guidance to help you to resolve it.

Once all the errors are resolved, semantic-release will release your package the next time you push a commit to the master branch. You can also manually restart the failed CI job that runs semantic-release.

If you are not sure how to resolve this, here is some links that can help you:

If those don’t help, or if this issue is reporting something you think isn’t right, you can always ask the humans behind semantic-release.


The commit message is not formatted correctly

Unfortunately this error doesn't have any additional information. Feel free to kindly ask the author of the @mixmaxhq/semantic-commitlint plugin to add more helpful information.


Good luck with your project ✨

Your semantic-release bot πŸ“¦πŸš€

I'm a bit confused over the generateCursorQuery expected params next / previous

/**
* Generates a cursor query that provides the offset capabilities
*
* @param {Object} params The params originally passed to `find` or `aggregate`
*
* @return {Object} a cursor offset query
*/
generateCursorQuery(params) {
if (!params.next && !params.previous) return {};
const sortAsc =
(!params.sortAscending && params.previous) || (params.sortAscending && !params.previous);
const comparisonOp = sortAsc ? '$gt' : '$lt';
const shouldSecondarySortOnId = params.paginatedField !== '_id';
// a `next` cursor will have precedence over a `previous` cursor.
const op = params.next || params.previous;
if (shouldSecondarySortOnId) {
return {
$or: [
{
[params.paginatedField]: {
[comparisonOp]: op[0],
},
},
{
[params.paginatedField]: {
$eq: op[0],
},
_id: {
[comparisonOp]: op[1],
},
},
],
};
}
return {
[params.paginatedField]: {
[comparisonOp]: op,
},
};
},

The docs say that next / previous should be a string and they are being fetched from the params here:

const op = params.next || params.previous;

but then they are being treated as an array here:

[comparisonOp]: op[0],

Also, is there a reason why an $or is performed on$lt/$gt and $eq over using $gte / $lte?

Thank you!

Where is _id, cmon :( ?

`module.exports = async(app) => {

app.addHook('onRequest', app.auth)

const queries = {
    'locations' : { fields: { _id: 1, name: 1 } },
    'companies': { fields: { _id: 1,  name:1 } },
    'users': { fields: { _id: 1, firstname: 1, lastname: 1 } }
}

app.get('/selection/:node', {} , async(r)=>{
    try {
        const node = xss(r.params.node)
        if(!queries.hasOwnProperty(node)) return app.warning();
        const params = await getParams(r.query)
        const ob = { ...params, ...queries[node] }
        console.log(ob)
        const rows = await cursor.find(app.db[node], ob)
        return app.success(rows)
    }catch(e){
        return onErr(app, e)
    }
})

}`

result:
{ results: [ { name: 'Krakov' }, { name: 'Tashkent' }, { name: 'London' }, { name: 'Aktau' }, { name: 'Turkistan' }, { name: 'Uralsk' }, { name: 'Aqtobe' }, { name: 'Astana' }, { name: 'Boston' }, { name: 'Amsterdam' } ], previous: 'eyIkb2lkIjoiNjE3NWI5MGRiZGY4ODQyZDIxYzFhMmQ1In0', hasPrevious: false, next: 'eyIkb2lkIjoiNjE2YWQ3NmQ4ODFhY2EyNDA1ZDI5ZmI2In0', hasNext: true }

Types for Typescript

Hi guys!
It's a good plugin to use with mongoose. The only thing I'm missing is support for TypeScript types. For example TypeScript doesn't know what is .paginate() in such expression: this.questionModel.paginate({})

Mongoose plugin does not cast the query before execution

I came across this issue today when querying a Mongoose schema with a ref and ObjectId to another collection.
Usually, Mongoose will cast the query before execution, and convert all ObjectId properties to ObjectIds before executing the query. When using the mongoose plugin, this does not happen, so fields that should be ObjectIds are instead queried as strings. The outcome is that no documents will be found.

Example:

const Model = new mongoose.Schema({
  otherId: {
    type: mongoose.Schema.Types.ObjectId,
    ref: 'OtherModel',
    index: true
  }
})

const options = {
  query: {
    { otherId: '619f1683a899a80029b18989' }
  }
}

const result = await Model.paginate(options)
console.log(result.results) // Length is 0 because otherId is handled as a string, not ObjectId.

The mongoose plugin should ideally cast the query before executing the find method. For example, the following casting works before executing the query:

options.query = new mongoose.Query().cast(Model, options.query)

Having the mongoose plugin handle this internally would likely be a good enhancement, so all of the Mongoose special sauce is handled ahead of the query on the collection.

Performance issues.

I have noticed a large performance hit when implementing mongo-cursor-pagination and I'm trying to determine if that is expected and if there is anything that can be improved. I'm using the Mongoose implementation set up like this:

const mongoose = require('mongoose');
const MongoPaging = require('mongo-cursor-pagination');

const logSchema = new mongoose.Schema({
  bucket: { type: mongoose.Schema.Types.ObjectId, ref: 'Bucket' },
  message: String,
  type: Number,
}, { timestamps: true });

logSchema.plugin(MongoPaging.mongoosePlugin);
logSchema.index({ createdAt: 1 });
logSchema.index({ updatedAt: 1 });
logSchema.index({ _id: 1 });

const Log = mongoose.model('Log', logSchema);
module.exports = Log;

I have created 300,000 test records.

As a control, I used a standard .find() call with no query and limited to 20 results.

console.time('test');
const logs = await Log.find({}).limit(20);
console.timeEnd('test');

Running it 3 times I got an average ~30ms response.

test: 32.08ms
test: 34.162ms
test: 29.232ms

Next I tried using .paginate based on createdAt (which is indexed), still with no query and same 20 limit.

console.time('test');
const logs = await Log.paginate({
    query: {},
    paginatedField: 'createdAt',
     limit: 20,
});
console.timeEnd('test');

Running it 3 times you can see I got an average ~500ms response, over 10x higher.

test: 571.058ms
test: 491.583ms
test: 483.037ms

Next I tried used the same .paginate query and limit, but added the next token returned from the previous response.

console.time('test');
const logs = await Log.paginate({
    query: {},
    paginatedField: 'createdAt',
    limit: 20,
    next: 'W3siJGRhdGUiOiIyMDIxLTA2LTE4VDE2OjQ2OjE5LjgxOFoifSx7IiRvaWQiOiI2MGNjY2RkYmUxZjZhMWM3NDZiZDYzMzUifV0',
});
console.timeEnd('test');

Running it 3 times I now got an average ~900ms response, almost 2x higher.

test: 878.252ms
test: 945.973ms
test: 854.034ms

Luckily it doesn't appear to get worse any further down the line, so it still more performant than a limit/offset style pagination. But it just seems odd to me that it is so much slower than a normal .find() when no extra querying is added and uses the same limit and indexed field. Plus it seems odd that adding a next param again dramatically increases response.

Getting findAsCursor is not a function exception

TypeError: collection.findAsCursor is not a function
    at Object.<anonymous> (/Users/bhaskaran/Documents/ksoc-new/api-graphql/node_modules/mongo-cursor-pagination/dist/node/find.js:129:36)
    at next (native)
    at step (/Users/bhaskaran/Documents/ksoc-new/api-graphql/node_modules/mongo-cursor-pagination/dist/node/find.js:1:191)
    at /Users/bhaskaran/Documents/ksoc-new/api-graphql/node_modules/mongo-cursor-pagination/dist/node/find.js:1:437

appreciate any help to fix this. cant find findAsCursor function in mongodb

_id as string regression

Hello !

The change from mongodb-extended-json to bson between version 7.2.0 and 7.3.0 apparently broke some of our queries.
We now get those errors:

Error: not an object instance
    at serializeDocument (./node_modules/mongo-cursor-pagination/node_modules/bson/lib/extended_json.js:304:53)
    at Object.stringify (./node_modules/mongo-cursor-pagination/node_modules/bson/lib/extended_json.js:192:7)
    at Object.module.exports.encode (./node_modules/mongo-cursor-pagination/src/utils/bsonUrlEncoding.js:11:33)
    at prepareResponse (./node_modules/mongo-cursor-pagination/src/utils/query.js:39:45)
    at module.exports (./node_modules/mongo-cursor-pagination/src/find.js:58:20)

Apparently bson refuses to double encode strings, where "foobar" would previously return "\"foobar\"", it now throws.
Should there be an exception in bsonUrlEncoding.js when it's already a string to proceed with JSON.stringify instead of EJSON.stringify ?

mongoosePlugin throws "collatedQuery.sort" in paginate method

Environment:

Node: v14.18.2
Npm: 6.14.15
Mongoose: 5.13.13
Mongo: 3.7.3
Mongo-cursor-pagination: 7.6.1

Steps to reproduce:

const mongoose = require('mongoose');
const MongoPaging = require('mongo-cursor-pagination');

const DummySchema = mongoose.Schema(
  {
    user: String,
  },
  {
    timestamps: { createdAt: 'date' },
  },
);

// this will add paginate function.
DummySchema.plugin(MongoPaging.mongoosePlugin);

const Dummy = mongoose.model('Dummy', DummySchema);

let paginateParams = {
  limit: 10,
  sort: '-date',
};

Dummy.paginate(paginateParams)
  .then(result => {
    console.log(result);
  })
  .catch(err => {
    console.log(err);
  });

Proposed solution:

mongo-cursor-pagination/src/find.js

change const query = collection[findMethod]({ $and: [cursorQuery, params.query] }, params.fields);
to: const query = await collection[findMethod]({ $and: [cursorQuery, params.query] }, params.fields);

The automated release is failing 🚨

🚨 The automated release from the master branch failed. 🚨

I recommend you give this issue a high priority, so other packages depending on you could benefit from your bug fixes and new features.

You can find below the list of errors reported by semantic-release. Each one of them has to be resolved in order to automatically publish your package. I’m sure you can resolve this πŸ’ͺ.

Errors are usually caused by a misconfiguration or an authentication problem. With each error reported below you will find explanation and guidance to help you to resolve it.

Once all the errors are resolved, semantic-release will release your package the next time you push a commit to the master branch. You can also manually restart the failed CI job that runs semantic-release.

If you are not sure how to resolve this, here is some links that can help you:

If those don’t help, or if this issue is reporting something you think isn’t right, you can always ask the humans behind semantic-release.


The commit message is not formatted correctly

Unfortunately this error doesn't have any additional information. Feel free to kindly ask the author of the @mixmaxhq/semantic-commitlint plugin to add more helpful information.


Good luck with your project ✨

Your semantic-release bot πŸ“¦πŸš€

Fields projection not working on .find method

For example, using the code below doesn't work

MongoPaging.find(this.collection, { query, next, limit, paginatedField, fields: { _id: 1, userName: 1, avatar: 1, cover: 1, biography: 1, role: 1, likesCount: 1 }, })

I was able to workaround this issue by adding:

MongoPaging.find(this.collection, { query, next, limit, paginatedField, fields: { projection: { _id: 1, userName: 1, avatar: 1, cover: 1, biography: 1, role: 1, likesCount: 1 } }, })

This should be fixed by changing on line 43 of the find.js to:

const query = collection[findMethod]({ $and: [cursorQuery, params.query] }, { projection: params.fields });

Support for MongoDB v4

Is this on your upgrade roadmap?

I actually started the process but there are a fair few failing tests so it might take some time to migrate. When trying to use the current codebase against v4, the pagination seems to not work at all - you can never request any page other than the first one (as it returns empty).

The automated release is failing 🚨

🚨 The automated release from the master branch failed. 🚨

I recommend you give this issue a high priority, so other packages depending on you could benefit from your bug fixes and new features.

You can find below the list of errors reported by semantic-release. Each one of them has to be resolved in order to automatically publish your package. I’m sure you can resolve this πŸ’ͺ.

Errors are usually caused by a misconfiguration or an authentication problem. With each error reported below you will find explanation and guidance to help you to resolve it.

Once all the errors are resolved, semantic-release will release your package the next time you push a commit to the master branch. You can also manually restart the failed CI job that runs semantic-release.

If you are not sure how to resolve this, here is some links that can help you:

If those don’t help, or if this issue is reporting something you think isn’t right, you can always ask the humans behind semantic-release.


The commit message is not formatted correctly

Unfortunately this error doesn't have any additional information. Feel free to kindly ask the author of the @mixmaxhq/semantic-commitlint plugin to add more helpful information.


Good luck with your project ✨

Your semantic-release bot πŸ“¦πŸš€

How to paginate an array within a document

Let's say i want to fetch a document by _id and want to paginate names field to retrieve only first two name objects

Document as below -

{
"_id": "1",
"names": [
   {
      "id": "344",
     "name": "A"
   }, 
  {
      "id": "144",
      "name": "B"
  },
  {
      "id": "244",
      "name": "C"
  },
  {
      "id": "444",
      "name": "D"
  }]
}

Expected output - Should also include hasNext, hasPrevious accordingly

{
"_id": "1",
"names": [
   {
      "id": "344",
     "name": "A"
   }, 
  {
      "id": "144",
      "name": "B"
  }]
}

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    πŸ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. πŸ“ŠπŸ“ˆπŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❀️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.