@classytic/mongokit 3.29.0 → 3.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/README.md +43 -0
  2. package/dist/{create-repository-D4ZU94b3.d.mts → Repository-BXHkUtLk.d.mts} +30 -57
  3. package/dist/{Repository-DThvFCaV.mjs → Repository-X7U2d0kU.mjs} +296 -42
  4. package/dist/actions/index.mjs +1 -1
  5. package/dist/adapter/index.mjs +1 -1
  6. package/dist/{adapter-CLvUHiTW.mjs → adapter-BitHg-CN.mjs} +1 -1
  7. package/dist/ai/index.d.mts +1 -1
  8. package/dist/better-auth/index.d.mts +13 -0
  9. package/dist/better-auth/index.mjs +76 -6
  10. package/dist/create-repository-CvD4wzuN.d.mts +54 -0
  11. package/dist/{create-repository-uhKt_oUT.mjs → create-repository-DYe5YEie.mjs} +3 -2
  12. package/dist/index.d.mts +275 -41
  13. package/dist/index.mjs +80 -7
  14. package/dist/kernel-conformance/index.d.mts +217 -0
  15. package/dist/kernel-conformance/index.mjs +603 -0
  16. package/dist/method-registry.plugin-DblM7V6j.mjs +112 -0
  17. package/dist/model-registry-ClKtWiHa.mjs +67 -0
  18. package/dist/{mongooseToJsonSchema-CByt1VhQ.mjs → mongooseToJsonSchema-DKbnKduN.mjs} +14 -3
  19. package/dist/{multi-tenant.plugin-DVyVGREb.d.mts → multi-tenant.plugin-2VtSSm1Y.d.mts} +1 -1
  20. package/dist/outbox/index.d.mts +63 -0
  21. package/dist/outbox/index.mjs +239 -0
  22. package/dist/{plugin-options-vFufQZMZ.d.mts → plugin-options-BsFH0sTi.d.mts} +1 -1
  23. package/dist/plugins/index.d.mts +4 -4
  24. package/dist/plugins/index.mjs +3 -2
  25. package/dist/{repository-DCHRWRXw.d.mts → repository-Q16ErTu8.d.mts} +19 -1
  26. package/dist/testkit/index.d.mts +8 -1
  27. package/dist/testkit/index.mjs +1 -1
  28. package/dist/{timestamp.plugin-CFL6LlYZ.mjs → timestamp.plugin-ChFRcLRJ.mjs} +32 -143
  29. package/dist/{to-plain-DkF5px0R.d.mts → to-plain-BuX1RUZV.d.mts} +12 -4
  30. package/dist/{update-CI2NdAW6.mjs → update-DHdXdOk_.mjs} +21 -2
  31. package/dist/utils/index.d.mts +2 -2
  32. package/dist/utils/index.mjs +1 -1
  33. package/dist/{validation-chain.plugin-BLecaKa0.d.mts → validation-chain.plugin-BSshLJTA.d.mts} +3 -3
  34. package/dist/{validation-chain.plugin-CwOcUJC1.mjs → validation-chain.plugin-BxTl_dYR.mjs} +4 -3
  35. package/package.json +191 -185
@@ -1,5 +1,5 @@
1
1
  import { i as isValidIdForType, t as getSchemaIdType } from "./id-resolution-BqDrk0S2.mjs";
2
- import { a as count, b as LookupBuilder, c as getByQuery, d as deleteById, f as deleteByQuery, h as createMany, l as getOrCreate, m as create, n as update, o as exists, r as updateByQuery, s as getById, t as findOneAndUpdate, x as compileFilterToMongo, y as distinct } from "./update-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-DHdXdOk_.mjs";
3
3
  import { n as warn } from "./logger-K7dPmu2U.mjs";
4
4
  import { n as isDuplicateKeyError, r as parseDuplicateKeyError, t as createError } from "./error-BuByZwKU.mjs";
5
5
  import { t as PaginationEngine } from "./PaginationEngine-CczbvCAq.mjs";
@@ -80,9 +80,7 @@ function createKeysetPurgePort(repo, baseFilter, session) {
80
80
  [strategy.deletedAtField ?? "deletedAt"]: /* @__PURE__ */ new Date()
81
81
  } }, baseOpts);
82
82
  break;
83
- case "anonymize":
84
- await repo.updateMany(chunkFilter, { $set: strategy.fields }, baseOpts);
85
- break;
83
+ case "anonymize": await repo.updateMany(chunkFilter, { $set: strategy.fields }, baseOpts);
86
84
  }
87
85
  cursor = ids[ids.length - 1];
88
86
  return idDocs.length;
@@ -127,21 +125,44 @@ async function purgeAnonymizeFunctional(repo, selectFilter, fields, limit, sessi
127
125
  }
128
126
  //#endregion
129
127
  //#region src/capabilities.ts
130
- const MONGOKIT_CAPABILITIES = {
128
+ /**
129
+ * Read the provenance of a capability descriptor's `transactions` flag.
130
+ * A descriptor without the field is a static declaration (`'declared'`) —
131
+ * which is exactly right for sqlitekit / prismakit / any kit whose answer
132
+ * genuinely does not depend on a live deployment.
133
+ */
134
+ function transactionResolutionOf(caps) {
135
+ const value = caps.transactionsResolution;
136
+ return value === "observed" || value === "unknown" ? value : "declared";
137
+ }
138
+ /**
139
+ * What MongoDB supports through mongokit's surface, DECLARED (not observed).
140
+ *
141
+ * Deployment-dependent flags (`transactions`, `nestedTransactions`,
142
+ * `changeStreams` — all three need a replica set or mongos) are `true` here
143
+ * because they describe the product, not the server you happen to be pointed
144
+ * at. **Do not read this constant to decide whether a live connection can run
145
+ * a transaction** — that is {@link resolveMongoCapabilities}, which is what
146
+ * `Repository#capabilities` returns. This one is for the cross-kit conformance
147
+ * harness (which overrides environment-specific limits where it is
148
+ * constructed) and as the base every observed descriptor is derived from.
149
+ */
150
+ const MONGOKIT_CAPABILITIES = Object.freeze({
151
+ transactionsResolution: "declared",
131
152
  transactions: true,
132
153
  nestedTransactions: true,
133
154
  upsert: true,
134
155
  duplicateKeyError: true,
135
156
  distinct: true,
136
157
  aggregate: true,
137
- aggregateOps: {
158
+ aggregateOps: Object.freeze({
138
159
  percentile: true,
139
160
  stddev: true,
140
161
  topN: true,
141
162
  customDateBuckets: true,
142
163
  dateBucketSubMinute: true,
143
164
  cache: true
144
- },
165
+ }),
145
166
  getOrCreate: true,
146
167
  countAndExists: true,
147
168
  purgeByField: true,
@@ -153,7 +174,195 @@ const MONGOKIT_CAPABILITIES = {
153
174
  lean: true,
154
175
  lookupPopulate: true,
155
176
  streaming: true
156
- };
177
+ });
178
+ /**
179
+ * Driver `ServerType`s that positively imply a session-capable, transaction-
180
+ * capable deployment. Verified against mongodb driver 7.x: a single-node
181
+ * replica set reports `RSPrimary` (topology `ReplicaSetWithPrimary`, or
182
+ * `Single` when reached with `directConnection`), a standalone `mongod`
183
+ * reports `Standalone`, and a mongos reports `Mongos`.
184
+ *
185
+ * **This is why the check reads SERVER type, not just topology type.** The
186
+ * pre-existing heuristic (`topology.description.type !== 'Single'`) has a
187
+ * false NEGATIVE: a directly-connected single-node replica set is `Single`
188
+ * yet runs transactions fine.
189
+ */
190
+ const TRANSACTION_CAPABLE_SERVER_TYPES = /* @__PURE__ */ new Set([
191
+ "RSPrimary",
192
+ "RSSecondary",
193
+ "RSArbiter",
194
+ "RSOther",
195
+ "RSGhost",
196
+ "Mongos",
197
+ "LoadBalancer"
198
+ ]);
199
+ /** Topology types that positively imply transactions regardless of server detail. */
200
+ const TRANSACTION_CAPABLE_TOPOLOGY_TYPES = /* @__PURE__ */ new Set([
201
+ "ReplicaSetWithPrimary",
202
+ "ReplicaSetNoPrimary",
203
+ "Sharded",
204
+ "LoadBalanced"
205
+ ]);
206
+ function clientOf(connection) {
207
+ const probe = connection;
208
+ if (!probe || typeof probe !== "object") return void 0;
209
+ try {
210
+ return probe.getClient?.() ?? probe.client ?? (probe.topology !== void 0 ? probe : void 0);
211
+ } catch {
212
+ return;
213
+ }
214
+ }
215
+ function serverTypes(description) {
216
+ const servers = description.servers;
217
+ if (!servers) return [];
218
+ return (typeof servers.values === "function" ? [...servers.values()] : Object.values(servers)).map((s) => s?.type).filter((t) => typeof t === "string");
219
+ }
220
+ /**
221
+ * Read whether a connection's LIVE deployment supports multi-document
222
+ * transactions, synchronously, from the driver's SDAM topology description.
223
+ *
224
+ * Free (a few property reads against an object the driver keeps current), so
225
+ * it is deliberately NOT memoized — a connection that opens after the repo was
226
+ * constructed upgrades its own answer with no invalidation step. What IS
227
+ * memoized is the async {@link probeMongoCapabilities} result, which this
228
+ * function consults first so an exotic deployment that answered `hello` but
229
+ * exposes no topology still reports definitively.
230
+ *
231
+ * @returns `'yes'` / `'no'` when the topology is conclusive, `'unknown'`
232
+ * otherwise — never optimistic. See the module docblock on rule 3.
233
+ */
234
+ function resolveTransactionSupport(connection) {
235
+ const cached = readProbeCache(connection);
236
+ if (cached) return cached;
237
+ const description = clientOf(connection)?.topology?.description;
238
+ if (!description) return "unknown";
239
+ const types = serverTypes(description);
240
+ if (types.some((t) => TRANSACTION_CAPABLE_SERVER_TYPES.has(t))) return "yes";
241
+ if (types.length > 0 && types.every((t) => t === "Standalone")) return "no";
242
+ const topologyType = description.type;
243
+ if (topologyType && TRANSACTION_CAPABLE_TOPOLOGY_TYPES.has(topologyType)) return "yes";
244
+ return "unknown";
245
+ }
246
+ function deriveCapabilities(support) {
247
+ const available = support === "yes";
248
+ return Object.freeze({
249
+ ...MONGOKIT_CAPABILITIES,
250
+ transactionsResolution: support === "unknown" ? "unknown" : "observed",
251
+ transactions: available,
252
+ nestedTransactions: available,
253
+ changeStreams: available
254
+ });
255
+ }
256
+ /** Three possible answers ⇒ three frozen singletons; the getter allocates nothing. */
257
+ const DERIVED = Object.freeze({
258
+ yes: deriveCapabilities("yes"),
259
+ no: deriveCapabilities("no"),
260
+ unknown: deriveCapabilities("unknown")
261
+ });
262
+ /**
263
+ * The capability descriptor for a LIVE connection — what
264
+ * `Repository#capabilities` returns.
265
+ *
266
+ * Every deployment-dependent flag reflects the observed topology; everything
267
+ * else comes from {@link MONGOKIT_CAPABILITIES}. Cheap enough to call per
268
+ * access (topology read + one frozen-object lookup).
269
+ */
270
+ function resolveMongoCapabilities(connection) {
271
+ return DERIVED[resolveTransactionSupport(connection)];
272
+ }
273
+ /** The three derived descriptors, for tests and for gates that want to compare identity. */
274
+ function capabilitiesForSupport(support) {
275
+ return DERIVED[support];
276
+ }
277
+ /** Definitive results only. Keyed on the connection object; never holds `'unknown'`. */
278
+ const PROBE_RESULTS = /* @__PURE__ */ new WeakMap();
279
+ /** In-flight de-duplication so N repos on one connection issue ONE `hello`. */
280
+ const PROBE_INFLIGHT = /* @__PURE__ */ new WeakMap();
281
+ function readProbeCache(connection) {
282
+ if (!connection || typeof connection !== "object") return void 0;
283
+ return PROBE_RESULTS.get(connection);
284
+ }
285
+ /** Test seam — drop memoized probe answers (a reused connection object across suites). */
286
+ function resetCapabilityProbeCache(connection) {
287
+ if (connection && typeof connection === "object") {
288
+ PROBE_RESULTS.delete(connection);
289
+ PROBE_INFLIGHT.delete(connection);
290
+ }
291
+ }
292
+ function withTimeout(promise, ms, onTimeout) {
293
+ return new Promise((resolve) => {
294
+ const timer = setTimeout(() => resolve(onTimeout), ms);
295
+ timer.unref?.();
296
+ promise.then((value) => {
297
+ clearTimeout(timer);
298
+ resolve(value);
299
+ }, () => {
300
+ clearTimeout(timer);
301
+ resolve(onTimeout);
302
+ });
303
+ });
304
+ }
305
+ /**
306
+ * Classify a `hello` (or legacy `isMaster`) reply.
307
+ *
308
+ * - `msg: 'isdbgrid'` ⇒ mongos ⇒ transactions (Mongo 4.2+ sharded).
309
+ * - `setName` ⇒ a replica-set member ⇒ transactions.
310
+ * - a reply that IS a hello (`isWritablePrimary` / `ismaster` present) with
311
+ * neither marker ⇒ standalone ⇒ **no**. Verified: a memory-server standalone
312
+ * returns `{ isWritablePrimary: true }` and nothing else.
313
+ * - anything unrecognised ⇒ `'unknown'`. A reply we cannot parse is not a yes.
314
+ */
315
+ function classifyHelloReply(reply) {
316
+ if (!reply || typeof reply !== "object") return "unknown";
317
+ const { msg, setName } = reply;
318
+ if (msg === "isdbgrid") return "yes";
319
+ if (typeof setName === "string" && setName.length > 0) return "yes";
320
+ return "isWritablePrimary" in reply || "ismaster" in reply ? "no" : "unknown";
321
+ }
322
+ async function runProbe(connection, timeoutMs) {
323
+ if (connection.readyState === 2 && typeof connection.asPromise === "function") await withTimeout(connection.asPromise().then(() => void 0, () => void 0), timeoutMs, void 0);
324
+ const fromTopology = resolveTransactionSupport(connection);
325
+ if (fromTopology !== "unknown") return fromTopology;
326
+ const admin = connection.db?.admin?.();
327
+ if (!admin) return "unknown";
328
+ return classifyHelloReply(await withTimeout(admin.command({ hello: 1 }).catch(async () => admin.command({ isMaster: 1 }).catch(() => void 0)), timeoutMs, void 0));
329
+ }
330
+ /**
331
+ * Resolve a connection's real capabilities, waiting for (and if necessary
332
+ * asking) the deployment. Memoized per connection: N repositories and N
333
+ * kernels on one connection issue at most ONE `hello`.
334
+ *
335
+ * Hosts do not have to call this — `Repository#capabilities` reads the live
336
+ * topology synchronously, which is already conclusive once
337
+ * `await mongoose.connect()` has returned (the Kernel Construction Standard's
338
+ * Describe → **Bind** ordering guarantees that). It exists for the cases the
339
+ * sync read cannot cover: binding while a connection is still opening, or a
340
+ * deployment whose driver surfaces no topology description. Calling it makes
341
+ * the SUBSEQUENT synchronous reads definitive, so nothing else needs rewiring:
342
+ *
343
+ * ```ts
344
+ * await probeMongoCapabilities(connection); // once, before bind
345
+ * const engine = defineWallet().bind(connection); // gate now sees the truth
346
+ * ```
347
+ *
348
+ * Only definitive answers are cached — an `'unknown'` may be re-probed later
349
+ * (the deployment may simply not have been up yet).
350
+ */
351
+ async function probeMongoCapabilities(connection, options = {}) {
352
+ if (!connection || typeof connection !== "object") return DERIVED.unknown;
353
+ const key = connection;
354
+ const cached = PROBE_RESULTS.get(key);
355
+ if (cached) return DERIVED[cached];
356
+ const inflight = PROBE_INFLIGHT.get(key);
357
+ if (inflight) return DERIVED[await inflight];
358
+ const run = runProbe(connection, options.timeoutMs ?? 2e3).catch(() => "unknown").then((support) => {
359
+ if (support !== "unknown") PROBE_RESULTS.set(key, support);
360
+ PROBE_INFLIGHT.delete(key);
361
+ return support;
362
+ });
363
+ PROBE_INFLIGHT.set(key, run);
364
+ return DERIVED[await run];
365
+ }
157
366
  //#endregion
158
367
  //#region src/operations.ts
159
368
  /**
@@ -705,7 +914,16 @@ function buildAggPipeline(req, schema) {
705
914
  const aliasSet = new Set((req.lookups ?? []).map((l) => l.as ?? l.from));
706
915
  const { base: baseFilter, joined: joinedFilter } = splitFilterByAlias(schema !== void 0 && req.filter !== void 0 ? castFilterToSchema(req.filter, schema) : req.filter, aliasSet);
707
916
  if (baseFilter !== void 0) {
708
- const match = compileFilterToMongo(baseFilter);
917
+ /**
918
+ * The SCHEMA is passed so the date coercion can consult it.
919
+ *
920
+ * `castFilterToSchema` above already casts operands to their declared types; without
921
+ * the schema here, `compileFilterToMongo`'s coercion then guessed from value shape
922
+ * alone and OVERRODE that cast — turning a `'2026-08-02'` bound on a String column
923
+ * into a Date, which BSON refuses to compare against the stored string. The aggregate
924
+ * returned zero rows and nothing indicated why.
925
+ */
926
+ const match = compileFilterToMongo(baseFilter, schema);
709
927
  if (Object.keys(match).length > 0) stages.push({ $match: match });
710
928
  }
711
929
  if (req.lookups && req.lookups.length > 0) appendLookupStages$1(stages, req.lookups);
@@ -993,7 +1211,7 @@ async function countAggGroups(Model, req, options = {}) {
993
1211
  const [row] = await aggregation.exec();
994
1212
  return row?.n ?? 0;
995
1213
  }
996
- const match = req.filter ? compileFilterToMongo(castFilterToSchema(req.filter, Model.schema)) : {};
1214
+ const match = req.filter ? compileFilterToMongo(castFilterToSchema(req.filter, Model.schema), Model.schema) : {};
997
1215
  if (groupCols.length === 0) {
998
1216
  const pipeline = [];
999
1217
  if (Object.keys(match).length > 0) pipeline.push({ $match: match });
@@ -1899,29 +2117,29 @@ function isTransactionUnsupported(error) {
1899
2117
  /**
1900
2118
  * PROACTIVELY report whether a connection's deployment supports multi-document
1901
2119
  * 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.
2120
+ * (which classifies a FAILED attempt). Lets a caller skip a doomed transaction
2121
+ * attempt on standalone dev Mongo rather than starting one just to catch the
2122
+ * error.
2123
+ *
2124
+ * Delegates to {@link resolveTransactionSupport} so mongokit has exactly ONE
2125
+ * topology reader. That also fixes a false NEGATIVE this function used to
2126
+ * have: it tested `description.type !== 'Single'`, but a single-node replica
2127
+ * set reached with `directConnection` is `Single` with a server type of
2128
+ * `RSPrimary` and runs transactions fine. The shared reader looks at the
2129
+ * SERVER descriptions first.
1904
2130
  *
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.
2131
+ * **`unknown` is optimistic HERE and pessimistic in `capabilities.ts` — the
2132
+ * difference is deliberate.** This answers "should I bother ATTEMPTING a
2133
+ * transaction?", where guessing yes costs one caught error (`allowFallback`
2134
+ * still recovers). The capability descriptor answers "may this deployment be
2135
+ * trusted with money?", where guessing yes is the bug class this repo keeps
2136
+ * hitting (AGENTS.md FAIL LOUD rule 3). Do not "unify" them.
1911
2137
  *
1912
2138
  * Accepts a Mongoose `Connection` (or anything exposing `getClient()`/`client`
1913
2139
  * with a driver `topology`), so it stays decoupled from mongoose types.
1914
2140
  */
1915
2141
  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";
2142
+ return resolveTransactionSupport(connection) !== "no";
1925
2143
  }
1926
2144
  //#endregion
1927
2145
  //#region src/Repository.ts
@@ -1986,8 +2204,31 @@ var Repository = class extends RepositoryBase {
1986
2204
  * Runtime capability descriptor required by `StandardRepo<TDoc>`
1987
2205
  * (repo-core 0.6.0). Hosts feature-detect once at boot instead of
1988
2206
  * try/catching `UnsupportedOperationError` per call.
2207
+ *
2208
+ * **OBSERVED, not declared** (changed 2026-08-05). Until then this was a
2209
+ * class field pointing at the static `MONGOKIT_CAPABILITIES`, so
2210
+ * `transactions` read `true` for every mongoose-backed repository —
2211
+ * including one pointed at a standalone `mongod`, which cannot run a
2212
+ * transaction at all. Every kernel boot gate that reads it (wallet, party,
2213
+ * invoice, purchase, flow, …) was therefore unfalsifiable decoration
2214
+ * (AGENTS.md FAIL LOUD rule 4).
2215
+ *
2216
+ * Now it derives the deployment-dependent flags (`transactions`,
2217
+ * `nestedTransactions`, `changeStreams`) from THIS model's connection and
2218
+ * its live SDAM topology. A getter rather than a constructor snapshot on
2219
+ * purpose: a repository built before `mongoose.connect()` resolves upgrades
2220
+ * its own answer the moment the connection is up, with no invalidation step.
2221
+ * When the topology cannot be read the answer is `transactions: false` +
2222
+ * `transactionsResolution: 'unknown'` — fail closed, never an optimistic
2223
+ * yes (rule 3). See `capabilities.ts` and `probeMongoCapabilities`.
2224
+ *
2225
+ * `options.capabilities` overrides the whole descriptor for hosts/tests that
2226
+ * genuinely know better (a proxied connection, a non-mongoose driver seam).
1989
2227
  */
1990
- capabilities = MONGOKIT_CAPABILITIES;
2228
+ get capabilities() {
2229
+ return this._capabilitiesOverride ?? resolveMongoCapabilities(this.Model?.db);
2230
+ }
2231
+ _capabilitiesOverride;
1991
2232
  Model;
1992
2233
  /**
1993
2234
  * Mongoose model name. Duplicated on the instance for BC — arc / catalog /
@@ -2020,6 +2261,7 @@ var Repository = class extends RepositoryBase {
2020
2261
  warn(m);
2021
2262
  });
2022
2263
  this.Model = Model;
2264
+ this._capabilitiesOverride = options.capabilities;
2023
2265
  this.model = Model.modelName;
2024
2266
  this._pagination = new PaginationEngine(Model, paginationConfig);
2025
2267
  this.idField = options.idField ?? "_id";
@@ -2318,7 +2560,8 @@ var Repository = class extends RepositoryBase {
2318
2560
  return result;
2319
2561
  }
2320
2562
  const wantsThrow = context.throwOnNotFound === true || options.throwOnNotFound === true;
2321
- if (!isValidIdForType(id, getSchemaIdType(this.Model.schema))) {
2563
+ const idType = getSchemaIdType(this.Model.schema);
2564
+ if (!isValidIdForType(id, idType)) {
2322
2565
  if (wantsThrow) throw createError(404, "Document not found");
2323
2566
  return null;
2324
2567
  }
@@ -2822,8 +3065,9 @@ var Repository = class extends RepositoryBase {
2822
3065
  /**
2823
3066
  * Update a document by id and return the post-update doc.
2824
3067
  *
2825
- * Compiles to `Model.findOneAndUpdate(filter, { $set: data }, { new:
2826
- * true, runValidators: true })` under the hood and routes through the
3068
+ * Compiles to `Model.findOneAndUpdate(filter, { $set: data },
3069
+ * { returnDocument: 'after', runValidators: true })` under the hood and
3070
+ * routes through the
2827
3071
  * full plugin pipeline — `multiTenantPlugin` injects the tenant scope,
2828
3072
  * `auditTrailPlugin` records the change, `cachePlugin` invalidates,
2829
3073
  * `softDeletePlugin` etc. all compose. Returns `null` on miss (the
@@ -2838,7 +3082,7 @@ var Repository = class extends RepositoryBase {
2838
3082
  * const doc = await CampaignModel.findOneAndUpdate(
2839
3083
  * { _id: id, organizationId: ctx.orgId }, // hand-rolled tenant filter
2840
3084
  * { $set: patch, $currentDate: { updatedAt: 1 } },
2841
- * { new: true, runValidators: true }
3085
+ * { returnDocument: 'after', runValidators: true }
2842
3086
  * );
2843
3087
  * return doc ? toEntity(doc) : null;
2844
3088
  *
@@ -2867,7 +3111,7 @@ var Repository = class extends RepositoryBase {
2867
3111
  * const doc = await CampaignModel.findOneAndUpdate(
2868
3112
  * { _id: id, organizationId: ctx.orgId, status: 'pending' },
2869
3113
  * { $set: { status: 'sent', sentAt: new Date() } },
2870
- * { new: true }
3114
+ * { returnDocument: 'after' }
2871
3115
  * );
2872
3116
  * if (!doc) throw new ConcurrentTransitionError(id);
2873
3117
  *
@@ -2921,7 +3165,8 @@ var Repository = class extends RepositoryBase {
2921
3165
  const wantsThrow = context.throwOnNotFound === true || options.throwOnNotFound === true;
2922
3166
  const effectiveIdField = options.idField ?? this.idField;
2923
3167
  if (effectiveIdField === "_id") {
2924
- if (!isValidIdForType(id, getSchemaIdType(this.Model.schema))) {
3168
+ const idType = getSchemaIdType(this.Model.schema);
3169
+ if (!isValidIdForType(id, idType)) {
2925
3170
  if (wantsThrow) throw createError(404, "Document not found");
2926
3171
  await this._emitHook("after:update", {
2927
3172
  context,
@@ -3211,7 +3456,8 @@ var Repository = class extends RepositoryBase {
3211
3456
  const finalQuery = context.query || filter;
3212
3457
  const finalUpdate = context.data || update;
3213
3458
  if (effectiveIdField === "_id" && !options.upsert) {
3214
- if (!isValidIdForType(id, getSchemaIdType(this.Model.schema))) return null;
3459
+ const idType = getSchemaIdType(this.Model.schema);
3460
+ if (!isValidIdForType(id, idType)) return null;
3215
3461
  }
3216
3462
  if (Object.keys(finalUpdate).length === 0) return await this.Model.findOne(finalQuery, null, { session: options.session });
3217
3463
  return await findOneAndUpdate(this.Model, finalQuery, finalUpdate, {
@@ -3444,7 +3690,8 @@ var Repository = class extends RepositoryBase {
3444
3690
  const finalQuery = context.query || filter;
3445
3691
  const finalUpdate = context.data || operatorUpdate;
3446
3692
  if (effectiveIdField === "_id") {
3447
- if (!isValidIdForType(id, getSchemaIdType(this.Model.schema))) return null;
3693
+ const idType = getSchemaIdType(this.Model.schema);
3694
+ if (!isValidIdForType(id, idType)) return null;
3448
3695
  }
3449
3696
  return await findOneAndUpdate(this.Model, finalQuery, finalUpdate, {
3450
3697
  returnDocument: "after",
@@ -3497,7 +3744,8 @@ var Repository = class extends RepositoryBase {
3497
3744
  const effectiveIdField = options.idField ?? this.idField;
3498
3745
  const wantsThrow = context.throwOnNotFound === true || options.throwOnNotFound === true;
3499
3746
  if (effectiveIdField === "_id") {
3500
- if (!isValidIdForType(id, getSchemaIdType(this.Model.schema))) {
3747
+ const idType = getSchemaIdType(this.Model.schema);
3748
+ if (!isValidIdForType(id, idType)) {
3501
3749
  if (wantsThrow) throw createError(404, "Document not found");
3502
3750
  await this._emitHook("after:delete", {
3503
3751
  context,
@@ -3630,7 +3878,8 @@ var Repository = class extends RepositoryBase {
3630
3878
  * the port implementation.
3631
3879
  */
3632
3880
  async purgeByField(field, value, strategy, options = {}) {
3633
- return runChunkedPurge(strategy, options, createMongoPurgePort(this, field, value, options.session));
3881
+ const port = createMongoPurgePort(this, field, value, options.session);
3882
+ return runChunkedPurge(strategy, options, port);
3634
3883
  }
3635
3884
  /**
3636
3885
  * Range/filter-scoped purge — see `StandardRepo.purgeByFilter` for the
@@ -3648,7 +3897,8 @@ var Repository = class extends RepositoryBase {
3648
3897
  */
3649
3898
  async purgeByFilter(filter, strategy, options = {}) {
3650
3899
  const compiled = compileFilterToMongo(filter);
3651
- return runChunkedPurge(strategy, options, createMongoPurgePortFromFilter(this, compiled, options.session));
3900
+ const port = createMongoPurgePortFromFilter(this, compiled, options.session);
3901
+ return runChunkedPurge(strategy, options, port);
3652
3902
  }
3653
3903
  /**
3654
3904
  * Chunked cold-storage extraction — see `StandardRepo.archiveByFilter`
@@ -3663,7 +3913,8 @@ var Repository = class extends RepositoryBase {
3663
3913
  */
3664
3914
  async archiveByFilter(filter, sink, options = {}) {
3665
3915
  const compiled = compileFilterToMongo(filter);
3666
- return runChunkedArchive(options, sink, createMongoArchivePort(this, compiled, options.session));
3916
+ const port = createMongoArchivePort(this, compiled, options.session);
3917
+ return runChunkedArchive(options, sink, port);
3667
3918
  }
3668
3919
  /**
3669
3920
  * Kit-native aggregation pipeline — takes a MongoDB stage array and
@@ -4410,7 +4661,10 @@ var Repository = class extends RepositoryBase {
4410
4661
  const labels = error.errorLabels;
4411
4662
  const hasLabel = error.hasErrorLabel;
4412
4663
  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(", ")}`);
4664
+ if (error instanceof mongoose.Error.ValidationError) {
4665
+ const messages = Object.values(error.errors).map((err) => err.message);
4666
+ return createError(400, `Validation Error: ${messages.join(", ")}`);
4667
+ }
4414
4668
  if (error instanceof mongoose.Error.CastError) return createError(400, `Invalid ${error.path}: ${error.value}`);
4415
4669
  const duplicateErr = parseDuplicateKeyError(error);
4416
4670
  if (duplicateErr) return duplicateErr;
@@ -4419,4 +4673,4 @@ var Repository = class extends RepositoryBase {
4419
4673
  }
4420
4674
  };
4421
4675
  //#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 };
4676
+ export { resetCapabilityProbeCache as _, supportsTransactions as a, transactionResolutionOf as b, ALL_OPERATIONS as c, READ_OPERATIONS as d, operationsByPolicyKey as f, probeMongoCapabilities as g, classifyHelloReply as h, isTransactionUnsupported as i, MUTATING_OPERATIONS as l, capabilitiesForSupport as m, Repository as n, withTransaction as o, MONGOKIT_CAPABILITIES as p, batchTransaction as r, AggregationBuilder as s, HOOK_PRIORITY$1 as t, OP_REGISTRY as u, resolveMongoCapabilities as v, createMongoPurgePort as x, resolveTransactionSupport as y };
@@ -1,5 +1,5 @@
1
1
  import { t as __exportAll } from "../rolldown-runtime-D7D4PA-g.mjs";
2
- import { g as create_exports, i as update_exports, p as delete_exports, u as read_exports, v as aggregate_exports } from "../update-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-DHdXdOk_.mjs";
3
3
  //#region src/actions/index.ts
4
4
  var actions_exports = /* @__PURE__ */ __exportAll({
5
5
  aggregate: () => aggregate_exports,
@@ -1,2 +1,2 @@
1
- import { n as createMongooseAdapter, r as isMongooseModel, t as MongooseAdapter } from "../adapter-CLvUHiTW.mjs";
1
+ import { n as createMongooseAdapter, r as isMongooseModel, t as MongooseAdapter } from "../adapter-BitHg-CN.mjs";
2
2
  export { MongooseAdapter, createMongooseAdapter, isMongooseModel };
@@ -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 */
@@ -58,6 +58,19 @@ interface RegisterBetterAuthStubsOptions {
58
58
  usePlural?: boolean;
59
59
  /** Per-collection model name override (mirrors BA's `user.modelName`). */
60
60
  modelOverrides?: Partial<Record<string, string>>;
61
+ /**
62
+ * Canonical collection names to SKIP — because something else owns their model.
63
+ *
64
+ * The normal reason is `createBetterAuthOverlay`: a host wants stubs for the collections it merely
65
+ * REFERENCES (`organization`, `member`) and a full overlay for the one it exposes CRUD on (`user`).
66
+ * Without this the two helpers collide — the stub registers `user` first, and the overlay then
67
+ * THROWS, correctly, because mongoose locks a schema on first `model()` and its `additionalFields`
68
+ * would be silently dropped.
69
+ *
70
+ * Names are CANONICAL (`'user'`, not a pluralised or overridden model name); exclusion is applied
71
+ * before `usePlural` / `modelOverrides`, so a caller does not have to predict the final name.
72
+ */
73
+ exclude?: string[];
61
74
  }
62
75
  /**
63
76
  * Register stub Mongoose models for Better Auth's collections so that
@@ -1,8 +1,45 @@
1
- import { n as Repository } from "../Repository-DThvFCaV.mjs";
2
- import { n as createMongooseAdapter } from "../adapter-CLvUHiTW.mjs";
1
+ import { n as Repository } from "../Repository-X7U2d0kU.mjs";
2
+ import { n as createMongooseAdapter } from "../adapter-BitHg-CN.mjs";
3
3
  import { pluralizeBetterAuthCollection, resolveBetterAuthCollections } from "@classytic/repo-core/better-auth";
4
4
  //#region src/better-auth/index.ts
5
5
  /**
6
+ * Field key with ORDER PRESERVED — a compound index is identified by its key sequence.
7
+ * `{ organizationId: 1, createdAt: -1 }` is a DIFFERENT index from `{ createdAt: -1, organizationId: 1 }`
8
+ * (they serve different queries), so sorting the keys here would wrongly equate them.
9
+ */
10
+ function indexFieldsKey(fields) {
11
+ return Object.entries(fields).map(([k, v]) => `${k}:${String(v)}`).join(",");
12
+ }
13
+ /** Deterministic canonical form (sorted object keys, recursive) for value equality. */
14
+ function canonical(value) {
15
+ if (Array.isArray(value)) return value.map(canonical);
16
+ if (value && typeof value === "object") {
17
+ const obj = value;
18
+ return Object.keys(obj).sort().reduce((acc, k) => {
19
+ acc[k] = canonical(obj[k]);
20
+ return acc;
21
+ }, {});
22
+ }
23
+ return value;
24
+ }
25
+ /**
26
+ * Is every DECLARED option satisfied by the existing index's options? A subset check, not
27
+ * equality: Mongoose augments stored index options with defaults (e.g. `background`), so the
28
+ * existing side legitimately carries extra keys. We require each declared key to deep-equal the
29
+ * existing value — covering ALL declared options (unique, sparse, expireAfterSeconds, collation,
30
+ * name, weights, default_language, partialFilterExpression, …) with no hand-maintained subset.
31
+ */
32
+ function declaredOptionsSatisfied(declared = {}, existing = {}) {
33
+ for (const [key, value] of Object.entries(declared)) if (JSON.stringify(canonical(value)) !== JSON.stringify(canonical(existing[key]))) return false;
34
+ return true;
35
+ }
36
+ /** Which declared indexes are NOT already present on the existing model's schema. */
37
+ function missingIndexesOn(existing, declared) {
38
+ if (declared.length === 0) return [];
39
+ const existingIndexes = (existing?.schema)?.indexes?.() ?? [];
40
+ return declared.filter((d) => !existingIndexes.some(([fields, options]) => indexFieldsKey(fields) === indexFieldsKey(d.fields) && declaredOptionsSatisfied(d.options ?? {}, options ?? {})));
41
+ }
42
+ /**
6
43
  * Register stub Mongoose models for Better Auth's collections so that
7
44
  * Mongoose-based resources can `.populate()` references to BA-owned
8
45
  * documents. Idempotent — safe to call multiple times.
@@ -22,10 +59,14 @@ import { pluralizeBetterAuthCollection, resolveBetterAuthCollections } from "@cl
22
59
  * already existed on `mongoose.models`).
23
60
  */
24
61
  function registerBetterAuthStubs(mongoose, options = {}) {
25
- const names = resolveBetterAuthCollections(options);
62
+ const excluded = new Set(options.exclude ?? []);
63
+ const names = resolveBetterAuthCollections({
64
+ ...options,
65
+ ...excluded.size ? { exclude: [...excluded] } : {}
66
+ });
26
67
  const registered = [];
27
68
  for (const finalName of names) {
28
- if (mongoose.models[finalName]) continue;
69
+ if (finalName in mongoose.models) continue;
29
70
  const schema = new mongoose.Schema({}, {
30
71
  strict: false,
31
72
  collection: finalName,
@@ -59,8 +100,37 @@ async function createBetterAuthOverlay(options) {
59
100
  const tableConfig = ctx.tables[collection];
60
101
  if (!tableConfig) throw new Error(`[mongokit:better-auth] Better Auth has no table named '${collection}'. Available: ${Object.keys(ctx.tables).join(", ")}. Did you enable the right plugin in your betterAuth() config?`);
61
102
  const finalName = usePlural ? pluralizeBetterAuthCollection(tableConfig.modelName) : tableConfig.modelName;
62
- if (mongoose.models[finalName]) {
63
- if (Object.keys(additionalFields).length > 0 || indexes.length > 0) throw new Error(`[mongokit:better-auth] '${finalName}' already registered on mongoose.models. Cannot apply additionalFields / indexes from this createBetterAuthOverlay() call — mongoose locks schema on first model() call. Either: (a) drop the prior registerBetterAuthStubs() call for '${finalName}', or (b) move additionalFields / indexes into the existing schema definition.`);
103
+ if (finalName in mongoose.models) {
104
+ const declared = Object.keys(additionalFields);
105
+ if (declared.length > 0 || indexes.length > 0) {
106
+ /**
107
+ * Distinguish "a STUB got here first" from "this same overlay already ran in this process".
108
+ *
109
+ * The guard exists because mongoose locks a schema on first `model()`, so declared
110
+ * `additionalFields` would be silently dropped. That danger is real when a
111
+ * `registerBetterAuthStubs()` call registered an EMPTY `strict: false` schema first.
112
+ *
113
+ * It is NOT real on a re-boot: a process that composes the app twice (every integration suite,
114
+ * and any host that recomposes) hits an existing model whose schema ALREADY carries these exact
115
+ * paths, because an identical overlay call put them there. Throwing then just makes the second
116
+ * boot impossible — which is precisely why hosts hand-rolled `models.X || model(X, schema)` and
117
+ * inherited the silent-drop bug this guard was written to stop.
118
+ *
119
+ * So the test is the SCHEMA, not the mere presence of a model: if every declared field is
120
+ * already a path, the fields are applied and reuse is safe. If any is missing, something else
121
+ * owns this model and the throw stands.
122
+ */
123
+ const existing = mongoose.models[finalName];
124
+ const paths = existing?.schema?.paths ?? {};
125
+ const missing = declared.filter((f) => !(f in paths));
126
+ const missingIdx = missingIndexesOn(existing, indexes);
127
+ if (missing.length > 0 || missingIdx.length > 0) {
128
+ const parts = [];
129
+ if (missing.length > 0) parts.push(`${missing.length === 1 ? "field" : "fields"} ${missing.map((f) => `'${f}'`).join(", ")}`);
130
+ if (missingIdx.length > 0) parts.push(`${missingIdx.length === 1 ? "index" : "indexes"} on ${missingIdx.map((i) => `{${Object.keys(i.fields).join(",")}}`).join(", ")}`);
131
+ throw new Error(`[mongokit:better-auth] '${finalName}' already registered on mongoose.models WITHOUT ${parts.join(" and ")}. Cannot apply additionalFields / indexes from this createBetterAuthOverlay() call — mongoose locks schema on first model() call, so they would be SILENTLY DROPPED. Either: (a) exclude '${finalName}' from the prior registerBetterAuthStubs() call (\`exclude: ['${collection}']\`), or (b) move additionalFields / indexes there.`);
132
+ }
133
+ }
64
134
  }
65
135
  let Model = mongoose.models[finalName];
66
136
  if (!Model) {