@classytic/mongokit 3.0.2 → 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.
- package/dist/actions/index.d.ts +2 -2
- package/dist/{index-CMCrkd2v.d.ts → index-wXrOSYWG.d.ts} +1 -1
- package/dist/index.d.ts +6 -6
- package/dist/index.js +466 -14
- package/dist/pagination/PaginationEngine.d.ts +2 -2
- package/dist/pagination/PaginationEngine.js +1 -0
- package/dist/plugins/index.d.ts +34 -2
- package/dist/plugins/index.js +148 -12
- package/dist/{memory-cache-Bn_-Kk-0.d.ts → queryParser-Bek4yy3x.d.ts} +110 -2
- package/dist/{types-B3dPUKjs.d.ts → types-DAl69QgM.d.ts} +48 -1
- package/dist/utils/index.d.ts +3 -58
- package/dist/utils/index.js +85 -8
- package/package.json +1 -1
package/dist/actions/index.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export { a as aggregate, c as create, _ as deleteActions, r as read, u as update } from '../index-
|
|
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-
|
|
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,
|
|
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-
|
|
2
|
-
export { c as AggregatePaginationOptions, j as AnyModel,
|
|
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 './
|
|
8
|
-
export { i as actions } from './index-
|
|
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
|
|
@@ -46,7 +46,7 @@ declare class Repository<TDoc = AnyDocument> {
|
|
|
46
46
|
readonly _pagination: PaginationEngine<TDoc>;
|
|
47
47
|
private readonly _hookMode;
|
|
48
48
|
[key: string]: unknown;
|
|
49
|
-
constructor(Model: Model<TDoc>, plugins?: PluginType[], paginationConfig?: PaginationConfig, options?: RepositoryOptions);
|
|
49
|
+
constructor(Model: Model<TDoc, any, any, any>, plugins?: PluginType[], paginationConfig?: PaginationConfig, options?: RepositoryOptions);
|
|
50
50
|
/**
|
|
51
51
|
* Register a plugin
|
|
52
52
|
*/
|
|
@@ -236,6 +236,6 @@ declare class Repository<TDoc = AnyDocument> {
|
|
|
236
236
|
* @example
|
|
237
237
|
* const userRepo = createRepository(UserModel, [timestampPlugin()]);
|
|
238
238
|
*/
|
|
239
|
-
declare function createRepository<TDoc>(Model: mongoose.Model<TDoc>, plugins?: PluginType[], paginationConfig?: PaginationConfig, options?: RepositoryOptions): Repository<TDoc>;
|
|
239
|
+
declare function createRepository<TDoc>(Model: mongoose.Model<TDoc, any, any, any>, plugins?: PluginType[], paginationConfig?: PaginationConfig, options?: RepositoryOptions): Repository<TDoc>;
|
|
240
240
|
|
|
241
241
|
export { AggregatePaginationResult, AnyDocument, HttpError, KeysetPaginationResult, ObjectId, OffsetPaginationResult, PaginationConfig, PaginationEngine, PluginType, PopulateSpec, Repository, RepositoryContext, RepositoryOptions, SelectSpec, SortSpec, UpdateOptions, createRepository, Repository as default };
|
package/dist/index.js
CHANGED
|
@@ -643,6 +643,7 @@ var PaginationEngine = class {
|
|
|
643
643
|
* @param Model - Mongoose model to paginate
|
|
644
644
|
* @param config - Pagination configuration
|
|
645
645
|
*/
|
|
646
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
646
647
|
constructor(Model, config = {}) {
|
|
647
648
|
this.Model = Model;
|
|
648
649
|
this.config = {
|
|
@@ -1000,7 +1001,7 @@ var Repository = class {
|
|
|
1000
1001
|
const hasCursorParam = "cursor" in params || "after" in params;
|
|
1001
1002
|
const hasExplicitSort = params.sort !== void 0;
|
|
1002
1003
|
const useKeyset = !hasPageParam && (hasCursorParam || hasExplicitSort);
|
|
1003
|
-
const filters = params.filters || {};
|
|
1004
|
+
const filters = context.filters || params.filters || {};
|
|
1004
1005
|
const search = params.search;
|
|
1005
1006
|
const sort = params.sort || "-createdAt";
|
|
1006
1007
|
const limit = params.limit || params.pagination?.limit || this._pagination.config.defaultLimit;
|
|
@@ -1328,12 +1329,45 @@ function auditLogPlugin(logger) {
|
|
|
1328
1329
|
}
|
|
1329
1330
|
|
|
1330
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
|
+
}
|
|
1331
1347
|
function softDeletePlugin(options = {}) {
|
|
1332
1348
|
const deletedField = options.deletedField || "deletedAt";
|
|
1333
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;
|
|
1334
1354
|
return {
|
|
1335
1355
|
name: "softDelete",
|
|
1336
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
|
+
}
|
|
1337
1371
|
repo.on("before:delete", async (context) => {
|
|
1338
1372
|
if (options.soft !== false) {
|
|
1339
1373
|
const updateData = {
|
|
@@ -1347,23 +1381,126 @@ function softDeletePlugin(options = {}) {
|
|
|
1347
1381
|
}
|
|
1348
1382
|
});
|
|
1349
1383
|
repo.on("before:getAll", (context) => {
|
|
1350
|
-
if (
|
|
1351
|
-
const
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
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
|
+
}
|
|
1357
1393
|
}
|
|
1358
1394
|
});
|
|
1359
1395
|
repo.on("before:getById", (context) => {
|
|
1360
|
-
if (
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
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
|
+
}
|
|
1365
1404
|
}
|
|
1366
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
|
+
}
|
|
1367
1504
|
}
|
|
1368
1505
|
};
|
|
1369
1506
|
}
|
|
@@ -2277,6 +2414,321 @@ function createMemoryCache(maxEntries = 1e3) {
|
|
|
2277
2414
|
}
|
|
2278
2415
|
};
|
|
2279
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;
|
|
2280
2732
|
|
|
2281
2733
|
// src/actions/index.ts
|
|
2282
2734
|
var actions_exports = {};
|
|
@@ -2332,4 +2784,4 @@ var index_default = Repository;
|
|
|
2332
2784
|
* ```
|
|
2333
2785
|
*/
|
|
2334
2786
|
|
|
2335
|
-
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-
|
|
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
|
|
@@ -50,7 +50,7 @@ declare class PaginationEngine<TDoc = AnyDocument> {
|
|
|
50
50
|
* @param Model - Mongoose model to paginate
|
|
51
51
|
* @param config - Pagination configuration
|
|
52
52
|
*/
|
|
53
|
-
constructor(Model: Model<TDoc>, config?: PaginationConfig);
|
|
53
|
+
constructor(Model: Model<TDoc, any, any, any>, config?: PaginationConfig);
|
|
54
54
|
/**
|
|
55
55
|
* Offset-based pagination using skip/limit
|
|
56
56
|
* Best for small datasets and when users need random page access
|
|
@@ -171,6 +171,7 @@ var PaginationEngine = class {
|
|
|
171
171
|
* @param Model - Mongoose model to paginate
|
|
172
172
|
* @param config - Pagination configuration
|
|
173
173
|
*/
|
|
174
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
174
175
|
constructor(Model, config = {}) {
|
|
175
176
|
this.Model = Model;
|
|
176
177
|
this.config = {
|
package/dist/plugins/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { F as FieldPreset, r as Plugin, L as Logger,
|
|
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
|
|
package/dist/plugins/index.js
CHANGED
|
@@ -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 (
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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 (
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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 };
|
package/dist/utils/index.d.ts
CHANGED
|
@@ -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 '../
|
|
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,
|
|
134
|
+
export { buildCrudSchemasFromModel, buildCrudSchemasFromMongooseSchema, byIdKey, byQueryKey, getImmutableFields, getSystemManagedFields, isFieldUpdateAllowed, listPattern, listQueryKey, modelPattern, validateUpdateBody, versionKey };
|
package/dist/utils/index.js
CHANGED
|
@@ -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
|
|
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,
|
|
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
|
-
|
|
169
|
-
|
|
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
|
-
|
|
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
|
|
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