@classytic/mongokit 3.39.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.
- package/README.md +71 -0
- package/dist/{PaginationEngine-D-P19Y9m.d.mts → PaginationEngine-CRHyuUtw.d.mts} +17 -3
- package/dist/{PaginationEngine-DQ7beTCR.mjs → PaginationEngine-CfT2Cxkj.mjs} +320 -145
- package/dist/{Repository-D7fDpyFx.mjs → Repository-CYhSC9ut.mjs} +844 -794
- package/dist/{Repository-IxI983_m.d.mts → Repository-pnLw1rG5.d.mts} +113 -47
- package/dist/actions/index.d.mts +1 -1
- package/dist/actions/index.mjs +1 -1
- package/dist/adapter/index.mjs +1 -1
- package/dist/{adapter-BrVfPFSW.mjs → adapter-CkMoMF83.mjs} +1 -1
- package/dist/ai/index.d.mts +1 -1
- package/dist/better-auth/index.mjs +2 -2
- package/dist/{create-repository-CRXjDa3N.d.mts → create-repository-BUx9vfG9.d.mts} +5 -5
- package/dist/{create-repository-DOe9E_5F.mjs → create-repository-BYyWzLbt.mjs} +2 -2
- package/dist/idempotency/index.d.mts +23 -0
- package/dist/idempotency/index.mjs +134 -0
- package/dist/{index-CmjNyulu.d.mts → index-SYF8N28D.d.mts} +37 -19
- package/dist/index.d.mts +105 -39
- package/dist/index.mjs +332 -676
- package/dist/indexes/verify-indexes.d.mts +14 -1
- package/dist/indexes/verify-indexes.mjs +54 -7
- package/dist/join-scope-DSAznE8E.mjs +168 -0
- package/dist/{mongooseToJsonSchema-waUVQwK4.mjs → mongooseToJsonSchema-C3k4ide0.mjs} +5 -1
- package/dist/{multi-tenant.plugin-B-tzNeYL.d.mts → multi-tenant.plugin-a3UhQARj.d.mts} +19 -3
- package/dist/{operations-CrRIoc8Z.d.mts → operations-XiFgG5US.d.mts} +36 -13
- package/dist/outbox/index.d.mts +1 -1
- package/dist/outbox/index.mjs +6 -4
- package/dist/pagination/PaginationEngine.d.mts +2 -2
- package/dist/pagination/PaginationEngine.mjs +1 -1
- package/dist/pagination/utils/limits.d.mts +1 -1
- package/dist/{pagination-BpIv2bdS.d.mts → pagination-D6psses0.d.mts} +116 -13
- package/dist/{plugin-options-BuvtOSZf.d.mts → plugin-options-CRhIvSzx.d.mts} +16 -2
- package/dist/plugins/index.d.mts +8 -3
- package/dist/plugins/index.mjs +98 -25
- package/dist/query-defaults-BxUcSOoF.mjs +208 -0
- package/dist/read-model/index.d.mts +66 -0
- package/dist/read-model/index.mjs +193 -0
- package/dist/{repository-ZbLtL7Nk.d.mts → repository-DNHfjAZX.d.mts} +36 -4
- package/dist/sync/index.d.mts +53 -0
- package/dist/sync/index.mjs +371 -0
- package/dist/tenant/index.mjs +2 -0
- package/dist/testkit/index.d.mts +40 -3
- package/dist/testkit/index.mjs +113 -2
- package/dist/{timestamp.plugin-B5eiaItQ.mjs → timestamp.plugin-zfdLYZiO.mjs} +135 -32
- package/dist/{to-plain-B8ufk3Qz.d.mts → to-plain-CjgEInWV.d.mts} +2 -2
- package/dist/transaction-BrRYtH_b.mjs +655 -0
- package/dist/{update-c_01A0if.mjs → update-WgeAUJ_I.mjs} +212 -33
- package/dist/utils/index.d.mts +1 -1
- package/dist/utils/index.mjs +1 -1
- package/dist/{validation-chain.plugin-Bju1D1Sn.mjs → validation-chain.plugin-CHUHWMtk.mjs} +204 -279
- package/dist/{validation-chain.plugin-DvrLJIbJ.d.mts → validation-chain.plugin-CTcNJCmV.d.mts} +27 -5
- package/package.json +18 -4
|
@@ -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 {
|
|
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). */
|
|
@@ -363,8 +377,23 @@ interface RepositoryContext {
|
|
|
363
377
|
_cacheResolved?: unknown;
|
|
364
378
|
/** Cache hit status (internal — `'fresh'` or `'stale'`) */
|
|
365
379
|
_cacheStatus?: 'fresh' | 'stale' | 'miss' | 'disabled' | 'bypass';
|
|
366
|
-
/**
|
|
367
|
-
|
|
380
|
+
/**
|
|
381
|
+
* Parent key values captured before a delete, by `parentKey` (internal —
|
|
382
|
+
* cascade plugin). Restrict, detach and cascade all read these.
|
|
383
|
+
*/
|
|
384
|
+
_cascadeParentKeys?: Map<string, unknown[]>;
|
|
385
|
+
/**
|
|
386
|
+
* Memo shared by every hook of THIS operation: N plugins that need the same read pay for
|
|
387
|
+
* one. Scoped to the operation, so it can never serve a value across writes. A rejected
|
|
388
|
+
* load is forgotten, so the next caller retries.
|
|
389
|
+
*/
|
|
390
|
+
memo?: <T>(key: string, load: () => Promise<T>) => Promise<T>;
|
|
391
|
+
/**
|
|
392
|
+
* The persisted document an id-addressed operation targets, as it is BEFORE the write,
|
|
393
|
+
* read once under the operation's session (not tenant-filtered — the write's own filter
|
|
394
|
+
* decides whether it applies). Use this instead of `Model.findById(context.id)` in a hook.
|
|
395
|
+
*/
|
|
396
|
+
loadTarget?: () => Promise<Record<string, unknown> | null>;
|
|
368
397
|
/** Custom context data from plugins */
|
|
369
398
|
[key: string]: unknown;
|
|
370
399
|
}
|
|
@@ -432,7 +461,7 @@ interface RepositoryInstance extends RepositoryBase {
|
|
|
432
461
|
[key: string]: unknown;
|
|
433
462
|
}
|
|
434
463
|
/** Repository operation names */
|
|
435
|
-
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';
|
|
436
465
|
/** Event lifecycle phases */
|
|
437
466
|
type EventPhase = 'before' | 'after' | 'error';
|
|
438
467
|
/** Repository event names (generated from template literals) */
|
|
@@ -528,6 +557,9 @@ type AllPluginMethods<TDoc> = {
|
|
|
528
557
|
deleteSubdocument(parentId: string | ObjectId, arrayPath: DocField<TDoc>, subId: string | ObjectId, options?: Record<string, unknown>): Promise<TDoc>;
|
|
529
558
|
restore(id: string | ObjectId, options?: {
|
|
530
559
|
session?: unknown;
|
|
560
|
+
organizationId?: unknown;
|
|
561
|
+
bypassTenant?: boolean;
|
|
562
|
+
[key: string]: unknown;
|
|
531
563
|
}): Promise<TDoc>;
|
|
532
564
|
getDeleted(params?: {
|
|
533
565
|
filters?: Record<string, unknown>;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { ClientSession, Connection, Model } from "mongoose";
|
|
2
|
+
import { ChangeLogStore, CommandStreamStore, CommandStreamTx } from "@classytic/repo-core/sync";
|
|
3
|
+
//#region src/sync/index.d.ts
|
|
4
|
+
interface ChangeLogModelOptions {
|
|
5
|
+
/** Entries collection. Default `sync_changes`; its counter lives in `<collection>_counter`. */
|
|
6
|
+
collection?: string;
|
|
7
|
+
/** Mongoose model name prefix. Default `SyncChange`. Must be unique per connection. */
|
|
8
|
+
modelName?: string;
|
|
9
|
+
}
|
|
10
|
+
interface ChangeLogModels {
|
|
11
|
+
entries: Model<Record<string, unknown>>;
|
|
12
|
+
counter: Model<Record<string, unknown>>;
|
|
13
|
+
/** The counter document's id (one feed per entries collection). */
|
|
14
|
+
feed: string;
|
|
15
|
+
}
|
|
16
|
+
declare function createChangeLogModels(connection: Connection, options?: ChangeLogModelOptions): ChangeLogModels;
|
|
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;
|
|
31
|
+
interface CommandStreamModelOptions {
|
|
32
|
+
/** Collection prefix: `<prefix>_streams`, `<prefix>_verdicts`, `<prefix>_aliases`. Default `sync`. */
|
|
33
|
+
prefix?: string;
|
|
34
|
+
/** Mongoose model name prefix. Default `SyncStream`. Must be unique per connection. */
|
|
35
|
+
modelName?: string;
|
|
36
|
+
}
|
|
37
|
+
interface CommandStreamModels {
|
|
38
|
+
streams: Model<Record<string, unknown>>;
|
|
39
|
+
verdicts: Model<Record<string, unknown>>;
|
|
40
|
+
aliases: Model<Record<string, unknown>>;
|
|
41
|
+
}
|
|
42
|
+
declare function createCommandStreamModels(connection: Connection, options?: CommandStreamModelOptions): CommandStreamModels;
|
|
43
|
+
interface MongoCommandStreamTx extends CommandStreamTx {
|
|
44
|
+
readonly session: ClientSession;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* The mongo {@link CommandStreamStore} (replica set). Proven by `runCommandStreamStoreConformance`.
|
|
48
|
+
* Concurrent transactions on one stream write-conflict on its document and `withTransaction`
|
|
49
|
+
* retries the loser, which then reads the winner's watermark.
|
|
50
|
+
*/
|
|
51
|
+
declare function createCommandStreamStore(connection: Pick<Connection, 'startSession'>, models: CommandStreamModels, changes: ChangeLogStore): CommandStreamStore<MongoCommandStreamTx>;
|
|
52
|
+
//#endregion
|
|
53
|
+
export { ChangeLogModelOptions, ChangeLogModels, CommandStreamModelOptions, CommandStreamModels, MongoChangeLogStore, MongoCommandStreamTx, createChangeLogModels, createChangeLogStore, createCommandStreamModels, createCommandStreamStore };
|