@classytic/mongokit 3.36.0 → 3.38.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,7 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.mjs";
2
2
  import { n as warn } from "./logger-K7dPmu2U.mjs";
3
3
  import { t as createError } from "./error-BuByZwKU.mjs";
4
+ import { calculateSkip, calculateTotalPages, shouldWarnDeepPagination, validateLimit, validatePage } from "./pagination/utils/limits.mjs";
4
5
  import mongoose from "mongoose";
5
6
  //#region src/pagination/utils/filter.ts
6
7
  /**
@@ -374,69 +375,6 @@ function hasCompatibleKeysetIndex(indexes, filterFields, sort) {
374
375
  return false;
375
376
  }
376
377
  //#endregion
377
- //#region src/pagination/utils/limits.ts
378
- /**
379
- * Validates and sanitizes limit value
380
- * Parses strings to numbers and prevents NaN bugs
381
- *
382
- * @param limit - Requested limit
383
- * @param config - Pagination configuration
384
- * @returns Sanitized limit between 1 and maxLimit
385
- */
386
- function validateLimit(limit, config) {
387
- const parsed = Number(limit);
388
- if (!Number.isFinite(parsed) || parsed < 1) return config.defaultLimit ?? 10;
389
- const max = config.maxLimit ?? 100;
390
- if (max === 0) return Math.floor(parsed);
391
- return Math.min(Math.floor(parsed), max);
392
- }
393
- /**
394
- * Validates and sanitizes page number
395
- * Parses strings to numbers and prevents NaN bugs
396
- *
397
- * @param page - Requested page (1-indexed)
398
- * @param config - Pagination configuration
399
- * @returns Sanitized page number >= 1
400
- * @throws Error if page exceeds maxPage
401
- */
402
- function validatePage(page, config) {
403
- const parsed = Number(page);
404
- if (!Number.isFinite(parsed) || parsed < 1) return 1;
405
- const sanitized = Math.floor(parsed);
406
- if (sanitized > (config.maxPage || 1e4)) throw new Error(`Page ${sanitized} exceeds maximum ${config.maxPage || 1e4}`);
407
- return sanitized;
408
- }
409
- /**
410
- * Checks if page number should trigger deep pagination warning
411
- *
412
- * @param page - Current page number
413
- * @param threshold - Warning threshold
414
- * @returns True if warning should be shown
415
- */
416
- function shouldWarnDeepPagination(page, threshold) {
417
- return page > threshold;
418
- }
419
- /**
420
- * Calculates number of documents to skip for offset pagination
421
- *
422
- * @param page - Page number (1-indexed)
423
- * @param limit - Documents per page
424
- * @returns Number of documents to skip
425
- */
426
- function calculateSkip(page, limit) {
427
- return (page - 1) * limit;
428
- }
429
- /**
430
- * Calculates total number of pages
431
- *
432
- * @param total - Total document count
433
- * @param limit - Documents per page
434
- * @returns Total number of pages
435
- */
436
- function calculateTotalPages(total, limit) {
437
- return Math.ceil(total / limit);
438
- }
439
- //#endregion
440
378
  //#region src/pagination/utils/sort.ts
441
379
  var sort_exports = /* @__PURE__ */ __exportAll({
442
380
  getPrimaryField: () => getPrimaryField,
@@ -794,4 +732,4 @@ var PaginationEngine = class {
794
732
  }
795
733
  };
796
734
  //#endregion
797
- export { cursor_exports as i, sort_exports as n, calculateTotalPages as r, PaginationEngine as t };
735
+ export { sort_exports as n, cursor_exports as r, PaginationEngine as t };
@@ -3,7 +3,8 @@ import { a as narrowToIds, i as keysetFilter, o as selectKeysetChunk } from "./i
3
3
  import { a as count, b as LookupBuilder, c as getByQuery, d as deleteById, f as deleteByQuery, h as createMany, l as getOrCreate, m as create, n as update, o as exists, r as updateByQuery, s as getById, t as findOneAndUpdate, x as compileFilterToMongo, y as distinct } from "./update-jSXp3nu3.mjs";
4
4
  import { n as warn } from "./logger-K7dPmu2U.mjs";
5
5
  import { n as isDuplicateKeyError, r as parseDuplicateKeyError, t as createError } from "./error-BuByZwKU.mjs";
6
- import { r as calculateTotalPages, t as PaginationEngine } from "./PaginationEngine-GdyX29tE.mjs";
6
+ import { t as PaginationEngine } from "./PaginationEngine-BX9o_-4R.mjs";
7
+ import { calculateTotalPages } from "./pagination/utils/limits.mjs";
7
8
  import { hasNearOperator, rewriteNearForCount } from "./query/primitives/geo.mjs";
8
9
  import { RepositoryBase, runChunkedArchive, runChunkedPurge, throwIfAborted, validatePluginOrder, withRetry } from "@classytic/repo-core/repository";
9
10
  import mongoose from "mongoose";
@@ -2635,6 +2636,10 @@ var Repository = class extends RepositoryBase {
2635
2636
  const idType = getSchemaIdType(this.Model.schema);
2636
2637
  if (!isValidIdForType(id, idType)) {
2637
2638
  if (wantsThrow) throw createError(404, "Document not found");
2639
+ await this._emitHook("after:getById", {
2640
+ context,
2641
+ result: null
2642
+ });
2638
2643
  return null;
2639
2644
  }
2640
2645
  const result = await this._withResilience(context, () => getById(this.Model, id, context));
@@ -4412,9 +4417,9 @@ var Repository = class extends RepositoryBase {
4412
4417
  const appendLookupStages$2 = (pipeline) => appendLookupStages(pipeline, lookups, projection, isKeyset && sort ? this._parseSort(sort) : void 0);
4413
4418
  if (isKeyset && sort) {
4414
4419
  const parsedSort = this._parseSort(sort);
4415
- const { validateKeysetSort } = await import("./PaginationEngine-GdyX29tE.mjs").then((n) => n.n);
4416
- const { encodeCursor, resolveCursorFilter } = await import("./PaginationEngine-GdyX29tE.mjs").then((n) => n.i);
4417
- const { getPrimaryField } = await import("./PaginationEngine-GdyX29tE.mjs").then((n) => n.n);
4420
+ const { validateKeysetSort } = await import("./PaginationEngine-BX9o_-4R.mjs").then((n) => n.n);
4421
+ const { encodeCursor, resolveCursorFilter } = await import("./PaginationEngine-BX9o_-4R.mjs").then((n) => n.r);
4422
+ const { getPrimaryField } = await import("./PaginationEngine-BX9o_-4R.mjs").then((n) => n.n);
4418
4423
  const normalizedSort = validateKeysetSort(parsedSort, this._pagination.config.strictKeysetSortFields);
4419
4424
  const cursorVersion = this._pagination.config.cursorVersion ?? 1;
4420
4425
  const minCursorVersion = this._pagination.config.minCursorVersion ?? 1;
@@ -259,6 +259,83 @@ declare function ensureBetterAuthSessionTtl(connection: {
259
259
  collection?: string;
260
260
  indexName?: string;
261
261
  }): Promise<boolean>;
262
+ /**
263
+ * One declared index on a Better Auth collection.
264
+ *
265
+ * `unique` is a REQUEST, not a guarantee: {@link ensureBetterAuthIndexes}
266
+ * downgrades to a non-unique index when existing rows already violate it,
267
+ * because failing a boot over historical duplicates is worse than running with
268
+ * a plain lookup index and reporting the degradation.
269
+ */
270
+ interface BetterAuthIndexSpec {
271
+ /** Canonical collection name (`'user'`, `'session'`, ...). */
272
+ collection: string;
273
+ keys: Record<string, 1 | -1>;
274
+ name: string;
275
+ unique?: boolean;
276
+ /** Present on self-expiring rows; always paired with `expireAfterSeconds: 0`. */
277
+ ttl?: boolean;
278
+ }
279
+ /**
280
+ * The indexes Better Auth's own queries need, which its mongo adapter does NOT
281
+ * create. Grouped by the plugin that owns the collection so a host only pays
282
+ * for what it enabled.
283
+ *
284
+ * Every entry below backs a query BA runs on a hot path:
285
+ * - `session.token` — read on EVERY authenticated request
286
+ * - `user.email` — read on every sign-in / sign-up, and must be unique
287
+ * - `account.(providerId|issuer, accountId)` — the credential lookup at sign-in
288
+ * - `member.(organizationId, userId)` — the membership check on org-scoped requests
289
+ * Without them each of those is a collection scan, and `email` / `slug` /
290
+ * `token` have no uniqueness guarantee at all.
291
+ *
292
+ * TTL entries (`expiresAt`) additionally keep the collection BOUNDED — see
293
+ * {@link ensureBetterAuthSessionTtl} for the measurements that motivated it.
294
+ */
295
+ declare const BA_INDEXES_BY_PLUGIN: Record<string, readonly BetterAuthIndexSpec[]>;
296
+ /** Outcome of one declared index. */
297
+ interface BetterAuthIndexResult {
298
+ collection: string;
299
+ name: string;
300
+ status: 'created' | 'degraded' | 'failed';
301
+ /** Set on `degraded` (unique dropped) and `failed`. */
302
+ reason?: string;
303
+ }
304
+ interface EnsureBetterAuthIndexesOptions {
305
+ /** Plugin sets to include beyond `core` (always implied). */
306
+ plugins?: BetterAuthPluginKey$1[];
307
+ /** Mirror BA's `usePlural` — appends `s` to every collection name. */
308
+ usePlural?: boolean;
309
+ /** Per-collection name override (mirrors BA's `user.modelName`). */
310
+ modelOverrides?: Partial<Record<string, string>>;
311
+ /** Canonical collection names to skip entirely. */
312
+ exclude?: string[];
313
+ }
314
+ interface IndexCreator {
315
+ collection(name: string): {
316
+ createIndex(keys: Record<string, 1 | -1>, opts: Record<string, unknown>): Promise<unknown>;
317
+ };
318
+ }
319
+ /**
320
+ * Create every index Better Auth's queries need. Idempotent, never throws.
321
+ *
322
+ * BA's mongo adapter creates NO indexes — not even on `session.token`, which is
323
+ * read on every authenticated request, nor a unique one on `user.email`. On a
324
+ * small database nobody notices; the cost arrives as a full collection scan per
325
+ * login once the tables grow, and until then nothing stops a duplicate email.
326
+ *
327
+ * This lives in the kit rather than a host for the same reason
328
+ * {@link ensureBetterAuthSessionTtl} does: the kit is what knows BA's collection
329
+ * names, so fixing it here protects every consumer instead of one host.
330
+ *
331
+ * **Uniqueness degrades rather than fails.** A `unique: true` spec is retried
332
+ * without uniqueness when the collection already holds violating rows, and
333
+ * reported as `degraded` — a boot must not die because historical data has a
334
+ * duplicate. Inspect the returned report to find those.
335
+ *
336
+ * Call once at boot, after the connection is live. Safe to call again.
337
+ */
338
+ declare function ensureBetterAuthIndexes(connection: IndexCreator, options?: EnsureBetterAuthIndexesOptions): Promise<BetterAuthIndexResult[]>;
262
339
  interface SessionUpdaterLike {
263
340
  updateMany(filter: Record<string, unknown>, update: Record<string, unknown>): Promise<{
264
341
  modifiedCount?: number;
@@ -293,4 +370,4 @@ declare function clearActiveOrganizationFromSessions(sessions: SessionUpdaterLik
293
370
  userId?: unknown;
294
371
  }): Promise<number>;
295
372
  //#endregion
296
- export { BetterAuthIdType, BetterAuthInstance, BetterAuthOverlayOptions, type BetterAuthPluginKey, MongooseLike, RegisterBetterAuthStubsOptions, SessionUpdaterLike, clearActiveOrganizationFromSessions, createBetterAuthOverlay, ensureBetterAuthSessionTtl, registerBetterAuthStubs };
373
+ export { BA_INDEXES_BY_PLUGIN, BetterAuthIdType, BetterAuthIndexResult, BetterAuthIndexSpec, BetterAuthInstance, BetterAuthOverlayOptions, type BetterAuthPluginKey, EnsureBetterAuthIndexesOptions, IndexCreator, MongooseLike, RegisterBetterAuthStubsOptions, SessionUpdaterLike, clearActiveOrganizationFromSessions, createBetterAuthOverlay, ensureBetterAuthIndexes, ensureBetterAuthSessionTtl, registerBetterAuthStubs };
@@ -1,4 +1,4 @@
1
- import { n as Repository } from "../Repository-BArvU32x.mjs";
1
+ import { n as Repository } from "../Repository-Ck5x43wF.mjs";
2
2
  import { n as createMongooseAdapter } from "../adapter-Bfqz8yHE.mjs";
3
3
  import { asReadOnlyRepo } from "@classytic/repo-core/repository";
4
4
  import { pluralizeBetterAuthCollection, resolveBetterAuthCollections } from "@classytic/repo-core/better-auth";
@@ -203,6 +203,236 @@ async function ensureBetterAuthSessionTtl(connection, options = {}) {
203
203
  }
204
204
  }
205
205
  /**
206
+ * The indexes Better Auth's own queries need, which its mongo adapter does NOT
207
+ * create. Grouped by the plugin that owns the collection so a host only pays
208
+ * for what it enabled.
209
+ *
210
+ * Every entry below backs a query BA runs on a hot path:
211
+ * - `session.token` — read on EVERY authenticated request
212
+ * - `user.email` — read on every sign-in / sign-up, and must be unique
213
+ * - `account.(providerId|issuer, accountId)` — the credential lookup at sign-in
214
+ * - `member.(organizationId, userId)` — the membership check on org-scoped requests
215
+ * Without them each of those is a collection scan, and `email` / `slug` /
216
+ * `token` have no uniqueness guarantee at all.
217
+ *
218
+ * TTL entries (`expiresAt`) additionally keep the collection BOUNDED — see
219
+ * {@link ensureBetterAuthSessionTtl} for the measurements that motivated it.
220
+ */
221
+ const BA_INDEXES_BY_PLUGIN = {
222
+ core: [
223
+ {
224
+ collection: "user",
225
+ keys: { email: 1 },
226
+ name: "ba_user_email",
227
+ unique: true
228
+ },
229
+ {
230
+ collection: "session",
231
+ keys: { token: 1 },
232
+ name: "ba_session_token",
233
+ unique: true
234
+ },
235
+ {
236
+ collection: "session",
237
+ keys: { userId: 1 },
238
+ name: "ba_session_userId"
239
+ },
240
+ {
241
+ collection: "session",
242
+ keys: { expiresAt: 1 },
243
+ name: "ttl_session_expiresAt",
244
+ ttl: true
245
+ },
246
+ {
247
+ collection: "account",
248
+ keys: { userId: 1 },
249
+ name: "ba_account_userId"
250
+ },
251
+ {
252
+ collection: "account",
253
+ keys: {
254
+ providerId: 1,
255
+ accountId: 1
256
+ },
257
+ name: "ba_account_provider_accountId"
258
+ },
259
+ {
260
+ collection: "account",
261
+ keys: {
262
+ issuer: 1,
263
+ accountId: 1
264
+ },
265
+ name: "ba_account_issuer_accountId"
266
+ },
267
+ {
268
+ collection: "verification",
269
+ keys: { identifier: 1 },
270
+ name: "ba_verification_identifier"
271
+ },
272
+ {
273
+ collection: "verification",
274
+ keys: { expiresAt: 1 },
275
+ name: "ttl_verification_expiresAt",
276
+ ttl: true
277
+ }
278
+ ],
279
+ organization: [
280
+ {
281
+ collection: "organization",
282
+ keys: { slug: 1 },
283
+ name: "ba_organization_slug",
284
+ unique: true
285
+ },
286
+ {
287
+ collection: "member",
288
+ keys: {
289
+ organizationId: 1,
290
+ userId: 1
291
+ },
292
+ name: "ba_member_org_user",
293
+ unique: true
294
+ },
295
+ {
296
+ collection: "member",
297
+ keys: { userId: 1 },
298
+ name: "ba_member_userId"
299
+ },
300
+ {
301
+ collection: "invitation",
302
+ keys: { organizationId: 1 },
303
+ name: "ba_invitation_org"
304
+ },
305
+ {
306
+ collection: "invitation",
307
+ keys: { email: 1 },
308
+ name: "ba_invitation_email"
309
+ },
310
+ {
311
+ collection: "invitation",
312
+ keys: { expiresAt: 1 },
313
+ name: "ttl_invitation_expiresAt",
314
+ ttl: true
315
+ }
316
+ ],
317
+ "organization-teams": [{
318
+ collection: "team",
319
+ keys: { organizationId: 1 },
320
+ name: "ba_team_org"
321
+ }, {
322
+ collection: "teamMember",
323
+ keys: {
324
+ teamId: 1,
325
+ userId: 1
326
+ },
327
+ name: "ba_teamMember_team_user"
328
+ }],
329
+ twoFactor: [{
330
+ collection: "twoFactor",
331
+ keys: { userId: 1 },
332
+ name: "ba_twoFactor_userId"
333
+ }],
334
+ deviceAuthorization: [
335
+ {
336
+ collection: "deviceCode",
337
+ keys: { deviceCode: 1 },
338
+ name: "ba_deviceCode_code"
339
+ },
340
+ {
341
+ collection: "deviceCode",
342
+ keys: { userCode: 1 },
343
+ name: "ba_deviceCode_userCode"
344
+ },
345
+ {
346
+ collection: "deviceCode",
347
+ keys: { expiresAt: 1 },
348
+ name: "ttl_deviceCode_expiresAt",
349
+ ttl: true
350
+ }
351
+ ]
352
+ };
353
+ /**
354
+ * Create every index Better Auth's queries need. Idempotent, never throws.
355
+ *
356
+ * BA's mongo adapter creates NO indexes — not even on `session.token`, which is
357
+ * read on every authenticated request, nor a unique one on `user.email`. On a
358
+ * small database nobody notices; the cost arrives as a full collection scan per
359
+ * login once the tables grow, and until then nothing stops a duplicate email.
360
+ *
361
+ * This lives in the kit rather than a host for the same reason
362
+ * {@link ensureBetterAuthSessionTtl} does: the kit is what knows BA's collection
363
+ * names, so fixing it here protects every consumer instead of one host.
364
+ *
365
+ * **Uniqueness degrades rather than fails.** A `unique: true` spec is retried
366
+ * without uniqueness when the collection already holds violating rows, and
367
+ * reported as `degraded` — a boot must not die because historical data has a
368
+ * duplicate. Inspect the returned report to find those.
369
+ *
370
+ * Call once at boot, after the connection is live. Safe to call again.
371
+ */
372
+ async function ensureBetterAuthIndexes(connection, options = {}) {
373
+ const plugins = options.plugins ?? [];
374
+ const excluded = new Set(options.exclude ?? []);
375
+ const keys = ["core", ...plugins];
376
+ const resolveName = (canonical) => {
377
+ const overridden = options.modelOverrides?.[canonical] ?? canonical;
378
+ return options.usePlural ? pluralizeBetterAuthCollection(overridden) : overridden;
379
+ };
380
+ const specs = [];
381
+ const seen = /* @__PURE__ */ new Set();
382
+ for (const key of keys) for (const spec of BA_INDEXES_BY_PLUGIN[key] ?? []) {
383
+ if (excluded.has(spec.collection)) continue;
384
+ const id = `${spec.collection}.${spec.name}`;
385
+ if (seen.has(id)) continue;
386
+ seen.add(id);
387
+ specs.push(spec);
388
+ }
389
+ const results = [];
390
+ for (const spec of specs) {
391
+ const target = resolveName(spec.collection);
392
+ const base = { name: spec.name };
393
+ if (spec.ttl) base.expireAfterSeconds = 0;
394
+ try {
395
+ await connection.collection(target).createIndex(spec.keys, {
396
+ ...base,
397
+ ...spec.unique ? { unique: true } : {}
398
+ });
399
+ results.push({
400
+ collection: target,
401
+ name: spec.name,
402
+ status: "created"
403
+ });
404
+ } catch (err) {
405
+ const reason = err instanceof Error ? err.message : String(err);
406
+ if (!spec.unique) {
407
+ results.push({
408
+ collection: target,
409
+ name: spec.name,
410
+ status: "failed",
411
+ reason
412
+ });
413
+ continue;
414
+ }
415
+ try {
416
+ await connection.collection(target).createIndex(spec.keys, base);
417
+ results.push({
418
+ collection: target,
419
+ name: spec.name,
420
+ status: "degraded",
421
+ reason
422
+ });
423
+ } catch (err2) {
424
+ results.push({
425
+ collection: target,
426
+ name: spec.name,
427
+ status: "failed",
428
+ reason: err2 instanceof Error ? err2.message : String(err2)
429
+ });
430
+ }
431
+ }
432
+ }
433
+ return results;
434
+ }
435
+ /**
206
436
  * Clear a dangling `activeOrganizationId` from Better Auth `session` rows.
207
437
  *
208
438
  * WHY THIS EXISTS: Better Auth purges its `member` / `invitation` rows when an
@@ -233,4 +463,4 @@ async function clearActiveOrganizationFromSessions(sessions, organizationId, opt
233
463
  return (await sessions.updateMany(filter, { $set: { activeOrganizationId: null } })).modifiedCount ?? 0;
234
464
  }
235
465
  //#endregion
236
- export { clearActiveOrganizationFromSessions, createBetterAuthOverlay, ensureBetterAuthSessionTtl, registerBetterAuthStubs };
466
+ export { BA_INDEXES_BY_PLUGIN, clearActiveOrganizationFromSessions, createBetterAuthOverlay, ensureBetterAuthIndexes, ensureBetterAuthSessionTtl, registerBetterAuthStubs };
@@ -1,5 +1,5 @@
1
- import { n as Repository } from "./Repository-BArvU32x.mjs";
2
- import { a as customIdPlugin, f as auditTrailPlugin, i as multiTenantPlugin, m as auditLogPlugin, n as softDeletePlugin, t as timestampPlugin, u as cachePlugin } from "./timestamp.plugin-BEc6TANm.mjs";
1
+ import { n as Repository } from "./Repository-Ck5x43wF.mjs";
2
+ import { a as customIdPlugin, f as auditTrailPlugin, i as multiTenantPlugin, m as auditLogPlugin, n as softDeletePlugin, t as timestampPlugin, u as cachePlugin } from "./timestamp.plugin-CBQukQzV.mjs";
3
3
  import { n as batchOperationsPlugin, t as methodRegistryPlugin } from "./method-registry.plugin-DblM7V6j.mjs";
4
4
  //#region src/create-repository.ts
5
5
  function isLoggerAudit(value) {
package/dist/index.mjs CHANGED
@@ -1,16 +1,16 @@
1
1
  import { n as idVariants } from "./id-resolution-BqDrk0S2.mjs";
2
- import { _ as resetCapabilityProbeCache, a as supportsTransactions, b as transactionResolutionOf, c as ALL_OPERATIONS, d as READ_OPERATIONS, f as operationsByPolicyKey, g as probeMongoCapabilities, h as classifyHelloReply, i as isTransactionUnsupported, l as MUTATING_OPERATIONS, m as capabilitiesForSupport, n as Repository, o as withTransaction, p as MONGOKIT_CAPABILITIES, r as batchTransaction, s as AggregationBuilder, t as HOOK_PRIORITY, u as OP_REGISTRY, v as resolveMongoCapabilities, x as createMongoPurgePort, y as resolveTransactionSupport } from "./Repository-BArvU32x.mjs";
2
+ import { _ as resetCapabilityProbeCache, a as supportsTransactions, b as transactionResolutionOf, c as ALL_OPERATIONS, d as READ_OPERATIONS, f as operationsByPolicyKey, g as probeMongoCapabilities, h as classifyHelloReply, i as isTransactionUnsupported, l as MUTATING_OPERATIONS, m as capabilitiesForSupport, n as Repository, o as withTransaction, p as MONGOKIT_CAPABILITIES, r as batchTransaction, s as AggregationBuilder, t as HOOK_PRIORITY, u as OP_REGISTRY, v as resolveMongoCapabilities, x as createMongoPurgePort, y as resolveTransactionSupport } from "./Repository-Ck5x43wF.mjs";
3
3
  import { b as LookupBuilder, x as compileFilterToMongo } from "./update-jSXp3nu3.mjs";
4
4
  import { n as warn, t as configureLogger } from "./logger-K7dPmu2U.mjs";
5
5
  import { n as isDuplicateKeyError, r as parseDuplicateKeyError, t as createError } from "./error-BuByZwKU.mjs";
6
6
  import { t as actions_exports } from "./actions/index.mjs";
7
- import { a as customIdPlugin, c as prefixedId, d as AuditTrailQuery, f as auditTrailPlugin, i as multiTenantPlugin, l as sequentialId, m as auditLogPlugin, n as softDeletePlugin, o as dateSequentialId, p as ensureAuditTrailReady, r as adminBypass, s as getNextSequence, t as timestampPlugin, u as cachePlugin } from "./timestamp.plugin-BEc6TANm.mjs";
7
+ import { a as customIdPlugin, c as prefixedId, d as AuditTrailQuery, f as auditTrailPlugin, i as multiTenantPlugin, l as sequentialId, m as auditLogPlugin, n as softDeletePlugin, o as dateSequentialId, p as ensureAuditTrailReady, r as adminBypass, s as getNextSequence, t as timestampPlugin, u as cachePlugin } from "./timestamp.plugin-CBQukQzV.mjs";
8
8
  import { n as batchOperationsPlugin, t as methodRegistryPlugin } from "./method-registry.plugin-DblM7V6j.mjs";
9
- import { t as PaginationEngine } from "./PaginationEngine-GdyX29tE.mjs";
9
+ import { t as PaginationEngine } from "./PaginationEngine-BX9o_-4R.mjs";
10
10
  import { isGeoOperator, parseGeoFilter } from "./query/primitives/geo.mjs";
11
- import { t as createRepository } from "./create-repository-CPNbktFG.mjs";
11
+ import { t as createRepository } from "./create-repository-B5EulQ31.mjs";
12
12
  import { n as defineModel, r as isModelRegistered, t as ModelCollisionError } from "./model-registry-ClKtWiHa.mjs";
13
- import { _ as cascadePlugin, a as uniqueField, c as subdocumentPlugin, d as mongoOperationsPlugin, f as leasePlugin, g as elasticSearchPlugin, h as fieldFilterPlugin, i as requireField, l as observabilityPlugin, m as idempotencyStorePlugin, n as blockIf, o as validationChainPlugin, p as immutableStatesPlugin, r as immutableField, s as createTenantContext, t as autoInject, u as MONGOKIT_PLUGIN_NAMES, v as appendOnlyPlugin, y as aggregateHelpersPlugin } from "./validation-chain.plugin-CyK1s9Ze.mjs";
13
+ import { _ as cascadePlugin, a as uniqueField, c as subdocumentPlugin, d as mongoOperationsPlugin, f as leasePlugin, g as elasticSearchPlugin, h as fieldFilterPlugin, i as requireField, l as observabilityPlugin, m as idempotencyStorePlugin, n as blockIf, o as validationChainPlugin, p as immutableStatesPlugin, r as immutableField, s as createTenantContext, t as autoInject, u as MONGOKIT_PLUGIN_NAMES, v as appendOnlyPlugin, y as aggregateHelpersPlugin } from "./validation-chain.plugin-DhNyxNCw.mjs";
14
14
  import { i as getMongooseProjection, n as filterResponseData, r as getFieldsForUser, t as createFieldPreset } from "./field-selection-FSQFLpsU.mjs";
15
15
  import { buildFieldTypeMap, coerceFieldValue, coerceHeuristic } from "./query/primitives/coercion.mjs";
16
16
  import { extractSchemaIndexes } from "./query/primitives/indexes.mjs";
@@ -509,6 +509,11 @@ function parseFilters(rt, filters, depth = 0) {
509
509
  rt.reject(`Blocked filter field not in allowlist: ${baseField}`, { field: baseField });
510
510
  continue;
511
511
  }
512
+ if (key === "$and") {
513
+ const branches = parseCompoundBranches(rt, value, depth);
514
+ if (branches.length > 0) parsedFilters.$and = branches;
515
+ continue;
516
+ }
512
517
  if (operatorMatch) {
513
518
  const [, , operator] = operatorMatch;
514
519
  if (rt.dangerousOperators.includes(`$${operator}`)) {
@@ -707,6 +712,24 @@ function handleBracketSyntax(rt, field, operators, parsedFilters, depth = 0) {
707
712
  if (typeof parsedFilters[field] === "object" && Object.keys(parsedFilters[field]).length === 0) delete parsedFilters[field];
708
713
  }
709
714
  /** Parse `?or=[...]` / `?OR=[...]` / `?$or=[...]` groups into an $or array. */
715
+ /**
716
+ * Parse the branches of a compound operator (`$and`, and the body of `$or`).
717
+ *
718
+ * Every branch goes back through `parseFilters`, so the operator block, the allowlist and the
719
+ * regex budget all apply inside it. Branches that parse to `{}` are DROPPED: a branch holding
720
+ * only a blocked operator would otherwise become match-all, which widens the query instead of
721
+ * failing it — the exact shape `parseOr` documents below.
722
+ */
723
+ function parseCompoundBranches(rt, raw, depth) {
724
+ const items = Array.isArray(raw) ? raw : typeof raw === "object" && raw ? Object.values(raw) : [];
725
+ const branches = [];
726
+ for (const item of items) {
727
+ if (typeof item !== "object" || !item) continue;
728
+ const parsed = parseFilters(rt, item, depth + 1);
729
+ if (Object.keys(parsed).length > 0) branches.push(parsed);
730
+ }
731
+ return branches;
732
+ }
710
733
  function parseOr(rt, query) {
711
734
  const orArray = [];
712
735
  const raw = query?.or || query?.OR || query?.$or;
@@ -0,0 +1,69 @@
1
+ //#region src/indexes/verify-indexes.d.ts
2
+ /**
3
+ * Compare DECLARED indexes against what a database actually has.
4
+ *
5
+ * `unique: true` on a schema is a correctness constraint only if the index was
6
+ * built. A skipped deploy step is otherwise SILENT — one deployment ran with
7
+ * 401 declared indexes absent, accumulated duplicate rows on a "unique" field,
8
+ * and nothing anywhere said the constraint did not exist. mongokit owns the
9
+ * declaration, so it owns the means to check it.
10
+ *
11
+ * Read-only. It NEVER builds: index builds belong to a deploy step, not to a
12
+ * serving process.
13
+ *
14
+ * Missing UNIQUE indexes are reported separately from missing regular ones —
15
+ * the first silently admits duplicate data, the second only costs a scan.
16
+ *
17
+ * DELIBERATELY ONE-DIRECTIONAL: an index present in the database but absent
18
+ * from a schema is NOT drift. Auth libraries and migrations manage indexes
19
+ * outside the ODM, and treating those as removable is exactly what makes a
20
+ * `syncIndexes()` unsafe.
21
+ *
22
+ * Sibling of `query/primitives/indexes`, which reads DECLARATIONS only and
23
+ * touches no connection. This module is the live comparison.
24
+ */
25
+ /** The slice of a Mongoose-like connection this needs. Structural on purpose. */
26
+ interface IndexVerifiableConnection {
27
+ modelNames(): string[];
28
+ model(name: string): IndexVerifiableModel;
29
+ }
30
+ interface IndexVerifiableModel {
31
+ schema: {
32
+ indexes(): Array<[Record<string, unknown>, Record<string, unknown>?]>;
33
+ };
34
+ collection: {
35
+ name: string;
36
+ listIndexes(): {
37
+ toArray(): Promise<Array<Record<string, unknown>>>;
38
+ };
39
+ };
40
+ }
41
+ interface MissingIndex {
42
+ model: string;
43
+ collection: string;
44
+ /** The declared key, JSON-stringified — stable to log and to diff. */
45
+ key: string;
46
+ }
47
+ interface IndexVerifyReport {
48
+ modelsChecked: number;
49
+ /** Declared `unique` indexes with nothing behind them. Duplicates can already exist. */
50
+ missingUnique: MissingIndex[];
51
+ missingRegular: MissingIndex[];
52
+ /** Models whose indexes could not be read — collection absent, or no permission. */
53
+ unreadable: string[];
54
+ }
55
+ interface VerifyIndexesOptions {
56
+ /**
57
+ * Include a model in the sweep. Default: every model.
58
+ *
59
+ * A kernel that materialises different model sets per mode must skip exactly
60
+ * the set its deploy step skips — indexing a model CREATES its collection, so
61
+ * a mismatch here reports permanent phantom drift instead of a real gap.
62
+ */
63
+ includeModel?: (modelName: string) => boolean;
64
+ }
65
+ declare function verifyIndexes(connection: IndexVerifiableConnection, options?: VerifyIndexesOptions): Promise<IndexVerifyReport>;
66
+ /** One-line summary for a boot log. */
67
+ declare function formatIndexReport(report: IndexVerifyReport): string;
68
+ //#endregion
69
+ export { IndexVerifiableConnection, IndexVerifiableModel, IndexVerifyReport, MissingIndex, VerifyIndexesOptions, formatIndexReport, verifyIndexes };
@@ -0,0 +1,76 @@
1
+ //#region src/indexes/verify-indexes.ts
2
+ /**
3
+ * Order-sensitive: a compound index is not the same index with keys swapped.
4
+ *
5
+ * A SINGLE-field numeric index is direction-agnostic though — mongo traverses
6
+ * an index either way, so `{t:1}` and `{t:-1}` are the same index and a schema
7
+ * declaring one against a database holding the other is not drift. Only numeric
8
+ * directions collapse; `2dsphere` / `hashed` are index TYPES, not directions.
9
+ */
10
+ function normalizeKey(key) {
11
+ const entries = Object.entries(key);
12
+ if (entries.length === 1 && typeof entries[0]?.[1] === "number") return JSON.stringify([[entries[0][0]]]);
13
+ return JSON.stringify(entries);
14
+ }
15
+ /**
16
+ * A TEXT index is stored under a key mongo invents (`{_fts, _ftsx}`) with the
17
+ * real fields moved to `weights`, so it never matches its own declaration by
18
+ * key. Compare the field SET instead, or every text index reads as permanently
19
+ * missing and the whole check becomes noise nobody reads.
20
+ */
21
+ function textFields(key) {
22
+ const fields = Object.entries(key).filter(([, dir]) => dir === "text").map(([field]) => field);
23
+ return fields.length > 0 ? fields.sort() : null;
24
+ }
25
+ async function verifyIndexes(connection, options = {}) {
26
+ const include = options.includeModel ?? (() => true);
27
+ const report = {
28
+ modelsChecked: 0,
29
+ missingUnique: [],
30
+ missingRegular: [],
31
+ unreadable: []
32
+ };
33
+ for (const name of connection.modelNames().sort()) {
34
+ if (!include(name)) continue;
35
+ const model = connection.model(name);
36
+ const declared = model.schema.indexes();
37
+ if (declared.length === 0) continue;
38
+ let existing;
39
+ let existingText;
40
+ try {
41
+ const live = await model.collection.listIndexes().toArray();
42
+ existing = new Set(live.map((i) => normalizeKey(i.key)));
43
+ existingText = new Set(live.filter((i) => i.weights).map((i) => JSON.stringify(Object.keys(i.weights).sort())));
44
+ } catch {
45
+ report.unreadable.push(name);
46
+ continue;
47
+ }
48
+ report.modelsChecked += 1;
49
+ for (const [key, indexOptions] of declared) {
50
+ const asText = textFields(key);
51
+ if (asText) {
52
+ if (existingText.has(JSON.stringify(asText))) continue;
53
+ } else if (existing.has(normalizeKey(key))) continue;
54
+ const entry = {
55
+ model: name,
56
+ collection: model.collection.name,
57
+ key: JSON.stringify(key)
58
+ };
59
+ if (indexOptions?.unique) report.missingUnique.push(entry);
60
+ else report.missingRegular.push(entry);
61
+ }
62
+ }
63
+ return report;
64
+ }
65
+ /** One-line summary for a boot log. */
66
+ function formatIndexReport(report) {
67
+ const parts = [
68
+ `${report.modelsChecked} models checked`,
69
+ `missing unique: ${report.missingUnique.length}`,
70
+ `missing regular: ${report.missingRegular.length}`
71
+ ];
72
+ if (report.unreadable.length > 0) parts.push(`unreadable: ${report.unreadable.length}`);
73
+ return `[index-check] ${parts.join(" — ")}`;
74
+ }
75
+ //#endregion
76
+ export { formatIndexReport, verifyIndexes };
@@ -1,4 +1,4 @@
1
- import { n as Repository } from "../Repository-BArvU32x.mjs";
1
+ import { n as Repository } from "../Repository-Ck5x43wF.mjs";
2
2
  import { n as batchOperationsPlugin, t as methodRegistryPlugin } from "../method-registry.plugin-DblM7V6j.mjs";
3
3
  import { Schema } from "mongoose";
4
4
  //#region src/outbox/index.ts
@@ -1,2 +1,2 @@
1
- import { t as PaginationEngine } from "../PaginationEngine-GdyX29tE.mjs";
1
+ import { t as PaginationEngine } from "../PaginationEngine-BX9o_-4R.mjs";
2
2
  export { PaginationEngine };
@@ -0,0 +1,47 @@
1
+ import { c as PaginationConfig } from "../../pagination-jSUdrACK.mjs";
2
+ //#region src/pagination/utils/limits.d.ts
3
+ /**
4
+ * Validates and sanitizes limit value
5
+ * Parses strings to numbers and prevents NaN bugs
6
+ *
7
+ * @param limit - Requested limit
8
+ * @param config - Pagination configuration
9
+ * @returns Sanitized limit between 1 and maxLimit
10
+ */
11
+ declare function validateLimit(limit: number | string, config: PaginationConfig): number;
12
+ /**
13
+ * Validates and sanitizes page number
14
+ * Parses strings to numbers and prevents NaN bugs
15
+ *
16
+ * @param page - Requested page (1-indexed)
17
+ * @param config - Pagination configuration
18
+ * @returns Sanitized page number >= 1
19
+ * @throws Error if page exceeds maxPage
20
+ */
21
+ declare function validatePage(page: number | string, config: PaginationConfig): number;
22
+ /**
23
+ * Checks if page number should trigger deep pagination warning
24
+ *
25
+ * @param page - Current page number
26
+ * @param threshold - Warning threshold
27
+ * @returns True if warning should be shown
28
+ */
29
+ declare function shouldWarnDeepPagination(page: number, threshold: number): boolean;
30
+ /**
31
+ * Calculates number of documents to skip for offset pagination
32
+ *
33
+ * @param page - Page number (1-indexed)
34
+ * @param limit - Documents per page
35
+ * @returns Number of documents to skip
36
+ */
37
+ declare function calculateSkip(page: number, limit: number): number;
38
+ /**
39
+ * Calculates total number of pages
40
+ *
41
+ * @param total - Total document count
42
+ * @param limit - Documents per page
43
+ * @returns Total number of pages
44
+ */
45
+ declare function calculateTotalPages(total: number, limit: number): number;
46
+ //#endregion
47
+ export { calculateSkip, calculateTotalPages, shouldWarnDeepPagination, validateLimit, validatePage };
@@ -0,0 +1,64 @@
1
+ //#region src/pagination/utils/limits.ts
2
+ /**
3
+ * Validates and sanitizes limit value
4
+ * Parses strings to numbers and prevents NaN bugs
5
+ *
6
+ * @param limit - Requested limit
7
+ * @param config - Pagination configuration
8
+ * @returns Sanitized limit between 1 and maxLimit
9
+ */
10
+ function validateLimit(limit, config) {
11
+ const parsed = Number(limit);
12
+ if (!Number.isFinite(parsed) || parsed < 1) return config.defaultLimit ?? 10;
13
+ const max = config.maxLimit ?? 100;
14
+ if (max === 0) return Math.floor(parsed);
15
+ return Math.min(Math.floor(parsed), max);
16
+ }
17
+ /**
18
+ * Validates and sanitizes page number
19
+ * Parses strings to numbers and prevents NaN bugs
20
+ *
21
+ * @param page - Requested page (1-indexed)
22
+ * @param config - Pagination configuration
23
+ * @returns Sanitized page number >= 1
24
+ * @throws Error if page exceeds maxPage
25
+ */
26
+ function validatePage(page, config) {
27
+ const parsed = Number(page);
28
+ if (!Number.isFinite(parsed) || parsed < 1) return 1;
29
+ const sanitized = Math.floor(parsed);
30
+ if (sanitized > (config.maxPage || 1e4)) throw new Error(`Page ${sanitized} exceeds maximum ${config.maxPage || 1e4}`);
31
+ return sanitized;
32
+ }
33
+ /**
34
+ * Checks if page number should trigger deep pagination warning
35
+ *
36
+ * @param page - Current page number
37
+ * @param threshold - Warning threshold
38
+ * @returns True if warning should be shown
39
+ */
40
+ function shouldWarnDeepPagination(page, threshold) {
41
+ return page > threshold;
42
+ }
43
+ /**
44
+ * Calculates number of documents to skip for offset pagination
45
+ *
46
+ * @param page - Page number (1-indexed)
47
+ * @param limit - Documents per page
48
+ * @returns Number of documents to skip
49
+ */
50
+ function calculateSkip(page, limit) {
51
+ return (page - 1) * limit;
52
+ }
53
+ /**
54
+ * Calculates total number of pages
55
+ *
56
+ * @param total - Total document count
57
+ * @param limit - Documents per page
58
+ * @returns Total number of pages
59
+ */
60
+ function calculateTotalPages(total, limit) {
61
+ return Math.ceil(total / limit);
62
+ }
63
+ //#endregion
64
+ export { calculateSkip, calculateTotalPages, shouldWarnDeepPagination, validateLimit, validatePage };
@@ -1,6 +1,6 @@
1
- import { a as customIdPlugin, c as prefixedId, d as AuditTrailQuery, f as auditTrailPlugin, i as multiTenantPlugin, l as sequentialId, m as auditLogPlugin, n as softDeletePlugin, o as dateSequentialId, p as ensureAuditTrailReady, s as getNextSequence, t as timestampPlugin, u as cachePlugin } from "../timestamp.plugin-BEc6TANm.mjs";
1
+ import { a as customIdPlugin, c as prefixedId, d as AuditTrailQuery, f as auditTrailPlugin, i as multiTenantPlugin, l as sequentialId, m as auditLogPlugin, n as softDeletePlugin, o as dateSequentialId, p as ensureAuditTrailReady, s as getNextSequence, t as timestampPlugin, u as cachePlugin } from "../timestamp.plugin-CBQukQzV.mjs";
2
2
  import { n as batchOperationsPlugin, t as methodRegistryPlugin } from "../method-registry.plugin-DblM7V6j.mjs";
3
- import { _ as cascadePlugin, a as uniqueField, c as subdocumentPlugin, d as mongoOperationsPlugin, f as leasePlugin, g as elasticSearchPlugin, h as fieldFilterPlugin, i as requireField, l as observabilityPlugin, m as idempotencyStorePlugin, n as blockIf, o as validationChainPlugin, p as immutableStatesPlugin, r as immutableField, s as createTenantContext, t as autoInject, u as MONGOKIT_PLUGIN_NAMES, v as appendOnlyPlugin, y as aggregateHelpersPlugin } from "../validation-chain.plugin-CyK1s9Ze.mjs";
3
+ import { _ as cascadePlugin, a as uniqueField, c as subdocumentPlugin, d as mongoOperationsPlugin, f as leasePlugin, g as elasticSearchPlugin, h as fieldFilterPlugin, i as requireField, l as observabilityPlugin, m as idempotencyStorePlugin, n as blockIf, o as validationChainPlugin, p as immutableStatesPlugin, r as immutableField, s as createTenantContext, t as autoInject, u as MONGOKIT_PLUGIN_NAMES, v as appendOnlyPlugin, y as aggregateHelpersPlugin } from "../validation-chain.plugin-DhNyxNCw.mjs";
4
4
  //#region src/plugins/change-log.plugin.ts
5
5
  const PLUGIN_NAME = "changeLog";
6
6
  function isSkipped(context) {
@@ -1,4 +1,4 @@
1
- import { t as createRepository } from "../create-repository-CPNbktFG.mjs";
1
+ import { t as createRepository } from "../create-repository-B5EulQ31.mjs";
2
2
  import { createConnection } from "mongoose";
3
3
  //#region src/testkit/index.ts
4
4
  function idempotentStop(server) {
@@ -1,4 +1,4 @@
1
- import { c as ALL_OPERATIONS, t as HOOK_PRIORITY, u as OP_REGISTRY } from "./Repository-BArvU32x.mjs";
1
+ import { c as ALL_OPERATIONS, t as HOOK_PRIORITY, u as OP_REGISTRY } from "./Repository-Ck5x43wF.mjs";
2
2
  import { n as warn } from "./logger-K7dPmu2U.mjs";
3
3
  import { throwIfAborted, withRetry } from "@classytic/repo-core/repository";
4
4
  import mongoose from "mongoose";
@@ -1290,6 +1290,50 @@ function softDeletePlugin(options = {}) {
1290
1290
  };
1291
1291
  }
1292
1292
  }, { priority: HOOK_PRIORITY.POLICY });
1293
+ /**
1294
+ * Hook: before:bulkWrite — every sub-op gets what its standalone form gets.
1295
+ *
1296
+ * This was skipped as "not a single filter we can scope", but a sub-op DOES carry
1297
+ * its own filter — the tenant plugin walks exactly these keys. Skipping it meant a
1298
+ * soft-delete repository's `bulkWrite` updated rows it had already deleted, and
1299
+ * HARD-deleted through `deleteOne`/`deleteMany`, destroying data the model says is
1300
+ * recoverable. The suite's own header claimed this was covered.
1301
+ *
1302
+ * `deleteMode: 'hard'` still bypasses, exactly as the single-doc path does — a purge
1303
+ * is a deliberate physical delete, and it reaches the driver directly anyway.
1304
+ */
1305
+ repo.on("before:bulkWrite", (context) => {
1306
+ if (options.soft === false || context.deleteMode === "hard") return;
1307
+ const ops = context.operations;
1308
+ if (!Array.isArray(ops)) return;
1309
+ const notDeleted = buildDeletedFilter(deletedField, filterMode, false);
1310
+ for (const subOp of ops) {
1311
+ if (!subOp || typeof subOp !== "object") continue;
1312
+ for (const key of [
1313
+ "updateOne",
1314
+ "updateMany",
1315
+ "replaceOne"
1316
+ ]) {
1317
+ const body = subOp[key];
1318
+ if (body?.filter) body.filter = {
1319
+ ...body.filter,
1320
+ ...notDeleted
1321
+ };
1322
+ }
1323
+ for (const [key, target] of [["deleteOne", "updateOne"], ["deleteMany", "updateMany"]]) {
1324
+ const body = subOp[key];
1325
+ if (!body?.filter) continue;
1326
+ subOp[target] = {
1327
+ filter: {
1328
+ ...body.filter,
1329
+ ...notDeleted
1330
+ },
1331
+ update: { $set: { [deletedField]: /* @__PURE__ */ new Date() } }
1332
+ };
1333
+ delete subOp[key];
1334
+ }
1335
+ }
1336
+ }, { priority: HOOK_PRIORITY.POLICY });
1293
1337
  repo.on("before:deleteMany", async (context) => {
1294
1338
  if (context.deleteMode === "hard") return;
1295
1339
  if (options.soft !== false) {
@@ -1,5 +1,5 @@
1
1
  import { n as collectIds, r as idChunks, t as DEFAULT_ID_CHUNK } from "./id-chunks-BmAwsqNk.mjs";
2
- import { c as ALL_OPERATIONS, u as OP_REGISTRY } from "./Repository-BArvU32x.mjs";
2
+ import { c as ALL_OPERATIONS, u as OP_REGISTRY } from "./Repository-Ck5x43wF.mjs";
3
3
  import { _ as upsert } from "./update-jSXp3nu3.mjs";
4
4
  import { n as warn } from "./logger-K7dPmu2U.mjs";
5
5
  import { t as createError } from "./error-BuByZwKU.mjs";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@classytic/mongokit",
3
- "version": "3.36.0",
3
+ "version": "3.38.1",
4
4
  "description": "Production-grade MongoDB repositories - smart pagination, events, and plugins. Built on @classytic/repo-core.",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -16,6 +16,10 @@
16
16
  "types": "./dist/pagination/PaginationEngine.d.mts",
17
17
  "default": "./dist/pagination/PaginationEngine.mjs"
18
18
  },
19
+ "./pagination/limits": {
20
+ "types": "./dist/pagination/utils/limits.d.mts",
21
+ "default": "./dist/pagination/utils/limits.mjs"
22
+ },
19
23
  "./plugins": {
20
24
  "types": "./dist/plugins/index.d.mts",
21
25
  "default": "./dist/plugins/index.mjs"
@@ -64,6 +68,10 @@
64
68
  "types": "./dist/query/primitives/indexes.d.mts",
65
69
  "default": "./dist/query/primitives/indexes.mjs"
66
70
  },
71
+ "./indexes": {
72
+ "types": "./dist/indexes/verify-indexes.d.mts",
73
+ "default": "./dist/indexes/verify-indexes.mjs"
74
+ },
67
75
  "./testkit": {
68
76
  "types": "./dist/testkit/index.d.mts",
69
77
  "default": "./dist/testkit/index.mjs"
@@ -122,7 +130,7 @@
122
130
  },
123
131
  "homepage": "https://github.com/classytic/mongokit#readme",
124
132
  "peerDependencies": {
125
- "@classytic/repo-core": ">=0.25.0",
133
+ "@classytic/repo-core": ">=0.27.0",
126
134
  "mongodb-memory-server": ">=10.4.3",
127
135
  "mongoose": ">=9.4.1"
128
136
  },
@@ -174,7 +182,7 @@
174
182
  "@better-auth/mongo-adapter": "^1.6.29",
175
183
  "@biomejs/biome": "^2.5.8",
176
184
  "@classytic/dev-tools": "^0.2.0",
177
- "@classytic/repo-core": "^0.25.0",
185
+ "@classytic/repo-core": "^0.27.0",
178
186
  "@types/node": "^26.2.0",
179
187
  "@vitest/coverage-v8": "^4.1.10",
180
188
  "ajv": "^8.20.0",