@classytic/mongokit 3.40.0 → 3.40.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/README.md +71 -0
  2. package/dist/{PaginationEngine-D-P19Y9m.d.mts → PaginationEngine-CRHyuUtw.d.mts} +17 -3
  3. package/dist/{PaginationEngine-Dvl8jcRY.mjs → PaginationEngine-CfT2Cxkj.mjs} +296 -137
  4. package/dist/{Repository-BMgPSkqZ.mjs → Repository-CYhSC9ut.mjs} +571 -175
  5. package/dist/{Repository-CR8Y9g2E.d.mts → Repository-pnLw1rG5.d.mts} +57 -16
  6. package/dist/actions/index.d.mts +1 -1
  7. package/dist/actions/index.mjs +1 -1
  8. package/dist/adapter/index.mjs +1 -1
  9. package/dist/{adapter-BrVfPFSW.mjs → adapter-CkMoMF83.mjs} +1 -1
  10. package/dist/ai/index.d.mts +1 -1
  11. package/dist/better-auth/index.mjs +2 -2
  12. package/dist/{create-repository-CAle1MSU.d.mts → create-repository-BUx9vfG9.d.mts} +5 -5
  13. package/dist/{create-repository-BzBUMvd1.mjs → create-repository-BYyWzLbt.mjs} +2 -2
  14. package/dist/{index-CmjNyulu.d.mts → index-SYF8N28D.d.mts} +37 -19
  15. package/dist/index.d.mts +55 -15
  16. package/dist/index.mjs +57 -11
  17. package/dist/join-scope-DSAznE8E.mjs +168 -0
  18. package/dist/{mongooseToJsonSchema-waUVQwK4.mjs → mongooseToJsonSchema-C3k4ide0.mjs} +5 -1
  19. package/dist/{multi-tenant.plugin-CBgCo9im.d.mts → multi-tenant.plugin-a3UhQARj.d.mts} +2 -2
  20. package/dist/{operations-CrRIoc8Z.d.mts → operations-XiFgG5US.d.mts} +36 -13
  21. package/dist/outbox/index.d.mts +1 -1
  22. package/dist/outbox/index.mjs +1 -1
  23. package/dist/pagination/PaginationEngine.d.mts +2 -2
  24. package/dist/pagination/PaginationEngine.mjs +1 -1
  25. package/dist/pagination/utils/limits.d.mts +1 -1
  26. package/dist/{pagination-BpIv2bdS.d.mts → pagination-D6psses0.d.mts} +116 -13
  27. package/dist/{plugin-options-Zk46lD6F.d.mts → plugin-options-CRhIvSzx.d.mts} +2 -2
  28. package/dist/plugins/index.d.mts +3 -3
  29. package/dist/plugins/index.mjs +2 -2
  30. package/dist/query-defaults-BxUcSOoF.mjs +208 -0
  31. package/dist/read-model/index.d.mts +66 -0
  32. package/dist/read-model/index.mjs +193 -0
  33. package/dist/{repository-odl03582.d.mts → repository-DNHfjAZX.d.mts} +16 -2
  34. package/dist/sync/index.d.mts +15 -2
  35. package/dist/sync/index.mjs +38 -1
  36. package/dist/testkit/index.d.mts +40 -3
  37. package/dist/testkit/index.mjs +113 -2
  38. package/dist/{timestamp.plugin-DF0gJB6p.mjs → timestamp.plugin-zfdLYZiO.mjs} +50 -31
  39. package/dist/{to-plain-k7SjNlSS.d.mts → to-plain-CjgEInWV.d.mts} +2 -2
  40. package/dist/{transaction-B5L9glO0.mjs → transaction-BrRYtH_b.mjs} +127 -5
  41. package/dist/{update-DG0fCwRJ.mjs → update-WgeAUJ_I.mjs} +44 -22
  42. package/dist/utils/index.d.mts +1 -1
  43. package/dist/utils/index.mjs +1 -1
  44. package/dist/{validation-chain.plugin-BLY2gLEk.mjs → validation-chain.plugin-CHUHWMtk.mjs} +15 -3
  45. package/dist/{validation-chain.plugin-DYp9E7K2.d.mts → validation-chain.plugin-CTcNJCmV.d.mts} +4 -4
  46. package/package.json +8 -3
@@ -1,6 +1,6 @@
1
- import { S as RepositoryPluginName, b as MONGOKIT_PLUGIN_NAMES, l as Plugin, x as MongoKitPluginName } from "../repository-odl03582.mjs";
2
- import { A as EnsureAuditTrailReadyOptions, C as AuditQueryOptions, D as AuditTrailOptions, E as AuditTrailMode, M as ensureAuditTrailReady, O as AuditTrailQuery, S as AuditOperation, T as AuditTrailMethods, _ as CacheOptions, a as DateSequentialIdOptions, b as cachePlugin, c as SequentialIdOptions, f as getNextSequence, g as CacheAdapter, h as sequentialId, i as CustomIdOptions, j as auditTrailPlugin, k as AuditTrailQueryOptions, l as customIdPlugin, o as IdGenerator, p as prefixedId, r as multiTenantPlugin, s as PrefixedIdOptions, t as MultiTenantOptions, u as dateSequentialId, v as RepositoryCacheHandle, w as AuditQueryResult, x as AuditEntry, y as RepositoryCachePluginOptions } from "../multi-tenant.plugin-CBgCo9im.mjs";
3
- import { A as IdempotencyStoreMethods, B as batchOperationsPlugin, C as LeaseMethods, D as ImmutableStatesPluginOptions, E as ImmutableClaimView, F as elasticSearchPlugin, G as aggregateHelpersPlugin, H as AppendOnlyPluginOptions, I as cascadePlugin, L as BatchOperationsMethods, M as idempotencyStorePlugin, N as fieldFilterPlugin, O as immutableStatesPlugin, P as ElasticSearchOptions, S as methodRegistryPlugin, T as leasePlugin, U as appendOnlyPlugin, V as auditLogPlugin, W as AggregateHelpersMethods, _ as OperationMetric, a as requireField, b as mongoOperationsPlugin, c as timestampPlugin, d as createTenantContext, f as SubdocumentMethods, g as ObservabilityOptions, h as softDeletePlugin, i as immutableField, j as IdempotencyStorePluginOptions, k as IdempotencyClaim, l as TenantContext, m as SoftDeleteMethods, n as autoInject, o as uniqueField, p as subdocumentPlugin, r as blockIf, s as validationChainPlugin, t as OperationType, u as TenantStore, v as observabilityPlugin, w as LeasePluginOptions, x as MethodRegistryRepository, y as MongoOperationsMethods } from "../validation-chain.plugin-DYp9E7K2.mjs";
1
+ import { S as RepositoryPluginName, b as MONGOKIT_PLUGIN_NAMES, l as Plugin, x as MongoKitPluginName } from "../repository-DNHfjAZX.mjs";
2
+ import { A as EnsureAuditTrailReadyOptions, C as AuditQueryOptions, D as AuditTrailOptions, E as AuditTrailMode, M as ensureAuditTrailReady, O as AuditTrailQuery, S as AuditOperation, T as AuditTrailMethods, _ as CacheOptions, a as DateSequentialIdOptions, b as cachePlugin, c as SequentialIdOptions, f as getNextSequence, g as CacheAdapter, h as sequentialId, i as CustomIdOptions, j as auditTrailPlugin, k as AuditTrailQueryOptions, l as customIdPlugin, o as IdGenerator, p as prefixedId, r as multiTenantPlugin, s as PrefixedIdOptions, t as MultiTenantOptions, u as dateSequentialId, v as RepositoryCacheHandle, w as AuditQueryResult, x as AuditEntry, y as RepositoryCachePluginOptions } from "../multi-tenant.plugin-a3UhQARj.mjs";
3
+ import { A as IdempotencyStoreMethods, B as batchOperationsPlugin, C as LeaseMethods, D as ImmutableStatesPluginOptions, E as ImmutableClaimView, F as elasticSearchPlugin, G as aggregateHelpersPlugin, H as AppendOnlyPluginOptions, I as cascadePlugin, L as BatchOperationsMethods, M as idempotencyStorePlugin, N as fieldFilterPlugin, O as immutableStatesPlugin, P as ElasticSearchOptions, S as methodRegistryPlugin, T as leasePlugin, U as appendOnlyPlugin, V as auditLogPlugin, W as AggregateHelpersMethods, _ as OperationMetric, a as requireField, b as mongoOperationsPlugin, c as timestampPlugin, d as createTenantContext, f as SubdocumentMethods, g as ObservabilityOptions, h as softDeletePlugin, i as immutableField, j as IdempotencyStorePluginOptions, k as IdempotencyClaim, l as TenantContext, m as SoftDeleteMethods, n as autoInject, o as uniqueField, p as subdocumentPlugin, r as blockIf, s as validationChainPlugin, t as OperationType, u as TenantStore, v as observabilityPlugin, w as LeasePluginOptions, x as MethodRegistryRepository, y as MongoOperationsMethods } from "../validation-chain.plugin-CTcNJCmV.mjs";
4
4
  import { ChangeLogStore } from "@classytic/repo-core/sync";
5
5
  //#region src/plugins/change-log.plugin.d.ts
6
6
  interface ChangeLogPluginOptions {
@@ -1,6 +1,6 @@
1
- import { _ as AuditTrailQuery, b as auditLogPlugin, f as getNextSequence, g as cachePlugin, h as sequentialId, i as multiTenantPlugin, l as customIdPlugin, n as softDeletePlugin, p as prefixedId, t as timestampPlugin, u as dateSequentialId, v as auditTrailPlugin, y as ensureAuditTrailReady } from "../timestamp.plugin-DF0gJB6p.mjs";
1
+ import { _ as AuditTrailQuery, b as auditLogPlugin, f as getNextSequence, g as cachePlugin, h as sequentialId, i as multiTenantPlugin, l as customIdPlugin, n as softDeletePlugin, p as prefixedId, t as timestampPlugin, u as dateSequentialId, v as auditTrailPlugin, y as ensureAuditTrailReady } from "../timestamp.plugin-zfdLYZiO.mjs";
2
2
  import { n as batchOperationsPlugin, t as methodRegistryPlugin } from "../method-registry.plugin-C63pGn1d.mjs";
3
- import { _ as cascadePlugin, a as uniqueField, c as subdocumentPlugin, d as mongoOperationsPlugin, f as leasePlugin, g as elasticSearchPlugin, h as fieldFilterPlugin, i as requireField, l as observabilityPlugin, m as idempotencyStorePlugin, n as blockIf, o as validationChainPlugin, p as immutableStatesPlugin, r as immutableField, s as createTenantContext, t as autoInject, u as MONGOKIT_PLUGIN_NAMES, v as appendOnlyPlugin, y as aggregateHelpersPlugin } from "../validation-chain.plugin-BLY2gLEk.mjs";
3
+ import { _ as cascadePlugin, a as uniqueField, c as subdocumentPlugin, d as mongoOperationsPlugin, f as leasePlugin, g as elasticSearchPlugin, h as fieldFilterPlugin, i as requireField, l as observabilityPlugin, m as idempotencyStorePlugin, n as blockIf, o as validationChainPlugin, p as immutableStatesPlugin, r as immutableField, s as createTenantContext, t as autoInject, u as MONGOKIT_PLUGIN_NAMES, v as appendOnlyPlugin, y as aggregateHelpersPlugin } from "../validation-chain.plugin-CHUHWMtk.mjs";
4
4
  //#region src/plugins/change-log.plugin.ts
5
5
  const PLUGIN_NAME = "changeLog";
6
6
  /** Marks a repository already captured: the store and scope it writes to. */
@@ -0,0 +1,208 @@
1
+ import { t as createError } from "./error-BuByZwKU.mjs";
2
+ import mongoose from "mongoose";
3
+ //#region src/repository/query-defaults.ts
4
+ /**
5
+ * Query defaults — the time bound and read/write concerns every repository command carries.
6
+ *
7
+ * Resolution, per option: per call → repository (`RepositoryOptions.queryDefaults` /
8
+ * `aggregateDefaults`) → deployment (`configureQueryDefaults` / `configureAggregateDefaults`).
9
+ * An explicit value always wins (FL1). For an aggregation the aggregate chain is consulted before
10
+ * the generic query chain, because it is the more specific instruction.
11
+ *
12
+ * The bound travels as per-op CSOT `timeoutMS` (the driver derives the wire `maxTimeMS` from
13
+ * it, and a client-level `timeoutMS` cannot override it). It and `comment` go to the commands Mongoose itself bounds with its global `maxTimeMS`
14
+ * (find, findOne, countDocuments, distinct, findOneAndUpdate, aggregate:
15
+ * lib/query.js:2465,2812,2905,3074,3610; lib/aggregate.js:1106). Writes get `writeConcern`. The
16
+ * connection's CSOT `timeoutMS` bounds everything else; {@link assertQueryDefaultsConfigured}
17
+ * checks both at boot. Inside a transaction a DEFAULT read/write concern or read preference is
18
+ * dropped (the transaction owns them); a per-call one is passed and the driver judges it.
19
+ */
20
+ /** On `globalThis`, for the reason `pagination/defaults.ts` gives: a duplicated install must share it. */
21
+ const POLICY_SLOT = Symbol.for("classytic.mongokit.queryDefaults");
22
+ function policy() {
23
+ const holder = globalThis;
24
+ holder[POLICY_SLOT] ??= {
25
+ query: {},
26
+ aggregate: {}
27
+ };
28
+ return holder[POLICY_SLOT];
29
+ }
30
+ function defined(input) {
31
+ return Object.fromEntries(Object.entries(input).filter(([, v]) => v !== void 0));
32
+ }
33
+ function assertPositive(name, value) {
34
+ if (value !== void 0 && !(Number.isFinite(value) && value > 0)) throw new TypeError(`[mongokit] ${name} must be a positive number of milliseconds, got ${value}`);
35
+ }
36
+ /** Set the deployment's query defaults. Merges; an explicit `undefined` changes nothing. */
37
+ function configureQueryDefaults(defaults) {
38
+ assertPositive("queryDefaults.maxTimeMS", defaults.maxTimeMS);
39
+ Object.assign(policy().query, defined(defaults));
40
+ }
41
+ /** Set the deployment's aggregation defaults. Merges; an explicit `undefined` changes nothing. */
42
+ function configureAggregateDefaults(defaults) {
43
+ assertPositive("aggregateDefaults.maxTimeMs", defaults.maxTimeMs);
44
+ Object.assign(policy().aggregate, defined(defaults));
45
+ }
46
+ /** A copy of the deployment defaults. */
47
+ function getQueryDefaults() {
48
+ const p = policy();
49
+ return {
50
+ query: { ...p.query },
51
+ aggregate: { ...p.aggregate }
52
+ };
53
+ }
54
+ /** Back to no deployment defaults. For tests. */
55
+ function resetQueryDefaults() {
56
+ const holder = globalThis;
57
+ holder[POLICY_SLOT] = {
58
+ query: {},
59
+ aggregate: {}
60
+ };
61
+ }
62
+ /**
63
+ * Boot check (M7): throws unless the deployment sets a query `maxTimeMS`, and — when a connection
64
+ * is passed — unless its client carries a CSOT `timeoutMS`. Returns what it verified.
65
+ */
66
+ function assertQueryDefaultsConfigured(connection) {
67
+ const maxTimeMS = policy().query.maxTimeMS;
68
+ if (maxTimeMS === void 0) throw createError(500, "[mongokit] no deployment query time bound: call configureQueryDefaults({ maxTimeMS }) at boot", {
69
+ code: QUERY_DEFAULTS_ERROR_CODES.NOT_CONFIGURED,
70
+ meta: { missing: "maxTimeMS" }
71
+ });
72
+ if (!connection) return { maxTimeMS };
73
+ const timeoutMS = connection.getClient().options.timeoutMS;
74
+ if (!(typeof timeoutMS === "number" && timeoutMS > 0)) throw createError(500, "[mongokit] the connection has no CSOT timeoutMS: pass { timeoutMS } to mongoose.connect()", {
75
+ code: QUERY_DEFAULTS_ERROR_CODES.NOT_CONFIGURED,
76
+ meta: { missing: "timeoutMS" }
77
+ });
78
+ return {
79
+ maxTimeMS,
80
+ timeoutMS
81
+ };
82
+ }
83
+ const QUERY_DEFAULTS_ERROR_CODES = {
84
+ NOT_CONFIGURED: "mongokit.query_defaults.not_configured",
85
+ /** A per-call bound inside a transaction that carries its own CSOT budget (the driver forbids both). */
86
+ TIMEOUT_IN_TRANSACTION: "mongokit.query_defaults.timeout_in_transaction"
87
+ };
88
+ const TX_DEADLINE = Symbol.for("@classytic/mongokit/transaction-deadline");
89
+ /**
90
+ * Set (or clear) the transaction budget's deadline (`performance.now()` ms) on a session.
91
+ * `runTransaction` sets it for each attempt; every mongokit op on the session is then bounded
92
+ * by what remains of it, so a transaction budget is ONE budget, as with the driver's own.
93
+ */
94
+ function setTransactionDeadline(session, deadline) {
95
+ Object.defineProperty(session, TX_DEADLINE, {
96
+ value: deadline,
97
+ writable: true,
98
+ configurable: true,
99
+ enumerable: false
100
+ });
101
+ }
102
+ function transactionRemaining(session) {
103
+ if (!session || typeof session !== "object") return void 0;
104
+ const deadline = session[TX_DEADLINE];
105
+ return deadline === void 0 ? void 0 : Math.floor(deadline - performance.now());
106
+ }
107
+ function inTransaction(session) {
108
+ const s = session;
109
+ return typeof s?.inTransaction === "function" && s.inTransaction();
110
+ }
111
+ /**
112
+ * A `withTransaction` running under a CSOT budget: the driver refuses any per-op `timeoutMS`
113
+ * there (mongodb lib/utils.js:451, `session.explicit && session.timeoutContext != null`).
114
+ */
115
+ function csotTransaction(session) {
116
+ const s = session;
117
+ return inTransaction(session) && s?.explicit === true && s.timeoutContext != null;
118
+ }
119
+ /** Resolve the options one command runs with. `repo` is the repository's own defaults. */
120
+ function resolveQueryOptions(kind, perCall, repo = {}) {
121
+ const deployment = policy();
122
+ const q = {
123
+ ...deployment.query,
124
+ ...defined(repo.query ?? {})
125
+ };
126
+ const agg = {
127
+ ...deployment.aggregate,
128
+ ...defined(repo.aggregate ?? {})
129
+ };
130
+ const tx = inTransaction(perCall.session);
131
+ const out = {};
132
+ const budget = tx ? transactionRemaining(perCall.session) : void 0;
133
+ if (budget !== void 0 && budget <= 0) throw new mongoose.mongo.MongoOperationTimeoutError("Transaction budget exhausted before this operation");
134
+ if (kind !== "write") {
135
+ if (csotTransaction(perCall.session) && perCall.maxTimeMS !== void 0) throw createError(400, "[mongokit] a per-call maxTimeMS cannot be honoured inside a transaction with its own timeoutMS budget; bound the transaction instead", { code: QUERY_DEFAULTS_ERROR_CODES.TIMEOUT_IN_TRANSACTION });
136
+ const bound = tx ? perCall.maxTimeMS : perCall.maxTimeMS ?? (kind === "aggregate" ? repo.aggregate?.maxTimeMs ?? deployment.aggregate.maxTimeMs : void 0) ?? q.maxTimeMS;
137
+ const capped = budget === void 0 ? bound : Math.min(bound ?? Number.POSITIVE_INFINITY, budget);
138
+ if (capped !== void 0) out.maxTimeMS = capped;
139
+ const comment = perCall.comment ?? q.comment;
140
+ if (comment !== void 0) out.comment = comment;
141
+ if (perCall.hint !== void 0) out.hint = perCall.hint;
142
+ }
143
+ if (kind === "read" || kind === "aggregate") {
144
+ const pref = perCall.readPreference ?? (tx ? void 0 : q.readPreference);
145
+ if (pref !== void 0) out.readPreference = pref;
146
+ const concern = perCall.readConcern ?? (tx ? void 0 : q.readConcern);
147
+ if (concern !== void 0) out.readConcern = concern;
148
+ }
149
+ if (kind === "aggregate") {
150
+ const disk = perCall.allowDiskUse ?? agg.allowDiskUse;
151
+ if (disk !== void 0) out.allowDiskUse = disk;
152
+ }
153
+ if (kind === "write" && budget !== void 0) out.maxTimeMS = budget;
154
+ if (kind === "write" || kind === "findAndModify") {
155
+ const wc = perCall.writeConcern ?? (tx ? void 0 : q.writeConcern);
156
+ if (wc !== void 0) out.writeConcern = wc;
157
+ }
158
+ return out;
159
+ }
160
+ const READ_PREFERENCE_MODES = [
161
+ "primary",
162
+ "primaryPreferred",
163
+ "secondary",
164
+ "secondaryPreferred",
165
+ "nearest"
166
+ ];
167
+ function isReadPreferenceMode(value) {
168
+ return READ_PREFERENCE_MODES.includes(value);
169
+ }
170
+ /** A read preference outside the driver's modes is refused, never passed through (FL2). */
171
+ function readPreferenceMode(value) {
172
+ if (isReadPreferenceMode(value)) return value;
173
+ throw new TypeError(`[mongokit] unknown readPreference '${value}'; expected one of ${READ_PREFERENCE_MODES.join(", ")}`);
174
+ }
175
+ /** Apply resolved options to a mongoose Query. */
176
+ function applyToQuery(query, o) {
177
+ if (o.maxTimeMS !== void 0) {
178
+ const csot = { timeoutMS: o.maxTimeMS };
179
+ query.setOptions(csot);
180
+ }
181
+ if (o.comment !== void 0) query.setOptions({ comment: o.comment });
182
+ if (o.readPreference !== void 0) query.read(readPreferenceMode(o.readPreference));
183
+ if (o.readConcern !== void 0) query.readConcern(o.readConcern);
184
+ if (o.writeConcern !== void 0) query.setOptions({ writeConcern: o.writeConcern });
185
+ if (o.hint !== void 0) query.hint(o.hint);
186
+ }
187
+ /**
188
+ * Make a find cursor's bound apply PER BATCH (CSOT `timeoutMode: 'iteration'`). Only when some
189
+ * `timeoutMS` exists (this op's, or the client's): the driver refuses a timeoutMode without one
190
+ * (mongodb lib/cursor/abstract_cursor.js:114). In iteration mode the driver enforces the budget
191
+ * client-side per batch and sends no wire maxTimeMS.
192
+ */
193
+ function applyCursorTimeoutMode(query, o, connection) {
194
+ const clientTimeout = connection.getClient().options.timeoutMS;
195
+ if (o.maxTimeMS === void 0 && clientTimeout === void 0) return;
196
+ query.setOptions({ timeoutMode: "iteration" });
197
+ }
198
+ /** Apply resolved options to a mongoose Aggregate. */
199
+ function applyToAggregate(aggregation, o) {
200
+ if (o.maxTimeMS !== void 0) aggregation.option({ timeoutMS: o.maxTimeMS });
201
+ if (o.comment !== void 0) aggregation.option({ comment: o.comment });
202
+ if (o.readPreference !== void 0) aggregation.read(readPreferenceMode(o.readPreference));
203
+ if (o.readConcern !== void 0) aggregation.readConcern(o.readConcern);
204
+ if (o.allowDiskUse !== void 0) aggregation.allowDiskUse(o.allowDiskUse);
205
+ if (o.hint !== void 0) aggregation.hint(o.hint);
206
+ }
207
+ //#endregion
208
+ export { assertQueryDefaultsConfigured as a, getQueryDefaults as c, setTransactionDeadline as d, applyToQuery as i, resetQueryDefaults as l, applyCursorTimeoutMode as n, configureAggregateDefaults as o, applyToAggregate as r, configureQueryDefaults as s, QUERY_DEFAULTS_ERROR_CODES as t, resolveQueryOptions as u };
@@ -0,0 +1,66 @@
1
+ import { r as Repository } from "../Repository-pnLw1rG5.mjs";
2
+ import { BulkUpsertResult } from "@classytic/repo-core/repository";
3
+ //#region src/read-model/index.d.ts
4
+ declare const READ_MODEL_ERROR_CODES: {
5
+ readonly MERGE_IN_TRANSACTION: "mongokit.read_model.merge_in_transaction";
6
+ readonly ON_NOT_UNIQUE: "mongokit.read_model.on_not_unique";
7
+ readonly ON_MISSING_SCOPE: "mongokit.read_model.on_missing_scope";
8
+ readonly DEDUPE_NEEDS_TRANSACTION: "mongokit.read_model.dedupe_needs_transaction";
9
+ readonly DEDUPE_KEY_MISSING: "mongokit.read_model.dedupe_key_missing";
10
+ readonly INCREMENT_FAILED: "mongokit.read_model.increment_failed";
11
+ readonly TOO_LARGE: "mongokit.read_model.too_large";
12
+ };
13
+ /** Caller scope forwarded to every repository call (tenant, bypass, attribution). */
14
+ type ReadModelScope = Record<string, unknown>;
15
+ interface IncrementGrain {
16
+ /** The read-model key (without the policy scope, which the repository adds). */
17
+ key: Record<string, unknown>;
18
+ inc: Record<string, number>;
19
+ setOnInsert?: Record<string, unknown>;
20
+ /** Identity of the event this grain comes from; required when a ledger is given. */
21
+ dedupeKey?: string;
22
+ }
23
+ /** Sum grains per key, apply them in one upsert; with a ledger, count each dedupeKey once. */
24
+ declare function applyIncrements<TDoc>(target: Repository<TDoc>, grains: readonly IncrementGrain[], options?: ReadModelScope & {
25
+ session?: unknown;
26
+ /** Repository over a collection unique on (scope, `dedupeKey`). */
27
+ ledger?: Repository<unknown>;
28
+ }): Promise<BulkUpsertResult>;
29
+ interface RebuildOptions {
30
+ /** Source stages producing one row per read-model grain (with every `on` field except scope fields). */
31
+ pipeline: readonly unknown[];
32
+ /** Fields identifying a grain; must equal a unique index of the target and include its tenant field. */
33
+ on: readonly string[];
34
+ whenMatched?: 'replace' | 'merge';
35
+ /** Caller scope (tenant etc.), applied to the source read and stamped on the target rows. */
36
+ scope?: ReadModelScope;
37
+ session?: unknown;
38
+ }
39
+ /** Rebuild the scope's slice of `target` from `source` with one `$merge`, then drop stale grains. */
40
+ declare function rebuildInto<TSrc, TDoc>(source: Repository<TSrc>, target: Repository<TDoc>, options: RebuildOptions): Promise<{
41
+ rebuildId: string;
42
+ removed: number;
43
+ }>;
44
+ interface ReconcileOptions {
45
+ pipeline: readonly unknown[];
46
+ on: readonly string[];
47
+ /** Fields compared between the recomputed and the stored row. */
48
+ measures: readonly string[];
49
+ scope?: ReadModelScope;
50
+ /** Refuse past this many rows on either side (both are held in memory). Default 100,000. */
51
+ maxRows?: number;
52
+ }
53
+ interface DriftRow {
54
+ key: Record<string, unknown>;
55
+ /** Recomputed measures, `null` when the source produces no such grain. */
56
+ expected: Record<string, unknown> | null;
57
+ /** Stored measures, `null` when the read model lacks the grain. */
58
+ actual: Record<string, unknown> | null;
59
+ }
60
+ /** Recompute the scope's grains from `source` and report every one that differs in `target`. */
61
+ declare function reconcile<TSrc, TDoc>(source: Repository<TSrc>, target: Repository<TDoc>, options: ReconcileOptions): Promise<{
62
+ drift: DriftRow[];
63
+ compared: number;
64
+ }>;
65
+ //#endregion
66
+ export { DriftRow, IncrementGrain, READ_MODEL_ERROR_CODES, ReadModelScope, RebuildOptions, ReconcileOptions, applyIncrements, rebuildInto, reconcile };
@@ -0,0 +1,193 @@
1
+ import { t as createError } from "../error-BuByZwKU.mjs";
2
+ import { i as scopeForCollection, o as tenantFieldsOf } from "../join-scope-DSAznE8E.mjs";
3
+ import { randomUUID } from "node:crypto";
4
+ //#region src/read-model/index.ts
5
+ /**
6
+ * `@classytic/mongokit/read-model` — maintain and verify a derived (read) collection.
7
+ *
8
+ * - {@link applyIncrements}: grains summed per key, then ONE `bulkUpsert` (`$inc` + `$setOnInsert`)
9
+ * inside the caller's unit of work. With a `ledger`, each grain's `dedupeKey` is recorded in the
10
+ * SAME transaction and a key already recorded is skipped, so a replayed event counts once.
11
+ * - {@link rebuildInto}: the source pipeline (scoped by the source repository) ends in a `$merge`
12
+ * on a unique `on`; grains of the scope the source no longer produces are then removed.
13
+ * `$merge` cannot run in a transaction, so a session inside one is refused.
14
+ * - {@link reconcile}: recompute from the source and compare with the read model; returns drift.
15
+ */
16
+ const READ_MODEL_ERROR_CODES = {
17
+ MERGE_IN_TRANSACTION: "mongokit.read_model.merge_in_transaction",
18
+ ON_NOT_UNIQUE: "mongokit.read_model.on_not_unique",
19
+ ON_MISSING_SCOPE: "mongokit.read_model.on_missing_scope",
20
+ DEDUPE_NEEDS_TRANSACTION: "mongokit.read_model.dedupe_needs_transaction",
21
+ DEDUPE_KEY_MISSING: "mongokit.read_model.dedupe_key_missing",
22
+ INCREMENT_FAILED: "mongokit.read_model.increment_failed",
23
+ TOO_LARGE: "mongokit.read_model.too_large"
24
+ };
25
+ function isRecord(value) {
26
+ return typeof value === "object" && value !== null && !Array.isArray(value);
27
+ }
28
+ function inTransaction(session) {
29
+ const s = session;
30
+ return typeof s?.inTransaction === "function" && s.inTransaction();
31
+ }
32
+ /** Sum grains per key, apply them in one upsert; with a ledger, count each dedupeKey once. */
33
+ async function applyIncrements(target, grains, options = {}) {
34
+ const { ledger, ...scope } = options;
35
+ let live = grains;
36
+ if (ledger) {
37
+ if (!inTransaction(scope.session)) throw createError(500, "[mongokit] applyIncrements: a dedupe ledger is only correct inside a transaction", { code: READ_MODEL_ERROR_CODES.DEDUPE_NEEDS_TRANSACTION });
38
+ if (grains.some((g) => !g.dedupeKey)) throw createError(400, "[mongokit] applyIncrements: every grain needs a dedupeKey when a ledger is given", { code: READ_MODEL_ERROR_CODES.DEDUPE_KEY_MISSING });
39
+ const keys = [...new Set(grains.flatMap((g) => g.dedupeKey ? [g.dedupeKey] : []))];
40
+ const seen = new Set((await ledger.findAll({ dedupeKey: { $in: keys } }, {
41
+ ...scope,
42
+ select: "dedupeKey",
43
+ lean: true
44
+ })).map((d) => d).filter(isRecord).map((d) => String(d.dedupeKey)));
45
+ const fresh = keys.filter((k) => !seen.has(k));
46
+ if (fresh.length > 0) await ledger.createMany(fresh.map((dedupeKey) => ({ dedupeKey })), scope);
47
+ const freshSet = new Set(fresh);
48
+ const taken = /* @__PURE__ */ new Set();
49
+ live = grains.filter((g) => {
50
+ const k = g.dedupeKey;
51
+ if (!k || !freshSet.has(k) || taken.has(k)) return false;
52
+ taken.add(k);
53
+ return true;
54
+ });
55
+ }
56
+ if (live.length === 0) return {
57
+ results: [],
58
+ inserted: 0,
59
+ updated: 0,
60
+ unchanged: 0,
61
+ failed: 0
62
+ };
63
+ const summed = /* @__PURE__ */ new Map();
64
+ for (const g of live) {
65
+ const id = JSON.stringify(Object.keys(g.key).sort().map((k) => [k, g.key[k]]));
66
+ const slot = summed.get(id) ?? {
67
+ key: g.key,
68
+ inc: {},
69
+ setOnInsert: { ...g.setOnInsert }
70
+ };
71
+ for (const [field, n] of Object.entries(g.inc)) slot.inc[field] = (slot.inc[field] ?? 0) + n;
72
+ summed.set(id, slot);
73
+ }
74
+ const rows = [...summed.values()].map((s) => ({
75
+ ...s.key,
76
+ ...s.inc,
77
+ ...s.setOnInsert
78
+ }));
79
+ const first = [...summed.values()][0];
80
+ const result = await target.bulkUpsert(rows, {
81
+ ...scope,
82
+ key: Object.keys(first?.key ?? {}),
83
+ inc: [...new Set([...summed.values()].flatMap((s) => Object.keys(s.inc)))],
84
+ setOnInsert: [...new Set([...summed.values()].flatMap((s) => Object.keys(s.setOnInsert)))]
85
+ });
86
+ if (result.failed > 0) throw createError(500, `[mongokit] applyIncrements: ${result.failed} grain(s) failed; the unit of work must abort`, {
87
+ code: READ_MODEL_ERROR_CODES.INCREMENT_FAILED,
88
+ meta: { failures: result.results.filter((r) => r.outcome === "failed") }
89
+ });
90
+ return result;
91
+ }
92
+ function contextOf(scope) {
93
+ return {
94
+ operation: "aggregatePipeline",
95
+ model: "read-model",
96
+ ...scope
97
+ };
98
+ }
99
+ /** The target's policy predicate for this scope (tenant + live docs), as equality fields. */
100
+ function targetScope(target, scope) {
101
+ const predicate = scopeForCollection(target.Model.collection.collectionName, {
102
+ connection: target.Model.db,
103
+ context: contextOf(scope),
104
+ operation: "aggregatePipeline"
105
+ });
106
+ if (!predicate) return {};
107
+ const parts = Array.isArray(predicate.$and) ? predicate.$and.filter(isRecord) : [predicate];
108
+ return Object.assign({}, ...parts);
109
+ }
110
+ /** Rebuild the scope's slice of `target` from `source` with one `$merge`, then drop stale grains. */
111
+ async function rebuildInto(source, target, options) {
112
+ const scope = options.scope ?? {};
113
+ if (inTransaction(options.session)) throw createError(500, "[mongokit] rebuildInto: $merge cannot run inside a transaction", { code: READ_MODEL_ERROR_CODES.MERGE_IN_TRANSACTION });
114
+ const on = [...options.on];
115
+ const missing = tenantFieldsOf(target.Model).filter((f) => !on.includes(f));
116
+ if (missing.length > 0) throw createError(400, `[mongokit] rebuildInto: 'on' must include the target's tenant field(s) ${missing.join(", ")}`, { code: READ_MODEL_ERROR_CODES.ON_MISSING_SCOPE });
117
+ if (!target.Model.schema.indexes().some(([fields, opts]) => opts.unique === true && sameSet(Object.keys(fields), on))) throw createError(400, `[mongokit] rebuildInto: $merge needs a unique index on exactly (${on.join(", ")})`, { code: READ_MODEL_ERROR_CODES.ON_NOT_UNIQUE });
118
+ const rebuildId = randomUUID();
119
+ const stamp = {
120
+ ...targetScope(target, scope),
121
+ _rebuildId: rebuildId
122
+ };
123
+ const stages = [
124
+ ...options.pipeline,
125
+ { $set: stamp },
126
+ { $merge: {
127
+ into: target.Model.collection.collectionName,
128
+ on,
129
+ whenMatched: options.whenMatched ?? "replace",
130
+ whenNotMatched: "insert"
131
+ } }
132
+ ];
133
+ await source.aggregatePipeline(stages, {
134
+ ...scope,
135
+ ...options.session ? { session: options.session } : {}
136
+ });
137
+ return {
138
+ rebuildId,
139
+ removed: (await target.deleteMany({ _rebuildId: { $ne: rebuildId } }, {
140
+ ...scope,
141
+ ...options.session ? { session: options.session } : {}
142
+ })).deletedCount
143
+ };
144
+ }
145
+ function sameSet(a, b) {
146
+ return a.length === b.length && a.every((x) => b.includes(x));
147
+ }
148
+ function canonical(value) {
149
+ if (value === null || value === void 0) return "null";
150
+ if (isRecord(value) && value._bsontype === "Decimal128") return String(Number(String(value)));
151
+ return JSON.stringify(value);
152
+ }
153
+ /** Recompute the scope's grains from `source` and report every one that differs in `target`. */
154
+ async function reconcile(source, target, options) {
155
+ const scope = options.scope ?? {};
156
+ const maxRows = options.maxRows ?? 1e5;
157
+ const stamp = targetScope(target, scope);
158
+ const keyFields = options.on.filter((f) => !(f in stamp));
159
+ const pick = (row, fields) => Object.fromEntries(fields.map((f) => [f, row[f] ?? null]));
160
+ const id = (row) => keyFields.map((f) => canonical(row[f])).join("|");
161
+ const expectedRows = await source.aggregatePipeline([...options.pipeline, { $limit: maxRows + 1 }], scope);
162
+ const actualRows = (await target.findAll({}, {
163
+ ...scope,
164
+ select: [...keyFields, ...options.measures].join(" "),
165
+ limit: maxRows + 1,
166
+ lean: true
167
+ })).map((d) => d).filter(isRecord);
168
+ if (expectedRows.length > maxRows || actualRows.length > maxRows) throw createError(500, `[mongokit] reconcile: more than ${maxRows} grains in scope; reconcile a narrower scope`, { code: READ_MODEL_ERROR_CODES.TOO_LARGE });
169
+ const actual = new Map(actualRows.map((r) => [id(r), r]));
170
+ const drift = [];
171
+ const seen = /* @__PURE__ */ new Set();
172
+ for (const e of expectedRows) {
173
+ const k = id(e);
174
+ seen.add(k);
175
+ const a = actual.get(k);
176
+ if (!a || options.measures.some((m) => canonical(e[m]) !== canonical(a[m]))) drift.push({
177
+ key: pick(e, keyFields),
178
+ expected: pick(e, options.measures),
179
+ actual: a ? pick(a, options.measures) : null
180
+ });
181
+ }
182
+ for (const [k, a] of actual) if (!seen.has(k)) drift.push({
183
+ key: pick(a, keyFields),
184
+ expected: null,
185
+ actual: pick(a, options.measures)
186
+ });
187
+ return {
188
+ drift,
189
+ compared: expectedRows.length
190
+ };
191
+ }
192
+ //#endregion
193
+ export { READ_MODEL_ERROR_CODES, applyIncrements, rebuildInto, reconcile };
@@ -1,4 +1,4 @@
1
- import { _ as PopulateSpec, g as ObjectId, h as FilterQuery, i as CountStrategy, p as AnyDocument, v as ReadPreferenceType, x as SortSpec, y as SelectSpec } from "./pagination-BpIv2bdS.mjs";
1
+ import { A as ObjectId, D as AnyDocument, F as SortSpec, M as ReadPreferenceType, N as SelectSpec, _ as QueryDefaults, b as WriteConcernSpec, i as CountStrategy, j as PopulateSpec, k as FilterQuery, p as AggregateDefaults, v as ReadConcernLevel } from "./pagination-D6psses0.mjs";
2
2
  import { r as DocField } from "./type-utils-CLzRFtIA.mjs";
3
3
  import { DistributionConfig, DistributionMissHandler, RepoCapabilities, RepositoryBase } from "@classytic/repo-core/repository";
4
4
  import { Model, PipelineStage } from "mongoose";
@@ -199,6 +199,13 @@ interface RepositoryOptions {
199
199
  * ```
200
200
  */
201
201
  requirePlugins?: readonly RepositoryPluginName[];
202
+ /**
203
+ * Time bound and concerns for every command this repository issues. A per-call value
204
+ * wins; unset fields fall back to `configureQueryDefaults` (see `repository/query-defaults.ts`).
205
+ */
206
+ queryDefaults?: QueryDefaults;
207
+ /** Aggregation-only defaults, consulted before `queryDefaults`. */
208
+ aggregateDefaults?: AggregateDefaults;
202
209
  /**
203
210
  * Declare the collection's shard key so every filter-taking verb
204
211
  * (`getAll`, `findAll`, `getOne`, `count`, `updateMany`, `aggregate`, …)
@@ -327,6 +334,13 @@ interface RepositoryContext {
327
334
  hint?: string | Record<string, 1 | -1>;
328
335
  /** Maximum execution time in milliseconds */
329
336
  maxTimeMS?: number;
337
+ /** Per-call read concern / comment / write concern / disk use (see `queryDefaults`). */
338
+ readConcern?: ReadConcernLevel;
339
+ comment?: string;
340
+ writeConcern?: WriteConcernSpec;
341
+ allowDiskUse?: boolean;
342
+ /** Collections the caller asserts are company-wide (joins only). */
343
+ unscopedJoins?: readonly string[];
330
344
  /** Count strategy for offset pagination. See {@link CountStrategy}. */
331
345
  countStrategy?: CountStrategy;
332
346
  /** Ceiling for `countStrategy: 'capped'` (default 10000). */
@@ -447,7 +461,7 @@ interface RepositoryInstance extends RepositoryBase {
447
461
  [key: string]: unknown;
448
462
  }
449
463
  /** Repository operation names */
450
- type RepositoryOperation = 'create' | 'createMany' | 'update' | 'updateMany' | 'findOneAndUpdate' | 'delete' | 'deleteMany' | 'restore' | 'getById' | 'getByQuery' | 'getOne' | 'getAll' | 'findAll' | 'getOrCreate' | 'count' | 'exists' | 'distinct' | 'aggregate' | 'aggregatePaginate' | 'aggregatePipeline' | 'aggregatePipelinePaginate' | 'lookupPopulate' | 'bulkWrite' | 'claim' | 'claimVersion' | 'cursor' | 'watch';
464
+ type RepositoryOperation = 'create' | 'createMany' | 'update' | 'updateMany' | 'findOneAndUpdate' | 'delete' | 'deleteMany' | 'restore' | 'getById' | 'getByQuery' | 'getOne' | 'getAll' | 'findAll' | 'getOrCreate' | 'count' | 'exists' | 'distinct' | 'aggregate' | 'aggregatePaginate' | 'aggregatePipeline' | 'aggregatePipelinePaginate' | 'lookupPopulate' | 'bulkWrite' | 'bulkUpsert' | 'claim' | 'claimVersion' | 'cursor' | 'iterate' | 'watch';
451
465
  /** Event lifecycle phases */
452
466
  type EventPhase = 'before' | 'after' | 'error';
453
467
  /** Repository event names (generated from template literals) */
@@ -14,7 +14,20 @@ interface ChangeLogModels {
14
14
  feed: string;
15
15
  }
16
16
  declare function createChangeLogModels(connection: Connection, options?: ChangeLogModelOptions): ChangeLogModels;
17
- declare function createChangeLogStore(models: ChangeLogModels): ChangeLogStore;
17
+ /** The mongo change feed: the `ChangeLogStore` contract plus superseded-entry compaction. */
18
+ interface MongoChangeLogStore extends ChangeLogStore {
19
+ /**
20
+ * Delete every entry a later COMMITTED entry of the same `(scope, docId)` replaced. Safe for any client
21
+ * cursor: entries carry whole documents, so a reader only ever needs the latest one; tombstones and
22
+ * each document's latest entry stay. Idempotent; run it on a schedule.
23
+ */
24
+ compactSuperseded(options?: {
25
+ batchSize?: number;
26
+ }): Promise<{
27
+ removed: number;
28
+ }>;
29
+ }
30
+ declare function createChangeLogStore(models: ChangeLogModels): MongoChangeLogStore;
18
31
  interface CommandStreamModelOptions {
19
32
  /** Collection prefix: `<prefix>_streams`, `<prefix>_verdicts`, `<prefix>_aliases`. Default `sync`. */
20
33
  prefix?: string;
@@ -37,4 +50,4 @@ interface MongoCommandStreamTx extends CommandStreamTx {
37
50
  */
38
51
  declare function createCommandStreamStore(connection: Pick<Connection, 'startSession'>, models: CommandStreamModels, changes: ChangeLogStore): CommandStreamStore<MongoCommandStreamTx>;
39
52
  //#endregion
40
- export { ChangeLogModelOptions, ChangeLogModels, CommandStreamModelOptions, CommandStreamModels, MongoCommandStreamTx, createChangeLogModels, createChangeLogStore, createCommandStreamModels, createCommandStreamStore };
53
+ export { ChangeLogModelOptions, ChangeLogModels, CommandStreamModelOptions, CommandStreamModels, MongoChangeLogStore, MongoCommandStreamTx, createChangeLogModels, createChangeLogStore, createCommandStreamModels, createCommandStreamStore };
@@ -1,4 +1,4 @@
1
- import { i as withTransaction } from "../transaction-B5L9glO0.mjs";
1
+ import { i as withTransaction } from "../transaction-BrRYtH_b.mjs";
2
2
  import { Schema } from "mongoose";
3
3
  //#region src/sync/index.ts
4
4
  function createChangeLogModels(connection, options = {}) {
@@ -49,6 +49,11 @@ function createChangeLogModels(connection, options = {}) {
49
49
  scope: 1,
50
50
  seq: 1
51
51
  }, { name: "tenant_scope_feed" });
52
+ schema.index({
53
+ scope: 1,
54
+ docId: 1,
55
+ seq: 1
56
+ }, { name: "doc_versions" });
52
57
  return schema;
53
58
  })()),
54
59
  counter: connection.models[counterName] ?? connection.model(counterName, new Schema({
@@ -138,6 +143,38 @@ function createChangeLogStore(models) {
138
143
  async latestCursor(options = {}) {
139
144
  const row = await entries.findOne(filterOf(0, options)).sort({ seq: -1 }).select({ seq: 1 }).lean().exec();
140
145
  return row?.seq ? toCursor(row.seq) : "";
146
+ },
147
+ async compactSuperseded(options = {}) {
148
+ const batchSize = options.batchSize ?? 1e3;
149
+ const groups = entries.aggregate([
150
+ { $group: {
151
+ _id: {
152
+ scope: "$scope",
153
+ docId: "$docId"
154
+ },
155
+ latest: { $max: "$seq" },
156
+ n: { $sum: 1 }
157
+ } },
158
+ { $match: { n: { $gt: 1 } } },
159
+ { $project: { latest: 1 } }
160
+ ]).allowDiskUse(true).cursor({ batchSize });
161
+ let removed = 0;
162
+ let ops = [];
163
+ const flush = async () => {
164
+ if (ops.length === 0) return;
165
+ removed += (await entries.bulkWrite(ops, { ordered: false })).deletedCount;
166
+ ops = [];
167
+ };
168
+ for await (const g of groups) {
169
+ ops.push({ deleteMany: { filter: {
170
+ scope: g._id.scope,
171
+ docId: g._id.docId,
172
+ seq: { $lt: g.latest }
173
+ } } });
174
+ if (ops.length >= batchSize) await flush();
175
+ }
176
+ await flush();
177
+ return { removed };
141
178
  }
142
179
  };
143
180
  }