@classytic/mongokit 3.0.5 → 3.1.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/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import mongoose4 from 'mongoose';
1
+ import mongoose from 'mongoose';
2
2
 
3
3
  var __defProp = Object.defineProperty;
4
4
  var __export = (target, all) => {
@@ -403,20 +403,51 @@ async function countBy(Model, field, query = {}, options = {}) {
403
403
  return aggregate(Model, pipeline, options);
404
404
  }
405
405
  async function lookup(Model, lookupOptions) {
406
- const { from, localField, foreignField, as, pipeline = [], query = {}, options = {} } = lookupOptions;
406
+ const { from, localField, foreignField, as, pipeline = [], let: letVars, query = {}, options = {} } = lookupOptions;
407
407
  const aggPipeline = [];
408
408
  if (Object.keys(query).length > 0) {
409
409
  aggPipeline.push({ $match: query });
410
410
  }
411
- aggPipeline.push({
412
- $lookup: {
413
- from,
414
- localField,
415
- foreignField,
416
- as,
417
- ...pipeline.length > 0 ? { pipeline } : {}
411
+ const usePipelineForm = pipeline.length > 0 || letVars;
412
+ if (usePipelineForm) {
413
+ if (pipeline.length === 0 && localField && foreignField) {
414
+ const autoPipeline = [
415
+ {
416
+ $match: {
417
+ $expr: {
418
+ $eq: [`$${foreignField}`, `$$${localField}`]
419
+ }
420
+ }
421
+ }
422
+ ];
423
+ aggPipeline.push({
424
+ $lookup: {
425
+ from,
426
+ let: { [localField]: `$${localField}`, ...letVars || {} },
427
+ pipeline: autoPipeline,
428
+ as
429
+ }
430
+ });
431
+ } else {
432
+ aggPipeline.push({
433
+ $lookup: {
434
+ from,
435
+ ...letVars && { let: letVars },
436
+ pipeline,
437
+ as
438
+ }
439
+ });
418
440
  }
419
- });
441
+ } else {
442
+ aggPipeline.push({
443
+ $lookup: {
444
+ from,
445
+ localField,
446
+ foreignField,
447
+ as
448
+ }
449
+ });
450
+ }
420
451
  return aggregate(Model, aggPipeline, options);
421
452
  }
422
453
  async function unwind(Model, field, options = {}) {
@@ -521,12 +552,12 @@ function validateCursorVersion(cursorVersion, expectedVersion) {
521
552
  }
522
553
  function serializeValue(value) {
523
554
  if (value instanceof Date) return value.toISOString();
524
- if (value instanceof mongoose4.Types.ObjectId) return value.toString();
555
+ if (value instanceof mongoose.Types.ObjectId) return value.toString();
525
556
  return value;
526
557
  }
527
558
  function getValueType(value) {
528
559
  if (value instanceof Date) return "date";
529
- if (value instanceof mongoose4.Types.ObjectId) return "objectid";
560
+ if (value instanceof mongoose.Types.ObjectId) return "objectid";
530
561
  if (typeof value === "boolean") return "boolean";
531
562
  if (typeof value === "number") return "number";
532
563
  if (typeof value === "string") return "string";
@@ -537,7 +568,7 @@ function rehydrateValue(serialized, type) {
537
568
  case "date":
538
569
  return new Date(serialized);
539
570
  case "objectid":
540
- return new mongoose4.Types.ObjectId(serialized);
571
+ return new mongoose.Types.ObjectId(serialized);
541
572
  case "boolean":
542
573
  return Boolean(serialized);
543
574
  case "number":
@@ -837,6 +868,640 @@ var PaginationEngine = class {
837
868
  }
838
869
  };
839
870
 
871
+ // src/query/LookupBuilder.ts
872
+ var LookupBuilder = class _LookupBuilder {
873
+ options = {};
874
+ constructor(from) {
875
+ if (from) this.options.from = from;
876
+ }
877
+ /**
878
+ * Set the collection to join with
879
+ */
880
+ from(collection) {
881
+ this.options.from = collection;
882
+ return this;
883
+ }
884
+ /**
885
+ * Set the local field (source collection)
886
+ * IMPORTANT: This field should be indexed for optimal performance
887
+ */
888
+ localField(field) {
889
+ this.options.localField = field;
890
+ return this;
891
+ }
892
+ /**
893
+ * Set the foreign field (target collection)
894
+ * IMPORTANT: This field should be indexed (preferably unique) for optimal performance
895
+ */
896
+ foreignField(field) {
897
+ this.options.foreignField = field;
898
+ return this;
899
+ }
900
+ /**
901
+ * Set the output field name
902
+ * Defaults to the collection name if not specified
903
+ */
904
+ as(fieldName) {
905
+ this.options.as = fieldName;
906
+ return this;
907
+ }
908
+ /**
909
+ * Mark this lookup as returning a single document
910
+ * Automatically unwraps the array result to a single object or null
911
+ */
912
+ single(isSingle = true) {
913
+ this.options.single = isSingle;
914
+ return this;
915
+ }
916
+ /**
917
+ * Add a pipeline to filter/transform joined documents
918
+ * Useful for filtering, sorting, or limiting joined results
919
+ *
920
+ * @example
921
+ * ```typescript
922
+ * lookup.pipeline([
923
+ * { $match: { status: 'active' } },
924
+ * { $sort: { priority: -1 } },
925
+ * { $limit: 5 }
926
+ * ]);
927
+ * ```
928
+ */
929
+ pipeline(stages) {
930
+ this.options.pipeline = stages;
931
+ return this;
932
+ }
933
+ /**
934
+ * Set let variables for use in pipeline
935
+ * Allows referencing local document fields in the pipeline
936
+ */
937
+ let(variables) {
938
+ this.options.let = variables;
939
+ return this;
940
+ }
941
+ /**
942
+ * Build the $lookup aggregation stage(s)
943
+ * Returns an array of pipeline stages including $lookup and optional $unwind
944
+ *
945
+ * IMPORTANT: MongoDB $lookup has two mutually exclusive forms:
946
+ * 1. Simple form: { from, localField, foreignField, as }
947
+ * 2. Pipeline form: { from, let, pipeline, as }
948
+ *
949
+ * When pipeline or let is specified, we use the pipeline form.
950
+ * Otherwise, we use the simpler localField/foreignField form.
951
+ */
952
+ build() {
953
+ const { from, localField, foreignField, as, single, pipeline, let: letVars } = this.options;
954
+ if (!from) {
955
+ throw new Error('LookupBuilder: "from" collection is required');
956
+ }
957
+ const outputField = as || from;
958
+ const stages = [];
959
+ const usePipelineForm = pipeline || letVars;
960
+ let lookupStage;
961
+ if (usePipelineForm) {
962
+ if (!pipeline || pipeline.length === 0) {
963
+ if (!localField || !foreignField) {
964
+ throw new Error(
965
+ "LookupBuilder: When using pipeline form without a custom pipeline, both localField and foreignField are required to auto-generate the pipeline"
966
+ );
967
+ }
968
+ const autoPipeline = [
969
+ {
970
+ $match: {
971
+ $expr: {
972
+ $eq: [`$${foreignField}`, `$$${localField}`]
973
+ }
974
+ }
975
+ }
976
+ ];
977
+ lookupStage = {
978
+ $lookup: {
979
+ from,
980
+ let: { [localField]: `$${localField}`, ...letVars || {} },
981
+ pipeline: autoPipeline,
982
+ as: outputField
983
+ }
984
+ };
985
+ } else {
986
+ lookupStage = {
987
+ $lookup: {
988
+ from,
989
+ ...letVars && { let: letVars },
990
+ pipeline,
991
+ as: outputField
992
+ }
993
+ };
994
+ }
995
+ } else {
996
+ if (!localField || !foreignField) {
997
+ throw new Error("LookupBuilder: localField and foreignField are required for simple lookup");
998
+ }
999
+ lookupStage = {
1000
+ $lookup: {
1001
+ from,
1002
+ localField,
1003
+ foreignField,
1004
+ as: outputField
1005
+ }
1006
+ };
1007
+ }
1008
+ stages.push(lookupStage);
1009
+ if (single) {
1010
+ stages.push({
1011
+ $unwind: {
1012
+ path: `$${outputField}`,
1013
+ preserveNullAndEmptyArrays: true
1014
+ // Keep documents even if no match found
1015
+ }
1016
+ });
1017
+ }
1018
+ return stages;
1019
+ }
1020
+ /**
1021
+ * Build and return only the $lookup stage (without $unwind)
1022
+ * Useful when you want to handle unwrapping yourself
1023
+ */
1024
+ buildLookupOnly() {
1025
+ const stages = this.build();
1026
+ return stages[0];
1027
+ }
1028
+ /**
1029
+ * Static helper: Create a simple lookup in one line
1030
+ */
1031
+ static simple(from, localField, foreignField, options = {}) {
1032
+ return new _LookupBuilder(from).localField(localField).foreignField(foreignField).as(options.as || from).single(options.single || false).build();
1033
+ }
1034
+ /**
1035
+ * Static helper: Create multiple lookups at once
1036
+ *
1037
+ * @example
1038
+ * ```typescript
1039
+ * const pipeline = LookupBuilder.multiple([
1040
+ * { from: 'departments', localField: 'deptSlug', foreignField: 'slug', single: true },
1041
+ * { from: 'managers', localField: 'managerId', foreignField: '_id', single: true }
1042
+ * ]);
1043
+ * ```
1044
+ */
1045
+ static multiple(lookups) {
1046
+ return lookups.flatMap((lookup2) => {
1047
+ const builder = new _LookupBuilder(lookup2.from).localField(lookup2.localField).foreignField(lookup2.foreignField);
1048
+ if (lookup2.as) builder.as(lookup2.as);
1049
+ if (lookup2.single) builder.single(lookup2.single);
1050
+ if (lookup2.pipeline) builder.pipeline(lookup2.pipeline);
1051
+ if (lookup2.let) builder.let(lookup2.let);
1052
+ return builder.build();
1053
+ });
1054
+ }
1055
+ /**
1056
+ * Static helper: Create a nested lookup (lookup within lookup)
1057
+ * Useful for multi-level joins like Order -> Product -> Category
1058
+ *
1059
+ * @example
1060
+ * ```typescript
1061
+ * // Join orders with products, then products with categories
1062
+ * const pipeline = LookupBuilder.nested([
1063
+ * { from: 'products', localField: 'productSku', foreignField: 'sku', as: 'product', single: true },
1064
+ * { from: 'categories', localField: 'product.categorySlug', foreignField: 'slug', as: 'product.category', single: true }
1065
+ * ]);
1066
+ * ```
1067
+ */
1068
+ static nested(lookups) {
1069
+ return lookups.flatMap((lookup2, index) => {
1070
+ const builder = new _LookupBuilder(lookup2.from).localField(lookup2.localField).foreignField(lookup2.foreignField);
1071
+ if (lookup2.as) builder.as(lookup2.as);
1072
+ if (lookup2.single !== void 0) builder.single(lookup2.single);
1073
+ if (lookup2.pipeline) builder.pipeline(lookup2.pipeline);
1074
+ if (lookup2.let) builder.let(lookup2.let);
1075
+ return builder.build();
1076
+ });
1077
+ }
1078
+ };
1079
+
1080
+ // src/query/AggregationBuilder.ts
1081
+ function normalizeSortSpec(sortSpec) {
1082
+ const normalized = {};
1083
+ for (const [field, order] of Object.entries(sortSpec)) {
1084
+ if (order === "asc") {
1085
+ normalized[field] = 1;
1086
+ } else if (order === "desc") {
1087
+ normalized[field] = -1;
1088
+ } else {
1089
+ normalized[field] = order;
1090
+ }
1091
+ }
1092
+ return normalized;
1093
+ }
1094
+ var AggregationBuilder = class _AggregationBuilder {
1095
+ pipeline = [];
1096
+ /**
1097
+ * Get the current pipeline
1098
+ */
1099
+ get() {
1100
+ return [...this.pipeline];
1101
+ }
1102
+ /**
1103
+ * Build and return the final pipeline
1104
+ */
1105
+ build() {
1106
+ return this.get();
1107
+ }
1108
+ /**
1109
+ * Reset the pipeline
1110
+ */
1111
+ reset() {
1112
+ this.pipeline = [];
1113
+ return this;
1114
+ }
1115
+ /**
1116
+ * Add a raw pipeline stage
1117
+ */
1118
+ addStage(stage) {
1119
+ this.pipeline.push(stage);
1120
+ return this;
1121
+ }
1122
+ /**
1123
+ * Add multiple raw pipeline stages
1124
+ */
1125
+ addStages(stages) {
1126
+ this.pipeline.push(...stages);
1127
+ return this;
1128
+ }
1129
+ // ============================================================
1130
+ // CORE AGGREGATION STAGES
1131
+ // ============================================================
1132
+ /**
1133
+ * $match - Filter documents
1134
+ * IMPORTANT: Place $match as early as possible for performance
1135
+ */
1136
+ match(query) {
1137
+ this.pipeline.push({ $match: query });
1138
+ return this;
1139
+ }
1140
+ /**
1141
+ * $project - Include/exclude fields or compute new fields
1142
+ */
1143
+ project(projection) {
1144
+ this.pipeline.push({ $project: projection });
1145
+ return this;
1146
+ }
1147
+ /**
1148
+ * $group - Group documents and compute aggregations
1149
+ *
1150
+ * @example
1151
+ * ```typescript
1152
+ * .group({
1153
+ * _id: '$department',
1154
+ * count: { $sum: 1 },
1155
+ * avgSalary: { $avg: '$salary' }
1156
+ * })
1157
+ * ```
1158
+ */
1159
+ group(groupSpec) {
1160
+ this.pipeline.push({ $group: groupSpec });
1161
+ return this;
1162
+ }
1163
+ /**
1164
+ * $sort - Sort documents
1165
+ */
1166
+ sort(sortSpec) {
1167
+ if (typeof sortSpec === "string") {
1168
+ const order = sortSpec.startsWith("-") ? -1 : 1;
1169
+ const field = sortSpec.startsWith("-") ? sortSpec.substring(1) : sortSpec;
1170
+ this.pipeline.push({ $sort: { [field]: order } });
1171
+ } else {
1172
+ this.pipeline.push({ $sort: normalizeSortSpec(sortSpec) });
1173
+ }
1174
+ return this;
1175
+ }
1176
+ /**
1177
+ * $limit - Limit number of documents
1178
+ */
1179
+ limit(count2) {
1180
+ this.pipeline.push({ $limit: count2 });
1181
+ return this;
1182
+ }
1183
+ /**
1184
+ * $skip - Skip documents
1185
+ */
1186
+ skip(count2) {
1187
+ this.pipeline.push({ $skip: count2 });
1188
+ return this;
1189
+ }
1190
+ /**
1191
+ * $unwind - Deconstruct array field
1192
+ */
1193
+ unwind(path, preserveNullAndEmptyArrays = false) {
1194
+ this.pipeline.push({
1195
+ $unwind: {
1196
+ path: path.startsWith("$") ? path : `$${path}`,
1197
+ preserveNullAndEmptyArrays
1198
+ }
1199
+ });
1200
+ return this;
1201
+ }
1202
+ /**
1203
+ * $addFields - Add new fields or replace existing fields
1204
+ */
1205
+ addFields(fields) {
1206
+ this.pipeline.push({ $addFields: fields });
1207
+ return this;
1208
+ }
1209
+ /**
1210
+ * $set - Alias for $addFields
1211
+ */
1212
+ set(fields) {
1213
+ return this.addFields(fields);
1214
+ }
1215
+ /**
1216
+ * $unset - Remove fields
1217
+ */
1218
+ unset(fields) {
1219
+ this.pipeline.push({ $unset: fields });
1220
+ return this;
1221
+ }
1222
+ /**
1223
+ * $replaceRoot - Replace the root document
1224
+ */
1225
+ replaceRoot(newRoot) {
1226
+ this.pipeline.push({
1227
+ $replaceRoot: {
1228
+ newRoot: typeof newRoot === "string" ? `$${newRoot}` : newRoot
1229
+ }
1230
+ });
1231
+ return this;
1232
+ }
1233
+ // ============================================================
1234
+ // LOOKUP (JOINS)
1235
+ // ============================================================
1236
+ /**
1237
+ * $lookup - Join with another collection (simple form)
1238
+ *
1239
+ * @param from - Collection to join with
1240
+ * @param localField - Field from source collection
1241
+ * @param foreignField - Field from target collection
1242
+ * @param as - Output field name
1243
+ * @param single - Unwrap array to single object
1244
+ *
1245
+ * @example
1246
+ * ```typescript
1247
+ * // Join employees with departments by slug
1248
+ * .lookup('departments', 'deptSlug', 'slug', 'department', true)
1249
+ * ```
1250
+ */
1251
+ lookup(from, localField, foreignField, as, single) {
1252
+ const stages = new LookupBuilder(from).localField(localField).foreignField(foreignField).as(as || from).single(single || false).build();
1253
+ this.pipeline.push(...stages);
1254
+ return this;
1255
+ }
1256
+ /**
1257
+ * $lookup - Join with another collection (advanced form with pipeline)
1258
+ *
1259
+ * @example
1260
+ * ```typescript
1261
+ * .lookupWithPipeline({
1262
+ * from: 'products',
1263
+ * localField: 'productIds',
1264
+ * foreignField: 'sku',
1265
+ * as: 'products',
1266
+ * pipeline: [
1267
+ * { $match: { status: 'active' } },
1268
+ * { $project: { name: 1, price: 1 } }
1269
+ * ]
1270
+ * })
1271
+ * ```
1272
+ */
1273
+ lookupWithPipeline(options) {
1274
+ const builder = new LookupBuilder(options.from).localField(options.localField).foreignField(options.foreignField);
1275
+ if (options.as) builder.as(options.as);
1276
+ if (options.single) builder.single(options.single);
1277
+ if (options.pipeline) builder.pipeline(options.pipeline);
1278
+ if (options.let) builder.let(options.let);
1279
+ this.pipeline.push(...builder.build());
1280
+ return this;
1281
+ }
1282
+ /**
1283
+ * Multiple lookups at once
1284
+ *
1285
+ * @example
1286
+ * ```typescript
1287
+ * .multiLookup([
1288
+ * { from: 'departments', localField: 'deptSlug', foreignField: 'slug', single: true },
1289
+ * { from: 'managers', localField: 'managerId', foreignField: '_id', single: true }
1290
+ * ])
1291
+ * ```
1292
+ */
1293
+ multiLookup(lookups) {
1294
+ const stages = LookupBuilder.multiple(lookups);
1295
+ this.pipeline.push(...stages);
1296
+ return this;
1297
+ }
1298
+ // ============================================================
1299
+ // ADVANCED OPERATORS (MongoDB 6+)
1300
+ // ============================================================
1301
+ /**
1302
+ * $facet - Process multiple aggregation pipelines in a single stage
1303
+ * Useful for computing multiple aggregations in parallel
1304
+ *
1305
+ * @example
1306
+ * ```typescript
1307
+ * .facet({
1308
+ * totalCount: [{ $count: 'count' }],
1309
+ * avgPrice: [{ $group: { _id: null, avg: { $avg: '$price' } } }],
1310
+ * topProducts: [{ $sort: { sales: -1 } }, { $limit: 10 }]
1311
+ * })
1312
+ * ```
1313
+ */
1314
+ facet(facets) {
1315
+ this.pipeline.push({ $facet: facets });
1316
+ return this;
1317
+ }
1318
+ /**
1319
+ * $bucket - Categorize documents into buckets
1320
+ *
1321
+ * @example
1322
+ * ```typescript
1323
+ * .bucket({
1324
+ * groupBy: '$price',
1325
+ * boundaries: [0, 50, 100, 200],
1326
+ * default: 'Other',
1327
+ * output: {
1328
+ * count: { $sum: 1 },
1329
+ * products: { $push: '$name' }
1330
+ * }
1331
+ * })
1332
+ * ```
1333
+ */
1334
+ bucket(options) {
1335
+ this.pipeline.push({ $bucket: options });
1336
+ return this;
1337
+ }
1338
+ /**
1339
+ * $bucketAuto - Automatically determine bucket boundaries
1340
+ */
1341
+ bucketAuto(options) {
1342
+ this.pipeline.push({ $bucketAuto: options });
1343
+ return this;
1344
+ }
1345
+ /**
1346
+ * $setWindowFields - Perform window functions (MongoDB 5.0+)
1347
+ * Useful for rankings, running totals, moving averages
1348
+ *
1349
+ * @example
1350
+ * ```typescript
1351
+ * .setWindowFields({
1352
+ * partitionBy: '$department',
1353
+ * sortBy: { salary: -1 },
1354
+ * output: {
1355
+ * rank: { $rank: {} },
1356
+ * runningTotal: { $sum: '$salary', window: { documents: ['unbounded', 'current'] } }
1357
+ * }
1358
+ * })
1359
+ * ```
1360
+ */
1361
+ setWindowFields(options) {
1362
+ const normalizedOptions = {
1363
+ ...options,
1364
+ sortBy: options.sortBy ? normalizeSortSpec(options.sortBy) : void 0
1365
+ };
1366
+ this.pipeline.push({ $setWindowFields: normalizedOptions });
1367
+ return this;
1368
+ }
1369
+ /**
1370
+ * $unionWith - Combine results from multiple collections (MongoDB 4.4+)
1371
+ *
1372
+ * @example
1373
+ * ```typescript
1374
+ * .unionWith({
1375
+ * coll: 'archivedOrders',
1376
+ * pipeline: [{ $match: { year: 2024 } }]
1377
+ * })
1378
+ * ```
1379
+ */
1380
+ unionWith(options) {
1381
+ this.pipeline.push({ $unionWith: options });
1382
+ return this;
1383
+ }
1384
+ /**
1385
+ * $densify - Fill gaps in data (MongoDB 5.1+)
1386
+ * Useful for time series data with missing points
1387
+ */
1388
+ densify(options) {
1389
+ this.pipeline.push({ $densify: options });
1390
+ return this;
1391
+ }
1392
+ /**
1393
+ * $fill - Fill null or missing field values (MongoDB 5.3+)
1394
+ */
1395
+ fill(options) {
1396
+ const normalizedOptions = {
1397
+ ...options,
1398
+ sortBy: options.sortBy ? normalizeSortSpec(options.sortBy) : void 0
1399
+ };
1400
+ this.pipeline.push({ $fill: normalizedOptions });
1401
+ return this;
1402
+ }
1403
+ // ============================================================
1404
+ // UTILITY METHODS
1405
+ // ============================================================
1406
+ /**
1407
+ * Paginate - Add skip and limit for offset-based pagination
1408
+ */
1409
+ paginate(page, limit) {
1410
+ const skip = (page - 1) * limit;
1411
+ return this.skip(skip).limit(limit);
1412
+ }
1413
+ /**
1414
+ * Count total documents (useful with $facet for pagination metadata)
1415
+ */
1416
+ count(outputField = "count") {
1417
+ this.pipeline.push({ $count: outputField });
1418
+ return this;
1419
+ }
1420
+ /**
1421
+ * Sample - Randomly select N documents
1422
+ */
1423
+ sample(size) {
1424
+ this.pipeline.push({ $sample: { size } });
1425
+ return this;
1426
+ }
1427
+ /**
1428
+ * Out - Write results to a collection
1429
+ */
1430
+ out(collection) {
1431
+ this.pipeline.push({ $out: collection });
1432
+ return this;
1433
+ }
1434
+ /**
1435
+ * Merge - Merge results into a collection
1436
+ */
1437
+ merge(options) {
1438
+ this.pipeline.push({
1439
+ $merge: typeof options === "string" ? { into: options } : options
1440
+ });
1441
+ return this;
1442
+ }
1443
+ /**
1444
+ * GeoNear - Perform geospatial queries
1445
+ */
1446
+ geoNear(options) {
1447
+ this.pipeline.push({ $geoNear: options });
1448
+ return this;
1449
+ }
1450
+ /**
1451
+ * GraphLookup - Perform recursive search (graph traversal)
1452
+ */
1453
+ graphLookup(options) {
1454
+ this.pipeline.push({ $graphLookup: options });
1455
+ return this;
1456
+ }
1457
+ // ============================================================
1458
+ // ATLAS SEARCH (MongoDB Atlas only)
1459
+ // ============================================================
1460
+ /**
1461
+ * $search - Atlas Search full-text search (Atlas only)
1462
+ *
1463
+ * @example
1464
+ * ```typescript
1465
+ * .search({
1466
+ * index: 'default',
1467
+ * text: {
1468
+ * query: 'laptop computer',
1469
+ * path: ['title', 'description'],
1470
+ * fuzzy: { maxEdits: 2 }
1471
+ * }
1472
+ * })
1473
+ * ```
1474
+ */
1475
+ search(options) {
1476
+ this.pipeline.push({ $search: options });
1477
+ return this;
1478
+ }
1479
+ /**
1480
+ * $searchMeta - Get Atlas Search metadata (Atlas only)
1481
+ */
1482
+ searchMeta(options) {
1483
+ this.pipeline.push({ $searchMeta: options });
1484
+ return this;
1485
+ }
1486
+ // ============================================================
1487
+ // HELPER FACTORY METHODS
1488
+ // ============================================================
1489
+ /**
1490
+ * Create a builder from an existing pipeline
1491
+ */
1492
+ static from(pipeline) {
1493
+ const builder = new _AggregationBuilder();
1494
+ builder.pipeline = [...pipeline];
1495
+ return builder;
1496
+ }
1497
+ /**
1498
+ * Create a builder with initial match stage
1499
+ */
1500
+ static startWith(query) {
1501
+ return new _AggregationBuilder().match(query);
1502
+ }
1503
+ };
1504
+
840
1505
  // src/Repository.ts
841
1506
  var Repository = class {
842
1507
  Model;
@@ -999,8 +1664,8 @@ var Repository = class {
999
1664
  }
1000
1665
  const hasPageParam = params.page !== void 0 || params.pagination;
1001
1666
  const hasCursorParam = "cursor" in params || "after" in params;
1002
- const hasExplicitSort = params.sort !== void 0;
1003
- const useKeyset = !hasPageParam && (hasCursorParam || hasExplicitSort);
1667
+ const hasSortParam = params.sort !== void 0;
1668
+ const useKeyset = !hasPageParam && (hasCursorParam || hasSortParam);
1004
1669
  const filters = context.filters || params.filters || {};
1005
1670
  const search = params.search;
1006
1671
  const sort = params.sort || "-createdAt";
@@ -1105,23 +1770,186 @@ var Repository = class {
1105
1770
  async distinct(field, query = {}, options = {}) {
1106
1771
  return distinct(this.Model, field, query, options);
1107
1772
  }
1773
+ /**
1774
+ * Query with custom field lookups ($lookup)
1775
+ * Best for: Joins on slugs, SKUs, codes, or other indexed custom fields
1776
+ *
1777
+ * @example
1778
+ * ```typescript
1779
+ * // Join employees with departments using slug instead of ObjectId
1780
+ * const employees = await employeeRepo.lookupPopulate({
1781
+ * filters: { status: 'active' },
1782
+ * lookups: [
1783
+ * {
1784
+ * from: 'departments',
1785
+ * localField: 'departmentSlug',
1786
+ * foreignField: 'slug',
1787
+ * as: 'department',
1788
+ * single: true
1789
+ * }
1790
+ * ],
1791
+ * sort: '-createdAt',
1792
+ * page: 1,
1793
+ * limit: 50
1794
+ * });
1795
+ * ```
1796
+ */
1797
+ async lookupPopulate(options) {
1798
+ const context = await this._buildContext("lookupPopulate", options);
1799
+ try {
1800
+ const builder = new AggregationBuilder();
1801
+ if (options.filters && Object.keys(options.filters).length > 0) {
1802
+ builder.match(options.filters);
1803
+ }
1804
+ builder.multiLookup(options.lookups);
1805
+ if (options.sort) {
1806
+ builder.sort(this._parseSort(options.sort));
1807
+ }
1808
+ const page = options.page || 1;
1809
+ const limit = options.limit || this._pagination.config.defaultLimit || 20;
1810
+ const skip = (page - 1) * limit;
1811
+ const SAFE_LIMIT = 1e3;
1812
+ const SAFE_MAX_OFFSET = 1e4;
1813
+ if (limit > SAFE_LIMIT) {
1814
+ console.warn(
1815
+ `[mongokit] Large limit (${limit}) in lookupPopulate. $facet results must be <16MB. Consider using smaller limits or stream-based pagination for large datasets.`
1816
+ );
1817
+ }
1818
+ if (skip > SAFE_MAX_OFFSET) {
1819
+ console.warn(
1820
+ `[mongokit] Large offset (${skip}) in lookupPopulate. $facet with high offsets can exceed 16MB. For deep pagination, consider using keyset/cursor-based pagination instead.`
1821
+ );
1822
+ }
1823
+ const dataStages = [
1824
+ { $skip: skip },
1825
+ { $limit: limit }
1826
+ ];
1827
+ if (options.select) {
1828
+ let projection;
1829
+ if (typeof options.select === "string") {
1830
+ projection = {};
1831
+ const fields = options.select.split(",").map((f) => f.trim());
1832
+ for (const field of fields) {
1833
+ if (field.startsWith("-")) {
1834
+ projection[field.substring(1)] = 0;
1835
+ } else {
1836
+ projection[field] = 1;
1837
+ }
1838
+ }
1839
+ } else if (Array.isArray(options.select)) {
1840
+ projection = {};
1841
+ for (const field of options.select) {
1842
+ if (field.startsWith("-")) {
1843
+ projection[field.substring(1)] = 0;
1844
+ } else {
1845
+ projection[field] = 1;
1846
+ }
1847
+ }
1848
+ } else {
1849
+ projection = options.select;
1850
+ }
1851
+ dataStages.push({ $project: projection });
1852
+ }
1853
+ builder.facet({
1854
+ metadata: [{ $count: "total" }],
1855
+ data: dataStages
1856
+ });
1857
+ const pipeline = builder.build();
1858
+ const results = await this.Model.aggregate(pipeline).session(options.session || null);
1859
+ const result = results[0] || { metadata: [], data: [] };
1860
+ const total = result.metadata[0]?.total || 0;
1861
+ const data = result.data || [];
1862
+ await this._emitHook("after:lookupPopulate", { context, result: data });
1863
+ return {
1864
+ data,
1865
+ total,
1866
+ page,
1867
+ limit
1868
+ };
1869
+ } catch (error) {
1870
+ await this._emitErrorHook("error:lookupPopulate", { context, error });
1871
+ throw this._handleError(error);
1872
+ }
1873
+ }
1874
+ /**
1875
+ * Create an aggregation builder for this model
1876
+ * Useful for building complex custom aggregations
1877
+ *
1878
+ * @example
1879
+ * ```typescript
1880
+ * const pipeline = repo.buildAggregation()
1881
+ * .match({ status: 'active' })
1882
+ * .lookup('departments', 'deptSlug', 'slug', 'department', true)
1883
+ * .group({ _id: '$department', count: { $sum: 1 } })
1884
+ * .sort({ count: -1 })
1885
+ * .build();
1886
+ *
1887
+ * const results = await repo.Model.aggregate(pipeline);
1888
+ * ```
1889
+ */
1890
+ buildAggregation() {
1891
+ return new AggregationBuilder();
1892
+ }
1893
+ /**
1894
+ * Create a lookup builder
1895
+ * Useful for building $lookup stages independently
1896
+ *
1897
+ * @example
1898
+ * ```typescript
1899
+ * const lookupStages = repo.buildLookup('departments')
1900
+ * .localField('deptSlug')
1901
+ * .foreignField('slug')
1902
+ * .as('department')
1903
+ * .single()
1904
+ * .build();
1905
+ *
1906
+ * const pipeline = [
1907
+ * { $match: { status: 'active' } },
1908
+ * ...lookupStages
1909
+ * ];
1910
+ * ```
1911
+ */
1912
+ buildLookup(from) {
1913
+ return new LookupBuilder(from);
1914
+ }
1108
1915
  /**
1109
1916
  * Execute callback within a transaction
1110
1917
  */
1111
- async withTransaction(callback) {
1112
- const session = await mongoose4.startSession();
1113
- session.startTransaction();
1918
+ async withTransaction(callback, options = {}) {
1919
+ const session = await mongoose.startSession();
1920
+ let started = false;
1114
1921
  try {
1922
+ session.startTransaction();
1923
+ started = true;
1115
1924
  const result = await callback(session);
1116
1925
  await session.commitTransaction();
1117
1926
  return result;
1118
1927
  } catch (error) {
1119
- await session.abortTransaction();
1120
- throw error;
1928
+ const err = error;
1929
+ if (options.allowFallback && this._isTransactionUnsupported(err)) {
1930
+ if (typeof options.onFallback === "function") {
1931
+ options.onFallback(err);
1932
+ }
1933
+ if (started && session.inTransaction()) {
1934
+ try {
1935
+ await session.abortTransaction();
1936
+ } catch {
1937
+ }
1938
+ }
1939
+ return await callback(null);
1940
+ }
1941
+ if (started && session.inTransaction()) {
1942
+ await session.abortTransaction();
1943
+ }
1944
+ throw err;
1121
1945
  } finally {
1122
1946
  session.endSession();
1123
1947
  }
1124
1948
  }
1949
+ _isTransactionUnsupported(error) {
1950
+ const message = (error.message || "").toLowerCase();
1951
+ return message.includes("transaction numbers are only allowed on a replica set member") || message.includes("replica set") || message.includes("mongos");
1952
+ }
1125
1953
  /**
1126
1954
  * Execute custom query with event emission
1127
1955
  */
@@ -1172,11 +2000,11 @@ var Repository = class {
1172
2000
  * Handle errors with proper HTTP status codes
1173
2001
  */
1174
2002
  _handleError(error) {
1175
- if (error instanceof mongoose4.Error.ValidationError) {
2003
+ if (error instanceof mongoose.Error.ValidationError) {
1176
2004
  const messages = Object.values(error.errors).map((err) => err.message);
1177
2005
  return createError(400, `Validation Error: ${messages.join(", ")}`);
1178
2006
  }
1179
- if (error instanceof mongoose4.Error.CastError) {
2007
+ if (error instanceof mongoose.Error.CastError) {
1180
2008
  return createError(400, `Invalid ${error.path}: ${error.value}`);
1181
2009
  }
1182
2010
  if (error.status && error.message) return error;
@@ -2207,7 +3035,7 @@ function cascadePlugin(options) {
2207
3035
  }
2208
3036
  const isSoftDelete = context.softDeleted === true;
2209
3037
  const cascadeDelete = async (relation) => {
2210
- const RelatedModel = mongoose4.models[relation.model];
3038
+ const RelatedModel = mongoose.models[relation.model];
2211
3039
  if (!RelatedModel) {
2212
3040
  logger?.warn?.(`Cascade delete skipped: model '${relation.model}' not found`, {
2213
3041
  parentModel: context.model,
@@ -2298,7 +3126,7 @@ function cascadePlugin(options) {
2298
3126
  }
2299
3127
  const isSoftDelete = context.softDeleted === true;
2300
3128
  const cascadeDeleteMany = async (relation) => {
2301
- const RelatedModel = mongoose4.models[relation.model];
3129
+ const RelatedModel = mongoose.models[relation.model];
2302
3130
  if (!RelatedModel) {
2303
3131
  logger?.warn?.(`Cascade deleteMany skipped: model '${relation.model}' not found`, {
2304
3132
  parentModel: context.model
@@ -2414,24 +3242,15 @@ function createMemoryCache(maxEntries = 1e3) {
2414
3242
  }
2415
3243
  };
2416
3244
  }
2417
- function isMongooseSchema(value) {
2418
- return value instanceof mongoose4.Schema;
2419
- }
2420
- function isPlainObject(value) {
2421
- return Object.prototype.toString.call(value) === "[object Object]";
2422
- }
2423
- function isObjectIdType(t) {
2424
- return t === mongoose4.Schema.Types.ObjectId || t === mongoose4.Types.ObjectId;
2425
- }
2426
3245
  function buildCrudSchemasFromMongooseSchema(mongooseSchema, options = {}) {
2427
- const tree = mongooseSchema?.obj || {};
2428
- const jsonCreate = buildJsonSchemaForCreate(tree, options);
3246
+ const jsonCreate = buildJsonSchemaFromPaths(mongooseSchema, options);
2429
3247
  const jsonUpdate = buildJsonSchemaForUpdate(jsonCreate, options);
2430
3248
  const jsonParams = {
2431
3249
  type: "object",
2432
3250
  properties: { id: { type: "string", pattern: "^[0-9a-fA-F]{24}$" } },
2433
3251
  required: ["id"]
2434
3252
  };
3253
+ const tree = mongooseSchema?.obj || {};
2435
3254
  const jsonQuery = buildJsonSchemaForQuery(tree, options);
2436
3255
  return { createBody: jsonCreate, updateBody: jsonUpdate, params: jsonParams, listQuery: jsonQuery };
2437
3256
  }
@@ -2485,88 +3304,37 @@ function validateUpdateBody(body = {}, options = {}) {
2485
3304
  violations
2486
3305
  };
2487
3306
  }
2488
- function jsonTypeFor(def, options, seen) {
2489
- if (Array.isArray(def)) {
2490
- if (def[0] === mongoose4.Schema.Types.Mixed) {
2491
- return { type: "array", items: { type: "object", additionalProperties: true } };
2492
- }
2493
- return { type: "array", items: jsonTypeFor(def[0] ?? String, options, seen) };
2494
- }
2495
- if (isPlainObject(def) && "type" in def) {
2496
- const typedDef = def;
2497
- if (typedDef.enum && Array.isArray(typedDef.enum) && typedDef.enum.length) {
2498
- return { type: "string", enum: typedDef.enum.map(String) };
2499
- }
2500
- if (Array.isArray(typedDef.type)) {
2501
- const inner = typedDef.type[0] !== void 0 ? typedDef.type[0] : String;
2502
- if (inner === mongoose4.Schema.Types.Mixed) {
2503
- return { type: "array", items: { type: "object", additionalProperties: true } };
3307
+ function buildJsonSchemaFromPaths(mongooseSchema, options) {
3308
+ const properties = {};
3309
+ const required = [];
3310
+ const paths = mongooseSchema.paths;
3311
+ const rootFields = /* @__PURE__ */ new Map();
3312
+ for (const [path, schemaType] of Object.entries(paths)) {
3313
+ if (path === "_id" || path === "__v") continue;
3314
+ const parts = path.split(".");
3315
+ const rootField = parts[0];
3316
+ if (!rootFields.has(rootField)) {
3317
+ rootFields.set(rootField, []);
3318
+ }
3319
+ rootFields.get(rootField).push({ path, schemaType });
3320
+ }
3321
+ for (const [rootField, fieldPaths] of rootFields.entries()) {
3322
+ if (fieldPaths.length === 1 && fieldPaths[0].path === rootField) {
3323
+ const schemaType = fieldPaths[0].schemaType;
3324
+ properties[rootField] = schemaTypeToJsonSchema(schemaType);
3325
+ if (schemaType.isRequired) {
3326
+ required.push(rootField);
2504
3327
  }
2505
- return { type: "array", items: jsonTypeFor(inner, options, seen) };
2506
- }
2507
- if (typedDef.type === String) return { type: "string" };
2508
- if (typedDef.type === Number) return { type: "number" };
2509
- if (typedDef.type === Boolean) return { type: "boolean" };
2510
- if (typedDef.type === Date) {
2511
- const mode = options?.dateAs || "datetime";
2512
- return mode === "date" ? { type: "string", format: "date" } : { type: "string", format: "date-time" };
2513
- }
2514
- if (typedDef.type === Map || typedDef.type === mongoose4.Schema.Types.Map) {
2515
- const ofSchema = jsonTypeFor(typedDef.of || String, options, seen);
2516
- return { type: "object", additionalProperties: ofSchema };
2517
- }
2518
- if (typedDef.type === mongoose4.Schema.Types.Mixed) {
2519
- return { type: "object", additionalProperties: true };
2520
- }
2521
- if (isObjectIdType(typedDef.type)) {
2522
- return { type: "string", pattern: "^[0-9a-fA-F]{24}$" };
2523
- }
2524
- if (isMongooseSchema(typedDef.type)) {
2525
- const obj = typedDef.type.obj;
2526
- if (obj && typeof obj === "object") {
2527
- if (seen.has(obj)) return { type: "object", additionalProperties: true };
2528
- seen.add(obj);
2529
- return convertTreeToJsonSchema(obj, options, seen);
3328
+ } else {
3329
+ const nestedSchema = buildNestedJsonSchema(fieldPaths, rootField);
3330
+ properties[rootField] = nestedSchema.schema;
3331
+ if (nestedSchema.required) {
3332
+ required.push(rootField);
2530
3333
  }
2531
3334
  }
2532
3335
  }
2533
- if (def === String) return { type: "string" };
2534
- if (def === Number) return { type: "number" };
2535
- if (def === Boolean) return { type: "boolean" };
2536
- if (def === Date) {
2537
- const mode = options?.dateAs || "datetime";
2538
- return mode === "date" ? { type: "string", format: "date" } : { type: "string", format: "date-time" };
2539
- }
2540
- if (isObjectIdType(def)) return { type: "string", pattern: "^[0-9a-fA-F]{24}$" };
2541
- if (isPlainObject(def)) {
2542
- if (seen.has(def)) return { type: "object", additionalProperties: true };
2543
- seen.add(def);
2544
- return convertTreeToJsonSchema(def, options, seen);
2545
- }
2546
- return {};
2547
- }
2548
- function convertTreeToJsonSchema(tree, options, seen = /* @__PURE__ */ new WeakSet()) {
2549
- if (!tree || typeof tree !== "object") {
2550
- return { type: "object", properties: {} };
2551
- }
2552
- if (seen.has(tree)) {
2553
- return { type: "object", additionalProperties: true };
2554
- }
2555
- seen.add(tree);
2556
- const properties = {};
2557
- const required = [];
2558
- for (const [key, val] of Object.entries(tree || {})) {
2559
- if (key === "__v" || key === "_id" || key === "id") continue;
2560
- const cfg = isPlainObject(val) && "type" in val ? val : { };
2561
- properties[key] = jsonTypeFor(val, options, seen);
2562
- if (cfg.required === true) required.push(key);
2563
- }
2564
3336
  const schema = { type: "object", properties };
2565
3337
  if (required.length) schema.required = required;
2566
- return schema;
2567
- }
2568
- function buildJsonSchemaForCreate(tree, options) {
2569
- const base = convertTreeToJsonSchema(tree, options, /* @__PURE__ */ new WeakSet());
2570
3338
  const fieldsToOmit = /* @__PURE__ */ new Set(["createdAt", "updatedAt", "__v"]);
2571
3339
  (options?.create?.omitFields || []).forEach((f) => fieldsToOmit.add(f));
2572
3340
  const fieldRules = options?.fieldRules || {};
@@ -2576,37 +3344,96 @@ function buildJsonSchemaForCreate(tree, options) {
2576
3344
  }
2577
3345
  });
2578
3346
  fieldsToOmit.forEach((field) => {
2579
- if (base.properties?.[field]) {
2580
- delete base.properties[field];
3347
+ if (schema.properties?.[field]) {
3348
+ delete schema.properties[field];
2581
3349
  }
2582
- if (base.required) {
2583
- base.required = base.required.filter((k) => k !== field);
3350
+ if (schema.required) {
3351
+ schema.required = schema.required.filter((k) => k !== field);
2584
3352
  }
2585
3353
  });
2586
3354
  const reqOv = options?.create?.requiredOverrides || {};
2587
3355
  const optOv = options?.create?.optionalOverrides || {};
2588
- base.required = base.required || [];
3356
+ schema.required = schema.required || [];
2589
3357
  for (const [k, v] of Object.entries(reqOv)) {
2590
- if (v && !base.required.includes(k)) base.required.push(k);
3358
+ if (v && !schema.required.includes(k)) schema.required.push(k);
2591
3359
  }
2592
3360
  for (const [k, v] of Object.entries(optOv)) {
2593
- if (v && base.required) base.required = base.required.filter((x) => x !== k);
3361
+ if (v && schema.required) schema.required = schema.required.filter((x) => x !== k);
2594
3362
  }
2595
3363
  Object.entries(fieldRules).forEach(([field, rules]) => {
2596
- if (rules.optional && base.required) {
2597
- base.required = base.required.filter((x) => x !== field);
3364
+ if (rules.optional && schema.required) {
3365
+ schema.required = schema.required.filter((x) => x !== field);
2598
3366
  }
2599
3367
  });
2600
3368
  const schemaOverrides = options?.create?.schemaOverrides || {};
2601
3369
  for (const [k, override] of Object.entries(schemaOverrides)) {
2602
- if (base.properties?.[k]) {
2603
- base.properties[k] = override;
3370
+ if (schema.properties?.[k]) {
3371
+ schema.properties[k] = override;
2604
3372
  }
2605
3373
  }
2606
3374
  if (options?.strictAdditionalProperties === true) {
2607
- base.additionalProperties = false;
3375
+ schema.additionalProperties = false;
2608
3376
  }
2609
- return base;
3377
+ return schema;
3378
+ }
3379
+ function buildNestedJsonSchema(fieldPaths, rootField) {
3380
+ const properties = {};
3381
+ const required = [];
3382
+ let hasRequiredFields = false;
3383
+ for (const { path, schemaType } of fieldPaths) {
3384
+ const relativePath = path.substring(rootField.length + 1);
3385
+ const parts = relativePath.split(".");
3386
+ if (parts.length === 1) {
3387
+ properties[parts[0]] = schemaTypeToJsonSchema(schemaType);
3388
+ if (schemaType.isRequired) {
3389
+ required.push(parts[0]);
3390
+ hasRequiredFields = true;
3391
+ }
3392
+ } else {
3393
+ const fieldName = parts[0];
3394
+ if (!properties[fieldName]) {
3395
+ properties[fieldName] = { type: "object", properties: {} };
3396
+ }
3397
+ const nestedObj = properties[fieldName];
3398
+ if (!nestedObj.properties) nestedObj.properties = {};
3399
+ const deepPath = parts.slice(1).join(".");
3400
+ nestedObj.properties[deepPath] = schemaTypeToJsonSchema(schemaType);
3401
+ }
3402
+ }
3403
+ const schema = { type: "object", properties };
3404
+ if (required.length) schema.required = required;
3405
+ return { schema, required: hasRequiredFields };
3406
+ }
3407
+ function schemaTypeToJsonSchema(schemaType) {
3408
+ const result = {};
3409
+ const instance = schemaType.instance;
3410
+ const options = schemaType.options || {};
3411
+ if (instance === "String") {
3412
+ result.type = "string";
3413
+ if (typeof options.minlength === "number") result.minLength = options.minlength;
3414
+ if (typeof options.maxlength === "number") result.maxLength = options.maxlength;
3415
+ if (options.match instanceof RegExp) result.pattern = options.match.source;
3416
+ if (options.enum && Array.isArray(options.enum)) result.enum = options.enum;
3417
+ } else if (instance === "Number") {
3418
+ result.type = "number";
3419
+ if (typeof options.min === "number") result.minimum = options.min;
3420
+ if (typeof options.max === "number") result.maximum = options.max;
3421
+ } else if (instance === "Boolean") {
3422
+ result.type = "boolean";
3423
+ } else if (instance === "Date") {
3424
+ result.type = "string";
3425
+ result.format = "date-time";
3426
+ } else if (instance === "ObjectId" || instance === "ObjectID") {
3427
+ result.type = "string";
3428
+ result.pattern = "^[0-9a-fA-F]{24}$";
3429
+ } else if (instance === "Array") {
3430
+ result.type = "array";
3431
+ result.items = { type: "string" };
3432
+ } else {
3433
+ result.type = "object";
3434
+ result.additionalProperties = true;
3435
+ }
3436
+ return result;
2610
3437
  }
2611
3438
  function buildJsonSchemaForUpdate(createJson, options) {
2612
3439
  const clone = JSON.parse(JSON.stringify(createJson));
@@ -2627,6 +3454,9 @@ function buildJsonSchemaForUpdate(createJson, options) {
2627
3454
  if (options?.strictAdditionalProperties === true) {
2628
3455
  clone.additionalProperties = false;
2629
3456
  }
3457
+ if (options?.update?.requireAtLeastOne === true) {
3458
+ clone.minProperties = 1;
3459
+ }
2630
3460
  return clone;
2631
3461
  }
2632
3462
  function buildJsonSchemaForQuery(_tree, options) {
@@ -2670,21 +3500,26 @@ var QueryParser = class {
2670
3500
  size: "$size",
2671
3501
  type: "$type"
2672
3502
  };
2673
- /**
2674
- * Dangerous MongoDB operators that should never be accepted from user input
2675
- * Security: Prevent NoSQL injection attacks
2676
- */
2677
3503
  dangerousOperators;
2678
3504
  /**
2679
- * Regex pattern characters that can cause catastrophic backtracking (ReDoS)
3505
+ * Regex patterns that can cause catastrophic backtracking (ReDoS attacks)
3506
+ * Detects:
3507
+ * - Quantifiers: {n,m}
3508
+ * - Possessive quantifiers: *+, ++, ?+
3509
+ * - Nested quantifiers: (a+)+, (a*)*
3510
+ * - Backreferences: \1, \2, etc.
3511
+ * - Complex character classes: [...]...[...]
2680
3512
  */
2681
- dangerousRegexPatterns = /(\{[0-9,]+\}|\*\+|\+\+|\?\+|(\([^)]*\))\1|\(\?[^)]*\)|[\[\]].*[\[\]])/;
3513
+ dangerousRegexPatterns = /(\{[0-9,]+\}|\*\+|\+\+|\?\+|(\(.+\))\+|\(\?\:|\\[0-9]|(\[.+\]).+(\[.+\]))/;
2682
3514
  constructor(options = {}) {
2683
3515
  this.options = {
2684
3516
  maxRegexLength: options.maxRegexLength ?? 500,
2685
3517
  maxSearchLength: options.maxSearchLength ?? 200,
2686
3518
  maxFilterDepth: options.maxFilterDepth ?? 10,
2687
- additionalDangerousOperators: options.additionalDangerousOperators ?? []
3519
+ maxLimit: options.maxLimit ?? 1e3,
3520
+ additionalDangerousOperators: options.additionalDangerousOperators ?? [],
3521
+ enableLookups: options.enableLookups ?? true,
3522
+ enableAggregations: options.enableAggregations ?? false
2688
3523
  };
2689
3524
  this.dangerousOperators = [
2690
3525
  "$where",
@@ -2695,9 +3530,16 @@ var QueryParser = class {
2695
3530
  ];
2696
3531
  }
2697
3532
  /**
2698
- * Parse query parameters into MongoDB query format
3533
+ * Parse URL query parameters into MongoDB query format
3534
+ *
3535
+ * @example
3536
+ * ```typescript
3537
+ * // URL: ?status=active&lookup[department][foreignField]=slug&sort=-createdAt&page=1
3538
+ * const query = parser.parse(req.query);
3539
+ * // Returns: { filters: {...}, lookups: [...], sort: {...}, page: 1 }
3540
+ * ```
2699
3541
  */
2700
- parseQuery(query) {
3542
+ parse(query) {
2701
3543
  const {
2702
3544
  page,
2703
3545
  limit = 20,
@@ -2706,15 +3548,35 @@ var QueryParser = class {
2706
3548
  search,
2707
3549
  after,
2708
3550
  cursor,
3551
+ select,
3552
+ lookup: lookup2,
3553
+ aggregate: aggregate2,
2709
3554
  ...filters
2710
3555
  } = query || {};
3556
+ let parsedLimit = parseInt(String(limit), 10);
3557
+ if (isNaN(parsedLimit) || parsedLimit < 1) {
3558
+ parsedLimit = 20;
3559
+ }
3560
+ if (parsedLimit > this.options.maxLimit) {
3561
+ console.warn(`[mongokit] Limit ${parsedLimit} exceeds maximum ${this.options.maxLimit}, capping to max`);
3562
+ parsedLimit = this.options.maxLimit;
3563
+ }
2711
3564
  const parsed = {
2712
3565
  filters: this._parseFilters(filters),
2713
- limit: parseInt(String(limit), 10),
3566
+ limit: parsedLimit,
2714
3567
  sort: this._parseSort(sort),
2715
3568
  populate,
2716
3569
  search: this._sanitizeSearch(search)
2717
3570
  };
3571
+ if (select) {
3572
+ parsed.select = this._parseSelect(select);
3573
+ }
3574
+ if (this.options.enableLookups && lookup2) {
3575
+ parsed.lookups = this._parseLookups(lookup2);
3576
+ }
3577
+ if (this.options.enableAggregations && aggregate2) {
3578
+ parsed.aggregation = this._parseAggregation(aggregate2);
3579
+ }
2718
3580
  if (after || cursor) {
2719
3581
  parsed.after = after || cursor;
2720
3582
  } else if (page !== void 0) {
@@ -2729,29 +3591,161 @@ var QueryParser = class {
2729
3591
  parsed.filters = this._enhanceWithBetween(parsed.filters);
2730
3592
  return parsed;
2731
3593
  }
3594
+ // ============================================================
3595
+ // LOOKUP PARSING (NEW)
3596
+ // ============================================================
2732
3597
  /**
2733
- * Parse sort parameter
2734
- * Converts string like '-createdAt' to { createdAt: -1 }
2735
- * Handles multiple sorts: '-createdAt,name' → { createdAt: -1, name: 1 }
3598
+ * Parse lookup configurations from URL parameters
3599
+ *
3600
+ * Supported formats:
3601
+ * 1. Simple: ?lookup[department]=slug
3602
+ * → Join with 'departments' collection on slug field
3603
+ *
3604
+ * 2. Detailed: ?lookup[department][localField]=deptSlug&lookup[department][foreignField]=slug
3605
+ * → Full control over join configuration
3606
+ *
3607
+ * 3. Multiple: ?lookup[department]=slug&lookup[category]=categorySlug
3608
+ * → Multiple lookups
3609
+ *
3610
+ * @example
3611
+ * ```typescript
3612
+ * // URL: ?lookup[department][localField]=deptSlug&lookup[department][foreignField]=slug&lookup[department][single]=true
3613
+ * const lookups = parser._parseLookups({
3614
+ * department: { localField: 'deptSlug', foreignField: 'slug', single: 'true' }
3615
+ * });
3616
+ * // Returns: [{ from: 'departments', localField: 'deptSlug', foreignField: 'slug', single: true }]
3617
+ * ```
2736
3618
  */
2737
- _parseSort(sort) {
2738
- if (!sort) return void 0;
2739
- if (typeof sort === "object") return sort;
2740
- const sortObj = {};
2741
- const fields = sort.split(",").map((s) => s.trim());
2742
- for (const field of fields) {
2743
- if (field.startsWith("-")) {
2744
- sortObj[field.substring(1)] = -1;
2745
- } else {
2746
- sortObj[field] = 1;
3619
+ _parseLookups(lookup2) {
3620
+ if (!lookup2 || typeof lookup2 !== "object") return [];
3621
+ const lookups = [];
3622
+ const lookupObj = lookup2;
3623
+ for (const [collectionName, config] of Object.entries(lookupObj)) {
3624
+ try {
3625
+ const lookupConfig = this._parseSingleLookup(collectionName, config);
3626
+ if (lookupConfig) {
3627
+ lookups.push(lookupConfig);
3628
+ }
3629
+ } catch (error) {
3630
+ console.warn(`[mongokit] Invalid lookup config for ${collectionName}:`, error);
2747
3631
  }
2748
3632
  }
2749
- return sortObj;
3633
+ return lookups;
3634
+ }
3635
+ /**
3636
+ * Parse a single lookup configuration
3637
+ */
3638
+ _parseSingleLookup(collectionName, config) {
3639
+ if (!config) return null;
3640
+ if (typeof config === "string") {
3641
+ return {
3642
+ from: this._pluralize(collectionName),
3643
+ localField: `${collectionName}${this._capitalize(config)}`,
3644
+ foreignField: config,
3645
+ as: collectionName,
3646
+ single: true
3647
+ };
3648
+ }
3649
+ if (typeof config === "object" && config !== null) {
3650
+ const opts = config;
3651
+ const from = opts.from || this._pluralize(collectionName);
3652
+ const localField = opts.localField;
3653
+ const foreignField = opts.foreignField;
3654
+ if (!localField || !foreignField) {
3655
+ console.warn(`[mongokit] Lookup requires localField and foreignField for ${collectionName}`);
3656
+ return null;
3657
+ }
3658
+ return {
3659
+ from,
3660
+ localField,
3661
+ foreignField,
3662
+ as: opts.as || collectionName,
3663
+ single: opts.single === true || opts.single === "true",
3664
+ ...opts.pipeline && Array.isArray(opts.pipeline) ? { pipeline: opts.pipeline } : {}
3665
+ };
3666
+ }
3667
+ return null;
3668
+ }
3669
+ // ============================================================
3670
+ // AGGREGATION PARSING (ADVANCED)
3671
+ // ============================================================
3672
+ /**
3673
+ * Parse aggregation pipeline from URL (advanced feature)
3674
+ *
3675
+ * @example
3676
+ * ```typescript
3677
+ * // URL: ?aggregate[group][_id]=$status&aggregate[group][count]=$sum:1
3678
+ * const pipeline = parser._parseAggregation({
3679
+ * group: { _id: '$status', count: '$sum:1' }
3680
+ * });
3681
+ * ```
3682
+ */
3683
+ _parseAggregation(aggregate2) {
3684
+ if (!aggregate2 || typeof aggregate2 !== "object") return void 0;
3685
+ const pipeline = [];
3686
+ const aggObj = aggregate2;
3687
+ for (const [stage, config] of Object.entries(aggObj)) {
3688
+ try {
3689
+ if (stage === "group" && typeof config === "object") {
3690
+ pipeline.push({ $group: config });
3691
+ } else if (stage === "match" && typeof config === "object") {
3692
+ const sanitizedMatch = this._sanitizeMatchConfig(config);
3693
+ if (Object.keys(sanitizedMatch).length > 0) {
3694
+ pipeline.push({ $match: sanitizedMatch });
3695
+ }
3696
+ } else if (stage === "sort" && typeof config === "object") {
3697
+ pipeline.push({ $sort: config });
3698
+ } else if (stage === "project" && typeof config === "object") {
3699
+ pipeline.push({ $project: config });
3700
+ }
3701
+ } catch (error) {
3702
+ console.warn(`[mongokit] Invalid aggregation stage ${stage}:`, error);
3703
+ }
3704
+ }
3705
+ return pipeline.length > 0 ? pipeline : void 0;
3706
+ }
3707
+ // ============================================================
3708
+ // SELECT/PROJECT PARSING
3709
+ // ============================================================
3710
+ /**
3711
+ * Parse select/project fields
3712
+ *
3713
+ * @example
3714
+ * ```typescript
3715
+ * // URL: ?select=name,email,-password
3716
+ * // Returns: { name: 1, email: 1, password: 0 }
3717
+ * ```
3718
+ */
3719
+ _parseSelect(select) {
3720
+ if (!select) return void 0;
3721
+ if (typeof select === "string") {
3722
+ const projection = {};
3723
+ const fields = select.split(",").map((f) => f.trim());
3724
+ for (const field of fields) {
3725
+ if (field.startsWith("-")) {
3726
+ projection[field.substring(1)] = 0;
3727
+ } else {
3728
+ projection[field] = 1;
3729
+ }
3730
+ }
3731
+ return projection;
3732
+ }
3733
+ if (typeof select === "object" && select !== null) {
3734
+ return select;
3735
+ }
3736
+ return void 0;
2750
3737
  }
3738
+ // ============================================================
3739
+ // FILTER PARSING (Enhanced from original)
3740
+ // ============================================================
2751
3741
  /**
2752
- * Parse standard filter parameter (filter[field]=value)
3742
+ * Parse filter parameters
2753
3743
  */
2754
- _parseFilters(filters) {
3744
+ _parseFilters(filters, depth = 0) {
3745
+ if (depth > this.options.maxFilterDepth) {
3746
+ console.warn(`[mongokit] Filter depth ${depth} exceeds maximum ${this.options.maxFilterDepth}, truncating`);
3747
+ return {};
3748
+ }
2755
3749
  const parsedFilters = {};
2756
3750
  const regexFields = {};
2757
3751
  for (const [key, value] of Object.entries(filters)) {
@@ -2759,7 +3753,7 @@ var QueryParser = class {
2759
3753
  console.warn(`[mongokit] Blocked dangerous operator: ${key}`);
2760
3754
  continue;
2761
3755
  }
2762
- if (["page", "limit", "sort", "populate", "search", "select", "lean", "includeDeleted"].includes(key)) {
3756
+ if (["page", "limit", "sort", "populate", "search", "select", "lean", "includeDeleted", "lookup", "aggregate"].includes(key)) {
2763
3757
  continue;
2764
3758
  }
2765
3759
  const operatorMatch = key.match(/^(.+)\[(.+)\]$/);
@@ -2773,7 +3767,7 @@ var QueryParser = class {
2773
3767
  continue;
2774
3768
  }
2775
3769
  if (typeof value === "object" && value !== null && !Array.isArray(value)) {
2776
- this._handleBracketSyntax(key, value, parsedFilters);
3770
+ this._handleBracketSyntax(key, value, parsedFilters, depth + 1);
2777
3771
  } else {
2778
3772
  parsedFilters[key] = this._convertValue(value);
2779
3773
  }
@@ -2785,6 +3779,9 @@ var QueryParser = class {
2785
3779
  */
2786
3780
  _handleOperatorSyntax(filters, regexFields, operatorMatch, value) {
2787
3781
  const [, field, operator] = operatorMatch;
3782
+ if (value === "" || value === null || value === void 0) {
3783
+ return;
3784
+ }
2788
3785
  if (operator.toLowerCase() === "options" && regexFields[field]) {
2789
3786
  const fieldValue = filters[field];
2790
3787
  if (typeof fieldValue === "object" && fieldValue !== null && "$regex" in fieldValue) {
@@ -2802,18 +3799,18 @@ var QueryParser = class {
2802
3799
  }
2803
3800
  const mongoOperator = this._toMongoOperator(operator);
2804
3801
  if (this.dangerousOperators.includes(mongoOperator)) {
2805
- console.warn(`[mongokit] Blocked dangerous operator in field[${operator}]: ${mongoOperator}`);
3802
+ console.warn(`[mongokit] Blocked dangerous operator: ${mongoOperator}`);
2806
3803
  return;
2807
3804
  }
2808
3805
  if (mongoOperator === "$eq") {
2809
3806
  filters[field] = value;
2810
3807
  } else if (mongoOperator === "$regex") {
2811
- filters[field] = { $regex: value };
2812
- regexFields[field] = true;
2813
- } else {
2814
- if (typeof filters[field] !== "object" || filters[field] === null || Array.isArray(filters[field])) {
2815
- filters[field] = {};
3808
+ const safeRegex = this._createSafeRegex(value);
3809
+ if (safeRegex) {
3810
+ filters[field] = { $regex: safeRegex };
3811
+ regexFields[field] = true;
2816
3812
  }
3813
+ } else {
2817
3814
  let processedValue;
2818
3815
  const op = operator.toLowerCase();
2819
3816
  if (["gt", "gte", "lt", "lte", "size"].includes(op)) {
@@ -2824,17 +3821,25 @@ var QueryParser = class {
2824
3821
  } else {
2825
3822
  processedValue = this._convertValue(value);
2826
3823
  }
3824
+ if (typeof filters[field] !== "object" || filters[field] === null || Array.isArray(filters[field])) {
3825
+ filters[field] = {};
3826
+ }
2827
3827
  filters[field][mongoOperator] = processedValue;
2828
3828
  }
2829
3829
  }
2830
3830
  /**
2831
3831
  * Handle bracket syntax with object value
2832
3832
  */
2833
- _handleBracketSyntax(field, operators, parsedFilters) {
3833
+ _handleBracketSyntax(field, operators, parsedFilters, depth = 0) {
3834
+ if (depth > this.options.maxFilterDepth) {
3835
+ console.warn(`[mongokit] Nested filter depth exceeds maximum, skipping field: ${field}`);
3836
+ return;
3837
+ }
2834
3838
  if (!parsedFilters[field]) {
2835
3839
  parsedFilters[field] = {};
2836
3840
  }
2837
3841
  for (const [operator, value] of Object.entries(operators)) {
3842
+ if (value === "" || value === null || value === void 0) continue;
2838
3843
  if (operator === "between") {
2839
3844
  parsedFilters[field].between = value;
2840
3845
  continue;
@@ -2847,7 +3852,7 @@ var QueryParser = class {
2847
3852
  if (isNaN(processedValue)) continue;
2848
3853
  } else if (operator === "in" || operator === "nin") {
2849
3854
  processedValue = Array.isArray(value) ? value : String(value).split(",").map((v) => v.trim());
2850
- } else if (operator === "like" || operator === "contains") {
3855
+ } else if (operator === "like" || operator === "contains" || operator === "regex") {
2851
3856
  const safeRegex = this._createSafeRegex(value);
2852
3857
  if (!safeRegex) continue;
2853
3858
  processedValue = safeRegex;
@@ -2857,31 +3862,40 @@ var QueryParser = class {
2857
3862
  parsedFilters[field][mongoOperator] = processedValue;
2858
3863
  }
2859
3864
  }
3865
+ if (typeof parsedFilters[field] === "object" && Object.keys(parsedFilters[field]).length === 0) {
3866
+ delete parsedFilters[field];
3867
+ }
3868
+ }
3869
+ // ============================================================
3870
+ // UTILITY METHODS
3871
+ // ============================================================
3872
+ _parseSort(sort) {
3873
+ if (!sort) return void 0;
3874
+ if (typeof sort === "object") return sort;
3875
+ const sortObj = {};
3876
+ const fields = sort.split(",").map((s) => s.trim());
3877
+ for (const field of fields) {
3878
+ if (field.startsWith("-")) {
3879
+ sortObj[field.substring(1)] = -1;
3880
+ } else {
3881
+ sortObj[field] = 1;
3882
+ }
3883
+ }
3884
+ return sortObj;
2860
3885
  }
2861
- /**
2862
- * Convert operator to MongoDB format
2863
- */
2864
3886
  _toMongoOperator(operator) {
2865
3887
  const op = operator.toLowerCase();
2866
3888
  return op.startsWith("$") ? op : "$" + op;
2867
3889
  }
2868
- /**
2869
- * Create a safe regex pattern with protection against ReDoS attacks
2870
- * @param pattern - The pattern string from user input
2871
- * @param flags - Regex flags (default: 'i' for case-insensitive)
2872
- * @returns A safe RegExp or null if pattern is invalid/dangerous
2873
- */
2874
3890
  _createSafeRegex(pattern, flags = "i") {
2875
- if (pattern === null || pattern === void 0) {
2876
- return null;
2877
- }
3891
+ if (pattern === null || pattern === void 0) return null;
2878
3892
  const patternStr = String(pattern);
2879
3893
  if (patternStr.length > this.options.maxRegexLength) {
2880
- console.warn(`[mongokit] Regex pattern too long (${patternStr.length} > ${this.options.maxRegexLength}), truncating`);
3894
+ console.warn(`[mongokit] Regex pattern too long, truncating`);
2881
3895
  return new RegExp(this._escapeRegex(patternStr.substring(0, this.options.maxRegexLength)), flags);
2882
3896
  }
2883
3897
  if (this.dangerousRegexPatterns.test(patternStr)) {
2884
- console.warn("[mongokit] Potentially dangerous regex pattern detected, escaping");
3898
+ console.warn("[mongokit] Potentially dangerous regex pattern, escaping");
2885
3899
  return new RegExp(this._escapeRegex(patternStr), flags);
2886
3900
  }
2887
3901
  try {
@@ -2890,34 +3904,45 @@ var QueryParser = class {
2890
3904
  return new RegExp(this._escapeRegex(patternStr), flags);
2891
3905
  }
2892
3906
  }
2893
- /**
2894
- * Escape special regex characters for literal matching
2895
- */
2896
3907
  _escapeRegex(str) {
2897
3908
  return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2898
3909
  }
2899
3910
  /**
2900
- * Sanitize text search query for MongoDB $text search
2901
- * @param search - Raw search input from user
2902
- * @returns Sanitized search string or undefined
3911
+ * Sanitize $match configuration to prevent dangerous operators
3912
+ * Recursively filters out operators like $where, $function, $accumulator
2903
3913
  */
2904
- _sanitizeSearch(search) {
2905
- if (search === null || search === void 0 || search === "") {
2906
- return void 0;
3914
+ _sanitizeMatchConfig(config) {
3915
+ const sanitized = {};
3916
+ for (const [key, value] of Object.entries(config)) {
3917
+ if (this.dangerousOperators.includes(key)) {
3918
+ console.warn(`[mongokit] Blocked dangerous operator in aggregation: ${key}`);
3919
+ continue;
3920
+ }
3921
+ if (value && typeof value === "object" && !Array.isArray(value)) {
3922
+ sanitized[key] = this._sanitizeMatchConfig(value);
3923
+ } else if (Array.isArray(value)) {
3924
+ sanitized[key] = value.map((item) => {
3925
+ if (item && typeof item === "object" && !Array.isArray(item)) {
3926
+ return this._sanitizeMatchConfig(item);
3927
+ }
3928
+ return item;
3929
+ });
3930
+ } else {
3931
+ sanitized[key] = value;
3932
+ }
2907
3933
  }
3934
+ return sanitized;
3935
+ }
3936
+ _sanitizeSearch(search) {
3937
+ if (search === null || search === void 0 || search === "") return void 0;
2908
3938
  let searchStr = String(search).trim();
2909
- if (!searchStr) {
2910
- return void 0;
2911
- }
3939
+ if (!searchStr) return void 0;
2912
3940
  if (searchStr.length > this.options.maxSearchLength) {
2913
- console.warn(`[mongokit] Search query too long (${searchStr.length} > ${this.options.maxSearchLength}), truncating`);
3941
+ console.warn(`[mongokit] Search query too long, truncating`);
2914
3942
  searchStr = searchStr.substring(0, this.options.maxSearchLength);
2915
3943
  }
2916
3944
  return searchStr;
2917
3945
  }
2918
- /**
2919
- * Convert values based on operator type
2920
- */
2921
3946
  _convertValue(value) {
2922
3947
  if (value === null || value === void 0) return value;
2923
3948
  if (Array.isArray(value)) return value.map((v) => this._convertValue(v));
@@ -2925,14 +3950,11 @@ var QueryParser = class {
2925
3950
  const stringValue = String(value);
2926
3951
  if (stringValue === "true") return true;
2927
3952
  if (stringValue === "false") return false;
2928
- if (mongoose4.Types.ObjectId.isValid(stringValue) && stringValue.length === 24) {
3953
+ if (mongoose.Types.ObjectId.isValid(stringValue) && stringValue.length === 24) {
2929
3954
  return stringValue;
2930
3955
  }
2931
3956
  return stringValue;
2932
3957
  }
2933
- /**
2934
- * Parse $or conditions
2935
- */
2936
3958
  _parseOr(query) {
2937
3959
  const orArray = [];
2938
3960
  const raw = query?.or || query?.OR || query?.$or;
@@ -2940,14 +3962,11 @@ var QueryParser = class {
2940
3962
  const items = Array.isArray(raw) ? raw : typeof raw === "object" ? Object.values(raw) : [];
2941
3963
  for (const item of items) {
2942
3964
  if (typeof item === "object" && item) {
2943
- orArray.push(this._parseFilters(item));
3965
+ orArray.push(this._parseFilters(item, 1));
2944
3966
  }
2945
3967
  }
2946
3968
  return orArray.length ? orArray : void 0;
2947
3969
  }
2948
- /**
2949
- * Enhance filters with between operator
2950
- */
2951
3970
  _enhanceWithBetween(filters) {
2952
3971
  const output = { ...filters };
2953
3972
  for (const [key, value] of Object.entries(filters || {})) {
@@ -2964,9 +3983,16 @@ var QueryParser = class {
2964
3983
  }
2965
3984
  return output;
2966
3985
  }
3986
+ // String helpers
3987
+ _pluralize(str) {
3988
+ if (str.endsWith("y")) return str.slice(0, -1) + "ies";
3989
+ if (str.endsWith("s")) return str;
3990
+ return str + "s";
3991
+ }
3992
+ _capitalize(str) {
3993
+ return str.charAt(0).toUpperCase() + str.slice(1);
3994
+ }
2967
3995
  };
2968
- var defaultQueryParser = new QueryParser();
2969
- var queryParser_default = defaultQueryParser;
2970
3996
 
2971
3997
  // src/actions/index.ts
2972
3998
  var actions_exports = {};
@@ -3022,4 +4048,4 @@ var index_default = Repository;
3022
4048
  * ```
3023
4049
  */
3024
4050
 
3025
- export { PaginationEngine, QueryParser, Repository, actions_exports as actions, aggregateHelpersPlugin, auditLogPlugin, autoInject, batchOperationsPlugin, blockIf, buildCrudSchemasFromModel, buildCrudSchemasFromMongooseSchema, cachePlugin, cascadePlugin, createError, createFieldPreset, createMemoryCache, createRepository, index_default as default, fieldFilterPlugin, filterResponseData, getFieldsForUser, getImmutableFields, getMongooseProjection, getSystemManagedFields, immutableField, isFieldUpdateAllowed, methodRegistryPlugin, mongoOperationsPlugin, queryParser_default as queryParser, requireField, softDeletePlugin, subdocumentPlugin, timestampPlugin, uniqueField, validateUpdateBody, validationChainPlugin };
4051
+ export { AggregationBuilder, LookupBuilder, PaginationEngine, QueryParser, Repository, actions_exports as actions, aggregateHelpersPlugin, auditLogPlugin, autoInject, batchOperationsPlugin, blockIf, buildCrudSchemasFromModel, buildCrudSchemasFromMongooseSchema, cachePlugin, cascadePlugin, createError, createFieldPreset, createMemoryCache, createRepository, index_default as default, fieldFilterPlugin, filterResponseData, getFieldsForUser, getImmutableFields, getMongooseProjection, getSystemManagedFields, immutableField, isFieldUpdateAllowed, methodRegistryPlugin, mongoOperationsPlugin, requireField, softDeletePlugin, subdocumentPlugin, timestampPlugin, uniqueField, validateUpdateBody, validationChainPlugin };