@classytic/mongokit 3.15.0 → 3.16.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 (28) hide show
  1. package/README.md +193 -319
  2. package/dist/{PaginationEngine-CfQRM1yz.mjs → PaginationEngine-0XXTW-9b.mjs} +2 -2
  3. package/dist/{Repository-BGL1Pq8w.mjs → Repository-DE7D2QeS.mjs} +240 -19
  4. package/dist/actions/index.d.mts +1 -1
  5. package/dist/ai/index.d.mts +1 -1
  6. package/dist/ai/index.mjs +14 -5
  7. package/dist/better-auth/index.d.mts +15 -0
  8. package/dist/better-auth/index.mjs +1 -1
  9. package/dist/{index-Dccha9dw.d.mts → index-CV5vv-8E.d.mts} +1 -1
  10. package/dist/index.d.mts +1182 -1079
  11. package/dist/index.mjs +32 -19
  12. package/dist/{mongooseToJsonSchema-BYbG894y.mjs → mongooseToJsonSchema-Bi0q0pYW.mjs} +1 -1
  13. package/dist/{mongooseToJsonSchema-BgRAaXX-.d.mts → mongooseToJsonSchema-GhvZUjRO.d.mts} +1 -1
  14. package/dist/pagination/PaginationEngine.d.mts +1 -1
  15. package/dist/pagination/PaginationEngine.mjs +1 -1
  16. package/dist/plugins/index.d.mts +1 -1
  17. package/dist/plugins/index.mjs +1 -1
  18. package/dist/query/primitives/coercion.mjs +1 -1
  19. package/dist/{types-BbaPZleN.d.mts → types-BWZILovv.d.mts} +26 -1
  20. package/dist/utils/index.d.mts +1 -1
  21. package/dist/utils/index.mjs +3 -3
  22. package/dist/{validation-chain.plugin-CWhvBRej.mjs → validation-chain.plugin-Crl5nQAY.mjs} +1172 -1137
  23. package/dist/{validation-chain.plugin-CAPYCj_T.d.mts → validation-chain.plugin-Iee2_puD.d.mts} +368 -344
  24. package/package.json +6 -6
  25. /package/dist/{cursor-J84rR6wG.mjs → cursor-4LMYa4ga.mjs} +0 -0
  26. /package/dist/{field-selection-Br3WPOEb.mjs → field-selection-CQW3WBiK.mjs} +0 -0
  27. /package/dist/{id-resolution-B0Odvvou.mjs → id-resolution-DduoO4LI.mjs} +0 -0
  28. /package/dist/{sort-CrZjTK25.mjs → sort-RPVz5u0w.mjs} +0 -0
@@ -1,4 +1,4 @@
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";
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-BWZILovv.mjs";
2
2
  import mongoose from "mongoose";
3
3
  import { CacheAdapter as CacheAdapter$1, CacheOptions, RepositoryCacheHandle, RepositoryCachePluginOptions, cachePlugin } from "@classytic/repo-core/cache";
4
4
  import { BulkWriteResult } from "@classytic/repo-core/repository";
@@ -6,94 +6,6 @@ import { AsyncLocalStorage } from "node:async_hooks";
6
6
  import { OffsetPaginationResult } from "@classytic/repo-core/pagination";
7
7
  import { TenantConfig } from "@classytic/repo-core/tenant";
8
8
 
9
- //#region src/plugins/aggregate-helpers.plugin.d.ts
10
- /**
11
- * Aggregate helpers plugin
12
- *
13
- * @example
14
- * const repo = new Repository(Model, [
15
- * methodRegistryPlugin(),
16
- * aggregateHelpersPlugin(),
17
- * ]);
18
- *
19
- * const groups = await repo.groupBy('category');
20
- * const total = await repo.sum('amount', { status: 'completed' });
21
- */
22
- declare function aggregateHelpersPlugin(): Plugin;
23
- /**
24
- * Type interface for repositories using aggregateHelpersPlugin
25
- *
26
- * @example
27
- * ```typescript
28
- * import { Repository, methodRegistryPlugin, aggregateHelpersPlugin } from '@classytic/mongokit';
29
- * import type { AggregateHelpersMethods } from '@classytic/mongokit';
30
- *
31
- * class OrderRepo extends Repository<IOrder> {}
32
- *
33
- * type OrderRepoWithAggregates = OrderRepo & AggregateHelpersMethods;
34
- *
35
- * const repo = new OrderRepo(OrderModel, [
36
- * methodRegistryPlugin(),
37
- * aggregateHelpersPlugin(),
38
- * ]) as OrderRepoWithAggregates;
39
- *
40
- * // TypeScript autocomplete works!
41
- * const groups = await repo.groupBy('status');
42
- * const total = await repo.sum('amount', { status: 'completed' });
43
- * const avg = await repo.average('amount');
44
- * ```
45
- */
46
- interface AggregateHelpersMethods {
47
- /**
48
- * Group documents by field value and count occurrences
49
- * @param field - Field to group by
50
- * @param options - Operation options
51
- * @returns Array of groups with _id and count
52
- */
53
- groupBy(field: string, options?: {
54
- limit?: number;
55
- session?: unknown;
56
- }): Promise<Array<{
57
- _id: unknown;
58
- count: number;
59
- }>>;
60
- /**
61
- * Calculate sum of field values
62
- * @param field - Field to sum
63
- * @param query - Filter query
64
- * @param options - Operation options
65
- * @returns Sum of field values
66
- */
67
- sum(field: string, query?: Record<string, unknown>, options?: Record<string, unknown>): Promise<number>;
68
- /**
69
- * Calculate average of field values
70
- * @param field - Field to average
71
- * @param query - Filter query
72
- * @param options - Operation options
73
- * @returns Average of field values
74
- */
75
- average(field: string, query?: Record<string, unknown>, options?: Record<string, unknown>): Promise<number>;
76
- /**
77
- * Get minimum field value
78
- * @param field - Field to get minimum from
79
- * @param query - Filter query
80
- * @param options - Operation options
81
- * @returns Minimum field value
82
- */
83
- min(field: string, query?: Record<string, unknown>, options?: Record<string, unknown>): Promise<number>;
84
- /**
85
- * Get maximum field value
86
- * @param field - Field to get maximum from
87
- * @param query - Filter query
88
- * @param options - Operation options
89
- * @returns Maximum field value
90
- */
91
- max(field: string, query?: Record<string, unknown>, options?: Record<string, unknown>): Promise<number>;
92
- }
93
- //#endregion
94
- //#region src/plugins/audit-log.plugin.d.ts
95
- declare function auditLogPlugin(logger: Logger): Plugin;
96
- //#endregion
97
9
  //#region src/plugins/audit-trail.plugin.d.ts
98
10
  interface AuditTrailOptions {
99
11
  /** Operations to track (default: ['create', 'update', 'delete']) */
@@ -256,53 +168,6 @@ interface AuditTrailMethods {
256
168
  }): Promise<AuditQueryResult>;
257
169
  }
258
170
  //#endregion
259
- //#region src/plugins/batch-operations.plugin.d.ts
260
- /**
261
- * Batch operations plugin.
262
- *
263
- * @example
264
- * const repo = new Repository(Model, [
265
- * methodRegistryPlugin(),
266
- * batchOperationsPlugin(),
267
- * ]);
268
- *
269
- * // updateMany + deleteMany are primitives on Repository — no plugin needed.
270
- * await repo.updateMany({ status: 'pending' }, { status: 'active' });
271
- * await repo.deleteMany({ status: 'deleted' });
272
- *
273
- * // bulkWrite is what this plugin contributes.
274
- * await repo.bulkWrite([...ops]);
275
- */
276
- declare function batchOperationsPlugin(): Plugin;
277
- interface BatchOperationsMethods {
278
- /**
279
- * Execute heterogeneous bulk write operations in a single database call.
280
- * Supports insertOne, updateOne, updateMany, deleteOne, deleteMany, replaceOne.
281
- *
282
- * @param operations - Array of bulk write operations
283
- * @param options - Options (session, ordered)
284
- * @returns Bulk write result with counts per operation type
285
- *
286
- * @example
287
- * const result = await repo.bulkWrite([
288
- * { insertOne: { document: { name: 'Item', price: 10 } } },
289
- * { updateOne: { filter: { _id: id }, update: { $inc: { views: 1 } } } },
290
- * { deleteOne: { filter: { _id: oldId } } },
291
- * ]);
292
- * console.log(result.insertedCount, result.modifiedCount, result.deletedCount);
293
- */
294
- bulkWrite(operations: Record<string, unknown>[], options?: {
295
- session?: unknown;
296
- ordered?: boolean;
297
- }): Promise<BulkWriteResult>;
298
- }
299
- //#endregion
300
- //#region src/plugins/cascade.plugin.d.ts
301
- /**
302
- * Cascade delete plugin.
303
- */
304
- declare function cascadePlugin(options: CascadeOptions): Plugin;
305
- //#endregion
306
171
  //#region src/plugins/custom-id.plugin.d.ts
307
172
  /**
308
173
  * Generator function that produces a unique ID.
@@ -380,87 +245,382 @@ interface DateSequentialIdOptions {
380
245
  /** Mongoose model — used to derive the counter key */
381
246
  model: mongoose.Model<any>;
382
247
  /**
383
- * Partition granularity — counter resets each period.
384
- * - 'yearly' → BILL-2026-0001, resets every January
385
- * - 'monthly' → BILL-2026-02-0001, resets every month
386
- * - 'daily' → BILL-2026-02-20-0001, resets every day
248
+ * Partition granularity — counter resets each period.
249
+ * - 'yearly' → BILL-2026-0001, resets every January
250
+ * - 'monthly' → BILL-2026-02-0001, resets every month
251
+ * - 'daily' → BILL-2026-02-20-0001, resets every day
252
+ */
253
+ partition?: 'yearly' | 'monthly' | 'daily';
254
+ /** Number of digits to pad to (default: 4) */
255
+ padding?: number;
256
+ /** Separator (default: '-') */
257
+ separator?: string;
258
+ }
259
+ /**
260
+ * Generator: Date-partitioned sequential counter.
261
+ * Counter resets per period — great for invoice/bill numbering.
262
+ *
263
+ * Produces IDs like:
264
+ * - yearly: `BILL-2026-0001`
265
+ * - monthly: `BILL-2026-02-0001`
266
+ * - daily: `BILL-2026-02-20-0001`
267
+ *
268
+ * @example
269
+ * ```typescript
270
+ * customIdPlugin({
271
+ * field: 'billNumber',
272
+ * generator: dateSequentialId({
273
+ * prefix: 'BILL',
274
+ * model: BillModel,
275
+ * partition: 'monthly',
276
+ * }),
277
+ * })
278
+ * ```
279
+ */
280
+ declare function dateSequentialId(options: DateSequentialIdOptions): IdGenerator;
281
+ interface PrefixedIdOptions {
282
+ /** Prefix string (e.g., 'USR', 'TXN') */
283
+ prefix: string;
284
+ /** Separator (default: '_') */
285
+ separator?: string;
286
+ /** Length of the random suffix (default: 12) */
287
+ length?: number;
288
+ }
289
+ /**
290
+ * Generator: Prefix + random alphanumeric suffix.
291
+ * Does NOT require a database round-trip — purely in-memory.
292
+ *
293
+ * Produces IDs like: `USR_a7b3xk9m2p1q`
294
+ *
295
+ * Good for: user-facing IDs where ordering doesn't matter.
296
+ * Not suitable for sequential numbering.
297
+ *
298
+ * @example
299
+ * ```typescript
300
+ * customIdPlugin({
301
+ * field: 'publicId',
302
+ * generator: prefixedId({ prefix: 'USR', length: 10 }),
303
+ * })
304
+ * ```
305
+ */
306
+ declare function prefixedId(options: PrefixedIdOptions): IdGenerator;
307
+ /**
308
+ * Custom ID plugin — injects generated IDs into documents before creation.
309
+ *
310
+ * @param options - Configuration for ID generation
311
+ * @returns Plugin instance
312
+ *
313
+ * @example
314
+ * ```typescript
315
+ * import { Repository, customIdPlugin, sequentialId } from '@classytic/mongokit';
316
+ *
317
+ * const invoiceRepo = new Repository(InvoiceModel, [
318
+ * customIdPlugin({
319
+ * field: 'invoiceNumber',
320
+ * generator: sequentialId({ prefix: 'INV', model: InvoiceModel }),
321
+ * }),
322
+ * ]);
323
+ *
324
+ * const inv = await invoiceRepo.create({ amount: 100 });
325
+ * console.log(inv.invoiceNumber); // "INV-0001"
326
+ * ```
327
+ */
328
+ declare function customIdPlugin(options: CustomIdOptions): Plugin;
329
+ //#endregion
330
+ //#region src/plugins/multi-tenant.plugin.d.ts
331
+ /**
332
+ * Static tenant fields are picked from `@classytic/repo-core/tenant` so the
333
+ * vocabulary (`tenantField`, `contextKey`, `required`, `fieldType`) stays
334
+ * locked to the org-wide canonical source. Mongokit-specific runtime
335
+ * callbacks (`skipWhen`, `resolveContext`, `skipOperations`,
336
+ * `allowDataInjection`) extend on top — those genuinely differ from
337
+ * sqlitekit's runtime shape because mongokit's `RepositoryContext` carries
338
+ * Mongoose-specific bits the resolver may inspect.
339
+ *
340
+ * Mongokit defaults `fieldType` to `'string'` (not repo-core's `'objectId'`)
341
+ * for back-compat — pre-3.x stored tenant ids as raw strings; flipping the
342
+ * default would silently change cast behavior for existing apps.
343
+ */
344
+ interface MultiTenantOptions extends Pick<TenantConfig, 'tenantField' | 'contextKey' | 'required' | 'fieldType'> {
345
+ /** Operations to skip tenant injection (e.g., for admin/system queries) */
346
+ skipOperations?: string[];
347
+ /**
348
+ * Dynamic skip — receives the context and operation name, returns true to
349
+ * bypass tenant scoping for this call. Use for role-based bypass (e.g.,
350
+ * super admin) without needing a separate repo instance.
351
+ *
352
+ * @example
353
+ * ```typescript
354
+ * skipWhen: (context) => context.role === 'superadmin'
355
+ * ```
356
+ */
357
+ skipWhen?: (context: RepositoryContext, operation: string) => boolean;
358
+ /**
359
+ * Resolve tenant ID from external source (e.g., AsyncLocalStorage, CLS).
360
+ * Called when tenant ID is not found in context. If it returns a value,
361
+ * that value is used as the tenant ID without requiring it in context.
362
+ *
363
+ * @example
364
+ * ```typescript
365
+ * resolveContext: () => asyncLocalStorage.getStore()?.tenantId
366
+ * ```
367
+ */
368
+ resolveContext?: () => string | undefined;
369
+ /**
370
+ * When `true`, bypass the `required` throw if the operation's policy
371
+ * target already carries the tenant field. This lets hosts that stamp
372
+ * the tenant into the payload themselves (e.g., `data[tenantField]`
373
+ * on writes, `filters[tenantField]` on paginated reads) use the plugin
374
+ * without having to hand-roll a `skipWhen` that duplicates this check.
375
+ *
376
+ * **Default: `false` (secure / fail-closed).** Tenant scope MUST come
377
+ * from `context` or `resolveContext`, otherwise the plugin throws
378
+ * (when `required: true`). This is the right posture for a security-
379
+ * sensitive boundary: caller-supplied scope on the payload cannot be
380
+ * trusted as authentication. If a host control-plane has already
381
+ * authenticated the tenant out-of-band and stamps it onto every
382
+ * write/read, set this to `true` explicitly — making the trust model
383
+ * visible at the call site.
384
+ *
385
+ * **Migration from < 4.0:** the prior default was `true`. To preserve
386
+ * the old behavior verbatim, set `allowDataInjection: true`. To get
387
+ * the new secure default, leave it unset.
388
+ *
389
+ * Composition: a user-supplied `skipWhen` still runs first and can
390
+ * always opt out. This option controls the behavior after `skipWhen`
391
+ * and `resolveContext` have had their turn.
392
+ *
393
+ * Safety: when the payload supplies the tenant AND this is `true`,
394
+ * the plugin skips its own injection (it does not overwrite). Cross-
395
+ * tenant isolation is therefore only as strong as the caller's own
396
+ * stamping — same guarantee as a hand-rolled `skipWhen`.
397
+ */
398
+ allowDataInjection?: boolean;
399
+ /**
400
+ * What to do when the call's filter/payload ALREADY carries
401
+ * `tenantField` with a value that does NOT match the resolved tenant
402
+ * scope (a caller-supplied cross-tenant value — fat-fingered org id,
403
+ * stale client state, or an injection attempt).
404
+ *
405
+ * - `'throw'` (default, fail-closed): reject the call with a clear
406
+ * error. A mismatch is never legitimate on a scoped call — callers
407
+ * who genuinely need cross-tenant access use `bypassTenant: true`
408
+ * (per-call) or `skipWhen` (plugin-level), both of which emit the
409
+ * `after:tenant-bypass` audit event. Operator-shaped values
410
+ * (`{ $in: [...] }`) on the tenant field can't be verified against
411
+ * the scope and are rejected too — multi-value tenant scoping is
412
+ * deliberately not supported (see CLAUDE.md); use an explicit
413
+ * bypass + manual filter.
414
+ * - `'overwrite'` (legacy, pre-3.16 behavior): silently replace the
415
+ * supplied value with the resolved scope. Still leak-safe (the
416
+ * resolved scope always wins) but hides caller bugs.
417
+ *
418
+ * Matching values (same tenant, any representation — string vs
419
+ * ObjectId) are never a mismatch; the plugin normalizes them to the
420
+ * configured `fieldType`.
421
+ */
422
+ onMismatch?: 'throw' | 'overwrite';
423
+ }
424
+ declare function multiTenantPlugin(options?: MultiTenantOptions): Plugin;
425
+ /**
426
+ * Build the canonical `skipWhen` callback for role-based tenant
427
+ * bypass — replaces the hand-rolled
428
+ * `(ctx) => ctx.role === 'superadmin'` callback that ~6 packages
429
+ * across the classytic codebase carry as boilerplate. Eliminates the
430
+ * "I forgot to include 'platform_admin'" drift class — the role
431
+ * vocabulary lives in one place.
432
+ *
433
+ * **Composes with `bypassTenant: true` per-call**, doesn't replace
434
+ * it. `bypassTenant` is the deliberate per-call form for migration
435
+ * scripts and one-off admin queries; `adminBypass` is the
436
+ * always-on role-based bypass for users whose JWT / session carries
437
+ * a privileged role.
438
+ *
439
+ * **NOT a substitute for authentication or RBAC.** This factory only
440
+ * answers "should the tenant scope hook fire for this call?" — it
441
+ * does NOT validate that the caller is authenticated, that their
442
+ * role claim is genuine, or that the requested operation is
443
+ * permitted. Always run auth + RBAC before the call reaches the
444
+ * repository layer; this is the LAST gate, not the only one.
445
+ *
446
+ * @example Single role
447
+ * ```ts
448
+ * import { adminBypass, multiTenantPlugin } from '@classytic/mongokit';
449
+ *
450
+ * const repo = new Repository(InvoiceModel, [
451
+ * multiTenantPlugin({
452
+ * tenantField: 'organizationId',
453
+ * skipWhen: adminBypass({ adminRoles: ['superadmin'] }),
454
+ * }),
455
+ * ]);
456
+ *
457
+ * await repo.getAll({}, { role: 'superadmin' }); // sees all orgs
458
+ * await repo.getAll({}, { role: 'user', organizationId: 'org-1' }); // scoped
459
+ * ```
460
+ *
461
+ * @example Multiple admin roles + custom field name
462
+ * ```ts
463
+ * skipWhen: adminBypass({
464
+ * roleField: 'principalRole', // ctx.principalRole, not ctx.role
465
+ * adminRoles: ['superadmin', 'platform_admin', 'support'],
466
+ * }),
467
+ * ```
468
+ *
469
+ * @example Composing with custom logic
470
+ * ```ts
471
+ * // adminBypass for the role check, then your own additional logic:
472
+ * const adminCheck = adminBypass({ adminRoles: ['superadmin'] });
473
+ * skipWhen: (ctx, op) => adminCheck(ctx, op) || ctx.internalSystemFlag === true,
474
+ * ```
475
+ *
476
+ * @param options.roleField - Path on the context to read the role
477
+ * from. Defaults to `'role'`. Top-level keys only — for nested
478
+ * shapes (e.g. `ctx.user.role`), wrap your own `(ctx) => …` callback
479
+ * instead.
480
+ * @param options.adminRoles - Role values that grant the bypass. The
481
+ * factory does an exact-match `includes` check — case-sensitive,
482
+ * no fuzzy matching. Lowercase your role vocabulary upstream.
483
+ * @returns A `skipWhen`-compatible callback `(ctx, op) → boolean`.
484
+ */
485
+ declare function adminBypass(options: {
486
+ roleField?: string;
487
+ adminRoles: readonly string[];
488
+ }): (context: RepositoryContext, operation: string) => boolean;
489
+ //#endregion
490
+ //#region src/plugins/aggregate-helpers.plugin.d.ts
491
+ /**
492
+ * Aggregate helpers plugin
493
+ *
494
+ * @example
495
+ * const repo = new Repository(Model, [
496
+ * methodRegistryPlugin(),
497
+ * aggregateHelpersPlugin(),
498
+ * ]);
499
+ *
500
+ * const groups = await repo.groupBy('category');
501
+ * const total = await repo.sum('amount', { status: 'completed' });
502
+ */
503
+ declare function aggregateHelpersPlugin(): Plugin;
504
+ /**
505
+ * Type interface for repositories using aggregateHelpersPlugin
506
+ *
507
+ * @example
508
+ * ```typescript
509
+ * import { Repository, methodRegistryPlugin, aggregateHelpersPlugin } from '@classytic/mongokit';
510
+ * import type { AggregateHelpersMethods } from '@classytic/mongokit';
511
+ *
512
+ * class OrderRepo extends Repository<IOrder> {}
513
+ *
514
+ * type OrderRepoWithAggregates = OrderRepo & AggregateHelpersMethods;
515
+ *
516
+ * const repo = new OrderRepo(OrderModel, [
517
+ * methodRegistryPlugin(),
518
+ * aggregateHelpersPlugin(),
519
+ * ]) as OrderRepoWithAggregates;
520
+ *
521
+ * // TypeScript autocomplete works!
522
+ * const groups = await repo.groupBy('status');
523
+ * const total = await repo.sum('amount', { status: 'completed' });
524
+ * const avg = await repo.average('amount');
525
+ * ```
526
+ */
527
+ interface AggregateHelpersMethods {
528
+ /**
529
+ * Group documents by field value and count occurrences
530
+ * @param field - Field to group by
531
+ * @param options - Operation options
532
+ * @returns Array of groups with _id and count
533
+ */
534
+ groupBy(field: string, options?: {
535
+ limit?: number;
536
+ session?: unknown;
537
+ }): Promise<Array<{
538
+ _id: unknown;
539
+ count: number;
540
+ }>>;
541
+ /**
542
+ * Calculate sum of field values
543
+ * @param field - Field to sum
544
+ * @param query - Filter query
545
+ * @param options - Operation options
546
+ * @returns Sum of field values
547
+ */
548
+ sum(field: string, query?: Record<string, unknown>, options?: Record<string, unknown>): Promise<number>;
549
+ /**
550
+ * Calculate average of field values
551
+ * @param field - Field to average
552
+ * @param query - Filter query
553
+ * @param options - Operation options
554
+ * @returns Average of field values
555
+ */
556
+ average(field: string, query?: Record<string, unknown>, options?: Record<string, unknown>): Promise<number>;
557
+ /**
558
+ * Get minimum field value
559
+ * @param field - Field to get minimum from
560
+ * @param query - Filter query
561
+ * @param options - Operation options
562
+ * @returns Minimum field value
563
+ */
564
+ min(field: string, query?: Record<string, unknown>, options?: Record<string, unknown>): Promise<number>;
565
+ /**
566
+ * Get maximum field value
567
+ * @param field - Field to get maximum from
568
+ * @param query - Filter query
569
+ * @param options - Operation options
570
+ * @returns Maximum field value
387
571
  */
388
- partition?: 'yearly' | 'monthly' | 'daily';
389
- /** Number of digits to pad to (default: 4) */
390
- padding?: number;
391
- /** Separator (default: '-') */
392
- separator?: string;
572
+ max(field: string, query?: Record<string, unknown>, options?: Record<string, unknown>): Promise<number>;
393
573
  }
574
+ //#endregion
575
+ //#region src/plugins/audit-log.plugin.d.ts
576
+ declare function auditLogPlugin(logger: Logger): Plugin;
577
+ //#endregion
578
+ //#region src/plugins/batch-operations.plugin.d.ts
394
579
  /**
395
- * Generator: Date-partitioned sequential counter.
396
- * Counter resets per period — great for invoice/bill numbering.
397
- *
398
- * Produces IDs like:
399
- * - yearly: `BILL-2026-0001`
400
- * - monthly: `BILL-2026-02-0001`
401
- * - daily: `BILL-2026-02-20-0001`
580
+ * Batch operations plugin.
402
581
  *
403
582
  * @example
404
- * ```typescript
405
- * customIdPlugin({
406
- * field: 'billNumber',
407
- * generator: dateSequentialId({
408
- * prefix: 'BILL',
409
- * model: BillModel,
410
- * partition: 'monthly',
411
- * }),
412
- * })
413
- * ```
414
- */
415
- declare function dateSequentialId(options: DateSequentialIdOptions): IdGenerator;
416
- interface PrefixedIdOptions {
417
- /** Prefix string (e.g., 'USR', 'TXN') */
418
- prefix: string;
419
- /** Separator (default: '_') */
420
- separator?: string;
421
- /** Length of the random suffix (default: 12) */
422
- length?: number;
423
- }
424
- /**
425
- * Generator: Prefix + random alphanumeric suffix.
426
- * Does NOT require a database round-trip — purely in-memory.
427
- *
428
- * Produces IDs like: `USR_a7b3xk9m2p1q`
583
+ * const repo = new Repository(Model, [
584
+ * methodRegistryPlugin(),
585
+ * batchOperationsPlugin(),
586
+ * ]);
429
587
  *
430
- * Good for: user-facing IDs where ordering doesn't matter.
431
- * Not suitable for sequential numbering.
588
+ * // updateMany + deleteMany are primitives on Repository — no plugin needed.
589
+ * await repo.updateMany({ status: 'pending' }, { status: 'active' });
590
+ * await repo.deleteMany({ status: 'deleted' });
432
591
  *
433
- * @example
434
- * ```typescript
435
- * customIdPlugin({
436
- * field: 'publicId',
437
- * generator: prefixedId({ prefix: 'USR', length: 10 }),
438
- * })
439
- * ```
592
+ * // bulkWrite is what this plugin contributes.
593
+ * await repo.bulkWrite([...ops]);
440
594
  */
441
- declare function prefixedId(options: PrefixedIdOptions): IdGenerator;
595
+ declare function batchOperationsPlugin(): Plugin;
596
+ interface BatchOperationsMethods {
597
+ /**
598
+ * Execute heterogeneous bulk write operations in a single database call.
599
+ * Supports insertOne, updateOne, updateMany, deleteOne, deleteMany, replaceOne.
600
+ *
601
+ * @param operations - Array of bulk write operations
602
+ * @param options - Options (session, ordered)
603
+ * @returns Bulk write result with counts per operation type
604
+ *
605
+ * @example
606
+ * const result = await repo.bulkWrite([
607
+ * { insertOne: { document: { name: 'Item', price: 10 } } },
608
+ * { updateOne: { filter: { _id: id }, update: { $inc: { views: 1 } } } },
609
+ * { deleteOne: { filter: { _id: oldId } } },
610
+ * ]);
611
+ * console.log(result.insertedCount, result.modifiedCount, result.deletedCount);
612
+ */
613
+ bulkWrite(operations: Record<string, unknown>[], options?: {
614
+ session?: unknown;
615
+ ordered?: boolean;
616
+ }): Promise<BulkWriteResult>;
617
+ }
618
+ //#endregion
619
+ //#region src/plugins/cascade.plugin.d.ts
442
620
  /**
443
- * Custom ID plugin — injects generated IDs into documents before creation.
444
- *
445
- * @param options - Configuration for ID generation
446
- * @returns Plugin instance
447
- *
448
- * @example
449
- * ```typescript
450
- * import { Repository, customIdPlugin, sequentialId } from '@classytic/mongokit';
451
- *
452
- * const invoiceRepo = new Repository(InvoiceModel, [
453
- * customIdPlugin({
454
- * field: 'invoiceNumber',
455
- * generator: sequentialId({ prefix: 'INV', model: InvoiceModel }),
456
- * }),
457
- * ]);
458
- *
459
- * const inv = await invoiceRepo.create({ amount: 100 });
460
- * console.log(inv.invoiceNumber); // "INV-0001"
461
- * ```
621
+ * Cascade delete plugin.
462
622
  */
463
- declare function customIdPlugin(options: CustomIdOptions): Plugin;
623
+ declare function cascadePlugin(options: CascadeOptions): Plugin;
464
624
  //#endregion
465
625
  //#region src/plugins/elastic.plugin.d.ts
466
626
  interface ElasticSearchOptions {
@@ -813,142 +973,6 @@ interface MongoOperationsMethods<TDoc> {
813
973
  incrementIfBelow(id: string | ObjectId, field: string, ceiling: number, by?: number, options?: Record<string, unknown>): Promise<TDoc | null>;
814
974
  }
815
975
  //#endregion
816
- //#region src/plugins/multi-tenant.plugin.d.ts
817
- /**
818
- * Static tenant fields are picked from `@classytic/repo-core/tenant` so the
819
- * vocabulary (`tenantField`, `contextKey`, `required`, `fieldType`) stays
820
- * locked to the org-wide canonical source. Mongokit-specific runtime
821
- * callbacks (`skipWhen`, `resolveContext`, `skipOperations`,
822
- * `allowDataInjection`) extend on top — those genuinely differ from
823
- * sqlitekit's runtime shape because mongokit's `RepositoryContext` carries
824
- * Mongoose-specific bits the resolver may inspect.
825
- *
826
- * Mongokit defaults `fieldType` to `'string'` (not repo-core's `'objectId'`)
827
- * for back-compat — pre-3.x stored tenant ids as raw strings; flipping the
828
- * default would silently change cast behavior for existing apps.
829
- */
830
- interface MultiTenantOptions extends Pick<TenantConfig, 'tenantField' | 'contextKey' | 'required' | 'fieldType'> {
831
- /** Operations to skip tenant injection (e.g., for admin/system queries) */
832
- skipOperations?: string[];
833
- /**
834
- * Dynamic skip — receives the context and operation name, returns true to
835
- * bypass tenant scoping for this call. Use for role-based bypass (e.g.,
836
- * super admin) without needing a separate repo instance.
837
- *
838
- * @example
839
- * ```typescript
840
- * skipWhen: (context) => context.role === 'superadmin'
841
- * ```
842
- */
843
- skipWhen?: (context: RepositoryContext, operation: string) => boolean;
844
- /**
845
- * Resolve tenant ID from external source (e.g., AsyncLocalStorage, CLS).
846
- * Called when tenant ID is not found in context. If it returns a value,
847
- * that value is used as the tenant ID without requiring it in context.
848
- *
849
- * @example
850
- * ```typescript
851
- * resolveContext: () => asyncLocalStorage.getStore()?.tenantId
852
- * ```
853
- */
854
- resolveContext?: () => string | undefined;
855
- /**
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]`
859
- * on writes, `filters[tenantField]` on paginated reads) use the plugin
860
- * without having to hand-roll a `skipWhen` that duplicates this check.
861
- *
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.
870
- *
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.
874
- *
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`.
883
- */
884
- allowDataInjection?: boolean;
885
- }
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;
951
- //#endregion
952
976
  //#region src/plugins/observability.plugin.d.ts
953
977
  interface OperationMetric {
954
978
  /** Operation name (e.g., 'create', 'getAll', 'update') */
@@ -1220,4 +1244,4 @@ declare function immutableField(field: string): ValidatorDefinition;
1220
1244
  */
1221
1245
  declare function uniqueField(field: string, errorMessage?: string): ValidatorDefinition;
1222
1246
  //#endregion
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 };
1247
+ export { AuditEntry as $, BulkWriteResult as A, IdGenerator as B, LeasePluginOptions as C, elasticSearchPlugin as D, ElasticSearchOptions as E, MultiTenantOptions as F, getNextSequence as G, SequentialIdOptions as H, adminBypass as I, CacheAdapter$1 as J, prefixedId as K, multiTenantPlugin as L, auditLogPlugin as M, AggregateHelpersMethods as N, cascadePlugin as O, aggregateHelpersPlugin as P, cachePlugin as Q, CustomIdOptions as R, LeaseMethods as S, fieldFilterPlugin as T, customIdPlugin as U, PrefixedIdOptions as V, dateSequentialId as W, RepositoryCacheHandle as X, CacheOptions as Y, RepositoryCachePluginOptions as Z, observabilityPlugin as _, uniqueField as a, AuditTrailQuery as at, MethodRegistryRepository as b, TenantContext as c, SubdocumentMethods as d, AuditOperation as et, subdocumentPlugin as f, OperationMetric as g, ObservabilityOptions as h, requireField as i, AuditTrailOptions as it, batchOperationsPlugin as j, BatchOperationsMethods as k, TenantStore as l, softDeletePlugin as m, blockIf as n, AuditQueryResult as nt, validationChainPlugin as o, auditTrailPlugin as ot, SoftDeleteMethods as p, sequentialId as q, immutableField as r, AuditTrailMethods as rt, timestampPlugin as s, autoInject as t, AuditQueryOptions as tt, createTenantContext as u, MongoOperationsMethods as v, leasePlugin as w, methodRegistryPlugin as x, mongoOperationsPlugin as y, DateSequentialIdOptions as z };