@classytic/mongokit 3.28.0 → 3.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +54 -1
  3. package/TRADEMARK.md +22 -0
  4. package/dist/{create-repository-D4ZU94b3.d.mts → Repository-BXHkUtLk.d.mts} +30 -57
  5. package/dist/{Repository-BFBTAMlA.mjs → Repository-X7U2d0kU.mjs} +417 -47
  6. package/dist/actions/index.mjs +1 -1
  7. package/dist/adapter/index.mjs +1 -1
  8. package/dist/{adapter-CLvUHiTW.mjs → adapter-BitHg-CN.mjs} +1 -1
  9. package/dist/ai/index.d.mts +1 -1
  10. package/dist/better-auth/index.d.mts +13 -0
  11. package/dist/better-auth/index.mjs +76 -6
  12. package/dist/create-repository-CvD4wzuN.d.mts +54 -0
  13. package/dist/{create-repository-2DMwkYVz.mjs → create-repository-DYe5YEie.mjs} +3 -2
  14. package/dist/index.d.mts +275 -41
  15. package/dist/index.mjs +80 -7
  16. package/dist/kernel-conformance/index.d.mts +217 -0
  17. package/dist/kernel-conformance/index.mjs +603 -0
  18. package/dist/method-registry.plugin-DblM7V6j.mjs +112 -0
  19. package/dist/model-registry-ClKtWiHa.mjs +67 -0
  20. package/dist/{mongooseToJsonSchema-CByt1VhQ.mjs → mongooseToJsonSchema-DKbnKduN.mjs} +14 -3
  21. package/dist/{multi-tenant.plugin-DVyVGREb.d.mts → multi-tenant.plugin-2VtSSm1Y.d.mts} +1 -1
  22. package/dist/outbox/index.d.mts +63 -0
  23. package/dist/outbox/index.mjs +239 -0
  24. package/dist/{plugin-options-vFufQZMZ.d.mts → plugin-options-BsFH0sTi.d.mts} +1 -1
  25. package/dist/plugins/index.d.mts +4 -4
  26. package/dist/plugins/index.mjs +3 -2
  27. package/dist/{repository-DCHRWRXw.d.mts → repository-Q16ErTu8.d.mts} +19 -1
  28. package/dist/testkit/index.d.mts +8 -1
  29. package/dist/testkit/index.mjs +1 -1
  30. package/dist/{timestamp.plugin-ennbatTW.mjs → timestamp.plugin-ChFRcLRJ.mjs} +32 -143
  31. package/dist/{to-plain-DkF5px0R.d.mts → to-plain-BuX1RUZV.d.mts} +12 -4
  32. package/dist/{update-1Lq9OMD2.mjs → update-DHdXdOk_.mjs} +43 -30
  33. package/dist/utils/index.d.mts +2 -2
  34. package/dist/utils/index.mjs +1 -1
  35. package/dist/{validation-chain.plugin-Cm0Ttnd8.d.mts → validation-chain.plugin-BSshLJTA.d.mts} +44 -44
  36. package/dist/{validation-chain.plugin-k9joH2qw.mjs → validation-chain.plugin-BxTl_dYR.mjs} +139 -138
  37. package/package.json +28 -15
@@ -1,5 +1,5 @@
1
1
  import { i as isValidIdForType, t as getSchemaIdType } from "./id-resolution-BqDrk0S2.mjs";
2
- import { a as count, b as LookupBuilder, c as getByQuery, d as deleteById, f as deleteByQuery, h as createMany, l as getOrCreate, m as create, n as update, o as exists, r as updateByQuery, s as getById, t as findOneAndUpdate, x as compileFilterToMongo, y as distinct } from "./update-1Lq9OMD2.mjs";
2
+ import { a as count, b as LookupBuilder, c as getByQuery, d as deleteById, f as deleteByQuery, h as createMany, l as getOrCreate, m as create, n as update, o as exists, r as updateByQuery, s as getById, t as findOneAndUpdate, x as compileFilterToMongo, y as distinct } from "./update-DHdXdOk_.mjs";
3
3
  import { n as warn } from "./logger-K7dPmu2U.mjs";
4
4
  import { n as isDuplicateKeyError, r as parseDuplicateKeyError, t as createError } from "./error-BuByZwKU.mjs";
5
5
  import { t as PaginationEngine } from "./PaginationEngine-CczbvCAq.mjs";
@@ -80,9 +80,7 @@ function createKeysetPurgePort(repo, baseFilter, session) {
80
80
  [strategy.deletedAtField ?? "deletedAt"]: /* @__PURE__ */ new Date()
81
81
  } }, baseOpts);
82
82
  break;
83
- case "anonymize":
84
- await repo.updateMany(chunkFilter, { $set: strategy.fields }, baseOpts);
85
- break;
83
+ case "anonymize": await repo.updateMany(chunkFilter, { $set: strategy.fields }, baseOpts);
86
84
  }
87
85
  cursor = ids[ids.length - 1];
88
86
  return idDocs.length;
@@ -127,21 +125,44 @@ async function purgeAnonymizeFunctional(repo, selectFilter, fields, limit, sessi
127
125
  }
128
126
  //#endregion
129
127
  //#region src/capabilities.ts
130
- const MONGOKIT_CAPABILITIES = {
128
+ /**
129
+ * Read the provenance of a capability descriptor's `transactions` flag.
130
+ * A descriptor without the field is a static declaration (`'declared'`) —
131
+ * which is exactly right for sqlitekit / prismakit / any kit whose answer
132
+ * genuinely does not depend on a live deployment.
133
+ */
134
+ function transactionResolutionOf(caps) {
135
+ const value = caps.transactionsResolution;
136
+ return value === "observed" || value === "unknown" ? value : "declared";
137
+ }
138
+ /**
139
+ * What MongoDB supports through mongokit's surface, DECLARED (not observed).
140
+ *
141
+ * Deployment-dependent flags (`transactions`, `nestedTransactions`,
142
+ * `changeStreams` — all three need a replica set or mongos) are `true` here
143
+ * because they describe the product, not the server you happen to be pointed
144
+ * at. **Do not read this constant to decide whether a live connection can run
145
+ * a transaction** — that is {@link resolveMongoCapabilities}, which is what
146
+ * `Repository#capabilities` returns. This one is for the cross-kit conformance
147
+ * harness (which overrides environment-specific limits where it is
148
+ * constructed) and as the base every observed descriptor is derived from.
149
+ */
150
+ const MONGOKIT_CAPABILITIES = Object.freeze({
151
+ transactionsResolution: "declared",
131
152
  transactions: true,
132
153
  nestedTransactions: true,
133
154
  upsert: true,
134
155
  duplicateKeyError: true,
135
156
  distinct: true,
136
157
  aggregate: true,
137
- aggregateOps: {
158
+ aggregateOps: Object.freeze({
138
159
  percentile: true,
139
160
  stddev: true,
140
161
  topN: true,
141
162
  customDateBuckets: true,
142
163
  dateBucketSubMinute: true,
143
164
  cache: true
144
- },
165
+ }),
145
166
  getOrCreate: true,
146
167
  countAndExists: true,
147
168
  purgeByField: true,
@@ -153,7 +174,195 @@ const MONGOKIT_CAPABILITIES = {
153
174
  lean: true,
154
175
  lookupPopulate: true,
155
176
  streaming: true
156
- };
177
+ });
178
+ /**
179
+ * Driver `ServerType`s that positively imply a session-capable, transaction-
180
+ * capable deployment. Verified against mongodb driver 7.x: a single-node
181
+ * replica set reports `RSPrimary` (topology `ReplicaSetWithPrimary`, or
182
+ * `Single` when reached with `directConnection`), a standalone `mongod`
183
+ * reports `Standalone`, and a mongos reports `Mongos`.
184
+ *
185
+ * **This is why the check reads SERVER type, not just topology type.** The
186
+ * pre-existing heuristic (`topology.description.type !== 'Single'`) has a
187
+ * false NEGATIVE: a directly-connected single-node replica set is `Single`
188
+ * yet runs transactions fine.
189
+ */
190
+ const TRANSACTION_CAPABLE_SERVER_TYPES = /* @__PURE__ */ new Set([
191
+ "RSPrimary",
192
+ "RSSecondary",
193
+ "RSArbiter",
194
+ "RSOther",
195
+ "RSGhost",
196
+ "Mongos",
197
+ "LoadBalancer"
198
+ ]);
199
+ /** Topology types that positively imply transactions regardless of server detail. */
200
+ const TRANSACTION_CAPABLE_TOPOLOGY_TYPES = /* @__PURE__ */ new Set([
201
+ "ReplicaSetWithPrimary",
202
+ "ReplicaSetNoPrimary",
203
+ "Sharded",
204
+ "LoadBalanced"
205
+ ]);
206
+ function clientOf(connection) {
207
+ const probe = connection;
208
+ if (!probe || typeof probe !== "object") return void 0;
209
+ try {
210
+ return probe.getClient?.() ?? probe.client ?? (probe.topology !== void 0 ? probe : void 0);
211
+ } catch {
212
+ return;
213
+ }
214
+ }
215
+ function serverTypes(description) {
216
+ const servers = description.servers;
217
+ if (!servers) return [];
218
+ return (typeof servers.values === "function" ? [...servers.values()] : Object.values(servers)).map((s) => s?.type).filter((t) => typeof t === "string");
219
+ }
220
+ /**
221
+ * Read whether a connection's LIVE deployment supports multi-document
222
+ * transactions, synchronously, from the driver's SDAM topology description.
223
+ *
224
+ * Free (a few property reads against an object the driver keeps current), so
225
+ * it is deliberately NOT memoized — a connection that opens after the repo was
226
+ * constructed upgrades its own answer with no invalidation step. What IS
227
+ * memoized is the async {@link probeMongoCapabilities} result, which this
228
+ * function consults first so an exotic deployment that answered `hello` but
229
+ * exposes no topology still reports definitively.
230
+ *
231
+ * @returns `'yes'` / `'no'` when the topology is conclusive, `'unknown'`
232
+ * otherwise — never optimistic. See the module docblock on rule 3.
233
+ */
234
+ function resolveTransactionSupport(connection) {
235
+ const cached = readProbeCache(connection);
236
+ if (cached) return cached;
237
+ const description = clientOf(connection)?.topology?.description;
238
+ if (!description) return "unknown";
239
+ const types = serverTypes(description);
240
+ if (types.some((t) => TRANSACTION_CAPABLE_SERVER_TYPES.has(t))) return "yes";
241
+ if (types.length > 0 && types.every((t) => t === "Standalone")) return "no";
242
+ const topologyType = description.type;
243
+ if (topologyType && TRANSACTION_CAPABLE_TOPOLOGY_TYPES.has(topologyType)) return "yes";
244
+ return "unknown";
245
+ }
246
+ function deriveCapabilities(support) {
247
+ const available = support === "yes";
248
+ return Object.freeze({
249
+ ...MONGOKIT_CAPABILITIES,
250
+ transactionsResolution: support === "unknown" ? "unknown" : "observed",
251
+ transactions: available,
252
+ nestedTransactions: available,
253
+ changeStreams: available
254
+ });
255
+ }
256
+ /** Three possible answers ⇒ three frozen singletons; the getter allocates nothing. */
257
+ const DERIVED = Object.freeze({
258
+ yes: deriveCapabilities("yes"),
259
+ no: deriveCapabilities("no"),
260
+ unknown: deriveCapabilities("unknown")
261
+ });
262
+ /**
263
+ * The capability descriptor for a LIVE connection — what
264
+ * `Repository#capabilities` returns.
265
+ *
266
+ * Every deployment-dependent flag reflects the observed topology; everything
267
+ * else comes from {@link MONGOKIT_CAPABILITIES}. Cheap enough to call per
268
+ * access (topology read + one frozen-object lookup).
269
+ */
270
+ function resolveMongoCapabilities(connection) {
271
+ return DERIVED[resolveTransactionSupport(connection)];
272
+ }
273
+ /** The three derived descriptors, for tests and for gates that want to compare identity. */
274
+ function capabilitiesForSupport(support) {
275
+ return DERIVED[support];
276
+ }
277
+ /** Definitive results only. Keyed on the connection object; never holds `'unknown'`. */
278
+ const PROBE_RESULTS = /* @__PURE__ */ new WeakMap();
279
+ /** In-flight de-duplication so N repos on one connection issue ONE `hello`. */
280
+ const PROBE_INFLIGHT = /* @__PURE__ */ new WeakMap();
281
+ function readProbeCache(connection) {
282
+ if (!connection || typeof connection !== "object") return void 0;
283
+ return PROBE_RESULTS.get(connection);
284
+ }
285
+ /** Test seam — drop memoized probe answers (a reused connection object across suites). */
286
+ function resetCapabilityProbeCache(connection) {
287
+ if (connection && typeof connection === "object") {
288
+ PROBE_RESULTS.delete(connection);
289
+ PROBE_INFLIGHT.delete(connection);
290
+ }
291
+ }
292
+ function withTimeout(promise, ms, onTimeout) {
293
+ return new Promise((resolve) => {
294
+ const timer = setTimeout(() => resolve(onTimeout), ms);
295
+ timer.unref?.();
296
+ promise.then((value) => {
297
+ clearTimeout(timer);
298
+ resolve(value);
299
+ }, () => {
300
+ clearTimeout(timer);
301
+ resolve(onTimeout);
302
+ });
303
+ });
304
+ }
305
+ /**
306
+ * Classify a `hello` (or legacy `isMaster`) reply.
307
+ *
308
+ * - `msg: 'isdbgrid'` ⇒ mongos ⇒ transactions (Mongo 4.2+ sharded).
309
+ * - `setName` ⇒ a replica-set member ⇒ transactions.
310
+ * - a reply that IS a hello (`isWritablePrimary` / `ismaster` present) with
311
+ * neither marker ⇒ standalone ⇒ **no**. Verified: a memory-server standalone
312
+ * returns `{ isWritablePrimary: true }` and nothing else.
313
+ * - anything unrecognised ⇒ `'unknown'`. A reply we cannot parse is not a yes.
314
+ */
315
+ function classifyHelloReply(reply) {
316
+ if (!reply || typeof reply !== "object") return "unknown";
317
+ const { msg, setName } = reply;
318
+ if (msg === "isdbgrid") return "yes";
319
+ if (typeof setName === "string" && setName.length > 0) return "yes";
320
+ return "isWritablePrimary" in reply || "ismaster" in reply ? "no" : "unknown";
321
+ }
322
+ async function runProbe(connection, timeoutMs) {
323
+ if (connection.readyState === 2 && typeof connection.asPromise === "function") await withTimeout(connection.asPromise().then(() => void 0, () => void 0), timeoutMs, void 0);
324
+ const fromTopology = resolveTransactionSupport(connection);
325
+ if (fromTopology !== "unknown") return fromTopology;
326
+ const admin = connection.db?.admin?.();
327
+ if (!admin) return "unknown";
328
+ return classifyHelloReply(await withTimeout(admin.command({ hello: 1 }).catch(async () => admin.command({ isMaster: 1 }).catch(() => void 0)), timeoutMs, void 0));
329
+ }
330
+ /**
331
+ * Resolve a connection's real capabilities, waiting for (and if necessary
332
+ * asking) the deployment. Memoized per connection: N repositories and N
333
+ * kernels on one connection issue at most ONE `hello`.
334
+ *
335
+ * Hosts do not have to call this — `Repository#capabilities` reads the live
336
+ * topology synchronously, which is already conclusive once
337
+ * `await mongoose.connect()` has returned (the Kernel Construction Standard's
338
+ * Describe → **Bind** ordering guarantees that). It exists for the cases the
339
+ * sync read cannot cover: binding while a connection is still opening, or a
340
+ * deployment whose driver surfaces no topology description. Calling it makes
341
+ * the SUBSEQUENT synchronous reads definitive, so nothing else needs rewiring:
342
+ *
343
+ * ```ts
344
+ * await probeMongoCapabilities(connection); // once, before bind
345
+ * const engine = defineWallet().bind(connection); // gate now sees the truth
346
+ * ```
347
+ *
348
+ * Only definitive answers are cached — an `'unknown'` may be re-probed later
349
+ * (the deployment may simply not have been up yet).
350
+ */
351
+ async function probeMongoCapabilities(connection, options = {}) {
352
+ if (!connection || typeof connection !== "object") return DERIVED.unknown;
353
+ const key = connection;
354
+ const cached = PROBE_RESULTS.get(key);
355
+ if (cached) return DERIVED[cached];
356
+ const inflight = PROBE_INFLIGHT.get(key);
357
+ if (inflight) return DERIVED[await inflight];
358
+ const run = runProbe(connection, options.timeoutMs ?? 2e3).catch(() => "unknown").then((support) => {
359
+ if (support !== "unknown") PROBE_RESULTS.set(key, support);
360
+ PROBE_INFLIGHT.delete(key);
361
+ return support;
362
+ });
363
+ PROBE_INFLIGHT.set(key, run);
364
+ return DERIVED[await run];
365
+ }
157
366
  //#endregion
158
367
  //#region src/operations.ts
159
368
  /**
@@ -317,6 +526,122 @@ function applyExecutionHints(aggregation, hints) {
317
526
  if (hints.indexHint !== void 0) aggregation.option({ hint: hints.indexHint });
318
527
  }
319
528
  //#endregion
529
+ //#region src/filter/cast-schema.ts
530
+ /** Operators whose operand is a single scalar to cast. */
531
+ const SCALAR_OPS = /* @__PURE__ */ new Set([
532
+ "$eq",
533
+ "$ne",
534
+ "$gt",
535
+ "$gte",
536
+ "$lt",
537
+ "$lte"
538
+ ]);
539
+ /** Operators whose operand is an array of scalars to cast element-wise. */
540
+ const ARRAY_OPS = /* @__PURE__ */ new Set(["$in", "$nin"]);
541
+ /** Logical operators whose operand is an ARRAY of sub-filters. */
542
+ const LOGICAL_ARRAY_OPS = /* @__PURE__ */ new Set([
543
+ "$and",
544
+ "$or",
545
+ "$nor"
546
+ ]);
547
+ /** Logical operators whose operand is a SINGLE nested sub-filter. */
548
+ const LOGICAL_OBJECT_OPS = /* @__PURE__ */ new Set(["$not"]);
549
+ function isPlainObject(value) {
550
+ return value !== null && typeof value === "object" && !Array.isArray(value) && !(value instanceof Date);
551
+ }
552
+ /**
553
+ * Cast one value through a schema path. Returns the original value when the
554
+ * path is unknown, the value is nullish, or the cast throws — never raises.
555
+ */
556
+ function castValue(schema, field, value) {
557
+ if (value === null || value === void 0) return value;
558
+ if (typeof value === "object" && !(value instanceof Date)) return value;
559
+ let schemaType;
560
+ try {
561
+ schemaType = schema.path(field);
562
+ } catch {
563
+ return value;
564
+ }
565
+ if (!schemaType || typeof schemaType.cast !== "function") return value;
566
+ try {
567
+ return schemaType.cast(value);
568
+ } catch {
569
+ return value;
570
+ }
571
+ }
572
+ /**
573
+ * Cast the operand(s) of a field-level operator object, leaving operators we
574
+ * don't understand (`$regex`, `$exists`, `$size`, `$elemMatch`, …) alone.
575
+ */
576
+ function castOperatorObject(schema, field, ops) {
577
+ let changed = false;
578
+ const out = {};
579
+ for (const [op, operand] of Object.entries(ops)) if (SCALAR_OPS.has(op)) {
580
+ const next = castValue(schema, field, operand);
581
+ out[op] = next;
582
+ if (next !== operand) changed = true;
583
+ } else if (ARRAY_OPS.has(op) && Array.isArray(operand)) {
584
+ const arr = operand.map((entry) => castValue(schema, field, entry));
585
+ out[op] = arr;
586
+ if (arr.some((v, i) => v !== operand[i])) changed = true;
587
+ } else out[op] = operand;
588
+ return changed ? out : ops;
589
+ }
590
+ /**
591
+ * Cast a record-shape Mongo filter against a Mongoose schema so it is safe to
592
+ * hand to a `$match` stage. Recurses `$and` / `$or` / `$nor` / `$not`, since a
593
+ * policy/tenant scope conjoined with a caller filter nests one level down.
594
+ *
595
+ * Filter IR nodes and non-objects pass through untouched — IR values are built
596
+ * in code (already correctly typed) and carry no schema paths to resolve.
597
+ *
598
+ * @example
599
+ * ```ts
600
+ * // organizationId is an ObjectId column; arc supplies the scope id as a string
601
+ * castFilterToSchema({ organizationId: '6a6a…' }, Order.schema)
602
+ * // → { organizationId: ObjectId('6a6a…') }
603
+ * ```
604
+ */
605
+ function castFilterToSchema(filter, schema) {
606
+ if (!isPlainObject(filter)) return filter;
607
+ if (typeof filter.op === "string") return filter;
608
+ let changed = false;
609
+ const out = {};
610
+ for (const [key, value] of Object.entries(filter)) {
611
+ if (LOGICAL_ARRAY_OPS.has(key) && Array.isArray(value)) {
612
+ const arr = value.map((entry) => castFilterToSchema(entry, schema));
613
+ out[key] = arr;
614
+ if (arr.some((v, i) => v !== value[i])) changed = true;
615
+ continue;
616
+ }
617
+ if (LOGICAL_OBJECT_OPS.has(key) && isPlainObject(value)) {
618
+ const nested = castFilterToSchema(value, schema);
619
+ out[key] = nested;
620
+ if (nested !== value) changed = true;
621
+ continue;
622
+ }
623
+ if (key.startsWith("$")) {
624
+ out[key] = value;
625
+ continue;
626
+ }
627
+ if (isPlainObject(value)) {
628
+ const keys = Object.keys(value);
629
+ if (keys.length > 0 && keys.every((k) => k.startsWith("$"))) {
630
+ const nextOps = castOperatorObject(schema, key, value);
631
+ out[key] = nextOps;
632
+ if (nextOps !== value) changed = true;
633
+ continue;
634
+ }
635
+ out[key] = value;
636
+ continue;
637
+ }
638
+ const next = castValue(schema, key, value);
639
+ out[key] = next;
640
+ if (next !== value) changed = true;
641
+ }
642
+ return changed ? out : filter;
643
+ }
644
+ //#endregion
320
645
  //#region src/actions/aggregate-ir/normalize.ts
321
646
  /**
322
647
  * AggRequest → input normalization (mongokit binding).
@@ -580,16 +905,25 @@ function compileMeasure(measure) {
580
905
  }
581
906
  //#endregion
582
907
  //#region src/actions/aggregate-ir/pipeline.ts
583
- function buildAggPipeline(req) {
908
+ function buildAggPipeline(req, schema) {
584
909
  validateMeasures$1(req.measures);
585
910
  const groupCols = normalizeGroupBy(req.groupBy);
586
911
  const bucketAliases = req.dateBuckets ? Object.keys(req.dateBuckets) : [];
587
912
  validateBucketAliases(bucketAliases, groupCols, req.measures);
588
913
  const stages = [];
589
914
  const aliasSet = new Set((req.lookups ?? []).map((l) => l.as ?? l.from));
590
- const { base: baseFilter, joined: joinedFilter } = splitFilterByAlias(req.filter, aliasSet);
915
+ const { base: baseFilter, joined: joinedFilter } = splitFilterByAlias(schema !== void 0 && req.filter !== void 0 ? castFilterToSchema(req.filter, schema) : req.filter, aliasSet);
591
916
  if (baseFilter !== void 0) {
592
- const match = compileFilterToMongo(baseFilter);
917
+ /**
918
+ * The SCHEMA is passed so the date coercion can consult it.
919
+ *
920
+ * `castFilterToSchema` above already casts operands to their declared types; without
921
+ * the schema here, `compileFilterToMongo`'s coercion then guessed from value shape
922
+ * alone and OVERRODE that cast — turning a `'2026-08-02'` bound on a String column
923
+ * into a Date, which BSON refuses to compare against the stored string. The aggregate
924
+ * returned zero rows and nothing indicated why.
925
+ */
926
+ const match = compileFilterToMongo(baseFilter, schema);
593
927
  if (Object.keys(match).length > 0) stages.push({ $match: match });
594
928
  }
595
929
  if (req.lookups && req.lookups.length > 0) appendLookupStages$1(stages, req.lookups);
@@ -869,7 +1203,7 @@ async function countAggGroups(Model, req, options = {}) {
869
1203
  const hasDottedGroupBy = groupCols.some((f) => f.includes("."));
870
1204
  const hasDateBuckets = !!req.dateBuckets && Object.keys(req.dateBuckets).length > 0;
871
1205
  if (req.having !== void 0 || req.lookups !== void 0 && req.lookups.length > 0 || hasDottedGroupBy || hasDateBuckets) {
872
- const { pipeline, prePaginationIndex } = buildAggPipeline(req);
1206
+ const { pipeline, prePaginationIndex } = buildAggPipeline(req, Model.schema);
873
1207
  const finalPipeline = [...pipeline.slice(0, prePaginationIndex), { $count: "n" }];
874
1208
  const aggregation = Model.aggregate(finalPipeline);
875
1209
  if (session) aggregation.session(session);
@@ -877,7 +1211,7 @@ async function countAggGroups(Model, req, options = {}) {
877
1211
  const [row] = await aggregation.exec();
878
1212
  return row?.n ?? 0;
879
1213
  }
880
- const match = req.filter ? compileFilterToMongo(req.filter) : {};
1214
+ const match = req.filter ? compileFilterToMongo(castFilterToSchema(req.filter, Model.schema), Model.schema) : {};
881
1215
  if (groupCols.length === 0) {
882
1216
  const pipeline = [];
883
1217
  if (Object.keys(match).length > 0) pipeline.push({ $match: match });
@@ -906,7 +1240,7 @@ async function countAggGroups(Model, req, options = {}) {
906
1240
  //#region src/actions/aggregate-ir/execute.ts
907
1241
  async function executeAgg(Model, req, options = {}) {
908
1242
  const session = options.session;
909
- const { pipeline } = buildAggPipeline(req);
1243
+ const { pipeline } = buildAggPipeline(req, Model.schema);
910
1244
  const aggregation = Model.aggregate(pipeline);
911
1245
  if (session) aggregation.session(session);
912
1246
  applyExecutionHints(aggregation, req.executionHints);
@@ -1783,29 +2117,29 @@ function isTransactionUnsupported(error) {
1783
2117
  /**
1784
2118
  * PROACTIVELY report whether a connection's deployment supports multi-document
1785
2119
  * transactions — the read-side companion to {@link isTransactionUnsupported}
1786
- * (which classifies a FAILED attempt). Single source of truth for the topology
1787
- * check so callers don't hand-roll `client.topology.description.type` probing.
2120
+ * (which classifies a FAILED attempt). Lets a caller skip a doomed transaction
2121
+ * attempt on standalone dev Mongo rather than starting one just to catch the
2122
+ * error.
1788
2123
  *
1789
- * Returns `false` ONLY when the topology is positively a standalone (`'Single'`)
1790
- * server; an unknown/undefined topology (not yet connected, non-standard
1791
- * connection object) returns `true` — optimistic, because a genuine failure is
1792
- * still caught reactively by `isTransactionUnsupported`. Lets a caller skip a
1793
- * doomed transaction attempt on standalone dev Mongo rather than starting one
1794
- * just to catch the error.
2124
+ * Delegates to {@link resolveTransactionSupport} so mongokit has exactly ONE
2125
+ * topology reader. That also fixes a false NEGATIVE this function used to
2126
+ * have: it tested `description.type !== 'Single'`, but a single-node replica
2127
+ * set reached with `directConnection` is `Single` with a server type of
2128
+ * `RSPrimary` and runs transactions fine. The shared reader looks at the
2129
+ * SERVER descriptions first.
2130
+ *
2131
+ * **`unknown` is optimistic HERE and pessimistic in `capabilities.ts` — the
2132
+ * difference is deliberate.** This answers "should I bother ATTEMPTING a
2133
+ * transaction?", where guessing yes costs one caught error (`allowFallback`
2134
+ * still recovers). The capability descriptor answers "may this deployment be
2135
+ * trusted with money?", where guessing yes is the bug class this repo keeps
2136
+ * hitting (AGENTS.md FAIL LOUD rule 3). Do not "unify" them.
1795
2137
  *
1796
2138
  * Accepts a Mongoose `Connection` (or anything exposing `getClient()`/`client`
1797
2139
  * with a driver `topology`), so it stays decoupled from mongoose types.
1798
2140
  */
1799
2141
  function supportsTransactions(connection) {
1800
- const probe = connection;
1801
- if (!probe) return true;
1802
- let client;
1803
- try {
1804
- client = probe.getClient?.() ?? probe.client ?? void 0;
1805
- } catch {
1806
- return true;
1807
- }
1808
- return client?.topology?.description?.type !== "Single";
2142
+ return resolveTransactionSupport(connection) !== "no";
1809
2143
  }
1810
2144
  //#endregion
1811
2145
  //#region src/Repository.ts
@@ -1870,8 +2204,31 @@ var Repository = class extends RepositoryBase {
1870
2204
  * Runtime capability descriptor required by `StandardRepo<TDoc>`
1871
2205
  * (repo-core 0.6.0). Hosts feature-detect once at boot instead of
1872
2206
  * try/catching `UnsupportedOperationError` per call.
2207
+ *
2208
+ * **OBSERVED, not declared** (changed 2026-08-05). Until then this was a
2209
+ * class field pointing at the static `MONGOKIT_CAPABILITIES`, so
2210
+ * `transactions` read `true` for every mongoose-backed repository —
2211
+ * including one pointed at a standalone `mongod`, which cannot run a
2212
+ * transaction at all. Every kernel boot gate that reads it (wallet, party,
2213
+ * invoice, purchase, flow, …) was therefore unfalsifiable decoration
2214
+ * (AGENTS.md FAIL LOUD rule 4).
2215
+ *
2216
+ * Now it derives the deployment-dependent flags (`transactions`,
2217
+ * `nestedTransactions`, `changeStreams`) from THIS model's connection and
2218
+ * its live SDAM topology. A getter rather than a constructor snapshot on
2219
+ * purpose: a repository built before `mongoose.connect()` resolves upgrades
2220
+ * its own answer the moment the connection is up, with no invalidation step.
2221
+ * When the topology cannot be read the answer is `transactions: false` +
2222
+ * `transactionsResolution: 'unknown'` — fail closed, never an optimistic
2223
+ * yes (rule 3). See `capabilities.ts` and `probeMongoCapabilities`.
2224
+ *
2225
+ * `options.capabilities` overrides the whole descriptor for hosts/tests that
2226
+ * genuinely know better (a proxied connection, a non-mongoose driver seam).
1873
2227
  */
1874
- capabilities = MONGOKIT_CAPABILITIES;
2228
+ get capabilities() {
2229
+ return this._capabilitiesOverride ?? resolveMongoCapabilities(this.Model?.db);
2230
+ }
2231
+ _capabilitiesOverride;
1875
2232
  Model;
1876
2233
  /**
1877
2234
  * Mongoose model name. Duplicated on the instance for BC — arc / catalog /
@@ -1904,6 +2261,7 @@ var Repository = class extends RepositoryBase {
1904
2261
  warn(m);
1905
2262
  });
1906
2263
  this.Model = Model;
2264
+ this._capabilitiesOverride = options.capabilities;
1907
2265
  this.model = Model.modelName;
1908
2266
  this._pagination = new PaginationEngine(Model, paginationConfig);
1909
2267
  this.idField = options.idField ?? "_id";
@@ -2202,7 +2560,8 @@ var Repository = class extends RepositoryBase {
2202
2560
  return result;
2203
2561
  }
2204
2562
  const wantsThrow = context.throwOnNotFound === true || options.throwOnNotFound === true;
2205
- if (!isValidIdForType(id, getSchemaIdType(this.Model.schema))) {
2563
+ const idType = getSchemaIdType(this.Model.schema);
2564
+ if (!isValidIdForType(id, idType)) {
2206
2565
  if (wantsThrow) throw createError(404, "Document not found");
2207
2566
  return null;
2208
2567
  }
@@ -2706,8 +3065,9 @@ var Repository = class extends RepositoryBase {
2706
3065
  /**
2707
3066
  * Update a document by id and return the post-update doc.
2708
3067
  *
2709
- * Compiles to `Model.findOneAndUpdate(filter, { $set: data }, { new:
2710
- * true, runValidators: true })` under the hood and routes through the
3068
+ * Compiles to `Model.findOneAndUpdate(filter, { $set: data },
3069
+ * { returnDocument: 'after', runValidators: true })` under the hood and
3070
+ * routes through the
2711
3071
  * full plugin pipeline — `multiTenantPlugin` injects the tenant scope,
2712
3072
  * `auditTrailPlugin` records the change, `cachePlugin` invalidates,
2713
3073
  * `softDeletePlugin` etc. all compose. Returns `null` on miss (the
@@ -2722,7 +3082,7 @@ var Repository = class extends RepositoryBase {
2722
3082
  * const doc = await CampaignModel.findOneAndUpdate(
2723
3083
  * { _id: id, organizationId: ctx.orgId }, // hand-rolled tenant filter
2724
3084
  * { $set: patch, $currentDate: { updatedAt: 1 } },
2725
- * { new: true, runValidators: true }
3085
+ * { returnDocument: 'after', runValidators: true }
2726
3086
  * );
2727
3087
  * return doc ? toEntity(doc) : null;
2728
3088
  *
@@ -2751,7 +3111,7 @@ var Repository = class extends RepositoryBase {
2751
3111
  * const doc = await CampaignModel.findOneAndUpdate(
2752
3112
  * { _id: id, organizationId: ctx.orgId, status: 'pending' },
2753
3113
  * { $set: { status: 'sent', sentAt: new Date() } },
2754
- * { new: true }
3114
+ * { returnDocument: 'after' }
2755
3115
  * );
2756
3116
  * if (!doc) throw new ConcurrentTransitionError(id);
2757
3117
  *
@@ -2805,7 +3165,8 @@ var Repository = class extends RepositoryBase {
2805
3165
  const wantsThrow = context.throwOnNotFound === true || options.throwOnNotFound === true;
2806
3166
  const effectiveIdField = options.idField ?? this.idField;
2807
3167
  if (effectiveIdField === "_id") {
2808
- if (!isValidIdForType(id, getSchemaIdType(this.Model.schema))) {
3168
+ const idType = getSchemaIdType(this.Model.schema);
3169
+ if (!isValidIdForType(id, idType)) {
2809
3170
  if (wantsThrow) throw createError(404, "Document not found");
2810
3171
  await this._emitHook("after:update", {
2811
3172
  context,
@@ -3095,7 +3456,8 @@ var Repository = class extends RepositoryBase {
3095
3456
  const finalQuery = context.query || filter;
3096
3457
  const finalUpdate = context.data || update;
3097
3458
  if (effectiveIdField === "_id" && !options.upsert) {
3098
- if (!isValidIdForType(id, getSchemaIdType(this.Model.schema))) return null;
3459
+ const idType = getSchemaIdType(this.Model.schema);
3460
+ if (!isValidIdForType(id, idType)) return null;
3099
3461
  }
3100
3462
  if (Object.keys(finalUpdate).length === 0) return await this.Model.findOne(finalQuery, null, { session: options.session });
3101
3463
  return await findOneAndUpdate(this.Model, finalQuery, finalUpdate, {
@@ -3328,7 +3690,8 @@ var Repository = class extends RepositoryBase {
3328
3690
  const finalQuery = context.query || filter;
3329
3691
  const finalUpdate = context.data || operatorUpdate;
3330
3692
  if (effectiveIdField === "_id") {
3331
- if (!isValidIdForType(id, getSchemaIdType(this.Model.schema))) return null;
3693
+ const idType = getSchemaIdType(this.Model.schema);
3694
+ if (!isValidIdForType(id, idType)) return null;
3332
3695
  }
3333
3696
  return await findOneAndUpdate(this.Model, finalQuery, finalUpdate, {
3334
3697
  returnDocument: "after",
@@ -3381,7 +3744,8 @@ var Repository = class extends RepositoryBase {
3381
3744
  const effectiveIdField = options.idField ?? this.idField;
3382
3745
  const wantsThrow = context.throwOnNotFound === true || options.throwOnNotFound === true;
3383
3746
  if (effectiveIdField === "_id") {
3384
- if (!isValidIdForType(id, getSchemaIdType(this.Model.schema))) {
3747
+ const idType = getSchemaIdType(this.Model.schema);
3748
+ if (!isValidIdForType(id, idType)) {
3385
3749
  if (wantsThrow) throw createError(404, "Document not found");
3386
3750
  await this._emitHook("after:delete", {
3387
3751
  context,
@@ -3514,7 +3878,8 @@ var Repository = class extends RepositoryBase {
3514
3878
  * the port implementation.
3515
3879
  */
3516
3880
  async purgeByField(field, value, strategy, options = {}) {
3517
- return runChunkedPurge(strategy, options, createMongoPurgePort(this, field, value, options.session));
3881
+ const port = createMongoPurgePort(this, field, value, options.session);
3882
+ return runChunkedPurge(strategy, options, port);
3518
3883
  }
3519
3884
  /**
3520
3885
  * Range/filter-scoped purge — see `StandardRepo.purgeByFilter` for the
@@ -3532,7 +3897,8 @@ var Repository = class extends RepositoryBase {
3532
3897
  */
3533
3898
  async purgeByFilter(filter, strategy, options = {}) {
3534
3899
  const compiled = compileFilterToMongo(filter);
3535
- return runChunkedPurge(strategy, options, createMongoPurgePortFromFilter(this, compiled, options.session));
3900
+ const port = createMongoPurgePortFromFilter(this, compiled, options.session);
3901
+ return runChunkedPurge(strategy, options, port);
3536
3902
  }
3537
3903
  /**
3538
3904
  * Chunked cold-storage extraction — see `StandardRepo.archiveByFilter`
@@ -3547,7 +3913,8 @@ var Repository = class extends RepositoryBase {
3547
3913
  */
3548
3914
  async archiveByFilter(filter, sink, options = {}) {
3549
3915
  const compiled = compileFilterToMongo(filter);
3550
- return runChunkedArchive(options, sink, createMongoArchivePort(this, compiled, options.session));
3916
+ const port = createMongoArchivePort(this, compiled, options.session);
3917
+ return runChunkedArchive(options, sink, port);
3551
3918
  }
3552
3919
  /**
3553
3920
  * Kit-native aggregation pipeline — takes a MongoDB stage array and
@@ -3746,7 +4113,7 @@ var Repository = class extends RepositoryBase {
3746
4113
  if (!finalReq.sort || Object.keys(finalReq.sort).length === 0) throw new Error("mongokit/aggregatePaginate: keyset pagination requires `sort` — the cursor anchors on the sort-key tuple");
3747
4114
  const cursor = finalReq.after ? decodeAggCursor$1(finalReq.after) : void 0;
3748
4115
  const keysetMatch = cursor ? buildKeysetPredicate(finalReq.sort, cursor) : void 0;
3749
- const { pipeline, prePaginationIndex } = buildAggPipeline(finalReq);
4116
+ const { pipeline, prePaginationIndex } = buildAggPipeline(finalReq, this.Model.schema);
3750
4117
  const headStages = pipeline.slice(0, prePaginationIndex);
3751
4118
  const tailStages = [];
3752
4119
  if (keysetMatch) tailStages.push(keysetMatch);
@@ -4294,7 +4661,10 @@ var Repository = class extends RepositoryBase {
4294
4661
  const labels = error.errorLabels;
4295
4662
  const hasLabel = error.hasErrorLabel;
4296
4663
  if (Array.isArray(labels) && (labels.includes("TransientTransactionError") || labels.includes("UnknownTransactionCommitResult")) || typeof hasLabel === "function" && (hasLabel.call(error, "TransientTransactionError") || hasLabel.call(error, "UnknownTransactionCommitResult"))) return error;
4297
- if (error instanceof mongoose.Error.ValidationError) return createError(400, `Validation Error: ${Object.values(error.errors).map((err) => err.message).join(", ")}`);
4664
+ if (error instanceof mongoose.Error.ValidationError) {
4665
+ const messages = Object.values(error.errors).map((err) => err.message);
4666
+ return createError(400, `Validation Error: ${messages.join(", ")}`);
4667
+ }
4298
4668
  if (error instanceof mongoose.Error.CastError) return createError(400, `Invalid ${error.path}: ${error.value}`);
4299
4669
  const duplicateErr = parseDuplicateKeyError(error);
4300
4670
  if (duplicateErr) return duplicateErr;
@@ -4303,4 +4673,4 @@ var Repository = class extends RepositoryBase {
4303
4673
  }
4304
4674
  };
4305
4675
  //#endregion
4306
- export { supportsTransactions as a, ALL_OPERATIONS as c, READ_OPERATIONS as d, operationsByPolicyKey as f, isTransactionUnsupported as i, MUTATING_OPERATIONS as l, createMongoPurgePort as m, Repository as n, withTransaction as o, MONGOKIT_CAPABILITIES as p, batchTransaction as r, AggregationBuilder as s, HOOK_PRIORITY$1 as t, OP_REGISTRY as u };
4676
+ export { resetCapabilityProbeCache as _, supportsTransactions as a, transactionResolutionOf as b, ALL_OPERATIONS as c, READ_OPERATIONS as d, operationsByPolicyKey as f, probeMongoCapabilities as g, classifyHelloReply as h, isTransactionUnsupported as i, MUTATING_OPERATIONS as l, capabilitiesForSupport as m, Repository as n, withTransaction as o, MONGOKIT_CAPABILITIES as p, batchTransaction as r, AggregationBuilder as s, HOOK_PRIORITY$1 as t, OP_REGISTRY as u, resolveMongoCapabilities as v, createMongoPurgePort as x, resolveTransactionSupport as y };
@@ -1,5 +1,5 @@
1
1
  import { t as __exportAll } from "../rolldown-runtime-D7D4PA-g.mjs";
2
- import { g as create_exports, i as update_exports, p as delete_exports, u as read_exports, v as aggregate_exports } from "../update-1Lq9OMD2.mjs";
2
+ import { g as create_exports, i as update_exports, p as delete_exports, u as read_exports, v as aggregate_exports } from "../update-DHdXdOk_.mjs";
3
3
  //#region src/actions/index.ts
4
4
  var actions_exports = /* @__PURE__ */ __exportAll({
5
5
  aggregate: () => aggregate_exports,
@@ -1,2 +1,2 @@
1
- import { n as createMongooseAdapter, r as isMongooseModel, t as MongooseAdapter } from "../adapter-CLvUHiTW.mjs";
1
+ import { n as createMongooseAdapter, r as isMongooseModel, t as MongooseAdapter } from "../adapter-BitHg-CN.mjs";
2
2
  export { MongooseAdapter, createMongooseAdapter, isMongooseModel };