@classytic/mongokit 3.12.0 → 3.13.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 (38) hide show
  1. package/README.md +74 -4
  2. package/dist/{PaginationEngine-BDaAsyPn.mjs → PaginationEngine-CfQRM1yz.mjs} +16 -16
  3. package/dist/Repository-BY40uhcv.mjs +3603 -0
  4. package/dist/actions/index.d.mts +1 -1
  5. package/dist/actions/index.mjs +1 -1
  6. package/dist/adapter/index.d.mts +190 -0
  7. package/dist/adapter/index.mjs +2 -0
  8. package/dist/adapter-DTDMnUOH.mjs +158 -0
  9. package/dist/ai/index.d.mts +1 -1
  10. package/dist/ai/index.mjs +1 -1
  11. package/dist/better-auth/index.d.mts +149 -0
  12. package/dist/better-auth/index.mjs +81 -0
  13. package/dist/field-selection-Br3WPOEb.mjs +99 -0
  14. package/dist/{index-D9cORR-o.d.mts → index-BbHaKwi5.d.mts} +33 -13
  15. package/dist/index.d.mts +667 -30
  16. package/dist/index.mjs +240 -17
  17. package/dist/{logger-8-VS1za6.mjs → logger-DprijP5h.mjs} +1 -5
  18. package/dist/{mongooseToJsonSchema-B187ufOP.mjs → mongooseToJsonSchema-BYbG894y.mjs} +1 -1
  19. package/dist/{mongooseToJsonSchema-rtkdbBhf.d.mts → mongooseToJsonSchema-BgRAaXX-.d.mts} +2 -1
  20. package/dist/pagination/PaginationEngine.d.mts +2 -2
  21. package/dist/pagination/PaginationEngine.mjs +1 -1
  22. package/dist/plugins/index.d.mts +2 -2
  23. package/dist/plugins/index.mjs +2 -2
  24. package/dist/query/primitives/coercion.d.mts +1 -1
  25. package/dist/query/primitives/coercion.mjs +1 -1
  26. package/dist/query/primitives/indexes.d.mts +1 -1
  27. package/dist/{types-DXscZnLu.d.mts → types-BbaPZleN.d.mts} +371 -112
  28. package/dist/{update-CDxPtpsk.mjs → update-DLcSSa5b.mjs} +41 -27
  29. package/dist/utils/index.d.mts +2 -91
  30. package/dist/utils/index.mjs +5 -5
  31. package/dist/{validation-chain.plugin-CH-xPoKM.d.mts → validation-chain.plugin-CAPYCj_T.d.mts} +212 -80
  32. package/dist/validation-chain.plugin-_-WuNml2.mjs +2399 -0
  33. package/package.json +20 -3
  34. package/dist/field-selection-dI0m4RBs.mjs +0 -221
  35. package/dist/validation-chain.plugin-BicIgzzt.mjs +0 -4970
  36. /package/dist/{coercion-oM8G4b6m.d.mts → coercion-TRMmR49h.d.mts} +0 -0
  37. /package/dist/{id-resolution-e4BT9zAV.mjs → id-resolution-B0Odvvou.mjs} +0 -0
  38. /package/dist/{indexes-C26GNZRU.d.mts → indexes-CgOF7Wjd.d.mts} +0 -0
@@ -1,5 +1,5 @@
1
1
  import { t as __exportAll } from "./chunk-CfYAbeIz.mjs";
2
- import { r as warn } from "./logger-8-VS1za6.mjs";
2
+ import { n as warn } from "./logger-DprijP5h.mjs";
3
3
  import { t as createError } from "./error-B00pI7dV.mjs";
4
4
  import { isFilter } from "@classytic/repo-core/filter";
5
5
  //#region src/filter/compile.ts
@@ -475,17 +475,17 @@ async function aggregatePaginate(Model, pipeline, options = {}) {
475
475
  const skip = (page - 1) * limit;
476
476
  if (limit > 1e3) warn(`[mongokit] Large aggregation limit (${limit}). $facet results must be <16MB. Consider using Repository.aggregatePaginate() for safer handling of large datasets.`);
477
477
  const facetPipeline = [...pipeline, { $facet: {
478
- docs: [{ $skip: skip }, { $limit: limit }],
478
+ data: [{ $skip: skip }, { $limit: limit }],
479
479
  total: [{ $count: "count" }]
480
480
  } }];
481
481
  const aggregation = Model.aggregate(facetPipeline);
482
482
  if (options.session) aggregation.session(options.session);
483
483
  const [result] = await aggregation.exec();
484
- const docs = result.docs || [];
484
+ const data = result.data || [];
485
485
  const total = result.total[0]?.count || 0;
486
486
  const pages = Math.ceil(total / limit);
487
487
  return {
488
- docs,
488
+ data,
489
489
  total,
490
490
  page,
491
491
  limit,
@@ -662,54 +662,49 @@ var delete_exports = /* @__PURE__ */ __exportAll({
662
662
  softDelete: () => softDelete
663
663
  });
664
664
  /**
665
- * Delete by ID
665
+ * Delete by ID. Returns `null` on miss (matches `update()` convention) —
666
+ * a second delete on the same id is a no-op, not an error.
666
667
  */
667
668
  async function deleteById(Model, id, options = {}) {
668
669
  const query = {
669
670
  _id: id,
670
671
  ...options.query
671
672
  };
672
- if (!await Model.findOneAndDelete(query).session(options.session ?? null)) return {
673
- success: false,
674
- message: "Document not found",
675
- id: String(id)
676
- };
673
+ if (!await Model.findOneAndDelete(query).session(options.session ?? null)) return null;
677
674
  return {
678
- success: true,
679
675
  message: "Deleted successfully",
680
676
  id: String(id)
681
677
  };
682
678
  }
683
679
  /**
684
- * Delete many documents
680
+ * Delete many documents. Always returns a {@link DeleteResult} (never
681
+ * null) — a deleteMany with zero matches is a successful op with
682
+ * `count: 0`, not a miss.
685
683
  */
686
684
  async function deleteMany(Model, query, options = {}) {
687
685
  return {
688
- success: true,
689
686
  count: (await Model.deleteMany(query).session(options.session ?? null)).deletedCount,
690
687
  message: "Deleted successfully"
691
688
  };
692
689
  }
693
690
  /**
694
- * Delete by query
691
+ * Delete by query. Returns `null` on miss unless `throwOnNotFound: true`.
695
692
  */
696
693
  async function deleteByQuery(Model, query, options = {}) {
697
694
  const document = await Model.findOneAndDelete(query).session(options.session ?? null);
698
695
  if (!document) {
699
696
  if (options.throwOnNotFound === true) throw createError(404, "Document not found");
700
- return {
701
- success: false,
702
- message: "Document not found"
703
- };
697
+ return null;
704
698
  }
705
699
  return {
706
- success: true,
707
700
  message: "Deleted successfully",
708
701
  id: String(document._id)
709
702
  };
710
703
  }
711
704
  /**
712
- * Soft delete (set deleted flag)
705
+ * Soft delete (set deleted flag). Returns `null` on miss for parity with
706
+ * `deleteById`; callers that prefer a throw can wrap or use `throwOnNotFound`
707
+ * pattern at the call site.
713
708
  */
714
709
  async function softDelete(Model, id, options = {}) {
715
710
  if (!await Model.findByIdAndUpdate(id, {
@@ -719,16 +714,15 @@ async function softDelete(Model, id, options = {}) {
719
714
  }, {
720
715
  returnDocument: "after",
721
716
  session: options.session
722
- })) throw createError(404, "Document not found");
717
+ })) return null;
723
718
  return {
724
- success: true,
725
719
  message: "Soft deleted successfully",
726
720
  id: String(id),
727
721
  soft: true
728
722
  };
729
723
  }
730
724
  /**
731
- * Restore soft deleted document
725
+ * Restore soft-deleted document. Returns `null` on miss.
732
726
  */
733
727
  async function restore(Model, id, options = {}) {
734
728
  if (!await Model.findByIdAndUpdate(id, {
@@ -738,9 +732,8 @@ async function restore(Model, id, options = {}) {
738
732
  }, {
739
733
  returnDocument: "after",
740
734
  session: options.session
741
- })) throw createError(404, "Document not found");
735
+ })) return null;
742
736
  return {
743
- success: true,
744
737
  message: "Restored successfully",
745
738
  id: String(id)
746
739
  };
@@ -834,16 +827,37 @@ async function getAll(Model, query = {}, options = {}) {
834
827
  return mongoQuery.exec();
835
828
  }
836
829
  /**
837
- * Get or create document (upsert)
830
+ * Get or create document (atomic upsert).
831
+ *
832
+ * Returns `{ doc, created }` so callers can disambiguate the two
833
+ * outcomes without a follow-up read — load-bearing for race-detection
834
+ * in idempotency stores, lock acquisition, and "ensure-exists" flows.
835
+ *
836
+ * `created: true` ⇔ this caller's `createData` won the race and was
837
+ * just inserted; `created: false` ⇔ a doc matching `query` already
838
+ * existed and was returned unchanged.
839
+ *
840
+ * Mongo: `includeResultMetadata: true` returns the raw `ModifyResult`
841
+ * (`{ value, lastErrorObject, ok }`). `lastErrorObject.upserted` is
842
+ * the `_id` of a freshly-inserted row — present iff this call inserted.
838
843
  */
839
844
  async function getOrCreate(Model, query, createData, options = {}) {
840
- return Model.findOneAndUpdate(query, { $setOnInsert: createData }, {
845
+ const result = await Model.findOneAndUpdate(query, { $setOnInsert: createData }, {
841
846
  upsert: true,
842
847
  returnDocument: "after",
843
848
  runValidators: true,
849
+ includeResultMetadata: true,
844
850
  session: options.session,
845
851
  ...options.updatePipeline !== void 0 ? { updatePipeline: options.updatePipeline } : {}
846
852
  });
853
+ if (result && typeof result === "object" && "value" in result) return {
854
+ doc: result.value,
855
+ created: result.lastErrorObject?.upserted != null
856
+ };
857
+ return {
858
+ doc: result,
859
+ created: false
860
+ };
847
861
  }
848
862
  /**
849
863
  * Count documents matching query
@@ -1,94 +1,5 @@
1
- import { lt as SortSpec, q as PopulateSpec, rt as SelectSpec } from "../types-DXscZnLu.mjs";
2
- import { a as createFieldPreset, c as getMongooseProjection, i as configureLogger, l as createError, n as buildCrudSchemasFromMongooseSchema, o as filterResponseData, r as createMemoryCache, s as getFieldsForUser, t as buildCrudSchemasFromModel } from "../mongooseToJsonSchema-rtkdbBhf.mjs";
1
+ import { a as createFieldPreset, c as getMongooseProjection, i as configureLogger, l as createError, n as buildCrudSchemasFromMongooseSchema, o as filterResponseData, r as createMemoryCache, s as getFieldsForUser, t as buildCrudSchemasFromModel } from "../mongooseToJsonSchema-BgRAaXX-.mjs";
3
2
 
4
- //#region src/utils/cache-keys.d.ts
5
- /**
6
- * Generate cache key for getById operations
7
- *
8
- * Includes select/populate/lean in the key so different query shapes
9
- * (e.g., plain vs populated, lean vs hydrated) never collide.
10
- *
11
- * Format: {prefix}:id:{model}:{documentId} (no options)
12
- * Format: {prefix}:id:{model}:{documentId}:{optionsHash} (with options)
13
- *
14
- * @example
15
- * byIdKey('mk', 'User', '507f1f77bcf86cd799439011')
16
- * // => 'mk:id:User:507f1f77bcf86cd799439011'
17
- * byIdKey('mk', 'User', '507f1f77bcf86cd799439011', { select: 'name email' })
18
- * // => 'mk:id:User:507f1f77bcf86cd799439011:a1b2c3d4'
19
- */
20
- declare function byIdKey(prefix: string, model: string, id: string, options?: {
21
- select?: SelectSpec;
22
- populate?: PopulateSpec;
23
- lean?: boolean;
24
- }): string;
25
- /**
26
- * Generate cache key for single-document queries
27
- *
28
- * Format: {prefix}:one:{model}:{queryHash}
29
- *
30
- * @example
31
- * byQueryKey('mk', 'User', { email: 'john@example.com' })
32
- * // => 'mk:one:User:a1b2c3d4'
33
- */
34
- declare function byQueryKey(prefix: string, model: string, version: number, query: Record<string, unknown>, options?: {
35
- select?: SelectSpec;
36
- populate?: PopulateSpec;
37
- }): string;
38
- /**
39
- * Generate cache key for paginated list queries
40
- *
41
- * Format: {prefix}:list:{model}:{version}:{queryHash}
42
- *
43
- * The version component enables efficient bulk invalidation:
44
- * - On any mutation, bump the version
45
- * - All list cache keys become invalid without scanning/deleting each
46
- *
47
- * @example
48
- * listQueryKey('mk', 'User', 1, { filters: { status: 'active' }, page: 1, limit: 20 })
49
- * // => 'mk:list:User:1:e5f6g7h8'
50
- */
51
- declare function listQueryKey(prefix: string, model: string, version: number, params: {
52
- filters?: Record<string, unknown>;
53
- sort?: SortSpec;
54
- page?: number;
55
- limit?: number;
56
- after?: string;
57
- select?: SelectSpec;
58
- populate?: PopulateSpec;
59
- search?: string;
60
- mode?: string;
61
- lean?: boolean;
62
- readPreference?: string;
63
- hint?: string | Record<string, unknown>;
64
- maxTimeMS?: number;
65
- countStrategy?: string;
66
- }): string;
67
- /**
68
- * Generate cache key for collection version tag
69
- *
70
- * Format: {prefix}:ver:{model}
71
- *
72
- * Used to track mutation version for list invalidation
73
- */
74
- declare function versionKey(prefix: string, model: string): string;
75
- /**
76
- * Generate pattern for clearing all cache keys for a model
77
- *
78
- * Format: {prefix}:*:{model}:*
79
- *
80
- * @example
81
- * modelPattern('mk', 'User')
82
- * // => 'mk:*:User:*'
83
- */
84
- declare function modelPattern(prefix: string, model: string): string;
85
- /**
86
- * Generate pattern for clearing all list cache keys for a model
87
- *
88
- * Format: {prefix}:list:{model}:*
89
- */
90
- declare function listPattern(prefix: string, model: string): string;
91
- //#endregion
92
3
  //#region src/utils/id-resolution.d.ts
93
4
  /**
94
5
  * ID Resolution Primitive
@@ -146,4 +57,4 @@ declare function getSchemaIdType(schema?: SchemaLike | null): IdType;
146
57
  */
147
58
  declare function isValidIdForType(id: unknown, idType: IdType): boolean;
148
59
  //#endregion
149
- export { type IdType, buildCrudSchemasFromModel, buildCrudSchemasFromMongooseSchema, byIdKey, byQueryKey, configureLogger, createError, createFieldPreset, createMemoryCache, filterResponseData, getFieldsForUser, getMongooseProjection, getSchemaIdType, isObjectIdInstance, isValidIdForType, listPattern, listQueryKey, modelPattern, versionKey };
60
+ export { type IdType, buildCrudSchemasFromModel, buildCrudSchemasFromMongooseSchema, configureLogger, createError, createFieldPreset, createMemoryCache, filterResponseData, getFieldsForUser, getMongooseProjection, getSchemaIdType, isObjectIdInstance, isValidIdForType };
@@ -1,6 +1,6 @@
1
- import { t as configureLogger } from "../logger-8-VS1za6.mjs";
1
+ import { t as configureLogger } from "../logger-DprijP5h.mjs";
2
2
  import { t as createError } from "../error-B00pI7dV.mjs";
3
- import { n as isObjectIdInstance, r as isValidIdForType, t as getSchemaIdType } from "../id-resolution-e4BT9zAV.mjs";
4
- import { a as byIdKey, c as listQueryKey, i as getMongooseProjection, l as modelPattern, n as filterResponseData, o as byQueryKey, r as getFieldsForUser, s as listPattern, t as createFieldPreset, u as versionKey } from "../field-selection-dI0m4RBs.mjs";
5
- import { n as buildCrudSchemasFromMongooseSchema, r as createMemoryCache, t as buildCrudSchemasFromModel } from "../mongooseToJsonSchema-B187ufOP.mjs";
6
- export { buildCrudSchemasFromModel, buildCrudSchemasFromMongooseSchema, byIdKey, byQueryKey, configureLogger, createError, createFieldPreset, createMemoryCache, filterResponseData, getFieldsForUser, getMongooseProjection, getSchemaIdType, isObjectIdInstance, isValidIdForType, listPattern, listQueryKey, modelPattern, versionKey };
3
+ import { i as getMongooseProjection, n as filterResponseData, r as getFieldsForUser, t as createFieldPreset } from "../field-selection-Br3WPOEb.mjs";
4
+ import { n as isObjectIdInstance, r as isValidIdForType, t as getSchemaIdType } from "../id-resolution-B0Odvvou.mjs";
5
+ import { n as buildCrudSchemasFromMongooseSchema, r as createMemoryCache, t as buildCrudSchemasFromModel } from "../mongooseToJsonSchema-BYbG894y.mjs";
6
+ export { buildCrudSchemasFromModel, buildCrudSchemasFromMongooseSchema, configureLogger, createError, createFieldPreset, createMemoryCache, filterResponseData, getFieldsForUser, getMongooseProjection, getSchemaIdType, isObjectIdInstance, isValidIdForType };
@@ -1,5 +1,6 @@
1
- import { $ as RepositoryInstance, E as FieldPreset, P as Logger, W as Plugin, Z as RepositoryContext, _t as ValidationChainOptions, f as CascadeOptions, l as CacheOptions, lt as SortSpec, ot as SoftDeleteOptions, q as PopulateSpec, rt as SelectSpec, u as CacheStats, vt as ValidatorDefinition, z as ObjectId } from "./types-DXscZnLu.mjs";
1
+ import { B as ObjectId, C as FieldPreset, G as Plugin, J as PopulateSpec, Q as RepositoryContext, _t as ValidationChainOptions, et as RepositoryInstance, it as SelectSpec, j as Logger, l as CascadeOptions, st as SoftDeleteOptions, ut as SortSpec, vt as ValidatorDefinition } from "./types-BbaPZleN.mjs";
2
2
  import mongoose from "mongoose";
3
+ import { CacheAdapter as CacheAdapter$1, CacheOptions, RepositoryCacheHandle, RepositoryCachePluginOptions, cachePlugin } from "@classytic/repo-core/cache";
3
4
  import { BulkWriteResult } from "@classytic/repo-core/repository";
4
5
  import { AsyncLocalStorage } from "node:async_hooks";
5
6
  import { OffsetPaginationResult } from "@classytic/repo-core/pagination";
@@ -91,12 +92,6 @@ interface AggregateHelpersMethods {
91
92
  }
92
93
  //#endregion
93
94
  //#region src/plugins/audit-log.plugin.d.ts
94
- /**
95
- * Audit log plugin that logs all repository operations
96
- *
97
- * @example
98
- * const repo = new Repository(Model, [auditLogPlugin(console)]);
99
- */
100
95
  declare function auditLogPlugin(logger: Logger): Plugin;
101
96
  //#endregion
102
97
  //#region src/plugins/audit-trail.plugin.d.ts
@@ -121,6 +116,22 @@ interface AuditTrailOptions {
121
116
  * These fields are redacted in the `changes` diff.
122
117
  */
123
118
  excludeFields?: string[];
119
+ /**
120
+ * Custom callback fired when an audit write fails. Receives the error
121
+ * and the entry that failed to land. Default: `console.warn` only.
122
+ *
123
+ * **Use this for compliance-grade audit trails (SOC 2 / PCI / HIPAA)
124
+ * where missing entries must be surfaced upstream** — e.g. forward to
125
+ * a dead-letter queue, page on-call, or short-circuit the request.
126
+ * The default `warn` log is appropriate for development and
127
+ * eventually-consistent observability use cases but is invisible to
128
+ * compliance review.
129
+ *
130
+ * The callback runs on the same fire-and-forget microtask that wrote
131
+ * the entry, so throwing from here will surface as an unhandled
132
+ * rejection — handle (or rethrow into your error reporter) inside.
133
+ */
134
+ onWriteError?: (error: Error, entry: Omit<AuditEntry, 'timestamp'>) => void;
124
135
  }
125
136
  type AuditOperation = 'create' | 'update' | 'delete' | 'findOneAndUpdate';
126
137
  interface AuditEntry {
@@ -150,7 +161,7 @@ interface AuditQueryOptions {
150
161
  limit?: number;
151
162
  }
152
163
  interface AuditQueryResult {
153
- docs: AuditEntry[];
164
+ data: AuditEntry[];
154
165
  page: number;
155
166
  limit: number;
156
167
  total: number;
@@ -286,64 +297,6 @@ interface BatchOperationsMethods {
286
297
  }): Promise<BulkWriteResult>;
287
298
  }
288
299
  //#endregion
289
- //#region src/plugins/cache.plugin.d.ts
290
- /**
291
- * Cache plugin factory
292
- *
293
- * @param options - Cache configuration
294
- * @returns Plugin instance
295
- */
296
- declare function cachePlugin(options: CacheOptions): Plugin;
297
- /**
298
- * TypeScript interface for cache plugin methods
299
- *
300
- * @example
301
- * ```typescript
302
- * import type { CacheMethods } from '@classytic/mongokit';
303
- *
304
- * type ProductRepoWithCache = ProductRepo & CacheMethods;
305
- *
306
- * const productRepo = new ProductRepo(ProductModel, [
307
- * methodRegistryPlugin(),
308
- * cachePlugin({ adapter: redisAdapter, ttl: 60 }),
309
- * ]) as ProductRepoWithCache;
310
- *
311
- * // TypeScript autocomplete for cache methods
312
- * await productRepo.invalidateCache(productId);
313
- * await productRepo.invalidateListCache();
314
- * await productRepo.invalidateAllCache();
315
- * const stats = productRepo.getCacheStats();
316
- * productRepo.resetCacheStats();
317
- * ```
318
- */
319
- interface CacheMethods {
320
- /**
321
- * Invalidate cache for a specific document
322
- * Use when document was updated outside this service
323
- * @param id - Document ID to invalidate
324
- */
325
- invalidateCache(id: string): Promise<void>;
326
- /**
327
- * Invalidate all list caches for this model
328
- * Use when bulk changes happened outside this service
329
- */
330
- invalidateListCache(): Promise<void>;
331
- /**
332
- * Invalidate ALL cache entries for this model
333
- * Nuclear option - use sparingly
334
- */
335
- invalidateAllCache(): Promise<void>;
336
- /**
337
- * Get cache statistics for monitoring
338
- * @returns Cache statistics (hits, misses, sets, invalidations)
339
- */
340
- getCacheStats(): CacheStats;
341
- /**
342
- * Reset cache statistics
343
- */
344
- resetCacheStats(): void;
345
- }
346
- //#endregion
347
300
  //#region src/plugins/cascade.plugin.d.ts
348
301
  /**
349
302
  * Cascade delete plugin.
@@ -535,6 +488,85 @@ declare function elasticSearchPlugin(options: ElasticSearchOptions): Plugin;
535
488
  */
536
489
  declare function fieldFilterPlugin(fieldPreset: FieldPreset): Plugin;
537
490
  //#endregion
491
+ //#region src/plugins/lease.plugin.d.ts
492
+ interface LeasePluginOptions {
493
+ /** Field carrying the row's status. @default `'status'` */
494
+ statusField?: string;
495
+ /** Field stamping the lease holder. @default `'leasedBy'` */
496
+ leasedByField?: string;
497
+ /** Field carrying the lease expiry timestamp. @default `'leaseExpiresAt'` */
498
+ leaseExpiresAtField?: string;
499
+ /** Status value an unleased row carries. @default `'pending'` */
500
+ pendingStatus?: string;
501
+ /** Status value a leased row carries. @default `'processing'` */
502
+ processingStatus?: string;
503
+ /** Status value a successfully released row carries. @default `'done'` */
504
+ doneStatus?: string;
505
+ /** Sort fields used to pick the FIFO winner from the lease pool. @default `{ createdAt: 1 }` */
506
+ sort?: Record<string, 1 | -1>;
507
+ }
508
+ /**
509
+ * Methods contributed by `leasePlugin()`. Use as a type assertion when
510
+ * constructing the repo so call sites get autocomplete:
511
+ *
512
+ * ```ts
513
+ * type OutboxRepo = Repository<IOutboxRow> & LeaseMethods<IOutboxRow>;
514
+ * const repo = new Repository(OutboxModel, [
515
+ * methodRegistryPlugin(),
516
+ * leasePlugin({ pendingStatus: 'queued' }),
517
+ * ]) as OutboxRepo;
518
+ * ```
519
+ */
520
+ interface LeaseMethods<TDoc> {
521
+ /**
522
+ * Atomically claim the next available row. Matches rows that are
523
+ * either in `pendingStatus` OR have an expired `leaseExpiresAt` (dead
524
+ * lease recovery). Sets status to `processingStatus`, stamps
525
+ * `leasedBy`, and pushes `leaseExpiresAt = now + leaseFor`.
526
+ *
527
+ * @returns The leased doc, or `null` when no row matches.
528
+ */
529
+ lease(opts: {
530
+ filter?: Record<string, unknown>;
531
+ leaseFor: number;
532
+ leasedBy: string;
533
+ options?: Record<string, unknown>;
534
+ }): Promise<TDoc | null>;
535
+ /**
536
+ * Push a held lease's expiry further. Only succeeds when the lease
537
+ * is still ours (`leasedBy === leasedBy && leaseExpiresAt > now`).
538
+ * Returns `null` if the lease was lost — caller should drop the
539
+ * work, since someone else has already started replaying it.
540
+ */
541
+ extend(id: string | ObjectId, opts: {
542
+ leasedBy: string;
543
+ leaseFor: number;
544
+ options?: Record<string, unknown>;
545
+ }): Promise<TDoc | null>;
546
+ /**
547
+ * Release a held lease. CAS-checks `leasedBy === opts.leasedBy` AND
548
+ * `leaseExpiresAt > now` so only the live lease holder may finalise
549
+ * the row — symmetrical with `extend()`. A worker whose lease has
550
+ * been recovered (lease lost) MUST NOT mark someone else's
551
+ * in-progress work as `done` / `failed`; this method returns `null`
552
+ * in that race rather than overwriting.
553
+ *
554
+ * Sets status to `opts.finalStatus` (default `doneStatus`), clears
555
+ * `leasedBy` and `leaseExpiresAt`. Use `{ finalStatus: 'failed' }`
556
+ * for error paths so terminal-status filters (next caller's
557
+ * `lease()` predicate) skip this row.
558
+ *
559
+ * Returns the released doc, or `null` when the CAS lost (lease
560
+ * expired / held by another worker / row gone).
561
+ */
562
+ release(id: string | ObjectId, opts: {
563
+ leasedBy: string;
564
+ finalStatus?: string;
565
+ options?: Record<string, unknown>;
566
+ }): Promise<TDoc | null>;
567
+ }
568
+ declare function leasePlugin(options?: LeasePluginOptions): Plugin;
569
+ //#endregion
538
570
  //#region src/plugins/method-registry.plugin.d.ts
539
571
  /**
540
572
  * Extended repository interface with method registry
@@ -752,6 +784,33 @@ interface MongoOperationsMethods<TDoc> {
752
784
  * });
753
785
  */
754
786
  atomicUpdate(id: string | ObjectId, operators: Record<string, Record<string, unknown>>, options?: Record<string, unknown>): Promise<TDoc>;
787
+ /**
788
+ * Atomic conditional increment — increments the field only when it is
789
+ * strictly below `ceiling`. Returns the updated document on success,
790
+ * `null` when already at/above the ceiling.
791
+ *
792
+ * Single-round-trip CAS via `findOneAndUpdate({ _id, field: { $lt:
793
+ * ceiling }}, { $inc: { field: by }})`. Use for per-document
794
+ * concurrency limits, quotas, and capacity caps where a read-then-
795
+ * write loop would race.
796
+ *
797
+ * @param id - Document ID
798
+ * @param field - Numeric field to increment
799
+ * @param ceiling - Increment only when the field is `< ceiling`
800
+ * @param by - Increment delta (default `1`; pass negative to decrement-with-floor by inverting the predicate via the call site)
801
+ * @param options - Operation options (session, etc.)
802
+ * @returns The updated document, or `null` when at/above ceiling
803
+ *
804
+ * @example Per-tenant concurrency limit
805
+ * ```ts
806
+ * const claimed = await repo.incrementIfBelow(tenantId, 'activeRuns', 10);
807
+ * if (!claimed) throw new ConcurrencyLimitExceeded();
808
+ * try { await runWorkflow(); } finally {
809
+ * await repo.decrement(tenantId, 'activeRuns');
810
+ * }
811
+ * ```
812
+ */
813
+ incrementIfBelow(id: string | ObjectId, field: string, ceiling: number, by?: number, options?: Record<string, unknown>): Promise<TDoc | null>;
755
814
  }
756
815
  //#endregion
757
816
  //#region src/plugins/multi-tenant.plugin.d.ts
@@ -794,28 +853,101 @@ interface MultiTenantOptions extends Pick<TenantConfig, 'tenantField' | 'context
794
853
  */
795
854
  resolveContext?: () => string | undefined;
796
855
  /**
797
- * When `true` (default), bypass the `required` throw if the operation's
798
- * policy target already carries the tenant field. This lets hosts that
799
- * stamp the tenant into the payload themselves (e.g., `data[tenantField]`
856
+ * When `true`, bypass the `required` throw if the operation's policy
857
+ * target already carries the tenant field. This lets hosts that stamp
858
+ * the tenant into the payload themselves (e.g., `data[tenantField]`
800
859
  * on writes, `filters[tenantField]` on paginated reads) use the plugin
801
860
  * without having to hand-roll a `skipWhen` that duplicates this check.
802
861
  *
803
- * Set to `false` to restore the pre-data-injection strict behavior: the
804
- * tenant MUST come from `context` or `resolveContext`, otherwise the
805
- * plugin throws (when `required: true`).
862
+ * **Default: `false` (secure / fail-closed).** Tenant scope MUST come
863
+ * from `context` or `resolveContext`, otherwise the plugin throws
864
+ * (when `required: true`). This is the right posture for a security-
865
+ * sensitive boundary: caller-supplied scope on the payload cannot be
866
+ * trusted as authentication. If a host control-plane has already
867
+ * authenticated the tenant out-of-band and stamps it onto every
868
+ * write/read, set this to `true` explicitly — making the trust model
869
+ * visible at the call site.
806
870
  *
807
- * Composition: a user-supplied `skipWhen` still runs first and can always
808
- * opt out. This option controls the behavior after `skipWhen` and
809
- * `resolveContext` have had their turn.
871
+ * **Migration from < 4.0:** the prior default was `true`. To preserve
872
+ * the old behavior verbatim, set `allowDataInjection: true`. To get
873
+ * the new secure default, leave it unset.
810
874
  *
811
- * Safety: when the payload supplies the tenant, the plugin skips its own
812
- * injection (it does not overwrite). Cross-tenant isolation is therefore
813
- * only as strong as the caller's own stamping — same guarantee as a
814
- * hand-rolled `skipWhen`.
875
+ * Composition: a user-supplied `skipWhen` still runs first and can
876
+ * always opt out. This option controls the behavior after `skipWhen`
877
+ * and `resolveContext` have had their turn.
878
+ *
879
+ * Safety: when the payload supplies the tenant AND this is `true`,
880
+ * the plugin skips its own injection (it does not overwrite). Cross-
881
+ * tenant isolation is therefore only as strong as the caller's own
882
+ * stamping — same guarantee as a hand-rolled `skipWhen`.
815
883
  */
816
884
  allowDataInjection?: boolean;
817
885
  }
818
886
  declare function multiTenantPlugin(options?: MultiTenantOptions): Plugin;
887
+ /**
888
+ * Build the canonical `skipWhen` callback for role-based tenant
889
+ * bypass — replaces the hand-rolled
890
+ * `(ctx) => ctx.role === 'superadmin'` callback that ~6 packages
891
+ * across the classytic codebase carry as boilerplate. Eliminates the
892
+ * "I forgot to include 'platform_admin'" drift class — the role
893
+ * vocabulary lives in one place.
894
+ *
895
+ * **Composes with `bypassTenant: true` per-call**, doesn't replace
896
+ * it. `bypassTenant` is the deliberate per-call form for migration
897
+ * scripts and one-off admin queries; `adminBypass` is the
898
+ * always-on role-based bypass for users whose JWT / session carries
899
+ * a privileged role.
900
+ *
901
+ * **NOT a substitute for authentication or RBAC.** This factory only
902
+ * answers "should the tenant scope hook fire for this call?" — it
903
+ * does NOT validate that the caller is authenticated, that their
904
+ * role claim is genuine, or that the requested operation is
905
+ * permitted. Always run auth + RBAC before the call reaches the
906
+ * repository layer; this is the LAST gate, not the only one.
907
+ *
908
+ * @example Single role
909
+ * ```ts
910
+ * import { adminBypass, multiTenantPlugin } from '@classytic/mongokit';
911
+ *
912
+ * const repo = new Repository(InvoiceModel, [
913
+ * multiTenantPlugin({
914
+ * tenantField: 'organizationId',
915
+ * skipWhen: adminBypass({ adminRoles: ['superadmin'] }),
916
+ * }),
917
+ * ]);
918
+ *
919
+ * await repo.getAll({}, { role: 'superadmin' }); // sees all orgs
920
+ * await repo.getAll({}, { role: 'user', organizationId: 'org-1' }); // scoped
921
+ * ```
922
+ *
923
+ * @example Multiple admin roles + custom field name
924
+ * ```ts
925
+ * skipWhen: adminBypass({
926
+ * roleField: 'principalRole', // ctx.principalRole, not ctx.role
927
+ * adminRoles: ['superadmin', 'platform_admin', 'support'],
928
+ * }),
929
+ * ```
930
+ *
931
+ * @example Composing with custom logic
932
+ * ```ts
933
+ * // adminBypass for the role check, then your own additional logic:
934
+ * const adminCheck = adminBypass({ adminRoles: ['superadmin'] });
935
+ * skipWhen: (ctx, op) => adminCheck(ctx, op) || ctx.internalSystemFlag === true,
936
+ * ```
937
+ *
938
+ * @param options.roleField - Path on the context to read the role
939
+ * from. Defaults to `'role'`. Top-level keys only — for nested
940
+ * shapes (e.g. `ctx.user.role`), wrap your own `(ctx) => …` callback
941
+ * instead.
942
+ * @param options.adminRoles - Role values that grant the bypass. The
943
+ * factory does an exact-match `includes` check — case-sensitive,
944
+ * no fuzzy matching. Lowercase your role vocabulary upstream.
945
+ * @returns A `skipWhen`-compatible callback `(ctx, op) → boolean`.
946
+ */
947
+ declare function adminBypass(options: {
948
+ roleField?: string;
949
+ adminRoles: readonly string[];
950
+ }): (context: RepositoryContext, operation: string) => boolean;
819
951
  //#endregion
820
952
  //#region src/plugins/observability.plugin.d.ts
821
953
  interface OperationMetric {
@@ -1088,4 +1220,4 @@ declare function immutableField(field: string): ValidatorDefinition;
1088
1220
  */
1089
1221
  declare function uniqueField(field: string, errorMessage?: string): ValidatorDefinition;
1090
1222
  //#endregion
1091
- export { aggregateHelpersPlugin as $, PrefixedIdOptions as A, BatchOperationsMethods as B, methodRegistryPlugin as C, CustomIdOptions as D, elasticSearchPlugin as E, prefixedId as F, AuditQueryOptions as G, batchOperationsPlugin as H, sequentialId as I, AuditTrailOptions as J, AuditQueryResult as K, cascadePlugin as L, customIdPlugin as M, dateSequentialId as N, DateSequentialIdOptions as O, getNextSequence as P, AggregateHelpersMethods as Q, CacheMethods as R, MethodRegistryRepository as S, ElasticSearchOptions as T, AuditEntry as U, BulkWriteResult as V, AuditOperation as W, auditTrailPlugin as X, AuditTrailQuery as Y, auditLogPlugin as Z, observabilityPlugin as _, uniqueField as a, MongoOperationsMethods as b, TenantContext as c, SubdocumentMethods as d, subdocumentPlugin as f, OperationMetric as g, ObservabilityOptions as h, requireField as i, SequentialIdOptions as j, IdGenerator as k, TenantStore as l, softDeletePlugin as m, blockIf as n, validationChainPlugin as o, SoftDeleteMethods as p, AuditTrailMethods as q, immutableField as r, timestampPlugin as s, autoInject as t, createTenantContext as u, MultiTenantOptions as v, fieldFilterPlugin as w, mongoOperationsPlugin as x, multiTenantPlugin as y, cachePlugin as z };
1223
+ export { AuditQueryResult as $, elasticSearchPlugin as A, sequentialId as B, MethodRegistryRepository as C, leasePlugin as D, LeasePluginOptions as E, SequentialIdOptions as F, RepositoryCachePluginOptions as G, CacheAdapter$1 as H, customIdPlugin as I, BulkWriteResult as J, cachePlugin as K, dateSequentialId as L, DateSequentialIdOptions as M, IdGenerator as N, fieldFilterPlugin as O, PrefixedIdOptions as P, AuditQueryOptions as Q, getNextSequence as R, mongoOperationsPlugin as S, LeaseMethods as T, CacheOptions as U, cascadePlugin as V, RepositoryCacheHandle as W, AuditEntry as X, batchOperationsPlugin as Y, AuditOperation as Z, observabilityPlugin as _, uniqueField as a, AggregateHelpersMethods as at, multiTenantPlugin as b, TenantContext as c, SubdocumentMethods as d, AuditTrailMethods as et, subdocumentPlugin as f, OperationMetric as g, ObservabilityOptions as h, requireField as i, auditLogPlugin as it, CustomIdOptions as j, ElasticSearchOptions as k, TenantStore as l, softDeletePlugin as m, blockIf as n, AuditTrailQuery as nt, validationChainPlugin as o, aggregateHelpersPlugin as ot, SoftDeleteMethods as p, BatchOperationsMethods as q, immutableField as r, auditTrailPlugin as rt, timestampPlugin as s, autoInject as t, AuditTrailOptions as tt, createTenantContext as u, MultiTenantOptions as v, methodRegistryPlugin as w, MongoOperationsMethods as x, adminBypass as y, prefixedId as z };