@classytic/mongokit 3.29.0 → 3.31.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 (40) hide show
  1. package/README.md +43 -0
  2. package/dist/{PaginationEngine-CczbvCAq.mjs → PaginationEngine-CuONvwCM.mjs} +21 -1
  3. package/dist/{create-repository-D4ZU94b3.d.mts → Repository-BAzymuOz.d.mts} +31 -58
  4. package/dist/{Repository-DThvFCaV.mjs → Repository-BQ3FEnOa.mjs} +352 -56
  5. package/dist/actions/index.d.mts +1 -1
  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-uhKt_oUT.mjs → create-repository-BOTVMsIg.mjs} +3 -2
  13. package/dist/create-repository-mEdfH9sz.d.mts +54 -0
  14. package/dist/{index-DceInCTX.d.mts → index-Beyr1r4d.d.mts} +1 -1
  15. package/dist/index.d.mts +305 -43
  16. package/dist/index.mjs +118 -11
  17. package/dist/kernel-conformance/index.d.mts +217 -0
  18. package/dist/kernel-conformance/index.mjs +603 -0
  19. package/dist/method-registry.plugin-DblM7V6j.mjs +112 -0
  20. package/dist/model-registry-ClKtWiHa.mjs +67 -0
  21. package/dist/{mongooseToJsonSchema-CByt1VhQ.mjs → mongooseToJsonSchema-DKbnKduN.mjs} +14 -3
  22. package/dist/{multi-tenant.plugin-DVyVGREb.d.mts → multi-tenant.plugin-2VtSSm1Y.d.mts} +1 -1
  23. package/dist/{operations-C8IcCHF9.d.mts → operations-CuzX9m2Z.d.mts} +42 -3
  24. package/dist/outbox/index.d.mts +63 -0
  25. package/dist/outbox/index.mjs +239 -0
  26. package/dist/pagination/PaginationEngine.mjs +1 -1
  27. package/dist/{plugin-options-vFufQZMZ.d.mts → plugin-options-BsFH0sTi.d.mts} +1 -1
  28. package/dist/plugins/index.d.mts +4 -4
  29. package/dist/plugins/index.mjs +3 -2
  30. package/dist/{repository-DCHRWRXw.d.mts → repository-Q16ErTu8.d.mts} +19 -1
  31. package/dist/testkit/index.d.mts +8 -1
  32. package/dist/testkit/index.mjs +1 -1
  33. package/dist/{timestamp.plugin-CFL6LlYZ.mjs → timestamp.plugin-i9Qxm6kn.mjs} +32 -143
  34. package/dist/{to-plain-DkF5px0R.d.mts → to-plain-BuX1RUZV.d.mts} +12 -4
  35. package/dist/{update-CI2NdAW6.mjs → update-DAO5STDv.mjs} +62 -7
  36. package/dist/utils/index.d.mts +2 -2
  37. package/dist/utils/index.mjs +1 -1
  38. package/dist/{validation-chain.plugin-BLecaKa0.d.mts → validation-chain.plugin-BSshLJTA.d.mts} +3 -3
  39. package/dist/{validation-chain.plugin-CwOcUJC1.mjs → validation-chain.plugin-CpcB6dOd.mjs} +4 -3
  40. package/package.json +193 -187
@@ -1,8 +1,8 @@
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-CI2NdAW6.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-DAO5STDv.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
- import { t as PaginationEngine } from "./PaginationEngine-CczbvCAq.mjs";
5
+ import { t as PaginationEngine } from "./PaginationEngine-CuONvwCM.mjs";
6
6
  import { hasNearOperator, rewriteNearForCount } from "./query/primitives/geo.mjs";
7
7
  import { RepositoryBase, runChunkedArchive, runChunkedPurge, throwIfAborted, validatePluginOrder, withRetry } from "@classytic/repo-core/repository";
8
8
  import mongoose from "mongoose";
@@ -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,45 @@ 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,
162
+ dateBucketTimezone: true,
141
163
  customDateBuckets: true,
142
164
  dateBucketSubMinute: true,
143
165
  cache: true
144
- },
166
+ }),
145
167
  getOrCreate: true,
146
168
  countAndExists: true,
147
169
  purgeByField: true,
@@ -152,8 +174,197 @@ const MONGOKIT_CAPABILITIES = {
152
174
  changeStreams: true,
153
175
  lean: true,
154
176
  lookupPopulate: true,
177
+ lookupCoerce: true,
155
178
  streaming: true
156
- };
179
+ });
180
+ /**
181
+ * Driver `ServerType`s that positively imply a session-capable, transaction-
182
+ * capable deployment. Verified against mongodb driver 7.x: a single-node
183
+ * replica set reports `RSPrimary` (topology `ReplicaSetWithPrimary`, or
184
+ * `Single` when reached with `directConnection`), a standalone `mongod`
185
+ * reports `Standalone`, and a mongos reports `Mongos`.
186
+ *
187
+ * **This is why the check reads SERVER type, not just topology type.** The
188
+ * pre-existing heuristic (`topology.description.type !== 'Single'`) has a
189
+ * false NEGATIVE: a directly-connected single-node replica set is `Single`
190
+ * yet runs transactions fine.
191
+ */
192
+ const TRANSACTION_CAPABLE_SERVER_TYPES = /* @__PURE__ */ new Set([
193
+ "RSPrimary",
194
+ "RSSecondary",
195
+ "RSArbiter",
196
+ "RSOther",
197
+ "RSGhost",
198
+ "Mongos",
199
+ "LoadBalancer"
200
+ ]);
201
+ /** Topology types that positively imply transactions regardless of server detail. */
202
+ const TRANSACTION_CAPABLE_TOPOLOGY_TYPES = /* @__PURE__ */ new Set([
203
+ "ReplicaSetWithPrimary",
204
+ "ReplicaSetNoPrimary",
205
+ "Sharded",
206
+ "LoadBalanced"
207
+ ]);
208
+ function clientOf(connection) {
209
+ const probe = connection;
210
+ if (!probe || typeof probe !== "object") return void 0;
211
+ try {
212
+ return probe.getClient?.() ?? probe.client ?? (probe.topology !== void 0 ? probe : void 0);
213
+ } catch {
214
+ return;
215
+ }
216
+ }
217
+ function serverTypes(description) {
218
+ const servers = description.servers;
219
+ if (!servers) return [];
220
+ return (typeof servers.values === "function" ? [...servers.values()] : Object.values(servers)).map((s) => s?.type).filter((t) => typeof t === "string");
221
+ }
222
+ /**
223
+ * Read whether a connection's LIVE deployment supports multi-document
224
+ * transactions, synchronously, from the driver's SDAM topology description.
225
+ *
226
+ * Free (a few property reads against an object the driver keeps current), so
227
+ * it is deliberately NOT memoized — a connection that opens after the repo was
228
+ * constructed upgrades its own answer with no invalidation step. What IS
229
+ * memoized is the async {@link probeMongoCapabilities} result, which this
230
+ * function consults first so an exotic deployment that answered `hello` but
231
+ * exposes no topology still reports definitively.
232
+ *
233
+ * @returns `'yes'` / `'no'` when the topology is conclusive, `'unknown'`
234
+ * otherwise — never optimistic. See the module docblock on rule 3.
235
+ */
236
+ function resolveTransactionSupport(connection) {
237
+ const cached = readProbeCache(connection);
238
+ if (cached) return cached;
239
+ const description = clientOf(connection)?.topology?.description;
240
+ if (!description) return "unknown";
241
+ const types = serverTypes(description);
242
+ if (types.some((t) => TRANSACTION_CAPABLE_SERVER_TYPES.has(t))) return "yes";
243
+ if (types.length > 0 && types.every((t) => t === "Standalone")) return "no";
244
+ const topologyType = description.type;
245
+ if (topologyType && TRANSACTION_CAPABLE_TOPOLOGY_TYPES.has(topologyType)) return "yes";
246
+ return "unknown";
247
+ }
248
+ function deriveCapabilities(support) {
249
+ const available = support === "yes";
250
+ return Object.freeze({
251
+ ...MONGOKIT_CAPABILITIES,
252
+ transactionsResolution: support === "unknown" ? "unknown" : "observed",
253
+ transactions: available,
254
+ nestedTransactions: available,
255
+ changeStreams: available
256
+ });
257
+ }
258
+ /** Three possible answers ⇒ three frozen singletons; the getter allocates nothing. */
259
+ const DERIVED = Object.freeze({
260
+ yes: deriveCapabilities("yes"),
261
+ no: deriveCapabilities("no"),
262
+ unknown: deriveCapabilities("unknown")
263
+ });
264
+ /**
265
+ * The capability descriptor for a LIVE connection — what
266
+ * `Repository#capabilities` returns.
267
+ *
268
+ * Every deployment-dependent flag reflects the observed topology; everything
269
+ * else comes from {@link MONGOKIT_CAPABILITIES}. Cheap enough to call per
270
+ * access (topology read + one frozen-object lookup).
271
+ */
272
+ function resolveMongoCapabilities(connection) {
273
+ return DERIVED[resolveTransactionSupport(connection)];
274
+ }
275
+ /** The three derived descriptors, for tests and for gates that want to compare identity. */
276
+ function capabilitiesForSupport(support) {
277
+ return DERIVED[support];
278
+ }
279
+ /** Definitive results only. Keyed on the connection object; never holds `'unknown'`. */
280
+ const PROBE_RESULTS = /* @__PURE__ */ new WeakMap();
281
+ /** In-flight de-duplication so N repos on one connection issue ONE `hello`. */
282
+ const PROBE_INFLIGHT = /* @__PURE__ */ new WeakMap();
283
+ function readProbeCache(connection) {
284
+ if (!connection || typeof connection !== "object") return void 0;
285
+ return PROBE_RESULTS.get(connection);
286
+ }
287
+ /** Test seam — drop memoized probe answers (a reused connection object across suites). */
288
+ function resetCapabilityProbeCache(connection) {
289
+ if (connection && typeof connection === "object") {
290
+ PROBE_RESULTS.delete(connection);
291
+ PROBE_INFLIGHT.delete(connection);
292
+ }
293
+ }
294
+ function withTimeout(promise, ms, onTimeout) {
295
+ return new Promise((resolve) => {
296
+ const timer = setTimeout(() => resolve(onTimeout), ms);
297
+ timer.unref?.();
298
+ promise.then((value) => {
299
+ clearTimeout(timer);
300
+ resolve(value);
301
+ }, () => {
302
+ clearTimeout(timer);
303
+ resolve(onTimeout);
304
+ });
305
+ });
306
+ }
307
+ /**
308
+ * Classify a `hello` (or legacy `isMaster`) reply.
309
+ *
310
+ * - `msg: 'isdbgrid'` ⇒ mongos ⇒ transactions (Mongo 4.2+ sharded).
311
+ * - `setName` ⇒ a replica-set member ⇒ transactions.
312
+ * - a reply that IS a hello (`isWritablePrimary` / `ismaster` present) with
313
+ * neither marker ⇒ standalone ⇒ **no**. Verified: a memory-server standalone
314
+ * returns `{ isWritablePrimary: true }` and nothing else.
315
+ * - anything unrecognised ⇒ `'unknown'`. A reply we cannot parse is not a yes.
316
+ */
317
+ function classifyHelloReply(reply) {
318
+ if (!reply || typeof reply !== "object") return "unknown";
319
+ const { msg, setName } = reply;
320
+ if (msg === "isdbgrid") return "yes";
321
+ if (typeof setName === "string" && setName.length > 0) return "yes";
322
+ return "isWritablePrimary" in reply || "ismaster" in reply ? "no" : "unknown";
323
+ }
324
+ async function runProbe(connection, timeoutMs) {
325
+ if (connection.readyState === 2 && typeof connection.asPromise === "function") await withTimeout(connection.asPromise().then(() => void 0, () => void 0), timeoutMs, void 0);
326
+ const fromTopology = resolveTransactionSupport(connection);
327
+ if (fromTopology !== "unknown") return fromTopology;
328
+ const admin = connection.db?.admin?.();
329
+ if (!admin) return "unknown";
330
+ return classifyHelloReply(await withTimeout(admin.command({ hello: 1 }).catch(async () => admin.command({ isMaster: 1 }).catch(() => void 0)), timeoutMs, void 0));
331
+ }
332
+ /**
333
+ * Resolve a connection's real capabilities, waiting for (and if necessary
334
+ * asking) the deployment. Memoized per connection: N repositories and N
335
+ * kernels on one connection issue at most ONE `hello`.
336
+ *
337
+ * Hosts do not have to call this — `Repository#capabilities` reads the live
338
+ * topology synchronously, which is already conclusive once
339
+ * `await mongoose.connect()` has returned (the Kernel Construction Standard's
340
+ * Describe → **Bind** ordering guarantees that). It exists for the cases the
341
+ * sync read cannot cover: binding while a connection is still opening, or a
342
+ * deployment whose driver surfaces no topology description. Calling it makes
343
+ * the SUBSEQUENT synchronous reads definitive, so nothing else needs rewiring:
344
+ *
345
+ * ```ts
346
+ * await probeMongoCapabilities(connection); // once, before bind
347
+ * const engine = defineWallet().bind(connection); // gate now sees the truth
348
+ * ```
349
+ *
350
+ * Only definitive answers are cached — an `'unknown'` may be re-probed later
351
+ * (the deployment may simply not have been up yet).
352
+ */
353
+ async function probeMongoCapabilities(connection, options = {}) {
354
+ if (!connection || typeof connection !== "object") return DERIVED.unknown;
355
+ const key = connection;
356
+ const cached = PROBE_RESULTS.get(key);
357
+ if (cached) return DERIVED[cached];
358
+ const inflight = PROBE_INFLIGHT.get(key);
359
+ if (inflight) return DERIVED[await inflight];
360
+ const run = runProbe(connection, options.timeoutMs ?? 2e3).catch(() => "unknown").then((support) => {
361
+ if (support !== "unknown") PROBE_RESULTS.set(key, support);
362
+ PROBE_INFLIGHT.delete(key);
363
+ return support;
364
+ });
365
+ PROBE_INFLIGHT.set(key, run);
366
+ return DERIVED[await run];
367
+ }
157
368
  //#endregion
158
369
  //#region src/operations.ts
159
370
  /**
@@ -457,8 +668,48 @@ function validateMeasures$1(measures) {
457
668
  * 2. Inside `$project` to flatten `_id.<alias>` into a top-level
458
669
  * column on the output row — matches the cross-kit shape.
459
670
  */
671
+ /**
672
+ * Reject an unknown IANA zone HERE, not at the server.
673
+ *
674
+ * `@classytic/primitives` owns the canonical `isValidTimeZone`, but this is a
675
+ * storage kit and must not take a domain dependency for one validator — so it
676
+ * mirrors the primitive's implementation exactly: construct an `Intl` formatter
677
+ * for the zone and treat the `RangeError` as the answer. ICU is the same source
678
+ * of truth either way, so the two cannot disagree about which zones exist.
679
+ *
680
+ * Without this, a typo (`Asia/Dahka`) travels all the way to Mongo and comes
681
+ * back as a server-side "unrecognized time zone identifier" attached to whatever
682
+ * query happened to run — at request time, far from the aggregation that set
683
+ * it. Failing at compile time names the field and the value.
684
+ */
685
+ const validatedZones = /* @__PURE__ */ new Set(["UTC"]);
686
+ function assertValidTimeZone(zone) {
687
+ if (validatedZones.has(zone)) return;
688
+ try {
689
+ new Intl.DateTimeFormat("en-US", { timeZone: zone });
690
+ } catch {
691
+ throw new Error(`mongokit/aggregate: dateBucket.timezone is not a valid IANA zone — got ${JSON.stringify(zone)}. Use a name like "Asia/Dhaka" or "America/New_York"; omit the field for UTC.`);
692
+ }
693
+ validatedZones.add(zone);
694
+ }
460
695
  function compileDateBucket(bucket) {
461
696
  const dateExpr = { $toDate: `$${bucket.field}` };
697
+ /**
698
+ * Bucket boundaries are drawn in `bucket.timezone`, defaulting to UTC.
699
+ *
700
+ * Every branch below used to hardcode `'UTC'`, which made the portable IR
701
+ * unusable for any business-day rollup: in a UTC+6 deployment the 18:00–
702
+ * midnight rows land on the previous day and, at month-end, in the previous
703
+ * month — silently. Consumers therefore abandoned the IR and hand-rolled
704
+ * `$dateToString` pipelines, where the zone became optional and was
705
+ * forgotten (see the daily profit rollup that bucketed on UTC days).
706
+ *
707
+ * Mongo takes an IANA zone natively on both operators, so honouring it here
708
+ * costs nothing and makes the correct thing expressible. Gated by the
709
+ * `dateBucketTimezone` capability for kits that cannot do it.
710
+ */
711
+ const timezone = bucket.timezone ?? "UTC";
712
+ assertValidTimeZone(timezone);
462
713
  if (typeof bucket.interval === "object") {
463
714
  const { every, unit } = bucket.interval;
464
715
  if (!Number.isInteger(every) || every <= 0) throw new Error(`mongokit/aggregate: dateBucket.interval.every must be a positive integer — got ${String(every)}`);
@@ -466,56 +717,56 @@ function compileDateBucket(bucket) {
466
717
  date: dateExpr,
467
718
  unit,
468
719
  binSize: every,
469
- timezone: "UTC"
720
+ timezone
470
721
  } };
471
722
  return { $dateToString: {
472
723
  format: formatForUnit(unit),
473
724
  date: truncated,
474
- timezone: "UTC"
725
+ timezone
475
726
  } };
476
727
  }
477
728
  switch (bucket.interval) {
478
729
  case "minute": return { $dateToString: {
479
730
  format: "%Y-%m-%dT%H:%M",
480
731
  date: dateExpr,
481
- timezone: "UTC"
732
+ timezone
482
733
  } };
483
734
  case "hour": return { $dateToString: {
484
735
  format: "%Y-%m-%dT%H:00",
485
736
  date: dateExpr,
486
- timezone: "UTC"
737
+ timezone
487
738
  } };
488
739
  case "day": return { $dateToString: {
489
740
  format: "%Y-%m-%d",
490
741
  date: dateExpr,
491
- timezone: "UTC"
742
+ timezone
492
743
  } };
493
744
  case "week": return { $dateToString: {
494
745
  format: "%G-W%V",
495
746
  date: dateExpr,
496
- timezone: "UTC"
747
+ timezone
497
748
  } };
498
749
  case "month": return { $dateToString: {
499
750
  format: "%Y-%m",
500
751
  date: dateExpr,
501
- timezone: "UTC"
752
+ timezone
502
753
  } };
503
754
  case "quarter": return { $concat: [
504
755
  { $dateToString: {
505
756
  format: "%Y",
506
757
  date: dateExpr,
507
- timezone: "UTC"
758
+ timezone
508
759
  } },
509
760
  "-Q",
510
761
  { $toString: { $ceil: { $divide: [{ $month: {
511
762
  date: dateExpr,
512
- timezone: "UTC"
763
+ timezone
513
764
  } }, 3] } } }
514
765
  ] };
515
766
  case "year": return { $dateToString: {
516
767
  format: "%Y",
517
768
  date: dateExpr,
518
- timezone: "UTC"
769
+ timezone
519
770
  } };
520
771
  }
521
772
  }
@@ -705,7 +956,16 @@ function buildAggPipeline(req, schema) {
705
956
  const aliasSet = new Set((req.lookups ?? []).map((l) => l.as ?? l.from));
706
957
  const { base: baseFilter, joined: joinedFilter } = splitFilterByAlias(schema !== void 0 && req.filter !== void 0 ? castFilterToSchema(req.filter, schema) : req.filter, aliasSet);
707
958
  if (baseFilter !== void 0) {
708
- const match = compileFilterToMongo(baseFilter);
959
+ /**
960
+ * The SCHEMA is passed so the date coercion can consult it.
961
+ *
962
+ * `castFilterToSchema` above already casts operands to their declared types; without
963
+ * the schema here, `compileFilterToMongo`'s coercion then guessed from value shape
964
+ * alone and OVERRODE that cast — turning a `'2026-08-02'` bound on a String column
965
+ * into a Date, which BSON refuses to compare against the stored string. The aggregate
966
+ * returned zero rows and nothing indicated why.
967
+ */
968
+ const match = compileFilterToMongo(baseFilter, schema);
709
969
  if (Object.keys(match).length > 0) stages.push({ $match: match });
710
970
  }
711
971
  if (req.lookups && req.lookups.length > 0) appendLookupStages$1(stages, req.lookups);
@@ -993,7 +1253,7 @@ async function countAggGroups(Model, req, options = {}) {
993
1253
  const [row] = await aggregation.exec();
994
1254
  return row?.n ?? 0;
995
1255
  }
996
- const match = req.filter ? compileFilterToMongo(castFilterToSchema(req.filter, Model.schema)) : {};
1256
+ const match = req.filter ? compileFilterToMongo(castFilterToSchema(req.filter, Model.schema), Model.schema) : {};
997
1257
  if (groupCols.length === 0) {
998
1258
  const pipeline = [];
999
1259
  if (Object.keys(match).length > 0) pipeline.push({ $match: match });
@@ -1899,29 +2159,29 @@ function isTransactionUnsupported(error) {
1899
2159
  /**
1900
2160
  * PROACTIVELY report whether a connection's deployment supports multi-document
1901
2161
  * transactions — the read-side companion to {@link isTransactionUnsupported}
1902
- * (which classifies a FAILED attempt). Single source of truth for the topology
1903
- * check so callers don't hand-roll `client.topology.description.type` probing.
2162
+ * (which classifies a FAILED attempt). Lets a caller skip a doomed transaction
2163
+ * attempt on standalone dev Mongo rather than starting one just to catch the
2164
+ * error.
2165
+ *
2166
+ * Delegates to {@link resolveTransactionSupport} so mongokit has exactly ONE
2167
+ * topology reader. That also fixes a false NEGATIVE this function used to
2168
+ * have: it tested `description.type !== 'Single'`, but a single-node replica
2169
+ * set reached with `directConnection` is `Single` with a server type of
2170
+ * `RSPrimary` and runs transactions fine. The shared reader looks at the
2171
+ * SERVER descriptions first.
1904
2172
  *
1905
- * Returns `false` ONLY when the topology is positively a standalone (`'Single'`)
1906
- * server; an unknown/undefined topology (not yet connected, non-standard
1907
- * connection object) returns `true` — optimistic, because a genuine failure is
1908
- * still caught reactively by `isTransactionUnsupported`. Lets a caller skip a
1909
- * doomed transaction attempt on standalone dev Mongo rather than starting one
1910
- * just to catch the error.
2173
+ * **`unknown` is optimistic HERE and pessimistic in `capabilities.ts` — the
2174
+ * difference is deliberate.** This answers "should I bother ATTEMPTING a
2175
+ * transaction?", where guessing yes costs one caught error (`allowFallback`
2176
+ * still recovers). The capability descriptor answers "may this deployment be
2177
+ * trusted with money?", where guessing yes is the bug class this repo keeps
2178
+ * hitting (AGENTS.md FAIL LOUD rule 3). Do not "unify" them.
1911
2179
  *
1912
2180
  * Accepts a Mongoose `Connection` (or anything exposing `getClient()`/`client`
1913
2181
  * with a driver `topology`), so it stays decoupled from mongoose types.
1914
2182
  */
1915
2183
  function supportsTransactions(connection) {
1916
- const probe = connection;
1917
- if (!probe) return true;
1918
- let client;
1919
- try {
1920
- client = probe.getClient?.() ?? probe.client ?? void 0;
1921
- } catch {
1922
- return true;
1923
- }
1924
- return client?.topology?.description?.type !== "Single";
2184
+ return resolveTransactionSupport(connection) !== "no";
1925
2185
  }
1926
2186
  //#endregion
1927
2187
  //#region src/Repository.ts
@@ -1986,8 +2246,31 @@ var Repository = class extends RepositoryBase {
1986
2246
  * Runtime capability descriptor required by `StandardRepo<TDoc>`
1987
2247
  * (repo-core 0.6.0). Hosts feature-detect once at boot instead of
1988
2248
  * try/catching `UnsupportedOperationError` per call.
2249
+ *
2250
+ * **OBSERVED, not declared** (changed 2026-08-05). Until then this was a
2251
+ * class field pointing at the static `MONGOKIT_CAPABILITIES`, so
2252
+ * `transactions` read `true` for every mongoose-backed repository —
2253
+ * including one pointed at a standalone `mongod`, which cannot run a
2254
+ * transaction at all. Every kernel boot gate that reads it (wallet, party,
2255
+ * invoice, purchase, flow, …) was therefore unfalsifiable decoration
2256
+ * (AGENTS.md FAIL LOUD rule 4).
2257
+ *
2258
+ * Now it derives the deployment-dependent flags (`transactions`,
2259
+ * `nestedTransactions`, `changeStreams`) from THIS model's connection and
2260
+ * its live SDAM topology. A getter rather than a constructor snapshot on
2261
+ * purpose: a repository built before `mongoose.connect()` resolves upgrades
2262
+ * its own answer the moment the connection is up, with no invalidation step.
2263
+ * When the topology cannot be read the answer is `transactions: false` +
2264
+ * `transactionsResolution: 'unknown'` — fail closed, never an optimistic
2265
+ * yes (rule 3). See `capabilities.ts` and `probeMongoCapabilities`.
2266
+ *
2267
+ * `options.capabilities` overrides the whole descriptor for hosts/tests that
2268
+ * genuinely know better (a proxied connection, a non-mongoose driver seam).
1989
2269
  */
1990
- capabilities = MONGOKIT_CAPABILITIES;
2270
+ get capabilities() {
2271
+ return this._capabilitiesOverride ?? resolveMongoCapabilities(this.Model?.db);
2272
+ }
2273
+ _capabilitiesOverride;
1991
2274
  Model;
1992
2275
  /**
1993
2276
  * Mongoose model name. Duplicated on the instance for BC — arc / catalog /
@@ -2020,6 +2303,7 @@ var Repository = class extends RepositoryBase {
2020
2303
  warn(m);
2021
2304
  });
2022
2305
  this.Model = Model;
2306
+ this._capabilitiesOverride = options.capabilities;
2023
2307
  this.model = Model.modelName;
2024
2308
  this._pagination = new PaginationEngine(Model, paginationConfig);
2025
2309
  this.idField = options.idField ?? "_id";
@@ -2318,7 +2602,8 @@ var Repository = class extends RepositoryBase {
2318
2602
  return result;
2319
2603
  }
2320
2604
  const wantsThrow = context.throwOnNotFound === true || options.throwOnNotFound === true;
2321
- if (!isValidIdForType(id, getSchemaIdType(this.Model.schema))) {
2605
+ const idType = getSchemaIdType(this.Model.schema);
2606
+ if (!isValidIdForType(id, idType)) {
2322
2607
  if (wantsThrow) throw createError(404, "Document not found");
2323
2608
  return null;
2324
2609
  }
@@ -2822,8 +3107,9 @@ var Repository = class extends RepositoryBase {
2822
3107
  /**
2823
3108
  * Update a document by id and return the post-update doc.
2824
3109
  *
2825
- * Compiles to `Model.findOneAndUpdate(filter, { $set: data }, { new:
2826
- * true, runValidators: true })` under the hood and routes through the
3110
+ * Compiles to `Model.findOneAndUpdate(filter, { $set: data },
3111
+ * { returnDocument: 'after', runValidators: true })` under the hood and
3112
+ * routes through the
2827
3113
  * full plugin pipeline — `multiTenantPlugin` injects the tenant scope,
2828
3114
  * `auditTrailPlugin` records the change, `cachePlugin` invalidates,
2829
3115
  * `softDeletePlugin` etc. all compose. Returns `null` on miss (the
@@ -2838,7 +3124,7 @@ var Repository = class extends RepositoryBase {
2838
3124
  * const doc = await CampaignModel.findOneAndUpdate(
2839
3125
  * { _id: id, organizationId: ctx.orgId }, // hand-rolled tenant filter
2840
3126
  * { $set: patch, $currentDate: { updatedAt: 1 } },
2841
- * { new: true, runValidators: true }
3127
+ * { returnDocument: 'after', runValidators: true }
2842
3128
  * );
2843
3129
  * return doc ? toEntity(doc) : null;
2844
3130
  *
@@ -2867,7 +3153,7 @@ var Repository = class extends RepositoryBase {
2867
3153
  * const doc = await CampaignModel.findOneAndUpdate(
2868
3154
  * { _id: id, organizationId: ctx.orgId, status: 'pending' },
2869
3155
  * { $set: { status: 'sent', sentAt: new Date() } },
2870
- * { new: true }
3156
+ * { returnDocument: 'after' }
2871
3157
  * );
2872
3158
  * if (!doc) throw new ConcurrentTransitionError(id);
2873
3159
  *
@@ -2921,7 +3207,8 @@ var Repository = class extends RepositoryBase {
2921
3207
  const wantsThrow = context.throwOnNotFound === true || options.throwOnNotFound === true;
2922
3208
  const effectiveIdField = options.idField ?? this.idField;
2923
3209
  if (effectiveIdField === "_id") {
2924
- if (!isValidIdForType(id, getSchemaIdType(this.Model.schema))) {
3210
+ const idType = getSchemaIdType(this.Model.schema);
3211
+ if (!isValidIdForType(id, idType)) {
2925
3212
  if (wantsThrow) throw createError(404, "Document not found");
2926
3213
  await this._emitHook("after:update", {
2927
3214
  context,
@@ -3211,7 +3498,8 @@ var Repository = class extends RepositoryBase {
3211
3498
  const finalQuery = context.query || filter;
3212
3499
  const finalUpdate = context.data || update;
3213
3500
  if (effectiveIdField === "_id" && !options.upsert) {
3214
- if (!isValidIdForType(id, getSchemaIdType(this.Model.schema))) return null;
3501
+ const idType = getSchemaIdType(this.Model.schema);
3502
+ if (!isValidIdForType(id, idType)) return null;
3215
3503
  }
3216
3504
  if (Object.keys(finalUpdate).length === 0) return await this.Model.findOne(finalQuery, null, { session: options.session });
3217
3505
  return await findOneAndUpdate(this.Model, finalQuery, finalUpdate, {
@@ -3444,7 +3732,8 @@ var Repository = class extends RepositoryBase {
3444
3732
  const finalQuery = context.query || filter;
3445
3733
  const finalUpdate = context.data || operatorUpdate;
3446
3734
  if (effectiveIdField === "_id") {
3447
- if (!isValidIdForType(id, getSchemaIdType(this.Model.schema))) return null;
3735
+ const idType = getSchemaIdType(this.Model.schema);
3736
+ if (!isValidIdForType(id, idType)) return null;
3448
3737
  }
3449
3738
  return await findOneAndUpdate(this.Model, finalQuery, finalUpdate, {
3450
3739
  returnDocument: "after",
@@ -3497,7 +3786,8 @@ var Repository = class extends RepositoryBase {
3497
3786
  const effectiveIdField = options.idField ?? this.idField;
3498
3787
  const wantsThrow = context.throwOnNotFound === true || options.throwOnNotFound === true;
3499
3788
  if (effectiveIdField === "_id") {
3500
- if (!isValidIdForType(id, getSchemaIdType(this.Model.schema))) {
3789
+ const idType = getSchemaIdType(this.Model.schema);
3790
+ if (!isValidIdForType(id, idType)) {
3501
3791
  if (wantsThrow) throw createError(404, "Document not found");
3502
3792
  await this._emitHook("after:delete", {
3503
3793
  context,
@@ -3630,7 +3920,8 @@ var Repository = class extends RepositoryBase {
3630
3920
  * the port implementation.
3631
3921
  */
3632
3922
  async purgeByField(field, value, strategy, options = {}) {
3633
- return runChunkedPurge(strategy, options, createMongoPurgePort(this, field, value, options.session));
3923
+ const port = createMongoPurgePort(this, field, value, options.session);
3924
+ return runChunkedPurge(strategy, options, port);
3634
3925
  }
3635
3926
  /**
3636
3927
  * Range/filter-scoped purge — see `StandardRepo.purgeByFilter` for the
@@ -3648,7 +3939,8 @@ var Repository = class extends RepositoryBase {
3648
3939
  */
3649
3940
  async purgeByFilter(filter, strategy, options = {}) {
3650
3941
  const compiled = compileFilterToMongo(filter);
3651
- return runChunkedPurge(strategy, options, createMongoPurgePortFromFilter(this, compiled, options.session));
3942
+ const port = createMongoPurgePortFromFilter(this, compiled, options.session);
3943
+ return runChunkedPurge(strategy, options, port);
3652
3944
  }
3653
3945
  /**
3654
3946
  * Chunked cold-storage extraction — see `StandardRepo.archiveByFilter`
@@ -3663,7 +3955,8 @@ var Repository = class extends RepositoryBase {
3663
3955
  */
3664
3956
  async archiveByFilter(filter, sink, options = {}) {
3665
3957
  const compiled = compileFilterToMongo(filter);
3666
- return runChunkedArchive(options, sink, createMongoArchivePort(this, compiled, options.session));
3958
+ const port = createMongoArchivePort(this, compiled, options.session);
3959
+ return runChunkedArchive(options, sink, port);
3667
3960
  }
3668
3961
  /**
3669
3962
  * Kit-native aggregation pipeline — takes a MongoDB stage array and
@@ -4017,9 +4310,9 @@ var Repository = class extends RepositoryBase {
4017
4310
  const appendLookupStages$2 = (pipeline) => appendLookupStages(pipeline, lookups, projection, isKeyset && sort ? this._parseSort(sort) : void 0);
4018
4311
  if (isKeyset && sort) {
4019
4312
  const parsedSort = this._parseSort(sort);
4020
- const { validateKeysetSort } = await import("./PaginationEngine-CczbvCAq.mjs").then((n) => n.n);
4021
- const { encodeCursor, resolveCursorFilter } = await import("./PaginationEngine-CczbvCAq.mjs").then((n) => n.r);
4022
- const { getPrimaryField } = await import("./PaginationEngine-CczbvCAq.mjs").then((n) => n.n);
4313
+ const { validateKeysetSort } = await import("./PaginationEngine-CuONvwCM.mjs").then((n) => n.n);
4314
+ const { encodeCursor, resolveCursorFilter } = await import("./PaginationEngine-CuONvwCM.mjs").then((n) => n.r);
4315
+ const { getPrimaryField } = await import("./PaginationEngine-CuONvwCM.mjs").then((n) => n.n);
4023
4316
  const normalizedSort = validateKeysetSort(parsedSort, this._pagination.config.strictKeysetSortFields);
4024
4317
  const cursorVersion = this._pagination.config.cursorVersion ?? 1;
4025
4318
  const minCursorVersion = this._pagination.config.minCursorVersion ?? 1;
@@ -4410,7 +4703,10 @@ var Repository = class extends RepositoryBase {
4410
4703
  const labels = error.errorLabels;
4411
4704
  const hasLabel = error.hasErrorLabel;
4412
4705
  if (Array.isArray(labels) && (labels.includes("TransientTransactionError") || labels.includes("UnknownTransactionCommitResult")) || typeof hasLabel === "function" && (hasLabel.call(error, "TransientTransactionError") || hasLabel.call(error, "UnknownTransactionCommitResult"))) return error;
4413
- if (error instanceof mongoose.Error.ValidationError) return createError(400, `Validation Error: ${Object.values(error.errors).map((err) => err.message).join(", ")}`);
4706
+ if (error instanceof mongoose.Error.ValidationError) {
4707
+ const messages = Object.values(error.errors).map((err) => err.message);
4708
+ return createError(400, `Validation Error: ${messages.join(", ")}`);
4709
+ }
4414
4710
  if (error instanceof mongoose.Error.CastError) return createError(400, `Invalid ${error.path}: ${error.value}`);
4415
4711
  const duplicateErr = parseDuplicateKeyError(error);
4416
4712
  if (duplicateErr) return duplicateErr;
@@ -4419,4 +4715,4 @@ var Repository = class extends RepositoryBase {
4419
4715
  }
4420
4716
  };
4421
4717
  //#endregion
4422
- 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 };
4718
+ 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,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-DceInCTX.mjs";
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-Beyr1r4d.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 };
@@ -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-CI2NdAW6.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-DAO5STDv.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 };
@@ -1,5 +1,5 @@
1
1
  import { n as warn } from "./logger-K7dPmu2U.mjs";
2
- import { t as buildCrudSchemasFromModel } from "./mongooseToJsonSchema-CByt1VhQ.mjs";
2
+ import { t as buildCrudSchemasFromModel } from "./mongooseToJsonSchema-DKbnKduN.mjs";
3
3
  import { matchesRecordFilter } from "@classytic/repo-core/filter";
4
4
  import { mergeFieldRuleConstraints } from "@classytic/repo-core/schema";
5
5
  import { asRepositoryLike, isRepository } from "@classytic/repo-core/adapter";
@@ -1,4 +1,4 @@
1
- import { l as Plugin } from "../repository-DCHRWRXw.mjs";
1
+ import { l as Plugin } from "../repository-Q16ErTu8.mjs";
2
2
  import { PipelineStage } from "mongoose";
3
3
  //#region src/ai/types.d.ts
4
4
  /** Supported similarity metrics for vector search */