@classytic/mongokit 3.1.3 → 3.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +59 -1
- package/dist/index.d.ts +84 -2
- package/dist/index.js +183 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
- **Event-driven** - Pre/post hooks for all operations
|
|
15
15
|
- **12 built-in plugins** - Caching, soft delete, validation, audit logs, and more
|
|
16
16
|
- **TypeScript first** - Full type safety with discriminated unions
|
|
17
|
-
- **
|
|
17
|
+
- **410+ passing tests** - Battle-tested and production-ready
|
|
18
18
|
|
|
19
19
|
## Installation
|
|
20
20
|
|
|
@@ -476,6 +476,16 @@ GET /users?sort=-createdAt,name
|
|
|
476
476
|
|
|
477
477
|
# Search (requires text index)
|
|
478
478
|
GET /users?search=john
|
|
479
|
+
|
|
480
|
+
# Simple populate
|
|
481
|
+
GET /posts?populate=author,category
|
|
482
|
+
|
|
483
|
+
# Advanced populate with options
|
|
484
|
+
GET /posts?populate[author][select]=name,email
|
|
485
|
+
GET /posts?populate[author][match][active]=true
|
|
486
|
+
GET /posts?populate[comments][limit]=10
|
|
487
|
+
GET /posts?populate[comments][sort]=-createdAt
|
|
488
|
+
GET /posts?populate[author][populate][department][select]=name # Nested
|
|
479
489
|
```
|
|
480
490
|
|
|
481
491
|
**Security features:**
|
|
@@ -483,6 +493,54 @@ GET /users?search=john
|
|
|
483
493
|
- ReDoS protection for regex patterns
|
|
484
494
|
- Max filter depth enforcement
|
|
485
495
|
- Collection allowlists for lookups
|
|
496
|
+
- Populate path sanitization (blocks `$where`, `__proto__`, etc.)
|
|
497
|
+
- Max populate depth limit (default: 5)
|
|
498
|
+
|
|
499
|
+
### Advanced Populate Options
|
|
500
|
+
|
|
501
|
+
QueryParser supports Mongoose populate options via URL query parameters:
|
|
502
|
+
|
|
503
|
+
```typescript
|
|
504
|
+
import { QueryParser } from '@classytic/mongokit';
|
|
505
|
+
|
|
506
|
+
const parser = new QueryParser();
|
|
507
|
+
|
|
508
|
+
// Parse URL: /posts?populate[author][select]=name,email&populate[author][match][active]=true
|
|
509
|
+
const parsed = parser.parse(req.query);
|
|
510
|
+
|
|
511
|
+
// Use with Repository
|
|
512
|
+
const posts = await postRepo.getAll(
|
|
513
|
+
{ filters: parsed.filters, page: parsed.page, limit: parsed.limit },
|
|
514
|
+
{ populateOptions: parsed.populateOptions }
|
|
515
|
+
);
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
**Supported populate options:**
|
|
519
|
+
|
|
520
|
+
| Option | URL Syntax | Description |
|
|
521
|
+
|--------|------------|-------------|
|
|
522
|
+
| `select` | `populate[path][select]=field1,field2` | Fields to include (space-separated in Mongoose) |
|
|
523
|
+
| `match` | `populate[path][match][field]=value` | Filter populated documents |
|
|
524
|
+
| `limit` | `populate[path][limit]=10` | Limit number of populated docs |
|
|
525
|
+
| `sort` | `populate[path][sort]=-createdAt` | Sort populated documents |
|
|
526
|
+
| `populate` | `populate[path][populate][nested][select]=field` | Nested populate (max depth: 5) |
|
|
527
|
+
|
|
528
|
+
**Example - Complex populate:**
|
|
529
|
+
|
|
530
|
+
```typescript
|
|
531
|
+
// URL: /posts?populate[author][select]=name,avatar&populate[comments][limit]=5&populate[comments][sort]=-createdAt&populate[comments][match][approved]=true
|
|
532
|
+
|
|
533
|
+
const parsed = parser.parse(req.query);
|
|
534
|
+
// parsed.populateOptions = [
|
|
535
|
+
// { path: 'author', select: 'name avatar' },
|
|
536
|
+
// { path: 'comments', match: { approved: true }, options: { limit: 5, sort: { createdAt: -1 } } }
|
|
537
|
+
// ]
|
|
538
|
+
|
|
539
|
+
// Simple string populate still works
|
|
540
|
+
// URL: /posts?populate=author,category
|
|
541
|
+
// parsed.populate = 'author,category'
|
|
542
|
+
// parsed.populateOptions = undefined
|
|
543
|
+
```
|
|
486
544
|
|
|
487
545
|
### JSON Schema Generation
|
|
488
546
|
|
package/dist/index.d.ts
CHANGED
|
@@ -833,6 +833,7 @@ declare class Repository<TDoc = AnyDocument> {
|
|
|
833
833
|
}, options?: {
|
|
834
834
|
select?: SelectSpec;
|
|
835
835
|
populate?: PopulateSpec;
|
|
836
|
+
populateOptions?: PopulateOptions[];
|
|
836
837
|
lean?: boolean;
|
|
837
838
|
session?: ClientSession;
|
|
838
839
|
skipCache?: boolean;
|
|
@@ -1069,14 +1070,46 @@ declare class Repository<TDoc = AnyDocument> {
|
|
|
1069
1070
|
|
|
1070
1071
|
type SortSpec = Record<string, 1 | -1>;
|
|
1071
1072
|
type FilterQuery = Record<string, unknown>;
|
|
1073
|
+
/**
|
|
1074
|
+
* Mongoose-compatible populate option
|
|
1075
|
+
* Supports advanced populate with select, match, limit, sort, and nested populate
|
|
1076
|
+
*
|
|
1077
|
+
* @example
|
|
1078
|
+
* ```typescript
|
|
1079
|
+
* // URL: ?populate[author][select]=name,email&populate[author][match][active]=true
|
|
1080
|
+
* // Generates: { path: 'author', select: 'name email', match: { active: true } }
|
|
1081
|
+
* ```
|
|
1082
|
+
*/
|
|
1083
|
+
interface PopulateOption {
|
|
1084
|
+
/** Field path to populate */
|
|
1085
|
+
path: string;
|
|
1086
|
+
/** Fields to select (space-separated) */
|
|
1087
|
+
select?: string;
|
|
1088
|
+
/** Filter conditions for populated documents */
|
|
1089
|
+
match?: Record<string, unknown>;
|
|
1090
|
+
/** Query options (limit, sort, skip) */
|
|
1091
|
+
options?: {
|
|
1092
|
+
limit?: number;
|
|
1093
|
+
sort?: SortSpec;
|
|
1094
|
+
skip?: number;
|
|
1095
|
+
};
|
|
1096
|
+
/** Nested populate configuration */
|
|
1097
|
+
populate?: PopulateOption;
|
|
1098
|
+
}
|
|
1072
1099
|
/** Parsed query result with optional lookup configuration */
|
|
1073
1100
|
interface ParsedQuery {
|
|
1074
1101
|
/** MongoDB filter query */
|
|
1075
1102
|
filters: FilterQuery;
|
|
1076
1103
|
/** Sort specification */
|
|
1077
1104
|
sort?: SortSpec;
|
|
1078
|
-
/** Fields to populate (
|
|
1105
|
+
/** Fields to populate (simple comma-separated string) */
|
|
1079
1106
|
populate?: string;
|
|
1107
|
+
/**
|
|
1108
|
+
* Advanced populate options (Mongoose-compatible)
|
|
1109
|
+
* When this is set, `populate` will be undefined
|
|
1110
|
+
* @example [{ path: 'author', select: 'name email' }]
|
|
1111
|
+
*/
|
|
1112
|
+
populateOptions?: PopulateOption[];
|
|
1080
1113
|
/** Page number for offset pagination */
|
|
1081
1114
|
page?: number;
|
|
1082
1115
|
/** Cursor for keyset pagination */
|
|
@@ -1092,6 +1125,8 @@ interface ParsedQuery {
|
|
|
1092
1125
|
/** Select/project fields */
|
|
1093
1126
|
select?: Record<string, 0 | 1>;
|
|
1094
1127
|
}
|
|
1128
|
+
/** Search mode for query parser */
|
|
1129
|
+
type SearchMode = 'text' | 'regex';
|
|
1095
1130
|
interface QueryParserOptions {
|
|
1096
1131
|
/** Maximum allowed regex pattern length (default: 500) */
|
|
1097
1132
|
maxRegexLength?: number;
|
|
@@ -1107,6 +1142,18 @@ interface QueryParserOptions {
|
|
|
1107
1142
|
enableLookups?: boolean;
|
|
1108
1143
|
/** Enable aggregation parsing (default: false - requires explicit opt-in) */
|
|
1109
1144
|
enableAggregations?: boolean;
|
|
1145
|
+
/**
|
|
1146
|
+
* Search mode (default: 'text')
|
|
1147
|
+
* - 'text': Uses MongoDB $text search (requires text index)
|
|
1148
|
+
* - 'regex': Uses $or with $regex across searchFields (no index required)
|
|
1149
|
+
*/
|
|
1150
|
+
searchMode?: SearchMode;
|
|
1151
|
+
/**
|
|
1152
|
+
* Fields to search when searchMode is 'regex'
|
|
1153
|
+
* Required when searchMode is 'regex'
|
|
1154
|
+
* @example ['name', 'description', 'sku', 'tags']
|
|
1155
|
+
*/
|
|
1156
|
+
searchFields?: string[];
|
|
1110
1157
|
}
|
|
1111
1158
|
/**
|
|
1112
1159
|
* Modern Query Parser
|
|
@@ -1187,6 +1234,27 @@ declare class QueryParser {
|
|
|
1187
1234
|
* ```
|
|
1188
1235
|
*/
|
|
1189
1236
|
private _parseSelect;
|
|
1237
|
+
/**
|
|
1238
|
+
* Parse populate parameter - handles both simple string and advanced object format
|
|
1239
|
+
*
|
|
1240
|
+
* @example
|
|
1241
|
+
* ```typescript
|
|
1242
|
+
* // Simple: ?populate=author,category
|
|
1243
|
+
* // Returns: { simplePopulate: 'author,category', populateOptions: undefined }
|
|
1244
|
+
*
|
|
1245
|
+
* // Advanced: ?populate[author][select]=name,email
|
|
1246
|
+
* // Returns: { simplePopulate: undefined, populateOptions: [{ path: 'author', select: 'name email' }] }
|
|
1247
|
+
* ```
|
|
1248
|
+
*/
|
|
1249
|
+
private _parsePopulate;
|
|
1250
|
+
/**
|
|
1251
|
+
* Parse a single populate configuration
|
|
1252
|
+
*/
|
|
1253
|
+
private _parseSinglePopulate;
|
|
1254
|
+
/**
|
|
1255
|
+
* Convert populate match values (handles boolean strings, etc.)
|
|
1256
|
+
*/
|
|
1257
|
+
private _convertPopulateMatch;
|
|
1190
1258
|
/**
|
|
1191
1259
|
* Parse filter parameters
|
|
1192
1260
|
*/
|
|
@@ -1209,6 +1277,20 @@ declare class QueryParser {
|
|
|
1209
1277
|
*/
|
|
1210
1278
|
private _sanitizeMatchConfig;
|
|
1211
1279
|
private _sanitizeSearch;
|
|
1280
|
+
/**
|
|
1281
|
+
* Build regex-based multi-field search filters
|
|
1282
|
+
* Creates an $or query with case-insensitive regex across all searchFields
|
|
1283
|
+
*
|
|
1284
|
+
* @example
|
|
1285
|
+
* // searchFields: ['name', 'description', 'sku']
|
|
1286
|
+
* // search: 'azure'
|
|
1287
|
+
* // Returns: [
|
|
1288
|
+
* // { name: { $regex: /azure/i } },
|
|
1289
|
+
* // { description: { $regex: /azure/i } },
|
|
1290
|
+
* // { sku: { $regex: /azure/i } }
|
|
1291
|
+
* // ]
|
|
1292
|
+
*/
|
|
1293
|
+
private _buildRegexSearch;
|
|
1212
1294
|
private _convertValue;
|
|
1213
1295
|
private _parseOr;
|
|
1214
1296
|
private _enhanceWithBetween;
|
|
@@ -1228,4 +1310,4 @@ declare class QueryParser {
|
|
|
1228
1310
|
*/
|
|
1229
1311
|
declare function createRepository<TDoc>(Model: mongoose.Model<TDoc, any, any, any>, plugins?: PluginType[], paginationConfig?: PaginationConfig, options?: RepositoryOptions): Repository<TDoc>;
|
|
1230
1312
|
|
|
1231
|
-
export { AggregatePaginationResult, AggregationBuilder, AnyDocument, type FilterQuery, HttpError, type IController, type IControllerResponse, type IRequestContext, type IResponseFormatter, KeysetPaginationResult, LookupBuilder, LookupOptions, ObjectId, OffsetPaginationResult, PaginationConfig, PaginationEngine, PaginationResult, type ParsedQuery, PluginType, PopulateSpec, QueryParser, type QueryParserOptions, Repository, RepositoryContext, RepositoryOptions, SelectSpec, SortSpec$2 as SortSpec, UpdateOptions, WithTransactionOptions, createRepository, Repository as default };
|
|
1313
|
+
export { AggregatePaginationResult, AggregationBuilder, AnyDocument, type FilterQuery, HttpError, type IController, type IControllerResponse, type IRequestContext, type IResponseFormatter, KeysetPaginationResult, LookupBuilder, LookupOptions, ObjectId, OffsetPaginationResult, PaginationConfig, PaginationEngine, PaginationResult, type ParsedQuery, PluginType, PopulateSpec, QueryParser, type QueryParserOptions, Repository, RepositoryContext, RepositoryOptions, type SearchMode, SelectSpec, SortSpec$2 as SortSpec, UpdateOptions, WithTransactionOptions, createRepository, Repository as default };
|
package/dist/index.js
CHANGED
|
@@ -815,11 +815,12 @@ var Repository = class {
|
|
|
815
815
|
const limit = params.limit || params.pagination?.limit || this._pagination.config.defaultLimit;
|
|
816
816
|
let query = { ...filters };
|
|
817
817
|
if (search) query.$text = { $search: search };
|
|
818
|
+
const populateSpec = options.populateOptions || context.populate || options.populate;
|
|
818
819
|
const paginationOptions = {
|
|
819
820
|
filters: query,
|
|
820
821
|
sort: this._parseSort(sort),
|
|
821
822
|
limit,
|
|
822
|
-
populate: this._parsePopulate(
|
|
823
|
+
populate: this._parsePopulate(populateSpec),
|
|
823
824
|
select: context.select || options.select,
|
|
824
825
|
lean: context.lean ?? options.lean ?? true,
|
|
825
826
|
session: options.session
|
|
@@ -1194,8 +1195,14 @@ var QueryParser = class {
|
|
|
1194
1195
|
maxLimit: options.maxLimit ?? 1e3,
|
|
1195
1196
|
additionalDangerousOperators: options.additionalDangerousOperators ?? [],
|
|
1196
1197
|
enableLookups: options.enableLookups ?? true,
|
|
1197
|
-
enableAggregations: options.enableAggregations ?? false
|
|
1198
|
+
enableAggregations: options.enableAggregations ?? false,
|
|
1199
|
+
searchMode: options.searchMode ?? "text",
|
|
1200
|
+
searchFields: options.searchFields
|
|
1198
1201
|
};
|
|
1202
|
+
if (this.options.searchMode === "regex" && (!this.options.searchFields || this.options.searchFields.length === 0)) {
|
|
1203
|
+
console.warn('[mongokit] searchMode "regex" requires searchFields to be specified. Falling back to "text" mode.');
|
|
1204
|
+
this.options.searchMode = "text";
|
|
1205
|
+
}
|
|
1199
1206
|
this.dangerousOperators = [
|
|
1200
1207
|
"$where",
|
|
1201
1208
|
"$function",
|
|
@@ -1236,13 +1243,34 @@ var QueryParser = class {
|
|
|
1236
1243
|
console.warn(`[mongokit] Limit ${parsedLimit} exceeds maximum ${this.options.maxLimit}, capping to max`);
|
|
1237
1244
|
parsedLimit = this.options.maxLimit;
|
|
1238
1245
|
}
|
|
1246
|
+
const sanitizedSearch = this._sanitizeSearch(search);
|
|
1247
|
+
const { simplePopulate, populateOptions } = this._parsePopulate(populate);
|
|
1239
1248
|
const parsed = {
|
|
1240
1249
|
filters: this._parseFilters(filters),
|
|
1241
1250
|
limit: parsedLimit,
|
|
1242
1251
|
sort: this._parseSort(sort),
|
|
1243
|
-
populate,
|
|
1244
|
-
|
|
1252
|
+
populate: simplePopulate,
|
|
1253
|
+
populateOptions,
|
|
1254
|
+
search: sanitizedSearch
|
|
1245
1255
|
};
|
|
1256
|
+
if (sanitizedSearch && this.options.searchMode === "regex" && this.options.searchFields) {
|
|
1257
|
+
const regexSearchFilters = this._buildRegexSearch(sanitizedSearch);
|
|
1258
|
+
if (regexSearchFilters) {
|
|
1259
|
+
if (parsed.filters.$or) {
|
|
1260
|
+
parsed.filters = {
|
|
1261
|
+
...parsed.filters,
|
|
1262
|
+
$and: [
|
|
1263
|
+
{ $or: parsed.filters.$or },
|
|
1264
|
+
{ $or: regexSearchFilters }
|
|
1265
|
+
]
|
|
1266
|
+
};
|
|
1267
|
+
delete parsed.filters.$or;
|
|
1268
|
+
} else {
|
|
1269
|
+
parsed.filters.$or = regexSearchFilters;
|
|
1270
|
+
}
|
|
1271
|
+
parsed.search = void 0;
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1246
1274
|
if (select) {
|
|
1247
1275
|
parsed.select = this._parseSelect(select);
|
|
1248
1276
|
}
|
|
@@ -1261,7 +1289,16 @@ var QueryParser = class {
|
|
|
1261
1289
|
}
|
|
1262
1290
|
const orGroup = this._parseOr(query);
|
|
1263
1291
|
if (orGroup) {
|
|
1264
|
-
|
|
1292
|
+
if (parsed.filters.$or) {
|
|
1293
|
+
const existingOr = parsed.filters.$or;
|
|
1294
|
+
delete parsed.filters.$or;
|
|
1295
|
+
parsed.filters.$and = [
|
|
1296
|
+
{ $or: existingOr },
|
|
1297
|
+
{ $or: orGroup }
|
|
1298
|
+
];
|
|
1299
|
+
} else {
|
|
1300
|
+
parsed.filters.$or = orGroup;
|
|
1301
|
+
}
|
|
1265
1302
|
}
|
|
1266
1303
|
parsed.filters = this._enhanceWithBetween(parsed.filters);
|
|
1267
1304
|
return parsed;
|
|
@@ -1411,6 +1448,117 @@ var QueryParser = class {
|
|
|
1411
1448
|
return void 0;
|
|
1412
1449
|
}
|
|
1413
1450
|
// ============================================================
|
|
1451
|
+
// POPULATE PARSING
|
|
1452
|
+
// ============================================================
|
|
1453
|
+
/**
|
|
1454
|
+
* Parse populate parameter - handles both simple string and advanced object format
|
|
1455
|
+
*
|
|
1456
|
+
* @example
|
|
1457
|
+
* ```typescript
|
|
1458
|
+
* // Simple: ?populate=author,category
|
|
1459
|
+
* // Returns: { simplePopulate: 'author,category', populateOptions: undefined }
|
|
1460
|
+
*
|
|
1461
|
+
* // Advanced: ?populate[author][select]=name,email
|
|
1462
|
+
* // Returns: { simplePopulate: undefined, populateOptions: [{ path: 'author', select: 'name email' }] }
|
|
1463
|
+
* ```
|
|
1464
|
+
*/
|
|
1465
|
+
_parsePopulate(populate) {
|
|
1466
|
+
if (!populate) {
|
|
1467
|
+
return {};
|
|
1468
|
+
}
|
|
1469
|
+
if (typeof populate === "string") {
|
|
1470
|
+
return { simplePopulate: populate };
|
|
1471
|
+
}
|
|
1472
|
+
if (typeof populate === "object" && populate !== null) {
|
|
1473
|
+
const populateObj = populate;
|
|
1474
|
+
if (Object.keys(populateObj).length === 0) {
|
|
1475
|
+
return {};
|
|
1476
|
+
}
|
|
1477
|
+
const populateOptions = [];
|
|
1478
|
+
for (const [path, config] of Object.entries(populateObj)) {
|
|
1479
|
+
if (path.startsWith("$") || this.dangerousOperators.includes(path)) {
|
|
1480
|
+
console.warn(`[mongokit] Blocked dangerous populate path: ${path}`);
|
|
1481
|
+
continue;
|
|
1482
|
+
}
|
|
1483
|
+
const option = this._parseSinglePopulate(path, config);
|
|
1484
|
+
if (option) {
|
|
1485
|
+
populateOptions.push(option);
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
return populateOptions.length > 0 ? { populateOptions } : {};
|
|
1489
|
+
}
|
|
1490
|
+
return {};
|
|
1491
|
+
}
|
|
1492
|
+
/**
|
|
1493
|
+
* Parse a single populate configuration
|
|
1494
|
+
*/
|
|
1495
|
+
_parseSinglePopulate(path, config, depth = 0) {
|
|
1496
|
+
if (depth > 5) {
|
|
1497
|
+
console.warn(`[mongokit] Populate depth exceeds maximum (5), truncating at path: ${path}`);
|
|
1498
|
+
return { path };
|
|
1499
|
+
}
|
|
1500
|
+
if (typeof config === "string") {
|
|
1501
|
+
if (config === "true" || config === "1") {
|
|
1502
|
+
return { path };
|
|
1503
|
+
}
|
|
1504
|
+
return { path, select: config.split(",").join(" ") };
|
|
1505
|
+
}
|
|
1506
|
+
if (typeof config === "object" && config !== null) {
|
|
1507
|
+
const opts = config;
|
|
1508
|
+
const option = { path };
|
|
1509
|
+
if (opts.select && typeof opts.select === "string") {
|
|
1510
|
+
option.select = opts.select.split(",").map((s) => s.trim()).join(" ");
|
|
1511
|
+
}
|
|
1512
|
+
if (opts.match && typeof opts.match === "object") {
|
|
1513
|
+
option.match = this._convertPopulateMatch(opts.match);
|
|
1514
|
+
}
|
|
1515
|
+
if (opts.limit !== void 0) {
|
|
1516
|
+
const limit = parseInt(String(opts.limit), 10);
|
|
1517
|
+
if (!isNaN(limit) && limit > 0) {
|
|
1518
|
+
option.options = option.options || {};
|
|
1519
|
+
option.options.limit = limit;
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
if (opts.sort && typeof opts.sort === "string") {
|
|
1523
|
+
const sortSpec = this._parseSort(opts.sort);
|
|
1524
|
+
if (sortSpec) {
|
|
1525
|
+
option.options = option.options || {};
|
|
1526
|
+
option.options.sort = sortSpec;
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
if (opts.skip !== void 0) {
|
|
1530
|
+
const skip = parseInt(String(opts.skip), 10);
|
|
1531
|
+
if (!isNaN(skip) && skip >= 0) {
|
|
1532
|
+
option.options = option.options || {};
|
|
1533
|
+
option.options.skip = skip;
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
if (opts.populate && typeof opts.populate === "object") {
|
|
1537
|
+
const nestedPopulate = opts.populate;
|
|
1538
|
+
const nestedEntries = Object.entries(nestedPopulate);
|
|
1539
|
+
if (nestedEntries.length > 0) {
|
|
1540
|
+
const [nestedPath, nestedConfig] = nestedEntries[0];
|
|
1541
|
+
const nestedOption = this._parseSinglePopulate(nestedPath, nestedConfig, depth + 1);
|
|
1542
|
+
if (nestedOption) {
|
|
1543
|
+
option.populate = nestedOption;
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
return option;
|
|
1548
|
+
}
|
|
1549
|
+
return null;
|
|
1550
|
+
}
|
|
1551
|
+
/**
|
|
1552
|
+
* Convert populate match values (handles boolean strings, etc.)
|
|
1553
|
+
*/
|
|
1554
|
+
_convertPopulateMatch(match) {
|
|
1555
|
+
const converted = {};
|
|
1556
|
+
for (const [key, value] of Object.entries(match)) {
|
|
1557
|
+
converted[key] = this._convertValue(value);
|
|
1558
|
+
}
|
|
1559
|
+
return converted;
|
|
1560
|
+
}
|
|
1561
|
+
// ============================================================
|
|
1414
1562
|
// FILTER PARSING (Enhanced from original)
|
|
1415
1563
|
// ============================================================
|
|
1416
1564
|
/**
|
|
@@ -1428,7 +1576,7 @@ var QueryParser = class {
|
|
|
1428
1576
|
console.warn(`[mongokit] Blocked dangerous operator: ${key}`);
|
|
1429
1577
|
continue;
|
|
1430
1578
|
}
|
|
1431
|
-
if (["page", "limit", "sort", "populate", "search", "select", "lean", "includeDeleted", "lookup", "aggregate"].includes(key)) {
|
|
1579
|
+
if (["page", "limit", "sort", "populate", "search", "select", "lean", "includeDeleted", "lookup", "aggregate", "or", "OR", "$or"].includes(key)) {
|
|
1432
1580
|
continue;
|
|
1433
1581
|
}
|
|
1434
1582
|
const operatorMatch = key.match(/^(.+)\[(.+)\]$/);
|
|
@@ -1618,6 +1766,35 @@ var QueryParser = class {
|
|
|
1618
1766
|
}
|
|
1619
1767
|
return searchStr;
|
|
1620
1768
|
}
|
|
1769
|
+
/**
|
|
1770
|
+
* Build regex-based multi-field search filters
|
|
1771
|
+
* Creates an $or query with case-insensitive regex across all searchFields
|
|
1772
|
+
*
|
|
1773
|
+
* @example
|
|
1774
|
+
* // searchFields: ['name', 'description', 'sku']
|
|
1775
|
+
* // search: 'azure'
|
|
1776
|
+
* // Returns: [
|
|
1777
|
+
* // { name: { $regex: /azure/i } },
|
|
1778
|
+
* // { description: { $regex: /azure/i } },
|
|
1779
|
+
* // { sku: { $regex: /azure/i } }
|
|
1780
|
+
* // ]
|
|
1781
|
+
*/
|
|
1782
|
+
_buildRegexSearch(searchTerm) {
|
|
1783
|
+
if (!this.options.searchFields || this.options.searchFields.length === 0) {
|
|
1784
|
+
return null;
|
|
1785
|
+
}
|
|
1786
|
+
const safeRegex = this._createSafeRegex(searchTerm, "i");
|
|
1787
|
+
if (!safeRegex) {
|
|
1788
|
+
return null;
|
|
1789
|
+
}
|
|
1790
|
+
const orConditions = [];
|
|
1791
|
+
for (const field of this.options.searchFields) {
|
|
1792
|
+
orConditions.push({
|
|
1793
|
+
[field]: { $regex: safeRegex }
|
|
1794
|
+
});
|
|
1795
|
+
}
|
|
1796
|
+
return orConditions.length > 0 ? orConditions : null;
|
|
1797
|
+
}
|
|
1621
1798
|
_convertValue(value) {
|
|
1622
1799
|
if (value === null || value === void 0) return value;
|
|
1623
1800
|
if (Array.isArray(value)) return value.map((v) => this._convertValue(v));
|
package/package.json
CHANGED