@classytic/mongokit 3.2.0 → 3.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/README.md +470 -193
  2. package/dist/actions/index.d.mts +9 -0
  3. package/dist/actions/index.mjs +15 -0
  4. package/dist/aggregate-BAi4Do-X.mjs +767 -0
  5. package/dist/aggregate-CCHI7F51.d.mts +269 -0
  6. package/dist/ai/index.d.mts +125 -0
  7. package/dist/ai/index.mjs +203 -0
  8. package/dist/cache-keys-C8Z9B5sw.mjs +204 -0
  9. package/dist/chunk-DQk6qfdC.mjs +18 -0
  10. package/dist/create-BuO6xt0v.mjs +55 -0
  11. package/dist/custom-id.plugin-B_zIs6gE.mjs +1818 -0
  12. package/dist/custom-id.plugin-BzZI4gnE.d.mts +893 -0
  13. package/dist/index.d.mts +1012 -0
  14. package/dist/index.mjs +1906 -0
  15. package/dist/limits-DsNeCx4D.mjs +299 -0
  16. package/dist/logger-D8ily-PP.mjs +51 -0
  17. package/dist/mongooseToJsonSchema-COdDEkIJ.mjs +317 -0
  18. package/dist/{mongooseToJsonSchema-CaRF_bCN.d.ts → mongooseToJsonSchema-Wbvjfwkn.d.mts} +16 -89
  19. package/dist/pagination/PaginationEngine.d.mts +93 -0
  20. package/dist/pagination/PaginationEngine.mjs +196 -0
  21. package/dist/plugins/index.d.mts +3 -0
  22. package/dist/plugins/index.mjs +3 -0
  23. package/dist/types-D-gploPr.d.mts +1241 -0
  24. package/dist/utils/{index.d.ts → index.d.mts} +14 -21
  25. package/dist/utils/index.mjs +5 -0
  26. package/package.json +21 -21
  27. package/dist/actions/index.d.ts +0 -3
  28. package/dist/actions/index.js +0 -5
  29. package/dist/ai/index.d.ts +0 -175
  30. package/dist/ai/index.js +0 -206
  31. package/dist/chunks/chunk-2ZN65ZOP.js +0 -93
  32. package/dist/chunks/chunk-44KXLGPO.js +0 -388
  33. package/dist/chunks/chunk-DEVXDBRL.js +0 -1226
  34. package/dist/chunks/chunk-I7CWNAJB.js +0 -46
  35. package/dist/chunks/chunk-JWUAVZ3L.js +0 -8
  36. package/dist/chunks/chunk-UE2IEXZJ.js +0 -306
  37. package/dist/chunks/chunk-URLJFIR7.js +0 -22
  38. package/dist/chunks/chunk-VWKIKZYF.js +0 -737
  39. package/dist/chunks/chunk-WSFCRVEQ.js +0 -7
  40. package/dist/index-BDn5fSTE.d.ts +0 -516
  41. package/dist/index.d.ts +0 -1422
  42. package/dist/index.js +0 -1893
  43. package/dist/pagination/PaginationEngine.d.ts +0 -117
  44. package/dist/pagination/PaginationEngine.js +0 -3
  45. package/dist/plugins/index.d.ts +0 -922
  46. package/dist/plugins/index.js +0 -6
  47. package/dist/types-Jni1KgkP.d.ts +0 -780
  48. package/dist/utils/index.js +0 -5
@@ -1,388 +0,0 @@
1
- import { createError } from './chunk-JWUAVZ3L.js';
2
- import mongoose from 'mongoose';
3
-
4
- function encodeCursor(doc, primaryField, sort, version = 1) {
5
- const primaryValue = doc[primaryField];
6
- const idValue = doc._id;
7
- const payload = {
8
- v: serializeValue(primaryValue),
9
- t: getValueType(primaryValue),
10
- id: serializeValue(idValue),
11
- idType: getValueType(idValue),
12
- sort,
13
- ver: version
14
- };
15
- return Buffer.from(JSON.stringify(payload)).toString("base64");
16
- }
17
- function decodeCursor(token) {
18
- try {
19
- const json = Buffer.from(token, "base64").toString("utf-8");
20
- const payload = JSON.parse(json);
21
- 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") {
22
- throw new Error("Malformed cursor payload");
23
- }
24
- const VALID_TYPES = ["date", "objectid", "boolean", "number", "string", "null", "unknown"];
25
- if (!VALID_TYPES.includes(payload.t) || !VALID_TYPES.includes(payload.idType)) {
26
- throw new Error("Invalid cursor value type");
27
- }
28
- return {
29
- value: rehydrateValue(payload.v, payload.t),
30
- id: rehydrateValue(payload.id, payload.idType),
31
- sort: payload.sort,
32
- version: payload.ver
33
- };
34
- } catch {
35
- throw new Error("Invalid cursor token");
36
- }
37
- }
38
- function validateCursorSort(cursorSort, currentSort) {
39
- const cursorSortStr = JSON.stringify(cursorSort);
40
- const currentSortStr = JSON.stringify(currentSort);
41
- if (cursorSortStr !== currentSortStr) {
42
- throw new Error("Cursor sort does not match current query sort");
43
- }
44
- }
45
- function validateCursorVersion(cursorVersion, expectedVersion) {
46
- if (cursorVersion !== expectedVersion) {
47
- throw new Error(`Cursor version ${cursorVersion} does not match expected version ${expectedVersion}`);
48
- }
49
- }
50
- function serializeValue(value) {
51
- if (value === null || value === void 0) return null;
52
- if (value instanceof Date) return value.toISOString();
53
- if (value instanceof mongoose.Types.ObjectId) return value.toString();
54
- return value;
55
- }
56
- function getValueType(value) {
57
- if (value === null || value === void 0) return "null";
58
- if (value instanceof Date) return "date";
59
- if (value instanceof mongoose.Types.ObjectId) return "objectid";
60
- if (typeof value === "boolean") return "boolean";
61
- if (typeof value === "number") return "number";
62
- if (typeof value === "string") return "string";
63
- return "unknown";
64
- }
65
- function rehydrateValue(serialized, type) {
66
- if (type === "null" || serialized === null) return null;
67
- switch (type) {
68
- case "date":
69
- return new Date(serialized);
70
- case "objectid":
71
- return new mongoose.Types.ObjectId(serialized);
72
- case "boolean":
73
- return Boolean(serialized);
74
- case "number":
75
- return Number(serialized);
76
- default:
77
- return serialized;
78
- }
79
- }
80
-
81
- // src/pagination/utils/sort.ts
82
- function normalizeSort(sort) {
83
- const normalized = {};
84
- Object.keys(sort).forEach((key) => {
85
- if (key !== "_id") normalized[key] = sort[key];
86
- });
87
- if (sort._id !== void 0) {
88
- normalized._id = sort._id;
89
- }
90
- return normalized;
91
- }
92
- function validateKeysetSort(sort) {
93
- const keys = Object.keys(sort);
94
- if (keys.length === 1 && keys[0] !== "_id") {
95
- const field = keys[0];
96
- const direction = sort[field];
97
- return normalizeSort({ [field]: direction, _id: direction });
98
- }
99
- if (keys.length === 1 && keys[0] === "_id") {
100
- return normalizeSort(sort);
101
- }
102
- if (keys.length === 2) {
103
- if (!keys.includes("_id")) {
104
- throw new Error("Keyset pagination requires _id as tie-breaker");
105
- }
106
- const primaryField = keys.find((k) => k !== "_id");
107
- const primaryDirection = sort[primaryField];
108
- const idDirection = sort._id;
109
- if (primaryDirection !== idDirection) {
110
- throw new Error("_id direction must match primary field direction");
111
- }
112
- return normalizeSort(sort);
113
- }
114
- throw new Error("Keyset pagination only supports single field + _id");
115
- }
116
- function getPrimaryField(sort) {
117
- const keys = Object.keys(sort);
118
- return keys.find((k) => k !== "_id") || "_id";
119
- }
120
-
121
- // src/pagination/utils/filter.ts
122
- function buildKeysetFilter(baseFilters, sort, cursorValue, cursorId) {
123
- const primaryField = Object.keys(sort).find((k) => k !== "_id") || "_id";
124
- const direction = sort[primaryField];
125
- const operator = direction === 1 ? "$gt" : "$lt";
126
- if (cursorValue === null || cursorValue === void 0) {
127
- if (direction === 1) {
128
- return {
129
- ...baseFilters,
130
- $or: [
131
- { [primaryField]: null, _id: { $gt: cursorId } },
132
- { [primaryField]: { $ne: null } }
133
- ]
134
- };
135
- } else {
136
- return {
137
- ...baseFilters,
138
- [primaryField]: null,
139
- _id: { $lt: cursorId }
140
- };
141
- }
142
- }
143
- return {
144
- ...baseFilters,
145
- $or: [
146
- { [primaryField]: { [operator]: cursorValue } },
147
- {
148
- [primaryField]: cursorValue,
149
- _id: { [operator]: cursorId }
150
- }
151
- ]
152
- };
153
- }
154
-
155
- // src/pagination/utils/limits.ts
156
- function validateLimit(limit, config) {
157
- const parsed = Number(limit);
158
- if (!Number.isFinite(parsed) || parsed < 1) {
159
- return config.defaultLimit || 10;
160
- }
161
- return Math.min(Math.floor(parsed), config.maxLimit || 100);
162
- }
163
- function validatePage(page, config) {
164
- const parsed = Number(page);
165
- if (!Number.isFinite(parsed) || parsed < 1) {
166
- return 1;
167
- }
168
- const sanitized = Math.floor(parsed);
169
- if (sanitized > (config.maxPage || 1e4)) {
170
- throw new Error(`Page ${sanitized} exceeds maximum ${config.maxPage || 1e4}`);
171
- }
172
- return sanitized;
173
- }
174
- function shouldWarnDeepPagination(page, threshold) {
175
- return page > threshold;
176
- }
177
- function calculateSkip(page, limit) {
178
- return (page - 1) * limit;
179
- }
180
- function calculateTotalPages(total, limit) {
181
- return Math.ceil(total / limit);
182
- }
183
-
184
- // src/pagination/PaginationEngine.ts
185
- var PaginationEngine = class {
186
- Model;
187
- config;
188
- /**
189
- * Create a new pagination engine
190
- *
191
- * @param Model - Mongoose model to paginate
192
- * @param config - Pagination configuration
193
- */
194
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
195
- constructor(Model, config = {}) {
196
- this.Model = Model;
197
- this.config = {
198
- defaultLimit: config.defaultLimit || 10,
199
- maxLimit: config.maxLimit || 100,
200
- maxPage: config.maxPage || 1e4,
201
- deepPageThreshold: config.deepPageThreshold || 100,
202
- cursorVersion: config.cursorVersion || 1,
203
- useEstimatedCount: config.useEstimatedCount || false
204
- };
205
- }
206
- /**
207
- * Offset-based pagination using skip/limit
208
- * Best for small datasets and when users need random page access
209
- * O(n) performance - slower for deep pages
210
- *
211
- * @param options - Pagination options
212
- * @returns Pagination result with total count
213
- *
214
- * @example
215
- * const result = await engine.paginate({
216
- * filters: { status: 'active' },
217
- * sort: { createdAt: -1 },
218
- * page: 1,
219
- * limit: 20
220
- * });
221
- * console.log(result.docs, result.total, result.hasNext);
222
- */
223
- async paginate(options = {}) {
224
- const {
225
- filters = {},
226
- sort = { _id: -1 },
227
- page = 1,
228
- limit = this.config.defaultLimit,
229
- select,
230
- populate = [],
231
- lean = true,
232
- session
233
- } = options;
234
- const sanitizedPage = validatePage(page, this.config);
235
- const sanitizedLimit = validateLimit(limit, this.config);
236
- const skip = calculateSkip(sanitizedPage, sanitizedLimit);
237
- let query = this.Model.find(filters);
238
- if (select) query = query.select(select);
239
- if (populate && (Array.isArray(populate) ? populate.length : populate)) {
240
- query = query.populate(populate);
241
- }
242
- query = query.sort(sort).skip(skip).limit(sanitizedLimit).lean(lean);
243
- if (session) query = query.session(session);
244
- const hasFilters = Object.keys(filters).length > 0;
245
- const useEstimated = this.config.useEstimatedCount && !hasFilters;
246
- const [docs, total] = await Promise.all([
247
- query.exec(),
248
- useEstimated ? this.Model.estimatedDocumentCount() : this.Model.countDocuments(filters).session(session ?? null)
249
- ]);
250
- const totalPages = calculateTotalPages(total, sanitizedLimit);
251
- const warning = shouldWarnDeepPagination(sanitizedPage, this.config.deepPageThreshold) ? `Deep pagination (page ${sanitizedPage}). Consider getAll({ after, sort, limit }) for better performance.` : void 0;
252
- return {
253
- method: "offset",
254
- docs,
255
- page: sanitizedPage,
256
- limit: sanitizedLimit,
257
- total,
258
- pages: totalPages,
259
- hasNext: sanitizedPage < totalPages,
260
- hasPrev: sanitizedPage > 1,
261
- ...warning && { warning }
262
- };
263
- }
264
- /**
265
- * Keyset (cursor-based) pagination for high-performance streaming
266
- * Best for large datasets, infinite scroll, real-time feeds
267
- * O(1) performance - consistent speed regardless of position
268
- *
269
- * @param options - Pagination options (sort is required)
270
- * @returns Pagination result with next cursor
271
- *
272
- * @example
273
- * // First page
274
- * const page1 = await engine.stream({
275
- * sort: { createdAt: -1 },
276
- * limit: 20
277
- * });
278
- *
279
- * // Next page using cursor
280
- * const page2 = await engine.stream({
281
- * sort: { createdAt: -1 },
282
- * after: page1.next,
283
- * limit: 20
284
- * });
285
- */
286
- async stream(options) {
287
- const {
288
- filters = {},
289
- sort,
290
- after,
291
- limit = this.config.defaultLimit,
292
- select,
293
- populate = [],
294
- lean = true,
295
- session
296
- } = options;
297
- if (!sort) {
298
- throw createError(400, "sort is required for keyset pagination");
299
- }
300
- const sanitizedLimit = validateLimit(limit, this.config);
301
- const normalizedSort = validateKeysetSort(sort);
302
- let query = { ...filters };
303
- if (after) {
304
- const cursor = decodeCursor(after);
305
- validateCursorVersion(cursor.version, this.config.cursorVersion);
306
- validateCursorSort(cursor.sort, normalizedSort);
307
- query = buildKeysetFilter(query, normalizedSort, cursor.value, cursor.id);
308
- }
309
- let mongoQuery = this.Model.find(query);
310
- if (select) mongoQuery = mongoQuery.select(select);
311
- if (populate && (Array.isArray(populate) ? populate.length : populate)) {
312
- mongoQuery = mongoQuery.populate(populate);
313
- }
314
- mongoQuery = mongoQuery.sort(normalizedSort).limit(sanitizedLimit + 1).lean(lean);
315
- if (session) mongoQuery = mongoQuery.session(session);
316
- const docs = await mongoQuery.exec();
317
- const hasMore = docs.length > sanitizedLimit;
318
- if (hasMore) docs.pop();
319
- const primaryField = getPrimaryField(normalizedSort);
320
- const nextCursor = hasMore && docs.length > 0 ? encodeCursor(docs[docs.length - 1], primaryField, normalizedSort, this.config.cursorVersion) : null;
321
- return {
322
- method: "keyset",
323
- docs,
324
- limit: sanitizedLimit,
325
- hasMore,
326
- next: nextCursor
327
- };
328
- }
329
- /**
330
- * Aggregate pipeline with pagination
331
- * Best for complex queries requiring aggregation stages
332
- * Uses $facet to combine results and count in single query
333
- *
334
- * @param options - Aggregation options
335
- * @returns Pagination result with total count
336
- *
337
- * @example
338
- * const result = await engine.aggregatePaginate({
339
- * pipeline: [
340
- * { $match: { status: 'active' } },
341
- * { $group: { _id: '$category', count: { $sum: 1 } } },
342
- * { $sort: { count: -1 } }
343
- * ],
344
- * page: 1,
345
- * limit: 20
346
- * });
347
- */
348
- async aggregatePaginate(options = {}) {
349
- const {
350
- pipeline = [],
351
- page = 1,
352
- limit = this.config.defaultLimit,
353
- session
354
- } = options;
355
- const sanitizedPage = validatePage(page, this.config);
356
- const sanitizedLimit = validateLimit(limit, this.config);
357
- const skip = calculateSkip(sanitizedPage, sanitizedLimit);
358
- const facetPipeline = [
359
- ...pipeline,
360
- {
361
- $facet: {
362
- docs: [{ $skip: skip }, { $limit: sanitizedLimit }],
363
- total: [{ $count: "count" }]
364
- }
365
- }
366
- ];
367
- const aggregation = this.Model.aggregate(facetPipeline);
368
- if (session) aggregation.session(session);
369
- const [result] = await aggregation.exec();
370
- const docs = result.docs;
371
- const total = result.total[0]?.count || 0;
372
- const totalPages = calculateTotalPages(total, sanitizedLimit);
373
- const warning = shouldWarnDeepPagination(sanitizedPage, this.config.deepPageThreshold) ? `Deep pagination in aggregate (page ${sanitizedPage}). Uses $skip internally.` : void 0;
374
- return {
375
- method: "aggregate",
376
- docs,
377
- page: sanitizedPage,
378
- limit: sanitizedLimit,
379
- total,
380
- pages: totalPages,
381
- hasNext: sanitizedPage < totalPages,
382
- hasPrev: sanitizedPage > 1,
383
- ...warning && { warning }
384
- };
385
- }
386
- };
387
-
388
- export { PaginationEngine };