@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,8 +1,8 @@
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-ZbLtL7Nk.mjs";
4
- import { t as PaginationEngine } from "./PaginationEngine-D-P19Y9m.mjs";
5
- import { AggPaginationRequest, AggRequest, AggResult, ArchiveOptions, ArchiveResult, ArchiveSink, ChangeEvent, FilterInput, KeysetAggPaginationResult, QueryOptions, RepoCapabilities, RepositoryBase, TenantPurgeOptions, TenantPurgeResult, TenantPurgeStrategy, WatchOptions } from "@classytic/repo-core/repository";
1
+ import { S as LookupOptions, _ as SessionOptions, b as WithTransactionOptions, c as DeleteResult, f as LookupPopulateOptions, g as ReadOptions, h as OperationOptions, i as CacheableOptions, l as FindOneAndUpdateOptions, n as AggregateOptions, o as CreateOptions, p as LookupPopulateResult, s as DeleteManyResult, t as AggregateCallOptions, u as GetByIdsOptions, v as UpdateManyResult, x as LookupBuilder, y as UpdateOptions } from "./operations-XiFgG5US.mjs";
2
+ import { A as ObjectId, F as SortSpec$1, M as ReadPreferenceType, N as SelectSpec, c as MongokitPageExtras, i as CountStrategy, j as PopulateSpec, r as CollationOptions, t as AggregatePaginationOptions, u as PaginationConfig } from "./pagination-D6psses0.mjs";
3
+ import { _ as RepositoryOptions, d as PluginType, f as PrioritizedHook, o as Middleware, p as RepositoryContext } from "./repository-DNHfjAZX.mjs";
4
+ import { n as PaginationEngine } from "./PaginationEngine-CRHyuUtw.mjs";
5
+ import { AggPaginationRequest, AggRequest, AggResult, ArchiveOptions, ArchiveResult, ArchiveSink, BulkUpsertOptions, BulkUpsertResult, ChangeEvent, FilterInput, KeysetAggPaginationResult, QueryOptions, RepoCapabilities, RepositoryBase, TenantPurgeOptions, TenantPurgeResult, TenantPurgeStrategy, WatchOptions } from "@classytic/repo-core/repository";
6
6
  import { ClientSession, Expression, Model, PipelineStage, PopulateOptions } from "mongoose";
7
7
  import { Filter } from "@classytic/repo-core/filter";
8
8
  import { RepositoryCacheHandle } from "@classytic/repo-core/cache";
@@ -519,6 +519,37 @@ interface TransitionMachine {
519
519
  readonly name: string;
520
520
  assertTransition(entityId: string, from: string, to: string): void;
521
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
+ };
522
553
  declare class Repository<TDoc = unknown> extends RepositoryBase {
523
554
  /**
524
555
  * Cache handle ATTACHED BY the unified cache plugin at wiring time —
@@ -591,6 +622,8 @@ declare class Repository<TDoc = unknown> extends RepositoryBase {
591
622
  * {@link ID_ADDRESSED_OPERATIONS}.
592
623
  */
593
624
  private readonly _guardIdAddressed;
625
+ /** This repository's own `queryDefaults` / `aggregateDefaults` (deployment ones fill the gaps). */
626
+ private readonly _queryDefaults;
594
627
  constructor(Model: Model<TDoc, any, any, any>, plugins?: PluginType[], paginationConfig?: PaginationConfig, options?: RepositoryOptions);
595
628
  /**
596
629
  * Live read-through view of the hook engine's listener registry.
@@ -741,6 +774,12 @@ declare class Repository<TDoc = unknown> extends RepositoryBase {
741
774
  * `_buildContext` inputs), so every op routed through `_runOp` — and the
742
775
  * inline-try methods that call this directly — honors them uniformly.
743
776
  */
777
+ /** One command's time bound and concerns: per call, then this repository, then the deployment. */
778
+ private _opts;
779
+ /** How an IR request runs: per-call hints, then this repository's and the deployment's defaults. */
780
+ private _aggRun;
781
+ /** Scope every join in `pipeline` under its owning collection's policy (see `join-scope.ts`). */
782
+ private _scopeJoins;
744
783
  private _withResilience;
745
784
  /**
746
785
  * Create single document
@@ -774,16 +813,15 @@ declare class Repository<TDoc = unknown> extends RepositoryBase {
774
813
  * const byId = await repo.getByIds(offerIds, ctx);
775
814
  * const doc = byId.get(offerId); // O(1); undefined if not found
776
815
  *
777
- * Sizing: there is no library-imposed cap — the bounds are MongoDB's (the
778
- * `$in` array must fit the 16 MB query document; `$in` on an indexed field
779
- * is n point-lookups). Keep batches ≤ ~10k ids per call and chunk beyond
780
- * that. Don't exclude the id field via `select` — result keys derive from
781
- * it.
816
+ * Chunking: ids are split into `$in` slices of `chunkSize` (default `DEFAULT_ID_CHUNK`), one
817
+ * command each — sequential inside a session (a transaction runs one operation at a time),
818
+ * at most `concurrency` (default 4) in flight outside one. `preserveOrder: true` returns an
819
+ * array aligned with `ids` (`undefined` for a miss). Don't exclude the id field via `select`.
782
820
  */
783
- getByIds(ids: ReadonlyArray<string | ObjectId>, options?: OperationOptions & {
784
- sort?: SortSpec$1 | string;
785
- limit?: number;
786
- }): Promise<Map<string, TDoc>>;
821
+ getByIds(ids: ReadonlyArray<string | ObjectId>, options: GetByIdsOptions & {
822
+ preserveOrder: true;
823
+ }): Promise<Array<TDoc | undefined>>;
824
+ getByIds(ids: ReadonlyArray<string | ObjectId>, options?: GetByIdsOptions): Promise<Map<string, TDoc>>;
787
825
  /**
788
826
  * Get single document by query
789
827
  */
@@ -813,6 +851,31 @@ declare class Repository<TDoc = unknown> extends RepositoryBase {
813
851
  sort?: SortSpec$1 | string;
814
852
  limit?: number;
815
853
  }): Promise<TDoc[]>;
854
+ /**
855
+ * The keyset cursor `getAll({ sort, filters, mode: 'keyset' })` would mint for `row`, under the
856
+ * same policy scope, so a page resumes AFTER it. For composing keyset walks (repo-core
857
+ * `mergeKeysetPages` via `keysetSource`), where the last CONSUMED row is not a page's last row.
858
+ */
859
+ keysetCursor(row: unknown, options: ReadOptions & {
860
+ sort: SortSpec$1 | string;
861
+ filters?: Record<string, unknown>;
862
+ collation?: CollationOptions;
863
+ }): Promise<string>;
864
+ /**
865
+ * Resumable keyset batches over `_id`: each batch is ONE bounded `find` (no long-lived server
866
+ * cursor, so no cursor timeout and no CSOT lifetime budget), memory is one batch, and every
867
+ * batch carries a `checkpoint`. Pass a checkpoint back as `after` to resume: rows written
868
+ * behind it are not revisited, rows ahead of it are reached. A checkpoint is bound to the
869
+ * collection, the post-policy filter (tenant included) and is refused elsewhere.
870
+ */
871
+ iterate(filter?: Record<string, unknown>, options?: ReadOptions & {
872
+ after?: string;
873
+ batchSize?: number;
874
+ select?: SelectSpec;
875
+ }): AsyncIterableIterator<{
876
+ docs: TDoc[];
877
+ checkpoint: string;
878
+ }>;
816
879
  /**
817
880
  * Streaming reads — async iterator over data, suitable for migrations,
818
881
  * backfills, schema audits, and any once-a-quarter "touch every row"
@@ -916,36 +979,19 @@ declare class Repository<TDoc = unknown> extends RepositoryBase {
916
979
  * // Skip cache for fresh data
917
980
  * await repo.getAll({ filters: { status: 'active' } }, { skipCache: true });
918
981
  */
919
- getAll(params?: {
920
- filters?: Record<string, unknown>;
921
- sort?: SortSpec$1 | string;
922
- cursor?: string;
923
- after?: string;
924
- /** Cursor for the PREVIOUS page. Mutually exclusive with `after`/`cursor`. */
925
- before?: string;
926
- page?: number;
927
- pagination?: {
928
- page?: number;
929
- limit?: number;
930
- };
931
- limit?: number;
932
- search?: string;
933
- mode?: 'offset' | 'keyset';
934
- hint?: string | Record<string, 1 | -1>;
935
- maxTimeMS?: number;
936
- countStrategy?: CountStrategy;
937
- /** Ceiling for `countStrategy: 'capped'`. */
938
- countLimit?: number;
939
- readPreference?: ReadPreferenceType;
940
- /** Advanced populate options (from QueryParser or Arc's BaseController) */
941
- populateOptions?: PopulateOptions[];
942
- /** Collation for locale-aware string comparison */
943
- collation?: CollationOptions;
944
- /** Lookup configurations for $lookup joins (from QueryParser or manual) */
945
- lookups?: LookupOptions[];
946
- /** Skip pagination entirely — returns raw TDoc[] (same as findAll) */
947
- noPagination?: boolean;
948
- }, options?: CacheableOptions): Promise<OffsetPaginationResult<TDoc> | KeysetPaginationResult<TDoc> | TDoc[]>;
982
+ /** An explicit `mode` (or `noPagination`) decides the envelope, so the result type follows it. */
983
+ getAll(params: GetAllParams & {
984
+ mode: 'offset';
985
+ noPagination?: false;
986
+ }, options?: CacheableOptions): Promise<OffsetPaginationResult<TDoc>>;
987
+ getAll(params: GetAllParams & {
988
+ mode: 'keyset';
989
+ noPagination?: false;
990
+ }, options?: CacheableOptions): Promise<KeysetPaginationResult<TDoc>>;
991
+ getAll(params: GetAllParams & {
992
+ noPagination: true;
993
+ }, options?: CacheableOptions): Promise<TDoc[]>;
994
+ getAll(params?: GetAllParams, options?: CacheableOptions): Promise<OffsetPaginationResult<TDoc> | KeysetPaginationResult<TDoc> | TDoc[]>;
949
995
  /**
950
996
  * Atomic "look up by filter, insert `data` if missing, return the doc."
951
997
  *
@@ -1268,6 +1314,11 @@ declare class Repository<TDoc = unknown> extends RepositoryBase {
1268
1314
  }, patch?: Record<string, unknown>, options?: SessionOptions & {
1269
1315
  idField?: string;
1270
1316
  upsert?: boolean;
1317
+ /**
1318
+ * The document as the caller just read it in this session. Hooks' `loadTarget()`
1319
+ * returns it instead of reading again. Pass only a read taken in the same session.
1320
+ */
1321
+ target?: Record<string, unknown>;
1271
1322
  }): Promise<TDoc | null>;
1272
1323
  /**
1273
1324
  * State-machine-backed CAS transition with status history — the
@@ -1543,6 +1594,13 @@ declare class Repository<TDoc = unknown> extends RepositoryBase {
1543
1594
  archiveByFilter(filter: Record<string, unknown> | Filter, sink: ArchiveSink<TDoc>, options?: ArchiveOptions & {
1544
1595
  session?: ClientSession;
1545
1596
  }): Promise<ArchiveResult>;
1597
+ /**
1598
+ * Many keyed upserts in ONE command with an exact per-row outcome (repo-core `bulkUpsert` IR).
1599
+ * Sub-ops pass through the `before:bulkUpsert` policy hooks (tenant, soft-delete) like bulkWrite
1600
+ * sub-ops. Refused unless a unique index is pinned by the key plus the policy scope. Needs
1601
+ * MongoDB 8.0 (client bulkWrite with verbose results).
1602
+ */
1603
+ bulkUpsert(rows: readonly Record<string, unknown>[], options: BulkUpsertOptions): Promise<BulkUpsertResult>;
1546
1604
  /**
1547
1605
  * Kit-native aggregation pipeline — takes a MongoDB stage array and
1548
1606
  * returns the raw pipeline output. Use this for `$lookup`, `$unwind`,
@@ -1582,7 +1640,7 @@ declare class Repository<TDoc = unknown> extends RepositoryBase {
1582
1640
  * Policy hooks (multi-tenant, soft-delete) inject context.filters
1583
1641
  * which are prepended as a `$match` stage.
1584
1642
  */
1585
- aggregatePipelinePaginate(options?: AggregatePaginationOptions): Promise<AggregatePaginationResult<TDoc>>;
1643
+ aggregatePipelinePaginate(options?: AggregatePaginationOptions): Promise<AggregatePaginationResult<TDoc, MongokitPageExtras>>;
1586
1644
  /**
1587
1645
  * Portable aggregation. Compiles the repo-core `AggRequest` IR to
1588
1646
  * `[{$match}, {$group}, {$addFields}, {$project}, {$match}, {$sort},
@@ -1605,7 +1663,9 @@ declare class Repository<TDoc = unknown> extends RepositoryBase {
1605
1663
  * `context.filter`, which is merged into the pre-aggregate `$match`
1606
1664
  * before compilation.
1607
1665
  */
1608
- aggregate<TRow extends Record<string, unknown> = Record<string, unknown>>(req: AggRequest, options?: QueryOptions): Promise<AggResult<TRow>>;
1666
+ aggregate<TRow extends Record<string, unknown> = Record<string, unknown>>(req: AggRequest, options?: QueryOptions & AggregateCallOptions): Promise<AggResult<TRow> & {
1667
+ plan?: unknown;
1668
+ }>;
1609
1669
  /**
1610
1670
  * Invalidate every aggregate-cache entry tagged with ANY of the
1611
1671
  * given tags. Delegates to the unified cache plugin's handle
@@ -1801,6 +1861,12 @@ declare class Repository<TDoc = unknown> extends RepositoryBase {
1801
1861
  * 4. DEFAULT (500) — user-registered hooks
1802
1862
  */
1803
1863
  _buildContext(operation: string, options: Record<string, unknown> | object): Promise<RepositoryContext>;
1864
+ /**
1865
+ * `memo` and (for an id-addressed op) `loadTarget` — attached before the before-hooks
1866
+ * run so every hook of one operation shares one read. A caller that has just read the
1867
+ * document in the same session passes it as `target`, and nothing is read at all.
1868
+ */
1869
+ private _sharedReadSeams;
1804
1870
  /**
1805
1871
  * Feed the resolved filter of a guarded verb to the distribution guard.
1806
1872
  * The slot follows `OP_REGISTRY[op].policyKey` (`query` vs `filters`);
@@ -1916,4 +1982,4 @@ declare class Repository<TDoc = unknown> extends RepositoryBase {
1916
1982
  private _translateError;
1917
1983
  }
1918
1984
  //#endregion
1919
- 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 };
1985
+ 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 { 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";
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-SYF8N28D.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 { b as create_exports, i as update_exports, m as aggregate_exports, p as delete_exports, u as read_exports } from "../update-c_01A0if.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-WgeAUJ_I.mjs";
3
3
  //#region src/actions/index.ts
4
4
  var actions_exports = /* @__PURE__ */ __exportAll({
5
5
  aggregate: () => aggregate_exports,
@@ -1,2 +1,2 @@
1
- import { n as createMongooseAdapter, r as isMongooseModel, t as MongooseAdapter } from "../adapter-BrVfPFSW.mjs";
1
+ import { n as createMongooseAdapter, r as isMongooseModel, t as MongooseAdapter } from "../adapter-CkMoMF83.mjs";
2
2
  export { MongooseAdapter, createMongooseAdapter, isMongooseModel };
@@ -1,5 +1,5 @@
1
1
  import { n as warn } from "./logger-K7dPmu2U.mjs";
2
- import { t as buildCrudSchemasFromModel } from "./mongooseToJsonSchema-waUVQwK4.mjs";
2
+ import { t as buildCrudSchemasFromModel } from "./mongooseToJsonSchema-C3k4ide0.mjs";
3
3
  import { matchesRecordFilter } from "@classytic/repo-core/filter";
4
4
  import { mergeFieldRuleConstraints } from "@classytic/repo-core/schema";
5
5
  import { asRepositoryLike, isRepository } from "@classytic/repo-core/adapter";
@@ -1,4 +1,4 @@
1
- import { l as Plugin } from "../repository-ZbLtL7Nk.mjs";
1
+ import { l as Plugin } from "../repository-DNHfjAZX.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-D7fDpyFx.mjs";
2
- import { n as createMongooseAdapter } from "../adapter-BrVfPFSW.mjs";
1
+ import { n as Repository } from "../Repository-CYhSC9ut.mjs";
2
+ import { n as createMongooseAdapter } from "../adapter-CkMoMF83.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,8 +1,8 @@
1
- import { u as PaginationConfig } from "./pagination-BpIv2bdS.mjs";
2
- import { _ as RepositoryOptions, d as PluginType } from "./repository-ZbLtL7Nk.mjs";
3
- import { T as AuditTrailOptions, _ as RepositoryCachePluginOptions, i as CustomIdOptions, t as MultiTenantOptions } from "./multi-tenant.plugin-B-tzNeYL.mjs";
4
- import { n as Repository } from "./Repository-IxI983_m.mjs";
5
- import { i as Logger, o as SoftDeleteOptions } from "./plugin-options-BuvtOSZf.mjs";
1
+ import { u as PaginationConfig } from "./pagination-D6psses0.mjs";
2
+ import { _ as RepositoryOptions, d as PluginType } from "./repository-DNHfjAZX.mjs";
3
+ import { D as AuditTrailOptions, i as CustomIdOptions, t as MultiTenantOptions, y as RepositoryCachePluginOptions } from "./multi-tenant.plugin-a3UhQARj.mjs";
4
+ import { r as Repository } from "./Repository-pnLw1rG5.mjs";
5
+ import { i as Logger, o as SoftDeleteOptions } from "./plugin-options-CRhIvSzx.mjs";
6
6
  import { Model } from "mongoose";
7
7
  //#region src/create-repository.d.ts
8
8
  /**
@@ -1,5 +1,5 @@
1
- import { n as Repository } from "./Repository-D7fDpyFx.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-B5eiaItQ.mjs";
1
+ import { n as Repository } from "./Repository-CYhSC9ut.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-zfdLYZiO.mjs";
3
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) {
@@ -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 };