@classytic/mongokit 3.39.0 → 3.40.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{PaginationEngine-DQ7beTCR.mjs → PaginationEngine-Dvl8jcRY.mjs} +33 -17
- package/dist/{Repository-D7fDpyFx.mjs → Repository-BMgPSkqZ.mjs} +279 -625
- package/dist/{Repository-IxI983_m.d.mts → Repository-CR8Y9g2E.d.mts} +57 -32
- package/dist/actions/index.mjs +1 -1
- package/dist/ai/index.d.mts +1 -1
- package/dist/better-auth/index.mjs +1 -1
- package/dist/{create-repository-DOe9E_5F.mjs → create-repository-BzBUMvd1.mjs} +2 -2
- package/dist/{create-repository-CRXjDa3N.d.mts → create-repository-CAle1MSU.d.mts} +4 -4
- package/dist/idempotency/index.d.mts +23 -0
- package/dist/idempotency/index.mjs +134 -0
- package/dist/index.d.mts +58 -32
- package/dist/index.mjs +285 -675
- package/dist/indexes/verify-indexes.d.mts +14 -1
- package/dist/indexes/verify-indexes.mjs +54 -7
- package/dist/{multi-tenant.plugin-B-tzNeYL.d.mts → multi-tenant.plugin-CBgCo9im.d.mts} +18 -2
- package/dist/outbox/index.d.mts +1 -1
- package/dist/outbox/index.mjs +6 -4
- package/dist/pagination/PaginationEngine.mjs +1 -1
- package/dist/{plugin-options-BuvtOSZf.d.mts → plugin-options-Zk46lD6F.d.mts} +15 -1
- package/dist/plugins/index.d.mts +8 -3
- package/dist/plugins/index.mjs +98 -25
- package/dist/{repository-ZbLtL7Nk.d.mts → repository-odl03582.d.mts} +20 -2
- package/dist/sync/index.d.mts +40 -0
- package/dist/sync/index.mjs +334 -0
- package/dist/tenant/index.mjs +2 -0
- package/dist/testkit/index.d.mts +2 -2
- package/dist/testkit/index.mjs +1 -1
- package/dist/{timestamp.plugin-B5eiaItQ.mjs → timestamp.plugin-DF0gJB6p.mjs} +86 -2
- package/dist/{to-plain-B8ufk3Qz.d.mts → to-plain-k7SjNlSS.d.mts} +2 -2
- package/dist/transaction-B5L9glO0.mjs +533 -0
- package/dist/{update-c_01A0if.mjs → update-DG0fCwRJ.mjs} +168 -11
- package/dist/utils/index.d.mts +1 -1
- package/dist/{validation-chain.plugin-Bju1D1Sn.mjs → validation-chain.plugin-BLY2gLEk.mjs} +192 -279
- package/dist/{validation-chain.plugin-DvrLJIbJ.d.mts → validation-chain.plugin-DYp9E7K2.d.mts} +25 -3
- package/package.json +13 -4
|
@@ -14,6 +14,10 @@
|
|
|
14
14
|
* Missing UNIQUE indexes are reported separately from missing regular ones —
|
|
15
15
|
* the first silently admits duplicate data, the second only costs a scan.
|
|
16
16
|
*
|
|
17
|
+
* Same keys are not the same index: a live index is compared on the options that decide
|
|
18
|
+
* correctness (`unique`, `partialFilterExpression`, `sparse`, `collation`, `expireAfterSeconds`).
|
|
19
|
+
* A same-key index that differs is reported as `incompatible` — its fix is a rebuild, not a create.
|
|
20
|
+
*
|
|
17
21
|
* DELIBERATELY ONE-DIRECTIONAL: an index present in the database but absent
|
|
18
22
|
* from a schema is NOT drift. Auth libraries and migrations manage indexes
|
|
19
23
|
* outside the ODM, and treating those as removable is exactly what makes a
|
|
@@ -44,11 +48,20 @@ interface MissingIndex {
|
|
|
44
48
|
/** The declared key, JSON-stringified — stable to log and to diff. */
|
|
45
49
|
key: string;
|
|
46
50
|
}
|
|
51
|
+
/** A live index on the declared keys whose correctness options differ from the declaration. */
|
|
52
|
+
interface IncompatibleIndex extends MissingIndex {
|
|
53
|
+
/** Declared `unique` — a mismatch here can already have admitted duplicates. */
|
|
54
|
+
unique: boolean;
|
|
55
|
+
/** One line per differing option: `unique: declared true, live false`. */
|
|
56
|
+
differences: string[];
|
|
57
|
+
}
|
|
47
58
|
interface IndexVerifyReport {
|
|
48
59
|
modelsChecked: number;
|
|
49
60
|
/** Declared `unique` indexes with nothing behind them. Duplicates can already exist. */
|
|
50
61
|
missingUnique: MissingIndex[];
|
|
51
62
|
missingRegular: MissingIndex[];
|
|
63
|
+
/** Same keys, different semantics — e.g. declared unique, built non-unique. */
|
|
64
|
+
incompatible: IncompatibleIndex[];
|
|
52
65
|
/** Models whose indexes could not be read — collection absent, or no permission. */
|
|
53
66
|
unreadable: string[];
|
|
54
67
|
}
|
|
@@ -66,4 +79,4 @@ declare function verifyIndexes(connection: IndexVerifiableConnection, options?:
|
|
|
66
79
|
/** One-line summary for a boot log. */
|
|
67
80
|
declare function formatIndexReport(report: IndexVerifyReport): string;
|
|
68
81
|
//#endregion
|
|
69
|
-
export { IndexVerifiableConnection, IndexVerifiableModel, IndexVerifyReport, MissingIndex, VerifyIndexesOptions, formatIndexReport, verifyIndexes };
|
|
82
|
+
export { IncompatibleIndex, IndexVerifiableConnection, IndexVerifiableModel, IndexVerifyReport, MissingIndex, VerifyIndexesOptions, formatIndexReport, verifyIndexes };
|
|
@@ -22,12 +22,45 @@ function textFields(key) {
|
|
|
22
22
|
const fields = Object.entries(key).filter(([, dir]) => dir === "text").map(([field]) => field);
|
|
23
23
|
return fields.length > 0 ? fields.sort() : null;
|
|
24
24
|
}
|
|
25
|
+
/** Order-free JSON, so `{a:1,b:2}` equals `{b:2,a:1}` — option objects are not order-sensitive. */
|
|
26
|
+
function canonical(value) {
|
|
27
|
+
if (value === null || typeof value !== "object") return JSON.stringify(value) ?? "undefined";
|
|
28
|
+
if (Array.isArray(value)) return `[${value.map(canonical).join(",")}]`;
|
|
29
|
+
return `{${Object.keys(value).sort().map((k) => `${JSON.stringify(k)}:${canonical(value[k])}`).join(",")}}`;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* The options that change what an index GUARANTEES, compared declared → live. Absent means
|
|
33
|
+
* off (`unique`, `sparse`) or none (`partialFilterExpression`, `expireAfterSeconds`). A collation
|
|
34
|
+
* is compared on the DECLARED fields only: the server fills in every default it does not store.
|
|
35
|
+
*/
|
|
36
|
+
function optionDifferences(declared, live) {
|
|
37
|
+
const diffs = [];
|
|
38
|
+
const flag = (name) => {
|
|
39
|
+
const d = declared[name] === true;
|
|
40
|
+
const l = live[name] === true;
|
|
41
|
+
if (d !== l) diffs.push(`${name}: declared ${d}, live ${l}`);
|
|
42
|
+
};
|
|
43
|
+
flag("unique");
|
|
44
|
+
flag("sparse");
|
|
45
|
+
for (const name of ["partialFilterExpression", "expireAfterSeconds"]) {
|
|
46
|
+
const d = declared[name] === void 0 ? "none" : canonical(declared[name]);
|
|
47
|
+
const l = live[name] === void 0 ? "none" : canonical(live[name]);
|
|
48
|
+
if (d !== l) diffs.push(`${name}: declared ${d}, live ${l}`);
|
|
49
|
+
}
|
|
50
|
+
const dColl = declared.collation;
|
|
51
|
+
const lColl = live.collation;
|
|
52
|
+
if (dColl) {
|
|
53
|
+
if (Object.keys(dColl).filter((k) => canonical(dColl[k]) !== canonical(lColl?.[k])).length > 0) diffs.push(`collation: declared ${canonical(dColl)}, live ${lColl ? canonical(lColl) : "none"}`);
|
|
54
|
+
} else if (lColl && lColl.locale !== "simple") diffs.push(`collation: declared none, live ${canonical(lColl)}`);
|
|
55
|
+
return diffs;
|
|
56
|
+
}
|
|
25
57
|
async function verifyIndexes(connection, options = {}) {
|
|
26
58
|
const include = options.includeModel ?? (() => true);
|
|
27
59
|
const report = {
|
|
28
60
|
modelsChecked: 0,
|
|
29
61
|
missingUnique: [],
|
|
30
62
|
missingRegular: [],
|
|
63
|
+
incompatible: [],
|
|
31
64
|
unreadable: []
|
|
32
65
|
};
|
|
33
66
|
for (const name of connection.modelNames().sort()) {
|
|
@@ -39,8 +72,12 @@ async function verifyIndexes(connection, options = {}) {
|
|
|
39
72
|
let existingText;
|
|
40
73
|
try {
|
|
41
74
|
const live = await model.collection.listIndexes().toArray();
|
|
42
|
-
existing =
|
|
43
|
-
existingText =
|
|
75
|
+
existing = /* @__PURE__ */ new Map();
|
|
76
|
+
existingText = /* @__PURE__ */ new Map();
|
|
77
|
+
for (const index of live) {
|
|
78
|
+
const [map, k] = index.weights ? [existingText, JSON.stringify(Object.keys(index.weights).sort())] : [existing, normalizeKey(index.key)];
|
|
79
|
+
map.set(k, [...map.get(k) ?? [], index]);
|
|
80
|
+
}
|
|
44
81
|
} catch {
|
|
45
82
|
report.unreadable.push(name);
|
|
46
83
|
continue;
|
|
@@ -48,15 +85,24 @@ async function verifyIndexes(connection, options = {}) {
|
|
|
48
85
|
report.modelsChecked += 1;
|
|
49
86
|
for (const [key, indexOptions] of declared) {
|
|
50
87
|
const asText = textFields(key);
|
|
51
|
-
|
|
52
|
-
if (existingText.has(JSON.stringify(asText))) continue;
|
|
53
|
-
} else if (existing.has(normalizeKey(key))) continue;
|
|
88
|
+
const sameKey = asText ? existingText.get(JSON.stringify(asText)) : existing.get(normalizeKey(key));
|
|
54
89
|
const entry = {
|
|
55
90
|
model: name,
|
|
56
91
|
collection: model.collection.name,
|
|
57
92
|
key: JSON.stringify(key)
|
|
58
93
|
};
|
|
59
|
-
|
|
94
|
+
const opts = indexOptions ?? {};
|
|
95
|
+
if (sameKey && sameKey.length > 0) {
|
|
96
|
+
const diffsPerCandidate = sameKey.map((live) => optionDifferences(opts, live));
|
|
97
|
+
if (diffsPerCandidate.some((d) => d.length === 0)) continue;
|
|
98
|
+
report.incompatible.push({
|
|
99
|
+
...entry,
|
|
100
|
+
unique: opts.unique === true,
|
|
101
|
+
differences: diffsPerCandidate[0] ?? []
|
|
102
|
+
});
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
if (opts.unique === true) report.missingUnique.push(entry);
|
|
60
106
|
else report.missingRegular.push(entry);
|
|
61
107
|
}
|
|
62
108
|
}
|
|
@@ -67,7 +113,8 @@ function formatIndexReport(report) {
|
|
|
67
113
|
const parts = [
|
|
68
114
|
`${report.modelsChecked} models checked`,
|
|
69
115
|
`missing unique: ${report.missingUnique.length}`,
|
|
70
|
-
`missing regular: ${report.missingRegular.length}
|
|
116
|
+
`missing regular: ${report.missingRegular.length}`,
|
|
117
|
+
`incompatible: ${report.incompatible.length}`
|
|
71
118
|
];
|
|
72
119
|
if (report.unreadable.length > 0) parts.push(`unreadable: ${report.unreadable.length}`);
|
|
73
120
|
return `[index-check] ${parts.join(" — ")}`;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { g as ObjectId } from "./pagination-BpIv2bdS.mjs";
|
|
2
|
-
import { l as Plugin, p as RepositoryContext } from "./repository-
|
|
2
|
+
import { l as Plugin, p as RepositoryContext } from "./repository-odl03582.mjs";
|
|
3
3
|
import mongoose, { Connection } from "mongoose";
|
|
4
4
|
import { CacheAdapter as CacheAdapter$1, CacheOptions, RepositoryCacheHandle as RepositoryCacheHandle$1, RepositoryCachePluginOptions, cachePlugin } from "@classytic/repo-core/cache";
|
|
5
5
|
import { TenantConfig } from "@classytic/repo-core/tenant";
|
|
@@ -307,6 +307,22 @@ interface CustomIdOptions {
|
|
|
307
307
|
* });
|
|
308
308
|
*/
|
|
309
309
|
declare function getNextSequence(counterKey: string, increment?: number, connection?: mongoose.Connection, session?: mongoose.ClientSession): Promise<number>;
|
|
310
|
+
/**
|
|
311
|
+
* Current value of a counter, or `null` when it has never been created.
|
|
312
|
+
* `null` and `0` differ: only a missing counter needs seeding.
|
|
313
|
+
*/
|
|
314
|
+
declare function readSequence(counterKey: string, connection?: mongoose.Connection): Promise<number | null>;
|
|
315
|
+
/**
|
|
316
|
+
* Raise a counter to at least `floor` — never lowers it. Atomic (`$max` +
|
|
317
|
+
* upsert), so concurrent seeders converge on the highest floor.
|
|
318
|
+
*
|
|
319
|
+
* Use before a counter's first increment when ids were already issued under
|
|
320
|
+
* another key (e.g. moving from a global to a per-tenant sequence); otherwise
|
|
321
|
+
* the new counter restarts at 1 and re-issues taken ids.
|
|
322
|
+
*
|
|
323
|
+
* @returns The counter's value after the raise.
|
|
324
|
+
*/
|
|
325
|
+
declare function ensureSequenceAtLeast(counterKey: string, floor: number, connection?: mongoose.Connection): Promise<number>;
|
|
310
326
|
interface SequentialIdOptions {
|
|
311
327
|
/** Prefix string (e.g., 'INV', 'ORD') */
|
|
312
328
|
prefix: string;
|
|
@@ -638,4 +654,4 @@ declare function adminBypass(options: {
|
|
|
638
654
|
adminRoles: readonly string[];
|
|
639
655
|
}): (context: RepositoryContext, operation: string) => boolean;
|
|
640
656
|
//#endregion
|
|
641
|
-
export {
|
|
657
|
+
export { EnsureAuditTrailReadyOptions as A, AuditQueryOptions as C, AuditTrailOptions as D, AuditTrailMode as E, ensureAuditTrailReady as M, AuditTrailQuery as O, AuditOperation as S, AuditTrailMethods as T, CacheOptions as _, DateSequentialIdOptions as a, cachePlugin as b, SequentialIdOptions as c, ensureSequenceAtLeast as d, getNextSequence as f, CacheAdapter$1 as g, sequentialId as h, CustomIdOptions as i, auditTrailPlugin as j, AuditTrailQueryOptions as k, customIdPlugin as l, readSequence as m, adminBypass as n, IdGenerator as o, prefixedId as p, multiTenantPlugin as r, PrefixedIdOptions as s, MultiTenantOptions as t, dateSequentialId as u, RepositoryCacheHandle$1 as v, AuditQueryResult as w, AuditEntry as x, RepositoryCachePluginOptions as y };
|
package/dist/outbox/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { r as Repository } from "../Repository-CR8Y9g2E.mjs";
|
|
2
2
|
import { Connection, Model } from "mongoose";
|
|
3
3
|
//#region src/outbox/index.d.ts
|
|
4
4
|
/** Delivery states. `dead_letter` is terminal and operator-visible. */
|
package/dist/outbox/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { n as Repository } from "../Repository-
|
|
1
|
+
import { n as Repository } from "../Repository-BMgPSkqZ.mjs";
|
|
2
2
|
import { n as batchOperationsPlugin, t as methodRegistryPlugin } from "../method-registry.plugin-C63pGn1d.mjs";
|
|
3
3
|
import { Schema } from "mongoose";
|
|
4
4
|
//#region src/outbox/index.ts
|
|
@@ -172,12 +172,14 @@ function createOutboxModel(connection, options = {}) {
|
|
|
172
172
|
*
|
|
173
173
|
* Mongo rejects `unique` combined with a `sparse: true` + partial filter ("cannot mix"), and
|
|
174
174
|
* the failure is that the index silently never builds — so duplicate events insert happily
|
|
175
|
-
* and the outbox's idempotency guarantee is gone with nothing in the logs.
|
|
176
|
-
*
|
|
175
|
+
* and the outbox's idempotency guarantee is gone with nothing in the logs. `$gte: ''` indexes
|
|
176
|
+
* only rows that carry a string key — the same rows as `$type: 'string'`, but the dedupe
|
|
177
|
+
* lookup by key can use it (a `$type` filter is never implied by an equality query).
|
|
177
178
|
*/
|
|
178
179
|
schema.index({ dedupeKey: 1 }, {
|
|
180
|
+
name: "dedupeKey_str",
|
|
179
181
|
unique: true,
|
|
180
|
-
partialFilterExpression: { dedupeKey: { $
|
|
182
|
+
partialFilterExpression: { dedupeKey: { $gte: "" } }
|
|
181
183
|
});
|
|
182
184
|
/**
|
|
183
185
|
* The DEAD-LETTER read. `getDeadLettered` is part of the `OutboxStore` contract and is what
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { t as PaginationEngine } from "../PaginationEngine-
|
|
1
|
+
import { t as PaginationEngine } from "../PaginationEngine-Dvl8jcRY.mjs";
|
|
2
2
|
export { PaginationEngine };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { _ as PopulateSpec, g as ObjectId, x as SortSpec, y as SelectSpec } from "./pagination-BpIv2bdS.mjs";
|
|
2
|
-
import { h as RepositoryInstance, p as RepositoryContext } from "./repository-
|
|
2
|
+
import { h as RepositoryInstance, p as RepositoryContext } from "./repository-odl03582.mjs";
|
|
3
3
|
import { OffsetPaginationResult } from "@classytic/repo-core/pagination";
|
|
4
4
|
//#region src/types/plugin-options.d.ts
|
|
5
5
|
/** Field preset configuration (field-filter plugin) */
|
|
@@ -69,6 +69,9 @@ interface SoftDeleteRepository<TDoc = unknown> {
|
|
|
69
69
|
*/
|
|
70
70
|
restore(id: string | ObjectId, options?: {
|
|
71
71
|
session?: unknown;
|
|
72
|
+
organizationId?: unknown;
|
|
73
|
+
bypassTenant?: boolean;
|
|
74
|
+
[key: string]: unknown;
|
|
72
75
|
}): Promise<TDoc>;
|
|
73
76
|
/**
|
|
74
77
|
* Get all soft-deleted documents
|
|
@@ -109,6 +112,17 @@ interface CascadeRelation {
|
|
|
109
112
|
model?: string;
|
|
110
113
|
/** Foreign key field in the related model that references the deleted document */
|
|
111
114
|
foreignKey: string;
|
|
115
|
+
/**
|
|
116
|
+
* WHICH parent field `foreignKey` stores. Default: the parent repository's
|
|
117
|
+
* `idField` (`_id` unless the repository declares another).
|
|
118
|
+
*
|
|
119
|
+
* Set it when the two differ. An order repository addressed by `orderNumber`
|
|
120
|
+
* whose children store the order's `_id` declares `parentKey: '_id'`;
|
|
121
|
+
* without it, `delete('ORD-2026-0001')` cascaded with the order NUMBER and
|
|
122
|
+
* every child delete failed on the ObjectId cast. The cascade reads the
|
|
123
|
+
* parent's value for this field before the delete, under the same session.
|
|
124
|
+
*/
|
|
125
|
+
parentKey?: string;
|
|
112
126
|
/** Whether to use soft delete if available (default: follows parent behavior) */
|
|
113
127
|
softDelete?: boolean;
|
|
114
128
|
/**
|
package/dist/plugins/index.d.mts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { S as RepositoryPluginName, b as MONGOKIT_PLUGIN_NAMES, l as Plugin, x as MongoKitPluginName } from "../repository-
|
|
2
|
-
import { A as
|
|
3
|
-
import { A as IdempotencyStoreMethods, B as
|
|
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";
|
|
4
4
|
import { ChangeLogStore } from "@classytic/repo-core/sync";
|
|
5
5
|
//#region src/plugins/change-log.plugin.d.ts
|
|
6
6
|
interface ChangeLogPluginOptions {
|
|
@@ -12,6 +12,11 @@ interface ChangeLogPluginOptions {
|
|
|
12
12
|
tenantField?: string;
|
|
13
13
|
/** Monotonic per-doc version field. Default `version` (kernel convention). */
|
|
14
14
|
versionField?: string;
|
|
15
|
+
/**
|
|
16
|
+
* The document as a subscriber may see it — fields picked, secrets (a cost price) dropped. Tenant
|
|
17
|
+
* and version are still read from the whole document. Default: the whole document.
|
|
18
|
+
*/
|
|
19
|
+
project?: (doc: Record<string, unknown>) => Record<string, unknown>;
|
|
15
20
|
}
|
|
16
21
|
declare function changeLogPlugin(options: ChangeLogPluginOptions): Plugin;
|
|
17
22
|
//#endregion
|
package/dist/plugins/index.mjs
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
|
-
import {
|
|
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";
|
|
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-
|
|
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";
|
|
4
4
|
//#region src/plugins/change-log.plugin.ts
|
|
5
5
|
const PLUGIN_NAME = "changeLog";
|
|
6
|
+
/** Marks a repository already captured: the store and scope it writes to. */
|
|
7
|
+
const CAPTURED = Symbol.for("mongokit.changeLog.capture");
|
|
8
|
+
/** Where `before:updateMany` / `before:deleteMany` leave the ids they matched for the after hook. */
|
|
9
|
+
const MATCHED_IDS = Symbol("changeLog.matchedIds");
|
|
6
10
|
function isSkipped(context) {
|
|
7
11
|
const list = context.skipPlugins;
|
|
8
12
|
return Array.isArray(list) && list.includes(PLUGIN_NAME);
|
|
9
13
|
}
|
|
10
14
|
function changeLogPlugin(options) {
|
|
11
|
-
const { store, scope, tenantField = "organizationId", versionField = "version" } = options;
|
|
15
|
+
const { store, scope, tenantField = "organizationId", versionField = "version", project } = options;
|
|
12
16
|
const toRecord = (doc) => {
|
|
13
17
|
if (!doc || typeof doc !== "object") return null;
|
|
14
18
|
const d = doc;
|
|
@@ -21,51 +25,120 @@ function changeLogPlugin(options) {
|
|
|
21
25
|
return updatedAt instanceof Date ? updatedAt.getTime() : 0;
|
|
22
26
|
};
|
|
23
27
|
const tenantOf = (doc, context) => {
|
|
24
|
-
const raw = doc[tenantField] ?? context.organizationId;
|
|
28
|
+
const raw = doc?.[tenantField] ?? context.organizationId;
|
|
25
29
|
return raw === void 0 || raw === null ? void 0 : String(raw);
|
|
26
30
|
};
|
|
27
|
-
const
|
|
31
|
+
const appendOptions = (context) => context.session !== void 0 ? { session: context.session } : void 0;
|
|
32
|
+
const upsert = async (doc, context) => {
|
|
28
33
|
const record = toRecord(doc);
|
|
29
34
|
if (!record || record._id === void 0) return;
|
|
30
35
|
const tenantId = tenantOf(record, context);
|
|
31
36
|
await store.append({
|
|
32
37
|
scope,
|
|
33
38
|
docId: String(record._id),
|
|
34
|
-
op,
|
|
39
|
+
op: "upsert",
|
|
35
40
|
version: versionOf(record),
|
|
36
|
-
|
|
41
|
+
doc: project ? project(record) : record,
|
|
37
42
|
...tenantId !== void 0 ? { tenantId } : {}
|
|
38
|
-
}, context
|
|
43
|
+
}, appendOptions(context));
|
|
44
|
+
};
|
|
45
|
+
const tombstone = async (docId, context) => {
|
|
46
|
+
if (docId === void 0 || docId === null) return;
|
|
47
|
+
const tenantId = tenantOf(null, context);
|
|
48
|
+
await store.append({
|
|
49
|
+
scope,
|
|
50
|
+
docId: String(docId),
|
|
51
|
+
op: "delete",
|
|
52
|
+
version: 0,
|
|
53
|
+
...tenantId !== void 0 ? { tenantId } : {}
|
|
54
|
+
}, appendOptions(context));
|
|
39
55
|
};
|
|
40
56
|
return {
|
|
41
57
|
name: PLUGIN_NAME,
|
|
42
58
|
apply(repo) {
|
|
59
|
+
const marked = repo;
|
|
60
|
+
const existing = marked[CAPTURED];
|
|
61
|
+
if (existing) {
|
|
62
|
+
if (existing.store === store && existing.scope === scope) return;
|
|
63
|
+
throw new Error(`[mongokit] changeLogPlugin is already capturing this repository as "${existing.scope}"`);
|
|
64
|
+
}
|
|
65
|
+
marked[CAPTURED] = {
|
|
66
|
+
store,
|
|
67
|
+
scope
|
|
68
|
+
};
|
|
69
|
+
/** The whole document as committed in this session — never a projection. */
|
|
70
|
+
const reread = async (id, context) => {
|
|
71
|
+
const query = repo.Model.findById(id).lean();
|
|
72
|
+
if (context.session !== void 0) query.session(context.session);
|
|
73
|
+
return query.exec();
|
|
74
|
+
};
|
|
75
|
+
const matchingIds = async (context, filter) => {
|
|
76
|
+
const query = repo.Model.find(filter ?? {}).select({ _id: 1 }).lean();
|
|
77
|
+
if (context.session !== void 0) query.session(context.session);
|
|
78
|
+
return (await query.exec()).map((d) => d._id);
|
|
79
|
+
};
|
|
43
80
|
repo.on("after:create", async ({ context, result }) => {
|
|
44
|
-
if (isSkipped(context)
|
|
45
|
-
await capture("upsert", result, context);
|
|
81
|
+
if (!isSkipped(context) && result) await upsert(result, context);
|
|
46
82
|
});
|
|
47
83
|
repo.on("after:createMany", async ({ context, result }) => {
|
|
48
84
|
if (isSkipped(context) || !Array.isArray(result)) return;
|
|
49
|
-
for (const doc of result) await
|
|
85
|
+
for (const doc of result) await upsert(doc, context);
|
|
50
86
|
});
|
|
51
87
|
repo.on("after:update", async ({ context, result }) => {
|
|
52
|
-
if (isSkipped(context)
|
|
53
|
-
|
|
88
|
+
if (!isSkipped(context) && result) await upsert(result, context);
|
|
89
|
+
});
|
|
90
|
+
for (const op of [
|
|
91
|
+
"claim",
|
|
92
|
+
"claimVersion",
|
|
93
|
+
"findOneAndUpdate",
|
|
94
|
+
"getOrCreate",
|
|
95
|
+
"restore"
|
|
96
|
+
]) repo.on(`after:${op}`, async ({ context, result }) => {
|
|
97
|
+
if (isSkipped(context)) return;
|
|
98
|
+
const id = toRecord(result)?._id;
|
|
99
|
+
if (id === void 0) return;
|
|
100
|
+
const doc = await reread(id, context);
|
|
101
|
+
if (doc) await upsert(doc, context);
|
|
102
|
+
else await tombstone(id, context);
|
|
103
|
+
});
|
|
104
|
+
for (const op of ["updateMany", "deleteMany"]) repo.on(`before:${op}`, async (context) => {
|
|
105
|
+
if (!isSkipped(context)) context[MATCHED_IDS] = await matchingIds(context, context.query);
|
|
106
|
+
});
|
|
107
|
+
/** Each re-read document as an upsert, or a tombstone where it is gone. */
|
|
108
|
+
const capture = async (ids, context) => {
|
|
109
|
+
for (const id of ids) {
|
|
110
|
+
const doc = await reread(id, context);
|
|
111
|
+
if (doc) await upsert(doc, context);
|
|
112
|
+
else await tombstone(id, context);
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
repo.on("after:updateMany", async ({ context }) => {
|
|
116
|
+
if (!isSkipped(context)) await capture(context[MATCHED_IDS] ?? [], context);
|
|
117
|
+
});
|
|
118
|
+
repo.on("after:anonymize", async ({ context, result }) => {
|
|
119
|
+
if (!isSkipped(context)) await capture(result.ids, context);
|
|
120
|
+
});
|
|
121
|
+
repo.on("after:deleteMany", async ({ context }) => {
|
|
122
|
+
if (!isSkipped(context)) await capture(context[MATCHED_IDS] ?? [], context);
|
|
54
123
|
});
|
|
55
124
|
repo.on("after:delete", async ({ context, result }) => {
|
|
56
125
|
if (isSkipped(context) || !result) return;
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
if (
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
126
|
+
await tombstone(result.id ?? context.id, context);
|
|
127
|
+
});
|
|
128
|
+
repo.on("before:bulkWrite", async (context) => {
|
|
129
|
+
if (isSkipped(context)) return;
|
|
130
|
+
const ids = [];
|
|
131
|
+
for (const op of context.operations ?? []) {
|
|
132
|
+
const [kind, spec] = Object.entries(op)[0] ?? [];
|
|
133
|
+
if (!kind || !spec) continue;
|
|
134
|
+
if (kind === "insertOne") ids.push(spec.document?._id);
|
|
135
|
+
else ids.push(...await matchingIds(context, spec.filter));
|
|
136
|
+
}
|
|
137
|
+
context[MATCHED_IDS] = ids.filter((id) => id !== void 0);
|
|
138
|
+
});
|
|
139
|
+
repo.on("after:bulkWrite", async ({ context, result }) => {
|
|
140
|
+
if (isSkipped(context)) return;
|
|
141
|
+
await capture([...context[MATCHED_IDS] ?? [], ...Object.values(result?.upsertedIds ?? {})], context);
|
|
69
142
|
});
|
|
70
143
|
}
|
|
71
144
|
};
|
|
@@ -363,8 +363,23 @@ interface RepositoryContext {
|
|
|
363
363
|
_cacheResolved?: unknown;
|
|
364
364
|
/** Cache hit status (internal — `'fresh'` or `'stale'`) */
|
|
365
365
|
_cacheStatus?: 'fresh' | 'stale' | 'miss' | 'disabled' | 'bypass';
|
|
366
|
-
/**
|
|
367
|
-
|
|
366
|
+
/**
|
|
367
|
+
* Parent key values captured before a delete, by `parentKey` (internal —
|
|
368
|
+
* cascade plugin). Restrict, detach and cascade all read these.
|
|
369
|
+
*/
|
|
370
|
+
_cascadeParentKeys?: Map<string, unknown[]>;
|
|
371
|
+
/**
|
|
372
|
+
* Memo shared by every hook of THIS operation: N plugins that need the same read pay for
|
|
373
|
+
* one. Scoped to the operation, so it can never serve a value across writes. A rejected
|
|
374
|
+
* load is forgotten, so the next caller retries.
|
|
375
|
+
*/
|
|
376
|
+
memo?: <T>(key: string, load: () => Promise<T>) => Promise<T>;
|
|
377
|
+
/**
|
|
378
|
+
* The persisted document an id-addressed operation targets, as it is BEFORE the write,
|
|
379
|
+
* read once under the operation's session (not tenant-filtered — the write's own filter
|
|
380
|
+
* decides whether it applies). Use this instead of `Model.findById(context.id)` in a hook.
|
|
381
|
+
*/
|
|
382
|
+
loadTarget?: () => Promise<Record<string, unknown> | null>;
|
|
368
383
|
/** Custom context data from plugins */
|
|
369
384
|
[key: string]: unknown;
|
|
370
385
|
}
|
|
@@ -528,6 +543,9 @@ type AllPluginMethods<TDoc> = {
|
|
|
528
543
|
deleteSubdocument(parentId: string | ObjectId, arrayPath: DocField<TDoc>, subId: string | ObjectId, options?: Record<string, unknown>): Promise<TDoc>;
|
|
529
544
|
restore(id: string | ObjectId, options?: {
|
|
530
545
|
session?: unknown;
|
|
546
|
+
organizationId?: unknown;
|
|
547
|
+
bypassTenant?: boolean;
|
|
548
|
+
[key: string]: unknown;
|
|
531
549
|
}): Promise<TDoc>;
|
|
532
550
|
getDeleted(params?: {
|
|
533
551
|
filters?: Record<string, unknown>;
|
|
@@ -0,0 +1,40 @@
|
|
|
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
|
+
declare function createChangeLogStore(models: ChangeLogModels): ChangeLogStore;
|
|
18
|
+
interface CommandStreamModelOptions {
|
|
19
|
+
/** Collection prefix: `<prefix>_streams`, `<prefix>_verdicts`, `<prefix>_aliases`. Default `sync`. */
|
|
20
|
+
prefix?: string;
|
|
21
|
+
/** Mongoose model name prefix. Default `SyncStream`. Must be unique per connection. */
|
|
22
|
+
modelName?: string;
|
|
23
|
+
}
|
|
24
|
+
interface CommandStreamModels {
|
|
25
|
+
streams: Model<Record<string, unknown>>;
|
|
26
|
+
verdicts: Model<Record<string, unknown>>;
|
|
27
|
+
aliases: Model<Record<string, unknown>>;
|
|
28
|
+
}
|
|
29
|
+
declare function createCommandStreamModels(connection: Connection, options?: CommandStreamModelOptions): CommandStreamModels;
|
|
30
|
+
interface MongoCommandStreamTx extends CommandStreamTx {
|
|
31
|
+
readonly session: ClientSession;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* The mongo {@link CommandStreamStore} (replica set). Proven by `runCommandStreamStoreConformance`.
|
|
35
|
+
* Concurrent transactions on one stream write-conflict on its document and `withTransaction`
|
|
36
|
+
* retries the loser, which then reads the winner's watermark.
|
|
37
|
+
*/
|
|
38
|
+
declare function createCommandStreamStore(connection: Pick<Connection, 'startSession'>, models: CommandStreamModels, changes: ChangeLogStore): CommandStreamStore<MongoCommandStreamTx>;
|
|
39
|
+
//#endregion
|
|
40
|
+
export { ChangeLogModelOptions, ChangeLogModels, CommandStreamModelOptions, CommandStreamModels, MongoCommandStreamTx, createChangeLogModels, createChangeLogStore, createCommandStreamModels, createCommandStreamStore };
|