@classytic/mongokit 3.23.0 → 3.24.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.
@@ -255,6 +255,59 @@ function resolveCursorFilter(after, sort, cursorVersion, baseFilters = {}, minCu
255
255
  //#endregion
256
256
  //#region src/pagination/utils/index-hint.ts
257
257
  /**
258
+ * The only operators that give the query planner a *bounded equality* it can
259
+ * lead an index with. Everything else (`$ne`, `$nin`, `$gt(e)`, `$lt(e)`,
260
+ * `$all`, `$elemMatch`, `$regex`, `$exists`, …) is a range predicate and, per ESR,
261
+ * must come AFTER the sort keys — never in the leading prefix.
262
+ *
263
+ * `$all` is intentionally excluded: although it combines equality matches,
264
+ * it targets multikey data, whose bounded index prefix cannot generally
265
+ * provide a subsequent index sort. Treating it as residual avoids claiming
266
+ * that a multikey-prefix compound index satisfies the sort.
267
+ */
268
+ const EQUALITY_OPERATORS = /* @__PURE__ */ new Set(["$eq", "$in"]);
269
+ /**
270
+ * Is a top-level filter value a RANGE predicate (as opposed to equality)?
271
+ *
272
+ * Only a plain operator object — one whose own keys start with `$` — can be a
273
+ * range. Scalars, `Date`, `ObjectId`, arrays, and embedded-doc equality
274
+ * matches (`{ nested: { a: 1 } }`) are all equality for index-leading
275
+ * purposes. An operator object is equality iff every `$`-operator it uses is
276
+ * in {@link EQUALITY_OPERATORS}.
277
+ */
278
+ function isRangePredicate(value) {
279
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
280
+ const operatorKeys = Object.keys(value).filter((k) => k.startsWith("$"));
281
+ if (operatorKeys.length === 0) return false;
282
+ return operatorKeys.some((k) => !EQUALITY_OPERATORS.has(k));
283
+ }
284
+ /**
285
+ * Split top-level filter fields into ESR equality vs range predicates.
286
+ *
287
+ * The keyset index check follows MongoDB's ESR rule (Equality → Sort →
288
+ * Range). Only equality predicates belong in an index's leading prefix; a
289
+ * range predicate placed there stops the planner from using any later key for
290
+ * bounds OR for satisfying the sort. So the compatibility check and the
291
+ * recommended-index shape must treat the two classes differently — a field
292
+ * filtered by a range should TRAIL the sort keys, not lead them.
293
+ *
294
+ * `$`-prefixed top-level keys (`$and`/`$or`/…) are ignored (the caller decides
295
+ * how to handle logical combinators).
296
+ */
297
+ function classifyFilterFields(filters) {
298
+ const equality = [];
299
+ const range = [];
300
+ for (const [key, value] of Object.entries(filters)) {
301
+ if (key.startsWith("$")) continue;
302
+ if (isRangePredicate(value)) range.push(key);
303
+ else equality.push(key);
304
+ }
305
+ return {
306
+ equality,
307
+ range
308
+ };
309
+ }
310
+ /**
258
311
  * Read a Mongoose schema's declared indexes, defensively.
259
312
  * Returns an empty array on any introspection failure.
260
313
  */
@@ -626,9 +679,17 @@ var PaginationEngine = class {
626
679
  const sortWithoutIdTail = stripTrailingIdTiebreaker(normalizedSort);
627
680
  const filterKeys = Object.keys(filters).filter((k) => !k.startsWith("$"));
628
681
  const effectiveSortFields = Object.keys(sortWithoutIdTail);
629
- if (process.env.NODE_ENV !== "test" && filterKeys.length > 0 && effectiveSortFields.length > 0 && !hasCompatibleKeysetIndex(this._getSchemaIndexes(), filterKeys, sortWithoutIdTail)) {
630
- const indexFields = [...filterKeys.map((f) => `${f}: 1`), ...effectiveSortFields.map((f) => `${f}: ${sortWithoutIdTail[f]}`)];
631
- warn(`[mongokit] Keyset pagination with filters [${filterKeys.join(", ")}] and sort [${effectiveSortFields.join(", ")}] has no matching schema-declared compound index. For O(1) performance, declare: { ${indexFields.join(", ")} }. (Collection-level indexes created outside the schema are not visible here.)`);
682
+ if (process.env.NODE_ENV !== "test" && filterKeys.length > 0 && effectiveSortFields.length > 0) {
683
+ const indexes = this._getSchemaIndexes();
684
+ const { equality, range } = classifyFilterFields(filters);
685
+ if (!(hasCompatibleKeysetIndex(indexes, filterKeys, sortWithoutIdTail) || equality.length !== filterKeys.length && hasCompatibleKeysetIndex(indexes, equality, sortWithoutIdTail))) {
686
+ const indexFields = [
687
+ ...equality.map((f) => `${f}: 1`),
688
+ ...effectiveSortFields.map((f) => `${f}: ${sortWithoutIdTail[f]}`),
689
+ ...range.map((f) => `${f}: 1`)
690
+ ];
691
+ warn(`[mongokit] Keyset pagination with filters [${filterKeys.join(", ")}] and sort [${effectiveSortFields.join(", ")}] has no matching schema-declared compound index. For O(1) performance, declare: { ${indexFields.join(", ")} }. (Collection-level indexes created outside the schema are not visible here.)`);
692
+ }
632
693
  }
633
694
  let query = { ...filters };
634
695
  if (after) query = resolveCursorFilter(after, normalizedSort, this.config.cursorVersion, query, this.config.minCursorVersion);
@@ -2,7 +2,7 @@ import { i as isValidIdForType, t as getSchemaIdType } from "./id-resolution-P2O
2
2
  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-1Lq9OMD2.mjs";
3
3
  import { n as warn } from "./logger-K7dPmu2U.mjs";
4
4
  import { n as isDuplicateKeyError, r as parseDuplicateKeyError, t as createError } from "./error-BuByZwKU.mjs";
5
- import { t as PaginationEngine } from "./PaginationEngine-BT1ywu9k.mjs";
5
+ import { t as PaginationEngine } from "./PaginationEngine-CczbvCAq.mjs";
6
6
  import { hasNearOperator, rewriteNearForCount } from "./query/primitives/geo.mjs";
7
7
  import { RepositoryBase, runChunkedArchive, runChunkedPurge, throwIfAborted, validatePluginOrder, withRetry } from "@classytic/repo-core/repository";
8
8
  import mongoose from "mongoose";
@@ -3778,9 +3778,9 @@ var Repository = class extends RepositoryBase {
3778
3778
  };
3779
3779
  if (isKeyset && sort) {
3780
3780
  const parsedSort = this._parseSort(sort);
3781
- const { validateKeysetSort } = await import("./PaginationEngine-BT1ywu9k.mjs").then((n) => n.n);
3782
- const { encodeCursor, resolveCursorFilter } = await import("./PaginationEngine-BT1ywu9k.mjs").then((n) => n.r);
3783
- const { getPrimaryField } = await import("./PaginationEngine-BT1ywu9k.mjs").then((n) => n.n);
3781
+ const { validateKeysetSort } = await import("./PaginationEngine-CczbvCAq.mjs").then((n) => n.n);
3782
+ const { encodeCursor, resolveCursorFilter } = await import("./PaginationEngine-CczbvCAq.mjs").then((n) => n.r);
3783
+ const { getPrimaryField } = await import("./PaginationEngine-CczbvCAq.mjs").then((n) => n.n);
3784
3784
  const normalizedSort = validateKeysetSort(parsedSort, this._pagination.config.strictKeysetSortFields);
3785
3785
  const cursorVersion = this._pagination.config.cursorVersion ?? 1;
3786
3786
  const minCursorVersion = this._pagination.config.minCursorVersion ?? 1;
@@ -1,4 +1,4 @@
1
- import { n as Repository } from "../Repository-CLQA3sEz.mjs";
1
+ import { n as Repository } from "../Repository-ClOraFjT.mjs";
2
2
  import { n as createMongooseAdapter } from "../adapter-BWgJ_Jdc.mjs";
3
3
  import { pluralizeBetterAuthCollection, resolveBetterAuthCollections } from "@classytic/repo-core/better-auth";
4
4
  //#region src/better-auth/index.ts
@@ -1,5 +1,5 @@
1
- import { n as Repository } from "./Repository-CLQA3sEz.mjs";
2
- import { a as methodRegistryPlugin, d as cachePlugin, f as batchOperationsPlugin, h as auditLogPlugin, i as multiTenantPlugin, m as auditTrailPlugin, n as softDeletePlugin, o as customIdPlugin, t as timestampPlugin } from "./timestamp.plugin-BnuuIDqJ.mjs";
1
+ import { n as Repository } from "./Repository-ClOraFjT.mjs";
2
+ import { a as methodRegistryPlugin, d as cachePlugin, f as batchOperationsPlugin, h as auditLogPlugin, i as multiTenantPlugin, m as auditTrailPlugin, n as softDeletePlugin, o as customIdPlugin, t as timestampPlugin } from "./timestamp.plugin-BhSX0Lu_.mjs";
3
3
  //#region src/create-repository.ts
4
4
  function isLoggerAudit(value) {
5
5
  const v = value;
package/dist/index.mjs CHANGED
@@ -1,14 +1,14 @@
1
1
  import { n as idVariants } from "./id-resolution-P2OzcyRl.mjs";
2
- import { a as withTransaction, c as MUTATING_OPERATIONS, d as operationsByPolicyKey, f as MONGOKIT_CAPABILITIES, i as isTransactionUnsupported, l as OP_REGISTRY, n as Repository, o as AggregationBuilder, p as createMongoPurgePort, r as batchTransaction, s as ALL_OPERATIONS, t as HOOK_PRIORITY, u as READ_OPERATIONS } from "./Repository-CLQA3sEz.mjs";
2
+ import { a as withTransaction, c as MUTATING_OPERATIONS, d as operationsByPolicyKey, f as MONGOKIT_CAPABILITIES, i as isTransactionUnsupported, l as OP_REGISTRY, n as Repository, o as AggregationBuilder, p as createMongoPurgePort, r as batchTransaction, s as ALL_OPERATIONS, t as HOOK_PRIORITY, u as READ_OPERATIONS } from "./Repository-ClOraFjT.mjs";
3
3
  import { b as LookupBuilder, x as compileFilterToMongo } from "./update-1Lq9OMD2.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 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-BnuuIDqJ.mjs";
8
- import { t as PaginationEngine } from "./PaginationEngine-BT1ywu9k.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-BhSX0Lu_.mjs";
8
+ import { t as PaginationEngine } from "./PaginationEngine-CczbvCAq.mjs";
9
9
  import { isGeoOperator, parseGeoFilter } from "./query/primitives/geo.mjs";
10
- import { t as createRepository } from "./create-repository-Bx1PpcdF.mjs";
11
- import { _ as appendOnlyPlugin, a as uniqueField, c as subdocumentPlugin, d as mongoOperationsPlugin, f as leasePlugin, g as cascadePlugin, h as elasticSearchPlugin, i as requireField, l as observabilityPlugin, m as fieldFilterPlugin, 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 aggregateHelpersPlugin } from "./validation-chain.plugin-ZFDHxXLn.mjs";
10
+ import { t as createRepository } from "./create-repository-CVupgeRG.mjs";
11
+ import { _ as appendOnlyPlugin, a as uniqueField, c as subdocumentPlugin, d as mongoOperationsPlugin, f as leasePlugin, g as cascadePlugin, h as elasticSearchPlugin, i as requireField, l as observabilityPlugin, m as fieldFilterPlugin, 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 aggregateHelpersPlugin } from "./validation-chain.plugin-Dtt4tO7R.mjs";
12
12
  import { i as getMongooseProjection, n as filterResponseData, r as getFieldsForUser, t as createFieldPreset } from "./field-selection-FSQFLpsU.mjs";
13
13
  import { buildFieldTypeMap, coerceFieldValue, coerceHeuristic } from "./query/primitives/coercion.mjs";
14
14
  import { extractSchemaIndexes } from "./query/primitives/indexes.mjs";
@@ -1,2 +1,2 @@
1
- import { t as PaginationEngine } from "../PaginationEngine-BT1ywu9k.mjs";
1
+ import { t as PaginationEngine } from "../PaginationEngine-CczbvCAq.mjs";
2
2
  export { PaginationEngine };
@@ -1,5 +1,5 @@
1
- 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, s as dateSequentialId, t as timestampPlugin, u as sequentialId } from "../timestamp.plugin-BnuuIDqJ.mjs";
2
- import { _ as appendOnlyPlugin, a as uniqueField, c as subdocumentPlugin, d as mongoOperationsPlugin, f as leasePlugin, g as cascadePlugin, h as elasticSearchPlugin, i as requireField, l as observabilityPlugin, m as fieldFilterPlugin, 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 aggregateHelpersPlugin } from "../validation-chain.plugin-ZFDHxXLn.mjs";
1
+ 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, s as dateSequentialId, t as timestampPlugin, u as sequentialId } from "../timestamp.plugin-BhSX0Lu_.mjs";
2
+ import { _ as appendOnlyPlugin, a as uniqueField, c as subdocumentPlugin, d as mongoOperationsPlugin, f as leasePlugin, g as cascadePlugin, h as elasticSearchPlugin, i as requireField, l as observabilityPlugin, m as fieldFilterPlugin, 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 aggregateHelpersPlugin } from "../validation-chain.plugin-Dtt4tO7R.mjs";
3
3
  //#region src/plugins/change-log.plugin.ts
4
4
  const PLUGIN_NAME = "changeLog";
5
5
  function isSkipped(context) {
@@ -1,4 +1,4 @@
1
- import { t as createRepository } from "../create-repository-Bx1PpcdF.mjs";
1
+ import { t as createRepository } from "../create-repository-CVupgeRG.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 { l as OP_REGISTRY, s as ALL_OPERATIONS, t as HOOK_PRIORITY } from "./Repository-CLQA3sEz.mjs";
1
+ import { l as OP_REGISTRY, s as ALL_OPERATIONS, t as HOOK_PRIORITY } from "./Repository-ClOraFjT.mjs";
2
2
  import { n as warn } from "./logger-K7dPmu2U.mjs";
3
3
  import { t as createError } from "./error-BuByZwKU.mjs";
4
4
  import { throwIfAborted, withRetry } from "@classytic/repo-core/repository";
@@ -1,4 +1,4 @@
1
- import { l as OP_REGISTRY, s as ALL_OPERATIONS } from "./Repository-CLQA3sEz.mjs";
1
+ import { l as OP_REGISTRY, s as ALL_OPERATIONS } from "./Repository-ClOraFjT.mjs";
2
2
  import { _ as upsert } from "./update-1Lq9OMD2.mjs";
3
3
  import { n as warn } from "./logger-K7dPmu2U.mjs";
4
4
  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.23.0",
3
+ "version": "3.24.0",
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,
@@ -140,7 +140,7 @@
140
140
  "format": "biome format src/ --write",
141
141
  "check": "biome ci src/ --diagnostic-level=error",
142
142
  "push": "classytic-push",
143
- "prepublishOnly": "npm run check && npm run build && npm run lint:package && npm run typecheck && npm run typecheck:tests && npm test",
143
+ "prepublishOnly": "npm run check && npm run build && publint && npm run typecheck && npm run typecheck:tests && npm test",
144
144
  "publish:dry": "npm publish --dry-run --access public",
145
145
  "publish:npm": "npm publish --access public",
146
146
  "release:tag": "node -e \"require('child_process').execSync('npm run push -- v'+require('./package.json').version,{stdio:'inherit'})\"",