@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.
- package/dist/{PaginationEngine-CSh-P8Nh.mjs → PaginationEngine-GdyX29tE.mjs} +13 -11
- package/dist/{Repository-B_1y9cCf.mjs → Repository-BArvU32x.mjs} +149 -36
- package/dist/{Repository-BAzymuOz.d.mts → Repository-BdSkQ8vc.d.mts} +27 -4
- package/dist/actions/index.d.mts +1 -1
- package/dist/actions/index.mjs +1 -1
- package/dist/ai/index.d.mts +1 -1
- package/dist/better-auth/index.d.mts +79 -1
- package/dist/better-auth/index.mjs +59 -6
- package/dist/{create-repository-mEdfH9sz.d.mts → create-repository-BU4x9Kb0.d.mts} +4 -4
- package/dist/{create-repository-a3DQ5tzt.mjs → create-repository-CPNbktFG.mjs} +2 -2
- package/dist/id-chunks-BmAwsqNk.mjs +67 -0
- package/dist/{index-Beyr1r4d.d.mts → index-DKEql4cL.d.mts} +1 -1
- package/dist/index.d.mts +15 -14
- package/dist/index.mjs +60 -54
- package/dist/{multi-tenant.plugin-2VtSSm1Y.d.mts → multi-tenant.plugin-BwSvTirB.d.mts} +3 -3
- package/dist/{operations-CuzX9m2Z.d.mts → operations-DZodvjC-.d.mts} +22 -3
- package/dist/outbox/index.d.mts +1 -1
- package/dist/outbox/index.mjs +1 -1
- package/dist/pagination/PaginationEngine.mjs +1 -1
- package/dist/{plugin-options-BsFH0sTi.d.mts → plugin-options-DXnlKM3B.d.mts} +31 -1
- package/dist/plugins/index.d.mts +3 -3
- package/dist/plugins/index.mjs +2 -2
- package/dist/{repository-Q16ErTu8.d.mts → repository-H_ffGgpE.d.mts} +8 -0
- package/dist/testkit/index.d.mts +2 -2
- package/dist/testkit/index.mjs +1 -1
- package/dist/{timestamp.plugin-CAXzHs9y.mjs → timestamp.plugin-BEc6TANm.mjs} +7 -5
- package/dist/{to-plain-BuX1RUZV.d.mts → to-plain-Dyz_qvuo.d.mts} +3 -3
- package/dist/{update-w5i-pa_c.mjs → update-jSXp3nu3.mjs} +48 -28
- package/dist/utils/index.d.mts +54 -2
- package/dist/utils/index.mjs +2 -1
- package/dist/{validation-chain.plugin-BihbMQRq.mjs → validation-chain.plugin-CyK1s9Ze.mjs} +219 -20
- package/dist/{validation-chain.plugin-BSshLJTA.d.mts → validation-chain.plugin-yQRob3Av.d.mts} +2 -2
- 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)
|
|
39
|
-
|
|
40
|
-
|
|
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: { $
|
|
43
|
-
}
|
|
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
|
|
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-
|
|
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 = () =>
|
|
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
|
|
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
|
|
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`, `
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
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:
|
|
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:
|
|
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
|
|
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 (
|
|
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)
|
|
3475
|
-
|
|
3476
|
-
|
|
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-
|
|
4314
|
-
const { encodeCursor, resolveCursorFilter } = await import("./PaginationEngine-
|
|
4315
|
-
const { getPrimaryField } = await import("./PaginationEngine-
|
|
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
|
|
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-
|
|
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 {
|
|
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
|
|
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
|
*
|
package/dist/actions/index.d.mts
CHANGED
|
@@ -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-
|
|
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 };
|
package/dist/actions/index.mjs
CHANGED
|
@@ -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-
|
|
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,
|
package/dist/ai/index.d.mts
CHANGED
|
@@ -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 };
|