@classytic/mongokit 3.5.3 → 3.5.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +163 -4
- package/dist/{PaginationEngine-DCs-zKwZ.mjs → PaginationEngine-CpsQl471.mjs} +5 -3
- package/dist/actions/index.d.mts +1 -1
- package/dist/ai/index.d.mts +1 -1
- package/dist/coercion-01tEHsKF.d.mts +106 -0
- package/dist/{index-D3ydSXYC.d.mts → index-Buj7P006.d.mts} +1 -1
- package/dist/index.d.mts +69 -6
- package/dist/index.mjs +100 -35
- package/dist/indexes-CiIT-Zli.d.mts +41 -0
- package/dist/{mongooseToJsonSchema-BivI9LWt.d.mts → mongooseToJsonSchema-BlDfbg_0.d.mts} +1 -1
- package/dist/{mongooseToJsonSchema-B6Qyl8BK.mjs → mongooseToJsonSchema-DpjhFfaO.mjs} +6 -1
- package/dist/pagination/PaginationEngine.d.mts +1 -1
- package/dist/pagination/PaginationEngine.mjs +1 -1
- package/dist/plugins/index.d.mts +1 -1
- package/dist/plugins/index.mjs +1 -1
- package/dist/query/primitives/coercion.d.mts +2 -0
- package/dist/query/primitives/coercion.mjs +147 -0
- package/dist/query/primitives/geo.d.mts +116 -0
- package/dist/query/primitives/geo.mjs +211 -0
- package/dist/query/primitives/indexes.d.mts +2 -0
- package/dist/query/primitives/indexes.mjs +46 -0
- package/dist/{types-wCkH8jE3.d.mts → types-CWUL0yiw.d.mts} +59 -0
- package/dist/utils/index.d.mts +2 -2
- package/dist/utils/index.mjs +1 -1
- package/dist/{validation-chain.plugin-KjPV_6_N.mjs → validation-chain.plugin-BCPh2UZG.mjs} +48 -11
- package/dist/{validation-chain.plugin-CGE_mKQr.d.mts → validation-chain.plugin-Dn41aAbK.d.mts} +1 -1
- package/package.json +15 -3
package/dist/index.mjs
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { a as warn, n as parseDuplicateKeyError, r as configureLogger, t as createError } from "./error-Bpbi_NKo.mjs";
|
|
2
2
|
import { y as LookupBuilder } from "./update-AVfKWNGt.mjs";
|
|
3
3
|
import { t as actions_exports } from "./actions/index.mjs";
|
|
4
|
-
import { t as PaginationEngine } from "./PaginationEngine-
|
|
5
|
-
import { A as aggregateHelpersPlugin, C as HOOK_PRIORITY, D as AuditTrailQuery, E as batchOperationsPlugin, O as auditTrailPlugin, S as cachePlugin, T as AggregationBuilder, _ as dateSequentialId, a as uniqueField, b as sequentialId, c as subdocumentPlugin, d as multiTenantPlugin, f as mongoOperationsPlugin, g as customIdPlugin, h as elasticSearchPlugin, i as requireField, k as auditLogPlugin, l as softDeletePlugin, m as fieldFilterPlugin, n as blockIf, o as validationChainPlugin, p as methodRegistryPlugin, r as immutableField, s as timestampPlugin, t as autoInject, u as observabilityPlugin, v as getNextSequence, w as Repository, x as cascadePlugin, y as prefixedId } from "./validation-chain.plugin-
|
|
4
|
+
import { t as PaginationEngine } from "./PaginationEngine-CpsQl471.mjs";
|
|
5
|
+
import { A as aggregateHelpersPlugin, C as HOOK_PRIORITY, D as AuditTrailQuery, E as batchOperationsPlugin, O as auditTrailPlugin, S as cachePlugin, T as AggregationBuilder, _ as dateSequentialId, a as uniqueField, b as sequentialId, c as subdocumentPlugin, d as multiTenantPlugin, f as mongoOperationsPlugin, g as customIdPlugin, h as elasticSearchPlugin, i as requireField, k as auditLogPlugin, l as softDeletePlugin, m as fieldFilterPlugin, n as blockIf, o as validationChainPlugin, p as methodRegistryPlugin, r as immutableField, s as timestampPlugin, t as autoInject, u as observabilityPlugin, v as getNextSequence, w as Repository, x as cascadePlugin, y as prefixedId } from "./validation-chain.plugin-BCPh2UZG.mjs";
|
|
6
|
+
import { isGeoOperator, parseGeoFilter } from "./query/primitives/geo.mjs";
|
|
6
7
|
import { i as getMongooseProjection, n as filterResponseData, r as getFieldsForUser, t as createFieldPreset } from "./field-selection-reyDRzXf.mjs";
|
|
7
|
-
import {
|
|
8
|
-
import
|
|
8
|
+
import { buildFieldTypeMap, coerceFieldValue, coerceHeuristic } from "./query/primitives/coercion.mjs";
|
|
9
|
+
import { extractSchemaIndexes } from "./query/primitives/indexes.mjs";
|
|
10
|
+
import { a as isFieldUpdateAllowed, i as getSystemManagedFields, n as buildCrudSchemasFromMongooseSchema, o as validateUpdateBody, r as getImmutableFields, s as createMemoryCache, t as buildCrudSchemasFromModel } from "./mongooseToJsonSchema-DpjhFfaO.mjs";
|
|
9
11
|
//#region src/query/QueryParser.ts
|
|
10
12
|
/**
|
|
11
13
|
* Modern Query Parser
|
|
@@ -31,6 +33,21 @@ var QueryParser = class {
|
|
|
31
33
|
};
|
|
32
34
|
dangerousOperators;
|
|
33
35
|
/**
|
|
36
|
+
* Normalized dot-notation field-type map. Built once at construction from
|
|
37
|
+
* `options.schema` (Mongoose) and `options.fieldTypes` (override map), with
|
|
38
|
+
* `fieldTypes` taking precedence. An empty map means schema-aware coercion
|
|
39
|
+
* is disabled and the heuristic in `_convertValue` runs unchanged — this
|
|
40
|
+
* preserves backwards compatibility for callers that don't opt in.
|
|
41
|
+
*/
|
|
42
|
+
_fieldTypes;
|
|
43
|
+
/**
|
|
44
|
+
* Structured schema-index info (geo / text / other), built once from
|
|
45
|
+
* `options.schema?.indexes()`. Used by the operator router to validate
|
|
46
|
+
* geo operators against actual geo-indexed fields, and exposed publicly
|
|
47
|
+
* via `parser.schemaIndexes` for downstream tools (Arc MCP, query planners).
|
|
48
|
+
*/
|
|
49
|
+
_schemaIndexes;
|
|
50
|
+
/**
|
|
34
51
|
* Regex patterns that can cause catastrophic backtracking (ReDoS attacks)
|
|
35
52
|
* Detects:
|
|
36
53
|
* - Quantifiers: {n,m}
|
|
@@ -67,6 +84,20 @@ var QueryParser = class {
|
|
|
67
84
|
"$expr",
|
|
68
85
|
...this.options.additionalDangerousOperators
|
|
69
86
|
];
|
|
87
|
+
this._fieldTypes = buildFieldTypeMap(options.schema, options.fieldTypes);
|
|
88
|
+
this._schemaIndexes = extractSchemaIndexes(options.schema);
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Structured view of the configured schema's indexes — geo fields, text
|
|
92
|
+
* fields, and other compound indexes. Empty arrays when no schema was
|
|
93
|
+
* provided. Stable across the parser's lifetime; mutating the returned
|
|
94
|
+
* object does not affect parser behavior.
|
|
95
|
+
*
|
|
96
|
+
* Useful for downstream tooling (Arc MCP, query planners, doc generators)
|
|
97
|
+
* that needs to know which fields support which query types.
|
|
98
|
+
*/
|
|
99
|
+
get schemaIndexes() {
|
|
100
|
+
return this._schemaIndexes;
|
|
70
101
|
}
|
|
71
102
|
/**
|
|
72
103
|
* Get the configured allowed filter fields.
|
|
@@ -520,7 +551,7 @@ var QueryParser = class {
|
|
|
520
551
|
*/
|
|
521
552
|
_convertPopulateMatch(match) {
|
|
522
553
|
const converted = {};
|
|
523
|
-
for (const [key, value] of Object.entries(match)) converted[key] =
|
|
554
|
+
for (const [key, value] of Object.entries(match)) converted[key] = coerceHeuristic(value);
|
|
524
555
|
return converted;
|
|
525
556
|
}
|
|
526
557
|
/**
|
|
@@ -569,7 +600,7 @@ var QueryParser = class {
|
|
|
569
600
|
continue;
|
|
570
601
|
}
|
|
571
602
|
if (typeof value === "object" && value !== null && !Array.isArray(value)) this._handleBracketSyntax(key, value, parsedFilters, depth + 1);
|
|
572
|
-
else parsedFilters[key] = this.
|
|
603
|
+
else parsedFilters[key] = coerceFieldValue(key, value, this._fieldTypes);
|
|
573
604
|
}
|
|
574
605
|
return parsedFilters;
|
|
575
606
|
}
|
|
@@ -597,12 +628,18 @@ var QueryParser = class {
|
|
|
597
628
|
}
|
|
598
629
|
return;
|
|
599
630
|
}
|
|
631
|
+
if (isGeoOperator(operator)) {
|
|
632
|
+
const geoFilter = parseGeoFilter(operator, value);
|
|
633
|
+
if (geoFilter) filters[field] = geoFilter;
|
|
634
|
+
else warn(`[mongokit] Invalid geo operator value for ${field}[${operator}]; dropping filter`);
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
600
637
|
const mongoOperator = this._toMongoOperator(operator);
|
|
601
638
|
if (this.dangerousOperators.includes(mongoOperator)) {
|
|
602
639
|
warn(`[mongokit] Blocked dangerous operator: ${mongoOperator}`);
|
|
603
640
|
return;
|
|
604
641
|
}
|
|
605
|
-
if (mongoOperator === "$eq") filters[field] = value;
|
|
642
|
+
if (mongoOperator === "$eq") filters[field] = coerceFieldValue(field, value, this._fieldTypes);
|
|
606
643
|
else if (mongoOperator === "$regex") {
|
|
607
644
|
const safeRegex = this._createSafeRegex(value);
|
|
608
645
|
if (safeRegex) {
|
|
@@ -612,17 +649,23 @@ var QueryParser = class {
|
|
|
612
649
|
} else {
|
|
613
650
|
let processedValue;
|
|
614
651
|
const op = operator.toLowerCase();
|
|
615
|
-
if (
|
|
652
|
+
if (op === "size") {
|
|
653
|
+
processedValue = parseFloat(String(value));
|
|
654
|
+
if (Number.isNaN(processedValue)) return;
|
|
655
|
+
} else if ([
|
|
616
656
|
"gt",
|
|
617
657
|
"gte",
|
|
618
658
|
"lt",
|
|
619
|
-
"lte"
|
|
620
|
-
|
|
621
|
-
|
|
659
|
+
"lte"
|
|
660
|
+
].includes(op)) if (this._fieldTypes.has(field)) {
|
|
661
|
+
processedValue = coerceFieldValue(field, value, this._fieldTypes);
|
|
662
|
+
if (typeof processedValue === "number" && Number.isNaN(processedValue)) return;
|
|
663
|
+
} else {
|
|
622
664
|
processedValue = parseFloat(String(value));
|
|
623
665
|
if (Number.isNaN(processedValue)) return;
|
|
624
|
-
}
|
|
625
|
-
else processedValue =
|
|
666
|
+
}
|
|
667
|
+
else if (op === "in" || op === "nin") processedValue = (Array.isArray(value) ? value : String(value).split(",").map((v) => v.trim())).map((elem) => coerceFieldValue(field, elem, this._fieldTypes));
|
|
668
|
+
else processedValue = coerceFieldValue(field, value, this._fieldTypes);
|
|
626
669
|
if (typeof filters[field] !== "object" || filters[field] === null || Array.isArray(filters[field])) filters[field] = {};
|
|
627
670
|
filters[field][mongoOperator] = processedValue;
|
|
628
671
|
}
|
|
@@ -642,6 +685,15 @@ var QueryParser = class {
|
|
|
642
685
|
parsedFilters[field].between = value;
|
|
643
686
|
continue;
|
|
644
687
|
}
|
|
688
|
+
if (isGeoOperator(operator)) {
|
|
689
|
+
const geoFilter = parseGeoFilter(operator, value);
|
|
690
|
+
if (geoFilter) parsedFilters[field] = geoFilter;
|
|
691
|
+
else {
|
|
692
|
+
warn(`[mongokit] Invalid geo operator value for ${field}[${operator}]; dropping filter`);
|
|
693
|
+
delete parsedFilters[field];
|
|
694
|
+
}
|
|
695
|
+
continue;
|
|
696
|
+
}
|
|
645
697
|
if (this.options.allowedOperators && !this.options.allowedOperators.includes(operator)) {
|
|
646
698
|
warn(`[mongokit] Operator not in allowlist: ${operator}`);
|
|
647
699
|
continue;
|
|
@@ -649,21 +701,27 @@ var QueryParser = class {
|
|
|
649
701
|
if (this.operators[operator]) {
|
|
650
702
|
const mongoOperator = this.operators[operator];
|
|
651
703
|
let processedValue;
|
|
652
|
-
if (
|
|
704
|
+
if (operator === "size") {
|
|
705
|
+
processedValue = parseFloat(String(value));
|
|
706
|
+
if (Number.isNaN(processedValue)) continue;
|
|
707
|
+
} else if ([
|
|
653
708
|
"gt",
|
|
654
709
|
"gte",
|
|
655
710
|
"lt",
|
|
656
|
-
"lte"
|
|
657
|
-
|
|
658
|
-
|
|
711
|
+
"lte"
|
|
712
|
+
].includes(operator)) if (this._fieldTypes.has(field)) {
|
|
713
|
+
processedValue = coerceFieldValue(field, value, this._fieldTypes);
|
|
714
|
+
if (typeof processedValue === "number" && Number.isNaN(processedValue)) continue;
|
|
715
|
+
} else {
|
|
659
716
|
processedValue = parseFloat(String(value));
|
|
660
717
|
if (Number.isNaN(processedValue)) continue;
|
|
661
|
-
}
|
|
718
|
+
}
|
|
719
|
+
else if (operator === "in" || operator === "nin") processedValue = (Array.isArray(value) ? value : String(value).split(",").map((v) => v.trim())).map((elem) => coerceFieldValue(field, elem, this._fieldTypes));
|
|
662
720
|
else if (operator === "like" || operator === "contains" || operator === "regex") {
|
|
663
721
|
const safeRegex = this._createSafeRegex(value);
|
|
664
722
|
if (!safeRegex) continue;
|
|
665
723
|
processedValue = safeRegex;
|
|
666
|
-
} else processedValue = this.
|
|
724
|
+
} else processedValue = coerceFieldValue(field, value, this._fieldTypes);
|
|
667
725
|
parsedFilters[field][mongoOperator] = processedValue;
|
|
668
726
|
}
|
|
669
727
|
}
|
|
@@ -727,17 +785,31 @@ var QueryParser = class {
|
|
|
727
785
|
*/
|
|
728
786
|
_sanitizeMatchConfig(config) {
|
|
729
787
|
const sanitized = {};
|
|
788
|
+
const logicalArrayOps = new Set([
|
|
789
|
+
"$or",
|
|
790
|
+
"$and",
|
|
791
|
+
"$nor"
|
|
792
|
+
]);
|
|
730
793
|
for (const [key, value] of Object.entries(config)) {
|
|
731
794
|
if (this.dangerousOperators.includes(key)) {
|
|
732
795
|
warn(`[mongokit] Blocked dangerous operator in aggregation: ${key}`);
|
|
733
796
|
continue;
|
|
734
797
|
}
|
|
735
798
|
if (value && typeof value === "object" && !Array.isArray(value)) sanitized[key] = this._sanitizeMatchConfig(value);
|
|
736
|
-
else if (Array.isArray(value))
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
799
|
+
else if (Array.isArray(value)) {
|
|
800
|
+
const sanitizedArray = value.map((item) => {
|
|
801
|
+
if (item && typeof item === "object" && !Array.isArray(item)) return this._sanitizeMatchConfig(item);
|
|
802
|
+
return item;
|
|
803
|
+
});
|
|
804
|
+
if (logicalArrayOps.has(key)) {
|
|
805
|
+
const filtered = sanitizedArray.filter((item) => !(item && typeof item === "object" && !Array.isArray(item) && Object.keys(item).length === 0));
|
|
806
|
+
if (filtered.length === 0) {
|
|
807
|
+
warn(`[mongokit] All branches of ${key} were blocked by sanitization; dropping the operator`);
|
|
808
|
+
continue;
|
|
809
|
+
}
|
|
810
|
+
sanitized[key] = filtered;
|
|
811
|
+
} else sanitized[key] = sanitizedArray;
|
|
812
|
+
} else sanitized[key] = value;
|
|
741
813
|
}
|
|
742
814
|
return sanitized;
|
|
743
815
|
}
|
|
@@ -822,22 +894,15 @@ var QueryParser = class {
|
|
|
822
894
|
for (const field of this.options.searchFields) orConditions.push({ [field]: { $regex: safeRegex } });
|
|
823
895
|
return orConditions.length > 0 ? orConditions : null;
|
|
824
896
|
}
|
|
825
|
-
_convertValue(value) {
|
|
826
|
-
if (value === null || value === void 0) return value;
|
|
827
|
-
if (Array.isArray(value)) return value.map((v) => this._convertValue(v));
|
|
828
|
-
if (typeof value === "object") return value;
|
|
829
|
-
const stringValue = String(value);
|
|
830
|
-
if (stringValue === "true") return true;
|
|
831
|
-
if (stringValue === "false") return false;
|
|
832
|
-
if (mongoose.Types.ObjectId.isValid(stringValue) && stringValue.length === 24) return stringValue;
|
|
833
|
-
return stringValue;
|
|
834
|
-
}
|
|
835
897
|
_parseOr(query) {
|
|
836
898
|
const orArray = [];
|
|
837
899
|
const raw = query?.or || query?.OR || query?.$or;
|
|
838
900
|
if (!raw) return void 0;
|
|
839
901
|
const items = Array.isArray(raw) ? raw : typeof raw === "object" ? Object.values(raw) : [];
|
|
840
|
-
for (const item of items) if (typeof item === "object" && item)
|
|
902
|
+
for (const item of items) if (typeof item === "object" && item) {
|
|
903
|
+
const parsedBranch = this._parseFilters(item, 1);
|
|
904
|
+
if (Object.keys(parsedBranch).length > 0) orArray.push(parsedBranch);
|
|
905
|
+
}
|
|
841
906
|
return orArray.length ? orArray : void 0;
|
|
842
907
|
}
|
|
843
908
|
_enhanceWithBetween(filters) {
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
//#region src/query/primitives/indexes.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Schema index introspection
|
|
4
|
+
*
|
|
5
|
+
* Pure functions that read a Mongoose-like schema's `.indexes()` output and
|
|
6
|
+
* extract structured information about which fields are indexed how. Used by
|
|
7
|
+
* QueryParser to:
|
|
8
|
+
*
|
|
9
|
+
* - Detect 2dsphere/2d-indexed fields → enable geo operator routing.
|
|
10
|
+
* - Detect text-indexed fields → expose to repositories that need it.
|
|
11
|
+
* - Surface compound index hints → useful for query planners and Arc MCP
|
|
12
|
+
* auto-generation.
|
|
13
|
+
*
|
|
14
|
+
* Stays decoupled from a specific Mongoose major version: only depends on
|
|
15
|
+
* `.indexes()` returning a list of `[indexSpec, options?]` tuples, which has
|
|
16
|
+
* been stable since Mongoose 4.x.
|
|
17
|
+
*/
|
|
18
|
+
/** Minimal structural type for a Mongoose-like schema. */
|
|
19
|
+
interface IndexableSchema {
|
|
20
|
+
indexes(): Array<[Record<string, unknown>, Record<string, unknown>?]>;
|
|
21
|
+
}
|
|
22
|
+
/** Structured view of a schema's indexes, indexed by purpose. */
|
|
23
|
+
interface SchemaIndexes {
|
|
24
|
+
/** Field paths with a 2dsphere or 2d index — eligible for geo operators */
|
|
25
|
+
geoFields: string[];
|
|
26
|
+
/** Field paths with a text index — eligible for $text queries */
|
|
27
|
+
textFields: string[];
|
|
28
|
+
/** All other indexes as raw spec/options pairs (for hints, plan analysis) */
|
|
29
|
+
other: Array<{
|
|
30
|
+
spec: Record<string, unknown>;
|
|
31
|
+
options?: Record<string, unknown>;
|
|
32
|
+
}>;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Extract structured index information from a Mongoose schema. Returns an
|
|
36
|
+
* empty `SchemaIndexes` shape (not null) when no schema is provided so callers
|
|
37
|
+
* can read `.geoFields`/`.textFields` unconditionally without null checks.
|
|
38
|
+
*/
|
|
39
|
+
declare function extractSchemaIndexes(schema?: IndexableSchema | null): SchemaIndexes;
|
|
40
|
+
//#endregion
|
|
41
|
+
export { SchemaIndexes as n, extractSchemaIndexes as r, IndexableSchema as t };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { T as FieldPreset, _ as CrudSchemas, k as HttpError, ot as SchemaBuilderOptions, s as CacheAdapter, xt as ValidationResult, yt as UserContext } from "./types-
|
|
1
|
+
import { T as FieldPreset, _ as CrudSchemas, k as HttpError, ot as SchemaBuilderOptions, s as CacheAdapter, xt as ValidationResult, yt as UserContext } from "./types-CWUL0yiw.mjs";
|
|
2
2
|
import mongoose, { Schema } from "mongoose";
|
|
3
3
|
|
|
4
4
|
//#region src/utils/error.d.ts
|
|
@@ -154,6 +154,11 @@ function buildJsonSchemaFromPaths(mongooseSchema, options) {
|
|
|
154
154
|
const properties = {};
|
|
155
155
|
const required = [];
|
|
156
156
|
const paths = mongooseSchema.paths;
|
|
157
|
+
const softRequiredSet = new Set(options?.softRequiredFields ?? []);
|
|
158
|
+
const isSoftRequired = (name, schemaType) => {
|
|
159
|
+
if (softRequiredSet.has(name)) return true;
|
|
160
|
+
return schemaType?.options?.softRequired === true;
|
|
161
|
+
};
|
|
157
162
|
const rootFields = /* @__PURE__ */ new Map();
|
|
158
163
|
for (const [path, schemaType] of Object.entries(paths)) {
|
|
159
164
|
if (path === "_id" || path === "__v") continue;
|
|
@@ -167,7 +172,7 @@ function buildJsonSchemaFromPaths(mongooseSchema, options) {
|
|
|
167
172
|
for (const [rootField, fieldPaths] of rootFields.entries()) if (fieldPaths.length === 1 && fieldPaths[0].path === rootField) {
|
|
168
173
|
const schemaType = fieldPaths[0].schemaType;
|
|
169
174
|
properties[rootField] = schemaTypeToJsonSchema(schemaType);
|
|
170
|
-
if (schemaType.isRequired) required.push(rootField);
|
|
175
|
+
if (schemaType.isRequired && !isSoftRequired(rootField, schemaType)) required.push(rootField);
|
|
171
176
|
} else {
|
|
172
177
|
const nestedSchema = buildNestedJsonSchema(fieldPaths, rootField);
|
|
173
178
|
properties[rootField] = nestedSchema.schema;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { F as KeysetPaginationResult, G as PaginationConfig, H as OffsetPaginationOptions, P as KeysetPaginationOptions, U as OffsetPaginationResult, a as AnyDocument, n as AggregatePaginationOptions, r as AggregatePaginationResult } from "../types-
|
|
1
|
+
import { F as KeysetPaginationResult, G as PaginationConfig, H as OffsetPaginationOptions, P as KeysetPaginationOptions, U as OffsetPaginationResult, a as AnyDocument, n as AggregatePaginationOptions, r as AggregatePaginationResult } from "../types-CWUL0yiw.mjs";
|
|
2
2
|
import { Model } from "mongoose";
|
|
3
3
|
|
|
4
4
|
//#region src/pagination/PaginationEngine.d.ts
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { t as PaginationEngine } from "../PaginationEngine-
|
|
1
|
+
import { t as PaginationEngine } from "../PaginationEngine-CpsQl471.mjs";
|
|
2
2
|
export { PaginationEngine };
|
package/dist/plugins/index.d.mts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { A as dateSequentialId, B as AuditEntry, C as elasticSearchPlugin, D as PrefixedIdOptions, E as IdGenerator, F as CacheMethods, G as AuditTrailOptions, H as AuditQueryOptions, I as cachePlugin, J as auditLogPlugin, K as AuditTrailQuery, L as BatchOperationsMethods, M as prefixedId, N as sequentialId, O as SequentialIdOptions, P as cascadePlugin, S as ElasticSearchOptions, T as DateSequentialIdOptions, U as AuditQueryResult, V as AuditOperation, W as AuditTrailMethods, X as aggregateHelpersPlugin, Y as AggregateHelpersMethods, _ as MongoOperationsMethods, a as uniqueField, b as methodRegistryPlugin, c as SubdocumentMethods, d as softDeletePlugin, f as ObservabilityOptions, g as multiTenantPlugin, h as MultiTenantOptions, i as requireField, j as getNextSequence, k as customIdPlugin, l as subdocumentPlugin, m as observabilityPlugin, n as blockIf, o as validationChainPlugin, p as OperationMetric, q as auditTrailPlugin, r as immutableField, s as timestampPlugin, t as autoInject, u as SoftDeleteMethods, v as mongoOperationsPlugin, w as CustomIdOptions, x as fieldFilterPlugin, y as MethodRegistryRepository, z as batchOperationsPlugin } from "../validation-chain.plugin-
|
|
1
|
+
import { A as dateSequentialId, B as AuditEntry, C as elasticSearchPlugin, D as PrefixedIdOptions, E as IdGenerator, F as CacheMethods, G as AuditTrailOptions, H as AuditQueryOptions, I as cachePlugin, J as auditLogPlugin, K as AuditTrailQuery, L as BatchOperationsMethods, M as prefixedId, N as sequentialId, O as SequentialIdOptions, P as cascadePlugin, S as ElasticSearchOptions, T as DateSequentialIdOptions, U as AuditQueryResult, V as AuditOperation, W as AuditTrailMethods, X as aggregateHelpersPlugin, Y as AggregateHelpersMethods, _ as MongoOperationsMethods, a as uniqueField, b as methodRegistryPlugin, c as SubdocumentMethods, d as softDeletePlugin, f as ObservabilityOptions, g as multiTenantPlugin, h as MultiTenantOptions, i as requireField, j as getNextSequence, k as customIdPlugin, l as subdocumentPlugin, m as observabilityPlugin, n as blockIf, o as validationChainPlugin, p as OperationMetric, q as auditTrailPlugin, r as immutableField, s as timestampPlugin, t as autoInject, u as SoftDeleteMethods, v as mongoOperationsPlugin, w as CustomIdOptions, x as fieldFilterPlugin, y as MethodRegistryRepository, z as batchOperationsPlugin } from "../validation-chain.plugin-Dn41aAbK.mjs";
|
|
2
2
|
export { type AggregateHelpersMethods, type AuditEntry, type AuditOperation, type AuditQueryOptions, type AuditQueryResult, type AuditTrailMethods, type AuditTrailOptions, AuditTrailQuery, type BatchOperationsMethods, type CacheMethods, type CustomIdOptions, type DateSequentialIdOptions, type ElasticSearchOptions, type IdGenerator, type MethodRegistryRepository, type MongoOperationsMethods, type MultiTenantOptions, type ObservabilityOptions, type OperationMetric, type PrefixedIdOptions, type SequentialIdOptions, type SoftDeleteMethods, type SubdocumentMethods, aggregateHelpersPlugin, auditLogPlugin, auditTrailPlugin, autoInject, batchOperationsPlugin, blockIf, cachePlugin, cascadePlugin, customIdPlugin, dateSequentialId, elasticSearchPlugin, fieldFilterPlugin, getNextSequence, immutableField, methodRegistryPlugin, mongoOperationsPlugin, multiTenantPlugin, observabilityPlugin, prefixedId, requireField, sequentialId, softDeletePlugin, subdocumentPlugin, timestampPlugin, uniqueField, validationChainPlugin };
|
package/dist/plugins/index.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { A as aggregateHelpersPlugin, D as AuditTrailQuery, E as batchOperationsPlugin, O as auditTrailPlugin, S as cachePlugin, _ as dateSequentialId, a as uniqueField, b as sequentialId, c as subdocumentPlugin, d as multiTenantPlugin, f as mongoOperationsPlugin, g as customIdPlugin, h as elasticSearchPlugin, i as requireField, k as auditLogPlugin, l as softDeletePlugin, m as fieldFilterPlugin, n as blockIf, o as validationChainPlugin, p as methodRegistryPlugin, r as immutableField, s as timestampPlugin, t as autoInject, u as observabilityPlugin, v as getNextSequence, x as cascadePlugin, y as prefixedId } from "../validation-chain.plugin-
|
|
1
|
+
import { A as aggregateHelpersPlugin, D as AuditTrailQuery, E as batchOperationsPlugin, O as auditTrailPlugin, S as cachePlugin, _ as dateSequentialId, a as uniqueField, b as sequentialId, c as subdocumentPlugin, d as multiTenantPlugin, f as mongoOperationsPlugin, g as customIdPlugin, h as elasticSearchPlugin, i as requireField, k as auditLogPlugin, l as softDeletePlugin, m as fieldFilterPlugin, n as blockIf, o as validationChainPlugin, p as methodRegistryPlugin, r as immutableField, s as timestampPlugin, t as autoInject, u as observabilityPlugin, v as getNextSequence, x as cascadePlugin, y as prefixedId } from "../validation-chain.plugin-BCPh2UZG.mjs";
|
|
2
2
|
export { AuditTrailQuery, aggregateHelpersPlugin, auditLogPlugin, auditTrailPlugin, autoInject, batchOperationsPlugin, blockIf, cachePlugin, cascadePlugin, customIdPlugin, dateSequentialId, elasticSearchPlugin, fieldFilterPlugin, getNextSequence, immutableField, methodRegistryPlugin, mongoOperationsPlugin, multiTenantPlugin, observabilityPlugin, prefixedId, requireField, sequentialId, softDeletePlugin, subdocumentPlugin, timestampPlugin, uniqueField, validationChainPlugin };
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { a as coerceHeuristic, i as coerceFieldValue, n as SchemaPathsLike, o as coerceToType, r as buildFieldTypeMap, s as normalizeMongooseType, t as FieldType } from "../../coercion-01tEHsKF.mjs";
|
|
2
|
+
export { FieldType, SchemaPathsLike, buildFieldTypeMap, coerceFieldValue, coerceHeuristic, coerceToType, normalizeMongooseType };
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import mongoose from "mongoose";
|
|
2
|
+
//#region src/query/primitives/coercion.ts
|
|
3
|
+
/**
|
|
4
|
+
* Value coercion primitives
|
|
5
|
+
*
|
|
6
|
+
* Pure functions that convert raw URL-decoded values into typed values
|
|
7
|
+
* suitable for MongoDB queries. Two layers:
|
|
8
|
+
*
|
|
9
|
+
* 1. `coerceHeuristic(value)` — string-shape based, used when no schema
|
|
10
|
+
* is configured. Safe defaults: rejects leading zeros, scientific
|
|
11
|
+
* notation, and strings >15 chars to preserve zip codes, phone codes,
|
|
12
|
+
* and long numeric IDs that exceed JS safe-integer precision.
|
|
13
|
+
*
|
|
14
|
+
* 2. `coerceToType(value, type)` — type-directed, used when a Mongoose
|
|
15
|
+
* schema or `fieldTypes` map declares the field's type. Strictly
|
|
16
|
+
* better than the heuristic when type information is available.
|
|
17
|
+
*
|
|
18
|
+
* QueryParser composes these via `coerceFieldValue(field, value, types)`
|
|
19
|
+
* which prefers the type-directed path and falls back to the heuristic.
|
|
20
|
+
*
|
|
21
|
+
* Field-type extraction from a Mongoose schema is also here so the
|
|
22
|
+
* normalization logic lives next to the coercion that consumes it.
|
|
23
|
+
*/
|
|
24
|
+
/**
|
|
25
|
+
* Map a Mongoose `SchemaType.instance` string to our normalized FieldType.
|
|
26
|
+
* For arrays (`type: [Number]` or `type: [{ type: Number }]`), Mongoose
|
|
27
|
+
* stores the element type on `embeddedSchemaType.instance` (Mongoose 8+) or
|
|
28
|
+
* `caster.instance` (older). We probe both — element-level coercion is what
|
|
29
|
+
* `[in]`/`[nin]` operators need against typed arrays.
|
|
30
|
+
*
|
|
31
|
+
* Returns null for paths we cannot classify (embedded documents handled by
|
|
32
|
+
* their own nested paths).
|
|
33
|
+
*/
|
|
34
|
+
function normalizeMongooseType(schemaType) {
|
|
35
|
+
const raw = schemaType.embeddedSchemaType?.instance ?? schemaType.caster?.instance ?? schemaType.instance;
|
|
36
|
+
if (!raw) return null;
|
|
37
|
+
switch (raw) {
|
|
38
|
+
case "String": return "string";
|
|
39
|
+
case "Number": return "number";
|
|
40
|
+
case "Boolean": return "boolean";
|
|
41
|
+
case "Date": return "date";
|
|
42
|
+
case "ObjectID":
|
|
43
|
+
case "ObjectId": return "objectid";
|
|
44
|
+
case "Mixed": return "mixed";
|
|
45
|
+
default: return null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Build a normalized field-type map from a Mongoose schema and an optional
|
|
50
|
+
* override map. The override wins on a per-path basis. Paths with unmappable
|
|
51
|
+
* types are silently skipped — `coerceFieldValue` will fall back to the
|
|
52
|
+
* heuristic for them.
|
|
53
|
+
*/
|
|
54
|
+
function buildFieldTypeMap(schema, overrides) {
|
|
55
|
+
const map = /* @__PURE__ */ new Map();
|
|
56
|
+
if (schema?.paths) for (const [path, schemaType] of Object.entries(schema.paths)) {
|
|
57
|
+
const normalized = normalizeMongooseType(schemaType);
|
|
58
|
+
if (normalized) map.set(path, normalized);
|
|
59
|
+
}
|
|
60
|
+
if (overrides) for (const [path, type] of Object.entries(overrides)) map.set(path, type);
|
|
61
|
+
return map;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* String-shape heuristic — used when no field type is known.
|
|
65
|
+
*
|
|
66
|
+
* Rules (deliberately conservative — silent miscoercion is worse than
|
|
67
|
+
* leaving a string):
|
|
68
|
+
* - `null`/`undefined` pass through
|
|
69
|
+
* - Already-typed primitives (number, boolean) pass through
|
|
70
|
+
* - Arrays recurse element-wise
|
|
71
|
+
* - Objects pass through (filter operator objects, etc.)
|
|
72
|
+
* - `'true'`/`'false'` → boolean
|
|
73
|
+
* - 24-char hex → kept as string (ObjectId)
|
|
74
|
+
* - Strings 1–15 chars matching `^-?(0|[1-9]\d*)(\.\d+)?$` → number
|
|
75
|
+
* (rejects leading zeros, scientific notation, hex/octal, NaN, Infinity)
|
|
76
|
+
* - Everything else stays a string
|
|
77
|
+
*/
|
|
78
|
+
function coerceHeuristic(value) {
|
|
79
|
+
if (value === null || value === void 0) return value;
|
|
80
|
+
if (Array.isArray(value)) return value.map((v) => coerceHeuristic(v));
|
|
81
|
+
if (typeof value === "object") return value;
|
|
82
|
+
if (typeof value === "number" || typeof value === "boolean") return value;
|
|
83
|
+
const s = String(value);
|
|
84
|
+
if (s === "true") return true;
|
|
85
|
+
if (s === "false") return false;
|
|
86
|
+
if (mongoose.Types.ObjectId.isValid(s) && s.length === 24) return s;
|
|
87
|
+
if (s.length > 0 && s.length <= 15 && /^-?(?:0|[1-9]\d*)(?:\.\d+)?$/.test(s)) {
|
|
88
|
+
const n = Number(s);
|
|
89
|
+
if (Number.isFinite(n)) return n;
|
|
90
|
+
}
|
|
91
|
+
return s;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Type-directed coercion — used when a field's declared type is known.
|
|
95
|
+
*
|
|
96
|
+
* Returns the original value unchanged when coercion is impossible
|
|
97
|
+
* (invalid date, non-numeric string for a Number field) — better to surface
|
|
98
|
+
* a no-match than to silently produce `Invalid Date` or `NaN` which MongoDB
|
|
99
|
+
* treats as legitimate query values.
|
|
100
|
+
*/
|
|
101
|
+
function coerceToType(value, type) {
|
|
102
|
+
if (value === null || value === void 0) return value;
|
|
103
|
+
if (Array.isArray(value)) return value.map((v) => coerceToType(v, type));
|
|
104
|
+
if (typeof value === "object") return value;
|
|
105
|
+
switch (type) {
|
|
106
|
+
case "number": {
|
|
107
|
+
if (typeof value === "number") return value;
|
|
108
|
+
const num = Number(String(value));
|
|
109
|
+
return Number.isFinite(num) ? num : value;
|
|
110
|
+
}
|
|
111
|
+
case "boolean": {
|
|
112
|
+
if (typeof value === "boolean") return value;
|
|
113
|
+
const s = String(value).toLowerCase();
|
|
114
|
+
if (s === "true" || s === "1") return true;
|
|
115
|
+
if (s === "false" || s === "0") return false;
|
|
116
|
+
return value;
|
|
117
|
+
}
|
|
118
|
+
case "date": {
|
|
119
|
+
if (value instanceof Date) return value;
|
|
120
|
+
const d = new Date(String(value));
|
|
121
|
+
return Number.isNaN(d.getTime()) ? value : d;
|
|
122
|
+
}
|
|
123
|
+
case "objectid": {
|
|
124
|
+
const sv = String(value);
|
|
125
|
+
if (mongoose.Types.ObjectId.isValid(sv) && sv.length === 24) return sv;
|
|
126
|
+
return value;
|
|
127
|
+
}
|
|
128
|
+
case "string": return String(value);
|
|
129
|
+
case "mixed": return coerceHeuristic(value);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Field-aware coercion. Looks up `field` in `types` and uses
|
|
134
|
+
* `coerceToType` when found, otherwise falls back to `coerceHeuristic`.
|
|
135
|
+
*
|
|
136
|
+
* This is the single choke point QueryParser uses for typed coercion of
|
|
137
|
+
* direct equality, `[eq]`, `[in]`, `[nin]`, `[gt]`/`[gte]`/`[lt]`/`[lte]`
|
|
138
|
+
* (the last only when a type is declared — see QueryParser for the
|
|
139
|
+
* preserves-pre-3.5.5-behavior fallback).
|
|
140
|
+
*/
|
|
141
|
+
function coerceFieldValue(field, value, types) {
|
|
142
|
+
const type = types.get(field);
|
|
143
|
+
if (!type) return coerceHeuristic(value);
|
|
144
|
+
return coerceToType(value, type);
|
|
145
|
+
}
|
|
146
|
+
//#endregion
|
|
147
|
+
export { buildFieldTypeMap, coerceFieldValue, coerceHeuristic, coerceToType, normalizeMongooseType };
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
//#region src/query/primitives/geo.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Geo query primitives
|
|
4
|
+
*
|
|
5
|
+
* Pure functions that translate ergonomic URL syntax into the canonical
|
|
6
|
+
* GeoJSON-shaped MongoDB query operators ($near, $nearSphere, $geoWithin).
|
|
7
|
+
*
|
|
8
|
+
* Coordinate convention: ALWAYS lng,lat (matches GeoJSON, opposite of Google
|
|
9
|
+
* Maps' lat,lng). The parser is responsible for documenting this so users
|
|
10
|
+
* don't pass coordinates in the wrong order.
|
|
11
|
+
*
|
|
12
|
+
* These functions are pure (no `this`, no I/O) and unit-testable in isolation.
|
|
13
|
+
* QueryParser orchestrates by calling them when it sees a `[near]`,
|
|
14
|
+
* `[nearSphere]`, or `[geoWithin]` operator on a field.
|
|
15
|
+
*/
|
|
16
|
+
/** Geo operators recognized by the parser */
|
|
17
|
+
declare const GEO_OPERATORS: readonly ["near", "nearSphere", "geoWithin", "withinRadius"];
|
|
18
|
+
type GeoOperator = (typeof GEO_OPERATORS)[number];
|
|
19
|
+
/**
|
|
20
|
+
* True iff `operator` is one of the geo operators we handle.
|
|
21
|
+
* Used by the operator router in QueryParser to short-circuit before the
|
|
22
|
+
* generic numeric/string operator handling kicks in.
|
|
23
|
+
*/
|
|
24
|
+
declare function isGeoOperator(operator: string): operator is GeoOperator;
|
|
25
|
+
/**
|
|
26
|
+
* Parse a comma-separated coordinate list into numbers, validating each value
|
|
27
|
+
* is a finite number. Returns null on any parse failure — callers must treat
|
|
28
|
+
* null as "drop this filter entirely" rather than substituting defaults
|
|
29
|
+
* (which would silently widen the query).
|
|
30
|
+
*/
|
|
31
|
+
declare function parseCoordinateList(raw: unknown): number[] | null;
|
|
32
|
+
/**
|
|
33
|
+
* Validate a [lng, lat] pair against MongoDB's accepted ranges.
|
|
34
|
+
* Longitude: [-180, 180]. Latitude: [-90, 90]. Out-of-range coordinates
|
|
35
|
+
* return false so the parser can drop the filter — emitting a $near query
|
|
36
|
+
* with `coordinates: [999, 40]` would either error inside Mongo or worse,
|
|
37
|
+
* silently behave unexpectedly.
|
|
38
|
+
*/
|
|
39
|
+
declare function isValidLngLat(lng: number, lat: number): boolean;
|
|
40
|
+
/**
|
|
41
|
+
* Build a `$near` or `$nearSphere` filter for a single field from a coordinate
|
|
42
|
+
* list. Accepts:
|
|
43
|
+
* [lng, lat] — proximity sort, no distance bound
|
|
44
|
+
* [lng, lat, maxDistance] — proximity sort within `maxDistance` meters
|
|
45
|
+
*
|
|
46
|
+
* Returns null on any validation failure (drop the filter).
|
|
47
|
+
*/
|
|
48
|
+
declare function buildNearFilter(variant: 'near' | 'nearSphere', coords: number[]): Record<string, unknown> | null;
|
|
49
|
+
/**
|
|
50
|
+
* Build a `$geoWithin: $centerSphere` filter from [lng, lat, radiusMeters].
|
|
51
|
+
*
|
|
52
|
+
* IMPORTANT: this is the count-compatible alternative to `$near`. MongoDB
|
|
53
|
+
* forbids `$near` / `$nearSphere` in any context that requires sorting (which
|
|
54
|
+
* `countDocuments`, `$lookup`, `$facet`, and several others do), because they
|
|
55
|
+
* are themselves sort operators. `$geoWithin: $centerSphere` is a *filter*
|
|
56
|
+
* operator, returns the same set of documents within a radius, and works
|
|
57
|
+
* everywhere `$near` does not — including paginated `Repository.getAll`.
|
|
58
|
+
*
|
|
59
|
+
* Use `[near]` when you want results sorted by distance and don't need a
|
|
60
|
+
* total count. Use `[withinRadius]` for pagination, counts, and joins.
|
|
61
|
+
*
|
|
62
|
+
* `$centerSphere` takes the radius in radians, so we convert from the
|
|
63
|
+
* caller-friendly meters using Earth's equatorial radius.
|
|
64
|
+
*/
|
|
65
|
+
declare function buildWithinRadiusFilter(coords: number[]): Record<string, unknown> | null;
|
|
66
|
+
/**
|
|
67
|
+
* Build a `$geoWithin: $box` filter from a 4-element coordinate list:
|
|
68
|
+
* [minLng, minLat, maxLng, maxLat]
|
|
69
|
+
*
|
|
70
|
+
* Returns null on any validation failure (drop the filter).
|
|
71
|
+
*/
|
|
72
|
+
declare function buildGeoWithinBoxFilter(coords: number[]): Record<string, unknown> | null;
|
|
73
|
+
/**
|
|
74
|
+
* Rewrite a filter object that contains `$near` / `$nearSphere` into an
|
|
75
|
+
* equivalent count-compatible filter using `$geoWithin: $centerSphere`.
|
|
76
|
+
*
|
|
77
|
+
* Background: MongoDB refuses `countDocuments` on any query that uses a
|
|
78
|
+
* sort-operator like `$near` (they consume the query plan slot that count
|
|
79
|
+
* would use). The equivalent `$geoWithin: $centerSphere` is NOT a sort
|
|
80
|
+
* operator and counts/composes freely — and since both are evaluated
|
|
81
|
+
* against the same 2dsphere index, they return the same document set.
|
|
82
|
+
*
|
|
83
|
+
* This lets Repository run `find().sort(...)` with `$near` for the page
|
|
84
|
+
* results AND `countDocuments()` with the rewritten filter for the total,
|
|
85
|
+
* without ever breaking the parser → repo contract.
|
|
86
|
+
*
|
|
87
|
+
* Returns null when:
|
|
88
|
+
* - `filters` is not an object
|
|
89
|
+
* - No `$near` / `$nearSphere` is present (caller can use filters as-is)
|
|
90
|
+
* - A `$near` operator lacks `$maxDistance` (unbounded — cannot be
|
|
91
|
+
* rewritten to a bounded `$centerSphere`; caller should fall back to
|
|
92
|
+
* `countStrategy: 'none'`)
|
|
93
|
+
*/
|
|
94
|
+
declare function rewriteNearForCount(filters: unknown): Record<string, unknown> | null;
|
|
95
|
+
/**
|
|
96
|
+
* Detect whether a Mongo filter object contains a `$near` or `$nearSphere`
|
|
97
|
+
* operator on any field at the top level. These are SORT operators in
|
|
98
|
+
* MongoDB — they cannot coexist with an explicit `.sort()` clause and they
|
|
99
|
+
* cannot be used with `countDocuments`. Repositories must check this before
|
|
100
|
+
* injecting their default sort or running a count.
|
|
101
|
+
*
|
|
102
|
+
* Walks the top level only. Geo operators nested inside `$or` / `$and` are
|
|
103
|
+
* also forbidden by MongoDB so we check those one level down too.
|
|
104
|
+
*
|
|
105
|
+
* Returns false for falsy / non-object inputs.
|
|
106
|
+
*/
|
|
107
|
+
declare function hasNearOperator(filters: unknown): boolean;
|
|
108
|
+
/**
|
|
109
|
+
* High-level entry point: given an operator name and the raw value from the
|
|
110
|
+
* URL, return the MongoDB filter for that field — or null if the operator is
|
|
111
|
+
* not a geo operator (caller should fall through to normal handling) or the
|
|
112
|
+
* value is invalid (caller should drop the filter).
|
|
113
|
+
*/
|
|
114
|
+
declare function parseGeoFilter(operator: string, raw: unknown): Record<string, unknown> | null;
|
|
115
|
+
//#endregion
|
|
116
|
+
export { GEO_OPERATORS, GeoOperator, buildGeoWithinBoxFilter, buildNearFilter, buildWithinRadiusFilter, hasNearOperator, isGeoOperator, isValidLngLat, parseCoordinateList, parseGeoFilter, rewriteNearForCount };
|