@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.
- package/dist/{PaginationEngine-CBS1XGzp.d.mts → PaginationEngine-D-P19Y9m.d.mts} +34 -9
- package/dist/PaginationEngine-Dvl8jcRY.mjs +1198 -0
- package/dist/{Repository-Ck5x43wF.mjs → Repository-BMgPSkqZ.mjs} +672 -655
- package/dist/{Repository-BdSkQ8vc.d.mts → Repository-CR8Y9g2E.d.mts} +106 -34
- package/dist/actions/index.d.mts +1 -1
- package/dist/actions/index.mjs +1 -1
- package/dist/adapter/index.d.mts +21 -0
- package/dist/adapter/index.mjs +1 -1
- package/dist/{adapter-Bfqz8yHE.mjs → adapter-BrVfPFSW.mjs} +45 -1
- package/dist/ai/index.d.mts +1 -1
- package/dist/better-auth/index.mjs +2 -2
- package/dist/{create-repository-B5EulQ31.mjs → create-repository-BzBUMvd1.mjs} +3 -3
- package/dist/{create-repository-BU4x9Kb0.d.mts → create-repository-CAle1MSU.d.mts} +5 -5
- package/dist/idempotency/index.d.mts +23 -0
- package/dist/idempotency/index.mjs +134 -0
- package/dist/{index-DKEql4cL.d.mts → index-CmjNyulu.d.mts} +67 -8
- package/dist/index.d.mts +125 -35
- package/dist/index.mjs +286 -676
- package/dist/indexes/verify-indexes.d.mts +14 -1
- package/dist/indexes/verify-indexes.mjs +54 -7
- package/dist/{method-registry.plugin-DblM7V6j.mjs → method-registry.plugin-C63pGn1d.mjs} +54 -4
- package/dist/{multi-tenant.plugin-BwSvTirB.d.mts → multi-tenant.plugin-CBgCo9im.d.mts} +77 -5
- package/dist/{operations-DZodvjC-.d.mts → operations-CrRIoc8Z.d.mts} +22 -7
- package/dist/outbox/index.d.mts +1 -1
- package/dist/outbox/index.mjs +7 -5
- package/dist/pagination/PaginationEngine.d.mts +1 -1
- package/dist/pagination/PaginationEngine.mjs +1 -1
- package/dist/pagination/utils/limits.d.mts +1 -1
- package/dist/pagination/utils/limits.mjs +22 -1
- package/dist/pagination-BpIv2bdS.d.mts +364 -0
- package/dist/{plugin-options-DXnlKM3B.d.mts → plugin-options-Zk46lD6F.d.mts} +16 -2
- package/dist/plugins/index.d.mts +8 -3
- package/dist/plugins/index.mjs +99 -26
- package/dist/{repository-H_ffGgpE.d.mts → repository-odl03582.d.mts} +50 -6
- package/dist/sync/index.d.mts +40 -0
- package/dist/sync/index.mjs +334 -0
- package/dist/tenant/index.d.mts +89 -0
- package/dist/tenant/index.mjs +96 -0
- package/dist/testkit/index.d.mts +2 -2
- package/dist/testkit/index.mjs +1 -1
- package/dist/{timestamp.plugin-CBQukQzV.mjs → timestamp.plugin-DF0gJB6p.mjs} +116 -9
- package/dist/{to-plain-Dyz_qvuo.d.mts → to-plain-k7SjNlSS.d.mts} +2 -2
- package/dist/transaction-B5L9glO0.mjs +533 -0
- package/dist/{update-jSXp3nu3.mjs → update-DG0fCwRJ.mjs} +293 -83
- package/dist/utils/index.d.mts +1 -1
- package/dist/{validation-chain.plugin-DhNyxNCw.mjs → validation-chain.plugin-BLY2gLEk.mjs} +192 -279
- package/dist/{validation-chain.plugin-yQRob3Av.d.mts → validation-chain.plugin-DYp9E7K2.d.mts} +26 -20
- package/package.json +17 -4
- package/dist/PaginationEngine-BX9o_-4R.mjs +0 -735
- package/dist/core-62oo3N1C.d.mts +0 -50
- 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 };
|