@classytic/mongokit 3.29.0 → 3.30.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.
Files changed (35) hide show
  1. package/README.md +43 -0
  2. package/dist/{create-repository-D4ZU94b3.d.mts → Repository-BXHkUtLk.d.mts} +30 -57
  3. package/dist/{Repository-DThvFCaV.mjs → Repository-X7U2d0kU.mjs} +296 -42
  4. package/dist/actions/index.mjs +1 -1
  5. package/dist/adapter/index.mjs +1 -1
  6. package/dist/{adapter-CLvUHiTW.mjs → adapter-BitHg-CN.mjs} +1 -1
  7. package/dist/ai/index.d.mts +1 -1
  8. package/dist/better-auth/index.d.mts +13 -0
  9. package/dist/better-auth/index.mjs +76 -6
  10. package/dist/create-repository-CvD4wzuN.d.mts +54 -0
  11. package/dist/{create-repository-uhKt_oUT.mjs → create-repository-DYe5YEie.mjs} +3 -2
  12. package/dist/index.d.mts +275 -41
  13. package/dist/index.mjs +80 -7
  14. package/dist/kernel-conformance/index.d.mts +217 -0
  15. package/dist/kernel-conformance/index.mjs +603 -0
  16. package/dist/method-registry.plugin-DblM7V6j.mjs +112 -0
  17. package/dist/model-registry-ClKtWiHa.mjs +67 -0
  18. package/dist/{mongooseToJsonSchema-CByt1VhQ.mjs → mongooseToJsonSchema-DKbnKduN.mjs} +14 -3
  19. package/dist/{multi-tenant.plugin-DVyVGREb.d.mts → multi-tenant.plugin-2VtSSm1Y.d.mts} +1 -1
  20. package/dist/outbox/index.d.mts +63 -0
  21. package/dist/outbox/index.mjs +239 -0
  22. package/dist/{plugin-options-vFufQZMZ.d.mts → plugin-options-BsFH0sTi.d.mts} +1 -1
  23. package/dist/plugins/index.d.mts +4 -4
  24. package/dist/plugins/index.mjs +3 -2
  25. package/dist/{repository-DCHRWRXw.d.mts → repository-Q16ErTu8.d.mts} +19 -1
  26. package/dist/testkit/index.d.mts +8 -1
  27. package/dist/testkit/index.mjs +1 -1
  28. package/dist/{timestamp.plugin-CFL6LlYZ.mjs → timestamp.plugin-ChFRcLRJ.mjs} +32 -143
  29. package/dist/{to-plain-DkF5px0R.d.mts → to-plain-BuX1RUZV.d.mts} +12 -4
  30. package/dist/{update-CI2NdAW6.mjs → update-DHdXdOk_.mjs} +21 -2
  31. package/dist/utils/index.d.mts +2 -2
  32. package/dist/utils/index.mjs +1 -1
  33. package/dist/{validation-chain.plugin-BLecaKa0.d.mts → validation-chain.plugin-BSshLJTA.d.mts} +3 -3
  34. package/dist/{validation-chain.plugin-CwOcUJC1.mjs → validation-chain.plugin-BxTl_dYR.mjs} +4 -3
  35. package/package.json +191 -185
@@ -0,0 +1,112 @@
1
+ import { t as createError } from "./error-BuByZwKU.mjs";
2
+ //#region src/plugins/batch-operations.plugin.ts
3
+ /**
4
+ * Batch operations plugin.
5
+ *
6
+ * @example
7
+ * const repo = new Repository(Model, [
8
+ * methodRegistryPlugin(),
9
+ * batchOperationsPlugin(),
10
+ * ]);
11
+ *
12
+ * // updateMany + deleteMany are primitives on Repository — no plugin needed.
13
+ * await repo.updateMany({ status: 'pending' }, { status: 'active' });
14
+ * await repo.deleteMany({ status: 'deleted' });
15
+ *
16
+ * // bulkWrite is what this plugin contributes.
17
+ * await repo.bulkWrite([...ops]);
18
+ */
19
+ function batchOperationsPlugin() {
20
+ return {
21
+ name: "batch-operations",
22
+ apply(repo) {
23
+ if (!repo.registerMethod) throw new Error("batchOperationsPlugin requires methodRegistryPlugin");
24
+ /**
25
+ * Execute heterogeneous bulk write operations in a single database call.
26
+ *
27
+ * Supports insertOne, updateOne, updateMany, deleteOne, deleteMany, and replaceOne
28
+ * operations mixed together for maximum efficiency.
29
+ *
30
+ * @example
31
+ * await repo.bulkWrite([
32
+ * { insertOne: { document: { name: 'New Item', price: 10 } } },
33
+ * { updateOne: { filter: { _id: id1 }, update: { $inc: { views: 1 } } } },
34
+ * { updateMany: { filter: { status: 'draft' }, update: { $set: { status: 'published' } } } },
35
+ * { deleteOne: { filter: { _id: id2 } } },
36
+ * ]);
37
+ */
38
+ repo.registerMethod("bulkWrite", async function(operations, options = {}) {
39
+ const context = await this._buildContext("bulkWrite", {
40
+ operations,
41
+ ...options
42
+ });
43
+ try {
44
+ const finalOps = context.operations || operations;
45
+ if (!finalOps || finalOps.length === 0) throw createError(400, "bulkWrite requires at least one operation");
46
+ const result = await this.Model.bulkWrite(finalOps, {
47
+ ordered: options.ordered ?? true,
48
+ session: options.session
49
+ });
50
+ const bulkResult = {
51
+ ok: result.ok,
52
+ insertedCount: result.insertedCount,
53
+ upsertedCount: result.upsertedCount,
54
+ matchedCount: result.matchedCount,
55
+ modifiedCount: result.modifiedCount,
56
+ deletedCount: result.deletedCount,
57
+ insertedIds: result.insertedIds,
58
+ upsertedIds: result.upsertedIds
59
+ };
60
+ await this.emitAsync("after:bulkWrite", {
61
+ context,
62
+ result: bulkResult
63
+ });
64
+ return bulkResult;
65
+ } catch (error) {
66
+ this.emit("error:bulkWrite", {
67
+ context,
68
+ error
69
+ });
70
+ throw this._handleError(error);
71
+ }
72
+ });
73
+ }
74
+ };
75
+ }
76
+ //#endregion
77
+ //#region src/plugins/method-registry.plugin.ts
78
+ /**
79
+ * Method registry plugin that enables dynamic method registration
80
+ */
81
+ function methodRegistryPlugin() {
82
+ return {
83
+ name: "method-registry",
84
+ apply(repo) {
85
+ const registeredMethods = [];
86
+ /**
87
+ * Register a new method on the repository instance
88
+ */
89
+ repo.registerMethod = (name, fn) => {
90
+ if (repo[name]) throw new Error(`Cannot register method '${name}': Method already exists on repository. Choose a different name or use a plugin that doesn't conflict.`);
91
+ if (!name || typeof name !== "string") throw new Error("Method name must be a non-empty string");
92
+ if (typeof fn !== "function") throw new Error(`Method '${name}' must be a function`);
93
+ repo[name] = fn.bind(repo);
94
+ registeredMethods.push(name);
95
+ repo.emit("method:registered", {
96
+ name,
97
+ fn
98
+ });
99
+ };
100
+ /**
101
+ * Check if a method is registered
102
+ */
103
+ repo.hasMethod = (name) => typeof repo[name] === "function";
104
+ /**
105
+ * Get list of all dynamically registered methods
106
+ */
107
+ repo.getRegisteredMethods = () => [...registeredMethods];
108
+ }
109
+ };
110
+ }
111
+ //#endregion
112
+ export { batchOperationsPlugin as n, methodRegistryPlugin as t };
@@ -0,0 +1,67 @@
1
+ import mongoose from "mongoose";
2
+ //#region src/model-registry.ts
3
+ /**
4
+ * Model registration — the ONE place `mongoose.models` is touched.
5
+ *
6
+ * ## Why this exists
7
+ *
8
+ * Mongoose locks a schema on the first `model()` call and throws (`OverwriteModelError`)
9
+ * on the second, so every caller that might register twice hand-rolls a guard:
10
+ *
11
+ * ```ts
12
+ * const Counter = mongoose.models.LoyaltyCardCounter ?? mongoose.model('LoyaltyCardCounter', schema);
13
+ * if (!mongoose.models.organization) mongoose.model('organization', new Schema({}, { strict: false }));
14
+ * ```
15
+ *
16
+ * That idiom was written at least five times across the hosts and twice inside mongokit
17
+ * itself. Worse, the hand-rolled form silently REUSES whatever is registered — so a second
18
+ * call passing extra fields or indexes has them dropped with no error, which is how a
19
+ * model ends up missing an index nobody can explain.
20
+ *
21
+ * ## The two policies, made explicit
22
+ *
23
+ * - `onExisting: 'reuse'` — idempotent registration. The caller adds nothing new, so an
24
+ * existing model is the right answer (stubs, counters, boot-time guards).
25
+ * - `onExisting: 'throw'` — the caller is contributing schema (extra fields, indexes) that
26
+ * mongoose CANNOT apply to a locked schema. Silently reusing loses it; this fails loud.
27
+ *
28
+ * There is deliberately no default. Picking one for the caller is how the silent-drop bug
29
+ * got in, and the two are not interchangeable.
30
+ */
31
+ /**
32
+ * A model name was already registered when a `throw` policy required it to be free.
33
+ *
34
+ * Typed (not a bare `Error`) so consumers — kernels wrapping their own domain collision error,
35
+ * tests asserting the failure — can `instanceof`-check it instead of matching a message string.
36
+ */
37
+ var ModelCollisionError = class extends Error {
38
+ modelName;
39
+ constructor(modelName, detail) {
40
+ super(`[mongokit] model '${modelName}' is already registered. Mongoose locks a schema on the first model() call, so anything this call adds (fields, indexes) would be SILENTLY DROPPED.${detail ? ` ${detail}` : ""}`);
41
+ this.name = "ModelCollisionError";
42
+ this.modelName = modelName;
43
+ }
44
+ };
45
+ /** Is this model name already registered? Prefer {@link defineModel} over asking. */
46
+ function isModelRegistered(name, connection) {
47
+ const registry = connection ? connection.models : mongoose.models;
48
+ return Boolean(registry[name]);
49
+ }
50
+ /**
51
+ * Register a model, or return the existing one, according to `onExisting`.
52
+ *
53
+ * `schema` is a FACTORY so it is never built when an existing model wins — building a
54
+ * schema has side effects (plugin application, index declaration) that should not run for
55
+ * a registration that is about to be discarded.
56
+ */
57
+ function defineModel(name, schema, options) {
58
+ const target = options.connection;
59
+ const existing = (target ? target.models : mongoose.models)[name];
60
+ if (existing) {
61
+ if (options.onExisting === "throw") throw new ModelCollisionError(name, options.throwHint);
62
+ return existing;
63
+ }
64
+ return target ? target.model(name, schema(), options.collection) : mongoose.model(name, schema(), options.collection);
65
+ }
66
+ //#endregion
67
+ export { defineModel as n, isModelRegistered as r, ModelCollisionError as t };
@@ -69,6 +69,14 @@ function buildCrudSchemasFromMongooseSchema(mongooseSchema, options = {}) {
69
69
  * field accepts this function directly with no glue. Future Mongo-shaped
70
70
  * kits implementing the same contract are interchangeable at the adapter
71
71
  * boundary.
72
+ *
73
+ * GENERIC over the document type, not fixed to `Model<unknown>`. Only
74
+ * `.schema` is read, so the document type is irrelevant here — but Mongoose's
75
+ * `Model<T>` is INVARIANT through `schema.static`, so a hard `Model<unknown>`
76
+ * parameter rejects every concretely-typed model a host actually has. That is
77
+ * not theoretical: it made `arc init`'s generated `example.schemas.ts` fail
78
+ * `tsc` on a brand-new project. Instantiating `TDoc = unknown` still satisfies
79
+ * `SchemaGenerator<Model<unknown>>`, so the contract claim above is unchanged.
72
80
  */
73
81
  function buildCrudSchemasFromModel(mongooseModel, options = {}) {
74
82
  if (!mongooseModel?.schema) throw new Error("Invalid mongoose model");
@@ -118,7 +126,8 @@ function buildJsonSchemaFromPaths(mongooseSchema, options) {
118
126
  properties
119
127
  };
120
128
  if (required.length) schema.required = required;
121
- applyFieldRules(schema, collectFieldsToOmit(options, "create"), options);
129
+ const fieldsToOmit = collectFieldsToOmit(options, "create");
130
+ applyFieldRules(schema, fieldsToOmit, options);
122
131
  const reqOv = options?.create?.requiredOverrides || {};
123
132
  const optOv = options?.create?.optionalOverrides || {};
124
133
  schema.required = schema.required || [];
@@ -468,7 +477,8 @@ function stripDefaultsDeep(node) {
468
477
  function buildJsonSchemaForUpdate(createJson, options) {
469
478
  const clone = JSON.parse(JSON.stringify(createJson));
470
479
  delete clone.required;
471
- applyFieldRules(clone, collectFieldsToOmit(options, "update"), options);
480
+ const fieldsToOmit = collectFieldsToOmit(options, "update");
481
+ applyFieldRules(clone, fieldsToOmit, options);
472
482
  stripDefaultsDeep(clone);
473
483
  if (options?.strictAdditionalProperties === true) clone.additionalProperties = false;
474
484
  if (options?.update?.requireAtLeastOne === true) clone.minProperties = 1;
@@ -499,7 +509,8 @@ function buildJsonSchemaForResponse(mongooseSchema, options) {
499
509
  fieldRules: stripCreateOnlyFieldRulesForResponse(options.fieldRules),
500
510
  create: void 0
501
511
  });
502
- applyFieldRules(responseSchema, collectFieldsToOmit(options, "response"), options);
512
+ const fieldsToOmit = collectFieldsToOmit(options, "response");
513
+ applyFieldRules(responseSchema, fieldsToOmit, options);
503
514
  responseSchema.required = void 0;
504
515
  if (options?.strictAdditionalProperties !== true) responseSchema.additionalProperties = true;
505
516
  if (fullSchema.title && !responseSchema.title) responseSchema.title = fullSchema.title;
@@ -1,5 +1,5 @@
1
1
  import { i as ObjectId } from "./core-62oo3N1C.mjs";
2
- import { l as Plugin, p as RepositoryContext } from "./repository-DCHRWRXw.mjs";
2
+ import { l as Plugin, p as RepositoryContext } from "./repository-Q16ErTu8.mjs";
3
3
  import mongoose, { Connection } from "mongoose";
4
4
  import { CacheAdapter as CacheAdapter$1, CacheOptions, RepositoryCacheHandle, RepositoryCachePluginOptions, cachePlugin } from "@classytic/repo-core/cache";
5
5
  import { TenantConfig } from "@classytic/repo-core/tenant";
@@ -0,0 +1,63 @@
1
+ import { n as Repository } from "../Repository-BXHkUtLk.mjs";
2
+ import { Connection, Model } from "mongoose";
3
+ //#region src/outbox/index.d.ts
4
+ /** Delivery states. `dead_letter` is terminal and operator-visible. */
5
+ declare const OUTBOX_STATUSES: readonly ["pending", "delivered", "dead_letter"];
6
+ interface OutboxModelOptions {
7
+ /** Collection name. Default `event_outbox`. */
8
+ collection?: string;
9
+ /** Mongoose model name. Default `EventOutbox`. Must be unique per connection. */
10
+ modelName?: string;
11
+ /**
12
+ * How long a DELIVERED row is retained before the TTL monitor removes it.
13
+ * Default 7 days. Pending and dead-lettered rows are never auto-removed —
14
+ * expiring an undelivered event would destroy the guarantee the table exists for.
15
+ */
16
+ ttlSeconds?: number;
17
+ /**
18
+ * Column holding the "not claimable before" timestamp. Default `visibleAt`.
19
+ *
20
+ * Configurable because an existing table cannot be renamed without rebuilding the claim
21
+ * index — the one index whose absence turns a busy outbox into a collection scan. One live
22
+ * host uses `nextVisibleAt`; it passes that here and keeps its data and indexes untouched.
23
+ * The same value must be given to `repositoryAsOutboxStore`, or the writer and the claimer
24
+ * disagree about which column means "visible" and the queue silently stops draining.
25
+ */
26
+ visibleAtField?: string;
27
+ }
28
+ /**
29
+ * Build (or return) the outbox model on `connection`.
30
+ *
31
+ * Idempotent per connection: calling twice returns the same model rather than throwing
32
+ * `OverwriteModelError`, so a host may build it lazily from several call sites.
33
+ */
34
+ declare function createOutboxModel(connection: Connection, options?: OutboxModelOptions): Model<Record<string, unknown>>;
35
+ /**
36
+ * The repository `repositoryAsOutboxStore` needs, with the plugin set it actually calls.
37
+ *
38
+ * The adapter drives `create` / `getOne` / `getAll` / `findOneAndUpdate` (base `Repository`)
39
+ * plus `deleteMany`, which is contributed by `batchOperationsPlugin` THROUGH the method
40
+ * registry. Both plugins are therefore required, and in that pairing — a host that passes only
41
+ * `batchOperationsPlugin()` gets a repository whose `deleteMany` is never registered.
42
+ *
43
+ * This existed as three hand-written lines in a host, which is two lines too many to get wrong
44
+ * silently: the wrong plugin list does not fail at boot, it fails the first time a delivered row
45
+ * is swept, on a code path nobody watches. `Repository` validates the registered set at
46
+ * construction and throws naming what is missing, so composing it HERE moves that check to
47
+ * every consumer instead of the one host that remembered.
48
+ *
49
+ * ```ts
50
+ * const store = repositoryAsOutboxStore(
51
+ * createOutboxRepository(connection), // this
52
+ * { visibleAtField: 'nextVisibleAt' }, // iff you passed the same to createOutboxModel
53
+ * );
54
+ * ```
55
+ *
56
+ * `plugins` appends to the required set rather than replacing it — for an observability or
57
+ * cache plugin a deployment wants on this table too.
58
+ */
59
+ declare function createOutboxRepository(connection: Connection, options?: OutboxModelOptions & {
60
+ plugins?: readonly unknown[];
61
+ }): Repository<Record<string, unknown>>;
62
+ //#endregion
63
+ export { OUTBOX_STATUSES, OutboxModelOptions, createOutboxModel, createOutboxRepository };
@@ -0,0 +1,239 @@
1
+ import { n as Repository } from "../Repository-X7U2d0kU.mjs";
2
+ import { n as batchOperationsPlugin, t as methodRegistryPlugin } from "../method-registry.plugin-DblM7V6j.mjs";
3
+ import { Schema } from "mongoose";
4
+ //#region src/outbox/index.ts
5
+ /**
6
+ * The mongo-backed OUTBOX table — schema, indexes and repository.
7
+ *
8
+ * ## Why this is here
9
+ *
10
+ * Two ERP hosts independently hand-rolled this: 465 lines in one, 301 in the other, both
11
+ * implementing the same `OutboxStore` contract over the same database. The second host's source
12
+ * even carries the comment *"be-prod learned that the hard way"* about a partial-index pitfall —
13
+ * the LESSON transferred and the code was rewritten anyway. That is the failure mode worth
14
+ * naming: duplication here does not fail loudly, it drifts, and this is the table that guarantees
15
+ * a collected payment is eventually recorded.
16
+ *
17
+ * ## What this module is, and is not
18
+ *
19
+ * It is the MONGO half only: a schema, the four indexes that make claim/dedupe/retention correct,
20
+ * and a repository over them. It does NOT implement `OutboxStore` — `@classytic/arc`'s
21
+ * `repositoryAsOutboxStore` already does that, generically, over any repository. Keeping the two
22
+ * apart is what avoids a dependency cycle (mongokit does not depend on arc) and what lets a
23
+ * non-mongo kit reuse the contract half.
24
+ *
25
+ * A host composes three lines:
26
+ *
27
+ * ```ts
28
+ * const model = createOutboxModel(connection, { visibleAtField: 'nextVisibleAt' });
29
+ * const repository = new Repository(model);
30
+ * const store = repositoryAsOutboxStore(repository, { visibleAtField: 'nextVisibleAt' });
31
+ * ```
32
+ */
33
+ /** Delivery states. `dead_letter` is terminal and operator-visible. */
34
+ const OUTBOX_STATUSES = [
35
+ "pending",
36
+ "delivered",
37
+ "dead_letter"
38
+ ];
39
+ const DEFAULT_TTL_SECONDS = 604800;
40
+ /**
41
+ * Build (or return) the outbox model on `connection`.
42
+ *
43
+ * Idempotent per connection: calling twice returns the same model rather than throwing
44
+ * `OverwriteModelError`, so a host may build it lazily from several call sites.
45
+ */
46
+ function createOutboxModel(connection, options = {}) {
47
+ const { collection = "event_outbox", modelName = "EventOutbox", ttlSeconds = DEFAULT_TTL_SECONDS, visibleAtField = "visibleAt" } = options;
48
+ const existing = connection.models[modelName];
49
+ if (existing) return existing;
50
+ /**
51
+ * The document shape is `repositoryAsOutboxStore`'s, not an invention of this module.
52
+ *
53
+ * Two things about it surprise people coming from a hand-rolled table, and both are
54
+ * deliberate:
55
+ *
56
+ * 1. **`_id` IS the event id** (a string), not an ObjectId with a separate `eventId`
57
+ * column. That is what makes `save` idempotent for free — a redelivered event is a
58
+ * duplicate-key error the adapter swallows, with no read-then-write race to lose.
59
+ * 2. **The whole `event` is nested**, rather than split into `payload` + `meta`. The relay
60
+ * republishes the event verbatim; splitting it means reassembling it on the way out, and
61
+ * a reassembly that drifts from the original is a corrupted replay nobody can detect.
62
+ *
63
+ * A host with an EXISTING outbox in a different shape cannot simply point this at its
64
+ * collection — the ids are of a different type. That is a data migration, not a config
65
+ * change, and it must be planned rather than discovered.
66
+ */
67
+ const schema = new Schema({
68
+ /** The event id itself. String, because the adapter writes `event.meta.id` here. */
69
+ _id: {
70
+ type: String,
71
+ required: true
72
+ },
73
+ /** The event, verbatim, as the relay will republish it. */
74
+ event: {
75
+ type: Schema.Types.Mixed,
76
+ required: true
77
+ },
78
+ type: {
79
+ type: String,
80
+ required: true
81
+ },
82
+ status: {
83
+ type: String,
84
+ enum: [...OUTBOX_STATUSES],
85
+ default: "pending"
86
+ },
87
+ attempts: {
88
+ type: Number,
89
+ default: 0
90
+ },
91
+ /**
92
+ * Caller-supplied dedupe key. `null` when absent — see the partial index below for
93
+ * why null must not participate in the unique constraint.
94
+ */
95
+ dedupeKey: {
96
+ type: String,
97
+ default: null
98
+ },
99
+ partitionKey: {
100
+ type: String,
101
+ default: null
102
+ },
103
+ headers: {
104
+ type: Schema.Types.Mixed,
105
+ default: null
106
+ },
107
+ leaseOwner: {
108
+ type: String,
109
+ default: null
110
+ },
111
+ leaseExpiresAt: {
112
+ type: Date,
113
+ default: null
114
+ },
115
+ [visibleAtField]: {
116
+ type: Date,
117
+ default: Date.now
118
+ },
119
+ deliveredAt: {
120
+ type: Date,
121
+ default: null
122
+ },
123
+ firstFailedAt: {
124
+ type: Date,
125
+ default: null
126
+ },
127
+ lastFailedAt: {
128
+ type: Date,
129
+ default: null
130
+ },
131
+ lastError: {
132
+ type: Schema.Types.Mixed,
133
+ default: null
134
+ },
135
+ createdAt: {
136
+ type: Date,
137
+ default: Date.now
138
+ }
139
+ }, {
140
+ /**
141
+ * `strict: false` so a host may stamp extra columns without a schema change, and
142
+ * `timestamps: false` because `createdAt` is set by the adapter — letting mongoose
143
+ * manage it would add an `updatedAt` the claim index does not cover.
144
+ *
145
+ * `_id: false` disables mongoose's ObjectId auto-generation; the adapter always
146
+ * supplies the id.
147
+ */
148
+ strict: false,
149
+ timestamps: false,
150
+ collection,
151
+ versionKey: false,
152
+ _id: false
153
+ });
154
+ /**
155
+ * THE CLAIM INDEX. The relay's hot query is
156
+ * `status = 'pending' AND <visibleAt> <= now`, ordered by `createdAt`.
157
+ * Without this the relay table-scans on every tick.
158
+ */
159
+ schema.index({
160
+ status: 1,
161
+ [visibleAtField]: 1,
162
+ createdAt: 1
163
+ });
164
+ /**
165
+ * Retention. Scoped to `deliveredAt`, which is only ever set on success — so a pending or
166
+ * dead-lettered row has `null` and is never expired. A TTL on `createdAt` would silently
167
+ * delete undelivered events, which is the one thing this table must never do.
168
+ */
169
+ schema.index({ deliveredAt: 1 }, { expireAfterSeconds: ttlSeconds });
170
+ /**
171
+ * Dedupe. `unique` + `partialFilterExpression` on the TYPE, not on `$ne: null`.
172
+ *
173
+ * Mongo rejects `unique` combined with a `sparse: true` + partial filter ("cannot mix"), and
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.
177
+ */
178
+ schema.index({ dedupeKey: 1 }, {
179
+ unique: true,
180
+ partialFilterExpression: { dedupeKey: { $type: "string" } }
181
+ });
182
+ /**
183
+ * The DEAD-LETTER read. `getDeadLettered` is part of the `OutboxStore` contract and is what
184
+ * an operator alert polls — hourly, forever, on a table that grows. Partial, because the
185
+ * rows an operator reads are a vanishing fraction of the rows written, and a full index on
186
+ * `status` would be almost entirely `delivered` entries nobody queries by status alone.
187
+ *
188
+ * A host that dropped this still worked: the alert just collection-scanned every hour, which
189
+ * is invisible until the table is large and then is a periodic production stall.
190
+ */
191
+ schema.index({
192
+ status: 1,
193
+ lastFailedAt: 1
194
+ }, { partialFilterExpression: { status: "dead_letter" } });
195
+ /**
196
+ * There is deliberately NO `claimToken` index.
197
+ *
198
+ * One host's fork claims with a generated token; `repositoryAsOutboxStore` claims with a
199
+ * LEASE (`leaseOwner` + `leaseExpiresAt`), already covered above. An index copied across
200
+ * from that fork would be permanently empty here — free to write, but it advertises a
201
+ * column this shape never sets, which is how the next reader concludes the two tables are
202
+ * interchangeable.
203
+ */
204
+ return connection.model(modelName, schema, collection);
205
+ }
206
+ /**
207
+ * The repository `repositoryAsOutboxStore` needs, with the plugin set it actually calls.
208
+ *
209
+ * The adapter drives `create` / `getOne` / `getAll` / `findOneAndUpdate` (base `Repository`)
210
+ * plus `deleteMany`, which is contributed by `batchOperationsPlugin` THROUGH the method
211
+ * registry. Both plugins are therefore required, and in that pairing — a host that passes only
212
+ * `batchOperationsPlugin()` gets a repository whose `deleteMany` is never registered.
213
+ *
214
+ * This existed as three hand-written lines in a host, which is two lines too many to get wrong
215
+ * silently: the wrong plugin list does not fail at boot, it fails the first time a delivered row
216
+ * is swept, on a code path nobody watches. `Repository` validates the registered set at
217
+ * construction and throws naming what is missing, so composing it HERE moves that check to
218
+ * every consumer instead of the one host that remembered.
219
+ *
220
+ * ```ts
221
+ * const store = repositoryAsOutboxStore(
222
+ * createOutboxRepository(connection), // this
223
+ * { visibleAtField: 'nextVisibleAt' }, // iff you passed the same to createOutboxModel
224
+ * );
225
+ * ```
226
+ *
227
+ * `plugins` appends to the required set rather than replacing it — for an observability or
228
+ * cache plugin a deployment wants on this table too.
229
+ */
230
+ function createOutboxRepository(connection, options = {}) {
231
+ const { plugins = [], ...modelOptions } = options;
232
+ return new Repository(createOutboxModel(connection, modelOptions), [
233
+ methodRegistryPlugin(),
234
+ batchOperationsPlugin(),
235
+ ...plugins
236
+ ]);
237
+ }
238
+ //#endregion
239
+ export { OUTBOX_STATUSES, createOutboxModel, createOutboxRepository };
@@ -1,5 +1,5 @@
1
1
  import { a as PopulateSpec, i as ObjectId, l as SortSpec, s as SelectSpec } from "./core-62oo3N1C.mjs";
2
- import { h as RepositoryInstance, p as RepositoryContext } from "./repository-DCHRWRXw.mjs";
2
+ import { h as RepositoryInstance, p as RepositoryContext } from "./repository-Q16ErTu8.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) */
@@ -1,6 +1,6 @@
1
- import { S as RepositoryPluginName, b as MONGOKIT_PLUGIN_NAMES, l as Plugin, x as MongoKitPluginName } from "../repository-DCHRWRXw.mjs";
2
- import { A as ensureAuditTrailReady, C as AuditTrailMethods, D as AuditTrailQueryOptions, E as AuditTrailQuery, O as EnsureAuditTrailReadyOptions, S as AuditQueryResult, T as AuditTrailOptions, _ as RepositoryCachePluginOptions, a as DateSequentialIdOptions, b as AuditOperation, c as SequentialIdOptions, d as getNextSequence, f as prefixedId, g as RepositoryCacheHandle, h as CacheOptions, i as CustomIdOptions, k as auditTrailPlugin, l as customIdPlugin, m as CacheAdapter, o as IdGenerator, p as sequentialId, r as multiTenantPlugin, s as PrefixedIdOptions, t as MultiTenantOptions, u as dateSequentialId, v as cachePlugin, w as AuditTrailMode, x as AuditQueryOptions, y as AuditEntry } from "../multi-tenant.plugin-DVyVGREb.mjs";
3
- import { A as IdempotencyStorePluginOptions, B as AppendOnlyPluginOptions, C as LeasePluginOptions, D as immutableStatesPlugin, E as ImmutableStatesPluginOptions, F as cascadePlugin, H as AggregateHelpersMethods, I as BatchOperationsMethods, M as fieldFilterPlugin, N as ElasticSearchOptions, O as IdempotencyClaim, P as elasticSearchPlugin, R as batchOperationsPlugin, S as LeaseMethods, T as ImmutableClaimView, U as aggregateHelpersPlugin, V as appendOnlyPlugin, _ as observabilityPlugin, a as uniqueField, b as MethodRegistryRepository, c as TenantContext, d as SubdocumentMethods, f as subdocumentPlugin, g as OperationMetric, h as ObservabilityOptions, i as requireField, j as idempotencyStorePlugin, k as IdempotencyStoreMethods, l as TenantStore, m as softDeletePlugin, n as blockIf, o as validationChainPlugin, p as SoftDeleteMethods, r as immutableField, s as timestampPlugin, t as autoInject, u as createTenantContext, v as MongoOperationsMethods, w as leasePlugin, x as methodRegistryPlugin, y as mongoOperationsPlugin, z as auditLogPlugin } from "../validation-chain.plugin-BLecaKa0.mjs";
1
+ import { S as RepositoryPluginName, b as MONGOKIT_PLUGIN_NAMES, l as Plugin, x as MongoKitPluginName } from "../repository-Q16ErTu8.mjs";
2
+ import { A as ensureAuditTrailReady, C as AuditTrailMethods, D as AuditTrailQueryOptions, E as AuditTrailQuery, O as EnsureAuditTrailReadyOptions, S as AuditQueryResult, T as AuditTrailOptions, _ as RepositoryCachePluginOptions, a as DateSequentialIdOptions, b as AuditOperation, c as SequentialIdOptions, d as getNextSequence, f as prefixedId, g as RepositoryCacheHandle, h as CacheOptions, i as CustomIdOptions, k as auditTrailPlugin, l as customIdPlugin, m as CacheAdapter, o as IdGenerator, p as sequentialId, r as multiTenantPlugin, s as PrefixedIdOptions, t as MultiTenantOptions, u as dateSequentialId, v as cachePlugin, w as AuditTrailMode, x as AuditQueryOptions, y as AuditEntry } from "../multi-tenant.plugin-2VtSSm1Y.mjs";
3
+ import { A as IdempotencyStoreMethods, B as auditLogPlugin, C as LeaseMethods, D as ImmutableStatesPluginOptions, E as ImmutableClaimView, F as elasticSearchPlugin, H as appendOnlyPlugin, 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 AggregateHelpersMethods, V as AppendOnlyPluginOptions, W as aggregateHelpersPlugin, _ 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, z as batchOperationsPlugin } from "../validation-chain.plugin-BSshLJTA.mjs";
4
4
  import { ChangeLogStore } from "@classytic/repo-core/sync";
5
5
  //#region src/plugins/change-log.plugin.d.ts
6
6
  interface ChangeLogPluginOptions {
@@ -15,4 +15,4 @@ interface ChangeLogPluginOptions {
15
15
  }
16
16
  declare function changeLogPlugin(options: ChangeLogPluginOptions): Plugin;
17
17
  //#endregion
18
- export { type AggregateHelpersMethods, type AppendOnlyPluginOptions, type AuditEntry, type AuditOperation, type AuditQueryOptions, type AuditQueryResult, type AuditTrailMethods, type AuditTrailMode, type AuditTrailOptions, AuditTrailQuery, type AuditTrailQueryOptions, type BatchOperationsMethods, type CacheAdapter, type CacheOptions, type ChangeLogPluginOptions, type CustomIdOptions, type DateSequentialIdOptions, type ElasticSearchOptions, type EnsureAuditTrailReadyOptions, type IdGenerator, type IdempotencyClaim, type IdempotencyStoreMethods, type IdempotencyStorePluginOptions, type ImmutableClaimView, type ImmutableStatesPluginOptions, type LeaseMethods, type LeasePluginOptions, MONGOKIT_PLUGIN_NAMES, type MethodRegistryRepository, type MongoKitPluginName, type MongoOperationsMethods, type MultiTenantOptions, type ObservabilityOptions, type OperationMetric, type PrefixedIdOptions, type RepositoryCacheHandle, type RepositoryCachePluginOptions, type RepositoryPluginName, type SequentialIdOptions, type SoftDeleteMethods, type SubdocumentMethods, type TenantContext, type TenantStore, aggregateHelpersPlugin, appendOnlyPlugin, auditLogPlugin, auditTrailPlugin, autoInject, batchOperationsPlugin, blockIf, cachePlugin, cascadePlugin, changeLogPlugin, createTenantContext, customIdPlugin, dateSequentialId, elasticSearchPlugin, ensureAuditTrailReady, fieldFilterPlugin, getNextSequence, idempotencyStorePlugin, immutableField, immutableStatesPlugin, leasePlugin, methodRegistryPlugin, mongoOperationsPlugin, multiTenantPlugin, observabilityPlugin, prefixedId, requireField, sequentialId, softDeletePlugin, subdocumentPlugin, timestampPlugin, uniqueField, validationChainPlugin };
18
+ export { type AggregateHelpersMethods, type AppendOnlyPluginOptions, type AuditEntry, type AuditOperation, type AuditQueryOptions, type AuditQueryResult, type AuditTrailMethods, type AuditTrailMode, type AuditTrailOptions, AuditTrailQuery, type AuditTrailQueryOptions, type BatchOperationsMethods, type CacheAdapter, type CacheOptions, type ChangeLogPluginOptions, type CustomIdOptions, type DateSequentialIdOptions, type ElasticSearchOptions, type EnsureAuditTrailReadyOptions, type IdGenerator, type IdempotencyClaim, type IdempotencyStoreMethods, type IdempotencyStorePluginOptions, type ImmutableClaimView, type ImmutableStatesPluginOptions, type LeaseMethods, type LeasePluginOptions, MONGOKIT_PLUGIN_NAMES, type MethodRegistryRepository, type MongoKitPluginName, type MongoOperationsMethods, type MultiTenantOptions, type ObservabilityOptions, type OperationMetric, type OperationType, type PrefixedIdOptions, type RepositoryCacheHandle, type RepositoryCachePluginOptions, type RepositoryPluginName, type SequentialIdOptions, type SoftDeleteMethods, type SubdocumentMethods, type TenantContext, type TenantStore, aggregateHelpersPlugin, appendOnlyPlugin, auditLogPlugin, auditTrailPlugin, autoInject, batchOperationsPlugin, blockIf, cachePlugin, cascadePlugin, changeLogPlugin, createTenantContext, customIdPlugin, dateSequentialId, elasticSearchPlugin, ensureAuditTrailReady, fieldFilterPlugin, getNextSequence, idempotencyStorePlugin, immutableField, immutableStatesPlugin, leasePlugin, methodRegistryPlugin, mongoOperationsPlugin, multiTenantPlugin, observabilityPlugin, prefixedId, requireField, sequentialId, softDeletePlugin, subdocumentPlugin, timestampPlugin, uniqueField, validationChainPlugin };
@@ -1,5 +1,6 @@
1
- import { a as methodRegistryPlugin, c as getNextSequence, d as cachePlugin, f as batchOperationsPlugin, g as auditLogPlugin, h as ensureAuditTrailReady, i as multiTenantPlugin, l as prefixedId, m as auditTrailPlugin, n as softDeletePlugin, o as customIdPlugin, p as AuditTrailQuery, s as dateSequentialId, t as timestampPlugin, u as sequentialId } from "../timestamp.plugin-CFL6LlYZ.mjs";
2
- 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-CwOcUJC1.mjs";
1
+ import { a as customIdPlugin, c as prefixedId, d as AuditTrailQuery, f as auditTrailPlugin, i as multiTenantPlugin, l as sequentialId, m as auditLogPlugin, n as softDeletePlugin, o as dateSequentialId, p as ensureAuditTrailReady, s as getNextSequence, t as timestampPlugin, u as cachePlugin } from "../timestamp.plugin-ChFRcLRJ.mjs";
2
+ import { n as batchOperationsPlugin, t as methodRegistryPlugin } from "../method-registry.plugin-DblM7V6j.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-BxTl_dYR.mjs";
3
4
  //#region src/plugins/change-log.plugin.ts
4
5
  const PLUGIN_NAME = "changeLog";
5
6
  function isSkipped(context) {
@@ -1,6 +1,6 @@
1
1
  import { a as PopulateSpec, i as ObjectId, l as SortSpec, o as ReadPreferenceType, r as FilterQuery, s as SelectSpec, t as AnyDocument } from "./core-62oo3N1C.mjs";
2
2
  import { r as DocField } from "./type-utils-CLzRFtIA.mjs";
3
- import { RepositoryBase } from "@classytic/repo-core/repository";
3
+ import { RepoCapabilities, RepositoryBase } from "@classytic/repo-core/repository";
4
4
  import { Model, PipelineStage } from "mongoose";
5
5
  import { HttpError } from "@classytic/repo-core/errors";
6
6
  import { OffsetPaginationResult } from "@classytic/repo-core/pagination";
@@ -191,6 +191,24 @@ interface RepositoryOptions {
191
191
  * ```
192
192
  */
193
193
  requirePlugins?: readonly RepositoryPluginName[];
194
+ /**
195
+ * Override the whole runtime capability descriptor.
196
+ *
197
+ * By DEFAULT `repo.capabilities` is OBSERVED from the model's live
198
+ * connection topology (see `capabilities.ts`) — a standalone `mongod`
199
+ * reports `transactions: false`, and an unreadable topology reports
200
+ * `false` with `transactionsResolution: 'unknown'` so money kernels fail
201
+ * closed. Supply this only when you genuinely know better than the driver:
202
+ * a proxied/wrapped connection, a non-mongoose seam, or a test that needs
203
+ * to pin a descriptor.
204
+ *
205
+ * **Never pass `{ ...MONGOKIT_CAPABILITIES }` to quiet a boot gate.** That
206
+ * re-creates the exact defect the observation replaced: a declared
207
+ * capability nothing can falsify. If a deployment truly cannot do
208
+ * transactions and you accept the risk, say so at the KERNEL
209
+ * (`allowNonTransactional: true`), where the degradation is logged.
210
+ */
211
+ capabilities?: RepoCapabilities;
194
212
  /**
195
213
  * Standard Schema validator (Zod 3.24+, Valibot 1+, ArkType 2+, ...)
196
214
  * for write payloads. Forwarded to `RepositoryBase`, which validates
@@ -1,4 +1,5 @@
1
- import { a as Repository, n as CreateRepositoryConfig } from "../create-repository-D4ZU94b3.mjs";
1
+ import { n as Repository } from "../Repository-BXHkUtLk.mjs";
2
+ import { n as CreateRepositoryConfig } from "../create-repository-CvD4wzuN.mjs";
2
3
  import { Connection, Model, Schema } from "mongoose";
3
4
  import { MongoMemoryReplSet, MongoMemoryServer } from "mongodb-memory-server";
4
5
  //#region src/testkit/index.d.ts
@@ -7,6 +8,12 @@ interface MongoMemoryOptions {
7
8
  * Boot a single-node replica set instead of a standalone mongod. Required for
8
9
  * any code that opens a `session.startTransaction()` — a standalone server
9
10
  * rejects it with error 263. Slightly slower to start (~2-3s). Default false.
11
+ *
12
+ * Since 2026-08-05 this also changes what `repo.capabilities` REPORTS:
13
+ * capabilities are observed from the live topology, so a standalone harness
14
+ * yields `transactions: false` and any kernel boot gate built on it will
15
+ * (correctly) refuse to bind. Set `replset: true` when the code under test
16
+ * constructs a money kernel — or pass that kernel's `allowNonTransactional`.
10
17
  */
11
18
  replset?: boolean;
12
19
  /** Database name baked into the generated URI. Default `"mongokit-test"`. */
@@ -1,4 +1,4 @@
1
- import { t as createRepository } from "../create-repository-uhKt_oUT.mjs";
1
+ import { t as createRepository } from "../create-repository-DYe5YEie.mjs";
2
2
  import { createConnection } from "mongoose";
3
3
  //#region src/testkit/index.ts
4
4
  function idempotentStop(server) {