@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
|
@@ -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(" — ")}`;
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { t as createError } from "./error-BuByZwKU.mjs";
|
|
2
|
+
import mongoose from "mongoose";
|
|
3
|
+
//#region src/repository/join-scope.ts
|
|
4
|
+
/**
|
|
5
|
+
* Join scope — a joined collection is read under ITS OWN policy, or not at all.
|
|
6
|
+
*
|
|
7
|
+
* Policy plugins (`multiTenantPlugin`, `softDeletePlugin`) declare a scope rule on the MODEL they
|
|
8
|
+
* are bound to, and every repository marks its model governed. {@link scopeJoins} walks a
|
|
9
|
+
* pipeline (nested sub-pipelines and `$facet` branches included) and, for each `$lookup`,
|
|
10
|
+
* `$unionWith` and `$graphLookup`, resolves `from` to the registered models that own that
|
|
11
|
+
* collection, then prepends the union of their rules (fail closed). It REFUSES, with a closed
|
|
12
|
+
* code, a `from` no model owns (the silent-empty-join: a wrong literal matches nothing and raises
|
|
13
|
+
* nothing) and a collection no repository governs. `unscopedJoins` names collections the caller
|
|
14
|
+
* asserts are company-wide; a collection with a declared rule cannot be unscoped that way —
|
|
15
|
+
* `bypassTenant` is the explicit cross-tenant form.
|
|
16
|
+
*/
|
|
17
|
+
const JOIN_ERROR_CODES = {
|
|
18
|
+
UNKNOWN_COLLECTION: "mongokit.join.unknown_collection",
|
|
19
|
+
UNGOVERNED_COLLECTION: "mongokit.join.ungoverned_collection",
|
|
20
|
+
UNSCOPE_REFUSED: "mongokit.join.unscope_refused",
|
|
21
|
+
UNRESOLVABLE_FROM: "mongokit.join.unresolvable_from"
|
|
22
|
+
};
|
|
23
|
+
const RULES = Symbol.for("@classytic/mongokit/collection-scope-rules");
|
|
24
|
+
const GOVERNED = Symbol.for("@classytic/mongokit/governed-model");
|
|
25
|
+
const TENANT_FIELDS = Symbol.for("@classytic/mongokit/tenant-fields");
|
|
26
|
+
/** Called by a policy plugin at bind: reads of this model's collection carry `rule`. */
|
|
27
|
+
function declareCollectionScope(model, rule, meta = {}) {
|
|
28
|
+
const holder = model;
|
|
29
|
+
if (!holder[RULES]) Object.defineProperty(holder, RULES, {
|
|
30
|
+
value: /* @__PURE__ */ new Set(),
|
|
31
|
+
enumerable: false
|
|
32
|
+
});
|
|
33
|
+
holder[RULES]?.add(rule);
|
|
34
|
+
if (meta.tenantField) {
|
|
35
|
+
if (!holder[TENANT_FIELDS]) Object.defineProperty(holder, TENANT_FIELDS, {
|
|
36
|
+
value: /* @__PURE__ */ new Set(),
|
|
37
|
+
enumerable: false
|
|
38
|
+
});
|
|
39
|
+
holder[TENANT_FIELDS]?.add(meta.tenantField);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/** The tenant fields declared on this model by its repositories' tenant plugins. */
|
|
43
|
+
function tenantFieldsOf(model) {
|
|
44
|
+
return [...model[TENANT_FIELDS] ?? []];
|
|
45
|
+
}
|
|
46
|
+
/** Called by every Repository: this model's scope is known (possibly none). */
|
|
47
|
+
function markGoverned(model) {
|
|
48
|
+
const holder = model;
|
|
49
|
+
if (!holder[GOVERNED]) Object.defineProperty(holder, GOVERNED, {
|
|
50
|
+
value: true,
|
|
51
|
+
enumerable: false
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
function owners(connection, collection) {
|
|
55
|
+
return Object.values(connection.models).filter((m) => m.collection?.collectionName === collection);
|
|
56
|
+
}
|
|
57
|
+
function resemblance(connection, from) {
|
|
58
|
+
const pluralize = mongoose.pluralize();
|
|
59
|
+
const target = from.toLowerCase();
|
|
60
|
+
return Object.values(connection.models).filter((m) => {
|
|
61
|
+
const name = m.modelName.toLowerCase();
|
|
62
|
+
return name === target || `${name}s` === target || pluralize?.(m.modelName) === target;
|
|
63
|
+
}).map((m) => `model '${m.modelName}' lives in collection '${m.collection?.collectionName}'`).join("; ");
|
|
64
|
+
}
|
|
65
|
+
/** The predicate a join into `from` must carry, `undefined` for none; throws when unknown. */
|
|
66
|
+
function scopeForCollection(from, env) {
|
|
67
|
+
if (typeof from !== "string" || from.length === 0) throw createError(500, `[mongokit] a join's 'from' must be a collection name, got ${JSON.stringify(from)}`, {
|
|
68
|
+
code: JOIN_ERROR_CODES.UNRESOLVABLE_FROM,
|
|
69
|
+
meta: { from }
|
|
70
|
+
});
|
|
71
|
+
const unscoped = env.unscopedJoins?.includes(from) === true;
|
|
72
|
+
const found = owners(env.connection, from);
|
|
73
|
+
if (found.length === 0) {
|
|
74
|
+
if (unscoped) return void 0;
|
|
75
|
+
const hint = resemblance(env.connection, from);
|
|
76
|
+
throw createError(500, `[mongokit] join into '${from}': no registered model owns that collection, so the join would match nothing.${hint ? ` Did you mean: ${hint}?` : ""} Take 'from' from Model.collection.name.`, {
|
|
77
|
+
code: JOIN_ERROR_CODES.UNKNOWN_COLLECTION,
|
|
78
|
+
meta: {
|
|
79
|
+
from,
|
|
80
|
+
suggestion: hint || void 0
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
const governed = found.some((m) => m[GOVERNED] === true);
|
|
85
|
+
const rules = /* @__PURE__ */ new Set();
|
|
86
|
+
for (const m of found) for (const r of m[RULES] ?? []) rules.add(r);
|
|
87
|
+
if (!governed && rules.size === 0) {
|
|
88
|
+
if (unscoped) return void 0;
|
|
89
|
+
throw createError(500, `[mongokit] join into '${from}': no repository governs it, so its tenant/soft-delete scope is unknown. Build its repository, or list it in unscopedJoins if it is company-wide.`, {
|
|
90
|
+
code: JOIN_ERROR_CODES.UNGOVERNED_COLLECTION,
|
|
91
|
+
meta: {
|
|
92
|
+
from,
|
|
93
|
+
models: found.map((m) => m.modelName)
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
const predicates = [];
|
|
98
|
+
for (const rule of rules) {
|
|
99
|
+
const p = rule(env.context, env.operation);
|
|
100
|
+
if (p && Object.keys(p).length > 0) predicates.push(p);
|
|
101
|
+
}
|
|
102
|
+
if (predicates.length === 0) return void 0;
|
|
103
|
+
if (unscoped) throw createError(500, `[mongokit] join into '${from}': it is scoped by its repository and cannot be listed in unscopedJoins. Use bypassTenant for a deliberate cross-tenant read.`, {
|
|
104
|
+
code: JOIN_ERROR_CODES.UNSCOPE_REFUSED,
|
|
105
|
+
meta: { from }
|
|
106
|
+
});
|
|
107
|
+
return predicates.length === 1 ? predicates[0] : { $and: predicates };
|
|
108
|
+
}
|
|
109
|
+
function scopedPipeline(sub, scope, env) {
|
|
110
|
+
const walked = walk(Array.isArray(sub) ? sub : [], env);
|
|
111
|
+
return scope ? [{ $match: scope }, ...walked] : walked;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Return `pipeline` with every join scoped; throws on an unknown or ungoverned `from`. The result
|
|
115
|
+
* is the one documented narrowing of a caller's stage array to mongoose's stage union (stage
|
|
116
|
+
* validity is the server's call); `aggregatePipeline` no longer carries its own.
|
|
117
|
+
*/
|
|
118
|
+
function scopeJoins(pipeline, env) {
|
|
119
|
+
return walk(pipeline, env);
|
|
120
|
+
}
|
|
121
|
+
function walk(pipeline, env) {
|
|
122
|
+
return pipeline.map((raw) => {
|
|
123
|
+
const stage = raw;
|
|
124
|
+
if (stage.$lookup) {
|
|
125
|
+
const l = stage.$lookup;
|
|
126
|
+
if (l.from === void 0) return { $lookup: {
|
|
127
|
+
...l,
|
|
128
|
+
pipeline: scopedPipeline(l.pipeline, void 0, env)
|
|
129
|
+
} };
|
|
130
|
+
const scope = scopeForCollection(l.from, env);
|
|
131
|
+
if (!scope && l.pipeline === void 0) return stage;
|
|
132
|
+
return { $lookup: {
|
|
133
|
+
...l,
|
|
134
|
+
pipeline: scopedPipeline(l.pipeline, scope, env)
|
|
135
|
+
} };
|
|
136
|
+
}
|
|
137
|
+
if (stage.$unionWith) {
|
|
138
|
+
const u = typeof stage.$unionWith === "string" ? { coll: stage.$unionWith } : stage.$unionWith;
|
|
139
|
+
if (u.coll === void 0) return { $unionWith: {
|
|
140
|
+
...u,
|
|
141
|
+
pipeline: scopedPipeline(u.pipeline, void 0, env)
|
|
142
|
+
} };
|
|
143
|
+
const scope = scopeForCollection(u.coll, env);
|
|
144
|
+
if (!scope && u.pipeline === void 0) return stage;
|
|
145
|
+
return { $unionWith: {
|
|
146
|
+
...u,
|
|
147
|
+
pipeline: scopedPipeline(u.pipeline, scope, env)
|
|
148
|
+
} };
|
|
149
|
+
}
|
|
150
|
+
if (stage.$graphLookup) {
|
|
151
|
+
const g = stage.$graphLookup;
|
|
152
|
+
const scope = scopeForCollection(g.from, env);
|
|
153
|
+
if (!scope) return stage;
|
|
154
|
+
const existing = g.restrictSearchWithMatch;
|
|
155
|
+
return { $graphLookup: {
|
|
156
|
+
...g,
|
|
157
|
+
restrictSearchWithMatch: existing ? { $and: [existing, scope] } : scope
|
|
158
|
+
} };
|
|
159
|
+
}
|
|
160
|
+
if (stage.$facet) {
|
|
161
|
+
const branches = stage.$facet;
|
|
162
|
+
return { $facet: Object.fromEntries(Object.entries(branches).map(([k, v]) => [k, walk(v, env)])) };
|
|
163
|
+
}
|
|
164
|
+
return stage;
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
//#endregion
|
|
168
|
+
export { scopeJoins as a, scopeForCollection as i, declareCollectionScope as n, tenantFieldsOf as o, markGoverned as r, JOIN_ERROR_CODES as t };
|
|
@@ -46,14 +46,18 @@ function buildCrudSchemasFromMongooseSchema(mongooseSchema, options = {}) {
|
|
|
46
46
|
const jsonCreate = buildJsonSchemaFromPaths(mongooseSchema, options);
|
|
47
47
|
const jsonUpdate = buildJsonSchemaForUpdate(jsonCreate, options);
|
|
48
48
|
const jsonResponse = buildJsonSchemaForResponse(mongooseSchema, options);
|
|
49
|
+
const idPath = mongooseSchema.path("_id");
|
|
49
50
|
return {
|
|
50
51
|
createBody: jsonCreate,
|
|
51
52
|
updateBody: jsonUpdate,
|
|
52
53
|
params: {
|
|
53
54
|
type: "object",
|
|
54
|
-
properties: { id: {
|
|
55
|
+
properties: { id: idPath === void 0 || isObjectIdInstance(idPath.instance ?? "") ? {
|
|
55
56
|
type: "string",
|
|
56
57
|
pattern: "^[0-9a-fA-F]{24}$"
|
|
58
|
+
} : {
|
|
59
|
+
type: "string",
|
|
60
|
+
minLength: 1
|
|
57
61
|
} },
|
|
58
62
|
required: ["id"]
|
|
59
63
|
},
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { l as Plugin, p as RepositoryContext } from "./repository-
|
|
1
|
+
import { A as ObjectId } from "./pagination-D6psses0.mjs";
|
|
2
|
+
import { l as Plugin, p as RepositoryContext } from "./repository-DNHfjAZX.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 };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { A as ObjectId, F as SortSpec, M as ReadPreferenceType, N as SelectSpec, b as WriteConcernSpec, i as CountStrategy, j as PopulateSpec, r as CollationOptions, v as ReadConcernLevel } from "./pagination-D6psses0.mjs";
|
|
2
2
|
import { DeleteManyResult, DeleteResult, LookupPopulateResult, UpdateManyResult } from "@classytic/repo-core/repository";
|
|
3
3
|
import { PipelineStage, PopulateOptions } from "mongoose";
|
|
4
4
|
import { Filter } from "@classytic/repo-core/filter";
|
|
@@ -312,13 +312,20 @@ interface SessionOptions {
|
|
|
312
312
|
* substitute for access control — gate it at the host layer.
|
|
313
313
|
*/
|
|
314
314
|
bypassAppendOnly?: boolean;
|
|
315
|
+
/** Write concern for this call; beats `queryDefaults.writeConcern`. Ignored by reads. */
|
|
316
|
+
writeConcern?: WriteConcernSpec;
|
|
315
317
|
/** Extensible — plugins can read custom fields from options */
|
|
316
318
|
[key: string]: unknown;
|
|
317
319
|
}
|
|
318
|
-
/** Read options
|
|
320
|
+
/** Read options. Each field beats the repository and deployment `queryDefaults`. */
|
|
319
321
|
interface ReadOptions extends SessionOptions {
|
|
320
322
|
/** Read preference for replica sets (e.g. 'secondaryPreferred') */
|
|
321
323
|
readPreference?: ReadPreferenceType;
|
|
324
|
+
/** Server-side time bound for this command. */
|
|
325
|
+
maxTimeMS?: number;
|
|
326
|
+
readConcern?: ReadConcernLevel;
|
|
327
|
+
/** Profiler / log tag. */
|
|
328
|
+
comment?: string;
|
|
322
329
|
}
|
|
323
330
|
/** Repository operation options */
|
|
324
331
|
interface OperationOptions extends ReadOptions {
|
|
@@ -436,21 +443,17 @@ interface FindOneAndUpdateOptions extends OperationOptions {
|
|
|
436
443
|
runValidators?: boolean;
|
|
437
444
|
/** Collation for locale-aware string comparison. */
|
|
438
445
|
collation?: CollationOptions;
|
|
439
|
-
/**
|
|
440
|
-
|
|
446
|
+
/** Force an index (name or key pattern). */
|
|
447
|
+
hint?: string | Record<string, 1 | -1>;
|
|
441
448
|
}
|
|
442
449
|
/** Aggregate operation options */
|
|
443
450
|
interface AggregateOptions extends ReadOptions {
|
|
444
451
|
/** Allow aggregation to use disk for large sorts/groups */
|
|
445
452
|
allowDiskUse?: boolean;
|
|
446
|
-
/**
|
|
447
|
-
|
|
448
|
-
/**
|
|
449
|
-
|
|
450
|
-
/** Read concern level */
|
|
451
|
-
readConcern?: {
|
|
452
|
-
level: string;
|
|
453
|
-
};
|
|
453
|
+
/** Force an index (name or key pattern). */
|
|
454
|
+
hint?: string | Record<string, 1 | -1>;
|
|
455
|
+
/** Collections the caller asserts are company-wide, joinable without a governing repository. */
|
|
456
|
+
unscopedJoins?: readonly string[];
|
|
454
457
|
/** Collation for locale-aware string comparison */
|
|
455
458
|
collation?: CollationOptions;
|
|
456
459
|
/** Maximum allowed pipeline stages (governance) */
|
|
@@ -532,5 +535,25 @@ interface MinMaxResult {
|
|
|
532
535
|
min: unknown;
|
|
533
536
|
max: unknown;
|
|
534
537
|
}
|
|
538
|
+
/** Kit-native options for the portable `aggregate(req)`; each field beats every default. */
|
|
539
|
+
interface AggregateCallOptions {
|
|
540
|
+
/** Also return the winning plan (a second command). */
|
|
541
|
+
explain?: 'queryPlanner' | 'executionStats' | 'allPlansExecution';
|
|
542
|
+
/** Collections the caller asserts are company-wide (see `join-scope.ts`). */
|
|
543
|
+
unscopedJoins?: readonly string[];
|
|
544
|
+
maxTimeMS?: number;
|
|
545
|
+
readPreference?: ReadPreferenceType;
|
|
546
|
+
readConcern?: ReadConcernLevel;
|
|
547
|
+
comment?: string;
|
|
548
|
+
}
|
|
549
|
+
/** `getByIds` options: chunked `$in` reads, optionally aligned with the input order. */
|
|
550
|
+
interface GetByIdsOptions extends OperationOptions {
|
|
551
|
+
/** Ids per `$in` command. Default `DEFAULT_ID_CHUNK` (10,000). */
|
|
552
|
+
chunkSize?: number;
|
|
553
|
+
/** Chunks in flight at once outside a session. Default 4. */
|
|
554
|
+
concurrency?: number;
|
|
555
|
+
/** Return `Array<TDoc | undefined>` aligned with the input ids instead of a Map. */
|
|
556
|
+
preserveOrder?: boolean;
|
|
557
|
+
}
|
|
535
558
|
//#endregion
|
|
536
|
-
export {
|
|
559
|
+
export { LookupOptions as S, SessionOptions as _, ConvenientTransactionOptions as a, WithTransactionOptions as b, DeleteResult as c, GroupResult as d, LookupPopulateOptions as f, ReadOptions as g, OperationOptions as h, CacheableOptions as i, FindOneAndUpdateOptions as l, MinMaxResult as m, AggregateOptions as n, CreateOptions as o, LookupPopulateResult as p, CacheOperationOptions as r, DeleteManyResult as s, AggregateCallOptions as t, GetByIdsOptions as u, UpdateManyResult as v, LookupBuilder as x, UpdateOptions as y };
|
package/dist/outbox/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { r as Repository } from "../Repository-pnLw1rG5.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-CYhSC9ut.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 {
|
|
2
|
-
export { PaginationEngine };
|
|
1
|
+
import { n as PaginationEngine, t as CountMemoKey } from "../PaginationEngine-CRHyuUtw.mjs";
|
|
2
|
+
export { CountMemoKey, PaginationEngine };
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { t as PaginationEngine } from "../PaginationEngine-
|
|
1
|
+
import { t as PaginationEngine } from "../PaginationEngine-CfT2Cxkj.mjs";
|
|
2
2
|
export { PaginationEngine };
|