Skip to content

Commit 6faffe8

Browse files
committedJun 26, 2022
feat: add sub document pagination (feats: #174)
1 parent f092f32 commit 6faffe8

File tree

3 files changed

+308
-9
lines changed

3 files changed

+308
-9
lines changed
 

‎index.d.ts

+35-8
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,12 @@ declare module 'mongoose' {
2323
collation?: import('mongodb').CollationOptions | undefined;
2424
sort?: object | string | undefined;
2525
populate?:
26-
| PopulateOptions[]
27-
| string[]
28-
| PopulateOptions
29-
| string
30-
| PopulateOptions
31-
| undefined;
26+
| PopulateOptions[]
27+
| string[]
28+
| PopulateOptions
29+
| string
30+
| PopulateOptions
31+
| undefined;
3232
projection?: any;
3333
lean?: boolean | undefined;
3434
leanWithId?: boolean | undefined;
@@ -46,6 +46,32 @@ declare module 'mongoose' {
4646
options?: QueryOptions | undefined;
4747
}
4848

49+
interface SubPaginateOptions {
50+
select?: object | string | undefined;
51+
populate?:
52+
| PopulateOptions[]
53+
| string[]
54+
| PopulateOptions
55+
| string
56+
| PopulateOptions
57+
| undefined;
58+
pagination?: boolean | undefined;
59+
read?: ReadOptions | undefined;
60+
pagingOptions: SubDocumentPagingOptions | undefined;
61+
}
62+
63+
interface SubDocumentPagingOptions {
64+
populate?:
65+
| PopulateOptions[]
66+
| string[]
67+
| PopulateOptions
68+
| string
69+
| PopulateOptions
70+
| undefined;
71+
page?: number | undefined;
72+
limit?: number | undefined;
73+
}
74+
4975
interface PaginateResult<T> {
5076
docs: T[];
5177
totalDocs: number;
@@ -69,8 +95,8 @@ declare module 'mongoose' {
6995
O extends PaginateOptions = {}
7096
> = O['lean'] extends true
7197
? O['leanWithId'] extends true
72-
? LeanDocument<T & { id: string }>
73-
: LeanDocument<T>
98+
? LeanDocument<T & { id: string }>
99+
: LeanDocument<T>
74100
: HydratedDocument<T, TMethods, TVirtuals>;
75101

76102
interface PaginateModel<T, TQueryHelpers = {}, TMethods = {}>
@@ -91,6 +117,7 @@ declare function _(schema: mongoose.Schema): void;
91117
export = _;
92118
declare namespace _ {
93119
const paginate: { options: mongoose.PaginateOptions };
120+
const paginateSubDocs: { options: mongoose.PaginateOptions };
94121
class PaginationParameters<T, O extends mongoose.PaginateOptions> {
95122
constructor(request: { query?: Record<string, any> });
96123
getOptions: () => O;

‎src/index.js

+203
Original file line numberDiff line numberDiff line change
@@ -283,12 +283,215 @@ function paginate(query, options, callback) {
283283
});
284284
}
285285

286+
/**
287+
* Pagination process for sub-documents
288+
* internally, it would call `query.findOne`, return only one document
289+
*
290+
* @param {Object} query
291+
* @param {Object} options
292+
* @param {Function} callback
293+
*/
294+
function paginateSubDocs(query, options, callback) {
295+
/**
296+
* Populate sub documents with pagination fields
297+
*
298+
* @param {Object} query
299+
* @param {Object} populate origin populate option
300+
* @param {Object} option
301+
*/
302+
function getSubDocsPopulate(option) {
303+
/**
304+
* options properties for sub-documents pagination
305+
*
306+
* @param {String} populate: populate option for sub documents
307+
* @param {Number} page
308+
* @param {Number} limit
309+
*
310+
* @returns {String} countLabel
311+
*/
312+
let { populate, page = 1, limit = 10 } = option;
313+
314+
if (!populate) {
315+
throw new Error('populate is required');
316+
}
317+
318+
const offset = (page - 1) * limit;
319+
option.offset = offset;
320+
const pagination = {
321+
skip: offset,
322+
limit: limit,
323+
};
324+
325+
if (typeof populate === 'string') {
326+
populate = {
327+
path: populate,
328+
...pagination,
329+
};
330+
} else if (typeof populate === 'object' && !Array.isArray(populate)) {
331+
populate = Object.assign(populate, pagination);
332+
}
333+
option.populate = populate;
334+
335+
return populate;
336+
}
337+
338+
function populateResult(result, populate, callback) {
339+
return result.populate(populate, callback);
340+
}
341+
342+
/**
343+
* Convert result of sub-docs list to pagination like docs
344+
*
345+
* @param {Object} result query result
346+
* @param {Object} option pagination option
347+
*/
348+
function constructDocs(paginatedResult, option) {
349+
let { populate, offset = 0, page = 1, limit = 10 } = option;
350+
351+
const path = populate.path;
352+
const count = option.count;
353+
const paginatedDocs = paginatedResult[path];
354+
355+
if (!paginatedDocs) {
356+
throw new Error(
357+
`Parse error! Cannot find key on result with path ${path}`
358+
);
359+
}
360+
361+
page = Math.ceil((offset + 1) / limit);
362+
363+
// set default meta
364+
const meta = {
365+
docs: paginatedDocs,
366+
totalDocs: count || 1,
367+
limit: limit,
368+
page: page,
369+
prevPage: null,
370+
nextPage: null,
371+
hasPrevPage: false,
372+
hasNextPage: false,
373+
};
374+
375+
const totalPages = limit > 0 ? Math.ceil(count / limit) || 1 : null;
376+
meta.totalPages = totalPages;
377+
meta.pagingCounter = (page - 1) * limit + 1;
378+
379+
// Set prev page
380+
if (page > 1) {
381+
meta.hasPrevPage = true;
382+
meta.prevPage = page - 1;
383+
} else if (page == 1 && offset !== 0) {
384+
meta.hasPrevPage = true;
385+
meta.prevPage = 1;
386+
}
387+
388+
// Set next page
389+
if (page < totalPages) {
390+
meta.hasNextPage = true;
391+
meta.nextPage = page + 1;
392+
}
393+
394+
if (limit == 0) {
395+
meta.limit = 0;
396+
meta.totalPages = 1;
397+
meta.page = 1;
398+
meta.pagingCounter = 1;
399+
}
400+
401+
Object.defineProperty(paginatedResult, path, {
402+
value: meta,
403+
writable: false,
404+
});
405+
}
406+
407+
options = Object.assign(options, {
408+
customLabels: defaultOptions.customLabels,
409+
});
410+
411+
// options properties for main document query
412+
const {
413+
populate,
414+
read = {},
415+
select = '',
416+
pagination = true,
417+
pagingOptions,
418+
} = options;
419+
420+
const mQuery = this.findOne(query, options.projection);
421+
422+
if (read && read.pref) {
423+
/**
424+
* Determines the MongoDB nodes from which to read.
425+
* @param read.pref one of the listed preference options or aliases
426+
* @param read.tags optional tags for this query
427+
*/
428+
mQuery.read(read.pref, read.tags);
429+
}
430+
431+
if (select) {
432+
mQuery.select(select);
433+
}
434+
435+
return new Promise((resolve, reject) => {
436+
mQuery
437+
.exec()
438+
.then((result) => {
439+
let newPopulate = [];
440+
441+
if (populate) {
442+
newPopulate.push(newPopulate);
443+
}
444+
445+
if (pagination && pagingOptions) {
446+
if (Array.isArray(pagingOptions)) {
447+
pagingOptions.forEach((option) => {
448+
let populate = getSubDocsPopulate(option);
449+
option.count = result[populate.path].length;
450+
newPopulate.push(populate);
451+
});
452+
} else {
453+
let populate = getSubDocsPopulate(pagingOptions);
454+
pagingOptions.count = result[populate.path].length;
455+
newPopulate.push(populate);
456+
}
457+
}
458+
459+
populateResult(result, newPopulate, (err, paginatedResult) => {
460+
if (err) {
461+
callback?.(err, null);
462+
reject(err);
463+
return;
464+
}
465+
// convert paginatedResult to pagination docs
466+
if (pagination && pagingOptions) {
467+
if (Array.isArray(pagingOptions)) {
468+
pagingOptions.forEach((option) => {
469+
constructDocs(paginatedResult, option);
470+
});
471+
} else {
472+
constructDocs(paginatedResult, pagingOptions);
473+
}
474+
}
475+
476+
callback?.(null, paginatedResult);
477+
resolve(paginatedResult);
478+
});
479+
})
480+
.catch((err) => {
481+
console.error(err.message);
482+
callback?.(err, null);
483+
});
484+
});
485+
}
486+
286487
/**
287488
* @param {Schema} schema
288489
*/
289490
module.exports = (schema) => {
290491
schema.statics.paginate = paginate;
492+
schema.statics.paginateSubDocs = paginateSubDocs;
291493
};
292494

293495
module.exports.PaginationParameters = PaginationParametersHelper;
294496
module.exports.paginate = paginate;
497+
module.exports.paginateSubDocs = paginateSubDocs;

‎tests/index.js

+70-1
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,18 @@ let PaginationParameters = require('../dist/pagination-parameters');
88

99
let MONGO_URI = 'mongodb://localhost/mongoose_paginate_test';
1010

11+
let UserSchema = new mongoose.Schema({
12+
name: String,
13+
age: Number,
14+
gender: Number,
15+
});
16+
1117
let AuthorSchema = new mongoose.Schema({
1218
name: String,
1319
});
20+
1421
let Author = mongoose.model('Author', AuthorSchema);
22+
let User = mongoose.model('User', UserSchema);
1523

1624
let BookSchema = new mongoose.Schema({
1725
title: String,
@@ -21,6 +29,12 @@ let BookSchema = new mongoose.Schema({
2129
type: mongoose.Schema.ObjectId,
2230
ref: 'Author',
2331
},
32+
used: [
33+
{
34+
type: mongoose.Schema.ObjectId,
35+
ref: 'User',
36+
},
37+
],
2438
loc: Object,
2539
});
2640

@@ -87,11 +101,23 @@ describe('mongoose-paginate', function () {
87101
mongoose.connection.db.dropDatabase(done);
88102
});
89103

90-
before(function () {
104+
before(async function () {
91105
let book,
92106
books = [];
93107
let date = new Date();
94108

109+
// create users
110+
let users = [];
111+
for (let i = 0; i < 10; ++i) {
112+
const user = new User({
113+
name: randomString(),
114+
gender: 1,
115+
age: i,
116+
});
117+
const newUser = await User.create(user);
118+
users.push(newUser);
119+
}
120+
95121
return Author.create({
96122
name: 'Arthur Conan Doyle',
97123
}).then(function (author) {
@@ -102,6 +128,7 @@ describe('mongoose-paginate', function () {
102128
title: 'Book #' + i,
103129
date: new Date(date.getTime() + i),
104130
author: author._id,
131+
used: users,
105132
loc: {
106133
type: 'Point',
107134
coordinates: [-10.97, 20.77],
@@ -505,6 +532,33 @@ describe('mongoose-paginate', function () {
505532
expect(result.meta.total).to.equal(100);
506533
});
507534
});
535+
536+
it('Sub documents pagination', () => {
537+
var query = { title: 'Book #1' };
538+
var option = {
539+
pagingOptions: {
540+
populate: {
541+
path: 'used',
542+
},
543+
page: 2,
544+
limit: 3,
545+
},
546+
};
547+
548+
return Book.paginateSubDocs(query, option).then((result) => {
549+
expect(result.used.docs).to.have.length(3);
550+
expect(result.used.totalPages).to.equal(4);
551+
expect(result.used.page).to.equal(2);
552+
expect(result.used.limit).to.equal(3);
553+
expect(result.used.hasPrevPage).to.equal(true);
554+
expect(result.used.hasNextPage).to.equal(true);
555+
expect(result.used.prevPage).to.equal(1);
556+
expect(result.used.nextPage).to.equal(3);
557+
expect(result.used.pagingCounter).to.equal(4);
558+
expect(result.used.docs[0].age).to.equal(3);
559+
});
560+
});
561+
508562
/*
509563
it('2dsphere', function () {
510564
var query = {
@@ -710,3 +764,18 @@ describe('mongoose-paginate', function () {
710764
mongoose.disconnect(done);
711765
});
712766
});
767+
768+
function randomString(strLength, charSet) {
769+
var result = [];
770+
771+
strLength = strLength || 5;
772+
charSet =
773+
charSet || 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
774+
775+
while (strLength--) {
776+
// (note, fixed typo)
777+
result.push(charSet.charAt(Math.floor(Math.random() * charSet.length)));
778+
}
779+
780+
return result.join('');
781+
}

0 commit comments

Comments
 (0)
Please sign in to comment.