@classytic/mongokit 3.3.2 → 3.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.
package/dist/index.mjs CHANGED
@@ -1,87 +1,13 @@
1
- import { i as createError, r as warn, t as configureLogger } from "./logger-D8ily-PP.mjs";
2
- import { r as LookupBuilder } from "./aggregate-BClp040M.mjs";
3
- import { PaginationEngine } from "./pagination/PaginationEngine.mjs";
4
- import { A as AggregationBuilder, C as methodRegistryPlugin, D as fieldFilterPlugin, E as timestampPlugin, O as HOOK_PRIORITY, S as validationChainPlugin, T as auditLogPlugin, _ as autoInject, a as sequentialId, b as requireField, c as auditTrailPlugin, d as cascadePlugin, f as cachePlugin, g as mongoOperationsPlugin, h as batchOperationsPlugin, i as prefixedId, k as Repository, l as observabilityPlugin, m as aggregateHelpersPlugin, n as dateSequentialId, o as elasticSearchPlugin, p as subdocumentPlugin, r as getNextSequence, s as AuditTrailQuery, t as customIdPlugin, u as multiTenantPlugin, v as blockIf, w as softDeletePlugin, x as uniqueField, y as immutableField } from "./custom-id.plugin-FInXDsUX.mjs";
5
- import { c as filterResponseData, l as getFieldsForUser, s as createFieldPreset, u as getMongooseProjection } from "./cache-keys-CzFwVnLy.mjs";
6
- import { a as isFieldUpdateAllowed, i as getSystemManagedFields, n as buildCrudSchemasFromMongooseSchema, o as validateUpdateBody, r as getImmutableFields, s as createMemoryCache, t as buildCrudSchemasFromModel } from "./mongooseToJsonSchema-D_i2Am_O.mjs";
1
+ import { a as warn, n as parseDuplicateKeyError, r as configureLogger, t as createError } from "./error-Bpbi_NKo.mjs";
2
+ import { _ as LookupBuilder } from "./update-DXwVh6M1.mjs";
7
3
  import { t as actions_exports } from "./actions/index.mjs";
4
+ import { t as PaginationEngine } from "./PaginationEngine-PLyDhrO7.mjs";
5
+ import { A as aggregateHelpersPlugin, C as HOOK_PRIORITY, D as AuditTrailQuery, E as batchOperationsPlugin, O as auditTrailPlugin, S as cachePlugin, T as AggregationBuilder, _ as dateSequentialId, a as uniqueField, b as sequentialId, c as subdocumentPlugin, d as multiTenantPlugin, f as mongoOperationsPlugin, g as customIdPlugin, h as elasticSearchPlugin, i as requireField, k as auditLogPlugin, l as softDeletePlugin, m as fieldFilterPlugin, n as blockIf, o as validationChainPlugin, p as methodRegistryPlugin, r as immutableField, s as timestampPlugin, t as autoInject, u as observabilityPlugin, v as getNextSequence, w as Repository, x as cascadePlugin, y as prefixedId } from "./validation-chain.plugin-Ow6EUIoo.mjs";
6
+ import { i as getMongooseProjection, n as filterResponseData, r as getFieldsForUser, t as createFieldPreset } from "./field-selection-CalOB7yM.mjs";
7
+ import { a as isFieldUpdateAllowed, i as getSystemManagedFields, n as buildCrudSchemasFromMongooseSchema, o as validateUpdateBody, r as getImmutableFields, s as createMemoryCache, t as buildCrudSchemasFromModel } from "./mongooseToJsonSchema-OmdmnHtx.mjs";
8
8
  import mongoose from "mongoose";
9
-
10
9
  //#region src/query/QueryParser.ts
11
10
  /**
12
- * Modern Query Parser - URL to MongoDB Query Transpiler
13
- *
14
- * Next-generation query parser that converts URL parameters to MongoDB aggregation pipelines.
15
- * Smarter than Prisma/tRPC for MongoDB with support for:
16
- * - Custom field lookups ($lookup)
17
- * - Complex filtering with operators
18
- * - Full-text search
19
- * - Aggregations via URL
20
- * - Security hardening
21
- *
22
- * @example
23
- * ```typescript
24
- * // Simple usage
25
- * const parser = new QueryParser();
26
- * const query = parser.parse(req.query);
27
- *
28
- * // URL: ?status=active&lookup[department]=slug&sort=-createdAt&page=1&limit=20
29
- * // Result: Complete MongoDB query with $lookup, filters, sort, pagination
30
- * ```
31
- *
32
- * ## SECURITY CONSIDERATIONS FOR PRODUCTION
33
- *
34
- * ### Aggregation Security (enableAggregations option)
35
- *
36
- * **IMPORTANT:** The `enableAggregations` option exposes powerful MongoDB aggregation
37
- * pipeline capabilities via URL parameters. While this feature includes sanitization
38
- * (blocks $where, $function, $accumulator), it should be used with caution:
39
- *
40
- * **Recommended security practices:**
41
- * 1. **Disable by default for public endpoints:**
42
- * ```typescript
43
- * const parser = new QueryParser({
44
- * enableAggregations: false // Default: disabled
45
- * });
46
- * ```
47
- *
48
- * 2. **Use per-route allowlists for trusted clients:**
49
- * ```typescript
50
- * // Admin/internal routes only
51
- * if (req.user?.role === 'admin') {
52
- * const allowedStages = ['$match', '$project', '$sort', '$limit'];
53
- * // Validate aggregate parameter against allowlist
54
- * }
55
- * ```
56
- *
57
- * 3. **Validate stage structure:** Even with sanitization, complex pipelines can
58
- * cause performance issues. Consider limiting:
59
- * - Number of pipeline stages (e.g., max 5)
60
- * - Specific allowed operators per stage
61
- * - Allowed fields in $project/$match
62
- *
63
- * 4. **Monitor resource usage:** Aggregation pipelines can be expensive.
64
- * Use MongoDB profiling to track slow operations.
65
- *
66
- * ### Lookup Security
67
- *
68
- * Lookup pipelines are sanitized by default:
69
- * - Dangerous stages blocked ($out, $merge, $unionWith, $collStats, $currentOp, $listSessions)
70
- * - Dangerous operators blocked inside $match/$addFields/$set ($where, $function, $accumulator, $expr)
71
- * - Optional collection whitelist via `allowedLookupCollections`
72
- * For maximum security, use per-collection field allowlists in your controller layer.
73
- *
74
- * ### Filter Security
75
- *
76
- * All filters are sanitized:
77
- * - Dangerous operators blocked ($where, $function, $accumulator, $expr)
78
- * - Regex patterns validated (ReDoS protection)
79
- * - Max filter depth enforced (prevents filter bombs)
80
- * - Max limit enforced (prevents resource exhaustion)
81
- *
82
- * @see {@link https://github.com/classytic/mongokit/blob/main/docs/SECURITY.md}
83
- */
84
- /**
85
11
  * Modern Query Parser
86
12
  * Converts URL parameters to MongoDB queries with $lookup support
87
13
  */
@@ -113,7 +39,7 @@ var QueryParser = class {
113
39
  * - Backreferences: \1, \2, etc.
114
40
  * - Complex character classes: [...]...[...]
115
41
  */
116
- dangerousRegexPatterns = /(\{[0-9,]+\}|\*\+|\+\+|\?\+|(\(.+\))\+|\(\?\:|\\[0-9]|(\[.+\]).+(\[.+\]))/;
42
+ dangerousRegexPatterns = /(\{[0-9,]+\}|\*\+|\+\+|\?\+|(\(.+\))\+|\(\?:|\\[0-9]|(\[.+\]).+(\[.+\]))/;
117
43
  constructor(options = {}) {
118
44
  this.options = {
119
45
  maxRegexLength: options.maxRegexLength ?? 500,
@@ -155,7 +81,7 @@ var QueryParser = class {
155
81
  parse(query) {
156
82
  const { page, limit = 20, sort = "-createdAt", populate, search, after, cursor, select, lookup, aggregate, ...filters } = query || {};
157
83
  let parsedLimit = parseInt(String(limit), 10);
158
- if (isNaN(parsedLimit) || parsedLimit < 1) parsedLimit = 20;
84
+ if (Number.isNaN(parsedLimit) || parsedLimit < 1) parsedLimit = 20;
159
85
  if (parsedLimit > this.options.maxLimit) {
160
86
  warn(`[mongokit] Limit ${parsedLimit} exceeds maximum ${this.options.maxLimit}, capping to max`);
161
87
  parsedLimit = this.options.maxLimit;
@@ -248,15 +174,15 @@ var QueryParser = class {
248
174
  description: "Cursor value for keyset pagination"
249
175
  }
250
176
  };
251
- if (this.options.enableLookups) properties["lookup"] = {
177
+ if (this.options.enableLookups) properties.lookup = {
252
178
  type: "object",
253
179
  description: "Custom field lookups ($lookup). Example: lookup[department]=slug or lookup[department][localField]=deptId&lookup[department][foreignField]=_id"
254
180
  };
255
- if (this.options.enableAggregations) properties["aggregate"] = {
181
+ if (this.options.enableAggregations) properties.aggregate = {
256
182
  type: "object",
257
183
  description: "Aggregation pipeline stages. Supports: group, match, sort, project. Example: aggregate[group][_id]=$status"
258
184
  };
259
- const availableOperators = this.options.allowedOperators ? Object.entries(this.operators).filter(([key]) => this.options.allowedOperators.includes(key)) : Object.entries(this.operators);
185
+ const availableOperators = this.options.allowedOperators ? Object.entries(this.operators).filter(([key]) => this.options.allowedOperators?.includes(key)) : Object.entries(this.operators);
260
186
  if (this.options.allowedFilterFields && this.options.allowedFilterFields.length > 0) for (const field of this.options.allowedFilterFields) {
261
187
  properties[field] = {
262
188
  type: "string",
@@ -283,8 +209,8 @@ var QueryParser = class {
283
209
  */
284
210
  getOpenAPIQuerySchema() {
285
211
  const schema = this.getQuerySchema();
286
- const availableOperators = this.options.allowedOperators ? Object.entries(this.operators).filter(([key]) => this.options.allowedOperators.includes(key)) : Object.entries(this.operators);
287
- schema.properties["_filterOperators"] = {
212
+ const availableOperators = this.options.allowedOperators ? Object.entries(this.operators).filter(([key]) => this.options.allowedOperators?.includes(key)) : Object.entries(this.operators);
213
+ schema.properties._filterOperators = {
288
214
  type: "string",
289
215
  description: this._buildOperatorDescription(availableOperators),
290
216
  "x-internal": true
@@ -420,6 +346,7 @@ var QueryParser = class {
420
346
  foreignField,
421
347
  as: opts.as || collectionName,
422
348
  single: opts.single === true || opts.single === "true",
349
+ ...opts.select ? { select: String(opts.select) } : {},
423
350
  ...opts.pipeline && Array.isArray(opts.pipeline) ? { pipeline: this._sanitizePipeline(opts.pipeline) } : {}
424
351
  };
425
352
  }
@@ -525,7 +452,7 @@ var QueryParser = class {
525
452
  if (opts.match && typeof opts.match === "object") option.match = this._convertPopulateMatch(opts.match);
526
453
  if (opts.limit !== void 0) {
527
454
  const limit = parseInt(String(opts.limit), 10);
528
- if (!isNaN(limit) && limit > 0) {
455
+ if (!Number.isNaN(limit) && limit > 0) {
529
456
  option.options = option.options || {};
530
457
  option.options.limit = limit;
531
458
  }
@@ -539,7 +466,7 @@ var QueryParser = class {
539
466
  }
540
467
  if (opts.skip !== void 0) {
541
468
  const skip = parseInt(String(opts.skip), 10);
542
- if (!isNaN(skip) && skip >= 0) {
469
+ if (!Number.isNaN(skip) && skip >= 0) {
543
470
  option.options = option.options || {};
544
471
  option.options.skip = skip;
545
472
  }
@@ -603,7 +530,7 @@ var QueryParser = class {
603
530
  }
604
531
  if (operatorMatch) {
605
532
  const [, , operator] = operatorMatch;
606
- if (this.dangerousOperators.includes("$" + operator)) {
533
+ if (this.dangerousOperators.includes(`$${operator}`)) {
607
534
  warn(`[mongokit] Blocked dangerous operator: ${operator}`);
608
535
  continue;
609
536
  }
@@ -662,7 +589,7 @@ var QueryParser = class {
662
589
  "size"
663
590
  ].includes(op)) {
664
591
  processedValue = parseFloat(String(value));
665
- if (isNaN(processedValue)) return;
592
+ if (Number.isNaN(processedValue)) return;
666
593
  } else if (op === "in" || op === "nin") processedValue = Array.isArray(value) ? value : String(value).split(",").map((v) => v.trim());
667
594
  else processedValue = this._convertValue(value);
668
595
  if (typeof filters[field] !== "object" || filters[field] === null || Array.isArray(filters[field])) filters[field] = {};
@@ -699,7 +626,7 @@ var QueryParser = class {
699
626
  "size"
700
627
  ].includes(operator)) {
701
628
  processedValue = parseFloat(String(value));
702
- if (isNaN(processedValue)) continue;
629
+ if (Number.isNaN(processedValue)) continue;
703
630
  } else if (operator === "in" || operator === "nin") processedValue = Array.isArray(value) ? value : String(value).split(",").map((v) => v.trim());
704
631
  else if (operator === "like" || operator === "contains" || operator === "regex") {
705
632
  const safeRegex = this._createSafeRegex(value);
@@ -741,7 +668,7 @@ var QueryParser = class {
741
668
  }
742
669
  _toMongoOperator(operator) {
743
670
  const op = operator.toLowerCase();
744
- return op.startsWith("$") ? op : "$" + op;
671
+ return op.startsWith("$") ? op : `$${op}`;
745
672
  }
746
673
  _createSafeRegex(pattern, flags = "i") {
747
674
  if (pattern === null || pattern === void 0) return null;
@@ -890,22 +817,21 @@ var QueryParser = class {
890
817
  const fromDate = from ? new Date(from) : void 0;
891
818
  const toDate = to ? new Date(to) : void 0;
892
819
  const range = {};
893
- if (fromDate && !isNaN(fromDate.getTime())) range.$gte = fromDate;
894
- if (toDate && !isNaN(toDate.getTime())) range.$lte = toDate;
820
+ if (fromDate && !Number.isNaN(fromDate.getTime())) range.$gte = fromDate;
821
+ if (toDate && !Number.isNaN(toDate.getTime())) range.$lte = toDate;
895
822
  output[key] = range;
896
823
  }
897
824
  return output;
898
825
  }
899
826
  _pluralize(str) {
900
- if (str.endsWith("y")) return str.slice(0, -1) + "ies";
827
+ if (str.endsWith("y")) return `${str.slice(0, -1)}ies`;
901
828
  if (str.endsWith("s")) return str;
902
- return str + "s";
829
+ return `${str}s`;
903
830
  }
904
831
  _capitalize(str) {
905
832
  return str.charAt(0).toUpperCase() + str.slice(1);
906
833
  }
907
834
  };
908
-
909
835
  //#endregion
910
836
  //#region src/index.ts
911
837
  /**
@@ -922,6 +848,5 @@ function createRepository(Model, plugins = [], paginationConfig = {}, options =
922
848
  return new Repository(Model, plugins, paginationConfig, options);
923
849
  }
924
850
  var src_default = Repository;
925
-
926
851
  //#endregion
927
- export { AggregationBuilder, AuditTrailQuery, HOOK_PRIORITY, LookupBuilder, PaginationEngine, QueryParser, Repository, actions_exports as actions, aggregateHelpersPlugin, auditLogPlugin, auditTrailPlugin, autoInject, batchOperationsPlugin, blockIf, buildCrudSchemasFromModel, buildCrudSchemasFromMongooseSchema, cachePlugin, cascadePlugin, configureLogger, createError, createFieldPreset, createMemoryCache, createRepository, customIdPlugin, dateSequentialId, src_default as default, elasticSearchPlugin, fieldFilterPlugin, filterResponseData, getFieldsForUser, getImmutableFields, getMongooseProjection, getNextSequence, getSystemManagedFields, immutableField, isFieldUpdateAllowed, methodRegistryPlugin, mongoOperationsPlugin, multiTenantPlugin, observabilityPlugin, prefixedId, requireField, sequentialId, softDeletePlugin, subdocumentPlugin, timestampPlugin, uniqueField, validateUpdateBody, validationChainPlugin };
852
+ export { AggregationBuilder, AuditTrailQuery, HOOK_PRIORITY, LookupBuilder, PaginationEngine, QueryParser, Repository, actions_exports as actions, aggregateHelpersPlugin, auditLogPlugin, auditTrailPlugin, autoInject, batchOperationsPlugin, blockIf, buildCrudSchemasFromModel, buildCrudSchemasFromMongooseSchema, cachePlugin, cascadePlugin, configureLogger, createError, createFieldPreset, createMemoryCache, createRepository, customIdPlugin, dateSequentialId, src_default as default, elasticSearchPlugin, fieldFilterPlugin, filterResponseData, getFieldsForUser, getImmutableFields, getMongooseProjection, getNextSequence, getSystemManagedFields, immutableField, isFieldUpdateAllowed, methodRegistryPlugin, mongoOperationsPlugin, multiTenantPlugin, observabilityPlugin, parseDuplicateKeyError, prefixedId, requireField, sequentialId, softDeletePlugin, subdocumentPlugin, timestampPlugin, uniqueField, validateUpdateBody, validationChainPlugin };
@@ -1,6 +1,28 @@
1
- import { $ as SchemaBuilderOptions, T as HttpError, dt as UserContext, m as CrudSchemas, o as CacheAdapter, pt as ValidationResult, x as FieldPreset } from "./types-pVY0w1Pp.mjs";
1
+ import { $ as SchemaBuilderOptions, T as HttpError, dt as UserContext, m as CrudSchemas, o as CacheAdapter, pt as ValidationResult, x as FieldPreset } from "./types-BlCwDszq.mjs";
2
2
  import mongoose, { Schema } from "mongoose";
3
3
 
4
+ //#region src/utils/error.d.ts
5
+ /**
6
+ * Creates an error with HTTP status code
7
+ *
8
+ * @param status - HTTP status code
9
+ * @param message - Error message
10
+ * @returns Error with status property
11
+ *
12
+ * @example
13
+ * throw createError(404, 'Document not found');
14
+ * throw createError(400, 'Invalid input');
15
+ * throw createError(403, 'Access denied');
16
+ */
17
+ declare function createError(status: number, message: string): HttpError;
18
+ /**
19
+ * Detect and convert a MongoDB E11000 duplicate-key error into
20
+ * a 409 HttpError with an actionable message.
21
+ *
22
+ * Returns `null` when the error is not a duplicate-key error.
23
+ */
24
+ declare function parseDuplicateKeyError(error: unknown): HttpError | null;
25
+ //#endregion
4
26
  //#region src/utils/field-selection.d.ts
5
27
  /**
6
28
  * Get allowed fields for a user based on their context
@@ -69,21 +91,6 @@ declare function filterResponseData<T extends Record<string, unknown>>(data: T |
69
91
  */
70
92
  declare function createFieldPreset(config: Partial<FieldPreset>): FieldPreset;
71
93
  //#endregion
72
- //#region src/utils/error.d.ts
73
- /**
74
- * Creates an error with HTTP status code
75
- *
76
- * @param status - HTTP status code
77
- * @param message - Error message
78
- * @returns Error with status property
79
- *
80
- * @example
81
- * throw createError(404, 'Document not found');
82
- * throw createError(400, 'Invalid input');
83
- * throw createError(403, 'Access denied');
84
- */
85
- declare function createError(status: number, message: string): HttpError;
86
- //#endregion
87
94
  //#region src/utils/logger.d.ts
88
95
  /**
89
96
  * Internal Logger
@@ -155,4 +162,4 @@ declare function isFieldUpdateAllowed(fieldName: string, options?: SchemaBuilder
155
162
  */
156
163
  declare function validateUpdateBody(body?: Record<string, unknown>, options?: SchemaBuilderOptions): ValidationResult;
157
164
  //#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 };
165
+ export { isFieldUpdateAllowed as a, configureLogger as c, getFieldsForUser as d, getMongooseProjection as f, getSystemManagedFields as i, createFieldPreset as l, parseDuplicateKeyError as m, buildCrudSchemasFromMongooseSchema as n, validateUpdateBody as o, createError as p, getImmutableFields as r, createMemoryCache as s, buildCrudSchemasFromModel as t, filterResponseData as u };
@@ -1,14 +1,13 @@
1
- import mongoose, { Schema } from "mongoose";
2
-
1
+ import "mongoose";
3
2
  //#region src/utils/memory-cache.ts
4
3
  /**
5
4
  * Creates an in-memory cache adapter
6
- *
5
+ *
7
6
  * Features:
8
7
  * - Automatic TTL expiration
9
8
  * - Pattern-based clearing (simple glob with *)
10
9
  * - Max entries limit to prevent memory leaks
11
- *
10
+ *
12
11
  * @param maxEntries - Maximum cache entries before oldest are evicted (default: 1000)
13
12
  */
14
13
  function createMemoryCache(maxEntries = 1e3) {
@@ -60,12 +59,11 @@ function createMemoryCache(maxEntries = 1e3) {
60
59
  return;
61
60
  }
62
61
  const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
63
- const regex = new RegExp("^" + escaped + "$");
62
+ const regex = new RegExp(`^${escaped}$`);
64
63
  for (const key of cache.keys()) if (regex.test(key)) cache.delete(key);
65
64
  }
66
65
  };
67
66
  }
68
-
69
67
  //#endregion
70
68
  //#region src/utils/mongooseToJsonSchema.ts
71
69
  /**
@@ -91,7 +89,7 @@ function buildCrudSchemasFromMongooseSchema(mongooseSchema, options = {}) {
91
89
  * Build CRUD schemas from Mongoose model
92
90
  */
93
91
  function buildCrudSchemasFromModel(mongooseModel, options = {}) {
94
- if (!mongooseModel || !mongooseModel.schema) throw new Error("Invalid mongoose model");
92
+ if (!mongooseModel?.schema) throw new Error("Invalid mongoose model");
95
93
  return buildCrudSchemasFromMongooseSchema(mongooseModel.schema, options);
96
94
  }
97
95
  /**
@@ -161,7 +159,7 @@ function buildJsonSchemaFromPaths(mongooseSchema, options) {
161
159
  if (path === "_id" || path === "__v") continue;
162
160
  const rootField = path.split(".")[0];
163
161
  if (!rootFields.has(rootField)) rootFields.set(rootField, []);
164
- rootFields.get(rootField).push({
162
+ rootFields.get(rootField)?.push({
165
163
  path,
166
164
  schemaType
167
165
  });
@@ -185,7 +183,9 @@ function buildJsonSchemaFromPaths(mongooseSchema, options) {
185
183
  "updatedAt",
186
184
  "__v"
187
185
  ]);
188
- (options?.create?.omitFields || []).forEach((f) => fieldsToOmit.add(f));
186
+ (options?.create?.omitFields || []).forEach((f) => {
187
+ fieldsToOmit.add(f);
188
+ });
189
189
  const fieldRules = options?.fieldRules || {};
190
190
  Object.entries(fieldRules).forEach(([field, rules]) => {
191
191
  if (rules.systemManaged) fieldsToOmit.add(field);
@@ -281,7 +281,9 @@ function buildJsonSchemaForUpdate(createJson, options) {
281
281
  const clone = JSON.parse(JSON.stringify(createJson));
282
282
  delete clone.required;
283
283
  const fieldsToOmit = /* @__PURE__ */ new Set();
284
- (options?.update?.omitFields || []).forEach((f) => fieldsToOmit.add(f));
284
+ (options?.update?.omitFields || []).forEach((f) => {
285
+ fieldsToOmit.add(f);
286
+ });
285
287
  const fieldRules = options?.fieldRules || {};
286
288
  Object.entries(fieldRules).forEach(([field, rules]) => {
287
289
  if (rules.immutable || rules.immutableAfterCreate) fieldsToOmit.add(field);
@@ -312,6 +314,5 @@ function buildJsonSchemaForQuery(_tree, options) {
312
314
  for (const [k, v] of Object.entries(filterable)) if (basePagination.properties) basePagination.properties[k] = v && typeof v === "object" && "type" in v ? v : { type: "string" };
313
315
  return basePagination;
314
316
  }
315
-
316
317
  //#endregion
317
- export { isFieldUpdateAllowed as a, getSystemManagedFields as i, buildCrudSchemasFromMongooseSchema as n, validateUpdateBody as o, getImmutableFields as r, createMemoryCache as s, buildCrudSchemasFromModel as t };
318
+ export { isFieldUpdateAllowed as a, getSystemManagedFields as i, buildCrudSchemasFromMongooseSchema as n, validateUpdateBody as o, getImmutableFields as r, createMemoryCache as s, buildCrudSchemasFromModel as t };
@@ -1,4 +1,4 @@
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-pVY0w1Pp.mjs";
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-BlCwDszq.mjs";
2
2
  import { Model } from "mongoose";
3
3
 
4
4
  //#region src/pagination/PaginationEngine.d.ts
@@ -1,209 +1,2 @@
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-s1-d8rWb.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
- const fetchLimit = countStrategy === "none" ? sanitizedLimit + 1 : sanitizedLimit;
52
- let query = this.Model.find(filters);
53
- if (select) query = query.select(select);
54
- if (populate && (Array.isArray(populate) ? populate.length : populate)) query = query.populate(populate);
55
- query = query.sort(sort).skip(skip).limit(fetchLimit).lean(lean);
56
- if (session) query = query.session(session);
57
- if (hint) query = query.hint(hint);
58
- if (maxTimeMS) query = query.maxTimeMS(maxTimeMS);
59
- if (readPreference) query = query.read(readPreference);
60
- const hasFilters = Object.keys(filters).length > 0;
61
- const useEstimated = this.config.useEstimatedCount && !hasFilters;
62
- let total = 0;
63
- if (countStrategy === "estimated" || useEstimated && countStrategy !== "exact") total = await this.Model.estimatedDocumentCount();
64
- else if (countStrategy === "exact") {
65
- const countQuery = this.Model.countDocuments(filters).session(session ?? null);
66
- if (hint) countQuery.hint(hint);
67
- if (maxTimeMS) countQuery.maxTimeMS(maxTimeMS);
68
- if (readPreference) countQuery.read(readPreference);
69
- total = await countQuery;
70
- }
71
- const [docs] = await Promise.all([query.exec()]);
72
- const totalPages = countStrategy === "none" ? 0 : calculateTotalPages(total, sanitizedLimit);
73
- let hasNext;
74
- if (countStrategy === "none") {
75
- hasNext = docs.length > sanitizedLimit;
76
- if (hasNext) docs.pop();
77
- } else hasNext = sanitizedPage < totalPages;
78
- const warning = shouldWarnDeepPagination(sanitizedPage, this.config.deepPageThreshold) ? `Deep pagination (page ${sanitizedPage}). Consider getAll({ after, sort, limit }) for better performance.` : void 0;
79
- return {
80
- method: "offset",
81
- docs,
82
- page: sanitizedPage,
83
- limit: sanitizedLimit,
84
- total,
85
- pages: totalPages,
86
- hasNext,
87
- hasPrev: sanitizedPage > 1,
88
- ...warning && { warning }
89
- };
90
- }
91
- /**
92
- * Keyset (cursor-based) pagination for high-performance streaming
93
- * Best for large datasets, infinite scroll, real-time feeds
94
- * O(1) performance - consistent speed regardless of position
95
- *
96
- * @param options - Pagination options (sort is required)
97
- * @returns Pagination result with next cursor
98
- *
99
- * @example
100
- * // First page
101
- * const page1 = await engine.stream({
102
- * sort: { createdAt: -1 },
103
- * limit: 20
104
- * });
105
- *
106
- * // Next page using cursor
107
- * const page2 = await engine.stream({
108
- * sort: { createdAt: -1 },
109
- * after: page1.next,
110
- * limit: 20
111
- * });
112
- */
113
- async stream(options) {
114
- const { filters = {}, sort, after, limit = this.config.defaultLimit, select, populate = [], lean = true, session, hint, maxTimeMS, readPreference } = options;
115
- if (!sort) throw createError(400, "sort is required for keyset pagination");
116
- const sanitizedLimit = validateLimit(limit, this.config);
117
- const normalizedSort = validateKeysetSort(sort);
118
- const filterKeys = Object.keys(filters).filter((k) => !k.startsWith("$"));
119
- const sortFields = Object.keys(normalizedSort);
120
- if (filterKeys.length > 0 && sortFields.length > 0) {
121
- const indexFields = [...filterKeys.map((f) => `${f}: 1`), ...sortFields.map((f) => `${f}: ${normalizedSort[f]}`)];
122
- 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(", ")} }`);
123
- }
124
- let query = { ...filters };
125
- if (after) {
126
- const cursor = decodeCursor(after);
127
- validateCursorVersion(cursor.version, this.config.cursorVersion);
128
- validateCursorSort(cursor.sort, normalizedSort);
129
- query = buildKeysetFilter(query, normalizedSort, cursor.value, cursor.id);
130
- }
131
- let mongoQuery = this.Model.find(query);
132
- if (select) mongoQuery = mongoQuery.select(select);
133
- if (populate && (Array.isArray(populate) ? populate.length : populate)) mongoQuery = mongoQuery.populate(populate);
134
- mongoQuery = mongoQuery.sort(normalizedSort).limit(sanitizedLimit + 1).lean(lean);
135
- if (session) mongoQuery = mongoQuery.session(session);
136
- if (hint) mongoQuery = mongoQuery.hint(hint);
137
- if (maxTimeMS) mongoQuery = mongoQuery.maxTimeMS(maxTimeMS);
138
- if (readPreference) mongoQuery = mongoQuery.read(readPreference);
139
- const docs = await mongoQuery.exec();
140
- const hasMore = docs.length > sanitizedLimit;
141
- if (hasMore) docs.pop();
142
- const primaryField = getPrimaryField(normalizedSort);
143
- return {
144
- method: "keyset",
145
- docs,
146
- limit: sanitizedLimit,
147
- hasMore,
148
- next: hasMore && docs.length > 0 ? encodeCursor(docs[docs.length - 1], primaryField, normalizedSort, this.config.cursorVersion) : null
149
- };
150
- }
151
- /**
152
- * Aggregate pipeline with pagination
153
- * Best for complex queries requiring aggregation stages
154
- * Uses $facet to combine results and count in single query
155
- *
156
- * @param options - Aggregation options
157
- * @returns Pagination result with total count
158
- *
159
- * @example
160
- * const result = await engine.aggregatePaginate({
161
- * pipeline: [
162
- * { $match: { status: 'active' } },
163
- * { $group: { _id: '$category', count: { $sum: 1 } } },
164
- * { $sort: { count: -1 } }
165
- * ],
166
- * page: 1,
167
- * limit: 20
168
- * });
169
- */
170
- async aggregatePaginate(options = {}) {
171
- const { pipeline = [], page = 1, limit = this.config.defaultLimit, session, hint, maxTimeMS, countStrategy = "exact", readPreference } = options;
172
- const sanitizedPage = validatePage(page, this.config);
173
- const sanitizedLimit = validateLimit(limit, this.config);
174
- const skip = calculateSkip(sanitizedPage, sanitizedLimit);
175
- const fetchLimit = countStrategy === "none" ? sanitizedLimit + 1 : sanitizedLimit;
176
- const facetStages = { docs: [{ $skip: skip }, { $limit: fetchLimit }] };
177
- if (countStrategy !== "none") facetStages.total = [{ $count: "count" }];
178
- const facetPipeline = [...pipeline, { $facet: facetStages }];
179
- const aggregation = this.Model.aggregate(facetPipeline);
180
- if (session) aggregation.session(session);
181
- if (hint) aggregation.hint(hint);
182
- if (maxTimeMS) aggregation.option({ maxTimeMS });
183
- if (readPreference) aggregation.read(readPreference);
184
- const [result] = await aggregation.exec();
185
- const docs = result.docs;
186
- const total = result.total?.[0]?.count || 0;
187
- const totalPages = countStrategy === "none" ? 0 : calculateTotalPages(total, sanitizedLimit);
188
- let hasNext;
189
- if (countStrategy === "none") {
190
- hasNext = docs.length > sanitizedLimit;
191
- if (hasNext) docs.pop();
192
- } else hasNext = sanitizedPage < totalPages;
193
- const warning = shouldWarnDeepPagination(sanitizedPage, this.config.deepPageThreshold) ? `Deep pagination in aggregate (page ${sanitizedPage}). Uses $skip internally.` : void 0;
194
- return {
195
- method: "aggregate",
196
- docs,
197
- page: sanitizedPage,
198
- limit: sanitizedLimit,
199
- total,
200
- pages: totalPages,
201
- hasNext,
202
- hasPrev: sanitizedPage > 1,
203
- ...warning && { warning }
204
- };
205
- }
206
- };
207
-
208
- //#endregion
209
- export { PaginationEngine };
1
+ import { t as PaginationEngine } from "../PaginationEngine-PLyDhrO7.mjs";
2
+ export { PaginationEngine };
@@ -1,3 +1,2 @@
1
- import "../types-pVY0w1Pp.mjs";
2
- import { A as subdocumentPlugin, B as immutableField, C as observabilityPlugin, D as CacheMethods, E as cascadePlugin, F as batchOperationsPlugin, G as methodRegistryPlugin, H as uniqueField, I as MongoOperationsMethods, J as auditLogPlugin, K as SoftDeleteMethods, L as mongoOperationsPlugin, M as aggregateHelpersPlugin, N as BatchOperationsMethods, O as cachePlugin, R as autoInject, S as OperationMetric, T as multiTenantPlugin, U as validationChainPlugin, V as requireField, W as MethodRegistryRepository, X as fieldFilterPlugin, Y as timestampPlugin, _ as AuditTrailMethods, a as SequentialIdOptions, b as auditTrailPlugin, c as getNextSequence, d as ElasticSearchOptions, f as elasticSearchPlugin, g as AuditQueryResult, h as AuditQueryOptions, i as PrefixedIdOptions, j as AggregateHelpersMethods, k as SubdocumentMethods, l as prefixedId, m as AuditOperation, n as DateSequentialIdOptions, o as customIdPlugin, p as AuditEntry, q as softDeletePlugin, r as IdGenerator, s as dateSequentialId, t as CustomIdOptions, u as sequentialId, v as AuditTrailOptions, w as MultiTenantOptions, x as ObservabilityOptions, y as AuditTrailQuery, z as blockIf } from "../custom-id.plugin-BJ3FSnzt.mjs";
1
+ import { A as dateSequentialId, B as AuditEntry, C as elasticSearchPlugin, D as PrefixedIdOptions, E as IdGenerator, F as CacheMethods, G as AuditTrailOptions, H as AuditQueryOptions, I as cachePlugin, J as auditLogPlugin, K as AuditTrailQuery, L as BatchOperationsMethods, M as prefixedId, N as sequentialId, O as SequentialIdOptions, P as cascadePlugin, S as ElasticSearchOptions, T as DateSequentialIdOptions, U as AuditQueryResult, V as AuditOperation, W as AuditTrailMethods, X as aggregateHelpersPlugin, Y as AggregateHelpersMethods, _ as MongoOperationsMethods, a as uniqueField, b as methodRegistryPlugin, c as SubdocumentMethods, d as softDeletePlugin, f as ObservabilityOptions, g as multiTenantPlugin, h as MultiTenantOptions, i as requireField, j as getNextSequence, k as customIdPlugin, l as subdocumentPlugin, m as observabilityPlugin, n as blockIf, o as validationChainPlugin, p as OperationMetric, q as auditTrailPlugin, r as immutableField, s as timestampPlugin, t as autoInject, u as SoftDeleteMethods, v as mongoOperationsPlugin, w as CustomIdOptions, x as fieldFilterPlugin, y as MethodRegistryRepository, z as batchOperationsPlugin } from "../validation-chain.plugin-DxqiHv-E.mjs";
3
2
  export { type AggregateHelpersMethods, type AuditEntry, type AuditOperation, type AuditQueryOptions, type AuditQueryResult, type AuditTrailMethods, type AuditTrailOptions, AuditTrailQuery, 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, auditTrailPlugin, autoInject, batchOperationsPlugin, blockIf, cachePlugin, cascadePlugin, customIdPlugin, dateSequentialId, elasticSearchPlugin, fieldFilterPlugin, getNextSequence, immutableField, methodRegistryPlugin, mongoOperationsPlugin, multiTenantPlugin, observabilityPlugin, prefixedId, requireField, sequentialId, softDeletePlugin, subdocumentPlugin, timestampPlugin, uniqueField, validationChainPlugin };
@@ -1,3 +1,2 @@
1
- import { C as methodRegistryPlugin, D as fieldFilterPlugin, E as timestampPlugin, S as validationChainPlugin, T as auditLogPlugin, _ as autoInject, a as sequentialId, b as requireField, c as auditTrailPlugin, d as cascadePlugin, f as cachePlugin, g as mongoOperationsPlugin, h as batchOperationsPlugin, i as prefixedId, l as observabilityPlugin, m as aggregateHelpersPlugin, n as dateSequentialId, o as elasticSearchPlugin, p as subdocumentPlugin, r as getNextSequence, s as AuditTrailQuery, t as customIdPlugin, u as multiTenantPlugin, v as blockIf, w as softDeletePlugin, x as uniqueField, y as immutableField } from "../custom-id.plugin-FInXDsUX.mjs";
2
-
3
- export { AuditTrailQuery, aggregateHelpersPlugin, auditLogPlugin, auditTrailPlugin, autoInject, batchOperationsPlugin, blockIf, cachePlugin, cascadePlugin, customIdPlugin, dateSequentialId, elasticSearchPlugin, fieldFilterPlugin, getNextSequence, immutableField, methodRegistryPlugin, mongoOperationsPlugin, multiTenantPlugin, observabilityPlugin, prefixedId, requireField, sequentialId, softDeletePlugin, subdocumentPlugin, timestampPlugin, uniqueField, validationChainPlugin };
1
+ import { A as aggregateHelpersPlugin, D as AuditTrailQuery, E as batchOperationsPlugin, O as auditTrailPlugin, S as cachePlugin, _ as dateSequentialId, a as uniqueField, b as sequentialId, c as subdocumentPlugin, d as multiTenantPlugin, f as mongoOperationsPlugin, g as customIdPlugin, h as elasticSearchPlugin, i as requireField, k as auditLogPlugin, l as softDeletePlugin, m as fieldFilterPlugin, n as blockIf, o as validationChainPlugin, p as methodRegistryPlugin, r as immutableField, s as timestampPlugin, t as autoInject, u as observabilityPlugin, v as getNextSequence, x as cascadePlugin, y as prefixedId } from "../validation-chain.plugin-Ow6EUIoo.mjs";
2
+ export { AuditTrailQuery, aggregateHelpersPlugin, auditLogPlugin, auditTrailPlugin, autoInject, batchOperationsPlugin, blockIf, cachePlugin, cascadePlugin, customIdPlugin, dateSequentialId, elasticSearchPlugin, fieldFilterPlugin, getNextSequence, immutableField, methodRegistryPlugin, mongoOperationsPlugin, multiTenantPlugin, observabilityPlugin, prefixedId, requireField, sequentialId, softDeletePlugin, subdocumentPlugin, timestampPlugin, uniqueField, validationChainPlugin };