@classytic/mongokit 3.43.1 → 3.45.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/README.md +18 -3
- package/dist/{PaginationEngine-DZnkbSrr.mjs → PaginationEngine-BRlLTQQz.mjs} +13 -27
- package/dist/{PaginationEngine-CRHyuUtw.d.mts → PaginationEngine-BWb-c47a.d.mts} +1 -3
- package/dist/{Repository-DnL4xk5w.d.mts → Repository-DJuRmBNx.d.mts} +44 -13
- package/dist/{Repository-Dj6xqPxJ.mjs → Repository-DheA7oW-.mjs} +330 -112
- package/dist/actions/index.d.mts +1 -1
- package/dist/actions/index.mjs +2 -2
- package/dist/{actions-ChYfkPYP.mjs → actions-DKQx9ToJ.mjs} +3 -4
- package/dist/adapter/index.d.mts +6 -10
- package/dist/adapter/index.mjs +1 -1
- package/dist/{adapter-CkMoMF83.mjs → adapter-Qzgc3ZD_.mjs} +4 -6
- package/dist/ai/index.d.mts +21 -8
- package/dist/ai/index.mjs +15 -9
- package/dist/better-auth/index.d.mts +21 -20
- package/dist/better-auth/index.mjs +12 -23
- package/dist/{coercion-BjdPX3YF.d.mts → coercion-COp9DRNw.d.mts} +0 -2
- package/dist/{create-repository-UjwPWR1z.mjs → create-repository-C9DHHcl7.mjs} +4 -5
- package/dist/{create-repository-CJtVdwig.d.mts → create-repository-DG-gwOf0.d.mts} +5 -7
- package/dist/{derive-DRBiegRH.mjs → derive-Dxsc31fV.mjs} +3 -6
- package/dist/{error-BuByZwKU.mjs → error-DjdmPLWO.mjs} +1 -2
- package/dist/{field-selection-FSQFLpsU.mjs → field-selection-DPFaFw9Y.mjs} +1 -2
- package/dist/{id-chunks-BmAwsqNk.mjs → id-chunks-DH-RAZWG.mjs} +13 -8
- package/dist/{id-resolution-BqDrk0S2.mjs → id-resolution-Cqh49d9J.mjs} +1 -2
- package/dist/idempotency/index.d.mts +4 -7
- package/dist/idempotency/index.mjs +1 -2
- package/dist/{index-SYF8N28D.d.mts → index-5mhQ63_B.d.mts} +3 -4
- package/dist/{index-C0AjVqX3.d.mts → index-ehGZLbrd.d.mts} +2 -8
- package/dist/index.d.mts +70 -102
- package/dist/index.mjs +36 -52
- package/dist/indexes/verify-indexes.d.mts +8 -11
- package/dist/indexes/verify-indexes.mjs +1 -2
- package/dist/{indexes-DIjMpYES.d.mts → indexes-FfyOuhDK.d.mts} +0 -2
- package/dist/{join-scope-DSAznE8E.mjs → join-scope-C1gOtZ59.mjs} +2 -3
- package/dist/kernel-conformance/index.d.mts +39 -13
- package/dist/kernel-conformance/index.mjs +4 -7
- package/dist/lock/index.d.mts +134 -7
- package/dist/lock/index.mjs +349 -14
- package/dist/{logger-K7dPmu2U.mjs → logger-hbWOlj_X.mjs} +1 -2
- package/dist/{method-registry.plugin-C63pGn1d.mjs → method-registry.plugin-BeJfQBJV.mjs} +5 -7
- package/dist/{model-registry-ClKtWiHa.mjs → model-registry-DrVfJ3Nt.mjs} +1 -2
- package/dist/{mongooseToJsonSchema-C3k4ide0.mjs → mongooseToJsonSchema-DMDotz4_.mjs} +2 -3
- package/dist/{multi-tenant.plugin-B_q9NUHc.d.mts → multi-tenant.plugin-BTPtCLNK.d.mts} +143 -11
- package/dist/{operations-XiFgG5US.d.mts → operations-D6XsHx4e.d.mts} +19 -11
- package/dist/outbox/index.d.mts +24 -9
- package/dist/outbox/index.mjs +17 -6
- package/dist/pagination/PaginationEngine.d.mts +1 -1
- package/dist/pagination/PaginationEngine.mjs +1 -1
- package/dist/pagination/utils/limits.d.mts +6 -9
- package/dist/pagination/utils/limits.mjs +2 -3
- package/dist/{pagination-D6psses0.d.mts → pagination-BAM9cA4N.d.mts} +6 -16
- package/dist/{plugin-options-Cirmb4Kp.d.mts → plugin-options-DrNGWV5b.d.mts} +2 -4
- package/dist/plugins/index.d.mts +6 -7
- package/dist/plugins/index.mjs +6 -7
- package/dist/query/primitives/coercion.d.mts +1 -1
- package/dist/query/primitives/coercion.mjs +2 -3
- package/dist/query/primitives/geo.d.mts +11 -14
- package/dist/query/primitives/geo.mjs +1 -2
- package/dist/query/primitives/indexes.d.mts +1 -1
- package/dist/query/primitives/indexes.mjs +1 -2
- package/dist/{query-defaults-BxUcSOoF.mjs → query-defaults-DwvWUjZU.mjs} +18 -54
- package/dist/read-model/index.d.mts +18 -21
- package/dist/read-model/index.mjs +3 -4
- package/dist/{repository-MqAP409e.d.mts → repository-B0aFVgP2.d.mts} +25 -29
- package/dist/retention-purge-CCd_VXUf.d.mts +38 -0
- package/dist/{rolldown-runtime-D7D4PA-g.mjs → rolldown-runtime-D8jQpDG7.mjs} +1 -2
- package/dist/search/index.d.mts +1 -1
- package/dist/search/index.mjs +1 -1
- package/dist/sync/index.d.mts +113 -17
- package/dist/sync/index.mjs +421 -65
- package/dist/tenant/index.d.mts +8 -11
- package/dist/tenant/index.mjs +1 -2
- package/dist/testkit/index.d.mts +179 -23
- package/dist/testkit/index.mjs +421 -23
- package/dist/{timestamp.plugin-DpOWavVI.mjs → timestamp.plugin-B-IJrlYb.mjs} +192 -49
- package/dist/{to-plain-vksRrLtc.mjs → to-plain-CEKv_ylg.mjs} +2 -4
- package/dist/{to-plain-o5tUY2nV.d.mts → to-plain-Do-8jTKC.d.mts} +2 -16
- package/dist/{transaction-DmiB4k2P.mjs → transaction-C6Oj941P.mjs} +18 -19
- package/dist/{type-utils-CLzRFtIA.d.mts → type-utils-CKvuewAL.d.mts} +0 -2
- package/dist/{update-WgeAUJ_I.mjs → update-CYaJMu6j.mjs} +13 -23
- package/dist/usage/index.d.mts +2 -5
- package/dist/usage/index.mjs +8 -6
- package/dist/utils/index.d.mts +8 -10
- package/dist/utils/index.mjs +7 -7
- package/dist/{validation-chain.plugin-BAeC5eVT.d.mts → validation-chain.plugin-COYrUdd8.d.mts} +150 -48
- package/dist/{validation-chain.plugin-DRApTmkN.mjs → validation-chain.plugin-DJplVJ12.mjs} +210 -55
- package/llms.txt +33 -0
- package/package.json +29 -25
- package/skills/mongokit/SKILL.md +112 -0
- package/skills/mongokit/references/pagination-search.md +87 -0
- package/skills/mongokit/references/plugins-tenancy.md +103 -0
- package/skills/mongokit/references/repository-api.md +83 -0
- package/skills/mongokit/references/transactions-outbox.md +103 -0
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
import { i as isValidIdForType, t as getSchemaIdType } from "./id-resolution-
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { n as
|
|
5
|
-
import { n as
|
|
6
|
-
import {
|
|
7
|
-
import { o as withTransaction, p as resolveMongoCapabilities, s as createTxBoundRepo } from "./transaction-
|
|
8
|
-
import { _ as resolveCursorSecrets, d as withGroupTiebreak, f as withIdTiebreak, g as attachSignature, i as validateKeysetSort, l as assertOffsetWithinCap, n as getPrimaryField, o as encodeCursor, s as resolveCursorFilter, t as PaginationEngine, u as cursorScope, v as verifySignature } from "./PaginationEngine-
|
|
1
|
+
import { i as isValidIdForType, t as getSchemaIdType } from "./id-resolution-Cqh49d9J.mjs";
|
|
2
|
+
import { n as isDuplicateKeyError, r as parseDuplicateKeyError, t as createError } from "./error-DjdmPLWO.mjs";
|
|
3
|
+
import { a as narrowToIds, i as keysetFilter, o as selectKeysetChunk, r as idChunks, t as DEFAULT_ID_CHUNK } from "./id-chunks-DH-RAZWG.mjs";
|
|
4
|
+
import { _ as SHORTHAND_OPS, a as count, b as create, c as getByQuery, d as deleteById, f as deleteByQuery, g as LookupBuilder, h as distinct, l as getOrCreate, n as update, o as exists, r as updateByQuery, s as getById, t as findOneAndUpdate, v as compileFilterToMongo, x as createMany, y as castFilterToSchema } from "./update-CYaJMu6j.mjs";
|
|
5
|
+
import { n as warn } from "./logger-hbWOlj_X.mjs";
|
|
6
|
+
import { l as resolveQueryOptions, n as applyToAggregate, r as applyToQuery } from "./query-defaults-DwvWUjZU.mjs";
|
|
7
|
+
import { o as withTransaction, p as resolveMongoCapabilities, s as createTxBoundRepo } from "./transaction-C6Oj941P.mjs";
|
|
8
|
+
import { _ as resolveCursorSecrets, d as withGroupTiebreak, f as withIdTiebreak, g as attachSignature, i as validateKeysetSort, l as assertOffsetWithinCap, n as getPrimaryField, o as encodeCursor, s as resolveCursorFilter, t as PaginationEngine, u as cursorScope, v as verifySignature } from "./PaginationEngine-BRlLTQQz.mjs";
|
|
9
9
|
import { calculateTotalPages } from "./pagination/utils/limits.mjs";
|
|
10
|
-
import { a as insertValues, c as writeKeys, d as buildSearchKeys, f as prefixSearchFilter, i as fieldValuesOf, n as SearchKeysWriteError, o as keysFromValues, r as analyzeUpdate, s as unresolvedForUpdate, t as SEARCH_KEYS_ERROR_CODES } from "./derive-
|
|
10
|
+
import { a as insertValues, c as writeKeys, d as buildSearchKeys, f as prefixSearchFilter, i as fieldValuesOf, n as SearchKeysWriteError, o as keysFromValues, r as analyzeUpdate, s as unresolvedForUpdate, t as SEARCH_KEYS_ERROR_CODES } from "./derive-Dxsc31fV.mjs";
|
|
11
11
|
import { hasNearOperator, rewriteNearForCount } from "./query/primitives/geo.mjs";
|
|
12
|
-
import { a as scopeJoins, r as markGoverned } from "./join-scope-
|
|
12
|
+
import { a as scopeJoins, r as markGoverned } from "./join-scope-C1gOtZ59.mjs";
|
|
13
13
|
import { BULK_UPSERT_ERROR_CODES, BULK_UPSERT_FAILURE_CODES, RepositoryBase, bulkUpsertOptionsError, createDistributionGuard, runChunkedArchive, runChunkedPurge, summarizeBulkUpsert, throwIfAborted, validatePluginOrder, withRetry } from "@classytic/repo-core/repository";
|
|
14
14
|
import mongoose from "mongoose";
|
|
15
15
|
import { isFilter } from "@classytic/repo-core/filter";
|
|
@@ -17,13 +17,135 @@ import { VersionConflictError, conservativeMongoIsTransientConflict } from "@cla
|
|
|
17
17
|
import { HOOK_PRIORITY } from "@classytic/repo-core/hooks";
|
|
18
18
|
import { compileUpdateSpecToMongo, isUpdateSpec } from "@classytic/repo-core/update";
|
|
19
19
|
import { decodeAggCursor, encodeAggCursor, isKeysetMode, normalizeGroupBy, validateMeasures } from "@classytic/repo-core/aggregate";
|
|
20
|
-
|
|
20
|
+
// src/plugins/retention-purge.ts
|
|
21
|
+
/** Error codes thrown for a malformed or unauthorized retention purge. */
|
|
22
|
+
const RETENTION_PURGE_ERROR_CODES = {
|
|
23
|
+
DISABLED: "RETENTION_PURGE_DISABLED",
|
|
24
|
+
REASON_REQUIRED: "RETENTION_PURGE_REASON_REQUIRED",
|
|
25
|
+
INVALID_ACTOR: "RETENTION_PURGE_INVALID_ACTOR",
|
|
26
|
+
HARD_ONLY: "RETENTION_PURGE_HARD_ONLY",
|
|
27
|
+
ARCHIVE_UNATTESTED: "RETENTION_PURGE_ARCHIVE_UNATTESTED"
|
|
28
|
+
};
|
|
29
|
+
/** Attested id sets, keyed by an object only `attestArchivedChunk` can mint. */
|
|
30
|
+
const ATTESTED_CHUNKS = /* @__PURE__ */ new WeakMap();
|
|
31
|
+
/**
|
|
32
|
+
* Mint the attestation for one archive chunk the sink has ACKNOWLEDGED. Internal to
|
|
33
|
+
* mongokit (not exported from the package entry): the archive port is its only caller.
|
|
34
|
+
*/
|
|
35
|
+
function attestArchivedChunk(ids) {
|
|
36
|
+
const token = Object.freeze({});
|
|
37
|
+
ATTESTED_CHUNKS.set(token, new Set(ids.map(String)));
|
|
38
|
+
return token;
|
|
39
|
+
}
|
|
40
|
+
/** Every `_id: { $in: [...] }` list in the query, through nested `$and`. */
|
|
41
|
+
function idListsIn(query) {
|
|
42
|
+
if (!query || typeof query !== "object") return [];
|
|
43
|
+
const q = query;
|
|
44
|
+
const lists = [];
|
|
45
|
+
const idPredicate = q._id;
|
|
46
|
+
if (idPredicate && typeof idPredicate === "object" && Array.isArray(idPredicate.$in)) lists.push(idPredicate.$in);
|
|
47
|
+
if (Array.isArray(q.$and)) for (const arm of q.$and) lists.push(...idListsIn(arm));
|
|
48
|
+
return lists;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Whether the context carries an attestation, and whether it covers the delete. A delete
|
|
52
|
+
* is covered only when some `_id.$in` it ANDs in is non-empty and wholly attested — an
|
|
53
|
+
* extra predicate can only narrow it further.
|
|
54
|
+
*/
|
|
55
|
+
function archiveAttestationOf(ctx) {
|
|
56
|
+
const token = ctx.archiveAttestation;
|
|
57
|
+
if (token === void 0) return "none";
|
|
58
|
+
const attested = typeof token === "object" && token ? ATTESTED_CHUNKS.get(token) : void 0;
|
|
59
|
+
if (!attested) return "invalid";
|
|
60
|
+
const query = ctx.query;
|
|
61
|
+
return idListsIn(query).some((ids) => ids.length > 0 && ids.every((id) => attested.has(String(id)))) ? "valid" : "invalid";
|
|
62
|
+
}
|
|
63
|
+
/** Validates the request's shape (reason, actor). Throws 400 on a malformed one. */
|
|
64
|
+
function assertRetentionPurgeShape(raw, op) {
|
|
65
|
+
const req = raw;
|
|
66
|
+
if (!req || typeof req !== "object" || typeof req.reason !== "string" || !req.reason.trim()) throw createError(400, `'${op}' with retentionPurge needs a non-blank reason.`, {
|
|
67
|
+
code: RETENTION_PURGE_ERROR_CODES.REASON_REQUIRED,
|
|
68
|
+
meta: { operation: op }
|
|
69
|
+
});
|
|
70
|
+
if (req.actor !== void 0 && (typeof req.actor !== "string" || !req.actor.trim())) throw createError(400, `'${op}' with retentionPurge: actor must be a non-blank string.`, {
|
|
71
|
+
code: RETENTION_PURGE_ERROR_CODES.INVALID_ACTOR,
|
|
72
|
+
meta: { operation: op }
|
|
73
|
+
});
|
|
74
|
+
return {
|
|
75
|
+
reason: req.reason,
|
|
76
|
+
actor: req.actor
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* The validated purge request on a delete context, or `undefined` for an
|
|
81
|
+
* ordinary delete. Refuses every malformed or unauthorized form rather than
|
|
82
|
+
* falling back to the fence: a purge that silently became a refused delete
|
|
83
|
+
* would hide the misconfiguration from the job.
|
|
84
|
+
*/
|
|
85
|
+
function readRetentionPurge(ctx, op, guard) {
|
|
86
|
+
const raw = ctx.retentionPurge;
|
|
87
|
+
if (raw === void 0 || raw === null || raw === false) return void 0;
|
|
88
|
+
const attestation = archiveAttestationOf(ctx);
|
|
89
|
+
if (attestation === "invalid") throw createError(403, `'${op}' carries an archive attestation that does not cover the rows it deletes: an archive delete may remove only the chunk its sink acknowledged.`, {
|
|
90
|
+
code: RETENTION_PURGE_ERROR_CODES.ARCHIVE_UNATTESTED,
|
|
91
|
+
meta: {
|
|
92
|
+
operation: op,
|
|
93
|
+
plugin: guard.plugin
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
if (!guard.allowPurge && !(guard.allowArchive && attestation === "valid")) throw createError(403, `'${op}' with retentionPurge is not enabled on this repository: construct ${guard.plugin} with allowPurge: true for the retention path` + (attestation === "valid" ? ", or allowArchive: true for archiveByFilter." : "."), {
|
|
97
|
+
code: RETENTION_PURGE_ERROR_CODES.DISABLED,
|
|
98
|
+
meta: {
|
|
99
|
+
operation: op,
|
|
100
|
+
plugin: guard.plugin
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
const req = assertRetentionPurgeShape(raw, op);
|
|
104
|
+
if (ctx.deleteMode !== "hard") throw createError(400, `'${op}' with retentionPurge must be a physical delete: pass mode: 'hard'.`, {
|
|
105
|
+
code: RETENTION_PURGE_ERROR_CODES.HARD_ONLY,
|
|
106
|
+
meta: { operation: op }
|
|
107
|
+
});
|
|
108
|
+
return req;
|
|
109
|
+
}
|
|
110
|
+
/** Builds the common audit payload from an `after:delete` / `after:deleteMany` emission. */
|
|
111
|
+
function retentionPurgeEvent(operation, context, result) {
|
|
112
|
+
const req = context.retentionPurge;
|
|
113
|
+
if (!req || typeof req !== "object") return void 0;
|
|
114
|
+
const ctx = context;
|
|
115
|
+
return {
|
|
116
|
+
operation,
|
|
117
|
+
reason: req.reason,
|
|
118
|
+
actor: req.actor,
|
|
119
|
+
id: operation === "delete" ? ctx.id : void 0,
|
|
120
|
+
filter: operation === "deleteMany" ? ctx.query : void 0,
|
|
121
|
+
deletedCount: operation === "delete" ? result ? 1 : 0 : result?.deletedCount ?? 0,
|
|
122
|
+
archived: archiveAttestationOf(context) === "valid",
|
|
123
|
+
session: context.session,
|
|
124
|
+
context
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
// src/actions/purge.ts
|
|
128
|
+
/**
|
|
129
|
+
* Validates the run's `retentionPurge` up front (before any chunk is
|
|
130
|
+
* selected) and returns the port options. A purge request is a physical
|
|
131
|
+
* delete only: with a `soft` / `anonymize` strategy it is refused.
|
|
132
|
+
*/
|
|
133
|
+
function purgePortOptions(op, strategy, options) {
|
|
134
|
+
const raw = options.retentionPurge;
|
|
135
|
+
if (raw === void 0 || raw === null || raw === false) return {};
|
|
136
|
+
const retentionPurge = assertRetentionPurgeShape(raw, op);
|
|
137
|
+
if (strategy.type !== "hard") throw createError(400, `'${op}' with retentionPurge must use the 'hard' strategy (got '${strategy.type}').`, {
|
|
138
|
+
code: RETENTION_PURGE_ERROR_CODES.HARD_ONLY,
|
|
139
|
+
meta: { operation: op }
|
|
140
|
+
});
|
|
141
|
+
return { retentionPurge };
|
|
142
|
+
}
|
|
21
143
|
/**
|
|
22
144
|
* Build a `PurgePort` bound to a repository + the `field = value`
|
|
23
145
|
* predicate the purge targets.
|
|
24
146
|
*/
|
|
25
|
-
function createMongoPurgePort(repo, field, value, session) {
|
|
26
|
-
return createKeysetPurgePort(repo, { [field]: value }, session);
|
|
147
|
+
function createMongoPurgePort(repo, field, value, session, portOptions = {}) {
|
|
148
|
+
return createKeysetPurgePort(repo, { [field]: value }, session, portOptions);
|
|
27
149
|
}
|
|
28
150
|
/**
|
|
29
151
|
* Build a `PurgePort` bound to a repository + a pre-compiled Mongo filter
|
|
@@ -39,27 +161,32 @@ function createMongoPurgePort(repo, field, value, session) {
|
|
|
39
161
|
* uses, so a row that leaves the matching set between the id select and the
|
|
40
162
|
* write is never touched.
|
|
41
163
|
*
|
|
42
|
-
* Mirrors `
|
|
164
|
+
* Mirrors `createMongoArchiveRun`, which likewise binds a pre-compiled
|
|
43
165
|
* filter rather than a `field/value` pair.
|
|
44
166
|
*/
|
|
45
|
-
function createMongoPurgePortFromFilter(repo, filter, session) {
|
|
46
|
-
return createKeysetPurgePort(repo, filter, session);
|
|
167
|
+
function createMongoPurgePortFromFilter(repo, filter, session, portOptions = {}) {
|
|
168
|
+
return createKeysetPurgePort(repo, filter, session, portOptions);
|
|
47
169
|
}
|
|
48
170
|
/**
|
|
49
171
|
* Shared keyset-progressed port both public factories delegate to. The
|
|
50
172
|
* closure's `cursor` is the port's single-run progression state.
|
|
51
173
|
*/
|
|
52
|
-
function createKeysetPurgePort(repo, baseFilter, session) {
|
|
174
|
+
function createKeysetPurgePort(repo, baseFilter, session, portOptions) {
|
|
53
175
|
const baseOpts = {
|
|
54
176
|
session,
|
|
55
177
|
bypassTenant: true
|
|
56
178
|
};
|
|
179
|
+
const hardOpts = {
|
|
180
|
+
...baseOpts,
|
|
181
|
+
mode: "hard",
|
|
182
|
+
...portOptions.retentionPurge ? { retentionPurge: portOptions.retentionPurge } : {}
|
|
183
|
+
};
|
|
57
184
|
let cursor = null;
|
|
58
185
|
const selectFilter = () => keysetFilter(baseFilter, cursor);
|
|
59
186
|
return { async purgeChunk(strategy, limit) {
|
|
60
187
|
if (strategy.type === "anonymize") {
|
|
61
188
|
if (Object.values(strategy.fields).some((v) => typeof v === "function")) {
|
|
62
|
-
const { processed, lastId } = await purgeAnonymizeFunctional(repo, selectFilter(), strategy.fields, limit, session);
|
|
189
|
+
const { processed, lastId } = await purgeAnonymizeFunctional(repo, selectFilter(), baseFilter, strategy.fields, limit, session, baseOpts);
|
|
63
190
|
if (lastId != null) cursor = lastId;
|
|
64
191
|
return processed;
|
|
65
192
|
}
|
|
@@ -73,10 +200,7 @@ function createKeysetPurgePort(repo, baseFilter, session) {
|
|
|
73
200
|
const chunkFilter = narrowToIds(baseFilter, ids);
|
|
74
201
|
switch (strategy.type) {
|
|
75
202
|
case "hard":
|
|
76
|
-
await repo.deleteMany(chunkFilter,
|
|
77
|
-
...baseOpts,
|
|
78
|
-
mode: "hard"
|
|
79
|
-
});
|
|
203
|
+
await repo.deleteMany(chunkFilter, hardOpts);
|
|
80
204
|
break;
|
|
81
205
|
case "soft":
|
|
82
206
|
await repo.updateMany(chunkFilter, { $set: {
|
|
@@ -92,47 +216,68 @@ function createKeysetPurgePort(repo, baseFilter, session) {
|
|
|
92
216
|
}
|
|
93
217
|
/**
|
|
94
218
|
* Anonymize with at least one function-form field replacer. Fetches the
|
|
95
|
-
* full docs (function needs the row)
|
|
96
|
-
*
|
|
97
|
-
*
|
|
219
|
+
* full docs (function needs the row), computes each row's values, and
|
|
220
|
+
* writes the chunk with ONE `repo.updateMany` whose pipeline picks each
|
|
221
|
+
* row's value by its `_id`. Two round-trips per chunk regardless of N.
|
|
222
|
+
*
|
|
223
|
+
* Through the repository, never a raw `Model.bulkWrite`: the write meets
|
|
224
|
+
* the same `before:updateMany` fences as the static form —
|
|
225
|
+
* `immutableStatesPlugin` refuses a chunk holding a frozen row,
|
|
226
|
+
* `appendOnlyPlugin` refuses the edit, tenant / soft-delete scope apply —
|
|
227
|
+
* and `after:updateMany` reaches the change log, cache and audit plugins.
|
|
98
228
|
*
|
|
99
229
|
* Selection is `_id`-ascending against the caller's keyset-narrowed
|
|
100
230
|
* filter; returns the chunk's last `_id` so the port can advance its
|
|
101
|
-
* cursor after the write commits.
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
* is the same write), this matches the at-least-once contract.
|
|
231
|
+
* cursor after the write commits. Values are cast by the field's schema
|
|
232
|
+
* type where it is a scalar (a pipeline update is not cast by mongoose);
|
|
233
|
+
* `undefined` is written as `null`. Idempotent per row, so the
|
|
234
|
+
* at-least-once contract holds.
|
|
106
235
|
*/
|
|
107
|
-
async function purgeAnonymizeFunctional(repo, selectFilter, fields, limit, session) {
|
|
236
|
+
async function purgeAnonymizeFunctional(repo, selectFilter, baseFilter, fields, limit, session, writeOpts) {
|
|
108
237
|
const docs = await selectKeysetChunk(repo.Model, selectFilter, limit, { session });
|
|
109
238
|
if (docs.length === 0) return {
|
|
110
239
|
processed: 0,
|
|
111
240
|
lastId: null
|
|
112
241
|
};
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
242
|
+
const ids = docs.map((doc) => doc._id);
|
|
243
|
+
const set = {};
|
|
244
|
+
for (const [k, v] of Object.entries(fields)) {
|
|
245
|
+
const values = docs.map((doc) => castScalar(repo.Model, k, typeof v === "function" ? v(doc) : v));
|
|
246
|
+
set[k] = { $let: {
|
|
247
|
+
vars: { i: { $indexOfArray: [{ $literal: ids }, "$_id"] } },
|
|
248
|
+
in: { $arrayElemAt: [{ $literal: values }, "$$i"] }
|
|
119
249
|
} };
|
|
120
|
-
}
|
|
121
|
-
await repo.
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
});
|
|
125
|
-
await repo.emitAsync("after:anonymize", {
|
|
126
|
-
context: { ...session ? { session } : {} },
|
|
127
|
-
result: { ids: docs.map((doc) => doc._id) }
|
|
250
|
+
}
|
|
251
|
+
await repo.updateMany(narrowToIds(baseFilter, ids), [{ $set: set }], {
|
|
252
|
+
...writeOpts,
|
|
253
|
+
updatePipeline: true
|
|
128
254
|
});
|
|
129
255
|
return {
|
|
130
256
|
processed: docs.length,
|
|
131
257
|
lastId: docs[docs.length - 1]?._id ?? null
|
|
132
258
|
};
|
|
133
259
|
}
|
|
134
|
-
|
|
135
|
-
|
|
260
|
+
/** Scalar schema types a pipeline write must be cast to by hand (mongoose casts `$set`, not pipelines). */
|
|
261
|
+
const SCALAR_INSTANCES = /* @__PURE__ */ new Set([
|
|
262
|
+
"String",
|
|
263
|
+
"Number",
|
|
264
|
+
"Date",
|
|
265
|
+
"Boolean",
|
|
266
|
+
"ObjectId",
|
|
267
|
+
"ObjectID",
|
|
268
|
+
"Decimal128",
|
|
269
|
+
"UUID",
|
|
270
|
+
"BigInt",
|
|
271
|
+
"Double",
|
|
272
|
+
"Int32"
|
|
273
|
+
]);
|
|
274
|
+
function castScalar(model, path, value) {
|
|
275
|
+
if (value === void 0 || value === null) return null;
|
|
276
|
+
const type = model.schema.path(path);
|
|
277
|
+
if (!type?.instance || !SCALAR_INSTANCES.has(type.instance) || !type.cast) return value;
|
|
278
|
+
return type.cast(value);
|
|
279
|
+
}
|
|
280
|
+
// src/operations.ts
|
|
136
281
|
/**
|
|
137
282
|
* The registry. Order intentionally groups by category for readability.
|
|
138
283
|
* Adding a new RepositoryOperation requires exactly one entry here, after
|
|
@@ -295,8 +440,7 @@ const READ_OPERATIONS = ALL_OPERATIONS.filter((op) => !OP_REGISTRY[op].mutates);
|
|
|
295
440
|
function operationsByPolicyKey(key) {
|
|
296
441
|
return ALL_OPERATIONS.filter((op) => OP_REGISTRY[op].policyKey === key);
|
|
297
442
|
}
|
|
298
|
-
|
|
299
|
-
//#region src/actions/aggregate-ir/hints.ts
|
|
443
|
+
// src/actions/aggregate-ir/hints.ts
|
|
300
444
|
function applyExecutionHints(aggregation, hints) {
|
|
301
445
|
if (!hints) return;
|
|
302
446
|
if (hints.allowDiskUse) aggregation.allowDiskUse(true);
|
|
@@ -328,8 +472,7 @@ function assertExpectedIndex(model, leadingKeys) {
|
|
|
328
472
|
if (!keys.some((key) => startsWith(key, 0) || startsWith(key, 1))) throw new Error(`[mongokit] ${model.modelName}: an aggregation expects an index leading with (${id}), but none is declared — declared: ${keys.map((k) => `(${k.join(",")})`).join(" ") || "none"}. Declare one, or drop the hint.`);
|
|
329
473
|
checked.set(model, (done ?? /* @__PURE__ */ new Set()).add(id));
|
|
330
474
|
}
|
|
331
|
-
|
|
332
|
-
//#region src/actions/aggregate-ir/dateBucket.ts
|
|
475
|
+
// src/actions/aggregate-ir/dateBucket.ts
|
|
333
476
|
/**
|
|
334
477
|
* Compile a `AggDateBucket` to the mongo expression that produces the
|
|
335
478
|
* bucket label string for a single document. Used in two places:
|
|
@@ -458,8 +601,7 @@ function formatForUnit(unit) {
|
|
|
458
601
|
case "year": return "%Y";
|
|
459
602
|
}
|
|
460
603
|
}
|
|
461
|
-
|
|
462
|
-
//#region src/filter/compile-expr.ts
|
|
604
|
+
// src/filter/compile-expr.ts
|
|
463
605
|
/**
|
|
464
606
|
* Compile a Filter IR node (or already-built expression) to a MongoDB
|
|
465
607
|
* aggregation expression. Returns the literal `true` for empty `and`
|
|
@@ -773,8 +915,7 @@ function likeToRegexPattern(pattern) {
|
|
|
773
915
|
function escapeRegexChar(ch) {
|
|
774
916
|
return /[.*+?^${}()|[\]\\]/.test(ch) ? `\\${ch}` : ch;
|
|
775
917
|
}
|
|
776
|
-
|
|
777
|
-
//#region src/actions/aggregate-ir/measure.ts
|
|
918
|
+
// src/actions/aggregate-ir/measure.ts
|
|
778
919
|
function compileMeasure(measure) {
|
|
779
920
|
const whereExpr = measure.where !== void 0 ? compileFilterToMongoExpr(measure.where) : void 0;
|
|
780
921
|
switch (measure.op) {
|
|
@@ -843,8 +984,7 @@ function compileMeasure(measure) {
|
|
|
843
984
|
};
|
|
844
985
|
}
|
|
845
986
|
}
|
|
846
|
-
|
|
847
|
-
//#region src/actions/aggregate-ir/normalize.ts
|
|
987
|
+
// src/actions/aggregate-ir/normalize.ts
|
|
848
988
|
/**
|
|
849
989
|
* AggRequest → input normalization (mongokit binding).
|
|
850
990
|
*
|
|
@@ -857,8 +997,7 @@ function compileMeasure(measure) {
|
|
|
857
997
|
function validateMeasures$1(measures) {
|
|
858
998
|
validateMeasures(measures, "mongokit");
|
|
859
999
|
}
|
|
860
|
-
|
|
861
|
-
//#region src/actions/aggregate-ir/pipeline.ts
|
|
1000
|
+
// src/actions/aggregate-ir/pipeline.ts
|
|
862
1001
|
function buildAggPipeline(req, schema) {
|
|
863
1002
|
validateMeasures$1(req.measures);
|
|
864
1003
|
const groupCols = normalizeGroupBy(req.groupBy);
|
|
@@ -1251,8 +1390,7 @@ function irReferencesAlias(filter, aliasSet) {
|
|
|
1251
1390
|
if (node.child) return irReferencesAlias(node.child, aliasSet);
|
|
1252
1391
|
return false;
|
|
1253
1392
|
}
|
|
1254
|
-
|
|
1255
|
-
//#region src/actions/aggregate-ir/execute.ts
|
|
1393
|
+
// src/actions/aggregate-ir/execute.ts
|
|
1256
1394
|
/** Build the aggregate for `stages` with every execution option applied, unexecuted. */
|
|
1257
1395
|
function prepareAgg(Model, stages, req, options = {}) {
|
|
1258
1396
|
const aggregation = Model.aggregate(options.finalize ? options.finalize(stages) : stages);
|
|
@@ -1271,8 +1409,7 @@ async function executeAgg(Model, req, options = {}) {
|
|
|
1271
1409
|
const { pipeline } = buildAggPipeline(req, Model.schema);
|
|
1272
1410
|
return runAgg(Model, pipeline, req, options);
|
|
1273
1411
|
}
|
|
1274
|
-
|
|
1275
|
-
//#region src/actions/aggregate-ir/count.ts
|
|
1412
|
+
// src/actions/aggregate-ir/count.ts
|
|
1276
1413
|
async function countAggGroups(Model, req, options = {}) {
|
|
1277
1414
|
const groupCols = normalizeGroupBy(req.groupBy);
|
|
1278
1415
|
const hasDottedGroupBy = groupCols.some((f) => f.includes("."));
|
|
@@ -1302,8 +1439,7 @@ async function countAggGroups(Model, req, options = {}) {
|
|
|
1302
1439
|
const [row] = await runAgg(Model, pipeline, req, options);
|
|
1303
1440
|
return row?.n ?? 0;
|
|
1304
1441
|
}
|
|
1305
|
-
|
|
1306
|
-
//#region src/actions/aggregate-ir/keyset.ts
|
|
1442
|
+
// src/actions/aggregate-ir/keyset.ts
|
|
1307
1443
|
/**
|
|
1308
1444
|
* Keyset (cursor) pagination helpers — mongokit binding.
|
|
1309
1445
|
*
|
|
@@ -1366,29 +1502,83 @@ function buildKeysetPredicate(sort, cursor) {
|
|
|
1366
1502
|
}
|
|
1367
1503
|
return { $match: branches.length === 1 ? branches[0] : { $or: branches } };
|
|
1368
1504
|
}
|
|
1369
|
-
|
|
1370
|
-
|
|
1505
|
+
// src/actions/archive.ts
|
|
1506
|
+
const idOf = (doc) => doc._id;
|
|
1507
|
+
const valueAt = (doc, path) => path.split(".").reduce((acc, key) => acc?.[key], doc);
|
|
1371
1508
|
/**
|
|
1372
|
-
*
|
|
1373
|
-
*
|
|
1509
|
+
* The fields that prove a row is unchanged since export: the `updatedAt` mongoose
|
|
1510
|
+
* timestamps maintain on every write path, plus the repository's `versionField` when
|
|
1511
|
+
* the schema has it. Empty only for a collection declared `immutable`. A collection with
|
|
1512
|
+
* no maintained `updatedAt` is refused before the run reads anything.
|
|
1374
1513
|
*/
|
|
1375
|
-
function
|
|
1514
|
+
function archiveChangeTokens(model, versionField, immutable) {
|
|
1515
|
+
if (immutable) return [];
|
|
1516
|
+
const ts = model.timestamps;
|
|
1517
|
+
const updatedAt = ts === true ? "updatedAt" : ts && typeof ts === "object" && ts.updatedAt !== false ? typeof ts.updatedAt === "string" ? ts.updatedAt : "updatedAt" : void 0;
|
|
1518
|
+
if (!updatedAt) throw createError(400, `archiveByFilter on '${model.modelName}': the schema maintains no updatedAt (timestamps), so a row edited between export and delete could not be told apart and would be deleted with its edit. Enable timestamps, or pass immutable: true for a collection whose rows never change.`, {
|
|
1519
|
+
code: "ARCHIVE_NO_CHANGE_TOKEN",
|
|
1520
|
+
meta: { model: model.modelName }
|
|
1521
|
+
});
|
|
1522
|
+
return model.hasVersionPath ? [updatedAt, versionField] : [updatedAt];
|
|
1523
|
+
}
|
|
1524
|
+
/** Each exported row pinned to its change tokens: an edited row no longer matches. */
|
|
1525
|
+
function unchangedSince(docs, tokens) {
|
|
1526
|
+
return { $or: docs.map((doc) => {
|
|
1527
|
+
const pin = { _id: idOf(doc) };
|
|
1528
|
+
for (const token of tokens) {
|
|
1529
|
+
const value = valueAt(doc, token);
|
|
1530
|
+
pin[token] = value === void 0 ? { $exists: false } : value;
|
|
1531
|
+
}
|
|
1532
|
+
return pin;
|
|
1533
|
+
}) };
|
|
1534
|
+
}
|
|
1535
|
+
/**
|
|
1536
|
+
* Build the sink + port pair for ONE `archiveByFilter` call. The returned sink
|
|
1537
|
+
* forwards to the caller's and records what it acknowledged; the port's delete
|
|
1538
|
+
* removes only acknowledged rows still unchanged since export, and throws on any other.
|
|
1539
|
+
* Progression is a keyset over `_id`, so a row left live is not revisited by this run.
|
|
1540
|
+
*/
|
|
1541
|
+
function createMongoArchiveRun(repo, filter, sink, session, retentionPurge, immutable) {
|
|
1542
|
+
const tokens = archiveChangeTokens({
|
|
1543
|
+
modelName: repo.Model.modelName,
|
|
1544
|
+
timestamps: repo.Model.schema.get("timestamps"),
|
|
1545
|
+
hasVersionPath: Boolean(repo.Model.schema.path(repo.versionField))
|
|
1546
|
+
}, repo.versionField, immutable);
|
|
1547
|
+
let acknowledged = /* @__PURE__ */ new Set();
|
|
1548
|
+
let cursor = null;
|
|
1376
1549
|
return {
|
|
1377
|
-
|
|
1378
|
-
|
|
1550
|
+
sink: {
|
|
1551
|
+
async write(docs) {
|
|
1552
|
+
await sink.write(docs);
|
|
1553
|
+
acknowledged = new Set(docs.map((doc) => String(idOf(doc))));
|
|
1554
|
+
},
|
|
1555
|
+
...sink.flush ? { flush: () => sink.flush?.() ?? Promise.resolve() } : {}
|
|
1379
1556
|
},
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1557
|
+
port: {
|
|
1558
|
+
async readChunk(limit) {
|
|
1559
|
+
return selectKeysetChunk(repo.Model, keysetFilter(filter, cursor), limit, { session });
|
|
1560
|
+
},
|
|
1561
|
+
async deleteChunk(docs) {
|
|
1562
|
+
const ids = docs.map(idOf);
|
|
1563
|
+
const unacknowledged = ids.filter((id) => !acknowledged.has(String(id)));
|
|
1564
|
+
if (unacknowledged.length > 0) throw new Error(`archiveByFilter: refusing to delete ${unacknowledged.length} row(s) the sink did not acknowledge`);
|
|
1565
|
+
const narrowed = narrowToIds(filter, ids);
|
|
1566
|
+
const result = await repo.deleteMany(tokens.length > 0 ? { $and: [narrowed, unchangedSince(docs, tokens)] } : narrowed, {
|
|
1567
|
+
session,
|
|
1568
|
+
bypassTenant: true,
|
|
1569
|
+
mode: "hard",
|
|
1570
|
+
...retentionPurge ? {
|
|
1571
|
+
retentionPurge,
|
|
1572
|
+
archiveAttestation: attestArchivedChunk(ids)
|
|
1573
|
+
} : {}
|
|
1574
|
+
});
|
|
1575
|
+
if (ids.length > 0) cursor = ids[ids.length - 1];
|
|
1576
|
+
return result.deletedCount;
|
|
1577
|
+
}
|
|
1387
1578
|
}
|
|
1388
1579
|
};
|
|
1389
1580
|
}
|
|
1390
|
-
|
|
1391
|
-
//#region src/actions/bulk-upsert.ts
|
|
1581
|
+
// src/actions/bulk-upsert.ts
|
|
1392
1582
|
/**
|
|
1393
1583
|
* `bulkUpsert` execution: keyed `updateOne` + `upsert` sub-ops in ONE `client.bulkWrite` with
|
|
1394
1584
|
* `verboseResults`, the only form that reports matched / modified / upserted PER OP (server 8.0,
|
|
@@ -1529,7 +1719,7 @@ async function executeUpserts(model, ops, options) {
|
|
|
1529
1719
|
verboseResults: true,
|
|
1530
1720
|
...options.session ? { session: options.session } : {},
|
|
1531
1721
|
...options.writeConcern ? { writeConcern: options.writeConcern } : {},
|
|
1532
|
-
...options.
|
|
1722
|
+
...options.maxTimeMS !== void 0 ? { maxTimeMS: options.maxTimeMS } : {}
|
|
1533
1723
|
});
|
|
1534
1724
|
} catch (err) {
|
|
1535
1725
|
throwIfUnsupported(err);
|
|
@@ -1569,8 +1759,7 @@ async function executeUpserts(model, ops, options) {
|
|
|
1569
1759
|
});
|
|
1570
1760
|
return results;
|
|
1571
1761
|
}
|
|
1572
|
-
|
|
1573
|
-
//#region src/plugins/search-keys.plugin.ts
|
|
1762
|
+
// src/plugins/search-keys.plugin.ts
|
|
1574
1763
|
/**
|
|
1575
1764
|
* Search-keys plugin: keeps the `searchMode: 'prefix'` keys field in step with
|
|
1576
1765
|
* its source fields on EVERY write path, inside the same write.
|
|
@@ -1680,8 +1869,7 @@ function searchKeysPlugin(options) {
|
|
|
1680
1869
|
}
|
|
1681
1870
|
};
|
|
1682
1871
|
}
|
|
1683
|
-
|
|
1684
|
-
//#region src/query/AggregationBuilder.ts
|
|
1872
|
+
// src/query/AggregationBuilder.ts
|
|
1685
1873
|
/**
|
|
1686
1874
|
* Normalize SortSpec to MongoDB's strict format (1 | -1)
|
|
1687
1875
|
* Converts 'asc' -> 1, 'desc' -> -1
|
|
@@ -2212,8 +2400,7 @@ var AggregationBuilder = class AggregationBuilder {
|
|
|
2212
2400
|
* .exec(Model)
|
|
2213
2401
|
* ```
|
|
2214
2402
|
*/
|
|
2215
|
-
|
|
2216
|
-
//#region src/repository/lookup-populate.ts
|
|
2403
|
+
// src/repository/lookup-populate.ts
|
|
2217
2404
|
/**
|
|
2218
2405
|
* Inclusion projections must carry the keyset sort fields (plus `_id`) or
|
|
2219
2406
|
* cursor encoding has no values to serialize. No-op for exclusion
|
|
@@ -2265,8 +2452,16 @@ function appendLookupStages(pipeline, lookups, projection, keysetSort) {
|
|
|
2265
2452
|
const finalProjection = ensureLookupProjectionIncludesCursorFields(projection, keysetSort);
|
|
2266
2453
|
if (finalProjection) pipeline.push({ $project: finalProjection });
|
|
2267
2454
|
}
|
|
2268
|
-
|
|
2269
|
-
|
|
2455
|
+
// src/Repository.ts
|
|
2456
|
+
/**
|
|
2457
|
+
* A CAS guard (`claim` / `claimVersion` `where`) as Mongo: Filter IR compiled, a plain
|
|
2458
|
+
* record (Mongo operators) as is. Spreading an IR node raw made the match require
|
|
2459
|
+
* `doc.op === 'eq'` — no document matched, and the miss read as a lost race.
|
|
2460
|
+
*/
|
|
2461
|
+
function casGuard(where) {
|
|
2462
|
+
if (where === void 0) return {};
|
|
2463
|
+
return isFilter(where) ? compileFilterToMongo(where) : where;
|
|
2464
|
+
}
|
|
2270
2465
|
/**
|
|
2271
2466
|
* Validates that an update patch is homogeneous — either all top-level
|
|
2272
2467
|
* keys are `$`-prefixed Mongo operators OR none are. Mixed shapes (e.g.
|
|
@@ -2990,7 +3185,7 @@ var Repository = class extends RepositoryBase {
|
|
|
2990
3185
|
}
|
|
2991
3186
|
/**
|
|
2992
3187
|
* Resumable keyset batches over `_id`: each batch is ONE bounded `find` (no long-lived server
|
|
2993
|
-
* cursor, so no cursor timeout and
|
|
3188
|
+
* cursor, so no cursor timeout and a bound on every batch), memory is one batch, and every
|
|
2994
3189
|
* batch carries a `checkpoint`. Pass a checkpoint back as `after` to resume: rows written
|
|
2995
3190
|
* behind it are not revisited, rows ahead of it are reached. A checkpoint is bound to the
|
|
2996
3191
|
* collection, the post-policy filter (tenant included) and is refused elsewhere.
|
|
@@ -3069,7 +3264,6 @@ var Repository = class extends RepositoryBase {
|
|
|
3069
3264
|
if (options.session) query.session(options.session);
|
|
3070
3265
|
const cursorOpts = this._opts("read", context);
|
|
3071
3266
|
applyToQuery(query, cursorOpts);
|
|
3072
|
-
applyCursorTimeoutMode(query, cursorOpts, this.Model.db);
|
|
3073
3267
|
const stream = query.cursor();
|
|
3074
3268
|
let yieldedCount = 0;
|
|
3075
3269
|
try {
|
|
@@ -3885,7 +4079,7 @@ var Repository = class extends RepositoryBase {
|
|
|
3885
4079
|
const effectiveIdField = options.idField ?? this.idField;
|
|
3886
4080
|
const fromSpec = Array.isArray(transition.from) ? { $in: transition.from } : transition.from;
|
|
3887
4081
|
const filter = {
|
|
3888
|
-
...transition.where
|
|
4082
|
+
...casGuard(transition.where),
|
|
3889
4083
|
[effectiveIdField]: id,
|
|
3890
4084
|
[stateField]: fromSpec
|
|
3891
4085
|
};
|
|
@@ -4008,13 +4202,32 @@ var Repository = class extends RepositoryBase {
|
|
|
4008
4202
|
machine.assertTransition(entityId, from, args.to);
|
|
4009
4203
|
}
|
|
4010
4204
|
const historyField = args.history === false ? null : args.history ?? "statusHistory";
|
|
4205
|
+
if (historyField === null && (args.priorAt !== void 0 || args.historyExtra !== void 0)) throw createError(400, "applyTransition: priorAt / historyExtra need history (got history: false)", { code: "TRANSITION_HISTORY_DISABLED" });
|
|
4206
|
+
if (args.priorAt !== void 0 && (!(args.priorAt instanceof Date) || Number.isNaN(args.priorAt.getTime()))) throw createError(400, "applyTransition: priorAt must be a valid Date", { code: "TRANSITION_INVALID_PRIOR_AT" });
|
|
4207
|
+
if (args.priorAt !== void 0 && args.at !== void 0 && (!(args.at instanceof Date) || Number.isNaN(args.at.getTime()))) throw createError(400, "applyTransition: at must be a valid Date when priorAt is given", { code: "TRANSITION_INVALID_AT" });
|
|
4208
|
+
if (args.historyExtra !== void 0 && (args.historyExtra === null || typeof args.historyExtra !== "object" || Array.isArray(args.historyExtra))) throw createError(400, "applyTransition: historyExtra must be a plain object", { code: "TRANSITION_INVALID_HISTORY_EXTRA" });
|
|
4209
|
+
if (args.historyExtra !== void 0) {
|
|
4210
|
+
const reserved = [
|
|
4211
|
+
"status",
|
|
4212
|
+
"occurredAt",
|
|
4213
|
+
"by",
|
|
4214
|
+
"note",
|
|
4215
|
+
"durationInPriorMs"
|
|
4216
|
+
].filter((k) => k in args.historyExtra);
|
|
4217
|
+
if (reserved.length > 0) throw createError(400, `applyTransition: historyExtra may not set reserved key(s) ${reserved.join(", ")}`, { code: "TRANSITION_HISTORY_RESERVED_KEY" });
|
|
4218
|
+
}
|
|
4011
4219
|
const pushSpec = { ...args.push ?? {} };
|
|
4012
|
-
if (historyField !== null)
|
|
4013
|
-
|
|
4014
|
-
|
|
4015
|
-
|
|
4016
|
-
|
|
4017
|
-
|
|
4220
|
+
if (historyField !== null) {
|
|
4221
|
+
const occurredAt = args.at ?? /* @__PURE__ */ new Date();
|
|
4222
|
+
pushSpec[historyField] = {
|
|
4223
|
+
...args.historyExtra ?? {},
|
|
4224
|
+
status: args.to,
|
|
4225
|
+
occurredAt,
|
|
4226
|
+
...args.by !== void 0 ? { by: args.by } : {},
|
|
4227
|
+
...args.note !== void 0 ? { note: args.note } : {},
|
|
4228
|
+
...args.priorAt !== void 0 ? { durationInPriorMs: Math.max(0, occurredAt.getTime() - args.priorAt.getTime()) } : {}
|
|
4229
|
+
};
|
|
4230
|
+
}
|
|
4018
4231
|
const patch = {
|
|
4019
4232
|
...args.set !== void 0 && Object.keys(args.set).length > 0 ? { $set: args.set } : {},
|
|
4020
4233
|
...Object.keys(pushSpec).length > 0 ? { $push: pushSpec } : {},
|
|
@@ -4156,7 +4369,7 @@ var Repository = class extends RepositoryBase {
|
|
|
4156
4369
|
};
|
|
4157
4370
|
const versionMatch = transition.from === void 0 ? null : transition.from;
|
|
4158
4371
|
const filter = {
|
|
4159
|
-
...transition.where
|
|
4372
|
+
...casGuard(transition.where),
|
|
4160
4373
|
[effectiveIdField]: id,
|
|
4161
4374
|
[versionField]: versionMatch
|
|
4162
4375
|
};
|
|
@@ -4370,7 +4583,7 @@ var Repository = class extends RepositoryBase {
|
|
|
4370
4583
|
* the port implementation.
|
|
4371
4584
|
*/
|
|
4372
4585
|
async purgeByField(field, value, strategy, options = {}) {
|
|
4373
|
-
const port = createMongoPurgePort(this, field, value, options.session);
|
|
4586
|
+
const port = createMongoPurgePort(this, field, value, options.session, purgePortOptions("purgeByField", strategy, options));
|
|
4374
4587
|
return runChunkedPurge(strategy, options, port);
|
|
4375
4588
|
}
|
|
4376
4589
|
/**
|
|
@@ -4388,25 +4601,31 @@ var Repository = class extends RepositoryBase {
|
|
|
4388
4601
|
* See [actions/purge.ts](./actions/purge.ts) for the port.
|
|
4389
4602
|
*/
|
|
4390
4603
|
async purgeByFilter(filter, strategy, options = {}) {
|
|
4391
|
-
const
|
|
4392
|
-
const port = createMongoPurgePortFromFilter(this, compiled, options.session);
|
|
4604
|
+
const port = createMongoPurgePortFromFilter(this, compileFilterToMongo(filter), options.session, purgePortOptions("purgeByFilter", strategy, options));
|
|
4393
4605
|
return runChunkedPurge(strategy, options, port);
|
|
4394
4606
|
}
|
|
4395
4607
|
/**
|
|
4396
4608
|
* Chunked cold-storage extraction — see `StandardRepo.archiveByFilter`
|
|
4397
4609
|
* for the cross-kit contract (write-before-delete, at-least-once,
|
|
4398
4610
|
* duplicate-tolerant sinks). Mongokit composes the kit-agnostic
|
|
4399
|
-
* `runChunkedArchive` orchestrator with `
|
|
4611
|
+
* `runChunkedArchive` orchestrator with `createMongoArchiveRun`
|
|
4400
4612
|
* (`_id`-ordered lean reads + plugin-routed deleteMany removal). See
|
|
4401
4613
|
* [actions/archive.ts](./actions/archive.ts) for the port.
|
|
4402
4614
|
*
|
|
4615
|
+
* `retentionPurge` rides every chunk delete with an attestation of the ids the
|
|
4616
|
+
* sink acknowledged, so a fence built with `allowArchive` (or `allowPurge`)
|
|
4617
|
+
* admits it; validated before any chunk is read. Each row is deleted only if its
|
|
4618
|
+
* `updatedAt` (+ `versionField`) still equals the exported value; an edited row stays
|
|
4619
|
+
* live for the next run.
|
|
4620
|
+
*
|
|
4403
4621
|
* Accepts Filter IR or a Mongo-shaped filter record — compiled once via
|
|
4404
4622
|
* `compileFilterToMongo`, same dual-dialect rule as every other verb.
|
|
4405
4623
|
*/
|
|
4406
4624
|
async archiveByFilter(filter, sink, options = {}) {
|
|
4407
4625
|
const compiled = compileFilterToMongo(filter);
|
|
4408
|
-
const
|
|
4409
|
-
|
|
4626
|
+
const retentionPurge = options.retentionPurge === void 0 ? void 0 : assertRetentionPurgeShape(options.retentionPurge, "archiveByFilter");
|
|
4627
|
+
const run = createMongoArchiveRun(this, compiled, sink, options.session, retentionPurge, options.immutable === true);
|
|
4628
|
+
return runChunkedArchive(options, run.sink, run.port);
|
|
4410
4629
|
}
|
|
4411
4630
|
/**
|
|
4412
4631
|
* Many keyed upserts in ONE command with an exact per-row outcome (repo-core `bulkUpsert` IR).
|
|
@@ -4467,7 +4686,7 @@ var Repository = class extends RepositoryBase {
|
|
|
4467
4686
|
ordered,
|
|
4468
4687
|
session: context.session,
|
|
4469
4688
|
writeConcern: writeOpts.writeConcern,
|
|
4470
|
-
|
|
4689
|
+
maxTimeMS: writeOpts.maxTimeMS
|
|
4471
4690
|
});
|
|
4472
4691
|
return summarizeBulkUpsert([...refused, ...sent].sort((a, b) => a.index - b.index));
|
|
4473
4692
|
});
|
|
@@ -4827,9 +5046,9 @@ var Repository = class extends RepositoryBase {
|
|
|
4827
5046
|
const appendLookupStages$2 = (pipeline) => appendLookupStages(pipeline, lookups, projection, isKeyset && sort ? this._parseSort(sort) : void 0);
|
|
4828
5047
|
if (isKeyset && sort) {
|
|
4829
5048
|
const parsedSort = this._parseSort(sort);
|
|
4830
|
-
const { validateKeysetSort } = await import("./PaginationEngine-
|
|
4831
|
-
const { encodeCursor, resolveCursorFilter } = await import("./PaginationEngine-
|
|
4832
|
-
const { getPrimaryField } = await import("./PaginationEngine-
|
|
5049
|
+
const { validateKeysetSort } = await import("./PaginationEngine-BRlLTQQz.mjs").then((n) => n.r);
|
|
5050
|
+
const { encodeCursor, resolveCursorFilter } = await import("./PaginationEngine-BRlLTQQz.mjs").then((n) => n.a);
|
|
5051
|
+
const { getPrimaryField } = await import("./PaginationEngine-BRlLTQQz.mjs").then((n) => n.r);
|
|
4833
5052
|
const normalizedSort = validateKeysetSort(parsedSort, this._pagination.config.strictKeysetSortFields);
|
|
4834
5053
|
const cursorVersion = this._pagination.config.cursorVersion ?? 1;
|
|
4835
5054
|
const minCursorVersion = this._pagination.config.minCursorVersion ?? 1;
|
|
@@ -5392,5 +5611,4 @@ var Repository = class extends RepositoryBase {
|
|
|
5392
5611
|
return createError(500, error.message || "Internal Server Error");
|
|
5393
5612
|
}
|
|
5394
5613
|
};
|
|
5395
|
-
|
|
5396
|
-
export { ALL_OPERATIONS as a, READ_OPERATIONS as c, searchKeysPlugin as i, operationsByPolicyKey as l, Repository as n, MUTATING_OPERATIONS as o, AggregationBuilder as r, OP_REGISTRY as s, HOOK_PRIORITY$1 as t, createMongoPurgePort as u };
|
|
5614
|
+
export { ALL_OPERATIONS as a, READ_OPERATIONS as c, RETENTION_PURGE_ERROR_CODES as d, readRetentionPurge as f, searchKeysPlugin as i, operationsByPolicyKey as l, Repository as n, MUTATING_OPERATIONS as o, retentionPurgeEvent as p, AggregationBuilder as r, OP_REGISTRY as s, HOOK_PRIORITY$1 as t, createMongoPurgePort as u };
|