@classytic/mongokit 3.3.2 → 3.4.1

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 (28) hide show
  1. package/README.md +137 -7
  2. package/dist/PaginationEngine-nY04eGUM.mjs +290 -0
  3. package/dist/actions/index.d.mts +2 -9
  4. package/dist/actions/index.mjs +3 -5
  5. package/dist/ai/index.d.mts +1 -1
  6. package/dist/ai/index.mjs +3 -3
  7. package/dist/chunk-CfYAbeIz.mjs +13 -0
  8. package/dist/{limits-s1-d8rWb.mjs → cursor-CHToazHy.mjs} +122 -171
  9. package/dist/{logger-D8ily-PP.mjs → error-Bpbi_NKo.mjs} +34 -22
  10. package/dist/{cache-keys-CzFwVnLy.mjs → field-selection-reyDRzXf.mjs} +110 -112
  11. package/dist/{aggregate-BkOG9qwr.d.mts → index-BuoZIZ15.d.mts} +132 -129
  12. package/dist/index.d.mts +549 -543
  13. package/dist/index.mjs +33 -101
  14. package/dist/{mongooseToJsonSchema-D_i2Am_O.mjs → mongooseToJsonSchema-B6Qyl8BK.mjs} +13 -12
  15. package/dist/{mongooseToJsonSchema-B6O2ED3n.d.mts → mongooseToJsonSchema-RX9YfJLu.d.mts} +24 -17
  16. package/dist/pagination/PaginationEngine.d.mts +1 -1
  17. package/dist/pagination/PaginationEngine.mjs +2 -209
  18. package/dist/plugins/index.d.mts +1 -2
  19. package/dist/plugins/index.mjs +2 -3
  20. package/dist/sort-C-BJEWUZ.mjs +57 -0
  21. package/dist/{types-pVY0w1Pp.d.mts → types-COINbsdL.d.mts} +57 -27
  22. package/dist/{aggregate-BClp040M.mjs → update-DGKMmBgG.mjs} +575 -565
  23. package/dist/utils/index.d.mts +2 -2
  24. package/dist/utils/index.mjs +4 -5
  25. package/dist/{custom-id.plugin-BJ3FSnzt.d.mts → validation-chain.plugin-BNoaKDOm.d.mts} +832 -832
  26. package/dist/{custom-id.plugin-FInXDsUX.mjs → validation-chain.plugin-da3fOo8A.mjs} +2410 -2246
  27. package/package.json +11 -6
  28. package/dist/chunk-DQk6qfdC.mjs +0 -18
@@ -1,412 +1,5 @@
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/create.ts
5
- var create_exports = /* @__PURE__ */ __exportAll({
6
- create: () => create,
7
- createDefault: () => createDefault,
8
- createMany: () => createMany,
9
- upsert: () => upsert
10
- });
11
- /**
12
- * Create single document
13
- */
14
- async function create(Model, data, options = {}) {
15
- const document = new Model(data);
16
- await document.save({ session: options.session });
17
- return document;
18
- }
19
- /**
20
- * Create multiple documents
21
- */
22
- async function createMany(Model, dataArray, options = {}) {
23
- return Model.insertMany(dataArray, {
24
- session: options.session,
25
- ordered: options.ordered !== false
26
- });
27
- }
28
- /**
29
- * Create with defaults (useful for initialization)
30
- */
31
- async function createDefault(Model, overrides = {}, options = {}) {
32
- const defaults = {};
33
- Model.schema.eachPath((path, schemaType) => {
34
- const schemaOptions = schemaType.options;
35
- if (schemaOptions.default !== void 0 && path !== "_id") defaults[path] = typeof schemaOptions.default === "function" ? schemaOptions.default() : schemaOptions.default;
36
- });
37
- return create(Model, {
38
- ...defaults,
39
- ...overrides
40
- }, options);
41
- }
42
- /**
43
- * Upsert (create or update)
44
- */
45
- async function upsert(Model, query, data, options = {}) {
46
- return Model.findOneAndUpdate(query, { $setOnInsert: data }, {
47
- upsert: true,
48
- returnDocument: "after",
49
- runValidators: true,
50
- session: options.session,
51
- ...options.updatePipeline !== void 0 ? { updatePipeline: options.updatePipeline } : {}
52
- });
53
- }
54
-
55
- //#endregion
56
- //#region src/actions/read.ts
57
- var read_exports = /* @__PURE__ */ __exportAll({
58
- count: () => count,
59
- exists: () => exists,
60
- getAll: () => getAll,
61
- getById: () => getById,
62
- getByQuery: () => getByQuery,
63
- getOrCreate: () => getOrCreate,
64
- tryGetByQuery: () => tryGetByQuery
65
- });
66
- /**
67
- * Parse populate specification into consistent format
68
- */
69
- function parsePopulate$1(populate) {
70
- if (!populate) return [];
71
- if (typeof populate === "string") return populate.split(",").map((p) => p.trim());
72
- if (Array.isArray(populate)) return populate.map((p) => typeof p === "string" ? p.trim() : p);
73
- return [populate];
74
- }
75
- /**
76
- * Get document by ID
77
- *
78
- * @param Model - Mongoose model
79
- * @param id - Document ID
80
- * @param options - Query options
81
- * @returns Document or null
82
- * @throws Error if document not found and throwOnNotFound is true
83
- */
84
- async function getById(Model, id, options = {}) {
85
- const query = options.query ? Model.findOne({
86
- _id: id,
87
- ...options.query
88
- }) : Model.findById(id);
89
- if (options.select) query.select(options.select);
90
- if (options.populate) query.populate(parsePopulate$1(options.populate));
91
- if (options.lean) query.lean();
92
- if (options.session) query.session(options.session);
93
- if (options.readPreference) query.read(options.readPreference);
94
- const document = await query.exec();
95
- if (!document && options.throwOnNotFound !== false) throw createError(404, "Document not found");
96
- return document;
97
- }
98
- /**
99
- * Get document by query
100
- *
101
- * @param Model - Mongoose model
102
- * @param query - MongoDB query
103
- * @param options - Query options
104
- * @returns Document or null
105
- * @throws Error if document not found and throwOnNotFound is true
106
- */
107
- async function getByQuery(Model, query, options = {}) {
108
- const mongoQuery = Model.findOne(query);
109
- if (options.select) mongoQuery.select(options.select);
110
- if (options.populate) mongoQuery.populate(parsePopulate$1(options.populate));
111
- if (options.lean) mongoQuery.lean();
112
- if (options.session) mongoQuery.session(options.session);
113
- if (options.readPreference) mongoQuery.read(options.readPreference);
114
- const document = await mongoQuery.exec();
115
- if (!document && options.throwOnNotFound !== false) throw createError(404, "Document not found");
116
- return document;
117
- }
118
- /**
119
- * Get document by query without throwing (returns null if not found)
120
- */
121
- async function tryGetByQuery(Model, query, options = {}) {
122
- return getByQuery(Model, query, {
123
- ...options,
124
- throwOnNotFound: false
125
- });
126
- }
127
- /**
128
- * Get all documents (basic query without pagination)
129
- * For pagination, use Repository.paginate() or Repository.stream()
130
- */
131
- async function getAll(Model, query = {}, options = {}) {
132
- let mongoQuery = Model.find(query);
133
- if (options.select) mongoQuery = mongoQuery.select(options.select);
134
- if (options.populate) mongoQuery = mongoQuery.populate(parsePopulate$1(options.populate));
135
- if (options.sort) mongoQuery = mongoQuery.sort(options.sort);
136
- if (options.limit) mongoQuery = mongoQuery.limit(options.limit);
137
- if (options.skip) mongoQuery = mongoQuery.skip(options.skip);
138
- mongoQuery = mongoQuery.lean(options.lean !== false);
139
- if (options.session) mongoQuery = mongoQuery.session(options.session);
140
- if (options.readPreference) mongoQuery = mongoQuery.read(options.readPreference);
141
- return mongoQuery.exec();
142
- }
143
- /**
144
- * Get or create document (upsert)
145
- */
146
- async function getOrCreate(Model, query, createData, options = {}) {
147
- return Model.findOneAndUpdate(query, { $setOnInsert: createData }, {
148
- upsert: true,
149
- returnDocument: "after",
150
- runValidators: true,
151
- session: options.session,
152
- ...options.updatePipeline !== void 0 ? { updatePipeline: options.updatePipeline } : {}
153
- });
154
- }
155
- /**
156
- * Count documents matching query
157
- */
158
- async function count(Model, query = {}, options = {}) {
159
- const q = Model.countDocuments(query).session(options.session ?? null);
160
- if (options.readPreference) q.read(options.readPreference);
161
- return q;
162
- }
163
- /**
164
- * Check if document exists
165
- */
166
- async function exists(Model, query, options = {}) {
167
- const q = Model.exists(query).session(options.session ?? null);
168
- if (options.readPreference) q.read(options.readPreference);
169
- return q;
170
- }
171
-
172
- //#endregion
173
- //#region src/actions/update.ts
174
- var update_exports = /* @__PURE__ */ __exportAll({
175
- increment: () => increment,
176
- pullFromArray: () => pullFromArray,
177
- pushToArray: () => pushToArray,
178
- update: () => update,
179
- updateByQuery: () => updateByQuery,
180
- updateMany: () => updateMany,
181
- updateWithConstraints: () => updateWithConstraints,
182
- updateWithValidation: () => updateWithValidation
183
- });
184
- function assertUpdatePipelineAllowed(update, updatePipeline) {
185
- 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.");
186
- }
187
- /**
188
- * Parse populate specification into consistent format
189
- */
190
- function parsePopulate(populate) {
191
- if (!populate) return [];
192
- if (typeof populate === "string") return populate.split(",").map((p) => p.trim());
193
- if (Array.isArray(populate)) return populate.map((p) => typeof p === "string" ? p.trim() : p);
194
- return [populate];
195
- }
196
- /**
197
- * Update by ID
198
- */
199
- async function update(Model, id, data, options = {}) {
200
- assertUpdatePipelineAllowed(data, options.updatePipeline);
201
- const query = {
202
- _id: id,
203
- ...options.query
204
- };
205
- const document = await Model.findOneAndUpdate(query, data, {
206
- returnDocument: "after",
207
- runValidators: true,
208
- session: options.session,
209
- ...options.updatePipeline !== void 0 ? { updatePipeline: options.updatePipeline } : {},
210
- ...options.arrayFilters ? { arrayFilters: options.arrayFilters } : {}
211
- }).select(options.select || "").populate(parsePopulate(options.populate)).lean(options.lean ?? false);
212
- if (!document) throw createError(404, "Document not found");
213
- return document;
214
- }
215
- /**
216
- * Update with query constraints (optimized)
217
- * Returns null if constraints not met (not an error)
218
- */
219
- async function updateWithConstraints(Model, id, data, constraints = {}, options = {}) {
220
- assertUpdatePipelineAllowed(data, options.updatePipeline);
221
- const query = {
222
- _id: id,
223
- ...constraints
224
- };
225
- return await Model.findOneAndUpdate(query, data, {
226
- returnDocument: "after",
227
- runValidators: true,
228
- session: options.session,
229
- ...options.updatePipeline !== void 0 ? { updatePipeline: options.updatePipeline } : {},
230
- ...options.arrayFilters ? { arrayFilters: options.arrayFilters } : {}
231
- }).select(options.select || "").populate(parsePopulate(options.populate)).lean(options.lean ?? false);
232
- }
233
- /**
234
- * Update with validation (smart optimization)
235
- * 1-query on success, 2-queries for detailed errors
236
- */
237
- async function updateWithValidation(Model, id, data, validationOptions = {}, options = {}) {
238
- const { buildConstraints, validateUpdate } = validationOptions;
239
- assertUpdatePipelineAllowed(data, options.updatePipeline);
240
- if (buildConstraints) {
241
- const document = await updateWithConstraints(Model, id, data, buildConstraints(data), options);
242
- if (document) return {
243
- success: true,
244
- data: document
245
- };
246
- }
247
- const findQuery = {
248
- _id: id,
249
- ...options.query
250
- };
251
- const existing = await Model.findOne(findQuery).select(options.select || "").session(options.session ?? null).lean();
252
- if (!existing) return {
253
- success: false,
254
- error: {
255
- code: 404,
256
- message: "Document not found"
257
- }
258
- };
259
- if (validateUpdate) {
260
- const validation = validateUpdate(existing, data);
261
- if (!validation.valid) return {
262
- success: false,
263
- error: {
264
- code: 403,
265
- message: validation.message || "Update not allowed",
266
- violations: validation.violations
267
- }
268
- };
269
- }
270
- return {
271
- success: true,
272
- data: await update(Model, id, data, options)
273
- };
274
- }
275
- /**
276
- * Update many documents
277
- */
278
- async function updateMany(Model, query, data, options = {}) {
279
- assertUpdatePipelineAllowed(data, options.updatePipeline);
280
- const result = await Model.updateMany(query, data, {
281
- runValidators: true,
282
- session: options.session,
283
- ...options.updatePipeline !== void 0 ? { updatePipeline: options.updatePipeline } : {},
284
- ...options.arrayFilters ? { arrayFilters: options.arrayFilters } : {}
285
- });
286
- return {
287
- matchedCount: result.matchedCount,
288
- modifiedCount: result.modifiedCount
289
- };
290
- }
291
- /**
292
- * Update by query
293
- */
294
- async function updateByQuery(Model, query, data, options = {}) {
295
- assertUpdatePipelineAllowed(data, options.updatePipeline);
296
- const document = await Model.findOneAndUpdate(query, data, {
297
- returnDocument: "after",
298
- runValidators: true,
299
- session: options.session,
300
- ...options.updatePipeline !== void 0 ? { updatePipeline: options.updatePipeline } : {},
301
- ...options.arrayFilters ? { arrayFilters: options.arrayFilters } : {}
302
- }).select(options.select || "").populate(parsePopulate(options.populate)).lean(options.lean ?? false);
303
- if (!document && options.throwOnNotFound !== false) throw createError(404, "Document not found");
304
- return document;
305
- }
306
- /**
307
- * Increment field
308
- */
309
- async function increment(Model, id, field, value = 1, options = {}) {
310
- return update(Model, id, { $inc: { [field]: value } }, options);
311
- }
312
- /**
313
- * Push to array
314
- */
315
- async function pushToArray(Model, id, field, value, options = {}) {
316
- return update(Model, id, { $push: { [field]: value } }, options);
317
- }
318
- /**
319
- * Pull from array
320
- */
321
- async function pullFromArray(Model, id, field, value, options = {}) {
322
- return update(Model, id, { $pull: { [field]: value } }, options);
323
- }
324
-
325
- //#endregion
326
- //#region src/actions/delete.ts
327
- var delete_exports = /* @__PURE__ */ __exportAll({
328
- deleteById: () => deleteById,
329
- deleteByQuery: () => deleteByQuery,
330
- deleteMany: () => deleteMany,
331
- restore: () => restore,
332
- softDelete: () => softDelete
333
- });
334
- /**
335
- * Delete by ID
336
- */
337
- async function deleteById(Model, id, options = {}) {
338
- const query = {
339
- _id: id,
340
- ...options.query
341
- };
342
- if (!await Model.findOneAndDelete(query).session(options.session ?? null)) throw createError(404, "Document not found");
343
- return {
344
- success: true,
345
- message: "Deleted successfully",
346
- id: String(id)
347
- };
348
- }
349
- /**
350
- * Delete many documents
351
- */
352
- async function deleteMany(Model, query, options = {}) {
353
- return {
354
- success: true,
355
- count: (await Model.deleteMany(query).session(options.session ?? null)).deletedCount,
356
- message: "Deleted successfully"
357
- };
358
- }
359
- /**
360
- * Delete by query
361
- */
362
- async function deleteByQuery(Model, query, options = {}) {
363
- const document = await Model.findOneAndDelete(query).session(options.session ?? null);
364
- if (!document && options.throwOnNotFound !== false) throw createError(404, "Document not found");
365
- return {
366
- success: true,
367
- message: "Deleted successfully",
368
- ...document ? { id: String(document._id) } : {}
369
- };
370
- }
371
- /**
372
- * Soft delete (set deleted flag)
373
- */
374
- async function softDelete(Model, id, options = {}) {
375
- if (!await Model.findByIdAndUpdate(id, {
376
- deleted: true,
377
- deletedAt: /* @__PURE__ */ new Date(),
378
- deletedBy: options.userId
379
- }, {
380
- returnDocument: "after",
381
- session: options.session
382
- })) throw createError(404, "Document not found");
383
- return {
384
- success: true,
385
- message: "Soft deleted successfully",
386
- id: String(id),
387
- soft: true
388
- };
389
- }
390
- /**
391
- * Restore soft deleted document
392
- */
393
- async function restore(Model, id, options = {}) {
394
- if (!await Model.findByIdAndUpdate(id, {
395
- deleted: false,
396
- deletedAt: null,
397
- deletedBy: null
398
- }, {
399
- returnDocument: "after",
400
- session: options.session
401
- })) throw createError(404, "Document not found");
402
- return {
403
- success: true,
404
- message: "Restored successfully",
405
- id: String(id)
406
- };
407
- }
408
-
409
- //#endregion
1
+ import { t as __exportAll } from "./chunk-CfYAbeIz.mjs";
2
+ import { a as warn, t as createError } from "./error-Bpbi_NKo.mjs";
410
3
  //#region src/query/LookupBuilder.ts
411
4
  /** Stages that are never valid inside a $lookup pipeline */
412
5
  const BLOCKED_PIPELINE_STAGES = [
@@ -417,12 +10,14 @@ const BLOCKED_PIPELINE_STAGES = [
417
10
  "$currentOp",
418
11
  "$listSessions"
419
12
  ];
420
- /** Operators that can enable arbitrary code execution */
13
+ /** Operators that can enable arbitrary code execution.
14
+ * Note: $expr is intentionally NOT blocked — it's needed for pipeline-form
15
+ * $lookup correlations (let + $match.$expr) and is a comparison operator,
16
+ * not a code execution vector like $where/$function/$accumulator. */
421
17
  const DANGEROUS_OPERATORS = [
422
18
  "$where",
423
19
  "$function",
424
- "$accumulator",
425
- "$expr"
20
+ "$accumulator"
426
21
  ];
427
22
  /**
428
23
  * Fluent builder for MongoDB $lookup aggregation stage
@@ -498,6 +93,14 @@ var LookupBuilder = class LookupBuilder {
498
93
  return this;
499
94
  }
500
95
  /**
96
+ * Control pipeline sanitization (default: true)
97
+ * Set to false for auto-generated pipelines that are known safe
98
+ */
99
+ sanitize(enabled) {
100
+ this.options.sanitize = enabled;
101
+ return this;
102
+ }
103
+ /**
501
104
  * Build the $lookup aggregation stage(s)
502
105
  * Returns an array of pipeline stages including $lookup and optional $unwind
503
106
  *
@@ -529,10 +132,16 @@ var LookupBuilder = class LookupBuilder {
529
132
  } };
530
133
  } else {
531
134
  const safePipeline = this.options.sanitize !== false ? LookupBuilder.sanitizePipeline(pipeline) : pipeline;
135
+ let effectiveLet = letVars;
136
+ let effectivePipeline = safePipeline;
137
+ if (localField && foreignField && !letVars) {
138
+ effectiveLet = { lookupJoinVal: `$${localField}` };
139
+ effectivePipeline = [{ $match: { $expr: { $eq: [`$${foreignField}`, "$$lookupJoinVal"] } } }, ...safePipeline];
140
+ }
532
141
  lookupStage = { $lookup: {
533
142
  from,
534
- ...letVars && { let: letVars },
535
- pipeline: safePipeline,
143
+ ...effectiveLet && { let: effectiveLet },
144
+ pipeline: effectivePipeline,
536
145
  as: outputField
537
146
  } };
538
147
  }
@@ -581,8 +190,29 @@ var LookupBuilder = class LookupBuilder {
581
190
  const builder = new LookupBuilder(lookup.from).localField(lookup.localField).foreignField(lookup.foreignField);
582
191
  if (lookup.as) builder.as(lookup.as);
583
192
  if (lookup.single) builder.single(lookup.single);
584
- if (lookup.pipeline) builder.pipeline(lookup.pipeline);
585
- if (lookup.let) builder.let(lookup.let);
193
+ if (lookup.select) {
194
+ let projection;
195
+ if (typeof lookup.select === "string") {
196
+ projection = {};
197
+ for (const field of lookup.select.split(",").map((f) => f.trim())) if (field.startsWith("-")) projection[field.substring(1)] = 0;
198
+ else projection[field] = 1;
199
+ } else projection = lookup.select;
200
+ const joinStage = { $match: { $expr: { $eq: [`$${lookup.foreignField}`, "$$lookupJoinVal"] } } };
201
+ const existing = lookup.pipeline || [];
202
+ builder.pipeline([
203
+ joinStage,
204
+ ...existing,
205
+ { $project: projection }
206
+ ]);
207
+ builder.let({
208
+ lookupJoinVal: `$${lookup.localField}`,
209
+ ...lookup.let || {}
210
+ });
211
+ builder.sanitize(false);
212
+ } else if (lookup.pipeline) {
213
+ builder.pipeline(lookup.pipeline);
214
+ if (lookup.let) builder.let(lookup.let);
215
+ } else if (lookup.let) builder.let(lookup.let);
586
216
  return builder.build();
587
217
  });
588
218
  }
@@ -600,7 +230,7 @@ var LookupBuilder = class LookupBuilder {
600
230
  * ```
601
231
  */
602
232
  static nested(lookups) {
603
- return lookups.flatMap((lookup, index) => {
233
+ return lookups.flatMap((lookup, _index) => {
604
234
  const builder = new LookupBuilder(lookup.from).localField(lookup.localField).foreignField(lookup.foreignField);
605
235
  if (lookup.as) builder.as(lookup.as);
606
236
  if (lookup.single !== void 0) builder.single(lookup.single);
@@ -649,187 +279,567 @@ var LookupBuilder = class LookupBuilder {
649
279
  return sanitized;
650
280
  }
651
281
  };
652
-
653
282
  //#endregion
654
- //#region src/actions/aggregate.ts
655
- var aggregate_exports = /* @__PURE__ */ __exportAll({
656
- aggregate: () => aggregate,
657
- aggregatePaginate: () => aggregatePaginate,
658
- average: () => average,
659
- countBy: () => countBy,
660
- distinct: () => distinct,
661
- facet: () => facet,
662
- groupBy: () => groupBy,
663
- lookup: () => lookup,
664
- minMax: () => minMax,
665
- sum: () => sum,
666
- unwind: () => unwind
283
+ //#region src/actions/aggregate.ts
284
+ var aggregate_exports = /* @__PURE__ */ __exportAll({
285
+ aggregate: () => aggregate,
286
+ aggregatePaginate: () => aggregatePaginate,
287
+ average: () => average,
288
+ countBy: () => countBy,
289
+ distinct: () => distinct,
290
+ facet: () => facet,
291
+ groupBy: () => groupBy,
292
+ lookup: () => lookup,
293
+ minMax: () => minMax,
294
+ sum: () => sum,
295
+ unwind: () => unwind
296
+ });
297
+ /**
298
+ * Execute aggregation pipeline
299
+ */
300
+ async function aggregate(Model, pipeline, options = {}) {
301
+ const aggregation = Model.aggregate(pipeline);
302
+ if (options.session) aggregation.session(options.session);
303
+ return aggregation.exec();
304
+ }
305
+ /**
306
+ * Aggregate with pagination using native MongoDB $facet
307
+ * WARNING: $facet results must be <16MB. For larger results (limit >1000),
308
+ * consider using Repository.aggregatePaginate() or splitting into separate queries.
309
+ */
310
+ async function aggregatePaginate(Model, pipeline, options = {}) {
311
+ const page = parseInt(String(options.page || 1), 10);
312
+ const limit = parseInt(String(options.limit || 10), 10);
313
+ const skip = (page - 1) * limit;
314
+ if (limit > 1e3) warn(`[mongokit] Large aggregation limit (${limit}). $facet results must be <16MB. Consider using Repository.aggregatePaginate() for safer handling of large datasets.`);
315
+ const facetPipeline = [...pipeline, { $facet: {
316
+ docs: [{ $skip: skip }, { $limit: limit }],
317
+ total: [{ $count: "count" }]
318
+ } }];
319
+ const aggregation = Model.aggregate(facetPipeline);
320
+ if (options.session) aggregation.session(options.session);
321
+ const [result] = await aggregation.exec();
322
+ const docs = result.docs || [];
323
+ const total = result.total[0]?.count || 0;
324
+ const pages = Math.ceil(total / limit);
325
+ return {
326
+ docs,
327
+ total,
328
+ page,
329
+ limit,
330
+ pages,
331
+ hasNext: page < pages,
332
+ hasPrev: page > 1
333
+ };
334
+ }
335
+ /**
336
+ * Group documents by field value
337
+ */
338
+ async function groupBy(Model, field, options = {}) {
339
+ const pipeline = [{ $group: {
340
+ _id: `$${field}`,
341
+ count: { $sum: 1 }
342
+ } }, { $sort: { count: -1 } }];
343
+ if (options.limit) pipeline.push({ $limit: options.limit });
344
+ return aggregate(Model, pipeline, options);
345
+ }
346
+ /**
347
+ * Count by field values
348
+ */
349
+ async function countBy(Model, field, query = {}, options = {}) {
350
+ const pipeline = [];
351
+ if (Object.keys(query).length > 0) pipeline.push({ $match: query });
352
+ pipeline.push({ $group: {
353
+ _id: `$${field}`,
354
+ count: { $sum: 1 }
355
+ } }, { $sort: { count: -1 } });
356
+ return aggregate(Model, pipeline, options);
357
+ }
358
+ /**
359
+ * Lookup (join) with another collection
360
+ *
361
+ * MongoDB $lookup has two mutually exclusive forms:
362
+ * 1. Simple form: { from, localField, foreignField, as }
363
+ * 2. Pipeline form: { from, let, pipeline, as }
364
+ *
365
+ * This function automatically selects the appropriate form based on parameters.
366
+ */
367
+ async function lookup(Model, lookupOptions) {
368
+ const { from, localField, foreignField, as, pipeline = [], let: letVars, query = {}, options = {} } = lookupOptions;
369
+ const aggPipeline = [];
370
+ if (Object.keys(query).length > 0) aggPipeline.push({ $match: query });
371
+ const builder = new LookupBuilder(from).localField(localField).foreignField(foreignField).as(as || from);
372
+ if (lookupOptions.single) builder.single(lookupOptions.single);
373
+ if (pipeline.length > 0) builder.pipeline(pipeline);
374
+ if (letVars) builder.let(letVars);
375
+ if (lookupOptions.sanitize === false) builder.sanitize(false);
376
+ aggPipeline.push(...builder.build());
377
+ return aggregate(Model, aggPipeline, options);
378
+ }
379
+ /**
380
+ * Unwind array field
381
+ */
382
+ async function unwind(Model, field, options = {}) {
383
+ return aggregate(Model, [{ $unwind: {
384
+ path: `$${field}`,
385
+ preserveNullAndEmptyArrays: options.preserveEmpty !== false
386
+ } }], { session: options.session });
387
+ }
388
+ /**
389
+ * Facet search (multiple aggregations in one query)
390
+ */
391
+ async function facet(Model, facets, options = {}) {
392
+ return aggregate(Model, [{ $facet: facets }], options);
393
+ }
394
+ /**
395
+ * Get distinct values
396
+ */
397
+ async function distinct(Model, field, query = {}, options = {}) {
398
+ const q = Model.distinct(field, query).session(options.session ?? null);
399
+ if (options.readPreference) q.read(options.readPreference);
400
+ return q;
401
+ }
402
+ /**
403
+ * Calculate sum
404
+ */
405
+ async function sum(Model, field, query = {}, options = {}) {
406
+ const pipeline = [];
407
+ if (Object.keys(query).length > 0) pipeline.push({ $match: query });
408
+ pipeline.push({ $group: {
409
+ _id: null,
410
+ total: { $sum: `$${field}` }
411
+ } });
412
+ return (await aggregate(Model, pipeline, options))[0]?.total || 0;
413
+ }
414
+ /**
415
+ * Calculate average
416
+ */
417
+ async function average(Model, field, query = {}, options = {}) {
418
+ const pipeline = [];
419
+ if (Object.keys(query).length > 0) pipeline.push({ $match: query });
420
+ pipeline.push({ $group: {
421
+ _id: null,
422
+ average: { $avg: `$${field}` }
423
+ } });
424
+ return (await aggregate(Model, pipeline, options))[0]?.average || 0;
425
+ }
426
+ /**
427
+ * Min/Max
428
+ */
429
+ async function minMax(Model, field, query = {}, options = {}) {
430
+ const pipeline = [];
431
+ if (Object.keys(query).length > 0) pipeline.push({ $match: query });
432
+ pipeline.push({ $group: {
433
+ _id: null,
434
+ min: { $min: `$${field}` },
435
+ max: { $max: `$${field}` }
436
+ } });
437
+ return (await aggregate(Model, pipeline, options))[0] || {
438
+ min: null,
439
+ max: null
440
+ };
441
+ }
442
+ //#endregion
443
+ //#region src/actions/create.ts
444
+ var create_exports = /* @__PURE__ */ __exportAll({
445
+ create: () => create,
446
+ createDefault: () => createDefault,
447
+ createMany: () => createMany,
448
+ upsert: () => upsert
449
+ });
450
+ /**
451
+ * Create single document
452
+ */
453
+ async function create(Model, data, options = {}) {
454
+ const document = new Model(data);
455
+ await document.save({ session: options.session });
456
+ return document;
457
+ }
458
+ /**
459
+ * Create multiple documents
460
+ */
461
+ async function createMany(Model, dataArray, options = {}) {
462
+ return Model.insertMany(dataArray, {
463
+ session: options.session,
464
+ ordered: options.ordered !== false
465
+ });
466
+ }
467
+ /**
468
+ * Create with defaults (useful for initialization)
469
+ */
470
+ async function createDefault(Model, overrides = {}, options = {}) {
471
+ const defaults = {};
472
+ Model.schema.eachPath((path, schemaType) => {
473
+ const schemaOptions = schemaType.options;
474
+ if (schemaOptions.default !== void 0 && path !== "_id") defaults[path] = typeof schemaOptions.default === "function" ? schemaOptions.default() : schemaOptions.default;
475
+ });
476
+ return create(Model, {
477
+ ...defaults,
478
+ ...overrides
479
+ }, options);
480
+ }
481
+ /**
482
+ * Upsert (create or update)
483
+ */
484
+ async function upsert(Model, query, data, options = {}) {
485
+ return Model.findOneAndUpdate(query, { $setOnInsert: data }, {
486
+ upsert: true,
487
+ returnDocument: "after",
488
+ runValidators: true,
489
+ session: options.session,
490
+ ...options.updatePipeline !== void 0 ? { updatePipeline: options.updatePipeline } : {}
491
+ });
492
+ }
493
+ //#endregion
494
+ //#region src/actions/delete.ts
495
+ var delete_exports = /* @__PURE__ */ __exportAll({
496
+ deleteById: () => deleteById,
497
+ deleteByQuery: () => deleteByQuery,
498
+ deleteMany: () => deleteMany,
499
+ restore: () => restore,
500
+ softDelete: () => softDelete
501
+ });
502
+ /**
503
+ * Delete by ID
504
+ */
505
+ async function deleteById(Model, id, options = {}) {
506
+ const query = {
507
+ _id: id,
508
+ ...options.query
509
+ };
510
+ if (!await Model.findOneAndDelete(query).session(options.session ?? null)) throw createError(404, "Document not found");
511
+ return {
512
+ success: true,
513
+ message: "Deleted successfully",
514
+ id: String(id)
515
+ };
516
+ }
517
+ /**
518
+ * Delete many documents
519
+ */
520
+ async function deleteMany(Model, query, options = {}) {
521
+ return {
522
+ success: true,
523
+ count: (await Model.deleteMany(query).session(options.session ?? null)).deletedCount,
524
+ message: "Deleted successfully"
525
+ };
526
+ }
527
+ /**
528
+ * Delete by query
529
+ */
530
+ async function deleteByQuery(Model, query, options = {}) {
531
+ const document = await Model.findOneAndDelete(query).session(options.session ?? null);
532
+ if (!document && options.throwOnNotFound !== false) throw createError(404, "Document not found");
533
+ return {
534
+ success: true,
535
+ message: "Deleted successfully",
536
+ ...document ? { id: String(document._id) } : {}
537
+ };
538
+ }
539
+ /**
540
+ * Soft delete (set deleted flag)
541
+ */
542
+ async function softDelete(Model, id, options = {}) {
543
+ if (!await Model.findByIdAndUpdate(id, {
544
+ deleted: true,
545
+ deletedAt: /* @__PURE__ */ new Date(),
546
+ deletedBy: options.userId
547
+ }, {
548
+ returnDocument: "after",
549
+ session: options.session
550
+ })) throw createError(404, "Document not found");
551
+ return {
552
+ success: true,
553
+ message: "Soft deleted successfully",
554
+ id: String(id),
555
+ soft: true
556
+ };
557
+ }
558
+ /**
559
+ * Restore soft deleted document
560
+ */
561
+ async function restore(Model, id, options = {}) {
562
+ if (!await Model.findByIdAndUpdate(id, {
563
+ deleted: false,
564
+ deletedAt: null,
565
+ deletedBy: null
566
+ }, {
567
+ returnDocument: "after",
568
+ session: options.session
569
+ })) throw createError(404, "Document not found");
570
+ return {
571
+ success: true,
572
+ message: "Restored successfully",
573
+ id: String(id)
574
+ };
575
+ }
576
+ //#endregion
577
+ //#region src/actions/read.ts
578
+ var read_exports = /* @__PURE__ */ __exportAll({
579
+ count: () => count,
580
+ exists: () => exists,
581
+ getAll: () => getAll,
582
+ getById: () => getById,
583
+ getByQuery: () => getByQuery,
584
+ getOrCreate: () => getOrCreate,
585
+ tryGetByQuery: () => tryGetByQuery
667
586
  });
668
587
  /**
669
- * Execute aggregation pipeline
588
+ * Parse populate specification into consistent format
589
+ */
590
+ function parsePopulate$1(populate) {
591
+ if (!populate) return [];
592
+ if (typeof populate === "string") return populate.split(",").map((p) => p.trim());
593
+ if (Array.isArray(populate)) return populate.map((p) => typeof p === "string" ? p.trim() : p);
594
+ return [populate];
595
+ }
596
+ /**
597
+ * Get document by ID
598
+ *
599
+ * @param Model - Mongoose model
600
+ * @param id - Document ID
601
+ * @param options - Query options
602
+ * @returns Document or null
603
+ * @throws Error if document not found and throwOnNotFound is true
604
+ */
605
+ async function getById(Model, id, options = {}) {
606
+ const query = options.query ? Model.findOne({
607
+ _id: id,
608
+ ...options.query
609
+ }) : Model.findById(id);
610
+ if (options.select) query.select(options.select);
611
+ if (options.populate) query.populate(parsePopulate$1(options.populate));
612
+ if (options.lean) query.lean();
613
+ if (options.session) query.session(options.session);
614
+ if (options.readPreference) query.read(options.readPreference);
615
+ const document = await query.exec();
616
+ if (!document && options.throwOnNotFound !== false) throw createError(404, "Document not found");
617
+ return document;
618
+ }
619
+ /**
620
+ * Get document by query
621
+ *
622
+ * @param Model - Mongoose model
623
+ * @param query - MongoDB query
624
+ * @param options - Query options
625
+ * @returns Document or null
626
+ * @throws Error if document not found and throwOnNotFound is true
627
+ */
628
+ async function getByQuery(Model, query, options = {}) {
629
+ const mongoQuery = Model.findOne(query);
630
+ if (options.select) mongoQuery.select(options.select);
631
+ if (options.populate) mongoQuery.populate(parsePopulate$1(options.populate));
632
+ if (options.lean) mongoQuery.lean();
633
+ if (options.session) mongoQuery.session(options.session);
634
+ if (options.readPreference) mongoQuery.read(options.readPreference);
635
+ const document = await mongoQuery.exec();
636
+ if (!document && options.throwOnNotFound !== false) throw createError(404, "Document not found");
637
+ return document;
638
+ }
639
+ /**
640
+ * Get document by query without throwing (returns null if not found)
641
+ */
642
+ async function tryGetByQuery(Model, query, options = {}) {
643
+ return getByQuery(Model, query, {
644
+ ...options,
645
+ throwOnNotFound: false
646
+ });
647
+ }
648
+ /**
649
+ * Get all documents (basic query without pagination)
650
+ * For pagination, use Repository.paginate() or Repository.stream()
651
+ */
652
+ async function getAll(Model, query = {}, options = {}) {
653
+ let mongoQuery = Model.find(query);
654
+ if (options.select) mongoQuery = mongoQuery.select(options.select);
655
+ if (options.populate) mongoQuery = mongoQuery.populate(parsePopulate$1(options.populate));
656
+ if (options.sort) mongoQuery = mongoQuery.sort(options.sort);
657
+ if (options.limit) mongoQuery = mongoQuery.limit(options.limit);
658
+ if (options.skip) mongoQuery = mongoQuery.skip(options.skip);
659
+ if (options.lean !== false) mongoQuery = mongoQuery.lean();
660
+ if (options.session) mongoQuery = mongoQuery.session(options.session);
661
+ if (options.readPreference) mongoQuery = mongoQuery.read(options.readPreference);
662
+ return mongoQuery.exec();
663
+ }
664
+ /**
665
+ * Get or create document (upsert)
666
+ */
667
+ async function getOrCreate(Model, query, createData, options = {}) {
668
+ return Model.findOneAndUpdate(query, { $setOnInsert: createData }, {
669
+ upsert: true,
670
+ returnDocument: "after",
671
+ runValidators: true,
672
+ session: options.session,
673
+ ...options.updatePipeline !== void 0 ? { updatePipeline: options.updatePipeline } : {}
674
+ });
675
+ }
676
+ /**
677
+ * Count documents matching query
670
678
  */
671
- async function aggregate(Model, pipeline, options = {}) {
672
- const aggregation = Model.aggregate(pipeline);
673
- if (options.session) aggregation.session(options.session);
674
- return aggregation.exec();
679
+ async function count(Model, query = {}, options = {}) {
680
+ const q = Model.countDocuments(query).session(options.session ?? null);
681
+ if (options.readPreference) q.read(options.readPreference);
682
+ return q;
675
683
  }
676
684
  /**
677
- * Aggregate with pagination using native MongoDB $facet
678
- * WARNING: $facet results must be <16MB. For larger results (limit >1000),
679
- * consider using Repository.aggregatePaginate() or splitting into separate queries.
685
+ * Check if document exists
680
686
  */
681
- async function aggregatePaginate(Model, pipeline, options = {}) {
682
- const page = parseInt(String(options.page || 1), 10);
683
- const limit = parseInt(String(options.limit || 10), 10);
684
- const skip = (page - 1) * limit;
685
- if (limit > 1e3) warn(`[mongokit] Large aggregation limit (${limit}). $facet results must be <16MB. Consider using Repository.aggregatePaginate() for safer handling of large datasets.`);
686
- const facetPipeline = [...pipeline, { $facet: {
687
- docs: [{ $skip: skip }, { $limit: limit }],
688
- total: [{ $count: "count" }]
689
- } }];
690
- const aggregation = Model.aggregate(facetPipeline);
691
- if (options.session) aggregation.session(options.session);
692
- const [result] = await aggregation.exec();
693
- const docs = result.docs || [];
694
- const total = result.total[0]?.count || 0;
695
- const pages = Math.ceil(total / limit);
696
- return {
697
- docs,
698
- total,
699
- page,
700
- limit,
701
- pages,
702
- hasNext: page < pages,
703
- hasPrev: page > 1
704
- };
687
+ async function exists(Model, query, options = {}) {
688
+ const q = Model.exists(query).session(options.session ?? null);
689
+ if (options.readPreference) q.read(options.readPreference);
690
+ return q;
691
+ }
692
+ //#endregion
693
+ //#region src/actions/update.ts
694
+ var update_exports = /* @__PURE__ */ __exportAll({
695
+ increment: () => increment,
696
+ pullFromArray: () => pullFromArray,
697
+ pushToArray: () => pushToArray,
698
+ update: () => update,
699
+ updateByQuery: () => updateByQuery,
700
+ updateMany: () => updateMany,
701
+ updateWithConstraints: () => updateWithConstraints,
702
+ updateWithValidation: () => updateWithValidation
703
+ });
704
+ function assertUpdatePipelineAllowed(update, updatePipeline) {
705
+ 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.");
705
706
  }
706
707
  /**
707
- * Group documents by field value
708
+ * Parse populate specification into consistent format
708
709
  */
709
- async function groupBy(Model, field, options = {}) {
710
- const pipeline = [{ $group: {
711
- _id: `$${field}`,
712
- count: { $sum: 1 }
713
- } }, { $sort: { count: -1 } }];
714
- if (options.limit) pipeline.push({ $limit: options.limit });
715
- return aggregate(Model, pipeline, options);
710
+ function parsePopulate(populate) {
711
+ if (!populate) return [];
712
+ if (typeof populate === "string") return populate.split(",").map((p) => p.trim());
713
+ if (Array.isArray(populate)) return populate.map((p) => typeof p === "string" ? p.trim() : p);
714
+ return [populate];
716
715
  }
717
716
  /**
718
- * Count by field values
717
+ * Update by ID
719
718
  */
720
- async function countBy(Model, field, query = {}, options = {}) {
721
- const pipeline = [];
722
- if (Object.keys(query).length > 0) pipeline.push({ $match: query });
723
- pipeline.push({ $group: {
724
- _id: `$${field}`,
725
- count: { $sum: 1 }
726
- } }, { $sort: { count: -1 } });
727
- return aggregate(Model, pipeline, options);
719
+ async function update(Model, id, data, options = {}) {
720
+ assertUpdatePipelineAllowed(data, options.updatePipeline);
721
+ const query = {
722
+ _id: id,
723
+ ...options.query
724
+ };
725
+ const document = await Model.findOneAndUpdate(query, data, {
726
+ returnDocument: "after",
727
+ runValidators: true,
728
+ session: options.session,
729
+ ...options.updatePipeline !== void 0 ? { updatePipeline: options.updatePipeline } : {},
730
+ ...options.arrayFilters ? { arrayFilters: options.arrayFilters } : {}
731
+ }).select(options.select || "").populate(parsePopulate(options.populate)).lean(options.lean ?? false);
732
+ if (!document) throw createError(404, "Document not found");
733
+ return document;
728
734
  }
729
735
  /**
730
- * Lookup (join) with another collection
731
- *
732
- * MongoDB $lookup has two mutually exclusive forms:
733
- * 1. Simple form: { from, localField, foreignField, as }
734
- * 2. Pipeline form: { from, let, pipeline, as }
735
- *
736
- * This function automatically selects the appropriate form based on parameters.
736
+ * Update with query constraints (optimized)
737
+ * Returns null if constraints not met (not an error)
737
738
  */
738
- async function lookup(Model, lookupOptions) {
739
- const { from, localField, foreignField, as, pipeline = [], let: letVars, query = {}, options = {} } = lookupOptions;
740
- const aggPipeline = [];
741
- if (Object.keys(query).length > 0) aggPipeline.push({ $match: query });
742
- if (pipeline.length > 0 || letVars) if (pipeline.length === 0 && localField && foreignField) {
743
- const autoPipeline = [{ $match: { $expr: { $eq: [`$${foreignField}`, `$$${localField}`] } } }];
744
- aggPipeline.push({ $lookup: {
745
- from,
746
- let: {
747
- [localField]: `$${localField}`,
748
- ...letVars || {}
749
- },
750
- pipeline: autoPipeline,
751
- as
752
- } });
753
- } else {
754
- const safePipeline = lookupOptions.sanitize !== false ? LookupBuilder.sanitizePipeline(pipeline) : pipeline;
755
- aggPipeline.push({ $lookup: {
756
- from,
757
- ...letVars && { let: letVars },
758
- pipeline: safePipeline,
759
- as
760
- } });
761
- }
762
- else aggPipeline.push({ $lookup: {
763
- from,
764
- localField,
765
- foreignField,
766
- as
767
- } });
768
- return aggregate(Model, aggPipeline, options);
739
+ async function updateWithConstraints(Model, id, data, constraints = {}, options = {}) {
740
+ assertUpdatePipelineAllowed(data, options.updatePipeline);
741
+ const query = {
742
+ _id: id,
743
+ ...constraints
744
+ };
745
+ return await Model.findOneAndUpdate(query, data, {
746
+ returnDocument: "after",
747
+ runValidators: true,
748
+ session: options.session,
749
+ ...options.updatePipeline !== void 0 ? { updatePipeline: options.updatePipeline } : {},
750
+ ...options.arrayFilters ? { arrayFilters: options.arrayFilters } : {}
751
+ }).select(options.select || "").populate(parsePopulate(options.populate)).lean(options.lean ?? false);
769
752
  }
770
753
  /**
771
- * Unwind array field
754
+ * Update with validation (smart optimization)
755
+ * 1-query on success, 2-queries for detailed errors
772
756
  */
773
- async function unwind(Model, field, options = {}) {
774
- return aggregate(Model, [{ $unwind: {
775
- path: `$${field}`,
776
- preserveNullAndEmptyArrays: options.preserveEmpty !== false
777
- } }], { session: options.session });
757
+ async function updateWithValidation(Model, id, data, validationOptions = {}, options = {}) {
758
+ const { buildConstraints, validateUpdate } = validationOptions;
759
+ assertUpdatePipelineAllowed(data, options.updatePipeline);
760
+ if (buildConstraints) {
761
+ const document = await updateWithConstraints(Model, id, data, buildConstraints(data), options);
762
+ if (document) return {
763
+ success: true,
764
+ data: document
765
+ };
766
+ }
767
+ const findQuery = {
768
+ _id: id,
769
+ ...options.query
770
+ };
771
+ const existing = await Model.findOne(findQuery).select(options.select || "").session(options.session ?? null).lean();
772
+ if (!existing) return {
773
+ success: false,
774
+ error: {
775
+ code: 404,
776
+ message: "Document not found"
777
+ }
778
+ };
779
+ if (validateUpdate) {
780
+ const validation = validateUpdate(existing, data);
781
+ if (!validation.valid) return {
782
+ success: false,
783
+ error: {
784
+ code: 403,
785
+ message: validation.message || "Update not allowed",
786
+ violations: validation.violations
787
+ }
788
+ };
789
+ }
790
+ return {
791
+ success: true,
792
+ data: await update(Model, id, data, options)
793
+ };
778
794
  }
779
795
  /**
780
- * Facet search (multiple aggregations in one query)
796
+ * Update many documents
781
797
  */
782
- async function facet(Model, facets, options = {}) {
783
- return aggregate(Model, [{ $facet: facets }], options);
798
+ async function updateMany(Model, query, data, options = {}) {
799
+ assertUpdatePipelineAllowed(data, options.updatePipeline);
800
+ const result = await Model.updateMany(query, data, {
801
+ runValidators: true,
802
+ session: options.session,
803
+ ...options.updatePipeline !== void 0 ? { updatePipeline: options.updatePipeline } : {},
804
+ ...options.arrayFilters ? { arrayFilters: options.arrayFilters } : {}
805
+ });
806
+ return {
807
+ matchedCount: result.matchedCount,
808
+ modifiedCount: result.modifiedCount
809
+ };
784
810
  }
785
811
  /**
786
- * Get distinct values
812
+ * Update by query
787
813
  */
788
- async function distinct(Model, field, query = {}, options = {}) {
789
- const q = Model.distinct(field, query).session(options.session ?? null);
790
- if (options.readPreference) q.read(options.readPreference);
791
- return q;
814
+ async function updateByQuery(Model, query, data, options = {}) {
815
+ assertUpdatePipelineAllowed(data, options.updatePipeline);
816
+ const document = await Model.findOneAndUpdate(query, data, {
817
+ returnDocument: "after",
818
+ runValidators: true,
819
+ session: options.session,
820
+ ...options.updatePipeline !== void 0 ? { updatePipeline: options.updatePipeline } : {},
821
+ ...options.arrayFilters ? { arrayFilters: options.arrayFilters } : {}
822
+ }).select(options.select || "").populate(parsePopulate(options.populate)).lean(options.lean ?? false);
823
+ if (!document && options.throwOnNotFound !== false) throw createError(404, "Document not found");
824
+ return document;
792
825
  }
793
826
  /**
794
- * Calculate sum
827
+ * Increment field
795
828
  */
796
- async function sum(Model, field, query = {}, options = {}) {
797
- const pipeline = [];
798
- if (Object.keys(query).length > 0) pipeline.push({ $match: query });
799
- pipeline.push({ $group: {
800
- _id: null,
801
- total: { $sum: `$${field}` }
802
- } });
803
- return (await aggregate(Model, pipeline, options))[0]?.total || 0;
829
+ async function increment(Model, id, field, value = 1, options = {}) {
830
+ return update(Model, id, { $inc: { [field]: value } }, options);
804
831
  }
805
832
  /**
806
- * Calculate average
833
+ * Push to array
807
834
  */
808
- async function average(Model, field, query = {}, options = {}) {
809
- const pipeline = [];
810
- if (Object.keys(query).length > 0) pipeline.push({ $match: query });
811
- pipeline.push({ $group: {
812
- _id: null,
813
- average: { $avg: `$${field}` }
814
- } });
815
- return (await aggregate(Model, pipeline, options))[0]?.average || 0;
835
+ async function pushToArray(Model, id, field, value, options = {}) {
836
+ return update(Model, id, { $push: { [field]: value } }, options);
816
837
  }
817
838
  /**
818
- * Min/Max
839
+ * Pull from array
819
840
  */
820
- async function minMax(Model, field, query = {}, options = {}) {
821
- const pipeline = [];
822
- if (Object.keys(query).length > 0) pipeline.push({ $match: query });
823
- pipeline.push({ $group: {
824
- _id: null,
825
- min: { $min: `$${field}` },
826
- max: { $max: `$${field}` }
827
- } });
828
- return (await aggregate(Model, pipeline, options))[0] || {
829
- min: null,
830
- max: null
831
- };
841
+ async function pullFromArray(Model, id, field, value, options = {}) {
842
+ return update(Model, id, { $pull: { [field]: value } }, options);
832
843
  }
833
-
834
844
  //#endregion
835
- export { upsert as _, delete_exports as a, count as c, getByQuery as d, getOrCreate as f, create_exports as g, createMany as h, deleteById as i, exists as l, create as m, distinct as n, update as o, read_exports as p, LookupBuilder as r, update_exports as s, aggregate_exports as t, getById as u };
845
+ export { LookupBuilder as _, getById as a, read_exports as c, create as d, createMany as f, distinct as g, aggregate_exports as h, exists as i, deleteById as l, upsert as m, update_exports as n, getByQuery as o, create_exports as p, count as r, getOrCreate as s, update as t, delete_exports as u };