@classytic/mongokit 2.0.0 → 3.0.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 (105) hide show
  1. package/README.md +221 -7
  2. package/dist/actions/index.d.ts +3 -0
  3. package/dist/actions/index.js +473 -0
  4. package/dist/actions/index.js.map +1 -0
  5. package/dist/index-CgOJ2pqz.d.ts +337 -0
  6. package/dist/index.d.ts +239 -0
  7. package/dist/index.js +2108 -0
  8. package/dist/index.js.map +1 -0
  9. package/dist/memory-cache-DG2oSSbx.d.ts +142 -0
  10. package/dist/pagination/PaginationEngine.d.ts +117 -0
  11. package/dist/pagination/PaginationEngine.js +369 -0
  12. package/dist/pagination/PaginationEngine.js.map +1 -0
  13. package/dist/plugins/index.d.ts +275 -0
  14. package/dist/plugins/index.js +857 -0
  15. package/dist/plugins/index.js.map +1 -0
  16. package/dist/types-Nxhmi1aI.d.ts +510 -0
  17. package/dist/utils/index.d.ts +189 -0
  18. package/dist/utils/index.js +643 -0
  19. package/dist/utils/index.js.map +1 -0
  20. package/package.json +38 -21
  21. package/src/Repository.js +0 -296
  22. package/src/actions/aggregate.js +0 -266
  23. package/src/actions/create.js +0 -59
  24. package/src/actions/delete.js +0 -88
  25. package/src/actions/index.js +0 -11
  26. package/src/actions/read.js +0 -188
  27. package/src/actions/update.js +0 -176
  28. package/src/hooks/lifecycle.js +0 -146
  29. package/src/index.js +0 -71
  30. package/src/pagination/PaginationEngine.js +0 -348
  31. package/src/pagination/utils/cursor.js +0 -119
  32. package/src/pagination/utils/filter.js +0 -42
  33. package/src/pagination/utils/limits.js +0 -82
  34. package/src/pagination/utils/sort.js +0 -101
  35. package/src/plugins/aggregate-helpers.plugin.js +0 -71
  36. package/src/plugins/audit-log.plugin.js +0 -60
  37. package/src/plugins/batch-operations.plugin.js +0 -66
  38. package/src/plugins/field-filter.plugin.js +0 -27
  39. package/src/plugins/index.js +0 -19
  40. package/src/plugins/method-registry.plugin.js +0 -140
  41. package/src/plugins/mongo-operations.plugin.js +0 -317
  42. package/src/plugins/soft-delete.plugin.js +0 -46
  43. package/src/plugins/subdocument.plugin.js +0 -66
  44. package/src/plugins/timestamp.plugin.js +0 -19
  45. package/src/plugins/validation-chain.plugin.js +0 -145
  46. package/src/types.d.ts +0 -87
  47. package/src/utils/error.js +0 -12
  48. package/src/utils/field-selection.js +0 -156
  49. package/src/utils/index.js +0 -12
  50. package/types/Repository.d.ts +0 -95
  51. package/types/Repository.d.ts.map +0 -1
  52. package/types/actions/aggregate.d.ts +0 -112
  53. package/types/actions/aggregate.d.ts.map +0 -1
  54. package/types/actions/create.d.ts +0 -21
  55. package/types/actions/create.d.ts.map +0 -1
  56. package/types/actions/delete.d.ts +0 -37
  57. package/types/actions/delete.d.ts.map +0 -1
  58. package/types/actions/index.d.ts +0 -6
  59. package/types/actions/index.d.ts.map +0 -1
  60. package/types/actions/read.d.ts +0 -135
  61. package/types/actions/read.d.ts.map +0 -1
  62. package/types/actions/update.d.ts +0 -58
  63. package/types/actions/update.d.ts.map +0 -1
  64. package/types/hooks/lifecycle.d.ts +0 -44
  65. package/types/hooks/lifecycle.d.ts.map +0 -1
  66. package/types/index.d.ts +0 -25
  67. package/types/index.d.ts.map +0 -1
  68. package/types/pagination/PaginationEngine.d.ts +0 -386
  69. package/types/pagination/PaginationEngine.d.ts.map +0 -1
  70. package/types/pagination/utils/cursor.d.ts +0 -40
  71. package/types/pagination/utils/cursor.d.ts.map +0 -1
  72. package/types/pagination/utils/filter.d.ts +0 -28
  73. package/types/pagination/utils/filter.d.ts.map +0 -1
  74. package/types/pagination/utils/limits.d.ts +0 -64
  75. package/types/pagination/utils/limits.d.ts.map +0 -1
  76. package/types/pagination/utils/sort.d.ts +0 -41
  77. package/types/pagination/utils/sort.d.ts.map +0 -1
  78. package/types/plugins/aggregate-helpers.plugin.d.ts +0 -6
  79. package/types/plugins/aggregate-helpers.plugin.d.ts.map +0 -1
  80. package/types/plugins/audit-log.plugin.d.ts +0 -6
  81. package/types/plugins/audit-log.plugin.d.ts.map +0 -1
  82. package/types/plugins/batch-operations.plugin.d.ts +0 -6
  83. package/types/plugins/batch-operations.plugin.d.ts.map +0 -1
  84. package/types/plugins/field-filter.plugin.d.ts +0 -6
  85. package/types/plugins/field-filter.plugin.d.ts.map +0 -1
  86. package/types/plugins/index.d.ts +0 -11
  87. package/types/plugins/index.d.ts.map +0 -1
  88. package/types/plugins/method-registry.plugin.d.ts +0 -3
  89. package/types/plugins/method-registry.plugin.d.ts.map +0 -1
  90. package/types/plugins/mongo-operations.plugin.d.ts +0 -4
  91. package/types/plugins/mongo-operations.plugin.d.ts.map +0 -1
  92. package/types/plugins/soft-delete.plugin.d.ts +0 -6
  93. package/types/plugins/soft-delete.plugin.d.ts.map +0 -1
  94. package/types/plugins/subdocument.plugin.d.ts +0 -6
  95. package/types/plugins/subdocument.plugin.d.ts.map +0 -1
  96. package/types/plugins/timestamp.plugin.d.ts +0 -6
  97. package/types/plugins/timestamp.plugin.d.ts.map +0 -1
  98. package/types/plugins/validation-chain.plugin.d.ts +0 -31
  99. package/types/plugins/validation-chain.plugin.d.ts.map +0 -1
  100. package/types/utils/error.d.ts +0 -11
  101. package/types/utils/error.d.ts.map +0 -1
  102. package/types/utils/field-selection.d.ts +0 -9
  103. package/types/utils/field-selection.d.ts.map +0 -1
  104. package/types/utils/index.d.ts +0 -2
  105. package/types/utils/index.d.ts.map +0 -1
@@ -0,0 +1,142 @@
1
+ import { o as UserContext, F as FieldPreset, H as HttpError, N as CacheAdapter } from './types-Nxhmi1aI.js';
2
+
3
+ /**
4
+ * Field Selection Utilities
5
+ *
6
+ * Provides explicit, performant field filtering using Mongoose projections.
7
+ *
8
+ * Philosophy:
9
+ * - Explicit is better than implicit
10
+ * - Filter at DB level (10x faster than in-memory)
11
+ * - Progressive disclosure (show more fields as trust increases)
12
+ *
13
+ * @example
14
+ * ```typescript
15
+ * // For Mongoose queries (PREFERRED - 90% of cases)
16
+ * const projection = getMongooseProjection(request.user, fieldPresets.gymPlans);
17
+ * const plans = await GymPlan.find().select(projection).lean();
18
+ *
19
+ * // For complex data (10% of cases - aggregations, multiple sources)
20
+ * const filtered = filterResponseData(complexData, fieldPresets.gymPlans, request.user);
21
+ * ```
22
+ */
23
+
24
+ /**
25
+ * Get allowed fields for a user based on their context
26
+ *
27
+ * @param user - User object from request.user (or null for public)
28
+ * @param preset - Field preset configuration
29
+ * @returns Array of allowed field names
30
+ *
31
+ * @example
32
+ * const fields = getFieldsForUser(request.user, {
33
+ * public: ['id', 'name', 'price'],
34
+ * authenticated: ['description', 'features'],
35
+ * admin: ['createdAt', 'internalNotes']
36
+ * });
37
+ */
38
+ declare function getFieldsForUser(user: UserContext | null | undefined, preset: FieldPreset): string[];
39
+ /**
40
+ * Get Mongoose projection string for query .select()
41
+ *
42
+ * @param user - User object from request.user
43
+ * @param preset - Field preset configuration
44
+ * @returns Space-separated field names for Mongoose .select()
45
+ *
46
+ * @example
47
+ * const projection = getMongooseProjection(request.user, fieldPresets.gymPlans);
48
+ * const plans = await GymPlan.find({ organizationId }).select(projection).lean();
49
+ */
50
+ declare function getMongooseProjection(user: UserContext | null | undefined, preset: FieldPreset): string;
51
+ /**
52
+ * Filter response data to include only allowed fields
53
+ *
54
+ * Use this for complex responses where Mongoose projections aren't applicable:
55
+ * - Aggregation pipeline results
56
+ * - Data from multiple sources
57
+ * - Custom computed fields
58
+ *
59
+ * For simple DB queries, prefer getMongooseProjection() (10x faster)
60
+ *
61
+ * @param data - Data to filter
62
+ * @param preset - Field preset configuration
63
+ * @param user - User object from request.user
64
+ * @returns Filtered data
65
+ *
66
+ * @example
67
+ * const stats = await calculateComplexStats();
68
+ * const filtered = filterResponseData(stats, fieldPresets.dashboard, request.user);
69
+ * return reply.send(filtered);
70
+ */
71
+ declare function filterResponseData<T extends Record<string, unknown>>(data: T | T[], preset: FieldPreset, user?: UserContext | null): Partial<T> | Partial<T>[];
72
+ /**
73
+ * Helper to create field presets (module-level)
74
+ *
75
+ * Each module should define its own field preset in its own directory.
76
+ * This keeps modules independent and self-contained.
77
+ *
78
+ * @param config - Field configuration
79
+ * @returns Field preset
80
+ *
81
+ * @example
82
+ * // In modules/gym-plan/gym-plan.fields.ts
83
+ * export const gymPlanFieldPreset = createFieldPreset({
84
+ * public: ['id', 'name', 'price'],
85
+ * authenticated: ['features', 'description'],
86
+ * admin: ['createdAt', 'updatedAt', 'internalNotes']
87
+ * });
88
+ */
89
+ declare function createFieldPreset(config: Partial<FieldPreset>): FieldPreset;
90
+
91
+ /**
92
+ * Error Utilities
93
+ *
94
+ * HTTP-compatible error creation for repository operations
95
+ */
96
+
97
+ /**
98
+ * Creates an error with HTTP status code
99
+ *
100
+ * @param status - HTTP status code
101
+ * @param message - Error message
102
+ * @returns Error with status property
103
+ *
104
+ * @example
105
+ * throw createError(404, 'Document not found');
106
+ * throw createError(400, 'Invalid input');
107
+ * throw createError(403, 'Access denied');
108
+ */
109
+ declare function createError(status: number, message: string): HttpError;
110
+
111
+ /**
112
+ * In-Memory Cache Adapter
113
+ *
114
+ * Simple cache adapter for development and testing.
115
+ * NOT recommended for production - use Redis or similar.
116
+ *
117
+ * @example
118
+ * ```typescript
119
+ * import { cachePlugin, createMemoryCache } from '@classytic/mongokit';
120
+ *
121
+ * const repo = new Repository(UserModel, [
122
+ * cachePlugin({
123
+ * adapter: createMemoryCache(),
124
+ * ttl: 60,
125
+ * })
126
+ * ]);
127
+ * ```
128
+ */
129
+
130
+ /**
131
+ * Creates an in-memory cache adapter
132
+ *
133
+ * Features:
134
+ * - Automatic TTL expiration
135
+ * - Pattern-based clearing (simple glob with *)
136
+ * - Max entries limit to prevent memory leaks
137
+ *
138
+ * @param maxEntries - Maximum cache entries before oldest are evicted (default: 1000)
139
+ */
140
+ declare function createMemoryCache(maxEntries?: number): CacheAdapter;
141
+
142
+ export { getMongooseProjection as a, createError as b, createFieldPreset as c, createMemoryCache as d, filterResponseData as f, getFieldsForUser as g };
@@ -0,0 +1,117 @@
1
+ import { Model } from 'mongoose';
2
+ import { A as AnyDocument, P as PaginationConfig, O as OffsetPaginationOptions, a as OffsetPaginationResult, K as KeysetPaginationOptions, b as KeysetPaginationResult, c as AggregatePaginationOptions, d as AggregatePaginationResult } from '../types-Nxhmi1aI.js';
3
+
4
+ /**
5
+ * Pagination Engine
6
+ *
7
+ * Production-grade pagination for MongoDB with support for:
8
+ * - Offset pagination (page-based) - Best for small datasets, random page access
9
+ * - Keyset pagination (cursor-based) - Best for large datasets, infinite scroll
10
+ * - Aggregate pagination - Best for complex queries requiring aggregation
11
+ *
12
+ * @example
13
+ * ```typescript
14
+ * const engine = new PaginationEngine(UserModel, {
15
+ * defaultLimit: 20,
16
+ * maxLimit: 100,
17
+ * useEstimatedCount: true
18
+ * });
19
+ *
20
+ * // Offset pagination
21
+ * const page1 = await engine.paginate({ page: 1, limit: 20 });
22
+ *
23
+ * // Keyset pagination (better for large datasets)
24
+ * const stream1 = await engine.stream({ sort: { createdAt: -1 }, limit: 20 });
25
+ * const stream2 = await engine.stream({ sort: { createdAt: -1 }, after: stream1.next });
26
+ * ```
27
+ */
28
+
29
+ /**
30
+ * Internal pagination config with required values
31
+ */
32
+ interface ResolvedPaginationConfig {
33
+ defaultLimit: number;
34
+ maxLimit: number;
35
+ maxPage: number;
36
+ deepPageThreshold: number;
37
+ cursorVersion: number;
38
+ useEstimatedCount: boolean;
39
+ }
40
+ /**
41
+ * Production-grade pagination engine for MongoDB
42
+ * Supports offset, keyset (cursor), and aggregate pagination
43
+ */
44
+ declare class PaginationEngine<TDoc = AnyDocument> {
45
+ readonly Model: Model<TDoc>;
46
+ readonly config: ResolvedPaginationConfig;
47
+ /**
48
+ * Create a new pagination engine
49
+ *
50
+ * @param Model - Mongoose model to paginate
51
+ * @param config - Pagination configuration
52
+ */
53
+ constructor(Model: Model<TDoc>, config?: PaginationConfig);
54
+ /**
55
+ * Offset-based pagination using skip/limit
56
+ * Best for small datasets and when users need random page access
57
+ * O(n) performance - slower for deep pages
58
+ *
59
+ * @param options - Pagination options
60
+ * @returns Pagination result with total count
61
+ *
62
+ * @example
63
+ * const result = await engine.paginate({
64
+ * filters: { status: 'active' },
65
+ * sort: { createdAt: -1 },
66
+ * page: 1,
67
+ * limit: 20
68
+ * });
69
+ * console.log(result.docs, result.total, result.hasNext);
70
+ */
71
+ paginate(options?: OffsetPaginationOptions): Promise<OffsetPaginationResult<TDoc>>;
72
+ /**
73
+ * Keyset (cursor-based) pagination for high-performance streaming
74
+ * Best for large datasets, infinite scroll, real-time feeds
75
+ * O(1) performance - consistent speed regardless of position
76
+ *
77
+ * @param options - Pagination options (sort is required)
78
+ * @returns Pagination result with next cursor
79
+ *
80
+ * @example
81
+ * // First page
82
+ * const page1 = await engine.stream({
83
+ * sort: { createdAt: -1 },
84
+ * limit: 20
85
+ * });
86
+ *
87
+ * // Next page using cursor
88
+ * const page2 = await engine.stream({
89
+ * sort: { createdAt: -1 },
90
+ * after: page1.next,
91
+ * limit: 20
92
+ * });
93
+ */
94
+ stream(options: KeysetPaginationOptions): Promise<KeysetPaginationResult<TDoc>>;
95
+ /**
96
+ * Aggregate pipeline with pagination
97
+ * Best for complex queries requiring aggregation stages
98
+ * Uses $facet to combine results and count in single query
99
+ *
100
+ * @param options - Aggregation options
101
+ * @returns Pagination result with total count
102
+ *
103
+ * @example
104
+ * const result = await engine.aggregatePaginate({
105
+ * pipeline: [
106
+ * { $match: { status: 'active' } },
107
+ * { $group: { _id: '$category', count: { $sum: 1 } } },
108
+ * { $sort: { count: -1 } }
109
+ * ],
110
+ * page: 1,
111
+ * limit: 20
112
+ * });
113
+ */
114
+ aggregatePaginate(options?: AggregatePaginationOptions): Promise<AggregatePaginationResult<TDoc>>;
115
+ }
116
+
117
+ export { PaginationEngine };
@@ -0,0 +1,369 @@
1
+ import mongoose from 'mongoose';
2
+
3
+ // src/pagination/utils/cursor.ts
4
+ function encodeCursor(doc, primaryField, sort, version = 1) {
5
+ const primaryValue = doc[primaryField];
6
+ const idValue = doc._id;
7
+ const payload = {
8
+ v: serializeValue(primaryValue),
9
+ t: getValueType(primaryValue),
10
+ id: serializeValue(idValue),
11
+ idType: getValueType(idValue),
12
+ sort,
13
+ ver: version
14
+ };
15
+ return Buffer.from(JSON.stringify(payload)).toString("base64");
16
+ }
17
+ function decodeCursor(token) {
18
+ try {
19
+ const json = Buffer.from(token, "base64").toString("utf-8");
20
+ const payload = JSON.parse(json);
21
+ return {
22
+ value: rehydrateValue(payload.v, payload.t),
23
+ id: rehydrateValue(payload.id, payload.idType),
24
+ sort: payload.sort,
25
+ version: payload.ver
26
+ };
27
+ } catch {
28
+ throw new Error("Invalid cursor token");
29
+ }
30
+ }
31
+ function validateCursorSort(cursorSort, currentSort) {
32
+ const cursorSortStr = JSON.stringify(cursorSort);
33
+ const currentSortStr = JSON.stringify(currentSort);
34
+ if (cursorSortStr !== currentSortStr) {
35
+ throw new Error("Cursor sort does not match current query sort");
36
+ }
37
+ }
38
+ function validateCursorVersion(cursorVersion, expectedVersion) {
39
+ if (cursorVersion !== expectedVersion) {
40
+ throw new Error(`Cursor version ${cursorVersion} does not match expected version ${expectedVersion}`);
41
+ }
42
+ }
43
+ function serializeValue(value) {
44
+ if (value instanceof Date) return value.toISOString();
45
+ if (value instanceof mongoose.Types.ObjectId) return value.toString();
46
+ return value;
47
+ }
48
+ function getValueType(value) {
49
+ if (value instanceof Date) return "date";
50
+ if (value instanceof mongoose.Types.ObjectId) return "objectid";
51
+ if (typeof value === "boolean") return "boolean";
52
+ if (typeof value === "number") return "number";
53
+ if (typeof value === "string") return "string";
54
+ return "unknown";
55
+ }
56
+ function rehydrateValue(serialized, type) {
57
+ switch (type) {
58
+ case "date":
59
+ return new Date(serialized);
60
+ case "objectid":
61
+ return new mongoose.Types.ObjectId(serialized);
62
+ case "boolean":
63
+ return Boolean(serialized);
64
+ case "number":
65
+ return Number(serialized);
66
+ default:
67
+ return serialized;
68
+ }
69
+ }
70
+
71
+ // src/pagination/utils/sort.ts
72
+ function normalizeSort(sort) {
73
+ const normalized = {};
74
+ Object.keys(sort).forEach((key) => {
75
+ if (key !== "_id") normalized[key] = sort[key];
76
+ });
77
+ if (sort._id !== void 0) {
78
+ normalized._id = sort._id;
79
+ }
80
+ return normalized;
81
+ }
82
+ function validateKeysetSort(sort) {
83
+ const keys = Object.keys(sort);
84
+ if (keys.length === 1 && keys[0] !== "_id") {
85
+ const field = keys[0];
86
+ const direction = sort[field];
87
+ return normalizeSort({ [field]: direction, _id: direction });
88
+ }
89
+ if (keys.length === 1 && keys[0] === "_id") {
90
+ return normalizeSort(sort);
91
+ }
92
+ if (keys.length === 2) {
93
+ if (!keys.includes("_id")) {
94
+ throw new Error("Keyset pagination requires _id as tie-breaker");
95
+ }
96
+ const primaryField = keys.find((k) => k !== "_id");
97
+ const primaryDirection = sort[primaryField];
98
+ const idDirection = sort._id;
99
+ if (primaryDirection !== idDirection) {
100
+ throw new Error("_id direction must match primary field direction");
101
+ }
102
+ return normalizeSort(sort);
103
+ }
104
+ throw new Error("Keyset pagination only supports single field + _id");
105
+ }
106
+ function getPrimaryField(sort) {
107
+ const keys = Object.keys(sort);
108
+ return keys.find((k) => k !== "_id") || "_id";
109
+ }
110
+
111
+ // src/pagination/utils/filter.ts
112
+ function buildKeysetFilter(baseFilters, sort, cursorValue, cursorId) {
113
+ const primaryField = Object.keys(sort).find((k) => k !== "_id") || "_id";
114
+ const direction = sort[primaryField];
115
+ const operator = direction === 1 ? "$gt" : "$lt";
116
+ return {
117
+ ...baseFilters,
118
+ $or: [
119
+ { [primaryField]: { [operator]: cursorValue } },
120
+ {
121
+ [primaryField]: cursorValue,
122
+ _id: { [operator]: cursorId }
123
+ }
124
+ ]
125
+ };
126
+ }
127
+
128
+ // src/pagination/utils/limits.ts
129
+ function validateLimit(limit, config) {
130
+ const parsed = Number(limit);
131
+ if (!Number.isFinite(parsed) || parsed < 1) {
132
+ return config.defaultLimit || 10;
133
+ }
134
+ return Math.min(Math.floor(parsed), config.maxLimit || 100);
135
+ }
136
+ function validatePage(page, config) {
137
+ const parsed = Number(page);
138
+ if (!Number.isFinite(parsed) || parsed < 1) {
139
+ return 1;
140
+ }
141
+ const sanitized = Math.floor(parsed);
142
+ if (sanitized > (config.maxPage || 1e4)) {
143
+ throw new Error(`Page ${sanitized} exceeds maximum ${config.maxPage || 1e4}`);
144
+ }
145
+ return sanitized;
146
+ }
147
+ function shouldWarnDeepPagination(page, threshold) {
148
+ return page > threshold;
149
+ }
150
+ function calculateSkip(page, limit) {
151
+ return (page - 1) * limit;
152
+ }
153
+ function calculateTotalPages(total, limit) {
154
+ return Math.ceil(total / limit);
155
+ }
156
+
157
+ // src/utils/error.ts
158
+ function createError(status, message) {
159
+ const error = new Error(message);
160
+ error.status = status;
161
+ return error;
162
+ }
163
+
164
+ // src/pagination/PaginationEngine.ts
165
+ var PaginationEngine = class {
166
+ Model;
167
+ config;
168
+ /**
169
+ * Create a new pagination engine
170
+ *
171
+ * @param Model - Mongoose model to paginate
172
+ * @param config - Pagination configuration
173
+ */
174
+ constructor(Model, config = {}) {
175
+ this.Model = Model;
176
+ this.config = {
177
+ defaultLimit: config.defaultLimit || 10,
178
+ maxLimit: config.maxLimit || 100,
179
+ maxPage: config.maxPage || 1e4,
180
+ deepPageThreshold: config.deepPageThreshold || 100,
181
+ cursorVersion: config.cursorVersion || 1,
182
+ useEstimatedCount: config.useEstimatedCount || false
183
+ };
184
+ }
185
+ /**
186
+ * Offset-based pagination using skip/limit
187
+ * Best for small datasets and when users need random page access
188
+ * O(n) performance - slower for deep pages
189
+ *
190
+ * @param options - Pagination options
191
+ * @returns Pagination result with total count
192
+ *
193
+ * @example
194
+ * const result = await engine.paginate({
195
+ * filters: { status: 'active' },
196
+ * sort: { createdAt: -1 },
197
+ * page: 1,
198
+ * limit: 20
199
+ * });
200
+ * console.log(result.docs, result.total, result.hasNext);
201
+ */
202
+ async paginate(options = {}) {
203
+ const {
204
+ filters = {},
205
+ sort = { _id: -1 },
206
+ page = 1,
207
+ limit = this.config.defaultLimit,
208
+ select,
209
+ populate = [],
210
+ lean = true,
211
+ session
212
+ } = options;
213
+ const sanitizedPage = validatePage(page, this.config);
214
+ const sanitizedLimit = validateLimit(limit, this.config);
215
+ const skip = calculateSkip(sanitizedPage, sanitizedLimit);
216
+ let query = this.Model.find(filters);
217
+ if (select) query = query.select(select);
218
+ if (populate && (Array.isArray(populate) ? populate.length : populate)) {
219
+ query = query.populate(populate);
220
+ }
221
+ query = query.sort(sort).skip(skip).limit(sanitizedLimit).lean(lean);
222
+ if (session) query = query.session(session);
223
+ const hasFilters = Object.keys(filters).length > 0;
224
+ const useEstimated = this.config.useEstimatedCount && !hasFilters;
225
+ const [docs, total] = await Promise.all([
226
+ query.exec(),
227
+ useEstimated ? this.Model.estimatedDocumentCount() : this.Model.countDocuments(filters).session(session ?? null)
228
+ ]);
229
+ const totalPages = calculateTotalPages(total, sanitizedLimit);
230
+ const warning = shouldWarnDeepPagination(sanitizedPage, this.config.deepPageThreshold) ? `Deep pagination (page ${sanitizedPage}). Consider getAll({ after, sort, limit }) for better performance.` : void 0;
231
+ return {
232
+ method: "offset",
233
+ docs,
234
+ page: sanitizedPage,
235
+ limit: sanitizedLimit,
236
+ total,
237
+ pages: totalPages,
238
+ hasNext: sanitizedPage < totalPages,
239
+ hasPrev: sanitizedPage > 1,
240
+ ...warning && { warning }
241
+ };
242
+ }
243
+ /**
244
+ * Keyset (cursor-based) pagination for high-performance streaming
245
+ * Best for large datasets, infinite scroll, real-time feeds
246
+ * O(1) performance - consistent speed regardless of position
247
+ *
248
+ * @param options - Pagination options (sort is required)
249
+ * @returns Pagination result with next cursor
250
+ *
251
+ * @example
252
+ * // First page
253
+ * const page1 = await engine.stream({
254
+ * sort: { createdAt: -1 },
255
+ * limit: 20
256
+ * });
257
+ *
258
+ * // Next page using cursor
259
+ * const page2 = await engine.stream({
260
+ * sort: { createdAt: -1 },
261
+ * after: page1.next,
262
+ * limit: 20
263
+ * });
264
+ */
265
+ async stream(options) {
266
+ const {
267
+ filters = {},
268
+ sort,
269
+ after,
270
+ limit = this.config.defaultLimit,
271
+ select,
272
+ populate = [],
273
+ lean = true,
274
+ session
275
+ } = options;
276
+ if (!sort) {
277
+ throw createError(400, "sort is required for keyset pagination");
278
+ }
279
+ const sanitizedLimit = validateLimit(limit, this.config);
280
+ const normalizedSort = validateKeysetSort(sort);
281
+ let query = { ...filters };
282
+ if (after) {
283
+ const cursor = decodeCursor(after);
284
+ validateCursorVersion(cursor.version, this.config.cursorVersion);
285
+ validateCursorSort(cursor.sort, normalizedSort);
286
+ query = buildKeysetFilter(query, normalizedSort, cursor.value, cursor.id);
287
+ }
288
+ let mongoQuery = this.Model.find(query);
289
+ if (select) mongoQuery = mongoQuery.select(select);
290
+ if (populate && (Array.isArray(populate) ? populate.length : populate)) {
291
+ mongoQuery = mongoQuery.populate(populate);
292
+ }
293
+ mongoQuery = mongoQuery.sort(normalizedSort).limit(sanitizedLimit + 1).lean(lean);
294
+ if (session) mongoQuery = mongoQuery.session(session);
295
+ const docs = await mongoQuery.exec();
296
+ const hasMore = docs.length > sanitizedLimit;
297
+ if (hasMore) docs.pop();
298
+ const primaryField = getPrimaryField(normalizedSort);
299
+ const nextCursor = hasMore && docs.length > 0 ? encodeCursor(docs[docs.length - 1], primaryField, normalizedSort, this.config.cursorVersion) : null;
300
+ return {
301
+ method: "keyset",
302
+ docs,
303
+ limit: sanitizedLimit,
304
+ hasMore,
305
+ next: nextCursor
306
+ };
307
+ }
308
+ /**
309
+ * Aggregate pipeline with pagination
310
+ * Best for complex queries requiring aggregation stages
311
+ * Uses $facet to combine results and count in single query
312
+ *
313
+ * @param options - Aggregation options
314
+ * @returns Pagination result with total count
315
+ *
316
+ * @example
317
+ * const result = await engine.aggregatePaginate({
318
+ * pipeline: [
319
+ * { $match: { status: 'active' } },
320
+ * { $group: { _id: '$category', count: { $sum: 1 } } },
321
+ * { $sort: { count: -1 } }
322
+ * ],
323
+ * page: 1,
324
+ * limit: 20
325
+ * });
326
+ */
327
+ async aggregatePaginate(options = {}) {
328
+ const {
329
+ pipeline = [],
330
+ page = 1,
331
+ limit = this.config.defaultLimit,
332
+ session
333
+ } = options;
334
+ const sanitizedPage = validatePage(page, this.config);
335
+ const sanitizedLimit = validateLimit(limit, this.config);
336
+ const skip = calculateSkip(sanitizedPage, sanitizedLimit);
337
+ const facetPipeline = [
338
+ ...pipeline,
339
+ {
340
+ $facet: {
341
+ docs: [{ $skip: skip }, { $limit: sanitizedLimit }],
342
+ total: [{ $count: "count" }]
343
+ }
344
+ }
345
+ ];
346
+ const aggregation = this.Model.aggregate(facetPipeline);
347
+ if (session) aggregation.session(session);
348
+ const [result] = await aggregation.exec();
349
+ const docs = result.docs;
350
+ const total = result.total[0]?.count || 0;
351
+ const totalPages = calculateTotalPages(total, sanitizedLimit);
352
+ const warning = shouldWarnDeepPagination(sanitizedPage, this.config.deepPageThreshold) ? `Deep pagination in aggregate (page ${sanitizedPage}). Uses $skip internally.` : void 0;
353
+ return {
354
+ method: "aggregate",
355
+ docs,
356
+ page: sanitizedPage,
357
+ limit: sanitizedLimit,
358
+ total,
359
+ pages: totalPages,
360
+ hasNext: sanitizedPage < totalPages,
361
+ hasPrev: sanitizedPage > 1,
362
+ ...warning && { warning }
363
+ };
364
+ }
365
+ };
366
+
367
+ export { PaginationEngine };
368
+ //# sourceMappingURL=PaginationEngine.js.map
369
+ //# sourceMappingURL=PaginationEngine.js.map