@classytic/mongokit 3.0.6 → 3.1.1
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.
- package/README.md +625 -463
- package/dist/actions/index.d.ts +2 -2
- package/dist/actions/index.js +3 -484
- package/dist/chunks/chunk-2ZN65ZOP.js +93 -0
- package/dist/chunks/chunk-CF6FLC2G.js +46 -0
- package/dist/chunks/chunk-CSLJ2PL2.js +1092 -0
- package/dist/chunks/chunk-IT7DCOKR.js +299 -0
- package/dist/chunks/chunk-M2XHQGZB.js +361 -0
- package/dist/chunks/chunk-SAKSLT47.js +470 -0
- package/dist/chunks/chunk-VJXDGP3C.js +14 -0
- package/dist/{index-CkwbNdpJ.d.ts → index-C2NCVxJK.d.ts} +170 -3
- package/dist/index.d.ts +997 -8
- package/dist/index.js +1143 -2476
- package/dist/{queryParser-Do3SgsyJ.d.ts → mongooseToJsonSchema-BKMxPbPp.d.ts} +8 -111
- package/dist/pagination/PaginationEngine.d.ts +1 -1
- package/dist/pagination/PaginationEngine.js +2 -368
- package/dist/plugins/index.d.ts +1 -1
- package/dist/plugins/index.js +4 -1170
- package/dist/{types-DDDYo18H.d.ts → types-DA0rs2Jh.d.ts} +109 -35
- package/dist/utils/index.d.ts +2 -2
- package/dist/utils/index.js +3 -711
- package/package.json +8 -3
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { U as UserContext, F as FieldPreset, H as HttpError, C as CacheAdapter, g as SchemaBuilderOptions, h as CrudSchemas, V as ValidationResult } from './types-DA0rs2Jh.js';
|
|
2
2
|
import mongoose__default, { Schema } from 'mongoose';
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -154,6 +154,8 @@ declare function createMemoryCache(maxEntries?: number): CacheAdapter;
|
|
|
154
154
|
* Additional Options:
|
|
155
155
|
* - strictAdditionalProperties: Set to true to add "additionalProperties: false" to schemas
|
|
156
156
|
* This makes Fastify reject unknown fields at validation level (default: false for backward compatibility)
|
|
157
|
+
* - update.requireAtLeastOne: Set to true to add "minProperties: 1" to update schema
|
|
158
|
+
* This prevents empty update payloads (default: false)
|
|
157
159
|
*
|
|
158
160
|
* @example
|
|
159
161
|
* buildCrudSchemasFromModel(Model, {
|
|
@@ -163,7 +165,10 @@ declare function createMemoryCache(maxEntries?: number): CacheAdapter;
|
|
|
163
165
|
* status: { systemManaged: true },
|
|
164
166
|
* },
|
|
165
167
|
* create: { omitFields: ['verifiedAt'] },
|
|
166
|
-
* update: {
|
|
168
|
+
* update: {
|
|
169
|
+
* omitFields: ['customerId'],
|
|
170
|
+
* requireAtLeastOne: true // Reject empty updates
|
|
171
|
+
* }
|
|
167
172
|
* })
|
|
168
173
|
*/
|
|
169
174
|
|
|
@@ -192,112 +197,4 @@ declare function isFieldUpdateAllowed(fieldName: string, options?: SchemaBuilder
|
|
|
192
197
|
*/
|
|
193
198
|
declare function validateUpdateBody(body?: Record<string, unknown>, options?: SchemaBuilderOptions): ValidationResult;
|
|
194
199
|
|
|
195
|
-
|
|
196
|
-
* Query Parser
|
|
197
|
-
*
|
|
198
|
-
* Parses HTTP query parameters into MongoDB-compatible query objects.
|
|
199
|
-
* Supports operators, pagination, sorting, and filtering.
|
|
200
|
-
*/
|
|
201
|
-
|
|
202
|
-
/** Operator mapping from query syntax to MongoDB operators */
|
|
203
|
-
type OperatorMap = Record<string, string>;
|
|
204
|
-
/** Possible values in filter parameters */
|
|
205
|
-
type FilterValue = string | number | boolean | null | undefined | Record<string, unknown> | unknown[];
|
|
206
|
-
/** Configuration options for QueryParser */
|
|
207
|
-
interface QueryParserOptions {
|
|
208
|
-
/** Maximum allowed regex pattern length (default: 500) */
|
|
209
|
-
maxRegexLength?: number;
|
|
210
|
-
/** Maximum allowed text search query length (default: 200) */
|
|
211
|
-
maxSearchLength?: number;
|
|
212
|
-
/** Maximum allowed filter depth (default: 10) */
|
|
213
|
-
maxFilterDepth?: number;
|
|
214
|
-
/** Additional operators to block */
|
|
215
|
-
additionalDangerousOperators?: string[];
|
|
216
|
-
}
|
|
217
|
-
/**
|
|
218
|
-
* Query Parser Class
|
|
219
|
-
*
|
|
220
|
-
* Parses HTTP query parameters into MongoDB-compatible query objects.
|
|
221
|
-
* Includes security measures against NoSQL injection and ReDoS attacks.
|
|
222
|
-
*
|
|
223
|
-
* @example
|
|
224
|
-
* ```typescript
|
|
225
|
-
* import { QueryParser } from '@classytic/mongokit';
|
|
226
|
-
*
|
|
227
|
-
* const parser = new QueryParser({ maxRegexLength: 100 });
|
|
228
|
-
* const query = parser.parseQuery(req.query);
|
|
229
|
-
* ```
|
|
230
|
-
*/
|
|
231
|
-
declare class QueryParser {
|
|
232
|
-
private readonly options;
|
|
233
|
-
private readonly operators;
|
|
234
|
-
/**
|
|
235
|
-
* Dangerous MongoDB operators that should never be accepted from user input
|
|
236
|
-
* Security: Prevent NoSQL injection attacks
|
|
237
|
-
*/
|
|
238
|
-
private readonly dangerousOperators;
|
|
239
|
-
/**
|
|
240
|
-
* Regex pattern characters that can cause catastrophic backtracking (ReDoS)
|
|
241
|
-
*/
|
|
242
|
-
private readonly dangerousRegexPatterns;
|
|
243
|
-
constructor(options?: QueryParserOptions);
|
|
244
|
-
/**
|
|
245
|
-
* Parse query parameters into MongoDB query format
|
|
246
|
-
*/
|
|
247
|
-
parseQuery(query: Record<string, unknown> | null | undefined): ParsedQuery;
|
|
248
|
-
/**
|
|
249
|
-
* Parse sort parameter
|
|
250
|
-
* Converts string like '-createdAt' to { createdAt: -1 }
|
|
251
|
-
* Handles multiple sorts: '-createdAt,name' → { createdAt: -1, name: 1 }
|
|
252
|
-
*/
|
|
253
|
-
private _parseSort;
|
|
254
|
-
/**
|
|
255
|
-
* Parse standard filter parameter (filter[field]=value)
|
|
256
|
-
*/
|
|
257
|
-
private _parseFilters;
|
|
258
|
-
/**
|
|
259
|
-
* Handle operator syntax: field[operator]=value
|
|
260
|
-
*/
|
|
261
|
-
private _handleOperatorSyntax;
|
|
262
|
-
/**
|
|
263
|
-
* Handle bracket syntax with object value
|
|
264
|
-
*/
|
|
265
|
-
private _handleBracketSyntax;
|
|
266
|
-
/**
|
|
267
|
-
* Convert operator to MongoDB format
|
|
268
|
-
*/
|
|
269
|
-
private _toMongoOperator;
|
|
270
|
-
/**
|
|
271
|
-
* Create a safe regex pattern with protection against ReDoS attacks
|
|
272
|
-
* @param pattern - The pattern string from user input
|
|
273
|
-
* @param flags - Regex flags (default: 'i' for case-insensitive)
|
|
274
|
-
* @returns A safe RegExp or null if pattern is invalid/dangerous
|
|
275
|
-
*/
|
|
276
|
-
private _createSafeRegex;
|
|
277
|
-
/**
|
|
278
|
-
* Escape special regex characters for literal matching
|
|
279
|
-
*/
|
|
280
|
-
private _escapeRegex;
|
|
281
|
-
/**
|
|
282
|
-
* Sanitize text search query for MongoDB $text search
|
|
283
|
-
* @param search - Raw search input from user
|
|
284
|
-
* @returns Sanitized search string or undefined
|
|
285
|
-
*/
|
|
286
|
-
private _sanitizeSearch;
|
|
287
|
-
/**
|
|
288
|
-
* Convert values based on operator type
|
|
289
|
-
*/
|
|
290
|
-
private _convertValue;
|
|
291
|
-
/**
|
|
292
|
-
* Parse $or conditions
|
|
293
|
-
*/
|
|
294
|
-
private _parseOr;
|
|
295
|
-
/**
|
|
296
|
-
* Enhance filters with between operator
|
|
297
|
-
*/
|
|
298
|
-
private _enhanceWithBetween;
|
|
299
|
-
}
|
|
300
|
-
/** Default query parser instance with standard options */
|
|
301
|
-
declare const defaultQueryParser: QueryParser;
|
|
302
|
-
|
|
303
|
-
export { type FilterValue as F, type OperatorMap as O, QueryParser as Q, getMongooseProjection as a, type QueryParserOptions as b, createFieldPreset as c, defaultQueryParser as d, buildCrudSchemasFromMongooseSchema as e, filterResponseData as f, getFieldsForUser as g, buildCrudSchemasFromModel as h, getImmutableFields as i, getSystemManagedFields as j, isFieldUpdateAllowed as k, createError as l, createMemoryCache as m, validateUpdateBody as v };
|
|
200
|
+
export { getMongooseProjection as a, buildCrudSchemasFromMongooseSchema as b, createFieldPreset as c, buildCrudSchemasFromModel as d, getImmutableFields as e, filterResponseData as f, getFieldsForUser as g, getSystemManagedFields as h, isFieldUpdateAllowed as i, createError as j, createMemoryCache as k, validateUpdateBody as v };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
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-
|
|
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-DA0rs2Jh.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Pagination Engine
|
|
@@ -1,368 +1,2 @@
|
|
|
1
|
-
|
|
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
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
175
|
-
constructor(Model, config = {}) {
|
|
176
|
-
this.Model = Model;
|
|
177
|
-
this.config = {
|
|
178
|
-
defaultLimit: config.defaultLimit || 10,
|
|
179
|
-
maxLimit: config.maxLimit || 100,
|
|
180
|
-
maxPage: config.maxPage || 1e4,
|
|
181
|
-
deepPageThreshold: config.deepPageThreshold || 100,
|
|
182
|
-
cursorVersion: config.cursorVersion || 1,
|
|
183
|
-
useEstimatedCount: config.useEstimatedCount || false
|
|
184
|
-
};
|
|
185
|
-
}
|
|
186
|
-
/**
|
|
187
|
-
* Offset-based pagination using skip/limit
|
|
188
|
-
* Best for small datasets and when users need random page access
|
|
189
|
-
* O(n) performance - slower for deep pages
|
|
190
|
-
*
|
|
191
|
-
* @param options - Pagination options
|
|
192
|
-
* @returns Pagination result with total count
|
|
193
|
-
*
|
|
194
|
-
* @example
|
|
195
|
-
* const result = await engine.paginate({
|
|
196
|
-
* filters: { status: 'active' },
|
|
197
|
-
* sort: { createdAt: -1 },
|
|
198
|
-
* page: 1,
|
|
199
|
-
* limit: 20
|
|
200
|
-
* });
|
|
201
|
-
* console.log(result.docs, result.total, result.hasNext);
|
|
202
|
-
*/
|
|
203
|
-
async paginate(options = {}) {
|
|
204
|
-
const {
|
|
205
|
-
filters = {},
|
|
206
|
-
sort = { _id: -1 },
|
|
207
|
-
page = 1,
|
|
208
|
-
limit = this.config.defaultLimit,
|
|
209
|
-
select,
|
|
210
|
-
populate = [],
|
|
211
|
-
lean = true,
|
|
212
|
-
session
|
|
213
|
-
} = options;
|
|
214
|
-
const sanitizedPage = validatePage(page, this.config);
|
|
215
|
-
const sanitizedLimit = validateLimit(limit, this.config);
|
|
216
|
-
const skip = calculateSkip(sanitizedPage, sanitizedLimit);
|
|
217
|
-
let query = this.Model.find(filters);
|
|
218
|
-
if (select) query = query.select(select);
|
|
219
|
-
if (populate && (Array.isArray(populate) ? populate.length : populate)) {
|
|
220
|
-
query = query.populate(populate);
|
|
221
|
-
}
|
|
222
|
-
query = query.sort(sort).skip(skip).limit(sanitizedLimit).lean(lean);
|
|
223
|
-
if (session) query = query.session(session);
|
|
224
|
-
const hasFilters = Object.keys(filters).length > 0;
|
|
225
|
-
const useEstimated = this.config.useEstimatedCount && !hasFilters;
|
|
226
|
-
const [docs, total] = await Promise.all([
|
|
227
|
-
query.exec(),
|
|
228
|
-
useEstimated ? this.Model.estimatedDocumentCount() : this.Model.countDocuments(filters).session(session ?? null)
|
|
229
|
-
]);
|
|
230
|
-
const totalPages = calculateTotalPages(total, sanitizedLimit);
|
|
231
|
-
const warning = shouldWarnDeepPagination(sanitizedPage, this.config.deepPageThreshold) ? `Deep pagination (page ${sanitizedPage}). Consider getAll({ after, sort, limit }) for better performance.` : void 0;
|
|
232
|
-
return {
|
|
233
|
-
method: "offset",
|
|
234
|
-
docs,
|
|
235
|
-
page: sanitizedPage,
|
|
236
|
-
limit: sanitizedLimit,
|
|
237
|
-
total,
|
|
238
|
-
pages: totalPages,
|
|
239
|
-
hasNext: sanitizedPage < totalPages,
|
|
240
|
-
hasPrev: sanitizedPage > 1,
|
|
241
|
-
...warning && { warning }
|
|
242
|
-
};
|
|
243
|
-
}
|
|
244
|
-
/**
|
|
245
|
-
* Keyset (cursor-based) pagination for high-performance streaming
|
|
246
|
-
* Best for large datasets, infinite scroll, real-time feeds
|
|
247
|
-
* O(1) performance - consistent speed regardless of position
|
|
248
|
-
*
|
|
249
|
-
* @param options - Pagination options (sort is required)
|
|
250
|
-
* @returns Pagination result with next cursor
|
|
251
|
-
*
|
|
252
|
-
* @example
|
|
253
|
-
* // First page
|
|
254
|
-
* const page1 = await engine.stream({
|
|
255
|
-
* sort: { createdAt: -1 },
|
|
256
|
-
* limit: 20
|
|
257
|
-
* });
|
|
258
|
-
*
|
|
259
|
-
* // Next page using cursor
|
|
260
|
-
* const page2 = await engine.stream({
|
|
261
|
-
* sort: { createdAt: -1 },
|
|
262
|
-
* after: page1.next,
|
|
263
|
-
* limit: 20
|
|
264
|
-
* });
|
|
265
|
-
*/
|
|
266
|
-
async stream(options) {
|
|
267
|
-
const {
|
|
268
|
-
filters = {},
|
|
269
|
-
sort,
|
|
270
|
-
after,
|
|
271
|
-
limit = this.config.defaultLimit,
|
|
272
|
-
select,
|
|
273
|
-
populate = [],
|
|
274
|
-
lean = true,
|
|
275
|
-
session
|
|
276
|
-
} = options;
|
|
277
|
-
if (!sort) {
|
|
278
|
-
throw createError(400, "sort is required for keyset pagination");
|
|
279
|
-
}
|
|
280
|
-
const sanitizedLimit = validateLimit(limit, this.config);
|
|
281
|
-
const normalizedSort = validateKeysetSort(sort);
|
|
282
|
-
let query = { ...filters };
|
|
283
|
-
if (after) {
|
|
284
|
-
const cursor = decodeCursor(after);
|
|
285
|
-
validateCursorVersion(cursor.version, this.config.cursorVersion);
|
|
286
|
-
validateCursorSort(cursor.sort, normalizedSort);
|
|
287
|
-
query = buildKeysetFilter(query, normalizedSort, cursor.value, cursor.id);
|
|
288
|
-
}
|
|
289
|
-
let mongoQuery = this.Model.find(query);
|
|
290
|
-
if (select) mongoQuery = mongoQuery.select(select);
|
|
291
|
-
if (populate && (Array.isArray(populate) ? populate.length : populate)) {
|
|
292
|
-
mongoQuery = mongoQuery.populate(populate);
|
|
293
|
-
}
|
|
294
|
-
mongoQuery = mongoQuery.sort(normalizedSort).limit(sanitizedLimit + 1).lean(lean);
|
|
295
|
-
if (session) mongoQuery = mongoQuery.session(session);
|
|
296
|
-
const docs = await mongoQuery.exec();
|
|
297
|
-
const hasMore = docs.length > sanitizedLimit;
|
|
298
|
-
if (hasMore) docs.pop();
|
|
299
|
-
const primaryField = getPrimaryField(normalizedSort);
|
|
300
|
-
const nextCursor = hasMore && docs.length > 0 ? encodeCursor(docs[docs.length - 1], primaryField, normalizedSort, this.config.cursorVersion) : null;
|
|
301
|
-
return {
|
|
302
|
-
method: "keyset",
|
|
303
|
-
docs,
|
|
304
|
-
limit: sanitizedLimit,
|
|
305
|
-
hasMore,
|
|
306
|
-
next: nextCursor
|
|
307
|
-
};
|
|
308
|
-
}
|
|
309
|
-
/**
|
|
310
|
-
* Aggregate pipeline with pagination
|
|
311
|
-
* Best for complex queries requiring aggregation stages
|
|
312
|
-
* Uses $facet to combine results and count in single query
|
|
313
|
-
*
|
|
314
|
-
* @param options - Aggregation options
|
|
315
|
-
* @returns Pagination result with total count
|
|
316
|
-
*
|
|
317
|
-
* @example
|
|
318
|
-
* const result = await engine.aggregatePaginate({
|
|
319
|
-
* pipeline: [
|
|
320
|
-
* { $match: { status: 'active' } },
|
|
321
|
-
* { $group: { _id: '$category', count: { $sum: 1 } } },
|
|
322
|
-
* { $sort: { count: -1 } }
|
|
323
|
-
* ],
|
|
324
|
-
* page: 1,
|
|
325
|
-
* limit: 20
|
|
326
|
-
* });
|
|
327
|
-
*/
|
|
328
|
-
async aggregatePaginate(options = {}) {
|
|
329
|
-
const {
|
|
330
|
-
pipeline = [],
|
|
331
|
-
page = 1,
|
|
332
|
-
limit = this.config.defaultLimit,
|
|
333
|
-
session
|
|
334
|
-
} = options;
|
|
335
|
-
const sanitizedPage = validatePage(page, this.config);
|
|
336
|
-
const sanitizedLimit = validateLimit(limit, this.config);
|
|
337
|
-
const skip = calculateSkip(sanitizedPage, sanitizedLimit);
|
|
338
|
-
const facetPipeline = [
|
|
339
|
-
...pipeline,
|
|
340
|
-
{
|
|
341
|
-
$facet: {
|
|
342
|
-
docs: [{ $skip: skip }, { $limit: sanitizedLimit }],
|
|
343
|
-
total: [{ $count: "count" }]
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
];
|
|
347
|
-
const aggregation = this.Model.aggregate(facetPipeline);
|
|
348
|
-
if (session) aggregation.session(session);
|
|
349
|
-
const [result] = await aggregation.exec();
|
|
350
|
-
const docs = result.docs;
|
|
351
|
-
const total = result.total[0]?.count || 0;
|
|
352
|
-
const totalPages = calculateTotalPages(total, sanitizedLimit);
|
|
353
|
-
const warning = shouldWarnDeepPagination(sanitizedPage, this.config.deepPageThreshold) ? `Deep pagination in aggregate (page ${sanitizedPage}). Uses $skip internally.` : void 0;
|
|
354
|
-
return {
|
|
355
|
-
method: "aggregate",
|
|
356
|
-
docs,
|
|
357
|
-
page: sanitizedPage,
|
|
358
|
-
limit: sanitizedLimit,
|
|
359
|
-
total,
|
|
360
|
-
pages: totalPages,
|
|
361
|
-
hasNext: sanitizedPage < totalPages,
|
|
362
|
-
hasPrev: sanitizedPage > 1,
|
|
363
|
-
...warning && { warning }
|
|
364
|
-
};
|
|
365
|
-
}
|
|
366
|
-
};
|
|
367
|
-
|
|
368
|
-
export { PaginationEngine };
|
|
1
|
+
export { PaginationEngine } from '../chunks/chunk-M2XHQGZB.js';
|
|
2
|
+
import '../chunks/chunk-VJXDGP3C.js';
|
package/dist/plugins/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { F as FieldPreset,
|
|
1
|
+
import { F as FieldPreset, G as Plugin, a2 as Logger, a3 as SoftDeleteOptions, L as RepositoryInstance, a0 as ValidatorDefinition, a1 as ValidationChainOptions, m as RepositoryContext, a8 as CacheOptions, ac as CascadeOptions } from '../types-DA0rs2Jh.js';
|
|
2
2
|
import 'mongoose';
|
|
3
3
|
|
|
4
4
|
/**
|