@classytic/mongokit 3.38.1 → 3.40.0
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/{PaginationEngine-CBS1XGzp.d.mts → PaginationEngine-D-P19Y9m.d.mts} +34 -9
- package/dist/PaginationEngine-Dvl8jcRY.mjs +1198 -0
- package/dist/{Repository-Ck5x43wF.mjs → Repository-BMgPSkqZ.mjs} +672 -655
- package/dist/{Repository-BdSkQ8vc.d.mts → Repository-CR8Y9g2E.d.mts} +106 -34
- package/dist/actions/index.d.mts +1 -1
- package/dist/actions/index.mjs +1 -1
- package/dist/adapter/index.d.mts +21 -0
- package/dist/adapter/index.mjs +1 -1
- package/dist/{adapter-Bfqz8yHE.mjs → adapter-BrVfPFSW.mjs} +45 -1
- package/dist/ai/index.d.mts +1 -1
- package/dist/better-auth/index.mjs +2 -2
- package/dist/{create-repository-B5EulQ31.mjs → create-repository-BzBUMvd1.mjs} +3 -3
- package/dist/{create-repository-BU4x9Kb0.d.mts → create-repository-CAle1MSU.d.mts} +5 -5
- package/dist/idempotency/index.d.mts +23 -0
- package/dist/idempotency/index.mjs +134 -0
- package/dist/{index-DKEql4cL.d.mts → index-CmjNyulu.d.mts} +67 -8
- package/dist/index.d.mts +125 -35
- package/dist/index.mjs +286 -676
- package/dist/indexes/verify-indexes.d.mts +14 -1
- package/dist/indexes/verify-indexes.mjs +54 -7
- package/dist/{method-registry.plugin-DblM7V6j.mjs → method-registry.plugin-C63pGn1d.mjs} +54 -4
- package/dist/{multi-tenant.plugin-BwSvTirB.d.mts → multi-tenant.plugin-CBgCo9im.d.mts} +77 -5
- package/dist/{operations-DZodvjC-.d.mts → operations-CrRIoc8Z.d.mts} +22 -7
- package/dist/outbox/index.d.mts +1 -1
- package/dist/outbox/index.mjs +7 -5
- package/dist/pagination/PaginationEngine.d.mts +1 -1
- package/dist/pagination/PaginationEngine.mjs +1 -1
- package/dist/pagination/utils/limits.d.mts +1 -1
- package/dist/pagination/utils/limits.mjs +22 -1
- package/dist/pagination-BpIv2bdS.d.mts +364 -0
- package/dist/{plugin-options-DXnlKM3B.d.mts → plugin-options-Zk46lD6F.d.mts} +16 -2
- package/dist/plugins/index.d.mts +8 -3
- package/dist/plugins/index.mjs +99 -26
- package/dist/{repository-H_ffGgpE.d.mts → repository-odl03582.d.mts} +50 -6
- package/dist/sync/index.d.mts +40 -0
- package/dist/sync/index.mjs +334 -0
- package/dist/tenant/index.d.mts +89 -0
- package/dist/tenant/index.mjs +96 -0
- package/dist/testkit/index.d.mts +2 -2
- package/dist/testkit/index.mjs +1 -1
- package/dist/{timestamp.plugin-CBQukQzV.mjs → timestamp.plugin-DF0gJB6p.mjs} +116 -9
- package/dist/{to-plain-Dyz_qvuo.d.mts → to-plain-k7SjNlSS.d.mts} +2 -2
- package/dist/transaction-B5L9glO0.mjs +533 -0
- package/dist/{update-jSXp3nu3.mjs → update-DG0fCwRJ.mjs} +293 -83
- package/dist/utils/index.d.mts +1 -1
- package/dist/{validation-chain.plugin-DhNyxNCw.mjs → validation-chain.plugin-BLY2gLEk.mjs} +192 -279
- package/dist/{validation-chain.plugin-yQRob3Av.d.mts → validation-chain.plugin-DYp9E7K2.d.mts} +26 -20
- package/package.json +17 -4
- package/dist/PaginationEngine-BX9o_-4R.mjs +0 -735
- package/dist/core-62oo3N1C.d.mts +0 -50
- package/dist/pagination-jSUdrACK.d.mts +0 -158
package/dist/index.mjs
CHANGED
|
@@ -1,23 +1,24 @@
|
|
|
1
1
|
import { n as idVariants } from "./id-resolution-BqDrk0S2.mjs";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { a as MUTATING_OPERATIONS, c as operationsByPolicyKey, i as ALL_OPERATIONS, l as createMongoPurgePort, n as Repository, o as OP_REGISTRY, r as AggregationBuilder, s as READ_OPERATIONS, t as HOOK_PRIORITY } from "./Repository-BMgPSkqZ.mjs";
|
|
3
|
+
import { C as isCreateManyPartialError, g as LookupBuilder, v as compileFilterToMongo } from "./update-DG0fCwRJ.mjs";
|
|
4
4
|
import { n as warn, t as configureLogger } from "./logger-K7dPmu2U.mjs";
|
|
5
5
|
import { n as isDuplicateKeyError, r as parseDuplicateKeyError, t as createError } from "./error-BuByZwKU.mjs";
|
|
6
6
|
import { t as actions_exports } from "./actions/index.mjs";
|
|
7
|
-
import {
|
|
8
|
-
import { n as
|
|
9
|
-
import { t as
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
7
|
+
import { c as classifyHelloReply, d as resolveMongoCapabilities, f as resolveTransactionSupport, i as withTransaction, l as probeMongoCapabilities, n as isTransactionUnsupported, o as MONGOKIT_CAPABILITIES, p as transactionResolutionOf, r as supportsTransactions, s as capabilitiesForSupport, t as batchTransaction, u as resetCapabilityProbeCache } from "./transaction-B5L9glO0.mjs";
|
|
8
|
+
import { _ as AuditTrailQuery, a as declareTenantContextKey, b as auditLogPlugin, c as tenantContextKeysOf, d as ensureSequenceAtLeast, f as getNextSequence, g as cachePlugin, h as sequentialId, i as multiTenantPlugin, l as customIdPlugin, m as readSequence, n as softDeletePlugin, o as forwardScope, p as prefixedId, r as adminBypass, s as isScopeKey, t as timestampPlugin, u as dateSequentialId, v as auditTrailPlugin, y as ensureAuditTrailReady } from "./timestamp.plugin-DF0gJB6p.mjs";
|
|
9
|
+
import { n as batchOperationsPlugin, t as methodRegistryPlugin } from "./method-registry.plugin-C63pGn1d.mjs";
|
|
10
|
+
import { a as getPaginationDefaults, i as configurePaginationDefaults, o as resetPaginationDefaults, t as PaginationEngine } from "./PaginationEngine-Dvl8jcRY.mjs";
|
|
11
|
+
import { GEO_OPERATORS, parseGeoFilter } from "./query/primitives/geo.mjs";
|
|
12
|
+
import { t as createRepository } from "./create-repository-BzBUMvd1.mjs";
|
|
12
13
|
import { n as defineModel, r as isModelRegistered, t as ModelCollisionError } from "./model-registry-ClKtWiHa.mjs";
|
|
13
|
-
import { _ as cascadePlugin, a as uniqueField, c as subdocumentPlugin, d as mongoOperationsPlugin, f as leasePlugin, g as elasticSearchPlugin, h as fieldFilterPlugin, i as requireField, l as observabilityPlugin, m as idempotencyStorePlugin, n as blockIf, o as validationChainPlugin, p as immutableStatesPlugin, r as immutableField, s as createTenantContext, t as autoInject, u as MONGOKIT_PLUGIN_NAMES, v as appendOnlyPlugin, y as aggregateHelpersPlugin } from "./validation-chain.plugin-
|
|
14
|
+
import { _ as cascadePlugin, a as uniqueField, c as subdocumentPlugin, d as mongoOperationsPlugin, f as leasePlugin, g as elasticSearchPlugin, h as fieldFilterPlugin, i as requireField, l as observabilityPlugin, m as idempotencyStorePlugin, n as blockIf, o as validationChainPlugin, p as immutableStatesPlugin, r as immutableField, s as createTenantContext, t as autoInject, u as MONGOKIT_PLUGIN_NAMES, v as appendOnlyPlugin, y as aggregateHelpersPlugin } from "./validation-chain.plugin-BLY2gLEk.mjs";
|
|
14
15
|
import { i as getMongooseProjection, n as filterResponseData, r as getFieldsForUser, t as createFieldPreset } from "./field-selection-FSQFLpsU.mjs";
|
|
15
|
-
import { buildFieldTypeMap,
|
|
16
|
+
import { buildFieldTypeMap, coerceHeuristic } from "./query/primitives/coercion.mjs";
|
|
16
17
|
import { extractSchemaIndexes } from "./query/primitives/indexes.mjs";
|
|
17
18
|
import { n as createMemoryCache, t as toPlain } from "./to-plain-vksRrLtc.mjs";
|
|
18
19
|
import { n as buildCrudSchemasFromMongooseSchema, t as buildCrudSchemasFromModel } from "./mongooseToJsonSchema-waUVQwK4.mjs";
|
|
19
20
|
import { runChunkedPurge } from "@classytic/repo-core/repository";
|
|
20
|
-
import {
|
|
21
|
+
import { OPERATOR_DESCRIPTIONS, QueryGrammarError, URL_OPERATORS, clausesToFilter, escapeRegex, isQueryGrammarError, readFilterClauses, readPageRequest, readSearch, readSelect, readSort } from "@classytic/repo-core/query-parser";
|
|
21
22
|
//#region src/actions/cascade-references.ts
|
|
22
23
|
/**
|
|
23
24
|
* cascadePurgeReferences — imperative, multi-relation "purge by reference".
|
|
@@ -357,410 +358,134 @@ function parseAggregation(rt, aggregate) {
|
|
|
357
358
|
return pipeline.length > 0 ? pipeline : void 0;
|
|
358
359
|
}
|
|
359
360
|
//#endregion
|
|
360
|
-
//#region src/query/parser/regex-safety.ts
|
|
361
|
-
/**
|
|
362
|
-
* Regex safety — ReDoS protection for user-supplied patterns.
|
|
363
|
-
*
|
|
364
|
-
* Two independent detectors feed the decision:
|
|
365
|
-
* 1. A heuristic pattern for classic catastrophic-backtracking shapes
|
|
366
|
-
* (nested quantifiers, possessive quantifiers, backreferences, …).
|
|
367
|
-
* 2. A deterministic static complexity budget, so patterns that evade the
|
|
368
|
-
* heuristic still fail over when their unbounded-quantifier density is
|
|
369
|
-
* pathological.
|
|
370
|
-
*
|
|
371
|
-
* Unsafe input is never executed as a regex: depending on the caller's
|
|
372
|
-
* intent it is either escaped to a literal match or rejected through the
|
|
373
|
-
* `invalidInput` policy.
|
|
374
|
-
*/
|
|
375
|
-
/**
|
|
376
|
-
* Regex patterns that can cause catastrophic backtracking (ReDoS attacks).
|
|
377
|
-
* Detects common footguns:
|
|
378
|
-
* - Quantifiers: {n,m}
|
|
379
|
-
* - Possessive quantifiers: *+, ++, ?+
|
|
380
|
-
* - Nested quantifiers: (a+)+, (a*)*
|
|
381
|
-
* - Backreferences: \1, \2, etc.
|
|
382
|
-
* - Complex character classes: [...]...[...]
|
|
383
|
-
*/
|
|
384
|
-
const DANGEROUS_REGEX_PATTERNS = /(\{[0-9,]+\}|\*\+|\+\+|\?\+|(\(.+\))\+|\(\?:|\\[0-9]|(\[.+\]).+(\[.+\]))/;
|
|
385
|
-
/**
|
|
386
|
-
* Static complexity budget for user-supplied regex patterns.
|
|
387
|
-
* Returns `true` if the pattern crosses a budget that empirically correlates
|
|
388
|
-
* with ReDoS risk — independent of the heuristic regex detector.
|
|
389
|
-
*
|
|
390
|
-
* Budget components (any single threshold crossed → over budget):
|
|
391
|
-
* - unbounded quantifiers (`*`, `+`, `?`) — hard cap 20
|
|
392
|
-
* - nested groups `(` — hard cap 8
|
|
393
|
-
* - alternations `|` — hard cap 10
|
|
394
|
-
* - combined: (quantifiers * groups) — hard cap 40
|
|
395
|
-
*/
|
|
396
|
-
function regexComplexityExceedsBudget(patternStr) {
|
|
397
|
-
const stripped = patternStr.replace(/\\[^\\]/g, "");
|
|
398
|
-
const unboundedQuantifiers = (stripped.match(/[*+?]/g) ?? []).length;
|
|
399
|
-
const groups = (stripped.match(/\(/g) ?? []).length;
|
|
400
|
-
const alternations = (stripped.match(/\|/g) ?? []).length;
|
|
401
|
-
if (unboundedQuantifiers > 20) return true;
|
|
402
|
-
if (groups > 8) return true;
|
|
403
|
-
if (alternations > 10) return true;
|
|
404
|
-
if (unboundedQuantifiers * groups > 40) return true;
|
|
405
|
-
return false;
|
|
406
|
-
}
|
|
407
|
-
/** Escape special regex characters (backslash first, then other metas). */
|
|
408
|
-
function escapeRegex(str) {
|
|
409
|
-
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
410
|
-
}
|
|
411
|
-
/**
|
|
412
|
-
* Build a safe RegExp from user input.
|
|
413
|
-
*
|
|
414
|
-
* `onUnsafe` selects what happens when the pattern is over-long,
|
|
415
|
-
* pathological, or syntactically invalid:
|
|
416
|
-
* - `'reject'`: route through the `invalidInput` policy — HTTP 400 in
|
|
417
|
-
* `'throw'` mode, warn + escape-to-literal in `'drop'` mode. Used for
|
|
418
|
-
* the explicit `regex` operator, where the caller claims to be sending
|
|
419
|
-
* a real regex.
|
|
420
|
-
* - `'escape'`: always warn + escape-to-literal, never throw. Used for
|
|
421
|
-
* `search` terms and the `like`/`contains` operators, whose documented
|
|
422
|
-
* semantic is a literal substring match — `?search=c++` must not 400.
|
|
423
|
-
*/
|
|
424
|
-
function createSafeRegex(rt, pattern, flags = "i", onUnsafe = "reject") {
|
|
425
|
-
if (pattern === null || pattern === void 0) return null;
|
|
426
|
-
const flag = (message, meta) => {
|
|
427
|
-
if (onUnsafe === "reject") rt.reject(message, meta);
|
|
428
|
-
else warn(`[mongokit] ${message}`);
|
|
429
|
-
};
|
|
430
|
-
const patternStr = String(pattern);
|
|
431
|
-
if (patternStr.length > rt.options.maxRegexLength) {
|
|
432
|
-
flag(`Regex pattern too long, truncating`, {
|
|
433
|
-
length: patternStr.length,
|
|
434
|
-
maxRegexLength: rt.options.maxRegexLength
|
|
435
|
-
});
|
|
436
|
-
return new RegExp(escapeRegex(patternStr.substring(0, rt.options.maxRegexLength)), flags);
|
|
437
|
-
}
|
|
438
|
-
if (DANGEROUS_REGEX_PATTERNS.test(patternStr)) {
|
|
439
|
-
flag("Potentially dangerous regex pattern, escaping");
|
|
440
|
-
return new RegExp(escapeRegex(patternStr), flags);
|
|
441
|
-
}
|
|
442
|
-
if (regexComplexityExceedsBudget(patternStr)) {
|
|
443
|
-
flag("Regex complexity budget exceeded, escaping");
|
|
444
|
-
return new RegExp(escapeRegex(patternStr), flags);
|
|
445
|
-
}
|
|
446
|
-
try {
|
|
447
|
-
return new RegExp(patternStr, flags);
|
|
448
|
-
} catch {
|
|
449
|
-
flag("Invalid regex pattern syntax, escaping to literal match");
|
|
450
|
-
return new RegExp(escapeRegex(patternStr), flags);
|
|
451
|
-
}
|
|
452
|
-
}
|
|
453
|
-
//#endregion
|
|
454
361
|
//#region src/query/parser/filter-compiler.ts
|
|
455
362
|
/**
|
|
456
|
-
*
|
|
457
|
-
*
|
|
458
|
-
*
|
|
459
|
-
* `
|
|
460
|
-
* and the `between` range expander. Every invalid or disallowed fragment
|
|
461
|
-
* routes through the runtime's `invalidInput` policy.
|
|
363
|
+
* URL filters → a MongoDB filter. The grammar (keys, operators, value coercion, refusals) is
|
|
364
|
+
* repo-core's `readFilterClauses`; its clauses compile through the Filter IR compiler every
|
|
365
|
+
* repository query uses, so URL and programmatic filters mean the same thing. Only mongokit's own
|
|
366
|
+
* operators (`size`, `type`, geo) are emitted here, natively.
|
|
462
367
|
*/
|
|
368
|
+
/** mongokit-native operators, beyond the shared grammar. */
|
|
369
|
+
const MONGOKIT_EXTENSION_OPERATORS = [
|
|
370
|
+
"size",
|
|
371
|
+
"type",
|
|
372
|
+
...GEO_OPERATORS
|
|
373
|
+
];
|
|
374
|
+
/** Group params — `or[0][status]=a&or[1][qty][gte]=5` — compiled branch by branch. */
|
|
375
|
+
const GROUP_PARAMS = [
|
|
376
|
+
"or",
|
|
377
|
+
"OR",
|
|
378
|
+
"$or",
|
|
379
|
+
"$and"
|
|
380
|
+
];
|
|
381
|
+
/** mongokit control params that are not filters. */
|
|
382
|
+
const MONGOKIT_RESERVED_PARAMS = [
|
|
383
|
+
"lean",
|
|
384
|
+
"includeDeleted",
|
|
385
|
+
"lookup",
|
|
386
|
+
"aggregate",
|
|
387
|
+
"cursor",
|
|
388
|
+
...GROUP_PARAMS
|
|
389
|
+
];
|
|
390
|
+
const BSON_TYPE_RE = /^(?:[a-zA-Z]+|\d{1,3})$/;
|
|
391
|
+
/** Compile every filter in `query` (group params excluded — see {@link compileGroups}). */
|
|
392
|
+
function compileUrlFilters(rt, query) {
|
|
393
|
+
const { clauses, extensions } = readFilterClauses(query, grammarOptions(rt));
|
|
394
|
+
return conjoin([compileFilterToMongo(clausesToFilter(clauses)), ...extensions.map((clause) => compileExtension(rt, clause)).filter((part) => part !== void 0)]);
|
|
395
|
+
}
|
|
463
396
|
/**
|
|
464
|
-
*
|
|
465
|
-
*
|
|
466
|
-
* caller wrote `?filters[limit]=5` instead of `?limit=5` and the qs parser
|
|
467
|
-
* nested the key under a filter object — the value is silently dropped
|
|
468
|
-
* without this guard. Kept as a Set for O(1) lookup; values mirror the
|
|
469
|
-
* reserved-key list at the top of `parseFilters`.
|
|
397
|
+
* `or` / `$or` and `$and` groups. Each branch is a full filter through the same grammar. An empty
|
|
398
|
+
* branch is refused — inside an OR it would match every document — and groups do not nest.
|
|
470
399
|
*/
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
"
|
|
475
|
-
|
|
476
|
-
]);
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
return op.startsWith("$") ? op : `$${op}`;
|
|
400
|
+
function compileGroups(rt, query) {
|
|
401
|
+
const out = {};
|
|
402
|
+
const orRaw = query?.or ?? query?.OR ?? query?.$or;
|
|
403
|
+
const orBranches = orRaw === void 0 ? [] : compileBranches(rt, "or", orRaw);
|
|
404
|
+
if (orBranches.length > 0) out.$or = orBranches;
|
|
405
|
+
const andBranches = query?.$and === void 0 ? [] : compileBranches(rt, "$and", query.$and);
|
|
406
|
+
if (andBranches.length > 0) out.$and = andBranches;
|
|
407
|
+
return out;
|
|
480
408
|
}
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
if (
|
|
484
|
-
rt.reject(
|
|
485
|
-
|
|
486
|
-
maxFilterDepth: rt.options.maxFilterDepth
|
|
487
|
-
});
|
|
488
|
-
return {};
|
|
409
|
+
function compileBranches(rt, param, raw) {
|
|
410
|
+
const items = Array.isArray(raw) ? raw : isRecord(raw) ? Object.values(raw) : void 0;
|
|
411
|
+
if (!items) {
|
|
412
|
+
rt.reject(`${param} takes branches: ${param}[0][field]=value`, { param });
|
|
413
|
+
return [];
|
|
489
414
|
}
|
|
490
|
-
const
|
|
491
|
-
const
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
rt.reject(`Blocked dangerous operator: ${key}`, { key });
|
|
415
|
+
const branches = [];
|
|
416
|
+
for (const item of items) {
|
|
417
|
+
if (!isRecord(item)) {
|
|
418
|
+
rt.reject(`each ${param} branch is a set of filters`, { param });
|
|
495
419
|
continue;
|
|
496
420
|
}
|
|
497
|
-
if (
|
|
498
|
-
|
|
499
|
-
"includeDeleted",
|
|
500
|
-
"lookup",
|
|
501
|
-
"aggregate",
|
|
502
|
-
"or",
|
|
503
|
-
"OR",
|
|
504
|
-
"$or"
|
|
505
|
-
].includes(key)) continue;
|
|
506
|
-
const operatorMatch = key.match(/^(.+)\[(.+)\]$/);
|
|
507
|
-
const baseField = operatorMatch ? operatorMatch[1] : key;
|
|
508
|
-
if (rt.options.allowedFilterFields && !rt.options.allowedFilterFields.includes(baseField)) {
|
|
509
|
-
rt.reject(`Blocked filter field not in allowlist: ${baseField}`, { field: baseField });
|
|
421
|
+
if (GROUP_PARAMS.some((group) => group in item)) {
|
|
422
|
+
rt.reject(`${param} groups do not nest`, { param });
|
|
510
423
|
continue;
|
|
511
424
|
}
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
425
|
+
const branch = compileUrlFilters(rt, item);
|
|
426
|
+
if (Object.keys(branch).length === 0) {
|
|
427
|
+
rt.reject(`an empty ${param} branch would match every document`, { param });
|
|
515
428
|
continue;
|
|
516
429
|
}
|
|
517
|
-
|
|
518
|
-
const [, , operator] = operatorMatch;
|
|
519
|
-
if (rt.dangerousOperators.includes(`$${operator}`)) {
|
|
520
|
-
rt.reject(`Blocked dangerous operator: ${operator}`, { operator });
|
|
521
|
-
continue;
|
|
522
|
-
}
|
|
523
|
-
handleOperatorSyntax(rt, parsedFilters, regexFields, operatorMatch, value);
|
|
524
|
-
continue;
|
|
525
|
-
}
|
|
526
|
-
if (typeof value === "object" && value !== null && !Array.isArray(value)) handleBracketSyntax(rt, key, value, parsedFilters, depth + 1);
|
|
527
|
-
else parsedFilters[key] = coerceFieldValue(key, value, rt.fieldTypes);
|
|
430
|
+
branches.push(branch);
|
|
528
431
|
}
|
|
529
|
-
return
|
|
432
|
+
return branches;
|
|
530
433
|
}
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
}
|
|
542
|
-
if (operator.toLowerCase() === "options" && regexFields[field]) {
|
|
543
|
-
const fieldValue = filters[field];
|
|
544
|
-
if (typeof fieldValue === "object" && fieldValue !== null && "$regex" in fieldValue) {
|
|
545
|
-
if (typeof value === "string" && /^[imsx]+$/.test(value)) fieldValue.$options = value;
|
|
546
|
-
else rt.reject(`Blocked invalid regex $options value: ${String(value)}. Allowed flags: i, m, s, x`, {
|
|
547
|
-
field,
|
|
548
|
-
value: String(value)
|
|
549
|
-
});
|
|
550
|
-
}
|
|
551
|
-
return;
|
|
552
|
-
}
|
|
553
|
-
if (operator.toLowerCase() === "between") {
|
|
554
|
-
if (typeof filters[field] !== "object" || filters[field] === null || Array.isArray(filters[field])) filters[field] = {};
|
|
555
|
-
filters[field].between = value;
|
|
556
|
-
return;
|
|
557
|
-
}
|
|
558
|
-
if (operator.toLowerCase() === "contains" || operator.toLowerCase() === "like") {
|
|
559
|
-
const safeRegex = createSafeRegex(rt, value, "i", "escape");
|
|
560
|
-
if (safeRegex) {
|
|
561
|
-
filters[field] = { $regex: safeRegex };
|
|
562
|
-
regexFields[field] = true;
|
|
563
|
-
}
|
|
564
|
-
return;
|
|
565
|
-
}
|
|
566
|
-
if (isGeoOperator(operator)) {
|
|
567
|
-
const geoFilter = parseGeoFilter(operator, value);
|
|
568
|
-
if (geoFilter) filters[field] = geoFilter;
|
|
569
|
-
else rt.reject(`Invalid geo operator value for ${field}[${operator}]; dropping filter`, {
|
|
570
|
-
field,
|
|
571
|
-
operator
|
|
572
|
-
});
|
|
573
|
-
return;
|
|
574
|
-
}
|
|
575
|
-
const mongoOperator = toMongoOperator(operator);
|
|
576
|
-
if (rt.dangerousOperators.includes(mongoOperator)) {
|
|
577
|
-
rt.reject(`Blocked dangerous operator: ${mongoOperator}`, { operator: mongoOperator });
|
|
578
|
-
return;
|
|
579
|
-
}
|
|
580
|
-
if (mongoOperator === "$eq") filters[field] = coerceFieldValue(field, value, rt.fieldTypes);
|
|
581
|
-
else if (mongoOperator === "$regex") {
|
|
582
|
-
const safeRegex = createSafeRegex(rt, value);
|
|
583
|
-
if (safeRegex) {
|
|
584
|
-
filters[field] = { $regex: safeRegex };
|
|
585
|
-
regexFields[field] = true;
|
|
586
|
-
}
|
|
587
|
-
} else {
|
|
588
|
-
let processedValue;
|
|
589
|
-
const op = operator.toLowerCase();
|
|
590
|
-
if (op === "size") {
|
|
591
|
-
processedValue = parseFloat(String(value));
|
|
592
|
-
if (Number.isNaN(processedValue)) {
|
|
593
|
-
rt.reject(`Non-numeric value for ${field}[${op}]: ${String(value)}`, {
|
|
594
|
-
field,
|
|
595
|
-
op
|
|
596
|
-
});
|
|
597
|
-
return;
|
|
598
|
-
}
|
|
599
|
-
} else if ([
|
|
600
|
-
"gt",
|
|
601
|
-
"gte",
|
|
602
|
-
"lt",
|
|
603
|
-
"lte"
|
|
604
|
-
].includes(op)) {
|
|
605
|
-
if (rt.fieldTypes.has(field)) {
|
|
606
|
-
processedValue = coerceFieldValue(field, value, rt.fieldTypes);
|
|
607
|
-
if (typeof processedValue === "number" && Number.isNaN(processedValue)) {
|
|
608
|
-
rt.reject(`Invalid value for ${field}[${op}]: ${String(value)}`, {
|
|
609
|
-
field,
|
|
610
|
-
op
|
|
611
|
-
});
|
|
612
|
-
return;
|
|
613
|
-
}
|
|
614
|
-
} else {
|
|
615
|
-
processedValue = parseFloat(String(value));
|
|
616
|
-
if (Number.isNaN(processedValue)) {
|
|
617
|
-
rt.reject(`Non-numeric value for ${field}[${op}]: ${String(value)}`, {
|
|
618
|
-
field,
|
|
619
|
-
op
|
|
620
|
-
});
|
|
621
|
-
return;
|
|
622
|
-
}
|
|
623
|
-
}
|
|
624
|
-
} else if (op === "in" || op === "nin") processedValue = (Array.isArray(value) ? value : String(value).split(",").map((v) => v.trim())).map((elem) => coerceFieldValue(field, elem, rt.fieldTypes));
|
|
625
|
-
else processedValue = coerceFieldValue(field, value, rt.fieldTypes);
|
|
626
|
-
if (typeof filters[field] !== "object" || filters[field] === null || Array.isArray(filters[field])) filters[field] = {};
|
|
627
|
-
filters[field][mongoOperator] = processedValue;
|
|
628
|
-
}
|
|
434
|
+
function grammarOptions(rt) {
|
|
435
|
+
return {
|
|
436
|
+
allowedFilterFields: rt.options.allowedFilterFields,
|
|
437
|
+
allowedOperators: rt.options.allowedOperators,
|
|
438
|
+
fieldTypes: rt.grammarFieldTypes,
|
|
439
|
+
extensionOperators: MONGOKIT_EXTENSION_OPERATORS,
|
|
440
|
+
reservedParams: MONGOKIT_RESERVED_PARAMS,
|
|
441
|
+
maxTextLength: rt.options.maxRegexLength,
|
|
442
|
+
onInvalid: rt.options.invalidInput === "drop" ? (error) => rt.reject(error.message) : void 0
|
|
443
|
+
};
|
|
629
444
|
}
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
}
|
|
647
|
-
if (isGeoOperator(operator)) {
|
|
648
|
-
const geoFilter = parseGeoFilter(operator, value);
|
|
649
|
-
if (geoFilter) parsedFilters[field] = geoFilter;
|
|
650
|
-
else {
|
|
651
|
-
rt.reject(`Invalid geo operator value for ${field}[${operator}]; dropping filter`, {
|
|
652
|
-
field,
|
|
653
|
-
operator
|
|
654
|
-
});
|
|
655
|
-
delete parsedFilters[field];
|
|
445
|
+
function compileExtension(rt, clause) {
|
|
446
|
+
const param = `${clause.field}[${clause.op}]`;
|
|
447
|
+
try {
|
|
448
|
+
switch (clause.op) {
|
|
449
|
+
case "size":
|
|
450
|
+
if (!/^\d+$/.test(clause.raw)) throw new QueryGrammarError(param, "size takes a whole number");
|
|
451
|
+
return { [clause.field]: { $size: Number(clause.raw) } };
|
|
452
|
+
case "type": {
|
|
453
|
+
if (!BSON_TYPE_RE.test(clause.raw)) throw new QueryGrammarError(param, "not a BSON type");
|
|
454
|
+
const code = Number(clause.raw);
|
|
455
|
+
return { [clause.field]: { $type: Number.isInteger(code) ? code : clause.raw } };
|
|
456
|
+
}
|
|
457
|
+
default: {
|
|
458
|
+
const geo = parseGeoFilter(clause.op, clause.raw);
|
|
459
|
+
if (!geo) throw new QueryGrammarError(param, `invalid ${clause.op} coordinates`);
|
|
460
|
+
return { [clause.field]: geo };
|
|
656
461
|
}
|
|
657
|
-
continue;
|
|
658
|
-
}
|
|
659
|
-
if (rt.options.allowedOperators && !rt.options.allowedOperators.includes(operator)) {
|
|
660
|
-
rt.reject(`Operator not in allowlist: ${operator}`, {
|
|
661
|
-
field,
|
|
662
|
-
operator
|
|
663
|
-
});
|
|
664
|
-
continue;
|
|
665
|
-
}
|
|
666
|
-
if (rt.operators[operator]) {
|
|
667
|
-
const mongoOperator = rt.operators[operator];
|
|
668
|
-
let processedValue;
|
|
669
|
-
if (operator === "size") {
|
|
670
|
-
processedValue = parseFloat(String(value));
|
|
671
|
-
if (Number.isNaN(processedValue)) {
|
|
672
|
-
rt.reject(`Non-numeric value for ${field}[${operator}]: ${String(value)}`, {
|
|
673
|
-
field,
|
|
674
|
-
operator
|
|
675
|
-
});
|
|
676
|
-
continue;
|
|
677
|
-
}
|
|
678
|
-
} else if ([
|
|
679
|
-
"gt",
|
|
680
|
-
"gte",
|
|
681
|
-
"lt",
|
|
682
|
-
"lte"
|
|
683
|
-
].includes(operator)) {
|
|
684
|
-
if (rt.fieldTypes.has(field)) {
|
|
685
|
-
processedValue = coerceFieldValue(field, value, rt.fieldTypes);
|
|
686
|
-
if (typeof processedValue === "number" && Number.isNaN(processedValue)) {
|
|
687
|
-
rt.reject(`Invalid value for ${field}[${operator}]: ${String(value)}`, {
|
|
688
|
-
field,
|
|
689
|
-
operator
|
|
690
|
-
});
|
|
691
|
-
continue;
|
|
692
|
-
}
|
|
693
|
-
} else {
|
|
694
|
-
processedValue = parseFloat(String(value));
|
|
695
|
-
if (Number.isNaN(processedValue)) {
|
|
696
|
-
rt.reject(`Non-numeric value for ${field}[${operator}]: ${String(value)}`, {
|
|
697
|
-
field,
|
|
698
|
-
operator
|
|
699
|
-
});
|
|
700
|
-
continue;
|
|
701
|
-
}
|
|
702
|
-
}
|
|
703
|
-
} else if (operator === "in" || operator === "nin") processedValue = (Array.isArray(value) ? value : String(value).split(",").map((v) => v.trim())).map((elem) => coerceFieldValue(field, elem, rt.fieldTypes));
|
|
704
|
-
else if (operator === "like" || operator === "contains" || operator === "regex") {
|
|
705
|
-
const safeRegex = createSafeRegex(rt, value, "i", operator === "regex" ? "reject" : "escape");
|
|
706
|
-
if (!safeRegex) continue;
|
|
707
|
-
processedValue = safeRegex;
|
|
708
|
-
} else processedValue = coerceFieldValue(field, value, rt.fieldTypes);
|
|
709
|
-
parsedFilters[field][mongoOperator] = processedValue;
|
|
710
462
|
}
|
|
463
|
+
} catch (error) {
|
|
464
|
+
if (!(error instanceof QueryGrammarError) || rt.options.invalidInput !== "drop") throw error;
|
|
465
|
+
rt.reject(error.message);
|
|
466
|
+
return;
|
|
711
467
|
}
|
|
712
|
-
if (typeof parsedFilters[field] === "object" && Object.keys(parsedFilters[field]).length === 0) delete parsedFilters[field];
|
|
713
468
|
}
|
|
714
|
-
/**
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
*/
|
|
723
|
-
function parseCompoundBranches(rt, raw, depth) {
|
|
724
|
-
const items = Array.isArray(raw) ? raw : typeof raw === "object" && raw ? Object.values(raw) : [];
|
|
725
|
-
const branches = [];
|
|
726
|
-
for (const item of items) {
|
|
727
|
-
if (typeof item !== "object" || !item) continue;
|
|
728
|
-
const parsed = parseFilters(rt, item, depth + 1);
|
|
729
|
-
if (Object.keys(parsed).length > 0) branches.push(parsed);
|
|
469
|
+
/** AND compiled parts: one object when fields don't collide, `$and` when they do. */
|
|
470
|
+
function conjoin(parts) {
|
|
471
|
+
const nonEmpty = parts.filter((part) => Object.keys(part).length > 0);
|
|
472
|
+
if (nonEmpty.length <= 1) return { ...nonEmpty[0] ?? {} };
|
|
473
|
+
const merged = {};
|
|
474
|
+
for (const part of nonEmpty) {
|
|
475
|
+
for (const key of Object.keys(part)) if (key in merged) return { $and: nonEmpty };
|
|
476
|
+
Object.assign(merged, part);
|
|
730
477
|
}
|
|
731
|
-
return
|
|
478
|
+
return merged;
|
|
732
479
|
}
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
const
|
|
736
|
-
if (
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
const parsedBranch = parseFilters(rt, item, 1);
|
|
740
|
-
if (Object.keys(parsedBranch).length > 0) orArray.push(parsedBranch);
|
|
741
|
-
}
|
|
742
|
-
return orArray.length ? orArray : void 0;
|
|
480
|
+
/** mongokit's schema-derived field types, narrowed to the grammar's portable set. */
|
|
481
|
+
function toGrammarFieldTypes(types) {
|
|
482
|
+
const out = {};
|
|
483
|
+
for (const [path, type] of types) if (type === "string" || type === "objectid") out[path] = "string";
|
|
484
|
+
else if (type === "number" || type === "boolean" || type === "date") out[path] = type;
|
|
485
|
+
return out;
|
|
743
486
|
}
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
const output = { ...filters };
|
|
747
|
-
for (const [key, value] of Object.entries(filters || {})) if (value && typeof value === "object" && "between" in value) {
|
|
748
|
-
const between = value.between;
|
|
749
|
-
const [from, to] = String(between).split(",").map((s) => s.trim());
|
|
750
|
-
const fromDate = from ? new Date(from) : void 0;
|
|
751
|
-
const toDate = to ? new Date(to) : void 0;
|
|
752
|
-
const range = {};
|
|
753
|
-
if (fromDate && !Number.isNaN(fromDate.getTime())) range.$gte = fromDate;
|
|
754
|
-
if (toDate && !Number.isNaN(toDate.getTime())) range.$lte = toDate;
|
|
755
|
-
if (Object.keys(range).length === 0) {
|
|
756
|
-
rt.reject(`Invalid 'between' value for ${key}: ${String(between)}`, {
|
|
757
|
-
field: key,
|
|
758
|
-
value: String(between)
|
|
759
|
-
});
|
|
760
|
-
delete output[key];
|
|
761
|
-
} else output[key] = range;
|
|
762
|
-
}
|
|
763
|
-
return output;
|
|
487
|
+
function isRecord(value) {
|
|
488
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
764
489
|
}
|
|
765
490
|
//#endregion
|
|
766
491
|
//#region src/query/parser/lookup.ts
|
|
@@ -838,97 +563,70 @@ function parseSingleLookup(rt, collectionName, config) {
|
|
|
838
563
|
return null;
|
|
839
564
|
}
|
|
840
565
|
//#endregion
|
|
841
|
-
//#region src/query/parser/
|
|
566
|
+
//#region src/query/parser/runtime.ts
|
|
842
567
|
/**
|
|
843
|
-
*
|
|
844
|
-
*
|
|
845
|
-
*
|
|
568
|
+
* Parser runtime — the shared context every parser module receives.
|
|
569
|
+
*
|
|
570
|
+
* The `QueryParser` facade resolves options once at construction and builds a
|
|
571
|
+
* single `ParserRuntime`; the extracted modules (filter compiler, sanitizers,
|
|
572
|
+
* populate/lookup/sort parsers, …) are pure functions over it. This keeps the
|
|
573
|
+
* modules independently testable and the facade thin, without threading six
|
|
574
|
+
* separate arguments through every call.
|
|
846
575
|
*/
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
}
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
576
|
+
/** Always-blocked MongoDB operators (extended via `additionalDangerousOperators`). */
|
|
577
|
+
const BASE_DANGEROUS_OPERATORS = [
|
|
578
|
+
"$where",
|
|
579
|
+
"$function",
|
|
580
|
+
"$accumulator",
|
|
581
|
+
"$expr"
|
|
582
|
+
];
|
|
583
|
+
function createReject(mode) {
|
|
584
|
+
return (message, meta) => {
|
|
585
|
+
if (mode === "throw") throw createError(400, `[mongokit] ${message}`, {
|
|
586
|
+
code: "INVALID_QUERY_INPUT",
|
|
587
|
+
meta
|
|
588
|
+
});
|
|
589
|
+
warn(`[mongokit] ${message}`);
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
/**
|
|
593
|
+
* Run a grammar reader under the `invalidInput` policy: `'throw'` lets its 400 through untouched;
|
|
594
|
+
* `'drop'` warns and returns `fallback`.
|
|
595
|
+
*/
|
|
596
|
+
function guarded(rt, read, fallback) {
|
|
597
|
+
try {
|
|
598
|
+
return read();
|
|
599
|
+
} catch (error) {
|
|
600
|
+
if (!isQueryGrammarError(error) || rt.options.invalidInput !== "drop") throw error;
|
|
601
|
+
rt.reject(error.message);
|
|
602
|
+
return fallback;
|
|
862
603
|
}
|
|
863
|
-
return n;
|
|
864
604
|
}
|
|
865
605
|
//#endregion
|
|
866
606
|
//#region src/query/parser/sort-select.ts
|
|
867
607
|
/**
|
|
868
|
-
*
|
|
869
|
-
*
|
|
870
|
-
* Exported so `parse()` can ask whether it is PERMITTED before applying it,
|
|
871
|
-
* rather than substituting it and then validating it like caller input.
|
|
608
|
+
* `?sort=-createdAt,name` and `?select=name,-password` — read through the shared grammar
|
|
609
|
+
* (`readSort` / `readSelect`), so an invalid or disallowed field is refused like any filter.
|
|
872
610
|
*/
|
|
873
|
-
const DEFAULT_PARSER_SORT = "-createdAt";
|
|
874
611
|
/**
|
|
875
|
-
*
|
|
876
|
-
*
|
|
877
|
-
* ask about a default the caller did not send.
|
|
612
|
+
* The sort applied when a request names none. Exported so `parse()` can ask whether it is
|
|
613
|
+
* PERMITTED before applying it, rather than validating it like caller input.
|
|
878
614
|
*/
|
|
615
|
+
const DEFAULT_PARSER_SORT = "-createdAt";
|
|
616
|
+
/** Is every field of `sort` inside `allowedSortFields`? Pure — never rejects. */
|
|
879
617
|
function isSortFieldAllowed(rt, sort) {
|
|
880
618
|
const allowed = rt.options.allowedSortFields;
|
|
881
619
|
if (!allowed || allowed.length === 0) return true;
|
|
882
|
-
return sort.split(",").map((s) => s.trim()).filter(Boolean).every((field) => allowed.includes(field.
|
|
620
|
+
return sort.split(",").map((s) => s.trim()).filter(Boolean).every((field) => allowed.includes(field.replace(/^[+-]/, "")));
|
|
883
621
|
}
|
|
884
|
-
/** Parse a sort spec (string or object form), honoring `allowedSortFields`. */
|
|
885
622
|
function parseSort(rt, sort) {
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
if (rt.options.allowedSortFields && !rt.options.allowedSortFields.includes(key)) {
|
|
891
|
-
rt.reject(`Blocked sort field not in allowlist: ${key}`, { field: key });
|
|
892
|
-
continue;
|
|
893
|
-
}
|
|
894
|
-
const strVal = String(value).toLowerCase();
|
|
895
|
-
sortObj[key] = strVal === "desc" || strVal === "-1" || value === -1 ? -1 : 1;
|
|
896
|
-
}
|
|
897
|
-
return Object.keys(sortObj).length > 0 ? sortObj : void 0;
|
|
898
|
-
}
|
|
899
|
-
const sortObj = {};
|
|
900
|
-
const fields = sort.split(",").map((s) => s.trim());
|
|
901
|
-
for (const field of fields) {
|
|
902
|
-
if (!field) continue;
|
|
903
|
-
const cleanField = field.startsWith("-") ? field.substring(1) : field;
|
|
904
|
-
if (rt.options.allowedSortFields && !rt.options.allowedSortFields.includes(cleanField)) {
|
|
905
|
-
rt.reject(`Blocked sort field not in allowlist: ${cleanField}`, { field: cleanField });
|
|
906
|
-
continue;
|
|
907
|
-
}
|
|
908
|
-
if (field.startsWith("-")) sortObj[field.substring(1)] = -1;
|
|
909
|
-
else sortObj[field] = 1;
|
|
910
|
-
}
|
|
911
|
-
return Object.keys(sortObj).length > 0 ? sortObj : void 0;
|
|
623
|
+
return readSort(sort, {
|
|
624
|
+
allowedSortFields: rt.options.allowedSortFields,
|
|
625
|
+
onInvalid: rt.options.invalidInput === "drop" ? (error) => rt.reject(error.message) : void 0
|
|
626
|
+
});
|
|
912
627
|
}
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
*
|
|
916
|
-
* @example
|
|
917
|
-
* ```typescript
|
|
918
|
-
* // URL: ?select=name,email,-password
|
|
919
|
-
* // Returns: { name: 1, email: 1, password: 0 }
|
|
920
|
-
* ```
|
|
921
|
-
*/
|
|
922
|
-
function parseSelect(select) {
|
|
923
|
-
if (!select) return void 0;
|
|
924
|
-
if (typeof select === "string") {
|
|
925
|
-
const projection = {};
|
|
926
|
-
const fields = select.split(",").map((f) => f.trim());
|
|
927
|
-
for (const field of fields) if (field.startsWith("-")) projection[field.substring(1)] = 0;
|
|
928
|
-
else projection[field] = 1;
|
|
929
|
-
return projection;
|
|
930
|
-
}
|
|
931
|
-
if (typeof select === "object" && select !== null) return select;
|
|
628
|
+
function parseSelect(rt, select) {
|
|
629
|
+
return guarded(rt, () => readSelect(select), void 0);
|
|
932
630
|
}
|
|
933
631
|
//#endregion
|
|
934
632
|
//#region src/query/parser/populate.ts
|
|
@@ -992,7 +690,10 @@ function parseSinglePopulate(rt, path, config, depth = 0) {
|
|
|
992
690
|
const opts = config;
|
|
993
691
|
const option = { path };
|
|
994
692
|
if (opts.select && typeof opts.select === "string") option.select = opts.select.split(",").map((s) => s.trim()).join(" ");
|
|
995
|
-
if (opts.match && typeof opts.match === "object")
|
|
693
|
+
if (opts.match && typeof opts.match === "object") {
|
|
694
|
+
const match = convertPopulateMatch(rt, path, opts.match);
|
|
695
|
+
if (match) option.match = match;
|
|
696
|
+
}
|
|
996
697
|
if (opts.limit !== void 0) {
|
|
997
698
|
const limit = parseInt(String(opts.limit), 10);
|
|
998
699
|
if (!Number.isNaN(limit) && limit > 0) {
|
|
@@ -1027,112 +728,56 @@ function parseSinglePopulate(rt, path, config, depth = 0) {
|
|
|
1027
728
|
}
|
|
1028
729
|
return null;
|
|
1029
730
|
}
|
|
1030
|
-
/**
|
|
1031
|
-
|
|
731
|
+
/**
|
|
732
|
+
* A populate `match` is plain field EQUALITY: a `$` key (`$where`) or a nested object — the same
|
|
733
|
+
* operator-smuggling route — is refused through the invalid-input policy, never passed to mongoose.
|
|
734
|
+
*/
|
|
735
|
+
function convertPopulateMatch(rt, path, match) {
|
|
1032
736
|
const converted = {};
|
|
1033
|
-
for (const [key, value] of Object.entries(match))
|
|
737
|
+
for (const [key, value] of Object.entries(match)) {
|
|
738
|
+
if (!/^[a-zA-Z_][a-zA-Z0-9_.]*$/.test(key) || value !== null && typeof value === "object") {
|
|
739
|
+
rt.reject(`Populate match on "${path}" accepts field equality only: "${key}"`, {
|
|
740
|
+
path,
|
|
741
|
+
key
|
|
742
|
+
});
|
|
743
|
+
return null;
|
|
744
|
+
}
|
|
745
|
+
converted[key] = coerceHeuristic(value);
|
|
746
|
+
}
|
|
1034
747
|
return converted;
|
|
1035
748
|
}
|
|
1036
749
|
//#endregion
|
|
1037
|
-
//#region src/query/parser/
|
|
750
|
+
//#region src/query/parser/schema-docs.ts
|
|
1038
751
|
/**
|
|
1039
|
-
*
|
|
1040
|
-
*
|
|
1041
|
-
*
|
|
1042
|
-
* single `ParserRuntime`; the extracted modules (filter compiler, sanitizers,
|
|
1043
|
-
* populate/lookup/sort parsers, …) are pure functions over it. This keeps the
|
|
1044
|
-
* modules independently testable and the facade thin, without threading six
|
|
1045
|
-
* separate arguments through every call.
|
|
752
|
+
* OpenAPI query-schema generation — Arc's `defineResource()` auto-detects
|
|
753
|
+
* `getQuerySchema()` on the parser and uses it to document list-endpoint
|
|
754
|
+
* query parameters in OpenAPI/Swagger.
|
|
1046
755
|
*/
|
|
1047
|
-
/**
|
|
1048
|
-
const
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
in: "$in",
|
|
1056
|
-
nin: "$nin",
|
|
1057
|
-
like: "$regex",
|
|
1058
|
-
contains: "$regex",
|
|
1059
|
-
regex: "$regex",
|
|
1060
|
-
exists: "$exists",
|
|
1061
|
-
size: "$size",
|
|
1062
|
-
type: "$type"
|
|
756
|
+
/** mongokit's own operators, described alongside the shared grammar's. */
|
|
757
|
+
const EXTENSION_DESCRIPTIONS = {
|
|
758
|
+
size: "Array has exactly N elements",
|
|
759
|
+
type: "Field is of a BSON type (name or number)",
|
|
760
|
+
near: "Nearest to lng,lat[,maxDistanceMeters]",
|
|
761
|
+
nearSphere: "Nearest on a sphere to lng,lat[,maxDistanceMeters]",
|
|
762
|
+
geoWithin: "Inside the box minLng,minLat,maxLng,maxLat",
|
|
763
|
+
withinRadius: "Within lng,lat,radiusMeters"
|
|
1063
764
|
};
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
"$function",
|
|
1068
|
-
"$accumulator",
|
|
1069
|
-
"$expr"
|
|
1070
|
-
];
|
|
1071
|
-
function createReject(mode) {
|
|
1072
|
-
return (message, meta) => {
|
|
1073
|
-
if (mode === "throw") throw createError(400, `[mongokit] ${message}`, {
|
|
1074
|
-
code: "INVALID_QUERY_INPUT",
|
|
1075
|
-
meta
|
|
1076
|
-
});
|
|
1077
|
-
warn(`[mongokit] ${message}`);
|
|
1078
|
-
};
|
|
765
|
+
function describeOperator(op) {
|
|
766
|
+
if (op in OPERATOR_DESCRIPTIONS) return OPERATOR_DESCRIPTIONS[op];
|
|
767
|
+
return EXTENSION_DESCRIPTIONS[op] ?? op;
|
|
1079
768
|
}
|
|
1080
|
-
//#endregion
|
|
1081
|
-
//#region src/query/parser/schema-docs.ts
|
|
1082
769
|
function availableOperators(rt) {
|
|
1083
|
-
|
|
770
|
+
const allowed = rt.options.allowedOperators;
|
|
771
|
+
return allowed ? rt.urlOperators.filter((op) => allowed.includes(op)) : rt.urlOperators;
|
|
1084
772
|
}
|
|
1085
|
-
/**
|
|
773
|
+
/** JSON Schema type of an operator's value. */
|
|
1086
774
|
function operatorSchemaType(op) {
|
|
1087
|
-
if (
|
|
1088
|
-
|
|
1089
|
-
"gte",
|
|
1090
|
-
"lt",
|
|
1091
|
-
"lte",
|
|
1092
|
-
"size"
|
|
1093
|
-
].includes(op)) return "number";
|
|
1094
|
-
if (["exists"].includes(op)) return "boolean";
|
|
775
|
+
if (op === "size") return "integer";
|
|
776
|
+
if (op === "exists") return "boolean";
|
|
1095
777
|
return "string";
|
|
1096
778
|
}
|
|
1097
|
-
/** Get a human-readable description for a filter operator. */
|
|
1098
|
-
function operatorDescription(op, field, mongoOp) {
|
|
1099
|
-
return {
|
|
1100
|
-
ne: `${field} not equal to value (${mongoOp})`,
|
|
1101
|
-
gt: `${field} greater than value (${mongoOp})`,
|
|
1102
|
-
gte: `${field} greater than or equal to value (${mongoOp})`,
|
|
1103
|
-
lt: `${field} less than value (${mongoOp})`,
|
|
1104
|
-
lte: `${field} less than or equal to value (${mongoOp})`,
|
|
1105
|
-
in: `${field} in comma-separated list (${mongoOp}). Example: value1,value2`,
|
|
1106
|
-
nin: `${field} not in comma-separated list (${mongoOp})`,
|
|
1107
|
-
like: `${field} matches pattern (case-insensitive regex)`,
|
|
1108
|
-
contains: `${field} contains substring (case-insensitive regex)`,
|
|
1109
|
-
regex: `${field} matches regex pattern (${mongoOp})`,
|
|
1110
|
-
exists: `Field ${field} exists (true/false)`,
|
|
1111
|
-
size: `Array field ${field} has exactly N elements (${mongoOp})`,
|
|
1112
|
-
type: `Field ${field} is of BSON type (${mongoOp})`
|
|
1113
|
-
}[op] || `Filter ${field} with ${mongoOp}`;
|
|
1114
|
-
}
|
|
1115
|
-
/** Build a summary description of all available filter operators. */
|
|
1116
779
|
function buildOperatorSummary(operators) {
|
|
1117
|
-
|
|
1118
|
-
const desc = {
|
|
1119
|
-
eq: "Equal (default when no operator specified)",
|
|
1120
|
-
ne: "Not equal",
|
|
1121
|
-
gt: "Greater than",
|
|
1122
|
-
gte: "Greater than or equal",
|
|
1123
|
-
lt: "Less than",
|
|
1124
|
-
lte: "Less than or equal",
|
|
1125
|
-
in: "In list (comma-separated values)",
|
|
1126
|
-
nin: "Not in list",
|
|
1127
|
-
like: "Pattern match (case-insensitive)",
|
|
1128
|
-
contains: "Contains substring (case-insensitive)",
|
|
1129
|
-
regex: "Regex pattern",
|
|
1130
|
-
exists: "Field exists (true/false)",
|
|
1131
|
-
size: "Array size equals",
|
|
1132
|
-
type: "BSON type check"
|
|
1133
|
-
};
|
|
1134
|
-
for (const [op, mongoOp] of operators) lines.push(` ${op} → ${mongoOp}: ${desc[op] || op}`);
|
|
1135
|
-
return lines.join("\n");
|
|
780
|
+
return ["Available filter operators (use as field[operator]=value):", ...operators.map((op) => ` ${op}: ${describeOperator(op)}`)].join("\n");
|
|
1136
781
|
}
|
|
1137
782
|
/**
|
|
1138
783
|
* Generate OpenAPI-compatible JSON Schema for query parameters.
|
|
@@ -1216,11 +861,11 @@ function buildQuerySchema(rt) {
|
|
|
1216
861
|
type: "string",
|
|
1217
862
|
description: `Filter by ${field} (exact match)`
|
|
1218
863
|
};
|
|
1219
|
-
for (const
|
|
864
|
+
for (const op of operators) {
|
|
1220
865
|
if (op === "eq") continue;
|
|
1221
866
|
properties[`${field}[${op}]`] = {
|
|
1222
867
|
type: operatorSchemaType(op),
|
|
1223
|
-
description:
|
|
868
|
+
description: `${field}: ${describeOperator(op)}`
|
|
1224
869
|
};
|
|
1225
870
|
}
|
|
1226
871
|
}
|
|
@@ -1246,39 +891,17 @@ function buildOpenAPIQuerySchema(rt) {
|
|
|
1246
891
|
//#endregion
|
|
1247
892
|
//#region src/query/parser/search.ts
|
|
1248
893
|
/**
|
|
1249
|
-
*
|
|
1250
|
-
*
|
|
1251
|
-
* patterns: special characters are escaped, not rejected.
|
|
894
|
+
* `?search=` — length-checked through the shared grammar. Regex mode matches the term as LITERAL
|
|
895
|
+
* text across `searchFields` (`c++` and `a.b` mean exactly those characters).
|
|
1252
896
|
*/
|
|
1253
|
-
/** Trim + length-cap the raw search term (reject over-long input per policy). */
|
|
1254
897
|
function sanitizeSearch(rt, search) {
|
|
1255
|
-
|
|
1256
|
-
let searchStr = String(search).trim();
|
|
1257
|
-
if (!searchStr) return void 0;
|
|
1258
|
-
if (searchStr.length > rt.options.maxSearchLength) {
|
|
1259
|
-
rt.reject(`Search query too long, truncating`, {
|
|
1260
|
-
length: searchStr.length,
|
|
1261
|
-
maxSearchLength: rt.options.maxSearchLength
|
|
1262
|
-
});
|
|
1263
|
-
searchStr = searchStr.substring(0, rt.options.maxSearchLength);
|
|
1264
|
-
}
|
|
1265
|
-
return searchStr;
|
|
898
|
+
return guarded(rt, () => readSearch(search, { maxSearchLength: rt.options.maxSearchLength }), void 0);
|
|
1266
899
|
}
|
|
1267
|
-
/**
|
|
1268
|
-
* Build regex-based multi-field search filters: an $or with case-insensitive
|
|
1269
|
-
* regex across all searchFields.
|
|
1270
|
-
*
|
|
1271
|
-
* @example
|
|
1272
|
-
* // searchFields: ['name', 'description', 'sku'], search: 'azure'
|
|
1273
|
-
* // Returns: [{ name: {$regex:/azure/i} }, { description: {...} }, { sku: {...} }]
|
|
1274
|
-
*/
|
|
1275
900
|
function buildRegexSearch(rt, searchTerm) {
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
for (const field of rt.options.searchFields) orConditions.push({ [field]: { $regex: safeRegex } });
|
|
1281
|
-
return orConditions.length > 0 ? orConditions : null;
|
|
901
|
+
const fields = rt.options.searchFields;
|
|
902
|
+
if (!fields || fields.length === 0) return null;
|
|
903
|
+
const pattern = new RegExp(escapeRegex(searchTerm), "i");
|
|
904
|
+
return fields.map((field) => ({ [field]: { $regex: pattern } }));
|
|
1282
905
|
}
|
|
1283
906
|
//#endregion
|
|
1284
907
|
//#region src/query/QueryParser.ts
|
|
@@ -1293,8 +916,7 @@ function buildRegexSearch(rt, searchTerm) {
|
|
|
1293
916
|
* orchestration, and the OpenAPI schema surface. The implementation lives in
|
|
1294
917
|
* focused modules under `./parser/`:
|
|
1295
918
|
*
|
|
1296
|
-
* - `parser/filter-compiler` — filters
|
|
1297
|
-
* - `parser/regex-safety` — ReDoS protection (escape/reject)
|
|
919
|
+
* - `parser/filter-compiler` — filters (repo-core's shared grammar → Filter IR → Mongo), groups
|
|
1298
920
|
* - `parser/pipeline-sanitizer` — $match / $lookup-pipeline / expression sanitizing
|
|
1299
921
|
* - `parser/lookup` — ?lookup[...] parsing + collection allowlist
|
|
1300
922
|
* - `parser/aggregation` — ?aggregate[...] parsing (opt-in)
|
|
@@ -1319,9 +941,9 @@ function buildRegexSearch(rt, searchTerm) {
|
|
|
1319
941
|
* being silently dropped (which broadens the result set). Opt into
|
|
1320
942
|
* `'drop'` only for trusted compat tooling.
|
|
1321
943
|
* - Dangerous operators blocked everywhere ($where, $function, $accumulator, $expr).
|
|
1322
|
-
* -
|
|
1323
|
-
*
|
|
1324
|
-
* -
|
|
944
|
+
* - Filters, paging, sort and select follow repo-core's shared query grammar — the same
|
|
945
|
+
* contract as arc's parser and `parseUrl` (`runQueryGrammarConformance` holds all three).
|
|
946
|
+
* - Text operators and search are literal text; only `regex` is a pattern, checked for ReDoS.
|
|
1325
947
|
* - Lookup pipelines sanitized; `enableAggregations` is opt-in — keep it off
|
|
1326
948
|
* for public endpoints or pair with per-route allowlists.
|
|
1327
949
|
*
|
|
@@ -1366,11 +988,13 @@ var QueryParser = class {
|
|
|
1366
988
|
warn("[mongokit] searchMode \"regex\" requires searchFields to be specified. Falling back to \"text\" mode.");
|
|
1367
989
|
resolved.searchMode = "text";
|
|
1368
990
|
}
|
|
991
|
+
const fieldTypes = buildFieldTypeMap(options.schema, options.fieldTypes);
|
|
1369
992
|
this.rt = {
|
|
1370
993
|
options: resolved,
|
|
1371
|
-
|
|
994
|
+
urlOperators: [...URL_OPERATORS, ...MONGOKIT_EXTENSION_OPERATORS],
|
|
1372
995
|
dangerousOperators: [...BASE_DANGEROUS_OPERATORS, ...resolved.additionalDangerousOperators],
|
|
1373
|
-
fieldTypes
|
|
996
|
+
fieldTypes,
|
|
997
|
+
grammarFieldTypes: toGrammarFieldTypes(fieldTypes),
|
|
1374
998
|
reject: createReject(resolved.invalidInput)
|
|
1375
999
|
};
|
|
1376
1000
|
this._schemaIndexes = extractSchemaIndexes(options.schema);
|
|
@@ -1434,67 +1058,41 @@ var QueryParser = class {
|
|
|
1434
1058
|
*/
|
|
1435
1059
|
parse(query) {
|
|
1436
1060
|
const rt = this.rt;
|
|
1437
|
-
const
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
*
|
|
1446
|
-
* `sort = '-createdAt'` was a DESTRUCTURING default, so an absent `?sort=`
|
|
1447
|
-
* became `-createdAt` and was then validated exactly like caller input. With
|
|
1448
|
-
* `allowedSortFields` set and `createdAt` not among them, the parser
|
|
1449
|
-
* REJECTED ITS OWN DEFAULT — and since 3.25 rejection throws, so `parse({})`
|
|
1450
|
-
* threw on an empty query. Every list call on such a resource answered 400
|
|
1451
|
-
* (`Blocked sort field not in allowlist: createdAt`), REST and MCP alike,
|
|
1452
|
-
* and no request the caller could make would avoid it.
|
|
1453
|
-
*
|
|
1454
|
-
* A default the resource forbade must simply not be applied: it is not a
|
|
1455
|
-
* caller error, and there is nobody to report it to. A sort the caller DID
|
|
1456
|
-
* send still rejects — that is the fail-closed behaviour 3.25 added, and it
|
|
1457
|
-
* is deliberately unchanged.
|
|
1458
|
-
*/
|
|
1061
|
+
const q = query ?? {};
|
|
1062
|
+
const { page, limit, sort, populate, search, after, cursor, select, lookup, aggregate } = q;
|
|
1063
|
+
const pageRequest = guarded(rt, () => readPageRequest({
|
|
1064
|
+
page,
|
|
1065
|
+
limit,
|
|
1066
|
+
after,
|
|
1067
|
+
cursor
|
|
1068
|
+
}, { maxLimit: rt.options.maxLimit }), readPageRequest({}, { maxLimit: rt.options.maxLimit }));
|
|
1459
1069
|
const effectiveSort = sort === void 0 && isSortFieldAllowed(rt, "-createdAt") ? DEFAULT_PARSER_SORT : sort;
|
|
1460
1070
|
const sanitizedSearch = sanitizeSearch(rt, search);
|
|
1461
1071
|
const { simplePopulate, populateOptions } = parsePopulate(rt, populate);
|
|
1462
1072
|
const parsed = {
|
|
1463
|
-
filters:
|
|
1464
|
-
limit:
|
|
1073
|
+
filters: compileUrlFilters(rt, q),
|
|
1074
|
+
limit: pageRequest.limit,
|
|
1465
1075
|
sort: parseSort(rt, effectiveSort),
|
|
1466
1076
|
populate: simplePopulate,
|
|
1467
1077
|
populateOptions,
|
|
1468
1078
|
search: sanitizedSearch
|
|
1469
1079
|
};
|
|
1470
1080
|
if (sanitizedSearch && rt.options.searchMode === "regex" && rt.options.searchFields) {
|
|
1471
|
-
const
|
|
1472
|
-
if (
|
|
1473
|
-
|
|
1474
|
-
parsed.filters = {
|
|
1475
|
-
...parsed.filters,
|
|
1476
|
-
$and: [{ $or: parsed.filters.$or }, { $or: regexSearchFilters }]
|
|
1477
|
-
};
|
|
1478
|
-
delete parsed.filters.$or;
|
|
1479
|
-
} else parsed.filters.$or = regexSearchFilters;
|
|
1081
|
+
const regexSearch = buildRegexSearch(rt, sanitizedSearch);
|
|
1082
|
+
if (regexSearch) {
|
|
1083
|
+
parsed.filters = attachCondition(parsed.filters, { $or: regexSearch });
|
|
1480
1084
|
parsed.search = void 0;
|
|
1481
1085
|
}
|
|
1482
1086
|
}
|
|
1483
|
-
|
|
1087
|
+
const groups = compileGroups(rt, q);
|
|
1088
|
+
if (groups.$or) parsed.filters = attachCondition(parsed.filters, { $or: groups.$or });
|
|
1089
|
+
if (groups.$and) parsed.filters = attachCondition(parsed.filters, { $and: groups.$and });
|
|
1090
|
+
const projection = parseSelect(rt, select);
|
|
1091
|
+
if (projection) parsed.select = projection;
|
|
1484
1092
|
if (rt.options.enableLookups && lookup) parsed.lookups = parseLookups(rt, lookup);
|
|
1485
1093
|
if (rt.options.enableAggregations && aggregate) parsed.aggregation = parseAggregation(rt, aggregate);
|
|
1486
|
-
if (after
|
|
1487
|
-
|
|
1488
|
-
if (parsedPage !== void 0) parsed.page = parsedPage;
|
|
1489
|
-
const orGroup = parseOr(rt, query);
|
|
1490
|
-
if (orGroup) {
|
|
1491
|
-
if (parsed.filters.$or) {
|
|
1492
|
-
const existingOr = parsed.filters.$or;
|
|
1493
|
-
delete parsed.filters.$or;
|
|
1494
|
-
parsed.filters.$and = [{ $or: existingOr }, { $or: orGroup }];
|
|
1495
|
-
} else parsed.filters.$or = orGroup;
|
|
1496
|
-
}
|
|
1497
|
-
parsed.filters = enhanceWithBetween(rt, parsed.filters);
|
|
1094
|
+
if (pageRequest.after !== void 0) parsed.after = pageRequest.after;
|
|
1095
|
+
if (pageRequest.page !== void 0) parsed.page = pageRequest.page;
|
|
1498
1096
|
return parsed;
|
|
1499
1097
|
}
|
|
1500
1098
|
/**
|
|
@@ -1515,6 +1113,29 @@ var QueryParser = class {
|
|
|
1515
1113
|
return buildOpenAPIQuerySchema(this.rt);
|
|
1516
1114
|
}
|
|
1517
1115
|
};
|
|
1116
|
+
/**
|
|
1117
|
+
* AND a top-level condition (`{ $or }` / `{ $and }`) into `filters`: alongside when its key is
|
|
1118
|
+
* free, otherwise both go under one `$and` so neither overwrites the other.
|
|
1119
|
+
*/
|
|
1120
|
+
function attachCondition(filters, condition) {
|
|
1121
|
+
const [key] = Object.keys(condition);
|
|
1122
|
+
if (!(key in filters)) return {
|
|
1123
|
+
...filters,
|
|
1124
|
+
...condition
|
|
1125
|
+
};
|
|
1126
|
+
const { [key]: existing, ...rest } = filters;
|
|
1127
|
+
const prior = key === "$and" ? existing : [{ [key]: existing }];
|
|
1128
|
+
const next = key === "$and" ? condition.$and : [condition];
|
|
1129
|
+
const and = rest.$and ? [
|
|
1130
|
+
...rest.$and,
|
|
1131
|
+
...prior,
|
|
1132
|
+
...next
|
|
1133
|
+
] : [...prior, ...next];
|
|
1134
|
+
return {
|
|
1135
|
+
...rest,
|
|
1136
|
+
$and: and
|
|
1137
|
+
};
|
|
1138
|
+
}
|
|
1518
1139
|
//#endregion
|
|
1519
1140
|
//#region src/utils/repo-options.ts
|
|
1520
1141
|
/**
|
|
@@ -1562,31 +1183,21 @@ var QueryParser = class {
|
|
|
1562
1183
|
* });
|
|
1563
1184
|
* ```
|
|
1564
1185
|
*
|
|
1565
|
-
*
|
|
1566
|
-
*
|
|
1186
|
+
* It is `forwardScope` (see `./scope.ts`), the same rule every internal
|
|
1187
|
+
* hop uses, so a request context and a cascade forward identically:
|
|
1567
1188
|
*
|
|
1568
|
-
* - `organizationId` —
|
|
1569
|
-
*
|
|
1570
|
-
*
|
|
1571
|
-
* - `
|
|
1572
|
-
*
|
|
1573
|
-
*
|
|
1574
|
-
* - `
|
|
1189
|
+
* - the tenant (`organizationId`, `tenantId`) — `multiTenantPlugin` casts
|
|
1190
|
+
* to `ObjectId` itself when configured with `fieldType: 'objectId'`;
|
|
1191
|
+
* do NOT pre-cast;
|
|
1192
|
+
* - `bypassTenant`, only when literally `true` AND no tenant is present
|
|
1193
|
+
* (the tenant plugin honours a bypass before a tenant, so carrying both
|
|
1194
|
+
* would widen a scoped call to every tenant);
|
|
1195
|
+
* - `session`, and `user` / `userId` / `requestId` for attribution.
|
|
1575
1196
|
*
|
|
1576
|
-
*
|
|
1577
|
-
* `repoOptionsFromCtx` deliberately stays narrow so adding a new
|
|
1578
|
-
* convention to the canonical set is a deliberate API decision, not
|
|
1579
|
-
* a quiet drift.
|
|
1197
|
+
* Domain fields beyond this set belong to `createOptionsExtractor`.
|
|
1580
1198
|
*/
|
|
1581
1199
|
function repoOptionsFromCtx(ctx) {
|
|
1582
|
-
|
|
1583
|
-
const out = {};
|
|
1584
|
-
if (ctx.organizationId !== void 0) out.organizationId = ctx.organizationId;
|
|
1585
|
-
if (ctx.userId !== void 0) out.userId = ctx.userId;
|
|
1586
|
-
if (ctx.user !== void 0) out.user = ctx.user;
|
|
1587
|
-
if (ctx.session !== void 0) out.session = ctx.session;
|
|
1588
|
-
if (ctx.requestId !== void 0) out.requestId = ctx.requestId;
|
|
1589
|
-
return out;
|
|
1200
|
+
return forwardScope(ctx);
|
|
1590
1201
|
}
|
|
1591
1202
|
/**
|
|
1592
1203
|
* Build a typed extractor for **consumer-defined** ctx → options
|
|
@@ -1608,12 +1219,11 @@ function repoOptionsFromCtx(ctx) {
|
|
|
1608
1219
|
* output keys are inferred from the input field tuple — you can
|
|
1609
1220
|
* destructure with full autocomplete.
|
|
1610
1221
|
*
|
|
1611
|
-
* **
|
|
1612
|
-
*
|
|
1613
|
-
* `
|
|
1614
|
-
*
|
|
1615
|
-
*
|
|
1616
|
-
* `idempotencyKey`, `sagaRunId`, ...) that should also forward.
|
|
1222
|
+
* **Scope is always included, and never from `fields`.** The extractor
|
|
1223
|
+
* returns `forwardScope(ctx)` plus the declared DOMAIN fields. Listing a
|
|
1224
|
+
* scope key (`organizationId`, `bypassTenant`, `session`, …) in `fields`
|
|
1225
|
+
* changes nothing: those follow the one scope rule, so an extractor cannot
|
|
1226
|
+
* forget the bypass, or carry a bypass beside a tenant.
|
|
1617
1227
|
*
|
|
1618
1228
|
* @example Per-package canonical extractor
|
|
1619
1229
|
* ```ts
|
|
@@ -1628,12 +1238,12 @@ function repoOptionsFromCtx(ctx) {
|
|
|
1628
1238
|
* idempotencyKey?: string;
|
|
1629
1239
|
* };
|
|
1630
1240
|
*
|
|
1241
|
+
* // Domain fields only: the scope (organizationId, session, bypass) is
|
|
1242
|
+
* // always included by the one scope rule.
|
|
1631
1243
|
* export const repoOptionsFromCtx = createOptionsExtractor<CommissionCtx>([
|
|
1632
|
-
* 'organizationId',
|
|
1633
1244
|
* 'actorRef',
|
|
1634
1245
|
* 'actorKind',
|
|
1635
1246
|
* 'correlationId',
|
|
1636
|
-
* 'session',
|
|
1637
1247
|
* 'idempotencyKey',
|
|
1638
1248
|
* ]);
|
|
1639
1249
|
*
|
|
@@ -1649,11 +1259,11 @@ function repoOptionsFromCtx(ctx) {
|
|
|
1649
1259
|
* options bag never erases inherited values).
|
|
1650
1260
|
*/
|
|
1651
1261
|
function createOptionsExtractor(fields) {
|
|
1652
|
-
const
|
|
1262
|
+
const domainFields = Object.freeze(fields.filter((field) => !isScopeKey(field)));
|
|
1653
1263
|
return function extractRepoOptions(ctx) {
|
|
1654
1264
|
if (!ctx) return {};
|
|
1655
|
-
const out =
|
|
1656
|
-
for (const field of
|
|
1265
|
+
const out = forwardScope(ctx);
|
|
1266
|
+
for (const field of domainFields) {
|
|
1657
1267
|
const v = ctx[field];
|
|
1658
1268
|
if (v !== void 0) out[field] = v;
|
|
1659
1269
|
}
|
|
@@ -1781,4 +1391,4 @@ function systemContext(extra) {
|
|
|
1781
1391
|
*/
|
|
1782
1392
|
var src_default = Repository;
|
|
1783
1393
|
//#endregion
|
|
1784
|
-
export { ALL_OPERATIONS, AggregationBuilder, AuditTrailQuery, HOOK_PRIORITY, LookupBuilder, MONGOKIT_CAPABILITIES, MONGOKIT_PLUGIN_NAMES, MUTATING_OPERATIONS, ModelCollisionError, OP_REGISTRY, PaginationEngine, QueryParser, READ_OPERATIONS, Repository, actions_exports as actions, adminBypass, aggregateHelpersPlugin, appendOnlyPlugin, auditLogPlugin, auditTrailPlugin, autoInject, batchOperationsPlugin, batchTransaction, blockIf, buildCrudSchemasFromModel, buildCrudSchemasFromMongooseSchema, cachePlugin, capabilitiesForSupport, cascadePlugin, cascadePurgeReferences, classifyHelloReply, compileFilterToMongo, configureLogger, createError, createFieldPreset, createMemoryCache, createOptionsExtractor, createRepository, createTenantContext, customIdPlugin, dateSequentialId, src_default as default, defineModel, defineModels, elasticSearchPlugin, ensureAuditTrailReady, fieldFilterPlugin, filterResponseData, getFieldsForUser, getMongooseProjection, getNextSequence, idVariants, idempotencyStorePlugin, immutableField, immutableStatesPlugin, isDuplicateKeyError, isModelRegistered, isTransactionUnsupported, leasePlugin, methodRegistryPlugin, mongoOperationsPlugin, multiTenantPlugin, observabilityPlugin, operationsByPolicyKey, parseDuplicateKeyError, prefixedId, probeMongoCapabilities, repoOptionsFromCtx, requireField, resetCapabilityProbeCache, resolveMongoCapabilities, resolveTransactionSupport, sequentialId, softDeletePlugin, subdocumentPlugin, supportsTransactions, systemContext, timestampPlugin, toPlain, transactionResolutionOf, uniqueField, validationChainPlugin, withTransaction };
|
|
1394
|
+
export { ALL_OPERATIONS, AggregationBuilder, AuditTrailQuery, HOOK_PRIORITY, LookupBuilder, MONGOKIT_CAPABILITIES, MONGOKIT_PLUGIN_NAMES, MUTATING_OPERATIONS, ModelCollisionError, OP_REGISTRY, PaginationEngine, QueryParser, READ_OPERATIONS, Repository, actions_exports as actions, adminBypass, aggregateHelpersPlugin, appendOnlyPlugin, auditLogPlugin, auditTrailPlugin, autoInject, batchOperationsPlugin, batchTransaction, blockIf, buildCrudSchemasFromModel, buildCrudSchemasFromMongooseSchema, cachePlugin, capabilitiesForSupport, cascadePlugin, cascadePurgeReferences, classifyHelloReply, compileFilterToMongo, configureLogger, configurePaginationDefaults, createError, createFieldPreset, createMemoryCache, createOptionsExtractor, createRepository, createTenantContext, customIdPlugin, dateSequentialId, declareTenantContextKey, src_default as default, defineModel, defineModels, elasticSearchPlugin, ensureAuditTrailReady, ensureSequenceAtLeast, fieldFilterPlugin, filterResponseData, forwardScope, getFieldsForUser, getMongooseProjection, getNextSequence, getPaginationDefaults, idVariants, idempotencyStorePlugin, immutableField, immutableStatesPlugin, isCreateManyPartialError, isDuplicateKeyError, isModelRegistered, isTransactionUnsupported, leasePlugin, methodRegistryPlugin, mongoOperationsPlugin, multiTenantPlugin, observabilityPlugin, operationsByPolicyKey, parseDuplicateKeyError, prefixedId, probeMongoCapabilities, readSequence, repoOptionsFromCtx, requireField, resetCapabilityProbeCache, resetPaginationDefaults, resolveMongoCapabilities, resolveTransactionSupport, sequentialId, softDeletePlugin, subdocumentPlugin, supportsTransactions, systemContext, tenantContextKeysOf, timestampPlugin, toPlain, transactionResolutionOf, uniqueField, validationChainPlugin, withTransaction };
|