@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.
Files changed (51) 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-DQ7beTCR.mjs → PaginationEngine-CfT2Cxkj.mjs} +320 -145
  4. package/dist/{Repository-D7fDpyFx.mjs → Repository-CYhSC9ut.mjs} +844 -794
  5. package/dist/{Repository-IxI983_m.d.mts → Repository-pnLw1rG5.d.mts} +113 -47
  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-CRXjDa3N.d.mts → create-repository-BUx9vfG9.d.mts} +5 -5
  13. package/dist/{create-repository-DOe9E_5F.mjs → create-repository-BYyWzLbt.mjs} +2 -2
  14. package/dist/idempotency/index.d.mts +23 -0
  15. package/dist/idempotency/index.mjs +134 -0
  16. package/dist/{index-CmjNyulu.d.mts → index-SYF8N28D.d.mts} +37 -19
  17. package/dist/index.d.mts +105 -39
  18. package/dist/index.mjs +332 -676
  19. package/dist/indexes/verify-indexes.d.mts +14 -1
  20. package/dist/indexes/verify-indexes.mjs +54 -7
  21. package/dist/join-scope-DSAznE8E.mjs +168 -0
  22. package/dist/{mongooseToJsonSchema-waUVQwK4.mjs → mongooseToJsonSchema-C3k4ide0.mjs} +5 -1
  23. package/dist/{multi-tenant.plugin-B-tzNeYL.d.mts → multi-tenant.plugin-a3UhQARj.d.mts} +19 -3
  24. package/dist/{operations-CrRIoc8Z.d.mts → operations-XiFgG5US.d.mts} +36 -13
  25. package/dist/outbox/index.d.mts +1 -1
  26. package/dist/outbox/index.mjs +6 -4
  27. package/dist/pagination/PaginationEngine.d.mts +2 -2
  28. package/dist/pagination/PaginationEngine.mjs +1 -1
  29. package/dist/pagination/utils/limits.d.mts +1 -1
  30. package/dist/{pagination-BpIv2bdS.d.mts → pagination-D6psses0.d.mts} +116 -13
  31. package/dist/{plugin-options-BuvtOSZf.d.mts → plugin-options-CRhIvSzx.d.mts} +16 -2
  32. package/dist/plugins/index.d.mts +8 -3
  33. package/dist/plugins/index.mjs +98 -25
  34. package/dist/query-defaults-BxUcSOoF.mjs +208 -0
  35. package/dist/read-model/index.d.mts +66 -0
  36. package/dist/read-model/index.mjs +193 -0
  37. package/dist/{repository-ZbLtL7Nk.d.mts → repository-DNHfjAZX.d.mts} +36 -4
  38. package/dist/sync/index.d.mts +53 -0
  39. package/dist/sync/index.mjs +371 -0
  40. package/dist/tenant/index.mjs +2 -0
  41. package/dist/testkit/index.d.mts +40 -3
  42. package/dist/testkit/index.mjs +113 -2
  43. package/dist/{timestamp.plugin-B5eiaItQ.mjs → timestamp.plugin-zfdLYZiO.mjs} +135 -32
  44. package/dist/{to-plain-B8ufk3Qz.d.mts → to-plain-CjgEInWV.d.mts} +2 -2
  45. package/dist/transaction-BrRYtH_b.mjs +655 -0
  46. package/dist/{update-c_01A0if.mjs → update-WgeAUJ_I.mjs} +212 -33
  47. package/dist/utils/index.d.mts +1 -1
  48. package/dist/utils/index.mjs +1 -1
  49. package/dist/{validation-chain.plugin-Bju1D1Sn.mjs → validation-chain.plugin-CHUHWMtk.mjs} +204 -279
  50. package/dist/{validation-chain.plugin-DvrLJIbJ.d.mts → validation-chain.plugin-CTcNJCmV.d.mts} +27 -5
  51. 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 = new Set(live.map((i) => normalizeKey(i.key)));
43
- existingText = new Set(live.filter((i) => i.weights).map((i) => JSON.stringify(Object.keys(i.weights).sort())));
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
- if (asText) {
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
- if (indexOptions?.unique) report.missingUnique.push(entry);
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 { g as ObjectId } from "./pagination-BpIv2bdS.mjs";
2
- import { l as Plugin, p as RepositoryContext } from "./repository-ZbLtL7Nk.mjs";
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 { ensureAuditTrailReady as A, AuditTrailMethods as C, AuditTrailQueryOptions as D, AuditTrailQuery as E, EnsureAuditTrailReadyOptions as O, AuditQueryResult as S, AuditTrailOptions as T, RepositoryCachePluginOptions as _, DateSequentialIdOptions as a, AuditOperation as b, SequentialIdOptions as c, getNextSequence as d, prefixedId as f, RepositoryCacheHandle$1 as g, CacheOptions as h, CustomIdOptions as i, auditTrailPlugin as k, customIdPlugin as l, CacheAdapter$1 as m, adminBypass as n, IdGenerator as o, sequentialId as p, multiTenantPlugin as r, PrefixedIdOptions as s, MultiTenantOptions as t, dateSequentialId as u, cachePlugin as v, AuditTrailMode as w, AuditQueryOptions as x, AuditEntry as y };
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 { _ as PopulateSpec, g as ObjectId, i as CountStrategy, r as CollationOptions, v as ReadPreferenceType, x as SortSpec, y as SelectSpec } from "./pagination-BpIv2bdS.mjs";
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 — session + readPreference for read-only operations */
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
- /** Maximum execution time in milliseconds. */
440
- maxTimeMS?: number;
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
- /** Comment for profiler/logs */
447
- comment?: string;
448
- /** Maximum execution time in milliseconds */
449
- maxTimeMS?: number;
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 { UpdateOptions as _, CreateOptions as a, LookupOptions as b, FindOneAndUpdateOptions as c, LookupPopulateResult as d, MinMaxResult as f, UpdateManyResult as g, SessionOptions as h, ConvenientTransactionOptions as i, GroupResult as l, ReadOptions as m, CacheOperationOptions as n, DeleteManyResult as o, OperationOptions as p, CacheableOptions as r, DeleteResult as s, AggregateOptions as t, LookupPopulateOptions as u, WithTransactionOptions as v, LookupBuilder as y };
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 };
@@ -1,4 +1,4 @@
1
- import { n as Repository } from "../Repository-IxI983_m.mjs";
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. */
@@ -1,4 +1,4 @@
1
- import { n as Repository } from "../Repository-D7fDpyFx.mjs";
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. Filtering on
176
- * `$type: 'string'` indexes only rows that actually carry a key.
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: { $type: "string" } }
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-D-P19Y9m.mjs";
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-DQ7beTCR.mjs";
1
+ import { t as PaginationEngine } from "../PaginationEngine-CfT2Cxkj.mjs";
2
2
  export { PaginationEngine };
@@ -1,4 +1,4 @@
1
- import { u as PaginationConfig } from "../../pagination-BpIv2bdS.mjs";
1
+ import { u as PaginationConfig } from "../../pagination-D6psses0.mjs";
2
2
  //#region src/pagination/utils/limits.d.ts
3
3
  /**
4
4
  * Validates and sanitizes limit value