@classytic/mongokit 3.39.0 → 3.40.1

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.
Files changed (51) hide show
  1. package/README.md +71 -0
  2. package/dist/{PaginationEngine-D-P19Y9m.d.mts → PaginationEngine-CRHyuUtw.d.mts} +17 -3
  3. package/dist/{PaginationEngine-DQ7beTCR.mjs → PaginationEngine-CfT2Cxkj.mjs} +320 -145
  4. package/dist/{Repository-D7fDpyFx.mjs → Repository-CYhSC9ut.mjs} +844 -794
  5. package/dist/{Repository-IxI983_m.d.mts → Repository-pnLw1rG5.d.mts} +113 -47
  6. package/dist/actions/index.d.mts +1 -1
  7. package/dist/actions/index.mjs +1 -1
  8. package/dist/adapter/index.mjs +1 -1
  9. package/dist/{adapter-BrVfPFSW.mjs → adapter-CkMoMF83.mjs} +1 -1
  10. package/dist/ai/index.d.mts +1 -1
  11. package/dist/better-auth/index.mjs +2 -2
  12. package/dist/{create-repository-CRXjDa3N.d.mts → create-repository-BUx9vfG9.d.mts} +5 -5
  13. package/dist/{create-repository-DOe9E_5F.mjs → create-repository-BYyWzLbt.mjs} +2 -2
  14. package/dist/idempotency/index.d.mts +23 -0
  15. package/dist/idempotency/index.mjs +134 -0
  16. package/dist/{index-CmjNyulu.d.mts → index-SYF8N28D.d.mts} +37 -19
  17. package/dist/index.d.mts +105 -39
  18. package/dist/index.mjs +332 -676
  19. package/dist/indexes/verify-indexes.d.mts +14 -1
  20. package/dist/indexes/verify-indexes.mjs +54 -7
  21. package/dist/join-scope-DSAznE8E.mjs +168 -0
  22. package/dist/{mongooseToJsonSchema-waUVQwK4.mjs → mongooseToJsonSchema-C3k4ide0.mjs} +5 -1
  23. package/dist/{multi-tenant.plugin-B-tzNeYL.d.mts → multi-tenant.plugin-a3UhQARj.d.mts} +19 -3
  24. package/dist/{operations-CrRIoc8Z.d.mts → operations-XiFgG5US.d.mts} +36 -13
  25. package/dist/outbox/index.d.mts +1 -1
  26. package/dist/outbox/index.mjs +6 -4
  27. package/dist/pagination/PaginationEngine.d.mts +2 -2
  28. package/dist/pagination/PaginationEngine.mjs +1 -1
  29. package/dist/pagination/utils/limits.d.mts +1 -1
  30. package/dist/{pagination-BpIv2bdS.d.mts → pagination-D6psses0.d.mts} +116 -13
  31. package/dist/{plugin-options-BuvtOSZf.d.mts → plugin-options-CRhIvSzx.d.mts} +16 -2
  32. package/dist/plugins/index.d.mts +8 -3
  33. package/dist/plugins/index.mjs +98 -25
  34. package/dist/query-defaults-BxUcSOoF.mjs +208 -0
  35. package/dist/read-model/index.d.mts +66 -0
  36. package/dist/read-model/index.mjs +193 -0
  37. package/dist/{repository-ZbLtL7Nk.d.mts → repository-DNHfjAZX.d.mts} +36 -4
  38. package/dist/sync/index.d.mts +53 -0
  39. package/dist/sync/index.mjs +371 -0
  40. package/dist/tenant/index.mjs +2 -0
  41. package/dist/testkit/index.d.mts +40 -3
  42. package/dist/testkit/index.mjs +113 -2
  43. package/dist/{timestamp.plugin-B5eiaItQ.mjs → timestamp.plugin-zfdLYZiO.mjs} +135 -32
  44. package/dist/{to-plain-B8ufk3Qz.d.mts → to-plain-CjgEInWV.d.mts} +2 -2
  45. package/dist/transaction-BrRYtH_b.mjs +655 -0
  46. package/dist/{update-c_01A0if.mjs → update-WgeAUJ_I.mjs} +212 -33
  47. package/dist/utils/index.d.mts +1 -1
  48. package/dist/utils/index.mjs +1 -1
  49. package/dist/{validation-chain.plugin-Bju1D1Sn.mjs → validation-chain.plugin-CHUHWMtk.mjs} +204 -279
  50. package/dist/{validation-chain.plugin-DvrLJIbJ.d.mts → validation-chain.plugin-CTcNJCmV.d.mts} +27 -5
  51. package/package.json +18 -4
package/dist/index.mjs CHANGED
@@ -1,23 +1,27 @@
1
1
  import { n as idVariants } from "./id-resolution-BqDrk0S2.mjs";
2
- import { _ as resetCapabilityProbeCache, a as supportsTransactions, b as transactionResolutionOf, c as ALL_OPERATIONS, d as READ_OPERATIONS, f as operationsByPolicyKey, g as probeMongoCapabilities, h as classifyHelloReply, i as isTransactionUnsupported, l as MUTATING_OPERATIONS, m as capabilitiesForSupport, n as Repository, o as withTransaction, p as MONGOKIT_CAPABILITIES, r as batchTransaction, s as AggregationBuilder, t as HOOK_PRIORITY, u as OP_REGISTRY, v as resolveMongoCapabilities, x as createMongoPurgePort, y as resolveTransactionSupport } from "./Repository-D7fDpyFx.mjs";
3
- import { _ as compileFilterToMongo, g as LookupBuilder, x as isCreateManyPartialError } from "./update-c_01A0if.mjs";
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-CYhSC9ut.mjs";
3
+ import { C as isCreateManyPartialError, g as LookupBuilder, v as compileFilterToMongo } from "./update-WgeAUJ_I.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
+ import { a as assertQueryDefaultsConfigured, c as getQueryDefaults, l as resetQueryDefaults, o as configureAggregateDefaults, s as configureQueryDefaults, t as QUERY_DEFAULTS_ERROR_CODES, u as resolveQueryOptions } from "./query-defaults-BxUcSOoF.mjs";
6
7
  import { t as actions_exports } from "./actions/index.mjs";
7
- import { a as customIdPlugin, c as prefixedId, d as AuditTrailQuery, f as auditTrailPlugin, i as multiTenantPlugin, l as sequentialId, m as auditLogPlugin, n as softDeletePlugin, o as dateSequentialId, p as ensureAuditTrailReady, r as adminBypass, s as getNextSequence, t as timestampPlugin, u as cachePlugin } from "./timestamp.plugin-B5eiaItQ.mjs";
8
+ 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-BrRYtH_b.mjs";
9
+ 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-zfdLYZiO.mjs";
8
10
  import { n as batchOperationsPlugin, t as methodRegistryPlugin } from "./method-registry.plugin-C63pGn1d.mjs";
9
- import { a as getPaginationDefaults, i as configurePaginationDefaults, o as resetPaginationDefaults, t as PaginationEngine } from "./PaginationEngine-DQ7beTCR.mjs";
10
- import { isGeoOperator, parseGeoFilter } from "./query/primitives/geo.mjs";
11
- import { t as createRepository } from "./create-repository-DOe9E_5F.mjs";
11
+ import { c as PAGINATION_ERROR_CODES, h as resetPaginationDefaults, m as getPaginationDefaults, p as configurePaginationDefaults, t as PaginationEngine } from "./PaginationEngine-CfT2Cxkj.mjs";
12
+ import { GEO_OPERATORS, parseGeoFilter } from "./query/primitives/geo.mjs";
13
+ import { n as declareCollectionScope, t as JOIN_ERROR_CODES } from "./join-scope-DSAznE8E.mjs";
14
+ import { t as createRepository } from "./create-repository-BYyWzLbt.mjs";
12
15
  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-Bju1D1Sn.mjs";
16
+ 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-CHUHWMtk.mjs";
14
17
  import { i as getMongooseProjection, n as filterResponseData, r as getFieldsForUser, t as createFieldPreset } from "./field-selection-FSQFLpsU.mjs";
15
- import { buildFieldTypeMap, coerceFieldValue, coerceHeuristic } from "./query/primitives/coercion.mjs";
18
+ import { buildFieldTypeMap, coerceHeuristic } from "./query/primitives/coercion.mjs";
16
19
  import { extractSchemaIndexes } from "./query/primitives/indexes.mjs";
17
20
  import { n as createMemoryCache, t as toPlain } from "./to-plain-vksRrLtc.mjs";
18
- import { n as buildCrudSchemasFromMongooseSchema, t as buildCrudSchemasFromModel } from "./mongooseToJsonSchema-waUVQwK4.mjs";
21
+ import { n as buildCrudSchemasFromMongooseSchema, t as buildCrudSchemasFromModel } from "./mongooseToJsonSchema-C3k4ide0.mjs";
19
22
  import { runChunkedPurge } from "@classytic/repo-core/repository";
20
- import { isControlParam } from "@classytic/repo-core/query-parser";
23
+ import { stableStringify } from "@classytic/repo-core/hash";
24
+ import { OPERATOR_DESCRIPTIONS, QueryGrammarError, URL_OPERATORS, clausesToFilter, escapeRegex, isQueryGrammarError, readFilterClauses, readPageRequest, readSearch, readSelect, readSort } from "@classytic/repo-core/query-parser";
21
25
  //#region src/actions/cascade-references.ts
22
26
  /**
23
27
  * cascadePurgeReferences — imperative, multi-relation "purge by reference".
@@ -218,6 +222,45 @@ function defineModels(spec) {
218
222
  };
219
223
  }
220
224
  //#endregion
225
+ //#region src/pagination/keyset-source.ts
226
+ /**
227
+ * `keysetSource(repo, { name, sort, filters, ...scope })` — a mongokit repository as a repo-core
228
+ * `KeysetSource`, for `mergeKeysetPages`. Pages are `getAll({ mode: 'keyset' })` under the given
229
+ * scope; the per-row cursor is `repo.keysetCursor` (scope-bound like every mongokit cursor); the
230
+ * source's `scope` string binds the composite cursor to (collection, filters, tenant values).
231
+ */
232
+ function keysetSource(repo, options) {
233
+ const { name, sort, filters = {}, ...scope } = options;
234
+ const { session: _session, user: _user, ...tenant } = forwardScope(scope);
235
+ return {
236
+ name,
237
+ scope: `${repo.Model.collection.collectionName}:${stableStringify({
238
+ filters,
239
+ tenant
240
+ })}`,
241
+ async page({ after, limit }) {
242
+ const page = await repo.getAll({
243
+ ...scope,
244
+ filters,
245
+ sort,
246
+ limit,
247
+ mode: "keyset",
248
+ ...after ? { after } : {}
249
+ });
250
+ if (page.method !== "keyset") throw new Error("[mongokit] keysetSource: getAll did not return a keyset page");
251
+ return {
252
+ data: page.data,
253
+ hasMore: page.hasMore
254
+ };
255
+ },
256
+ cursorOf: (row) => repo.keysetCursor(row, {
257
+ ...scope,
258
+ sort,
259
+ filters
260
+ })
261
+ };
262
+ }
263
+ //#endregion
221
264
  //#region src/query/parser/pipeline-sanitizer.ts
222
265
  /**
223
266
  * Sanitize $match configuration to prevent dangerous operators.
@@ -357,410 +400,134 @@ function parseAggregation(rt, aggregate) {
357
400
  return pipeline.length > 0 ? pipeline : void 0;
358
401
  }
359
402
  //#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
403
  //#region src/query/parser/filter-compiler.ts
455
404
  /**
456
- * Filter compiler — URL filter parameters → MongoDB filter query.
457
- *
458
- * Owns the three entry shapes (flat `field=value`, flat operator syntax
459
- * `field[gte]=10`, nested bracket objects from qs), the `$or` group router,
460
- * and the `between` range expander. Every invalid or disallowed fragment
461
- * routes through the runtime's `invalidInput` policy.
405
+ * URL filters → a MongoDB filter. The grammar (keys, operators, value coercion, refusals) is
406
+ * repo-core's `readFilterClauses`; its clauses compile through the Filter IR compiler every
407
+ * repository query uses, so URL and programmatic filters mean the same thing. Only mongokit's own
408
+ * operators (`size`, `type`, geo) are emitted here, natively.
462
409
  */
410
+ /** mongokit-native operators, beyond the shared grammar. */
411
+ const MONGOKIT_EXTENSION_OPERATORS = [
412
+ "size",
413
+ "type",
414
+ ...GEO_OPERATORS
415
+ ];
416
+ /** Group params — `or[0][status]=a&or[1][qty][gte]=5` — compiled branch by branch. */
417
+ const GROUP_PARAMS = [
418
+ "or",
419
+ "OR",
420
+ "$or",
421
+ "$and"
422
+ ];
423
+ /** mongokit control params that are not filters. */
424
+ const MONGOKIT_RESERVED_PARAMS = [
425
+ "lean",
426
+ "includeDeleted",
427
+ "lookup",
428
+ "aggregate",
429
+ "cursor",
430
+ ...GROUP_PARAMS
431
+ ];
432
+ const BSON_TYPE_RE = /^(?:[a-zA-Z]+|\d{1,3})$/;
433
+ /** Compile every filter in `query` (group params excluded — see {@link compileGroups}). */
434
+ function compileUrlFilters(rt, query) {
435
+ const { clauses, extensions } = readFilterClauses(query, grammarOptions(rt));
436
+ return conjoin([compileFilterToMongo(clausesToFilter(clauses)), ...extensions.map((clause) => compileExtension(rt, clause)).filter((part) => part !== void 0)]);
437
+ }
463
438
  /**
464
- * Top-level query parameters that mongokit handles outside the filter
465
- * pipeline. Used by `handleBracketSyntax` to detect a common typo where a
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`.
439
+ * `or` / `$or` and `$and` groups. Each branch is a full filter through the same grammar. An empty
440
+ * branch is refused — inside an OR it would match every document — and groups do not nest.
470
441
  */
471
- const RESERVED_PAGINATION_KEYS = /* @__PURE__ */ new Set([
472
- "page",
473
- "limit",
474
- "sort",
475
- "select"
476
- ]);
477
- function toMongoOperator(operator) {
478
- const op = operator.toLowerCase();
479
- return op.startsWith("$") ? op : `$${op}`;
442
+ function compileGroups(rt, query) {
443
+ const out = {};
444
+ const orRaw = query?.or ?? query?.OR ?? query?.$or;
445
+ const orBranches = orRaw === void 0 ? [] : compileBranches(rt, "or", orRaw);
446
+ if (orBranches.length > 0) out.$or = orBranches;
447
+ const andBranches = query?.$and === void 0 ? [] : compileBranches(rt, "$and", query.$and);
448
+ if (andBranches.length > 0) out.$and = andBranches;
449
+ return out;
480
450
  }
481
- /** Parse filter parameters into a MongoDB filter query. */
482
- function parseFilters(rt, filters, depth = 0) {
483
- if (depth > rt.options.maxFilterDepth) {
484
- rt.reject(`Filter depth ${depth} exceeds maximum ${rt.options.maxFilterDepth}, truncating`, {
485
- depth,
486
- maxFilterDepth: rt.options.maxFilterDepth
487
- });
488
- return {};
451
+ function compileBranches(rt, param, raw) {
452
+ const items = Array.isArray(raw) ? raw : isRecord(raw) ? Object.values(raw) : void 0;
453
+ if (!items) {
454
+ rt.reject(`${param} takes branches: ${param}[0][field]=value`, { param });
455
+ return [];
489
456
  }
490
- const parsedFilters = {};
491
- const regexFields = {};
492
- for (const [key, value] of Object.entries(filters)) {
493
- if (rt.dangerousOperators.includes(key) || key.startsWith("$") && !["$or", "$and"].includes(key)) {
494
- rt.reject(`Blocked dangerous operator: ${key}`, { key });
495
- continue;
496
- }
497
- if (isControlParam(key) || [
498
- "lean",
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 });
457
+ const branches = [];
458
+ for (const item of items) {
459
+ if (!isRecord(item)) {
460
+ rt.reject(`each ${param} branch is a set of filters`, { param });
510
461
  continue;
511
462
  }
512
- if (key === "$and") {
513
- const branches = parseCompoundBranches(rt, value, depth);
514
- if (branches.length > 0) parsedFilters.$and = branches;
463
+ if (GROUP_PARAMS.some((group) => group in item)) {
464
+ rt.reject(`${param} groups do not nest`, { param });
515
465
  continue;
516
466
  }
517
- if (operatorMatch) {
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);
467
+ const branch = compileUrlFilters(rt, item);
468
+ if (Object.keys(branch).length === 0) {
469
+ rt.reject(`an empty ${param} branch would match every document`, { param });
524
470
  continue;
525
471
  }
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);
472
+ branches.push(branch);
528
473
  }
529
- return parsedFilters;
474
+ return branches;
530
475
  }
531
- /** Handle flat operator syntax: field[operator]=value */
532
- function handleOperatorSyntax(rt, filters, regexFields, operatorMatch, value) {
533
- const [, field, operator] = operatorMatch;
534
- if (value === "" || value === null || value === void 0) return;
535
- if (rt.options.allowedOperators && !rt.options.allowedOperators.includes(operator.toLowerCase())) {
536
- rt.reject(`Operator not in allowlist: ${operator}`, {
537
- field,
538
- operator
539
- });
540
- return;
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
- }
476
+ function grammarOptions(rt) {
477
+ return {
478
+ allowedFilterFields: rt.options.allowedFilterFields,
479
+ allowedOperators: rt.options.allowedOperators,
480
+ fieldTypes: rt.grammarFieldTypes,
481
+ extensionOperators: MONGOKIT_EXTENSION_OPERATORS,
482
+ reservedParams: MONGOKIT_RESERVED_PARAMS,
483
+ maxTextLength: rt.options.maxRegexLength,
484
+ onInvalid: rt.options.invalidInput === "drop" ? (error) => rt.reject(error.message) : void 0
485
+ };
629
486
  }
630
- /** Handle bracket syntax with object value (qs-parsed nested operators). */
631
- function handleBracketSyntax(rt, field, operators, parsedFilters, depth = 0) {
632
- if (depth > rt.options.maxFilterDepth) {
633
- rt.reject(`Nested filter depth exceeds maximum, skipping field: ${field}`, {
634
- field,
635
- maxFilterDepth: rt.options.maxFilterDepth
636
- });
637
- return;
638
- }
639
- for (const op of Object.keys(operators)) if (RESERVED_PAGINATION_KEYS.has(op)) warn(`[mongokit] Nested filter contains reserved key '${op}' at '${field}.${op}' — did you mean ?${op}=... at the top level? The nested value is ignored.`);
640
- if (!parsedFilters[field]) parsedFilters[field] = {};
641
- for (const [operator, value] of Object.entries(operators)) {
642
- if (value === "" || value === null || value === void 0) continue;
643
- if (operator === "between") {
644
- parsedFilters[field].between = value;
645
- continue;
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];
487
+ function compileExtension(rt, clause) {
488
+ const param = `${clause.field}[${clause.op}]`;
489
+ try {
490
+ switch (clause.op) {
491
+ case "size":
492
+ if (!/^\d+$/.test(clause.raw)) throw new QueryGrammarError(param, "size takes a whole number");
493
+ return { [clause.field]: { $size: Number(clause.raw) } };
494
+ case "type": {
495
+ if (!BSON_TYPE_RE.test(clause.raw)) throw new QueryGrammarError(param, "not a BSON type");
496
+ const code = Number(clause.raw);
497
+ return { [clause.field]: { $type: Number.isInteger(code) ? code : clause.raw } };
498
+ }
499
+ default: {
500
+ const geo = parseGeoFilter(clause.op, clause.raw);
501
+ if (!geo) throw new QueryGrammarError(param, `invalid ${clause.op} coordinates`);
502
+ return { [clause.field]: geo };
656
503
  }
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
504
  }
505
+ } catch (error) {
506
+ if (!(error instanceof QueryGrammarError) || rt.options.invalidInput !== "drop") throw error;
507
+ rt.reject(error.message);
508
+ return;
711
509
  }
712
- if (typeof parsedFilters[field] === "object" && Object.keys(parsedFilters[field]).length === 0) delete parsedFilters[field];
713
510
  }
714
- /** Parse `?or=[...]` / `?OR=[...]` / `?$or=[...]` groups into an $or array. */
715
- /**
716
- * Parse the branches of a compound operator (`$and`, and the body of `$or`).
717
- *
718
- * Every branch goes back through `parseFilters`, so the operator block, the allowlist and the
719
- * regex budget all apply inside it. Branches that parse to `{}` are DROPPED: a branch holding
720
- * only a blocked operator would otherwise become match-all, which widens the query instead of
721
- * failing it — the exact shape `parseOr` documents below.
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);
511
+ /** AND compiled parts: one object when fields don't collide, `$and` when they do. */
512
+ function conjoin(parts) {
513
+ const nonEmpty = parts.filter((part) => Object.keys(part).length > 0);
514
+ if (nonEmpty.length <= 1) return { ...nonEmpty[0] ?? {} };
515
+ const merged = {};
516
+ for (const part of nonEmpty) {
517
+ for (const key of Object.keys(part)) if (key in merged) return { $and: nonEmpty };
518
+ Object.assign(merged, part);
730
519
  }
731
- return branches;
520
+ return merged;
732
521
  }
733
- function parseOr(rt, query) {
734
- const orArray = [];
735
- const raw = query?.or || query?.OR || query?.$or;
736
- if (!raw) return void 0;
737
- const items = Array.isArray(raw) ? raw : typeof raw === "object" ? Object.values(raw) : [];
738
- for (const item of items) if (typeof item === "object" && item) {
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;
522
+ /** mongokit's schema-derived field types, narrowed to the grammar's portable set. */
523
+ function toGrammarFieldTypes(types) {
524
+ const out = {};
525
+ for (const [path, type] of types) if (type === "string" || type === "objectid") out[path] = "string";
526
+ else if (type === "number" || type === "boolean" || type === "date") out[path] = type;
527
+ return out;
743
528
  }
744
- /** Expand `{ field: { between: 'a,b' } }` markers into `$gte`/`$lte` date ranges. */
745
- function enhanceWithBetween(rt, filters) {
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;
529
+ function isRecord(value) {
530
+ return typeof value === "object" && value !== null && !Array.isArray(value);
764
531
  }
765
532
  //#endregion
766
533
  //#region src/query/parser/lookup.ts
@@ -838,97 +605,70 @@ function parseSingleLookup(rt, collectionName, config) {
838
605
  return null;
839
606
  }
840
607
  //#endregion
841
- //#region src/query/parser/pagination-input.ts
608
+ //#region src/query/parser/runtime.ts
842
609
  /**
843
- * Parse a pagination input to a positive integer, or `undefined` when the
844
- * value is absent, empty, or rejected under `'drop'` mode. Rejects (per
845
- * policy) on non-integers, negatives, zero, decimals, and trailing garbage.
610
+ * Parser runtime — the shared context every parser module receives.
611
+ *
612
+ * The `QueryParser` facade resolves options once at construction and builds a
613
+ * single `ParserRuntime`; the extracted modules (filter compiler, sanitizers,
614
+ * populate/lookup/sort parsers, …) are pure functions over it. This keeps the
615
+ * modules independently testable and the facade thin, without threading six
616
+ * separate arguments through every call.
846
617
  */
847
- function parsePositiveInt(rt, raw, label) {
848
- if (raw === void 0 || raw === null || raw === "") return void 0;
849
- let n;
850
- if (typeof raw === "number") n = raw;
851
- else {
852
- const s = String(raw).trim();
853
- if (!/^\d+$/.test(s)) {
854
- rt.reject(`Invalid ${label} value: ${String(raw)} (expected a positive integer)`, { [label]: String(raw) });
855
- return;
856
- }
857
- n = Number(s);
858
- }
859
- if (!Number.isInteger(n) || n < 1) {
860
- rt.reject(`Invalid ${label} value: ${String(raw)} (expected a positive integer)`, { [label]: String(raw) });
861
- return;
618
+ /** Always-blocked MongoDB operators (extended via `additionalDangerousOperators`). */
619
+ const BASE_DANGEROUS_OPERATORS = [
620
+ "$where",
621
+ "$function",
622
+ "$accumulator",
623
+ "$expr"
624
+ ];
625
+ function createReject(mode) {
626
+ return (message, meta) => {
627
+ if (mode === "throw") throw createError(400, `[mongokit] ${message}`, {
628
+ code: "INVALID_QUERY_INPUT",
629
+ meta
630
+ });
631
+ warn(`[mongokit] ${message}`);
632
+ };
633
+ }
634
+ /**
635
+ * Run a grammar reader under the `invalidInput` policy: `'throw'` lets its 400 through untouched;
636
+ * `'drop'` warns and returns `fallback`.
637
+ */
638
+ function guarded(rt, read, fallback) {
639
+ try {
640
+ return read();
641
+ } catch (error) {
642
+ if (!isQueryGrammarError(error) || rt.options.invalidInput !== "drop") throw error;
643
+ rt.reject(error.message);
644
+ return fallback;
862
645
  }
863
- return n;
864
646
  }
865
647
  //#endregion
866
648
  //#region src/query/parser/sort-select.ts
867
649
  /**
868
- * The sort applied when a request names none. Mongo/mongokit convention.
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.
650
+ * `?sort=-createdAt,name` and `?select=name,-password` — read through the shared grammar
651
+ * (`readSort` / `readSelect`), so an invalid or disallowed field is refused like any filter.
872
652
  */
873
- const DEFAULT_PARSER_SORT = "-createdAt";
874
653
  /**
875
- * Is every field of `sort` inside `allowedSortFields`? True when no allowlist
876
- * is configured. Pure predicate — never rejects, never warns, so it is safe to
877
- * ask about a default the caller did not send.
654
+ * The sort applied when a request names none. Exported so `parse()` can ask whether it is
655
+ * PERMITTED before applying it, rather than validating it like caller input.
878
656
  */
657
+ const DEFAULT_PARSER_SORT = "-createdAt";
658
+ /** Is every field of `sort` inside `allowedSortFields`? Pure — never rejects. */
879
659
  function isSortFieldAllowed(rt, sort) {
880
660
  const allowed = rt.options.allowedSortFields;
881
661
  if (!allowed || allowed.length === 0) return true;
882
- return sort.split(",").map((s) => s.trim()).filter(Boolean).every((field) => allowed.includes(field.startsWith("-") ? field.substring(1) : field));
662
+ return sort.split(",").map((s) => s.trim()).filter(Boolean).every((field) => allowed.includes(field.replace(/^[+-]/, "")));
883
663
  }
884
- /** Parse a sort spec (string or object form), honoring `allowedSortFields`. */
885
664
  function parseSort(rt, sort) {
886
- if (!sort) return void 0;
887
- if (typeof sort === "object") {
888
- const sortObj = {};
889
- for (const [key, value] of Object.entries(sort)) {
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;
665
+ return readSort(sort, {
666
+ allowedSortFields: rt.options.allowedSortFields,
667
+ onInvalid: rt.options.invalidInput === "drop" ? (error) => rt.reject(error.message) : void 0
668
+ });
912
669
  }
913
- /**
914
- * Parse select/project fields.
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;
670
+ function parseSelect(rt, select) {
671
+ return guarded(rt, () => readSelect(select), void 0);
932
672
  }
933
673
  //#endregion
934
674
  //#region src/query/parser/populate.ts
@@ -992,7 +732,10 @@ function parseSinglePopulate(rt, path, config, depth = 0) {
992
732
  const opts = config;
993
733
  const option = { path };
994
734
  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") option.match = convertPopulateMatch(opts.match);
735
+ if (opts.match && typeof opts.match === "object") {
736
+ const match = convertPopulateMatch(rt, path, opts.match);
737
+ if (match) option.match = match;
738
+ }
996
739
  if (opts.limit !== void 0) {
997
740
  const limit = parseInt(String(opts.limit), 10);
998
741
  if (!Number.isNaN(limit) && limit > 0) {
@@ -1027,112 +770,56 @@ function parseSinglePopulate(rt, path, config, depth = 0) {
1027
770
  }
1028
771
  return null;
1029
772
  }
1030
- /** Convert populate match values (handles boolean strings, etc.) */
1031
- function convertPopulateMatch(match) {
773
+ /**
774
+ * A populate `match` is plain field EQUALITY: a `$` key (`$where`) or a nested object — the same
775
+ * operator-smuggling route — is refused through the invalid-input policy, never passed to mongoose.
776
+ */
777
+ function convertPopulateMatch(rt, path, match) {
1032
778
  const converted = {};
1033
- for (const [key, value] of Object.entries(match)) converted[key] = coerceHeuristic(value);
779
+ for (const [key, value] of Object.entries(match)) {
780
+ if (!/^[a-zA-Z_][a-zA-Z0-9_.]*$/.test(key) || value !== null && typeof value === "object") {
781
+ rt.reject(`Populate match on "${path}" accepts field equality only: "${key}"`, {
782
+ path,
783
+ key
784
+ });
785
+ return null;
786
+ }
787
+ converted[key] = coerceHeuristic(value);
788
+ }
1034
789
  return converted;
1035
790
  }
1036
791
  //#endregion
1037
- //#region src/query/parser/runtime.ts
792
+ //#region src/query/parser/schema-docs.ts
1038
793
  /**
1039
- * Parser runtime — the shared context every parser module receives.
1040
- *
1041
- * The `QueryParser` facade resolves options once at construction and builds a
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.
794
+ * OpenAPI query-schema generation — Arc's `defineResource()` auto-detects
795
+ * `getQuerySchema()` on the parser and uses it to document list-endpoint
796
+ * query parameters in OpenAPI/Swagger.
1046
797
  */
1047
- /** URL operator key → MongoDB operator. */
1048
- const OPERATOR_MAP = {
1049
- eq: "$eq",
1050
- ne: "$ne",
1051
- gt: "$gt",
1052
- gte: "$gte",
1053
- lt: "$lt",
1054
- lte: "$lte",
1055
- in: "$in",
1056
- nin: "$nin",
1057
- like: "$regex",
1058
- contains: "$regex",
1059
- regex: "$regex",
1060
- exists: "$exists",
1061
- size: "$size",
1062
- type: "$type"
798
+ /** mongokit's own operators, described alongside the shared grammar's. */
799
+ const EXTENSION_DESCRIPTIONS = {
800
+ size: "Array has exactly N elements",
801
+ type: "Field is of a BSON type (name or number)",
802
+ near: "Nearest to lng,lat[,maxDistanceMeters]",
803
+ nearSphere: "Nearest on a sphere to lng,lat[,maxDistanceMeters]",
804
+ geoWithin: "Inside the box minLng,minLat,maxLng,maxLat",
805
+ withinRadius: "Within lng,lat,radiusMeters"
1063
806
  };
1064
- /** Always-blocked MongoDB operators (extended via `additionalDangerousOperators`). */
1065
- const BASE_DANGEROUS_OPERATORS = [
1066
- "$where",
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
- };
807
+ function describeOperator(op) {
808
+ if (op in OPERATOR_DESCRIPTIONS) return OPERATOR_DESCRIPTIONS[op];
809
+ return EXTENSION_DESCRIPTIONS[op] ?? op;
1079
810
  }
1080
- //#endregion
1081
- //#region src/query/parser/schema-docs.ts
1082
811
  function availableOperators(rt) {
1083
- return rt.options.allowedOperators ? Object.entries(rt.operators).filter(([key]) => rt.options.allowedOperators?.includes(key)) : Object.entries(rt.operators);
812
+ const allowed = rt.options.allowedOperators;
813
+ return allowed ? rt.urlOperators.filter((op) => allowed.includes(op)) : rt.urlOperators;
1084
814
  }
1085
- /** Get the JSON Schema type for a filter operator. */
815
+ /** JSON Schema type of an operator's value. */
1086
816
  function operatorSchemaType(op) {
1087
- if ([
1088
- "gt",
1089
- "gte",
1090
- "lt",
1091
- "lte",
1092
- "size"
1093
- ].includes(op)) return "number";
1094
- if (["exists"].includes(op)) return "boolean";
817
+ if (op === "size") return "integer";
818
+ if (op === "exists") return "boolean";
1095
819
  return "string";
1096
820
  }
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
821
  function buildOperatorSummary(operators) {
1117
- const lines = ["Available filter operators (use as field[operator]=value):"];
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");
822
+ return ["Available filter operators (use as field[operator]=value):", ...operators.map((op) => ` ${op}: ${describeOperator(op)}`)].join("\n");
1136
823
  }
1137
824
  /**
1138
825
  * Generate OpenAPI-compatible JSON Schema for query parameters.
@@ -1216,11 +903,11 @@ function buildQuerySchema(rt) {
1216
903
  type: "string",
1217
904
  description: `Filter by ${field} (exact match)`
1218
905
  };
1219
- for (const [op, mongoOp] of operators) {
906
+ for (const op of operators) {
1220
907
  if (op === "eq") continue;
1221
908
  properties[`${field}[${op}]`] = {
1222
909
  type: operatorSchemaType(op),
1223
- description: operatorDescription(op, field, mongoOp)
910
+ description: `${field}: ${describeOperator(op)}`
1224
911
  };
1225
912
  }
1226
913
  }
@@ -1246,39 +933,17 @@ function buildOpenAPIQuerySchema(rt) {
1246
933
  //#endregion
1247
934
  //#region src/query/parser/search.ts
1248
935
  /**
1249
- * Search-term handling — length-capped sanitization plus the regex-mode
1250
- * multi-field `$or` builder. Search terms are literal text, never regex
1251
- * patterns: special characters are escaped, not rejected.
936
+ * `?search=` — length-checked through the shared grammar. Regex mode matches the term as LITERAL
937
+ * text across `searchFields` (`c++` and `a.b` mean exactly those characters).
1252
938
  */
1253
- /** Trim + length-cap the raw search term (reject over-long input per policy). */
1254
939
  function sanitizeSearch(rt, search) {
1255
- if (search === null || search === void 0 || search === "") return void 0;
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;
940
+ return guarded(rt, () => readSearch(search, { maxSearchLength: rt.options.maxSearchLength }), void 0);
1266
941
  }
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
942
  function buildRegexSearch(rt, searchTerm) {
1276
- if (!rt.options.searchFields || rt.options.searchFields.length === 0) return null;
1277
- const safeRegex = createSafeRegex(rt, searchTerm, "i", "escape");
1278
- if (!safeRegex) return null;
1279
- const orConditions = [];
1280
- for (const field of rt.options.searchFields) orConditions.push({ [field]: { $regex: safeRegex } });
1281
- return orConditions.length > 0 ? orConditions : null;
943
+ const fields = rt.options.searchFields;
944
+ if (!fields || fields.length === 0) return null;
945
+ const pattern = new RegExp(escapeRegex(searchTerm), "i");
946
+ return fields.map((field) => ({ [field]: { $regex: pattern } }));
1282
947
  }
1283
948
  //#endregion
1284
949
  //#region src/query/QueryParser.ts
@@ -1293,8 +958,7 @@ function buildRegexSearch(rt, searchTerm) {
1293
958
  * orchestration, and the OpenAPI schema surface. The implementation lives in
1294
959
  * focused modules under `./parser/`:
1295
960
  *
1296
- * - `parser/filter-compiler` — filters, operator syntax, $or, between
1297
- * - `parser/regex-safety` — ReDoS protection (escape/reject)
961
+ * - `parser/filter-compiler` — filters (repo-core's shared grammar → Filter IR → Mongo), groups
1298
962
  * - `parser/pipeline-sanitizer` — $match / $lookup-pipeline / expression sanitizing
1299
963
  * - `parser/lookup` — ?lookup[...] parsing + collection allowlist
1300
964
  * - `parser/aggregation` — ?aggregate[...] parsing (opt-in)
@@ -1319,9 +983,9 @@ function buildRegexSearch(rt, searchTerm) {
1319
983
  * being silently dropped (which broadens the result set). Opt into
1320
984
  * `'drop'` only for trusted compat tooling.
1321
985
  * - Dangerous operators blocked everywhere ($where, $function, $accumulator, $expr).
1322
- * - Regex patterns validated (ReDoS protection); search/like/contains are
1323
- * literal text (escaped, never rejected).
1324
- * - Max filter depth + max limit enforced.
986
+ * - Filters, paging, sort and select follow repo-core's shared query grammar — the same
987
+ * contract as arc's parser and `parseUrl` (`runQueryGrammarConformance` holds all three).
988
+ * - Text operators and search are literal text; only `regex` is a pattern, checked for ReDoS.
1325
989
  * - Lookup pipelines sanitized; `enableAggregations` is opt-in — keep it off
1326
990
  * for public endpoints or pair with per-route allowlists.
1327
991
  *
@@ -1366,11 +1030,13 @@ var QueryParser = class {
1366
1030
  warn("[mongokit] searchMode \"regex\" requires searchFields to be specified. Falling back to \"text\" mode.");
1367
1031
  resolved.searchMode = "text";
1368
1032
  }
1033
+ const fieldTypes = buildFieldTypeMap(options.schema, options.fieldTypes);
1369
1034
  this.rt = {
1370
1035
  options: resolved,
1371
- operators: OPERATOR_MAP,
1036
+ urlOperators: [...URL_OPERATORS, ...MONGOKIT_EXTENSION_OPERATORS],
1372
1037
  dangerousOperators: [...BASE_DANGEROUS_OPERATORS, ...resolved.additionalDangerousOperators],
1373
- fieldTypes: buildFieldTypeMap(options.schema, options.fieldTypes),
1038
+ fieldTypes,
1039
+ grammarFieldTypes: toGrammarFieldTypes(fieldTypes),
1374
1040
  reject: createReject(resolved.invalidInput)
1375
1041
  };
1376
1042
  this._schemaIndexes = extractSchemaIndexes(options.schema);
@@ -1434,67 +1100,45 @@ var QueryParser = class {
1434
1100
  */
1435
1101
  parse(query) {
1436
1102
  const rt = this.rt;
1437
- const { page, limit = 20, sort, populate, search, after, cursor, select, lookup, aggregate, ...filters } = query || {};
1438
- let parsedLimit = parsePositiveInt(rt, limit, "limit") ?? 20;
1439
- if (parsedLimit > rt.options.maxLimit) {
1440
- warn(`[mongokit] Limit ${parsedLimit} exceeds maximum ${rt.options.maxLimit}, capping to max`);
1441
- parsedLimit = rt.options.maxLimit;
1442
- }
1443
- /**
1444
- * The DEFAULT sort applies only when the allowlist permits it.
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
- */
1103
+ const q = query ?? {};
1104
+ const { page, limit, sort, populate, search, after, cursor, before, mode, select, lookup, aggregate } = q;
1105
+ const pageRequest = guarded(rt, () => readPageRequest({
1106
+ page,
1107
+ limit,
1108
+ after,
1109
+ cursor,
1110
+ before,
1111
+ mode
1112
+ }, { maxLimit: rt.options.maxLimit }), readPageRequest({}, { maxLimit: rt.options.maxLimit }));
1459
1113
  const effectiveSort = sort === void 0 && isSortFieldAllowed(rt, "-createdAt") ? DEFAULT_PARSER_SORT : sort;
1460
1114
  const sanitizedSearch = sanitizeSearch(rt, search);
1461
1115
  const { simplePopulate, populateOptions } = parsePopulate(rt, populate);
1462
1116
  const parsed = {
1463
- filters: parseFilters(rt, filters),
1464
- limit: parsedLimit,
1117
+ filters: compileUrlFilters(rt, q),
1118
+ limit: pageRequest.limit,
1465
1119
  sort: parseSort(rt, effectiveSort),
1466
1120
  populate: simplePopulate,
1467
1121
  populateOptions,
1468
1122
  search: sanitizedSearch
1469
1123
  };
1470
1124
  if (sanitizedSearch && rt.options.searchMode === "regex" && rt.options.searchFields) {
1471
- const regexSearchFilters = buildRegexSearch(rt, sanitizedSearch);
1472
- if (regexSearchFilters) {
1473
- if (parsed.filters.$or) {
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;
1125
+ const regexSearch = buildRegexSearch(rt, sanitizedSearch);
1126
+ if (regexSearch) {
1127
+ parsed.filters = attachCondition(parsed.filters, { $or: regexSearch });
1480
1128
  parsed.search = void 0;
1481
1129
  }
1482
1130
  }
1483
- if (select) parsed.select = parseSelect(select);
1131
+ const groups = compileGroups(rt, q);
1132
+ if (groups.$or) parsed.filters = attachCondition(parsed.filters, { $or: groups.$or });
1133
+ if (groups.$and) parsed.filters = attachCondition(parsed.filters, { $and: groups.$and });
1134
+ const projection = parseSelect(rt, select);
1135
+ if (projection) parsed.select = projection;
1484
1136
  if (rt.options.enableLookups && lookup) parsed.lookups = parseLookups(rt, lookup);
1485
1137
  if (rt.options.enableAggregations && aggregate) parsed.aggregation = parseAggregation(rt, aggregate);
1486
- if (after || cursor) parsed.after = String(after || cursor);
1487
- const parsedPage = parsePositiveInt(rt, page, "page");
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);
1138
+ if (pageRequest.after !== void 0) parsed.after = pageRequest.after;
1139
+ if (pageRequest.before !== void 0) parsed.before = pageRequest.before;
1140
+ if (pageRequest.mode !== void 0) parsed.mode = pageRequest.mode;
1141
+ if (pageRequest.page !== void 0) parsed.page = pageRequest.page;
1498
1142
  return parsed;
1499
1143
  }
1500
1144
  /**
@@ -1515,6 +1159,29 @@ var QueryParser = class {
1515
1159
  return buildOpenAPIQuerySchema(this.rt);
1516
1160
  }
1517
1161
  };
1162
+ /**
1163
+ * AND a top-level condition (`{ $or }` / `{ $and }`) into `filters`: alongside when its key is
1164
+ * free, otherwise both go under one `$and` so neither overwrites the other.
1165
+ */
1166
+ function attachCondition(filters, condition) {
1167
+ const [key] = Object.keys(condition);
1168
+ if (!(key in filters)) return {
1169
+ ...filters,
1170
+ ...condition
1171
+ };
1172
+ const { [key]: existing, ...rest } = filters;
1173
+ const prior = key === "$and" ? existing : [{ [key]: existing }];
1174
+ const next = key === "$and" ? condition.$and : [condition];
1175
+ const and = rest.$and ? [
1176
+ ...rest.$and,
1177
+ ...prior,
1178
+ ...next
1179
+ ] : [...prior, ...next];
1180
+ return {
1181
+ ...rest,
1182
+ $and: and
1183
+ };
1184
+ }
1518
1185
  //#endregion
1519
1186
  //#region src/utils/repo-options.ts
1520
1187
  /**
@@ -1562,31 +1229,21 @@ var QueryParser = class {
1562
1229
  * });
1563
1230
  * ```
1564
1231
  *
1565
- * The fields read (all optional — extracted only when present and not
1566
- * `undefined`):
1232
+ * It is `forwardScope` (see `./scope.ts`), the same rule every internal
1233
+ * hop uses, so a request context and a cascade forward identically:
1567
1234
  *
1568
- * - `organizationId` — multi-tenant scope key. The
1569
- * `multiTenantPlugin` casts to `ObjectId` automatically when
1570
- * configured with `fieldType: 'objectId'`; do NOT pre-cast.
1571
- * - `userId` / `user` — audit attribution (audit-log + audit-trail
1572
- * plugins read these for the `who` column).
1573
- * - `session` — mongoose `ClientSession` for transaction threading.
1574
- * - `requestId` — observability correlation id.
1235
+ * - the tenant (`organizationId`, `tenantId`) — `multiTenantPlugin` casts
1236
+ * to `ObjectId` itself when configured with `fieldType: 'objectId'`;
1237
+ * do NOT pre-cast;
1238
+ * - `bypassTenant`, only when literally `true` AND no tenant is present
1239
+ * (the tenant plugin honours a bypass before a tenant, so carrying both
1240
+ * would widen a scoped call to every tenant);
1241
+ * - `session`, and `user` / `userId` / `requestId` for attribution.
1575
1242
  *
1576
- * Fields beyond this set should be added inline at the call site —
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.
1243
+ * Domain fields beyond this set belong to `createOptionsExtractor`.
1580
1244
  */
1581
1245
  function repoOptionsFromCtx(ctx) {
1582
- if (!ctx) return {};
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;
1246
+ return forwardScope(ctx);
1590
1247
  }
1591
1248
  /**
1592
1249
  * Build a typed extractor for **consumer-defined** ctx → options
@@ -1608,12 +1265,11 @@ function repoOptionsFromCtx(ctx) {
1608
1265
  * output keys are inferred from the input field tuple — you can
1609
1266
  * destructure with full autocomplete.
1610
1267
  *
1611
- * **Difference from `repoOptionsFromCtx`.** That helper hardcodes the
1612
- * fields **mongokit's bundled plugins** read (`organizationId`,
1613
- * `userId`, `user`, `session`, `requestId`). Use it when those are
1614
- * sufficient. Reach for `createOptionsExtractor` when your domain has
1615
- * its own canonical fields (`actorRef`, `actorKind`, `correlationId`,
1616
- * `idempotencyKey`, `sagaRunId`, ...) that should also forward.
1268
+ * **Scope is always included, and never from `fields`.** The extractor
1269
+ * returns `forwardScope(ctx)` plus the declared DOMAIN fields. Listing a
1270
+ * scope key (`organizationId`, `bypassTenant`, `session`, …) in `fields`
1271
+ * changes nothing: those follow the one scope rule, so an extractor cannot
1272
+ * forget the bypass, or carry a bypass beside a tenant.
1617
1273
  *
1618
1274
  * @example Per-package canonical extractor
1619
1275
  * ```ts
@@ -1628,12 +1284,12 @@ function repoOptionsFromCtx(ctx) {
1628
1284
  * idempotencyKey?: string;
1629
1285
  * };
1630
1286
  *
1287
+ * // Domain fields only: the scope (organizationId, session, bypass) is
1288
+ * // always included by the one scope rule.
1631
1289
  * export const repoOptionsFromCtx = createOptionsExtractor<CommissionCtx>([
1632
- * 'organizationId',
1633
1290
  * 'actorRef',
1634
1291
  * 'actorKind',
1635
1292
  * 'correlationId',
1636
- * 'session',
1637
1293
  * 'idempotencyKey',
1638
1294
  * ]);
1639
1295
  *
@@ -1649,11 +1305,11 @@ function repoOptionsFromCtx(ctx) {
1649
1305
  * options bag never erases inherited values).
1650
1306
  */
1651
1307
  function createOptionsExtractor(fields) {
1652
- const frozen = Object.freeze([...fields]);
1308
+ const domainFields = Object.freeze(fields.filter((field) => !isScopeKey(field)));
1653
1309
  return function extractRepoOptions(ctx) {
1654
1310
  if (!ctx) return {};
1655
- const out = {};
1656
- for (const field of frozen) {
1311
+ const out = forwardScope(ctx);
1312
+ for (const field of domainFields) {
1657
1313
  const v = ctx[field];
1658
1314
  if (v !== void 0) out[field] = v;
1659
1315
  }
@@ -1781,4 +1437,4 @@ function systemContext(extra) {
1781
1437
  */
1782
1438
  var src_default = Repository;
1783
1439
  //#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, configurePaginationDefaults, createError, createFieldPreset, createMemoryCache, createOptionsExtractor, createRepository, createTenantContext, customIdPlugin, dateSequentialId, src_default as default, defineModel, defineModels, elasticSearchPlugin, ensureAuditTrailReady, fieldFilterPlugin, filterResponseData, getFieldsForUser, getMongooseProjection, getNextSequence, getPaginationDefaults, idVariants, idempotencyStorePlugin, immutableField, immutableStatesPlugin, isCreateManyPartialError, isDuplicateKeyError, isModelRegistered, isTransactionUnsupported, leasePlugin, methodRegistryPlugin, mongoOperationsPlugin, multiTenantPlugin, observabilityPlugin, operationsByPolicyKey, parseDuplicateKeyError, prefixedId, probeMongoCapabilities, repoOptionsFromCtx, requireField, resetCapabilityProbeCache, resetPaginationDefaults, resolveMongoCapabilities, resolveTransactionSupport, sequentialId, softDeletePlugin, subdocumentPlugin, supportsTransactions, systemContext, timestampPlugin, toPlain, transactionResolutionOf, uniqueField, validationChainPlugin, withTransaction };
1440
+ export { ALL_OPERATIONS, AggregationBuilder, AuditTrailQuery, HOOK_PRIORITY, JOIN_ERROR_CODES, LookupBuilder, MONGOKIT_CAPABILITIES, MONGOKIT_PLUGIN_NAMES, MUTATING_OPERATIONS, ModelCollisionError, OP_REGISTRY, PAGINATION_ERROR_CODES, PaginationEngine, QUERY_DEFAULTS_ERROR_CODES, QueryParser, READ_OPERATIONS, Repository, actions_exports as actions, adminBypass, aggregateHelpersPlugin, appendOnlyPlugin, assertQueryDefaultsConfigured, auditLogPlugin, auditTrailPlugin, autoInject, batchOperationsPlugin, batchTransaction, blockIf, buildCrudSchemasFromModel, buildCrudSchemasFromMongooseSchema, cachePlugin, capabilitiesForSupport, cascadePlugin, cascadePurgeReferences, classifyHelloReply, compileFilterToMongo, configureAggregateDefaults, configureLogger, configurePaginationDefaults, configureQueryDefaults, createError, createFieldPreset, createMemoryCache, createOptionsExtractor, createRepository, createTenantContext, customIdPlugin, dateSequentialId, declareCollectionScope, declareTenantContextKey, src_default as default, defineModel, defineModels, elasticSearchPlugin, ensureAuditTrailReady, ensureSequenceAtLeast, fieldFilterPlugin, filterResponseData, forwardScope, getFieldsForUser, getMongooseProjection, getNextSequence, getPaginationDefaults, getQueryDefaults, idVariants, idempotencyStorePlugin, immutableField, immutableStatesPlugin, isCreateManyPartialError, isDuplicateKeyError, isModelRegistered, isTransactionUnsupported, keysetSource, leasePlugin, methodRegistryPlugin, mongoOperationsPlugin, multiTenantPlugin, observabilityPlugin, operationsByPolicyKey, parseDuplicateKeyError, prefixedId, probeMongoCapabilities, readSequence, repoOptionsFromCtx, requireField, resetCapabilityProbeCache, resetPaginationDefaults, resetQueryDefaults, resolveMongoCapabilities, resolveQueryOptions, resolveTransactionSupport, sequentialId, softDeletePlugin, subdocumentPlugin, supportsTransactions, systemContext, tenantContextKeysOf, timestampPlugin, toPlain, transactionResolutionOf, uniqueField, validationChainPlugin, withTransaction };