@classytic/mongokit 3.14.0 → 3.16.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 (28) hide show
  1. package/README.md +193 -319
  2. package/dist/{PaginationEngine-CfQRM1yz.mjs → PaginationEngine-0XXTW-9b.mjs} +2 -2
  3. package/dist/{Repository-BGL1Pq8w.mjs → Repository-DE7D2QeS.mjs} +240 -19
  4. package/dist/actions/index.d.mts +1 -1
  5. package/dist/ai/index.d.mts +1 -1
  6. package/dist/ai/index.mjs +14 -5
  7. package/dist/better-auth/index.d.mts +55 -1
  8. package/dist/better-auth/index.mjs +32 -2
  9. package/dist/{index-Dccha9dw.d.mts → index-CV5vv-8E.d.mts} +1 -1
  10. package/dist/index.d.mts +1182 -1079
  11. package/dist/index.mjs +32 -19
  12. package/dist/{mongooseToJsonSchema-BYbG894y.mjs → mongooseToJsonSchema-Bi0q0pYW.mjs} +1 -1
  13. package/dist/{mongooseToJsonSchema-BgRAaXX-.d.mts → mongooseToJsonSchema-GhvZUjRO.d.mts} +1 -1
  14. package/dist/pagination/PaginationEngine.d.mts +1 -1
  15. package/dist/pagination/PaginationEngine.mjs +1 -1
  16. package/dist/plugins/index.d.mts +1 -1
  17. package/dist/plugins/index.mjs +1 -1
  18. package/dist/query/primitives/coercion.mjs +1 -1
  19. package/dist/{types-BbaPZleN.d.mts → types-BWZILovv.d.mts} +26 -1
  20. package/dist/utils/index.d.mts +1 -1
  21. package/dist/utils/index.mjs +3 -3
  22. package/dist/{validation-chain.plugin-CWhvBRej.mjs → validation-chain.plugin-Crl5nQAY.mjs} +1172 -1137
  23. package/dist/{validation-chain.plugin-CAPYCj_T.d.mts → validation-chain.plugin-Iee2_puD.d.mts} +368 -344
  24. package/package.json +6 -6
  25. /package/dist/{cursor-J84rR6wG.mjs → cursor-4LMYa4ga.mjs} +0 -0
  26. /package/dist/{field-selection-Br3WPOEb.mjs → field-selection-CQW3WBiK.mjs} +0 -0
  27. /package/dist/{id-resolution-B0Odvvou.mjs → id-resolution-DduoO4LI.mjs} +0 -0
  28. /package/dist/{sort-CrZjTK25.mjs → sort-RPVz5u0w.mjs} +0 -0
@@ -1,7 +1,7 @@
1
1
  import { n as warn } from "./logger-DprijP5h.mjs";
2
2
  import { t as createError } from "./error-B00pI7dV.mjs";
3
- import { n as encodeCursor, r as resolveCursorFilter } from "./cursor-J84rR6wG.mjs";
4
- import { r as validateKeysetSort, t as getPrimaryField } from "./sort-CrZjTK25.mjs";
3
+ import { n as encodeCursor, r as resolveCursorFilter } from "./cursor-4LMYa4ga.mjs";
4
+ import { r as validateKeysetSort, t as getPrimaryField } from "./sort-RPVz5u0w.mjs";
5
5
  //#region src/pagination/utils/index-hint.ts
6
6
  /**
7
7
  * Read a Mongoose schema's declared indexes, defensively.
@@ -1,15 +1,42 @@
1
1
  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-BXMnCvO6.mjs";
2
2
  import { n as warn } from "./logger-DprijP5h.mjs";
3
3
  import { n as isDuplicateKeyError, r as parseDuplicateKeyError, t as createError } from "./error-B00pI7dV.mjs";
4
- import { t as PaginationEngine } from "./PaginationEngine-CfQRM1yz.mjs";
4
+ import { t as PaginationEngine } from "./PaginationEngine-0XXTW-9b.mjs";
5
5
  import { hasNearOperator, rewriteNearForCount } from "./query/primitives/geo.mjs";
6
- import { r as isValidIdForType, t as getSchemaIdType } from "./id-resolution-B0Odvvou.mjs";
6
+ import { r as isValidIdForType, t as getSchemaIdType } from "./id-resolution-DduoO4LI.mjs";
7
7
  import { isFilter } from "@classytic/repo-core/filter";
8
8
  import mongoose from "mongoose";
9
9
  import { HOOK_PRIORITY } from "@classytic/repo-core/hooks";
10
- import { RepositoryBase, runChunkedPurge, validatePluginOrder } from "@classytic/repo-core/repository";
10
+ import { RepositoryBase, runChunkedPurge, throwIfAborted, validatePluginOrder, withRetry } from "@classytic/repo-core/repository";
11
11
  import { compileUpdateSpecToMongo, isUpdateSpec } from "@classytic/repo-core/update";
12
12
  import { decodeAggCursor, encodeAggCursor, isKeysetMode, normalizeGroupBy, validateMeasures } from "@classytic/repo-core/aggregate";
13
+ //#region src/capabilities.ts
14
+ const MONGOKIT_CAPABILITIES = {
15
+ transactions: true,
16
+ nestedTransactions: true,
17
+ upsert: true,
18
+ duplicateKeyError: true,
19
+ distinct: true,
20
+ aggregate: true,
21
+ aggregateOps: {
22
+ percentile: true,
23
+ stddev: true,
24
+ topN: true,
25
+ customDateBuckets: true,
26
+ dateBucketSubMinute: true,
27
+ cache: true
28
+ },
29
+ getOrCreate: true,
30
+ countAndExists: true,
31
+ purgeByField: true,
32
+ arrayOperators: true,
33
+ regexFilter: true,
34
+ changeStreams: true,
35
+ lean: true,
36
+ lookupPopulate: true,
37
+ streaming: true
38
+ };
39
+ //#endregion
13
40
  //#region src/operations.ts
14
41
  /**
15
42
  * The registry. Order intentionally groups by category for readability.
@@ -127,6 +154,11 @@ const OP_REGISTRY = {
127
154
  mutates: false,
128
155
  hasIdContext: false
129
156
  },
157
+ watch: {
158
+ policyKey: "query",
159
+ mutates: false,
160
+ hasIdContext: false
161
+ },
130
162
  getAll: {
131
163
  policyKey: "filters",
132
164
  mutates: false,
@@ -1649,6 +1681,34 @@ function assertNoMixedPatchShape(opName, patch) {
1649
1681
  const flatKeys = patchKeys.filter((k) => !k.startsWith("$"));
1650
1682
  throw createError(400, `[${opName}] assertNoMixedPatchShape: patch mixes Mongo operators (${operatorKeys.join(", ")}) with raw field keys (${flatKeys.join(", ")}). Mongo would silently DROP the flat keys — that's a write-loss bug we refuse to forward. Either wrap the flat keys in $set explicitly, or remove the operator keys.`);
1651
1683
  }
1684
+ /** Mongo `operationType` → portable `ChangeEvent.operation`. */
1685
+ const CHANGE_OPERATION_MAP = {
1686
+ insert: "create",
1687
+ update: "update",
1688
+ replace: "replace",
1689
+ delete: "delete"
1690
+ };
1691
+ /**
1692
+ * Re-root a compiled Mongo match document under a prefix (`fullDocument`)
1693
+ * so a caller filter like `{ status: 'pending' }` matches the change
1694
+ * stream's post-image. Logical operators (`$and` / `$or` / `$nor`)
1695
+ * recurse; `$`-prefixed top-level operators ($expr etc.) pass through
1696
+ * unprefixed — they reference paths explicitly.
1697
+ */
1698
+ function prefixMatchPaths(match, prefix) {
1699
+ const out = {};
1700
+ for (const [key, value] of Object.entries(match)) if (key === "$and" || key === "$or" || key === "$nor") out[key] = value.map((clause) => prefixMatchPaths(clause, prefix));
1701
+ else if (key.startsWith("$")) out[key] = value;
1702
+ else out[`${prefix}.${key}`] = value;
1703
+ return out;
1704
+ }
1705
+ /** Best-available commit timestamp for a change-stream document. */
1706
+ function changeEventTimestamp(change) {
1707
+ if (change.wallTime instanceof Date) return change.wallTime;
1708
+ const highBits = change.clusterTime?.getHighBits?.();
1709
+ if (typeof highBits === "number" && highBits > 0) return /* @__PURE__ */ new Date(highBits * 1e3);
1710
+ return /* @__PURE__ */ new Date();
1711
+ }
1652
1712
  /**
1653
1713
  * Plugin phase priorities (lower = runs first).
1654
1714
  *
@@ -1673,6 +1733,12 @@ const HOOK_PRIORITY$1 = HOOK_PRIORITY;
1673
1733
  * upgrading from 3.9 need no code changes.
1674
1734
  */
1675
1735
  var Repository = class extends RepositoryBase {
1736
+ /**
1737
+ * Runtime capability descriptor required by `StandardRepo<TDoc>`
1738
+ * (repo-core 0.6.0). Hosts feature-detect once at boot instead of
1739
+ * try/catching `UnsupportedOperationError` per call.
1740
+ */
1741
+ capabilities = MONGOKIT_CAPABILITIES;
1676
1742
  Model;
1677
1743
  /**
1678
1744
  * Mongoose model name. Duplicated on the instance for BC — arc / catalog /
@@ -1696,7 +1762,10 @@ var Repository = class extends RepositoryBase {
1696
1762
  super({
1697
1763
  name: Model.modelName,
1698
1764
  hooks: options.hooks ?? "async",
1699
- pluginOrderChecks: "off"
1765
+ pluginOrderChecks: "off",
1766
+ schema: options.schema,
1767
+ updateSchema: options.updateSchema,
1768
+ events: options.events
1700
1769
  });
1701
1770
  validatePluginOrder(plugins, Model.modelName, options.pluginOrderChecks ?? "warn", (m) => {
1702
1771
  warn(m);
@@ -1909,7 +1978,7 @@ var Repository = class extends RepositoryBase {
1909
1978
  async _runOp(op, context, fn) {
1910
1979
  return this._composeMiddleware(op, context, async () => {
1911
1980
  try {
1912
- const result = await fn();
1981
+ const result = await this._withResilience(context, fn);
1913
1982
  await this._emitHook(`after:${op}`, {
1914
1983
  context,
1915
1984
  result
@@ -1925,6 +1994,28 @@ var Repository = class extends RepositoryBase {
1925
1994
  });
1926
1995
  }
1927
1996
  /**
1997
+ * Resilience envelope around the DRIVER round-trip only — the repo-core
1998
+ * 0.6.0 `QueryOptions.signal` / `QueryOptions.retryPolicy` contract.
1999
+ *
2000
+ * `throwIfAborted` stops a cancelled request before the next driver call;
2001
+ * `withRetry` retries transient failures with exponential backoff (and is
2002
+ * a zero-cost passthrough when no policy was passed, so we wrap
2003
+ * unconditionally). Crucially this wraps `fn` AFTER `_buildContext` ran —
2004
+ * before-hooks (validation, tenant scope, audit, events) execute exactly
2005
+ * once per logical call and are NEVER re-run on retry; only the driver
2006
+ * call repeats.
2007
+ *
2008
+ * Both knobs are read from the context (the options bag is spread into
2009
+ * `_buildContext` inputs), so every op routed through `_runOp` — and the
2010
+ * inline-try methods that call this directly — honors them uniformly.
2011
+ */
2012
+ _withResilience(context, fn) {
2013
+ const signal = context.signal;
2014
+ const retryPolicy = context.retryPolicy;
2015
+ throwIfAborted(signal);
2016
+ return withRetry(fn, retryPolicy, signal);
2017
+ }
2018
+ /**
1928
2019
  * Create single document
1929
2020
  */
1930
2021
  async create(data, options = {}) {
@@ -1967,10 +2058,10 @@ var Repository = class extends RepositoryBase {
1967
2058
  try {
1968
2059
  const effectiveIdField = options.idField ?? this.idField;
1969
2060
  if (effectiveIdField !== "_id") {
1970
- const result = await getByQuery(this.Model, {
2061
+ const result = await this._withResilience(context, () => getByQuery(this.Model, {
1971
2062
  [effectiveIdField]: id,
1972
2063
  ...context.query || {}
1973
- }, context);
2064
+ }, context));
1974
2065
  await this._emitHook("after:getById", {
1975
2066
  context,
1976
2067
  result
@@ -1982,7 +2073,7 @@ var Repository = class extends RepositoryBase {
1982
2073
  if (wantsThrow) throw createError(404, "Document not found");
1983
2074
  return null;
1984
2075
  }
1985
- const result = await getById(this.Model, id, context);
2076
+ const result = await this._withResilience(context, () => getById(this.Model, id, context));
1986
2077
  if (!result && wantsThrow) throw createError(404, "Document not found");
1987
2078
  await this._emitHook("after:getById", {
1988
2079
  context,
@@ -2047,7 +2138,7 @@ var Repository = class extends RepositoryBase {
2047
2138
  }
2048
2139
  const finalQuery = context.query || query;
2049
2140
  try {
2050
- const result = await getByQuery(this.Model, finalQuery, context);
2141
+ const result = await this._withResilience(context, () => getByQuery(this.Model, finalQuery, context));
2051
2142
  await this._emitHook("after:getOne", {
2052
2143
  context,
2053
2144
  result
@@ -2155,6 +2246,135 @@ var Repository = class extends RepositoryBase {
2155
2246
  }
2156
2247
  }
2157
2248
  /**
2249
+ * Portable change feed — `StandardRepo.watch()` over Mongo change
2250
+ * streams (`Model.watch`). Requires a replica set (or mongos); a
2251
+ * standalone mongod rejects change streams at open time — gate on
2252
+ * `repo.capabilities.changeStreams` plus your deployment topology.
2253
+ *
2254
+ * Semantics:
2255
+ * - **Plugin-routed like every other read.** The filter goes through
2256
+ * the standard `before:watch` hook pipeline (`OP_REGISTRY.watch`,
2257
+ * policyKey `'query'`) BEFORE the change-stream pipeline is built —
2258
+ * `multiTenantPlugin` injects the tenant scope (and throws under
2259
+ * `required: true` exactly like other reads), `softDeletePlugin`
2260
+ * injects the deletion-state predicate. A tenant-scoped repo never
2261
+ * streams cross-tenant changes.
2262
+ * - `fullDocument: 'updateLookup'` — update events carry the post-
2263
+ * image document when it still exists at lookup time.
2264
+ * - `filter` (record or Filter IR) is compiled with the standard
2265
+ * filter compiler and applied against `fullDocument.*` paths, so
2266
+ * `watch({ status: 'pending' })` matches the post-image. Delete
2267
+ * events have no `fullDocument` and therefore don't match a
2268
+ * non-empty post-policy filter — use `bypassTenant` / an unscoped
2269
+ * repo without a filter to observe deletes.
2270
+ * - `options.signal` ends the iterator (closes the stream). A
2271
+ * pre-aborted signal rejects at the op boundary like every other op.
2272
+ * - `options.resumeAfter` forwards a previously captured Mongo
2273
+ * resume token for at-least-once consumption across restarts.
2274
+ *
2275
+ * Context keys for policy plugins (`organizationId`, `bypassTenant`,
2276
+ * `includeDeleted`, ...) ride the options bag at runtime — same as every
2277
+ * other op. The contract's `WatchOptions` type doesn't declare an index
2278
+ * signature, so typed callers widen at the call site (or rely on an
2279
+ * ALS `resolveContext` so no per-call tenant key is needed).
2280
+ *
2281
+ * @example
2282
+ * ```ts
2283
+ * const ac = new AbortController();
2284
+ * for await (const change of repo.watch({ status: 'pending' }, { signal: ac.signal })) {
2285
+ * if (change.operation === 'create') enqueue(change.doc!);
2286
+ * }
2287
+ * ```
2288
+ */
2289
+ async *watch(filter, options = {}) {
2290
+ const context = await this._buildContext("watch", {
2291
+ query: filter ?? {},
2292
+ ...options
2293
+ });
2294
+ const pipeline = [{ $match: { operationType: { $in: [
2295
+ "insert",
2296
+ "update",
2297
+ "replace",
2298
+ "delete"
2299
+ ] } } }];
2300
+ const compiled = compileFilterToMongo(context.query ?? {});
2301
+ if (Object.keys(compiled).length > 0) pipeline.push({ $match: prefixMatchPaths(compiled, "fullDocument") });
2302
+ const stream = this.Model.watch(pipeline, {
2303
+ fullDocument: "updateLookup",
2304
+ ...options.resumeAfter !== void 0 ? { resumeAfter: options.resumeAfter } : {}
2305
+ });
2306
+ const queue = [];
2307
+ let streamError = null;
2308
+ let ended = false;
2309
+ let notify = null;
2310
+ const wake = () => {
2311
+ const resolve = notify;
2312
+ notify = null;
2313
+ resolve?.();
2314
+ };
2315
+ const onChange = (change) => {
2316
+ queue.push(change);
2317
+ wake();
2318
+ };
2319
+ const onError = (error) => {
2320
+ streamError = error;
2321
+ wake();
2322
+ };
2323
+ const onClose = () => {
2324
+ ended = true;
2325
+ wake();
2326
+ };
2327
+ stream.on("change", onChange);
2328
+ stream.on("error", onError);
2329
+ stream.on("close", onClose);
2330
+ const closeStream = async () => {
2331
+ try {
2332
+ await stream.close();
2333
+ } catch {}
2334
+ };
2335
+ const signal = options.signal;
2336
+ const onAbort = () => {
2337
+ ended = true;
2338
+ wake();
2339
+ closeStream();
2340
+ };
2341
+ try {
2342
+ if (signal) {
2343
+ if (signal.aborted) return;
2344
+ signal.addEventListener("abort", onAbort, { once: true });
2345
+ }
2346
+ while (true) {
2347
+ if (signal?.aborted) return;
2348
+ const change = queue.shift();
2349
+ if (change) {
2350
+ const operation = CHANGE_OPERATION_MAP[change.operationType];
2351
+ if (!operation) continue;
2352
+ yield {
2353
+ operation,
2354
+ id: change.documentKey?._id,
2355
+ ...change.fullDocument !== void 0 ? { doc: change.fullDocument } : {},
2356
+ timestamp: changeEventTimestamp(change)
2357
+ };
2358
+ continue;
2359
+ }
2360
+ if (streamError) {
2361
+ if (signal?.aborted) return;
2362
+ throw streamError;
2363
+ }
2364
+ if (ended) return;
2365
+ await new Promise((resolve) => {
2366
+ notify = resolve;
2367
+ });
2368
+ }
2369
+ } finally {
2370
+ if (signal) signal.removeEventListener("abort", onAbort);
2371
+ stream.removeListener("change", onChange);
2372
+ stream.removeListener("error", onError);
2373
+ stream.removeListener("close", onClose);
2374
+ await closeStream();
2375
+ }
2376
+ }
2377
+ /**
2158
2378
  * Unified pagination - auto-detects offset vs keyset based on params
2159
2379
  *
2160
2380
  * Auto-detection logic:
@@ -2428,11 +2648,11 @@ var Repository = class extends RepositoryBase {
2428
2648
  }
2429
2649
  }
2430
2650
  let result;
2431
- if (effectiveIdField !== "_id") result = await updateByQuery(this.Model, {
2651
+ if (effectiveIdField !== "_id") result = await this._withResilience(context, () => updateByQuery(this.Model, {
2432
2652
  [effectiveIdField]: id,
2433
2653
  ...context.query || {}
2434
- }, context.data || data, context);
2435
- else result = await update(this.Model, id, context.data || data, context);
2654
+ }, context.data || data, context));
2655
+ else result = await this._withResilience(context, () => update(this.Model, id, context.data || data, context));
2436
2656
  if (!result && wantsThrow) throw createError(404, "Document not found");
2437
2657
  await this._emitHook("after:update", {
2438
2658
  context,
@@ -2903,10 +3123,10 @@ var Repository = class extends RepositoryBase {
2903
3123
  [effectiveIdField]: id,
2904
3124
  ...context.query || {}
2905
3125
  } : void 0;
2906
- const result = deleteQuery ? await deleteByQuery(this.Model, deleteQuery, { session: options.session }) : await deleteById(this.Model, id, {
3126
+ const result = await this._withResilience(context, () => deleteQuery ? deleteByQuery(this.Model, deleteQuery, { session: options.session }) : deleteById(this.Model, id, {
2907
3127
  session: options.session,
2908
3128
  query: context.query
2909
- });
3129
+ }));
2910
3130
  if (!result && wantsThrow) throw createError(404, "Document not found");
2911
3131
  await this._emitHook("after:delete", {
2912
3132
  context,
@@ -2999,7 +3219,7 @@ var Repository = class extends RepositoryBase {
2999
3219
  }
3000
3220
  const finalQuery = context.query || filter;
3001
3221
  if (!finalQuery || Object.keys(finalQuery).length === 0) throw createError(400, "deleteMany requires a non-empty query filter after policy hooks.");
3002
- const result = await this.Model.deleteMany(finalQuery, { session: options.session }).exec();
3222
+ const result = await this._withResilience(context, () => this.Model.deleteMany(finalQuery, { session: options.session }).exec());
3003
3223
  await this._emitHook("after:deleteMany", {
3004
3224
  context,
3005
3225
  result
@@ -3394,9 +3614,9 @@ var Repository = class extends RepositoryBase {
3394
3614
  };
3395
3615
  if (isKeyset && sort) {
3396
3616
  const parsedSort = this._parseSort(sort);
3397
- const { validateKeysetSort } = await import("./sort-CrZjTK25.mjs").then((n) => n.n);
3398
- const { encodeCursor, resolveCursorFilter } = await import("./cursor-J84rR6wG.mjs").then((n) => n.t);
3399
- const { getPrimaryField } = await import("./sort-CrZjTK25.mjs").then((n) => n.n);
3617
+ const { validateKeysetSort } = await import("./sort-RPVz5u0w.mjs").then((n) => n.n);
3618
+ const { encodeCursor, resolveCursorFilter } = await import("./cursor-4LMYa4ga.mjs").then((n) => n.t);
3619
+ const { getPrimaryField } = await import("./sort-RPVz5u0w.mjs").then((n) => n.n);
3400
3620
  const normalizedSort = validateKeysetSort(parsedSort, this._pagination.config.strictKeysetSortFields);
3401
3621
  const cursorVersion = this._pagination.config.cursorVersion ?? 1;
3402
3622
  const minCursorVersion = this._pagination.config.minCursorVersion ?? 1;
@@ -3621,6 +3841,7 @@ var Repository = class extends RepositoryBase {
3621
3841
  * 4. DEFAULT (500) — user-registered hooks
3622
3842
  */
3623
3843
  async _buildContext(operation, options) {
3844
+ throwIfAborted(options.signal);
3624
3845
  const normalized = this._normalizeFilterSlots(options);
3625
3846
  return await super._buildContext(operation, normalized);
3626
3847
  }
@@ -3795,4 +4016,4 @@ var Repository = class extends RepositoryBase {
3795
4016
  }
3796
4017
  };
3797
4018
  //#endregion
3798
- export { withTransaction as a, MUTATING_OPERATIONS as c, operationsByPolicyKey as d, isTransactionUnsupported as i, OP_REGISTRY as l, Repository as n, AggregationBuilder as o, batchTransaction as r, ALL_OPERATIONS as s, HOOK_PRIORITY$1 as t, READ_OPERATIONS as u };
4019
+ export { withTransaction as a, MUTATING_OPERATIONS as c, operationsByPolicyKey as d, MONGOKIT_CAPABILITIES as f, isTransactionUnsupported as i, OP_REGISTRY as l, Repository as n, AggregationBuilder as o, batchTransaction as r, ALL_OPERATIONS as s, HOOK_PRIORITY$1 as t, READ_OPERATIONS as u };
@@ -1,2 +1,2 @@
1
- import { a as create_d_exports, i as delete_d_exports, n as update_d_exports, o as aggregate_d_exports, r as read_d_exports } from "../index-Dccha9dw.mjs";
1
+ import { a as create_d_exports, i as delete_d_exports, n as update_d_exports, o as aggregate_d_exports, r as read_d_exports } from "../index-CV5vv-8E.mjs";
2
2
  export { aggregate_d_exports as aggregate, create_d_exports as create, delete_d_exports as deleteActions, read_d_exports as read, update_d_exports as update };
@@ -1,4 +1,4 @@
1
- import { G as Plugin } from "../types-BbaPZleN.mjs";
1
+ import { G as Plugin } from "../types-BWZILovv.mjs";
2
2
  import { PipelineStage } from "mongoose";
3
3
 
4
4
  //#region src/ai/types.d.ts
package/dist/ai/index.mjs CHANGED
@@ -269,7 +269,9 @@ function vectorPlugin(options) {
269
269
  name: "vector",
270
270
  apply(repo) {
271
271
  if (!repo.registerMethod) throw new Error("[mongokit] vectorPlugin requires methodRegistryPlugin");
272
+ let vectorSearchUnavailable = null;
272
273
  repo.registerMethod("searchSimilar", async function searchSimilar(params) {
274
+ if (vectorSearchUnavailable) throw vectorSearchUnavailable;
273
275
  const field = resolveField(fields, params.field);
274
276
  let queryVector;
275
277
  if (Array.isArray(params.query)) queryVector = params.query;
@@ -281,11 +283,18 @@ function vectorPlugin(options) {
281
283
  const pipeline = buildVectorSearchPipeline(field, queryVector, params);
282
284
  const agg = repo.Model.aggregate(pipeline);
283
285
  if (params.session) agg.session(params.session);
284
- return (await withVectorErrorHints(() => agg.exec(), {
285
- indexName: field.index,
286
- dimensions: field.dimensions,
287
- filterPaths: params.filter ? Object.keys(params.filter) : void 0
288
- })).map((doc) => {
286
+ let results;
287
+ try {
288
+ results = await withVectorErrorHints(() => agg.exec(), {
289
+ indexName: field.index,
290
+ dimensions: field.dimensions,
291
+ filterPaths: params.filter ? Object.keys(params.filter) : void 0
292
+ });
293
+ } catch (err) {
294
+ if (err.code === "NOT_ATLAS") vectorSearchUnavailable = err;
295
+ throw err;
296
+ }
297
+ return results.map((doc) => {
289
298
  const score = doc._score ?? 0;
290
299
  const { _score, ...rest } = doc;
291
300
  return {
@@ -119,6 +119,21 @@ interface BetterAuthOverlayOptions<TDoc = Record<string, unknown>> {
119
119
  * Subclass `Repository<TDoc>` to add domain methods (e.g. `getByEmail`,
120
120
  * `deactivate`, `getAdmins`). When omitted, the default `Repository<TDoc>`
121
121
  * is used — the standard CRUD surface is already complete.
122
+ *
123
+ * **Plugin composition warning.** The default overlay repo is built with
124
+ * NO plugins, deliberately:
125
+ *
126
+ * - **Do NOT apply `multiTenantPlugin` to BA overlays.** Better Auth's
127
+ * tables are global by design — `user`, `session`, `account`,
128
+ * `verification` have no tenant column at all, and `member` /
129
+ * `invitation` carry `organizationId` under BA's OWN semantics
130
+ * (membership rows, not tenant-scoped data). A tenant plugin with
131
+ * `required: true` would break every read; scope BA queries with
132
+ * explicit filters (`{ organizationId }`) at the call site instead.
133
+ * - **`cachePlugin` is safe only because the data is global** — if you
134
+ * wire it in a custom `RepositoryClass`, remember BA's own writes go
135
+ * through BA's driver and will NOT invalidate mongokit's cache. Use
136
+ * short TTLs or skip caching BA collections entirely.
122
137
  */
123
138
  RepositoryClass?: new (Model: ReturnType<MongooseLike['model']>) => RepositoryLike<TDoc>;
124
139
  /**
@@ -145,5 +160,44 @@ interface BetterAuthOverlayOptions<TDoc = Record<string, unknown>> {
145
160
  * would silently drop this call's `additionalFields` / `indexes`).
146
161
  */
147
162
  declare function createBetterAuthOverlay<TDoc = Record<string, unknown>>(options: BetterAuthOverlayOptions<TDoc>): Promise<DataAdapter<TDoc>>;
163
+ /**
164
+ * Minimal structural surface for the one write we need. Satisfied by BOTH a
165
+ * native MongoDB `Collection` (`db.collection('session')`) and a Mongoose
166
+ * `Model` — so the host passes whichever it has, and mongokit keeps mongoose +
167
+ * the mongodb driver as peer deps with zero runtime import.
168
+ */
169
+ interface SessionUpdaterLike {
170
+ updateMany(filter: Record<string, unknown>, update: Record<string, unknown>): Promise<{
171
+ modifiedCount?: number;
172
+ }>;
173
+ }
174
+ /**
175
+ * Clear a dangling `activeOrganizationId` from Better Auth `session` rows.
176
+ *
177
+ * WHY THIS EXISTS: Better Auth purges its `member` / `invitation` rows when an
178
+ * org is deleted (or a member removed), but it NEVER clears
179
+ * `session.activeOrganizationId`. A session left pointing at an org the user is
180
+ * no longer a member of makes `organization.getActiveMember()` return
181
+ * `MEMBER_NOT_FOUND`, and any frontend that trusts that stale pointer hangs —
182
+ * amplified by `session.cookieCache`, which serves the dead pointer for minutes.
183
+ *
184
+ * This is intentionally a host-invoked helper, NOT part of the arc org-delete
185
+ * cascade: arc is DB-agnostic (it purges through `@classytic/repo-core`
186
+ * adapters and must not reach a raw collection), whereas the `session`
187
+ * collection is a Better-Auth/mongo concern this kit already owns. Hosts wire
188
+ * it into their `organizationHooks`:
189
+ * - `afterDeleteOrganization` → org-wide (omit `userId`)
190
+ * - `afterRemoveMember` → one user (pass `userId`)
191
+ *
192
+ * The mongodb adapter stores `activeOrganizationId` as a STRING, so the org id
193
+ * is matched as-is (no ObjectId construction). `session.userId` is an ObjectId,
194
+ * so for the member-removal path pass `userId` already in the form your driver
195
+ * compares against (a constructed ObjectId) — it's matched verbatim.
196
+ *
197
+ * @returns the number of sessions whose pointer was cleared.
198
+ */
199
+ declare function clearActiveOrganizationFromSessions(sessions: SessionUpdaterLike, organizationId: string, options?: {
200
+ userId?: unknown;
201
+ }): Promise<number>;
148
202
  //#endregion
149
- export { BetterAuthInstance, BetterAuthOverlayOptions, type BetterAuthPluginKey, MongooseLike, RegisterBetterAuthStubsOptions, createBetterAuthOverlay, registerBetterAuthStubs };
203
+ export { BetterAuthInstance, BetterAuthOverlayOptions, type BetterAuthPluginKey, MongooseLike, RegisterBetterAuthStubsOptions, SessionUpdaterLike, clearActiveOrganizationFromSessions, createBetterAuthOverlay, registerBetterAuthStubs };
@@ -1,4 +1,4 @@
1
- import { n as Repository } from "../Repository-BGL1Pq8w.mjs";
1
+ import { n as Repository } from "../Repository-DE7D2QeS.mjs";
2
2
  import { n as createMongooseAdapter } from "../adapter-gG_uyFSZ.mjs";
3
3
  import { pluralizeBetterAuthCollection, resolveBetterAuthCollections } from "@classytic/repo-core/better-auth";
4
4
  //#region src/better-auth/index.ts
@@ -77,5 +77,35 @@ async function createBetterAuthOverlay(options) {
77
77
  schemaGenerator
78
78
  });
79
79
  }
80
+ /**
81
+ * Clear a dangling `activeOrganizationId` from Better Auth `session` rows.
82
+ *
83
+ * WHY THIS EXISTS: Better Auth purges its `member` / `invitation` rows when an
84
+ * org is deleted (or a member removed), but it NEVER clears
85
+ * `session.activeOrganizationId`. A session left pointing at an org the user is
86
+ * no longer a member of makes `organization.getActiveMember()` return
87
+ * `MEMBER_NOT_FOUND`, and any frontend that trusts that stale pointer hangs —
88
+ * amplified by `session.cookieCache`, which serves the dead pointer for minutes.
89
+ *
90
+ * This is intentionally a host-invoked helper, NOT part of the arc org-delete
91
+ * cascade: arc is DB-agnostic (it purges through `@classytic/repo-core`
92
+ * adapters and must not reach a raw collection), whereas the `session`
93
+ * collection is a Better-Auth/mongo concern this kit already owns. Hosts wire
94
+ * it into their `organizationHooks`:
95
+ * - `afterDeleteOrganization` → org-wide (omit `userId`)
96
+ * - `afterRemoveMember` → one user (pass `userId`)
97
+ *
98
+ * The mongodb adapter stores `activeOrganizationId` as a STRING, so the org id
99
+ * is matched as-is (no ObjectId construction). `session.userId` is an ObjectId,
100
+ * so for the member-removal path pass `userId` already in the form your driver
101
+ * compares against (a constructed ObjectId) — it's matched verbatim.
102
+ *
103
+ * @returns the number of sessions whose pointer was cleared.
104
+ */
105
+ async function clearActiveOrganizationFromSessions(sessions, organizationId, options) {
106
+ const filter = { activeOrganizationId: organizationId };
107
+ if (options?.userId != null) filter.userId = options.userId;
108
+ return (await sessions.updateMany(filter, { $set: { activeOrganizationId: null } })).modifiedCount ?? 0;
109
+ }
80
110
  //#endregion
81
- export { createBetterAuthOverlay, registerBetterAuthStubs };
111
+ export { clearActiveOrganizationFromSessions, createBetterAuthOverlay, registerBetterAuthStubs };
@@ -1,4 +1,4 @@
1
- import { B as ObjectId, Dt as LookupOptions, H as OperationOptions, I as MinMaxResult, J as PopulateSpec, T as GroupResult, Z as ReadPreferenceType, ft as UpdateManyResult, ht as UpdateWithValidationResult, i as AnyDocument, it as SelectSpec, p as CreateOptions, pt as UpdateOptions, ut as SortSpec, v as DeleteResult, w as FindOneAndUpdateOptions } from "./types-BbaPZleN.mjs";
1
+ import { B as ObjectId, Dt as LookupOptions, H as OperationOptions, I as MinMaxResult, J as PopulateSpec, T as GroupResult, Z as ReadPreferenceType, ft as UpdateManyResult, ht as UpdateWithValidationResult, i as AnyDocument, it as SelectSpec, p as CreateOptions, pt as UpdateOptions, ut as SortSpec, v as DeleteResult, w as FindOneAndUpdateOptions } from "./types-BWZILovv.mjs";
2
2
  import { Model, PipelineStage } from "mongoose";
3
3
 
4
4
  //#region src/actions/aggregate.d.ts