Skip to content

Commit 6559852

Browse files
committed
refactor: streamline mongoose plugin and enhance pagination functionality
1 parent 938fa61 commit 6559852

File tree

5 files changed

+98
-105
lines changed

5 files changed

+98
-105
lines changed

src/mongoose.plugin.ts

Lines changed: 15 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,63 +1,53 @@
1-
import { Schema } from 'mongoose';
21
import _ from 'underscore';
2+
import { Schema } from 'mongoose';
33
import find, { FindParams } from './find';
44
import search, { SearchParams } from './search';
55

66
interface PaginatePluginOptions {
7-
name?: string; // Name of the pagination function
8-
searchFnName?: string; // Name of the search function
7+
name?: string;
8+
searchFnName?: string;
99
}
1010

1111
/**
12-
* Mongoose plugin for adding `paginate` and `search` functionality.
13-
*
14-
* @param schema - The Mongoose schema to enhance.
15-
* @param options - Configuration options for the plugin.
12+
* Mongoose plugin
13+
* @param schema mongoose schema.
14+
* @param options plugin options
1615
*/
1716
export default function paginatePlugin(schema: Schema, options?: PaginatePluginOptions): void {
1817
/**
19-
* `paginate` function for querying paginated results.
20-
*
21-
* @param params - Query parameters for the pagination.
22-
* @returns The paginated results.
18+
* paginate function
19+
* @param params required parameter
2320
*/
24-
const findFn = async function(this: any, params: FindParams): Promise<any> {
21+
const findFn = function(this: any, params: FindParams): Promise<any> {
2522
if (!this.collection) {
2623
throw new Error('collection property not found');
2724
}
28-
2925
params = _.extend({}, params);
26+
3027
return find(this.collection, params);
3128
};
3229

3330
/**
34-
* `search` function for performing a search query.
35-
*
36-
* @param searchString - The string to search for.
37-
* @param params - Additional query parameters.
38-
* @returns The search results.
31+
* search function
32+
* @param searchString String to search on. Required parameter
33+
* @param params search parameters
3934
*/
40-
const searchFn = async function(
41-
this: any,
42-
searchString: string,
43-
params: SearchParams
44-
): Promise<any> {
35+
const searchFn = function(this: any, searchString: string, params: SearchParams): Promise<any> {
4536
if (!this.collection) {
4637
throw new Error('collection property not found');
4738
}
4839

4940
params = _.extend({}, params);
41+
5042
return search(this.collection, searchString, params);
5143
};
5244

53-
// Attach the `paginate` function to the schema statics
5445
if (options?.name) {
5546
schema.statics[options.name] = findFn;
5647
} else {
5748
schema.statics.paginate = findFn;
5849
}
5950

60-
// Attach the `search` function to the schema statics
6151
if (options?.searchFnName) {
6252
schema.statics[options.searchFnName] = searchFn;
6353
} else {

src/search.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ export default async function search<T = Document>(
3535
searchString: string,
3636
params: SearchParams
3737
): Promise<SearchResponse<T>> {
38+
if (_.isString(params.limit)) params.limit = parseInt((params.limit as any) as string, 10);
3839
if (params.next) {
3940
params.next = bsonUrlEncoding.decode(params.next as string);
4041
}

src/utils/query.ts

Lines changed: 71 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,127 +1,144 @@
11
import objectPath from 'object-path';
22
import bsonUrlEncoding from './bsonUrlEncoding';
33

4+
export type PaginationToken = { _id: string; [key: string]: any } | string | [any, unknown];
5+
46
export type PaginationParams = {
57
paginatedField?: string;
68
sortCaseInsensitive?: boolean;
79
sortAscending?: boolean;
8-
previous?: string | [unknown, unknown];
9-
next?: string | [unknown, unknown];
10+
previous?: PaginationToken;
11+
next?: PaginationToken;
1012
limit?: number;
11-
after?: string | [unknown, unknown];
13+
after?: PaginationToken;
1214
hint?: string;
1315
before?: string;
1416
};
1517

1618
export type PaginationResponse<T> = {
1719
results: T[];
18-
previous: string | null;
20+
previous: PaginationToken; // Updated to reflect a more specific type
1921
hasPrevious: boolean;
20-
next: string | null;
22+
next: PaginationToken; // Updated to reflect a more specific type
2123
hasNext: boolean;
2224
};
2325

24-
type SortObject = Record<string, 1 | -1>;
25-
26-
type CursorQuery = Record<string, any>;
27-
2826
/**
2927
* Helper function to encode pagination tokens.
3028
*
3129
* NOTE: this function modifies the passed-in `response` argument directly.
3230
*
33-
* @param params - Pagination parameters
34-
* @param response - The response object to modify
31+
* @param {Object} params
32+
* @param {String} paginatedField
33+
* @param {boolean} sortCaseInsensitive
34+
*
35+
* @param {Object} response The response
36+
* @param {String?} previous
37+
* @param {String?} next
38+
*
39+
* @returns void
3540
*/
36-
export function encodePaginationTokens<T>(
37-
params: PaginationParams,
38-
response: PaginationResponse<T>,
39-
previous: T | null,
40-
next: T | null
41-
): void {
41+
function encodePaginationTokens(params: PaginationParams, response: PaginationResponse<any>): void {
4242
const shouldSecondarySortOnId = params.paginatedField !== '_id';
4343

44-
if (previous) {
45-
let previousPaginatedField = objectPath.get(previous, params.paginatedField);
44+
if (response.previous) {
45+
let previousPaginatedField = objectPath.get(response.previous, params.paginatedField);
4646
if (params.sortCaseInsensitive) {
4747
previousPaginatedField = previousPaginatedField?.toLowerCase?.() ?? '';
4848
}
49-
response.previous = shouldSecondarySortOnId
50-
? bsonUrlEncoding.encode([previousPaginatedField, (previous as any)._id])
51-
: bsonUrlEncoding.encode(previousPaginatedField);
49+
if (shouldSecondarySortOnId) {
50+
if (
51+
typeof response.previous === 'object' &&
52+
response.previous !== null &&
53+
'_id' in response.previous
54+
) {
55+
response.previous = bsonUrlEncoding.encode([previousPaginatedField, response.previous._id]);
56+
}
57+
} else {
58+
response.previous = bsonUrlEncoding.encode(previousPaginatedField);
59+
}
5260
}
53-
54-
if (next) {
55-
let nextPaginatedField = objectPath.get(next, params.paginatedField);
61+
if (response.next) {
62+
let nextPaginatedField = objectPath.get(response.next, params.paginatedField);
5663
if (params.sortCaseInsensitive) {
5764
nextPaginatedField = nextPaginatedField?.toLowerCase?.() ?? '';
5865
}
59-
response.next = shouldSecondarySortOnId
60-
? bsonUrlEncoding.encode([nextPaginatedField, (next as any)._id])
61-
: bsonUrlEncoding.encode(nextPaginatedField);
66+
if (shouldSecondarySortOnId) {
67+
if (typeof response.next === 'object' && response.next !== null && '_id' in response.next) {
68+
response.next = bsonUrlEncoding.encode([nextPaginatedField, response.next._id]);
69+
}
70+
} else {
71+
response.next = bsonUrlEncoding.encode(nextPaginatedField);
72+
}
6273
}
6374
}
6475

6576
/**
6677
* Parses the raw results from a find or aggregate query and generates a response object that
67-
* contains various pagination properties.
78+
* contain the various pagination properties
79+
*
80+
* @param {Object[]} results the results from a query
81+
* @param {Object} params The params originally passed to `find` or `aggregate`
6882
*
69-
* @param results - The results from a query
70-
* @param params - The parameters originally passed to `find` or `aggregate`
71-
* @returns The object containing pagination properties
83+
* @return {Object} The object containing pagination properties
7284
*/
73-
export function prepareResponse<T>(results: T[], params: PaginationParams): PaginationResponse<T> {
85+
function prepareResponse(results: any[], params: any): any {
7486
const hasMore = results.length > params.limit;
75-
87+
// Remove the extra element that we added to 'peek' to see if there were more entries.
7688
if (hasMore) results.pop();
7789

7890
const hasPrevious = !!params.next || !!(params.previous && hasMore);
7991
const hasNext = !!params.previous || hasMore;
8092

93+
// If we sorted reverse to get the previous page, correct the sort order.
8194
if (params.previous) results = results.reverse();
8295

83-
const response: PaginationResponse<T> = {
96+
const response = {
8497
results,
98+
previous: results[0],
8599
hasPrevious,
100+
next: results[results.length - 1],
86101
hasNext,
87-
previous: null,
88-
next: null,
89102
};
90103

91-
const previous = results[0] || null;
92-
const next = results[results.length - 1] || null;
93-
94-
encodePaginationTokens(params, response, previous, next);
104+
encodePaginationTokens(params, response);
95105

96106
return response;
97107
}
98108

99109
/**
100-
* Generates a `$sort` object given the parameters.
110+
* Generates a `$sort` object given the parameters
111+
*
112+
* @param {Object} params The params originally passed to `find` or `aggregate`
101113
*
102-
* @param params - The parameters originally passed to `find` or `aggregate`
103-
* @returns A sort object
114+
* @return {Object} a sort object
104115
*/
105-
export function generateSort(params: PaginationParams): SortObject {
116+
function generateSort(params: any): any {
106117
const sortAsc =
107118
(!params.sortAscending && params.previous) || (params.sortAscending && !params.previous);
108119
const sortDir = sortAsc ? 1 : -1;
109120

110-
if (params.paginatedField === '_id') {
111-
return { _id: sortDir };
121+
if (params.paginatedField == '_id') {
122+
return {
123+
_id: sortDir,
124+
};
112125
} else {
113126
const field = params.sortCaseInsensitive ? '__lc' : params.paginatedField;
114-
return { [field]: sortDir, _id: sortDir };
127+
return {
128+
[field]: sortDir,
129+
_id: sortDir,
130+
};
115131
}
116132
}
117133

118-
/**
119-
* Generates a cursor query that provides the offset capabilities.
134+
function /**
135+
* Generates a cursor query that provides the offset capabilities
120136
*
121-
* @param params - The parameters originally passed to `find` or `aggregate`
122-
* @returns A cursor offset query
137+
* @param {Object} params The params originally passed to `find` or `aggregate`
138+
*
139+
* @return {Object} a cursor offset query
123140
*/
124-
export function generateCursorQuery(params: PaginationParams): CursorQuery {
141+
generateCursorQuery(params: any): any {
125142
if (!params.next && !params.previous) return {};
126143

127144
const sortAsc =
@@ -213,9 +230,4 @@ export function generateCursorQuery(params: PaginationParams): CursorQuery {
213230
}
214231
}
215232

216-
export default {
217-
prepareResponse,
218-
encodePaginationTokens,
219-
generateSort,
220-
generateCursorQuery,
221-
};
233+
export { encodePaginationTokens, prepareResponse, generateSort, generateCursorQuery };

test/mongoosePlugin.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ describe('mongoose plugin', () => {
6767

6868
it('returns data in the expected format', async () => {
6969
const data = await (Post as any).paginate();
70-
var hasOwnProperty = Object.prototype.hasOwnProperty;
70+
const hasOwnProperty = Object.prototype.hasOwnProperty;
7171
expect(hasOwnProperty.call(data, 'results')).toBe(true);
7272
expect(hasOwnProperty.call(data, 'previous')).toBe(true);
7373
expect(hasOwnProperty.call(data, 'hasPrevious')).toBe(true);
@@ -88,7 +88,7 @@ describe('mongoose plugin', () => {
8888

8989
it('returns data in the expected format for search function', async () => {
9090
const data = await (Post as any).search('Post #1', { limit: 3 });
91-
var hasOwnProperty = Object.prototype.hasOwnProperty;
91+
const hasOwnProperty = Object.prototype.hasOwnProperty;
9292
expect(hasOwnProperty.call(data, 'results')).toBe(true);
9393
expect(hasOwnProperty.call(data, 'next')).toBe(true);
9494
});

test/utils/query.test.ts

Lines changed: 9 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,38 @@
11
import bsonUrlEncoding from '../../src/utils/bsonUrlEncoding';
2-
import {
3-
encodePaginationTokens,
4-
PaginationResponse,
5-
generateCursorQuery,
6-
} from '../../src/utils/query';
2+
import { encodePaginationTokens, generateCursorQuery } from '../../src/utils/query';
73

84
describe('encodePaginationTokens', () => {
95
it('encodes the pagination tokens on the passed-in response object', () => {
10-
type PaginatedType = { _id: string };
116
const params = {
127
paginatedField: '_id',
138
};
14-
const response: PaginationResponse<PaginatedType> = {
9+
const response = {
1510
results: [],
16-
previous: null,
11+
previous: { _id: '456' },
1712
hasPrevious: false,
18-
next: null,
13+
next: { _id: '789' },
1914
hasNext: false,
2015
};
21-
const previous: PaginatedType = { _id: '456' };
22-
const next: PaginatedType = { _id: '789' };
2316

24-
encodePaginationTokens<PaginatedType>(params, response, previous, next);
17+
encodePaginationTokens(params, response);
2518

2619
expect(response.next).toEqual(bsonUrlEncoding.encode('789'));
2720
expect(response.previous).toEqual(bsonUrlEncoding.encode('456'));
2821
});
2922

3023
it("constructs pagination tokens using both the _id and the paginatedField if the latter isn't the former", () => {
31-
type PaginatedType = { _id: string; name: string };
3224
const params = {
3325
paginatedField: 'name',
3426
};
35-
const response: PaginationResponse<PaginatedType> = {
27+
const response = {
3628
results: [],
37-
previous: null,
29+
previous: { _id: '456', name: 'Test' },
3830
hasPrevious: false,
39-
next: null,
31+
next: { _id: '789', name: 'Test 2' },
4032
hasNext: false,
4133
};
42-
const previous: PaginatedType = { _id: '456', name: 'Test' };
43-
const next: PaginatedType = { _id: '789', name: 'Test 2' };
4434

45-
encodePaginationTokens<PaginatedType>(params, response, previous, next);
35+
encodePaginationTokens(params, response);
4636

4737
expect(response.next).toEqual(bsonUrlEncoding.encode(['Test 2', '789']));
4838
expect(response.previous).toEqual(bsonUrlEncoding.encode(['Test', '456']));

0 commit comments

Comments
 (0)