@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/README.md
CHANGED
|
@@ -15,9 +15,13 @@
|
|
|
15
15
|
- **17 built-in plugins** - Caching, soft delete, audit trail, validation, multi-tenant, custom IDs, observability, Elasticsearch, and more
|
|
16
16
|
- **Distributed cache safety** - List cache versions stored in the adapter (Redis) for multi-pod correctness
|
|
17
17
|
- **Search governance** - Text index guard (throws `400` if no index), allowlisted sort/filter fields, ReDoS protection
|
|
18
|
+
- **Geo queries** - `[near]`, `[nearSphere]`, `[withinRadius]`, `[geoWithin]` URL operators with auto sort/count handling
|
|
19
|
+
- **Schema-aware coercion** - Pass `schema: Model.schema` to QueryParser for deterministic type-safe filter values
|
|
20
|
+
- **String/UUID/Number `_id` support** - Works out of the box with any `_id` type, not just ObjectId
|
|
21
|
+
- **Query primitives** - Composable pure functions (`coercion`, `geo`, `indexes`) importable standalone for custom parsers
|
|
18
22
|
- **Vector search** - MongoDB Atlas `$vectorSearch` with auto-embedding and multimodal support
|
|
19
23
|
- **TypeScript first** - Full type safety with discriminated unions, typed events, and field autocomplete
|
|
20
|
-
- **Extensively tested** — battle-tested and production-ready
|
|
24
|
+
- **Extensively tested** — 1500+ tests, Mongoose 9.4.1 aligned, battle-tested and production-ready
|
|
21
25
|
|
|
22
26
|
## Installation
|
|
23
27
|
|
|
@@ -25,7 +29,7 @@
|
|
|
25
29
|
npm install @classytic/mongokit mongoose
|
|
26
30
|
```
|
|
27
31
|
|
|
28
|
-
> Requires Mongoose
|
|
32
|
+
> Requires Mongoose `>=9.4.1` | Node.js `>=22`
|
|
29
33
|
|
|
30
34
|
## Quick Start
|
|
31
35
|
|
|
@@ -192,6 +196,128 @@ const repo = new Repository(UserModel, plugins, {
|
|
|
192
196
|
});
|
|
193
197
|
```
|
|
194
198
|
|
|
199
|
+
### Search configuration
|
|
200
|
+
|
|
201
|
+
`Repository.getAll({ search })` has three layers, ordered from simplest to most
|
|
202
|
+
flexible. Pick the lowest one that solves your problem.
|
|
203
|
+
|
|
204
|
+
| Layer | When to use | Where it lives |
|
|
205
|
+
| --------------------------- | ------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------- |
|
|
206
|
+
| **`searchMode: 'text'`** | You have a MongoDB text index and want `$text`-ranked results. | Repository option (default) |
|
|
207
|
+
| **`searchMode: 'regex'`** | Small/medium collection, no text index, simple substring matching across known fields. Works behind any HTTP framework. | Repository option |
|
|
208
|
+
| **Search-resolver plugin** | External engine (Elasticsearch, Meilisearch, Typesense, pgvector, hybrid BM25+vector). Composes with cache, multi-tenant. | `before:getAll` plugin — see Plugins below |
|
|
209
|
+
|
|
210
|
+
```javascript
|
|
211
|
+
// Built-in regex mode — no MongoDB text index required
|
|
212
|
+
const repo = new Repository(
|
|
213
|
+
MemoModel,
|
|
214
|
+
[],
|
|
215
|
+
{},
|
|
216
|
+
{
|
|
217
|
+
// 'text' → default, requires text index
|
|
218
|
+
// 'regex' → case-insensitive $or of $regex across searchFields, no index needed
|
|
219
|
+
// 'auto' → text if a text index exists, otherwise regex
|
|
220
|
+
searchMode: "regex",
|
|
221
|
+
searchFields: ["title", "scope", "body"],
|
|
222
|
+
},
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
await repo.getAll({ search: "alpha", filters: { scope: "public" } });
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
User input is regex-escaped (literal substring match), and any pre-existing
|
|
229
|
+
`$or` filter is preserved by promoting both clauses to `$and`.
|
|
230
|
+
|
|
231
|
+
> **QueryParser ↔ Repository:** `QueryParser` also exposes `searchMode`/`searchFields`
|
|
232
|
+
> for the HTTP boundary (`?search=foo` URL → parsed query). The two layers are
|
|
233
|
+
> independent: configure the parser if you want URL-time consumption (Arc does
|
|
234
|
+
> this), configure the Repository if you want data-layer consumption
|
|
235
|
+
> (Express/Nest/raw handlers), or both for defense in depth — they will not
|
|
236
|
+
> conflict because the parser clears `parsed.search` before it reaches the repo.
|
|
237
|
+
|
|
238
|
+
### Geo queries
|
|
239
|
+
|
|
240
|
+
QueryParser supports 4 geo operators that compile to canonical GeoJSON-shaped
|
|
241
|
+
MongoDB queries. All validate coordinate ranges and reject malformed input by
|
|
242
|
+
dropping the filter (not silently widening).
|
|
243
|
+
|
|
244
|
+
```javascript
|
|
245
|
+
const parser = new QueryParser({ schema: PlaceModel.schema });
|
|
246
|
+
|
|
247
|
+
// Distance-sorted (implicit distance ordering, no explicit sort allowed)
|
|
248
|
+
const near = parser.parse({ "location[near]": "-73.98,40.76,5000" });
|
|
249
|
+
// → { location: { $near: { $geometry: { type: 'Point', coordinates: [-73.98, 40.76] }, $maxDistance: 5000 } } }
|
|
250
|
+
|
|
251
|
+
// Count-compatible radius — use this for paginated getAll (supports sort + count)
|
|
252
|
+
const radius = parser.parse({ "location[withinRadius]": "-73.98,40.76,5000" });
|
|
253
|
+
// → { location: { $geoWithin: { $centerSphere: [[-73.98, 40.76], 0.000783...] } } }
|
|
254
|
+
|
|
255
|
+
// Bounding box (works without a 2dsphere index)
|
|
256
|
+
const box = parser.parse({ "location[geoWithin]": "-74,40.7,-73.9,40.8" });
|
|
257
|
+
// → { location: { $geoWithin: { $box: [[-74, 40.7], [-73.9, 40.8]] } } }
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
**Repository integration:** `Repository.getAll` auto-detects `$near` /
|
|
261
|
+
`$nearSphere` in filters and handles the MongoDB constraints transparently:
|
|
262
|
+
|
|
263
|
+
- Skips the default `-createdAt` sort (MongoDB forbids explicit sort with `$near`)
|
|
264
|
+
- Warns and drops any caller-supplied sort that conflicts
|
|
265
|
+
- Rewrites the count query via `$geoWithin: $centerSphere` for accurate `total`
|
|
266
|
+
- Falls back to `countStrategy: 'none'` for unbounded `$near` (no `$maxDistance`)
|
|
267
|
+
- All plugins (multi-tenant, soft-delete, cache, populate) compose unchanged
|
|
268
|
+
|
|
269
|
+
Use `[near]` when you need distance-sorted results. Use `[withinRadius]` when
|
|
270
|
+
you need paginated lists with `total` and custom sort.
|
|
271
|
+
|
|
272
|
+
### String / UUID / Number `_id`
|
|
273
|
+
|
|
274
|
+
Schemas that declare `_id` as `String`, `Number`, or `UUID` instead of the
|
|
275
|
+
default `ObjectId` work out of the box — no configuration needed:
|
|
276
|
+
|
|
277
|
+
```typescript
|
|
278
|
+
const SessionSchema = new Schema({
|
|
279
|
+
_id: { type: String, default: () => randomUUID() },
|
|
280
|
+
userId: { type: String, required: true },
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
const repo = new Repository(SessionModel);
|
|
284
|
+
await repo.create({ userId: "user-1" }); // _id auto-generated as UUID
|
|
285
|
+
await repo.getById("550e8400-e29b-..."); // works — no ObjectId validation
|
|
286
|
+
await repo.update("550e8400-e29b-...", { ... }); // works
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
The `id-resolution` utility detects the `_id` type from the schema and
|
|
290
|
+
validates accordingly. Schema generation also includes String `_id` as an
|
|
291
|
+
optional field in `createBody` so Fastify/AJV validation accepts user-supplied
|
|
292
|
+
IDs. Available standalone:
|
|
293
|
+
|
|
294
|
+
```typescript
|
|
295
|
+
import { getSchemaIdType, isValidIdForType } from "@classytic/mongokit/utils";
|
|
296
|
+
|
|
297
|
+
const idType = getSchemaIdType(SessionModel.schema); // 'string'
|
|
298
|
+
isValidIdForType("550e8400-...", idType); // true
|
|
299
|
+
isValidIdForType("not-valid", "objectid"); // false
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
### Query primitives
|
|
303
|
+
|
|
304
|
+
Pure, composable functions that power QueryParser internals — importable
|
|
305
|
+
standalone for custom parsers, Arc controllers, migration scripts, and AI
|
|
306
|
+
agents that build queries programmatically:
|
|
307
|
+
|
|
308
|
+
```typescript
|
|
309
|
+
// Geo
|
|
310
|
+
import { parseGeoFilter, hasNearOperator } from "@classytic/mongokit/query/primitives/geo";
|
|
311
|
+
// Coercion
|
|
312
|
+
import { coerceFieldValue, buildFieldTypeMap } from "@classytic/mongokit/query/primitives/coercion";
|
|
313
|
+
// Schema indexes
|
|
314
|
+
import { extractSchemaIndexes } from "@classytic/mongokit/query/primitives/indexes";
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
Each primitive is independently unit-tested, tree-shake friendly (no barrel),
|
|
318
|
+
and decoupled from Mongoose — they accept structural types, not concrete
|
|
319
|
+
Mongoose instances.
|
|
320
|
+
|
|
195
321
|
## Plugins
|
|
196
322
|
|
|
197
323
|
### Using Plugins
|
|
@@ -754,6 +880,62 @@ const results = await repo.searchSimilar({ query: [0.1, 0.2, ...], limit: 5 });
|
|
|
754
880
|
const vector = await repo.embed('some text');
|
|
755
881
|
```
|
|
756
882
|
|
|
883
|
+
### Custom Search Backends (search-resolver plugin contract)
|
|
884
|
+
|
|
885
|
+
The built-in `searchMode: 'text' | 'regex'` is sugar over a more general
|
|
886
|
+
**plugin contract** for any search backend — Elasticsearch, Meilisearch,
|
|
887
|
+
Typesense, Algolia, pgvector, Pinecone, hybrid BM25+vector. The contract
|
|
888
|
+
keeps mongokit core free of backend-specific dependencies, and the same
|
|
889
|
+
plugin works behind any HTTP framework (Arc, Express, Nest, raw handlers)
|
|
890
|
+
without further wiring.
|
|
891
|
+
|
|
892
|
+
**The contract** (4 lines):
|
|
893
|
+
|
|
894
|
+
1. Hook `before:getAll`.
|
|
895
|
+
2. If `ctx.search` is set, resolve it against your backend (returns IDs).
|
|
896
|
+
3. Mutate `ctx.filters` to constrain the Mongo query: `_id: { $in: ids }`.
|
|
897
|
+
4. Set `ctx.search = undefined` so Repository's text-index check is bypassed.
|
|
898
|
+
|
|
899
|
+
```javascript
|
|
900
|
+
function meilisearchPlugin({ client, index }) {
|
|
901
|
+
return (repo) => {
|
|
902
|
+
repo.on("before:getAll", async (ctx) => {
|
|
903
|
+
if (!ctx.search) return;
|
|
904
|
+
const hits = await client.index(index).search(ctx.search, {
|
|
905
|
+
limit: ctx.limit ?? 20,
|
|
906
|
+
});
|
|
907
|
+
const ids = hits.hits.map((h) => h.id);
|
|
908
|
+
ctx.filters = { ...(ctx.filters ?? {}), _id: { $in: ids } };
|
|
909
|
+
ctx.search = undefined; // bypass text-index check
|
|
910
|
+
ctx._meiliRanking = ids; // stash for after:getAll re-sort
|
|
911
|
+
});
|
|
912
|
+
repo.on("after:getAll", (ctx) => {
|
|
913
|
+
if (!ctx._meiliRanking) return;
|
|
914
|
+
const order = new Map(ctx._meiliRanking.map((id, i) => [String(id), i]));
|
|
915
|
+
ctx.result.docs.sort(
|
|
916
|
+
(a, b) => order.get(String(a._id)) - order.get(String(b._id)),
|
|
917
|
+
);
|
|
918
|
+
});
|
|
919
|
+
};
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
// Wire at Repository construction — works in Arc, Express, Nest, anywhere
|
|
923
|
+
const repo = new Repository(ProductModel, [
|
|
924
|
+
meilisearchPlugin({ client: meili, index: "products" }),
|
|
925
|
+
cachePlugin({ adapter: createMemoryCache(), ttl: 60 }), // composes cleanly
|
|
926
|
+
]);
|
|
927
|
+
```
|
|
928
|
+
|
|
929
|
+
**Why this contract works:**
|
|
930
|
+
|
|
931
|
+
- `before:getAll` runs inside `_buildContext`, _before_ Repository reads `search` or hits the text-index check. A plugin that clears `ctx.search` short-circuits the built-in search path entirely.
|
|
932
|
+
- `Repository.getAll` reads `search` only from the post-hook context (no fallback to original params) — this is the framework-level guarantee that makes `ctx.search = undefined` a real clear, not a silently overridden no-op.
|
|
933
|
+
- The caller's existing filters are preserved by spreading: `ctx.filters = { ...(ctx.filters ?? {}), _id: ... }`. Multi-tenant scoping (which also runs in `before:getAll`), soft-delete filters, and the search plugin compose without ordering concerns — they all mutate `ctx.filters` independently and all survive.
|
|
934
|
+
- Cache plugin composes too: it sees the post-hook context (filters with the resolved ID list), so cache keys are accurate. ES/Meili results get cached just like Mongo results.
|
|
935
|
+
- The same plugin shape works for every backend; only the resolver call changes.
|
|
936
|
+
|
|
937
|
+
The bundled `elasticSearchPlugin` (next section) is the canonical reference implementation, and `tests/repository-search-mode.test.ts` exercises this contract end-to-end with multi-tenant + soft-delete + cache composition.
|
|
938
|
+
|
|
757
939
|
### Elasticsearch / OpenSearch Plugin
|
|
758
940
|
|
|
759
941
|
Delegates heavy text and semantic search to an external search engine while fetching full documents from MongoDB. Keeps your OLTP (transactional) MongoDB operations fast by separating search I/O.
|
|
@@ -1044,12 +1226,13 @@ Converts HTTP query strings to MongoDB queries with built-in security:
|
|
|
1044
1226
|
import { QueryParser } from "@classytic/mongokit";
|
|
1045
1227
|
|
|
1046
1228
|
const parser = new QueryParser({
|
|
1229
|
+
schema: ProductModel.schema, // Schema-aware coercion (recommended)
|
|
1047
1230
|
maxLimit: 100, // Prevent excessive queries
|
|
1048
1231
|
maxFilterDepth: 5, // Prevent nested injection
|
|
1049
1232
|
maxRegexLength: 100, // ReDoS protection
|
|
1050
|
-
allowedFilterFields: [
|
|
1051
|
-
allowedSortFields: [
|
|
1052
|
-
allowedOperators: [
|
|
1233
|
+
allowedFilterFields: ["status", "name", "email"], // Whitelist filter fields
|
|
1234
|
+
allowedSortFields: ["createdAt", "name"], // Whitelist sort fields
|
|
1235
|
+
allowedOperators: ["eq", "ne", "in", "gt", "lt"], // Whitelist operators
|
|
1053
1236
|
});
|
|
1054
1237
|
|
|
1055
1238
|
// Parse request query
|
|
@@ -1061,6 +1244,35 @@ parser.allowedSortFields; // ['createdAt', 'name'] or undefined
|
|
|
1061
1244
|
parser.allowedOperators; // ['eq', 'ne', 'in', 'gt', 'lt'] or undefined
|
|
1062
1245
|
```
|
|
1063
1246
|
|
|
1247
|
+
**Schema-aware value coercion (recommended):** Pass `schema: Model.schema` and
|
|
1248
|
+
the parser coerces filter values exactly to each field's declared type. With
|
|
1249
|
+
schema configured, `?stock=50` against a `Number` field becomes `50` (number),
|
|
1250
|
+
`?name=12345` against a `String` field stays `'12345'`, `?releasedAt=2026-04-07`
|
|
1251
|
+
against a `Date` field becomes a `Date` instance, `?address.zip=01234` against
|
|
1252
|
+
a nested `String` field preserves the leading zero. Direct equality and
|
|
1253
|
+
operator syntax (`stock[gte]=50`) coerce identically — no more "why does
|
|
1254
|
+
operator work but direct equality doesn't" surprises. Unknown fields fall
|
|
1255
|
+
through to a safe heuristic so ad-hoc filters still work.
|
|
1256
|
+
|
|
1257
|
+
For DB-agnostic setups (raw MongoDB, Prisma, computed fields, models you
|
|
1258
|
+
don't own), use `fieldTypes` instead of `schema`. The two can be combined —
|
|
1259
|
+
`fieldTypes` overrides `schema` per path:
|
|
1260
|
+
|
|
1261
|
+
```typescript
|
|
1262
|
+
const parser = new QueryParser({
|
|
1263
|
+
schema: ProductModel.schema,
|
|
1264
|
+
fieldTypes: {
|
|
1265
|
+
"address.zip": "string", // override a Mongoose path
|
|
1266
|
+
legacyCount: "number", // declare a non-schema field
|
|
1267
|
+
},
|
|
1268
|
+
});
|
|
1269
|
+
```
|
|
1270
|
+
|
|
1271
|
+
Without either option, the parser falls back to a string-shape heuristic
|
|
1272
|
+
(numeric strings without leading zeros and ≤15 chars become numbers; ObjectId
|
|
1273
|
+
hex stays a string; `'true'`/`'false'` become booleans). The heuristic is
|
|
1274
|
+
safe but inherently lossy — schema-aware mode is strictly better.
|
|
1275
|
+
|
|
1064
1276
|
**Supported query patterns:**
|
|
1065
1277
|
|
|
1066
1278
|
```bash
|
|
@@ -1078,9 +1290,15 @@ GET /users?after=eyJfaWQiOi...&limit=20 # Cursor-based
|
|
|
1078
1290
|
# Sorting
|
|
1079
1291
|
GET /users?sort=-createdAt,name
|
|
1080
1292
|
|
|
1081
|
-
# Search (
|
|
1293
|
+
# Search (default: text index; or configure searchMode: 'regex' / 'auto')
|
|
1082
1294
|
GET /users?search=john
|
|
1083
1295
|
|
|
1296
|
+
# Geo queries (requires 2dsphere index for near/nearSphere)
|
|
1297
|
+
GET /places?location[near]=-73.98,40.76,5000 # Within 5km, distance-sorted
|
|
1298
|
+
GET /places?location[nearSphere]=-73.98,40.76,5000 # Spherical variant
|
|
1299
|
+
GET /places?location[withinRadius]=-73.98,40.76,5000 # Count-compatible radius (use for paginated lists)
|
|
1300
|
+
GET /places?location[geoWithin]=-74,40.7,-73.9,40.8 # Bounding box (no index required)
|
|
1301
|
+
|
|
1084
1302
|
# Simple populate
|
|
1085
1303
|
GET /posts?populate=author,category
|
|
1086
1304
|
|
|
@@ -1476,14 +1694,15 @@ await repo.getById('my-chat-uuid', { idField: 'chatId' }); // by chatId
|
|
|
1476
1694
|
|
|
1477
1695
|
All plugins respect `idField`: soft-delete, cascade, audit-log, audit-trail, validation-chain, elastic, cache, observability.
|
|
1478
1696
|
|
|
1479
|
-
##
|
|
1697
|
+
## Mongoose Compatibility
|
|
1480
1698
|
|
|
1481
|
-
|
|
1699
|
+
Requires Mongoose `>=9.4.1`. Tested and aligned with Mongoose 9.4.1. The package:
|
|
1482
1700
|
|
|
1483
1701
|
- Uses its own event system (not Mongoose middleware)
|
|
1484
1702
|
- Defines its own `FilterQuery` type (unaffected by Mongoose 9 rename)
|
|
1485
1703
|
- Properly gates update pipelines (safe for Mongoose 9's stricter defaults)
|
|
1486
|
-
-
|
|
1704
|
+
- Reads `embeddedSchemaType.instance` for array element types (Mongoose 9.x)
|
|
1705
|
+
- 1500+ tests pass on Mongoose 9.4.1
|
|
1487
1706
|
|
|
1488
1707
|
## License
|
|
1489
1708
|
|
|
@@ -129,7 +129,7 @@ var PaginationEngine = class {
|
|
|
129
129
|
* console.log(result.docs, result.total, result.hasNext);
|
|
130
130
|
*/
|
|
131
131
|
async paginate(options = {}) {
|
|
132
|
-
const { filters = {}, sort
|
|
132
|
+
const { filters = {}, sort, countFilters, page = 1, limit = this.config.defaultLimit, select, populate = [], lean = true, session, hint, maxTimeMS, countStrategy = "exact", readPreference, collation } = options;
|
|
133
133
|
const sanitizedPage = validatePage(page, this.config);
|
|
134
134
|
const sanitizedLimit = validateLimit(limit, this.config);
|
|
135
135
|
const skip = calculateSkip(sanitizedPage, sanitizedLimit);
|
|
@@ -137,7 +137,8 @@ var PaginationEngine = class {
|
|
|
137
137
|
let query = this.Model.find(filters);
|
|
138
138
|
if (select) query = query.select(select);
|
|
139
139
|
if (populate && (Array.isArray(populate) ? populate.length : populate)) query = query.populate(populate);
|
|
140
|
-
query = query.sort(sort)
|
|
140
|
+
if (sort) query = query.sort(sort);
|
|
141
|
+
query = query.skip(skip).limit(fetchLimit).lean(lean);
|
|
141
142
|
if (collation) query = query.collation(collation);
|
|
142
143
|
if (session) query = query.session(session);
|
|
143
144
|
if (hint) query = query.hint(hint);
|
|
@@ -149,7 +150,8 @@ var PaginationEngine = class {
|
|
|
149
150
|
if ((countStrategy === "estimated" || useEstimated) && !hasFilters) countPromise = this.Model.estimatedDocumentCount();
|
|
150
151
|
else if (countStrategy === "none") countPromise = Promise.resolve(0);
|
|
151
152
|
else {
|
|
152
|
-
const
|
|
153
|
+
const countTarget = countFilters ?? filters;
|
|
154
|
+
const countQuery = this.Model.countDocuments(countTarget).session(session ?? null);
|
|
153
155
|
if (hint) countQuery.hint(hint);
|
|
154
156
|
if (maxTimeMS) countQuery.maxTimeMS(maxTimeMS);
|
|
155
157
|
if (readPreference) countQuery.read(readPreference);
|
package/dist/actions/index.d.mts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as create_d_exports, i as delete_d_exports, n as update_d_exports, o as aggregate_d_exports, r as read_d_exports } from "../index-
|
|
1
|
+
import { a as create_d_exports, i as delete_d_exports, n as update_d_exports, o as aggregate_d_exports, r as read_d_exports } from "../index-DmVbUzDL.mjs";
|
|
2
2
|
export { aggregate_d_exports as aggregate, create_d_exports as create, delete_d_exports as deleteActions, read_d_exports as read, update_d_exports as update };
|
package/dist/ai/index.d.mts
CHANGED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
//#region src/query/primitives/coercion.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Value coercion primitives
|
|
4
|
+
*
|
|
5
|
+
* Pure functions that convert raw URL-decoded values into typed values
|
|
6
|
+
* suitable for MongoDB queries. Two layers:
|
|
7
|
+
*
|
|
8
|
+
* 1. `coerceHeuristic(value)` — string-shape based, used when no schema
|
|
9
|
+
* is configured. Safe defaults: rejects leading zeros, scientific
|
|
10
|
+
* notation, and strings >15 chars to preserve zip codes, phone codes,
|
|
11
|
+
* and long numeric IDs that exceed JS safe-integer precision.
|
|
12
|
+
*
|
|
13
|
+
* 2. `coerceToType(value, type)` — type-directed, used when a Mongoose
|
|
14
|
+
* schema or `fieldTypes` map declares the field's type. Strictly
|
|
15
|
+
* better than the heuristic when type information is available.
|
|
16
|
+
*
|
|
17
|
+
* QueryParser composes these via `coerceFieldValue(field, value, types)`
|
|
18
|
+
* which prefers the type-directed path and falls back to the heuristic.
|
|
19
|
+
*
|
|
20
|
+
* Field-type extraction from a Mongoose schema is also here so the
|
|
21
|
+
* normalization logic lives next to the coercion that consumes it.
|
|
22
|
+
*/
|
|
23
|
+
/** Normalized field type used for schema-aware coercion */
|
|
24
|
+
type FieldType = 'string' | 'number' | 'boolean' | 'date' | 'objectid' | 'mixed';
|
|
25
|
+
/**
|
|
26
|
+
* Minimal structural type for a Mongoose schema (read-only `.paths`).
|
|
27
|
+
* Mongoose stores array element types in two different shadow properties
|
|
28
|
+
* depending on version and declaration style:
|
|
29
|
+
* - `caster.instance` — older / some array forms
|
|
30
|
+
* - `embeddedSchemaType.instance` — Mongoose 8+, both `[Type]` and `[{ type: Type }]`
|
|
31
|
+
* We read both and prefer whichever is populated.
|
|
32
|
+
*/
|
|
33
|
+
interface SchemaPathsLike {
|
|
34
|
+
paths: Record<string, {
|
|
35
|
+
instance?: string;
|
|
36
|
+
caster?: {
|
|
37
|
+
instance?: string;
|
|
38
|
+
};
|
|
39
|
+
embeddedSchemaType?: {
|
|
40
|
+
instance?: string;
|
|
41
|
+
};
|
|
42
|
+
}>;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Map a Mongoose `SchemaType.instance` string to our normalized FieldType.
|
|
46
|
+
* For arrays (`type: [Number]` or `type: [{ type: Number }]`), Mongoose
|
|
47
|
+
* stores the element type on `embeddedSchemaType.instance` (Mongoose 8+) or
|
|
48
|
+
* `caster.instance` (older). We probe both — element-level coercion is what
|
|
49
|
+
* `[in]`/`[nin]` operators need against typed arrays.
|
|
50
|
+
*
|
|
51
|
+
* Returns null for paths we cannot classify (embedded documents handled by
|
|
52
|
+
* their own nested paths).
|
|
53
|
+
*/
|
|
54
|
+
declare function normalizeMongooseType(schemaType: {
|
|
55
|
+
instance?: string;
|
|
56
|
+
caster?: {
|
|
57
|
+
instance?: string;
|
|
58
|
+
};
|
|
59
|
+
embeddedSchemaType?: {
|
|
60
|
+
instance?: string;
|
|
61
|
+
};
|
|
62
|
+
}): FieldType | null;
|
|
63
|
+
/**
|
|
64
|
+
* Build a normalized field-type map from a Mongoose schema and an optional
|
|
65
|
+
* override map. The override wins on a per-path basis. Paths with unmappable
|
|
66
|
+
* types are silently skipped — `coerceFieldValue` will fall back to the
|
|
67
|
+
* heuristic for them.
|
|
68
|
+
*/
|
|
69
|
+
declare function buildFieldTypeMap(schema?: SchemaPathsLike | null, overrides?: Record<string, FieldType>): Map<string, FieldType>;
|
|
70
|
+
/**
|
|
71
|
+
* String-shape heuristic — used when no field type is known.
|
|
72
|
+
*
|
|
73
|
+
* Rules (deliberately conservative — silent miscoercion is worse than
|
|
74
|
+
* leaving a string):
|
|
75
|
+
* - `null`/`undefined` pass through
|
|
76
|
+
* - Already-typed primitives (number, boolean) pass through
|
|
77
|
+
* - Arrays recurse element-wise
|
|
78
|
+
* - Objects pass through (filter operator objects, etc.)
|
|
79
|
+
* - `'true'`/`'false'` → boolean
|
|
80
|
+
* - 24-char hex → kept as string (ObjectId)
|
|
81
|
+
* - Strings 1–15 chars matching `^-?(0|[1-9]\d*)(\.\d+)?$` → number
|
|
82
|
+
* (rejects leading zeros, scientific notation, hex/octal, NaN, Infinity)
|
|
83
|
+
* - Everything else stays a string
|
|
84
|
+
*/
|
|
85
|
+
declare function coerceHeuristic(value: unknown): unknown;
|
|
86
|
+
/**
|
|
87
|
+
* Type-directed coercion — used when a field's declared type is known.
|
|
88
|
+
*
|
|
89
|
+
* Returns the original value unchanged when coercion is impossible
|
|
90
|
+
* (invalid date, non-numeric string for a Number field) — better to surface
|
|
91
|
+
* a no-match than to silently produce `Invalid Date` or `NaN` which MongoDB
|
|
92
|
+
* treats as legitimate query values.
|
|
93
|
+
*/
|
|
94
|
+
declare function coerceToType(value: unknown, type: FieldType): unknown;
|
|
95
|
+
/**
|
|
96
|
+
* Field-aware coercion. Looks up `field` in `types` and uses
|
|
97
|
+
* `coerceToType` when found, otherwise falls back to `coerceHeuristic`.
|
|
98
|
+
*
|
|
99
|
+
* This is the single choke point QueryParser uses for typed coercion of
|
|
100
|
+
* direct equality, `[eq]`, `[in]`, `[nin]`, `[gt]`/`[gte]`/`[lt]`/`[lte]`
|
|
101
|
+
* (the last only when a type is declared — see QueryParser for the
|
|
102
|
+
* preserves-pre-3.5.5-behavior fallback).
|
|
103
|
+
*/
|
|
104
|
+
declare function coerceFieldValue(field: string, value: unknown, types: Map<string, FieldType>): unknown;
|
|
105
|
+
//#endregion
|
|
106
|
+
export { coerceHeuristic as a, coerceFieldValue as i, SchemaPathsLike as n, coerceToType as o, buildFieldTypeMap as r, normalizeMongooseType as s, FieldType as t };
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import mongoose from "mongoose";
|
|
2
|
+
//#region src/utils/id-resolution.ts
|
|
3
|
+
/**
|
|
4
|
+
* ID Resolution Primitive
|
|
5
|
+
*
|
|
6
|
+
* Pure functions for detecting a Mongoose schema's _id type and validating
|
|
7
|
+
* id values against it. Used by Repository.getById to decide whether to
|
|
8
|
+
* run ObjectId validation (for ObjectId schemas) or skip it (for String /
|
|
9
|
+
* Number / UUID schemas).
|
|
10
|
+
*
|
|
11
|
+
* Also usable standalone — Arc controllers, custom repositories, migration
|
|
12
|
+
* scripts, or test harnesses can import these directly to make the same
|
|
13
|
+
* decision without depending on Repository.
|
|
14
|
+
*
|
|
15
|
+
* Design: no `this`, no Model, no DB calls. Only reads `schema.paths._id.instance`.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* Check if a Mongoose SchemaType `instance` string is an ObjectId variant.
|
|
19
|
+
* Mongoose 9.x uses `'ObjectId'`, older versions use `'ObjectID'`. This
|
|
20
|
+
* is the single source of truth — use it instead of inline
|
|
21
|
+
* `instance === 'ObjectId' || instance === 'ObjectID'` checks.
|
|
22
|
+
*/
|
|
23
|
+
function isObjectIdInstance(instance) {
|
|
24
|
+
return instance === "ObjectId" || instance === "ObjectID";
|
|
25
|
+
}
|
|
26
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
27
|
+
/**
|
|
28
|
+
* Detect the _id path type from a Mongoose schema.
|
|
29
|
+
*
|
|
30
|
+
* Returns 'objectid' as the safe default when:
|
|
31
|
+
* - No schema is provided (mock models, test harnesses)
|
|
32
|
+
* - The schema has no `paths._id` (auto-generated ObjectId)
|
|
33
|
+
* - The instance string is unrecognized
|
|
34
|
+
*
|
|
35
|
+
* This safe-default means existing callers that don't set _id explicitly
|
|
36
|
+
* get the same ObjectId validation they always had — the behavioral change
|
|
37
|
+
* is strictly opt-in via schema declaration.
|
|
38
|
+
*/
|
|
39
|
+
function getSchemaIdType(schema) {
|
|
40
|
+
const instance = schema?.paths?._id?.instance;
|
|
41
|
+
if (!instance) return "objectid";
|
|
42
|
+
if (isObjectIdInstance(instance)) return "objectid";
|
|
43
|
+
switch (instance) {
|
|
44
|
+
case "String": return "string";
|
|
45
|
+
case "Number": return "number";
|
|
46
|
+
case "UUID": return "uuid";
|
|
47
|
+
default: return "unknown";
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Validate an id value against a known _id type — WITHOUT hitting the DB.
|
|
52
|
+
*
|
|
53
|
+
* Returns `true` when the value is structurally valid for the given type.
|
|
54
|
+
* Returns `false` when it's definitely invalid (Repository should return
|
|
55
|
+
* 404 / null immediately rather than wasting a round-trip).
|
|
56
|
+
*
|
|
57
|
+
* For `'string'` and `'unknown'` types, validation is intentionally
|
|
58
|
+
* permissive: any truthy, non-empty value is accepted. The DB is the
|
|
59
|
+
* authority for whether the document exists — we only reject values that
|
|
60
|
+
* are structurally impossible (null, undefined, empty string).
|
|
61
|
+
*/
|
|
62
|
+
function isValidIdForType(id, idType) {
|
|
63
|
+
if (id === null || id === void 0) return false;
|
|
64
|
+
switch (idType) {
|
|
65
|
+
case "objectid": {
|
|
66
|
+
if (typeof id === "object") return mongoose.Types.ObjectId.isValid(id);
|
|
67
|
+
const s = String(id);
|
|
68
|
+
return s.length > 0 && mongoose.Types.ObjectId.isValid(s);
|
|
69
|
+
}
|
|
70
|
+
case "string": return String(id).length > 0;
|
|
71
|
+
case "number": {
|
|
72
|
+
if (typeof id === "number") return Number.isFinite(id);
|
|
73
|
+
const n = Number(id);
|
|
74
|
+
return Number.isFinite(n);
|
|
75
|
+
}
|
|
76
|
+
case "uuid": return typeof id === "string" && UUID_RE.test(id);
|
|
77
|
+
case "unknown": return String(id).length > 0;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
//#endregion
|
|
81
|
+
export { isObjectIdInstance as n, isValidIdForType as r, getSchemaIdType as t };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $ as ReadPreferenceType, At as LookupOptions, D as GroupResult, V as ObjectId, W as OperationOptions, Z as PopulateSpec, _t as UpdateOptions, a as AnyDocument, b as DeleteResult, g as CreateOptions, gt as UpdateManyResult, pt as SortSpec, st as SelectSpec, vt as UpdateWithValidationResult, z as MinMaxResult } from "./types-
|
|
1
|
+
import { $ as ReadPreferenceType, At as LookupOptions, D as GroupResult, V as ObjectId, W as OperationOptions, Z as PopulateSpec, _t as UpdateOptions, a as AnyDocument, b as DeleteResult, g as CreateOptions, gt as UpdateManyResult, pt as SortSpec, st as SelectSpec, vt as UpdateWithValidationResult, z as MinMaxResult } from "./types-BwO4u3JN.mjs";
|
|
2
2
|
import { ClientSession, Model, PipelineStage } from "mongoose";
|
|
3
3
|
|
|
4
4
|
//#region src/actions/aggregate.d.ts
|