@classytic/mongokit 3.32.0 → 3.33.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 +19 -0
- package/dist/better-auth/index.mjs +5 -3
- 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
|
@@ -156,6 +156,25 @@ interface BetterAuthOverlayOptions<TDoc = Record<string, unknown>> {
|
|
|
156
156
|
* `@classytic/mongokit` to wire OpenAPI auto-gen.
|
|
157
157
|
*/
|
|
158
158
|
schemaGenerator?: any;
|
|
159
|
+
/**
|
|
160
|
+
* Return a WRITABLE repository. Default `false` — the overlay is a
|
|
161
|
+
* read-side projection, and its repository is sealed
|
|
162
|
+
* (`asReadOnlyRepo`): writes throw, and `capabilities.readOnly` lets a
|
|
163
|
+
* host refuse write ROUTES at boot.
|
|
164
|
+
*
|
|
165
|
+
* Better Auth owns writes to these collections and enforces invariants
|
|
166
|
+
* the documents cannot: password hashing, session revocation, org
|
|
167
|
+
* membership cascades, and every plugin hook (passkey, SSO, ...). A
|
|
168
|
+
* generic `POST /users` through this repository bypasses all of it and
|
|
169
|
+
* writes a row Better Auth never saw. That is one route-config line away
|
|
170
|
+
* whenever the repository is writable, so the seal is the default rather
|
|
171
|
+
* than a docstring.
|
|
172
|
+
*
|
|
173
|
+
* Set `true` ONLY for administrative repair paths where you have
|
|
174
|
+
* accepted that responsibility. Normal identity mutations go through
|
|
175
|
+
* `auth.api`.
|
|
176
|
+
*/
|
|
177
|
+
unsafeWritable?: boolean;
|
|
159
178
|
}
|
|
160
179
|
/**
|
|
161
180
|
* Create a `DataAdapter<TDoc>` over a Better Auth collection.
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { n as Repository } from "../Repository-
|
|
1
|
+
import { n as Repository } from "../Repository-BArvU32x.mjs";
|
|
2
2
|
import { n as createMongooseAdapter } from "../adapter-BitHg-CN.mjs";
|
|
3
|
+
import { asReadOnlyRepo } from "@classytic/repo-core/repository";
|
|
3
4
|
import { pluralizeBetterAuthCollection, resolveBetterAuthCollections } from "@classytic/repo-core/better-auth";
|
|
4
5
|
//#region src/better-auth/index.ts
|
|
5
6
|
/**
|
|
@@ -95,7 +96,7 @@ function registerBetterAuthStubs(mongoose, options = {}) {
|
|
|
95
96
|
* would silently drop this call's `additionalFields` / `indexes`).
|
|
96
97
|
*/
|
|
97
98
|
async function createBetterAuthOverlay(options) {
|
|
98
|
-
const { auth, mongoose, collection, additionalFields = {}, indexes = [], usePlural = false, RepositoryClass, schemaGenerator } = options;
|
|
99
|
+
const { auth, mongoose, collection, additionalFields = {}, indexes = [], usePlural = false, RepositoryClass, schemaGenerator, unsafeWritable = false } = options;
|
|
99
100
|
const ctx = await auth.$context;
|
|
100
101
|
const tableConfig = ctx.tables[collection];
|
|
101
102
|
if (!tableConfig) throw new Error(`[mongokit:better-auth] Better Auth has no table named '${collection}'. Available: ${Object.keys(ctx.tables).join(", ")}. Did you enable the right plugin in your betterAuth() config?`);
|
|
@@ -142,7 +143,8 @@ async function createBetterAuthOverlay(options) {
|
|
|
142
143
|
for (const idx of indexes) schema.index(idx.fields, idx.options);
|
|
143
144
|
Model = mongoose.model(finalName, schema);
|
|
144
145
|
}
|
|
145
|
-
const
|
|
146
|
+
const built = new (RepositoryClass ?? Repository)(Model);
|
|
147
|
+
const repository = unsafeWritable ? built : asReadOnlyRepo(built, { reason: `Better Auth owns writes to '${finalName}' (hashing, session revocation, org cascades, plugin hooks) — mutate via auth.api, not generic CRUD. Pass \`unsafeWritable: true\` to this overlay if raw administrative writes are required` });
|
|
146
148
|
return createMongooseAdapter({
|
|
147
149
|
model: Model,
|
|
148
150
|
repository,
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { c as PaginationConfig } from "./pagination-jSUdrACK.mjs";
|
|
2
|
-
import { _ as RepositoryOptions, d as PluginType } from "./repository-
|
|
3
|
-
import { T as AuditTrailOptions, _ as RepositoryCachePluginOptions, i as CustomIdOptions, t as MultiTenantOptions } from "./multi-tenant.plugin-
|
|
4
|
-
import { n as Repository } from "./Repository-
|
|
5
|
-
import { i as Logger, o as SoftDeleteOptions } from "./plugin-options-
|
|
2
|
+
import { _ as RepositoryOptions, d as PluginType } from "./repository-H_ffGgpE.mjs";
|
|
3
|
+
import { T as AuditTrailOptions, _ as RepositoryCachePluginOptions, i as CustomIdOptions, t as MultiTenantOptions } from "./multi-tenant.plugin-BwSvTirB.mjs";
|
|
4
|
+
import { n as Repository } from "./Repository-BdSkQ8vc.mjs";
|
|
5
|
+
import { i as Logger, o as SoftDeleteOptions } from "./plugin-options-DXnlKM3B.mjs";
|
|
6
6
|
import { Model } from "mongoose";
|
|
7
7
|
//#region src/create-repository.d.ts
|
|
8
8
|
/**
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { n as Repository } from "./Repository-
|
|
2
|
-
import { a as customIdPlugin, f as auditTrailPlugin, i as multiTenantPlugin, m as auditLogPlugin, n as softDeletePlugin, t as timestampPlugin, u as cachePlugin } from "./timestamp.plugin-
|
|
1
|
+
import { n as Repository } from "./Repository-BArvU32x.mjs";
|
|
2
|
+
import { a as customIdPlugin, f as auditTrailPlugin, i as multiTenantPlugin, m as auditLogPlugin, n as softDeletePlugin, t as timestampPlugin, u as cachePlugin } from "./timestamp.plugin-BEc6TANm.mjs";
|
|
3
3
|
import { n as batchOperationsPlugin, t as methodRegistryPlugin } from "./method-registry.plugin-DblM7V6j.mjs";
|
|
4
4
|
//#region src/create-repository.ts
|
|
5
5
|
function isLoggerAudit(value) {
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
//#region src/utils/id-chunks.ts
|
|
2
|
+
/**
|
|
3
|
+
* Default slice for `$in` fan-out. Larger than `runChunkedPurge`'s 1000 on purpose:
|
|
4
|
+
* a purge chunk bounds the WRITE LOCK SCOPE of each progressive strategy step, while a
|
|
5
|
+
* snapshot slice bounds only the query-document size and the per-call working set —
|
|
6
|
+
* 10k ObjectIds is ~120 KB of `$in`, far under every limit, at 10× fewer round trips.
|
|
7
|
+
*/
|
|
8
|
+
const DEFAULT_ID_CHUNK = 1e4;
|
|
9
|
+
/**
|
|
10
|
+
* Collect the bare id values matching `filter`, streaming via a driver cursor so the
|
|
11
|
+
* only O(n) allocation is the returned array itself (a `find().lean()` materializes an
|
|
12
|
+
* intermediate array of `{_id}` documents first — roughly double the peak for large n).
|
|
13
|
+
*/
|
|
14
|
+
async function collectIds(model, filter, options = {}) {
|
|
15
|
+
const idField = options.idField ?? "_id";
|
|
16
|
+
const ids = [];
|
|
17
|
+
const cursor = model.find(filter, { [idField]: 1 }).session(options.session ?? null).lean().cursor();
|
|
18
|
+
for await (const doc of cursor) ids.push(doc[idField]);
|
|
19
|
+
return ids;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Yield `ids` in consecutive slices of at most `size`. Yields nothing for an empty
|
|
23
|
+
* array. `size` must be a positive integer — sliced `$in` correctness depends on it,
|
|
24
|
+
* so a nonsense size throws instead of silently degrading to one giant slice.
|
|
25
|
+
*/
|
|
26
|
+
function* idChunks(ids, size) {
|
|
27
|
+
if (!Number.isInteger(size) || size < 1) throw new Error(`idChunks: size must be a positive integer, got ${String(size)}`);
|
|
28
|
+
for (let i = 0; i < ids.length; i += size) yield ids.slice(i, i + size);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Compose the keyset-progressed select filter: the base predicate, narrowed to rows past
|
|
32
|
+
* the last committed `_id`. `cursor == null` means "from the start". Selection paired
|
|
33
|
+
* with this must sort `_id`-ascending ({@link selectKeysetChunk} does), so `$gt: cursor`
|
|
34
|
+
* never revisits a processed row and never skips an unprocessed one. Never mutates
|
|
35
|
+
* `baseFilter`.
|
|
36
|
+
*/
|
|
37
|
+
function keysetFilter(baseFilter, cursor) {
|
|
38
|
+
return cursor == null ? baseFilter : {
|
|
39
|
+
...baseFilter,
|
|
40
|
+
_id: { $gt: cursor }
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* The `_id`-ascending bounded chunk select both action ports share:
|
|
45
|
+
* `find(filter).sort({_id: 1}).limit(limit).lean()`. Lean on purpose — chunk consumers
|
|
46
|
+
* want plain objects, not hydrated documents. Pass `projection: {_id: 1}` when only ids
|
|
47
|
+
* are needed (the purge port's common path); omit it for full docs (archive's readChunk,
|
|
48
|
+
* anonymize-functional).
|
|
49
|
+
*/
|
|
50
|
+
async function selectKeysetChunk(model, filter, limit, options = {}) {
|
|
51
|
+
return await model.find(filter, options.projection).sort({ _id: 1 }).limit(limit).session(options.session ?? null).lean().exec();
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Re-assert the base predicate on an id-narrowed WRITE: `{...base, _id: {$in: ids}}`.
|
|
55
|
+
* The `$in` alone would be enough to address the chunk, but carrying the base predicate
|
|
56
|
+
* defends against a row whose scope fields changed between the chunk select and the
|
|
57
|
+
* write — such a row is silently skipped instead of written outside its cohort. Both
|
|
58
|
+
* action ports use this shape; use it for any select-then-write over a chunk.
|
|
59
|
+
*/
|
|
60
|
+
function narrowToIds(baseFilter, ids) {
|
|
61
|
+
return {
|
|
62
|
+
...baseFilter,
|
|
63
|
+
_id: { $in: ids }
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
//#endregion
|
|
67
|
+
export { narrowToIds as a, keysetFilter as i, collectIds as n, selectKeysetChunk as o, idChunks as r, DEFAULT_ID_CHUNK as t };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { _ as UpdateOptions, a as CreateOptions, b as LookupOptions, c as FindOneAndUpdateOptions, f as MinMaxResult, g as UpdateManyResult, l as GroupResult, p as OperationOptions, s as DeleteResult } from "./operations-DZodvjC-.mjs";
|
|
2
2
|
import { a as PopulateSpec, i as ObjectId, l as SortSpec, o as ReadPreferenceType, s as SelectSpec, t as AnyDocument } from "./core-62oo3N1C.mjs";
|
|
3
3
|
import { p as UpdateWithValidationResult } from "./type-utils-CLzRFtIA.mjs";
|
|
4
4
|
import { Model, PipelineStage } from "mongoose";
|