@classytic/mongokit 3.2.5 → 3.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/actions/index.d.mts +2 -2
- package/dist/actions/index.mjs +1 -2
- package/dist/{aggregate-BAi4Do-X.mjs → aggregate-BClp040M.mjs} +80 -12
- package/dist/{aggregate-D9x87vej.d.mts → aggregate-BkOG9qwr.d.mts} +3 -1
- package/dist/ai/index.d.mts +1 -1
- package/dist/{cache-keys-C8Z9B5sw.mjs → cache-keys-CzFwVnLy.mjs} +28 -9
- package/dist/{custom-id.plugin-Dc4Y3Eie.d.mts → custom-id.plugin-BJ3FSnzt.d.mts} +51 -3
- package/dist/{custom-id.plugin-m0VW6yYm.mjs → custom-id.plugin-FInXDsUX.mjs} +1744 -76
- package/dist/index.d.mts +63 -15
- package/dist/index.mjs +5 -1144
- package/dist/{limits-DsNeCx4D.mjs → limits-s1-d8rWb.mjs} +2 -2
- package/dist/{mongooseToJsonSchema-1qQMScHL.d.mts → mongooseToJsonSchema-B6O2ED3n.d.mts} +1 -1
- package/dist/pagination/PaginationEngine.d.mts +1 -1
- package/dist/pagination/PaginationEngine.mjs +18 -5
- package/dist/plugins/index.d.mts +2 -2
- package/dist/plugins/index.mjs +1 -1
- package/dist/{types-Bnwv9NV6.d.mts → types-pVY0w1Pp.d.mts} +29 -3
- package/dist/utils/index.d.mts +22 -5
- package/dist/utils/index.mjs +2 -2
- package/package.json +4 -4
- package/dist/create-BuO6xt0v.mjs +0 -55
- /package/dist/{mongooseToJsonSchema-COdDEkIJ.mjs → mongooseToJsonSchema-D_i2Am_O.mjs} +0 -0
package/dist/index.mjs
CHANGED
|
@@ -1,1151 +1,12 @@
|
|
|
1
1
|
import { i as createError, r as warn, t as configureLogger } from "./logger-D8ily-PP.mjs";
|
|
2
|
-
import {
|
|
3
|
-
import { a as deleteById, d as getById, f as getByQuery, i as LookupBuilder, l as count, p as getOrCreate, r as distinct, s as update, t as aggregate, u as exists } from "./aggregate-BAi4Do-X.mjs";
|
|
2
|
+
import { r as LookupBuilder } from "./aggregate-BClp040M.mjs";
|
|
4
3
|
import { PaginationEngine } from "./pagination/PaginationEngine.mjs";
|
|
5
|
-
import { c as
|
|
6
|
-
import {
|
|
7
|
-
import { a as isFieldUpdateAllowed, i as getSystemManagedFields, n as buildCrudSchemasFromMongooseSchema, o as validateUpdateBody, r as getImmutableFields, s as createMemoryCache, t as buildCrudSchemasFromModel } from "./mongooseToJsonSchema-
|
|
4
|
+
import { A as AggregationBuilder, C as methodRegistryPlugin, D as fieldFilterPlugin, E as timestampPlugin, O as HOOK_PRIORITY, S as validationChainPlugin, T as auditLogPlugin, _ as autoInject, a as sequentialId, b as requireField, c as auditTrailPlugin, d as cascadePlugin, f as cachePlugin, g as mongoOperationsPlugin, h as batchOperationsPlugin, i as prefixedId, k as Repository, l as observabilityPlugin, m as aggregateHelpersPlugin, n as dateSequentialId, o as elasticSearchPlugin, p as subdocumentPlugin, r as getNextSequence, s as AuditTrailQuery, t as customIdPlugin, u as multiTenantPlugin, v as blockIf, w as softDeletePlugin, x as uniqueField, y as immutableField } from "./custom-id.plugin-FInXDsUX.mjs";
|
|
5
|
+
import { c as filterResponseData, l as getFieldsForUser, s as createFieldPreset, u as getMongooseProjection } from "./cache-keys-CzFwVnLy.mjs";
|
|
6
|
+
import { a as isFieldUpdateAllowed, i as getSystemManagedFields, n as buildCrudSchemasFromMongooseSchema, o as validateUpdateBody, r as getImmutableFields, s as createMemoryCache, t as buildCrudSchemasFromModel } from "./mongooseToJsonSchema-D_i2Am_O.mjs";
|
|
8
7
|
import { t as actions_exports } from "./actions/index.mjs";
|
|
9
8
|
import mongoose from "mongoose";
|
|
10
9
|
|
|
11
|
-
//#region src/query/AggregationBuilder.ts
|
|
12
|
-
/**
|
|
13
|
-
* Normalize SortSpec to MongoDB's strict format (1 | -1)
|
|
14
|
-
* Converts 'asc' -> 1, 'desc' -> -1
|
|
15
|
-
*/
|
|
16
|
-
function normalizeSortSpec(sortSpec) {
|
|
17
|
-
const normalized = {};
|
|
18
|
-
for (const [field, order] of Object.entries(sortSpec)) if (order === "asc") normalized[field] = 1;
|
|
19
|
-
else if (order === "desc") normalized[field] = -1;
|
|
20
|
-
else normalized[field] = order;
|
|
21
|
-
return normalized;
|
|
22
|
-
}
|
|
23
|
-
/**
|
|
24
|
-
* Fluent builder for MongoDB aggregation pipelines
|
|
25
|
-
* Optimized for complex queries at scale
|
|
26
|
-
*/
|
|
27
|
-
var AggregationBuilder = class AggregationBuilder {
|
|
28
|
-
pipeline = [];
|
|
29
|
-
_diskUse = false;
|
|
30
|
-
/**
|
|
31
|
-
* Get the current pipeline
|
|
32
|
-
*/
|
|
33
|
-
get() {
|
|
34
|
-
return [...this.pipeline];
|
|
35
|
-
}
|
|
36
|
-
/**
|
|
37
|
-
* Build and return the final pipeline
|
|
38
|
-
*/
|
|
39
|
-
build() {
|
|
40
|
-
return this.get();
|
|
41
|
-
}
|
|
42
|
-
/**
|
|
43
|
-
* Build pipeline with execution options (allowDiskUse, etc.)
|
|
44
|
-
*/
|
|
45
|
-
plan() {
|
|
46
|
-
return {
|
|
47
|
-
pipeline: this.get(),
|
|
48
|
-
allowDiskUse: this._diskUse
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
|
-
/**
|
|
52
|
-
* Build and execute the pipeline against a model
|
|
53
|
-
*
|
|
54
|
-
* @example
|
|
55
|
-
* ```typescript
|
|
56
|
-
* const results = await new AggregationBuilder()
|
|
57
|
-
* .match({ status: 'active' })
|
|
58
|
-
* .allowDiskUse()
|
|
59
|
-
* .exec(MyModel);
|
|
60
|
-
* ```
|
|
61
|
-
*/
|
|
62
|
-
async exec(model, session) {
|
|
63
|
-
const agg = model.aggregate(this.build());
|
|
64
|
-
if (this._diskUse) agg.allowDiskUse(true);
|
|
65
|
-
if (session) agg.session(session);
|
|
66
|
-
return agg.exec();
|
|
67
|
-
}
|
|
68
|
-
/**
|
|
69
|
-
* Reset the pipeline
|
|
70
|
-
*/
|
|
71
|
-
reset() {
|
|
72
|
-
this.pipeline = [];
|
|
73
|
-
this._diskUse = false;
|
|
74
|
-
return this;
|
|
75
|
-
}
|
|
76
|
-
/**
|
|
77
|
-
* Add a raw pipeline stage
|
|
78
|
-
*/
|
|
79
|
-
addStage(stage) {
|
|
80
|
-
this.pipeline.push(stage);
|
|
81
|
-
return this;
|
|
82
|
-
}
|
|
83
|
-
/**
|
|
84
|
-
* Add multiple raw pipeline stages
|
|
85
|
-
*/
|
|
86
|
-
addStages(stages) {
|
|
87
|
-
this.pipeline.push(...stages);
|
|
88
|
-
return this;
|
|
89
|
-
}
|
|
90
|
-
/**
|
|
91
|
-
* $match - Filter documents
|
|
92
|
-
* IMPORTANT: Place $match as early as possible for performance
|
|
93
|
-
*/
|
|
94
|
-
match(query) {
|
|
95
|
-
this.pipeline.push({ $match: query });
|
|
96
|
-
return this;
|
|
97
|
-
}
|
|
98
|
-
/**
|
|
99
|
-
* $project - Include/exclude fields or compute new fields
|
|
100
|
-
*/
|
|
101
|
-
project(projection) {
|
|
102
|
-
this.pipeline.push({ $project: projection });
|
|
103
|
-
return this;
|
|
104
|
-
}
|
|
105
|
-
/**
|
|
106
|
-
* $group - Group documents and compute aggregations
|
|
107
|
-
*
|
|
108
|
-
* @example
|
|
109
|
-
* ```typescript
|
|
110
|
-
* .group({
|
|
111
|
-
* _id: '$department',
|
|
112
|
-
* count: { $sum: 1 },
|
|
113
|
-
* avgSalary: { $avg: '$salary' }
|
|
114
|
-
* })
|
|
115
|
-
* ```
|
|
116
|
-
*/
|
|
117
|
-
group(groupSpec) {
|
|
118
|
-
this.pipeline.push({ $group: groupSpec });
|
|
119
|
-
return this;
|
|
120
|
-
}
|
|
121
|
-
/**
|
|
122
|
-
* $sort - Sort documents
|
|
123
|
-
*/
|
|
124
|
-
sort(sortSpec) {
|
|
125
|
-
if (typeof sortSpec === "string") {
|
|
126
|
-
const order = sortSpec.startsWith("-") ? -1 : 1;
|
|
127
|
-
const field = sortSpec.startsWith("-") ? sortSpec.substring(1) : sortSpec;
|
|
128
|
-
this.pipeline.push({ $sort: { [field]: order } });
|
|
129
|
-
} else this.pipeline.push({ $sort: normalizeSortSpec(sortSpec) });
|
|
130
|
-
return this;
|
|
131
|
-
}
|
|
132
|
-
/**
|
|
133
|
-
* $limit - Limit number of documents
|
|
134
|
-
*/
|
|
135
|
-
limit(count) {
|
|
136
|
-
this.pipeline.push({ $limit: count });
|
|
137
|
-
return this;
|
|
138
|
-
}
|
|
139
|
-
/**
|
|
140
|
-
* $skip - Skip documents
|
|
141
|
-
*/
|
|
142
|
-
skip(count) {
|
|
143
|
-
this.pipeline.push({ $skip: count });
|
|
144
|
-
return this;
|
|
145
|
-
}
|
|
146
|
-
/**
|
|
147
|
-
* $unwind - Deconstruct array field
|
|
148
|
-
*/
|
|
149
|
-
unwind(path, preserveNullAndEmptyArrays = false) {
|
|
150
|
-
this.pipeline.push({ $unwind: {
|
|
151
|
-
path: path.startsWith("$") ? path : `$${path}`,
|
|
152
|
-
preserveNullAndEmptyArrays
|
|
153
|
-
} });
|
|
154
|
-
return this;
|
|
155
|
-
}
|
|
156
|
-
/**
|
|
157
|
-
* $addFields - Add new fields or replace existing fields
|
|
158
|
-
*/
|
|
159
|
-
addFields(fields) {
|
|
160
|
-
this.pipeline.push({ $addFields: fields });
|
|
161
|
-
return this;
|
|
162
|
-
}
|
|
163
|
-
/**
|
|
164
|
-
* $set - Alias for $addFields
|
|
165
|
-
*/
|
|
166
|
-
set(fields) {
|
|
167
|
-
return this.addFields(fields);
|
|
168
|
-
}
|
|
169
|
-
/**
|
|
170
|
-
* $unset - Remove fields
|
|
171
|
-
*/
|
|
172
|
-
unset(fields) {
|
|
173
|
-
this.pipeline.push({ $unset: fields });
|
|
174
|
-
return this;
|
|
175
|
-
}
|
|
176
|
-
/**
|
|
177
|
-
* $replaceRoot - Replace the root document
|
|
178
|
-
*/
|
|
179
|
-
replaceRoot(newRoot) {
|
|
180
|
-
this.pipeline.push({ $replaceRoot: { newRoot: typeof newRoot === "string" ? `$${newRoot}` : newRoot } });
|
|
181
|
-
return this;
|
|
182
|
-
}
|
|
183
|
-
/**
|
|
184
|
-
* $lookup - Join with another collection (simple form)
|
|
185
|
-
*
|
|
186
|
-
* @param from - Collection to join with
|
|
187
|
-
* @param localField - Field from source collection
|
|
188
|
-
* @param foreignField - Field from target collection
|
|
189
|
-
* @param as - Output field name
|
|
190
|
-
* @param single - Unwrap array to single object
|
|
191
|
-
*
|
|
192
|
-
* @example
|
|
193
|
-
* ```typescript
|
|
194
|
-
* // Join employees with departments by slug
|
|
195
|
-
* .lookup('departments', 'deptSlug', 'slug', 'department', true)
|
|
196
|
-
* ```
|
|
197
|
-
*/
|
|
198
|
-
lookup(from, localField, foreignField, as, single) {
|
|
199
|
-
const stages = new LookupBuilder(from).localField(localField).foreignField(foreignField).as(as || from).single(single || false).build();
|
|
200
|
-
this.pipeline.push(...stages);
|
|
201
|
-
return this;
|
|
202
|
-
}
|
|
203
|
-
/**
|
|
204
|
-
* $lookup - Join with another collection (advanced form with pipeline)
|
|
205
|
-
*
|
|
206
|
-
* @example
|
|
207
|
-
* ```typescript
|
|
208
|
-
* .lookupWithPipeline({
|
|
209
|
-
* from: 'products',
|
|
210
|
-
* localField: 'productIds',
|
|
211
|
-
* foreignField: 'sku',
|
|
212
|
-
* as: 'products',
|
|
213
|
-
* pipeline: [
|
|
214
|
-
* { $match: { status: 'active' } },
|
|
215
|
-
* { $project: { name: 1, price: 1 } }
|
|
216
|
-
* ]
|
|
217
|
-
* })
|
|
218
|
-
* ```
|
|
219
|
-
*/
|
|
220
|
-
lookupWithPipeline(options) {
|
|
221
|
-
const builder = new LookupBuilder(options.from).localField(options.localField).foreignField(options.foreignField);
|
|
222
|
-
if (options.as) builder.as(options.as);
|
|
223
|
-
if (options.single) builder.single(options.single);
|
|
224
|
-
if (options.pipeline) builder.pipeline(options.pipeline);
|
|
225
|
-
if (options.let) builder.let(options.let);
|
|
226
|
-
this.pipeline.push(...builder.build());
|
|
227
|
-
return this;
|
|
228
|
-
}
|
|
229
|
-
/**
|
|
230
|
-
* Multiple lookups at once
|
|
231
|
-
*
|
|
232
|
-
* @example
|
|
233
|
-
* ```typescript
|
|
234
|
-
* .multiLookup([
|
|
235
|
-
* { from: 'departments', localField: 'deptSlug', foreignField: 'slug', single: true },
|
|
236
|
-
* { from: 'managers', localField: 'managerId', foreignField: '_id', single: true }
|
|
237
|
-
* ])
|
|
238
|
-
* ```
|
|
239
|
-
*/
|
|
240
|
-
multiLookup(lookups) {
|
|
241
|
-
const stages = LookupBuilder.multiple(lookups);
|
|
242
|
-
this.pipeline.push(...stages);
|
|
243
|
-
return this;
|
|
244
|
-
}
|
|
245
|
-
/**
|
|
246
|
-
* $facet - Process multiple aggregation pipelines in a single stage
|
|
247
|
-
* Useful for computing multiple aggregations in parallel
|
|
248
|
-
*
|
|
249
|
-
* @example
|
|
250
|
-
* ```typescript
|
|
251
|
-
* .facet({
|
|
252
|
-
* totalCount: [{ $count: 'count' }],
|
|
253
|
-
* avgPrice: [{ $group: { _id: null, avg: { $avg: '$price' } } }],
|
|
254
|
-
* topProducts: [{ $sort: { sales: -1 } }, { $limit: 10 }]
|
|
255
|
-
* })
|
|
256
|
-
* ```
|
|
257
|
-
*/
|
|
258
|
-
facet(facets) {
|
|
259
|
-
this.pipeline.push({ $facet: facets });
|
|
260
|
-
return this;
|
|
261
|
-
}
|
|
262
|
-
/**
|
|
263
|
-
* $bucket - Categorize documents into buckets
|
|
264
|
-
*
|
|
265
|
-
* @example
|
|
266
|
-
* ```typescript
|
|
267
|
-
* .bucket({
|
|
268
|
-
* groupBy: '$price',
|
|
269
|
-
* boundaries: [0, 50, 100, 200],
|
|
270
|
-
* default: 'Other',
|
|
271
|
-
* output: {
|
|
272
|
-
* count: { $sum: 1 },
|
|
273
|
-
* products: { $push: '$name' }
|
|
274
|
-
* }
|
|
275
|
-
* })
|
|
276
|
-
* ```
|
|
277
|
-
*/
|
|
278
|
-
bucket(options) {
|
|
279
|
-
this.pipeline.push({ $bucket: options });
|
|
280
|
-
return this;
|
|
281
|
-
}
|
|
282
|
-
/**
|
|
283
|
-
* $bucketAuto - Automatically determine bucket boundaries
|
|
284
|
-
*/
|
|
285
|
-
bucketAuto(options) {
|
|
286
|
-
this.pipeline.push({ $bucketAuto: options });
|
|
287
|
-
return this;
|
|
288
|
-
}
|
|
289
|
-
/**
|
|
290
|
-
* $setWindowFields - Perform window functions (MongoDB 5.0+)
|
|
291
|
-
* Useful for rankings, running totals, moving averages
|
|
292
|
-
*
|
|
293
|
-
* @example
|
|
294
|
-
* ```typescript
|
|
295
|
-
* .setWindowFields({
|
|
296
|
-
* partitionBy: '$department',
|
|
297
|
-
* sortBy: { salary: -1 },
|
|
298
|
-
* output: {
|
|
299
|
-
* rank: { $rank: {} },
|
|
300
|
-
* runningTotal: { $sum: '$salary', window: { documents: ['unbounded', 'current'] } }
|
|
301
|
-
* }
|
|
302
|
-
* })
|
|
303
|
-
* ```
|
|
304
|
-
*/
|
|
305
|
-
setWindowFields(options) {
|
|
306
|
-
const normalizedOptions = {
|
|
307
|
-
...options,
|
|
308
|
-
sortBy: options.sortBy ? normalizeSortSpec(options.sortBy) : void 0
|
|
309
|
-
};
|
|
310
|
-
this.pipeline.push({ $setWindowFields: normalizedOptions });
|
|
311
|
-
return this;
|
|
312
|
-
}
|
|
313
|
-
/**
|
|
314
|
-
* $unionWith - Combine results from multiple collections (MongoDB 4.4+)
|
|
315
|
-
*
|
|
316
|
-
* @example
|
|
317
|
-
* ```typescript
|
|
318
|
-
* .unionWith({
|
|
319
|
-
* coll: 'archivedOrders',
|
|
320
|
-
* pipeline: [{ $match: { year: 2024 } }]
|
|
321
|
-
* })
|
|
322
|
-
* ```
|
|
323
|
-
*/
|
|
324
|
-
unionWith(options) {
|
|
325
|
-
this.pipeline.push({ $unionWith: options });
|
|
326
|
-
return this;
|
|
327
|
-
}
|
|
328
|
-
/**
|
|
329
|
-
* $densify - Fill gaps in data (MongoDB 5.1+)
|
|
330
|
-
* Useful for time series data with missing points
|
|
331
|
-
*/
|
|
332
|
-
densify(options) {
|
|
333
|
-
this.pipeline.push({ $densify: options });
|
|
334
|
-
return this;
|
|
335
|
-
}
|
|
336
|
-
/**
|
|
337
|
-
* $fill - Fill null or missing field values (MongoDB 5.3+)
|
|
338
|
-
*/
|
|
339
|
-
fill(options) {
|
|
340
|
-
const normalizedOptions = {
|
|
341
|
-
...options,
|
|
342
|
-
sortBy: options.sortBy ? normalizeSortSpec(options.sortBy) : void 0
|
|
343
|
-
};
|
|
344
|
-
this.pipeline.push({ $fill: normalizedOptions });
|
|
345
|
-
return this;
|
|
346
|
-
}
|
|
347
|
-
/**
|
|
348
|
-
* Enable allowDiskUse for large aggregations that exceed 100MB memory limit
|
|
349
|
-
*
|
|
350
|
-
* @example
|
|
351
|
-
* ```typescript
|
|
352
|
-
* const results = await new AggregationBuilder()
|
|
353
|
-
* .match({ status: 'active' })
|
|
354
|
-
* .group({ _id: '$category', total: { $sum: '$amount' } })
|
|
355
|
-
* .allowDiskUse()
|
|
356
|
-
* .exec(Model);
|
|
357
|
-
* ```
|
|
358
|
-
*/
|
|
359
|
-
allowDiskUse(enable = true) {
|
|
360
|
-
this._diskUse = enable;
|
|
361
|
-
return this;
|
|
362
|
-
}
|
|
363
|
-
/**
|
|
364
|
-
* Paginate - Add skip and limit for offset-based pagination
|
|
365
|
-
*/
|
|
366
|
-
paginate(page, limit) {
|
|
367
|
-
const skip = (page - 1) * limit;
|
|
368
|
-
return this.skip(skip).limit(limit);
|
|
369
|
-
}
|
|
370
|
-
/**
|
|
371
|
-
* Count total documents (useful with $facet for pagination metadata)
|
|
372
|
-
*/
|
|
373
|
-
count(outputField = "count") {
|
|
374
|
-
this.pipeline.push({ $count: outputField });
|
|
375
|
-
return this;
|
|
376
|
-
}
|
|
377
|
-
/**
|
|
378
|
-
* Sample - Randomly select N documents
|
|
379
|
-
*/
|
|
380
|
-
sample(size) {
|
|
381
|
-
this.pipeline.push({ $sample: { size } });
|
|
382
|
-
return this;
|
|
383
|
-
}
|
|
384
|
-
/**
|
|
385
|
-
* Out - Write results to a collection
|
|
386
|
-
*/
|
|
387
|
-
out(collection) {
|
|
388
|
-
this.pipeline.push({ $out: collection });
|
|
389
|
-
return this;
|
|
390
|
-
}
|
|
391
|
-
/**
|
|
392
|
-
* Merge - Merge results into a collection
|
|
393
|
-
*/
|
|
394
|
-
merge(options) {
|
|
395
|
-
this.pipeline.push({ $merge: typeof options === "string" ? { into: options } : options });
|
|
396
|
-
return this;
|
|
397
|
-
}
|
|
398
|
-
/**
|
|
399
|
-
* GeoNear - Perform geospatial queries
|
|
400
|
-
*/
|
|
401
|
-
geoNear(options) {
|
|
402
|
-
this.pipeline.push({ $geoNear: options });
|
|
403
|
-
return this;
|
|
404
|
-
}
|
|
405
|
-
/**
|
|
406
|
-
* GraphLookup - Perform recursive search (graph traversal)
|
|
407
|
-
*/
|
|
408
|
-
graphLookup(options) {
|
|
409
|
-
this.pipeline.push({ $graphLookup: options });
|
|
410
|
-
return this;
|
|
411
|
-
}
|
|
412
|
-
/**
|
|
413
|
-
* $search - Atlas Search full-text search (Atlas only)
|
|
414
|
-
*
|
|
415
|
-
* @example
|
|
416
|
-
* ```typescript
|
|
417
|
-
* .search({
|
|
418
|
-
* index: 'default',
|
|
419
|
-
* text: {
|
|
420
|
-
* query: 'laptop computer',
|
|
421
|
-
* path: ['title', 'description'],
|
|
422
|
-
* fuzzy: { maxEdits: 2 }
|
|
423
|
-
* }
|
|
424
|
-
* })
|
|
425
|
-
* ```
|
|
426
|
-
*/
|
|
427
|
-
search(options) {
|
|
428
|
-
this.pipeline.push({ $search: options });
|
|
429
|
-
return this;
|
|
430
|
-
}
|
|
431
|
-
/**
|
|
432
|
-
* $searchMeta - Get Atlas Search metadata (Atlas only)
|
|
433
|
-
*/
|
|
434
|
-
searchMeta(options) {
|
|
435
|
-
this.pipeline.push({ $searchMeta: options });
|
|
436
|
-
return this;
|
|
437
|
-
}
|
|
438
|
-
/**
|
|
439
|
-
* $vectorSearch - Semantic similarity search using vector embeddings (Atlas only)
|
|
440
|
-
*
|
|
441
|
-
* Requires an Atlas Vector Search index on the target field.
|
|
442
|
-
* Must be the first stage in the pipeline.
|
|
443
|
-
*
|
|
444
|
-
* @example
|
|
445
|
-
* ```typescript
|
|
446
|
-
* const results = await new AggregationBuilder()
|
|
447
|
-
* .vectorSearch({
|
|
448
|
-
* index: 'vector_index',
|
|
449
|
-
* path: 'embedding',
|
|
450
|
-
* queryVector: await getEmbedding('running shoes'),
|
|
451
|
-
* limit: 10,
|
|
452
|
-
* numCandidates: 100,
|
|
453
|
-
* filter: { category: 'footwear' }
|
|
454
|
-
* })
|
|
455
|
-
* .project({ embedding: 0, score: { $meta: 'vectorSearchScore' } })
|
|
456
|
-
* .exec(ProductModel);
|
|
457
|
-
* ```
|
|
458
|
-
*/
|
|
459
|
-
vectorSearch(options) {
|
|
460
|
-
if (this.pipeline.length > 0) throw new Error("[mongokit] $vectorSearch must be the first stage in the pipeline");
|
|
461
|
-
const rawCandidates = options.numCandidates ?? Math.max(options.limit * 10, 100);
|
|
462
|
-
const numCandidates = Math.min(Math.max(rawCandidates, options.limit), 1e4);
|
|
463
|
-
this.pipeline.push({ $vectorSearch: {
|
|
464
|
-
index: options.index,
|
|
465
|
-
path: options.path,
|
|
466
|
-
queryVector: options.queryVector,
|
|
467
|
-
numCandidates,
|
|
468
|
-
limit: options.limit,
|
|
469
|
-
...options.filter && { filter: options.filter },
|
|
470
|
-
...options.exact && { exact: options.exact }
|
|
471
|
-
} });
|
|
472
|
-
return this;
|
|
473
|
-
}
|
|
474
|
-
/**
|
|
475
|
-
* Add vectorSearchScore as a field after $vectorSearch
|
|
476
|
-
* Convenience for `.addFields({ score: { $meta: 'vectorSearchScore' } })`
|
|
477
|
-
*/
|
|
478
|
-
withVectorScore(fieldName = "score") {
|
|
479
|
-
return this.addFields({ [fieldName]: { $meta: "vectorSearchScore" } });
|
|
480
|
-
}
|
|
481
|
-
/**
|
|
482
|
-
* Create a builder from an existing pipeline
|
|
483
|
-
*/
|
|
484
|
-
static from(pipeline) {
|
|
485
|
-
const builder = new AggregationBuilder();
|
|
486
|
-
builder.pipeline = [...pipeline];
|
|
487
|
-
return builder;
|
|
488
|
-
}
|
|
489
|
-
/**
|
|
490
|
-
* Create a builder with initial match stage
|
|
491
|
-
*/
|
|
492
|
-
static startWith(query) {
|
|
493
|
-
return new AggregationBuilder().match(query);
|
|
494
|
-
}
|
|
495
|
-
};
|
|
496
|
-
|
|
497
|
-
//#endregion
|
|
498
|
-
//#region src/Repository.ts
|
|
499
|
-
/**
|
|
500
|
-
* Repository Pattern - Data Access Layer
|
|
501
|
-
*
|
|
502
|
-
* Event-driven, plugin-based abstraction for MongoDB operations
|
|
503
|
-
* Inspired by Meta & Stripe's repository patterns
|
|
504
|
-
*
|
|
505
|
-
* @example
|
|
506
|
-
* ```typescript
|
|
507
|
-
* const userRepo = new Repository(UserModel, [
|
|
508
|
-
* timestampPlugin(),
|
|
509
|
-
* softDeletePlugin(),
|
|
510
|
-
* ]);
|
|
511
|
-
*
|
|
512
|
-
* // Create
|
|
513
|
-
* const user = await userRepo.create({ name: 'John', email: 'john@example.com' });
|
|
514
|
-
*
|
|
515
|
-
* // Read with pagination
|
|
516
|
-
* const users = await userRepo.getAll({ page: 1, limit: 20, filters: { status: 'active' } });
|
|
517
|
-
*
|
|
518
|
-
* // Update
|
|
519
|
-
* const updated = await userRepo.update(user._id, { name: 'John Doe' });
|
|
520
|
-
*
|
|
521
|
-
* // Delete
|
|
522
|
-
* await userRepo.delete(user._id);
|
|
523
|
-
* ```
|
|
524
|
-
*/
|
|
525
|
-
/**
|
|
526
|
-
* Production-grade repository for MongoDB
|
|
527
|
-
* Event-driven, plugin-based, with smart pagination
|
|
528
|
-
*/
|
|
529
|
-
var Repository = class {
|
|
530
|
-
Model;
|
|
531
|
-
model;
|
|
532
|
-
_hooks;
|
|
533
|
-
_pagination;
|
|
534
|
-
_hookMode;
|
|
535
|
-
_hasTextIndex = null;
|
|
536
|
-
constructor(Model, plugins = [], paginationConfig = {}, options = {}) {
|
|
537
|
-
this.Model = Model;
|
|
538
|
-
this.model = Model.modelName;
|
|
539
|
-
this._hooks = /* @__PURE__ */ new Map();
|
|
540
|
-
this._pagination = new PaginationEngine(Model, paginationConfig);
|
|
541
|
-
this._hookMode = options.hooks ?? "async";
|
|
542
|
-
plugins.forEach((plugin) => this.use(plugin));
|
|
543
|
-
}
|
|
544
|
-
/**
|
|
545
|
-
* Register a plugin
|
|
546
|
-
*/
|
|
547
|
-
use(plugin) {
|
|
548
|
-
if (typeof plugin === "function") plugin(this);
|
|
549
|
-
else if (plugin && typeof plugin.apply === "function") plugin.apply(this);
|
|
550
|
-
return this;
|
|
551
|
-
}
|
|
552
|
-
/**
|
|
553
|
-
* Register event listener
|
|
554
|
-
*/
|
|
555
|
-
on(event, listener) {
|
|
556
|
-
if (!this._hooks.has(event)) this._hooks.set(event, []);
|
|
557
|
-
this._hooks.get(event).push(listener);
|
|
558
|
-
return this;
|
|
559
|
-
}
|
|
560
|
-
/**
|
|
561
|
-
* Remove a specific event listener
|
|
562
|
-
*/
|
|
563
|
-
off(event, listener) {
|
|
564
|
-
const listeners = this._hooks.get(event);
|
|
565
|
-
if (listeners) {
|
|
566
|
-
const idx = listeners.indexOf(listener);
|
|
567
|
-
if (idx !== -1) listeners.splice(idx, 1);
|
|
568
|
-
}
|
|
569
|
-
return this;
|
|
570
|
-
}
|
|
571
|
-
/**
|
|
572
|
-
* Remove all listeners for an event, or all listeners entirely
|
|
573
|
-
*/
|
|
574
|
-
removeAllListeners(event) {
|
|
575
|
-
if (event) this._hooks.delete(event);
|
|
576
|
-
else this._hooks.clear();
|
|
577
|
-
return this;
|
|
578
|
-
}
|
|
579
|
-
/**
|
|
580
|
-
* Emit event (sync - for backwards compatibility)
|
|
581
|
-
*/
|
|
582
|
-
emit(event, data) {
|
|
583
|
-
const listeners = this._hooks.get(event) || [];
|
|
584
|
-
for (const listener of listeners) try {
|
|
585
|
-
const result = listener(data);
|
|
586
|
-
if (result && typeof result.then === "function") result.catch((error) => {
|
|
587
|
-
if (event === "error:hook") return;
|
|
588
|
-
const err = error instanceof Error ? error : new Error(String(error));
|
|
589
|
-
this.emit("error:hook", {
|
|
590
|
-
event,
|
|
591
|
-
error: err
|
|
592
|
-
});
|
|
593
|
-
});
|
|
594
|
-
} catch (error) {
|
|
595
|
-
if (event === "error:hook") continue;
|
|
596
|
-
const err = error instanceof Error ? error : new Error(String(error));
|
|
597
|
-
this.emit("error:hook", {
|
|
598
|
-
event,
|
|
599
|
-
error: err
|
|
600
|
-
});
|
|
601
|
-
}
|
|
602
|
-
}
|
|
603
|
-
/**
|
|
604
|
-
* Emit event and await all async handlers
|
|
605
|
-
*/
|
|
606
|
-
async emitAsync(event, data) {
|
|
607
|
-
const listeners = this._hooks.get(event) || [];
|
|
608
|
-
for (const listener of listeners) await listener(data);
|
|
609
|
-
}
|
|
610
|
-
async _emitHook(event, data) {
|
|
611
|
-
if (this._hookMode === "async") {
|
|
612
|
-
await this.emitAsync(event, data);
|
|
613
|
-
return;
|
|
614
|
-
}
|
|
615
|
-
this.emit(event, data);
|
|
616
|
-
}
|
|
617
|
-
async _emitErrorHook(event, data) {
|
|
618
|
-
try {
|
|
619
|
-
await this._emitHook(event, data);
|
|
620
|
-
} catch (hookError) {
|
|
621
|
-
warn(`[${this.model}] Error hook '${event}' threw: ${hookError instanceof Error ? hookError.message : String(hookError)}`);
|
|
622
|
-
}
|
|
623
|
-
}
|
|
624
|
-
/**
|
|
625
|
-
* Create single document
|
|
626
|
-
*/
|
|
627
|
-
async create(data, options = {}) {
|
|
628
|
-
const context = await this._buildContext("create", {
|
|
629
|
-
data,
|
|
630
|
-
...options
|
|
631
|
-
});
|
|
632
|
-
try {
|
|
633
|
-
const result = await create(this.Model, context.data || data, options);
|
|
634
|
-
await this._emitHook("after:create", {
|
|
635
|
-
context,
|
|
636
|
-
result
|
|
637
|
-
});
|
|
638
|
-
return result;
|
|
639
|
-
} catch (error) {
|
|
640
|
-
await this._emitErrorHook("error:create", {
|
|
641
|
-
context,
|
|
642
|
-
error
|
|
643
|
-
});
|
|
644
|
-
throw this._handleError(error);
|
|
645
|
-
}
|
|
646
|
-
}
|
|
647
|
-
/**
|
|
648
|
-
* Create multiple documents
|
|
649
|
-
*/
|
|
650
|
-
async createMany(dataArray, options = {}) {
|
|
651
|
-
const context = await this._buildContext("createMany", {
|
|
652
|
-
dataArray,
|
|
653
|
-
...options
|
|
654
|
-
});
|
|
655
|
-
try {
|
|
656
|
-
const result = await createMany(this.Model, context.dataArray || dataArray, options);
|
|
657
|
-
await this._emitHook("after:createMany", {
|
|
658
|
-
context,
|
|
659
|
-
result
|
|
660
|
-
});
|
|
661
|
-
return result;
|
|
662
|
-
} catch (error) {
|
|
663
|
-
await this._emitErrorHook("error:createMany", {
|
|
664
|
-
context,
|
|
665
|
-
error
|
|
666
|
-
});
|
|
667
|
-
throw this._handleError(error);
|
|
668
|
-
}
|
|
669
|
-
}
|
|
670
|
-
/**
|
|
671
|
-
* Get document by ID
|
|
672
|
-
*/
|
|
673
|
-
async getById(id, options = {}) {
|
|
674
|
-
const populateSpec = options.populateOptions || options.populate;
|
|
675
|
-
const context = await this._buildContext("getById", {
|
|
676
|
-
id,
|
|
677
|
-
...options,
|
|
678
|
-
populate: populateSpec
|
|
679
|
-
});
|
|
680
|
-
if (context._cacheHit) return context._cachedResult;
|
|
681
|
-
try {
|
|
682
|
-
const result = await getById(this.Model, id, context);
|
|
683
|
-
await this._emitHook("after:getById", {
|
|
684
|
-
context,
|
|
685
|
-
result
|
|
686
|
-
});
|
|
687
|
-
return result;
|
|
688
|
-
} catch (error) {
|
|
689
|
-
await this._emitErrorHook("error:getById", {
|
|
690
|
-
context,
|
|
691
|
-
error
|
|
692
|
-
});
|
|
693
|
-
throw this._handleError(error);
|
|
694
|
-
}
|
|
695
|
-
}
|
|
696
|
-
/**
|
|
697
|
-
* Get single document by query
|
|
698
|
-
*/
|
|
699
|
-
async getByQuery(query, options = {}) {
|
|
700
|
-
const populateSpec = options.populateOptions || options.populate;
|
|
701
|
-
const context = await this._buildContext("getByQuery", {
|
|
702
|
-
query,
|
|
703
|
-
...options,
|
|
704
|
-
populate: populateSpec
|
|
705
|
-
});
|
|
706
|
-
if (context._cacheHit) return context._cachedResult;
|
|
707
|
-
const finalQuery = context.query || query;
|
|
708
|
-
try {
|
|
709
|
-
const result = await getByQuery(this.Model, finalQuery, context);
|
|
710
|
-
await this._emitHook("after:getByQuery", {
|
|
711
|
-
context,
|
|
712
|
-
result
|
|
713
|
-
});
|
|
714
|
-
return result;
|
|
715
|
-
} catch (error) {
|
|
716
|
-
await this._emitErrorHook("error:getByQuery", {
|
|
717
|
-
context,
|
|
718
|
-
error
|
|
719
|
-
});
|
|
720
|
-
throw this._handleError(error);
|
|
721
|
-
}
|
|
722
|
-
}
|
|
723
|
-
/**
|
|
724
|
-
* Unified pagination - auto-detects offset vs keyset based on params
|
|
725
|
-
*
|
|
726
|
-
* Auto-detection logic:
|
|
727
|
-
* - If params has 'cursor' or 'after' → uses keyset pagination (stream)
|
|
728
|
-
* - If params has 'pagination' or 'page' → uses offset pagination (paginate)
|
|
729
|
-
* - Else → defaults to offset pagination with page=1
|
|
730
|
-
*
|
|
731
|
-
* @example
|
|
732
|
-
* // Offset pagination (page-based)
|
|
733
|
-
* await repo.getAll({ page: 1, limit: 50, filters: { status: 'active' } });
|
|
734
|
-
* await repo.getAll({ pagination: { page: 2, limit: 20 } });
|
|
735
|
-
*
|
|
736
|
-
* // Keyset pagination (cursor-based)
|
|
737
|
-
* await repo.getAll({ cursor: 'eyJ2Ij...', limit: 50 });
|
|
738
|
-
* await repo.getAll({ after: 'eyJ2Ij...', sort: { createdAt: -1 } });
|
|
739
|
-
*
|
|
740
|
-
* // Simple query (defaults to page 1)
|
|
741
|
-
* await repo.getAll({ filters: { status: 'active' } });
|
|
742
|
-
*
|
|
743
|
-
* // Skip cache for fresh data
|
|
744
|
-
* await repo.getAll({ filters: { status: 'active' } }, { skipCache: true });
|
|
745
|
-
*/
|
|
746
|
-
async getAll(params = {}, options = {}) {
|
|
747
|
-
const context = await this._buildContext("getAll", {
|
|
748
|
-
...params,
|
|
749
|
-
...options
|
|
750
|
-
});
|
|
751
|
-
if (context._cacheHit) return context._cachedResult;
|
|
752
|
-
const filters = context.filters ?? params.filters ?? {};
|
|
753
|
-
const search = context.search ?? params.search;
|
|
754
|
-
const sort = context.sort ?? params.sort ?? "-createdAt";
|
|
755
|
-
const limit = context.limit ?? params.limit ?? params.pagination?.limit ?? this._pagination.config.defaultLimit;
|
|
756
|
-
const page = context.page ?? params.pagination?.page ?? params.page;
|
|
757
|
-
const after = context.after ?? params.cursor ?? params.after;
|
|
758
|
-
const mode = context.mode ?? params.mode;
|
|
759
|
-
let useKeyset = false;
|
|
760
|
-
if (mode) useKeyset = mode === "keyset";
|
|
761
|
-
else useKeyset = !page && !!(after || sort !== "-createdAt" && (context.sort ?? params.sort));
|
|
762
|
-
let query = { ...filters };
|
|
763
|
-
if (search) {
|
|
764
|
-
if (this._hasTextIndex === null) this._hasTextIndex = this.Model.schema.indexes().some((idx) => idx[0] && Object.values(idx[0]).includes("text"));
|
|
765
|
-
if (this._hasTextIndex) query.$text = { $search: search };
|
|
766
|
-
else throw createError(400, `No text index found for ${this.model}. Cannot perform text search.`);
|
|
767
|
-
}
|
|
768
|
-
const populateSpec = options.populateOptions || params.populateOptions || context.populate || options.populate;
|
|
769
|
-
const paginationOptions = {
|
|
770
|
-
filters: query,
|
|
771
|
-
sort: this._parseSort(sort),
|
|
772
|
-
limit,
|
|
773
|
-
populate: this._parsePopulate(populateSpec),
|
|
774
|
-
select: context.select || options.select,
|
|
775
|
-
lean: context.lean ?? options.lean ?? true,
|
|
776
|
-
session: options.session,
|
|
777
|
-
hint: context.hint ?? params.hint,
|
|
778
|
-
maxTimeMS: context.maxTimeMS ?? params.maxTimeMS,
|
|
779
|
-
readPreference: context.readPreference ?? options.readPreference ?? params.readPreference
|
|
780
|
-
};
|
|
781
|
-
try {
|
|
782
|
-
let result;
|
|
783
|
-
if (useKeyset) result = await this._pagination.stream({
|
|
784
|
-
...paginationOptions,
|
|
785
|
-
sort: paginationOptions.sort,
|
|
786
|
-
after
|
|
787
|
-
});
|
|
788
|
-
else result = await this._pagination.paginate({
|
|
789
|
-
...paginationOptions,
|
|
790
|
-
page: page || 1,
|
|
791
|
-
countStrategy: context.countStrategy ?? params.countStrategy
|
|
792
|
-
});
|
|
793
|
-
await this._emitHook("after:getAll", {
|
|
794
|
-
context,
|
|
795
|
-
result
|
|
796
|
-
});
|
|
797
|
-
return result;
|
|
798
|
-
} catch (error) {
|
|
799
|
-
await this._emitErrorHook("error:getAll", {
|
|
800
|
-
context,
|
|
801
|
-
error
|
|
802
|
-
});
|
|
803
|
-
throw this._handleError(error);
|
|
804
|
-
}
|
|
805
|
-
}
|
|
806
|
-
/**
|
|
807
|
-
* Get or create document
|
|
808
|
-
*/
|
|
809
|
-
async getOrCreate(query, createData, options = {}) {
|
|
810
|
-
return getOrCreate(this.Model, query, createData, options);
|
|
811
|
-
}
|
|
812
|
-
/**
|
|
813
|
-
* Count documents
|
|
814
|
-
*/
|
|
815
|
-
async count(query = {}, options = {}) {
|
|
816
|
-
return count(this.Model, query, options);
|
|
817
|
-
}
|
|
818
|
-
/**
|
|
819
|
-
* Check if document exists
|
|
820
|
-
*/
|
|
821
|
-
async exists(query, options = {}) {
|
|
822
|
-
return exists(this.Model, query, options);
|
|
823
|
-
}
|
|
824
|
-
/**
|
|
825
|
-
* Update document by ID
|
|
826
|
-
*/
|
|
827
|
-
async update(id, data, options = {}) {
|
|
828
|
-
const context = await this._buildContext("update", {
|
|
829
|
-
id,
|
|
830
|
-
data,
|
|
831
|
-
...options
|
|
832
|
-
});
|
|
833
|
-
try {
|
|
834
|
-
const result = await update(this.Model, id, context.data || data, context);
|
|
835
|
-
await this._emitHook("after:update", {
|
|
836
|
-
context,
|
|
837
|
-
result
|
|
838
|
-
});
|
|
839
|
-
return result;
|
|
840
|
-
} catch (error) {
|
|
841
|
-
await this._emitErrorHook("error:update", {
|
|
842
|
-
context,
|
|
843
|
-
error
|
|
844
|
-
});
|
|
845
|
-
throw this._handleError(error);
|
|
846
|
-
}
|
|
847
|
-
}
|
|
848
|
-
/**
|
|
849
|
-
* Delete document by ID
|
|
850
|
-
*/
|
|
851
|
-
async delete(id, options = {}) {
|
|
852
|
-
const context = await this._buildContext("delete", {
|
|
853
|
-
id,
|
|
854
|
-
...options
|
|
855
|
-
});
|
|
856
|
-
try {
|
|
857
|
-
if (context.softDeleted) {
|
|
858
|
-
const result = {
|
|
859
|
-
success: true,
|
|
860
|
-
message: "Soft deleted successfully"
|
|
861
|
-
};
|
|
862
|
-
await this._emitHook("after:delete", {
|
|
863
|
-
context,
|
|
864
|
-
result
|
|
865
|
-
});
|
|
866
|
-
return result;
|
|
867
|
-
}
|
|
868
|
-
const result = await deleteById(this.Model, id, {
|
|
869
|
-
session: options.session,
|
|
870
|
-
query: context.query
|
|
871
|
-
});
|
|
872
|
-
await this._emitHook("after:delete", {
|
|
873
|
-
context,
|
|
874
|
-
result
|
|
875
|
-
});
|
|
876
|
-
return result;
|
|
877
|
-
} catch (error) {
|
|
878
|
-
await this._emitErrorHook("error:delete", {
|
|
879
|
-
context,
|
|
880
|
-
error
|
|
881
|
-
});
|
|
882
|
-
throw this._handleError(error);
|
|
883
|
-
}
|
|
884
|
-
}
|
|
885
|
-
/**
|
|
886
|
-
* Execute aggregation pipeline
|
|
887
|
-
*/
|
|
888
|
-
async aggregate(pipeline, options = {}) {
|
|
889
|
-
return aggregate(this.Model, pipeline, options);
|
|
890
|
-
}
|
|
891
|
-
/**
|
|
892
|
-
* Aggregate pipeline with pagination
|
|
893
|
-
* Best for: Complex queries, grouping, joins
|
|
894
|
-
*/
|
|
895
|
-
async aggregatePaginate(options = {}) {
|
|
896
|
-
const context = await this._buildContext("aggregatePaginate", options);
|
|
897
|
-
return this._pagination.aggregatePaginate(context);
|
|
898
|
-
}
|
|
899
|
-
/**
|
|
900
|
-
* Get distinct values
|
|
901
|
-
*/
|
|
902
|
-
async distinct(field, query = {}, options = {}) {
|
|
903
|
-
return distinct(this.Model, field, query, options);
|
|
904
|
-
}
|
|
905
|
-
/**
|
|
906
|
-
* Query with custom field lookups ($lookup)
|
|
907
|
-
* Best for: Joins on slugs, SKUs, codes, or other indexed custom fields
|
|
908
|
-
*
|
|
909
|
-
* @example
|
|
910
|
-
* ```typescript
|
|
911
|
-
* // Join employees with departments using slug instead of ObjectId
|
|
912
|
-
* const employees = await employeeRepo.lookupPopulate({
|
|
913
|
-
* filters: { status: 'active' },
|
|
914
|
-
* lookups: [
|
|
915
|
-
* {
|
|
916
|
-
* from: 'departments',
|
|
917
|
-
* localField: 'departmentSlug',
|
|
918
|
-
* foreignField: 'slug',
|
|
919
|
-
* as: 'department',
|
|
920
|
-
* single: true
|
|
921
|
-
* }
|
|
922
|
-
* ],
|
|
923
|
-
* sort: '-createdAt',
|
|
924
|
-
* page: 1,
|
|
925
|
-
* limit: 50
|
|
926
|
-
* });
|
|
927
|
-
* ```
|
|
928
|
-
*/
|
|
929
|
-
async lookupPopulate(options) {
|
|
930
|
-
const context = await this._buildContext("lookupPopulate", options);
|
|
931
|
-
try {
|
|
932
|
-
const builder = new AggregationBuilder();
|
|
933
|
-
const filters = context.filters ?? options.filters;
|
|
934
|
-
if (filters && Object.keys(filters).length > 0) builder.match(filters);
|
|
935
|
-
builder.multiLookup(options.lookups);
|
|
936
|
-
if (options.sort) builder.sort(this._parseSort(options.sort));
|
|
937
|
-
const page = options.page || 1;
|
|
938
|
-
const limit = options.limit || this._pagination.config.defaultLimit || 20;
|
|
939
|
-
const skip = (page - 1) * limit;
|
|
940
|
-
const SAFE_LIMIT = 1e3;
|
|
941
|
-
const SAFE_MAX_OFFSET = 1e4;
|
|
942
|
-
if (limit > SAFE_LIMIT) warn(`[mongokit] Large limit (${limit}) in lookupPopulate. $facet results must be <16MB. Consider using smaller limits or stream-based pagination for large datasets.`);
|
|
943
|
-
if (skip > SAFE_MAX_OFFSET) warn(`[mongokit] Large offset (${skip}) in lookupPopulate. $facet with high offsets can exceed 16MB. For deep pagination, consider using keyset/cursor-based pagination instead.`);
|
|
944
|
-
const dataStages = [{ $skip: skip }, { $limit: limit }];
|
|
945
|
-
if (options.select) {
|
|
946
|
-
let projection;
|
|
947
|
-
if (typeof options.select === "string") {
|
|
948
|
-
projection = {};
|
|
949
|
-
const fields = options.select.split(",").map((f) => f.trim());
|
|
950
|
-
for (const field of fields) if (field.startsWith("-")) projection[field.substring(1)] = 0;
|
|
951
|
-
else projection[field] = 1;
|
|
952
|
-
} else if (Array.isArray(options.select)) {
|
|
953
|
-
projection = {};
|
|
954
|
-
for (const field of options.select) if (field.startsWith("-")) projection[field.substring(1)] = 0;
|
|
955
|
-
else projection[field] = 1;
|
|
956
|
-
} else projection = options.select;
|
|
957
|
-
dataStages.push({ $project: projection });
|
|
958
|
-
}
|
|
959
|
-
builder.facet({
|
|
960
|
-
metadata: [{ $count: "total" }],
|
|
961
|
-
data: dataStages
|
|
962
|
-
});
|
|
963
|
-
const pipeline = builder.build();
|
|
964
|
-
const result = (await this.Model.aggregate(pipeline).session(options.session || null))[0] || {
|
|
965
|
-
metadata: [],
|
|
966
|
-
data: []
|
|
967
|
-
};
|
|
968
|
-
const total = result.metadata[0]?.total || 0;
|
|
969
|
-
const data = result.data || [];
|
|
970
|
-
await this._emitHook("after:lookupPopulate", {
|
|
971
|
-
context,
|
|
972
|
-
result: data
|
|
973
|
-
});
|
|
974
|
-
return {
|
|
975
|
-
data,
|
|
976
|
-
total,
|
|
977
|
-
page,
|
|
978
|
-
limit
|
|
979
|
-
};
|
|
980
|
-
} catch (error) {
|
|
981
|
-
await this._emitErrorHook("error:lookupPopulate", {
|
|
982
|
-
context,
|
|
983
|
-
error
|
|
984
|
-
});
|
|
985
|
-
throw this._handleError(error);
|
|
986
|
-
}
|
|
987
|
-
}
|
|
988
|
-
/**
|
|
989
|
-
* Create an aggregation builder for this model
|
|
990
|
-
* Useful for building complex custom aggregations
|
|
991
|
-
*
|
|
992
|
-
* @example
|
|
993
|
-
* ```typescript
|
|
994
|
-
* const pipeline = repo.buildAggregation()
|
|
995
|
-
* .match({ status: 'active' })
|
|
996
|
-
* .lookup('departments', 'deptSlug', 'slug', 'department', true)
|
|
997
|
-
* .group({ _id: '$department', count: { $sum: 1 } })
|
|
998
|
-
* .sort({ count: -1 })
|
|
999
|
-
* .build();
|
|
1000
|
-
*
|
|
1001
|
-
* const results = await repo.Model.aggregate(pipeline);
|
|
1002
|
-
* ```
|
|
1003
|
-
*/
|
|
1004
|
-
buildAggregation() {
|
|
1005
|
-
return new AggregationBuilder();
|
|
1006
|
-
}
|
|
1007
|
-
/**
|
|
1008
|
-
* Create a lookup builder
|
|
1009
|
-
* Useful for building $lookup stages independently
|
|
1010
|
-
*
|
|
1011
|
-
* @example
|
|
1012
|
-
* ```typescript
|
|
1013
|
-
* const lookupStages = repo.buildLookup('departments')
|
|
1014
|
-
* .localField('deptSlug')
|
|
1015
|
-
* .foreignField('slug')
|
|
1016
|
-
* .as('department')
|
|
1017
|
-
* .single()
|
|
1018
|
-
* .build();
|
|
1019
|
-
*
|
|
1020
|
-
* const pipeline = [
|
|
1021
|
-
* { $match: { status: 'active' } },
|
|
1022
|
-
* ...lookupStages
|
|
1023
|
-
* ];
|
|
1024
|
-
* ```
|
|
1025
|
-
*/
|
|
1026
|
-
buildLookup(from) {
|
|
1027
|
-
return new LookupBuilder(from);
|
|
1028
|
-
}
|
|
1029
|
-
/**
|
|
1030
|
-
* Execute callback within a transaction with automatic retry on transient failures.
|
|
1031
|
-
*
|
|
1032
|
-
* Uses the MongoDB driver's `session.withTransaction()` which automatically retries
|
|
1033
|
-
* on `TransientTransactionError` and `UnknownTransactionCommitResult`.
|
|
1034
|
-
*
|
|
1035
|
-
* The callback always receives a `ClientSession`. When `allowFallback` is true
|
|
1036
|
-
* and the MongoDB deployment doesn't support transactions (e.g., standalone),
|
|
1037
|
-
* the callback runs without a transaction on the same session.
|
|
1038
|
-
*
|
|
1039
|
-
* @param callback - Receives a `ClientSession` to pass to repository operations
|
|
1040
|
-
* @param options.allowFallback - Run without transaction on standalone MongoDB (default: false)
|
|
1041
|
-
* @param options.onFallback - Called when falling back to non-transactional execution
|
|
1042
|
-
* @param options.transactionOptions - MongoDB driver transaction options (readConcern, writeConcern, etc.)
|
|
1043
|
-
*
|
|
1044
|
-
* @example
|
|
1045
|
-
* ```typescript
|
|
1046
|
-
* const result = await repo.withTransaction(async (session) => {
|
|
1047
|
-
* const order = await repo.create({ total: 100 }, { session });
|
|
1048
|
-
* await paymentRepo.create({ orderId: order._id }, { session });
|
|
1049
|
-
* return order;
|
|
1050
|
-
* });
|
|
1051
|
-
*
|
|
1052
|
-
* // With fallback for standalone/dev environments
|
|
1053
|
-
* await repo.withTransaction(callback, {
|
|
1054
|
-
* allowFallback: true,
|
|
1055
|
-
* onFallback: (err) => logger.warn('Running without transaction', err),
|
|
1056
|
-
* });
|
|
1057
|
-
* ```
|
|
1058
|
-
*/
|
|
1059
|
-
async withTransaction(callback, options = {}) {
|
|
1060
|
-
const session = await mongoose.startSession();
|
|
1061
|
-
try {
|
|
1062
|
-
return await session.withTransaction(() => callback(session), options.transactionOptions);
|
|
1063
|
-
} catch (error) {
|
|
1064
|
-
const err = error;
|
|
1065
|
-
if (options.allowFallback && this._isTransactionUnsupported(err)) {
|
|
1066
|
-
options.onFallback?.(err);
|
|
1067
|
-
return await callback(session);
|
|
1068
|
-
}
|
|
1069
|
-
throw err;
|
|
1070
|
-
} finally {
|
|
1071
|
-
await session.endSession();
|
|
1072
|
-
}
|
|
1073
|
-
}
|
|
1074
|
-
_isTransactionUnsupported(error) {
|
|
1075
|
-
const message = (error.message || "").toLowerCase();
|
|
1076
|
-
return message.includes("transaction numbers are only allowed on a replica set member") || message.includes("replica set") || message.includes("mongos");
|
|
1077
|
-
}
|
|
1078
|
-
/**
|
|
1079
|
-
* Execute custom query with event emission
|
|
1080
|
-
*/
|
|
1081
|
-
async _executeQuery(buildQuery) {
|
|
1082
|
-
const operation = buildQuery.name || "custom";
|
|
1083
|
-
const context = await this._buildContext(operation, {});
|
|
1084
|
-
try {
|
|
1085
|
-
const result = await buildQuery(this.Model);
|
|
1086
|
-
await this._emitHook(`after:${operation}`, {
|
|
1087
|
-
context,
|
|
1088
|
-
result
|
|
1089
|
-
});
|
|
1090
|
-
return result;
|
|
1091
|
-
} catch (error) {
|
|
1092
|
-
await this._emitErrorHook(`error:${operation}`, {
|
|
1093
|
-
context,
|
|
1094
|
-
error
|
|
1095
|
-
});
|
|
1096
|
-
throw this._handleError(error);
|
|
1097
|
-
}
|
|
1098
|
-
}
|
|
1099
|
-
/**
|
|
1100
|
-
* Build operation context and run before hooks
|
|
1101
|
-
*/
|
|
1102
|
-
async _buildContext(operation, options) {
|
|
1103
|
-
const context = {
|
|
1104
|
-
operation,
|
|
1105
|
-
model: this.model,
|
|
1106
|
-
...options
|
|
1107
|
-
};
|
|
1108
|
-
const event = `before:${operation}`;
|
|
1109
|
-
const hooks = this._hooks.get(event) || [];
|
|
1110
|
-
for (const hook of hooks) await hook(context);
|
|
1111
|
-
return context;
|
|
1112
|
-
}
|
|
1113
|
-
/**
|
|
1114
|
-
* Parse sort string or object
|
|
1115
|
-
*/
|
|
1116
|
-
_parseSort(sort) {
|
|
1117
|
-
if (!sort) return { createdAt: -1 };
|
|
1118
|
-
if (typeof sort === "object") {
|
|
1119
|
-
if (Object.keys(sort).length === 0) return { createdAt: -1 };
|
|
1120
|
-
return sort;
|
|
1121
|
-
}
|
|
1122
|
-
const sortObj = {};
|
|
1123
|
-
const fields = sort.split(",").map((s) => s.trim());
|
|
1124
|
-
for (const field of fields) if (field.startsWith("-")) sortObj[field.substring(1)] = -1;
|
|
1125
|
-
else sortObj[field] = 1;
|
|
1126
|
-
return sortObj;
|
|
1127
|
-
}
|
|
1128
|
-
/**
|
|
1129
|
-
* Parse populate specification
|
|
1130
|
-
*/
|
|
1131
|
-
_parsePopulate(populate) {
|
|
1132
|
-
if (!populate) return [];
|
|
1133
|
-
if (typeof populate === "string") return populate.split(",").map((p) => p.trim());
|
|
1134
|
-
if (Array.isArray(populate)) return populate.map((p) => typeof p === "string" ? p.trim() : p);
|
|
1135
|
-
return [populate];
|
|
1136
|
-
}
|
|
1137
|
-
/**
|
|
1138
|
-
* Handle errors with proper HTTP status codes
|
|
1139
|
-
*/
|
|
1140
|
-
_handleError(error) {
|
|
1141
|
-
if (error instanceof mongoose.Error.ValidationError) return createError(400, `Validation Error: ${Object.values(error.errors).map((err) => err.message).join(", ")}`);
|
|
1142
|
-
if (error instanceof mongoose.Error.CastError) return createError(400, `Invalid ${error.path}: ${error.value}`);
|
|
1143
|
-
if (error.status && error.message) return error;
|
|
1144
|
-
return createError(500, error.message || "Internal Server Error");
|
|
1145
|
-
}
|
|
1146
|
-
};
|
|
1147
|
-
|
|
1148
|
-
//#endregion
|
|
1149
10
|
//#region src/query/QueryParser.ts
|
|
1150
11
|
/**
|
|
1151
12
|
* Modern Query Parser - URL to MongoDB Query Transpiler
|
|
@@ -2049,4 +910,4 @@ function createRepository(Model, plugins = [], paginationConfig = {}, options =
|
|
|
2049
910
|
var src_default = Repository;
|
|
2050
911
|
|
|
2051
912
|
//#endregion
|
|
2052
|
-
export { AggregationBuilder, AuditTrailQuery, LookupBuilder, PaginationEngine, QueryParser, Repository, actions_exports as actions, aggregateHelpersPlugin, auditLogPlugin, auditTrailPlugin, autoInject, batchOperationsPlugin, blockIf, buildCrudSchemasFromModel, buildCrudSchemasFromMongooseSchema, cachePlugin, cascadePlugin, configureLogger, createError, createFieldPreset, createMemoryCache, createRepository, customIdPlugin, dateSequentialId, src_default as default, elasticSearchPlugin, fieldFilterPlugin, filterResponseData, getFieldsForUser, getImmutableFields, getMongooseProjection, getNextSequence, getSystemManagedFields, immutableField, isFieldUpdateAllowed, methodRegistryPlugin, mongoOperationsPlugin, multiTenantPlugin, observabilityPlugin, prefixedId, requireField, sequentialId, softDeletePlugin, subdocumentPlugin, timestampPlugin, uniqueField, validateUpdateBody, validationChainPlugin };
|
|
913
|
+
export { AggregationBuilder, AuditTrailQuery, HOOK_PRIORITY, LookupBuilder, PaginationEngine, QueryParser, Repository, actions_exports as actions, aggregateHelpersPlugin, auditLogPlugin, auditTrailPlugin, autoInject, batchOperationsPlugin, blockIf, buildCrudSchemasFromModel, buildCrudSchemasFromMongooseSchema, cachePlugin, cascadePlugin, configureLogger, createError, createFieldPreset, createMemoryCache, createRepository, customIdPlugin, dateSequentialId, src_default as default, elasticSearchPlugin, fieldFilterPlugin, filterResponseData, getFieldsForUser, getImmutableFields, getMongooseProjection, getNextSequence, getSystemManagedFields, immutableField, isFieldUpdateAllowed, methodRegistryPlugin, mongoOperationsPlugin, multiTenantPlugin, observabilityPlugin, prefixedId, requireField, sequentialId, softDeletePlugin, subdocumentPlugin, timestampPlugin, uniqueField, validateUpdateBody, validationChainPlugin };
|