@classytic/mongokit 3.38.1 → 3.40.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/dist/{PaginationEngine-CBS1XGzp.d.mts → PaginationEngine-D-P19Y9m.d.mts} +34 -9
  2. package/dist/PaginationEngine-Dvl8jcRY.mjs +1198 -0
  3. package/dist/{Repository-Ck5x43wF.mjs → Repository-BMgPSkqZ.mjs} +672 -655
  4. package/dist/{Repository-BdSkQ8vc.d.mts → Repository-CR8Y9g2E.d.mts} +106 -34
  5. package/dist/actions/index.d.mts +1 -1
  6. package/dist/actions/index.mjs +1 -1
  7. package/dist/adapter/index.d.mts +21 -0
  8. package/dist/adapter/index.mjs +1 -1
  9. package/dist/{adapter-Bfqz8yHE.mjs → adapter-BrVfPFSW.mjs} +45 -1
  10. package/dist/ai/index.d.mts +1 -1
  11. package/dist/better-auth/index.mjs +2 -2
  12. package/dist/{create-repository-B5EulQ31.mjs → create-repository-BzBUMvd1.mjs} +3 -3
  13. package/dist/{create-repository-BU4x9Kb0.d.mts → create-repository-CAle1MSU.d.mts} +5 -5
  14. package/dist/idempotency/index.d.mts +23 -0
  15. package/dist/idempotency/index.mjs +134 -0
  16. package/dist/{index-DKEql4cL.d.mts → index-CmjNyulu.d.mts} +67 -8
  17. package/dist/index.d.mts +125 -35
  18. package/dist/index.mjs +286 -676
  19. package/dist/indexes/verify-indexes.d.mts +14 -1
  20. package/dist/indexes/verify-indexes.mjs +54 -7
  21. package/dist/{method-registry.plugin-DblM7V6j.mjs → method-registry.plugin-C63pGn1d.mjs} +54 -4
  22. package/dist/{multi-tenant.plugin-BwSvTirB.d.mts → multi-tenant.plugin-CBgCo9im.d.mts} +77 -5
  23. package/dist/{operations-DZodvjC-.d.mts → operations-CrRIoc8Z.d.mts} +22 -7
  24. package/dist/outbox/index.d.mts +1 -1
  25. package/dist/outbox/index.mjs +7 -5
  26. package/dist/pagination/PaginationEngine.d.mts +1 -1
  27. package/dist/pagination/PaginationEngine.mjs +1 -1
  28. package/dist/pagination/utils/limits.d.mts +1 -1
  29. package/dist/pagination/utils/limits.mjs +22 -1
  30. package/dist/pagination-BpIv2bdS.d.mts +364 -0
  31. package/dist/{plugin-options-DXnlKM3B.d.mts → plugin-options-Zk46lD6F.d.mts} +16 -2
  32. package/dist/plugins/index.d.mts +8 -3
  33. package/dist/plugins/index.mjs +99 -26
  34. package/dist/{repository-H_ffGgpE.d.mts → repository-odl03582.d.mts} +50 -6
  35. package/dist/sync/index.d.mts +40 -0
  36. package/dist/sync/index.mjs +334 -0
  37. package/dist/tenant/index.d.mts +89 -0
  38. package/dist/tenant/index.mjs +96 -0
  39. package/dist/testkit/index.d.mts +2 -2
  40. package/dist/testkit/index.mjs +1 -1
  41. package/dist/{timestamp.plugin-CBQukQzV.mjs → timestamp.plugin-DF0gJB6p.mjs} +116 -9
  42. package/dist/{to-plain-Dyz_qvuo.d.mts → to-plain-k7SjNlSS.d.mts} +2 -2
  43. package/dist/transaction-B5L9glO0.mjs +533 -0
  44. package/dist/{update-jSXp3nu3.mjs → update-DG0fCwRJ.mjs} +293 -83
  45. package/dist/utils/index.d.mts +1 -1
  46. package/dist/{validation-chain.plugin-DhNyxNCw.mjs → validation-chain.plugin-BLY2gLEk.mjs} +192 -279
  47. package/dist/{validation-chain.plugin-yQRob3Av.d.mts → validation-chain.plugin-DYp9E7K2.d.mts} +26 -20
  48. package/package.json +17 -4
  49. package/dist/PaginationEngine-BX9o_-4R.mjs +0 -735
  50. package/dist/core-62oo3N1C.d.mts +0 -50
  51. package/dist/pagination-jSUdrACK.d.mts +0 -158
@@ -0,0 +1,364 @@
1
+ import { Document, Model, PipelineStage, PopulateOptions, Types } from "mongoose";
2
+ //#region src/types/core.d.ts
3
+ /**
4
+ * Core query/document primitives shared across every mongokit module —
5
+ * sort/select/populate/filter specs and the mongoose-facing aliases.
6
+ *
7
+ * Also the home of the `softRequired` Mongoose declaration merge (this file
8
+ * is loaded by every module, so the augmentation is always in the program).
9
+ */
10
+ /**
11
+ * Global declaration merge: adds `softRequired` to Mongoose's SchemaTypeOptions
12
+ * so consumers can annotate paths type-safely without importing anything.
13
+ *
14
+ * A path with `softRequired: true` keeps its DB-level `required: true`
15
+ * invariant but is excluded from the auto-generated CRUD body `required[]`
16
+ * array (see buildCrudSchemasFromModel).
17
+ */
18
+ declare module 'mongoose' {
19
+ interface SchemaTypeOptions<T, EnforcedDocType = any, THydratedDocumentType = HydratedDocument<EnforcedDocType>> {
20
+ /** mongokit: omit from auto-generated CRUD body `required` array. */
21
+ softRequired?: boolean;
22
+ }
23
+ }
24
+ /** Read Preference Type for replica sets */
25
+ type ReadPreferenceType = 'primary' | 'primaryPreferred' | 'secondary' | 'secondaryPreferred' | 'nearest' | (string & {});
26
+ /** Re-export mongoose ObjectId */
27
+ type ObjectId = Types.ObjectId;
28
+ /** Generic document type */
29
+ type AnyDocument = Document & Record<string, unknown>;
30
+ /** Generic model type */
31
+ type AnyModel = Model<AnyDocument>;
32
+ /** Sort direction */
33
+ type SortDirection = 1 | -1;
34
+ /** Sort specification for MongoDB queries */
35
+ type SortSpec = Record<string, SortDirection>;
36
+ /** Populate specification */
37
+ type PopulateSpec = string | string[] | PopulateOptions | PopulateOptions[];
38
+ /**
39
+ * Select specification. `readonly string[]` widens the legacy `string[]`
40
+ * form to also accept the immutable variant repo-core's contract uses
41
+ * (`LookupPopulateOptions.select: readonly string[] | Record<string, 0
42
+ * | 1>`), so `Repository<TDoc>` assigns to `StandardRepo<TDoc>` without
43
+ * TS2322 on the `select` field. Any existing mutable-array caller stays
44
+ * compatible — `string[]` is assignable to `readonly string[]`.
45
+ */
46
+ type SelectSpec = string | readonly string[] | Record<string, 0 | 1>;
47
+ /** Filter query type for MongoDB queries (compatible with Mongoose 8 & 9) */
48
+ type FilterQuery<_T = unknown> = Record<string, unknown>;
49
+ //#endregion
50
+ //#region src/pagination/utils/cursor-signing.d.ts
51
+ /**
52
+ * HMAC signing for pagination cursors.
53
+ *
54
+ * ## What this protects, and what it does not
55
+ *
56
+ * A cursor is a POSITION, and the server's own filters decide what a caller may
57
+ * read — so a stolen or shared cursor cannot reach another tenant's rows, and
58
+ * signing is not a substitute for scoping. What an unsigned cursor gives away
59
+ * is the ability to MOVE the position: edit the encoded sort value, re-submit,
60
+ * and the response says whether any row sorts past it. Repeat, and that is a
61
+ * binary search over a column the caller can never read directly — "where do
62
+ * salaries above X start", against a list that only ever showed them names.
63
+ *
64
+ * Signing closes that. It does NOT hide the cursor's contents: the payload is
65
+ * still base64url JSON and still readable by anyone holding the token, field
66
+ * names included. **Signing is integrity, not confidentiality.** A deployment
67
+ * that must not reveal which column it sorts by needs encryption, which this is
68
+ * not.
69
+ *
70
+ * ## The rules that make it worth having
71
+ *
72
+ * - **A configured secret makes a signature REQUIRED.** Accepting an unsigned
73
+ * token "for compatibility" means an attacker strips the signature and the
74
+ * protection evaporates while still looking enabled — the exact shape of a
75
+ * specific instruction defeated by a general default.
76
+ * - **An unusable secret THROWS.** `cursorSecret: ''` (an unset env var read
77
+ * without a check) would otherwise mean "signing on, key guessable", and
78
+ * nothing would say so.
79
+ * - **Comparison is timing-safe.** `===` on a signature leaks it byte by byte
80
+ * to anyone willing to time the responses.
81
+ * - **Rotation takes a list.** Sign with the first, accept any — otherwise
82
+ * changing the key invalidates every cursor a client is holding.
83
+ */
84
+ /** One key, or several during a rotation (the FIRST signs; all verify). */
85
+ type CursorSecret = string | readonly string[];
86
+ //#endregion
87
+ //#region src/types/pagination.d.ts
88
+ /**
89
+ * How an offset page learns how many rows sit behind it.
90
+ *
91
+ * | | cost | `total` reports |
92
+ * |---|---|---|
93
+ * | `exact` | O(matching index keys) — the whole range, every page | the truth |
94
+ * | `estimated` | O(1) collection metadata, but IGNORES filters, so it falls back to `exact` whenever any filter is present | the truth, or the collection size |
95
+ * | `capped` | O(countLimit) — the scan stops at the ceiling | the truth below the ceiling, else the ceiling, with `totalIsLowerBound: true` |
96
+ * | `none` | free — `limit + 1` is fetched instead | `0` |
97
+ *
98
+ * `exact` is the default and is right until a filter matches enough rows for
99
+ * the count to outgrow the find it runs beside. Past that it is the dominant
100
+ * cost of a page the user reads fifteen rows of, and it is paid again on every
101
+ * page.
102
+ *
103
+ * `capped` is what a product ships instead: the row count a list header shows
104
+ * is only actionable while it is small, so bound the work and say `10,000+`
105
+ * above the bound. Elasticsearch (`track_total_hits`), Odoo (`search_count`'s
106
+ * limit) and GitHub's `1000+` are all this strategy.
107
+ *
108
+ * Declared ONCE and referenced everywhere — this union was hand-copied to eight
109
+ * declarations, which is how the aggregate variant silently lost a member.
110
+ */
111
+ type CountStrategy = 'exact' | 'estimated' | 'capped' | 'none';
112
+ /**
113
+ * What mongokit adds to repo-core's offset envelope, via its reserved `TExtra`
114
+ * slot: `OffsetPaginationResult<TDoc, MongokitPageExtras>`.
115
+ *
116
+ * Both members are optional and absent on an ordinary page, so every existing
117
+ * consumer of `OffsetPaginationResult<TDoc>` stays assignable.
118
+ *
119
+ * A `type` and not an `interface`: repo-core constrains `TExtra` to
120
+ * `Record<string, unknown>`, and an interface has no implicit index signature.
121
+ */
122
+ type MongokitPageExtras = {
123
+ /** Deep-page or clamped-limit advice. Human-readable; never branch on it. */
124
+ warning?: string;
125
+ /**
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.
134
+ */
135
+ totalIsLowerBound?: boolean;
136
+ };
137
+ /** Pagination configuration */
138
+ interface PaginationConfig {
139
+ /** Default number of documents per page (default: 10) */
140
+ defaultLimit?: number;
141
+ /** Maximum allowed limit (default: 100) */
142
+ maxLimit?: number;
143
+ /** Maximum allowed page number (default: 10000) */
144
+ maxPage?: number;
145
+ /** Page number that triggers performance warning (default: 100) */
146
+ deepPageThreshold?: number;
147
+ /** Cursor version for forward compatibility (default: 1) */
148
+ cursorVersion?: number;
149
+ /**
150
+ * Minimum cursor version accepted. Bump alongside `cursorVersion` when a
151
+ * breaking format change ships so stale client cursors are rejected with a
152
+ * clear error instead of silently paginating from the wrong position.
153
+ * Default: 1 (accept any cursor <= cursorVersion).
154
+ */
155
+ minCursorVersion?: number;
156
+ /**
157
+ * Allowlist of primary sort fields for keyset pagination. When set, any
158
+ * `getAll({ sort })` whose primary (non-_id) field is not in this list
159
+ * throws at validation time.
160
+ *
161
+ * Use this to lock down keyset pagination to fields that are structurally
162
+ * guaranteed non-null in your schema — keyset sort across null/non-null
163
+ * boundaries is lossy (MongoDB's `$lt/$gt` semantics leave a gap at the
164
+ * type boundary, so not every doc is reachable).
165
+ *
166
+ * `_id` is always allowed regardless of this list.
167
+ * Undefined (default) = no allowlist, any sort field accepted.
168
+ */
169
+ strictKeysetSortFields?: string[];
170
+ /** Use estimatedDocumentCount for faster counts on large collections */
171
+ useEstimatedCount?: boolean;
172
+ /**
173
+ * Repo-wide `countStrategy` for `getAll` / `lookupPopulate` /
174
+ * `aggregatePaginate` calls that don't set their own (default: 'exact' —
175
+ * a `countDocuments` round-trip per page). `'none'` suits list endpoints
176
+ * that only need `hasNext`: one query per page, `total` / `pages` report
177
+ * 0. A per-call `countStrategy` always wins.
178
+ */
179
+ defaultCountStrategy?: CountStrategy;
180
+ /**
181
+ * Ceiling for `countStrategy: 'capped'` when a call names none. Default 10000.
182
+ * See {@link OffsetPaginationOptions.countLimit}.
183
+ */
184
+ defaultCountLimit?: number;
185
+ /**
186
+ * HMAC key for cursor tokens. Absent (default) = cursors are unsigned, as
187
+ * before. A list rotates keys: the FIRST signs, all of them verify.
188
+ *
189
+ * Signing stops a caller EDITING a cursor to move the position — re-submit a
190
+ * tampered sort value and the response says whether any row sorts past it,
191
+ * which is a binary search over a column the caller may never read. It does
192
+ * NOT hide the token's contents: the payload stays readable base64url JSON.
193
+ * Integrity, not confidentiality.
194
+ *
195
+ * **Switching it on is a cutover.** Cursors already in flight carry no
196
+ * signature and will be refused — the client restarts from page one. That is
197
+ * deliberate: a "tolerate unsigned during rollout" flag is a permanent
198
+ * downgrade switch, and stripping the signature is the first thing an
199
+ * attacker tries.
200
+ *
201
+ * A key under 16 characters throws rather than being accepted, because an
202
+ * unset environment variable reads as `''` and would otherwise mean "signing
203
+ * on, key guessable" with nothing to say so.
204
+ */
205
+ cursorSecret?: CursorSecret;
206
+ /**
207
+ * Repo-wide pagination mode for a `getAll` call that carries no explicit
208
+ * `mode`, no `page` and no cursor (`after`) — the plain
209
+ * `getAll({ filters })` case, which otherwise picks offset (or keyset
210
+ * only when a non-default sort is given). `'keyset'` makes those calls
211
+ * cursor-paginated: no `skip`, O(1) deep pages. Explicit per-call
212
+ * signals still win — `page` → offset, `after` → keyset, `mode` → as
213
+ * given. Default: undefined (the sort-based auto-detection above).
214
+ */
215
+ defaultMode?: 'offset' | 'keyset';
216
+ }
217
+ /** MongoDB collation options for locale-aware string comparison */
218
+ interface CollationOptions {
219
+ locale: string;
220
+ caseLevel?: boolean;
221
+ caseFirst?: 'upper' | 'lower' | 'off';
222
+ strength?: 1 | 2 | 3 | 4 | 5;
223
+ numericOrdering?: boolean;
224
+ alternate?: 'non-ignorable' | 'shifted';
225
+ maxVariable?: 'punct' | 'space';
226
+ backwards?: boolean;
227
+ }
228
+ /** Base pagination options */
229
+ interface BasePaginationOptions {
230
+ /** Pagination mode (explicit override) */
231
+ mode?: 'offset' | 'keyset';
232
+ /** MongoDB query filters */
233
+ filters?: FilterQuery<AnyDocument>;
234
+ /** Sort specification */
235
+ sort?: SortSpec;
236
+ /** Number of documents per page */
237
+ limit?: number;
238
+ /** Fields to select */
239
+ select?: SelectSpec;
240
+ /** Fields to populate */
241
+ populate?: PopulateSpec;
242
+ /** Return plain JavaScript objects */
243
+ lean?: boolean;
244
+ /** MongoDB session for transactions */
245
+ session?: unknown;
246
+ /** Query hint (index name or document) */
247
+ hint?: string | Record<string, 1 | -1>;
248
+ /** Maximum execution time in milliseconds */
249
+ maxTimeMS?: number;
250
+ /** Read preference for replica sets (e.g. 'secondaryPreferred') */
251
+ readPreference?: ReadPreferenceType;
252
+ /** Collation for locale-aware string comparison and case-insensitive sorting */
253
+ collation?: CollationOptions;
254
+ }
255
+ /** Offset pagination options */
256
+ interface OffsetPaginationOptions extends BasePaginationOptions {
257
+ /** Page number (1-indexed) */
258
+ page?: number;
259
+ /** Count strategy for filtered queries (default: 'exact'). See {@link CountStrategy}. */
260
+ countStrategy?: CountStrategy;
261
+ /**
262
+ * Ceiling for `countStrategy: 'capped'` (default: the repository's
263
+ * `defaultCountLimit`, else 10000). Ignored by every other strategy.
264
+ *
265
+ * A `total` that comes back EQUAL to this is a lower bound, not a count —
266
+ * `totalIsLowerBound` on the result says so, because `total === countLimit`
267
+ * is also what an exactly-full collection legitimately returns and the two
268
+ * are otherwise indistinguishable.
269
+ */
270
+ countLimit?: number;
271
+ /**
272
+ * Optional alternate filter used for the count query only. The primary
273
+ * `filters` is still used for `.find()`. Lets callers (Repository) run a
274
+ * sort-only query like `$near` for results while counting with a
275
+ * count-compatible rewrite (e.g. `$geoWithin: $centerSphere`) that
276
+ * returns the same document set but works with `countDocuments`.
277
+ */
278
+ countFilters?: FilterQuery<AnyDocument>;
279
+ }
280
+ /** Keyset (cursor) pagination options */
281
+ interface KeysetPaginationOptions extends BasePaginationOptions {
282
+ /** Cursor token for next page */
283
+ after?: string;
284
+ /**
285
+ * Cursor token for the PREVIOUS page — the page ending just before this
286
+ * position. Mutually exclusive with `after`; passing both throws.
287
+ *
288
+ * Feed back the `prev` from a result. Rows come back in the sort order you
289
+ * asked for, not reversed: the query runs inverted internally and the page
290
+ * is turned back around before it is returned.
291
+ */
292
+ before?: string;
293
+ /** Sort is required for keyset pagination */
294
+ sort: SortSpec;
295
+ }
296
+ /** Aggregate pagination options */
297
+ interface AggregatePaginationOptions {
298
+ /** Aggregation pipeline stages */
299
+ pipeline?: PipelineStage[];
300
+ /** Page number (1-indexed) */
301
+ page?: number;
302
+ /** Number of documents per page */
303
+ limit?: number;
304
+ /** MongoDB session for transactions */
305
+ session?: unknown;
306
+ /** Query hint (index name or document) for aggregation */
307
+ hint?: string | Record<string, 1 | -1>;
308
+ /** Maximum execution time in milliseconds */
309
+ maxTimeMS?: number;
310
+ /** Count strategy (default: 'exact' via $facet).
311
+ * 'estimated' is accepted but treated as 'exact' in aggregation context
312
+ * since estimatedDocumentCount is not available inside pipelines.
313
+ * 'capped' bounds the count facet with a `$limit` — see {@link CountStrategy}. */
314
+ countStrategy?: CountStrategy;
315
+ /** Ceiling for `countStrategy: 'capped'`. See {@link OffsetPaginationOptions.countLimit}. */
316
+ countLimit?: number;
317
+ /**
318
+ * Let `$sort` / `$group` spill to disk past the server's 100MB per-stage
319
+ * memory limit. Off by default — spilling hides a missing index behind a
320
+ * slow success, and on MongoDB >= 6.0 the server's own
321
+ * `allowDiskUseByDefault` already decides this when a caller says nothing.
322
+ */
323
+ allowDiskUse?: boolean;
324
+ /** Pagination mode (reserved for API consistency) */
325
+ mode?: 'offset';
326
+ /** Read preference for replica sets (e.g. 'secondaryPreferred') */
327
+ readPreference?: ReadPreferenceType;
328
+ }
329
+ /** Value type identifier for cursor serialization */
330
+ type ValueType = 'date' | 'objectid' | 'boolean' | 'number' | 'string' | 'null' | 'unknown';
331
+ /** Cursor payload */
332
+ interface CursorPayload {
333
+ /** Primary sort field value (legacy single-field) */
334
+ v: string | number | boolean | null;
335
+ /** Value type identifier (legacy single-field) */
336
+ t: ValueType;
337
+ /** Document ID */
338
+ id: string;
339
+ /** ID type identifier */
340
+ idType: ValueType;
341
+ /** Sort specification */
342
+ sort: SortSpec;
343
+ /** Cursor version */
344
+ ver: number;
345
+ /** Compound sort field values (multi-field keyset) */
346
+ vals?: Record<string, string | number | boolean | null>;
347
+ /** Compound sort value types */
348
+ types?: Record<string, ValueType>;
349
+ }
350
+ /** Decoded cursor */
351
+ interface DecodedCursor {
352
+ /** Primary sort field value (rehydrated) — legacy compat */
353
+ value: unknown;
354
+ /** Document ID (rehydrated) */
355
+ id: ObjectId | string;
356
+ /** Sort specification */
357
+ sort: SortSpec;
358
+ /** Cursor version */
359
+ version: number;
360
+ /** All sort field values (rehydrated) — for compound sort */
361
+ values?: Record<string, unknown>;
362
+ }
363
+ //#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 };
@@ -1,5 +1,5 @@
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-H_ffGgpE.mjs";
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-odl03582.mjs";
3
3
  import { OffsetPaginationResult } from "@classytic/repo-core/pagination";
4
4
  //#region src/types/plugin-options.d.ts
5
5
  /** Field preset configuration (field-filter plugin) */
@@ -69,6 +69,9 @@ interface SoftDeleteRepository<TDoc = unknown> {
69
69
  */
70
70
  restore(id: string | ObjectId, options?: {
71
71
  session?: unknown;
72
+ organizationId?: unknown;
73
+ bypassTenant?: boolean;
74
+ [key: string]: unknown;
72
75
  }): Promise<TDoc>;
73
76
  /**
74
77
  * Get all soft-deleted documents
@@ -109,6 +112,17 @@ interface CascadeRelation {
109
112
  model?: string;
110
113
  /** Foreign key field in the related model that references the deleted document */
111
114
  foreignKey: string;
115
+ /**
116
+ * WHICH parent field `foreignKey` stores. Default: the parent repository's
117
+ * `idField` (`_id` unless the repository declares another).
118
+ *
119
+ * Set it when the two differ. An order repository addressed by `orderNumber`
120
+ * whose children store the order's `_id` declares `parentKey: '_id'`;
121
+ * without it, `delete('ORD-2026-0001')` cascaded with the order NUMBER and
122
+ * every child delete failed on the ObjectId cast. The cascade reads the
123
+ * parent's value for this field before the delete, under the same session.
124
+ */
125
+ parentKey?: string;
112
126
  /** Whether to use soft delete if available (default: follows parent behavior) */
113
127
  softDelete?: boolean;
114
128
  /**
@@ -1,6 +1,6 @@
1
- import { S as RepositoryPluginName, b as MONGOKIT_PLUGIN_NAMES, l as Plugin, x as MongoKitPluginName } from "../repository-H_ffGgpE.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-BwSvTirB.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-yQRob3Av.mjs";
1
+ import { S as RepositoryPluginName, b as MONGOKIT_PLUGIN_NAMES, l as Plugin, x as MongoKitPluginName } from "../repository-odl03582.mjs";
2
+ import { A as EnsureAuditTrailReadyOptions, C as AuditQueryOptions, D as AuditTrailOptions, E as AuditTrailMode, M as ensureAuditTrailReady, O as AuditTrailQuery, S as AuditOperation, T as AuditTrailMethods, _ as CacheOptions, a as DateSequentialIdOptions, b as cachePlugin, c as SequentialIdOptions, f as getNextSequence, g as CacheAdapter, h as sequentialId, i as CustomIdOptions, j as auditTrailPlugin, k as AuditTrailQueryOptions, l as customIdPlugin, o as IdGenerator, p as prefixedId, r as multiTenantPlugin, s as PrefixedIdOptions, t as MultiTenantOptions, u as dateSequentialId, v as RepositoryCacheHandle, w as AuditQueryResult, x as AuditEntry, y as RepositoryCachePluginOptions } from "../multi-tenant.plugin-CBgCo9im.mjs";
3
+ import { A as IdempotencyStoreMethods, B as batchOperationsPlugin, C as LeaseMethods, D as ImmutableStatesPluginOptions, E as ImmutableClaimView, F as elasticSearchPlugin, G as aggregateHelpersPlugin, H as AppendOnlyPluginOptions, I as cascadePlugin, L as BatchOperationsMethods, M as idempotencyStorePlugin, N as fieldFilterPlugin, O as immutableStatesPlugin, P as ElasticSearchOptions, S as methodRegistryPlugin, T as leasePlugin, U as appendOnlyPlugin, V as auditLogPlugin, W as AggregateHelpersMethods, _ as OperationMetric, a as requireField, b as mongoOperationsPlugin, c as timestampPlugin, d as createTenantContext, f as SubdocumentMethods, g as ObservabilityOptions, h as softDeletePlugin, i as immutableField, j as IdempotencyStorePluginOptions, k as IdempotencyClaim, l as TenantContext, m as SoftDeleteMethods, n as autoInject, o as uniqueField, p as subdocumentPlugin, r as blockIf, s as validationChainPlugin, t as OperationType, u as TenantStore, v as observabilityPlugin, w as LeasePluginOptions, x as MethodRegistryRepository, y as MongoOperationsMethods } from "../validation-chain.plugin-DYp9E7K2.mjs";
4
4
  import { ChangeLogStore } from "@classytic/repo-core/sync";
5
5
  //#region src/plugins/change-log.plugin.d.ts
6
6
  interface ChangeLogPluginOptions {
@@ -12,6 +12,11 @@ interface ChangeLogPluginOptions {
12
12
  tenantField?: string;
13
13
  /** Monotonic per-doc version field. Default `version` (kernel convention). */
14
14
  versionField?: string;
15
+ /**
16
+ * The document as a subscriber may see it — fields picked, secrets (a cost price) dropped. Tenant
17
+ * and version are still read from the whole document. Default: the whole document.
18
+ */
19
+ project?: (doc: Record<string, unknown>) => Record<string, unknown>;
15
20
  }
16
21
  declare function changeLogPlugin(options: ChangeLogPluginOptions): Plugin;
17
22
  //#endregion
@@ -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-CBQukQzV.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-DhNyxNCw.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-DF0gJB6p.mjs";
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-BLY2gLEk.mjs";
4
4
  //#region src/plugins/change-log.plugin.ts
5
5
  const PLUGIN_NAME = "changeLog";
6
+ /** Marks a repository already captured: the store and scope it writes to. */
7
+ const CAPTURED = Symbol.for("mongokit.changeLog.capture");
8
+ /** Where `before:updateMany` / `before:deleteMany` leave the ids they matched for the after hook. */
9
+ const MATCHED_IDS = Symbol("changeLog.matchedIds");
6
10
  function isSkipped(context) {
7
11
  const list = context.skipPlugins;
8
12
  return Array.isArray(list) && list.includes(PLUGIN_NAME);
9
13
  }
10
14
  function changeLogPlugin(options) {
11
- const { store, scope, tenantField = "organizationId", versionField = "version" } = options;
15
+ const { store, scope, tenantField = "organizationId", versionField = "version", project } = options;
12
16
  const toRecord = (doc) => {
13
17
  if (!doc || typeof doc !== "object") return null;
14
18
  const d = doc;
@@ -21,51 +25,120 @@ function changeLogPlugin(options) {
21
25
  return updatedAt instanceof Date ? updatedAt.getTime() : 0;
22
26
  };
23
27
  const tenantOf = (doc, context) => {
24
- const raw = doc[tenantField] ?? context.organizationId;
28
+ const raw = doc?.[tenantField] ?? context.organizationId;
25
29
  return raw === void 0 || raw === null ? void 0 : String(raw);
26
30
  };
27
- const 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
  };