@classytic/mongokit 3.0.3 → 3.0.4

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.
@@ -1,3 +1,3 @@
1
- export { a as aggregate, c as create, _ as deleteActions, r as read, u as update } from '../index-CMCrkd2v.js';
1
+ export { a as aggregate, c as create, _ as deleteActions, r as read, u as update } from '../index-wXrOSYWG.js';
2
2
  import 'mongoose';
3
- import '../types-B3dPUKjs.js';
3
+ import '../types-DAl69QgM.js';
@@ -1,5 +1,5 @@
1
1
  import { Model, ClientSession, PipelineStage } from 'mongoose';
2
- import { A as AnyDocument, C as CreateOptions, f as ObjectId, n as OperationOptions, S as SelectSpec, g as PopulateSpec, h as SortSpec, U as UpdateOptions, p as UpdateWithValidationResult, o as UpdateManyResult, D as DeleteResult, N as GroupResult, M as LookupOptions, Q as MinMaxResult } from './types-B3dPUKjs.js';
2
+ import { A as AnyDocument, C as CreateOptions, f as ObjectId, n as OperationOptions, S as SelectSpec, g as PopulateSpec, h as SortSpec, U as UpdateOptions, p as UpdateWithValidationResult, o as UpdateManyResult, D as DeleteResult, W as GroupResult, T as LookupOptions, X as MinMaxResult } from './types-DAl69QgM.js';
3
3
 
4
4
  /**
5
5
  * Create Actions
package/dist/index.d.ts CHANGED
@@ -1,11 +1,11 @@
1
- import { A as AnyDocument, e as PluginType, P as PaginationConfig, R as RepositoryOptions, f as ObjectId, S as SelectSpec, g as PopulateSpec, h as SortSpec, a as OffsetPaginationResult, b as KeysetPaginationResult, U as UpdateOptions, d as AggregatePaginationResult, i as RepositoryContext, H as HttpError } from './types-B3dPUKjs.js';
2
- export { c as AggregatePaginationOptions, j as AnyModel, T as CacheAdapter, X as CacheOperationOptions, W as CacheOptions, Y as CacheStats, _ as CascadeOptions, Z as CascadeRelation, C as CreateOptions, y as CrudSchemas, z as DecodedCursor, D as DeleteResult, E as EventPayload, F as FieldPreset, w as FieldRules, N as GroupResult, l as HookMode, J as JsonSchema, K as KeysetPaginationOptions, L as Logger, M as LookupOptions, Q as MinMaxResult, O as OffsetPaginationOptions, n as OperationOptions, m as PaginationResult, v as ParsedQuery, r as Plugin, s as PluginFunction, u as RepositoryEvent, t as RepositoryInstance, x as SchemaBuilderOptions, I as SoftDeleteOptions, k as SortDirection, o as UpdateManyResult, p as UpdateWithValidationResult, q as UserContext, G as ValidationChainOptions, V as ValidationResult, B as ValidatorDefinition } from './types-B3dPUKjs.js';
1
+ import { A as AnyDocument, e as PluginType, P as PaginationConfig, R as RepositoryOptions, f as ObjectId, S as SelectSpec, g as PopulateSpec, h as SortSpec, a as OffsetPaginationResult, b as KeysetPaginationResult, U as UpdateOptions, d as AggregatePaginationResult, i as RepositoryContext, H as HttpError } from './types-DAl69QgM.js';
2
+ export { c as AggregatePaginationOptions, j as AnyModel, Y as CacheAdapter, _ as CacheOperationOptions, Z as CacheOptions, $ as CacheStats, a1 as CascadeOptions, a0 as CascadeRelation, C as CreateOptions, z as CrudSchemas, B as DecodedCursor, D as DeleteResult, E as EventPayload, F as FieldPreset, x as FieldRules, w as FilterQuery, W as GroupResult, l as HookMode, J as JsonSchema, K as KeysetPaginationOptions, L as Logger, T as LookupOptions, X as MinMaxResult, O as OffsetPaginationOptions, n as OperationOptions, m as PaginationResult, v as ParsedQuery, r as Plugin, s as PluginFunction, u as RepositoryEvent, t as RepositoryInstance, y as SchemaBuilderOptions, N as SoftDeleteFilterMode, M as SoftDeleteOptions, Q as SoftDeleteRepository, k as SortDirection, o as UpdateManyResult, p as UpdateWithValidationResult, q as UserContext, I as ValidationChainOptions, V as ValidationResult, G as ValidatorDefinition } from './types-DAl69QgM.js';
3
3
  import * as mongoose from 'mongoose';
4
4
  import { Model, ClientSession, PipelineStage, PopulateOptions } from 'mongoose';
5
5
  import { PaginationEngine } from './pagination/PaginationEngine.js';
6
6
  export { aggregateHelpersPlugin, auditLogPlugin, autoInject, batchOperationsPlugin, blockIf, cachePlugin, cascadePlugin, fieldFilterPlugin, immutableField, methodRegistryPlugin, mongoOperationsPlugin, requireField, softDeletePlugin, subdocumentPlugin, timestampPlugin, uniqueField, validationChainPlugin } from './plugins/index.js';
7
- export { b as createError, c as createFieldPreset, d as createMemoryCache, f as filterResponseData, g as getFieldsForUser, a as getMongooseProjection } from './memory-cache-Bn_-Kk-0.js';
8
- export { i as actions } from './index-CMCrkd2v.js';
7
+ export { F as FilterValue, O as OperatorMap, Q as QueryParser, h as QueryParserOptions, b as createError, c as createFieldPreset, d as createMemoryCache, f as filterResponseData, g as getFieldsForUser, a as getMongooseProjection, e as queryParser } from './queryParser-Bek4yy3x.js';
8
+ export { i as actions } from './index-wXrOSYWG.js';
9
9
 
10
10
  /**
11
11
  * Repository Pattern - Data Access Layer
package/dist/index.js CHANGED
@@ -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;
@@ -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
+ }
1366
1404
  }
1367
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
+ }
1415
+ }
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
  }
@@ -2278,6 +2414,321 @@ function createMemoryCache(maxEntries = 1e3) {
2278
2414
  }
2279
2415
  };
2280
2416
  }
2417
+ var QueryParser = class {
2418
+ options;
2419
+ operators = {
2420
+ eq: "$eq",
2421
+ ne: "$ne",
2422
+ gt: "$gt",
2423
+ gte: "$gte",
2424
+ lt: "$lt",
2425
+ lte: "$lte",
2426
+ in: "$in",
2427
+ nin: "$nin",
2428
+ like: "$regex",
2429
+ contains: "$regex",
2430
+ regex: "$regex",
2431
+ exists: "$exists",
2432
+ size: "$size",
2433
+ type: "$type"
2434
+ };
2435
+ /**
2436
+ * Dangerous MongoDB operators that should never be accepted from user input
2437
+ * Security: Prevent NoSQL injection attacks
2438
+ */
2439
+ dangerousOperators;
2440
+ /**
2441
+ * Regex pattern characters that can cause catastrophic backtracking (ReDoS)
2442
+ */
2443
+ dangerousRegexPatterns = /(\{[0-9,]+\}|\*\+|\+\+|\?\+|(\([^)]*\))\1|\(\?[^)]*\)|[\[\]].*[\[\]])/;
2444
+ constructor(options = {}) {
2445
+ this.options = {
2446
+ maxRegexLength: options.maxRegexLength ?? 500,
2447
+ maxSearchLength: options.maxSearchLength ?? 200,
2448
+ maxFilterDepth: options.maxFilterDepth ?? 10,
2449
+ additionalDangerousOperators: options.additionalDangerousOperators ?? []
2450
+ };
2451
+ this.dangerousOperators = [
2452
+ "$where",
2453
+ "$function",
2454
+ "$accumulator",
2455
+ "$expr",
2456
+ ...this.options.additionalDangerousOperators
2457
+ ];
2458
+ }
2459
+ /**
2460
+ * Parse query parameters into MongoDB query format
2461
+ */
2462
+ parseQuery(query) {
2463
+ const {
2464
+ page,
2465
+ limit = 20,
2466
+ sort = "-createdAt",
2467
+ populate,
2468
+ search,
2469
+ after,
2470
+ cursor,
2471
+ ...filters
2472
+ } = query || {};
2473
+ const parsed = {
2474
+ filters: this._parseFilters(filters),
2475
+ limit: parseInt(String(limit), 10),
2476
+ sort: this._parseSort(sort),
2477
+ populate,
2478
+ search: this._sanitizeSearch(search)
2479
+ };
2480
+ if (after || cursor) {
2481
+ parsed.after = after || cursor;
2482
+ } else if (page !== void 0) {
2483
+ parsed.page = parseInt(String(page), 10);
2484
+ } else {
2485
+ parsed.page = 1;
2486
+ }
2487
+ const orGroup = this._parseOr(query);
2488
+ if (orGroup) {
2489
+ parsed.filters = { ...parsed.filters, $or: orGroup };
2490
+ }
2491
+ parsed.filters = this._enhanceWithBetween(parsed.filters);
2492
+ return parsed;
2493
+ }
2494
+ /**
2495
+ * Parse sort parameter
2496
+ * Converts string like '-createdAt' to { createdAt: -1 }
2497
+ * Handles multiple sorts: '-createdAt,name' → { createdAt: -1, name: 1 }
2498
+ */
2499
+ _parseSort(sort) {
2500
+ if (!sort) return void 0;
2501
+ if (typeof sort === "object") return sort;
2502
+ const sortObj = {};
2503
+ const fields = sort.split(",").map((s) => s.trim());
2504
+ for (const field of fields) {
2505
+ if (field.startsWith("-")) {
2506
+ sortObj[field.substring(1)] = -1;
2507
+ } else {
2508
+ sortObj[field] = 1;
2509
+ }
2510
+ }
2511
+ return sortObj;
2512
+ }
2513
+ /**
2514
+ * Parse standard filter parameter (filter[field]=value)
2515
+ */
2516
+ _parseFilters(filters) {
2517
+ const parsedFilters = {};
2518
+ const regexFields = {};
2519
+ for (const [key, value] of Object.entries(filters)) {
2520
+ if (this.dangerousOperators.includes(key) || key.startsWith("$") && !["$or", "$and"].includes(key)) {
2521
+ console.warn(`[mongokit] Blocked dangerous operator: ${key}`);
2522
+ continue;
2523
+ }
2524
+ if (["page", "limit", "sort", "populate", "search", "select", "lean", "includeDeleted"].includes(key)) {
2525
+ continue;
2526
+ }
2527
+ const operatorMatch = key.match(/^(.+)\[(.+)\]$/);
2528
+ if (operatorMatch) {
2529
+ const [, , operator] = operatorMatch;
2530
+ if (this.dangerousOperators.includes("$" + operator)) {
2531
+ console.warn(`[mongokit] Blocked dangerous operator: ${operator}`);
2532
+ continue;
2533
+ }
2534
+ this._handleOperatorSyntax(parsedFilters, regexFields, operatorMatch, value);
2535
+ continue;
2536
+ }
2537
+ if (typeof value === "object" && value !== null && !Array.isArray(value)) {
2538
+ this._handleBracketSyntax(key, value, parsedFilters);
2539
+ } else {
2540
+ parsedFilters[key] = this._convertValue(value);
2541
+ }
2542
+ }
2543
+ return parsedFilters;
2544
+ }
2545
+ /**
2546
+ * Handle operator syntax: field[operator]=value
2547
+ */
2548
+ _handleOperatorSyntax(filters, regexFields, operatorMatch, value) {
2549
+ const [, field, operator] = operatorMatch;
2550
+ if (operator.toLowerCase() === "options" && regexFields[field]) {
2551
+ const fieldValue = filters[field];
2552
+ if (typeof fieldValue === "object" && fieldValue !== null && "$regex" in fieldValue) {
2553
+ fieldValue.$options = value;
2554
+ }
2555
+ return;
2556
+ }
2557
+ if (operator.toLowerCase() === "contains" || operator.toLowerCase() === "like") {
2558
+ const safeRegex = this._createSafeRegex(value);
2559
+ if (safeRegex) {
2560
+ filters[field] = { $regex: safeRegex };
2561
+ regexFields[field] = true;
2562
+ }
2563
+ return;
2564
+ }
2565
+ const mongoOperator = this._toMongoOperator(operator);
2566
+ if (this.dangerousOperators.includes(mongoOperator)) {
2567
+ console.warn(`[mongokit] Blocked dangerous operator in field[${operator}]: ${mongoOperator}`);
2568
+ return;
2569
+ }
2570
+ if (mongoOperator === "$eq") {
2571
+ filters[field] = value;
2572
+ } else if (mongoOperator === "$regex") {
2573
+ filters[field] = { $regex: value };
2574
+ regexFields[field] = true;
2575
+ } else {
2576
+ if (typeof filters[field] !== "object" || filters[field] === null || Array.isArray(filters[field])) {
2577
+ filters[field] = {};
2578
+ }
2579
+ let processedValue;
2580
+ const op = operator.toLowerCase();
2581
+ if (["gt", "gte", "lt", "lte", "size"].includes(op)) {
2582
+ processedValue = parseFloat(String(value));
2583
+ if (isNaN(processedValue)) return;
2584
+ } else if (op === "in" || op === "nin") {
2585
+ processedValue = Array.isArray(value) ? value : String(value).split(",").map((v) => v.trim());
2586
+ } else {
2587
+ processedValue = this._convertValue(value);
2588
+ }
2589
+ filters[field][mongoOperator] = processedValue;
2590
+ }
2591
+ }
2592
+ /**
2593
+ * Handle bracket syntax with object value
2594
+ */
2595
+ _handleBracketSyntax(field, operators, parsedFilters) {
2596
+ if (!parsedFilters[field]) {
2597
+ parsedFilters[field] = {};
2598
+ }
2599
+ for (const [operator, value] of Object.entries(operators)) {
2600
+ if (operator === "between") {
2601
+ parsedFilters[field].between = value;
2602
+ continue;
2603
+ }
2604
+ if (this.operators[operator]) {
2605
+ const mongoOperator = this.operators[operator];
2606
+ let processedValue;
2607
+ if (["gt", "gte", "lt", "lte", "size"].includes(operator)) {
2608
+ processedValue = parseFloat(String(value));
2609
+ if (isNaN(processedValue)) continue;
2610
+ } else if (operator === "in" || operator === "nin") {
2611
+ processedValue = Array.isArray(value) ? value : String(value).split(",").map((v) => v.trim());
2612
+ } else if (operator === "like" || operator === "contains") {
2613
+ const safeRegex = this._createSafeRegex(value);
2614
+ if (!safeRegex) continue;
2615
+ processedValue = safeRegex;
2616
+ } else {
2617
+ processedValue = this._convertValue(value);
2618
+ }
2619
+ parsedFilters[field][mongoOperator] = processedValue;
2620
+ }
2621
+ }
2622
+ }
2623
+ /**
2624
+ * Convert operator to MongoDB format
2625
+ */
2626
+ _toMongoOperator(operator) {
2627
+ const op = operator.toLowerCase();
2628
+ return op.startsWith("$") ? op : "$" + op;
2629
+ }
2630
+ /**
2631
+ * Create a safe regex pattern with protection against ReDoS attacks
2632
+ * @param pattern - The pattern string from user input
2633
+ * @param flags - Regex flags (default: 'i' for case-insensitive)
2634
+ * @returns A safe RegExp or null if pattern is invalid/dangerous
2635
+ */
2636
+ _createSafeRegex(pattern, flags = "i") {
2637
+ if (pattern === null || pattern === void 0) {
2638
+ return null;
2639
+ }
2640
+ const patternStr = String(pattern);
2641
+ if (patternStr.length > this.options.maxRegexLength) {
2642
+ console.warn(`[mongokit] Regex pattern too long (${patternStr.length} > ${this.options.maxRegexLength}), truncating`);
2643
+ return new RegExp(this._escapeRegex(patternStr.substring(0, this.options.maxRegexLength)), flags);
2644
+ }
2645
+ if (this.dangerousRegexPatterns.test(patternStr)) {
2646
+ console.warn("[mongokit] Potentially dangerous regex pattern detected, escaping");
2647
+ return new RegExp(this._escapeRegex(patternStr), flags);
2648
+ }
2649
+ try {
2650
+ return new RegExp(patternStr, flags);
2651
+ } catch {
2652
+ return new RegExp(this._escapeRegex(patternStr), flags);
2653
+ }
2654
+ }
2655
+ /**
2656
+ * Escape special regex characters for literal matching
2657
+ */
2658
+ _escapeRegex(str) {
2659
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2660
+ }
2661
+ /**
2662
+ * Sanitize text search query for MongoDB $text search
2663
+ * @param search - Raw search input from user
2664
+ * @returns Sanitized search string or undefined
2665
+ */
2666
+ _sanitizeSearch(search) {
2667
+ if (search === null || search === void 0 || search === "") {
2668
+ return void 0;
2669
+ }
2670
+ let searchStr = String(search).trim();
2671
+ if (!searchStr) {
2672
+ return void 0;
2673
+ }
2674
+ if (searchStr.length > this.options.maxSearchLength) {
2675
+ console.warn(`[mongokit] Search query too long (${searchStr.length} > ${this.options.maxSearchLength}), truncating`);
2676
+ searchStr = searchStr.substring(0, this.options.maxSearchLength);
2677
+ }
2678
+ return searchStr;
2679
+ }
2680
+ /**
2681
+ * Convert values based on operator type
2682
+ */
2683
+ _convertValue(value) {
2684
+ if (value === null || value === void 0) return value;
2685
+ if (Array.isArray(value)) return value.map((v) => this._convertValue(v));
2686
+ if (typeof value === "object") return value;
2687
+ const stringValue = String(value);
2688
+ if (stringValue === "true") return true;
2689
+ if (stringValue === "false") return false;
2690
+ if (mongoose.Types.ObjectId.isValid(stringValue) && stringValue.length === 24) {
2691
+ return stringValue;
2692
+ }
2693
+ return stringValue;
2694
+ }
2695
+ /**
2696
+ * Parse $or conditions
2697
+ */
2698
+ _parseOr(query) {
2699
+ const orArray = [];
2700
+ const raw = query?.or || query?.OR || query?.$or;
2701
+ if (!raw) return void 0;
2702
+ const items = Array.isArray(raw) ? raw : typeof raw === "object" ? Object.values(raw) : [];
2703
+ for (const item of items) {
2704
+ if (typeof item === "object" && item) {
2705
+ orArray.push(this._parseFilters(item));
2706
+ }
2707
+ }
2708
+ return orArray.length ? orArray : void 0;
2709
+ }
2710
+ /**
2711
+ * Enhance filters with between operator
2712
+ */
2713
+ _enhanceWithBetween(filters) {
2714
+ const output = { ...filters };
2715
+ for (const [key, value] of Object.entries(filters || {})) {
2716
+ if (value && typeof value === "object" && "between" in value) {
2717
+ const between = value.between;
2718
+ const [from, to] = String(between).split(",").map((s) => s.trim());
2719
+ const fromDate = from ? new Date(from) : void 0;
2720
+ const toDate = to ? new Date(to) : void 0;
2721
+ const range = {};
2722
+ if (fromDate && !isNaN(fromDate.getTime())) range.$gte = fromDate;
2723
+ if (toDate && !isNaN(toDate.getTime())) range.$lte = toDate;
2724
+ output[key] = range;
2725
+ }
2726
+ }
2727
+ return output;
2728
+ }
2729
+ };
2730
+ var defaultQueryParser = new QueryParser();
2731
+ var queryParser_default = defaultQueryParser;
2281
2732
 
2282
2733
  // src/actions/index.ts
2283
2734
  var actions_exports = {};
@@ -2333,4 +2784,4 @@ var index_default = Repository;
2333
2784
  * ```
2334
2785
  */
2335
2786
 
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 };
2787
+ export { PaginationEngine, QueryParser, 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, queryParser_default as queryParser, requireField, softDeletePlugin, subdocumentPlugin, timestampPlugin, uniqueField, validationChainPlugin };
@@ -1,5 +1,5 @@
1
1
  import { Model } from 'mongoose';
2
- import { A as AnyDocument, P as PaginationConfig, O as OffsetPaginationOptions, a as OffsetPaginationResult, K as KeysetPaginationOptions, b as KeysetPaginationResult, c as AggregatePaginationOptions, d as AggregatePaginationResult } from '../types-B3dPUKjs.js';
2
+ import { A as AnyDocument, P as PaginationConfig, O as OffsetPaginationOptions, a as OffsetPaginationResult, K as KeysetPaginationOptions, b as KeysetPaginationResult, c as AggregatePaginationOptions, d as AggregatePaginationResult } from '../types-DAl69QgM.js';
3
3
 
4
4
  /**
5
5
  * Pagination Engine
@@ -1,4 +1,4 @@
1
- import { F as FieldPreset, r as Plugin, L as Logger, I as SoftDeleteOptions, t as RepositoryInstance, B as ValidatorDefinition, G as ValidationChainOptions, i as RepositoryContext, W as CacheOptions, _ as CascadeOptions } from '../types-B3dPUKjs.js';
1
+ import { F as FieldPreset, r as Plugin, L as Logger, M as SoftDeleteOptions, t as RepositoryInstance, G as ValidatorDefinition, I as ValidationChainOptions, i as RepositoryContext, Z as CacheOptions, a1 as CascadeOptions } from '../types-DAl69QgM.js';
2
2
  import 'mongoose';
3
3
 
4
4
  /**
@@ -54,10 +54,42 @@ declare function auditLogPlugin(logger: Logger): Plugin;
54
54
  /**
55
55
  * Soft delete plugin
56
56
  *
57
- * @example
57
+ * @example Basic usage
58
+ * ```typescript
58
59
  * const repo = new Repository(Model, [
59
60
  * softDeletePlugin({ deletedField: 'deletedAt' })
60
61
  * ]);
62
+ *
63
+ * // Delete (soft)
64
+ * await repo.delete(id);
65
+ *
66
+ * // Restore
67
+ * await repo.restore(id);
68
+ *
69
+ * // Get deleted documents
70
+ * await repo.getDeleted({ page: 1, limit: 20 });
71
+ * ```
72
+ *
73
+ * @example With null filter mode (for schemas with default: null)
74
+ * ```typescript
75
+ * // Schema: { deletedAt: { type: Date, default: null } }
76
+ * const repo = new Repository(Model, [
77
+ * softDeletePlugin({
78
+ * deletedField: 'deletedAt',
79
+ * filterMode: 'null', // default - works with default: null
80
+ * })
81
+ * ]);
82
+ * ```
83
+ *
84
+ * @example With TTL for auto-cleanup
85
+ * ```typescript
86
+ * const repo = new Repository(Model, [
87
+ * softDeletePlugin({
88
+ * deletedField: 'deletedAt',
89
+ * ttlDays: 30, // Auto-delete after 30 days
90
+ * })
91
+ * ]);
92
+ * ```
61
93
  */
62
94
  declare function softDeletePlugin(options?: SoftDeleteOptions): Plugin;
63
95
 
@@ -115,12 +115,45 @@ function auditLogPlugin(logger) {
115
115
  }
116
116
 
117
117
  // src/plugins/soft-delete.plugin.ts
118
+ function buildDeletedFilter(deletedField, filterMode, includeDeleted) {
119
+ if (includeDeleted) {
120
+ return {};
121
+ }
122
+ if (filterMode === "exists") {
123
+ return { [deletedField]: { $exists: false } };
124
+ }
125
+ return { [deletedField]: null };
126
+ }
127
+ function buildGetDeletedFilter(deletedField, filterMode) {
128
+ if (filterMode === "exists") {
129
+ return { [deletedField]: { $exists: true, $ne: null } };
130
+ }
131
+ return { [deletedField]: { $ne: null } };
132
+ }
118
133
  function softDeletePlugin(options = {}) {
119
134
  const deletedField = options.deletedField || "deletedAt";
120
135
  const deletedByField = options.deletedByField || "deletedBy";
136
+ const filterMode = options.filterMode || "null";
137
+ const addRestoreMethod = options.addRestoreMethod !== false;
138
+ const addGetDeletedMethod = options.addGetDeletedMethod !== false;
139
+ const ttlDays = options.ttlDays;
121
140
  return {
122
141
  name: "softDelete",
123
142
  apply(repo) {
143
+ if (ttlDays !== void 0 && ttlDays > 0) {
144
+ const ttlSeconds = ttlDays * 24 * 60 * 60;
145
+ repo.Model.collection.createIndex(
146
+ { [deletedField]: 1 },
147
+ {
148
+ expireAfterSeconds: ttlSeconds,
149
+ partialFilterExpression: { [deletedField]: { $type: "date" } }
150
+ }
151
+ ).catch((err) => {
152
+ if (!err.message.includes("already exists")) {
153
+ console.warn(`[softDeletePlugin] Failed to create TTL index: ${err.message}`);
154
+ }
155
+ });
156
+ }
124
157
  repo.on("before:delete", async (context) => {
125
158
  if (options.soft !== false) {
126
159
  const updateData = {
@@ -134,23 +167,126 @@ function softDeletePlugin(options = {}) {
134
167
  }
135
168
  });
136
169
  repo.on("before:getAll", (context) => {
137
- if (!context.includeDeleted && options.soft !== false) {
138
- const queryParams = context.queryParams || {};
139
- queryParams.filters = {
140
- ...queryParams.filters || {},
141
- [deletedField]: { $exists: false }
142
- };
143
- context.queryParams = queryParams;
170
+ if (options.soft !== false) {
171
+ const deleteFilter = buildDeletedFilter(deletedField, filterMode, !!context.includeDeleted);
172
+ if (Object.keys(deleteFilter).length > 0) {
173
+ const existingFilters = context.filters || {};
174
+ context.filters = {
175
+ ...existingFilters,
176
+ ...deleteFilter
177
+ };
178
+ }
144
179
  }
145
180
  });
146
181
  repo.on("before:getById", (context) => {
147
- if (!context.includeDeleted && options.soft !== false) {
148
- context.query = {
149
- ...context.query || {},
150
- [deletedField]: { $exists: false }
151
- };
182
+ if (options.soft !== false) {
183
+ const deleteFilter = buildDeletedFilter(deletedField, filterMode, !!context.includeDeleted);
184
+ if (Object.keys(deleteFilter).length > 0) {
185
+ context.query = {
186
+ ...context.query || {},
187
+ ...deleteFilter
188
+ };
189
+ }
152
190
  }
153
191
  });
192
+ repo.on("before:getByQuery", (context) => {
193
+ if (options.soft !== false) {
194
+ const deleteFilter = buildDeletedFilter(deletedField, filterMode, !!context.includeDeleted);
195
+ if (Object.keys(deleteFilter).length > 0) {
196
+ context.query = {
197
+ ...context.query || {},
198
+ ...deleteFilter
199
+ };
200
+ }
201
+ }
202
+ });
203
+ if (addRestoreMethod) {
204
+ const restoreMethod = async function(id, restoreOptions = {}) {
205
+ const updateData = {
206
+ [deletedField]: null,
207
+ [deletedByField]: null
208
+ };
209
+ const result = await this.Model.findByIdAndUpdate(id, { $set: updateData }, {
210
+ new: true,
211
+ session: restoreOptions.session
212
+ });
213
+ if (!result) {
214
+ const error = new Error(`Document with id '${id}' not found`);
215
+ error.status = 404;
216
+ throw error;
217
+ }
218
+ await this.emitAsync("after:restore", { id, result });
219
+ return result;
220
+ };
221
+ if (typeof repo.registerMethod === "function") {
222
+ repo.registerMethod("restore", restoreMethod);
223
+ } else {
224
+ repo.restore = restoreMethod.bind(repo);
225
+ }
226
+ }
227
+ if (addGetDeletedMethod) {
228
+ const getDeletedMethod = async function(params = {}, getDeletedOptions = {}) {
229
+ const deletedFilter = buildGetDeletedFilter(deletedField, filterMode);
230
+ const combinedFilters = {
231
+ ...params.filters || {},
232
+ ...deletedFilter
233
+ };
234
+ const page = params.page || 1;
235
+ const limit = params.limit || 20;
236
+ const skip = (page - 1) * limit;
237
+ let sortSpec = { [deletedField]: -1 };
238
+ if (params.sort) {
239
+ if (typeof params.sort === "string") {
240
+ const sortOrder = params.sort.startsWith("-") ? -1 : 1;
241
+ const sortField = params.sort.startsWith("-") ? params.sort.substring(1) : params.sort;
242
+ sortSpec = { [sortField]: sortOrder };
243
+ } else {
244
+ sortSpec = params.sort;
245
+ }
246
+ }
247
+ let query = this.Model.find(combinedFilters).sort(sortSpec).skip(skip).limit(limit);
248
+ if (getDeletedOptions.session) {
249
+ query = query.session(getDeletedOptions.session);
250
+ }
251
+ if (getDeletedOptions.select) {
252
+ const selectValue = Array.isArray(getDeletedOptions.select) ? getDeletedOptions.select.join(" ") : getDeletedOptions.select;
253
+ query = query.select(selectValue);
254
+ }
255
+ if (getDeletedOptions.populate) {
256
+ const populateSpec = getDeletedOptions.populate;
257
+ if (typeof populateSpec === "string") {
258
+ query = query.populate(populateSpec.split(",").map((p) => p.trim()));
259
+ } else if (Array.isArray(populateSpec)) {
260
+ query = query.populate(populateSpec);
261
+ } else {
262
+ query = query.populate(populateSpec);
263
+ }
264
+ }
265
+ if (getDeletedOptions.lean !== false) {
266
+ query = query.lean();
267
+ }
268
+ const [docs, total] = await Promise.all([
269
+ query.exec(),
270
+ this.Model.countDocuments(combinedFilters)
271
+ ]);
272
+ const pages = Math.ceil(total / limit);
273
+ return {
274
+ method: "offset",
275
+ docs,
276
+ page,
277
+ limit,
278
+ total,
279
+ pages,
280
+ hasNext: page < pages,
281
+ hasPrev: page > 1
282
+ };
283
+ };
284
+ if (typeof repo.registerMethod === "function") {
285
+ repo.registerMethod("getDeleted", getDeletedMethod);
286
+ } else {
287
+ repo.getDeleted = getDeletedMethod.bind(repo);
288
+ }
289
+ }
154
290
  }
155
291
  };
156
292
  }
@@ -1,4 +1,4 @@
1
- import { q as UserContext, F as FieldPreset, H as HttpError, T as CacheAdapter } from './types-B3dPUKjs.js';
1
+ import { q as UserContext, F as FieldPreset, H as HttpError, Y as CacheAdapter, v as ParsedQuery } from './types-DAl69QgM.js';
2
2
 
3
3
  /**
4
4
  * Field Selection Utilities
@@ -139,4 +139,112 @@ declare function createError(status: number, message: string): HttpError;
139
139
  */
140
140
  declare function createMemoryCache(maxEntries?: number): CacheAdapter;
141
141
 
142
- export { getMongooseProjection as a, createError as b, createFieldPreset as c, createMemoryCache as d, filterResponseData as f, getFieldsForUser as g };
142
+ /**
143
+ * Query Parser
144
+ *
145
+ * Parses HTTP query parameters into MongoDB-compatible query objects.
146
+ * Supports operators, pagination, sorting, and filtering.
147
+ */
148
+
149
+ /** Operator mapping from query syntax to MongoDB operators */
150
+ type OperatorMap = Record<string, string>;
151
+ /** Possible values in filter parameters */
152
+ type FilterValue = string | number | boolean | null | undefined | Record<string, unknown> | unknown[];
153
+ /** Configuration options for QueryParser */
154
+ interface QueryParserOptions {
155
+ /** Maximum allowed regex pattern length (default: 500) */
156
+ maxRegexLength?: number;
157
+ /** Maximum allowed text search query length (default: 200) */
158
+ maxSearchLength?: number;
159
+ /** Maximum allowed filter depth (default: 10) */
160
+ maxFilterDepth?: number;
161
+ /** Additional operators to block */
162
+ additionalDangerousOperators?: string[];
163
+ }
164
+ /**
165
+ * Query Parser Class
166
+ *
167
+ * Parses HTTP query parameters into MongoDB-compatible query objects.
168
+ * Includes security measures against NoSQL injection and ReDoS attacks.
169
+ *
170
+ * @example
171
+ * ```typescript
172
+ * import { QueryParser } from '@classytic/mongokit';
173
+ *
174
+ * const parser = new QueryParser({ maxRegexLength: 100 });
175
+ * const query = parser.parseQuery(req.query);
176
+ * ```
177
+ */
178
+ declare class QueryParser {
179
+ private readonly options;
180
+ private readonly operators;
181
+ /**
182
+ * Dangerous MongoDB operators that should never be accepted from user input
183
+ * Security: Prevent NoSQL injection attacks
184
+ */
185
+ private readonly dangerousOperators;
186
+ /**
187
+ * Regex pattern characters that can cause catastrophic backtracking (ReDoS)
188
+ */
189
+ private readonly dangerousRegexPatterns;
190
+ constructor(options?: QueryParserOptions);
191
+ /**
192
+ * Parse query parameters into MongoDB query format
193
+ */
194
+ parseQuery(query: Record<string, unknown> | null | undefined): ParsedQuery;
195
+ /**
196
+ * Parse sort parameter
197
+ * Converts string like '-createdAt' to { createdAt: -1 }
198
+ * Handles multiple sorts: '-createdAt,name' → { createdAt: -1, name: 1 }
199
+ */
200
+ private _parseSort;
201
+ /**
202
+ * Parse standard filter parameter (filter[field]=value)
203
+ */
204
+ private _parseFilters;
205
+ /**
206
+ * Handle operator syntax: field[operator]=value
207
+ */
208
+ private _handleOperatorSyntax;
209
+ /**
210
+ * Handle bracket syntax with object value
211
+ */
212
+ private _handleBracketSyntax;
213
+ /**
214
+ * Convert operator to MongoDB format
215
+ */
216
+ private _toMongoOperator;
217
+ /**
218
+ * Create a safe regex pattern with protection against ReDoS attacks
219
+ * @param pattern - The pattern string from user input
220
+ * @param flags - Regex flags (default: 'i' for case-insensitive)
221
+ * @returns A safe RegExp or null if pattern is invalid/dangerous
222
+ */
223
+ private _createSafeRegex;
224
+ /**
225
+ * Escape special regex characters for literal matching
226
+ */
227
+ private _escapeRegex;
228
+ /**
229
+ * Sanitize text search query for MongoDB $text search
230
+ * @param search - Raw search input from user
231
+ * @returns Sanitized search string or undefined
232
+ */
233
+ private _sanitizeSearch;
234
+ /**
235
+ * Convert values based on operator type
236
+ */
237
+ private _convertValue;
238
+ /**
239
+ * Parse $or conditions
240
+ */
241
+ private _parseOr;
242
+ /**
243
+ * Enhance filters with between operator
244
+ */
245
+ private _enhanceWithBetween;
246
+ }
247
+ /** Default query parser instance with standard options */
248
+ declare const defaultQueryParser: QueryParser;
249
+
250
+ export { type FilterValue as F, type OperatorMap as O, QueryParser as Q, getMongooseProjection as a, createError as b, createFieldPreset as c, createMemoryCache as d, defaultQueryParser as e, filterResponseData as f, getFieldsForUser as g, type QueryParserOptions as h };
@@ -405,6 +405,8 @@ interface Logger {
405
405
  warn?(message: string, meta?: Record<string, unknown>): void;
406
406
  debug?(message: string, meta?: Record<string, unknown>): void;
407
407
  }
408
+ /** Filter mode for soft delete queries */
409
+ type SoftDeleteFilterMode = 'null' | 'exists';
408
410
  /** Soft delete plugin options */
409
411
  interface SoftDeleteOptions {
410
412
  /** Field name for deletion timestamp (default: 'deletedAt') */
@@ -413,6 +415,51 @@ interface SoftDeleteOptions {
413
415
  deletedByField?: string;
414
416
  /** Enable soft delete (default: true) */
415
417
  soft?: boolean;
418
+ /**
419
+ * Filter mode for excluding deleted documents (default: 'null')
420
+ * - 'null': Filters where deletedField is null (works with `default: null` in schema)
421
+ * - 'exists': Filters where deletedField does not exist (legacy behavior)
422
+ */
423
+ filterMode?: SoftDeleteFilterMode;
424
+ /** Add restore method to repository (default: true) */
425
+ addRestoreMethod?: boolean;
426
+ /** Add getDeleted method to repository (default: true) */
427
+ addGetDeletedMethod?: boolean;
428
+ /**
429
+ * TTL in days for auto-cleanup of deleted documents.
430
+ * When set, creates a TTL index on the deletedField.
431
+ * Documents will be automatically removed after the specified days.
432
+ */
433
+ ttlDays?: number;
434
+ }
435
+ /** Repository with soft delete methods */
436
+ interface SoftDeleteRepository {
437
+ /**
438
+ * Restore a soft-deleted document by setting deletedAt to null
439
+ * @param id - Document ID to restore
440
+ * @param options - Optional session for transactions
441
+ * @returns The restored document
442
+ */
443
+ restore(id: string | ObjectId, options?: {
444
+ session?: ClientSession;
445
+ }): Promise<unknown>;
446
+ /**
447
+ * Get all soft-deleted documents
448
+ * @param params - Query parameters (filters, pagination, etc.)
449
+ * @param options - Query options (select, populate, etc.)
450
+ * @returns Paginated result of deleted documents
451
+ */
452
+ getDeleted(params?: {
453
+ filters?: Record<string, unknown>;
454
+ sort?: SortSpec | string;
455
+ page?: number;
456
+ limit?: number;
457
+ }, options?: {
458
+ select?: SelectSpec;
459
+ populate?: PopulateSpec;
460
+ lean?: boolean;
461
+ session?: ClientSession;
462
+ }): Promise<OffsetPaginationResult<unknown>>;
416
463
  }
417
464
  /** Lookup options for aggregate */
418
465
  interface LookupOptions {
@@ -533,4 +580,4 @@ interface HttpError extends Error {
533
580
  }>;
534
581
  }
535
582
 
536
- export type { AnyDocument as A, ValidatorDefinition as B, CreateOptions as C, DeleteResult as D, EventPayload as E, FieldPreset as F, ValidationChainOptions as G, HttpError as H, SoftDeleteOptions as I, JsonSchema as J, KeysetPaginationOptions as K, Logger as L, LookupOptions as M, GroupResult as N, OffsetPaginationOptions as O, PaginationConfig as P, MinMaxResult as Q, RepositoryOptions as R, SelectSpec as S, CacheAdapter as T, UpdateOptions as U, ValidationResult as V, CacheOptions as W, CacheOperationOptions as X, CacheStats as Y, CascadeRelation as Z, CascadeOptions as _, OffsetPaginationResult as a, KeysetPaginationResult as b, AggregatePaginationOptions as c, AggregatePaginationResult as d, PluginType as e, ObjectId as f, PopulateSpec as g, SortSpec as h, RepositoryContext as i, AnyModel as j, SortDirection as k, HookMode as l, PaginationResult as m, OperationOptions as n, UpdateManyResult as o, UpdateWithValidationResult as p, UserContext as q, Plugin as r, PluginFunction as s, RepositoryInstance as t, RepositoryEvent as u, ParsedQuery as v, FieldRules as w, SchemaBuilderOptions as x, CrudSchemas as y, DecodedCursor as z };
583
+ export type { CacheStats as $, AnyDocument as A, DecodedCursor as B, CreateOptions as C, DeleteResult as D, EventPayload as E, FieldPreset as F, ValidatorDefinition as G, HttpError as H, ValidationChainOptions as I, JsonSchema as J, KeysetPaginationOptions as K, Logger as L, SoftDeleteOptions as M, SoftDeleteFilterMode as N, OffsetPaginationOptions as O, PaginationConfig as P, SoftDeleteRepository as Q, RepositoryOptions as R, SelectSpec as S, LookupOptions as T, UpdateOptions as U, ValidationResult as V, GroupResult as W, MinMaxResult as X, CacheAdapter as Y, CacheOptions as Z, CacheOperationOptions as _, OffsetPaginationResult as a, CascadeRelation as a0, CascadeOptions as a1, KeysetPaginationResult as b, AggregatePaginationOptions as c, AggregatePaginationResult as d, PluginType as e, ObjectId as f, PopulateSpec as g, SortSpec as h, RepositoryContext as i, AnyModel as j, SortDirection as k, HookMode as l, PaginationResult as m, OperationOptions as n, UpdateManyResult as o, UpdateWithValidationResult as p, UserContext as q, Plugin as r, PluginFunction as s, RepositoryInstance as t, RepositoryEvent as u, ParsedQuery as v, FilterQuery as w, FieldRules as x, SchemaBuilderOptions as y, CrudSchemas as z };
@@ -1,61 +1,6 @@
1
- export { b as createError, c as createFieldPreset, d as createMemoryCache, f as filterResponseData, g as getFieldsForUser, a as getMongooseProjection } from '../memory-cache-Bn_-Kk-0.js';
2
- import { v as ParsedQuery, x as SchemaBuilderOptions, y as CrudSchemas, V as ValidationResult, S as SelectSpec, g as PopulateSpec, h as SortSpec } from '../types-B3dPUKjs.js';
1
+ export { F as FilterValue, O as OperatorMap, Q as QueryParser, h as QueryParserOptions, b as createError, c as createFieldPreset, d as createMemoryCache, f as filterResponseData, g as getFieldsForUser, a as getMongooseProjection, e as queryParser } from '../queryParser-Bek4yy3x.js';
3
2
  import mongoose__default, { Schema } from 'mongoose';
4
-
5
- /**
6
- * Query Parser
7
- *
8
- * Parses HTTP query parameters into MongoDB-compatible query objects.
9
- * Supports operators, pagination, sorting, and filtering.
10
- */
11
-
12
- declare class QueryParser {
13
- private operators;
14
- /**
15
- * Dangerous MongoDB operators that should never be accepted from user input
16
- * Security: Prevent NoSQL injection attacks
17
- */
18
- private dangerousOperators;
19
- /**
20
- * Parse query parameters into MongoDB query format
21
- */
22
- parseQuery(query: Record<string, unknown> | null | undefined): ParsedQuery;
23
- /**
24
- * Parse sort parameter
25
- * Converts string like '-createdAt' to { createdAt: -1 }
26
- * Handles multiple sorts: '-createdAt,name' → { createdAt: -1, name: 1 }
27
- */
28
- private _parseSort;
29
- /**
30
- * Parse standard filter parameter (filter[field]=value)
31
- */
32
- private _parseFilters;
33
- /**
34
- * Handle operator syntax: field[operator]=value
35
- */
36
- private _handleOperatorSyntax;
37
- /**
38
- * Handle bracket syntax with object value
39
- */
40
- private _handleBracketSyntax;
41
- /**
42
- * Convert operator to MongoDB format
43
- */
44
- private _toMongoOperator;
45
- /**
46
- * Convert values based on operator type
47
- */
48
- private _convertValue;
49
- /**
50
- * Parse $or conditions
51
- */
52
- private _parseOr;
53
- /**
54
- * Enhance filters with between operator
55
- */
56
- private _enhanceWithBetween;
57
- }
58
- declare const _default: QueryParser;
3
+ import { y as SchemaBuilderOptions, z as CrudSchemas, V as ValidationResult, S as SelectSpec, g as PopulateSpec, h as SortSpec } from '../types-DAl69QgM.js';
59
4
 
60
5
  /**
61
6
  * Mongoose to JSON Schema Converter with Field Rules
@@ -186,4 +131,4 @@ declare function modelPattern(prefix: string, model: string): string;
186
131
  */
187
132
  declare function listPattern(prefix: string, model: string): string;
188
133
 
189
- export { buildCrudSchemasFromModel, buildCrudSchemasFromMongooseSchema, byIdKey, byQueryKey, getImmutableFields, getSystemManagedFields, isFieldUpdateAllowed, listPattern, listQueryKey, modelPattern, _default as queryParser, validateUpdateBody, versionKey };
134
+ export { buildCrudSchemasFromModel, buildCrudSchemasFromMongooseSchema, byIdKey, byQueryKey, getImmutableFields, getSystemManagedFields, isFieldUpdateAllowed, listPattern, listQueryKey, modelPattern, validateUpdateBody, versionKey };
@@ -46,6 +46,7 @@ function createFieldPreset(config) {
46
46
  };
47
47
  }
48
48
  var QueryParser = class {
49
+ options;
49
50
  operators = {
50
51
  eq: "$eq",
51
52
  ne: "$ne",
@@ -66,7 +67,26 @@ var QueryParser = class {
66
67
  * Dangerous MongoDB operators that should never be accepted from user input
67
68
  * Security: Prevent NoSQL injection attacks
68
69
  */
69
- dangerousOperators = ["$where", "$function", "$accumulator", "$expr"];
70
+ dangerousOperators;
71
+ /**
72
+ * Regex pattern characters that can cause catastrophic backtracking (ReDoS)
73
+ */
74
+ dangerousRegexPatterns = /(\{[0-9,]+\}|\*\+|\+\+|\?\+|(\([^)]*\))\1|\(\?[^)]*\)|[\[\]].*[\[\]])/;
75
+ constructor(options = {}) {
76
+ this.options = {
77
+ maxRegexLength: options.maxRegexLength ?? 500,
78
+ maxSearchLength: options.maxSearchLength ?? 200,
79
+ maxFilterDepth: options.maxFilterDepth ?? 10,
80
+ additionalDangerousOperators: options.additionalDangerousOperators ?? []
81
+ };
82
+ this.dangerousOperators = [
83
+ "$where",
84
+ "$function",
85
+ "$accumulator",
86
+ "$expr",
87
+ ...this.options.additionalDangerousOperators
88
+ ];
89
+ }
70
90
  /**
71
91
  * Parse query parameters into MongoDB query format
72
92
  */
@@ -86,7 +106,7 @@ var QueryParser = class {
86
106
  limit: parseInt(String(limit), 10),
87
107
  sort: this._parseSort(sort),
88
108
  populate,
89
- search
109
+ search: this._sanitizeSearch(search)
90
110
  };
91
111
  if (after || cursor) {
92
112
  parsed.after = after || cursor;
@@ -126,6 +146,7 @@ var QueryParser = class {
126
146
  */
127
147
  _parseFilters(filters) {
128
148
  const parsedFilters = {};
149
+ const regexFields = {};
129
150
  for (const [key, value] of Object.entries(filters)) {
130
151
  if (this.dangerousOperators.includes(key) || key.startsWith("$") && !["$or", "$and"].includes(key)) {
131
152
  console.warn(`[mongokit] Blocked dangerous operator: ${key}`);
@@ -141,7 +162,7 @@ var QueryParser = class {
141
162
  console.warn(`[mongokit] Blocked dangerous operator: ${operator}`);
142
163
  continue;
143
164
  }
144
- this._handleOperatorSyntax(parsedFilters, {}, operatorMatch, value);
165
+ this._handleOperatorSyntax(parsedFilters, regexFields, operatorMatch, value);
145
166
  continue;
146
167
  }
147
168
  if (typeof value === "object" && value !== null && !Array.isArray(value)) {
@@ -165,8 +186,11 @@ var QueryParser = class {
165
186
  return;
166
187
  }
167
188
  if (operator.toLowerCase() === "contains" || operator.toLowerCase() === "like") {
168
- filters[field] = { $regex: new RegExp(String(value), "i") };
169
- regexFields[field] = true;
189
+ const safeRegex = this._createSafeRegex(value);
190
+ if (safeRegex) {
191
+ filters[field] = { $regex: safeRegex };
192
+ regexFields[field] = true;
193
+ }
170
194
  return;
171
195
  }
172
196
  const mongoOperator = this._toMongoOperator(operator);
@@ -217,7 +241,9 @@ var QueryParser = class {
217
241
  } else if (operator === "in" || operator === "nin") {
218
242
  processedValue = Array.isArray(value) ? value : String(value).split(",").map((v) => v.trim());
219
243
  } else if (operator === "like" || operator === "contains") {
220
- processedValue = value !== void 0 && value !== null ? new RegExp(String(value), "i") : /.*/;
244
+ const safeRegex = this._createSafeRegex(value);
245
+ if (!safeRegex) continue;
246
+ processedValue = safeRegex;
221
247
  } else {
222
248
  processedValue = this._convertValue(value);
223
249
  }
@@ -232,6 +258,56 @@ var QueryParser = class {
232
258
  const op = operator.toLowerCase();
233
259
  return op.startsWith("$") ? op : "$" + op;
234
260
  }
261
+ /**
262
+ * Create a safe regex pattern with protection against ReDoS attacks
263
+ * @param pattern - The pattern string from user input
264
+ * @param flags - Regex flags (default: 'i' for case-insensitive)
265
+ * @returns A safe RegExp or null if pattern is invalid/dangerous
266
+ */
267
+ _createSafeRegex(pattern, flags = "i") {
268
+ if (pattern === null || pattern === void 0) {
269
+ return null;
270
+ }
271
+ const patternStr = String(pattern);
272
+ if (patternStr.length > this.options.maxRegexLength) {
273
+ console.warn(`[mongokit] Regex pattern too long (${patternStr.length} > ${this.options.maxRegexLength}), truncating`);
274
+ return new RegExp(this._escapeRegex(patternStr.substring(0, this.options.maxRegexLength)), flags);
275
+ }
276
+ if (this.dangerousRegexPatterns.test(patternStr)) {
277
+ console.warn("[mongokit] Potentially dangerous regex pattern detected, escaping");
278
+ return new RegExp(this._escapeRegex(patternStr), flags);
279
+ }
280
+ try {
281
+ return new RegExp(patternStr, flags);
282
+ } catch {
283
+ return new RegExp(this._escapeRegex(patternStr), flags);
284
+ }
285
+ }
286
+ /**
287
+ * Escape special regex characters for literal matching
288
+ */
289
+ _escapeRegex(str) {
290
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
291
+ }
292
+ /**
293
+ * Sanitize text search query for MongoDB $text search
294
+ * @param search - Raw search input from user
295
+ * @returns Sanitized search string or undefined
296
+ */
297
+ _sanitizeSearch(search) {
298
+ if (search === null || search === void 0 || search === "") {
299
+ return void 0;
300
+ }
301
+ let searchStr = String(search).trim();
302
+ if (!searchStr) {
303
+ return void 0;
304
+ }
305
+ if (searchStr.length > this.options.maxSearchLength) {
306
+ console.warn(`[mongokit] Search query too long (${searchStr.length} > ${this.options.maxSearchLength}), truncating`);
307
+ searchStr = searchStr.substring(0, this.options.maxSearchLength);
308
+ }
309
+ return searchStr;
310
+ }
235
311
  /**
236
312
  * Convert values based on operator type
237
313
  */
@@ -282,7 +358,8 @@ var QueryParser = class {
282
358
  return output;
283
359
  }
284
360
  };
285
- var queryParser_default = new QueryParser();
361
+ var defaultQueryParser = new QueryParser();
362
+ var queryParser_default = defaultQueryParser;
286
363
  function isMongooseSchema(value) {
287
364
  return value instanceof mongoose2.Schema;
288
365
  }
@@ -638,4 +715,4 @@ function listPattern(prefix, model) {
638
715
  return `${prefix}:list:${model}:*`;
639
716
  }
640
717
 
641
- export { buildCrudSchemasFromModel, buildCrudSchemasFromMongooseSchema, byIdKey, byQueryKey, createError, createFieldPreset, createMemoryCache, filterResponseData, getFieldsForUser, getImmutableFields, getMongooseProjection, getSystemManagedFields, isFieldUpdateAllowed, listPattern, listQueryKey, modelPattern, queryParser_default as queryParser, validateUpdateBody, versionKey };
718
+ export { QueryParser, buildCrudSchemasFromModel, buildCrudSchemasFromMongooseSchema, byIdKey, byQueryKey, createError, createFieldPreset, createMemoryCache, filterResponseData, getFieldsForUser, getImmutableFields, getMongooseProjection, getSystemManagedFields, isFieldUpdateAllowed, listPattern, listQueryKey, modelPattern, queryParser_default as queryParser, validateUpdateBody, versionKey };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@classytic/mongokit",
3
- "version": "3.0.3",
3
+ "version": "3.0.4",
4
4
  "description": "Production-grade MongoDB repositories with zero dependencies - smart pagination, events, and plugins",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",