@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
@@ -1,8 +1,7 @@
1
- import { _ as UpdateOptions, a as CreateOptions, b as LookupOptions, c as FindOneAndUpdateOptions, d as LookupPopulateResult, g as UpdateManyResult, h as SessionOptions, m as ReadOptions, o as DeleteManyResult, p as OperationOptions, r as CacheableOptions, s as DeleteResult, t as AggregateOptions, u as LookupPopulateOptions, v as WithTransactionOptions, y as LookupBuilder } from "./operations-DZodvjC-.mjs";
2
- import { a as PopulateSpec, i as ObjectId, l as SortSpec$1, o as ReadPreferenceType, s as SelectSpec } from "./core-62oo3N1C.mjs";
3
- import { c as PaginationConfig, r as CollationOptions, t as AggregatePaginationOptions } from "./pagination-jSUdrACK.mjs";
4
- import { _ as RepositoryOptions, d as PluginType, f as PrioritizedHook, o as Middleware, p as RepositoryContext } from "./repository-H_ffGgpE.mjs";
5
- import { t as PaginationEngine } from "./PaginationEngine-CBS1XGzp.mjs";
1
+ import { _ as UpdateOptions, a as CreateOptions, b as LookupOptions, c as FindOneAndUpdateOptions, d as LookupPopulateResult, g as UpdateManyResult, h as SessionOptions, m as ReadOptions, o as DeleteManyResult, p as OperationOptions, r as CacheableOptions, s as DeleteResult, t as AggregateOptions, u as LookupPopulateOptions, v as WithTransactionOptions, y as LookupBuilder } from "./operations-CrRIoc8Z.mjs";
2
+ import { _ as PopulateSpec, g as ObjectId, i as CountStrategy, r as CollationOptions, t as AggregatePaginationOptions, u as PaginationConfig, v as ReadPreferenceType, x as SortSpec$1, y as SelectSpec } from "./pagination-BpIv2bdS.mjs";
3
+ import { _ as RepositoryOptions, d as PluginType, f as PrioritizedHook, o as Middleware, p as RepositoryContext } from "./repository-odl03582.mjs";
4
+ import { t as PaginationEngine } from "./PaginationEngine-D-P19Y9m.mjs";
6
5
  import { AggPaginationRequest, AggRequest, AggResult, ArchiveOptions, ArchiveResult, ArchiveSink, ChangeEvent, FilterInput, KeysetAggPaginationResult, QueryOptions, RepoCapabilities, RepositoryBase, TenantPurgeOptions, TenantPurgeResult, TenantPurgeStrategy, WatchOptions } from "@classytic/repo-core/repository";
7
6
  import { ClientSession, Expression, Model, PipelineStage, PopulateOptions } from "mongoose";
8
7
  import { Filter } from "@classytic/repo-core/filter";
@@ -520,6 +519,37 @@ interface TransitionMachine {
520
519
  readonly name: string;
521
520
  assertTransition(entityId: string, from: string, to: string): void;
522
521
  }
522
+ /** `Repository.getAll` parameters. */
523
+ type GetAllParams = {
524
+ filters?: Record<string, unknown>;
525
+ sort?: SortSpec$1 | string;
526
+ cursor?: string;
527
+ after?: string;
528
+ /** Cursor for the PREVIOUS page. Mutually exclusive with `after`/`cursor`. */
529
+ before?: string;
530
+ page?: number;
531
+ pagination?: {
532
+ page?: number;
533
+ limit?: number;
534
+ };
535
+ limit?: number;
536
+ search?: string;
537
+ mode?: 'offset' | 'keyset';
538
+ hint?: string | Record<string, 1 | -1>;
539
+ maxTimeMS?: number;
540
+ countStrategy?: CountStrategy;
541
+ /** Ceiling for `countStrategy: 'capped'`. */
542
+ countLimit?: number;
543
+ readPreference?: ReadPreferenceType;
544
+ /** Advanced populate options (from QueryParser or Arc's BaseController) */
545
+ populateOptions?: PopulateOptions[];
546
+ /** Collation for locale-aware string comparison */
547
+ collation?: CollationOptions;
548
+ /** Lookup configurations for $lookup joins (from QueryParser or manual) */
549
+ lookups?: LookupOptions[];
550
+ /** Skip pagination entirely — returns raw TDoc[] (same as findAll) */
551
+ noPagination?: boolean;
552
+ };
523
553
  declare class Repository<TDoc = unknown> extends RepositoryBase {
524
554
  /**
525
555
  * Cache handle ATTACHED BY the unified cache plugin at wiring time —
@@ -580,6 +610,18 @@ declare class Repository<TDoc = unknown> extends RepositoryBase {
580
610
  * the `Middleware<TDoc>` type for the protocol.
581
611
  */
582
612
  private readonly _middlewares;
613
+ /**
614
+ * Shard-key guard built once from `options.distribution`; `undefined`
615
+ * when not configured so the per-op check is a single falsy test.
616
+ */
617
+ private readonly _checkDistribution;
618
+ /**
619
+ * Whether id-addressed verbs are guarded too — true unless the
620
+ * distribution key IS the id field, where an `_id` match is genuinely
621
+ * routed and guarding would flag every by-id call. See
622
+ * {@link ID_ADDRESSED_OPERATIONS}.
623
+ */
624
+ private readonly _guardIdAddressed;
583
625
  constructor(Model: Model<TDoc, any, any, any>, plugins?: PluginType[], paginationConfig?: PaginationConfig, options?: RepositoryOptions);
584
626
  /**
585
627
  * Live read-through view of the hook engine's listener registry.
@@ -860,8 +902,12 @@ declare class Repository<TDoc = unknown> extends RepositoryBase {
860
902
  * repo without a filter to observe deletes.
861
903
  * - `options.signal` ends the iterator (closes the stream). A
862
904
  * pre-aborted signal rejects at the op boundary like every other op.
863
- * - `options.resumeAfter` forwards a previously captured Mongo
864
- * resume token for at-least-once consumption across restarts.
905
+ * - Every yielded event carries `resumeToken` (the change document's
906
+ * `_id`). Persist the last one you finished processing and pass it
907
+ * back as `options.resumeAfter` — or `options.startAfter` to also
908
+ * step past an invalidate event — for at-least-once consumption
909
+ * across restarts. Setting both is rejected up front (Mongo forbids
910
+ * the combination; the driver error is far less legible).
865
911
  *
866
912
  * Context keys for policy plugins (`organizationId`, `bypassTenant`,
867
913
  * `includeDeleted`, ...) ride the options bag at runtime — same as every
@@ -901,32 +947,19 @@ declare class Repository<TDoc = unknown> extends RepositoryBase {
901
947
  * // Skip cache for fresh data
902
948
  * await repo.getAll({ filters: { status: 'active' } }, { skipCache: true });
903
949
  */
904
- getAll(params?: {
905
- filters?: Record<string, unknown>;
906
- sort?: SortSpec$1 | string;
907
- cursor?: string;
908
- after?: string;
909
- page?: number;
910
- pagination?: {
911
- page?: number;
912
- limit?: number;
913
- };
914
- limit?: number;
915
- search?: string;
916
- mode?: 'offset' | 'keyset';
917
- hint?: string | Record<string, 1 | -1>;
918
- maxTimeMS?: number;
919
- countStrategy?: 'exact' | 'estimated' | 'none';
920
- readPreference?: ReadPreferenceType;
921
- /** Advanced populate options (from QueryParser or Arc's BaseController) */
922
- populateOptions?: PopulateOptions[];
923
- /** Collation for locale-aware string comparison */
924
- collation?: CollationOptions;
925
- /** Lookup configurations for $lookup joins (from QueryParser or manual) */
926
- lookups?: LookupOptions[];
927
- /** Skip pagination entirely — returns raw TDoc[] (same as findAll) */
928
- noPagination?: boolean;
929
- }, options?: CacheableOptions): Promise<OffsetPaginationResult<TDoc> | KeysetPaginationResult<TDoc> | TDoc[]>;
950
+ /** An explicit `mode` (or `noPagination`) decides the envelope, so the result type follows it. */
951
+ getAll(params: GetAllParams & {
952
+ mode: 'offset';
953
+ noPagination?: false;
954
+ }, options?: CacheableOptions): Promise<OffsetPaginationResult<TDoc>>;
955
+ getAll(params: GetAllParams & {
956
+ mode: 'keyset';
957
+ noPagination?: false;
958
+ }, options?: CacheableOptions): Promise<KeysetPaginationResult<TDoc>>;
959
+ getAll(params: GetAllParams & {
960
+ noPagination: true;
961
+ }, options?: CacheableOptions): Promise<TDoc[]>;
962
+ getAll(params?: GetAllParams, options?: CacheableOptions): Promise<OffsetPaginationResult<TDoc> | KeysetPaginationResult<TDoc> | TDoc[]>;
930
963
  /**
931
964
  * Atomic "look up by filter, insert `data` if missing, return the doc."
932
965
  *
@@ -1249,6 +1282,11 @@ declare class Repository<TDoc = unknown> extends RepositoryBase {
1249
1282
  }, patch?: Record<string, unknown>, options?: SessionOptions & {
1250
1283
  idField?: string;
1251
1284
  upsert?: boolean;
1285
+ /**
1286
+ * The document as the caller just read it in this session. Hooks' `loadTarget()`
1287
+ * returns it instead of reading again. Pass only a read taken in the same session.
1288
+ */
1289
+ target?: Record<string, unknown>;
1252
1290
  }): Promise<TDoc | null>;
1253
1291
  /**
1254
1292
  * State-machine-backed CAS transition with status history — the
@@ -1782,6 +1820,22 @@ declare class Repository<TDoc = unknown> extends RepositoryBase {
1782
1820
  * 4. DEFAULT (500) — user-registered hooks
1783
1821
  */
1784
1822
  _buildContext(operation: string, options: Record<string, unknown> | object): Promise<RepositoryContext>;
1823
+ /**
1824
+ * `memo` and (for an id-addressed op) `loadTarget` — attached before the before-hooks
1825
+ * run so every hook of one operation shares one read. A caller that has just read the
1826
+ * document in the same session passes it as `target`, and nothing is read at all.
1827
+ */
1828
+ private _sharedReadSeams;
1829
+ /**
1830
+ * Feed the resolved filter of a guarded verb to the distribution guard.
1831
+ * The slot follows `OP_REGISTRY[op].policyKey` (`query` vs `filters`);
1832
+ * the portable aggregate verbs ALSO carry the caller's filter on
1833
+ * `aggRequest.filter` (merged into the pipeline later by
1834
+ * `_injectPolicyScopeIntoAgg`), and the kit-native pipeline verbs may
1835
+ * carry it as leading `$match` stages — both are folded in so a key the
1836
+ * caller did supply is never reported missing.
1837
+ */
1838
+ private _guardDistribution;
1785
1839
  /**
1786
1840
  * Walk the well-known filter-carrying keys (`query`, `filters`,
1787
1841
  * `having`) and route any Filter IR through `compileFilterToMongo`.
@@ -1796,6 +1850,9 @@ declare class Repository<TDoc = unknown> extends RepositoryBase {
1796
1850
  private _resolveSort;
1797
1851
  /**
1798
1852
  * Detect whether to use keyset (cursor) or offset (page) pagination.
1853
+ * Precedence: explicit `mode` → `page` (offset) → `after` (keyset) →
1854
+ * `PaginationConfig.defaultMode` → the sort heuristic (a non-default
1855
+ * sort with no page means the caller wants a cursor).
1799
1856
  */
1800
1857
  private _detectPaginationMode;
1801
1858
  /**
@@ -1866,7 +1923,22 @@ declare class Repository<TDoc = unknown> extends RepositoryBase {
1866
1923
  * `errorLabels` exactly as we received it. The caller still sees a
1867
1924
  * thrown error — only the wrapping layer is skipped.
1868
1925
  */
1926
+ /**
1927
+ * Translate a driver error into the repository's error contract, WITHOUT
1928
+ * losing structured diagnostics an action attached to it.
1929
+ *
1930
+ * The translation replaces the object — an E11000 becomes a 409 — so any
1931
+ * property a caller was meant to read went with it. `createMany` attaches a
1932
+ * `partial` report naming which documents landed, and it arrived nowhere:
1933
+ * the report existed, the test for it failed, and the only evidence was the
1934
+ * absence.
1935
+ *
1936
+ * Carried explicitly rather than by copying every own-property, because the
1937
+ * wrap is deliberate: the point is to hide driver internals, and a blanket
1938
+ * copy would put them back.
1939
+ */
1869
1940
  _handleError(error: Error): HttpError;
1941
+ private _translateError;
1870
1942
  }
1871
1943
  //#endregion
1872
- export { AggregationPlan as a, SortOrder as c, AggregationBuilder as i, SortSpec as l, Repository as n, GroupSpec as o, TransitionMachine as r, ProjectionSpec as s, HOOK_PRIORITY as t, VectorSearchOptions as u };
1944
+ export { AggregationBuilder as a, ProjectionSpec as c, VectorSearchOptions as d, TransitionMachine as i, SortOrder as l, HOOK_PRIORITY as n, AggregationPlan as o, Repository as r, GroupSpec as s, GetAllParams as t, SortSpec as u };
@@ -1,2 +1,2 @@
1
- import { a as create_d_exports, i as delete_d_exports, n as update_d_exports, o as aggregate_d_exports, r as read_d_exports } from "../index-DKEql4cL.mjs";
1
+ import { c as create_d_exports, i as delete_d_exports, n as update_d_exports, r as read_d_exports, u as aggregate_d_exports } from "../index-CmjNyulu.mjs";
2
2
  export { aggregate_d_exports as aggregate, create_d_exports as create, delete_d_exports as deleteActions, read_d_exports as read, update_d_exports as update };
@@ -1,5 +1,5 @@
1
1
  import { t as __exportAll } from "../rolldown-runtime-D7D4PA-g.mjs";
2
- import { g as create_exports, i as update_exports, p as delete_exports, u as read_exports, v as aggregate_exports } from "../update-jSXp3nu3.mjs";
2
+ import { S as create_exports, i as update_exports, m as aggregate_exports, p as delete_exports, u as read_exports } from "../update-DG0fCwRJ.mjs";
3
3
  //#region src/actions/index.ts
4
4
  var actions_exports = /* @__PURE__ */ __exportAll({
5
5
  aggregate: () => aggregate_exports,
@@ -129,6 +129,27 @@ declare class MongooseAdapter<TDoc = unknown, TModelDoc = unknown> implements Da
129
129
  * `matchesRecordFilter` — one shared contract/IR across every kit; its
130
130
  * evaluator is id-coercion aware so a Mongo `ObjectId` `_id` matches its
131
131
  * string form. Arrow field: keeps its binding when passed by reference.
132
+ *
133
+ * The document is flattened to a plain record FIRST, and that step is the
134
+ * whole reason this is not a one-liner. The evaluator resolves field paths
135
+ * with `Object.hasOwn`, own-properties-only and deliberately so: it must not
136
+ * walk a prototype chain, or a crafted `{ 'constructor.x': … }` policy filter
137
+ * could match an inherited member. A HYDRATED Mongoose document keeps its
138
+ * values in `_doc` and exposes them through prototype getters, so it has no
139
+ * own `status`, no own `organizationId` — every path resolves to absent and
140
+ * the filter matches NOTHING. Fail-closed, so it does not leak; it denies.
141
+ *
142
+ * That turned any policy at all into a blanket 404 on every non-compound
143
+ * fetch (`getBySlug`, cache revalidation, the realtime feed) while the same
144
+ * policy worked perfectly on list/get, which filter at the DB. The asymmetry
145
+ * is what made it so hard to see: a resource reads fine until the day someone
146
+ * gives it a row-level policy, and then one route starts lying.
147
+ *
148
+ * Knowing that a Mongoose document is not a plain record is the MONGO kit's
149
+ * job, not the dialect-agnostic evaluator's, so the normalisation lives here.
150
+ * `toObject` rather than `toJSON`: it preserves `ObjectId` and `Date` values,
151
+ * which the evaluator compares by type. A `.lean()` result is already plain
152
+ * and passes through untouched.
132
153
  */
133
154
  readonly matchesFilter: (item: unknown, filters: Record<string, unknown>) => boolean;
134
155
  /**
@@ -1,2 +1,2 @@
1
- import { n as createMongooseAdapter, r as isMongooseModel, t as MongooseAdapter } from "../adapter-Bfqz8yHE.mjs";
1
+ import { n as createMongooseAdapter, r as isMongooseModel, t as MongooseAdapter } from "../adapter-BrVfPFSW.mjs";
2
2
  export { MongooseAdapter, createMongooseAdapter, isMongooseModel };
@@ -19,6 +19,29 @@ const REQUIRED_REPO_METHODS = [
19
19
  "update",
20
20
  "delete"
21
21
  ];
22
+ /**
23
+ * A Mongoose document as a plain record, for anything that reads fields directly.
24
+ *
25
+ * Hydrated documents hold their values in `_doc` and surface them through prototype getters, so
26
+ * `Object.hasOwn(doc, 'status')` is FALSE even though `doc.status` reads fine. Any consumer that
27
+ * refuses to walk the prototype chain — as a prototype-pollution-safe field resolver must — sees
28
+ * an empty document. See `matchesFilter` for what that cost us.
29
+ *
30
+ * `toObject` over `toJSON`: it keeps `ObjectId` and `Date` as values rather than stringifying
31
+ * them, and callers here compare by type. Anything already plain (a `.lean()` result, a POJO) is
32
+ * returned untouched, and a `toObject` that throws degrades to the original rather than taking
33
+ * the caller down.
34
+ */
35
+ function toPlainRecord(item) {
36
+ if (item === null || typeof item !== "object") return item;
37
+ const maybe = item;
38
+ if (typeof maybe.toObject !== "function") return item;
39
+ try {
40
+ return maybe.toObject();
41
+ } catch {
42
+ return item;
43
+ }
44
+ }
22
45
  function describeValue(value) {
23
46
  if (value === null) return "null";
24
47
  if (value === void 0) return "undefined";
@@ -215,8 +238,29 @@ var MongooseAdapter = class {
215
238
  * `matchesRecordFilter` — one shared contract/IR across every kit; its
216
239
  * evaluator is id-coercion aware so a Mongo `ObjectId` `_id` matches its
217
240
  * string form. Arrow field: keeps its binding when passed by reference.
241
+ *
242
+ * The document is flattened to a plain record FIRST, and that step is the
243
+ * whole reason this is not a one-liner. The evaluator resolves field paths
244
+ * with `Object.hasOwn`, own-properties-only and deliberately so: it must not
245
+ * walk a prototype chain, or a crafted `{ 'constructor.x': … }` policy filter
246
+ * could match an inherited member. A HYDRATED Mongoose document keeps its
247
+ * values in `_doc` and exposes them through prototype getters, so it has no
248
+ * own `status`, no own `organizationId` — every path resolves to absent and
249
+ * the filter matches NOTHING. Fail-closed, so it does not leak; it denies.
250
+ *
251
+ * That turned any policy at all into a blanket 404 on every non-compound
252
+ * fetch (`getBySlug`, cache revalidation, the realtime feed) while the same
253
+ * policy worked perfectly on list/get, which filter at the DB. The asymmetry
254
+ * is what made it so hard to see: a resource reads fine until the day someone
255
+ * gives it a row-level policy, and then one route starts lying.
256
+ *
257
+ * Knowing that a Mongoose document is not a plain record is the MONGO kit's
258
+ * job, not the dialect-agnostic evaluator's, so the normalisation lives here.
259
+ * `toObject` rather than `toJSON`: it preserves `ObjectId` and `Date` values,
260
+ * which the evaluator compares by type. A `.lean()` result is already plain
261
+ * and passes through untouched.
218
262
  */
219
- matchesFilter = (item, filters) => matchesRecordFilter(item, filters);
263
+ matchesFilter = (item, filters) => matchesRecordFilter(toPlainRecord(item), filters);
220
264
  /**
221
265
  * No-op — mongokit's per-call resources (`watch()` change streams,
222
266
  * `cursor()` iterators) are released by their own `AbortSignal` /
@@ -1,4 +1,4 @@
1
- import { l as Plugin } from "../repository-H_ffGgpE.mjs";
1
+ import { l as Plugin } from "../repository-odl03582.mjs";
2
2
  import { PipelineStage } from "mongoose";
3
3
  //#region src/ai/types.d.ts
4
4
  /** Supported similarity metrics for vector search */
@@ -1,5 +1,5 @@
1
- import { n as Repository } from "../Repository-Ck5x43wF.mjs";
2
- import { n as createMongooseAdapter } from "../adapter-Bfqz8yHE.mjs";
1
+ import { n as Repository } from "../Repository-BMgPSkqZ.mjs";
2
+ import { n as createMongooseAdapter } from "../adapter-BrVfPFSW.mjs";
3
3
  import { asReadOnlyRepo } from "@classytic/repo-core/repository";
4
4
  import { pluralizeBetterAuthCollection, resolveBetterAuthCollections } from "@classytic/repo-core/better-auth";
5
5
  //#region src/better-auth/index.ts
@@ -1,6 +1,6 @@
1
- import { n as Repository } from "./Repository-Ck5x43wF.mjs";
2
- import { a as customIdPlugin, f as auditTrailPlugin, i as multiTenantPlugin, m as auditLogPlugin, n as softDeletePlugin, t as timestampPlugin, u as cachePlugin } from "./timestamp.plugin-CBQukQzV.mjs";
3
- import { n as batchOperationsPlugin, t as methodRegistryPlugin } from "./method-registry.plugin-DblM7V6j.mjs";
1
+ import { n as Repository } from "./Repository-BMgPSkqZ.mjs";
2
+ import { b as auditLogPlugin, g as cachePlugin, i as multiTenantPlugin, l as customIdPlugin, n as softDeletePlugin, t as timestampPlugin, v as auditTrailPlugin } from "./timestamp.plugin-DF0gJB6p.mjs";
3
+ import { n as batchOperationsPlugin, t as methodRegistryPlugin } from "./method-registry.plugin-C63pGn1d.mjs";
4
4
  //#region src/create-repository.ts
5
5
  function isLoggerAudit(value) {
6
6
  const v = value;
@@ -1,8 +1,8 @@
1
- import { c as PaginationConfig } from "./pagination-jSUdrACK.mjs";
2
- import { _ as RepositoryOptions, d as PluginType } from "./repository-H_ffGgpE.mjs";
3
- import { T as AuditTrailOptions, _ as RepositoryCachePluginOptions, i as CustomIdOptions, t as MultiTenantOptions } from "./multi-tenant.plugin-BwSvTirB.mjs";
4
- import { n as Repository } from "./Repository-BdSkQ8vc.mjs";
5
- import { i as Logger, o as SoftDeleteOptions } from "./plugin-options-DXnlKM3B.mjs";
1
+ import { u as PaginationConfig } from "./pagination-BpIv2bdS.mjs";
2
+ import { _ as RepositoryOptions, d as PluginType } from "./repository-odl03582.mjs";
3
+ import { D as AuditTrailOptions, i as CustomIdOptions, t as MultiTenantOptions, y as RepositoryCachePluginOptions } from "./multi-tenant.plugin-CBgCo9im.mjs";
4
+ import { r as Repository } from "./Repository-CR8Y9g2E.mjs";
5
+ import { i as Logger, o as SoftDeleteOptions } from "./plugin-options-Zk46lD6F.mjs";
6
6
  import { Model } from "mongoose";
7
7
  //#region src/create-repository.d.ts
8
8
  /**
@@ -0,0 +1,23 @@
1
+ import { Connection, Model } from "mongoose";
2
+ import { IdempotencyClaimStore } from "@classytic/repo-core/idempotency";
3
+ //#region src/idempotency/index.d.ts
4
+ interface IdempotencyModelOptions {
5
+ /** Collection name. Default `idempotency_claims`. */
6
+ collection?: string;
7
+ /** Mongoose model name. Default `IdempotencyClaim`. Must be unique per connection. */
8
+ modelName?: string;
9
+ }
10
+ interface IdempotencyStoreOptions {
11
+ /**
12
+ * How long a claim is kept after its last write before the TTL monitor removes it.
13
+ * Default 7 days. Size it to the longest a caller may retry — a retry after the claim
14
+ * is gone executes again. An offline till that syncs days later needs more.
15
+ */
16
+ retentionSeconds?: number;
17
+ }
18
+ /** Build (or return) the claims model on `connection`. Idempotent per connection. */
19
+ declare function createIdempotencyModel(connection: Connection, options?: IdempotencyModelOptions): Model<Record<string, unknown>>;
20
+ /** The store over a model from {@link createIdempotencyModel}. */
21
+ declare function createIdempotencyStore(model: Model<Record<string, unknown>>, options?: IdempotencyStoreOptions): IdempotencyClaimStore;
22
+ //#endregion
23
+ export { IdempotencyModelOptions, IdempotencyStoreOptions, createIdempotencyModel, createIdempotencyStore };
@@ -0,0 +1,134 @@
1
+ import { Schema } from "mongoose";
2
+ import { identityKey } from "@classytic/repo-core/idempotency";
3
+ //#region src/idempotency/index.ts
4
+ /**
5
+ * The mongo {@link IdempotencyClaimStore} — use this with `runIdempotent` from
6
+ * `@classytic/repo-core/idempotency`; never keep a per-module claims table.
7
+ *
8
+ * ```ts
9
+ * const store = createIdempotencyStore(createIdempotencyModel(connection));
10
+ * const run = await runIdempotent({ store, identity, requestFingerprint, execute });
11
+ * ```
12
+ *
13
+ * One document per identity, `_id` = `identityKey(identity)`, so "insert iff absent" is
14
+ * the `_id` index and every compare-and-set is a single-document filter. Proven by
15
+ * `runIdempotencyStoreConformance` (`@classytic/repo-core/testing`) in this package's tests.
16
+ */
17
+ const DEFAULT_RETENTION_SECONDS = 604800;
18
+ /** Build (or return) the claims model on `connection`. Idempotent per connection. */
19
+ function createIdempotencyModel(connection, options = {}) {
20
+ const { collection = "idempotency_claims", modelName = "IdempotencyClaim" } = options;
21
+ const existing = connection.models[modelName];
22
+ if (existing) return existing;
23
+ const schema = new Schema({
24
+ _id: {
25
+ type: String,
26
+ required: true
27
+ },
28
+ identity: {
29
+ type: Schema.Types.Mixed,
30
+ required: true
31
+ },
32
+ requestFingerprint: {
33
+ type: String,
34
+ required: true
35
+ },
36
+ state: {
37
+ type: String,
38
+ enum: [
39
+ "in_flight",
40
+ "succeeded",
41
+ "failed"
42
+ ],
43
+ required: true
44
+ },
45
+ leaseToken: {
46
+ type: String,
47
+ required: true
48
+ },
49
+ leaseExpiresAt: {
50
+ type: Date,
51
+ required: true
52
+ },
53
+ createdAt: {
54
+ type: Date,
55
+ required: true
56
+ },
57
+ completedAt: { type: Date },
58
+ result: { type: Schema.Types.Mixed },
59
+ attempts: {
60
+ type: Number,
61
+ required: true
62
+ },
63
+ progress: { type: Schema.Types.Mixed },
64
+ context: { type: Schema.Types.Mixed },
65
+ lastError: { type: String },
66
+ purgeAt: {
67
+ type: Date,
68
+ required: true
69
+ }
70
+ }, {
71
+ collection,
72
+ versionKey: false,
73
+ minimize: false
74
+ });
75
+ schema.index({ purgeAt: 1 }, {
76
+ expireAfterSeconds: 0,
77
+ name: "retention"
78
+ });
79
+ schema.index({
80
+ "identity.operation": 1,
81
+ state: 1,
82
+ leaseExpiresAt: 1
83
+ }, { name: "lapsed_sweep" });
84
+ return connection.model(modelName, schema);
85
+ }
86
+ /** The store over a model from {@link createIdempotencyModel}. */
87
+ function createIdempotencyStore(model, options = {}) {
88
+ const retentionMs = (options.retentionSeconds ?? DEFAULT_RETENTION_SECONDS) * 1e3;
89
+ const claims = () => model.collection;
90
+ const toDoc = (claim) => ({
91
+ ...claim,
92
+ _id: identityKey(claim.identity),
93
+ purgeAt: new Date(Date.now() + retentionMs)
94
+ });
95
+ /** The caller's transaction, so a claim commits or rolls back with the writes it describes. */
96
+ const withSession = (options) => options?.session ? { session: options.session } : {};
97
+ const ownedInFlight = (identity, leaseToken) => ({
98
+ _id: identityKey(identity),
99
+ state: "in_flight",
100
+ leaseToken
101
+ });
102
+ return {
103
+ async get(identity, options) {
104
+ const doc = await claims().findOne({ _id: identityKey(identity) }, withSession(options));
105
+ if (!doc) return null;
106
+ const { _id, purgeAt, ...claim } = doc;
107
+ return claim;
108
+ },
109
+ async insert(claim, options) {
110
+ try {
111
+ await claims().insertOne(toDoc(claim), withSession(options));
112
+ return true;
113
+ } catch (error) {
114
+ if (error.code === 11e3) return false;
115
+ throw error;
116
+ }
117
+ },
118
+ async swap(identity, expectedLeaseToken, next, options) {
119
+ return (await claims().replaceOne(ownedInFlight(identity, expectedLeaseToken), toDoc(next), withSession(options))).matchedCount === 1;
120
+ },
121
+ async release(identity, expectedLeaseToken, options) {
122
+ return (await claims().deleteOne(ownedInFlight(identity, expectedLeaseToken), withSession(options))).deletedCount === 1;
123
+ },
124
+ async listLapsed(operation, now, limit) {
125
+ return (await claims().find({
126
+ "identity.operation": operation,
127
+ state: "in_flight",
128
+ leaseExpiresAt: { $lte: now }
129
+ }).sort({ leaseExpiresAt: 1 }).limit(limit).toArray()).map(({ _id, purgeAt, ...claim }) => claim);
130
+ }
131
+ };
132
+ }
133
+ //#endregion
134
+ export { createIdempotencyModel, createIdempotencyStore };
@@ -1,5 +1,5 @@
1
- import { _ as UpdateOptions, a as CreateOptions, b as LookupOptions, c as FindOneAndUpdateOptions, f as MinMaxResult, g as UpdateManyResult, l as GroupResult, p as OperationOptions, s as DeleteResult } from "./operations-DZodvjC-.mjs";
2
- import { a as PopulateSpec, i as ObjectId, l as SortSpec, o as ReadPreferenceType, s as SelectSpec, t as AnyDocument } from "./core-62oo3N1C.mjs";
1
+ import { _ as UpdateOptions, a as CreateOptions, b as LookupOptions, c as FindOneAndUpdateOptions, f as MinMaxResult, g as UpdateManyResult, l as GroupResult, p as OperationOptions, s as DeleteResult } from "./operations-CrRIoc8Z.mjs";
2
+ import { _ as PopulateSpec, g as ObjectId, p as AnyDocument, v as ReadPreferenceType, x as SortSpec, y as SelectSpec } from "./pagination-BpIv2bdS.mjs";
3
3
  import { p as UpdateWithValidationResult } from "./type-utils-CLzRFtIA.mjs";
4
4
  import { Model, PipelineStage } from "mongoose";
5
5
  declare namespace aggregate_d_exports {
@@ -12,9 +12,18 @@ declare function aggregate<TResult = unknown>(Model: Model<any>, pipeline: Pipel
12
12
  session?: unknown;
13
13
  }): Promise<TResult[]>;
14
14
  /**
15
- * Aggregate with pagination using native MongoDB $facet
16
- * WARNING: $facet results must be <16MB. For larger results (limit >1000),
17
- * consider using Repository.aggregatePaginate() or splitting into separate queries.
15
+ * Aggregate with pagination.
16
+ *
17
+ * Page and count run as TWO concurrent pipelines rather than one `$facet`.
18
+ * `$facet` bundles every returned document plus the count into a SINGLE output
19
+ * document, and no stage may exceed 16MB — so a page of large documents failed
20
+ * with `BSONObjectTooLarge` instead of paginating, and the only defence was a
21
+ * warning above an arbitrary limit of 1000 that told the caller to go
22
+ * elsewhere. `$facet` also cannot use an index for the stages inside it.
23
+ *
24
+ * The trade: two reads, so a concurrent write between them can leave `total`
25
+ * a document out of step with `data` — the same trade `PaginationEngine`
26
+ * already makes on both its offset and aggregate paths.
18
27
  */
19
28
  declare function aggregatePaginate<TDoc = AnyDocument>(Model: Model<TDoc>, pipeline: PipelineStage[], options?: {
20
29
  page?: number;
@@ -91,14 +100,64 @@ declare function minMax(Model: Model<any>, field: string, query?: Record<string,
91
100
  session?: unknown;
92
101
  }): Promise<MinMaxResult>;
93
102
  declare namespace create_d_exports {
94
- export { create, createDefault, createMany, upsert };
103
+ export { CreateManyFailure, CreateManyPartial, CreateManyPartialError, create, createDefault, createMany, isCreateManyPartialError, upsert };
95
104
  }
96
105
  /**
97
106
  * Create single document
98
107
  */
99
108
  declare function create<TDoc = AnyDocument>(Model: Model<TDoc>, data: Record<string, unknown>, options?: CreateOptions): Promise<TDoc>;
100
109
  /**
101
- * Create multiple documents
110
+ * One rejected input, keyed by its position in the array that was passed in.
111
+ *
112
+ * The index is the load-bearing part: it is the only way to tie a failure back
113
+ * to the caller's own record — the driver reports failures positionally, and a
114
+ * rejected document never comes back with an `_id` to match on.
115
+ */
116
+ interface CreateManyFailure {
117
+ /** Position in the ORIGINAL `dataArray`. */
118
+ index: number;
119
+ /** The input that was rejected, as supplied. */
120
+ doc: Record<string, unknown>;
121
+ /** MongoDB error code, e.g. 11000 for a duplicate key. */
122
+ code?: number;
123
+ message: string;
124
+ }
125
+ /**
126
+ * What a partially-successful `createMany` actually did.
127
+ *
128
+ * Attached to the thrown error as `partial`. `createMany` still REJECTS — that
129
+ * is unchanged and deliberate — but the rejection now says which documents
130
+ * landed and which did not, instead of leaving the caller to dig through a
131
+ * driver error to find out.
132
+ */
133
+ interface CreateManyPartial<TDoc = AnyDocument> {
134
+ /** Documents the server accepted. */
135
+ inserted: TDoc[];
136
+ /** Every rejected input, in input order. */
137
+ failed: CreateManyFailure[];
138
+ }
139
+ /** The error `createMany` throws when some documents landed and some did not. */
140
+ type CreateManyPartialError<TDoc = AnyDocument> = Error & {
141
+ partial?: CreateManyPartial<TDoc>;
142
+ };
143
+ /** Narrow a caught error to one carrying a partial-write report. */
144
+ declare function isCreateManyPartialError<TDoc = AnyDocument>(err: unknown): err is CreateManyPartialError<TDoc> & {
145
+ partial: CreateManyPartial<TDoc>;
146
+ };
147
+ /**
148
+ * Create multiple documents.
149
+ *
150
+ * **A rejection does NOT mean nothing was written.** Neither ordering does:
151
+ * `ordered: true` stops at the first failure and keeps everything before it,
152
+ * `ordered: false` keeps everything valid. Measured on a six-document batch
153
+ * with a conflict in the middle — ordered wrote 3, unordered wrote 5. Only a
154
+ * transaction gives all-or-nothing.
155
+ *
156
+ * So the rejection carries a `partial` report naming what landed and what did
157
+ * not, because the alternative is a caller retrying the whole batch and
158
+ * double-writing everything that had already succeeded. Use it to retry only
159
+ * `failed`, or to reconcile — and prefer a unique index or an idempotency key
160
+ * for anything that will be retried automatically.
102
161
  */
103
162
  declare function createMany<TDoc = AnyDocument>(Model: Model<TDoc>, dataArray: Record<string, unknown>[], options?: CreateOptions): Promise<TDoc[]>;
104
163
  /**
@@ -301,4 +360,4 @@ declare namespace index_d_exports {
301
360
  export { aggregate_d_exports as aggregate, create_d_exports as create, delete_d_exports as deleteActions, read_d_exports as read, update_d_exports as update };
302
361
  }
303
362
  //#endregion
304
- export { create_d_exports as a, delete_d_exports as i, update_d_exports as n, aggregate_d_exports as o, read_d_exports as r, index_d_exports as t };
363
+ export { CreateManyFailure as a, create_d_exports as c, delete_d_exports as i, isCreateManyPartialError as l, update_d_exports as n, CreateManyPartial as o, read_d_exports as r, CreateManyPartialError as s, index_d_exports as t, aggregate_d_exports as u };