Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add sub-document pagination (feats: #174) #175

Merged
merged 4 commits into from
Jun 30, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 23 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ Prior to version `1.5.0`, types need to be installed from [DefinitelyTyped](http
To declare a `PaginateModel` in your Typescript files:

```ts
import mongoose from 'mongoose'
import mongoose from 'mongoose';
import paginate from 'mongoose-paginate-v2';

// declare your schema
Expand Down Expand Up @@ -361,6 +361,28 @@ Model.paginate({}, options, function (err, result) {
});
```

### Pagination for sub documents

If you want to paginate your sub-documents, here is the method you can use.

```js
var query = { name: 'John' }
var option = {
select: 'name follower'
pagingOptions: {
// your populate option
populate: {
path: 'follower',
},
page: 2,
limit: 10,
},
};

// Only one document (which object key with name John) will be return
const result = await Book.paginateSubDocs(query, option);
```

#### AllowDiskUse for large datasets

Sets the allowDiskUse option, which allows the MongoDB server to use more than 100 MB for query. This option can let you work around `QueryExceededMemoryLimitNoDiskUseAllowed` errors from the MongoDB server.
Expand Down
43 changes: 35 additions & 8 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,12 @@ declare module 'mongoose' {
collation?: import('mongodb').CollationOptions | undefined;
sort?: object | string | undefined;
populate?:
| PopulateOptions[]
| string[]
| PopulateOptions
| string
| PopulateOptions
| undefined;
| PopulateOptions[]
| string[]
| PopulateOptions
| string
| PopulateOptions
| undefined;
projection?: any;
lean?: boolean | undefined;
leanWithId?: boolean | undefined;
Expand All @@ -46,6 +46,32 @@ declare module 'mongoose' {
options?: QueryOptions | undefined;
}

interface SubPaginateOptions {
select?: object | string | undefined;
populate?:
| PopulateOptions[]
| string[]
| PopulateOptions
| string
| PopulateOptions
| undefined;
pagination?: boolean | undefined;
read?: ReadOptions | undefined;
pagingOptions: SubDocumentPagingOptions | undefined;
}

interface SubDocumentPagingOptions {
populate?:
| PopulateOptions[]
| string[]
| PopulateOptions
| string
| PopulateOptions
| undefined;
page?: number | undefined;
limit?: number | undefined;
}

interface PaginateResult<T> {
docs: T[];
totalDocs: number;
Expand All @@ -69,8 +95,8 @@ declare module 'mongoose' {
O extends PaginateOptions = {}
> = O['lean'] extends true
? O['leanWithId'] extends true
? LeanDocument<T & { id: string }>
: LeanDocument<T>
? LeanDocument<T & { id: string }>
: LeanDocument<T>
: HydratedDocument<T, TMethods, TVirtuals>;

interface PaginateModel<T, TQueryHelpers = {}, TMethods = {}>
Expand All @@ -91,6 +117,7 @@ declare function _(schema: mongoose.Schema): void;
export = _;
declare namespace _ {
const paginate: { options: mongoose.PaginateOptions };
const paginateSubDocs: { options: mongoose.PaginateOptions };
class PaginationParameters<T, O extends mongoose.PaginateOptions> {
constructor(request: { query?: Record<string, any> });
getOptions: () => O;
Expand Down
195 changes: 195 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -283,12 +283,207 @@ function paginate(query, options, callback) {
});
}

/**
* Pagination process for sub-documents
* internally, it would call `query.findOne`, return only one document
*
* @param {Object} query
* @param {Object} options
* @param {Function} callback
*/
function paginateSubDocs(query, options, callback) {
/**
* Populate sub documents with pagination fields
*
* @param {Object} query
* @param {Object} populate origin populate option
* @param {Object} option
*/
function getSubDocsPopulate(option) {
// options properties for sub-documents pagination
let { populate, page = 1, limit = 10 } = option;

if (!populate) {
throw new Error('populate is required');
}

const offset = (page - 1) * limit;
option.offset = offset;
const pagination = {
skip: offset,
limit: limit,
};

if (typeof populate === 'string') {
populate = {
path: populate,
...pagination,
};
} else if (typeof populate === 'object' && !Array.isArray(populate)) {
populate = Object.assign(populate, pagination);
}
option.populate = populate;

return populate;
}

function populateResult(result, populate, callback) {
return result.populate(populate, callback);
}

/**
* Convert result of sub-docs list to pagination like docs
*
* @param {Object} result query result
* @param {Object} option pagination option
*/
function constructDocs(paginatedResult, option) {
let { populate, offset = 0, page = 1, limit = 10 } = option;

const path = populate.path;
const count = option.count;
const paginatedDocs = paginatedResult[path];

if (!paginatedDocs) {
throw new Error(
`Parse error! Cannot find key on result with path ${path}`
);
}

page = Math.ceil((offset + 1) / limit);

// set default meta
const meta = {
docs: paginatedDocs,
totalDocs: count || 1,
limit: limit,
page: page,
prevPage: null,
nextPage: null,
hasPrevPage: false,
hasNextPage: false,
};

const totalPages = limit > 0 ? Math.ceil(count / limit) || 1 : null;
meta.totalPages = totalPages;
meta.pagingCounter = (page - 1) * limit + 1;

// Set prev page
if (page > 1) {
meta.hasPrevPage = true;
meta.prevPage = page - 1;
} else if (page == 1 && offset !== 0) {
meta.hasPrevPage = true;
meta.prevPage = 1;
}

// Set next page
if (page < totalPages) {
meta.hasNextPage = true;
meta.nextPage = page + 1;
}

if (limit == 0) {
meta.limit = 0;
meta.totalPages = 1;
meta.page = 1;
meta.pagingCounter = 1;
}

Object.defineProperty(paginatedResult, path, {
value: meta,
writable: false,
});
}

options = Object.assign(options, {
customLabels: defaultOptions.customLabels,
});

// options properties for main document query
const {
populate,
read = {},
select = '',
pagination = true,
pagingOptions,
} = options;

const mQuery = this.findOne(query, options.projection);

if (read && read.pref) {
/**
* Determines the MongoDB nodes from which to read.
* @param read.pref one of the listed preference options or aliases
* @param read.tags optional tags for this query
*/
mQuery.read(read.pref, read.tags);
}

if (select) {
mQuery.select(select);
}

return new Promise((resolve, reject) => {
mQuery
.exec()
.then((result) => {
let newPopulate = [];

if (populate) {
newPopulate.push(newPopulate);
}

if (pagination && pagingOptions) {
if (Array.isArray(pagingOptions)) {
pagingOptions.forEach((option) => {
let populate = getSubDocsPopulate(option);
option.count = result[populate.path].length;
newPopulate.push(populate);
});
} else {
let populate = getSubDocsPopulate(pagingOptions);
pagingOptions.count = result[populate.path].length;
newPopulate.push(populate);
}
}

populateResult(result, newPopulate, (err, paginatedResult) => {
if (err) {
callback?.(err, null);
reject(err);
return;
}
// convert paginatedResult to pagination docs
if (pagination && pagingOptions) {
if (Array.isArray(pagingOptions)) {
pagingOptions.forEach((option) => {
constructDocs(paginatedResult, option);
});
} else {
constructDocs(paginatedResult, pagingOptions);
}
}

callback?.(null, paginatedResult);
resolve(paginatedResult);
});
})
.catch((err) => {
console.error(err.message);
callback?.(err, null);
});
});
}

/**
* @param {Schema} schema
*/
module.exports = (schema) => {
schema.statics.paginate = paginate;
schema.statics.paginateSubDocs = paginateSubDocs;
};

module.exports.PaginationParameters = PaginationParametersHelper;
module.exports.paginate = paginate;
module.exports.paginateSubDocs = paginateSubDocs;
Loading