@classytic/mongokit 3.32.0 → 3.34.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 (33) hide show
  1. package/dist/{PaginationEngine-CSh-P8Nh.mjs → PaginationEngine-GdyX29tE.mjs} +13 -11
  2. package/dist/{Repository-B_1y9cCf.mjs → Repository-BArvU32x.mjs} +149 -36
  3. package/dist/{Repository-BAzymuOz.d.mts → Repository-BdSkQ8vc.d.mts} +27 -4
  4. package/dist/actions/index.d.mts +1 -1
  5. package/dist/actions/index.mjs +1 -1
  6. package/dist/ai/index.d.mts +1 -1
  7. package/dist/better-auth/index.d.mts +79 -1
  8. package/dist/better-auth/index.mjs +59 -6
  9. package/dist/{create-repository-mEdfH9sz.d.mts → create-repository-BU4x9Kb0.d.mts} +4 -4
  10. package/dist/{create-repository-a3DQ5tzt.mjs → create-repository-CPNbktFG.mjs} +2 -2
  11. package/dist/id-chunks-BmAwsqNk.mjs +67 -0
  12. package/dist/{index-Beyr1r4d.d.mts → index-DKEql4cL.d.mts} +1 -1
  13. package/dist/index.d.mts +15 -14
  14. package/dist/index.mjs +60 -54
  15. package/dist/{multi-tenant.plugin-2VtSSm1Y.d.mts → multi-tenant.plugin-BwSvTirB.d.mts} +3 -3
  16. package/dist/{operations-CuzX9m2Z.d.mts → operations-DZodvjC-.d.mts} +22 -3
  17. package/dist/outbox/index.d.mts +1 -1
  18. package/dist/outbox/index.mjs +1 -1
  19. package/dist/pagination/PaginationEngine.mjs +1 -1
  20. package/dist/{plugin-options-BsFH0sTi.d.mts → plugin-options-DXnlKM3B.d.mts} +31 -1
  21. package/dist/plugins/index.d.mts +3 -3
  22. package/dist/plugins/index.mjs +2 -2
  23. package/dist/{repository-Q16ErTu8.d.mts → repository-H_ffGgpE.d.mts} +8 -0
  24. package/dist/testkit/index.d.mts +2 -2
  25. package/dist/testkit/index.mjs +1 -1
  26. package/dist/{timestamp.plugin-CAXzHs9y.mjs → timestamp.plugin-BEc6TANm.mjs} +7 -5
  27. package/dist/{to-plain-BuX1RUZV.d.mts → to-plain-Dyz_qvuo.d.mts} +3 -3
  28. package/dist/{update-w5i-pa_c.mjs → update-jSXp3nu3.mjs} +48 -28
  29. package/dist/utils/index.d.mts +54 -2
  30. package/dist/utils/index.mjs +2 -1
  31. package/dist/{validation-chain.plugin-BihbMQRq.mjs → validation-chain.plugin-CyK1s9Ze.mjs} +219 -20
  32. package/dist/{validation-chain.plugin-BSshLJTA.d.mts → validation-chain.plugin-yQRob3Av.d.mts} +2 -2
  33. package/package.json +17 -17
@@ -35,18 +35,20 @@ function buildKeysetFilter(baseFilters, sort, cursorValue, cursorId, cursorValue
35
35
  const primaryField = sortFields[0] || "_id";
36
36
  const direction = sort[primaryField];
37
37
  const operator = direction === 1 ? "$gt" : "$lt";
38
- if (cursorValue === null || cursorValue === void 0) if (direction === 1) return {
39
- ...baseFilters,
40
- $or: [{
38
+ if (cursorValue === null || cursorValue === void 0) {
39
+ if (direction === 1) return {
40
+ ...baseFilters,
41
+ $or: [{
42
+ [primaryField]: null,
43
+ _id: { $gt: cursorId }
44
+ }, { [primaryField]: { $ne: null } }]
45
+ };
46
+ else return {
47
+ ...baseFilters,
41
48
  [primaryField]: null,
42
- _id: { $gt: cursorId }
43
- }, { [primaryField]: { $ne: null } }]
44
- };
45
- else return {
46
- ...baseFilters,
47
- [primaryField]: null,
48
- _id: { $lt: cursorId }
49
- };
49
+ _id: { $lt: cursorId }
50
+ };
51
+ }
50
52
  return {
51
53
  ...baseFilters,
52
54
  $or: [{ [primaryField]: { [operator]: cursorValue } }, {
@@ -1,12 +1,14 @@
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-w5i-pa_c.mjs";
2
+ import { a as narrowToIds, i as keysetFilter, o as selectKeysetChunk } from "./id-chunks-BmAwsqNk.mjs";
3
+ 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-jSXp3nu3.mjs";
3
4
  import { n as warn } from "./logger-K7dPmu2U.mjs";
4
5
  import { n as isDuplicateKeyError, r as parseDuplicateKeyError, t as createError } from "./error-BuByZwKU.mjs";
5
- import { r as calculateTotalPages, t as PaginationEngine } from "./PaginationEngine-CSh-P8Nh.mjs";
6
+ import { r as calculateTotalPages, t as PaginationEngine } from "./PaginationEngine-GdyX29tE.mjs";
6
7
  import { hasNearOperator, rewriteNearForCount } from "./query/primitives/geo.mjs";
7
8
  import { RepositoryBase, runChunkedArchive, runChunkedPurge, throwIfAborted, validatePluginOrder, withRetry } from "@classytic/repo-core/repository";
8
9
  import mongoose from "mongoose";
9
10
  import { isFilter } from "@classytic/repo-core/filter";
11
+ import { VersionConflictError, conservativeMongoIsTransientConflict } from "@classytic/repo-core/errors";
10
12
  import { HOOK_PRIORITY } from "@classytic/repo-core/hooks";
11
13
  import { compileUpdateSpecToMongo, isUpdateSpec } from "@classytic/repo-core/update";
12
14
  import { decodeAggCursor, encodeAggCursor, isKeysetMode, normalizeGroupBy, validateMeasures } from "@classytic/repo-core/aggregate";
@@ -48,10 +50,7 @@ function createKeysetPurgePort(repo, baseFilter, session) {
48
50
  bypassTenant: true
49
51
  };
50
52
  let cursor = null;
51
- const selectFilter = () => cursor == null ? baseFilter : {
52
- ...baseFilter,
53
- _id: { $gt: cursor }
54
- };
53
+ const selectFilter = () => keysetFilter(baseFilter, cursor);
55
54
  return { async purgeChunk(strategy, limit) {
56
55
  if (strategy.type === "anonymize") {
57
56
  if (Object.values(strategy.fields).some((v) => typeof v === "function")) {
@@ -60,13 +59,13 @@ function createKeysetPurgePort(repo, baseFilter, session) {
60
59
  return processed;
61
60
  }
62
61
  }
63
- const idDocs = await repo.Model.find(selectFilter(), { _id: 1 }).sort({ _id: 1 }).limit(limit).session(session ?? null).lean().exec();
62
+ const idDocs = await selectKeysetChunk(repo.Model, selectFilter(), limit, {
63
+ session,
64
+ projection: { _id: 1 }
65
+ });
64
66
  if (idDocs.length === 0) return 0;
65
67
  const ids = idDocs.map((d) => d._id);
66
- const chunkFilter = {
67
- ...baseFilter,
68
- _id: { $in: ids }
69
- };
68
+ const chunkFilter = narrowToIds(baseFilter, ids);
70
69
  switch (strategy.type) {
71
70
  case "hard":
72
71
  await repo.deleteMany(chunkFilter, {
@@ -101,7 +100,7 @@ function createKeysetPurgePort(repo, baseFilter, session) {
101
100
  * is the same write), this matches the at-least-once contract.
102
101
  */
103
102
  async function purgeAnonymizeFunctional(repo, selectFilter, fields, limit, session) {
104
- const docs = await repo.Model.find(selectFilter).sort({ _id: 1 }).limit(limit).session(session ?? null).lean().exec();
103
+ const docs = await selectKeysetChunk(repo.Model, selectFilter, limit, { session });
105
104
  if (docs.length === 0) return {
106
105
  processed: 0,
107
106
  lastId: null
@@ -138,10 +137,11 @@ function transactionResolutionOf(caps) {
138
137
  /**
139
138
  * What MongoDB supports through mongokit's surface, DECLARED (not observed).
140
139
  *
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
140
+ * Deployment-dependent flags (`transactions`, `changeStreams` — both need a
141
+ * replica set or mongos) are `true` here because they describe the product,
142
+ * not the server you happen to be pointed at. `nestedTransactions` is NOT one
143
+ * of them: it is `false` unconditionally, because the refusal comes from
144
+ * mongokit's own tx-bound proxy rather than from the deployment. **Do not read this constant to decide whether a live connection can run
145
145
  * a transaction** — that is {@link resolveMongoCapabilities}, which is what
146
146
  * `Repository#capabilities` returns. This one is for the cross-kit conformance
147
147
  * harness (which overrides environment-specific limits where it is
@@ -150,9 +150,11 @@ function transactionResolutionOf(caps) {
150
150
  const MONGOKIT_CAPABILITIES = Object.freeze({
151
151
  transactionsResolution: "declared",
152
152
  transactions: true,
153
- nestedTransactions: true,
153
+ nestedTransactions: false,
154
+ transactionRetry: "managed",
154
155
  upsert: true,
155
156
  duplicateKeyError: true,
157
+ optimisticConcurrency: true,
156
158
  distinct: true,
157
159
  aggregate: true,
158
160
  aggregateOps: Object.freeze({
@@ -251,7 +253,7 @@ function deriveCapabilities(support) {
251
253
  ...MONGOKIT_CAPABILITIES,
252
254
  transactionsResolution: support === "unknown" ? "unknown" : "observed",
253
255
  transactions: available,
254
- nestedTransactions: available,
256
+ nestedTransactions: false,
255
257
  changeStreams: available
256
258
  });
257
259
  }
@@ -830,7 +832,8 @@ function compile(filter) {
830
832
  } };
831
833
  case "regex": return { $regexMatch: {
832
834
  input: `$${filter.field}`,
833
- regex: filter.pattern
835
+ regex: filter.pattern,
836
+ ...filter.flags ? { options: filter.flags } : {}
834
837
  } };
835
838
  case "and": {
836
839
  if (filter.children.length === 0) return true;
@@ -1347,14 +1350,11 @@ function buildKeysetPredicate(sort, cursor) {
1347
1350
  function createMongoArchivePort(repo, filter, session) {
1348
1351
  return {
1349
1352
  async readChunk(limit) {
1350
- return await repo.Model.find(filter).sort({ _id: 1 }).limit(limit).session(session ?? null).lean().exec();
1353
+ return selectKeysetChunk(repo.Model, filter, limit, { session });
1351
1354
  },
1352
1355
  async deleteChunk(docs) {
1353
1356
  const ids = docs.map((doc) => doc._id);
1354
- return (await repo.deleteMany({
1355
- ...filter,
1356
- _id: { $in: ids }
1357
- }, {
1357
+ return (await repo.deleteMany(narrowToIds(filter, ids), {
1358
1358
  session,
1359
1359
  bypassTenant: true,
1360
1360
  mode: "hard"
@@ -2204,6 +2204,33 @@ function assertNoMixedPatchShape(opName, patch) {
2204
2204
  const flatKeys = patchKeys.filter((k) => !k.startsWith("$"));
2205
2205
  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.`);
2206
2206
  }
2207
+ /**
2208
+ * Under `ifVersion`, the CAS owns the version field EXCLUSIVELY.
2209
+ *
2210
+ * The write merges `$inc: { [versionField]: 1 }` into the caller's payload, so
2211
+ * a payload that also touches the version field produces one of two silent
2212
+ * wrongs: Mongo rejects the whole update with a path-conflict error that names
2213
+ * an internal field the caller never wrote (`$set: { __v }`, `$unset`,
2214
+ * `$rename`), or the merge quietly overrides the caller's intent (`$inc: { __v: 5 }`
2215
+ * becomes `1`). Neither is a failure a caller can act on, so the payload is
2216
+ * refused up front and the reason is named.
2217
+ *
2218
+ * `$inc: { [versionField]: 1 }` is permitted: it is exactly what the CAS does
2219
+ * anyway, so a caller declaring it redundantly is stating the truth.
2220
+ */
2221
+ function assertCasOwnsVersionField(opName, patch, versionField) {
2222
+ for (const [op, value] of Object.entries(patch)) {
2223
+ if (!op.startsWith("$")) {
2224
+ if (op === versionField) throw createError(400, `[${opName}] payload sets '${versionField}' directly, but the optimistic-concurrency CAS owns that field — it increments it as part of the same write. Remove it; the expected value belongs in \`options.ifVersion\`, not the payload.`);
2225
+ continue;
2226
+ }
2227
+ if (typeof value !== "object" || value === null) continue;
2228
+ const fields = value;
2229
+ if (!(versionField in fields)) continue;
2230
+ if (op === "$inc" && fields[versionField] === 1) continue;
2231
+ throw createError(400, `[${opName}] payload's ${op} targets '${versionField}', but the optimistic-concurrency CAS owns that field — it increments it as part of the same write. ` + (op === "$inc" ? `Only \`$inc: { ${versionField}: 1 }\` is accepted (it matches what the CAS does); any other increment would be silently overridden. ` : `A conflicting ${op} would either fail the update with a Mongo path conflict or be silently overridden. `) + `Remove it; the expected version belongs in \`options.ifVersion\`.`);
2232
+ }
2233
+ }
2207
2234
  /** Mongo `operationType` → portable `ChangeEvent.operation`. */
2208
2235
  const CHANGE_OPERATION_MAP = {
2209
2236
  insert: "create",
@@ -2280,6 +2307,8 @@ var Repository = class extends RepositoryBase {
2280
2307
  model;
2281
2308
  _pagination;
2282
2309
  idField;
2310
+ /** Version field for `ifVersion` CAS writes. Default '__v'. */
2311
+ versionField;
2283
2312
  searchMode;
2284
2313
  searchFields;
2285
2314
  _hasTextIndex = null;
@@ -2307,6 +2336,7 @@ var Repository = class extends RepositoryBase {
2307
2336
  this.model = Model.modelName;
2308
2337
  this._pagination = new PaginationEngine(Model, paginationConfig);
2309
2338
  this.idField = options.idField ?? "_id";
2339
+ this.versionField = options.versionField ?? "__v";
2310
2340
  this.searchMode = options.searchMode ?? "text";
2311
2341
  this.searchFields = options.searchFields;
2312
2342
  this._wireStrictQueryStripDiagnostic(options.warnOnStrictQueryStrip === true);
@@ -3218,7 +3248,78 @@ var Repository = class extends RepositoryBase {
3218
3248
  }
3219
3249
  }
3220
3250
  let result;
3221
- if (effectiveIdField !== "_id") result = await this._withResilience(context, () => updateByQuery(this.Model, {
3251
+ if (options.ifVersion !== void 0) {
3252
+ /**
3253
+ * Optimistic CAS: the version predicate joins the match filter, so
3254
+ * the check and the write are ONE round-trip — no read-then-write
3255
+ * window. The `$inc` rides the same operation; a versioned write
3256
+ * that did not bump the version would let the NEXT writer pass the
3257
+ * same CAS and silently clobber this one.
3258
+ */
3259
+ const vf = this.versionField;
3260
+ const payload = context.data || data;
3261
+ assertNoMixedPatchShape("update(ifVersion)", payload);
3262
+ assertCasOwnsVersionField("update(ifVersion)", payload, vf);
3263
+ const casUpdate = Object.keys(payload).some((k) => k.startsWith("$")) ? {
3264
+ ...payload,
3265
+ $inc: {
3266
+ ...payload.$inc ?? {},
3267
+ [vf]: 1
3268
+ }
3269
+ } : {
3270
+ $set: payload,
3271
+ $inc: { [vf]: 1 }
3272
+ };
3273
+ /**
3274
+ * `throwOnNotFound` MUST NOT ride into the CAS write. `updateByQuery`
3275
+ * throws its own 404 on ANY filter miss when the flag is set — and a CAS
3276
+ * miss is usually not "not found", it is "found at another version". The
3277
+ * disambiguation below is the contract's authority on which of the two
3278
+ * happened; letting the inner 404 fire first made it unreachable, so a
3279
+ * caller passing `throwOnNotFound: true` got "Document not found" for a
3280
+ * document that exists — a false statement, and one that invites the
3281
+ * blind retry the CAS exists to prevent. The genuinely-gone case still
3282
+ * honours the flag via the shared `!result && wantsThrow` throw below.
3283
+ */
3284
+ result = await this._withResilience(context, () => updateByQuery(
3285
+ this.Model,
3286
+ /**
3287
+ * Injected scope FIRST, caller authority LAST. `context.query`
3288
+ * is hook-supplied (tenant scoping, soft-delete filters) and
3289
+ * must only ever NARROW the match — spread last it could
3290
+ * REPLACE `_id` (retargeting the write) or `__v` (defeating the
3291
+ * CAS that is the entire point of this branch).
3292
+ */
3293
+ {
3294
+ ...context.query || {},
3295
+ [effectiveIdField]: id,
3296
+ [vf]: options.ifVersion
3297
+ },
3298
+ casUpdate,
3299
+ {
3300
+ ...context,
3301
+ throwOnNotFound: false
3302
+ }
3303
+ ));
3304
+ if (!result) {
3305
+ /**
3306
+ * Disambiguate the miss — the contract's teeth. Filter-matched-
3307
+ * nothing is TWO different facts: record gone (stays null, the
3308
+ * MinimalRepo miss contract) or record moved past the expected
3309
+ * version (MUST throw — null would read as not-found and invite
3310
+ * a blind retry that overwrites the concurrent write).
3311
+ */
3312
+ const current = await this.Model.findOne({
3313
+ ...context.query || {},
3314
+ [effectiveIdField]: id
3315
+ }).session(context.session ?? null).lean().exec();
3316
+ if (current) throw new VersionConflictError({
3317
+ expectedVersion: options.ifVersion,
3318
+ ...typeof current[vf] === "number" ? { actualVersion: current[vf] } : {},
3319
+ id: String(id)
3320
+ });
3321
+ }
3322
+ } else if (effectiveIdField !== "_id") result = await this._withResilience(context, () => updateByQuery(this.Model, {
3222
3323
  [effectiveIdField]: id,
3223
3324
  ...context.query || {}
3224
3325
  }, context.data || data, context));
@@ -3471,14 +3572,15 @@ var Repository = class extends RepositoryBase {
3471
3572
  } };
3472
3573
  } else {
3473
3574
  const callerSet = patch.$set ?? {};
3474
- if (isStateNoop) if (Object.keys(callerSet).length === 0) {
3475
- const { $set, ...rest } = patch;
3476
- update = rest;
3575
+ if (isStateNoop) {
3576
+ if (Object.keys(callerSet).length === 0) {
3577
+ const { $set, ...rest } = patch;
3578
+ update = rest;
3579
+ } else update = {
3580
+ ...patch,
3581
+ $set: { ...callerSet }
3582
+ };
3477
3583
  } else update = {
3478
- ...patch,
3479
- $set: { ...callerSet }
3480
- };
3481
- else update = {
3482
3584
  ...patch,
3483
3585
  $set: {
3484
3586
  ...callerSet,
@@ -4310,9 +4412,9 @@ var Repository = class extends RepositoryBase {
4310
4412
  const appendLookupStages$2 = (pipeline) => appendLookupStages(pipeline, lookups, projection, isKeyset && sort ? this._parseSort(sort) : void 0);
4311
4413
  if (isKeyset && sort) {
4312
4414
  const parsedSort = this._parseSort(sort);
4313
- const { validateKeysetSort } = await import("./PaginationEngine-CSh-P8Nh.mjs").then((n) => n.n);
4314
- const { encodeCursor, resolveCursorFilter } = await import("./PaginationEngine-CSh-P8Nh.mjs").then((n) => n.i);
4315
- const { getPrimaryField } = await import("./PaginationEngine-CSh-P8Nh.mjs").then((n) => n.n);
4415
+ const { validateKeysetSort } = await import("./PaginationEngine-GdyX29tE.mjs").then((n) => n.n);
4416
+ const { encodeCursor, resolveCursorFilter } = await import("./PaginationEngine-GdyX29tE.mjs").then((n) => n.i);
4417
+ const { getPrimaryField } = await import("./PaginationEngine-GdyX29tE.mjs").then((n) => n.n);
4316
4418
  const normalizedSort = validateKeysetSort(parsedSort, this._pagination.config.strictKeysetSortFields);
4317
4419
  const cursorVersion = this._pagination.config.cursorVersion ?? 1;
4318
4420
  const minCursorVersion = this._pagination.config.minCursorVersion ?? 1;
@@ -4511,7 +4613,7 @@ var Repository = class extends RepositoryBase {
4511
4613
  */
4512
4614
  async withTransaction(callback, options = {}) {
4513
4615
  return withTransaction(this.Model.db, async (session) => {
4514
- return callback(createTxBoundRepo(this, session));
4616
+ return callback(createTxBoundRepo(this, session), { session });
4515
4617
  }, options);
4516
4618
  }
4517
4619
  /**
@@ -4684,6 +4786,16 @@ var Repository = class extends RepositoryBase {
4684
4786
  return isDuplicateKeyError(err);
4685
4787
  }
4686
4788
  /**
4789
+ * Classify a transactional-write failure as a TRANSIENT concurrency
4790
+ * conflict — one MongoDB designed callers to recover from by re-running
4791
+ * the transaction (`TransientTransactionError` label / WriteConflict 112).
4792
+ * Consumed by repo-core's `retryingTransaction`; conservative on purpose —
4793
+ * never classifies from message text.
4794
+ */
4795
+ isTransientConflictError(err) {
4796
+ return conservativeMongoIsTransientConflict(err);
4797
+ }
4798
+ /**
4687
4799
  * Handle errors with proper HTTP status codes.
4688
4800
  *
4689
4801
  * **Transactional retry signal preservation.** MongoDB driver errors
@@ -4703,6 +4815,7 @@ var Repository = class extends RepositoryBase {
4703
4815
  const labels = error.errorLabels;
4704
4816
  const hasLabel = error.hasErrorLabel;
4705
4817
  if (Array.isArray(labels) && (labels.includes("TransientTransactionError") || labels.includes("UnknownTransactionCommitResult")) || typeof hasLabel === "function" && (hasLabel.call(error, "TransientTransactionError") || hasLabel.call(error, "UnknownTransactionCommitResult"))) return error;
4818
+ if (error instanceof mongoose.mongo.MongoOperationTimeoutError) return error;
4706
4819
  if (error instanceof mongoose.Error.ValidationError) {
4707
4820
  const messages = Object.values(error.errors).map((err) => err.message);
4708
4821
  return createError(400, `Validation Error: ${messages.join(", ")}`);
@@ -1,13 +1,14 @@
1
- import { _ as WithTransactionOptions, a as DeleteManyResult, f as OperationOptions, g as UpdateOptions, h as UpdateManyResult, i as CreateOptions, l as LookupPopulateOptions, m as SessionOptions, o as DeleteResult, p as ReadOptions, r as CacheableOptions, s as FindOneAndUpdateOptions, t as AggregateOptions, u as LookupPopulateResult, v as LookupBuilder, y as LookupOptions } from "./operations-CuzX9m2Z.mjs";
1
+ import { _ as UpdateOptions, a as CreateOptions, b as LookupOptions, c as FindOneAndUpdateOptions, d as LookupPopulateResult, g as UpdateManyResult, h as SessionOptions, m as ReadOptions, o as DeleteManyResult, p as OperationOptions, r as CacheableOptions, s as DeleteResult, t as AggregateOptions, u as LookupPopulateOptions, v as WithTransactionOptions, y as LookupBuilder } from "./operations-DZodvjC-.mjs";
2
2
  import { a as PopulateSpec, i as ObjectId, l as SortSpec$1, o as ReadPreferenceType, s as SelectSpec } from "./core-62oo3N1C.mjs";
3
3
  import { c as PaginationConfig, r as CollationOptions, t as AggregatePaginationOptions } from "./pagination-jSUdrACK.mjs";
4
- import { _ as RepositoryOptions, d as PluginType, f as PrioritizedHook, o as Middleware, p as RepositoryContext } from "./repository-Q16ErTu8.mjs";
4
+ import { _ as RepositoryOptions, d as PluginType, f as PrioritizedHook, o as Middleware, p as RepositoryContext } from "./repository-H_ffGgpE.mjs";
5
5
  import { t as PaginationEngine } from "./PaginationEngine-CBS1XGzp.mjs";
6
6
  import { AggPaginationRequest, AggRequest, AggResult, ArchiveOptions, ArchiveResult, ArchiveSink, ChangeEvent, FilterInput, KeysetAggPaginationResult, QueryOptions, RepoCapabilities, RepositoryBase, TenantPurgeOptions, TenantPurgeResult, TenantPurgeStrategy, WatchOptions } from "@classytic/repo-core/repository";
7
7
  import { ClientSession, Expression, Model, PipelineStage, PopulateOptions } from "mongoose";
8
8
  import { Filter } from "@classytic/repo-core/filter";
9
- import { UpdateInput } from "@classytic/repo-core/update";
9
+ import { RepositoryCacheHandle } from "@classytic/repo-core/cache";
10
10
  import { HttpError } from "@classytic/repo-core/errors";
11
+ import { UpdateInput } from "@classytic/repo-core/update";
11
12
  import { AggregatePaginationResult, KeysetPaginationResult, OffsetPaginationResult, OffsetPaginationResultCore } from "@classytic/repo-core/pagination";
12
13
  //#region src/query/AggregationBuilder.d.ts
13
14
  /** Options for $vectorSearch stage (Atlas only) */
@@ -520,6 +521,16 @@ interface TransitionMachine {
520
521
  assertTransition(entityId: string, from: string, to: string): void;
521
522
  }
522
523
  declare class Repository<TDoc = unknown> extends RepositoryBase {
524
+ /**
525
+ * Cache handle ATTACHED BY the unified cache plugin at wiring time —
526
+ * `undefined` when no cache is configured. Declared (optional) so
527
+ * consumers and this class's own methods read `this.cache` typed instead
528
+ * of probing through `as unknown as { cache?: … }` — mongokit itself
529
+ * carried that erasure in `invalidateAggregateCache`, and every host that
530
+ * wanted the handle copied it. The plugin's `Object.assign` keeps working
531
+ * unchanged; nothing here constructs the handle.
532
+ */
533
+ cache?: RepositoryCacheHandle;
523
534
  /**
524
535
  * Runtime capability descriptor required by `StandardRepo<TDoc>`
525
536
  * (repo-core 0.6.0). Hosts feature-detect once at boot instead of
@@ -556,6 +567,8 @@ declare class Repository<TDoc = unknown> extends RepositoryBase {
556
567
  readonly model: string;
557
568
  readonly _pagination: PaginationEngine<TDoc>;
558
569
  readonly idField: string;
570
+ /** Version field for `ifVersion` CAS writes. Default '__v'. */
571
+ readonly versionField: string;
559
572
  readonly searchMode: 'text' | 'regex' | 'auto';
560
573
  readonly searchFields: string[] | undefined;
561
574
  [key: string]: unknown;
@@ -1747,7 +1760,9 @@ declare class Repository<TDoc = unknown> extends RepositoryBase {
1747
1760
  * });
1748
1761
  * ```
1749
1762
  */
1750
- withTransaction<T>(callback: (txRepo: this) => Promise<T>, options?: WithTransactionOptions): Promise<T>;
1763
+ withTransaction<T>(callback: (txRepo: this, uow?: {
1764
+ session?: ClientSession;
1765
+ }) => Promise<T>, options?: WithTransactionOptions): Promise<T>;
1751
1766
  /**
1752
1767
  * Execute custom query with event emission
1753
1768
  */
@@ -1827,6 +1842,14 @@ declare class Repository<TDoc = unknown> extends RepositoryBase {
1827
1842
  * }
1828
1843
  */
1829
1844
  isDuplicateKeyError(err: unknown): boolean;
1845
+ /**
1846
+ * Classify a transactional-write failure as a TRANSIENT concurrency
1847
+ * conflict — one MongoDB designed callers to recover from by re-running
1848
+ * the transaction (`TransientTransactionError` label / WriteConflict 112).
1849
+ * Consumed by repo-core's `retryingTransaction`; conservative on purpose —
1850
+ * never classifies from message text.
1851
+ */
1852
+ isTransientConflictError(err: unknown): boolean;
1830
1853
  /**
1831
1854
  * Handle errors with proper HTTP status codes.
1832
1855
  *
@@ -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-Beyr1r4d.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-DKEql4cL.mjs";
2
2
  export { aggregate_d_exports as aggregate, create_d_exports as create, delete_d_exports as deleteActions, read_d_exports as read, update_d_exports as update };
@@ -1,5 +1,5 @@
1
1
  import { t as __exportAll } from "../rolldown-runtime-D7D4PA-g.mjs";
2
- import { g as create_exports, i as update_exports, p as delete_exports, u as read_exports, v as aggregate_exports } from "../update-w5i-pa_c.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-jSXp3nu3.mjs";
3
3
  //#region src/actions/index.ts
4
4
  var actions_exports = /* @__PURE__ */ __exportAll({
5
5
  aggregate: () => aggregate_exports,
@@ -1,4 +1,4 @@
1
- import { l as Plugin } from "../repository-Q16ErTu8.mjs";
1
+ import { l as Plugin } from "../repository-H_ffGgpE.mjs";
2
2
  import { PipelineStage } from "mongoose";
3
3
  //#region src/ai/types.d.ts
4
4
  /** Supported similarity metrics for vector search */
@@ -49,9 +49,32 @@ interface BAFieldAttribute {
49
49
  };
50
50
  defaultValue?: unknown;
51
51
  }
52
+ /**
53
+ * How Better Auth's `_id` values are stored, so mongoose casts queries the same way.
54
+ *
55
+ * - `'objectid'` (DEFAULT) — BA's mongo adapter generating its own ids writes
56
+ * real ObjectIds. Mongoose's default `_id` SchemaType casts a hex string to
57
+ * ObjectId, which is what makes `findById`, `_id` filters and `populate`
58
+ * resolve.
59
+ * - `'string'` — the host overrode `advanced.database.generateId` (commonly to
60
+ * `randomBytes(12).toString('hex')`, which AVOIDS a cross-driver ObjectId
61
+ * `instanceof` failure). BA then stores those ids as STRINGS.
62
+ *
63
+ * Getting this wrong fails in the quiet direction, in BOTH directions: an
64
+ * ObjectId-typed schema over string ids casts every lookup to an ObjectId that
65
+ * matches nothing, so `getById` 404s and `populate` yields null — no error, no
66
+ * log. Measured on a real deployment: `GET /me`, `GET /users/:id` and
67
+ * `GET /branches/code/:code` all 404'd for rows that existed.
68
+ *
69
+ * There is no safe default that covers both, which is why this is an explicit
70
+ * option rather than a guess: read `typeof doc._id` from a live row.
71
+ */
72
+ type BetterAuthIdType = 'objectid' | 'string';
52
73
  interface RegisterBetterAuthStubsOptions {
53
74
  /** Plugin sets to include. `core` is always implied. */
54
75
  plugins?: BetterAuthPluginKey$1[];
76
+ /** How BA stores `_id` — see {@link BetterAuthIdType}. Default `'objectid'`. */
77
+ idType?: BetterAuthIdType;
55
78
  /** Additional collection names beyond the plugin set. */
56
79
  extraCollections?: string[];
57
80
  /** Mirror BA's `usePlural` flag — appends `s` to every collection name. */
@@ -115,6 +138,8 @@ interface BetterAuthOverlayOptions<TDoc = Record<string, unknown>> {
115
138
  * Mongoose-side concerns (validators, indexes, defaults).
116
139
  */
117
140
  additionalFields?: Record<string, unknown>;
141
+ /** How BA stores `_id` — see {@link BetterAuthIdType}. Default `'objectid'`. */
142
+ idType?: BetterAuthIdType;
118
143
  /**
119
144
  * Mongoose schema indexes to attach to the overlay model.
120
145
  *
@@ -156,6 +181,25 @@ interface BetterAuthOverlayOptions<TDoc = Record<string, unknown>> {
156
181
  * `@classytic/mongokit` to wire OpenAPI auto-gen.
157
182
  */
158
183
  schemaGenerator?: any;
184
+ /**
185
+ * Return a WRITABLE repository. Default `false` — the overlay is a
186
+ * read-side projection, and its repository is sealed
187
+ * (`asReadOnlyRepo`): writes throw, and `capabilities.readOnly` lets a
188
+ * host refuse write ROUTES at boot.
189
+ *
190
+ * Better Auth owns writes to these collections and enforces invariants
191
+ * the documents cannot: password hashing, session revocation, org
192
+ * membership cascades, and every plugin hook (passkey, SSO, ...). A
193
+ * generic `POST /users` through this repository bypasses all of it and
194
+ * writes a row Better Auth never saw. That is one route-config line away
195
+ * whenever the repository is writable, so the seal is the default rather
196
+ * than a docstring.
197
+ *
198
+ * Set `true` ONLY for administrative repair paths where you have
199
+ * accepted that responsibility. Normal identity mutations go through
200
+ * `auth.api`.
201
+ */
202
+ unsafeWritable?: boolean;
159
203
  }
160
204
  /**
161
205
  * Create a `DataAdapter<TDoc>` over a Better Auth collection.
@@ -181,6 +225,40 @@ declare function createBetterAuthOverlay<TDoc = Record<string, unknown>>(options
181
225
  * `Model` — so the host passes whichever it has, and mongokit keeps mongoose +
182
226
  * the mongodb driver as peer deps with zero runtime import.
183
227
  */
228
+ /**
229
+ * Ensure the TTL index that reaps expired Better Auth sessions.
230
+ *
231
+ * BA stamps `expiresAt` on every session (tunable via `session.expiresIn`,
232
+ * default 7 days) but its mongo adapter creates NO index for it, and nothing
233
+ * else sweeps the collection — so `session` grows without bound on the hottest
234
+ * auth table. Measured on two live deployments before this existed: 568 rows
235
+ * (233 expired) and 909 rows of which EVERY ONE was expired, the oldest five
236
+ * months old.
237
+ *
238
+ * It lives here rather than in a host or a spine module because this kit is
239
+ * what knows Better Auth's collection names — a host-side fix only protects
240
+ * that host, and every other consumer keeps leaking.
241
+ *
242
+ * `expireAfterSeconds: 0` means "delete once `expiresAt` has passed", so
243
+ * mongo's background task keeps the collection bounded with no application
244
+ * code. Idempotent — re-creating an existing index is a no-op.
245
+ *
246
+ * NOT called automatically by {@link registerBetterAuthStubs}: that function is
247
+ * synchronous and writes nothing to the database, and silently issuing DDL from
248
+ * a model-registration helper would be a surprise. Call this once at boot.
249
+ *
250
+ * @returns `true` when an index was created or already present, `false` when
251
+ * the attempt failed (logged by the caller, never thrown — a missing
252
+ * TTL degrades storage, and refusing to boot over it is worse).
253
+ */
254
+ declare function ensureBetterAuthSessionTtl(connection: {
255
+ collection(name: string): {
256
+ createIndex(keys: Record<string, 1>, opts: Record<string, unknown>): Promise<unknown>;
257
+ };
258
+ }, options?: {
259
+ collection?: string;
260
+ indexName?: string;
261
+ }): Promise<boolean>;
184
262
  interface SessionUpdaterLike {
185
263
  updateMany(filter: Record<string, unknown>, update: Record<string, unknown>): Promise<{
186
264
  modifiedCount?: number;
@@ -215,4 +293,4 @@ declare function clearActiveOrganizationFromSessions(sessions: SessionUpdaterLik
215
293
  userId?: unknown;
216
294
  }): Promise<number>;
217
295
  //#endregion
218
- export { BetterAuthInstance, BetterAuthOverlayOptions, type BetterAuthPluginKey, MongooseLike, RegisterBetterAuthStubsOptions, SessionUpdaterLike, clearActiveOrganizationFromSessions, createBetterAuthOverlay, registerBetterAuthStubs };
296
+ export { BetterAuthIdType, BetterAuthInstance, BetterAuthOverlayOptions, type BetterAuthPluginKey, MongooseLike, RegisterBetterAuthStubsOptions, SessionUpdaterLike, clearActiveOrganizationFromSessions, createBetterAuthOverlay, ensureBetterAuthSessionTtl, registerBetterAuthStubs };