@classytic/mongokit 3.1.4 → 3.1.6
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 +88 -2
- package/dist/index.js +187 -8
- 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
|
@@ -777,6 +777,7 @@ declare class Repository<TDoc = AnyDocument> {
|
|
|
777
777
|
getById(id: string | ObjectId, options?: {
|
|
778
778
|
select?: SelectSpec;
|
|
779
779
|
populate?: PopulateSpec;
|
|
780
|
+
populateOptions?: PopulateOptions[];
|
|
780
781
|
lean?: boolean;
|
|
781
782
|
session?: ClientSession;
|
|
782
783
|
throwOnNotFound?: boolean;
|
|
@@ -789,6 +790,7 @@ declare class Repository<TDoc = AnyDocument> {
|
|
|
789
790
|
getByQuery(query: Record<string, unknown>, options?: {
|
|
790
791
|
select?: SelectSpec;
|
|
791
792
|
populate?: PopulateSpec;
|
|
793
|
+
populateOptions?: PopulateOptions[];
|
|
792
794
|
lean?: boolean;
|
|
793
795
|
session?: ClientSession;
|
|
794
796
|
throwOnNotFound?: boolean;
|
|
@@ -830,9 +832,12 @@ declare class Repository<TDoc = AnyDocument> {
|
|
|
830
832
|
};
|
|
831
833
|
limit?: number;
|
|
832
834
|
search?: string;
|
|
835
|
+
/** Advanced populate options (from QueryParser or Arc's BaseController) */
|
|
836
|
+
populateOptions?: PopulateOptions[];
|
|
833
837
|
}, options?: {
|
|
834
838
|
select?: SelectSpec;
|
|
835
839
|
populate?: PopulateSpec;
|
|
840
|
+
populateOptions?: PopulateOptions[];
|
|
836
841
|
lean?: boolean;
|
|
837
842
|
session?: ClientSession;
|
|
838
843
|
skipCache?: boolean;
|
|
@@ -1069,14 +1074,46 @@ declare class Repository<TDoc = AnyDocument> {
|
|
|
1069
1074
|
|
|
1070
1075
|
type SortSpec = Record<string, 1 | -1>;
|
|
1071
1076
|
type FilterQuery = Record<string, unknown>;
|
|
1077
|
+
/**
|
|
1078
|
+
* Mongoose-compatible populate option
|
|
1079
|
+
* Supports advanced populate with select, match, limit, sort, and nested populate
|
|
1080
|
+
*
|
|
1081
|
+
* @example
|
|
1082
|
+
* ```typescript
|
|
1083
|
+
* // URL: ?populate[author][select]=name,email&populate[author][match][active]=true
|
|
1084
|
+
* // Generates: { path: 'author', select: 'name email', match: { active: true } }
|
|
1085
|
+
* ```
|
|
1086
|
+
*/
|
|
1087
|
+
interface PopulateOption {
|
|
1088
|
+
/** Field path to populate */
|
|
1089
|
+
path: string;
|
|
1090
|
+
/** Fields to select (space-separated) */
|
|
1091
|
+
select?: string;
|
|
1092
|
+
/** Filter conditions for populated documents */
|
|
1093
|
+
match?: Record<string, unknown>;
|
|
1094
|
+
/** Query options (limit, sort, skip) */
|
|
1095
|
+
options?: {
|
|
1096
|
+
limit?: number;
|
|
1097
|
+
sort?: SortSpec;
|
|
1098
|
+
skip?: number;
|
|
1099
|
+
};
|
|
1100
|
+
/** Nested populate configuration */
|
|
1101
|
+
populate?: PopulateOption;
|
|
1102
|
+
}
|
|
1072
1103
|
/** Parsed query result with optional lookup configuration */
|
|
1073
1104
|
interface ParsedQuery {
|
|
1074
1105
|
/** MongoDB filter query */
|
|
1075
1106
|
filters: FilterQuery;
|
|
1076
1107
|
/** Sort specification */
|
|
1077
1108
|
sort?: SortSpec;
|
|
1078
|
-
/** Fields to populate (
|
|
1109
|
+
/** Fields to populate (simple comma-separated string) */
|
|
1079
1110
|
populate?: string;
|
|
1111
|
+
/**
|
|
1112
|
+
* Advanced populate options (Mongoose-compatible)
|
|
1113
|
+
* When this is set, `populate` will be undefined
|
|
1114
|
+
* @example [{ path: 'author', select: 'name email' }]
|
|
1115
|
+
*/
|
|
1116
|
+
populateOptions?: PopulateOption[];
|
|
1080
1117
|
/** Page number for offset pagination */
|
|
1081
1118
|
page?: number;
|
|
1082
1119
|
/** Cursor for keyset pagination */
|
|
@@ -1092,6 +1129,8 @@ interface ParsedQuery {
|
|
|
1092
1129
|
/** Select/project fields */
|
|
1093
1130
|
select?: Record<string, 0 | 1>;
|
|
1094
1131
|
}
|
|
1132
|
+
/** Search mode for query parser */
|
|
1133
|
+
type SearchMode = 'text' | 'regex';
|
|
1095
1134
|
interface QueryParserOptions {
|
|
1096
1135
|
/** Maximum allowed regex pattern length (default: 500) */
|
|
1097
1136
|
maxRegexLength?: number;
|
|
@@ -1107,6 +1146,18 @@ interface QueryParserOptions {
|
|
|
1107
1146
|
enableLookups?: boolean;
|
|
1108
1147
|
/** Enable aggregation parsing (default: false - requires explicit opt-in) */
|
|
1109
1148
|
enableAggregations?: boolean;
|
|
1149
|
+
/**
|
|
1150
|
+
* Search mode (default: 'text')
|
|
1151
|
+
* - 'text': Uses MongoDB $text search (requires text index)
|
|
1152
|
+
* - 'regex': Uses $or with $regex across searchFields (no index required)
|
|
1153
|
+
*/
|
|
1154
|
+
searchMode?: SearchMode;
|
|
1155
|
+
/**
|
|
1156
|
+
* Fields to search when searchMode is 'regex'
|
|
1157
|
+
* Required when searchMode is 'regex'
|
|
1158
|
+
* @example ['name', 'description', 'sku', 'tags']
|
|
1159
|
+
*/
|
|
1160
|
+
searchFields?: string[];
|
|
1110
1161
|
}
|
|
1111
1162
|
/**
|
|
1112
1163
|
* Modern Query Parser
|
|
@@ -1187,6 +1238,27 @@ declare class QueryParser {
|
|
|
1187
1238
|
* ```
|
|
1188
1239
|
*/
|
|
1189
1240
|
private _parseSelect;
|
|
1241
|
+
/**
|
|
1242
|
+
* Parse populate parameter - handles both simple string and advanced object format
|
|
1243
|
+
*
|
|
1244
|
+
* @example
|
|
1245
|
+
* ```typescript
|
|
1246
|
+
* // Simple: ?populate=author,category
|
|
1247
|
+
* // Returns: { simplePopulate: 'author,category', populateOptions: undefined }
|
|
1248
|
+
*
|
|
1249
|
+
* // Advanced: ?populate[author][select]=name,email
|
|
1250
|
+
* // Returns: { simplePopulate: undefined, populateOptions: [{ path: 'author', select: 'name email' }] }
|
|
1251
|
+
* ```
|
|
1252
|
+
*/
|
|
1253
|
+
private _parsePopulate;
|
|
1254
|
+
/**
|
|
1255
|
+
* Parse a single populate configuration
|
|
1256
|
+
*/
|
|
1257
|
+
private _parseSinglePopulate;
|
|
1258
|
+
/**
|
|
1259
|
+
* Convert populate match values (handles boolean strings, etc.)
|
|
1260
|
+
*/
|
|
1261
|
+
private _convertPopulateMatch;
|
|
1190
1262
|
/**
|
|
1191
1263
|
* Parse filter parameters
|
|
1192
1264
|
*/
|
|
@@ -1209,6 +1281,20 @@ declare class QueryParser {
|
|
|
1209
1281
|
*/
|
|
1210
1282
|
private _sanitizeMatchConfig;
|
|
1211
1283
|
private _sanitizeSearch;
|
|
1284
|
+
/**
|
|
1285
|
+
* Build regex-based multi-field search filters
|
|
1286
|
+
* Creates an $or query with case-insensitive regex across all searchFields
|
|
1287
|
+
*
|
|
1288
|
+
* @example
|
|
1289
|
+
* // searchFields: ['name', 'description', 'sku']
|
|
1290
|
+
* // search: 'azure'
|
|
1291
|
+
* // Returns: [
|
|
1292
|
+
* // { name: { $regex: /azure/i } },
|
|
1293
|
+
* // { description: { $regex: /azure/i } },
|
|
1294
|
+
* // { sku: { $regex: /azure/i } }
|
|
1295
|
+
* // ]
|
|
1296
|
+
*/
|
|
1297
|
+
private _buildRegexSearch;
|
|
1212
1298
|
private _convertValue;
|
|
1213
1299
|
private _parseOr;
|
|
1214
1300
|
private _enhanceWithBetween;
|
|
@@ -1228,4 +1314,4 @@ declare class QueryParser {
|
|
|
1228
1314
|
*/
|
|
1229
1315
|
declare function createRepository<TDoc>(Model: mongoose.Model<TDoc, any, any, any>, plugins?: PluginType[], paginationConfig?: PaginationConfig, options?: RepositoryOptions): Repository<TDoc>;
|
|
1230
1316
|
|
|
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 };
|
|
1317
|
+
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, type PopulateOption, 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
|
@@ -756,7 +756,8 @@ var Repository = class {
|
|
|
756
756
|
* Get document by ID
|
|
757
757
|
*/
|
|
758
758
|
async getById(id, options = {}) {
|
|
759
|
-
const
|
|
759
|
+
const populateSpec = options.populateOptions || options.populate;
|
|
760
|
+
const context = await this._buildContext("getById", { id, ...options, populate: populateSpec });
|
|
760
761
|
if (context._cacheHit) {
|
|
761
762
|
return context._cachedResult;
|
|
762
763
|
}
|
|
@@ -768,7 +769,8 @@ var Repository = class {
|
|
|
768
769
|
* Get single document by query
|
|
769
770
|
*/
|
|
770
771
|
async getByQuery(query, options = {}) {
|
|
771
|
-
const
|
|
772
|
+
const populateSpec = options.populateOptions || options.populate;
|
|
773
|
+
const context = await this._buildContext("getByQuery", { query, ...options, populate: populateSpec });
|
|
772
774
|
if (context._cacheHit) {
|
|
773
775
|
return context._cachedResult;
|
|
774
776
|
}
|
|
@@ -815,11 +817,12 @@ var Repository = class {
|
|
|
815
817
|
const limit = params.limit || params.pagination?.limit || this._pagination.config.defaultLimit;
|
|
816
818
|
let query = { ...filters };
|
|
817
819
|
if (search) query.$text = { $search: search };
|
|
820
|
+
const populateSpec = options.populateOptions || params.populateOptions || context.populate || options.populate;
|
|
818
821
|
const paginationOptions = {
|
|
819
822
|
filters: query,
|
|
820
823
|
sort: this._parseSort(sort),
|
|
821
824
|
limit,
|
|
822
|
-
populate: this._parsePopulate(
|
|
825
|
+
populate: this._parsePopulate(populateSpec),
|
|
823
826
|
select: context.select || options.select,
|
|
824
827
|
lean: context.lean ?? options.lean ?? true,
|
|
825
828
|
session: options.session
|
|
@@ -1194,8 +1197,14 @@ var QueryParser = class {
|
|
|
1194
1197
|
maxLimit: options.maxLimit ?? 1e3,
|
|
1195
1198
|
additionalDangerousOperators: options.additionalDangerousOperators ?? [],
|
|
1196
1199
|
enableLookups: options.enableLookups ?? true,
|
|
1197
|
-
enableAggregations: options.enableAggregations ?? false
|
|
1200
|
+
enableAggregations: options.enableAggregations ?? false,
|
|
1201
|
+
searchMode: options.searchMode ?? "text",
|
|
1202
|
+
searchFields: options.searchFields
|
|
1198
1203
|
};
|
|
1204
|
+
if (this.options.searchMode === "regex" && (!this.options.searchFields || this.options.searchFields.length === 0)) {
|
|
1205
|
+
console.warn('[mongokit] searchMode "regex" requires searchFields to be specified. Falling back to "text" mode.');
|
|
1206
|
+
this.options.searchMode = "text";
|
|
1207
|
+
}
|
|
1199
1208
|
this.dangerousOperators = [
|
|
1200
1209
|
"$where",
|
|
1201
1210
|
"$function",
|
|
@@ -1236,13 +1245,34 @@ var QueryParser = class {
|
|
|
1236
1245
|
console.warn(`[mongokit] Limit ${parsedLimit} exceeds maximum ${this.options.maxLimit}, capping to max`);
|
|
1237
1246
|
parsedLimit = this.options.maxLimit;
|
|
1238
1247
|
}
|
|
1248
|
+
const sanitizedSearch = this._sanitizeSearch(search);
|
|
1249
|
+
const { simplePopulate, populateOptions } = this._parsePopulate(populate);
|
|
1239
1250
|
const parsed = {
|
|
1240
1251
|
filters: this._parseFilters(filters),
|
|
1241
1252
|
limit: parsedLimit,
|
|
1242
1253
|
sort: this._parseSort(sort),
|
|
1243
|
-
populate,
|
|
1244
|
-
|
|
1254
|
+
populate: simplePopulate,
|
|
1255
|
+
populateOptions,
|
|
1256
|
+
search: sanitizedSearch
|
|
1245
1257
|
};
|
|
1258
|
+
if (sanitizedSearch && this.options.searchMode === "regex" && this.options.searchFields) {
|
|
1259
|
+
const regexSearchFilters = this._buildRegexSearch(sanitizedSearch);
|
|
1260
|
+
if (regexSearchFilters) {
|
|
1261
|
+
if (parsed.filters.$or) {
|
|
1262
|
+
parsed.filters = {
|
|
1263
|
+
...parsed.filters,
|
|
1264
|
+
$and: [
|
|
1265
|
+
{ $or: parsed.filters.$or },
|
|
1266
|
+
{ $or: regexSearchFilters }
|
|
1267
|
+
]
|
|
1268
|
+
};
|
|
1269
|
+
delete parsed.filters.$or;
|
|
1270
|
+
} else {
|
|
1271
|
+
parsed.filters.$or = regexSearchFilters;
|
|
1272
|
+
}
|
|
1273
|
+
parsed.search = void 0;
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1246
1276
|
if (select) {
|
|
1247
1277
|
parsed.select = this._parseSelect(select);
|
|
1248
1278
|
}
|
|
@@ -1261,7 +1291,16 @@ var QueryParser = class {
|
|
|
1261
1291
|
}
|
|
1262
1292
|
const orGroup = this._parseOr(query);
|
|
1263
1293
|
if (orGroup) {
|
|
1264
|
-
|
|
1294
|
+
if (parsed.filters.$or) {
|
|
1295
|
+
const existingOr = parsed.filters.$or;
|
|
1296
|
+
delete parsed.filters.$or;
|
|
1297
|
+
parsed.filters.$and = [
|
|
1298
|
+
{ $or: existingOr },
|
|
1299
|
+
{ $or: orGroup }
|
|
1300
|
+
];
|
|
1301
|
+
} else {
|
|
1302
|
+
parsed.filters.$or = orGroup;
|
|
1303
|
+
}
|
|
1265
1304
|
}
|
|
1266
1305
|
parsed.filters = this._enhanceWithBetween(parsed.filters);
|
|
1267
1306
|
return parsed;
|
|
@@ -1411,6 +1450,117 @@ var QueryParser = class {
|
|
|
1411
1450
|
return void 0;
|
|
1412
1451
|
}
|
|
1413
1452
|
// ============================================================
|
|
1453
|
+
// POPULATE PARSING
|
|
1454
|
+
// ============================================================
|
|
1455
|
+
/**
|
|
1456
|
+
* Parse populate parameter - handles both simple string and advanced object format
|
|
1457
|
+
*
|
|
1458
|
+
* @example
|
|
1459
|
+
* ```typescript
|
|
1460
|
+
* // Simple: ?populate=author,category
|
|
1461
|
+
* // Returns: { simplePopulate: 'author,category', populateOptions: undefined }
|
|
1462
|
+
*
|
|
1463
|
+
* // Advanced: ?populate[author][select]=name,email
|
|
1464
|
+
* // Returns: { simplePopulate: undefined, populateOptions: [{ path: 'author', select: 'name email' }] }
|
|
1465
|
+
* ```
|
|
1466
|
+
*/
|
|
1467
|
+
_parsePopulate(populate) {
|
|
1468
|
+
if (!populate) {
|
|
1469
|
+
return {};
|
|
1470
|
+
}
|
|
1471
|
+
if (typeof populate === "string") {
|
|
1472
|
+
return { simplePopulate: populate };
|
|
1473
|
+
}
|
|
1474
|
+
if (typeof populate === "object" && populate !== null) {
|
|
1475
|
+
const populateObj = populate;
|
|
1476
|
+
if (Object.keys(populateObj).length === 0) {
|
|
1477
|
+
return {};
|
|
1478
|
+
}
|
|
1479
|
+
const populateOptions = [];
|
|
1480
|
+
for (const [path, config] of Object.entries(populateObj)) {
|
|
1481
|
+
if (path.startsWith("$") || this.dangerousOperators.includes(path)) {
|
|
1482
|
+
console.warn(`[mongokit] Blocked dangerous populate path: ${path}`);
|
|
1483
|
+
continue;
|
|
1484
|
+
}
|
|
1485
|
+
const option = this._parseSinglePopulate(path, config);
|
|
1486
|
+
if (option) {
|
|
1487
|
+
populateOptions.push(option);
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
return populateOptions.length > 0 ? { populateOptions } : {};
|
|
1491
|
+
}
|
|
1492
|
+
return {};
|
|
1493
|
+
}
|
|
1494
|
+
/**
|
|
1495
|
+
* Parse a single populate configuration
|
|
1496
|
+
*/
|
|
1497
|
+
_parseSinglePopulate(path, config, depth = 0) {
|
|
1498
|
+
if (depth > 5) {
|
|
1499
|
+
console.warn(`[mongokit] Populate depth exceeds maximum (5), truncating at path: ${path}`);
|
|
1500
|
+
return { path };
|
|
1501
|
+
}
|
|
1502
|
+
if (typeof config === "string") {
|
|
1503
|
+
if (config === "true" || config === "1") {
|
|
1504
|
+
return { path };
|
|
1505
|
+
}
|
|
1506
|
+
return { path, select: config.split(",").join(" ") };
|
|
1507
|
+
}
|
|
1508
|
+
if (typeof config === "object" && config !== null) {
|
|
1509
|
+
const opts = config;
|
|
1510
|
+
const option = { path };
|
|
1511
|
+
if (opts.select && typeof opts.select === "string") {
|
|
1512
|
+
option.select = opts.select.split(",").map((s) => s.trim()).join(" ");
|
|
1513
|
+
}
|
|
1514
|
+
if (opts.match && typeof opts.match === "object") {
|
|
1515
|
+
option.match = this._convertPopulateMatch(opts.match);
|
|
1516
|
+
}
|
|
1517
|
+
if (opts.limit !== void 0) {
|
|
1518
|
+
const limit = parseInt(String(opts.limit), 10);
|
|
1519
|
+
if (!isNaN(limit) && limit > 0) {
|
|
1520
|
+
option.options = option.options || {};
|
|
1521
|
+
option.options.limit = limit;
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
if (opts.sort && typeof opts.sort === "string") {
|
|
1525
|
+
const sortSpec = this._parseSort(opts.sort);
|
|
1526
|
+
if (sortSpec) {
|
|
1527
|
+
option.options = option.options || {};
|
|
1528
|
+
option.options.sort = sortSpec;
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
if (opts.skip !== void 0) {
|
|
1532
|
+
const skip = parseInt(String(opts.skip), 10);
|
|
1533
|
+
if (!isNaN(skip) && skip >= 0) {
|
|
1534
|
+
option.options = option.options || {};
|
|
1535
|
+
option.options.skip = skip;
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
if (opts.populate && typeof opts.populate === "object") {
|
|
1539
|
+
const nestedPopulate = opts.populate;
|
|
1540
|
+
const nestedEntries = Object.entries(nestedPopulate);
|
|
1541
|
+
if (nestedEntries.length > 0) {
|
|
1542
|
+
const [nestedPath, nestedConfig] = nestedEntries[0];
|
|
1543
|
+
const nestedOption = this._parseSinglePopulate(nestedPath, nestedConfig, depth + 1);
|
|
1544
|
+
if (nestedOption) {
|
|
1545
|
+
option.populate = nestedOption;
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
return option;
|
|
1550
|
+
}
|
|
1551
|
+
return null;
|
|
1552
|
+
}
|
|
1553
|
+
/**
|
|
1554
|
+
* Convert populate match values (handles boolean strings, etc.)
|
|
1555
|
+
*/
|
|
1556
|
+
_convertPopulateMatch(match) {
|
|
1557
|
+
const converted = {};
|
|
1558
|
+
for (const [key, value] of Object.entries(match)) {
|
|
1559
|
+
converted[key] = this._convertValue(value);
|
|
1560
|
+
}
|
|
1561
|
+
return converted;
|
|
1562
|
+
}
|
|
1563
|
+
// ============================================================
|
|
1414
1564
|
// FILTER PARSING (Enhanced from original)
|
|
1415
1565
|
// ============================================================
|
|
1416
1566
|
/**
|
|
@@ -1428,7 +1578,7 @@ var QueryParser = class {
|
|
|
1428
1578
|
console.warn(`[mongokit] Blocked dangerous operator: ${key}`);
|
|
1429
1579
|
continue;
|
|
1430
1580
|
}
|
|
1431
|
-
if (["page", "limit", "sort", "populate", "search", "select", "lean", "includeDeleted", "lookup", "aggregate"].includes(key)) {
|
|
1581
|
+
if (["page", "limit", "sort", "populate", "search", "select", "lean", "includeDeleted", "lookup", "aggregate", "or", "OR", "$or"].includes(key)) {
|
|
1432
1582
|
continue;
|
|
1433
1583
|
}
|
|
1434
1584
|
const operatorMatch = key.match(/^(.+)\[(.+)\]$/);
|
|
@@ -1618,6 +1768,35 @@ var QueryParser = class {
|
|
|
1618
1768
|
}
|
|
1619
1769
|
return searchStr;
|
|
1620
1770
|
}
|
|
1771
|
+
/**
|
|
1772
|
+
* Build regex-based multi-field search filters
|
|
1773
|
+
* Creates an $or query with case-insensitive regex across all searchFields
|
|
1774
|
+
*
|
|
1775
|
+
* @example
|
|
1776
|
+
* // searchFields: ['name', 'description', 'sku']
|
|
1777
|
+
* // search: 'azure'
|
|
1778
|
+
* // Returns: [
|
|
1779
|
+
* // { name: { $regex: /azure/i } },
|
|
1780
|
+
* // { description: { $regex: /azure/i } },
|
|
1781
|
+
* // { sku: { $regex: /azure/i } }
|
|
1782
|
+
* // ]
|
|
1783
|
+
*/
|
|
1784
|
+
_buildRegexSearch(searchTerm) {
|
|
1785
|
+
if (!this.options.searchFields || this.options.searchFields.length === 0) {
|
|
1786
|
+
return null;
|
|
1787
|
+
}
|
|
1788
|
+
const safeRegex = this._createSafeRegex(searchTerm, "i");
|
|
1789
|
+
if (!safeRegex) {
|
|
1790
|
+
return null;
|
|
1791
|
+
}
|
|
1792
|
+
const orConditions = [];
|
|
1793
|
+
for (const field of this.options.searchFields) {
|
|
1794
|
+
orConditions.push({
|
|
1795
|
+
[field]: { $regex: safeRegex }
|
|
1796
|
+
});
|
|
1797
|
+
}
|
|
1798
|
+
return orConditions.length > 0 ? orConditions : null;
|
|
1799
|
+
}
|
|
1621
1800
|
_convertValue(value) {
|
|
1622
1801
|
if (value === null || value === void 0) return value;
|
|
1623
1802
|
if (Array.isArray(value)) return value.map((v) => this._convertValue(v));
|
package/package.json
CHANGED