@andrew_l/mongo-pagination 0.3.21 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/dist/_chunks/QueryPaginator.d.mts +198 -0
  2. package/dist/_chunks/QueryPaginator.d.mts.map +1 -0
  3. package/dist/_chunks/withMongoosePagination.mjs +489 -0
  4. package/dist/_chunks/withMongoosePagination.mjs.map +1 -0
  5. package/dist/index.d.mts +13 -14
  6. package/dist/index.d.mts.map +1 -0
  7. package/dist/index.mjs +36 -46
  8. package/dist/index.mjs.map +1 -1
  9. package/dist/mongoose-7.d.mts +9 -12
  10. package/dist/mongoose-7.d.mts.map +1 -0
  11. package/dist/mongoose-7.mjs +14 -27
  12. package/dist/mongoose-7.mjs.map +1 -1
  13. package/dist/mongoose-8.d.mts +9 -12
  14. package/dist/mongoose-8.d.mts.map +1 -0
  15. package/dist/mongoose-8.mjs +14 -27
  16. package/dist/mongoose-8.mjs.map +1 -1
  17. package/package.json +14 -11
  18. package/dist/index.cjs +0 -56
  19. package/dist/index.cjs.map +0 -1
  20. package/dist/index.d.cts +0 -150
  21. package/dist/index.d.ts +0 -150
  22. package/dist/mongoose-7.cjs +0 -36
  23. package/dist/mongoose-7.cjs.map +0 -1
  24. package/dist/mongoose-7.d.cts +0 -16
  25. package/dist/mongoose-7.d.ts +0 -16
  26. package/dist/mongoose-8.cjs +0 -36
  27. package/dist/mongoose-8.cjs.map +0 -1
  28. package/dist/mongoose-8.d.cts +0 -16
  29. package/dist/mongoose-8.d.ts +0 -16
  30. package/dist/shared/mongo-pagination.C762g8l8.mjs +0 -663
  31. package/dist/shared/mongo-pagination.C762g8l8.mjs.map +0 -1
  32. package/dist/shared/mongo-pagination.D7Mb8JCq.d.cts +0 -195
  33. package/dist/shared/mongo-pagination.D7Mb8JCq.d.mts +0 -195
  34. package/dist/shared/mongo-pagination.D7Mb8JCq.d.ts +0 -195
  35. package/dist/shared/mongo-pagination.DO2_rA5-.cjs +0 -676
  36. package/dist/shared/mongo-pagination.DO2_rA5-.cjs.map +0 -1
@@ -1,663 +0,0 @@
1
- import { crc32, base62Fast, intersection, isEmpty, isObject, isFunction, isNumber, deepClone, cleanEmpty, toError, pick } from '@andrew_l/toolkit';
2
- import Kareem from 'kareem';
3
- import { ObjectId, MongoInvalidArgumentError } from 'mongodb';
4
- import { createExtension, defineStructure, CORE_TYPES } from '@andrew_l/tl-pack';
5
- import d from 'debug';
6
- import { Transform } from 'node:stream';
7
-
8
- const OBJECT_ID_TOKEN = 100;
9
- const extensions = [
10
- createExtension(OBJECT_ID_TOKEN, {
11
- encode(value) {
12
- if (value?._bsontype?.toLowerCase() === "objectid") {
13
- this.writeBytes(value.id);
14
- }
15
- },
16
- decode() {
17
- const bytes = this.readBytes();
18
- return new ObjectId(bytes);
19
- }
20
- })
21
- ];
22
- const TokenStructure = defineStructure({
23
- name: "PaginationToken",
24
- version: 1,
25
- checksum: true,
26
- properties: {
27
- modelNameCrc: { type: CORE_TYPES.Int32, required: true },
28
- keys: { type: [String], required: true },
29
- sortDirection: { type: [null], required: true },
30
- sortValues: { type: [null], required: true },
31
- payload: { type: Object }
32
- }
33
- });
34
- class Token {
35
- /**
36
- * The CRC of the model name, used for verify the model.
37
- */
38
- modelNameCRC = 0;
39
- /**
40
- * The sorting direction used for pagination building pagination query.
41
- */
42
- sortDirection = {};
43
- /**
44
- * The sorting values associated with the pagination.
45
- */
46
- sortValues = {};
47
- /**
48
- * The payload of the token, which can contain any associated metadata or data (may be `null`).
49
- */
50
- payload = null;
51
- constructor(options) {
52
- if (options) {
53
- Object.assign(this, { ...options, modelName: void 0 });
54
- if (options.modelName) {
55
- this.modelNameCRC = crc32(options.modelName);
56
- }
57
- }
58
- }
59
- /**
60
- * Retrieves the string representation of token.
61
- */
62
- stringify() {
63
- return base62Fast.encode(this.buffer());
64
- }
65
- /**
66
- * Retrieves binary buffer representation of token.
67
- */
68
- buffer() {
69
- const struct = new TokenStructure({
70
- modelNameCrc: this.modelNameCRC,
71
- sortDirection: Object.values(this.sortDirection),
72
- sortValues: Object.values(this.sortValues),
73
- keys: Object.keys(this.sortValues),
74
- payload: this.payload
75
- });
76
- return struct.toBuffer({ extensions });
77
- }
78
- /**
79
- * Encode token from provided value
80
- */
81
- static from(value) {
82
- const buffer = Buffer.isBuffer(value) || value instanceof Uint8Array ? value : base62Fast.decode(value);
83
- const struct = TokenStructure.fromBuffer(buffer);
84
- const token = new Token();
85
- token.modelNameCRC = struct.modelNameCrc;
86
- token.sortDirection = {};
87
- token.sortValues = {};
88
- token.payload = struct.payload ?? {};
89
- for (let idx = 0; idx < struct.keys.length; idx++) {
90
- token.sortValues[struct.keys[idx]] = struct.sortValues[idx];
91
- token.sortDirection[struct.keys[idx]] = struct.sortDirection[idx];
92
- }
93
- return token;
94
- }
95
- }
96
- function parseToken(value) {
97
- return Token.from(value);
98
- }
99
- function createToken(options) {
100
- return new Token(options);
101
- }
102
-
103
- const debug = {
104
- init: d("mongo-pagination:init"),
105
- tweak: d("mongo-pagination:tweak"),
106
- exec: d("mongo-pagination:exec"),
107
- stream: d("mongo-pagination:query:stream")
108
- };
109
-
110
- function isEmptyObject(obj) {
111
- return !isObject(obj) || isEmpty(obj);
112
- }
113
- function sameKeys(...args) {
114
- return intersection(...args.map((v) => Object.keys(v)));
115
- }
116
- function defineGetter(obj, key, getterFn) {
117
- delete obj[key];
118
- Object.defineProperty(obj, key, {
119
- get: getterFn,
120
- enumerable: true,
121
- configurable: true
122
- });
123
- }
124
- function isLastKeys(value, keys, strictOrder) {
125
- const objKeys = isObject(value) ? Object.keys(value) : [];
126
- const lastKeys = objKeys.slice(-keys.length);
127
- if (lastKeys.length !== keys.length) {
128
- return false;
129
- }
130
- for (let idx = 0; idx < keys.length; idx++) {
131
- {
132
- if (!lastKeys.includes(keys[idx])) {
133
- return false;
134
- }
135
- }
136
- }
137
- return true;
138
- }
139
- function toObject(value) {
140
- return isFunction(value?.toObject) ? value?.toObject() : value;
141
- }
142
-
143
- function prepareDirection(direction = 1) {
144
- const value = `${direction}`.toLowerCase();
145
- if (isMeta(direction)) return direction;
146
- switch (value) {
147
- case "ascending":
148
- case "asc":
149
- case "1":
150
- return 1;
151
- case "descending":
152
- case "desc":
153
- case "-1":
154
- return -1;
155
- default:
156
- throw new MongoInvalidArgumentError(
157
- `Invalid sort direction: ${JSON.stringify(direction)}`
158
- );
159
- }
160
- }
161
- function isMeta(t) {
162
- return typeof t === "object" && t != null && "$meta" in t && typeof t.$meta === "string";
163
- }
164
- function isPair(t) {
165
- if (Array.isArray(t) && t.length === 2) {
166
- try {
167
- prepareDirection(t[1]);
168
- return true;
169
- } catch {
170
- return false;
171
- }
172
- }
173
- return false;
174
- }
175
- function isDeep(t) {
176
- return Array.isArray(t) && Array.isArray(t[0]);
177
- }
178
- function isMap(t) {
179
- return t instanceof Map && t.size > 0;
180
- }
181
- function pairToMap(v) {
182
- return /* @__PURE__ */ new Map([[`${v[0]}`, prepareDirection([v[1]])]]);
183
- }
184
- function deepToMap(t) {
185
- const sortEntries = t.map(([k, v]) => [
186
- `${k}`,
187
- prepareDirection(v)
188
- ]);
189
- return new Map(sortEntries);
190
- }
191
- function stringsToMap(t) {
192
- const sortEntries = t.map((key) => [`${key}`, 1]);
193
- return new Map(sortEntries);
194
- }
195
- function objectToMap(t) {
196
- const sortEntries = Object.entries(t).map(([k, v]) => [
197
- `${k}`,
198
- prepareDirection(v)
199
- ]);
200
- return new Map(sortEntries);
201
- }
202
- function mapToMap(t) {
203
- const sortEntries = Array.from(t).map(([k, v]) => [
204
- `${k}`,
205
- prepareDirection(v)
206
- ]);
207
- return new Map(sortEntries);
208
- }
209
- function formatSort(sort, direction) {
210
- if (sort == null) return void 0;
211
- if (typeof sort === "string")
212
- return /* @__PURE__ */ new Map([[sort, prepareDirection(direction)]]);
213
- if (typeof sort !== "object") {
214
- throw new MongoInvalidArgumentError(
215
- `Invalid sort format: ${JSON.stringify(sort)} Sort must be a valid object`
216
- );
217
- }
218
- if (!Array.isArray(sort)) {
219
- return isMap(sort) ? mapToMap(sort) : Object.keys(sort).length ? objectToMap(sort) : void 0;
220
- }
221
- if (!sort.length) return void 0;
222
- if (isDeep(sort)) return deepToMap(sort);
223
- if (isPair(sort)) return pairToMap(sort);
224
- return stringsToMap(sort);
225
- }
226
-
227
- const DEF_LIMIT = 10;
228
- function createRangeFilter(sortDirection, sortValues) {
229
- const sortCmd = formatSort(sortDirection) ?? {};
230
- const keys = [];
231
- for (const [sortKey, sortValue] of sortCmd.entries()) {
232
- if (sortValues[sortKey] === void 0) continue;
233
- if (!isNumber(sortValue)) continue;
234
- keys.push(sortKey);
235
- }
236
- const op = (sortKey) => {
237
- const direction = sortCmd.get(sortKey);
238
- return direction === 1 ? "$gt" : "$lt";
239
- };
240
- const val = (sortKey) => sortValues[sortKey];
241
- if (keys.length === 0) {
242
- return {};
243
- } else if (keys.length === 1) {
244
- const sortKey = keys[0];
245
- return {
246
- [sortKey]: {
247
- [op(sortKey)]: val(sortKey)
248
- }
249
- };
250
- }
251
- const $or = [];
252
- const keysAmount = keys.length;
253
- for (let i = 0; i < keysAmount; i++) {
254
- const sortKey = keys[i];
255
- if (i === 0) {
256
- $or.push({
257
- [sortKey]: { [op(sortKey)]: val(sortKey) }
258
- });
259
- } else {
260
- const filter = {};
261
- keys.slice(0, i).map((sortKey3) => {
262
- filter[sortKey3] = { $eq: val(sortKey3) };
263
- });
264
- const sortKey2 = keys[i];
265
- filter[sortKey2] = {
266
- [op(sortKey2)]: val(sortKey2)
267
- };
268
- $or.push(filter);
269
- }
270
- }
271
- if ($or.length === 1) return $or[0];
272
- return { $or };
273
- }
274
- function mergeFilters(first, second) {
275
- switch (true) {
276
- // Just return second
277
- case isEmptyObject(first):
278
- return deepClone(second);
279
- // Just return first
280
- case isEmptyObject(second):
281
- return deepClone(first);
282
- // Just assign when no conflicts
283
- case sameKeys(first, second).length === 0:
284
- return Object.assign(deepClone(first), deepClone(second));
285
- // Merge with $and way
286
- case Array.isArray(first.$and):
287
- first = deepClone(first);
288
- second = deepClone(second);
289
- if (Object.keys(second).length === 1 && second.$and) {
290
- first.$and.push(...second.$and);
291
- } else {
292
- first.$and.push(second);
293
- }
294
- return first;
295
- // Regular merge via $and wrapping
296
- default:
297
- return {
298
- $and: [deepClone(first), deepClone(second)]
299
- };
300
- }
301
- }
302
- function tweakQuery({
303
- paginationFields,
304
- token,
305
- query
306
- }) {
307
- let queryOptions = deepClone(query.getOptions());
308
- let queryFilter = deepClone(query.getFilter());
309
- paginationFields = deepClone(paginationFields);
310
- if (queryOptions.sort) {
311
- const sortMap = formatSort(queryOptions.sort);
312
- if (sortMap) {
313
- queryOptions.sort = Object.fromEntries(sortMap.entries());
314
- }
315
- }
316
- if (token) {
317
- if (!isEmptyObject(token.sortDirection)) {
318
- debug.tweak(
319
- "set from token [queryOptions.sort] = %o",
320
- token.sortDirection
321
- );
322
- queryOptions.sort = token.sortDirection;
323
- }
324
- if (!isEmptyObject(token.sortValues)) {
325
- const rangeFilter = createRangeFilter(
326
- queryOptions.sort,
327
- token.sortValues
328
- );
329
- debug.tweak(
330
- "range conditions:\n sortDirection = %o\n sortValues = %o\n result = %o",
331
- queryOptions.sort,
332
- token.sortValues,
333
- rangeFilter
334
- );
335
- queryFilter = mergeFilters(queryFilter, rangeFilter);
336
- }
337
- }
338
- if (isEmptyObject(queryOptions.sort)) {
339
- queryOptions.sort = { _id: -1 };
340
- debug.tweak("set default [queryOptions.sort] = %o", queryOptions.sort);
341
- }
342
- if (!Array.isArray(paginationFields) || !paginationFields.length) {
343
- paginationFields = Object.keys(queryOptions.sort);
344
- debug.tweak("set default [paginationFields] = %o", paginationFields);
345
- }
346
- if (!isLastKeys(queryOptions.sort, paginationFields)) {
347
- const expectedKeys = paginationFields.join(", ");
348
- throw new MongoInvalidArgumentError(
349
- `The query sort keys must ends with "${expectedKeys}".`
350
- );
351
- }
352
- if (!queryOptions.limit) {
353
- queryOptions.limit = DEF_LIMIT + 1;
354
- debug.tweak("set default [queryOptions.limit] = %d", queryOptions.limit);
355
- } else {
356
- queryOptions.limit = Math.max(queryOptions.limit || DEF_LIMIT, 1) + 1;
357
- }
358
- query.setOptions(queryOptions);
359
- query.setFilter(queryFilter);
360
- return paginationFields;
361
- }
362
-
363
- class QueryAdapterMongoose {
364
- constructor(query) {
365
- this.query = query;
366
- }
367
- getModelName() {
368
- return this.query.model.modelName;
369
- }
370
- getOptions() {
371
- const options = this.query.getOptions();
372
- return {
373
- limit: options.limit,
374
- sort: options.sort
375
- };
376
- }
377
- getFilter() {
378
- return this.query.getFilter();
379
- }
380
- setOptions({ limit, sort }) {
381
- this.query.setOptions(cleanEmpty({ limit, sort }));
382
- }
383
- setFilter(filter) {
384
- this.query.setQuery(filter);
385
- }
386
- toArray() {
387
- return this.query.exec();
388
- }
389
- stream() {
390
- return this.query.cursor();
391
- }
392
- }
393
-
394
- class Filter extends Transform {
395
- constructor(has, options) {
396
- super({
397
- autoDestroy: true,
398
- objectMode: true,
399
- ...options,
400
- readableObjectMode: true,
401
- writableObjectMode: true,
402
- highWaterMark: 16
403
- });
404
- this.has = has;
405
- this._count = -1;
406
- }
407
- _count = 0;
408
- _transform(chunk, encoding, next) {
409
- const result = this.has(chunk, ++this._count);
410
- if (typeof result === "boolean") {
411
- if (result) {
412
- return next(null, chunk);
413
- } else {
414
- return next();
415
- }
416
- }
417
- if ("then" in result) {
418
- return void result.then((r) => {
419
- if (r) next(null, chunk);
420
- }).catch(next);
421
- }
422
- next();
423
- }
424
- }
425
- function streamFilter(fn, options) {
426
- return new Filter(fn, options);
427
- }
428
-
429
- class QueryPaginator {
430
- constructor(query, {
431
- paginationFields = [],
432
- postQuery,
433
- preQuery,
434
- next: previousToken,
435
- debugQuery = false
436
- } = {}) {
437
- this.query = query;
438
- this.paginationFields = paginationFields;
439
- this.debugQuery = debugQuery;
440
- this.next = createToken({
441
- modelName: query.getModelName(),
442
- sortDirection: {},
443
- sortValues: {},
444
- payload: {}
445
- });
446
- if (previousToken) {
447
- try {
448
- this.previous = Token.from(previousToken);
449
- this.paginationFields = Object.keys(this.previous.sortValues);
450
- debug.init("parse previous = %o", this.previous);
451
- debug.init(
452
- "set from previous [paginationFields] = %o",
453
- this.paginationFields
454
- );
455
- } catch (orignErr) {
456
- const err = new MongoInvalidArgumentError(
457
- "Failed to parse next pagination cursor.",
458
- { cause: toError(orignErr) }
459
- );
460
- throw err;
461
- }
462
- const modelNameCRC = crc32(query.getModelName());
463
- debug.init(
464
- "compare model name: previous = %d, query = %d",
465
- this.previous.modelNameCRC,
466
- modelNameCRC
467
- );
468
- if (modelNameCRC !== this.previous.modelNameCRC) {
469
- throw new MongoInvalidArgumentError(
470
- "The model name of next cursor token is not equal with query model name. Expected: " + this.query.getModelName()
471
- );
472
- }
473
- }
474
- if (preQuery) this.pre("query", preQuery);
475
- if (postQuery) this.post("query", postQuery);
476
- defineGetter(this.next, "sortDirection", () => {
477
- const sortMap = formatSort(this.query.getOptions().sort);
478
- return sortMap ? Object.fromEntries(sortMap.entries()) : {};
479
- });
480
- defineGetter(this.next, "sortValues", () => {
481
- if (!this.resLastItem) {
482
- return {};
483
- }
484
- return pick(this.resLastItem, this.paginationFields);
485
- });
486
- debug.init("with options = %o", arguments[1]);
487
- }
488
- hooks = new Kareem();
489
- next;
490
- previous;
491
- resHasNext = false;
492
- resLastItem = null;
493
- paginationFields;
494
- tweaked = false;
495
- debugQuery = false;
496
- getMetadata = () => this.metadata;
497
- /**
498
- * Retrieves metadata about the current state of pagination.
499
- */
500
- get metadata() {
501
- return {
502
- hasNext: this.hasNext,
503
- next: this.nextTokenString
504
- };
505
- }
506
- /**
507
- * Indicates whether there is a next page available.
508
- */
509
- get hasNext() {
510
- return this.resHasNext;
511
- }
512
- /**
513
- * Retrieves the token for the next page.
514
- */
515
- get nextToken() {
516
- return this.next;
517
- }
518
- /**
519
- * Retrieves the string representation of the next page token.
520
- * If there is no next page, returns `null`.
521
- */
522
- get nextTokenString() {
523
- if (!this.resHasNext) {
524
- return null;
525
- }
526
- return this.next.stringify();
527
- }
528
- /**
529
- * Retrieves the token for the previous page.
530
- */
531
- get prevToken() {
532
- return this.previous || null;
533
- }
534
- /**
535
- * Retrieves the string representation of the previous page token.
536
- */
537
- get prevTokenString() {
538
- return this.previous ? this.previous.stringify() : null;
539
- }
540
- getQuery() {
541
- return this.query;
542
- }
543
- pre(hookName, fn) {
544
- this.hooks.pre(hookName, fn);
545
- return this;
546
- }
547
- post(hookName, fn) {
548
- this.hooks.post(hookName, fn);
549
- return this;
550
- }
551
- stream() {
552
- return new Promise((resolve, reject) => {
553
- this.hooks.execPre("query", this, [], (error) => {
554
- if (error) return reject(error);
555
- try {
556
- this.maybeTweak();
557
- } catch (err) {
558
- return reject(err);
559
- }
560
- const limit = this.query.getOptions().limit;
561
- const filter = streamFilter((item, index) => {
562
- if (index + 1 === limit) {
563
- this.resHasNext = true;
564
- debug.stream(
565
- "has next:\n cutoff_idx = %d\n next = %j",
566
- index,
567
- this.next
568
- );
569
- return false;
570
- } else {
571
- this.resLastItem = toObject(item);
572
- }
573
- return true;
574
- });
575
- const stream = this.query.stream().pipe(filter);
576
- resolve(stream);
577
- });
578
- });
579
- }
580
- /** @internal */
581
- maybeTweak() {
582
- if (!this.tweaked) {
583
- this.tweak();
584
- }
585
- return this;
586
- }
587
- /** @internal */
588
- tweak() {
589
- this.paginationFields = tweakQuery({
590
- query: this.query,
591
- paginationFields: this.paginationFields,
592
- token: this.previous
593
- });
594
- this.tweaked = true;
595
- return this;
596
- }
597
- /** @internal */
598
- _handleResult(items) {
599
- const limit = this.query.getOptions().limit;
600
- if (items.length === limit) {
601
- items.pop();
602
- this.resHasNext = true;
603
- }
604
- this.resLastItem = toObject(items.at(-1));
605
- if (this.resHasNext) {
606
- debug.exec(
607
- "has next:\n cutoff_idx = %d\n next = %j",
608
- limit - 1,
609
- this.next
610
- );
611
- }
612
- const metadata = this.getMetadata();
613
- return { metadata, items };
614
- }
615
- /**
616
- * Executes the query and returns the results along with pagination metadata.
617
- */
618
- exec() {
619
- return new Promise((resolve, reject) => {
620
- this.hooks.execPre("query", this, [], (preHookErr) => {
621
- if (preHookErr) {
622
- return reject(preHookErr);
623
- }
624
- if (this.debugQuery) {
625
- console.info("pre tweak", {
626
- queryFilter: this.query.getFilter(),
627
- getOptions: this.query.getOptions(),
628
- prevToken: this.prevToken
629
- });
630
- }
631
- this.maybeTweak();
632
- if (this.debugQuery) {
633
- console.info("post tweak", {
634
- queryFilter: this.query.getFilter(),
635
- getOptions: this.query.getOptions(),
636
- prevToken: this.prevToken
637
- });
638
- }
639
- this.query.toArray().then((items) => {
640
- const result = this._handleResult(items);
641
- this.hooks.execPost("query", this, [], (postHookErr) => {
642
- if (postHookErr) {
643
- return reject(postHookErr);
644
- }
645
- return resolve(result);
646
- });
647
- }).catch(reject);
648
- });
649
- });
650
- }
651
- then = (onfulfilled, onrejected) => this.exec().then(onfulfilled, onrejected);
652
- cath = (onrejected) => this.exec().catch(onrejected);
653
- finally = (onfinally) => this.exec().finally(onfinally);
654
- }
655
-
656
- function withMongoosePagination(query, options) {
657
- const adapter = new QueryAdapterMongoose(query);
658
- const pagin = new QueryPaginator(adapter, options);
659
- return pagin;
660
- }
661
-
662
- export { QueryPaginator as Q, createRangeFilter as a, createToken as c, mergeFilters as m, parseToken as p, tweakQuery as t, withMongoosePagination as w };
663
- //# sourceMappingURL=mongo-pagination.C762g8l8.mjs.map