@classytic/mongokit 3.38.1 → 3.40.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 (51) hide show
  1. package/dist/{PaginationEngine-CBS1XGzp.d.mts → PaginationEngine-D-P19Y9m.d.mts} +34 -9
  2. package/dist/PaginationEngine-Dvl8jcRY.mjs +1198 -0
  3. package/dist/{Repository-Ck5x43wF.mjs → Repository-BMgPSkqZ.mjs} +672 -655
  4. package/dist/{Repository-BdSkQ8vc.d.mts → Repository-CR8Y9g2E.d.mts} +106 -34
  5. package/dist/actions/index.d.mts +1 -1
  6. package/dist/actions/index.mjs +1 -1
  7. package/dist/adapter/index.d.mts +21 -0
  8. package/dist/adapter/index.mjs +1 -1
  9. package/dist/{adapter-Bfqz8yHE.mjs → adapter-BrVfPFSW.mjs} +45 -1
  10. package/dist/ai/index.d.mts +1 -1
  11. package/dist/better-auth/index.mjs +2 -2
  12. package/dist/{create-repository-B5EulQ31.mjs → create-repository-BzBUMvd1.mjs} +3 -3
  13. package/dist/{create-repository-BU4x9Kb0.d.mts → create-repository-CAle1MSU.d.mts} +5 -5
  14. package/dist/idempotency/index.d.mts +23 -0
  15. package/dist/idempotency/index.mjs +134 -0
  16. package/dist/{index-DKEql4cL.d.mts → index-CmjNyulu.d.mts} +67 -8
  17. package/dist/index.d.mts +125 -35
  18. package/dist/index.mjs +286 -676
  19. package/dist/indexes/verify-indexes.d.mts +14 -1
  20. package/dist/indexes/verify-indexes.mjs +54 -7
  21. package/dist/{method-registry.plugin-DblM7V6j.mjs → method-registry.plugin-C63pGn1d.mjs} +54 -4
  22. package/dist/{multi-tenant.plugin-BwSvTirB.d.mts → multi-tenant.plugin-CBgCo9im.d.mts} +77 -5
  23. package/dist/{operations-DZodvjC-.d.mts → operations-CrRIoc8Z.d.mts} +22 -7
  24. package/dist/outbox/index.d.mts +1 -1
  25. package/dist/outbox/index.mjs +7 -5
  26. package/dist/pagination/PaginationEngine.d.mts +1 -1
  27. package/dist/pagination/PaginationEngine.mjs +1 -1
  28. package/dist/pagination/utils/limits.d.mts +1 -1
  29. package/dist/pagination/utils/limits.mjs +22 -1
  30. package/dist/pagination-BpIv2bdS.d.mts +364 -0
  31. package/dist/{plugin-options-DXnlKM3B.d.mts → plugin-options-Zk46lD6F.d.mts} +16 -2
  32. package/dist/plugins/index.d.mts +8 -3
  33. package/dist/plugins/index.mjs +99 -26
  34. package/dist/{repository-H_ffGgpE.d.mts → repository-odl03582.d.mts} +50 -6
  35. package/dist/sync/index.d.mts +40 -0
  36. package/dist/sync/index.mjs +334 -0
  37. package/dist/tenant/index.d.mts +89 -0
  38. package/dist/tenant/index.mjs +96 -0
  39. package/dist/testkit/index.d.mts +2 -2
  40. package/dist/testkit/index.mjs +1 -1
  41. package/dist/{timestamp.plugin-CBQukQzV.mjs → timestamp.plugin-DF0gJB6p.mjs} +116 -9
  42. package/dist/{to-plain-Dyz_qvuo.d.mts → to-plain-k7SjNlSS.d.mts} +2 -2
  43. package/dist/transaction-B5L9glO0.mjs +533 -0
  44. package/dist/{update-jSXp3nu3.mjs → update-DG0fCwRJ.mjs} +293 -83
  45. package/dist/utils/index.d.mts +1 -1
  46. package/dist/{validation-chain.plugin-DhNyxNCw.mjs → validation-chain.plugin-BLY2gLEk.mjs} +192 -279
  47. package/dist/{validation-chain.plugin-yQRob3Av.d.mts → validation-chain.plugin-DYp9E7K2.d.mts} +26 -20
  48. package/package.json +17 -4
  49. package/dist/PaginationEngine-BX9o_-4R.mjs +0 -735
  50. package/dist/core-62oo3N1C.d.mts +0 -50
  51. package/dist/pagination-jSUdrACK.d.mts +0 -158
@@ -0,0 +1,533 @@
1
+ //#region src/capabilities.ts
2
+ /**
3
+ * Read the provenance of a capability descriptor's `transactions` flag.
4
+ * A descriptor without the field is a static declaration (`'declared'`) —
5
+ * which is exactly right for sqlitekit / prismakit / any kit whose answer
6
+ * genuinely does not depend on a live deployment.
7
+ */
8
+ function transactionResolutionOf(caps) {
9
+ const value = caps.transactionsResolution;
10
+ return value === "observed" || value === "unknown" ? value : "declared";
11
+ }
12
+ /**
13
+ * What MongoDB supports through mongokit's surface, DECLARED (not observed).
14
+ *
15
+ * Deployment-dependent flags (`transactions`, `changeStreams` — both need a
16
+ * replica set or mongos) are `true` here because they describe the product,
17
+ * not the server you happen to be pointed at. `nestedTransactions` is NOT one
18
+ * of them: it is `false` unconditionally, because the refusal comes from
19
+ * mongokit's own tx-bound proxy rather than from the deployment. **Do not read this constant to decide whether a live connection can run
20
+ * a transaction** — that is {@link resolveMongoCapabilities}, which is what
21
+ * `Repository#capabilities` returns. This one is for the cross-kit conformance
22
+ * harness (which overrides environment-specific limits where it is
23
+ * constructed) and as the base every observed descriptor is derived from.
24
+ */
25
+ const MONGOKIT_CAPABILITIES = Object.freeze({
26
+ transactionsResolution: "declared",
27
+ transactions: true,
28
+ nestedTransactions: false,
29
+ transactionRetry: "managed",
30
+ upsert: true,
31
+ duplicateKeyError: true,
32
+ optimisticConcurrency: true,
33
+ distinct: true,
34
+ aggregate: true,
35
+ aggregateOps: Object.freeze({
36
+ percentile: true,
37
+ stddev: true,
38
+ topN: true,
39
+ dateBucketTimezone: true,
40
+ customDateBuckets: true,
41
+ dateBucketSubMinute: true,
42
+ cache: true
43
+ }),
44
+ getOrCreate: true,
45
+ countAndExists: true,
46
+ purgeByField: true,
47
+ purgeByFilter: true,
48
+ archiveByFilter: true,
49
+ arrayOperators: true,
50
+ regexFilter: true,
51
+ changeStreams: true,
52
+ lean: true,
53
+ lookupPopulate: true,
54
+ lookupCoerce: true,
55
+ streaming: true
56
+ });
57
+ /**
58
+ * Driver `ServerType`s that positively imply a session-capable, transaction-
59
+ * capable deployment. Verified against mongodb driver 7.x: a single-node
60
+ * replica set reports `RSPrimary` (topology `ReplicaSetWithPrimary`, or
61
+ * `Single` when reached with `directConnection`), a standalone `mongod`
62
+ * reports `Standalone`, and a mongos reports `Mongos`.
63
+ *
64
+ * **This is why the check reads SERVER type, not just topology type.** The
65
+ * pre-existing heuristic (`topology.description.type !== 'Single'`) has a
66
+ * false NEGATIVE: a directly-connected single-node replica set is `Single`
67
+ * yet runs transactions fine.
68
+ */
69
+ const TRANSACTION_CAPABLE_SERVER_TYPES = /* @__PURE__ */ new Set([
70
+ "RSPrimary",
71
+ "RSSecondary",
72
+ "RSArbiter",
73
+ "RSOther",
74
+ "RSGhost",
75
+ "Mongos",
76
+ "LoadBalancer"
77
+ ]);
78
+ /** Topology types that positively imply transactions regardless of server detail. */
79
+ const TRANSACTION_CAPABLE_TOPOLOGY_TYPES = /* @__PURE__ */ new Set([
80
+ "ReplicaSetWithPrimary",
81
+ "ReplicaSetNoPrimary",
82
+ "Sharded",
83
+ "LoadBalanced"
84
+ ]);
85
+ function clientOf(connection) {
86
+ const probe = connection;
87
+ if (!probe || typeof probe !== "object") return void 0;
88
+ try {
89
+ return probe.getClient?.() ?? probe.client ?? (probe.topology !== void 0 ? probe : void 0);
90
+ } catch {
91
+ return;
92
+ }
93
+ }
94
+ function serverTypes(description) {
95
+ const servers = description.servers;
96
+ if (!servers) return [];
97
+ return (typeof servers.values === "function" ? [...servers.values()] : Object.values(servers)).map((s) => s?.type).filter((t) => typeof t === "string");
98
+ }
99
+ /**
100
+ * Read whether a connection's LIVE deployment supports multi-document
101
+ * transactions, synchronously, from the driver's SDAM topology description.
102
+ *
103
+ * Free (a few property reads against an object the driver keeps current), so
104
+ * it is deliberately NOT memoized — a connection that opens after the repo was
105
+ * constructed upgrades its own answer with no invalidation step. What IS
106
+ * memoized is the async {@link probeMongoCapabilities} result, which this
107
+ * function consults first so an exotic deployment that answered `hello` but
108
+ * exposes no topology still reports definitively.
109
+ *
110
+ * @returns `'yes'` / `'no'` when the topology is conclusive, `'unknown'`
111
+ * otherwise — never optimistic. See the module docblock on rule 3.
112
+ */
113
+ function resolveTransactionSupport(connection) {
114
+ const cached = readProbeCache(connection);
115
+ if (cached) return cached;
116
+ const description = clientOf(connection)?.topology?.description;
117
+ if (!description) return "unknown";
118
+ const types = serverTypes(description);
119
+ if (types.some((t) => TRANSACTION_CAPABLE_SERVER_TYPES.has(t))) return "yes";
120
+ if (types.length > 0 && types.every((t) => t === "Standalone")) return "no";
121
+ const topologyType = description.type;
122
+ if (topologyType && TRANSACTION_CAPABLE_TOPOLOGY_TYPES.has(topologyType)) return "yes";
123
+ return "unknown";
124
+ }
125
+ function deriveCapabilities(support) {
126
+ const available = support === "yes";
127
+ return Object.freeze({
128
+ ...MONGOKIT_CAPABILITIES,
129
+ transactionsResolution: support === "unknown" ? "unknown" : "observed",
130
+ transactions: available,
131
+ nestedTransactions: false,
132
+ changeStreams: available
133
+ });
134
+ }
135
+ /** Three possible answers ⇒ three frozen singletons; the getter allocates nothing. */
136
+ const DERIVED = Object.freeze({
137
+ yes: deriveCapabilities("yes"),
138
+ no: deriveCapabilities("no"),
139
+ unknown: deriveCapabilities("unknown")
140
+ });
141
+ /**
142
+ * The capability descriptor for a LIVE connection — what
143
+ * `Repository#capabilities` returns.
144
+ *
145
+ * Every deployment-dependent flag reflects the observed topology; everything
146
+ * else comes from {@link MONGOKIT_CAPABILITIES}. Cheap enough to call per
147
+ * access (topology read + one frozen-object lookup).
148
+ */
149
+ function resolveMongoCapabilities(connection) {
150
+ return DERIVED[resolveTransactionSupport(connection)];
151
+ }
152
+ /** The three derived descriptors, for tests and for gates that want to compare identity. */
153
+ function capabilitiesForSupport(support) {
154
+ return DERIVED[support];
155
+ }
156
+ /** Definitive results only. Keyed on the connection object; never holds `'unknown'`. */
157
+ const PROBE_RESULTS = /* @__PURE__ */ new WeakMap();
158
+ /** In-flight de-duplication so N repos on one connection issue ONE `hello`. */
159
+ const PROBE_INFLIGHT = /* @__PURE__ */ new WeakMap();
160
+ function readProbeCache(connection) {
161
+ if (!connection || typeof connection !== "object") return void 0;
162
+ return PROBE_RESULTS.get(connection);
163
+ }
164
+ /** Test seam — drop memoized probe answers (a reused connection object across suites). */
165
+ function resetCapabilityProbeCache(connection) {
166
+ if (connection && typeof connection === "object") {
167
+ PROBE_RESULTS.delete(connection);
168
+ PROBE_INFLIGHT.delete(connection);
169
+ }
170
+ }
171
+ function withTimeout(promise, ms, onTimeout) {
172
+ return new Promise((resolve) => {
173
+ const timer = setTimeout(() => resolve(onTimeout), ms);
174
+ timer.unref?.();
175
+ promise.then((value) => {
176
+ clearTimeout(timer);
177
+ resolve(value);
178
+ }, () => {
179
+ clearTimeout(timer);
180
+ resolve(onTimeout);
181
+ });
182
+ });
183
+ }
184
+ /**
185
+ * Classify a `hello` (or legacy `isMaster`) reply.
186
+ *
187
+ * - `msg: 'isdbgrid'` ⇒ mongos ⇒ transactions (Mongo 4.2+ sharded).
188
+ * - `setName` ⇒ a replica-set member ⇒ transactions.
189
+ * - a reply that IS a hello (`isWritablePrimary` / `ismaster` present) with
190
+ * neither marker ⇒ standalone ⇒ **no**. Verified: a memory-server standalone
191
+ * returns `{ isWritablePrimary: true }` and nothing else.
192
+ * - anything unrecognised ⇒ `'unknown'`. A reply we cannot parse is not a yes.
193
+ */
194
+ function classifyHelloReply(reply) {
195
+ if (!reply || typeof reply !== "object") return "unknown";
196
+ const { msg, setName } = reply;
197
+ if (msg === "isdbgrid") return "yes";
198
+ if (typeof setName === "string" && setName.length > 0) return "yes";
199
+ return "isWritablePrimary" in reply || "ismaster" in reply ? "no" : "unknown";
200
+ }
201
+ async function runProbe(connection, timeoutMs) {
202
+ if (connection.readyState === 2 && typeof connection.asPromise === "function") await withTimeout(connection.asPromise().then(() => void 0, () => void 0), timeoutMs, void 0);
203
+ const fromTopology = resolveTransactionSupport(connection);
204
+ if (fromTopology !== "unknown") return fromTopology;
205
+ const admin = connection.db?.admin?.();
206
+ if (!admin) return "unknown";
207
+ return classifyHelloReply(await withTimeout(admin.command({ hello: 1 }).catch(async () => admin.command({ isMaster: 1 }).catch(() => void 0)), timeoutMs, void 0));
208
+ }
209
+ /**
210
+ * Resolve a connection's real capabilities, waiting for (and if necessary
211
+ * asking) the deployment. Memoized per connection: N repositories and N
212
+ * kernels on one connection issue at most ONE `hello`.
213
+ *
214
+ * Hosts do not have to call this — `Repository#capabilities` reads the live
215
+ * topology synchronously, which is already conclusive once
216
+ * `await mongoose.connect()` has returned (the Kernel Construction Standard's
217
+ * Describe → **Bind** ordering guarantees that). It exists for the cases the
218
+ * sync read cannot cover: binding while a connection is still opening, or a
219
+ * deployment whose driver surfaces no topology description. Calling it makes
220
+ * the SUBSEQUENT synchronous reads definitive, so nothing else needs rewiring:
221
+ *
222
+ * ```ts
223
+ * await probeMongoCapabilities(connection); // once, before bind
224
+ * const engine = defineWallet().bind(connection); // gate now sees the truth
225
+ * ```
226
+ *
227
+ * Only definitive answers are cached — an `'unknown'` may be re-probed later
228
+ * (the deployment may simply not have been up yet).
229
+ */
230
+ async function probeMongoCapabilities(connection, options = {}) {
231
+ if (!connection || typeof connection !== "object") return DERIVED.unknown;
232
+ const key = connection;
233
+ const cached = PROBE_RESULTS.get(key);
234
+ if (cached) return DERIVED[cached];
235
+ const inflight = PROBE_INFLIGHT.get(key);
236
+ if (inflight) return DERIVED[await inflight];
237
+ const run = runProbe(connection, options.timeoutMs ?? 2e3).catch(() => "unknown").then((support) => {
238
+ if (support !== "unknown") PROBE_RESULTS.set(key, support);
239
+ PROBE_INFLIGHT.delete(key);
240
+ return support;
241
+ });
242
+ PROBE_INFLIGHT.set(key, run);
243
+ return DERIVED[await run];
244
+ }
245
+ //#endregion
246
+ //#region src/tx-bound.ts
247
+ /**
248
+ * Map of session-aware method name → positional index of the `{ session }`
249
+ * options bag. Keep this list in sync with the method signatures in
250
+ * `Repository.ts` and the plugin method definitions. Unknown plugin
251
+ * methods pass through unwrapped (callers use the standalone helper).
252
+ */
253
+ const SESSION_OPTIONS_INDEX = Object.freeze({
254
+ create: 1,
255
+ update: 2,
256
+ delete: 1,
257
+ getById: 1,
258
+ getAll: 1,
259
+ getByIds: 1,
260
+ createMany: 1,
261
+ findAll: 1,
262
+ getOrCreate: 2,
263
+ count: 1,
264
+ exists: 1,
265
+ getByQuery: 1,
266
+ getOne: 1,
267
+ findOneAndUpdate: 2,
268
+ distinct: 2,
269
+ aggregate: 1,
270
+ aggregatePaginate: 0,
271
+ aggregatePipeline: 1,
272
+ aggregatePipelinePaginate: 0,
273
+ lookupPopulate: 0,
274
+ cursor: 1,
275
+ claim: 3,
276
+ claimVersion: 3,
277
+ applyTransition: 3,
278
+ archiveByFilter: 2,
279
+ purgeByField: 3,
280
+ purgeByFilter: 2,
281
+ upsert: 2,
282
+ increment: 3,
283
+ decrement: 3,
284
+ multiplyField: 3,
285
+ setMin: 3,
286
+ setMax: 3,
287
+ pushToArray: 3,
288
+ pullFromArray: 3,
289
+ addToSet: 3,
290
+ setField: 3,
291
+ unsetField: 2,
292
+ renameField: 3,
293
+ atomicUpdate: 2,
294
+ updateMany: 2,
295
+ deleteMany: 1,
296
+ bulkWrite: 1,
297
+ restore: 1,
298
+ getDeleted: 1,
299
+ addSubdocument: 3,
300
+ getSubdocument: 3,
301
+ updateSubdocument: 4,
302
+ deleteSubdocument: 3,
303
+ groupBy: 1,
304
+ sum: 2,
305
+ average: 2,
306
+ min: 2,
307
+ max: 2
308
+ });
309
+ /**
310
+ * Public methods that pass through bound to the outer repo, WITHOUT the session — each for a stated
311
+ * reason. Anything neither here nor in `SESSION_OPTIONS_INDEX` throws when called.
312
+ */
313
+ const PASS_THROUGH = /* @__PURE__ */ new Set([
314
+ "on",
315
+ "off",
316
+ "emit",
317
+ "emitAsync",
318
+ "removeAllListeners",
319
+ "use",
320
+ "useMiddleware",
321
+ "buildAggregation",
322
+ "buildLookup",
323
+ "isDuplicateKeyError",
324
+ "isTransientConflictError",
325
+ "invalidateAggregateCache",
326
+ "registerMethod",
327
+ "hasMethod",
328
+ "getRegisteredMethods",
329
+ "embed",
330
+ "search",
331
+ "lease",
332
+ "extend",
333
+ "release",
334
+ "claimKey",
335
+ "completeClaim",
336
+ "expireClaim",
337
+ "failClaim",
338
+ "releaseClaim",
339
+ "saveClaimProgress",
340
+ "searchSimilar"
341
+ ]);
342
+ /** Methods MongoDB cannot run inside a transaction. */
343
+ const REFUSED_IN_TX = Object.freeze({ watch: "a change stream cannot be opened inside a transaction" });
344
+ /**
345
+ * Build a session-threaded proxy over `outer`. The returned object has the
346
+ * same method signatures, but every CRUD call auto-injects the supplied
347
+ * session into the options bag. Non-CRUD properties (Model, modelName,
348
+ * hook API, utility helpers) pass through.
349
+ */
350
+ function createTxBoundRepo(outer, session) {
351
+ return new Proxy(outer, { get(target, prop, receiver) {
352
+ if (prop === "withTransaction") return () => {
353
+ throw new Error("[mongokit] Nested withTransaction is not supported on a tx-bound repository. Reuse the outer `txRepo` directly, or collapse the nesting.");
354
+ };
355
+ const value = Reflect.get(target, prop, receiver);
356
+ if (typeof value !== "function") return value;
357
+ if (prop === "Model") return value;
358
+ if (typeof prop === "symbol") return value.bind(target);
359
+ if (prop.startsWith("_")) return value.bind(target);
360
+ const optionsIndex = SESSION_OPTIONS_INDEX[prop];
361
+ if (optionsIndex === void 0) {
362
+ if (PASS_THROUGH.has(prop)) return value.bind(target);
363
+ const refused = REFUSED_IN_TX[prop];
364
+ return () => {
365
+ throw new Error(refused ? `[mongokit] ${prop}() cannot run on a tx-bound repository: ${refused}.` : `[mongokit] ${prop}() is not classified for transactions, so on a tx-bound repository it would run OUTSIDE the transaction. Call it on the outer repository with the session the callback receives — \`repo.${prop}(..., { session: uow.session })\` — or add it to tx-bound's SESSION_OPTIONS_INDEX / PASS_THROUGH.`);
366
+ };
367
+ }
368
+ return function txBoundMethod(...args) {
369
+ while (args.length <= optionsIndex) args.push(void 0);
370
+ const current = args[optionsIndex];
371
+ if (current === void 0) args[optionsIndex] = { session };
372
+ else if (typeof current === "object" && current !== null && !Array.isArray(current)) args[optionsIndex] = {
373
+ ...current,
374
+ session
375
+ };
376
+ else throw new TypeError(`[mongokit] ${prop}(): argument ${optionsIndex} must be an options object on a tx-bound repository (got ${Array.isArray(current) ? "an array" : typeof current}) — the session could not be threaded.`);
377
+ return value.apply(target, args);
378
+ };
379
+ } });
380
+ }
381
+ //#endregion
382
+ //#region src/transaction.ts
383
+ /**
384
+ * Run a callback inside a MongoDB transaction on the given connection.
385
+ *
386
+ * - Starts a session, runs `session.withTransaction(callback)` (which auto-retries
387
+ * on `TransientTransactionError` and `UnknownTransactionCommitResult`), and
388
+ * always ends the session in `finally`.
389
+ * - When `allowFallback` is true and the deployment doesn't support transactions
390
+ * (e.g. standalone MongoDB in dev), the callback runs once without a
391
+ * transaction on the same session. `onFallback` is invoked with the original
392
+ * error so hosts can log the degradation.
393
+ *
394
+ * @example
395
+ * ```ts
396
+ * import mongoose from 'mongoose';
397
+ * import { withTransaction } from '@classytic/mongokit';
398
+ *
399
+ * await withTransaction(mongoose.connection, async (session) => {
400
+ * const order = await orderRepo.create(data, { session });
401
+ * await inventoryRepo.decrement(order.items, { session });
402
+ * return order;
403
+ * });
404
+ * ```
405
+ */
406
+ async function withTransaction(connection, callback, options = {}) {
407
+ const session = await connection.startSession();
408
+ try {
409
+ return await session.withTransaction(() => callback(session), options.transactionOptions);
410
+ } catch (error) {
411
+ const err = error;
412
+ if (options.allowFallback && isTransactionUnsupported(err)) {
413
+ options.onFallback?.(err);
414
+ return await callback(session);
415
+ }
416
+ throw err;
417
+ } finally {
418
+ await session.endSession();
419
+ }
420
+ }
421
+ /**
422
+ * Multi-repo transactional batch — every repo in `repos` becomes a
423
+ * session-bound proxy inside the callback. Eliminates the per-call
424
+ * `{ session }` threading that was repeated across ~20 call sites
425
+ * (be-prod outbox writes, order placement, transfer source/dest pairs,
426
+ * payrun saga steps).
427
+ *
428
+ * Same retry + fallback semantics as `withTransaction`: the
429
+ * `transactionOptions` / `allowFallback` / `onFallback` knobs apply.
430
+ *
431
+ * Each property of the input `repos` map is rebound via the
432
+ * `createTxBoundRepo` proxy — every CRUD method on the bound repo
433
+ * auto-injects `session` into its options bag, including
434
+ * `claim` / `claimVersion` / `findOneAndUpdate` and plugin-contributed
435
+ * methods. Non-CRUD properties (Model, modelName, hook engine,
436
+ * idField) pass through to the underlying repo. Nested
437
+ * `boundRepo.withTransaction(...)` throws — reuse the outer bound
438
+ * repos.
439
+ *
440
+ * @example Order placement across three repos in one transaction
441
+ * ```ts
442
+ * import { batchTransaction } from '@classytic/mongokit';
443
+ *
444
+ * const order = await batchTransaction(
445
+ * mongoose.connection,
446
+ * { orders: orderRepo, events: eventRepo, inventory: inventoryRepo },
447
+ * async ({ orders, events, inventory }) => {
448
+ * const created = await orders.create(orderData); // session auto-injected
449
+ * await events.create({ type: 'order.placed', orderId: created._id });
450
+ * await inventory.claim(skuId, { from: 'available', to: 'reserved' });
451
+ * return created;
452
+ * },
453
+ * );
454
+ * ```
455
+ *
456
+ * @example With fallback for standalone-mongo dev environments
457
+ * ```ts
458
+ * await batchTransaction(
459
+ * mongoose.connection,
460
+ * { orders, events },
461
+ * async ({ orders, events }) => { ... },
462
+ * { allowFallback: true, onFallback: (err) => log.warn(err) },
463
+ * );
464
+ * ```
465
+ *
466
+ * @param connection - Mongoose connection (or anything with
467
+ * `startSession()`). Single source of session truth — every bound
468
+ * repo shares this session.
469
+ * @param repos - Map of repo instances to rebind. Keys become the
470
+ * property names on the callback's argument.
471
+ * @param callback - Receives the bound repo map. Return value flows
472
+ * through to the outer `Promise`.
473
+ */
474
+ async function batchTransaction(connection, repos, callback, options = {}) {
475
+ return withTransaction(connection, async (session) => {
476
+ const bound = {};
477
+ for (const key of Object.keys(repos)) {
478
+ const repo = repos[key];
479
+ bound[key] = createTxBoundRepo(repo, session);
480
+ }
481
+ return callback(bound);
482
+ }, options);
483
+ }
484
+ /**
485
+ * Detect whether an error indicates the MongoDB deployment does not support
486
+ * multi-document transactions (standalone server, older topology, etc.).
487
+ *
488
+ * Checks MongoDB error codes first — 263 (standalone) and 20 (unsupported
489
+ * topology) — with a message-matching fallback for edge cases surfaced by
490
+ * driver versions that throw before the proper code lands.
491
+ *
492
+ * **Driver-version drift:** modern mongoose / mongodb-driver versions hit
493
+ * the standalone case via the retryable-writes precondition rather than
494
+ * the transaction precondition (`retryWrites=true` is on by default,
495
+ * standalone Mongo rejects it before the transaction is even attempted).
496
+ * So we accept that message as equivalent — the underlying topology
497
+ * problem is the same, and the fallback semantics are correct either way.
498
+ */
499
+ function isTransactionUnsupported(error) {
500
+ const code = error.code;
501
+ if (code === 263 || code === 20) return true;
502
+ const message = (error.message || "").toLowerCase();
503
+ return message.includes("transaction numbers are only allowed on a replica set member") || message.includes("transaction is not supported") || message.includes("does not support retryable writes");
504
+ }
505
+ /**
506
+ * PROACTIVELY report whether a connection's deployment supports multi-document
507
+ * transactions — the read-side companion to {@link isTransactionUnsupported}
508
+ * (which classifies a FAILED attempt). Lets a caller skip a doomed transaction
509
+ * attempt on standalone dev Mongo rather than starting one just to catch the
510
+ * error.
511
+ *
512
+ * Delegates to {@link resolveTransactionSupport} so mongokit has exactly ONE
513
+ * topology reader. That also fixes a false NEGATIVE this function used to
514
+ * have: it tested `description.type !== 'Single'`, but a single-node replica
515
+ * set reached with `directConnection` is `Single` with a server type of
516
+ * `RSPrimary` and runs transactions fine. The shared reader looks at the
517
+ * SERVER descriptions first.
518
+ *
519
+ * **`unknown` is optimistic HERE and pessimistic in `capabilities.ts` — the
520
+ * difference is deliberate.** This answers "should I bother ATTEMPTING a
521
+ * transaction?", where guessing yes costs one caught error (`allowFallback`
522
+ * still recovers). The capability descriptor answers "may this deployment be
523
+ * trusted with money?", where guessing yes is the bug class this repo keeps
524
+ * hitting (AGENTS.md FAIL LOUD rule 3). Do not "unify" them.
525
+ *
526
+ * Accepts a Mongoose `Connection` (or anything exposing `getClient()`/`client`
527
+ * with a driver `topology`), so it stays decoupled from mongoose types.
528
+ */
529
+ function supportsTransactions(connection) {
530
+ return resolveTransactionSupport(connection) !== "no";
531
+ }
532
+ //#endregion
533
+ export { createTxBoundRepo as a, classifyHelloReply as c, resolveMongoCapabilities as d, resolveTransactionSupport as f, withTransaction as i, probeMongoCapabilities as l, isTransactionUnsupported as n, MONGOKIT_CAPABILITIES as o, transactionResolutionOf as p, supportsTransactions as r, capabilitiesForSupport as s, batchTransaction as t, resetCapabilityProbeCache as u };