@classytic/mongokit 3.2.0 → 3.2.2
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 +470 -193
- package/dist/actions/index.d.mts +9 -0
- package/dist/actions/index.mjs +15 -0
- package/dist/aggregate-BAi4Do-X.mjs +767 -0
- package/dist/aggregate-CCHI7F51.d.mts +269 -0
- package/dist/ai/index.d.mts +125 -0
- package/dist/ai/index.mjs +203 -0
- package/dist/cache-keys-C8Z9B5sw.mjs +204 -0
- package/dist/chunk-DQk6qfdC.mjs +18 -0
- package/dist/create-BuO6xt0v.mjs +55 -0
- package/dist/custom-id.plugin-B_zIs6gE.mjs +1818 -0
- package/dist/custom-id.plugin-BzZI4gnE.d.mts +893 -0
- package/dist/index.d.mts +1012 -0
- package/dist/index.mjs +1906 -0
- package/dist/limits-DsNeCx4D.mjs +299 -0
- package/dist/logger-D8ily-PP.mjs +51 -0
- package/dist/mongooseToJsonSchema-COdDEkIJ.mjs +317 -0
- package/dist/{mongooseToJsonSchema-CaRF_bCN.d.ts → mongooseToJsonSchema-Wbvjfwkn.d.mts} +16 -89
- package/dist/pagination/PaginationEngine.d.mts +93 -0
- package/dist/pagination/PaginationEngine.mjs +196 -0
- package/dist/plugins/index.d.mts +3 -0
- package/dist/plugins/index.mjs +3 -0
- package/dist/types-D-gploPr.d.mts +1241 -0
- package/dist/utils/{index.d.ts → index.d.mts} +14 -21
- package/dist/utils/index.mjs +5 -0
- package/package.json +21 -21
- package/dist/actions/index.d.ts +0 -3
- package/dist/actions/index.js +0 -5
- package/dist/ai/index.d.ts +0 -175
- package/dist/ai/index.js +0 -206
- package/dist/chunks/chunk-2ZN65ZOP.js +0 -93
- package/dist/chunks/chunk-44KXLGPO.js +0 -388
- package/dist/chunks/chunk-DEVXDBRL.js +0 -1226
- package/dist/chunks/chunk-I7CWNAJB.js +0 -46
- package/dist/chunks/chunk-JWUAVZ3L.js +0 -8
- package/dist/chunks/chunk-UE2IEXZJ.js +0 -306
- package/dist/chunks/chunk-URLJFIR7.js +0 -22
- package/dist/chunks/chunk-VWKIKZYF.js +0 -737
- package/dist/chunks/chunk-WSFCRVEQ.js +0 -7
- package/dist/index-BDn5fSTE.d.ts +0 -516
- package/dist/index.d.ts +0 -1422
- package/dist/index.js +0 -1893
- package/dist/pagination/PaginationEngine.d.ts +0 -117
- package/dist/pagination/PaginationEngine.js +0 -3
- package/dist/plugins/index.d.ts +0 -922
- package/dist/plugins/index.js +0 -6
- package/dist/types-Jni1KgkP.d.ts +0 -780
- package/dist/utils/index.js +0 -5
|
@@ -1,27 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Field Selection Utilities
|
|
6
|
-
*
|
|
7
|
-
* Provides explicit, performant field filtering using Mongoose projections.
|
|
8
|
-
*
|
|
9
|
-
* Philosophy:
|
|
10
|
-
* - Explicit is better than implicit
|
|
11
|
-
* - Filter at DB level (10x faster than in-memory)
|
|
12
|
-
* - Progressive disclosure (show more fields as trust increases)
|
|
13
|
-
*
|
|
14
|
-
* @example
|
|
15
|
-
* ```typescript
|
|
16
|
-
* // For Mongoose queries (PREFERRED - 90% of cases)
|
|
17
|
-
* const projection = getMongooseProjection(request.user, fieldPresets.gymPlans);
|
|
18
|
-
* const plans = await GymPlan.find().select(projection).lean();
|
|
19
|
-
*
|
|
20
|
-
* // For complex data (10% of cases - aggregations, multiple sources)
|
|
21
|
-
* const filtered = filterResponseData(complexData, fieldPresets.gymPlans, request.user);
|
|
22
|
-
* ```
|
|
23
|
-
*/
|
|
1
|
+
import { $ as SchemaBuilderOptions, T as HttpError, dt as UserContext, m as CrudSchemas, o as CacheAdapter, pt as ValidationResult, x as FieldPreset } from "./types-D-gploPr.mjs";
|
|
2
|
+
import mongoose, { Schema } from "mongoose";
|
|
24
3
|
|
|
4
|
+
//#region src/utils/field-selection.d.ts
|
|
25
5
|
/**
|
|
26
6
|
* Get allowed fields for a user based on their context
|
|
27
7
|
*
|
|
@@ -88,13 +68,8 @@ declare function filterResponseData<T extends Record<string, unknown>>(data: T |
|
|
|
88
68
|
* });
|
|
89
69
|
*/
|
|
90
70
|
declare function createFieldPreset(config: Partial<FieldPreset>): FieldPreset;
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
* Error Utilities
|
|
94
|
-
*
|
|
95
|
-
* HTTP-compatible error creation for repository operations
|
|
96
|
-
*/
|
|
97
|
-
|
|
71
|
+
//#endregion
|
|
72
|
+
//#region src/utils/error.d.ts
|
|
98
73
|
/**
|
|
99
74
|
* Creates an error with HTTP status code
|
|
100
75
|
*
|
|
@@ -108,7 +83,8 @@ declare function createFieldPreset(config: Partial<FieldPreset>): FieldPreset;
|
|
|
108
83
|
* throw createError(403, 'Access denied');
|
|
109
84
|
*/
|
|
110
85
|
declare function createError(status: number, message: string): HttpError;
|
|
111
|
-
|
|
86
|
+
//#endregion
|
|
87
|
+
//#region src/utils/logger.d.ts
|
|
112
88
|
/**
|
|
113
89
|
* Internal Logger
|
|
114
90
|
*
|
|
@@ -131,34 +107,16 @@ declare function createError(status: number, message: string): HttpError;
|
|
|
131
107
|
*/
|
|
132
108
|
type LogFn = (message: string, ...args: unknown[]) => void;
|
|
133
109
|
interface LoggerConfig {
|
|
134
|
-
|
|
135
|
-
|
|
110
|
+
warn: LogFn;
|
|
111
|
+
debug: LogFn;
|
|
136
112
|
}
|
|
137
113
|
/**
|
|
138
114
|
* Configure the internal logger.
|
|
139
115
|
* Pass `false` to silence all output.
|
|
140
116
|
*/
|
|
141
117
|
declare function configureLogger(config: Partial<LoggerConfig> | false): void;
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
* In-Memory Cache Adapter
|
|
145
|
-
*
|
|
146
|
-
* Simple cache adapter for development and testing.
|
|
147
|
-
* NOT recommended for production - use Redis or similar.
|
|
148
|
-
*
|
|
149
|
-
* @example
|
|
150
|
-
* ```typescript
|
|
151
|
-
* import { cachePlugin, createMemoryCache } from '@classytic/mongokit';
|
|
152
|
-
*
|
|
153
|
-
* const repo = new Repository(UserModel, [
|
|
154
|
-
* cachePlugin({
|
|
155
|
-
* adapter: createMemoryCache(),
|
|
156
|
-
* ttl: 60,
|
|
157
|
-
* })
|
|
158
|
-
* ]);
|
|
159
|
-
* ```
|
|
160
|
-
*/
|
|
161
|
-
|
|
118
|
+
//#endregion
|
|
119
|
+
//#region src/utils/memory-cache.d.ts
|
|
162
120
|
/**
|
|
163
121
|
* Creates an in-memory cache adapter
|
|
164
122
|
*
|
|
@@ -170,39 +128,8 @@ declare function configureLogger(config: Partial<LoggerConfig> | false): void;
|
|
|
170
128
|
* @param maxEntries - Maximum cache entries before oldest are evicted (default: 1000)
|
|
171
129
|
*/
|
|
172
130
|
declare function createMemoryCache(maxEntries?: number): CacheAdapter;
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
* Mongoose to JSON Schema Converter with Field Rules
|
|
176
|
-
*
|
|
177
|
-
* Generates Fastify JSON schemas from Mongoose models with declarative field rules.
|
|
178
|
-
*
|
|
179
|
-
* Field Rules (options.fieldRules):
|
|
180
|
-
* - immutable: Field cannot be updated (omitted from update schema)
|
|
181
|
-
* - immutableAfterCreate: Alias for immutable
|
|
182
|
-
* - systemManaged: System-only field (omitted from create/update)
|
|
183
|
-
* - optional: Remove from required array
|
|
184
|
-
*
|
|
185
|
-
* Additional Options:
|
|
186
|
-
* - strictAdditionalProperties: Set to true to add "additionalProperties: false" to schemas
|
|
187
|
-
* This makes Fastify reject unknown fields at validation level (default: false for backward compatibility)
|
|
188
|
-
* - update.requireAtLeastOne: Set to true to add "minProperties: 1" to update schema
|
|
189
|
-
* This prevents empty update payloads (default: false)
|
|
190
|
-
*
|
|
191
|
-
* @example
|
|
192
|
-
* buildCrudSchemasFromModel(Model, {
|
|
193
|
-
* strictAdditionalProperties: true, // Reject unknown fields
|
|
194
|
-
* fieldRules: {
|
|
195
|
-
* organizationId: { immutable: true },
|
|
196
|
-
* status: { systemManaged: true },
|
|
197
|
-
* },
|
|
198
|
-
* create: { omitFields: ['verifiedAt'] },
|
|
199
|
-
* update: {
|
|
200
|
-
* omitFields: ['customerId'],
|
|
201
|
-
* requireAtLeastOne: true // Reject empty updates
|
|
202
|
-
* }
|
|
203
|
-
* })
|
|
204
|
-
*/
|
|
205
|
-
|
|
131
|
+
//#endregion
|
|
132
|
+
//#region src/utils/mongooseToJsonSchema.d.ts
|
|
206
133
|
/**
|
|
207
134
|
* Build CRUD schemas from Mongoose schema
|
|
208
135
|
*/
|
|
@@ -210,7 +137,7 @@ declare function buildCrudSchemasFromMongooseSchema(mongooseSchema: Schema, opti
|
|
|
210
137
|
/**
|
|
211
138
|
* Build CRUD schemas from Mongoose model
|
|
212
139
|
*/
|
|
213
|
-
declare function buildCrudSchemasFromModel(mongooseModel:
|
|
140
|
+
declare function buildCrudSchemasFromModel(mongooseModel: mongoose.Model<unknown>, options?: SchemaBuilderOptions): CrudSchemas;
|
|
214
141
|
/**
|
|
215
142
|
* Get fields that are immutable (cannot be updated)
|
|
216
143
|
*/
|
|
@@ -227,5 +154,5 @@ declare function isFieldUpdateAllowed(fieldName: string, options?: SchemaBuilder
|
|
|
227
154
|
* Validate update body against field rules
|
|
228
155
|
*/
|
|
229
156
|
declare function validateUpdateBody(body?: Record<string, unknown>, options?: SchemaBuilderOptions): ValidationResult;
|
|
230
|
-
|
|
231
|
-
export {
|
|
157
|
+
//#endregion
|
|
158
|
+
export { isFieldUpdateAllowed as a, configureLogger as c, filterResponseData as d, getFieldsForUser as f, getSystemManagedFields as i, createError as l, buildCrudSchemasFromMongooseSchema as n, validateUpdateBody as o, getMongooseProjection as p, getImmutableFields as r, createMemoryCache as s, buildCrudSchemasFromModel as t, createFieldPreset as u };
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { A as KeysetPaginationOptions, I as OffsetPaginationOptions, L as OffsetPaginationResult, i as AnyDocument, j as KeysetPaginationResult, n as AggregatePaginationResult, t as AggregatePaginationOptions, z as PaginationConfig } from "../types-D-gploPr.mjs";
|
|
2
|
+
import { Model } from "mongoose";
|
|
3
|
+
|
|
4
|
+
//#region src/pagination/PaginationEngine.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* Internal pagination config with required values
|
|
7
|
+
*/
|
|
8
|
+
interface ResolvedPaginationConfig {
|
|
9
|
+
defaultLimit: number;
|
|
10
|
+
maxLimit: number;
|
|
11
|
+
maxPage: number;
|
|
12
|
+
deepPageThreshold: number;
|
|
13
|
+
cursorVersion: number;
|
|
14
|
+
useEstimatedCount: boolean;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Production-grade pagination engine for MongoDB
|
|
18
|
+
* Supports offset, keyset (cursor), and aggregate pagination
|
|
19
|
+
*/
|
|
20
|
+
declare class PaginationEngine<TDoc = AnyDocument> {
|
|
21
|
+
readonly Model: Model<TDoc>;
|
|
22
|
+
readonly config: ResolvedPaginationConfig;
|
|
23
|
+
/**
|
|
24
|
+
* Create a new pagination engine
|
|
25
|
+
*
|
|
26
|
+
* @param Model - Mongoose model to paginate
|
|
27
|
+
* @param config - Pagination configuration
|
|
28
|
+
*/
|
|
29
|
+
constructor(Model: Model<TDoc, any, any, any>, config?: PaginationConfig);
|
|
30
|
+
/**
|
|
31
|
+
* Offset-based pagination using skip/limit
|
|
32
|
+
* Best for small datasets and when users need random page access
|
|
33
|
+
* O(n) performance - slower for deep pages
|
|
34
|
+
*
|
|
35
|
+
* @param options - Pagination options
|
|
36
|
+
* @returns Pagination result with total count
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* const result = await engine.paginate({
|
|
40
|
+
* filters: { status: 'active' },
|
|
41
|
+
* sort: { createdAt: -1 },
|
|
42
|
+
* page: 1,
|
|
43
|
+
* limit: 20
|
|
44
|
+
* });
|
|
45
|
+
* console.log(result.docs, result.total, result.hasNext);
|
|
46
|
+
*/
|
|
47
|
+
paginate(options?: OffsetPaginationOptions): Promise<OffsetPaginationResult<TDoc>>;
|
|
48
|
+
/**
|
|
49
|
+
* Keyset (cursor-based) pagination for high-performance streaming
|
|
50
|
+
* Best for large datasets, infinite scroll, real-time feeds
|
|
51
|
+
* O(1) performance - consistent speed regardless of position
|
|
52
|
+
*
|
|
53
|
+
* @param options - Pagination options (sort is required)
|
|
54
|
+
* @returns Pagination result with next cursor
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* // First page
|
|
58
|
+
* const page1 = await engine.stream({
|
|
59
|
+
* sort: { createdAt: -1 },
|
|
60
|
+
* limit: 20
|
|
61
|
+
* });
|
|
62
|
+
*
|
|
63
|
+
* // Next page using cursor
|
|
64
|
+
* const page2 = await engine.stream({
|
|
65
|
+
* sort: { createdAt: -1 },
|
|
66
|
+
* after: page1.next,
|
|
67
|
+
* limit: 20
|
|
68
|
+
* });
|
|
69
|
+
*/
|
|
70
|
+
stream(options: KeysetPaginationOptions): Promise<KeysetPaginationResult<TDoc>>;
|
|
71
|
+
/**
|
|
72
|
+
* Aggregate pipeline with pagination
|
|
73
|
+
* Best for complex queries requiring aggregation stages
|
|
74
|
+
* Uses $facet to combine results and count in single query
|
|
75
|
+
*
|
|
76
|
+
* @param options - Aggregation options
|
|
77
|
+
* @returns Pagination result with total count
|
|
78
|
+
*
|
|
79
|
+
* @example
|
|
80
|
+
* const result = await engine.aggregatePaginate({
|
|
81
|
+
* pipeline: [
|
|
82
|
+
* { $match: { status: 'active' } },
|
|
83
|
+
* { $group: { _id: '$category', count: { $sum: 1 } } },
|
|
84
|
+
* { $sort: { count: -1 } }
|
|
85
|
+
* ],
|
|
86
|
+
* page: 1,
|
|
87
|
+
* limit: 20
|
|
88
|
+
* });
|
|
89
|
+
*/
|
|
90
|
+
aggregatePaginate(options?: AggregatePaginationOptions): Promise<AggregatePaginationResult<TDoc>>;
|
|
91
|
+
}
|
|
92
|
+
//#endregion
|
|
93
|
+
export { PaginationEngine };
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { i as createError, r as warn } from "../logger-D8ily-PP.mjs";
|
|
2
|
+
import { a as validatePage, c as validateKeysetSort, d as validateCursorSort, f as validateCursorVersion, i as validateLimit, l as decodeCursor, n as calculateTotalPages, o as buildKeysetFilter, r as shouldWarnDeepPagination, s as getPrimaryField, t as calculateSkip, u as encodeCursor } from "../limits-DsNeCx4D.mjs";
|
|
3
|
+
|
|
4
|
+
//#region src/pagination/PaginationEngine.ts
|
|
5
|
+
/**
|
|
6
|
+
* Production-grade pagination engine for MongoDB
|
|
7
|
+
* Supports offset, keyset (cursor), and aggregate pagination
|
|
8
|
+
*/
|
|
9
|
+
var PaginationEngine = class {
|
|
10
|
+
Model;
|
|
11
|
+
config;
|
|
12
|
+
/**
|
|
13
|
+
* Create a new pagination engine
|
|
14
|
+
*
|
|
15
|
+
* @param Model - Mongoose model to paginate
|
|
16
|
+
* @param config - Pagination configuration
|
|
17
|
+
*/
|
|
18
|
+
constructor(Model, config = {}) {
|
|
19
|
+
this.Model = Model;
|
|
20
|
+
this.config = {
|
|
21
|
+
defaultLimit: config.defaultLimit || 10,
|
|
22
|
+
maxLimit: config.maxLimit || 100,
|
|
23
|
+
maxPage: config.maxPage || 1e4,
|
|
24
|
+
deepPageThreshold: config.deepPageThreshold || 100,
|
|
25
|
+
cursorVersion: config.cursorVersion || 1,
|
|
26
|
+
useEstimatedCount: config.useEstimatedCount || false
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Offset-based pagination using skip/limit
|
|
31
|
+
* Best for small datasets and when users need random page access
|
|
32
|
+
* O(n) performance - slower for deep pages
|
|
33
|
+
*
|
|
34
|
+
* @param options - Pagination options
|
|
35
|
+
* @returns Pagination result with total count
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* const result = await engine.paginate({
|
|
39
|
+
* filters: { status: 'active' },
|
|
40
|
+
* sort: { createdAt: -1 },
|
|
41
|
+
* page: 1,
|
|
42
|
+
* limit: 20
|
|
43
|
+
* });
|
|
44
|
+
* console.log(result.docs, result.total, result.hasNext);
|
|
45
|
+
*/
|
|
46
|
+
async paginate(options = {}) {
|
|
47
|
+
const { filters = {}, sort = { _id: -1 }, page = 1, limit = this.config.defaultLimit, select, populate = [], lean = true, session, hint, maxTimeMS, countStrategy = "exact", readPreference } = options;
|
|
48
|
+
const sanitizedPage = validatePage(page, this.config);
|
|
49
|
+
const sanitizedLimit = validateLimit(limit, this.config);
|
|
50
|
+
const skip = calculateSkip(sanitizedPage, sanitizedLimit);
|
|
51
|
+
let query = this.Model.find(filters);
|
|
52
|
+
if (select) query = query.select(select);
|
|
53
|
+
if (populate && (Array.isArray(populate) ? populate.length : populate)) query = query.populate(populate);
|
|
54
|
+
query = query.sort(sort).skip(skip).limit(sanitizedLimit).lean(lean);
|
|
55
|
+
if (session) query = query.session(session);
|
|
56
|
+
if (hint) query = query.hint(hint);
|
|
57
|
+
if (maxTimeMS) query = query.maxTimeMS(maxTimeMS);
|
|
58
|
+
if (readPreference) query = query.read(readPreference);
|
|
59
|
+
const hasFilters = Object.keys(filters).length > 0;
|
|
60
|
+
const useEstimated = this.config.useEstimatedCount && !hasFilters;
|
|
61
|
+
let total = 0;
|
|
62
|
+
if (countStrategy === "estimated" || useEstimated && countStrategy !== "exact") total = await this.Model.estimatedDocumentCount();
|
|
63
|
+
else if (countStrategy === "exact") {
|
|
64
|
+
const countQuery = this.Model.countDocuments(filters).session(session ?? null);
|
|
65
|
+
if (hint) countQuery.hint(hint);
|
|
66
|
+
if (maxTimeMS) countQuery.maxTimeMS(maxTimeMS);
|
|
67
|
+
if (readPreference) countQuery.read(readPreference);
|
|
68
|
+
total = await countQuery;
|
|
69
|
+
}
|
|
70
|
+
const [docs] = await Promise.all([query.exec()]);
|
|
71
|
+
const totalPages = countStrategy === "none" ? 0 : calculateTotalPages(total, sanitizedLimit);
|
|
72
|
+
const warning = shouldWarnDeepPagination(sanitizedPage, this.config.deepPageThreshold) ? `Deep pagination (page ${sanitizedPage}). Consider getAll({ after, sort, limit }) for better performance.` : void 0;
|
|
73
|
+
return {
|
|
74
|
+
method: "offset",
|
|
75
|
+
docs,
|
|
76
|
+
page: sanitizedPage,
|
|
77
|
+
limit: sanitizedLimit,
|
|
78
|
+
total,
|
|
79
|
+
pages: totalPages,
|
|
80
|
+
hasNext: countStrategy === "none" ? docs.length === sanitizedLimit : sanitizedPage < totalPages,
|
|
81
|
+
hasPrev: sanitizedPage > 1,
|
|
82
|
+
...warning && { warning }
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Keyset (cursor-based) pagination for high-performance streaming
|
|
87
|
+
* Best for large datasets, infinite scroll, real-time feeds
|
|
88
|
+
* O(1) performance - consistent speed regardless of position
|
|
89
|
+
*
|
|
90
|
+
* @param options - Pagination options (sort is required)
|
|
91
|
+
* @returns Pagination result with next cursor
|
|
92
|
+
*
|
|
93
|
+
* @example
|
|
94
|
+
* // First page
|
|
95
|
+
* const page1 = await engine.stream({
|
|
96
|
+
* sort: { createdAt: -1 },
|
|
97
|
+
* limit: 20
|
|
98
|
+
* });
|
|
99
|
+
*
|
|
100
|
+
* // Next page using cursor
|
|
101
|
+
* const page2 = await engine.stream({
|
|
102
|
+
* sort: { createdAt: -1 },
|
|
103
|
+
* after: page1.next,
|
|
104
|
+
* limit: 20
|
|
105
|
+
* });
|
|
106
|
+
*/
|
|
107
|
+
async stream(options) {
|
|
108
|
+
const { filters = {}, sort, after, limit = this.config.defaultLimit, select, populate = [], lean = true, session, hint, maxTimeMS, readPreference } = options;
|
|
109
|
+
if (!sort) throw createError(400, "sort is required for keyset pagination");
|
|
110
|
+
const sanitizedLimit = validateLimit(limit, this.config);
|
|
111
|
+
const normalizedSort = validateKeysetSort(sort);
|
|
112
|
+
const filterKeys = Object.keys(filters).filter((k) => !k.startsWith("$"));
|
|
113
|
+
const sortFields = Object.keys(normalizedSort);
|
|
114
|
+
if (filterKeys.length > 0 && sortFields.length > 0) {
|
|
115
|
+
const indexFields = [...filterKeys.map((f) => `${f}: 1`), ...sortFields.map((f) => `${f}: ${normalizedSort[f]}`)];
|
|
116
|
+
warn(`[mongokit] Keyset pagination with filters [${filterKeys.join(", ")}] and sort [${sortFields.join(", ")}] requires a compound index for O(1) performance. Ensure index exists: { ${indexFields.join(", ")} }`);
|
|
117
|
+
}
|
|
118
|
+
let query = { ...filters };
|
|
119
|
+
if (after) {
|
|
120
|
+
const cursor = decodeCursor(after);
|
|
121
|
+
validateCursorVersion(cursor.version, this.config.cursorVersion);
|
|
122
|
+
validateCursorSort(cursor.sort, normalizedSort);
|
|
123
|
+
query = buildKeysetFilter(query, normalizedSort, cursor.value, cursor.id);
|
|
124
|
+
}
|
|
125
|
+
let mongoQuery = this.Model.find(query);
|
|
126
|
+
if (select) mongoQuery = mongoQuery.select(select);
|
|
127
|
+
if (populate && (Array.isArray(populate) ? populate.length : populate)) mongoQuery = mongoQuery.populate(populate);
|
|
128
|
+
mongoQuery = mongoQuery.sort(normalizedSort).limit(sanitizedLimit + 1).lean(lean);
|
|
129
|
+
if (session) mongoQuery = mongoQuery.session(session);
|
|
130
|
+
if (hint) mongoQuery = mongoQuery.hint(hint);
|
|
131
|
+
if (maxTimeMS) mongoQuery = mongoQuery.maxTimeMS(maxTimeMS);
|
|
132
|
+
if (readPreference) mongoQuery = mongoQuery.read(readPreference);
|
|
133
|
+
const docs = await mongoQuery.exec();
|
|
134
|
+
const hasMore = docs.length > sanitizedLimit;
|
|
135
|
+
if (hasMore) docs.pop();
|
|
136
|
+
const primaryField = getPrimaryField(normalizedSort);
|
|
137
|
+
return {
|
|
138
|
+
method: "keyset",
|
|
139
|
+
docs,
|
|
140
|
+
limit: sanitizedLimit,
|
|
141
|
+
hasMore,
|
|
142
|
+
next: hasMore && docs.length > 0 ? encodeCursor(docs[docs.length - 1], primaryField, normalizedSort, this.config.cursorVersion) : null
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Aggregate pipeline with pagination
|
|
147
|
+
* Best for complex queries requiring aggregation stages
|
|
148
|
+
* Uses $facet to combine results and count in single query
|
|
149
|
+
*
|
|
150
|
+
* @param options - Aggregation options
|
|
151
|
+
* @returns Pagination result with total count
|
|
152
|
+
*
|
|
153
|
+
* @example
|
|
154
|
+
* const result = await engine.aggregatePaginate({
|
|
155
|
+
* pipeline: [
|
|
156
|
+
* { $match: { status: 'active' } },
|
|
157
|
+
* { $group: { _id: '$category', count: { $sum: 1 } } },
|
|
158
|
+
* { $sort: { count: -1 } }
|
|
159
|
+
* ],
|
|
160
|
+
* page: 1,
|
|
161
|
+
* limit: 20
|
|
162
|
+
* });
|
|
163
|
+
*/
|
|
164
|
+
async aggregatePaginate(options = {}) {
|
|
165
|
+
const { pipeline = [], page = 1, limit = this.config.defaultLimit, session, hint, maxTimeMS, countStrategy = "exact", readPreference } = options;
|
|
166
|
+
const sanitizedPage = validatePage(page, this.config);
|
|
167
|
+
const sanitizedLimit = validateLimit(limit, this.config);
|
|
168
|
+
const facetStages = { docs: [{ $skip: calculateSkip(sanitizedPage, sanitizedLimit) }, { $limit: sanitizedLimit }] };
|
|
169
|
+
if (countStrategy !== "none") facetStages.total = [{ $count: "count" }];
|
|
170
|
+
const facetPipeline = [...pipeline, { $facet: facetStages }];
|
|
171
|
+
const aggregation = this.Model.aggregate(facetPipeline);
|
|
172
|
+
if (session) aggregation.session(session);
|
|
173
|
+
if (hint) aggregation.hint(hint);
|
|
174
|
+
if (maxTimeMS) aggregation.option({ maxTimeMS });
|
|
175
|
+
if (readPreference) aggregation.read(readPreference);
|
|
176
|
+
const [result] = await aggregation.exec();
|
|
177
|
+
const docs = result.docs;
|
|
178
|
+
const total = result.total?.[0]?.count || 0;
|
|
179
|
+
const totalPages = countStrategy === "none" ? 0 : calculateTotalPages(total, sanitizedLimit);
|
|
180
|
+
const warning = shouldWarnDeepPagination(sanitizedPage, this.config.deepPageThreshold) ? `Deep pagination in aggregate (page ${sanitizedPage}). Uses $skip internally.` : void 0;
|
|
181
|
+
return {
|
|
182
|
+
method: "aggregate",
|
|
183
|
+
docs,
|
|
184
|
+
page: sanitizedPage,
|
|
185
|
+
limit: sanitizedLimit,
|
|
186
|
+
total,
|
|
187
|
+
pages: totalPages,
|
|
188
|
+
hasNext: countStrategy === "none" ? docs.length === sanitizedLimit : sanitizedPage < totalPages,
|
|
189
|
+
hasPrev: sanitizedPage > 1,
|
|
190
|
+
...warning && { warning }
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
//#endregion
|
|
196
|
+
export { PaginationEngine };
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import "../types-D-gploPr.mjs";
|
|
2
|
+
import { A as blockIf, B as timestampPlugin, C as AggregateHelpersMethods, D as MongoOperationsMethods, E as batchOperationsPlugin, F as MethodRegistryRepository, I as methodRegistryPlugin, L as SoftDeleteMethods, M as requireField, N as uniqueField, O as mongoOperationsPlugin, P as validationChainPlugin, R as softDeletePlugin, S as subdocumentPlugin, T as BatchOperationsMethods, V as fieldFilterPlugin, _ as multiTenantPlugin, a as SequentialIdOptions, b as cachePlugin, c as getNextSequence, d as ElasticSearchOptions, f as elasticSearchPlugin, g as MultiTenantOptions, h as observabilityPlugin, i as PrefixedIdOptions, j as immutableField, k as autoInject, l as prefixedId, m as OperationMetric, n as DateSequentialIdOptions, o as customIdPlugin, p as ObservabilityOptions, r as IdGenerator, s as dateSequentialId, t as CustomIdOptions, u as sequentialId, v as cascadePlugin, w as aggregateHelpersPlugin, x as SubdocumentMethods, y as CacheMethods, z as auditLogPlugin } from "../custom-id.plugin-BzZI4gnE.mjs";
|
|
3
|
+
export { type AggregateHelpersMethods, type BatchOperationsMethods, type CacheMethods, type CustomIdOptions, type DateSequentialIdOptions, type ElasticSearchOptions, type IdGenerator, type MethodRegistryRepository, type MongoOperationsMethods, type MultiTenantOptions, type ObservabilityOptions, type OperationMetric, type PrefixedIdOptions, type SequentialIdOptions, type SoftDeleteMethods, type SubdocumentMethods, aggregateHelpersPlugin, auditLogPlugin, autoInject, batchOperationsPlugin, blockIf, cachePlugin, cascadePlugin, customIdPlugin, dateSequentialId, elasticSearchPlugin, fieldFilterPlugin, getNextSequence, immutableField, methodRegistryPlugin, mongoOperationsPlugin, multiTenantPlugin, observabilityPlugin, prefixedId, requireField, sequentialId, softDeletePlugin, subdocumentPlugin, timestampPlugin, uniqueField, validationChainPlugin };
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { C as auditLogPlugin, S as softDeletePlugin, T as fieldFilterPlugin, _ as immutableField, a as sequentialId, b as validationChainPlugin, c as multiTenantPlugin, d as subdocumentPlugin, f as aggregateHelpersPlugin, g as blockIf, h as autoInject, i as prefixedId, l as cascadePlugin, m as mongoOperationsPlugin, n as dateSequentialId, o as elasticSearchPlugin, p as batchOperationsPlugin, r as getNextSequence, s as observabilityPlugin, t as customIdPlugin, u as cachePlugin, v as requireField, w as timestampPlugin, x as methodRegistryPlugin, y as uniqueField } from "../custom-id.plugin-B_zIs6gE.mjs";
|
|
2
|
+
|
|
3
|
+
export { aggregateHelpersPlugin, auditLogPlugin, autoInject, batchOperationsPlugin, blockIf, cachePlugin, cascadePlugin, customIdPlugin, dateSequentialId, elasticSearchPlugin, fieldFilterPlugin, getNextSequence, immutableField, methodRegistryPlugin, mongoOperationsPlugin, multiTenantPlugin, observabilityPlugin, prefixedId, requireField, sequentialId, softDeletePlugin, subdocumentPlugin, timestampPlugin, uniqueField, validationChainPlugin };
|