@classytic/mongokit 3.4.0 → 3.4.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 CHANGED
@@ -17,7 +17,7 @@
17
17
  - **Search governance** - Text index guard (throws `400` if no index), allowlisted sort/filter fields, ReDoS protection
18
18
  - **Vector search** - MongoDB Atlas `$vectorSearch` with auto-embedding and multimodal support
19
19
  - **TypeScript first** - Full type safety with discriminated unions
20
- - **940+ passing tests** - Battle-tested and production-ready
20
+ - **1090+ passing tests** - Battle-tested and production-ready
21
21
 
22
22
  ## Installation
23
23
 
@@ -25,7 +25,7 @@
25
25
  npm install @classytic/mongokit mongoose
26
26
  ```
27
27
 
28
- > Supports Mongoose `^9.0.0`
28
+ > Requires Mongoose `^9.0.0` | Node.js `>=22`
29
29
 
30
30
  ## Quick Start
31
31
 
@@ -1059,7 +1059,7 @@ GET /posts?populate[author][populate][department][select]=name # Nested
1059
1059
 
1060
1060
  **Security features:**
1061
1061
 
1062
- - Blocks `$where`, `$function`, `$accumulator`, `$expr` operators
1062
+ - Blocks `$where`, `$function`, `$accumulator` operators (`$expr` allowed for `$lookup` correlation)
1063
1063
  - ReDoS protection for regex patterns
1064
1064
  - Max filter depth enforcement
1065
1065
  - Collection allowlists for lookups
@@ -1377,7 +1377,7 @@ Extending Repository works exactly the same with Mongoose 8 and 9. The package:
1377
1377
  - Uses its own event system (not Mongoose middleware)
1378
1378
  - Defines its own `FilterQuery` type (unaffected by Mongoose 9 rename)
1379
1379
  - Properly gates update pipelines (safe for Mongoose 9's stricter defaults)
1380
- - All 940+ tests pass on Mongoose 9
1380
+ - All 1090+ tests pass on Mongoose 9
1381
1381
 
1382
1382
  ## License
1383
1383
 
@@ -1,183 +1,6 @@
1
1
  import { a as warn, t as createError } from "./error-Bpbi_NKo.mjs";
2
- import mongoose from "mongoose";
3
- //#region src/pagination/utils/cursor.ts
4
- /**
5
- * Cursor Utilities
6
- *
7
- * Encoding and decoding of cursor tokens for keyset pagination.
8
- * Cursors are base64-encoded JSON containing position data and metadata.
9
- */
10
- /**
11
- * Encodes document values and sort metadata into a base64 cursor token
12
- *
13
- * @param doc - Document to extract cursor values from
14
- * @param primaryField - Primary sort field name
15
- * @param sort - Normalized sort specification
16
- * @param version - Cursor version for forward compatibility
17
- * @returns Base64-encoded cursor token
18
- */
19
- function encodeCursor(doc, primaryField, sort, version = 1) {
20
- const primaryValue = doc[primaryField];
21
- const idValue = doc._id;
22
- const payload = {
23
- v: serializeValue(primaryValue),
24
- t: getValueType(primaryValue),
25
- id: serializeValue(idValue),
26
- idType: getValueType(idValue),
27
- sort,
28
- ver: version
29
- };
30
- return Buffer.from(JSON.stringify(payload)).toString("base64");
31
- }
32
- /**
33
- * Decodes a cursor token back into document values and sort metadata
34
- *
35
- * @param token - Base64-encoded cursor token
36
- * @returns Decoded cursor data
37
- * @throws Error if token is invalid or malformed
38
- */
39
- function decodeCursor(token) {
40
- let json;
41
- try {
42
- json = Buffer.from(token, "base64").toString("utf-8");
43
- } catch {
44
- throw new Error("Invalid cursor token: not valid base64");
45
- }
46
- let payload;
47
- try {
48
- payload = JSON.parse(json);
49
- } catch {
50
- throw new Error("Invalid cursor token: not valid JSON");
51
- }
52
- if (!payload || typeof payload !== "object" || !("v" in payload) || !("t" in payload) || !("id" in payload) || !("idType" in payload) || !payload.sort || typeof payload.sort !== "object" || typeof payload.ver !== "number") throw new Error("Invalid cursor token: malformed payload structure");
53
- const VALID_TYPES = [
54
- "date",
55
- "objectid",
56
- "boolean",
57
- "number",
58
- "string",
59
- "null",
60
- "unknown"
61
- ];
62
- if (!VALID_TYPES.includes(payload.t) || !VALID_TYPES.includes(payload.idType)) throw new Error("Invalid cursor token: unrecognized value type");
63
- try {
64
- return {
65
- value: rehydrateValue(payload.v, payload.t),
66
- id: rehydrateValue(payload.id, payload.idType),
67
- sort: payload.sort,
68
- version: payload.ver
69
- };
70
- } catch {
71
- throw new Error("Invalid cursor token: failed to rehydrate values");
72
- }
73
- }
74
- /**
75
- * Validates that cursor sort matches current query sort
76
- *
77
- * @param cursorSort - Sort specification from cursor
78
- * @param currentSort - Sort specification from query
79
- * @throws Error if sorts don't match
80
- */
81
- function validateCursorSort(cursorSort, currentSort) {
82
- if (JSON.stringify(cursorSort) !== JSON.stringify(currentSort)) throw new Error("Cursor sort does not match current query sort");
83
- }
84
- /**
85
- * Validates cursor version matches expected version
86
- *
87
- * @param cursorVersion - Version from cursor
88
- * @param expectedVersion - Expected version from config
89
- * @throws Error if versions don't match
90
- */
91
- function validateCursorVersion(cursorVersion, expectedVersion) {
92
- if (cursorVersion > expectedVersion) throw new Error(`Cursor version ${cursorVersion} is newer than expected version ${expectedVersion}. Please upgrade.`);
93
- }
94
- /**
95
- * Serializes a value for cursor storage
96
- */
97
- function serializeValue(value) {
98
- if (value === null || value === void 0) return null;
99
- if (value instanceof Date) return value.toISOString();
100
- if (value instanceof mongoose.Types.ObjectId) return value.toString();
101
- return value;
102
- }
103
- /**
104
- * Gets the type identifier for a value
105
- */
106
- function getValueType(value) {
107
- if (value === null || value === void 0) return "null";
108
- if (value instanceof Date) return "date";
109
- if (value instanceof mongoose.Types.ObjectId) return "objectid";
110
- if (typeof value === "boolean") return "boolean";
111
- if (typeof value === "number") return "number";
112
- if (typeof value === "string") return "string";
113
- return "unknown";
114
- }
115
- /**
116
- * Rehydrates a serialized value back to its original type
117
- */
118
- function rehydrateValue(serialized, type) {
119
- if (type === "null" || serialized === null) return null;
120
- switch (type) {
121
- case "date": return new Date(serialized);
122
- case "objectid": return new mongoose.Types.ObjectId(serialized);
123
- case "boolean": return serialized === true || serialized === "true";
124
- case "number": return Number(serialized);
125
- default: return serialized;
126
- }
127
- }
128
- //#endregion
129
- //#region src/pagination/utils/filter.ts
130
- /**
131
- * Builds MongoDB filter for keyset pagination
132
- * Creates compound $or condition for proper cursor-based filtering
133
- *
134
- * @param baseFilters - Existing query filters
135
- * @param sort - Normalized sort specification
136
- * @param cursorValue - Primary field value from cursor
137
- * @param cursorId - _id value from cursor
138
- * @returns MongoDB filter with keyset condition
139
- *
140
- * @example
141
- * buildKeysetFilter(
142
- * { status: 'active' },
143
- * { createdAt: -1, _id: -1 },
144
- * new Date('2024-01-01'),
145
- * new ObjectId('...')
146
- * )
147
- * // Returns:
148
- * // {
149
- * // status: 'active',
150
- * // $or: [
151
- * // { createdAt: { $lt: Date('2024-01-01') } },
152
- * // { createdAt: Date('2024-01-01'), _id: { $lt: ObjectId('...') } }
153
- * // ]
154
- * // }
155
- */
156
- function buildKeysetFilter(baseFilters, sort, cursorValue, cursorId) {
157
- const primaryField = Object.keys(sort).find((k) => k !== "_id") || "_id";
158
- const direction = sort[primaryField];
159
- const operator = direction === 1 ? "$gt" : "$lt";
160
- if (cursorValue === null || cursorValue === void 0) if (direction === 1) return {
161
- ...baseFilters,
162
- $or: [{
163
- [primaryField]: null,
164
- _id: { $gt: cursorId }
165
- }, { [primaryField]: { $ne: null } }]
166
- };
167
- else return {
168
- ...baseFilters,
169
- [primaryField]: null,
170
- _id: { $lt: cursorId }
171
- };
172
- return {
173
- ...baseFilters,
174
- $or: [{ [primaryField]: { [operator]: cursorValue } }, {
175
- [primaryField]: cursorValue,
176
- _id: { [operator]: cursorId }
177
- }]
178
- };
179
- }
180
- //#endregion
2
+ import { n as encodeCursor, r as resolveCursorFilter } from "./cursor-CHToazHy.mjs";
3
+ import { r as validateKeysetSort, t as getPrimaryField } from "./sort-C-BJEWUZ.mjs";
181
4
  //#region src/pagination/utils/limits.ts
182
5
  /**
183
6
  * Validates and sanitizes limit value
@@ -239,60 +62,29 @@ function calculateTotalPages(total, limit) {
239
62
  return Math.ceil(total / limit);
240
63
  }
241
64
  //#endregion
242
- //#region src/pagination/utils/sort.ts
243
- /**
244
- * Normalizes sort object to ensure stable key order
245
- * Primary fields first, _id last (not alphabetical)
246
- *
247
- * @param sort - Sort specification
248
- * @returns Normalized sort with stable key order
249
- */
250
- function normalizeSort(sort) {
251
- const normalized = {};
252
- Object.keys(sort).forEach((key) => {
253
- if (key !== "_id") normalized[key] = sort[key];
254
- });
255
- if (sort._id !== void 0) normalized._id = sort._id;
256
- return normalized;
257
- }
258
- /**
259
- * Validates and normalizes sort for keyset pagination
260
- * Auto-adds _id tie-breaker if needed
261
- * Ensures _id direction matches primary field
262
- *
263
- * @param sort - Sort specification
264
- * @returns Validated and normalized sort
265
- * @throws Error if sort is invalid for keyset pagination
266
- */
267
- function validateKeysetSort(sort) {
268
- const keys = Object.keys(sort);
269
- if (keys.length === 1 && keys[0] !== "_id") {
270
- const field = keys[0];
271
- const direction = sort[field];
272
- return normalizeSort({
273
- [field]: direction,
274
- _id: direction
275
- });
65
+ //#region src/pagination/PaginationEngine.ts
66
+ function ensureKeysetSelectIncludesCursorFields(select, sort) {
67
+ if (!select) return select;
68
+ const requiredFields = new Set([...Object.keys(sort), "_id"]);
69
+ if (typeof select === "string") {
70
+ const fields = select.split(/[,\s]+/).map((field) => field.trim()).filter(Boolean);
71
+ if (fields.length > 0 && fields.every((field) => field.startsWith("-"))) return select;
72
+ const merged = new Set(fields);
73
+ for (const field of requiredFields) merged.add(field);
74
+ return Array.from(merged).join(" ");
276
75
  }
277
- if (keys.length === 1 && keys[0] === "_id") return normalizeSort(sort);
278
- if (keys.length === 2) {
279
- if (!keys.includes("_id")) throw new Error("Keyset pagination requires _id as tie-breaker");
280
- if (sort[keys.find((k) => k !== "_id")] !== sort._id) throw new Error("_id direction must match primary field direction");
281
- return normalizeSort(sort);
76
+ if (Array.isArray(select)) {
77
+ const fields = select.map((field) => field.trim()).filter(Boolean);
78
+ if (fields.length > 0 && fields.every((field) => field.startsWith("-"))) return select;
79
+ const merged = new Set(fields);
80
+ for (const field of requiredFields) merged.add(field);
81
+ return Array.from(merged);
282
82
  }
283
- throw new Error("Keyset pagination only supports single field + _id");
284
- }
285
- /**
286
- * Extracts primary sort field (first non-_id field)
287
- *
288
- * @param sort - Sort specification
289
- * @returns Primary field name
290
- */
291
- function getPrimaryField(sort) {
292
- return Object.keys(sort).find((k) => k !== "_id") || "_id";
83
+ const projection = { ...select };
84
+ if (!Object.values(projection).some((value) => value === 1)) return select;
85
+ for (const field of requiredFields) projection[field] = 1;
86
+ return projection;
293
87
  }
294
- //#endregion
295
- //#region src/pagination/PaginationEngine.ts
296
88
  /**
297
89
  * Production-grade pagination engine for MongoDB
298
90
  * Supports offset, keyset (cursor), and aggregate pagination
@@ -335,7 +127,7 @@ var PaginationEngine = class {
335
127
  * console.log(result.docs, result.total, result.hasNext);
336
128
  */
337
129
  async paginate(options = {}) {
338
- const { filters = {}, sort = { _id: -1 }, page = 1, limit = this.config.defaultLimit, select, populate = [], lean = true, session, hint, maxTimeMS, countStrategy = "exact", readPreference } = options;
130
+ const { filters = {}, sort = { _id: -1 }, page = 1, limit = this.config.defaultLimit, select, populate = [], lean = true, session, hint, maxTimeMS, countStrategy = "exact", readPreference, collation } = options;
339
131
  const sanitizedPage = validatePage(page, this.config);
340
132
  const sanitizedLimit = validateLimit(limit, this.config);
341
133
  const skip = calculateSkip(sanitizedPage, sanitizedLimit);
@@ -344,6 +136,7 @@ var PaginationEngine = class {
344
136
  if (select) query = query.select(select);
345
137
  if (populate && (Array.isArray(populate) ? populate.length : populate)) query = query.populate(populate);
346
138
  query = query.sort(sort).skip(skip).limit(fetchLimit).lean(lean);
139
+ if (collation) query = query.collation(collation);
347
140
  if (session) query = query.session(session);
348
141
  if (hint) query = query.hint(hint);
349
142
  if (maxTimeMS) query = query.maxTimeMS(maxTimeMS);
@@ -351,14 +144,15 @@ var PaginationEngine = class {
351
144
  const hasFilters = Object.keys(filters).length > 0;
352
145
  const useEstimated = this.config.useEstimatedCount && !hasFilters;
353
146
  let countPromise;
354
- if (countStrategy === "estimated" || useEstimated && countStrategy !== "exact") countPromise = this.Model.estimatedDocumentCount();
355
- else if (countStrategy === "exact") {
147
+ if ((countStrategy === "estimated" || useEstimated) && !hasFilters) countPromise = this.Model.estimatedDocumentCount();
148
+ else if (countStrategy === "none") countPromise = Promise.resolve(0);
149
+ else {
356
150
  const countQuery = this.Model.countDocuments(filters).session(session ?? null);
357
151
  if (hint) countQuery.hint(hint);
358
152
  if (maxTimeMS) countQuery.maxTimeMS(maxTimeMS);
359
153
  if (readPreference) countQuery.read(readPreference);
360
154
  countPromise = countQuery.exec();
361
- } else countPromise = Promise.resolve(0);
155
+ }
362
156
  const [docs, total] = await Promise.all([query.exec(), countPromise]);
363
157
  const totalPages = countStrategy === "none" ? 0 : calculateTotalPages(total, sanitizedLimit);
364
158
  let hasNext;
@@ -402,7 +196,7 @@ var PaginationEngine = class {
402
196
  * });
403
197
  */
404
198
  async stream(options) {
405
- const { filters = {}, sort, after, limit = this.config.defaultLimit, select, populate = [], lean = true, session, hint, maxTimeMS, readPreference } = options;
199
+ const { filters = {}, sort, after, limit = this.config.defaultLimit, select, populate = [], lean = true, session, hint, maxTimeMS, readPreference, collation } = options;
406
200
  if (!sort) throw createError(400, "sort is required for keyset pagination");
407
201
  const sanitizedLimit = validateLimit(limit, this.config);
408
202
  const normalizedSort = validateKeysetSort(sort);
@@ -413,16 +207,13 @@ var PaginationEngine = class {
413
207
  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(", ")} }`);
414
208
  }
415
209
  let query = { ...filters };
416
- if (after) {
417
- const cursor = decodeCursor(after);
418
- validateCursorVersion(cursor.version, this.config.cursorVersion);
419
- validateCursorSort(cursor.sort, normalizedSort);
420
- query = buildKeysetFilter(query, normalizedSort, cursor.value, cursor.id);
421
- }
210
+ if (after) query = resolveCursorFilter(after, normalizedSort, this.config.cursorVersion, query);
211
+ const effectiveSelect = ensureKeysetSelectIncludesCursorFields(select, normalizedSort);
422
212
  let mongoQuery = this.Model.find(query);
423
- if (select) mongoQuery = mongoQuery.select(select);
213
+ if (effectiveSelect) mongoQuery = mongoQuery.select(effectiveSelect);
424
214
  if (populate && (Array.isArray(populate) ? populate.length : populate)) mongoQuery = mongoQuery.populate(populate);
425
215
  mongoQuery = mongoQuery.sort(normalizedSort).limit(sanitizedLimit + 1).lean(lean);
216
+ if (collation) mongoQuery = mongoQuery.collation(collation);
426
217
  if (session) mongoQuery = mongoQuery.session(session);
427
218
  if (hint) mongoQuery = mongoQuery.hint(hint);
428
219
  if (maxTimeMS) mongoQuery = mongoQuery.maxTimeMS(maxTimeMS);
@@ -1,2 +1,2 @@
1
- import { a as create_d_exports, i as delete_d_exports, n as update_d_exports, o as aggregate_d_exports, r as read_d_exports } from "../index-Df3ernpC.mjs";
1
+ import { a as create_d_exports, i as delete_d_exports, n as update_d_exports, o as aggregate_d_exports, r as read_d_exports } from "../index-BuoZIZ15.mjs";
2
2
  export { aggregate_d_exports as aggregate, create_d_exports as create, delete_d_exports as deleteActions, read_d_exports as read, update_d_exports as update };
@@ -1,5 +1,5 @@
1
1
  import { t as __exportAll } from "../chunk-CfYAbeIz.mjs";
2
- import { c as read_exports, h as aggregate_exports, n as update_exports, p as create_exports, u as delete_exports } from "../update-DXwVh6M1.mjs";
2
+ import { c as read_exports, h as aggregate_exports, n as update_exports, p as create_exports, u as delete_exports } from "../update-DGKMmBgG.mjs";
3
3
  //#region src/actions/index.ts
4
4
  var actions_exports = /* @__PURE__ */ __exportAll({
5
5
  aggregate: () => aggregate_exports,
@@ -1,4 +1,4 @@
1
- import { H as Plugin } from "../types-BlCwDszq.mjs";
1
+ import { U as Plugin } from "../types-COINbsdL.mjs";
2
2
  import { ClientSession, PipelineStage } from "mongoose";
3
3
 
4
4
  //#region src/ai/types.d.ts
package/dist/ai/index.mjs CHANGED
@@ -175,9 +175,10 @@ function vectorPlugin(options) {
175
175
  });
176
176
  repo.on("before:update", async (context) => {
177
177
  if (!context.data) return;
178
+ const contextData = context.data;
178
179
  const fieldsToEmbed = fields.filter((field) => {
179
180
  const allFields = [...field.sourceFields ?? [], ...field.mediaFields ?? []];
180
- return allFields.length > 0 && allFields.some((f) => f in context.data);
181
+ return allFields.length > 0 && contextData && allFields.some((f) => f in contextData);
181
182
  });
182
183
  if (!fieldsToEmbed.length) return;
183
184
  const existing = await repo.Model.findById(context.id).lean().session(context.session ?? null);
@@ -0,0 +1,250 @@
1
+ import { t as __exportAll } from "./chunk-CfYAbeIz.mjs";
2
+ import mongoose from "mongoose";
3
+ //#region src/pagination/utils/filter.ts
4
+ /**
5
+ * Builds MongoDB filter for keyset pagination
6
+ * Creates compound $or condition for proper cursor-based filtering
7
+ *
8
+ * @param baseFilters - Existing query filters
9
+ * @param sort - Normalized sort specification
10
+ * @param cursorValue - Primary field value from cursor
11
+ * @param cursorId - _id value from cursor
12
+ * @returns MongoDB filter with keyset condition
13
+ *
14
+ * @example
15
+ * buildKeysetFilter(
16
+ * { status: 'active' },
17
+ * { createdAt: -1, _id: -1 },
18
+ * new Date('2024-01-01'),
19
+ * new ObjectId('...')
20
+ * )
21
+ * // Returns:
22
+ * // {
23
+ * // status: 'active',
24
+ * // $or: [
25
+ * // { createdAt: { $lt: Date('2024-01-01') } },
26
+ * // { createdAt: Date('2024-01-01'), _id: { $lt: ObjectId('...') } }
27
+ * // ]
28
+ * // }
29
+ */
30
+ function buildKeysetFilter(baseFilters, sort, cursorValue, cursorId, cursorValues) {
31
+ const sortFields = Object.keys(sort).filter((k) => k !== "_id");
32
+ if (sortFields.length <= 1 && !cursorValues) {
33
+ const primaryField = sortFields[0] || "_id";
34
+ const direction = sort[primaryField];
35
+ const operator = direction === 1 ? "$gt" : "$lt";
36
+ if (cursorValue === null || cursorValue === void 0) if (direction === 1) return {
37
+ ...baseFilters,
38
+ $or: [{
39
+ [primaryField]: null,
40
+ _id: { $gt: cursorId }
41
+ }, { [primaryField]: { $ne: null } }]
42
+ };
43
+ else return {
44
+ ...baseFilters,
45
+ [primaryField]: null,
46
+ _id: { $lt: cursorId }
47
+ };
48
+ return {
49
+ ...baseFilters,
50
+ $or: [{ [primaryField]: { [operator]: cursorValue } }, {
51
+ [primaryField]: cursorValue,
52
+ _id: { [operator]: cursorId }
53
+ }]
54
+ };
55
+ }
56
+ const values = cursorValues || { [sortFields[0]]: cursorValue };
57
+ const allFields = [...sortFields, "_id"];
58
+ const allValues = {
59
+ ...values,
60
+ _id: cursorId
61
+ };
62
+ const orConditions = [];
63
+ for (let i = 0; i < allFields.length; i++) {
64
+ const field = allFields[i];
65
+ const operator = (sort[field] ?? sort[sortFields[0]]) === 1 ? "$gt" : "$lt";
66
+ const condition = {};
67
+ for (let j = 0; j < i; j++) condition[allFields[j]] = allValues[allFields[j]];
68
+ condition[field] = { [operator]: allValues[field] };
69
+ orConditions.push(condition);
70
+ }
71
+ return {
72
+ ...baseFilters,
73
+ $or: orConditions
74
+ };
75
+ }
76
+ //#endregion
77
+ //#region src/pagination/utils/cursor.ts
78
+ /**
79
+ * Cursor Utilities
80
+ *
81
+ * Encoding and decoding of cursor tokens for keyset pagination.
82
+ * Cursors are base64-encoded JSON containing position data and metadata.
83
+ */
84
+ var cursor_exports = /* @__PURE__ */ __exportAll({
85
+ decodeCursor: () => decodeCursor,
86
+ encodeCursor: () => encodeCursor,
87
+ resolveCursorFilter: () => resolveCursorFilter,
88
+ validateCursorSort: () => validateCursorSort,
89
+ validateCursorVersion: () => validateCursorVersion
90
+ });
91
+ /**
92
+ * Encodes document values and sort metadata into a base64 cursor token
93
+ *
94
+ * @param doc - Document to extract cursor values from
95
+ * @param primaryField - Primary sort field name
96
+ * @param sort - Normalized sort specification
97
+ * @param version - Cursor version for forward compatibility
98
+ * @returns Base64-encoded cursor token
99
+ */
100
+ function encodeCursor(doc, primaryField, sort, version = 1) {
101
+ const primaryValue = doc[primaryField];
102
+ const idValue = doc._id;
103
+ const sortFields = Object.keys(sort).filter((k) => k !== "_id");
104
+ const vals = {};
105
+ const types = {};
106
+ for (const field of sortFields) {
107
+ vals[field] = serializeValue(doc[field]);
108
+ types[field] = getValueType(doc[field]);
109
+ }
110
+ const payload = {
111
+ v: serializeValue(primaryValue),
112
+ t: getValueType(primaryValue),
113
+ id: serializeValue(idValue),
114
+ idType: getValueType(idValue),
115
+ sort,
116
+ ver: version,
117
+ ...sortFields.length > 1 && {
118
+ vals,
119
+ types
120
+ }
121
+ };
122
+ return Buffer.from(JSON.stringify(payload)).toString("base64");
123
+ }
124
+ /**
125
+ * Decodes a cursor token back into document values and sort metadata
126
+ *
127
+ * @param token - Base64-encoded cursor token
128
+ * @returns Decoded cursor data
129
+ * @throws Error if token is invalid or malformed
130
+ */
131
+ function decodeCursor(token) {
132
+ let json;
133
+ try {
134
+ json = Buffer.from(token, "base64").toString("utf-8");
135
+ } catch {
136
+ throw new Error("Invalid cursor token: not valid base64");
137
+ }
138
+ let payload;
139
+ try {
140
+ payload = JSON.parse(json);
141
+ } catch {
142
+ throw new Error("Invalid cursor token: not valid JSON");
143
+ }
144
+ if (!payload || typeof payload !== "object" || !("v" in payload) || !("t" in payload) || !("id" in payload) || !("idType" in payload) || !payload.sort || typeof payload.sort !== "object" || typeof payload.ver !== "number") throw new Error("Invalid cursor token: malformed payload structure");
145
+ const VALID_TYPES = [
146
+ "date",
147
+ "objectid",
148
+ "boolean",
149
+ "number",
150
+ "string",
151
+ "null",
152
+ "unknown"
153
+ ];
154
+ if (!VALID_TYPES.includes(payload.t) || !VALID_TYPES.includes(payload.idType)) throw new Error("Invalid cursor token: unrecognized value type");
155
+ try {
156
+ let values;
157
+ if (payload.vals && payload.types) {
158
+ values = {};
159
+ for (const [field, serialized] of Object.entries(payload.vals)) values[field] = rehydrateValue(serialized, payload.types[field]);
160
+ }
161
+ return {
162
+ value: rehydrateValue(payload.v, payload.t),
163
+ id: rehydrateValue(payload.id, payload.idType),
164
+ sort: payload.sort,
165
+ version: payload.ver,
166
+ ...values && { values }
167
+ };
168
+ } catch {
169
+ throw new Error("Invalid cursor token: failed to rehydrate values");
170
+ }
171
+ }
172
+ /**
173
+ * Validates that cursor sort matches current query sort
174
+ *
175
+ * @param cursorSort - Sort specification from cursor
176
+ * @param currentSort - Sort specification from query
177
+ * @throws Error if sorts don't match
178
+ */
179
+ function validateCursorSort(cursorSort, currentSort) {
180
+ if (JSON.stringify(cursorSort) !== JSON.stringify(currentSort)) throw new Error("Cursor sort does not match current query sort");
181
+ }
182
+ /**
183
+ * Validates cursor version matches expected version
184
+ *
185
+ * @param cursorVersion - Version from cursor
186
+ * @param expectedVersion - Expected version from config
187
+ * @throws Error if versions don't match
188
+ */
189
+ function validateCursorVersion(cursorVersion, expectedVersion) {
190
+ if (cursorVersion > expectedVersion) throw new Error(`Cursor version ${cursorVersion} is newer than expected version ${expectedVersion}. Please upgrade.`);
191
+ }
192
+ /**
193
+ * Serializes a value for cursor storage
194
+ */
195
+ function serializeValue(value) {
196
+ if (value === null || value === void 0) return null;
197
+ if (value instanceof Date) return value.toISOString();
198
+ if (value instanceof mongoose.Types.ObjectId) return value.toString();
199
+ return value;
200
+ }
201
+ /**
202
+ * Gets the type identifier for a value
203
+ */
204
+ function getValueType(value) {
205
+ if (value === null || value === void 0) return "null";
206
+ if (value instanceof Date) return "date";
207
+ if (value instanceof mongoose.Types.ObjectId) return "objectid";
208
+ if (typeof value === "boolean") return "boolean";
209
+ if (typeof value === "number") return "number";
210
+ if (typeof value === "string") return "string";
211
+ return "unknown";
212
+ }
213
+ /**
214
+ * Rehydrates a serialized value back to its original type
215
+ */
216
+ function rehydrateValue(serialized, type) {
217
+ if (type === "null" || serialized === null) return null;
218
+ switch (type) {
219
+ case "date": return new Date(serialized);
220
+ case "objectid": return new mongoose.Types.ObjectId(serialized);
221
+ case "boolean": return serialized === true || serialized === "true";
222
+ case "number": return Number(serialized);
223
+ default: return serialized;
224
+ }
225
+ }
226
+ /**
227
+ * Resolves cursor token into MongoDB query filters.
228
+ * Shared by PaginationEngine.stream() and Repository.lookupPopulate() keyset path.
229
+ *
230
+ * Handles:
231
+ * - Plain 24-char hex ObjectId strings (fallback cursor)
232
+ * - Base64-encoded cursor tokens (standard cursor)
233
+ * - Cursor version and sort validation
234
+ */
235
+ function resolveCursorFilter(after, sort, cursorVersion, baseFilters = {}) {
236
+ if (/^[a-f0-9]{24}$/i.test(after)) {
237
+ const objectId = new mongoose.Types.ObjectId(after);
238
+ const idOperator = (sort._id || -1) === 1 ? "$gt" : "$lt";
239
+ return {
240
+ ...baseFilters,
241
+ _id: { [idOperator]: objectId }
242
+ };
243
+ }
244
+ const cursor = decodeCursor(after);
245
+ validateCursorVersion(cursor.version, cursorVersion);
246
+ validateCursorSort(cursor.sort, sort);
247
+ return buildKeysetFilter(baseFilters, sort, cursor.value, cursor.id, cursor.values);
248
+ }
249
+ //#endregion
250
+ export { encodeCursor as n, resolveCursorFilter as r, cursor_exports as t };
@@ -1,4 +1,4 @@
1
- import { C as GroupResult, F as ObjectId, G as PopulateSpec, K as ReadPreferenceType, N as MinMaxResult, R as OperationOptions, St as LookupOptions, _ as DeleteResult, at as SortSpec, ct as UpdateManyResult, et as SelectSpec, i as AnyDocument, lt as UpdateOptions, p as CreateOptions, ut as UpdateWithValidationResult } from "./types-BlCwDszq.mjs";
1
+ import { Ct as LookupOptions, I as ObjectId, K as PopulateSpec, P as MinMaxResult, dt as UpdateWithValidationResult, i as AnyDocument, lt as UpdateManyResult, m as CreateOptions, ot as SortSpec, q as ReadPreferenceType, tt as SelectSpec, ut as UpdateOptions, v as DeleteResult, w as GroupResult, z as OperationOptions } from "./types-COINbsdL.mjs";
2
2
  import { ClientSession, Model, PipelineStage } from "mongoose";
3
3
 
4
4
  //#region src/actions/aggregate.d.ts