@classytic/mongokit 3.17.0 → 3.19.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 (32) hide show
  1. package/README.md +68 -1
  2. package/dist/PaginationEngine-4SjzfBQE.d.mts +103 -0
  3. package/dist/actions/index.d.mts +1 -1
  4. package/dist/adapter/index.mjs +1 -1
  5. package/dist/ai/index.d.mts +1 -1
  6. package/dist/better-auth/index.mjs +1 -1
  7. package/dist/create-repository-D5YX0wGs.mjs +29 -0
  8. package/dist/create-repository-K0yft2ht.d.mts +1663 -0
  9. package/dist/{index-fi91ycQc.d.mts → index-BgSBwlWu.d.mts} +1 -1
  10. package/dist/index.d.mts +11 -1665
  11. package/dist/index.mjs +5 -29
  12. package/dist/{mongooseToJsonSchema-BdQJU39w.d.mts → mongooseToJsonSchema-Bz47aMfH.d.mts} +1 -1
  13. package/dist/multi-tenant.plugin-D6lw3BsI.d.mts +487 -0
  14. package/dist/pagination/PaginationEngine.d.mts +1 -102
  15. package/dist/plugins/index.d.mts +19 -2
  16. package/dist/plugins/index.mjs +73 -2
  17. package/dist/query/primitives/coercion.d.mts +1 -1
  18. package/dist/query/primitives/indexes.d.mts +1 -1
  19. package/dist/testkit/index.d.mts +129 -0
  20. package/dist/testkit/index.mjs +139 -0
  21. package/dist/{validation-chain.plugin-DJtC0KJ-.mjs → timestamp.plugin-B1A9U-P7.mjs} +1 -1052
  22. package/dist/utils/index.d.mts +1 -1
  23. package/dist/utils/index.mjs +2 -2
  24. package/dist/{validation-chain.plugin-BpMfy18N.d.mts → validation-chain.plugin-CYOPfFGF.d.mts} +2 -486
  25. package/dist/validation-chain.plugin-DHZbpb4t.mjs +1056 -0
  26. package/package.json +16 -4
  27. /package/dist/{adapter-DYEMkLwK.mjs → adapter-BSxhT0Ip.mjs} +0 -0
  28. /package/dist/{coercion-TRMmR49h.d.mts → coercion-BxxHyLjP.d.mts} +0 -0
  29. /package/dist/{field-selection-CQW3WBiK.mjs → field-selection-d1qdRiMr.mjs} +0 -0
  30. /package/dist/{indexes-CgOF7Wjd.d.mts → indexes-3eHBWNZ2.d.mts} +0 -0
  31. /package/dist/{mongooseToJsonSchema-CBEQmeEa.mjs → mongooseToJsonSchema-ChtXSrLT.mjs} +0 -0
  32. /package/dist/{types-CU3SEDbD.d.mts → types-BA8oXjFe.d.mts} +0 -0
package/dist/index.mjs CHANGED
@@ -4,13 +4,15 @@ import { b as LookupBuilder, x as compileFilterToMongo } from "./update-CvCMv5Cl
4
4
  import { n as warn, t as configureLogger } from "./logger-B8wf-Mtb.mjs";
5
5
  import { n as isDuplicateKeyError, r as parseDuplicateKeyError, t as createError } from "./error-BTeepSNL.mjs";
6
6
  import { t as actions_exports } from "./actions/index.mjs";
7
- import { A as auditLogPlugin, C as getNextSequence, D as batchOperationsPlugin, E as cachePlugin, O as AuditTrailQuery, S as dateSequentialId, T as sequentialId, _ as softDeletePlugin, a as uniqueField, b as methodRegistryPlugin, c as subdocumentPlugin, d as leasePlugin, f as fieldFilterPlugin, g as timestampPlugin, h as aggregateHelpersPlugin, i as requireField, k as auditTrailPlugin, l as observabilityPlugin, m as cascadePlugin, n as blockIf, o as validationChainPlugin, p as elasticSearchPlugin, r as immutableField, s as createTenantContext, t as autoInject, u as mongoOperationsPlugin, v as adminBypass, w as prefixedId, x as customIdPlugin, y as multiTenantPlugin } from "./validation-chain.plugin-DJtC0KJ-.mjs";
7
+ import { a as methodRegistryPlugin, c as getNextSequence, d as cachePlugin, f as batchOperationsPlugin, h as auditLogPlugin, i as multiTenantPlugin, l as prefixedId, m as auditTrailPlugin, n as softDeletePlugin, o as customIdPlugin, p as AuditTrailQuery, r as adminBypass, s as dateSequentialId, t as timestampPlugin, u as sequentialId } from "./timestamp.plugin-B1A9U-P7.mjs";
8
8
  import { t as PaginationEngine } from "./PaginationEngine-BcvaXBQ-.mjs";
9
9
  import { isGeoOperator, parseGeoFilter } from "./query/primitives/geo.mjs";
10
- import { i as getMongooseProjection, n as filterResponseData, r as getFieldsForUser, t as createFieldPreset } from "./field-selection-CQW3WBiK.mjs";
10
+ import { t as createRepository } from "./create-repository-D5YX0wGs.mjs";
11
+ import { a as uniqueField, c as subdocumentPlugin, d as leasePlugin, f as fieldFilterPlugin, h as aggregateHelpersPlugin, i as requireField, l as observabilityPlugin, m as cascadePlugin, n as blockIf, o as validationChainPlugin, p as elasticSearchPlugin, r as immutableField, s as createTenantContext, t as autoInject, u as mongoOperationsPlugin } from "./validation-chain.plugin-DHZbpb4t.mjs";
12
+ import { i as getMongooseProjection, n as filterResponseData, r as getFieldsForUser, t as createFieldPreset } from "./field-selection-d1qdRiMr.mjs";
11
13
  import { buildFieldTypeMap, coerceFieldValue, coerceHeuristic } from "./query/primitives/coercion.mjs";
12
14
  import { extractSchemaIndexes } from "./query/primitives/indexes.mjs";
13
- import { n as buildCrudSchemasFromMongooseSchema, r as createMemoryCache, t as buildCrudSchemasFromModel } from "./mongooseToJsonSchema-CBEQmeEa.mjs";
15
+ import { n as buildCrudSchemasFromMongooseSchema, r as createMemoryCache, t as buildCrudSchemasFromModel } from "./mongooseToJsonSchema-ChtXSrLT.mjs";
14
16
  import { runChunkedPurge } from "@classytic/repo-core/repository";
15
17
  import { isControlParam } from "@classytic/repo-core/query-parser";
16
18
  //#region src/actions/cascade-references.ts
@@ -142,32 +144,6 @@ async function cascadePurgeReferences(options) {
142
144
  return out;
143
145
  }
144
146
  //#endregion
145
- //#region src/create-repository.ts
146
- function isLoggerAudit(value) {
147
- const v = value;
148
- return typeof v.info === "function" || typeof v.warn === "function" || typeof v.error === "function" || typeof v.debug === "function";
149
- }
150
- /**
151
- * Build a `Repository` from a declarative config. See module JSDoc for
152
- * ordering guarantees and an example.
153
- */
154
- function createRepository(model, config = {}) {
155
- const { tenant, softDelete, timestamps, batch, cache, audit, customId, plugins = [], pagination = {}, ...repositoryOptions } = config;
156
- const stack = [methodRegistryPlugin()];
157
- if (tenant) stack.push(multiTenantPlugin(tenant));
158
- if (softDelete) stack.push(softDeletePlugin(softDelete === true ? {} : softDelete));
159
- if (timestamps) stack.push(timestampPlugin());
160
- if (customId) stack.push(customIdPlugin(customId));
161
- if (cache) stack.push(cachePlugin(cache));
162
- if (audit) stack.push(isLoggerAudit(audit) ? auditLogPlugin(audit) : auditTrailPlugin(audit));
163
- if (batch) stack.push(batchOperationsPlugin());
164
- stack.push(...plugins);
165
- return new Repository(model, stack, pagination, {
166
- pluginOrderChecks: repositoryOptions.pluginOrderChecks ?? "throw",
167
- ...repositoryOptions
168
- });
169
- }
170
- //#endregion
171
147
  //#region src/query/QueryParser.ts
172
148
  /**
173
149
  * Modern Query Parser - URL to MongoDB Query Transpiler
@@ -1,4 +1,4 @@
1
- import { C as FieldPreset, gt as UserContext } from "./types-CU3SEDbD.mjs";
1
+ import { C as FieldPreset, gt as UserContext } from "./types-BA8oXjFe.mjs";
2
2
  import mongoose, { Schema } from "mongoose";
3
3
  import { CacheAdapter } from "@classytic/repo-core/cache";
4
4
  import { CrudSchemas, SchemaBuilderOptions } from "@classytic/repo-core/schema";
@@ -0,0 +1,487 @@
1
+ import { B as ObjectId, G as Plugin, Q as RepositoryContext } from "./types-BA8oXjFe.mjs";
2
+ import mongoose from "mongoose";
3
+ import { CacheAdapter as CacheAdapter$1, CacheOptions, RepositoryCacheHandle, RepositoryCachePluginOptions, cachePlugin } from "@classytic/repo-core/cache";
4
+ import { TenantConfig } from "@classytic/repo-core/tenant";
5
+
6
+ //#region src/plugins/audit-trail.plugin.d.ts
7
+ interface AuditTrailOptions {
8
+ /** Operations to track (default: ['create', 'update', 'delete']) */
9
+ operations?: AuditOperation[];
10
+ /** Store field-level before/after diff on updates (default: true) */
11
+ trackChanges?: boolean;
12
+ /** Store full document snapshot on create (default: false — can be heavy) */
13
+ trackDocument?: boolean;
14
+ /** Auto-purge after N days via MongoDB TTL index (default: undefined — keep forever) */
15
+ ttlDays?: number;
16
+ /** MongoDB collection name (default: 'audit_trails') */
17
+ collectionName?: string;
18
+ /**
19
+ * Extract custom metadata from the repository context.
20
+ * Returned object is stored on the audit entry as `metadata`.
21
+ */
22
+ metadata?: (context: RepositoryContext) => Record<string, unknown>;
23
+ /**
24
+ * Fields to exclude from change tracking (e.g., passwords, tokens).
25
+ * These fields are redacted in the `changes` diff.
26
+ */
27
+ excludeFields?: string[];
28
+ /**
29
+ * Custom callback fired when an audit write fails. Receives the error
30
+ * and the entry that failed to land. Default: `console.warn` only.
31
+ *
32
+ * **Use this for compliance-grade audit trails (SOC 2 / PCI / HIPAA)
33
+ * where missing entries must be surfaced upstream** — e.g. forward to
34
+ * a dead-letter queue, page on-call, or short-circuit the request.
35
+ * The default `warn` log is appropriate for development and
36
+ * eventually-consistent observability use cases but is invisible to
37
+ * compliance review.
38
+ *
39
+ * The callback runs on the same fire-and-forget microtask that wrote
40
+ * the entry, so throwing from here will surface as an unhandled
41
+ * rejection — handle (or rethrow into your error reporter) inside.
42
+ */
43
+ onWriteError?: (error: Error, entry: Omit<AuditEntry, 'timestamp'>) => void;
44
+ }
45
+ type AuditOperation = 'create' | 'update' | 'delete' | 'findOneAndUpdate';
46
+ interface AuditEntry {
47
+ model: string;
48
+ operation: AuditOperation;
49
+ documentId: unknown;
50
+ userId?: unknown;
51
+ orgId?: unknown;
52
+ changes?: Record<string, {
53
+ from: unknown;
54
+ to: unknown;
55
+ }>;
56
+ document?: Record<string, unknown>;
57
+ metadata?: Record<string, unknown>;
58
+ timestamp: Date;
59
+ }
60
+ declare function auditTrailPlugin(options?: AuditTrailOptions): Plugin;
61
+ interface AuditQueryOptions {
62
+ model?: string;
63
+ documentId?: string | ObjectId;
64
+ userId?: string | ObjectId;
65
+ orgId?: string | ObjectId;
66
+ operation?: AuditOperation;
67
+ from?: Date;
68
+ to?: Date;
69
+ page?: number;
70
+ limit?: number;
71
+ }
72
+ interface AuditQueryResult {
73
+ data: AuditEntry[];
74
+ page: number;
75
+ limit: number;
76
+ total: number;
77
+ pages: number;
78
+ hasNext: boolean;
79
+ hasPrev: boolean;
80
+ }
81
+ /**
82
+ * Standalone audit trail query utility.
83
+ * Use this to query audits across all models — e.g., admin dashboards, audit APIs.
84
+ *
85
+ * @example
86
+ * ```typescript
87
+ * import { AuditTrailQuery } from '@classytic/mongokit';
88
+ *
89
+ * const auditQuery = new AuditTrailQuery(); // defaults to 'audit_trails' collection
90
+ *
91
+ * // All audits for an org
92
+ * const orgAudits = await auditQuery.query({ orgId: '...' });
93
+ *
94
+ * // All updates by a user
95
+ * const userUpdates = await auditQuery.query({
96
+ * userId: '...',
97
+ * operation: 'update',
98
+ * });
99
+ *
100
+ * // All audits for a specific document
101
+ * const docHistory = await auditQuery.query({
102
+ * model: 'Job',
103
+ * documentId: '...',
104
+ * });
105
+ *
106
+ * // Date range
107
+ * const recent = await auditQuery.query({
108
+ * from: new Date('2025-01-01'),
109
+ * to: new Date(),
110
+ * page: 1,
111
+ * limit: 50,
112
+ * });
113
+ *
114
+ * // Direct model access for custom queries
115
+ * const model = auditQuery.getModel();
116
+ * const count = await model.countDocuments({ operation: 'delete' });
117
+ * ```
118
+ */
119
+ declare class AuditTrailQuery {
120
+ private model;
121
+ constructor(collectionName?: string, ttlDays?: number);
122
+ /**
123
+ * Get the underlying Mongoose model for custom queries
124
+ */
125
+ getModel(): mongoose.Model<AuditEntry>;
126
+ /**
127
+ * Query audit entries with filters and pagination
128
+ */
129
+ query(options?: AuditQueryOptions): Promise<AuditQueryResult>;
130
+ /**
131
+ * Get audit trail for a specific document
132
+ */
133
+ getDocumentTrail(model: string, documentId: string | ObjectId, options?: {
134
+ page?: number;
135
+ limit?: number;
136
+ operation?: AuditOperation;
137
+ }): Promise<AuditQueryResult>;
138
+ /**
139
+ * Get all audits for a user
140
+ */
141
+ getUserTrail(userId: string | ObjectId, options?: {
142
+ page?: number;
143
+ limit?: number;
144
+ operation?: AuditOperation;
145
+ orgId?: string | ObjectId;
146
+ }): Promise<AuditQueryResult>;
147
+ /**
148
+ * Get all audits for an organization
149
+ */
150
+ getOrgTrail(orgId: string | ObjectId, options?: {
151
+ page?: number;
152
+ limit?: number;
153
+ operation?: AuditOperation;
154
+ model?: string;
155
+ }): Promise<AuditQueryResult>;
156
+ }
157
+ interface AuditTrailMethods {
158
+ /**
159
+ * Get paginated audit trail for a document
160
+ */
161
+ getAuditTrail(documentId: string | ObjectId, options?: {
162
+ page?: number;
163
+ limit?: number;
164
+ operation?: AuditOperation;
165
+ }): Promise<AuditQueryResult>;
166
+ }
167
+ //#endregion
168
+ //#region src/plugins/custom-id.plugin.d.ts
169
+ /**
170
+ * Generator function that produces a unique ID.
171
+ * Receives the full repository context for conditional logic.
172
+ */
173
+ type IdGenerator = (context: RepositoryContext) => string | Promise<string>;
174
+ interface CustomIdOptions {
175
+ /** Field to store the custom ID (default: 'customId') */
176
+ field?: string;
177
+ /** Function to generate the ID. Can be async. */
178
+ generator: IdGenerator;
179
+ /** Only generate if the field is missing/empty (default: true) */
180
+ generateOnlyIfEmpty?: boolean;
181
+ }
182
+ /**
183
+ * Atomically increment and return the next sequence value for a given key.
184
+ * Uses `findOneAndUpdate` with `upsert` + `$inc` — fully atomic even under
185
+ * heavy concurrency.
186
+ *
187
+ * @param counterKey - Unique key identifying this counter (e.g., "Invoice" or "Invoice:2026-02")
188
+ * @param increment - Value to increment by (default: 1)
189
+ * @param connection - Mongoose connection to use (defaults to the default connection)
190
+ * @param session - Optional client session. When provided, the counter bump
191
+ * participates in the caller's transaction — if the surrounding transaction
192
+ * aborts, the counter does NOT advance. Without a session, the bump commits
193
+ * immediately and is not rolled back if a sibling write fails.
194
+ * @returns The next sequence number (after increment)
195
+ *
196
+ * @example
197
+ * const seq = await getNextSequence('invoices');
198
+ * // First call → 1, second → 2, ...
199
+ *
200
+ * @example Batch increment for createMany
201
+ * const startSeq = await getNextSequence('invoices', 5);
202
+ * // If current was 10, returns 15 (you use 11, 12, 13, 14, 15)
203
+ *
204
+ * @example Transactional counter bump
205
+ * await withTransaction(conn, async (session) => {
206
+ * const seq = await getNextSequence('invoices', 1, conn, session);
207
+ * await invoiceRepo.create({ invoiceNumber: `INV-${seq}` }, { session });
208
+ * // if this throws, the counter is rolled back with the insert
209
+ * });
210
+ */
211
+ declare function getNextSequence(counterKey: string, increment?: number, connection?: mongoose.Connection, session?: mongoose.ClientSession): Promise<number>;
212
+ interface SequentialIdOptions {
213
+ /** Prefix string (e.g., 'INV', 'ORD') */
214
+ prefix: string;
215
+ /** Mongoose model — used to derive the counter key from model name */
216
+ model: mongoose.Model<any>;
217
+ /** Number of digits to pad to (default: 4 → "0001") */
218
+ padding?: number;
219
+ /** Separator between prefix and number (default: '-') */
220
+ separator?: string;
221
+ /** Custom counter key override (default: model.modelName) */
222
+ counterKey?: string;
223
+ }
224
+ /**
225
+ * Generator: Simple sequential counter.
226
+ * Produces IDs like `INV-0001`, `INV-0002`, etc.
227
+ *
228
+ * Uses atomic MongoDB counters — safe under concurrency.
229
+ *
230
+ * @example
231
+ * ```typescript
232
+ * customIdPlugin({
233
+ * field: 'invoiceNumber',
234
+ * generator: sequentialId({ prefix: 'INV', model: InvoiceModel }),
235
+ * })
236
+ * ```
237
+ */
238
+ declare function sequentialId(options: SequentialIdOptions): IdGenerator;
239
+ interface DateSequentialIdOptions {
240
+ /** Prefix string (e.g., 'BILL', 'INV') */
241
+ prefix: string;
242
+ /** Mongoose model — used to derive the counter key */
243
+ model: mongoose.Model<any>;
244
+ /**
245
+ * Partition granularity — counter resets each period.
246
+ * - 'yearly' → BILL-2026-0001, resets every January
247
+ * - 'monthly' → BILL-2026-02-0001, resets every month
248
+ * - 'daily' → BILL-2026-02-20-0001, resets every day
249
+ */
250
+ partition?: 'yearly' | 'monthly' | 'daily';
251
+ /** Number of digits to pad to (default: 4) */
252
+ padding?: number;
253
+ /** Separator (default: '-') */
254
+ separator?: string;
255
+ }
256
+ /**
257
+ * Generator: Date-partitioned sequential counter.
258
+ * Counter resets per period — great for invoice/bill numbering.
259
+ *
260
+ * Produces IDs like:
261
+ * - yearly: `BILL-2026-0001`
262
+ * - monthly: `BILL-2026-02-0001`
263
+ * - daily: `BILL-2026-02-20-0001`
264
+ *
265
+ * @example
266
+ * ```typescript
267
+ * customIdPlugin({
268
+ * field: 'billNumber',
269
+ * generator: dateSequentialId({
270
+ * prefix: 'BILL',
271
+ * model: BillModel,
272
+ * partition: 'monthly',
273
+ * }),
274
+ * })
275
+ * ```
276
+ */
277
+ declare function dateSequentialId(options: DateSequentialIdOptions): IdGenerator;
278
+ interface PrefixedIdOptions {
279
+ /** Prefix string (e.g., 'USR', 'TXN') */
280
+ prefix: string;
281
+ /** Separator (default: '_') */
282
+ separator?: string;
283
+ /** Length of the random suffix (default: 12) */
284
+ length?: number;
285
+ }
286
+ /**
287
+ * Generator: Prefix + random alphanumeric suffix.
288
+ * Does NOT require a database round-trip — purely in-memory.
289
+ *
290
+ * Produces IDs like: `USR_a7b3xk9m2p1q`
291
+ *
292
+ * Good for: user-facing IDs where ordering doesn't matter.
293
+ * Not suitable for sequential numbering.
294
+ *
295
+ * @example
296
+ * ```typescript
297
+ * customIdPlugin({
298
+ * field: 'publicId',
299
+ * generator: prefixedId({ prefix: 'USR', length: 10 }),
300
+ * })
301
+ * ```
302
+ */
303
+ declare function prefixedId(options: PrefixedIdOptions): IdGenerator;
304
+ /**
305
+ * Custom ID plugin — injects generated IDs into documents before creation.
306
+ *
307
+ * @param options - Configuration for ID generation
308
+ * @returns Plugin instance
309
+ *
310
+ * @example
311
+ * ```typescript
312
+ * import { Repository, customIdPlugin, sequentialId } from '@classytic/mongokit';
313
+ *
314
+ * const invoiceRepo = new Repository(InvoiceModel, [
315
+ * customIdPlugin({
316
+ * field: 'invoiceNumber',
317
+ * generator: sequentialId({ prefix: 'INV', model: InvoiceModel }),
318
+ * }),
319
+ * ]);
320
+ *
321
+ * const inv = await invoiceRepo.create({ amount: 100 });
322
+ * console.log(inv.invoiceNumber); // "INV-0001"
323
+ * ```
324
+ */
325
+ declare function customIdPlugin(options: CustomIdOptions): Plugin;
326
+ //#endregion
327
+ //#region src/plugins/multi-tenant.plugin.d.ts
328
+ /**
329
+ * Static tenant fields are picked from `@classytic/repo-core/tenant` so the
330
+ * vocabulary (`tenantField`, `contextKey`, `required`, `fieldType`) stays
331
+ * locked to the org-wide canonical source. Mongokit-specific runtime
332
+ * callbacks (`skipWhen`, `resolveContext`, `skipOperations`,
333
+ * `allowDataInjection`) extend on top — those genuinely differ from
334
+ * sqlitekit's runtime shape because mongokit's `RepositoryContext` carries
335
+ * Mongoose-specific bits the resolver may inspect.
336
+ *
337
+ * Mongokit defaults `fieldType` to `'string'` (not repo-core's `'objectId'`)
338
+ * for back-compat — pre-3.x stored tenant ids as raw strings; flipping the
339
+ * default would silently change cast behavior for existing apps.
340
+ */
341
+ interface MultiTenantOptions extends Pick<TenantConfig, 'tenantField' | 'contextKey' | 'required' | 'fieldType'> {
342
+ /** Operations to skip tenant injection (e.g., for admin/system queries) */
343
+ skipOperations?: string[];
344
+ /**
345
+ * Dynamic skip — receives the context and operation name, returns true to
346
+ * bypass tenant scoping for this call. Use for role-based bypass (e.g.,
347
+ * super admin) without needing a separate repo instance.
348
+ *
349
+ * @example
350
+ * ```typescript
351
+ * skipWhen: (context) => context.role === 'superadmin'
352
+ * ```
353
+ */
354
+ skipWhen?: (context: RepositoryContext, operation: string) => boolean;
355
+ /**
356
+ * Resolve tenant ID from external source (e.g., AsyncLocalStorage, CLS).
357
+ * Called when tenant ID is not found in context. If it returns a value,
358
+ * that value is used as the tenant ID without requiring it in context.
359
+ *
360
+ * @example
361
+ * ```typescript
362
+ * resolveContext: () => asyncLocalStorage.getStore()?.tenantId
363
+ * ```
364
+ */
365
+ resolveContext?: () => string | undefined;
366
+ /**
367
+ * When `true`, bypass the `required` throw if the operation's policy
368
+ * target already carries the tenant field. This lets hosts that stamp
369
+ * the tenant into the payload themselves (e.g., `data[tenantField]`
370
+ * on writes, `filters[tenantField]` on paginated reads) use the plugin
371
+ * without having to hand-roll a `skipWhen` that duplicates this check.
372
+ *
373
+ * **Default: `false` (secure / fail-closed).** Tenant scope MUST come
374
+ * from `context` or `resolveContext`, otherwise the plugin throws
375
+ * (when `required: true`). This is the right posture for a security-
376
+ * sensitive boundary: caller-supplied scope on the payload cannot be
377
+ * trusted as authentication. If a host control-plane has already
378
+ * authenticated the tenant out-of-band and stamps it onto every
379
+ * write/read, set this to `true` explicitly — making the trust model
380
+ * visible at the call site.
381
+ *
382
+ * **Migration from < 4.0:** the prior default was `true`. To preserve
383
+ * the old behavior verbatim, set `allowDataInjection: true`. To get
384
+ * the new secure default, leave it unset.
385
+ *
386
+ * Composition: a user-supplied `skipWhen` still runs first and can
387
+ * always opt out. This option controls the behavior after `skipWhen`
388
+ * and `resolveContext` have had their turn.
389
+ *
390
+ * Safety: when the payload supplies the tenant AND this is `true`,
391
+ * the plugin skips its own injection (it does not overwrite). Cross-
392
+ * tenant isolation is therefore only as strong as the caller's own
393
+ * stamping — same guarantee as a hand-rolled `skipWhen`.
394
+ */
395
+ allowDataInjection?: boolean;
396
+ /**
397
+ * What to do when the call's filter/payload ALREADY carries
398
+ * `tenantField` with a value that does NOT match the resolved tenant
399
+ * scope (a caller-supplied cross-tenant value — fat-fingered org id,
400
+ * stale client state, or an injection attempt).
401
+ *
402
+ * - `'throw'` (default, fail-closed): reject the call with a clear
403
+ * error. A mismatch is never legitimate on a scoped call — callers
404
+ * who genuinely need cross-tenant access use `bypassTenant: true`
405
+ * (per-call) or `skipWhen` (plugin-level), both of which emit the
406
+ * `after:tenant-bypass` audit event. Operator-shaped values
407
+ * (`{ $in: [...] }`) on the tenant field can't be verified against
408
+ * the scope and are rejected too — multi-value tenant scoping is
409
+ * deliberately not supported (see CLAUDE.md); use an explicit
410
+ * bypass + manual filter.
411
+ * - `'overwrite'` (legacy, pre-3.16 behavior): silently replace the
412
+ * supplied value with the resolved scope. Still leak-safe (the
413
+ * resolved scope always wins) but hides caller bugs.
414
+ *
415
+ * Matching values (same tenant, any representation — string vs
416
+ * ObjectId) are never a mismatch; the plugin normalizes them to the
417
+ * configured `fieldType`.
418
+ */
419
+ onMismatch?: 'throw' | 'overwrite';
420
+ }
421
+ declare function multiTenantPlugin(options?: MultiTenantOptions): Plugin;
422
+ /**
423
+ * Build the canonical `skipWhen` callback for role-based tenant
424
+ * bypass — replaces the hand-rolled
425
+ * `(ctx) => ctx.role === 'superadmin'` callback that ~6 packages
426
+ * across the classytic codebase carry as boilerplate. Eliminates the
427
+ * "I forgot to include 'platform_admin'" drift class — the role
428
+ * vocabulary lives in one place.
429
+ *
430
+ * **Composes with `bypassTenant: true` per-call**, doesn't replace
431
+ * it. `bypassTenant` is the deliberate per-call form for migration
432
+ * scripts and one-off admin queries; `adminBypass` is the
433
+ * always-on role-based bypass for users whose JWT / session carries
434
+ * a privileged role.
435
+ *
436
+ * **NOT a substitute for authentication or RBAC.** This factory only
437
+ * answers "should the tenant scope hook fire for this call?" — it
438
+ * does NOT validate that the caller is authenticated, that their
439
+ * role claim is genuine, or that the requested operation is
440
+ * permitted. Always run auth + RBAC before the call reaches the
441
+ * repository layer; this is the LAST gate, not the only one.
442
+ *
443
+ * @example Single role
444
+ * ```ts
445
+ * import { adminBypass, multiTenantPlugin } from '@classytic/mongokit';
446
+ *
447
+ * const repo = new Repository(InvoiceModel, [
448
+ * multiTenantPlugin({
449
+ * tenantField: 'organizationId',
450
+ * skipWhen: adminBypass({ adminRoles: ['superadmin'] }),
451
+ * }),
452
+ * ]);
453
+ *
454
+ * await repo.getAll({}, { role: 'superadmin' }); // sees all orgs
455
+ * await repo.getAll({}, { role: 'user', organizationId: 'org-1' }); // scoped
456
+ * ```
457
+ *
458
+ * @example Multiple admin roles + custom field name
459
+ * ```ts
460
+ * skipWhen: adminBypass({
461
+ * roleField: 'principalRole', // ctx.principalRole, not ctx.role
462
+ * adminRoles: ['superadmin', 'platform_admin', 'support'],
463
+ * }),
464
+ * ```
465
+ *
466
+ * @example Composing with custom logic
467
+ * ```ts
468
+ * // adminBypass for the role check, then your own additional logic:
469
+ * const adminCheck = adminBypass({ adminRoles: ['superadmin'] });
470
+ * skipWhen: (ctx, op) => adminCheck(ctx, op) || ctx.internalSystemFlag === true,
471
+ * ```
472
+ *
473
+ * @param options.roleField - Path on the context to read the role
474
+ * from. Defaults to `'role'`. Top-level keys only — for nested
475
+ * shapes (e.g. `ctx.user.role`), wrap your own `(ctx) => …` callback
476
+ * instead.
477
+ * @param options.adminRoles - Role values that grant the bypass. The
478
+ * factory does an exact-match `includes` check — case-sensitive,
479
+ * no fuzzy matching. Lowercase your role vocabulary upstream.
480
+ * @returns A `skipWhen`-compatible callback `(ctx, op) → boolean`.
481
+ */
482
+ declare function adminBypass(options: {
483
+ roleField?: string;
484
+ adminRoles: readonly string[];
485
+ }): (context: RepositoryContext, operation: string) => boolean;
486
+ //#endregion
487
+ export { AuditTrailMethods as C, auditTrailPlugin as E, AuditQueryResult as S, AuditTrailQuery as T, RepositoryCachePluginOptions as _, DateSequentialIdOptions as a, AuditOperation as b, SequentialIdOptions as c, getNextSequence as d, prefixedId as f, RepositoryCacheHandle as g, CacheOptions as h, CustomIdOptions as i, customIdPlugin as l, CacheAdapter$1 as m, adminBypass as n, IdGenerator as o, sequentialId as p, multiTenantPlugin as r, PrefixedIdOptions as s, MultiTenantOptions as t, dateSequentialId as u, cachePlugin as v, AuditTrailOptions as w, AuditQueryOptions as x, AuditEntry as y };
@@ -1,103 +1,2 @@
1
- import { A as KeysetPaginationOptions, U as PaginationConfig, V as OffsetPaginationOptions, i as AnyDocument, n as AggregatePaginationOptions } from "../types-CU3SEDbD.mjs";
2
- import { Model } from "mongoose";
3
- import { AggregatePaginationResult, KeysetPaginationResult, OffsetPaginationResult } from "@classytic/repo-core/pagination";
4
-
5
- //#region src/pagination/PaginationEngine.d.ts
6
- /**
7
- * Internal pagination config with required values
8
- */
9
- interface ResolvedPaginationConfig {
10
- defaultLimit: number;
11
- maxLimit: number;
12
- maxPage: number;
13
- deepPageThreshold: number;
14
- cursorVersion: number;
15
- minCursorVersion: number;
16
- strictKeysetSortFields: string[] | undefined;
17
- useEstimatedCount: boolean;
18
- }
19
- /**
20
- * Production-grade pagination engine for MongoDB
21
- * Supports offset, keyset (cursor), and aggregate pagination
22
- */
23
- declare class PaginationEngine<TDoc = AnyDocument> {
24
- readonly Model: Model<TDoc>;
25
- readonly config: ResolvedPaginationConfig;
26
- /**
27
- * Lazily-cached schema index snapshot used by stream() to decide whether
28
- * to emit the "missing compound index" warning. Computed on first use.
29
- */
30
- private _cachedSchemaIndexes;
31
- /**
32
- * Create a new pagination engine
33
- *
34
- * @param Model - Mongoose model to paginate
35
- * @param config - Pagination configuration
36
- */
37
- constructor(Model: Model<TDoc, any, any, any>, config?: PaginationConfig);
38
- /** Memoized schema index lookup — avoids re-walking schema on every stream(). */
39
- private _getSchemaIndexes;
40
- /**
41
- * Offset-based pagination using skip/limit
42
- * Best for small datasets and when users need random page access
43
- * O(n) performance - slower for deep pages
44
- *
45
- * @param options - Pagination options
46
- * @returns Pagination result with total count
47
- *
48
- * @example
49
- * const result = await engine.paginate({
50
- * filters: { status: 'active' },
51
- * sort: { createdAt: -1 },
52
- * page: 1,
53
- * limit: 20
54
- * });
55
- * console.log(result.data, result.total, result.hasNext);
56
- */
57
- paginate(options?: OffsetPaginationOptions): Promise<OffsetPaginationResult<TDoc>>;
58
- /**
59
- * Keyset (cursor-based) pagination for high-performance streaming
60
- * Best for large datasets, infinite scroll, real-time feeds
61
- * O(1) performance - consistent speed regardless of position
62
- *
63
- * @param options - Pagination options (sort is required)
64
- * @returns Pagination result with next cursor
65
- *
66
- * @example
67
- * // First page
68
- * const page1 = await engine.stream({
69
- * sort: { createdAt: -1 },
70
- * limit: 20
71
- * });
72
- *
73
- * // Next page using cursor
74
- * const page2 = await engine.stream({
75
- * sort: { createdAt: -1 },
76
- * after: page1.next,
77
- * limit: 20
78
- * });
79
- */
80
- stream(options: KeysetPaginationOptions): Promise<KeysetPaginationResult<TDoc>>;
81
- /**
82
- * Aggregate pipeline with pagination
83
- * Best for complex queries requiring aggregation stages
84
- * Uses $facet to combine results and count in single query
85
- *
86
- * @param options - Aggregation options
87
- * @returns Pagination result with total count
88
- *
89
- * @example
90
- * const result = await engine.aggregatePaginate({
91
- * pipeline: [
92
- * { $match: { status: 'active' } },
93
- * { $group: { _id: '$category', count: { $sum: 1 } } },
94
- * { $sort: { count: -1 } }
95
- * ],
96
- * page: 1,
97
- * limit: 20
98
- * });
99
- */
100
- aggregatePaginate(options?: AggregatePaginationOptions): Promise<AggregatePaginationResult<TDoc>>;
101
- }
102
- //#endregion
1
+ import { t as PaginationEngine } from "../PaginationEngine-4SjzfBQE.mjs";
103
2
  export { PaginationEngine };