@classytic/mongokit 3.0.3 → 3.0.5

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 mongoose from 'mongoose';
1
+ import mongoose4 from 'mongoose';
2
2
 
3
3
  var __defProp = Object.defineProperty;
4
4
  var __export = (target, all) => {
@@ -521,12 +521,12 @@ function validateCursorVersion(cursorVersion, expectedVersion) {
521
521
  }
522
522
  function serializeValue(value) {
523
523
  if (value instanceof Date) return value.toISOString();
524
- if (value instanceof mongoose.Types.ObjectId) return value.toString();
524
+ if (value instanceof mongoose4.Types.ObjectId) return value.toString();
525
525
  return value;
526
526
  }
527
527
  function getValueType(value) {
528
528
  if (value instanceof Date) return "date";
529
- if (value instanceof mongoose.Types.ObjectId) return "objectid";
529
+ if (value instanceof mongoose4.Types.ObjectId) return "objectid";
530
530
  if (typeof value === "boolean") return "boolean";
531
531
  if (typeof value === "number") return "number";
532
532
  if (typeof value === "string") return "string";
@@ -537,7 +537,7 @@ function rehydrateValue(serialized, type) {
537
537
  case "date":
538
538
  return new Date(serialized);
539
539
  case "objectid":
540
- return new mongoose.Types.ObjectId(serialized);
540
+ return new mongoose4.Types.ObjectId(serialized);
541
541
  case "boolean":
542
542
  return Boolean(serialized);
543
543
  case "number":
@@ -1001,7 +1001,7 @@ var Repository = class {
1001
1001
  const hasCursorParam = "cursor" in params || "after" in params;
1002
1002
  const hasExplicitSort = params.sort !== void 0;
1003
1003
  const useKeyset = !hasPageParam && (hasCursorParam || hasExplicitSort);
1004
- const filters = params.filters || {};
1004
+ const filters = context.filters || params.filters || {};
1005
1005
  const search = params.search;
1006
1006
  const sort = params.sort || "-createdAt";
1007
1007
  const limit = params.limit || params.pagination?.limit || this._pagination.config.defaultLimit;
@@ -1109,7 +1109,7 @@ var Repository = class {
1109
1109
  * Execute callback within a transaction
1110
1110
  */
1111
1111
  async withTransaction(callback) {
1112
- const session = await mongoose.startSession();
1112
+ const session = await mongoose4.startSession();
1113
1113
  session.startTransaction();
1114
1114
  try {
1115
1115
  const result = await callback(session);
@@ -1172,11 +1172,11 @@ var Repository = class {
1172
1172
  * Handle errors with proper HTTP status codes
1173
1173
  */
1174
1174
  _handleError(error) {
1175
- if (error instanceof mongoose.Error.ValidationError) {
1175
+ if (error instanceof mongoose4.Error.ValidationError) {
1176
1176
  const messages = Object.values(error.errors).map((err) => err.message);
1177
1177
  return createError(400, `Validation Error: ${messages.join(", ")}`);
1178
1178
  }
1179
- if (error instanceof mongoose.Error.CastError) {
1179
+ if (error instanceof mongoose4.Error.CastError) {
1180
1180
  return createError(400, `Invalid ${error.path}: ${error.value}`);
1181
1181
  }
1182
1182
  if (error.status && error.message) return error;
@@ -1329,12 +1329,45 @@ function auditLogPlugin(logger) {
1329
1329
  }
1330
1330
 
1331
1331
  // src/plugins/soft-delete.plugin.ts
1332
+ function buildDeletedFilter(deletedField, filterMode, includeDeleted) {
1333
+ if (includeDeleted) {
1334
+ return {};
1335
+ }
1336
+ if (filterMode === "exists") {
1337
+ return { [deletedField]: { $exists: false } };
1338
+ }
1339
+ return { [deletedField]: null };
1340
+ }
1341
+ function buildGetDeletedFilter(deletedField, filterMode) {
1342
+ if (filterMode === "exists") {
1343
+ return { [deletedField]: { $exists: true, $ne: null } };
1344
+ }
1345
+ return { [deletedField]: { $ne: null } };
1346
+ }
1332
1347
  function softDeletePlugin(options = {}) {
1333
1348
  const deletedField = options.deletedField || "deletedAt";
1334
1349
  const deletedByField = options.deletedByField || "deletedBy";
1350
+ const filterMode = options.filterMode || "null";
1351
+ const addRestoreMethod = options.addRestoreMethod !== false;
1352
+ const addGetDeletedMethod = options.addGetDeletedMethod !== false;
1353
+ const ttlDays = options.ttlDays;
1335
1354
  return {
1336
1355
  name: "softDelete",
1337
1356
  apply(repo) {
1357
+ if (ttlDays !== void 0 && ttlDays > 0) {
1358
+ const ttlSeconds = ttlDays * 24 * 60 * 60;
1359
+ repo.Model.collection.createIndex(
1360
+ { [deletedField]: 1 },
1361
+ {
1362
+ expireAfterSeconds: ttlSeconds,
1363
+ partialFilterExpression: { [deletedField]: { $type: "date" } }
1364
+ }
1365
+ ).catch((err) => {
1366
+ if (!err.message.includes("already exists")) {
1367
+ console.warn(`[softDeletePlugin] Failed to create TTL index: ${err.message}`);
1368
+ }
1369
+ });
1370
+ }
1338
1371
  repo.on("before:delete", async (context) => {
1339
1372
  if (options.soft !== false) {
1340
1373
  const updateData = {
@@ -1348,23 +1381,126 @@ function softDeletePlugin(options = {}) {
1348
1381
  }
1349
1382
  });
1350
1383
  repo.on("before:getAll", (context) => {
1351
- if (!context.includeDeleted && options.soft !== false) {
1352
- const queryParams = context.queryParams || {};
1353
- queryParams.filters = {
1354
- ...queryParams.filters || {},
1355
- [deletedField]: { $exists: false }
1356
- };
1357
- context.queryParams = queryParams;
1384
+ if (options.soft !== false) {
1385
+ const deleteFilter = buildDeletedFilter(deletedField, filterMode, !!context.includeDeleted);
1386
+ if (Object.keys(deleteFilter).length > 0) {
1387
+ const existingFilters = context.filters || {};
1388
+ context.filters = {
1389
+ ...existingFilters,
1390
+ ...deleteFilter
1391
+ };
1392
+ }
1358
1393
  }
1359
1394
  });
1360
1395
  repo.on("before:getById", (context) => {
1361
- if (!context.includeDeleted && options.soft !== false) {
1362
- context.query = {
1363
- ...context.query || {},
1364
- [deletedField]: { $exists: false }
1365
- };
1396
+ if (options.soft !== false) {
1397
+ const deleteFilter = buildDeletedFilter(deletedField, filterMode, !!context.includeDeleted);
1398
+ if (Object.keys(deleteFilter).length > 0) {
1399
+ context.query = {
1400
+ ...context.query || {},
1401
+ ...deleteFilter
1402
+ };
1403
+ }
1404
+ }
1405
+ });
1406
+ repo.on("before:getByQuery", (context) => {
1407
+ if (options.soft !== false) {
1408
+ const deleteFilter = buildDeletedFilter(deletedField, filterMode, !!context.includeDeleted);
1409
+ if (Object.keys(deleteFilter).length > 0) {
1410
+ context.query = {
1411
+ ...context.query || {},
1412
+ ...deleteFilter
1413
+ };
1414
+ }
1366
1415
  }
1367
1416
  });
1417
+ if (addRestoreMethod) {
1418
+ const restoreMethod = async function(id, restoreOptions = {}) {
1419
+ const updateData = {
1420
+ [deletedField]: null,
1421
+ [deletedByField]: null
1422
+ };
1423
+ const result = await this.Model.findByIdAndUpdate(id, { $set: updateData }, {
1424
+ new: true,
1425
+ session: restoreOptions.session
1426
+ });
1427
+ if (!result) {
1428
+ const error = new Error(`Document with id '${id}' not found`);
1429
+ error.status = 404;
1430
+ throw error;
1431
+ }
1432
+ await this.emitAsync("after:restore", { id, result });
1433
+ return result;
1434
+ };
1435
+ if (typeof repo.registerMethod === "function") {
1436
+ repo.registerMethod("restore", restoreMethod);
1437
+ } else {
1438
+ repo.restore = restoreMethod.bind(repo);
1439
+ }
1440
+ }
1441
+ if (addGetDeletedMethod) {
1442
+ const getDeletedMethod = async function(params = {}, getDeletedOptions = {}) {
1443
+ const deletedFilter = buildGetDeletedFilter(deletedField, filterMode);
1444
+ const combinedFilters = {
1445
+ ...params.filters || {},
1446
+ ...deletedFilter
1447
+ };
1448
+ const page = params.page || 1;
1449
+ const limit = params.limit || 20;
1450
+ const skip = (page - 1) * limit;
1451
+ let sortSpec = { [deletedField]: -1 };
1452
+ if (params.sort) {
1453
+ if (typeof params.sort === "string") {
1454
+ const sortOrder = params.sort.startsWith("-") ? -1 : 1;
1455
+ const sortField = params.sort.startsWith("-") ? params.sort.substring(1) : params.sort;
1456
+ sortSpec = { [sortField]: sortOrder };
1457
+ } else {
1458
+ sortSpec = params.sort;
1459
+ }
1460
+ }
1461
+ let query = this.Model.find(combinedFilters).sort(sortSpec).skip(skip).limit(limit);
1462
+ if (getDeletedOptions.session) {
1463
+ query = query.session(getDeletedOptions.session);
1464
+ }
1465
+ if (getDeletedOptions.select) {
1466
+ const selectValue = Array.isArray(getDeletedOptions.select) ? getDeletedOptions.select.join(" ") : getDeletedOptions.select;
1467
+ query = query.select(selectValue);
1468
+ }
1469
+ if (getDeletedOptions.populate) {
1470
+ const populateSpec = getDeletedOptions.populate;
1471
+ if (typeof populateSpec === "string") {
1472
+ query = query.populate(populateSpec.split(",").map((p) => p.trim()));
1473
+ } else if (Array.isArray(populateSpec)) {
1474
+ query = query.populate(populateSpec);
1475
+ } else {
1476
+ query = query.populate(populateSpec);
1477
+ }
1478
+ }
1479
+ if (getDeletedOptions.lean !== false) {
1480
+ query = query.lean();
1481
+ }
1482
+ const [docs, total] = await Promise.all([
1483
+ query.exec(),
1484
+ this.Model.countDocuments(combinedFilters)
1485
+ ]);
1486
+ const pages = Math.ceil(total / limit);
1487
+ return {
1488
+ method: "offset",
1489
+ docs,
1490
+ page,
1491
+ limit,
1492
+ total,
1493
+ pages,
1494
+ hasNext: page < pages,
1495
+ hasPrev: page > 1
1496
+ };
1497
+ };
1498
+ if (typeof repo.registerMethod === "function") {
1499
+ repo.registerMethod("getDeleted", getDeletedMethod);
1500
+ } else {
1501
+ repo.getDeleted = getDeletedMethod.bind(repo);
1502
+ }
1503
+ }
1368
1504
  }
1369
1505
  };
1370
1506
  }
@@ -2071,7 +2207,7 @@ function cascadePlugin(options) {
2071
2207
  }
2072
2208
  const isSoftDelete = context.softDeleted === true;
2073
2209
  const cascadeDelete = async (relation) => {
2074
- const RelatedModel = mongoose.models[relation.model];
2210
+ const RelatedModel = mongoose4.models[relation.model];
2075
2211
  if (!RelatedModel) {
2076
2212
  logger?.warn?.(`Cascade delete skipped: model '${relation.model}' not found`, {
2077
2213
  parentModel: context.model,
@@ -2162,7 +2298,7 @@ function cascadePlugin(options) {
2162
2298
  }
2163
2299
  const isSoftDelete = context.softDeleted === true;
2164
2300
  const cascadeDeleteMany = async (relation) => {
2165
- const RelatedModel = mongoose.models[relation.model];
2301
+ const RelatedModel = mongoose4.models[relation.model];
2166
2302
  if (!RelatedModel) {
2167
2303
  logger?.warn?.(`Cascade deleteMany skipped: model '${relation.model}' not found`, {
2168
2304
  parentModel: context.model
@@ -2278,6 +2414,559 @@ function createMemoryCache(maxEntries = 1e3) {
2278
2414
  }
2279
2415
  };
2280
2416
  }
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
+ function buildCrudSchemasFromMongooseSchema(mongooseSchema, options = {}) {
2427
+ const tree = mongooseSchema?.obj || {};
2428
+ const jsonCreate = buildJsonSchemaForCreate(tree, options);
2429
+ const jsonUpdate = buildJsonSchemaForUpdate(jsonCreate, options);
2430
+ const jsonParams = {
2431
+ type: "object",
2432
+ properties: { id: { type: "string", pattern: "^[0-9a-fA-F]{24}$" } },
2433
+ required: ["id"]
2434
+ };
2435
+ const jsonQuery = buildJsonSchemaForQuery(tree, options);
2436
+ return { createBody: jsonCreate, updateBody: jsonUpdate, params: jsonParams, listQuery: jsonQuery };
2437
+ }
2438
+ function buildCrudSchemasFromModel(mongooseModel, options = {}) {
2439
+ if (!mongooseModel || !mongooseModel.schema) {
2440
+ throw new Error("Invalid mongoose model");
2441
+ }
2442
+ return buildCrudSchemasFromMongooseSchema(mongooseModel.schema, options);
2443
+ }
2444
+ function getImmutableFields(options = {}) {
2445
+ const immutable = [];
2446
+ const fieldRules = options?.fieldRules || {};
2447
+ Object.entries(fieldRules).forEach(([field, rules]) => {
2448
+ if (rules.immutable || rules.immutableAfterCreate) {
2449
+ immutable.push(field);
2450
+ }
2451
+ });
2452
+ (options?.update?.omitFields || []).forEach((f) => {
2453
+ if (!immutable.includes(f)) immutable.push(f);
2454
+ });
2455
+ return immutable;
2456
+ }
2457
+ function getSystemManagedFields(options = {}) {
2458
+ const systemManaged = [];
2459
+ const fieldRules = options?.fieldRules || {};
2460
+ Object.entries(fieldRules).forEach(([field, rules]) => {
2461
+ if (rules.systemManaged) {
2462
+ systemManaged.push(field);
2463
+ }
2464
+ });
2465
+ return systemManaged;
2466
+ }
2467
+ function isFieldUpdateAllowed(fieldName, options = {}) {
2468
+ const immutableFields = getImmutableFields(options);
2469
+ const systemManagedFields = getSystemManagedFields(options);
2470
+ return !immutableFields.includes(fieldName) && !systemManagedFields.includes(fieldName);
2471
+ }
2472
+ function validateUpdateBody(body = {}, options = {}) {
2473
+ const violations = [];
2474
+ const immutableFields = getImmutableFields(options);
2475
+ const systemManagedFields = getSystemManagedFields(options);
2476
+ Object.keys(body).forEach((field) => {
2477
+ if (immutableFields.includes(field)) {
2478
+ violations.push({ field, reason: "Field is immutable" });
2479
+ } else if (systemManagedFields.includes(field)) {
2480
+ violations.push({ field, reason: "Field is system-managed" });
2481
+ }
2482
+ });
2483
+ return {
2484
+ valid: violations.length === 0,
2485
+ violations
2486
+ };
2487
+ }
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 } };
2504
+ }
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);
2530
+ }
2531
+ }
2532
+ }
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
+ const schema = { type: "object", properties };
2565
+ 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
+ const fieldsToOmit = /* @__PURE__ */ new Set(["createdAt", "updatedAt", "__v"]);
2571
+ (options?.create?.omitFields || []).forEach((f) => fieldsToOmit.add(f));
2572
+ const fieldRules = options?.fieldRules || {};
2573
+ Object.entries(fieldRules).forEach(([field, rules]) => {
2574
+ if (rules.systemManaged) {
2575
+ fieldsToOmit.add(field);
2576
+ }
2577
+ });
2578
+ fieldsToOmit.forEach((field) => {
2579
+ if (base.properties?.[field]) {
2580
+ delete base.properties[field];
2581
+ }
2582
+ if (base.required) {
2583
+ base.required = base.required.filter((k) => k !== field);
2584
+ }
2585
+ });
2586
+ const reqOv = options?.create?.requiredOverrides || {};
2587
+ const optOv = options?.create?.optionalOverrides || {};
2588
+ base.required = base.required || [];
2589
+ for (const [k, v] of Object.entries(reqOv)) {
2590
+ if (v && !base.required.includes(k)) base.required.push(k);
2591
+ }
2592
+ for (const [k, v] of Object.entries(optOv)) {
2593
+ if (v && base.required) base.required = base.required.filter((x) => x !== k);
2594
+ }
2595
+ Object.entries(fieldRules).forEach(([field, rules]) => {
2596
+ if (rules.optional && base.required) {
2597
+ base.required = base.required.filter((x) => x !== field);
2598
+ }
2599
+ });
2600
+ const schemaOverrides = options?.create?.schemaOverrides || {};
2601
+ for (const [k, override] of Object.entries(schemaOverrides)) {
2602
+ if (base.properties?.[k]) {
2603
+ base.properties[k] = override;
2604
+ }
2605
+ }
2606
+ if (options?.strictAdditionalProperties === true) {
2607
+ base.additionalProperties = false;
2608
+ }
2609
+ return base;
2610
+ }
2611
+ function buildJsonSchemaForUpdate(createJson, options) {
2612
+ const clone = JSON.parse(JSON.stringify(createJson));
2613
+ delete clone.required;
2614
+ const fieldsToOmit = /* @__PURE__ */ new Set();
2615
+ (options?.update?.omitFields || []).forEach((f) => fieldsToOmit.add(f));
2616
+ const fieldRules = options?.fieldRules || {};
2617
+ Object.entries(fieldRules).forEach(([field, rules]) => {
2618
+ if (rules.immutable || rules.immutableAfterCreate) {
2619
+ fieldsToOmit.add(field);
2620
+ }
2621
+ });
2622
+ fieldsToOmit.forEach((field) => {
2623
+ if (clone.properties?.[field]) {
2624
+ delete clone.properties[field];
2625
+ }
2626
+ });
2627
+ if (options?.strictAdditionalProperties === true) {
2628
+ clone.additionalProperties = false;
2629
+ }
2630
+ return clone;
2631
+ }
2632
+ function buildJsonSchemaForQuery(_tree, options) {
2633
+ const basePagination = {
2634
+ type: "object",
2635
+ properties: {
2636
+ page: { type: "string" },
2637
+ limit: { type: "string" },
2638
+ sort: { type: "string" },
2639
+ populate: { type: "string" },
2640
+ search: { type: "string" },
2641
+ select: { type: "string" },
2642
+ lean: { type: "string" },
2643
+ includeDeleted: { type: "string" }
2644
+ },
2645
+ additionalProperties: true
2646
+ };
2647
+ const filterable = options?.query?.filterableFields || {};
2648
+ for (const [k, v] of Object.entries(filterable)) {
2649
+ if (basePagination.properties) {
2650
+ basePagination.properties[k] = v && typeof v === "object" && "type" in v ? v : { type: "string" };
2651
+ }
2652
+ }
2653
+ return basePagination;
2654
+ }
2655
+ var QueryParser = class {
2656
+ options;
2657
+ operators = {
2658
+ eq: "$eq",
2659
+ ne: "$ne",
2660
+ gt: "$gt",
2661
+ gte: "$gte",
2662
+ lt: "$lt",
2663
+ lte: "$lte",
2664
+ in: "$in",
2665
+ nin: "$nin",
2666
+ like: "$regex",
2667
+ contains: "$regex",
2668
+ regex: "$regex",
2669
+ exists: "$exists",
2670
+ size: "$size",
2671
+ type: "$type"
2672
+ };
2673
+ /**
2674
+ * Dangerous MongoDB operators that should never be accepted from user input
2675
+ * Security: Prevent NoSQL injection attacks
2676
+ */
2677
+ dangerousOperators;
2678
+ /**
2679
+ * Regex pattern characters that can cause catastrophic backtracking (ReDoS)
2680
+ */
2681
+ dangerousRegexPatterns = /(\{[0-9,]+\}|\*\+|\+\+|\?\+|(\([^)]*\))\1|\(\?[^)]*\)|[\[\]].*[\[\]])/;
2682
+ constructor(options = {}) {
2683
+ this.options = {
2684
+ maxRegexLength: options.maxRegexLength ?? 500,
2685
+ maxSearchLength: options.maxSearchLength ?? 200,
2686
+ maxFilterDepth: options.maxFilterDepth ?? 10,
2687
+ additionalDangerousOperators: options.additionalDangerousOperators ?? []
2688
+ };
2689
+ this.dangerousOperators = [
2690
+ "$where",
2691
+ "$function",
2692
+ "$accumulator",
2693
+ "$expr",
2694
+ ...this.options.additionalDangerousOperators
2695
+ ];
2696
+ }
2697
+ /**
2698
+ * Parse query parameters into MongoDB query format
2699
+ */
2700
+ parseQuery(query) {
2701
+ const {
2702
+ page,
2703
+ limit = 20,
2704
+ sort = "-createdAt",
2705
+ populate,
2706
+ search,
2707
+ after,
2708
+ cursor,
2709
+ ...filters
2710
+ } = query || {};
2711
+ const parsed = {
2712
+ filters: this._parseFilters(filters),
2713
+ limit: parseInt(String(limit), 10),
2714
+ sort: this._parseSort(sort),
2715
+ populate,
2716
+ search: this._sanitizeSearch(search)
2717
+ };
2718
+ if (after || cursor) {
2719
+ parsed.after = after || cursor;
2720
+ } else if (page !== void 0) {
2721
+ parsed.page = parseInt(String(page), 10);
2722
+ } else {
2723
+ parsed.page = 1;
2724
+ }
2725
+ const orGroup = this._parseOr(query);
2726
+ if (orGroup) {
2727
+ parsed.filters = { ...parsed.filters, $or: orGroup };
2728
+ }
2729
+ parsed.filters = this._enhanceWithBetween(parsed.filters);
2730
+ return parsed;
2731
+ }
2732
+ /**
2733
+ * Parse sort parameter
2734
+ * Converts string like '-createdAt' to { createdAt: -1 }
2735
+ * Handles multiple sorts: '-createdAt,name' → { createdAt: -1, name: 1 }
2736
+ */
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;
2747
+ }
2748
+ }
2749
+ return sortObj;
2750
+ }
2751
+ /**
2752
+ * Parse standard filter parameter (filter[field]=value)
2753
+ */
2754
+ _parseFilters(filters) {
2755
+ const parsedFilters = {};
2756
+ const regexFields = {};
2757
+ for (const [key, value] of Object.entries(filters)) {
2758
+ if (this.dangerousOperators.includes(key) || key.startsWith("$") && !["$or", "$and"].includes(key)) {
2759
+ console.warn(`[mongokit] Blocked dangerous operator: ${key}`);
2760
+ continue;
2761
+ }
2762
+ if (["page", "limit", "sort", "populate", "search", "select", "lean", "includeDeleted"].includes(key)) {
2763
+ continue;
2764
+ }
2765
+ const operatorMatch = key.match(/^(.+)\[(.+)\]$/);
2766
+ if (operatorMatch) {
2767
+ const [, , operator] = operatorMatch;
2768
+ if (this.dangerousOperators.includes("$" + operator)) {
2769
+ console.warn(`[mongokit] Blocked dangerous operator: ${operator}`);
2770
+ continue;
2771
+ }
2772
+ this._handleOperatorSyntax(parsedFilters, regexFields, operatorMatch, value);
2773
+ continue;
2774
+ }
2775
+ if (typeof value === "object" && value !== null && !Array.isArray(value)) {
2776
+ this._handleBracketSyntax(key, value, parsedFilters);
2777
+ } else {
2778
+ parsedFilters[key] = this._convertValue(value);
2779
+ }
2780
+ }
2781
+ return parsedFilters;
2782
+ }
2783
+ /**
2784
+ * Handle operator syntax: field[operator]=value
2785
+ */
2786
+ _handleOperatorSyntax(filters, regexFields, operatorMatch, value) {
2787
+ const [, field, operator] = operatorMatch;
2788
+ if (operator.toLowerCase() === "options" && regexFields[field]) {
2789
+ const fieldValue = filters[field];
2790
+ if (typeof fieldValue === "object" && fieldValue !== null && "$regex" in fieldValue) {
2791
+ fieldValue.$options = value;
2792
+ }
2793
+ return;
2794
+ }
2795
+ if (operator.toLowerCase() === "contains" || operator.toLowerCase() === "like") {
2796
+ const safeRegex = this._createSafeRegex(value);
2797
+ if (safeRegex) {
2798
+ filters[field] = { $regex: safeRegex };
2799
+ regexFields[field] = true;
2800
+ }
2801
+ return;
2802
+ }
2803
+ const mongoOperator = this._toMongoOperator(operator);
2804
+ if (this.dangerousOperators.includes(mongoOperator)) {
2805
+ console.warn(`[mongokit] Blocked dangerous operator in field[${operator}]: ${mongoOperator}`);
2806
+ return;
2807
+ }
2808
+ if (mongoOperator === "$eq") {
2809
+ filters[field] = value;
2810
+ } 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] = {};
2816
+ }
2817
+ let processedValue;
2818
+ const op = operator.toLowerCase();
2819
+ if (["gt", "gte", "lt", "lte", "size"].includes(op)) {
2820
+ processedValue = parseFloat(String(value));
2821
+ if (isNaN(processedValue)) return;
2822
+ } else if (op === "in" || op === "nin") {
2823
+ processedValue = Array.isArray(value) ? value : String(value).split(",").map((v) => v.trim());
2824
+ } else {
2825
+ processedValue = this._convertValue(value);
2826
+ }
2827
+ filters[field][mongoOperator] = processedValue;
2828
+ }
2829
+ }
2830
+ /**
2831
+ * Handle bracket syntax with object value
2832
+ */
2833
+ _handleBracketSyntax(field, operators, parsedFilters) {
2834
+ if (!parsedFilters[field]) {
2835
+ parsedFilters[field] = {};
2836
+ }
2837
+ for (const [operator, value] of Object.entries(operators)) {
2838
+ if (operator === "between") {
2839
+ parsedFilters[field].between = value;
2840
+ continue;
2841
+ }
2842
+ if (this.operators[operator]) {
2843
+ const mongoOperator = this.operators[operator];
2844
+ let processedValue;
2845
+ if (["gt", "gte", "lt", "lte", "size"].includes(operator)) {
2846
+ processedValue = parseFloat(String(value));
2847
+ if (isNaN(processedValue)) continue;
2848
+ } else if (operator === "in" || operator === "nin") {
2849
+ processedValue = Array.isArray(value) ? value : String(value).split(",").map((v) => v.trim());
2850
+ } else if (operator === "like" || operator === "contains") {
2851
+ const safeRegex = this._createSafeRegex(value);
2852
+ if (!safeRegex) continue;
2853
+ processedValue = safeRegex;
2854
+ } else {
2855
+ processedValue = this._convertValue(value);
2856
+ }
2857
+ parsedFilters[field][mongoOperator] = processedValue;
2858
+ }
2859
+ }
2860
+ }
2861
+ /**
2862
+ * Convert operator to MongoDB format
2863
+ */
2864
+ _toMongoOperator(operator) {
2865
+ const op = operator.toLowerCase();
2866
+ return op.startsWith("$") ? op : "$" + op;
2867
+ }
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
+ _createSafeRegex(pattern, flags = "i") {
2875
+ if (pattern === null || pattern === void 0) {
2876
+ return null;
2877
+ }
2878
+ const patternStr = String(pattern);
2879
+ if (patternStr.length > this.options.maxRegexLength) {
2880
+ console.warn(`[mongokit] Regex pattern too long (${patternStr.length} > ${this.options.maxRegexLength}), truncating`);
2881
+ return new RegExp(this._escapeRegex(patternStr.substring(0, this.options.maxRegexLength)), flags);
2882
+ }
2883
+ if (this.dangerousRegexPatterns.test(patternStr)) {
2884
+ console.warn("[mongokit] Potentially dangerous regex pattern detected, escaping");
2885
+ return new RegExp(this._escapeRegex(patternStr), flags);
2886
+ }
2887
+ try {
2888
+ return new RegExp(patternStr, flags);
2889
+ } catch {
2890
+ return new RegExp(this._escapeRegex(patternStr), flags);
2891
+ }
2892
+ }
2893
+ /**
2894
+ * Escape special regex characters for literal matching
2895
+ */
2896
+ _escapeRegex(str) {
2897
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2898
+ }
2899
+ /**
2900
+ * Sanitize text search query for MongoDB $text search
2901
+ * @param search - Raw search input from user
2902
+ * @returns Sanitized search string or undefined
2903
+ */
2904
+ _sanitizeSearch(search) {
2905
+ if (search === null || search === void 0 || search === "") {
2906
+ return void 0;
2907
+ }
2908
+ let searchStr = String(search).trim();
2909
+ if (!searchStr) {
2910
+ return void 0;
2911
+ }
2912
+ if (searchStr.length > this.options.maxSearchLength) {
2913
+ console.warn(`[mongokit] Search query too long (${searchStr.length} > ${this.options.maxSearchLength}), truncating`);
2914
+ searchStr = searchStr.substring(0, this.options.maxSearchLength);
2915
+ }
2916
+ return searchStr;
2917
+ }
2918
+ /**
2919
+ * Convert values based on operator type
2920
+ */
2921
+ _convertValue(value) {
2922
+ if (value === null || value === void 0) return value;
2923
+ if (Array.isArray(value)) return value.map((v) => this._convertValue(v));
2924
+ if (typeof value === "object") return value;
2925
+ const stringValue = String(value);
2926
+ if (stringValue === "true") return true;
2927
+ if (stringValue === "false") return false;
2928
+ if (mongoose4.Types.ObjectId.isValid(stringValue) && stringValue.length === 24) {
2929
+ return stringValue;
2930
+ }
2931
+ return stringValue;
2932
+ }
2933
+ /**
2934
+ * Parse $or conditions
2935
+ */
2936
+ _parseOr(query) {
2937
+ const orArray = [];
2938
+ const raw = query?.or || query?.OR || query?.$or;
2939
+ if (!raw) return void 0;
2940
+ const items = Array.isArray(raw) ? raw : typeof raw === "object" ? Object.values(raw) : [];
2941
+ for (const item of items) {
2942
+ if (typeof item === "object" && item) {
2943
+ orArray.push(this._parseFilters(item));
2944
+ }
2945
+ }
2946
+ return orArray.length ? orArray : void 0;
2947
+ }
2948
+ /**
2949
+ * Enhance filters with between operator
2950
+ */
2951
+ _enhanceWithBetween(filters) {
2952
+ const output = { ...filters };
2953
+ for (const [key, value] of Object.entries(filters || {})) {
2954
+ if (value && typeof value === "object" && "between" in value) {
2955
+ const between = value.between;
2956
+ const [from, to] = String(between).split(",").map((s) => s.trim());
2957
+ const fromDate = from ? new Date(from) : void 0;
2958
+ const toDate = to ? new Date(to) : void 0;
2959
+ const range = {};
2960
+ if (fromDate && !isNaN(fromDate.getTime())) range.$gte = fromDate;
2961
+ if (toDate && !isNaN(toDate.getTime())) range.$lte = toDate;
2962
+ output[key] = range;
2963
+ }
2964
+ }
2965
+ return output;
2966
+ }
2967
+ };
2968
+ var defaultQueryParser = new QueryParser();
2969
+ var queryParser_default = defaultQueryParser;
2281
2970
 
2282
2971
  // src/actions/index.ts
2283
2972
  var actions_exports = {};
@@ -2333,4 +3022,4 @@ var index_default = Repository;
2333
3022
  * ```
2334
3023
  */
2335
3024
 
2336
- export { PaginationEngine, Repository, actions_exports as actions, aggregateHelpersPlugin, auditLogPlugin, autoInject, batchOperationsPlugin, blockIf, cachePlugin, cascadePlugin, createError, createFieldPreset, createMemoryCache, createRepository, index_default as default, fieldFilterPlugin, filterResponseData, getFieldsForUser, getMongooseProjection, immutableField, methodRegistryPlugin, mongoOperationsPlugin, requireField, softDeletePlugin, subdocumentPlugin, timestampPlugin, uniqueField, validationChainPlugin };
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 };