@classytic/mongokit 3.2.1 → 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-5G42WJHC.js +0 -737
  34. package/dist/chunks/chunk-B64F5ZWE.js +0 -1226
  35. package/dist/chunks/chunk-GZBKEPVE.js +0 -46
  36. package/dist/chunks/chunk-JWUAVZ3L.js +0 -8
  37. package/dist/chunks/chunk-UE2IEXZJ.js +0 -306
  38. package/dist/chunks/chunk-URLJFIR7.js +0 -22
  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
@@ -0,0 +1,767 @@
1
+ import { t as __exportAll } from "./chunk-DQk6qfdC.mjs";
2
+ import { i as createError, r as warn } from "./logger-D8ily-PP.mjs";
3
+
4
+ //#region src/actions/read.ts
5
+ var read_exports = /* @__PURE__ */ __exportAll({
6
+ count: () => count,
7
+ exists: () => exists,
8
+ getAll: () => getAll,
9
+ getById: () => getById,
10
+ getByQuery: () => getByQuery,
11
+ getOrCreate: () => getOrCreate,
12
+ tryGetByQuery: () => tryGetByQuery
13
+ });
14
+ /**
15
+ * Parse populate specification into consistent format
16
+ */
17
+ function parsePopulate$1(populate) {
18
+ if (!populate) return [];
19
+ if (typeof populate === "string") return populate.split(",").map((p) => p.trim());
20
+ if (Array.isArray(populate)) return populate.map((p) => typeof p === "string" ? p.trim() : p);
21
+ return [populate];
22
+ }
23
+ /**
24
+ * Get document by ID
25
+ *
26
+ * @param Model - Mongoose model
27
+ * @param id - Document ID
28
+ * @param options - Query options
29
+ * @returns Document or null
30
+ * @throws Error if document not found and throwOnNotFound is true
31
+ */
32
+ async function getById(Model, id, options = {}) {
33
+ const query = options.query ? Model.findOne({
34
+ _id: id,
35
+ ...options.query
36
+ }) : Model.findById(id);
37
+ if (options.select) query.select(options.select);
38
+ if (options.populate) query.populate(parsePopulate$1(options.populate));
39
+ if (options.lean) query.lean();
40
+ if (options.session) query.session(options.session);
41
+ if (options.readPreference) query.read(options.readPreference);
42
+ const document = await query.exec();
43
+ if (!document && options.throwOnNotFound !== false) throw createError(404, "Document not found");
44
+ return document;
45
+ }
46
+ /**
47
+ * Get document by query
48
+ *
49
+ * @param Model - Mongoose model
50
+ * @param query - MongoDB query
51
+ * @param options - Query options
52
+ * @returns Document or null
53
+ * @throws Error if document not found and throwOnNotFound is true
54
+ */
55
+ async function getByQuery(Model, query, options = {}) {
56
+ const mongoQuery = Model.findOne(query);
57
+ if (options.select) mongoQuery.select(options.select);
58
+ if (options.populate) mongoQuery.populate(parsePopulate$1(options.populate));
59
+ if (options.lean) mongoQuery.lean();
60
+ if (options.session) mongoQuery.session(options.session);
61
+ if (options.readPreference) mongoQuery.read(options.readPreference);
62
+ const document = await mongoQuery.exec();
63
+ if (!document && options.throwOnNotFound !== false) throw createError(404, "Document not found");
64
+ return document;
65
+ }
66
+ /**
67
+ * Get document by query without throwing (returns null if not found)
68
+ */
69
+ async function tryGetByQuery(Model, query, options = {}) {
70
+ return getByQuery(Model, query, {
71
+ ...options,
72
+ throwOnNotFound: false
73
+ });
74
+ }
75
+ /**
76
+ * Get all documents (basic query without pagination)
77
+ * For pagination, use Repository.paginate() or Repository.stream()
78
+ */
79
+ async function getAll(Model, query = {}, options = {}) {
80
+ let mongoQuery = Model.find(query);
81
+ if (options.select) mongoQuery = mongoQuery.select(options.select);
82
+ if (options.populate) mongoQuery = mongoQuery.populate(parsePopulate$1(options.populate));
83
+ if (options.sort) mongoQuery = mongoQuery.sort(options.sort);
84
+ if (options.limit) mongoQuery = mongoQuery.limit(options.limit);
85
+ if (options.skip) mongoQuery = mongoQuery.skip(options.skip);
86
+ mongoQuery = mongoQuery.lean(options.lean !== false);
87
+ if (options.session) mongoQuery = mongoQuery.session(options.session);
88
+ if (options.readPreference) mongoQuery = mongoQuery.read(options.readPreference);
89
+ return mongoQuery.exec();
90
+ }
91
+ /**
92
+ * Get or create document (upsert)
93
+ */
94
+ async function getOrCreate(Model, query, createData, options = {}) {
95
+ return Model.findOneAndUpdate(query, { $setOnInsert: createData }, {
96
+ upsert: true,
97
+ returnDocument: "after",
98
+ runValidators: true,
99
+ session: options.session,
100
+ ...options.updatePipeline !== void 0 ? { updatePipeline: options.updatePipeline } : {}
101
+ });
102
+ }
103
+ /**
104
+ * Count documents matching query
105
+ */
106
+ async function count(Model, query = {}, options = {}) {
107
+ const q = Model.countDocuments(query).session(options.session ?? null);
108
+ if (options.readPreference) q.read(options.readPreference);
109
+ return q;
110
+ }
111
+ /**
112
+ * Check if document exists
113
+ */
114
+ async function exists(Model, query, options = {}) {
115
+ const q = Model.exists(query).session(options.session ?? null);
116
+ if (options.readPreference) q.read(options.readPreference);
117
+ return q;
118
+ }
119
+
120
+ //#endregion
121
+ //#region src/actions/update.ts
122
+ var update_exports = /* @__PURE__ */ __exportAll({
123
+ increment: () => increment,
124
+ pullFromArray: () => pullFromArray,
125
+ pushToArray: () => pushToArray,
126
+ update: () => update,
127
+ updateByQuery: () => updateByQuery,
128
+ updateMany: () => updateMany,
129
+ updateWithConstraints: () => updateWithConstraints,
130
+ updateWithValidation: () => updateWithValidation
131
+ });
132
+ function assertUpdatePipelineAllowed(update, updatePipeline) {
133
+ if (Array.isArray(update) && updatePipeline !== true) throw createError(400, "Update pipelines (array updates) are disabled by default; pass `{ updatePipeline: true }` to explicitly allow pipeline-style updates.");
134
+ }
135
+ /**
136
+ * Parse populate specification into consistent format
137
+ */
138
+ function parsePopulate(populate) {
139
+ if (!populate) return [];
140
+ if (typeof populate === "string") return populate.split(",").map((p) => p.trim());
141
+ if (Array.isArray(populate)) return populate.map((p) => typeof p === "string" ? p.trim() : p);
142
+ return [populate];
143
+ }
144
+ /**
145
+ * Update by ID
146
+ */
147
+ async function update(Model, id, data, options = {}) {
148
+ assertUpdatePipelineAllowed(data, options.updatePipeline);
149
+ const query = {
150
+ _id: id,
151
+ ...options.query
152
+ };
153
+ const document = await Model.findOneAndUpdate(query, data, {
154
+ returnDocument: "after",
155
+ runValidators: true,
156
+ session: options.session,
157
+ ...options.updatePipeline !== void 0 ? { updatePipeline: options.updatePipeline } : {}
158
+ }).select(options.select || "").populate(parsePopulate(options.populate)).lean(options.lean ?? false);
159
+ if (!document) throw createError(404, "Document not found");
160
+ return document;
161
+ }
162
+ /**
163
+ * Update with query constraints (optimized)
164
+ * Returns null if constraints not met (not an error)
165
+ */
166
+ async function updateWithConstraints(Model, id, data, constraints = {}, options = {}) {
167
+ assertUpdatePipelineAllowed(data, options.updatePipeline);
168
+ const query = {
169
+ _id: id,
170
+ ...constraints
171
+ };
172
+ return await Model.findOneAndUpdate(query, data, {
173
+ returnDocument: "after",
174
+ runValidators: true,
175
+ session: options.session,
176
+ ...options.updatePipeline !== void 0 ? { updatePipeline: options.updatePipeline } : {}
177
+ }).select(options.select || "").populate(parsePopulate(options.populate)).lean(options.lean ?? false);
178
+ }
179
+ /**
180
+ * Update with validation (smart optimization)
181
+ * 1-query on success, 2-queries for detailed errors
182
+ */
183
+ async function updateWithValidation(Model, id, data, validationOptions = {}, options = {}) {
184
+ const { buildConstraints, validateUpdate } = validationOptions;
185
+ assertUpdatePipelineAllowed(data, options.updatePipeline);
186
+ if (buildConstraints) {
187
+ const document = await updateWithConstraints(Model, id, data, buildConstraints(data), options);
188
+ if (document) return {
189
+ success: true,
190
+ data: document
191
+ };
192
+ }
193
+ const existing = await Model.findById(id).select(options.select || "").lean();
194
+ if (!existing) return {
195
+ success: false,
196
+ error: {
197
+ code: 404,
198
+ message: "Document not found"
199
+ }
200
+ };
201
+ if (validateUpdate) {
202
+ const validation = validateUpdate(existing, data);
203
+ if (!validation.valid) return {
204
+ success: false,
205
+ error: {
206
+ code: 403,
207
+ message: validation.message || "Update not allowed",
208
+ violations: validation.violations
209
+ }
210
+ };
211
+ }
212
+ return {
213
+ success: true,
214
+ data: await update(Model, id, data, options)
215
+ };
216
+ }
217
+ /**
218
+ * Update many documents
219
+ */
220
+ async function updateMany(Model, query, data, options = {}) {
221
+ assertUpdatePipelineAllowed(data, options.updatePipeline);
222
+ const result = await Model.updateMany(query, data, {
223
+ runValidators: true,
224
+ session: options.session,
225
+ ...options.updatePipeline !== void 0 ? { updatePipeline: options.updatePipeline } : {}
226
+ });
227
+ return {
228
+ matchedCount: result.matchedCount,
229
+ modifiedCount: result.modifiedCount
230
+ };
231
+ }
232
+ /**
233
+ * Update by query
234
+ */
235
+ async function updateByQuery(Model, query, data, options = {}) {
236
+ assertUpdatePipelineAllowed(data, options.updatePipeline);
237
+ const document = await Model.findOneAndUpdate(query, data, {
238
+ returnDocument: "after",
239
+ runValidators: true,
240
+ session: options.session,
241
+ ...options.updatePipeline !== void 0 ? { updatePipeline: options.updatePipeline } : {}
242
+ }).select(options.select || "").populate(parsePopulate(options.populate)).lean(options.lean ?? false);
243
+ if (!document && options.throwOnNotFound !== false) throw createError(404, "Document not found");
244
+ return document;
245
+ }
246
+ /**
247
+ * Increment field
248
+ */
249
+ async function increment(Model, id, field, value = 1, options = {}) {
250
+ return update(Model, id, { $inc: { [field]: value } }, options);
251
+ }
252
+ /**
253
+ * Push to array
254
+ */
255
+ async function pushToArray(Model, id, field, value, options = {}) {
256
+ return update(Model, id, { $push: { [field]: value } }, options);
257
+ }
258
+ /**
259
+ * Pull from array
260
+ */
261
+ async function pullFromArray(Model, id, field, value, options = {}) {
262
+ return update(Model, id, { $pull: { [field]: value } }, options);
263
+ }
264
+
265
+ //#endregion
266
+ //#region src/actions/delete.ts
267
+ var delete_exports = /* @__PURE__ */ __exportAll({
268
+ deleteById: () => deleteById,
269
+ deleteByQuery: () => deleteByQuery,
270
+ deleteMany: () => deleteMany,
271
+ restore: () => restore,
272
+ softDelete: () => softDelete
273
+ });
274
+ /**
275
+ * Delete by ID
276
+ */
277
+ async function deleteById(Model, id, options = {}) {
278
+ const query = {
279
+ _id: id,
280
+ ...options.query
281
+ };
282
+ if (!await Model.findOneAndDelete(query).session(options.session ?? null)) throw createError(404, "Document not found");
283
+ return {
284
+ success: true,
285
+ message: "Deleted successfully"
286
+ };
287
+ }
288
+ /**
289
+ * Delete many documents
290
+ */
291
+ async function deleteMany(Model, query, options = {}) {
292
+ return {
293
+ success: true,
294
+ count: (await Model.deleteMany(query).session(options.session ?? null)).deletedCount,
295
+ message: "Deleted successfully"
296
+ };
297
+ }
298
+ /**
299
+ * Delete by query
300
+ */
301
+ async function deleteByQuery(Model, query, options = {}) {
302
+ if (!await Model.findOneAndDelete(query).session(options.session ?? null) && options.throwOnNotFound !== false) throw createError(404, "Document not found");
303
+ return {
304
+ success: true,
305
+ message: "Deleted successfully"
306
+ };
307
+ }
308
+ /**
309
+ * Soft delete (set deleted flag)
310
+ */
311
+ async function softDelete(Model, id, options = {}) {
312
+ if (!await Model.findByIdAndUpdate(id, {
313
+ deleted: true,
314
+ deletedAt: /* @__PURE__ */ new Date(),
315
+ deletedBy: options.userId
316
+ }, {
317
+ returnDocument: "after",
318
+ session: options.session
319
+ })) throw createError(404, "Document not found");
320
+ return {
321
+ success: true,
322
+ message: "Soft deleted successfully"
323
+ };
324
+ }
325
+ /**
326
+ * Restore soft deleted document
327
+ */
328
+ async function restore(Model, id, options = {}) {
329
+ if (!await Model.findByIdAndUpdate(id, {
330
+ deleted: false,
331
+ deletedAt: null,
332
+ deletedBy: null
333
+ }, {
334
+ returnDocument: "after",
335
+ session: options.session
336
+ })) throw createError(404, "Document not found");
337
+ return {
338
+ success: true,
339
+ message: "Restored successfully"
340
+ };
341
+ }
342
+
343
+ //#endregion
344
+ //#region src/query/LookupBuilder.ts
345
+ /** Stages that are never valid inside a $lookup pipeline */
346
+ const BLOCKED_PIPELINE_STAGES = [
347
+ "$out",
348
+ "$merge",
349
+ "$unionWith",
350
+ "$collStats",
351
+ "$currentOp",
352
+ "$listSessions"
353
+ ];
354
+ /** Operators that can enable arbitrary code execution */
355
+ const DANGEROUS_OPERATORS = [
356
+ "$where",
357
+ "$function",
358
+ "$accumulator",
359
+ "$expr"
360
+ ];
361
+ /**
362
+ * Fluent builder for MongoDB $lookup aggregation stage
363
+ * Optimized for custom field joins at scale
364
+ */
365
+ var LookupBuilder = class LookupBuilder {
366
+ options = {};
367
+ constructor(from) {
368
+ if (from) this.options.from = from;
369
+ }
370
+ /**
371
+ * Set the collection to join with
372
+ */
373
+ from(collection) {
374
+ this.options.from = collection;
375
+ return this;
376
+ }
377
+ /**
378
+ * Set the local field (source collection)
379
+ * IMPORTANT: This field should be indexed for optimal performance
380
+ */
381
+ localField(field) {
382
+ this.options.localField = field;
383
+ return this;
384
+ }
385
+ /**
386
+ * Set the foreign field (target collection)
387
+ * IMPORTANT: This field should be indexed (preferably unique) for optimal performance
388
+ */
389
+ foreignField(field) {
390
+ this.options.foreignField = field;
391
+ return this;
392
+ }
393
+ /**
394
+ * Set the output field name
395
+ * Defaults to the collection name if not specified
396
+ */
397
+ as(fieldName) {
398
+ this.options.as = fieldName;
399
+ return this;
400
+ }
401
+ /**
402
+ * Mark this lookup as returning a single document
403
+ * Automatically unwraps the array result to a single object or null
404
+ */
405
+ single(isSingle = true) {
406
+ this.options.single = isSingle;
407
+ return this;
408
+ }
409
+ /**
410
+ * Add a pipeline to filter/transform joined documents
411
+ * Useful for filtering, sorting, or limiting joined results
412
+ *
413
+ * @example
414
+ * ```typescript
415
+ * lookup.pipeline([
416
+ * { $match: { status: 'active' } },
417
+ * { $sort: { priority: -1 } },
418
+ * { $limit: 5 }
419
+ * ]);
420
+ * ```
421
+ */
422
+ pipeline(stages) {
423
+ this.options.pipeline = stages;
424
+ return this;
425
+ }
426
+ /**
427
+ * Set let variables for use in pipeline
428
+ * Allows referencing local document fields in the pipeline
429
+ */
430
+ let(variables) {
431
+ this.options.let = variables;
432
+ return this;
433
+ }
434
+ /**
435
+ * Build the $lookup aggregation stage(s)
436
+ * Returns an array of pipeline stages including $lookup and optional $unwind
437
+ *
438
+ * IMPORTANT: MongoDB $lookup has two mutually exclusive forms:
439
+ * 1. Simple form: { from, localField, foreignField, as }
440
+ * 2. Pipeline form: { from, let, pipeline, as }
441
+ *
442
+ * When pipeline or let is specified, we use the pipeline form.
443
+ * Otherwise, we use the simpler localField/foreignField form.
444
+ */
445
+ build() {
446
+ const { from, localField, foreignField, as, single, pipeline, let: letVars } = this.options;
447
+ if (!from) throw new Error("LookupBuilder: \"from\" collection is required");
448
+ const outputField = as || from;
449
+ const stages = [];
450
+ const usePipelineForm = pipeline || letVars;
451
+ let lookupStage;
452
+ if (usePipelineForm) if (!pipeline || pipeline.length === 0) {
453
+ if (!localField || !foreignField) throw new Error("LookupBuilder: When using pipeline form without a custom pipeline, both localField and foreignField are required to auto-generate the pipeline");
454
+ const autoPipeline = [{ $match: { $expr: { $eq: [`$${foreignField}`, `$$${localField}`] } } }];
455
+ lookupStage = { $lookup: {
456
+ from,
457
+ let: {
458
+ [localField]: `$${localField}`,
459
+ ...letVars || {}
460
+ },
461
+ pipeline: autoPipeline,
462
+ as: outputField
463
+ } };
464
+ } else {
465
+ const safePipeline = this.options.sanitize !== false ? LookupBuilder.sanitizePipeline(pipeline) : pipeline;
466
+ lookupStage = { $lookup: {
467
+ from,
468
+ ...letVars && { let: letVars },
469
+ pipeline: safePipeline,
470
+ as: outputField
471
+ } };
472
+ }
473
+ else {
474
+ if (!localField || !foreignField) throw new Error("LookupBuilder: localField and foreignField are required for simple lookup");
475
+ lookupStage = { $lookup: {
476
+ from,
477
+ localField,
478
+ foreignField,
479
+ as: outputField
480
+ } };
481
+ }
482
+ stages.push(lookupStage);
483
+ if (single) stages.push({ $unwind: {
484
+ path: `$${outputField}`,
485
+ preserveNullAndEmptyArrays: true
486
+ } });
487
+ return stages;
488
+ }
489
+ /**
490
+ * Build and return only the $lookup stage (without $unwind)
491
+ * Useful when you want to handle unwrapping yourself
492
+ */
493
+ buildLookupOnly() {
494
+ return this.build()[0];
495
+ }
496
+ /**
497
+ * Static helper: Create a simple lookup in one line
498
+ */
499
+ static simple(from, localField, foreignField, options = {}) {
500
+ return new LookupBuilder(from).localField(localField).foreignField(foreignField).as(options.as || from).single(options.single || false).build();
501
+ }
502
+ /**
503
+ * Static helper: Create multiple lookups at once
504
+ *
505
+ * @example
506
+ * ```typescript
507
+ * const pipeline = LookupBuilder.multiple([
508
+ * { from: 'departments', localField: 'deptSlug', foreignField: 'slug', single: true },
509
+ * { from: 'managers', localField: 'managerId', foreignField: '_id', single: true }
510
+ * ]);
511
+ * ```
512
+ */
513
+ static multiple(lookups) {
514
+ return lookups.flatMap((lookup) => {
515
+ const builder = new LookupBuilder(lookup.from).localField(lookup.localField).foreignField(lookup.foreignField);
516
+ if (lookup.as) builder.as(lookup.as);
517
+ if (lookup.single) builder.single(lookup.single);
518
+ if (lookup.pipeline) builder.pipeline(lookup.pipeline);
519
+ if (lookup.let) builder.let(lookup.let);
520
+ return builder.build();
521
+ });
522
+ }
523
+ /**
524
+ * Static helper: Create a nested lookup (lookup within lookup)
525
+ * Useful for multi-level joins like Order -> Product -> Category
526
+ *
527
+ * @example
528
+ * ```typescript
529
+ * // Join orders with products, then products with categories
530
+ * const pipeline = LookupBuilder.nested([
531
+ * { from: 'products', localField: 'productSku', foreignField: 'sku', as: 'product', single: true },
532
+ * { from: 'categories', localField: 'product.categorySlug', foreignField: 'slug', as: 'product.category', single: true }
533
+ * ]);
534
+ * ```
535
+ */
536
+ static nested(lookups) {
537
+ return lookups.flatMap((lookup, index) => {
538
+ const builder = new LookupBuilder(lookup.from).localField(lookup.localField).foreignField(lookup.foreignField);
539
+ if (lookup.as) builder.as(lookup.as);
540
+ if (lookup.single !== void 0) builder.single(lookup.single);
541
+ if (lookup.pipeline) builder.pipeline(lookup.pipeline);
542
+ if (lookup.let) builder.let(lookup.let);
543
+ return builder.build();
544
+ });
545
+ }
546
+ /**
547
+ * Sanitize pipeline stages by blocking dangerous stages and operators.
548
+ * Used internally by build() and available for external use (e.g., aggregate.ts).
549
+ */
550
+ static sanitizePipeline(stages) {
551
+ const sanitized = [];
552
+ for (const stage of stages) {
553
+ if (!stage || typeof stage !== "object") continue;
554
+ const entries = Object.entries(stage);
555
+ if (entries.length !== 1) continue;
556
+ const [op, config] = entries[0];
557
+ if (BLOCKED_PIPELINE_STAGES.includes(op)) {
558
+ warn(`[mongokit] Blocked dangerous pipeline stage in lookup: ${op}`);
559
+ continue;
560
+ }
561
+ if ((op === "$match" || op === "$addFields" || op === "$set") && typeof config === "object" && config !== null) sanitized.push({ [op]: LookupBuilder._sanitizeDeep(config) });
562
+ else sanitized.push(stage);
563
+ }
564
+ return sanitized;
565
+ }
566
+ /**
567
+ * Recursively remove dangerous operators from an expression object.
568
+ */
569
+ static _sanitizeDeep(config) {
570
+ const sanitized = {};
571
+ for (const [key, value] of Object.entries(config)) {
572
+ if (DANGEROUS_OPERATORS.includes(key)) {
573
+ warn(`[mongokit] Blocked dangerous operator in lookup pipeline: ${key}`);
574
+ continue;
575
+ }
576
+ if (value && typeof value === "object" && !Array.isArray(value)) sanitized[key] = LookupBuilder._sanitizeDeep(value);
577
+ else if (Array.isArray(value)) sanitized[key] = value.map((item) => {
578
+ if (item && typeof item === "object" && !Array.isArray(item)) return LookupBuilder._sanitizeDeep(item);
579
+ return item;
580
+ });
581
+ else sanitized[key] = value;
582
+ }
583
+ return sanitized;
584
+ }
585
+ };
586
+
587
+ //#endregion
588
+ //#region src/actions/aggregate.ts
589
+ var aggregate_exports = /* @__PURE__ */ __exportAll({
590
+ aggregate: () => aggregate,
591
+ aggregatePaginate: () => aggregatePaginate,
592
+ average: () => average,
593
+ countBy: () => countBy,
594
+ distinct: () => distinct,
595
+ facet: () => facet,
596
+ groupBy: () => groupBy,
597
+ lookup: () => lookup,
598
+ minMax: () => minMax,
599
+ sum: () => sum,
600
+ unwind: () => unwind
601
+ });
602
+ /**
603
+ * Execute aggregation pipeline
604
+ */
605
+ async function aggregate(Model, pipeline, options = {}) {
606
+ const aggregation = Model.aggregate(pipeline);
607
+ if (options.session) aggregation.session(options.session);
608
+ return aggregation.exec();
609
+ }
610
+ /**
611
+ * Aggregate with pagination using native MongoDB $facet
612
+ * WARNING: $facet results must be <16MB. For larger results (limit >1000),
613
+ * consider using Repository.aggregatePaginate() or splitting into separate queries.
614
+ */
615
+ async function aggregatePaginate(Model, pipeline, options = {}) {
616
+ const page = parseInt(String(options.page || 1), 10);
617
+ const limit = parseInt(String(options.limit || 10), 10);
618
+ const skip = (page - 1) * limit;
619
+ if (limit > 1e3) warn(`[mongokit] Large aggregation limit (${limit}). $facet results must be <16MB. Consider using Repository.aggregatePaginate() for safer handling of large datasets.`);
620
+ const facetPipeline = [...pipeline, { $facet: {
621
+ docs: [{ $skip: skip }, { $limit: limit }],
622
+ total: [{ $count: "count" }]
623
+ } }];
624
+ const aggregation = Model.aggregate(facetPipeline);
625
+ if (options.session) aggregation.session(options.session);
626
+ const [result] = await aggregation.exec();
627
+ const docs = result.docs || [];
628
+ const total = result.total[0]?.count || 0;
629
+ const pages = Math.ceil(total / limit);
630
+ return {
631
+ docs,
632
+ total,
633
+ page,
634
+ limit,
635
+ pages,
636
+ hasNext: page < pages,
637
+ hasPrev: page > 1
638
+ };
639
+ }
640
+ /**
641
+ * Group documents by field value
642
+ */
643
+ async function groupBy(Model, field, options = {}) {
644
+ const pipeline = [{ $group: {
645
+ _id: `$${field}`,
646
+ count: { $sum: 1 }
647
+ } }, { $sort: { count: -1 } }];
648
+ if (options.limit) pipeline.push({ $limit: options.limit });
649
+ return aggregate(Model, pipeline, options);
650
+ }
651
+ /**
652
+ * Count by field values
653
+ */
654
+ async function countBy(Model, field, query = {}, options = {}) {
655
+ const pipeline = [];
656
+ if (Object.keys(query).length > 0) pipeline.push({ $match: query });
657
+ pipeline.push({ $group: {
658
+ _id: `$${field}`,
659
+ count: { $sum: 1 }
660
+ } }, { $sort: { count: -1 } });
661
+ return aggregate(Model, pipeline, options);
662
+ }
663
+ /**
664
+ * Lookup (join) with another collection
665
+ *
666
+ * MongoDB $lookup has two mutually exclusive forms:
667
+ * 1. Simple form: { from, localField, foreignField, as }
668
+ * 2. Pipeline form: { from, let, pipeline, as }
669
+ *
670
+ * This function automatically selects the appropriate form based on parameters.
671
+ */
672
+ async function lookup(Model, lookupOptions) {
673
+ const { from, localField, foreignField, as, pipeline = [], let: letVars, query = {}, options = {} } = lookupOptions;
674
+ const aggPipeline = [];
675
+ if (Object.keys(query).length > 0) aggPipeline.push({ $match: query });
676
+ if (pipeline.length > 0 || letVars) if (pipeline.length === 0 && localField && foreignField) {
677
+ const autoPipeline = [{ $match: { $expr: { $eq: [`$${foreignField}`, `$$${localField}`] } } }];
678
+ aggPipeline.push({ $lookup: {
679
+ from,
680
+ let: {
681
+ [localField]: `$${localField}`,
682
+ ...letVars || {}
683
+ },
684
+ pipeline: autoPipeline,
685
+ as
686
+ } });
687
+ } else {
688
+ const safePipeline = lookupOptions.sanitize !== false ? LookupBuilder.sanitizePipeline(pipeline) : pipeline;
689
+ aggPipeline.push({ $lookup: {
690
+ from,
691
+ ...letVars && { let: letVars },
692
+ pipeline: safePipeline,
693
+ as
694
+ } });
695
+ }
696
+ else aggPipeline.push({ $lookup: {
697
+ from,
698
+ localField,
699
+ foreignField,
700
+ as
701
+ } });
702
+ return aggregate(Model, aggPipeline, options);
703
+ }
704
+ /**
705
+ * Unwind array field
706
+ */
707
+ async function unwind(Model, field, options = {}) {
708
+ return aggregate(Model, [{ $unwind: {
709
+ path: `$${field}`,
710
+ preserveNullAndEmptyArrays: options.preserveEmpty !== false
711
+ } }], { session: options.session });
712
+ }
713
+ /**
714
+ * Facet search (multiple aggregations in one query)
715
+ */
716
+ async function facet(Model, facets, options = {}) {
717
+ return aggregate(Model, [{ $facet: facets }], options);
718
+ }
719
+ /**
720
+ * Get distinct values
721
+ */
722
+ async function distinct(Model, field, query = {}, options = {}) {
723
+ return Model.distinct(field, query).session(options.session ?? null);
724
+ }
725
+ /**
726
+ * Calculate sum
727
+ */
728
+ async function sum(Model, field, query = {}, options = {}) {
729
+ const pipeline = [];
730
+ if (Object.keys(query).length > 0) pipeline.push({ $match: query });
731
+ pipeline.push({ $group: {
732
+ _id: null,
733
+ total: { $sum: `$${field}` }
734
+ } });
735
+ return (await aggregate(Model, pipeline, options))[0]?.total || 0;
736
+ }
737
+ /**
738
+ * Calculate average
739
+ */
740
+ async function average(Model, field, query = {}, options = {}) {
741
+ const pipeline = [];
742
+ if (Object.keys(query).length > 0) pipeline.push({ $match: query });
743
+ pipeline.push({ $group: {
744
+ _id: null,
745
+ average: { $avg: `$${field}` }
746
+ } });
747
+ return (await aggregate(Model, pipeline, options))[0]?.average || 0;
748
+ }
749
+ /**
750
+ * Min/Max
751
+ */
752
+ async function minMax(Model, field, query = {}, options = {}) {
753
+ const pipeline = [];
754
+ if (Object.keys(query).length > 0) pipeline.push({ $match: query });
755
+ pipeline.push({ $group: {
756
+ _id: null,
757
+ min: { $min: `$${field}` },
758
+ max: { $max: `$${field}` }
759
+ } });
760
+ return (await aggregate(Model, pipeline, options))[0] || {
761
+ min: null,
762
+ max: null
763
+ };
764
+ }
765
+
766
+ //#endregion
767
+ export { deleteById as a, update_exports as c, getById as d, getByQuery as f, LookupBuilder as i, count as l, read_exports as m, aggregate_exports as n, delete_exports as o, getOrCreate as p, distinct as r, update as s, aggregate as t, exists as u };