@classytic/mongokit 3.5.4 → 3.5.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +228 -9
- 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-B19MD38s.d.mts +106 -0
- package/dist/id-resolution-LZ-cCwR5.mjs +81 -0
- package/dist/{index-BNpVzw_-.d.mts → index-DmVbUzDL.d.mts} +1 -1
- package/dist/index.d.mts +84 -6
- package/dist/index.mjs +101 -36
- package/dist/indexes-qIBCZ3mY.d.mts +41 -0
- package/dist/{mongooseToJsonSchema-DpjhFfaO.mjs → mongooseToJsonSchema-8Cyu3NFJ.mjs} +50 -45
- package/dist/{mongooseToJsonSchema-Dsx3YAAx.d.mts → mongooseToJsonSchema-DQMU0Dlv.d.mts} +4 -3
- 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 +148 -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-CkE3mOaW.d.mts → types-BwO4u3JN.d.mts} +25 -0
- package/dist/utils/index.d.mts +60 -3
- package/dist/utils/index.mjs +4 -3
- package/dist/{validation-chain.plugin-BYMZB8dN.d.mts → validation-chain.plugin-BtmT-FFB.d.mts} +1 -1
- package/dist/{validation-chain.plugin-KjPV_6_N.mjs → validation-chain.plugin-DCYjnPau.mjs} +81 -20
- package/package.json +15 -3
- /package/dist/{field-selection-reyDRzXf.mjs → field-selection-C5o7jyyA.mjs} +0 -0
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-DCYjnPau.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-B19MD38s.mjs";
|
|
2
|
+
export { FieldType, SchemaPathsLike, buildFieldTypeMap, coerceFieldValue, coerceHeuristic, coerceToType, normalizeMongooseType };
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { n as isObjectIdInstance } from "../../id-resolution-LZ-cCwR5.mjs";
|
|
2
|
+
import mongoose from "mongoose";
|
|
3
|
+
//#region src/query/primitives/coercion.ts
|
|
4
|
+
/**
|
|
5
|
+
* Value coercion primitives
|
|
6
|
+
*
|
|
7
|
+
* Pure functions that convert raw URL-decoded values into typed values
|
|
8
|
+
* suitable for MongoDB queries. Two layers:
|
|
9
|
+
*
|
|
10
|
+
* 1. `coerceHeuristic(value)` — string-shape based, used when no schema
|
|
11
|
+
* is configured. Safe defaults: rejects leading zeros, scientific
|
|
12
|
+
* notation, and strings >15 chars to preserve zip codes, phone codes,
|
|
13
|
+
* and long numeric IDs that exceed JS safe-integer precision.
|
|
14
|
+
*
|
|
15
|
+
* 2. `coerceToType(value, type)` — type-directed, used when a Mongoose
|
|
16
|
+
* schema or `fieldTypes` map declares the field's type. Strictly
|
|
17
|
+
* better than the heuristic when type information is available.
|
|
18
|
+
*
|
|
19
|
+
* QueryParser composes these via `coerceFieldValue(field, value, types)`
|
|
20
|
+
* which prefers the type-directed path and falls back to the heuristic.
|
|
21
|
+
*
|
|
22
|
+
* Field-type extraction from a Mongoose schema is also here so the
|
|
23
|
+
* normalization logic lives next to the coercion that consumes it.
|
|
24
|
+
*/
|
|
25
|
+
/**
|
|
26
|
+
* Map a Mongoose `SchemaType.instance` string to our normalized FieldType.
|
|
27
|
+
* For arrays (`type: [Number]` or `type: [{ type: Number }]`), Mongoose
|
|
28
|
+
* stores the element type on `embeddedSchemaType.instance` (Mongoose 8+) or
|
|
29
|
+
* `caster.instance` (older). We probe both — element-level coercion is what
|
|
30
|
+
* `[in]`/`[nin]` operators need against typed arrays.
|
|
31
|
+
*
|
|
32
|
+
* Returns null for paths we cannot classify (embedded documents handled by
|
|
33
|
+
* their own nested paths).
|
|
34
|
+
*/
|
|
35
|
+
function normalizeMongooseType(schemaType) {
|
|
36
|
+
const raw = schemaType.embeddedSchemaType?.instance ?? schemaType.caster?.instance ?? schemaType.instance;
|
|
37
|
+
if (!raw) return null;
|
|
38
|
+
switch (raw) {
|
|
39
|
+
case "String": return "string";
|
|
40
|
+
case "Number": return "number";
|
|
41
|
+
case "Boolean": return "boolean";
|
|
42
|
+
case "Date": return "date";
|
|
43
|
+
case "Mixed": return "mixed";
|
|
44
|
+
default:
|
|
45
|
+
if (isObjectIdInstance(raw)) return "objectid";
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Build a normalized field-type map from a Mongoose schema and an optional
|
|
51
|
+
* override map. The override wins on a per-path basis. Paths with unmappable
|
|
52
|
+
* types are silently skipped — `coerceFieldValue` will fall back to the
|
|
53
|
+
* heuristic for them.
|
|
54
|
+
*/
|
|
55
|
+
function buildFieldTypeMap(schema, overrides) {
|
|
56
|
+
const map = /* @__PURE__ */ new Map();
|
|
57
|
+
if (schema?.paths) for (const [path, schemaType] of Object.entries(schema.paths)) {
|
|
58
|
+
const normalized = normalizeMongooseType(schemaType);
|
|
59
|
+
if (normalized) map.set(path, normalized);
|
|
60
|
+
}
|
|
61
|
+
if (overrides) for (const [path, type] of Object.entries(overrides)) map.set(path, type);
|
|
62
|
+
return map;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* String-shape heuristic — used when no field type is known.
|
|
66
|
+
*
|
|
67
|
+
* Rules (deliberately conservative — silent miscoercion is worse than
|
|
68
|
+
* leaving a string):
|
|
69
|
+
* - `null`/`undefined` pass through
|
|
70
|
+
* - Already-typed primitives (number, boolean) pass through
|
|
71
|
+
* - Arrays recurse element-wise
|
|
72
|
+
* - Objects pass through (filter operator objects, etc.)
|
|
73
|
+
* - `'true'`/`'false'` → boolean
|
|
74
|
+
* - 24-char hex → kept as string (ObjectId)
|
|
75
|
+
* - Strings 1–15 chars matching `^-?(0|[1-9]\d*)(\.\d+)?$` → number
|
|
76
|
+
* (rejects leading zeros, scientific notation, hex/octal, NaN, Infinity)
|
|
77
|
+
* - Everything else stays a string
|
|
78
|
+
*/
|
|
79
|
+
function coerceHeuristic(value) {
|
|
80
|
+
if (value === null || value === void 0) return value;
|
|
81
|
+
if (Array.isArray(value)) return value.map((v) => coerceHeuristic(v));
|
|
82
|
+
if (typeof value === "object") return value;
|
|
83
|
+
if (typeof value === "number" || typeof value === "boolean") return value;
|
|
84
|
+
const s = String(value);
|
|
85
|
+
if (s === "true") return true;
|
|
86
|
+
if (s === "false") return false;
|
|
87
|
+
if (mongoose.Types.ObjectId.isValid(s) && s.length === 24) return s;
|
|
88
|
+
if (s.length > 0 && s.length <= 15 && /^-?(?:0|[1-9]\d*)(?:\.\d+)?$/.test(s)) {
|
|
89
|
+
const n = Number(s);
|
|
90
|
+
if (Number.isFinite(n)) return n;
|
|
91
|
+
}
|
|
92
|
+
return s;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Type-directed coercion — used when a field's declared type is known.
|
|
96
|
+
*
|
|
97
|
+
* Returns the original value unchanged when coercion is impossible
|
|
98
|
+
* (invalid date, non-numeric string for a Number field) — better to surface
|
|
99
|
+
* a no-match than to silently produce `Invalid Date` or `NaN` which MongoDB
|
|
100
|
+
* treats as legitimate query values.
|
|
101
|
+
*/
|
|
102
|
+
function coerceToType(value, type) {
|
|
103
|
+
if (value === null || value === void 0) return value;
|
|
104
|
+
if (Array.isArray(value)) return value.map((v) => coerceToType(v, type));
|
|
105
|
+
if (typeof value === "object") return value;
|
|
106
|
+
switch (type) {
|
|
107
|
+
case "number": {
|
|
108
|
+
if (typeof value === "number") return value;
|
|
109
|
+
const num = Number(String(value));
|
|
110
|
+
return Number.isFinite(num) ? num : value;
|
|
111
|
+
}
|
|
112
|
+
case "boolean": {
|
|
113
|
+
if (typeof value === "boolean") return value;
|
|
114
|
+
const s = String(value).toLowerCase();
|
|
115
|
+
if (s === "true" || s === "1") return true;
|
|
116
|
+
if (s === "false" || s === "0") return false;
|
|
117
|
+
return value;
|
|
118
|
+
}
|
|
119
|
+
case "date": {
|
|
120
|
+
if (value instanceof Date) return value;
|
|
121
|
+
const d = new Date(String(value));
|
|
122
|
+
return Number.isNaN(d.getTime()) ? value : d;
|
|
123
|
+
}
|
|
124
|
+
case "objectid": {
|
|
125
|
+
const sv = String(value);
|
|
126
|
+
if (mongoose.Types.ObjectId.isValid(sv) && sv.length === 24) return sv;
|
|
127
|
+
return value;
|
|
128
|
+
}
|
|
129
|
+
case "string": return String(value);
|
|
130
|
+
case "mixed": return coerceHeuristic(value);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Field-aware coercion. Looks up `field` in `types` and uses
|
|
135
|
+
* `coerceToType` when found, otherwise falls back to `coerceHeuristic`.
|
|
136
|
+
*
|
|
137
|
+
* This is the single choke point QueryParser uses for typed coercion of
|
|
138
|
+
* direct equality, `[eq]`, `[in]`, `[nin]`, `[gt]`/`[gte]`/`[lt]`/`[lte]`
|
|
139
|
+
* (the last only when a type is declared — see QueryParser for the
|
|
140
|
+
* preserves-pre-3.5.5-behavior fallback).
|
|
141
|
+
*/
|
|
142
|
+
function coerceFieldValue(field, value, types) {
|
|
143
|
+
const type = types.get(field);
|
|
144
|
+
if (!type) return coerceHeuristic(value);
|
|
145
|
+
return coerceToType(value, type);
|
|
146
|
+
}
|
|
147
|
+
//#endregion
|
|
148
|
+
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 };
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
//#region src/query/primitives/geo.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
|
+
const GEO_OPERATORS = [
|
|
18
|
+
"near",
|
|
19
|
+
"nearSphere",
|
|
20
|
+
"geoWithin",
|
|
21
|
+
"withinRadius"
|
|
22
|
+
];
|
|
23
|
+
/** Earth radius in meters — used to convert meters → radians for $centerSphere */
|
|
24
|
+
const EARTH_RADIUS_METERS = 6378137;
|
|
25
|
+
/**
|
|
26
|
+
* True iff `operator` is one of the geo operators we handle.
|
|
27
|
+
* Used by the operator router in QueryParser to short-circuit before the
|
|
28
|
+
* generic numeric/string operator handling kicks in.
|
|
29
|
+
*/
|
|
30
|
+
function isGeoOperator(operator) {
|
|
31
|
+
return GEO_OPERATORS.includes(operator);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Parse a comma-separated coordinate list into numbers, validating each value
|
|
35
|
+
* is a finite number. Returns null on any parse failure — callers must treat
|
|
36
|
+
* null as "drop this filter entirely" rather than substituting defaults
|
|
37
|
+
* (which would silently widen the query).
|
|
38
|
+
*/
|
|
39
|
+
function parseCoordinateList(raw) {
|
|
40
|
+
if (typeof raw !== "string" && !Array.isArray(raw)) return null;
|
|
41
|
+
const parts = Array.isArray(raw) ? raw : raw.split(",").map((s) => s.trim());
|
|
42
|
+
const nums = [];
|
|
43
|
+
for (const part of parts) {
|
|
44
|
+
const n = Number(String(part));
|
|
45
|
+
if (!Number.isFinite(n)) return null;
|
|
46
|
+
nums.push(n);
|
|
47
|
+
}
|
|
48
|
+
return nums;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Validate a [lng, lat] pair against MongoDB's accepted ranges.
|
|
52
|
+
* Longitude: [-180, 180]. Latitude: [-90, 90]. Out-of-range coordinates
|
|
53
|
+
* return false so the parser can drop the filter — emitting a $near query
|
|
54
|
+
* with `coordinates: [999, 40]` would either error inside Mongo or worse,
|
|
55
|
+
* silently behave unexpectedly.
|
|
56
|
+
*/
|
|
57
|
+
function isValidLngLat(lng, lat) {
|
|
58
|
+
return lng >= -180 && lng <= 180 && lat >= -90 && lat <= 90;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Build a `$near` or `$nearSphere` filter for a single field from a coordinate
|
|
62
|
+
* list. Accepts:
|
|
63
|
+
* [lng, lat] — proximity sort, no distance bound
|
|
64
|
+
* [lng, lat, maxDistance] — proximity sort within `maxDistance` meters
|
|
65
|
+
*
|
|
66
|
+
* Returns null on any validation failure (drop the filter).
|
|
67
|
+
*/
|
|
68
|
+
function buildNearFilter(variant, coords) {
|
|
69
|
+
if (coords.length < 2 || coords.length > 3) return null;
|
|
70
|
+
const [lng, lat, maxDistance] = coords;
|
|
71
|
+
if (!isValidLngLat(lng, lat)) return null;
|
|
72
|
+
const operator = variant === "near" ? "$near" : "$nearSphere";
|
|
73
|
+
const inner = { $geometry: {
|
|
74
|
+
type: "Point",
|
|
75
|
+
coordinates: [lng, lat]
|
|
76
|
+
} };
|
|
77
|
+
if (maxDistance !== void 0) {
|
|
78
|
+
if (!Number.isFinite(maxDistance) || maxDistance < 0) return null;
|
|
79
|
+
inner.$maxDistance = maxDistance;
|
|
80
|
+
}
|
|
81
|
+
return { [operator]: inner };
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Build a `$geoWithin: $centerSphere` filter from [lng, lat, radiusMeters].
|
|
85
|
+
*
|
|
86
|
+
* IMPORTANT: this is the count-compatible alternative to `$near`. MongoDB
|
|
87
|
+
* forbids `$near` / `$nearSphere` in any context that requires sorting (which
|
|
88
|
+
* `countDocuments`, `$lookup`, `$facet`, and several others do), because they
|
|
89
|
+
* are themselves sort operators. `$geoWithin: $centerSphere` is a *filter*
|
|
90
|
+
* operator, returns the same set of documents within a radius, and works
|
|
91
|
+
* everywhere `$near` does not — including paginated `Repository.getAll`.
|
|
92
|
+
*
|
|
93
|
+
* Use `[near]` when you want results sorted by distance and don't need a
|
|
94
|
+
* total count. Use `[withinRadius]` for pagination, counts, and joins.
|
|
95
|
+
*
|
|
96
|
+
* `$centerSphere` takes the radius in radians, so we convert from the
|
|
97
|
+
* caller-friendly meters using Earth's equatorial radius.
|
|
98
|
+
*/
|
|
99
|
+
function buildWithinRadiusFilter(coords) {
|
|
100
|
+
if (coords.length !== 3) return null;
|
|
101
|
+
const [lng, lat, radiusMeters] = coords;
|
|
102
|
+
if (!isValidLngLat(lng, lat)) return null;
|
|
103
|
+
if (!Number.isFinite(radiusMeters) || radiusMeters < 0) return null;
|
|
104
|
+
const radiusRadians = radiusMeters / EARTH_RADIUS_METERS;
|
|
105
|
+
return { $geoWithin: { $centerSphere: [[lng, lat], radiusRadians] } };
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Build a `$geoWithin: $box` filter from a 4-element coordinate list:
|
|
109
|
+
* [minLng, minLat, maxLng, maxLat]
|
|
110
|
+
*
|
|
111
|
+
* Returns null on any validation failure (drop the filter).
|
|
112
|
+
*/
|
|
113
|
+
function buildGeoWithinBoxFilter(coords) {
|
|
114
|
+
if (coords.length !== 4) return null;
|
|
115
|
+
const [minLng, minLat, maxLng, maxLat] = coords;
|
|
116
|
+
if (!isValidLngLat(minLng, minLat) || !isValidLngLat(maxLng, maxLat)) return null;
|
|
117
|
+
if (minLng > maxLng || minLat > maxLat) return null;
|
|
118
|
+
return { $geoWithin: { $box: [[minLng, minLat], [maxLng, maxLat]] } };
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Rewrite a filter object that contains `$near` / `$nearSphere` into an
|
|
122
|
+
* equivalent count-compatible filter using `$geoWithin: $centerSphere`.
|
|
123
|
+
*
|
|
124
|
+
* Background: MongoDB refuses `countDocuments` on any query that uses a
|
|
125
|
+
* sort-operator like `$near` (they consume the query plan slot that count
|
|
126
|
+
* would use). The equivalent `$geoWithin: $centerSphere` is NOT a sort
|
|
127
|
+
* operator and counts/composes freely — and since both are evaluated
|
|
128
|
+
* against the same 2dsphere index, they return the same document set.
|
|
129
|
+
*
|
|
130
|
+
* This lets Repository run `find().sort(...)` with `$near` for the page
|
|
131
|
+
* results AND `countDocuments()` with the rewritten filter for the total,
|
|
132
|
+
* without ever breaking the parser → repo contract.
|
|
133
|
+
*
|
|
134
|
+
* Returns null when:
|
|
135
|
+
* - `filters` is not an object
|
|
136
|
+
* - No `$near` / `$nearSphere` is present (caller can use filters as-is)
|
|
137
|
+
* - A `$near` operator lacks `$maxDistance` (unbounded — cannot be
|
|
138
|
+
* rewritten to a bounded `$centerSphere`; caller should fall back to
|
|
139
|
+
* `countStrategy: 'none'`)
|
|
140
|
+
*/
|
|
141
|
+
function rewriteNearForCount(filters) {
|
|
142
|
+
if (!filters || typeof filters !== "object" || Array.isArray(filters)) return null;
|
|
143
|
+
const src = filters;
|
|
144
|
+
const rewritten = {};
|
|
145
|
+
let anyRewritten = false;
|
|
146
|
+
for (const [key, value] of Object.entries(src)) {
|
|
147
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
148
|
+
rewritten[key] = value;
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
const inner = value;
|
|
152
|
+
const nearOp = "$near" in inner ? "$near" : "$nearSphere" in inner ? "$nearSphere" : null;
|
|
153
|
+
if (!nearOp) {
|
|
154
|
+
rewritten[key] = value;
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
const nearVal = inner[nearOp];
|
|
158
|
+
const geometry = nearVal?.$geometry;
|
|
159
|
+
const maxDistance = nearVal?.$maxDistance;
|
|
160
|
+
if (!geometry?.coordinates || geometry.coordinates.length < 2 || typeof maxDistance !== "number" || !Number.isFinite(maxDistance) || maxDistance < 0) return null;
|
|
161
|
+
const [lng, lat] = geometry.coordinates;
|
|
162
|
+
if (!isValidLngLat(lng, lat)) return null;
|
|
163
|
+
const radiusRadians = maxDistance / EARTH_RADIUS_METERS;
|
|
164
|
+
rewritten[key] = { $geoWithin: { $centerSphere: [[lng, lat], radiusRadians] } };
|
|
165
|
+
anyRewritten = true;
|
|
166
|
+
}
|
|
167
|
+
return anyRewritten ? rewritten : null;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Detect whether a Mongo filter object contains a `$near` or `$nearSphere`
|
|
171
|
+
* operator on any field at the top level. These are SORT operators in
|
|
172
|
+
* MongoDB — they cannot coexist with an explicit `.sort()` clause and they
|
|
173
|
+
* cannot be used with `countDocuments`. Repositories must check this before
|
|
174
|
+
* injecting their default sort or running a count.
|
|
175
|
+
*
|
|
176
|
+
* Walks the top level only. Geo operators nested inside `$or` / `$and` are
|
|
177
|
+
* also forbidden by MongoDB so we check those one level down too.
|
|
178
|
+
*
|
|
179
|
+
* Returns false for falsy / non-object inputs.
|
|
180
|
+
*/
|
|
181
|
+
function hasNearOperator(filters) {
|
|
182
|
+
if (!filters || typeof filters !== "object" || Array.isArray(filters)) return false;
|
|
183
|
+
for (const value of Object.values(filters)) {
|
|
184
|
+
if (!value || typeof value !== "object") continue;
|
|
185
|
+
if (Array.isArray(value)) {
|
|
186
|
+
for (const branch of value) if (hasNearOperator(branch)) return true;
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
const inner = value;
|
|
190
|
+
if ("$near" in inner || "$nearSphere" in inner) return true;
|
|
191
|
+
}
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* High-level entry point: given an operator name and the raw value from the
|
|
196
|
+
* URL, return the MongoDB filter for that field — or null if the operator is
|
|
197
|
+
* not a geo operator (caller should fall through to normal handling) or the
|
|
198
|
+
* value is invalid (caller should drop the filter).
|
|
199
|
+
*/
|
|
200
|
+
function parseGeoFilter(operator, raw) {
|
|
201
|
+
if (!isGeoOperator(operator)) return null;
|
|
202
|
+
const coords = parseCoordinateList(raw);
|
|
203
|
+
if (!coords) return null;
|
|
204
|
+
if (operator === "near") return buildNearFilter("near", coords);
|
|
205
|
+
if (operator === "nearSphere") return buildNearFilter("nearSphere", coords);
|
|
206
|
+
if (operator === "geoWithin") return buildGeoWithinBoxFilter(coords);
|
|
207
|
+
if (operator === "withinRadius") return buildWithinRadiusFilter(coords);
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
//#endregion
|
|
211
|
+
export { GEO_OPERATORS, buildGeoWithinBoxFilter, buildNearFilter, buildWithinRadiusFilter, hasNearOperator, isGeoOperator, isValidLngLat, parseCoordinateList, parseGeoFilter, rewriteNearForCount };
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
//#region src/query/primitives/indexes.ts
|
|
2
|
+
const EMPTY = {
|
|
3
|
+
geoFields: [],
|
|
4
|
+
textFields: [],
|
|
5
|
+
other: []
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* Extract structured index information from a Mongoose schema. Returns an
|
|
9
|
+
* empty `SchemaIndexes` shape (not null) when no schema is provided so callers
|
|
10
|
+
* can read `.geoFields`/`.textFields` unconditionally without null checks.
|
|
11
|
+
*/
|
|
12
|
+
function extractSchemaIndexes(schema) {
|
|
13
|
+
if (!schema || typeof schema.indexes !== "function") return EMPTY;
|
|
14
|
+
const geoFields = [];
|
|
15
|
+
const textFields = [];
|
|
16
|
+
const other = [];
|
|
17
|
+
let raw;
|
|
18
|
+
try {
|
|
19
|
+
raw = schema.indexes();
|
|
20
|
+
} catch {
|
|
21
|
+
return EMPTY;
|
|
22
|
+
}
|
|
23
|
+
for (const entry of raw) {
|
|
24
|
+
const [spec, options] = entry;
|
|
25
|
+
if (!spec || typeof spec !== "object") continue;
|
|
26
|
+
let classified = false;
|
|
27
|
+
for (const [field, value] of Object.entries(spec)) if (value === "2dsphere" || value === "2d") {
|
|
28
|
+
geoFields.push(field);
|
|
29
|
+
classified = true;
|
|
30
|
+
} else if (value === "text") {
|
|
31
|
+
textFields.push(field);
|
|
32
|
+
classified = true;
|
|
33
|
+
}
|
|
34
|
+
if (!classified) other.push({
|
|
35
|
+
spec,
|
|
36
|
+
options
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
geoFields,
|
|
41
|
+
textFields,
|
|
42
|
+
other
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
//#endregion
|
|
46
|
+
export { extractSchemaIndexes };
|
|
@@ -546,6 +546,8 @@ type CreateInput<TDoc> = Omit<TDoc, '_id' | 'createdAt' | 'updatedAt' | '__v'>;
|
|
|
546
546
|
type UpdateInput<TDoc> = Partial<Omit<TDoc, '_id' | 'createdAt' | '__v'>>;
|
|
547
547
|
/** Hook execution mode */
|
|
548
548
|
type HookMode = 'sync' | 'async';
|
|
549
|
+
/** Search strategy used by Repository.getAll when `search` is provided */
|
|
550
|
+
type RepositorySearchMode = 'text' | 'regex' | 'auto';
|
|
549
551
|
/** Repository options */
|
|
550
552
|
interface RepositoryOptions {
|
|
551
553
|
/** Whether repository event hooks are awaited */
|
|
@@ -553,6 +555,21 @@ interface RepositoryOptions {
|
|
|
553
555
|
/** Custom ID field used by getById/update/delete (default: '_id').
|
|
554
556
|
* Set to 'slug', 'code', 'chatId', etc. for non-ObjectId lookups. */
|
|
555
557
|
idField?: string;
|
|
558
|
+
/**
|
|
559
|
+
* How Repository.getAll handles the `search` parameter.
|
|
560
|
+
* - 'text' (default): uses MongoDB $text; requires a text index.
|
|
561
|
+
* - 'regex': builds case-insensitive $or of $regex across `searchFields`. No index required.
|
|
562
|
+
* - 'auto': uses 'text' if a text index exists, otherwise falls back to 'regex' when `searchFields` is set.
|
|
563
|
+
*
|
|
564
|
+
* Works standalone (Express/Nest/etc.) or alongside QueryParser — when the parser
|
|
565
|
+
* already consumed `search` into filters, Repository does nothing extra.
|
|
566
|
+
*/
|
|
567
|
+
searchMode?: RepositorySearchMode;
|
|
568
|
+
/**
|
|
569
|
+
* Fields used when searchMode is 'regex' (or 'auto' falling back to regex).
|
|
570
|
+
* Required for regex mode to take effect.
|
|
571
|
+
*/
|
|
572
|
+
searchFields?: string[];
|
|
556
573
|
}
|
|
557
574
|
/** Pagination configuration */
|
|
558
575
|
interface PaginationConfig {
|
|
@@ -613,6 +630,14 @@ interface OffsetPaginationOptions extends BasePaginationOptions {
|
|
|
613
630
|
page?: number;
|
|
614
631
|
/** Count strategy for filtered queries (default: 'exact') */
|
|
615
632
|
countStrategy?: 'exact' | 'estimated' | 'none';
|
|
633
|
+
/**
|
|
634
|
+
* Optional alternate filter used for the count query only. The primary
|
|
635
|
+
* `filters` is still used for `.find()`. Lets callers (Repository) run a
|
|
636
|
+
* sort-only query like `$near` for results while counting with a
|
|
637
|
+
* count-compatible rewrite (e.g. `$geoWithin: $centerSphere`) that
|
|
638
|
+
* returns the same document set but works with `countDocuments`.
|
|
639
|
+
*/
|
|
640
|
+
countFilters?: FilterQuery<AnyDocument>;
|
|
616
641
|
}
|
|
617
642
|
/** Keyset (cursor) pagination options */
|
|
618
643
|
interface KeysetPaginationOptions extends BasePaginationOptions {
|
package/dist/utils/index.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { Z as PopulateSpec, pt as SortSpec, st as SelectSpec } from "../types-
|
|
2
|
-
import { a as isFieldUpdateAllowed, c as configureLogger, d as getFieldsForUser, f as getMongooseProjection, i as getSystemManagedFields, l as createFieldPreset, n as buildCrudSchemasFromMongooseSchema, o as validateUpdateBody, p as createError, r as getImmutableFields, s as createMemoryCache, t as buildCrudSchemasFromModel, u as filterResponseData } from "../mongooseToJsonSchema-
|
|
1
|
+
import { Z as PopulateSpec, pt as SortSpec, st as SelectSpec } from "../types-BwO4u3JN.mjs";
|
|
2
|
+
import { a as isFieldUpdateAllowed, c as configureLogger, d as getFieldsForUser, f as getMongooseProjection, i as getSystemManagedFields, l as createFieldPreset, n as buildCrudSchemasFromMongooseSchema, o as validateUpdateBody, p as createError, r as getImmutableFields, s as createMemoryCache, t as buildCrudSchemasFromModel, u as filterResponseData } from "../mongooseToJsonSchema-DQMU0Dlv.mjs";
|
|
3
3
|
|
|
4
4
|
//#region src/utils/cache-keys.d.ts
|
|
5
5
|
/**
|
|
@@ -89,4 +89,61 @@ declare function modelPattern(prefix: string, model: string): string;
|
|
|
89
89
|
*/
|
|
90
90
|
declare function listPattern(prefix: string, model: string): string;
|
|
91
91
|
//#endregion
|
|
92
|
-
|
|
92
|
+
//#region src/utils/id-resolution.d.ts
|
|
93
|
+
/**
|
|
94
|
+
* ID Resolution Primitive
|
|
95
|
+
*
|
|
96
|
+
* Pure functions for detecting a Mongoose schema's _id type and validating
|
|
97
|
+
* id values against it. Used by Repository.getById to decide whether to
|
|
98
|
+
* run ObjectId validation (for ObjectId schemas) or skip it (for String /
|
|
99
|
+
* Number / UUID schemas).
|
|
100
|
+
*
|
|
101
|
+
* Also usable standalone — Arc controllers, custom repositories, migration
|
|
102
|
+
* scripts, or test harnesses can import these directly to make the same
|
|
103
|
+
* decision without depending on Repository.
|
|
104
|
+
*
|
|
105
|
+
* Design: no `this`, no Model, no DB calls. Only reads `schema.paths._id.instance`.
|
|
106
|
+
*/
|
|
107
|
+
/** Normalized _id type — covers the common Mongoose _id declarations */
|
|
108
|
+
type IdType = 'objectid' | 'string' | 'number' | 'uuid' | 'unknown';
|
|
109
|
+
/**
|
|
110
|
+
* Check if a Mongoose SchemaType `instance` string is an ObjectId variant.
|
|
111
|
+
* Mongoose 9.x uses `'ObjectId'`, older versions use `'ObjectID'`. This
|
|
112
|
+
* is the single source of truth — use it instead of inline
|
|
113
|
+
* `instance === 'ObjectId' || instance === 'ObjectID'` checks.
|
|
114
|
+
*/
|
|
115
|
+
declare function isObjectIdInstance(instance: string | undefined): boolean;
|
|
116
|
+
/** Minimal structural type for schema introspection (decoupled from Mongoose version) */
|
|
117
|
+
interface SchemaLike {
|
|
118
|
+
paths?: Record<string, {
|
|
119
|
+
instance?: string;
|
|
120
|
+
}>;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Detect the _id path type from a Mongoose schema.
|
|
124
|
+
*
|
|
125
|
+
* Returns 'objectid' as the safe default when:
|
|
126
|
+
* - No schema is provided (mock models, test harnesses)
|
|
127
|
+
* - The schema has no `paths._id` (auto-generated ObjectId)
|
|
128
|
+
* - The instance string is unrecognized
|
|
129
|
+
*
|
|
130
|
+
* This safe-default means existing callers that don't set _id explicitly
|
|
131
|
+
* get the same ObjectId validation they always had — the behavioral change
|
|
132
|
+
* is strictly opt-in via schema declaration.
|
|
133
|
+
*/
|
|
134
|
+
declare function getSchemaIdType(schema?: SchemaLike | null): IdType;
|
|
135
|
+
/**
|
|
136
|
+
* Validate an id value against a known _id type — WITHOUT hitting the DB.
|
|
137
|
+
*
|
|
138
|
+
* Returns `true` when the value is structurally valid for the given type.
|
|
139
|
+
* Returns `false` when it's definitely invalid (Repository should return
|
|
140
|
+
* 404 / null immediately rather than wasting a round-trip).
|
|
141
|
+
*
|
|
142
|
+
* For `'string'` and `'unknown'` types, validation is intentionally
|
|
143
|
+
* permissive: any truthy, non-empty value is accepted. The DB is the
|
|
144
|
+
* authority for whether the document exists — we only reject values that
|
|
145
|
+
* are structurally impossible (null, undefined, empty string).
|
|
146
|
+
*/
|
|
147
|
+
declare function isValidIdForType(id: unknown, idType: IdType): boolean;
|
|
148
|
+
//#endregion
|
|
149
|
+
export { type IdType, buildCrudSchemasFromModel, buildCrudSchemasFromMongooseSchema, byIdKey, byQueryKey, configureLogger, createError, createFieldPreset, createMemoryCache, filterResponseData, getFieldsForUser, getImmutableFields, getMongooseProjection, getSchemaIdType, getSystemManagedFields, isFieldUpdateAllowed, isObjectIdInstance, isValidIdForType, listPattern, listQueryKey, modelPattern, validateUpdateBody, versionKey };
|
package/dist/utils/index.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { r as configureLogger, t as createError } from "../error-Bpbi_NKo.mjs";
|
|
2
|
-
import {
|
|
3
|
-
import { a as
|
|
4
|
-
|
|
2
|
+
import { n as isObjectIdInstance, r as isValidIdForType, t as getSchemaIdType } from "../id-resolution-LZ-cCwR5.mjs";
|
|
3
|
+
import { a as byIdKey, c as listQueryKey, i as getMongooseProjection, l as modelPattern, n as filterResponseData, o as byQueryKey, r as getFieldsForUser, s as listPattern, t as createFieldPreset, u as versionKey } from "../field-selection-C5o7jyyA.mjs";
|
|
4
|
+
import { a as isFieldUpdateAllowed, i as getSystemManagedFields, n as buildCrudSchemasFromMongooseSchema, o as validateUpdateBody, r as getImmutableFields, s as createMemoryCache, t as buildCrudSchemasFromModel } from "../mongooseToJsonSchema-8Cyu3NFJ.mjs";
|
|
5
|
+
export { buildCrudSchemasFromModel, buildCrudSchemasFromMongooseSchema, byIdKey, byQueryKey, configureLogger, createError, createFieldPreset, createMemoryCache, filterResponseData, getFieldsForUser, getImmutableFields, getMongooseProjection, getSchemaIdType, getSystemManagedFields, isFieldUpdateAllowed, isObjectIdInstance, isValidIdForType, listPattern, listQueryKey, modelPattern, validateUpdateBody, versionKey };
|