@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
|
@@ -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 };
|
|
@@ -457,6 +457,27 @@ interface IResponseFormatter {
|
|
|
457
457
|
}
|
|
458
458
|
//#endregion
|
|
459
459
|
//#region src/types.d.ts
|
|
460
|
+
/**
|
|
461
|
+
* MongoKit Type Definitions
|
|
462
|
+
*
|
|
463
|
+
* Production-grade types for MongoDB repository pattern with TypeScript
|
|
464
|
+
*
|
|
465
|
+
* @module @classytic/mongokit
|
|
466
|
+
*/
|
|
467
|
+
/**
|
|
468
|
+
* Global declaration merge: adds `softRequired` to Mongoose's SchemaTypeOptions
|
|
469
|
+
* so consumers can annotate paths type-safely without importing anything.
|
|
470
|
+
*
|
|
471
|
+
* A path with `softRequired: true` keeps its DB-level `required: true`
|
|
472
|
+
* invariant but is excluded from the auto-generated CRUD body `required[]`
|
|
473
|
+
* array (see buildCrudSchemasFromModel).
|
|
474
|
+
*/
|
|
475
|
+
declare module 'mongoose' {
|
|
476
|
+
interface SchemaTypeOptions<T, EnforcedDocType = any, THydratedDocumentType = HydratedDocument<EnforcedDocType>> {
|
|
477
|
+
/** mongokit: omit from auto-generated CRUD body `required` array. */
|
|
478
|
+
softRequired?: boolean;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
460
481
|
/** Read Preference Type for replica sets */
|
|
461
482
|
type ReadPreferenceType = 'primary' | 'primaryPreferred' | 'secondary' | 'secondaryPreferred' | 'nearest' | (string & {});
|
|
462
483
|
/** Re-export mongoose ObjectId */
|
|
@@ -525,6 +546,8 @@ type CreateInput<TDoc> = Omit<TDoc, '_id' | 'createdAt' | 'updatedAt' | '__v'>;
|
|
|
525
546
|
type UpdateInput<TDoc> = Partial<Omit<TDoc, '_id' | 'createdAt' | '__v'>>;
|
|
526
547
|
/** Hook execution mode */
|
|
527
548
|
type HookMode = 'sync' | 'async';
|
|
549
|
+
/** Search strategy used by Repository.getAll when `search` is provided */
|
|
550
|
+
type RepositorySearchMode = 'text' | 'regex' | 'auto';
|
|
528
551
|
/** Repository options */
|
|
529
552
|
interface RepositoryOptions {
|
|
530
553
|
/** Whether repository event hooks are awaited */
|
|
@@ -532,6 +555,21 @@ interface RepositoryOptions {
|
|
|
532
555
|
/** Custom ID field used by getById/update/delete (default: '_id').
|
|
533
556
|
* Set to 'slug', 'code', 'chatId', etc. for non-ObjectId lookups. */
|
|
534
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[];
|
|
535
573
|
}
|
|
536
574
|
/** Pagination configuration */
|
|
537
575
|
interface PaginationConfig {
|
|
@@ -592,6 +630,14 @@ interface OffsetPaginationOptions extends BasePaginationOptions {
|
|
|
592
630
|
page?: number;
|
|
593
631
|
/** Count strategy for filtered queries (default: 'exact') */
|
|
594
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>;
|
|
595
641
|
}
|
|
596
642
|
/** Keyset (cursor) pagination options */
|
|
597
643
|
interface KeysetPaginationOptions extends BasePaginationOptions {
|
|
@@ -998,6 +1044,19 @@ interface SchemaBuilderOptions {
|
|
|
998
1044
|
optionalOverrides?: Record<string, boolean>; /** Schema overrides */
|
|
999
1045
|
schemaOverrides?: Record<string, unknown>;
|
|
1000
1046
|
};
|
|
1047
|
+
/**
|
|
1048
|
+
* Field names to mark as soft-required: they remain in the generated body
|
|
1049
|
+
* schema's `properties` (still validated when present) but are excluded
|
|
1050
|
+
* from the `required[]` array so the client may omit them.
|
|
1051
|
+
*
|
|
1052
|
+
* Combines with per-path `softRequired: true` Mongoose schema option.
|
|
1053
|
+
* Use this when the Mongoose model is owned by an upstream package and
|
|
1054
|
+
* you can't annotate the schema directly.
|
|
1055
|
+
*
|
|
1056
|
+
* The DB-level `required: true` invariant is unaffected — Mongoose still
|
|
1057
|
+
* rejects null on save. This flag only affects HTTP body validation.
|
|
1058
|
+
*/
|
|
1059
|
+
softRequiredFields?: string[];
|
|
1001
1060
|
/** Update schema options */
|
|
1002
1061
|
update?: {
|
|
1003
1062
|
/** Fields to omit from update schema */omitFields?: string[]; /** Require at least one field to be provided (default: false) */
|
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-CWUL0yiw.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-BlDfbg_0.mjs";
|
|
3
3
|
|
|
4
4
|
//#region src/utils/cache-keys.d.ts
|
|
5
5
|
/**
|
package/dist/utils/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import { r as configureLogger, t as createError } from "../error-Bpbi_NKo.mjs";
|
|
2
2
|
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-reyDRzXf.mjs";
|
|
3
|
-
import { a as isFieldUpdateAllowed, i as getSystemManagedFields, n as buildCrudSchemasFromMongooseSchema, o as validateUpdateBody, r as getImmutableFields, s as createMemoryCache, t as buildCrudSchemasFromModel } from "../mongooseToJsonSchema-
|
|
3
|
+
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";
|
|
4
4
|
export { buildCrudSchemasFromModel, buildCrudSchemasFromMongooseSchema, byIdKey, byQueryKey, configureLogger, createError, createFieldPreset, createMemoryCache, filterResponseData, getFieldsForUser, getImmutableFields, getMongooseProjection, getSystemManagedFields, isFieldUpdateAllowed, listPattern, listQueryKey, modelPattern, validateUpdateBody, versionKey };
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { a as warn, i as debug, n as parseDuplicateKeyError, t as createError } from "./error-Bpbi_NKo.mjs";
|
|
2
2
|
import { a as exists, c as getOrCreate, d as deleteByQuery, g as upsert, i as count, m as createMany, n as updateByQuery, o as getById, p as create, s as getByQuery, t as update, u as deleteById, v as distinct, y as LookupBuilder } from "./update-AVfKWNGt.mjs";
|
|
3
|
-
import { t as PaginationEngine } from "./PaginationEngine-
|
|
3
|
+
import { t as PaginationEngine } from "./PaginationEngine-CpsQl471.mjs";
|
|
4
|
+
import { hasNearOperator, rewriteNearForCount } from "./query/primitives/geo.mjs";
|
|
4
5
|
import { a as byIdKey, c as listQueryKey, l as modelPattern, o as byQueryKey, r as getFieldsForUser, u as versionKey } from "./field-selection-reyDRzXf.mjs";
|
|
5
6
|
import mongoose from "mongoose";
|
|
6
7
|
//#region src/plugins/aggregate-helpers.plugin.ts
|
|
@@ -1182,6 +1183,8 @@ var Repository = class {
|
|
|
1182
1183
|
_pagination;
|
|
1183
1184
|
_hookMode;
|
|
1184
1185
|
idField;
|
|
1186
|
+
searchMode;
|
|
1187
|
+
searchFields;
|
|
1185
1188
|
_hasTextIndex = null;
|
|
1186
1189
|
constructor(Model, plugins = [], paginationConfig = {}, options = {}) {
|
|
1187
1190
|
this.Model = Model;
|
|
@@ -1190,6 +1193,9 @@ var Repository = class {
|
|
|
1190
1193
|
this._pagination = new PaginationEngine(Model, paginationConfig);
|
|
1191
1194
|
this._hookMode = options.hooks ?? "async";
|
|
1192
1195
|
this.idField = options.idField ?? "_id";
|
|
1196
|
+
this.searchMode = options.searchMode ?? "text";
|
|
1197
|
+
this.searchFields = options.searchFields;
|
|
1198
|
+
if (this.searchMode === "regex" && (!this.searchFields || this.searchFields.length === 0)) warn(`[mongokit] Repository "${this.model}" configured with searchMode: 'regex' but no searchFields provided. getAll({ search }) will throw until searchFields is set.`);
|
|
1193
1199
|
plugins.forEach((plugin) => {
|
|
1194
1200
|
this.use(plugin);
|
|
1195
1201
|
});
|
|
@@ -1542,8 +1548,11 @@ var Repository = class {
|
|
|
1542
1548
|
sort: context.sort ?? params.sort
|
|
1543
1549
|
});
|
|
1544
1550
|
const filters = context.filters ?? params.filters ?? {};
|
|
1545
|
-
const search = context.search
|
|
1546
|
-
const
|
|
1551
|
+
const search = context.search;
|
|
1552
|
+
const isNearQuery = hasNearOperator(context.filters ?? params.filters ?? {});
|
|
1553
|
+
const explicitSort = context.sort ?? params.sort;
|
|
1554
|
+
if (isNearQuery && explicitSort !== void 0) warn(`[mongokit] Repository "${this.model}" dropping explicit sort (${JSON.stringify(explicitSort)}) because the filter contains $near / $nearSphere. MongoDB forbids explicit sort with $near. Use [withinRadius] instead of [near] if you need a custom sort.`);
|
|
1555
|
+
const sort = isNearQuery ? void 0 : explicitSort ?? "-createdAt";
|
|
1547
1556
|
const limit = context.limit ?? params.limit ?? params.pagination?.limit ?? this._pagination.config.defaultLimit;
|
|
1548
1557
|
const page = context.page ?? params.pagination?.page ?? params.page;
|
|
1549
1558
|
const after = context.after ?? params.cursor ?? params.after;
|
|
@@ -1554,13 +1563,31 @@ var Repository = class {
|
|
|
1554
1563
|
const query = { ...filters };
|
|
1555
1564
|
if (search) {
|
|
1556
1565
|
if (this._hasTextIndex === null) this._hasTextIndex = this.Model.schema.indexes().some((idx) => idx[0] && Object.values(idx[0]).includes("text"));
|
|
1557
|
-
|
|
1558
|
-
|
|
1566
|
+
let effectiveMode = this.searchMode === "regex" ? "regex" : "text";
|
|
1567
|
+
if (this.searchMode === "auto") effectiveMode = this._hasTextIndex ? "text" : "regex";
|
|
1568
|
+
if (effectiveMode === "regex") {
|
|
1569
|
+
if (!this.searchFields || this.searchFields.length === 0) throw createError(400, `Repository "${this.model}" configured with searchMode: '${this.searchMode}' but no searchFields provided.`);
|
|
1570
|
+
const escaped = String(search).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1571
|
+
const orConds = this.searchFields.map((field) => ({ [field]: {
|
|
1572
|
+
$regex: escaped,
|
|
1573
|
+
$options: "i"
|
|
1574
|
+
} }));
|
|
1575
|
+
if (Array.isArray(query.$or)) {
|
|
1576
|
+
const existingOr = query.$or;
|
|
1577
|
+
delete query.$or;
|
|
1578
|
+
query.$and = [
|
|
1579
|
+
...Array.isArray(query.$and) ? query.$and : [],
|
|
1580
|
+
{ $or: existingOr },
|
|
1581
|
+
{ $or: orConds }
|
|
1582
|
+
];
|
|
1583
|
+
} else query.$or = orConds;
|
|
1584
|
+
} else if (this._hasTextIndex) query.$text = { $search: search };
|
|
1585
|
+
else throw createError(400, `No text index found for ${this.model}. Cannot perform text search. Configure Repository with searchMode: 'regex' (and searchFields) or 'auto' to enable index-free search.`);
|
|
1559
1586
|
}
|
|
1560
1587
|
const populateSpec = options.populateOptions || params.populateOptions || context.populate || options.populate;
|
|
1561
1588
|
const paginationOptions = {
|
|
1562
1589
|
filters: query,
|
|
1563
|
-
sort: this._parseSort(sort),
|
|
1590
|
+
sort: isNearQuery ? void 0 : this._parseSort(sort),
|
|
1564
1591
|
limit,
|
|
1565
1592
|
populate: this._parsePopulate(populateSpec),
|
|
1566
1593
|
select: context.select || options.select,
|
|
@@ -1630,11 +1657,21 @@ var Repository = class {
|
|
|
1630
1657
|
sort: paginationOptions.sort,
|
|
1631
1658
|
after
|
|
1632
1659
|
});
|
|
1633
|
-
else
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1660
|
+
else {
|
|
1661
|
+
let countFilters;
|
|
1662
|
+
let forcedCountStrategy;
|
|
1663
|
+
if (isNearQuery) {
|
|
1664
|
+
const rewritten = rewriteNearForCount(query);
|
|
1665
|
+
if (rewritten) countFilters = rewritten;
|
|
1666
|
+
else forcedCountStrategy = "none";
|
|
1667
|
+
}
|
|
1668
|
+
result = await this._pagination.paginate({
|
|
1669
|
+
...paginationOptions,
|
|
1670
|
+
page: page || 1,
|
|
1671
|
+
countFilters,
|
|
1672
|
+
countStrategy: context.countStrategy ?? params.countStrategy ?? forcedCountStrategy
|
|
1673
|
+
});
|
|
1674
|
+
}
|
|
1638
1675
|
await this._emitHook("after:getAll", {
|
|
1639
1676
|
context,
|
|
1640
1677
|
result
|
package/dist/{validation-chain.plugin-CGE_mKQr.d.mts → validation-chain.plugin-Dn41aAbK.d.mts}
RENAMED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { I as Logger, J as Plugin, St as ValidatorDefinition, T as FieldPreset, U as OffsetPaginationResult, V as ObjectId, Z as PopulateSpec, bt as ValidationChainOptions, et as RepositoryContext, f as CascadeOptions, l as CacheOptions, nt as RepositoryInstance, pt as SortSpec, st as SelectSpec, u as CacheStats, ut as SoftDeleteOptions } from "./types-
|
|
1
|
+
import { I as Logger, J as Plugin, St as ValidatorDefinition, T as FieldPreset, U as OffsetPaginationResult, V as ObjectId, Z as PopulateSpec, bt as ValidationChainOptions, et as RepositoryContext, f as CascadeOptions, l as CacheOptions, nt as RepositoryInstance, pt as SortSpec, st as SelectSpec, u as CacheStats, ut as SoftDeleteOptions } from "./types-CWUL0yiw.mjs";
|
|
2
2
|
import mongoose, { ClientSession } from "mongoose";
|
|
3
3
|
|
|
4
4
|
//#region src/plugins/aggregate-helpers.plugin.d.ts
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@classytic/mongokit",
|
|
3
|
-
"version": "3.5.
|
|
3
|
+
"version": "3.5.5",
|
|
4
4
|
"description": "Production-grade MongoDB repositories with zero dependencies - smart pagination, events, and plugins",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -31,6 +31,18 @@
|
|
|
31
31
|
"./ai": {
|
|
32
32
|
"types": "./dist/ai/index.d.mts",
|
|
33
33
|
"default": "./dist/ai/index.mjs"
|
|
34
|
+
},
|
|
35
|
+
"./query/primitives/geo": {
|
|
36
|
+
"types": "./dist/query/primitives/geo.d.mts",
|
|
37
|
+
"default": "./dist/query/primitives/geo.mjs"
|
|
38
|
+
},
|
|
39
|
+
"./query/primitives/coercion": {
|
|
40
|
+
"types": "./dist/query/primitives/coercion.d.mts",
|
|
41
|
+
"default": "./dist/query/primitives/coercion.mjs"
|
|
42
|
+
},
|
|
43
|
+
"./query/primitives/indexes": {
|
|
44
|
+
"types": "./dist/query/primitives/indexes.d.mts",
|
|
45
|
+
"default": "./dist/query/primitives/indexes.mjs"
|
|
34
46
|
}
|
|
35
47
|
},
|
|
36
48
|
"files": [
|
|
@@ -81,7 +93,7 @@
|
|
|
81
93
|
},
|
|
82
94
|
"homepage": "https://github.com/classytic/mongokit#readme",
|
|
83
95
|
"peerDependencies": {
|
|
84
|
-
"mongoose": "^9.
|
|
96
|
+
"mongoose": "^9.4.1"
|
|
85
97
|
},
|
|
86
98
|
"engines": {
|
|
87
99
|
"node": ">=22"
|
|
@@ -111,7 +123,7 @@
|
|
|
111
123
|
"@vitest/coverage-v8": "^3.2.4",
|
|
112
124
|
"knip": "^6.3.0",
|
|
113
125
|
"mongodb-memory-server": "^10.4.3",
|
|
114
|
-
"mongoose": "^9.1
|
|
126
|
+
"mongoose": "^9.4.1",
|
|
115
127
|
"tsdown": "^0.21.7",
|
|
116
128
|
"typescript": "^5.7.0",
|
|
117
129
|
"vitest": "^3.2.4"
|