@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 +4 -4
- package/dist/{PaginationEngine-PLyDhrO7.mjs → PaginationEngine-nY04eGUM.mjs} +33 -242
- package/dist/actions/index.d.mts +1 -1
- package/dist/actions/index.mjs +1 -1
- package/dist/ai/index.d.mts +1 -1
- package/dist/ai/index.mjs +2 -1
- package/dist/cursor-CHToazHy.mjs +250 -0
- package/dist/{index-Df3ernpC.d.mts → index-BuoZIZ15.d.mts} +1 -1
- package/dist/index.d.mts +14 -8
- package/dist/index.mjs +13 -6
- package/dist/{mongooseToJsonSchema-BqgVOlrR.d.mts → mongooseToJsonSchema-RX9YfJLu.d.mts} +1 -1
- package/dist/pagination/PaginationEngine.d.mts +1 -1
- package/dist/pagination/PaginationEngine.mjs +1 -1
- package/dist/plugins/index.d.mts +1 -1
- package/dist/plugins/index.mjs +1 -1
- package/dist/sort-C-BJEWUZ.mjs +57 -0
- package/dist/{types-BlCwDszq.d.mts → types-COINbsdL.d.mts} +32 -4
- package/dist/{update-DXwVh6M1.mjs → update-DGKMmBgG.mjs} +42 -35
- package/dist/utils/index.d.mts +2 -2
- package/dist/utils/index.mjs +2 -2
- package/dist/{validation-chain.plugin-DxqiHv-E.d.mts → validation-chain.plugin-BNoaKDOm.d.mts} +2 -2
- package/dist/{validation-chain.plugin-Ow6EUIoo.mjs → validation-chain.plugin-Cp5X5IZu.mjs} +157 -54
- package/package.json +3 -3
- /package/dist/{field-selection-CalOB7yM.mjs → field-selection-reyDRzXf.mjs} +0 -0
- /package/dist/{mongooseToJsonSchema-OmdmnHtx.mjs → mongooseToJsonSchema-B6Qyl8BK.mjs} +0 -0
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
|
-
- **
|
|
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
|
-
>
|
|
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
|
|
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
|
|
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
|
|
3
|
-
|
|
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/
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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 (
|
|
278
|
-
|
|
279
|
-
if (
|
|
280
|
-
|
|
281
|
-
|
|
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
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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 &&
|
|
355
|
-
else if (countStrategy === "
|
|
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
|
-
}
|
|
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
|
-
|
|
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 (
|
|
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);
|
package/dist/actions/index.d.mts
CHANGED
|
@@ -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-
|
|
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 };
|
package/dist/actions/index.mjs
CHANGED
|
@@ -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-
|
|
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,
|
package/dist/ai/index.d.mts
CHANGED
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
|
|
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 {
|
|
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
|