@classytic/mongokit 3.1.6 → 3.2.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.
- package/README.md +89 -4
- package/dist/actions/index.d.ts +2 -2
- package/dist/actions/index.js +5 -3
- package/dist/ai/index.d.ts +175 -0
- package/dist/ai/index.js +206 -0
- package/dist/chunks/{chunk-M2XHQGZB.js → chunk-44KXLGPO.js} +28 -1
- package/dist/chunks/{chunk-SAKSLT47.js → chunk-5G42WJHC.js} +280 -13
- package/dist/chunks/{chunk-CSLJ2PL2.js → chunk-B64F5ZWE.js} +145 -11
- package/dist/chunks/{chunk-CF6FLC2G.js → chunk-GZBKEPVE.js} +2 -2
- package/dist/chunks/chunk-JWUAVZ3L.js +8 -0
- package/dist/chunks/{chunk-IT7DCOKR.js → chunk-UE2IEXZJ.js} +15 -8
- package/dist/chunks/chunk-URLJFIR7.js +22 -0
- package/dist/chunks/chunk-WSFCRVEQ.js +7 -0
- package/dist/{index-BXSSv1pW.d.ts → index-BDn5fSTE.d.ts} +13 -1
- package/dist/index.d.ts +146 -41
- package/dist/index.js +294 -296
- package/dist/{mongooseToJsonSchema-Cc5AwuDu.d.ts → mongooseToJsonSchema-CaRF_bCN.d.ts} +33 -2
- package/dist/pagination/PaginationEngine.d.ts +1 -1
- package/dist/pagination/PaginationEngine.js +3 -2
- package/dist/plugins/index.d.ts +125 -2
- package/dist/plugins/index.js +5 -3
- package/dist/{types-B5Uv6Ak7.d.ts → types-Jni1KgkP.d.ts} +18 -11
- package/dist/utils/index.d.ts +2 -2
- package/dist/utils/index.js +4 -2
- package/package.json +10 -2
- package/dist/chunks/chunk-VJXDGP3C.js +0 -14
package/dist/index.js
CHANGED
|
@@ -1,224 +1,18 @@
|
|
|
1
|
-
import { getById, getByQuery, getOrCreate, count, exists, update, deleteById, aggregate, distinct } from './chunks/chunk-
|
|
2
|
-
export { actions_exports as actions } from './chunks/chunk-
|
|
3
|
-
import { PaginationEngine } from './chunks/chunk-
|
|
4
|
-
export { PaginationEngine } from './chunks/chunk-
|
|
5
|
-
export { aggregateHelpersPlugin, auditLogPlugin, autoInject, batchOperationsPlugin, blockIf, cachePlugin, cascadePlugin, fieldFilterPlugin, immutableField, methodRegistryPlugin, mongoOperationsPlugin, requireField, softDeletePlugin, subdocumentPlugin, timestampPlugin, uniqueField, validationChainPlugin } from './chunks/chunk-
|
|
6
|
-
import { create, createMany } from './chunks/chunk-
|
|
7
|
-
export { buildCrudSchemasFromModel, buildCrudSchemasFromMongooseSchema, createMemoryCache, getImmutableFields, getSystemManagedFields, isFieldUpdateAllowed, validateUpdateBody } from './chunks/chunk-
|
|
1
|
+
import { LookupBuilder, getById, getByQuery, getOrCreate, count, exists, update, deleteById, aggregate, distinct } from './chunks/chunk-5G42WJHC.js';
|
|
2
|
+
export { LookupBuilder, actions_exports as actions } from './chunks/chunk-5G42WJHC.js';
|
|
3
|
+
import { PaginationEngine } from './chunks/chunk-44KXLGPO.js';
|
|
4
|
+
export { PaginationEngine } from './chunks/chunk-44KXLGPO.js';
|
|
5
|
+
export { aggregateHelpersPlugin, auditLogPlugin, autoInject, batchOperationsPlugin, blockIf, cachePlugin, cascadePlugin, fieldFilterPlugin, immutableField, methodRegistryPlugin, mongoOperationsPlugin, multiTenantPlugin, observabilityPlugin, requireField, softDeletePlugin, subdocumentPlugin, timestampPlugin, uniqueField, validationChainPlugin } from './chunks/chunk-B64F5ZWE.js';
|
|
6
|
+
import { create, createMany } from './chunks/chunk-GZBKEPVE.js';
|
|
7
|
+
export { buildCrudSchemasFromModel, buildCrudSchemasFromMongooseSchema, createMemoryCache, getImmutableFields, getSystemManagedFields, isFieldUpdateAllowed, validateUpdateBody } from './chunks/chunk-UE2IEXZJ.js';
|
|
8
8
|
export { createFieldPreset, filterResponseData, getFieldsForUser, getMongooseProjection } from './chunks/chunk-2ZN65ZOP.js';
|
|
9
|
-
import {
|
|
10
|
-
export {
|
|
9
|
+
import { warn } from './chunks/chunk-URLJFIR7.js';
|
|
10
|
+
export { configureLogger } from './chunks/chunk-URLJFIR7.js';
|
|
11
|
+
import { createError } from './chunks/chunk-JWUAVZ3L.js';
|
|
12
|
+
export { createError } from './chunks/chunk-JWUAVZ3L.js';
|
|
13
|
+
import './chunks/chunk-WSFCRVEQ.js';
|
|
11
14
|
import mongoose from 'mongoose';
|
|
12
15
|
|
|
13
|
-
// src/query/LookupBuilder.ts
|
|
14
|
-
var LookupBuilder = class _LookupBuilder {
|
|
15
|
-
options = {};
|
|
16
|
-
constructor(from) {
|
|
17
|
-
if (from) this.options.from = from;
|
|
18
|
-
}
|
|
19
|
-
/**
|
|
20
|
-
* Set the collection to join with
|
|
21
|
-
*/
|
|
22
|
-
from(collection) {
|
|
23
|
-
this.options.from = collection;
|
|
24
|
-
return this;
|
|
25
|
-
}
|
|
26
|
-
/**
|
|
27
|
-
* Set the local field (source collection)
|
|
28
|
-
* IMPORTANT: This field should be indexed for optimal performance
|
|
29
|
-
*/
|
|
30
|
-
localField(field) {
|
|
31
|
-
this.options.localField = field;
|
|
32
|
-
return this;
|
|
33
|
-
}
|
|
34
|
-
/**
|
|
35
|
-
* Set the foreign field (target collection)
|
|
36
|
-
* IMPORTANT: This field should be indexed (preferably unique) for optimal performance
|
|
37
|
-
*/
|
|
38
|
-
foreignField(field) {
|
|
39
|
-
this.options.foreignField = field;
|
|
40
|
-
return this;
|
|
41
|
-
}
|
|
42
|
-
/**
|
|
43
|
-
* Set the output field name
|
|
44
|
-
* Defaults to the collection name if not specified
|
|
45
|
-
*/
|
|
46
|
-
as(fieldName) {
|
|
47
|
-
this.options.as = fieldName;
|
|
48
|
-
return this;
|
|
49
|
-
}
|
|
50
|
-
/**
|
|
51
|
-
* Mark this lookup as returning a single document
|
|
52
|
-
* Automatically unwraps the array result to a single object or null
|
|
53
|
-
*/
|
|
54
|
-
single(isSingle = true) {
|
|
55
|
-
this.options.single = isSingle;
|
|
56
|
-
return this;
|
|
57
|
-
}
|
|
58
|
-
/**
|
|
59
|
-
* Add a pipeline to filter/transform joined documents
|
|
60
|
-
* Useful for filtering, sorting, or limiting joined results
|
|
61
|
-
*
|
|
62
|
-
* @example
|
|
63
|
-
* ```typescript
|
|
64
|
-
* lookup.pipeline([
|
|
65
|
-
* { $match: { status: 'active' } },
|
|
66
|
-
* { $sort: { priority: -1 } },
|
|
67
|
-
* { $limit: 5 }
|
|
68
|
-
* ]);
|
|
69
|
-
* ```
|
|
70
|
-
*/
|
|
71
|
-
pipeline(stages) {
|
|
72
|
-
this.options.pipeline = stages;
|
|
73
|
-
return this;
|
|
74
|
-
}
|
|
75
|
-
/**
|
|
76
|
-
* Set let variables for use in pipeline
|
|
77
|
-
* Allows referencing local document fields in the pipeline
|
|
78
|
-
*/
|
|
79
|
-
let(variables) {
|
|
80
|
-
this.options.let = variables;
|
|
81
|
-
return this;
|
|
82
|
-
}
|
|
83
|
-
/**
|
|
84
|
-
* Build the $lookup aggregation stage(s)
|
|
85
|
-
* Returns an array of pipeline stages including $lookup and optional $unwind
|
|
86
|
-
*
|
|
87
|
-
* IMPORTANT: MongoDB $lookup has two mutually exclusive forms:
|
|
88
|
-
* 1. Simple form: { from, localField, foreignField, as }
|
|
89
|
-
* 2. Pipeline form: { from, let, pipeline, as }
|
|
90
|
-
*
|
|
91
|
-
* When pipeline or let is specified, we use the pipeline form.
|
|
92
|
-
* Otherwise, we use the simpler localField/foreignField form.
|
|
93
|
-
*/
|
|
94
|
-
build() {
|
|
95
|
-
const { from, localField, foreignField, as, single, pipeline, let: letVars } = this.options;
|
|
96
|
-
if (!from) {
|
|
97
|
-
throw new Error('LookupBuilder: "from" collection is required');
|
|
98
|
-
}
|
|
99
|
-
const outputField = as || from;
|
|
100
|
-
const stages = [];
|
|
101
|
-
const usePipelineForm = pipeline || letVars;
|
|
102
|
-
let lookupStage;
|
|
103
|
-
if (usePipelineForm) {
|
|
104
|
-
if (!pipeline || pipeline.length === 0) {
|
|
105
|
-
if (!localField || !foreignField) {
|
|
106
|
-
throw new Error(
|
|
107
|
-
"LookupBuilder: When using pipeline form without a custom pipeline, both localField and foreignField are required to auto-generate the pipeline"
|
|
108
|
-
);
|
|
109
|
-
}
|
|
110
|
-
const autoPipeline = [
|
|
111
|
-
{
|
|
112
|
-
$match: {
|
|
113
|
-
$expr: {
|
|
114
|
-
$eq: [`$${foreignField}`, `$$${localField}`]
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
];
|
|
119
|
-
lookupStage = {
|
|
120
|
-
$lookup: {
|
|
121
|
-
from,
|
|
122
|
-
let: { [localField]: `$${localField}`, ...letVars || {} },
|
|
123
|
-
pipeline: autoPipeline,
|
|
124
|
-
as: outputField
|
|
125
|
-
}
|
|
126
|
-
};
|
|
127
|
-
} else {
|
|
128
|
-
lookupStage = {
|
|
129
|
-
$lookup: {
|
|
130
|
-
from,
|
|
131
|
-
...letVars && { let: letVars },
|
|
132
|
-
pipeline,
|
|
133
|
-
as: outputField
|
|
134
|
-
}
|
|
135
|
-
};
|
|
136
|
-
}
|
|
137
|
-
} else {
|
|
138
|
-
if (!localField || !foreignField) {
|
|
139
|
-
throw new Error("LookupBuilder: localField and foreignField are required for simple lookup");
|
|
140
|
-
}
|
|
141
|
-
lookupStage = {
|
|
142
|
-
$lookup: {
|
|
143
|
-
from,
|
|
144
|
-
localField,
|
|
145
|
-
foreignField,
|
|
146
|
-
as: outputField
|
|
147
|
-
}
|
|
148
|
-
};
|
|
149
|
-
}
|
|
150
|
-
stages.push(lookupStage);
|
|
151
|
-
if (single) {
|
|
152
|
-
stages.push({
|
|
153
|
-
$unwind: {
|
|
154
|
-
path: `$${outputField}`,
|
|
155
|
-
preserveNullAndEmptyArrays: true
|
|
156
|
-
// Keep documents even if no match found
|
|
157
|
-
}
|
|
158
|
-
});
|
|
159
|
-
}
|
|
160
|
-
return stages;
|
|
161
|
-
}
|
|
162
|
-
/**
|
|
163
|
-
* Build and return only the $lookup stage (without $unwind)
|
|
164
|
-
* Useful when you want to handle unwrapping yourself
|
|
165
|
-
*/
|
|
166
|
-
buildLookupOnly() {
|
|
167
|
-
const stages = this.build();
|
|
168
|
-
return stages[0];
|
|
169
|
-
}
|
|
170
|
-
/**
|
|
171
|
-
* Static helper: Create a simple lookup in one line
|
|
172
|
-
*/
|
|
173
|
-
static simple(from, localField, foreignField, options = {}) {
|
|
174
|
-
return new _LookupBuilder(from).localField(localField).foreignField(foreignField).as(options.as || from).single(options.single || false).build();
|
|
175
|
-
}
|
|
176
|
-
/**
|
|
177
|
-
* Static helper: Create multiple lookups at once
|
|
178
|
-
*
|
|
179
|
-
* @example
|
|
180
|
-
* ```typescript
|
|
181
|
-
* const pipeline = LookupBuilder.multiple([
|
|
182
|
-
* { from: 'departments', localField: 'deptSlug', foreignField: 'slug', single: true },
|
|
183
|
-
* { from: 'managers', localField: 'managerId', foreignField: '_id', single: true }
|
|
184
|
-
* ]);
|
|
185
|
-
* ```
|
|
186
|
-
*/
|
|
187
|
-
static multiple(lookups) {
|
|
188
|
-
return lookups.flatMap((lookup) => {
|
|
189
|
-
const builder = new _LookupBuilder(lookup.from).localField(lookup.localField).foreignField(lookup.foreignField);
|
|
190
|
-
if (lookup.as) builder.as(lookup.as);
|
|
191
|
-
if (lookup.single) builder.single(lookup.single);
|
|
192
|
-
if (lookup.pipeline) builder.pipeline(lookup.pipeline);
|
|
193
|
-
if (lookup.let) builder.let(lookup.let);
|
|
194
|
-
return builder.build();
|
|
195
|
-
});
|
|
196
|
-
}
|
|
197
|
-
/**
|
|
198
|
-
* Static helper: Create a nested lookup (lookup within lookup)
|
|
199
|
-
* Useful for multi-level joins like Order -> Product -> Category
|
|
200
|
-
*
|
|
201
|
-
* @example
|
|
202
|
-
* ```typescript
|
|
203
|
-
* // Join orders with products, then products with categories
|
|
204
|
-
* const pipeline = LookupBuilder.nested([
|
|
205
|
-
* { from: 'products', localField: 'productSku', foreignField: 'sku', as: 'product', single: true },
|
|
206
|
-
* { from: 'categories', localField: 'product.categorySlug', foreignField: 'slug', as: 'product.category', single: true }
|
|
207
|
-
* ]);
|
|
208
|
-
* ```
|
|
209
|
-
*/
|
|
210
|
-
static nested(lookups) {
|
|
211
|
-
return lookups.flatMap((lookup, index) => {
|
|
212
|
-
const builder = new _LookupBuilder(lookup.from).localField(lookup.localField).foreignField(lookup.foreignField);
|
|
213
|
-
if (lookup.as) builder.as(lookup.as);
|
|
214
|
-
if (lookup.single !== void 0) builder.single(lookup.single);
|
|
215
|
-
if (lookup.pipeline) builder.pipeline(lookup.pipeline);
|
|
216
|
-
if (lookup.let) builder.let(lookup.let);
|
|
217
|
-
return builder.build();
|
|
218
|
-
});
|
|
219
|
-
}
|
|
220
|
-
};
|
|
221
|
-
|
|
222
16
|
// src/query/AggregationBuilder.ts
|
|
223
17
|
function normalizeSortSpec(sortSpec) {
|
|
224
18
|
const normalized = {};
|
|
@@ -235,6 +29,7 @@ function normalizeSortSpec(sortSpec) {
|
|
|
235
29
|
}
|
|
236
30
|
var AggregationBuilder = class _AggregationBuilder {
|
|
237
31
|
pipeline = [];
|
|
32
|
+
_diskUse = false;
|
|
238
33
|
/**
|
|
239
34
|
* Get the current pipeline
|
|
240
35
|
*/
|
|
@@ -247,11 +42,35 @@ var AggregationBuilder = class _AggregationBuilder {
|
|
|
247
42
|
build() {
|
|
248
43
|
return this.get();
|
|
249
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* Build pipeline with execution options (allowDiskUse, etc.)
|
|
47
|
+
*/
|
|
48
|
+
plan() {
|
|
49
|
+
return { pipeline: this.get(), allowDiskUse: this._diskUse };
|
|
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
|
+
}
|
|
250
68
|
/**
|
|
251
69
|
* Reset the pipeline
|
|
252
70
|
*/
|
|
253
71
|
reset() {
|
|
254
72
|
this.pipeline = [];
|
|
73
|
+
this._diskUse = false;
|
|
255
74
|
return this;
|
|
256
75
|
}
|
|
257
76
|
/**
|
|
@@ -543,6 +362,25 @@ var AggregationBuilder = class _AggregationBuilder {
|
|
|
543
362
|
return this;
|
|
544
363
|
}
|
|
545
364
|
// ============================================================
|
|
365
|
+
// EXECUTION OPTIONS
|
|
366
|
+
// ============================================================
|
|
367
|
+
/**
|
|
368
|
+
* Enable allowDiskUse for large aggregations that exceed 100MB memory limit
|
|
369
|
+
*
|
|
370
|
+
* @example
|
|
371
|
+
* ```typescript
|
|
372
|
+
* const results = await new AggregationBuilder()
|
|
373
|
+
* .match({ status: 'active' })
|
|
374
|
+
* .group({ _id: '$category', total: { $sum: '$amount' } })
|
|
375
|
+
* .allowDiskUse()
|
|
376
|
+
* .exec(Model);
|
|
377
|
+
* ```
|
|
378
|
+
*/
|
|
379
|
+
allowDiskUse(enable = true) {
|
|
380
|
+
this._diskUse = enable;
|
|
381
|
+
return this;
|
|
382
|
+
}
|
|
383
|
+
// ============================================================
|
|
546
384
|
// UTILITY METHODS
|
|
547
385
|
// ============================================================
|
|
548
386
|
/**
|
|
@@ -626,6 +464,56 @@ var AggregationBuilder = class _AggregationBuilder {
|
|
|
626
464
|
return this;
|
|
627
465
|
}
|
|
628
466
|
// ============================================================
|
|
467
|
+
// ATLAS VECTOR SEARCH (MongoDB Atlas 7.0+)
|
|
468
|
+
// ============================================================
|
|
469
|
+
/**
|
|
470
|
+
* $vectorSearch - Semantic similarity search using vector embeddings (Atlas only)
|
|
471
|
+
*
|
|
472
|
+
* Requires an Atlas Vector Search index on the target field.
|
|
473
|
+
* Must be the first stage in the pipeline.
|
|
474
|
+
*
|
|
475
|
+
* @example
|
|
476
|
+
* ```typescript
|
|
477
|
+
* const results = await new AggregationBuilder()
|
|
478
|
+
* .vectorSearch({
|
|
479
|
+
* index: 'vector_index',
|
|
480
|
+
* path: 'embedding',
|
|
481
|
+
* queryVector: await getEmbedding('running shoes'),
|
|
482
|
+
* limit: 10,
|
|
483
|
+
* numCandidates: 100,
|
|
484
|
+
* filter: { category: 'footwear' }
|
|
485
|
+
* })
|
|
486
|
+
* .project({ embedding: 0, score: { $meta: 'vectorSearchScore' } })
|
|
487
|
+
* .exec(ProductModel);
|
|
488
|
+
* ```
|
|
489
|
+
*/
|
|
490
|
+
vectorSearch(options) {
|
|
491
|
+
if (this.pipeline.length > 0) {
|
|
492
|
+
throw new Error("[mongokit] $vectorSearch must be the first stage in the pipeline");
|
|
493
|
+
}
|
|
494
|
+
const rawCandidates = options.numCandidates ?? Math.max(options.limit * 10, 100);
|
|
495
|
+
const numCandidates = Math.min(Math.max(rawCandidates, options.limit), 1e4);
|
|
496
|
+
this.pipeline.push({
|
|
497
|
+
$vectorSearch: {
|
|
498
|
+
index: options.index,
|
|
499
|
+
path: options.path,
|
|
500
|
+
queryVector: options.queryVector,
|
|
501
|
+
numCandidates,
|
|
502
|
+
limit: options.limit,
|
|
503
|
+
...options.filter && { filter: options.filter },
|
|
504
|
+
...options.exact && { exact: options.exact }
|
|
505
|
+
}
|
|
506
|
+
});
|
|
507
|
+
return this;
|
|
508
|
+
}
|
|
509
|
+
/**
|
|
510
|
+
* Add vectorSearchScore as a field after $vectorSearch
|
|
511
|
+
* Convenience for `.addFields({ score: { $meta: 'vectorSearchScore' } })`
|
|
512
|
+
*/
|
|
513
|
+
withVectorScore(fieldName = "score") {
|
|
514
|
+
return this.addFields({ [fieldName]: { $meta: "vectorSearchScore" } });
|
|
515
|
+
}
|
|
516
|
+
// ============================================================
|
|
629
517
|
// HELPER FACTORY METHODS
|
|
630
518
|
// ============================================================
|
|
631
519
|
/**
|
|
@@ -680,6 +568,28 @@ var Repository = class {
|
|
|
680
568
|
this._hooks.get(event).push(listener);
|
|
681
569
|
return this;
|
|
682
570
|
}
|
|
571
|
+
/**
|
|
572
|
+
* Remove a specific event listener
|
|
573
|
+
*/
|
|
574
|
+
off(event, listener) {
|
|
575
|
+
const listeners = this._hooks.get(event);
|
|
576
|
+
if (listeners) {
|
|
577
|
+
const idx = listeners.indexOf(listener);
|
|
578
|
+
if (idx !== -1) listeners.splice(idx, 1);
|
|
579
|
+
}
|
|
580
|
+
return this;
|
|
581
|
+
}
|
|
582
|
+
/**
|
|
583
|
+
* Remove all listeners for an event, or all listeners entirely
|
|
584
|
+
*/
|
|
585
|
+
removeAllListeners(event) {
|
|
586
|
+
if (event) {
|
|
587
|
+
this._hooks.delete(event);
|
|
588
|
+
} else {
|
|
589
|
+
this._hooks.clear();
|
|
590
|
+
}
|
|
591
|
+
return this;
|
|
592
|
+
}
|
|
683
593
|
/**
|
|
684
594
|
* Emit event (sync - for backwards compatibility)
|
|
685
595
|
*/
|
|
@@ -761,9 +671,14 @@ var Repository = class {
|
|
|
761
671
|
if (context._cacheHit) {
|
|
762
672
|
return context._cachedResult;
|
|
763
673
|
}
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
674
|
+
try {
|
|
675
|
+
const result = await getById(this.Model, id, context);
|
|
676
|
+
await this._emitHook("after:getById", { context, result });
|
|
677
|
+
return result;
|
|
678
|
+
} catch (error) {
|
|
679
|
+
await this._emitErrorHook("error:getById", { context, error });
|
|
680
|
+
throw this._handleError(error);
|
|
681
|
+
}
|
|
767
682
|
}
|
|
768
683
|
/**
|
|
769
684
|
* Get single document by query
|
|
@@ -775,9 +690,14 @@ var Repository = class {
|
|
|
775
690
|
return context._cachedResult;
|
|
776
691
|
}
|
|
777
692
|
const finalQuery = context.query || query;
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
693
|
+
try {
|
|
694
|
+
const result = await getByQuery(this.Model, finalQuery, context);
|
|
695
|
+
await this._emitHook("after:getByQuery", { context, result });
|
|
696
|
+
return result;
|
|
697
|
+
} catch (error) {
|
|
698
|
+
await this._emitErrorHook("error:getByQuery", { context, error });
|
|
699
|
+
throw this._handleError(error);
|
|
700
|
+
}
|
|
781
701
|
}
|
|
782
702
|
/**
|
|
783
703
|
* Unified pagination - auto-detects offset vs keyset based on params
|
|
@@ -807,14 +727,13 @@ var Repository = class {
|
|
|
807
727
|
if (context._cacheHit) {
|
|
808
728
|
return context._cachedResult;
|
|
809
729
|
}
|
|
810
|
-
const
|
|
811
|
-
const
|
|
812
|
-
const
|
|
813
|
-
const
|
|
814
|
-
const
|
|
815
|
-
const
|
|
816
|
-
const
|
|
817
|
-
const limit = params.limit || params.pagination?.limit || this._pagination.config.defaultLimit;
|
|
730
|
+
const filters = context.filters ?? params.filters ?? {};
|
|
731
|
+
const search = context.search ?? params.search;
|
|
732
|
+
const sort = context.sort ?? params.sort ?? "-createdAt";
|
|
733
|
+
const limit = context.limit ?? params.limit ?? params.pagination?.limit ?? this._pagination.config.defaultLimit;
|
|
734
|
+
const page = context.page ?? params.pagination?.page ?? params.page;
|
|
735
|
+
const after = context.after ?? params.cursor ?? params.after;
|
|
736
|
+
const useKeyset = !page && (after || sort !== "-createdAt" && (context.sort ?? params.sort));
|
|
818
737
|
let query = { ...filters };
|
|
819
738
|
if (search) query.$text = { $search: search };
|
|
820
739
|
const populateSpec = options.populateOptions || params.populateOptions || context.populate || options.populate;
|
|
@@ -827,23 +746,26 @@ var Repository = class {
|
|
|
827
746
|
lean: context.lean ?? options.lean ?? true,
|
|
828
747
|
session: options.session
|
|
829
748
|
};
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
}
|
|
749
|
+
try {
|
|
750
|
+
let result;
|
|
751
|
+
if (useKeyset) {
|
|
752
|
+
result = await this._pagination.stream({
|
|
753
|
+
...paginationOptions,
|
|
754
|
+
sort: paginationOptions.sort,
|
|
755
|
+
after
|
|
756
|
+
});
|
|
757
|
+
} else {
|
|
758
|
+
result = await this._pagination.paginate({
|
|
759
|
+
...paginationOptions,
|
|
760
|
+
page: page || 1
|
|
761
|
+
});
|
|
762
|
+
}
|
|
763
|
+
await this._emitHook("after:getAll", { context, result });
|
|
764
|
+
return result;
|
|
765
|
+
} catch (error) {
|
|
766
|
+
await this._emitErrorHook("error:getAll", { context, error });
|
|
767
|
+
throw this._handleError(error);
|
|
844
768
|
}
|
|
845
|
-
await this._emitHook("after:getAll", { context, result });
|
|
846
|
-
return result;
|
|
847
769
|
}
|
|
848
770
|
/**
|
|
849
771
|
* Get or create document
|
|
@@ -888,7 +810,7 @@ var Repository = class {
|
|
|
888
810
|
await this._emitHook("after:delete", { context, result: result2 });
|
|
889
811
|
return result2;
|
|
890
812
|
}
|
|
891
|
-
const result = await deleteById(this.Model, id, options);
|
|
813
|
+
const result = await deleteById(this.Model, id, { session: options.session, query: context.query });
|
|
892
814
|
await this._emitHook("after:delete", { context, result });
|
|
893
815
|
return result;
|
|
894
816
|
} catch (error) {
|
|
@@ -944,8 +866,9 @@ var Repository = class {
|
|
|
944
866
|
const context = await this._buildContext("lookupPopulate", options);
|
|
945
867
|
try {
|
|
946
868
|
const builder = new AggregationBuilder();
|
|
947
|
-
|
|
948
|
-
|
|
869
|
+
const filters = context.filters ?? options.filters;
|
|
870
|
+
if (filters && Object.keys(filters).length > 0) {
|
|
871
|
+
builder.match(filters);
|
|
949
872
|
}
|
|
950
873
|
builder.multiLookup(options.lookups);
|
|
951
874
|
if (options.sort) {
|
|
@@ -957,12 +880,12 @@ var Repository = class {
|
|
|
957
880
|
const SAFE_LIMIT = 1e3;
|
|
958
881
|
const SAFE_MAX_OFFSET = 1e4;
|
|
959
882
|
if (limit > SAFE_LIMIT) {
|
|
960
|
-
|
|
883
|
+
warn(
|
|
961
884
|
`[mongokit] Large limit (${limit}) in lookupPopulate. $facet results must be <16MB. Consider using smaller limits or stream-based pagination for large datasets.`
|
|
962
885
|
);
|
|
963
886
|
}
|
|
964
887
|
if (skip > SAFE_MAX_OFFSET) {
|
|
965
|
-
|
|
888
|
+
warn(
|
|
966
889
|
`[mongokit] Large offset (${skip}) in lookupPopulate. $facet with high offsets can exceed 16MB. For deep pagination, consider using keyset/cursor-based pagination instead.`
|
|
967
890
|
);
|
|
968
891
|
}
|
|
@@ -1059,40 +982,52 @@ var Repository = class {
|
|
|
1059
982
|
return new LookupBuilder(from);
|
|
1060
983
|
}
|
|
1061
984
|
/**
|
|
1062
|
-
* Execute callback within a transaction
|
|
985
|
+
* Execute callback within a transaction with automatic retry on transient failures.
|
|
986
|
+
*
|
|
987
|
+
* Uses the MongoDB driver's `session.withTransaction()` which automatically retries
|
|
988
|
+
* on `TransientTransactionError` and `UnknownTransactionCommitResult`.
|
|
989
|
+
*
|
|
990
|
+
* The callback always receives a `ClientSession`. When `allowFallback` is true
|
|
991
|
+
* and the MongoDB deployment doesn't support transactions (e.g., standalone),
|
|
992
|
+
* the callback runs without a transaction on the same session.
|
|
993
|
+
*
|
|
994
|
+
* @param callback - Receives a `ClientSession` to pass to repository operations
|
|
995
|
+
* @param options.allowFallback - Run without transaction on standalone MongoDB (default: false)
|
|
996
|
+
* @param options.onFallback - Called when falling back to non-transactional execution
|
|
997
|
+
* @param options.transactionOptions - MongoDB driver transaction options (readConcern, writeConcern, etc.)
|
|
998
|
+
*
|
|
999
|
+
* @example
|
|
1000
|
+
* ```typescript
|
|
1001
|
+
* const result = await repo.withTransaction(async (session) => {
|
|
1002
|
+
* const order = await repo.create({ total: 100 }, { session });
|
|
1003
|
+
* await paymentRepo.create({ orderId: order._id }, { session });
|
|
1004
|
+
* return order;
|
|
1005
|
+
* });
|
|
1006
|
+
*
|
|
1007
|
+
* // With fallback for standalone/dev environments
|
|
1008
|
+
* await repo.withTransaction(callback, {
|
|
1009
|
+
* allowFallback: true,
|
|
1010
|
+
* onFallback: (err) => logger.warn('Running without transaction', err),
|
|
1011
|
+
* });
|
|
1012
|
+
* ```
|
|
1063
1013
|
*/
|
|
1064
1014
|
async withTransaction(callback, options = {}) {
|
|
1065
1015
|
const session = await mongoose.startSession();
|
|
1066
|
-
let started = false;
|
|
1067
1016
|
try {
|
|
1068
|
-
session.
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1017
|
+
const result = await session.withTransaction(
|
|
1018
|
+
() => callback(session),
|
|
1019
|
+
options.transactionOptions
|
|
1020
|
+
);
|
|
1072
1021
|
return result;
|
|
1073
1022
|
} catch (error) {
|
|
1074
1023
|
const err = error;
|
|
1075
1024
|
if (options.allowFallback && this._isTransactionUnsupported(err)) {
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
}
|
|
1079
|
-
if (started) {
|
|
1080
|
-
try {
|
|
1081
|
-
await session.abortTransaction();
|
|
1082
|
-
} catch {
|
|
1083
|
-
}
|
|
1084
|
-
}
|
|
1085
|
-
return await callback(null);
|
|
1086
|
-
}
|
|
1087
|
-
if (started) {
|
|
1088
|
-
try {
|
|
1089
|
-
await session.abortTransaction();
|
|
1090
|
-
} catch {
|
|
1091
|
-
}
|
|
1025
|
+
options.onFallback?.(err);
|
|
1026
|
+
return await callback(session);
|
|
1092
1027
|
}
|
|
1093
1028
|
throw err;
|
|
1094
1029
|
} finally {
|
|
1095
|
-
session.endSession();
|
|
1030
|
+
await session.endSession();
|
|
1096
1031
|
}
|
|
1097
1032
|
}
|
|
1098
1033
|
_isTransactionUnsupported(error) {
|
|
@@ -1199,10 +1134,11 @@ var QueryParser = class {
|
|
|
1199
1134
|
enableLookups: options.enableLookups ?? true,
|
|
1200
1135
|
enableAggregations: options.enableAggregations ?? false,
|
|
1201
1136
|
searchMode: options.searchMode ?? "text",
|
|
1202
|
-
searchFields: options.searchFields
|
|
1137
|
+
searchFields: options.searchFields,
|
|
1138
|
+
allowedLookupCollections: options.allowedLookupCollections
|
|
1203
1139
|
};
|
|
1204
1140
|
if (this.options.searchMode === "regex" && (!this.options.searchFields || this.options.searchFields.length === 0)) {
|
|
1205
|
-
|
|
1141
|
+
warn('[mongokit] searchMode "regex" requires searchFields to be specified. Falling back to "text" mode.');
|
|
1206
1142
|
this.options.searchMode = "text";
|
|
1207
1143
|
}
|
|
1208
1144
|
this.dangerousOperators = [
|
|
@@ -1242,7 +1178,7 @@ var QueryParser = class {
|
|
|
1242
1178
|
parsedLimit = 20;
|
|
1243
1179
|
}
|
|
1244
1180
|
if (parsedLimit > this.options.maxLimit) {
|
|
1245
|
-
|
|
1181
|
+
warn(`[mongokit] Limit ${parsedLimit} exceeds maximum ${this.options.maxLimit}, capping to max`);
|
|
1246
1182
|
parsedLimit = this.options.maxLimit;
|
|
1247
1183
|
}
|
|
1248
1184
|
const sanitizedSearch = this._sanitizeSearch(search);
|
|
@@ -1341,7 +1277,7 @@ var QueryParser = class {
|
|
|
1341
1277
|
lookups.push(lookupConfig);
|
|
1342
1278
|
}
|
|
1343
1279
|
} catch (error) {
|
|
1344
|
-
|
|
1280
|
+
warn(`[mongokit] Invalid lookup config for ${collectionName}:`, error);
|
|
1345
1281
|
}
|
|
1346
1282
|
}
|
|
1347
1283
|
return lookups;
|
|
@@ -1352,8 +1288,13 @@ var QueryParser = class {
|
|
|
1352
1288
|
_parseSingleLookup(collectionName, config) {
|
|
1353
1289
|
if (!config) return null;
|
|
1354
1290
|
if (typeof config === "string") {
|
|
1291
|
+
const from = this._pluralize(collectionName);
|
|
1292
|
+
if (this.options.allowedLookupCollections && !this.options.allowedLookupCollections.includes(from)) {
|
|
1293
|
+
warn(`[mongokit] Blocked lookup to disallowed collection: ${from}`);
|
|
1294
|
+
return null;
|
|
1295
|
+
}
|
|
1355
1296
|
return {
|
|
1356
|
-
from
|
|
1297
|
+
from,
|
|
1357
1298
|
localField: `${collectionName}${this._capitalize(config)}`,
|
|
1358
1299
|
foreignField: config,
|
|
1359
1300
|
as: collectionName,
|
|
@@ -1365,8 +1306,12 @@ var QueryParser = class {
|
|
|
1365
1306
|
const from = opts.from || this._pluralize(collectionName);
|
|
1366
1307
|
const localField = opts.localField;
|
|
1367
1308
|
const foreignField = opts.foreignField;
|
|
1309
|
+
if (this.options.allowedLookupCollections && !this.options.allowedLookupCollections.includes(from)) {
|
|
1310
|
+
warn(`[mongokit] Blocked lookup to disallowed collection: ${from}`);
|
|
1311
|
+
return null;
|
|
1312
|
+
}
|
|
1368
1313
|
if (!localField || !foreignField) {
|
|
1369
|
-
|
|
1314
|
+
warn(`[mongokit] Lookup requires localField and foreignField for ${collectionName}`);
|
|
1370
1315
|
return null;
|
|
1371
1316
|
}
|
|
1372
1317
|
return {
|
|
@@ -1375,7 +1320,7 @@ var QueryParser = class {
|
|
|
1375
1320
|
foreignField,
|
|
1376
1321
|
as: opts.as || collectionName,
|
|
1377
1322
|
single: opts.single === true || opts.single === "true",
|
|
1378
|
-
...opts.pipeline && Array.isArray(opts.pipeline) ? { pipeline: opts.pipeline } : {}
|
|
1323
|
+
...opts.pipeline && Array.isArray(opts.pipeline) ? { pipeline: this._sanitizePipeline(opts.pipeline) } : {}
|
|
1379
1324
|
};
|
|
1380
1325
|
}
|
|
1381
1326
|
return null;
|
|
@@ -1413,7 +1358,7 @@ var QueryParser = class {
|
|
|
1413
1358
|
pipeline.push({ $project: config });
|
|
1414
1359
|
}
|
|
1415
1360
|
} catch (error) {
|
|
1416
|
-
|
|
1361
|
+
warn(`[mongokit] Invalid aggregation stage ${stage}:`, error);
|
|
1417
1362
|
}
|
|
1418
1363
|
}
|
|
1419
1364
|
return pipeline.length > 0 ? pipeline : void 0;
|
|
@@ -1479,7 +1424,7 @@ var QueryParser = class {
|
|
|
1479
1424
|
const populateOptions = [];
|
|
1480
1425
|
for (const [path, config] of Object.entries(populateObj)) {
|
|
1481
1426
|
if (path.startsWith("$") || this.dangerousOperators.includes(path)) {
|
|
1482
|
-
|
|
1427
|
+
warn(`[mongokit] Blocked dangerous populate path: ${path}`);
|
|
1483
1428
|
continue;
|
|
1484
1429
|
}
|
|
1485
1430
|
const option = this._parseSinglePopulate(path, config);
|
|
@@ -1496,7 +1441,7 @@ var QueryParser = class {
|
|
|
1496
1441
|
*/
|
|
1497
1442
|
_parseSinglePopulate(path, config, depth = 0) {
|
|
1498
1443
|
if (depth > 5) {
|
|
1499
|
-
|
|
1444
|
+
warn(`[mongokit] Populate depth exceeds maximum (5), truncating at path: ${path}`);
|
|
1500
1445
|
return { path };
|
|
1501
1446
|
}
|
|
1502
1447
|
if (typeof config === "string") {
|
|
@@ -1568,14 +1513,14 @@ var QueryParser = class {
|
|
|
1568
1513
|
*/
|
|
1569
1514
|
_parseFilters(filters, depth = 0) {
|
|
1570
1515
|
if (depth > this.options.maxFilterDepth) {
|
|
1571
|
-
|
|
1516
|
+
warn(`[mongokit] Filter depth ${depth} exceeds maximum ${this.options.maxFilterDepth}, truncating`);
|
|
1572
1517
|
return {};
|
|
1573
1518
|
}
|
|
1574
1519
|
const parsedFilters = {};
|
|
1575
1520
|
const regexFields = {};
|
|
1576
1521
|
for (const [key, value] of Object.entries(filters)) {
|
|
1577
1522
|
if (this.dangerousOperators.includes(key) || key.startsWith("$") && !["$or", "$and"].includes(key)) {
|
|
1578
|
-
|
|
1523
|
+
warn(`[mongokit] Blocked dangerous operator: ${key}`);
|
|
1579
1524
|
continue;
|
|
1580
1525
|
}
|
|
1581
1526
|
if (["page", "limit", "sort", "populate", "search", "select", "lean", "includeDeleted", "lookup", "aggregate", "or", "OR", "$or"].includes(key)) {
|
|
@@ -1585,7 +1530,7 @@ var QueryParser = class {
|
|
|
1585
1530
|
if (operatorMatch) {
|
|
1586
1531
|
const [, , operator] = operatorMatch;
|
|
1587
1532
|
if (this.dangerousOperators.includes("$" + operator)) {
|
|
1588
|
-
|
|
1533
|
+
warn(`[mongokit] Blocked dangerous operator: ${operator}`);
|
|
1589
1534
|
continue;
|
|
1590
1535
|
}
|
|
1591
1536
|
this._handleOperatorSyntax(parsedFilters, regexFields, operatorMatch, value);
|
|
@@ -1624,7 +1569,7 @@ var QueryParser = class {
|
|
|
1624
1569
|
}
|
|
1625
1570
|
const mongoOperator = this._toMongoOperator(operator);
|
|
1626
1571
|
if (this.dangerousOperators.includes(mongoOperator)) {
|
|
1627
|
-
|
|
1572
|
+
warn(`[mongokit] Blocked dangerous operator: ${mongoOperator}`);
|
|
1628
1573
|
return;
|
|
1629
1574
|
}
|
|
1630
1575
|
if (mongoOperator === "$eq") {
|
|
@@ -1657,7 +1602,7 @@ var QueryParser = class {
|
|
|
1657
1602
|
*/
|
|
1658
1603
|
_handleBracketSyntax(field, operators, parsedFilters, depth = 0) {
|
|
1659
1604
|
if (depth > this.options.maxFilterDepth) {
|
|
1660
|
-
|
|
1605
|
+
warn(`[mongokit] Nested filter depth exceeds maximum, skipping field: ${field}`);
|
|
1661
1606
|
return;
|
|
1662
1607
|
}
|
|
1663
1608
|
if (!parsedFilters[field]) {
|
|
@@ -1716,11 +1661,11 @@ var QueryParser = class {
|
|
|
1716
1661
|
if (pattern === null || pattern === void 0) return null;
|
|
1717
1662
|
const patternStr = String(pattern);
|
|
1718
1663
|
if (patternStr.length > this.options.maxRegexLength) {
|
|
1719
|
-
|
|
1664
|
+
warn(`[mongokit] Regex pattern too long, truncating`);
|
|
1720
1665
|
return new RegExp(this._escapeRegex(patternStr.substring(0, this.options.maxRegexLength)), flags);
|
|
1721
1666
|
}
|
|
1722
1667
|
if (this.dangerousRegexPatterns.test(patternStr)) {
|
|
1723
|
-
|
|
1668
|
+
warn("[mongokit] Potentially dangerous regex pattern, escaping");
|
|
1724
1669
|
return new RegExp(this._escapeRegex(patternStr), flags);
|
|
1725
1670
|
}
|
|
1726
1671
|
try {
|
|
@@ -1740,7 +1685,7 @@ var QueryParser = class {
|
|
|
1740
1685
|
const sanitized = {};
|
|
1741
1686
|
for (const [key, value] of Object.entries(config)) {
|
|
1742
1687
|
if (this.dangerousOperators.includes(key)) {
|
|
1743
|
-
|
|
1688
|
+
warn(`[mongokit] Blocked dangerous operator in aggregation: ${key}`);
|
|
1744
1689
|
continue;
|
|
1745
1690
|
}
|
|
1746
1691
|
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
@@ -1758,12 +1703,65 @@ var QueryParser = class {
|
|
|
1758
1703
|
}
|
|
1759
1704
|
return sanitized;
|
|
1760
1705
|
}
|
|
1706
|
+
/**
|
|
1707
|
+
* Sanitize pipeline stages for use in $lookup.
|
|
1708
|
+
* Blocks dangerous stages ($out, $merge, etc.) and recursively sanitizes
|
|
1709
|
+
* operator expressions within $match, $addFields, and $set stages.
|
|
1710
|
+
*/
|
|
1711
|
+
_sanitizePipeline(stages) {
|
|
1712
|
+
const blockedStages = ["$out", "$merge", "$unionWith", "$collStats", "$currentOp", "$listSessions"];
|
|
1713
|
+
const sanitized = [];
|
|
1714
|
+
for (const stage of stages) {
|
|
1715
|
+
if (!stage || typeof stage !== "object") continue;
|
|
1716
|
+
const entries = Object.entries(stage);
|
|
1717
|
+
if (entries.length !== 1) continue;
|
|
1718
|
+
const [op, config] = entries[0];
|
|
1719
|
+
if (blockedStages.includes(op)) {
|
|
1720
|
+
warn(`[mongokit] Blocked dangerous pipeline stage in lookup: ${op}`);
|
|
1721
|
+
continue;
|
|
1722
|
+
}
|
|
1723
|
+
if (op === "$match" && typeof config === "object" && config !== null) {
|
|
1724
|
+
sanitized.push({ $match: this._sanitizeMatchConfig(config) });
|
|
1725
|
+
} else if ((op === "$addFields" || op === "$set") && typeof config === "object" && config !== null) {
|
|
1726
|
+
sanitized.push({ [op]: this._sanitizeExpressions(config) });
|
|
1727
|
+
} else {
|
|
1728
|
+
sanitized.push(stage);
|
|
1729
|
+
}
|
|
1730
|
+
}
|
|
1731
|
+
return sanitized;
|
|
1732
|
+
}
|
|
1733
|
+
/**
|
|
1734
|
+
* Recursively sanitize expression objects, blocking dangerous operators
|
|
1735
|
+
* like $where, $function, $accumulator inside $addFields/$set stages.
|
|
1736
|
+
*/
|
|
1737
|
+
_sanitizeExpressions(config) {
|
|
1738
|
+
const sanitized = {};
|
|
1739
|
+
for (const [key, value] of Object.entries(config)) {
|
|
1740
|
+
if (this.dangerousOperators.includes(key)) {
|
|
1741
|
+
warn(`[mongokit] Blocked dangerous operator in pipeline expression: ${key}`);
|
|
1742
|
+
continue;
|
|
1743
|
+
}
|
|
1744
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
1745
|
+
sanitized[key] = this._sanitizeExpressions(value);
|
|
1746
|
+
} else if (Array.isArray(value)) {
|
|
1747
|
+
sanitized[key] = value.map((item) => {
|
|
1748
|
+
if (item && typeof item === "object" && !Array.isArray(item)) {
|
|
1749
|
+
return this._sanitizeExpressions(item);
|
|
1750
|
+
}
|
|
1751
|
+
return item;
|
|
1752
|
+
});
|
|
1753
|
+
} else {
|
|
1754
|
+
sanitized[key] = value;
|
|
1755
|
+
}
|
|
1756
|
+
}
|
|
1757
|
+
return sanitized;
|
|
1758
|
+
}
|
|
1761
1759
|
_sanitizeSearch(search) {
|
|
1762
1760
|
if (search === null || search === void 0 || search === "") return void 0;
|
|
1763
1761
|
let searchStr = String(search).trim();
|
|
1764
1762
|
if (!searchStr) return void 0;
|
|
1765
1763
|
if (searchStr.length > this.options.maxSearchLength) {
|
|
1766
|
-
|
|
1764
|
+
warn(`[mongokit] Search query too long, truncating`);
|
|
1767
1765
|
searchStr = searchStr.substring(0, this.options.maxSearchLength);
|
|
1768
1766
|
}
|
|
1769
1767
|
return searchStr;
|
|
@@ -1892,4 +1890,4 @@ var index_default = Repository;
|
|
|
1892
1890
|
* ```
|
|
1893
1891
|
*/
|
|
1894
1892
|
|
|
1895
|
-
export { AggregationBuilder,
|
|
1893
|
+
export { AggregationBuilder, QueryParser, Repository, createRepository, index_default as default };
|