@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
@@ -1,4 +1,4 @@
1
- import { Document, Model, PipelineStage, PopulateOptions, Types } from "mongoose";
1
+ import { Connection, Document, Model, PipelineStage, PopulateOptions, Types } from "mongoose";
2
2
  //#region src/types/core.d.ts
3
3
  /**
4
4
  * Core query/document primitives shared across every mongokit module —
@@ -47,6 +47,79 @@ type SelectSpec = string | readonly string[] | Record<string, 0 | 1>;
47
47
  /** Filter query type for MongoDB queries (compatible with Mongoose 8 & 9) */
48
48
  type FilterQuery<_T = unknown> = Record<string, unknown>;
49
49
  //#endregion
50
+ //#region src/repository/query-defaults.d.ts
51
+ type ReadConcernLevel = 'local' | 'available' | 'majority' | 'linearizable' | 'snapshot';
52
+ interface WriteConcernSpec {
53
+ w?: number | 'majority';
54
+ j?: boolean;
55
+ wtimeoutMS?: number;
56
+ }
57
+ /** Defaults for every repository command. */
58
+ interface QueryDefaults {
59
+ maxTimeMS?: number;
60
+ readPreference?: ReadPreferenceType;
61
+ readConcern?: ReadConcernLevel;
62
+ writeConcern?: WriteConcernSpec;
63
+ /** Profiler / log tag. */
64
+ comment?: string;
65
+ }
66
+ /** Defaults for aggregations only; consulted before {@link QueryDefaults}. */
67
+ interface AggregateDefaults {
68
+ maxTimeMs?: number;
69
+ allowDiskUse?: boolean;
70
+ }
71
+ /** What one call may pass. Every field beats every default. */
72
+ interface PerCallQueryOptions {
73
+ maxTimeMS?: number;
74
+ readPreference?: ReadPreferenceType;
75
+ readConcern?: ReadConcernLevel;
76
+ writeConcern?: WriteConcernSpec;
77
+ comment?: string;
78
+ hint?: string | Record<string, 1 | -1>;
79
+ allowDiskUse?: boolean;
80
+ session?: unknown;
81
+ }
82
+ /** The options a command is executed with, after resolution. */
83
+ interface ResolvedQueryOptions {
84
+ maxTimeMS?: number;
85
+ readPreference?: ReadPreferenceType;
86
+ readConcern?: ReadConcernLevel;
87
+ writeConcern?: WriteConcernSpec;
88
+ comment?: string;
89
+ hint?: string | Record<string, 1 | -1>;
90
+ allowDiskUse?: boolean;
91
+ }
92
+ type CommandKind = 'read' | 'aggregate' | 'write' | 'findAndModify';
93
+ /** Set the deployment's query defaults. Merges; an explicit `undefined` changes nothing. */
94
+ declare function configureQueryDefaults(defaults: QueryDefaults): void;
95
+ /** Set the deployment's aggregation defaults. Merges; an explicit `undefined` changes nothing. */
96
+ declare function configureAggregateDefaults(defaults: AggregateDefaults): void;
97
+ /** A copy of the deployment defaults. */
98
+ declare function getQueryDefaults(): {
99
+ query: QueryDefaults;
100
+ aggregate: AggregateDefaults;
101
+ };
102
+ /** Back to no deployment defaults. For tests. */
103
+ declare function resetQueryDefaults(): void;
104
+ /**
105
+ * Boot check (M7): throws unless the deployment sets a query `maxTimeMS`, and — when a connection
106
+ * is passed — unless its client carries a CSOT `timeoutMS`. Returns what it verified.
107
+ */
108
+ declare function assertQueryDefaultsConfigured(connection?: Connection): {
109
+ maxTimeMS: number;
110
+ timeoutMS?: number;
111
+ };
112
+ declare const QUERY_DEFAULTS_ERROR_CODES: {
113
+ readonly NOT_CONFIGURED: "mongokit.query_defaults.not_configured";
114
+ /** A per-call bound inside a transaction that carries its own CSOT budget (the driver forbids both). */
115
+ readonly TIMEOUT_IN_TRANSACTION: "mongokit.query_defaults.timeout_in_transaction";
116
+ };
117
+ /** Resolve the options one command runs with. `repo` is the repository's own defaults. */
118
+ declare function resolveQueryOptions(kind: CommandKind, perCall: PerCallQueryOptions, repo?: {
119
+ query?: QueryDefaults;
120
+ aggregate?: AggregateDefaults;
121
+ }): ResolvedQueryOptions;
122
+ //#endregion
50
123
  //#region src/pagination/utils/cursor-signing.d.ts
51
124
  /**
52
125
  * HMAC signing for pagination cursors.
@@ -108,7 +181,11 @@ type CursorSecret = string | readonly string[];
108
181
  * Declared ONCE and referenced everywhere — this union was hand-copied to eight
109
182
  * declarations, which is how the aggregate variant silently lost a member.
110
183
  */
111
- type CountStrategy = 'exact' | 'estimated' | 'capped' | 'none';
184
+ /**
185
+ * `cached`: the capped count memoised per (collection, filter, collation, hint) for
186
+ * `countCacheTtlMs`; never inside a transaction (it would publish uncommitted rows).
187
+ */
188
+ type CountStrategy = 'exact' | 'estimated' | 'capped' | 'none' | 'cached';
112
189
  /**
113
190
  * What mongokit adds to repo-core's offset envelope, via its reserved `TExtra`
114
191
  * slot: `OffsetPaginationResult<TDoc, MongokitPageExtras>`.
@@ -123,16 +200,13 @@ type MongokitPageExtras = {
123
200
  /** Deep-page or clamped-limit advice. Human-readable; never branch on it. */
124
201
  warning?: string;
125
202
  /**
126
- * `total` is a FLOOR, not a count — the scan stopped at `countLimit`. Render
127
- * it as `10,000+`, and never compute a percentage or a last-page number from
128
- * a total carrying this flag.
129
- *
130
- * A separate boolean rather than a sentinel `total` (`-1`, `null`): a total
131
- * that lies in a field typed `number` is read as a number by every consumer
132
- * that has not been taught otherwise, which is the failure this flag exists
133
- * to make impossible to miss.
203
+ * `total` is not an exact count of the rows matching right now: a capped count at its
204
+ * ceiling (a floor — render `10,000+`), an estimate, a memoised (`cached`) count, or no count
205
+ * (`none`). Never derive a last-page number or a percentage from a total carrying it.
134
206
  */
135
- totalIsLowerBound?: boolean;
207
+ totalIsEstimate: boolean;
208
+ /** When `total` was counted; `null` when nothing was counted (`none`). */
209
+ countedAt: Date | null;
136
210
  };
137
211
  /** Pagination configuration */
138
212
  interface PaginationConfig {
@@ -142,6 +216,13 @@ interface PaginationConfig {
142
216
  maxLimit?: number;
143
217
  /** Maximum allowed page number (default: 10000) */
144
218
  maxPage?: number;
219
+ /**
220
+ * Deepest `skip` an offset page may ask for (default 100,000). Past it the call is refused
221
+ * with `mongokit.pagination.offset_too_deep` (400): walk with keyset (`after`) instead.
222
+ */
223
+ maxOffset?: number;
224
+ /** Freshness window of `countStrategy: 'cached'` (default 30,000 ms). */
225
+ countCacheTtlMs?: number;
145
226
  /** Page number that triggers performance warning (default: 100) */
146
227
  deepPageThreshold?: number;
147
228
  /** Cursor version for forward compatibility (default: 1) */
@@ -249,6 +330,13 @@ interface BasePaginationOptions {
249
330
  maxTimeMS?: number;
250
331
  /** Read preference for replica sets (e.g. 'secondaryPreferred') */
251
332
  readPreference?: ReadPreferenceType;
333
+ readConcern?: ReadConcernLevel;
334
+ comment?: string;
335
+ /**
336
+ * Already-resolved time bound and concerns (the Repository passes these, defaults folded in).
337
+ * When absent the engine resolves the per-call fields above against the deployment defaults.
338
+ */
339
+ queryOptions?: ResolvedQueryOptions;
252
340
  /** Collation for locale-aware string comparison and case-insensitive sorting */
253
341
  collation?: CollationOptions;
254
342
  }
@@ -325,9 +413,20 @@ interface AggregatePaginationOptions {
325
413
  mode?: 'offset';
326
414
  /** Read preference for replica sets (e.g. 'secondaryPreferred') */
327
415
  readPreference?: ReadPreferenceType;
416
+ readConcern?: ReadConcernLevel;
417
+ comment?: string;
418
+ /** Collections the caller asserts are company-wide (see `join-scope.ts`). */
419
+ unscopedJoins?: readonly string[];
420
+ /**
421
+ * The caller states the pipeline's last `$sort` is a TOTAL order (unique per output row), so the
422
+ * not-provably-total warning is silenced. Only for a sort total by domain knowledge.
423
+ */
424
+ sortIsTotal?: boolean;
425
+ /** Already-resolved options; see {@link BasePaginationOptions.queryOptions}. */
426
+ queryOptions?: ResolvedQueryOptions;
328
427
  }
329
428
  /** Value type identifier for cursor serialization */
330
- type ValueType = 'date' | 'objectid' | 'boolean' | 'number' | 'string' | 'null' | 'unknown';
429
+ type ValueType = 'date' | 'objectid' | 'decimal' | 'boolean' | 'number' | 'string' | 'null' | 'unknown';
331
430
  /** Cursor payload */
332
431
  interface CursorPayload {
333
432
  /** Primary sort field value (legacy single-field) */
@@ -346,6 +445,8 @@ interface CursorPayload {
346
445
  vals?: Record<string, string | number | boolean | null>;
347
446
  /** Compound sort value types */
348
447
  types?: Record<string, ValueType>;
448
+ /** Scope fingerprint: collection + post-policy filter + collation (see `utils/guards.ts`). */
449
+ fp: string;
349
450
  }
350
451
  /** Decoded cursor */
351
452
  interface DecodedCursor {
@@ -359,6 +460,8 @@ interface DecodedCursor {
359
460
  version: number;
360
461
  /** All sort field values (rehydrated) — for compound sort */
361
462
  values?: Record<string, unknown>;
463
+ /** Scope fingerprint the cursor was minted under. */
464
+ scope: string;
362
465
  }
363
466
  //#endregion
364
- export { PopulateSpec as _, CursorPayload as a, SortDirection as b, MongokitPageExtras as c, ValueType as d, CursorSecret as f, ObjectId as g, FilterQuery as h, CountStrategy as i, OffsetPaginationOptions as l, AnyModel as m, BasePaginationOptions as n, DecodedCursor as o, AnyDocument as p, CollationOptions as r, KeysetPaginationOptions as s, AggregatePaginationOptions as t, PaginationConfig as u, ReadPreferenceType as v, SortSpec as x, SelectSpec as y };
467
+ export { ObjectId as A, configureQueryDefaults as C, AnyDocument as D, resolveQueryOptions as E, SortSpec as F, ReadPreferenceType as M, SelectSpec as N, AnyModel as O, SortDirection as P, configureAggregateDefaults as S, resetQueryDefaults as T, QueryDefaults as _, CursorPayload as a, WriteConcernSpec as b, MongokitPageExtras as c, ValueType as d, CursorSecret as f, QUERY_DEFAULTS_ERROR_CODES as g, PerCallQueryOptions as h, CountStrategy as i, PopulateSpec as j, FilterQuery as k, OffsetPaginationOptions as l, CommandKind as m, BasePaginationOptions as n, DecodedCursor as o, AggregateDefaults as p, CollationOptions as r, KeysetPaginationOptions as s, AggregatePaginationOptions as t, PaginationConfig as u, ReadConcernLevel as v, getQueryDefaults as w, assertQueryDefaultsConfigured as x, ResolvedQueryOptions as y };
@@ -1,5 +1,5 @@
1
- import { _ as PopulateSpec, g as ObjectId, x as SortSpec, y as SelectSpec } from "./pagination-BpIv2bdS.mjs";
2
- import { h as RepositoryInstance, p as RepositoryContext } from "./repository-ZbLtL7Nk.mjs";
1
+ import { A as ObjectId, F as SortSpec, N as SelectSpec, j as PopulateSpec } from "./pagination-D6psses0.mjs";
2
+ import { h as RepositoryInstance, p as RepositoryContext } from "./repository-DNHfjAZX.mjs";
3
3
  import { OffsetPaginationResult } from "@classytic/repo-core/pagination";
4
4
  //#region src/types/plugin-options.d.ts
5
5
  /** Field preset configuration (field-filter plugin) */
@@ -69,6 +69,9 @@ interface SoftDeleteRepository<TDoc = unknown> {
69
69
  */
70
70
  restore(id: string | ObjectId, options?: {
71
71
  session?: unknown;
72
+ organizationId?: unknown;
73
+ bypassTenant?: boolean;
74
+ [key: string]: unknown;
72
75
  }): Promise<TDoc>;
73
76
  /**
74
77
  * Get all soft-deleted documents
@@ -109,6 +112,17 @@ interface CascadeRelation {
109
112
  model?: string;
110
113
  /** Foreign key field in the related model that references the deleted document */
111
114
  foreignKey: string;
115
+ /**
116
+ * WHICH parent field `foreignKey` stores. Default: the parent repository's
117
+ * `idField` (`_id` unless the repository declares another).
118
+ *
119
+ * Set it when the two differ. An order repository addressed by `orderNumber`
120
+ * whose children store the order's `_id` declares `parentKey: '_id'`;
121
+ * without it, `delete('ORD-2026-0001')` cascaded with the order NUMBER and
122
+ * every child delete failed on the ObjectId cast. The cascade reads the
123
+ * parent's value for this field before the delete, under the same session.
124
+ */
125
+ parentKey?: string;
112
126
  /** Whether to use soft delete if available (default: follows parent behavior) */
113
127
  softDelete?: boolean;
114
128
  /**
@@ -1,6 +1,6 @@
1
- import { S as RepositoryPluginName, b as MONGOKIT_PLUGIN_NAMES, l as Plugin, x as MongoKitPluginName } from "../repository-ZbLtL7Nk.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-B-tzNeYL.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-DvrLJIbJ.mjs";
1
+ import { S as RepositoryPluginName, b as MONGOKIT_PLUGIN_NAMES, l as Plugin, x as MongoKitPluginName } from "../repository-DNHfjAZX.mjs";
2
+ import { A as EnsureAuditTrailReadyOptions, C as AuditQueryOptions, D as AuditTrailOptions, E as AuditTrailMode, M as ensureAuditTrailReady, O as AuditTrailQuery, S as AuditOperation, T as AuditTrailMethods, _ as CacheOptions, a as DateSequentialIdOptions, b as cachePlugin, c as SequentialIdOptions, f as getNextSequence, g as CacheAdapter, h as sequentialId, i as CustomIdOptions, j as auditTrailPlugin, k as AuditTrailQueryOptions, l as customIdPlugin, o as IdGenerator, p as prefixedId, r as multiTenantPlugin, s as PrefixedIdOptions, t as MultiTenantOptions, u as dateSequentialId, v as RepositoryCacheHandle, w as AuditQueryResult, x as AuditEntry, y as RepositoryCachePluginOptions } from "../multi-tenant.plugin-a3UhQARj.mjs";
3
+ import { A as IdempotencyStoreMethods, B as batchOperationsPlugin, C as LeaseMethods, D as ImmutableStatesPluginOptions, E as ImmutableClaimView, F as elasticSearchPlugin, G as aggregateHelpersPlugin, H as AppendOnlyPluginOptions, I as cascadePlugin, L as BatchOperationsMethods, M as idempotencyStorePlugin, N as fieldFilterPlugin, O as immutableStatesPlugin, P as ElasticSearchOptions, S as methodRegistryPlugin, T as leasePlugin, U as appendOnlyPlugin, V as auditLogPlugin, W as AggregateHelpersMethods, _ as OperationMetric, a as requireField, b as mongoOperationsPlugin, c as timestampPlugin, d as createTenantContext, f as SubdocumentMethods, g as ObservabilityOptions, h as softDeletePlugin, i as immutableField, j as IdempotencyStorePluginOptions, k as IdempotencyClaim, l as TenantContext, m as SoftDeleteMethods, n as autoInject, o as uniqueField, p as subdocumentPlugin, r as blockIf, s as validationChainPlugin, t as OperationType, u as TenantStore, v as observabilityPlugin, w as LeasePluginOptions, x as MethodRegistryRepository, y as MongoOperationsMethods } from "../validation-chain.plugin-CTcNJCmV.mjs";
4
4
  import { ChangeLogStore } from "@classytic/repo-core/sync";
5
5
  //#region src/plugins/change-log.plugin.d.ts
6
6
  interface ChangeLogPluginOptions {
@@ -12,6 +12,11 @@ interface ChangeLogPluginOptions {
12
12
  tenantField?: string;
13
13
  /** Monotonic per-doc version field. Default `version` (kernel convention). */
14
14
  versionField?: string;
15
+ /**
16
+ * The document as a subscriber may see it — fields picked, secrets (a cost price) dropped. Tenant
17
+ * and version are still read from the whole document. Default: the whole document.
18
+ */
19
+ project?: (doc: Record<string, unknown>) => Record<string, unknown>;
15
20
  }
16
21
  declare function changeLogPlugin(options: ChangeLogPluginOptions): Plugin;
17
22
  //#endregion
@@ -1,14 +1,18 @@
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-B5eiaItQ.mjs";
1
+ import { _ as AuditTrailQuery, b as auditLogPlugin, f as getNextSequence, g as cachePlugin, h as sequentialId, i as multiTenantPlugin, l as customIdPlugin, n as softDeletePlugin, p as prefixedId, t as timestampPlugin, u as dateSequentialId, v as auditTrailPlugin, y as ensureAuditTrailReady } from "../timestamp.plugin-zfdLYZiO.mjs";
2
2
  import { n as batchOperationsPlugin, t as methodRegistryPlugin } from "../method-registry.plugin-C63pGn1d.mjs";
3
- import { _ as cascadePlugin, a as uniqueField, c as subdocumentPlugin, d as mongoOperationsPlugin, f as leasePlugin, g as elasticSearchPlugin, h as fieldFilterPlugin, i as requireField, l as observabilityPlugin, m as idempotencyStorePlugin, n as blockIf, o as validationChainPlugin, p as immutableStatesPlugin, r as immutableField, s as createTenantContext, t as autoInject, u as MONGOKIT_PLUGIN_NAMES, v as appendOnlyPlugin, y as aggregateHelpersPlugin } from "../validation-chain.plugin-Bju1D1Sn.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-CHUHWMtk.mjs";
4
4
  //#region src/plugins/change-log.plugin.ts
5
5
  const PLUGIN_NAME = "changeLog";
6
+ /** Marks a repository already captured: the store and scope it writes to. */
7
+ const CAPTURED = Symbol.for("mongokit.changeLog.capture");
8
+ /** Where `before:updateMany` / `before:deleteMany` leave the ids they matched for the after hook. */
9
+ const MATCHED_IDS = Symbol("changeLog.matchedIds");
6
10
  function isSkipped(context) {
7
11
  const list = context.skipPlugins;
8
12
  return Array.isArray(list) && list.includes(PLUGIN_NAME);
9
13
  }
10
14
  function changeLogPlugin(options) {
11
- const { store, scope, tenantField = "organizationId", versionField = "version" } = options;
15
+ const { store, scope, tenantField = "organizationId", versionField = "version", project } = options;
12
16
  const toRecord = (doc) => {
13
17
  if (!doc || typeof doc !== "object") return null;
14
18
  const d = doc;
@@ -21,51 +25,120 @@ function changeLogPlugin(options) {
21
25
  return updatedAt instanceof Date ? updatedAt.getTime() : 0;
22
26
  };
23
27
  const tenantOf = (doc, context) => {
24
- const raw = doc[tenantField] ?? context.organizationId;
28
+ const raw = doc?.[tenantField] ?? context.organizationId;
25
29
  return raw === void 0 || raw === null ? void 0 : String(raw);
26
30
  };
27
- const capture = async (op, doc, context) => {
31
+ const appendOptions = (context) => context.session !== void 0 ? { session: context.session } : void 0;
32
+ const upsert = async (doc, context) => {
28
33
  const record = toRecord(doc);
29
34
  if (!record || record._id === void 0) return;
30
35
  const tenantId = tenantOf(record, context);
31
36
  await store.append({
32
37
  scope,
33
38
  docId: String(record._id),
34
- op,
39
+ op: "upsert",
35
40
  version: versionOf(record),
36
- ...op === "upsert" ? { doc: record } : {},
41
+ doc: project ? project(record) : record,
37
42
  ...tenantId !== void 0 ? { tenantId } : {}
38
- }, context.session !== void 0 ? { session: context.session } : void 0);
43
+ }, appendOptions(context));
44
+ };
45
+ const tombstone = async (docId, context) => {
46
+ if (docId === void 0 || docId === null) return;
47
+ const tenantId = tenantOf(null, context);
48
+ await store.append({
49
+ scope,
50
+ docId: String(docId),
51
+ op: "delete",
52
+ version: 0,
53
+ ...tenantId !== void 0 ? { tenantId } : {}
54
+ }, appendOptions(context));
39
55
  };
40
56
  return {
41
57
  name: PLUGIN_NAME,
42
58
  apply(repo) {
59
+ const marked = repo;
60
+ const existing = marked[CAPTURED];
61
+ if (existing) {
62
+ if (existing.store === store && existing.scope === scope) return;
63
+ throw new Error(`[mongokit] changeLogPlugin is already capturing this repository as "${existing.scope}"`);
64
+ }
65
+ marked[CAPTURED] = {
66
+ store,
67
+ scope
68
+ };
69
+ /** The whole document as committed in this session — never a projection. */
70
+ const reread = async (id, context) => {
71
+ const query = repo.Model.findById(id).lean();
72
+ if (context.session !== void 0) query.session(context.session);
73
+ return query.exec();
74
+ };
75
+ const matchingIds = async (context, filter) => {
76
+ const query = repo.Model.find(filter ?? {}).select({ _id: 1 }).lean();
77
+ if (context.session !== void 0) query.session(context.session);
78
+ return (await query.exec()).map((d) => d._id);
79
+ };
43
80
  repo.on("after:create", async ({ context, result }) => {
44
- if (isSkipped(context) || !result) return;
45
- await capture("upsert", result, context);
81
+ if (!isSkipped(context) && result) await upsert(result, context);
46
82
  });
47
83
  repo.on("after:createMany", async ({ context, result }) => {
48
84
  if (isSkipped(context) || !Array.isArray(result)) return;
49
- for (const doc of result) await capture("upsert", doc, context);
85
+ for (const doc of result) await upsert(doc, context);
50
86
  });
51
87
  repo.on("after:update", async ({ context, result }) => {
52
- if (isSkipped(context) || !result) return;
53
- await capture("upsert", result, context);
88
+ if (!isSkipped(context) && result) await upsert(result, context);
89
+ });
90
+ for (const op of [
91
+ "claim",
92
+ "claimVersion",
93
+ "findOneAndUpdate",
94
+ "getOrCreate",
95
+ "restore"
96
+ ]) repo.on(`after:${op}`, async ({ context, result }) => {
97
+ if (isSkipped(context)) return;
98
+ const id = toRecord(result)?._id;
99
+ if (id === void 0) return;
100
+ const doc = await reread(id, context);
101
+ if (doc) await upsert(doc, context);
102
+ else await tombstone(id, context);
103
+ });
104
+ for (const op of ["updateMany", "deleteMany"]) repo.on(`before:${op}`, async (context) => {
105
+ if (!isSkipped(context)) context[MATCHED_IDS] = await matchingIds(context, context.query);
106
+ });
107
+ /** Each re-read document as an upsert, or a tombstone where it is gone. */
108
+ const capture = async (ids, context) => {
109
+ for (const id of ids) {
110
+ const doc = await reread(id, context);
111
+ if (doc) await upsert(doc, context);
112
+ else await tombstone(id, context);
113
+ }
114
+ };
115
+ repo.on("after:updateMany", async ({ context }) => {
116
+ if (!isSkipped(context)) await capture(context[MATCHED_IDS] ?? [], context);
117
+ });
118
+ repo.on("after:anonymize", async ({ context, result }) => {
119
+ if (!isSkipped(context)) await capture(result.ids, context);
120
+ });
121
+ repo.on("after:deleteMany", async ({ context }) => {
122
+ if (!isSkipped(context)) await capture(context[MATCHED_IDS] ?? [], context);
54
123
  });
55
124
  repo.on("after:delete", async ({ context, result }) => {
56
125
  if (isSkipped(context) || !result) return;
57
- const summary = result;
58
- const ctxId = context.id;
59
- const docId = summary.id ?? ctxId;
60
- if (docId === void 0 || docId === null) return;
61
- const tenantRaw = context.organizationId;
62
- await store.append({
63
- scope,
64
- docId: String(docId),
65
- op: "delete",
66
- version: 0,
67
- ...tenantRaw !== void 0 && tenantRaw !== null ? { tenantId: String(tenantRaw) } : {}
68
- }, context.session !== void 0 ? { session: context.session } : void 0);
126
+ await tombstone(result.id ?? context.id, context);
127
+ });
128
+ repo.on("before:bulkWrite", async (context) => {
129
+ if (isSkipped(context)) return;
130
+ const ids = [];
131
+ for (const op of context.operations ?? []) {
132
+ const [kind, spec] = Object.entries(op)[0] ?? [];
133
+ if (!kind || !spec) continue;
134
+ if (kind === "insertOne") ids.push(spec.document?._id);
135
+ else ids.push(...await matchingIds(context, spec.filter));
136
+ }
137
+ context[MATCHED_IDS] = ids.filter((id) => id !== void 0);
138
+ });
139
+ repo.on("after:bulkWrite", async ({ context, result }) => {
140
+ if (isSkipped(context)) return;
141
+ await capture([...context[MATCHED_IDS] ?? [], ...Object.values(result?.upsertedIds ?? {})], context);
69
142
  });
70
143
  }
71
144
  };
@@ -0,0 +1,208 @@
1
+ import { t as createError } from "./error-BuByZwKU.mjs";
2
+ import mongoose from "mongoose";
3
+ //#region src/repository/query-defaults.ts
4
+ /**
5
+ * Query defaults — the time bound and read/write concerns every repository command carries.
6
+ *
7
+ * Resolution, per option: per call → repository (`RepositoryOptions.queryDefaults` /
8
+ * `aggregateDefaults`) → deployment (`configureQueryDefaults` / `configureAggregateDefaults`).
9
+ * An explicit value always wins (FL1). For an aggregation the aggregate chain is consulted before
10
+ * the generic query chain, because it is the more specific instruction.
11
+ *
12
+ * The bound travels as per-op CSOT `timeoutMS` (the driver derives the wire `maxTimeMS` from
13
+ * it, and a client-level `timeoutMS` cannot override it). It and `comment` go to the commands Mongoose itself bounds with its global `maxTimeMS`
14
+ * (find, findOne, countDocuments, distinct, findOneAndUpdate, aggregate:
15
+ * lib/query.js:2465,2812,2905,3074,3610; lib/aggregate.js:1106). Writes get `writeConcern`. The
16
+ * connection's CSOT `timeoutMS` bounds everything else; {@link assertQueryDefaultsConfigured}
17
+ * checks both at boot. Inside a transaction a DEFAULT read/write concern or read preference is
18
+ * dropped (the transaction owns them); a per-call one is passed and the driver judges it.
19
+ */
20
+ /** On `globalThis`, for the reason `pagination/defaults.ts` gives: a duplicated install must share it. */
21
+ const POLICY_SLOT = Symbol.for("classytic.mongokit.queryDefaults");
22
+ function policy() {
23
+ const holder = globalThis;
24
+ holder[POLICY_SLOT] ??= {
25
+ query: {},
26
+ aggregate: {}
27
+ };
28
+ return holder[POLICY_SLOT];
29
+ }
30
+ function defined(input) {
31
+ return Object.fromEntries(Object.entries(input).filter(([, v]) => v !== void 0));
32
+ }
33
+ function assertPositive(name, value) {
34
+ if (value !== void 0 && !(Number.isFinite(value) && value > 0)) throw new TypeError(`[mongokit] ${name} must be a positive number of milliseconds, got ${value}`);
35
+ }
36
+ /** Set the deployment's query defaults. Merges; an explicit `undefined` changes nothing. */
37
+ function configureQueryDefaults(defaults) {
38
+ assertPositive("queryDefaults.maxTimeMS", defaults.maxTimeMS);
39
+ Object.assign(policy().query, defined(defaults));
40
+ }
41
+ /** Set the deployment's aggregation defaults. Merges; an explicit `undefined` changes nothing. */
42
+ function configureAggregateDefaults(defaults) {
43
+ assertPositive("aggregateDefaults.maxTimeMs", defaults.maxTimeMs);
44
+ Object.assign(policy().aggregate, defined(defaults));
45
+ }
46
+ /** A copy of the deployment defaults. */
47
+ function getQueryDefaults() {
48
+ const p = policy();
49
+ return {
50
+ query: { ...p.query },
51
+ aggregate: { ...p.aggregate }
52
+ };
53
+ }
54
+ /** Back to no deployment defaults. For tests. */
55
+ function resetQueryDefaults() {
56
+ const holder = globalThis;
57
+ holder[POLICY_SLOT] = {
58
+ query: {},
59
+ aggregate: {}
60
+ };
61
+ }
62
+ /**
63
+ * Boot check (M7): throws unless the deployment sets a query `maxTimeMS`, and — when a connection
64
+ * is passed — unless its client carries a CSOT `timeoutMS`. Returns what it verified.
65
+ */
66
+ function assertQueryDefaultsConfigured(connection) {
67
+ const maxTimeMS = policy().query.maxTimeMS;
68
+ if (maxTimeMS === void 0) throw createError(500, "[mongokit] no deployment query time bound: call configureQueryDefaults({ maxTimeMS }) at boot", {
69
+ code: QUERY_DEFAULTS_ERROR_CODES.NOT_CONFIGURED,
70
+ meta: { missing: "maxTimeMS" }
71
+ });
72
+ if (!connection) return { maxTimeMS };
73
+ const timeoutMS = connection.getClient().options.timeoutMS;
74
+ if (!(typeof timeoutMS === "number" && timeoutMS > 0)) throw createError(500, "[mongokit] the connection has no CSOT timeoutMS: pass { timeoutMS } to mongoose.connect()", {
75
+ code: QUERY_DEFAULTS_ERROR_CODES.NOT_CONFIGURED,
76
+ meta: { missing: "timeoutMS" }
77
+ });
78
+ return {
79
+ maxTimeMS,
80
+ timeoutMS
81
+ };
82
+ }
83
+ const QUERY_DEFAULTS_ERROR_CODES = {
84
+ NOT_CONFIGURED: "mongokit.query_defaults.not_configured",
85
+ /** A per-call bound inside a transaction that carries its own CSOT budget (the driver forbids both). */
86
+ TIMEOUT_IN_TRANSACTION: "mongokit.query_defaults.timeout_in_transaction"
87
+ };
88
+ const TX_DEADLINE = Symbol.for("@classytic/mongokit/transaction-deadline");
89
+ /**
90
+ * Set (or clear) the transaction budget's deadline (`performance.now()` ms) on a session.
91
+ * `runTransaction` sets it for each attempt; every mongokit op on the session is then bounded
92
+ * by what remains of it, so a transaction budget is ONE budget, as with the driver's own.
93
+ */
94
+ function setTransactionDeadline(session, deadline) {
95
+ Object.defineProperty(session, TX_DEADLINE, {
96
+ value: deadline,
97
+ writable: true,
98
+ configurable: true,
99
+ enumerable: false
100
+ });
101
+ }
102
+ function transactionRemaining(session) {
103
+ if (!session || typeof session !== "object") return void 0;
104
+ const deadline = session[TX_DEADLINE];
105
+ return deadline === void 0 ? void 0 : Math.floor(deadline - performance.now());
106
+ }
107
+ function inTransaction(session) {
108
+ const s = session;
109
+ return typeof s?.inTransaction === "function" && s.inTransaction();
110
+ }
111
+ /**
112
+ * A `withTransaction` running under a CSOT budget: the driver refuses any per-op `timeoutMS`
113
+ * there (mongodb lib/utils.js:451, `session.explicit && session.timeoutContext != null`).
114
+ */
115
+ function csotTransaction(session) {
116
+ const s = session;
117
+ return inTransaction(session) && s?.explicit === true && s.timeoutContext != null;
118
+ }
119
+ /** Resolve the options one command runs with. `repo` is the repository's own defaults. */
120
+ function resolveQueryOptions(kind, perCall, repo = {}) {
121
+ const deployment = policy();
122
+ const q = {
123
+ ...deployment.query,
124
+ ...defined(repo.query ?? {})
125
+ };
126
+ const agg = {
127
+ ...deployment.aggregate,
128
+ ...defined(repo.aggregate ?? {})
129
+ };
130
+ const tx = inTransaction(perCall.session);
131
+ const out = {};
132
+ const budget = tx ? transactionRemaining(perCall.session) : void 0;
133
+ if (budget !== void 0 && budget <= 0) throw new mongoose.mongo.MongoOperationTimeoutError("Transaction budget exhausted before this operation");
134
+ if (kind !== "write") {
135
+ if (csotTransaction(perCall.session) && perCall.maxTimeMS !== void 0) throw createError(400, "[mongokit] a per-call maxTimeMS cannot be honoured inside a transaction with its own timeoutMS budget; bound the transaction instead", { code: QUERY_DEFAULTS_ERROR_CODES.TIMEOUT_IN_TRANSACTION });
136
+ const bound = tx ? perCall.maxTimeMS : perCall.maxTimeMS ?? (kind === "aggregate" ? repo.aggregate?.maxTimeMs ?? deployment.aggregate.maxTimeMs : void 0) ?? q.maxTimeMS;
137
+ const capped = budget === void 0 ? bound : Math.min(bound ?? Number.POSITIVE_INFINITY, budget);
138
+ if (capped !== void 0) out.maxTimeMS = capped;
139
+ const comment = perCall.comment ?? q.comment;
140
+ if (comment !== void 0) out.comment = comment;
141
+ if (perCall.hint !== void 0) out.hint = perCall.hint;
142
+ }
143
+ if (kind === "read" || kind === "aggregate") {
144
+ const pref = perCall.readPreference ?? (tx ? void 0 : q.readPreference);
145
+ if (pref !== void 0) out.readPreference = pref;
146
+ const concern = perCall.readConcern ?? (tx ? void 0 : q.readConcern);
147
+ if (concern !== void 0) out.readConcern = concern;
148
+ }
149
+ if (kind === "aggregate") {
150
+ const disk = perCall.allowDiskUse ?? agg.allowDiskUse;
151
+ if (disk !== void 0) out.allowDiskUse = disk;
152
+ }
153
+ if (kind === "write" && budget !== void 0) out.maxTimeMS = budget;
154
+ if (kind === "write" || kind === "findAndModify") {
155
+ const wc = perCall.writeConcern ?? (tx ? void 0 : q.writeConcern);
156
+ if (wc !== void 0) out.writeConcern = wc;
157
+ }
158
+ return out;
159
+ }
160
+ const READ_PREFERENCE_MODES = [
161
+ "primary",
162
+ "primaryPreferred",
163
+ "secondary",
164
+ "secondaryPreferred",
165
+ "nearest"
166
+ ];
167
+ function isReadPreferenceMode(value) {
168
+ return READ_PREFERENCE_MODES.includes(value);
169
+ }
170
+ /** A read preference outside the driver's modes is refused, never passed through (FL2). */
171
+ function readPreferenceMode(value) {
172
+ if (isReadPreferenceMode(value)) return value;
173
+ throw new TypeError(`[mongokit] unknown readPreference '${value}'; expected one of ${READ_PREFERENCE_MODES.join(", ")}`);
174
+ }
175
+ /** Apply resolved options to a mongoose Query. */
176
+ function applyToQuery(query, o) {
177
+ if (o.maxTimeMS !== void 0) {
178
+ const csot = { timeoutMS: o.maxTimeMS };
179
+ query.setOptions(csot);
180
+ }
181
+ if (o.comment !== void 0) query.setOptions({ comment: o.comment });
182
+ if (o.readPreference !== void 0) query.read(readPreferenceMode(o.readPreference));
183
+ if (o.readConcern !== void 0) query.readConcern(o.readConcern);
184
+ if (o.writeConcern !== void 0) query.setOptions({ writeConcern: o.writeConcern });
185
+ if (o.hint !== void 0) query.hint(o.hint);
186
+ }
187
+ /**
188
+ * Make a find cursor's bound apply PER BATCH (CSOT `timeoutMode: 'iteration'`). Only when some
189
+ * `timeoutMS` exists (this op's, or the client's): the driver refuses a timeoutMode without one
190
+ * (mongodb lib/cursor/abstract_cursor.js:114). In iteration mode the driver enforces the budget
191
+ * client-side per batch and sends no wire maxTimeMS.
192
+ */
193
+ function applyCursorTimeoutMode(query, o, connection) {
194
+ const clientTimeout = connection.getClient().options.timeoutMS;
195
+ if (o.maxTimeMS === void 0 && clientTimeout === void 0) return;
196
+ query.setOptions({ timeoutMode: "iteration" });
197
+ }
198
+ /** Apply resolved options to a mongoose Aggregate. */
199
+ function applyToAggregate(aggregation, o) {
200
+ if (o.maxTimeMS !== void 0) aggregation.option({ timeoutMS: o.maxTimeMS });
201
+ if (o.comment !== void 0) aggregation.option({ comment: o.comment });
202
+ if (o.readPreference !== void 0) aggregation.read(readPreferenceMode(o.readPreference));
203
+ if (o.readConcern !== void 0) aggregation.readConcern(o.readConcern);
204
+ if (o.allowDiskUse !== void 0) aggregation.allowDiskUse(o.allowDiskUse);
205
+ if (o.hint !== void 0) aggregation.hint(o.hint);
206
+ }
207
+ //#endregion
208
+ export { assertQueryDefaultsConfigured as a, getQueryDefaults as c, setTransactionDeadline as d, applyToQuery as i, resetQueryDefaults as l, applyCursorTimeoutMode as n, configureAggregateDefaults as o, applyToAggregate as r, configureQueryDefaults as s, QUERY_DEFAULTS_ERROR_CODES as t, resolveQueryOptions as u };