@classytic/mongokit 3.11.1 → 3.12.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.
package/README.md CHANGED
@@ -9,7 +9,7 @@ Production-grade MongoDB repository pattern for Node.js. Zero runtime deps — M
9
9
  npm install @classytic/mongokit @classytic/repo-core mongoose
10
10
  ```
11
11
 
12
- Requires Mongoose `>=9.4.1`, `@classytic/repo-core` `>=0.1.0`, Node.js `>=22`.
12
+ Requires Mongoose `>=9.4.1`, `@classytic/repo-core` `>=0.3.0`, Node.js `>=22`.
13
13
 
14
14
  > **Swap-able with sqlitekit.** Mongokit implements the `StandardRepo<TDoc>` contract from `@classytic/repo-core/repository`. Controller code written against the contract runs unchanged on [@classytic/sqlitekit](https://www.npmjs.com/package/@classytic/sqlitekit) — both kits share an identical conformance suite.
15
15
 
@@ -180,7 +180,7 @@ Sketch:
180
180
  ```ts
181
181
  // host: src/outbox/wire.ts
182
182
  import { wireOutbox, MongoOutboxStore } from './outbox-recipe.js';
183
- import { RedisEventTransport } from '@classytic/arc/events';
183
+ import { RedisEventTransport } from '@classytic/arc/events/redis';
184
184
 
185
185
  const store = new MongoOutboxStore({ connection: mongoose.connection, name: 'outbox' });
186
186
  const transport = new RedisEventTransport({ url: process.env.REDIS_URL });
@@ -1,4 +1,4 @@
1
- import { r as warn } from "./logger-DJ7egMnd.mjs";
1
+ import { r as warn } from "./logger-8-VS1za6.mjs";
2
2
  import { t as createError } from "./error-B00pI7dV.mjs";
3
3
  import { n as encodeCursor, r as resolveCursorFilter } from "./cursor-J84rR6wG.mjs";
4
4
  import { r as validateKeysetSort, t as getPrimaryField } from "./sort-CrZjTK25.mjs";
@@ -1,2 +1,2 @@
1
- import { a as create_d_exports, i as delete_d_exports, n as update_d_exports, o as aggregate_d_exports, r as read_d_exports } from "../index-dNTmpgK8.mjs";
1
+ import { a as create_d_exports, i as delete_d_exports, n as update_d_exports, o as aggregate_d_exports, r as read_d_exports } from "../index-D9cORR-o.mjs";
2
2
  export { aggregate_d_exports as aggregate, create_d_exports as create, delete_d_exports as deleteActions, read_d_exports as read, update_d_exports as update };
@@ -1,5 +1,5 @@
1
1
  import { t as __exportAll } from "../chunk-CfYAbeIz.mjs";
2
- import { g as create_exports, i as update_exports, p as delete_exports, u as read_exports, v as aggregate_exports } from "../update-DEJYcgfV.mjs";
2
+ import { g as create_exports, i as update_exports, p as delete_exports, u as read_exports, v as aggregate_exports } from "../update-CDxPtpsk.mjs";
3
3
  //#region src/actions/index.ts
4
4
  var actions_exports = /* @__PURE__ */ __exportAll({
5
5
  aggregate: () => aggregate_exports,
@@ -1,4 +1,4 @@
1
- import { q as Plugin } from "../types-Dc2aDVUi.mjs";
1
+ import { W as Plugin } from "../types-DXscZnLu.mjs";
2
2
  import { PipelineStage } from "mongoose";
3
3
 
4
4
  //#region src/ai/types.d.ts
package/dist/ai/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { r as warn } from "../logger-DJ7egMnd.mjs";
1
+ import { r as warn } from "../logger-8-VS1za6.mjs";
2
2
  //#region src/ai/url-policy.ts
3
3
  /**
4
4
  * URL policy helpers for the vector plugin media pipeline.
@@ -45,7 +45,7 @@ function matchesOriginEntry(origin, entry, url) {
45
45
  if (wildcardMatch) {
46
46
  const [, entryProtocol, suffix] = wildcardMatch;
47
47
  if (url.protocol.toLowerCase() !== entryProtocol.toLowerCase()) return false;
48
- return url.hostname.toLowerCase().endsWith("." + suffix.toLowerCase());
48
+ return url.hostname.toLowerCase().endsWith(`.${suffix.toLowerCase()}`);
49
49
  }
50
50
  return false;
51
51
  }
@@ -137,7 +137,7 @@ async function withVectorErrorHints(op, ctx) {
137
137
  } catch (err) {
138
138
  const hint = classifyVectorSearchError(err, ctx);
139
139
  if (hint.code === "UNKNOWN") throw err;
140
- const message = `[mongokit:vector] ${hint.summary}\n` + hint.hints.map((h) => ` • ${h}`).join("\n");
140
+ const message = `[mongokit:vector] ${hint.summary}\n${hint.hints.map((h) => ` • ${h}`).join("\n")}`;
141
141
  const wrapped = new Error(message, { cause: err });
142
142
  wrapped.code = hint.code;
143
143
  throw wrapped;
@@ -1,4 +1,4 @@
1
- import { $ as ReadPreferenceType, B as ObjectId, D as GroupResult, E as FindOneAndUpdateOptions, R as MinMaxResult, U as OperationOptions, X as PopulateSpec, a as AnyDocument, b as DeleteResult, ft as SortSpec, g as CreateOptions, gt as UpdateOptions, ht as UpdateManyResult, kt as LookupOptions, ot as SelectSpec, vt as UpdateWithValidationResult } from "./types-Dc2aDVUi.mjs";
1
+ import { D as FindOneAndUpdateOptions, Dt as LookupOptions, L as MinMaxResult, O as GroupResult, V as OperationOptions, X as ReadPreferenceType, ft as UpdateManyResult, g as CreateOptions, ht as UpdateWithValidationResult, i as AnyDocument, lt as SortSpec, pt as UpdateOptions, q as PopulateSpec, rt as SelectSpec, x as DeleteResult, z as ObjectId } from "./types-DXscZnLu.mjs";
2
2
  import { Model, PipelineStage } from "mongoose";
3
3
 
4
4
  //#region src/actions/aggregate.d.ts
package/dist/index.d.mts CHANGED
@@ -1,15 +1,16 @@
1
- import { $ as ReadPreferenceType, A as InferDocument, B as ObjectId, C as EventPayload, Ct as WithTransactionOptions, D as GroupResult, Dt as IResponseFormatter, E as FindOneAndUpdateOptions, Et as IRequestContext, F as Logger, G as PaginationResult, H as OffsetPaginationResult, I as LookupPopulateOptions, J as PluginFunction, K as PartialBy, L as LookupPopulateResult, M as KeysOfType, N as KeysetPaginationOptions, O as HookMode, Ot as LookupBuilder, P as KeysetPaginationResult, Q as ReadOptions, R as MinMaxResult, S as EventHandlers, St as WithPlugins, T as FieldPreset, Tt as IControllerResponse, U as OperationOptions, V as OffsetPaginationOptions, W as PaginationConfig, X as PopulateSpec, Y as PluginType, Z as PrioritizedHook, _ as DecodedCursor, _t as UpdatePatch, a as AnyDocument, at as RequiredBy, b as DeleteResult, bt as ValidationChainOptions, c as CacheOperationOptions, ct as SoftDeleteFilterMode, d as CacheableOptions, dt as SortDirection, et as RepositoryContext, f as CascadeOptions, ft as SortSpec, g as CreateOptions, gt as UpdateOptions, h as CreateInput, ht as UpdateManyResult, i as AllPluginMethods, it as RepositoryOptions, j as InferRawDoc, k as HttpError, kt as LookupOptions, l as CacheOptions, lt as SoftDeleteOptions, m as CollationOptions, mt as UpdateInput, n as AggregatePaginationOptions, nt as RepositoryInstance, o as AnyModel, ot as SelectSpec, p as CascadeRelation, pt as Strict, q as Plugin, r as AggregatePaginationResult, rt as RepositoryOperation, s as CacheAdapter, st as SessionOptions, t as AggregateOptions, tt as RepositoryEvent, u as CacheStats, ut as SoftDeleteRepository, v as DeepPartial, vt as UpdateWithValidationResult, w as EventPhase, wt as IController, x as DocField, xt as ValidatorDefinition, y as DeleteManyResult, yt as UserContext, z as NonNullableFields } from "./types-Dc2aDVUi.mjs";
2
- import { t as index_d_exports } from "./index-dNTmpgK8.mjs";
1
+ import { $ as RepositoryInstance, A as InferDocument, B as OffsetPaginationOptions, C as EventHandlers, Ct as IControllerResponse, D as FindOneAndUpdateOptions, Dt as LookupOptions, E as FieldPreset, Et as LookupBuilder, F as LookupPopulateOptions, G as PluginFunction, H as PaginationConfig, I as LookupPopulateResult, J as PrioritizedHook, K as PluginType, L as MinMaxResult, M as KeysOfType, N as KeysetPaginationOptions, O as GroupResult, P as Logger, Q as RepositoryEvent, R as NonNullableFields, S as DocField, St as IController, T as EventPhase, Tt as IResponseFormatter, U as PartialBy, V as OperationOptions, W as Plugin, X as ReadPreferenceType, Y as ReadOptions, Z as RepositoryContext, _ as CursorPayload, _t as ValidationChainOptions, a as AnyModel, at as SoftDeleteFilterMode, b as DeleteManyResult, bt as WithPlugins, c as CacheOperationOptions, ct as SortDirection, d as CacheableOptions, dt as UpdateInput, et as RepositoryOperation, f as CascadeOptions, ft as UpdateManyResult, g as CreateOptions, gt as UserContext, h as CreateInput, ht as UpdateWithValidationResult, i as AnyDocument, it as SessionOptions, j as InferRawDoc, k as HookMode, l as CacheOptions, lt as SortSpec, m as CollationOptions, mt as UpdatePatch, n as AggregatePaginationOptions, nt as RequiredBy, o as BasePaginationOptions, ot as SoftDeleteOptions, p as CascadeRelation, pt as UpdateOptions, q as PopulateSpec, r as AllPluginMethods, rt as SelectSpec, s as CacheAdapter, st as SoftDeleteRepository, t as AggregateOptions, tt as RepositoryOptions, u as CacheStats, ut as Strict, v as DecodedCursor, vt as ValidatorDefinition, w as EventPayload, wt as IRequestContext, x as DeleteResult, xt as WithTransactionOptions, y as DeepPartial, yt as ValueType, z as ObjectId } from "./types-DXscZnLu.mjs";
2
+ import { t as index_d_exports } from "./index-D9cORR-o.mjs";
3
3
  import { PaginationEngine } from "./pagination/PaginationEngine.mjs";
4
- import { $ as aggregateHelpersPlugin, A as PrefixedIdOptions, B as BatchOperationsMethods, C as methodRegistryPlugin, D as CustomIdOptions, E as elasticSearchPlugin, F as prefixedId, G as AuditQueryOptions, H as batchOperationsPlugin, I as sequentialId, J as AuditTrailOptions, K as AuditQueryResult, L as cascadePlugin, M as customIdPlugin, N as dateSequentialId, O as DateSequentialIdOptions, P as getNextSequence, Q as AggregateHelpersMethods, R as CacheMethods, T as ElasticSearchOptions, U as AuditEntry, V as BulkWriteResult, W as AuditOperation, X as auditTrailPlugin, Y as AuditTrailQuery, Z as auditLogPlugin, _ as observabilityPlugin, a as uniqueField, b as MongoOperationsMethods, c as TenantContext, d as SubdocumentMethods, f as subdocumentPlugin, g as OperationMetric, h as ObservabilityOptions, i as requireField, j as SequentialIdOptions, k as IdGenerator, l as TenantStore, m as softDeletePlugin, n as blockIf, o as validationChainPlugin, p as SoftDeleteMethods, q as AuditTrailMethods, r as immutableField, s as timestampPlugin, t as autoInject, u as createTenantContext, v as MultiTenantOptions, w as fieldFilterPlugin, x as mongoOperationsPlugin, y as multiTenantPlugin, z as cachePlugin } from "./validation-chain.plugin-B0VSdcwa.mjs";
4
+ import { $ as aggregateHelpersPlugin, A as PrefixedIdOptions, B as BatchOperationsMethods, C as methodRegistryPlugin, D as CustomIdOptions, E as elasticSearchPlugin, F as prefixedId, G as AuditQueryOptions, H as batchOperationsPlugin, I as sequentialId, J as AuditTrailOptions, K as AuditQueryResult, L as cascadePlugin, M as customIdPlugin, N as dateSequentialId, O as DateSequentialIdOptions, P as getNextSequence, Q as AggregateHelpersMethods, R as CacheMethods, T as ElasticSearchOptions, U as AuditEntry, V as BulkWriteResult, W as AuditOperation, X as auditTrailPlugin, Y as AuditTrailQuery, Z as auditLogPlugin, _ as observabilityPlugin, a as uniqueField, b as MongoOperationsMethods, c as TenantContext, d as SubdocumentMethods, f as subdocumentPlugin, g as OperationMetric, h as ObservabilityOptions, i as requireField, j as SequentialIdOptions, k as IdGenerator, l as TenantStore, m as softDeletePlugin, n as blockIf, o as validationChainPlugin, p as SoftDeleteMethods, q as AuditTrailMethods, r as immutableField, s as timestampPlugin, t as autoInject, u as createTenantContext, v as MultiTenantOptions, w as fieldFilterPlugin, x as mongoOperationsPlugin, y as multiTenantPlugin, z as cachePlugin } from "./validation-chain.plugin-CH-xPoKM.mjs";
5
5
  import { n as SchemaPathsLike, t as FieldType$1 } from "./coercion-oM8G4b6m.mjs";
6
6
  import { n as SchemaIndexes } from "./indexes-C26GNZRU.mjs";
7
- import { a as createFieldPreset, c as getMongooseProjection, d as parseDuplicateKeyError, i as configureLogger, l as createError, n as buildCrudSchemasFromMongooseSchema, o as filterResponseData, r as createMemoryCache, s as getFieldsForUser, t as buildCrudSchemasFromModel, u as isDuplicateKeyError } from "./mongooseToJsonSchema-DXxjFkLB.mjs";
7
+ import { a as createFieldPreset, c as getMongooseProjection, d as parseDuplicateKeyError, i as configureLogger, l as createError, n as buildCrudSchemasFromMongooseSchema, o as filterResponseData, r as createMemoryCache, s as getFieldsForUser, t as buildCrudSchemasFromModel, u as isDuplicateKeyError } from "./mongooseToJsonSchema-rtkdbBhf.mjs";
8
8
  import * as _$mongoose from "mongoose";
9
9
  import { ClientSession, Expression, Model, PipelineStage, PopulateOptions } from "mongoose";
10
10
  import { AggPaginationRequest, AggRequest, AggResult, RepositoryBase } from "@classytic/repo-core/repository";
11
11
  import { UpdateInput as UpdateInput$1 } from "@classytic/repo-core/update";
12
- import { OffsetPaginationResultCore } from "@classytic/repo-core/pagination";
12
+ import { AggregatePaginationResult, KeysetPaginationResult, OffsetPaginationResult, OffsetPaginationResultCore } from "@classytic/repo-core/pagination";
13
+ import { HttpError } from "@classytic/repo-core/errors";
13
14
 
14
15
  //#region src/operations.d.ts
15
16
  /**
@@ -1210,12 +1211,16 @@ declare class Repository<TDoc = unknown> extends RepositoryBase {
1210
1211
  * Merge policy-hook-injected filters (multi-tenant scope,
1211
1212
  * soft-delete) into the pre-aggregate `filter` slot of the request.
1212
1213
  * Plugins speak mongo-query shape (`context.query` / `context.filters`);
1213
- * we merge those with the portable `req.filter` using `$and`.
1214
+ * the caller's `req.filter` may be either Filter IR or a mongo query.
1214
1215
  *
1215
- * When the caller's filter is a Filter IR node we don't unwrap it —
1216
- * the pipeline compiler does Filter→mongo translation. Merging here
1217
- * as a mongo-shaped `$and` is safe because `compileFilterToMongo`
1218
- * passes through already-mongo queries unchanged.
1216
+ * Each part is compiled through `compileFilterToMongo` BEFORE the
1217
+ * `$and` merge — otherwise an IR node like `eq('status', 'paid')`
1218
+ * (`{ op, field, value }`) gets nested inside `$and` as a literal,
1219
+ * `compileFilterToMongo` of the merged value finds no top-level `op`
1220
+ * (so `isFilter` is false), and Mongo receives the IR object as a
1221
+ * raw query clause that matches no real document. Symptom: aggregates
1222
+ * silently return 0 rows whenever a callerpasses IR + a policy
1223
+ * plugin is installed.
1219
1224
  */
1220
1225
  private _injectPolicyScopeIntoAgg;
1221
1226
  /**
@@ -1472,4 +1477,4 @@ declare function isTransactionUnsupported(error: Error): boolean;
1472
1477
  */
1473
1478
  declare function createRepository<TDoc>(Model: _$mongoose.Model<TDoc, any, any, any>, plugins?: PluginType[], paginationConfig?: PaginationConfig, options?: RepositoryOptions): Repository<TDoc>;
1474
1479
  //#endregion
1475
- export { ALL_OPERATIONS, type AggregateHelpersMethods, type AggregateOptions, type AggregatePaginationOptions, type AggregatePaginationResult, AggregationBuilder, type AllPluginMethods, type AnyDocument, type AnyModel, type AuditEntry, type AuditOperation, type AuditQueryOptions, type AuditQueryResult, type AuditTrailMethods, type AuditTrailOptions, AuditTrailQuery, type BatchOperationsMethods, type BulkWriteResult, type CacheAdapter, type CacheMethods, type CacheOperationOptions, type CacheOptions, type CacheStats, type CacheableOptions, type CascadeOptions, type CascadeRelation, type CollationOptions, type CreateInput, type CreateOptions, type CustomIdOptions, type DateSequentialIdOptions, type DecodedCursor, type DeepPartial, type DeleteResult, type DocField, type ElasticSearchOptions, type EventHandlers, type EventPayload, type EventPhase, type FieldPreset, type FieldType, type FilterQuery, type FindOneAndUpdateOptions, type GroupResult, HOOK_PRIORITY, type HookMode, type HttpError, type IController, type IControllerResponse, type IRequestContext, type IResponseFormatter, type IdGenerator, type InferDocument, type InferRawDoc, type KeysOfType, type KeysetPaginationOptions, type KeysetPaginationResult, type Logger, LookupBuilder, type LookupOptions, type LookupPopulateOptions, type LookupPopulateResult, MUTATING_OPERATIONS, type MinMaxResult, type MongoOperationsMethods, type MultiTenantOptions, type NonNullableFields, OP_REGISTRY, type ObjectId, type ObservabilityOptions, type OffsetPaginationOptions, type OffsetPaginationResult, type OperationDescriptor, type OperationMetric, type OperationOptions, type PaginationConfig, PaginationEngine, type PaginationResult, type ParsedQuery, type PartialBy, type Plugin, type PluginFunction, type PluginType, type PolicyKey, type PopulateOption, type PopulateSpec, type PrefixedIdOptions, QueryParser, type QueryParserOptions, READ_OPERATIONS, type ReadOptions, type ReadPreferenceType, Repository, Repository as default, type RepositoryContext, type RepositoryEvent, type RepositoryInstance, type RepositoryOperation, type RepositoryOptions, type RequiredBy, type SchemaLike, type SearchMode, type SelectSpec, type SequentialIdOptions, type SessionOptions, type SoftDeleteFilterMode, type SoftDeleteMethods, type SoftDeleteOptions, type SoftDeleteRepository, type SortDirection, type SortSpec, type Strict, type SubdocumentMethods, type TenantContext, type TenantStore, type UpdateInput, type UpdateManyResult, type UpdateOptions, type UpdatePatch, type UpdateWithValidationResult, type UserContext, type ValidationChainOptions, type ValidatorDefinition, type WithPlugins, type WithTransactionOptions, index_d_exports as actions, aggregateHelpersPlugin, auditLogPlugin, auditTrailPlugin, autoInject, batchOperationsPlugin, blockIf, buildCrudSchemasFromModel, buildCrudSchemasFromMongooseSchema, cachePlugin, cascadePlugin, configureLogger, createError, createFieldPreset, createMemoryCache, createRepository, createTenantContext, customIdPlugin, dateSequentialId, elasticSearchPlugin, fieldFilterPlugin, filterResponseData, getFieldsForUser, getMongooseProjection, getNextSequence, immutableField, isDuplicateKeyError, isTransactionUnsupported, methodRegistryPlugin, mongoOperationsPlugin, multiTenantPlugin, observabilityPlugin, operationsByPolicyKey, parseDuplicateKeyError, prefixedId, requireField, sequentialId, softDeletePlugin, subdocumentPlugin, timestampPlugin, uniqueField, validationChainPlugin, withTransaction };
1480
+ export { ALL_OPERATIONS, type AggregateHelpersMethods, type AggregateOptions, type AggregatePaginationOptions, AggregationBuilder, type AllPluginMethods, type AnyDocument, type AnyModel, type AuditEntry, type AuditOperation, type AuditQueryOptions, type AuditQueryResult, type AuditTrailMethods, type AuditTrailOptions, AuditTrailQuery, type BasePaginationOptions, type BatchOperationsMethods, type BulkWriteResult, type CacheAdapter, type CacheMethods, type CacheOperationOptions, type CacheOptions, type CacheStats, type CacheableOptions, type CascadeOptions, type CascadeRelation, type CollationOptions, type CreateInput, type CreateOptions, type CursorPayload, type CustomIdOptions, type DateSequentialIdOptions, type DecodedCursor, type DeepPartial, type DeleteResult, type DocField, type ElasticSearchOptions, type EventHandlers, type EventPayload, type EventPhase, type FieldPreset, type FieldType, type FilterQuery, type FindOneAndUpdateOptions, type GroupResult, HOOK_PRIORITY, type HookMode, type IController, type IControllerResponse, type IRequestContext, type IResponseFormatter, type IdGenerator, type InferDocument, type InferRawDoc, type KeysOfType, type KeysetPaginationOptions, type Logger, LookupBuilder, type LookupOptions, type LookupPopulateOptions, type LookupPopulateResult, MUTATING_OPERATIONS, type MinMaxResult, type MongoOperationsMethods, type MultiTenantOptions, type NonNullableFields, OP_REGISTRY, type ObjectId, type ObservabilityOptions, type OffsetPaginationOptions, type OperationDescriptor, type OperationMetric, type OperationOptions, type PaginationConfig, PaginationEngine, type ParsedQuery, type PartialBy, type Plugin, type PluginFunction, type PluginType, type PolicyKey, type PopulateOption, type PopulateSpec, type PrefixedIdOptions, type PrioritizedHook, QueryParser, type QueryParserOptions, READ_OPERATIONS, type ReadOptions, type ReadPreferenceType, Repository, Repository as default, type RepositoryContext, type RepositoryEvent, type RepositoryInstance, type RepositoryOperation, type RepositoryOptions, type RequiredBy, type SchemaLike, type SearchMode, type SelectSpec, type SequentialIdOptions, type SessionOptions, type SoftDeleteFilterMode, type SoftDeleteMethods, type SoftDeleteOptions, type SoftDeleteRepository, type SortDirection, type SortSpec, type Strict, type SubdocumentMethods, type TenantContext, type TenantStore, type UpdateInput, type UpdateManyResult, type UpdateOptions, type UpdatePatch, type UpdateWithValidationResult, type UserContext, type ValidationChainOptions, type ValidatorDefinition, type ValueType, type WithPlugins, type WithTransactionOptions, index_d_exports as actions, aggregateHelpersPlugin, auditLogPlugin, auditTrailPlugin, autoInject, batchOperationsPlugin, blockIf, buildCrudSchemasFromModel, buildCrudSchemasFromMongooseSchema, cachePlugin, cascadePlugin, configureLogger, createError, createFieldPreset, createMemoryCache, createRepository, createTenantContext, customIdPlugin, dateSequentialId, elasticSearchPlugin, fieldFilterPlugin, filterResponseData, getFieldsForUser, getMongooseProjection, getNextSequence, immutableField, isDuplicateKeyError, isTransactionUnsupported, methodRegistryPlugin, mongoOperationsPlugin, multiTenantPlugin, observabilityPlugin, operationsByPolicyKey, parseDuplicateKeyError, prefixedId, requireField, sequentialId, softDeletePlugin, subdocumentPlugin, timestampPlugin, uniqueField, validationChainPlugin, withTransaction };
package/dist/index.mjs CHANGED
@@ -1,14 +1,14 @@
1
- import { r as warn, t as configureLogger } from "./logger-DJ7egMnd.mjs";
2
- import { b as LookupBuilder } from "./update-DEJYcgfV.mjs";
1
+ import { b as LookupBuilder } from "./update-CDxPtpsk.mjs";
2
+ import { r as warn, t as configureLogger } from "./logger-8-VS1za6.mjs";
3
3
  import { n as isDuplicateKeyError, r as parseDuplicateKeyError, t as createError } from "./error-B00pI7dV.mjs";
4
4
  import { t as actions_exports } from "./actions/index.mjs";
5
- import { A as AuditTrailQuery, C as cachePlugin, D as withTransaction, E as isTransactionUnsupported, F as MUTATING_OPERATIONS, I as OP_REGISTRY, L as READ_OPERATIONS, M as auditLogPlugin, N as aggregateHelpersPlugin, O as AggregationBuilder, P as ALL_OPERATIONS, R as operationsByPolicyKey, S as cascadePlugin, T as Repository, _ as customIdPlugin, a as uniqueField, b as prefixedId, c as createTenantContext, d as observabilityPlugin, f as multiTenantPlugin, g as elasticSearchPlugin, h as fieldFilterPlugin, i as requireField, j as auditTrailPlugin, k as batchOperationsPlugin, l as subdocumentPlugin, m as methodRegistryPlugin, n as blockIf, o as validationChainPlugin, p as mongoOperationsPlugin, r as immutableField, s as timestampPlugin, t as autoInject, u as softDeletePlugin, v as dateSequentialId, w as HOOK_PRIORITY, x as sequentialId, y as getNextSequence } from "./validation-chain.plugin-Dllv-cml.mjs";
6
- import { t as PaginationEngine } from "./PaginationEngine-CvDFfVXT.mjs";
5
+ import { A as AuditTrailQuery, C as cachePlugin, D as withTransaction, E as isTransactionUnsupported, F as MUTATING_OPERATIONS, I as OP_REGISTRY, L as READ_OPERATIONS, M as auditLogPlugin, N as aggregateHelpersPlugin, O as AggregationBuilder, P as ALL_OPERATIONS, R as operationsByPolicyKey, S as cascadePlugin, T as Repository, _ as customIdPlugin, a as uniqueField, b as prefixedId, c as createTenantContext, d as observabilityPlugin, f as multiTenantPlugin, g as elasticSearchPlugin, h as fieldFilterPlugin, i as requireField, j as auditTrailPlugin, k as batchOperationsPlugin, l as subdocumentPlugin, m as methodRegistryPlugin, n as blockIf, o as validationChainPlugin, p as mongoOperationsPlugin, r as immutableField, s as timestampPlugin, t as autoInject, u as softDeletePlugin, v as dateSequentialId, w as HOOK_PRIORITY, x as sequentialId, y as getNextSequence } from "./validation-chain.plugin-BicIgzzt.mjs";
6
+ import { t as PaginationEngine } from "./PaginationEngine-BDaAsyPn.mjs";
7
7
  import { isGeoOperator, parseGeoFilter } from "./query/primitives/geo.mjs";
8
8
  import { i as getMongooseProjection, n as filterResponseData, r as getFieldsForUser, t as createFieldPreset } from "./field-selection-dI0m4RBs.mjs";
9
9
  import { buildFieldTypeMap, coerceFieldValue, coerceHeuristic } from "./query/primitives/coercion.mjs";
10
10
  import { extractSchemaIndexes } from "./query/primitives/indexes.mjs";
11
- import { n as buildCrudSchemasFromMongooseSchema, r as createMemoryCache, t as buildCrudSchemasFromModel } from "./mongooseToJsonSchema-D1sdCOmj.mjs";
11
+ import { n as buildCrudSchemasFromMongooseSchema, r as createMemoryCache, t as buildCrudSchemasFromModel } from "./mongooseToJsonSchema-B187ufOP.mjs";
12
12
  //#region src/query/QueryParser.ts
13
13
  /**
14
14
  * Top-level query parameters that mongokit handles outside the filter
@@ -110,9 +110,11 @@ function isObjectIdType(t) {
110
110
  */
111
111
  function buildCrudSchemasFromMongooseSchema(mongooseSchema, options = {}) {
112
112
  const jsonCreate = buildJsonSchemaFromPaths(mongooseSchema, options);
113
+ const jsonUpdate = buildJsonSchemaForUpdate(jsonCreate, options);
114
+ const jsonResponse = buildJsonSchemaForResponse(mongooseSchema, options);
113
115
  return {
114
116
  createBody: jsonCreate,
115
- updateBody: buildJsonSchemaForUpdate(jsonCreate, options),
117
+ updateBody: jsonUpdate,
116
118
  params: {
117
119
  type: "object",
118
120
  properties: { id: {
@@ -121,11 +123,18 @@ function buildCrudSchemasFromMongooseSchema(mongooseSchema, options = {}) {
121
123
  } },
122
124
  required: ["id"]
123
125
  },
124
- listQuery: buildJsonSchemaForQuery(mongooseSchema?.obj || {}, options)
126
+ listQuery: buildJsonSchemaForQuery(mongooseSchema?.obj || {}, options),
127
+ response: jsonResponse
125
128
  };
126
129
  }
127
130
  /**
128
- * Build CRUD schemas from Mongoose model
131
+ * Build CRUD schemas from Mongoose model.
132
+ *
133
+ * Satisfies the canonical `SchemaGenerator<Model<unknown>>` contract from
134
+ * `@classytic/repo-core/schema`, so arc's `MongooseAdapter.schemaGenerator`
135
+ * field accepts this function directly with no glue. Future Mongo-shaped
136
+ * kits implementing the same contract are interchangeable at the adapter
137
+ * boundary.
129
138
  */
130
139
  function buildCrudSchemasFromModel(mongooseModel, options = {}) {
131
140
  if (!mongooseModel?.schema) throw new Error("Invalid mongoose model");
@@ -249,10 +258,7 @@ function introspectArrayItems(schemaType, options = {}) {
249
258
  const declaredType = schemaType?.options?.type;
250
259
  if (Array.isArray(declaredType) && declaredType.length > 0) {
251
260
  const inner = declaredType[0];
252
- if (inner === mongoose.Schema.Types.Mixed) return {
253
- type: "object",
254
- additionalProperties: true
255
- };
261
+ if (inner === mongoose.Schema.Types.Mixed) return { additionalProperties: true };
256
262
  return jsonTypeFor(inner, options, /* @__PURE__ */ new WeakSet());
257
263
  }
258
264
  return {
@@ -328,6 +334,7 @@ function schemaTypeToJsonSchema(schemaType, builderOptions = {}) {
328
334
  if (ofDef !== void 0 && ofDef !== null) result.additionalProperties = jsonTypeFor(ofDef, {}, /* @__PURE__ */ new WeakSet());
329
335
  else result.additionalProperties = true;
330
336
  } else if (hasInnerSchema(schemaType)) return subSchemaToJsonSchema(schemaType.schema, builderOptions);
337
+ else if (instance === "Mixed") result.additionalProperties = true;
331
338
  else {
332
339
  result.type = "object";
333
340
  result.additionalProperties = true;
@@ -335,6 +342,7 @@ function schemaTypeToJsonSchema(schemaType, builderOptions = {}) {
335
342
  if (options.default === null && typeof result.type === "string") {
336
343
  result.type = [result.type, "null"];
337
344
  result.default = null;
345
+ if (Array.isArray(result.enum) && !result.enum.includes(null)) result.enum = [...result.enum, null];
338
346
  }
339
347
  if (typeof options.description === "string" && options.description.length > 0) result.description = options.description;
340
348
  if (typeof options.title === "string" && options.title.length > 0) result.title = options.title;
@@ -348,10 +356,7 @@ function jsonTypeFor(def, options, seen) {
348
356
  if (Array.isArray(def)) {
349
357
  if (def[0] === mongoose.Schema.Types.Mixed) return {
350
358
  type: "array",
351
- items: {
352
- type: "object",
353
- additionalProperties: true
354
- }
359
+ items: { additionalProperties: true }
355
360
  };
356
361
  return {
357
362
  type: "array",
@@ -368,10 +373,7 @@ function jsonTypeFor(def, options, seen) {
368
373
  const inner = typedDef.type[0] !== void 0 ? typedDef.type[0] : String;
369
374
  if (inner === mongoose.Schema.Types.Mixed) return {
370
375
  type: "array",
371
- items: {
372
- type: "object",
373
- additionalProperties: true
374
- }
376
+ items: { additionalProperties: true }
375
377
  };
376
378
  return {
377
379
  type: "array",
@@ -407,10 +409,7 @@ function jsonTypeFor(def, options, seen) {
407
409
  type: "object",
408
410
  additionalProperties: jsonTypeFor(typedDef.of || String, options, seen)
409
411
  };
410
- if (typedDef.type === mongoose.Schema.Types.Mixed) return {
411
- type: "object",
412
- additionalProperties: true
413
- };
412
+ if (typedDef.type === mongoose.Schema.Types.Mixed) return { additionalProperties: true };
414
413
  if (typedDef.type === Object) {
415
414
  if (isPlainObject(typedDef) && Object.keys(typedDef).some((k) => k !== "type")) {
416
415
  const nested = {};
@@ -514,6 +513,55 @@ function buildJsonSchemaForUpdate(createJson, options) {
514
513
  if (options?.update?.requireAtLeastOne === true) clone.minProperties = 1;
515
514
  return clone;
516
515
  }
516
+ /**
517
+ * Build the response-shape JSON Schema.
518
+ *
519
+ * Walks every schema path INCLUDING server-set fields (`createdAt`,
520
+ * `updatedAt`, `_id`, immutable / readonly fields) because those ARE
521
+ * returned to clients. Only `fieldRules[field].hidden: true` fields are
522
+ * stripped — passwords, secrets, internal scoring.
523
+ *
524
+ * `additionalProperties: true` so virtuals, computed fields, and any
525
+ * non-schema-declared keys flow through. Strict response validation is
526
+ * opt-in via `options.strictAdditionalProperties` (rare; most hosts want
527
+ * lean responses but don't want to break virtuals).
528
+ *
529
+ * No `required` array — response schemas are *documentation* of what
530
+ * MAY be returned. Marking fields required would force every endpoint
531
+ * that occasionally returns sparse data (lookup-by-id with selected
532
+ * fields, partial projections) to fail validation.
533
+ */
534
+ function buildJsonSchemaForResponse(mongooseSchema, options) {
535
+ const fullSchema = buildJsonSchemaFromPaths(mongooseSchema, options);
536
+ const responseSchema = buildJsonSchemaFromPaths(mongooseSchema, {
537
+ ...options,
538
+ fieldRules: stripCreateOnlyFieldRulesForResponse(options.fieldRules),
539
+ create: void 0
540
+ });
541
+ applyFieldRules(responseSchema, collectFieldsToOmit(options, "response"), options);
542
+ responseSchema.required = void 0;
543
+ if (options?.strictAdditionalProperties !== true) responseSchema.additionalProperties = true;
544
+ if (fullSchema.title && !responseSchema.title) responseSchema.title = fullSchema.title;
545
+ return responseSchema;
546
+ }
547
+ /**
548
+ * Strip request-side-only field-rule flags (`systemManaged`, `immutable`,
549
+ * `immutableAfterCreate`, `optional`) from a fieldRules map so the
550
+ * response builder doesn't accidentally hide server-set fields.
551
+ *
552
+ * Keeps `hidden` (response-relevant), `nullable`, `description`,
553
+ * constraint flags (`minLength`/`maxLength`/`min`/`max`/`pattern`/`enum`)
554
+ * — they all describe response shape, not request acceptance.
555
+ */
556
+ function stripCreateOnlyFieldRulesForResponse(rules) {
557
+ if (!rules) return rules;
558
+ const result = {};
559
+ for (const [field, rule] of Object.entries(rules)) {
560
+ const { systemManaged: _sm, immutable: _i, immutableAfterCreate: _iac, optional: _o, ...rest } = rule;
561
+ result[field] = rest;
562
+ }
563
+ return result;
564
+ }
517
565
  function buildJsonSchemaForQuery(_tree, options) {
518
566
  const basePagination = {
519
567
  type: "object",
@@ -1,6 +1,7 @@
1
- import { T as FieldPreset, k as HttpError, s as CacheAdapter, yt as UserContext } from "./types-Dc2aDVUi.mjs";
1
+ import { E as FieldPreset, gt as UserContext, s as CacheAdapter } from "./types-DXscZnLu.mjs";
2
2
  import mongoose, { Schema } from "mongoose";
3
3
  import { CrudSchemas, SchemaBuilderOptions } from "@classytic/repo-core/schema";
4
+ import { HttpError } from "@classytic/repo-core/errors";
4
5
 
5
6
  //#region src/utils/error.d.ts
6
7
  /** Optional structured fields for createError. */
@@ -205,7 +206,13 @@ type CrudSchemasFramework = CrudSchemas & Record<string, unknown>;
205
206
  */
206
207
  declare function buildCrudSchemasFromMongooseSchema(mongooseSchema: Schema, options?: SchemaBuilderOptions): CrudSchemasFramework;
207
208
  /**
208
- * Build CRUD schemas from Mongoose model
209
+ * Build CRUD schemas from Mongoose model.
210
+ *
211
+ * Satisfies the canonical `SchemaGenerator<Model<unknown>>` contract from
212
+ * `@classytic/repo-core/schema`, so arc's `MongooseAdapter.schemaGenerator`
213
+ * field accepts this function directly with no glue. Future Mongo-shaped
214
+ * kits implementing the same contract are interchangeable at the adapter
215
+ * boundary.
209
216
  */
210
217
  declare function buildCrudSchemasFromModel(mongooseModel: mongoose.Model<unknown>, options?: SchemaBuilderOptions): CrudSchemasFramework;
211
218
  //#endregion
@@ -1,5 +1,6 @@
1
- import { H as OffsetPaginationResult, N as KeysetPaginationOptions, P as KeysetPaginationResult, V as OffsetPaginationOptions, W as PaginationConfig, a as AnyDocument, n as AggregatePaginationOptions, r as AggregatePaginationResult } from "../types-Dc2aDVUi.mjs";
1
+ import { B as OffsetPaginationOptions, H as PaginationConfig, N as KeysetPaginationOptions, i as AnyDocument, n as AggregatePaginationOptions } from "../types-DXscZnLu.mjs";
2
2
  import { Model } from "mongoose";
3
+ import { AggregatePaginationResult, KeysetPaginationResult, OffsetPaginationResult } from "@classytic/repo-core/pagination";
3
4
 
4
5
  //#region src/pagination/PaginationEngine.d.ts
5
6
  /**
@@ -1,2 +1,2 @@
1
- import { t as PaginationEngine } from "../PaginationEngine-CvDFfVXT.mjs";
1
+ import { t as PaginationEngine } from "../PaginationEngine-BDaAsyPn.mjs";
2
2
  export { PaginationEngine };
@@ -1,2 +1,2 @@
1
- import { $ as aggregateHelpersPlugin, A as PrefixedIdOptions, B as BatchOperationsMethods, C as methodRegistryPlugin, D as CustomIdOptions, E as elasticSearchPlugin, F as prefixedId, G as AuditQueryOptions, H as batchOperationsPlugin, I as sequentialId, J as AuditTrailOptions, K as AuditQueryResult, L as cascadePlugin, M as customIdPlugin, N as dateSequentialId, O as DateSequentialIdOptions, P as getNextSequence, Q as AggregateHelpersMethods, R as CacheMethods, S as MethodRegistryRepository, T as ElasticSearchOptions, U as AuditEntry, W as AuditOperation, X as auditTrailPlugin, Y as AuditTrailQuery, Z as auditLogPlugin, _ as observabilityPlugin, a as uniqueField, b as MongoOperationsMethods, c as TenantContext, d as SubdocumentMethods, f as subdocumentPlugin, g as OperationMetric, h as ObservabilityOptions, i as requireField, j as SequentialIdOptions, k as IdGenerator, l as TenantStore, m as softDeletePlugin, n as blockIf, o as validationChainPlugin, p as SoftDeleteMethods, q as AuditTrailMethods, r as immutableField, s as timestampPlugin, t as autoInject, u as createTenantContext, v as MultiTenantOptions, w as fieldFilterPlugin, x as mongoOperationsPlugin, y as multiTenantPlugin, z as cachePlugin } from "../validation-chain.plugin-B0VSdcwa.mjs";
1
+ import { $ as aggregateHelpersPlugin, A as PrefixedIdOptions, B as BatchOperationsMethods, C as methodRegistryPlugin, D as CustomIdOptions, E as elasticSearchPlugin, F as prefixedId, G as AuditQueryOptions, H as batchOperationsPlugin, I as sequentialId, J as AuditTrailOptions, K as AuditQueryResult, L as cascadePlugin, M as customIdPlugin, N as dateSequentialId, O as DateSequentialIdOptions, P as getNextSequence, Q as AggregateHelpersMethods, R as CacheMethods, S as MethodRegistryRepository, T as ElasticSearchOptions, U as AuditEntry, W as AuditOperation, X as auditTrailPlugin, Y as AuditTrailQuery, Z as auditLogPlugin, _ as observabilityPlugin, a as uniqueField, b as MongoOperationsMethods, c as TenantContext, d as SubdocumentMethods, f as subdocumentPlugin, g as OperationMetric, h as ObservabilityOptions, i as requireField, j as SequentialIdOptions, k as IdGenerator, l as TenantStore, m as softDeletePlugin, n as blockIf, o as validationChainPlugin, p as SoftDeleteMethods, q as AuditTrailMethods, r as immutableField, s as timestampPlugin, t as autoInject, u as createTenantContext, v as MultiTenantOptions, w as fieldFilterPlugin, x as mongoOperationsPlugin, y as multiTenantPlugin, z as cachePlugin } from "../validation-chain.plugin-CH-xPoKM.mjs";
2
2
  export { type AggregateHelpersMethods, type AuditEntry, type AuditOperation, type AuditQueryOptions, type AuditQueryResult, type AuditTrailMethods, type AuditTrailOptions, AuditTrailQuery, type BatchOperationsMethods, type CacheMethods, type CustomIdOptions, type DateSequentialIdOptions, type ElasticSearchOptions, type IdGenerator, type MethodRegistryRepository, type MongoOperationsMethods, type MultiTenantOptions, type ObservabilityOptions, type OperationMetric, type PrefixedIdOptions, type SequentialIdOptions, type SoftDeleteMethods, type SubdocumentMethods, type TenantContext, type TenantStore, aggregateHelpersPlugin, auditLogPlugin, auditTrailPlugin, autoInject, batchOperationsPlugin, blockIf, cachePlugin, cascadePlugin, createTenantContext, customIdPlugin, dateSequentialId, elasticSearchPlugin, fieldFilterPlugin, getNextSequence, immutableField, methodRegistryPlugin, mongoOperationsPlugin, multiTenantPlugin, observabilityPlugin, prefixedId, requireField, sequentialId, softDeletePlugin, subdocumentPlugin, timestampPlugin, uniqueField, validationChainPlugin };
@@ -1,2 +1,2 @@
1
- import { A as AuditTrailQuery, C as cachePlugin, M as auditLogPlugin, N as aggregateHelpersPlugin, S as cascadePlugin, _ as customIdPlugin, a as uniqueField, b as prefixedId, c as createTenantContext, d as observabilityPlugin, f as multiTenantPlugin, g as elasticSearchPlugin, h as fieldFilterPlugin, i as requireField, j as auditTrailPlugin, k as batchOperationsPlugin, l as subdocumentPlugin, m as methodRegistryPlugin, n as blockIf, o as validationChainPlugin, p as mongoOperationsPlugin, r as immutableField, s as timestampPlugin, t as autoInject, u as softDeletePlugin, v as dateSequentialId, x as sequentialId, y as getNextSequence } from "../validation-chain.plugin-Dllv-cml.mjs";
1
+ import { A as AuditTrailQuery, C as cachePlugin, M as auditLogPlugin, N as aggregateHelpersPlugin, S as cascadePlugin, _ as customIdPlugin, a as uniqueField, b as prefixedId, c as createTenantContext, d as observabilityPlugin, f as multiTenantPlugin, g as elasticSearchPlugin, h as fieldFilterPlugin, i as requireField, j as auditTrailPlugin, k as batchOperationsPlugin, l as subdocumentPlugin, m as methodRegistryPlugin, n as blockIf, o as validationChainPlugin, p as mongoOperationsPlugin, r as immutableField, s as timestampPlugin, t as autoInject, u as softDeletePlugin, v as dateSequentialId, x as sequentialId, y as getNextSequence } from "../validation-chain.plugin-BicIgzzt.mjs";
2
2
  export { AuditTrailQuery, aggregateHelpersPlugin, auditLogPlugin, auditTrailPlugin, autoInject, batchOperationsPlugin, blockIf, cachePlugin, cascadePlugin, createTenantContext, customIdPlugin, dateSequentialId, elasticSearchPlugin, fieldFilterPlugin, getNextSequence, immutableField, methodRegistryPlugin, mongoOperationsPlugin, multiTenantPlugin, observabilityPlugin, prefixedId, requireField, sequentialId, softDeletePlugin, subdocumentPlugin, timestampPlugin, uniqueField, validationChainPlugin };
@@ -1,9 +1,12 @@
1
+ import * as _$_classytic_repo_core_filter0 from "@classytic/repo-core/filter";
2
+ import { Filter } from "@classytic/repo-core/filter";
1
3
  import * as _$mongoose from "mongoose";
2
4
  import { Document, Model, PipelineStage, PopulateOptions, Types } from "mongoose";
3
- import * as _$_classytic_repo_core_filter0 from "@classytic/repo-core/filter";
4
5
  import * as _$_classytic_repo_core_repository0 from "@classytic/repo-core/repository";
5
6
  import { DeleteManyResult, DeleteResult, LookupPopulateResult, UpdateManyResult } from "@classytic/repo-core/repository";
6
7
  import { ValidationResult } from "@classytic/repo-core/schema";
8
+ import { AnyPaginationResult, OffsetPaginationResult } from "@classytic/repo-core/pagination";
9
+ import { HttpError } from "@classytic/repo-core/errors";
7
10
 
8
11
  //#region src/query/LookupBuilder.d.ts
9
12
  interface LookupOptions {
@@ -17,8 +20,15 @@ interface LookupOptions {
17
20
  as?: string;
18
21
  /** Whether to unwrap array to single object */
19
22
  single?: boolean;
20
- /** Field selection on the joined collection (shorthand for $project pipeline) */
21
- select?: string | Record<string, 0 | 1>;
23
+ /**
24
+ * Field selection on the joined collection (shorthand for `$project`).
25
+ * Three accepted shapes — all map to a single `$project` stage:
26
+ * - `string` — CSV form (`'name,price'`); leading `-` marks an exclusion.
27
+ * - `readonly string[]` — array form (`['name', 'price']` / `['-status']`);
28
+ * mirrors `repo-core/lookup` `LookupSpec.select` for cross-kit parity.
29
+ * - `Record<string, 0 | 1>` — explicit Mongo projection map.
30
+ */
31
+ select?: string | readonly string[] | Record<string, 0 | 1>;
22
32
  /** Additional pipeline to run on the joined collection */
23
33
  pipeline?: PipelineStage[];
24
34
  /** Optional let variables for pipeline */
@@ -31,6 +41,18 @@ interface LookupOptions {
31
41
  };
32
42
  /** Sanitize pipeline stages (default: true). Set false only for trusted server-side pipelines */
33
43
  sanitize?: boolean;
44
+ /**
45
+ * Joined-side filter, compiled into a `$match` stage inside the lookup
46
+ * pipeline AFTER the auto-generated join correlation. Mirrors
47
+ * `repo-core/lookup/types.ts#LookupSpec.where` and sqlitekit's
48
+ * predicate-on-the-JOIN behavior, so cross-kit code that narrows the
49
+ * foreign side (`where: eq('status', 'active')`) gets identical rows
50
+ * from both backends.
51
+ *
52
+ * Accepts Filter IR or a plain mongo query — `compileFilterToMongo`
53
+ * routes each correctly.
54
+ */
55
+ where?: Filter | Record<string, unknown>;
34
56
  }
35
57
  /**
36
58
  * Fluent builder for MongoDB $lookup aggregation stage
@@ -307,7 +329,7 @@ interface IController<TDoc> {
307
329
  * }
308
330
  * ```
309
331
  */
310
- list(context: IRequestContext): Promise<IControllerResponse<PaginationResult<TDoc>>>;
332
+ list(context: IRequestContext): Promise<IControllerResponse<AnyPaginationResult<TDoc>>>;
311
333
  /**
312
334
  * Get single resource by ID
313
335
  *
@@ -457,7 +479,7 @@ interface IResponseFormatter {
457
479
  * Format paginated response
458
480
  * @param result - Pagination result from Repository.getAll()
459
481
  */
460
- paginated<T>(result: PaginationResult<T>): IControllerResponse<PaginationResult<T>>;
482
+ paginated<T>(result: AnyPaginationResult<T>): IControllerResponse<AnyPaginationResult<T>>;
461
483
  }
462
484
  //#endregion
463
485
  //#region src/types.d.ts
@@ -732,63 +754,6 @@ interface AggregatePaginationOptions {
732
754
  /** Read preference for replica sets (e.g. 'secondaryPreferred') */
733
755
  readPreference?: ReadPreferenceType;
734
756
  }
735
- /** Offset pagination result */
736
- interface OffsetPaginationResult<T = unknown> {
737
- /** Pagination method used */
738
- method: 'offset';
739
- /** Array of documents */
740
- docs: T[];
741
- /** Current page number */
742
- page: number;
743
- /** Documents per page */
744
- limit: number;
745
- /** Total document count */
746
- total: number;
747
- /** Total page count */
748
- pages: number;
749
- /** Whether next page exists */
750
- hasNext: boolean;
751
- /** Whether previous page exists */
752
- hasPrev: boolean;
753
- /** Performance warning for deep pagination */
754
- warning?: string;
755
- }
756
- /** Keyset pagination result */
757
- interface KeysetPaginationResult<T = unknown> {
758
- /** Pagination method used */
759
- method: 'keyset';
760
- /** Array of documents */
761
- docs: T[];
762
- /** Documents per page */
763
- limit: number;
764
- /** Whether more documents exist */
765
- hasMore: boolean;
766
- /** Cursor token for next page */
767
- next: string | null;
768
- }
769
- /** Aggregate pagination result */
770
- interface AggregatePaginationResult<T = unknown> {
771
- /** Pagination method used */
772
- method: 'aggregate';
773
- /** Array of documents */
774
- docs: T[];
775
- /** Current page number */
776
- page: number;
777
- /** Documents per page */
778
- limit: number;
779
- /** Total document count */
780
- total: number;
781
- /** Total page count */
782
- pages: number;
783
- /** Whether next page exists */
784
- hasNext: boolean;
785
- /** Whether previous page exists */
786
- hasPrev: boolean;
787
- /** Performance warning for deep pagination */
788
- warning?: string;
789
- }
790
- /** Union type for all pagination results */
791
- type PaginationResult<T = unknown> = OffsetPaginationResult<T> | KeysetPaginationResult<T> | AggregatePaginationResult<T>;
792
757
  /** Session-only options — shared base for lightweight operations */
793
758
  interface SessionOptions {
794
759
  /** MongoDB session for transactions */
@@ -1136,6 +1101,27 @@ interface FieldPreset {
1136
1101
  /** Additional fields for admins */
1137
1102
  admin?: string[];
1138
1103
  }
1104
+ /** Value type identifier for cursor serialization */
1105
+ type ValueType = 'date' | 'objectid' | 'boolean' | 'number' | 'string' | 'null' | 'unknown';
1106
+ /** Cursor payload */
1107
+ interface CursorPayload {
1108
+ /** Primary sort field value (legacy single-field) */
1109
+ v: string | number | boolean | null;
1110
+ /** Value type identifier (legacy single-field) */
1111
+ t: ValueType;
1112
+ /** Document ID */
1113
+ id: string;
1114
+ /** ID type identifier */
1115
+ idType: ValueType;
1116
+ /** Sort specification */
1117
+ sort: SortSpec;
1118
+ /** Cursor version */
1119
+ ver: number;
1120
+ /** Compound sort field values (multi-field keyset) */
1121
+ vals?: Record<string, string | number | boolean | null>;
1122
+ /** Compound sort value types */
1123
+ types?: Record<string, ValueType>;
1124
+ }
1139
1125
  /** Decoded cursor */
1140
1126
  interface DecodedCursor {
1141
1127
  /** Primary sort field value (rehydrated) — legacy compat */
@@ -1349,37 +1335,6 @@ interface CascadeOptions {
1349
1335
  /** Logger for cascade operations */
1350
1336
  logger?: Logger;
1351
1337
  }
1352
- /** HTTP Error with status code */
1353
- interface HttpError extends Error {
1354
- status: number;
1355
- /**
1356
- * Stable machine-readable error code. Hosts can switch on this in catch
1357
- * blocks instead of grepping the message string. Currently emitted for:
1358
- * - `SEARCH_NOT_CONFIGURED` — search requested on a repo with no text
1359
- * index AND no `searchMode: 'regex' | 'auto'` + `searchFields` config.
1360
- */
1361
- code?: string;
1362
- /**
1363
- * Free-form structured metadata for diagnostics. Pairs with `code` so
1364
- * hosts can render a clearer message in their own UI without parsing
1365
- * the human-readable `message` string. Safe for logs.
1366
- */
1367
- meta?: Record<string, unknown>;
1368
- validationErrors?: Array<{
1369
- validator: string;
1370
- error: string;
1371
- }>;
1372
- /**
1373
- * Structured metadata for duplicate-key (E11000) errors. Safe to surface in
1374
- * logs/audit. Includes the offending field names and a boolean-only mirror
1375
- * of whether each field had a duplicate value — never the value itself
1376
- * unless `parseDuplicateKeyError` was called with `{ exposeValues: true }`.
1377
- */
1378
- duplicate?: {
1379
- fields: string[]; /** Only populated when parseDuplicateKeyError was called with exposeValues:true. */
1380
- values?: Record<string, unknown>;
1381
- };
1382
- }
1383
1338
  /**
1384
1339
  * Combines all plugin method types into a single type
1385
1340
  * Useful when you're using all plugins and want full type safety
@@ -1510,4 +1465,4 @@ type AllPluginMethods<TDoc> = {
1510
1465
  */
1511
1466
  type WithPlugins<TDoc, TRepo extends RepositoryInstance = RepositoryInstance> = TRepo & AllPluginMethods<TDoc>;
1512
1467
  //#endregion
1513
- export { ReadPreferenceType as $, InferDocument as A, ObjectId as B, EventPayload as C, WithTransactionOptions as Ct, GroupResult as D, IResponseFormatter as Dt, FindOneAndUpdateOptions as E, IRequestContext as Et, Logger as F, PaginationResult as G, OffsetPaginationResult as H, LookupPopulateOptions as I, PluginFunction as J, PartialBy as K, LookupPopulateResult as L, KeysOfType as M, KeysetPaginationOptions as N, HookMode as O, LookupBuilder as Ot, KeysetPaginationResult as P, ReadOptions as Q, MinMaxResult as R, EventHandlers as S, WithPlugins as St, FieldPreset as T, IControllerResponse as Tt, OperationOptions as U, OffsetPaginationOptions as V, PaginationConfig as W, PopulateSpec as X, PluginType as Y, PrioritizedHook as Z, DecodedCursor as _, UpdatePatch as _t, AnyDocument as a, RequiredBy as at, DeleteResult as b, ValidationChainOptions as bt, CacheOperationOptions as c, SoftDeleteFilterMode as ct, CacheableOptions as d, SortDirection as dt, RepositoryContext as et, CascadeOptions as f, SortSpec as ft, CreateOptions as g, UpdateOptions as gt, CreateInput as h, UpdateManyResult as ht, AllPluginMethods as i, RepositoryOptions as it, InferRawDoc as j, HttpError as k, LookupOptions as kt, CacheOptions as l, SoftDeleteOptions as lt, CollationOptions as m, UpdateInput as mt, AggregatePaginationOptions as n, RepositoryInstance as nt, AnyModel as o, SelectSpec as ot, CascadeRelation as p, Strict as pt, Plugin as q, AggregatePaginationResult as r, RepositoryOperation as rt, CacheAdapter as s, SessionOptions as st, AggregateOptions as t, RepositoryEvent as tt, CacheStats as u, SoftDeleteRepository as ut, DeepPartial as v, UpdateWithValidationResult as vt, EventPhase as w, IController as wt, DocField as x, ValidatorDefinition as xt, DeleteManyResult as y, UserContext as yt, NonNullableFields as z };
1468
+ export { RepositoryInstance as $, InferDocument as A, OffsetPaginationOptions as B, EventHandlers as C, IControllerResponse as Ct, FindOneAndUpdateOptions as D, LookupOptions as Dt, FieldPreset as E, LookupBuilder as Et, LookupPopulateOptions as F, PluginFunction as G, PaginationConfig as H, LookupPopulateResult as I, PrioritizedHook as J, PluginType as K, MinMaxResult as L, KeysOfType as M, KeysetPaginationOptions as N, GroupResult as O, Logger as P, RepositoryEvent as Q, NonNullableFields as R, DocField as S, IController as St, EventPhase as T, IResponseFormatter as Tt, PartialBy as U, OperationOptions as V, Plugin as W, ReadPreferenceType as X, ReadOptions as Y, RepositoryContext as Z, CursorPayload as _, ValidationChainOptions as _t, AnyModel as a, SoftDeleteFilterMode as at, DeleteManyResult as b, WithPlugins as bt, CacheOperationOptions as c, SortDirection as ct, CacheableOptions as d, UpdateInput as dt, RepositoryOperation as et, CascadeOptions as f, UpdateManyResult as ft, CreateOptions as g, UserContext as gt, CreateInput as h, UpdateWithValidationResult as ht, AnyDocument as i, SessionOptions as it, InferRawDoc as j, HookMode as k, CacheOptions as l, SortSpec as lt, CollationOptions as m, UpdatePatch as mt, AggregatePaginationOptions as n, RequiredBy as nt, BasePaginationOptions as o, SoftDeleteOptions as ot, CascadeRelation as p, UpdateOptions as pt, PopulateSpec as q, AllPluginMethods as r, SelectSpec as rt, CacheAdapter as s, SoftDeleteRepository as st, AggregateOptions as t, RepositoryOptions as tt, CacheStats as u, Strict as ut, DecodedCursor as v, ValidatorDefinition as vt, EventPayload as w, IRequestContext as wt, DeleteResult as x, WithTransactionOptions as xt, DeepPartial as y, ValueType as yt, ObjectId as z };
@@ -1,6 +1,103 @@
1
1
  import { t as __exportAll } from "./chunk-CfYAbeIz.mjs";
2
- import { r as warn } from "./logger-DJ7egMnd.mjs";
2
+ import { r as warn } from "./logger-8-VS1za6.mjs";
3
3
  import { t as createError } from "./error-B00pI7dV.mjs";
4
+ import { isFilter } from "@classytic/repo-core/filter";
5
+ //#region src/filter/compile.ts
6
+ /**
7
+ * Compile a Filter IR node to a MongoDB query object. Returns `{}`
8
+ * when the node is `TRUE` (or an empty `and`) so callers can merge
9
+ * with other scopes without special-casing.
10
+ *
11
+ * The `input` is typed as `unknown` so callers can pass either a
12
+ * Filter IR node or an already-built Mongo query and we dispatch
13
+ * appropriately. This keeps the portable API ergonomic for apps that
14
+ * write Mongo queries directly today.
15
+ */
16
+ function compileFilterToMongo(input) {
17
+ if (!input) return {};
18
+ if (!isFilter(input)) return input;
19
+ return compile(input);
20
+ }
21
+ function compile(filter) {
22
+ switch (filter.op) {
23
+ case "true": return {};
24
+ case "false": return { $expr: false };
25
+ case "eq": return { [filter.field]: filter.value };
26
+ case "ne": return filter.value === null ? { [filter.field]: { $ne: null } } : { $and: [{ [filter.field]: { $ne: filter.value } }, { [filter.field]: { $ne: null } }] };
27
+ case "gt": return { [filter.field]: { $gt: filter.value } };
28
+ case "gte": return { [filter.field]: { $gte: filter.value } };
29
+ case "lt": return { [filter.field]: { $lt: filter.value } };
30
+ case "lte": return { [filter.field]: { $lte: filter.value } };
31
+ case "in":
32
+ if (filter.values.length === 0) return { $expr: false };
33
+ return { [filter.field]: { $in: [...filter.values] } };
34
+ case "nin":
35
+ if (filter.values.length === 0) return {};
36
+ return { [filter.field]: { $nin: [...filter.values] } };
37
+ case "exists": return filter.exists ? { [filter.field]: { $ne: null } } : { [filter.field]: null };
38
+ case "like": return { [filter.field]: likeToRegex(filter.pattern, filter.caseSensitivity) };
39
+ case "regex": return { [filter.field]: { $regex: filter.pattern } };
40
+ case "and": {
41
+ if (filter.children.length === 0) return {};
42
+ const parts = filter.children.map(compile);
43
+ if (parts.length === 1) return parts[0];
44
+ return { $and: parts };
45
+ }
46
+ case "or": {
47
+ if (filter.children.length === 0) return { $expr: false };
48
+ const parts = filter.children.map(compile);
49
+ if (parts.length === 1) return parts[0];
50
+ return { $or: parts };
51
+ }
52
+ case "not": return { $nor: [compile(filter.child)] };
53
+ case "raw": throw new Error("mongokit/filter: `raw` is a SQL escape hatch and has no safe MongoDB translation. Pass a Mongo query object directly to the filter slot instead.");
54
+ }
55
+ }
56
+ /**
57
+ * Translate a SQL-style LIKE pattern to a MongoDB regex.
58
+ *
59
+ * SQL semantics:
60
+ * - `%` → match zero or more characters (`.*`)
61
+ * - `_` → match exactly one character (`.`)
62
+ * - `\%` → literal percent (escape via backslash)
63
+ * - `\_` → literal underscore
64
+ * - `\\` → literal backslash
65
+ * - everything else is literal — regex metacharacters get escaped.
66
+ *
67
+ * The naive implementation `pattern.replace(/%/g, '.*')` is wrong:
68
+ * it also expands escaped `\%` into a wildcard. We walk the pattern
69
+ * char-by-char, honoring backslash escapes, so `like('notes', '50\\% off')`
70
+ * matches the literal string `50% off` instead of `50<anything> off`.
71
+ *
72
+ * Anchored with `^` / `$` so LIKE matches the whole field value, the
73
+ * same way sqlitekit's `LIKE` semantically does.
74
+ */
75
+ function likeToRegex(pattern, caseSensitivity) {
76
+ let out = "^";
77
+ for (let i = 0; i < pattern.length; i++) {
78
+ const ch = pattern[i];
79
+ if (ch === "\\" && i + 1 < pattern.length) {
80
+ const next = pattern[i + 1];
81
+ if (next === "%" || next === "_" || next === "\\") {
82
+ out += escapeRegexChar(next);
83
+ i++;
84
+ continue;
85
+ }
86
+ }
87
+ if (ch === "%") out += ".*";
88
+ else if (ch === "_") out += ".";
89
+ else out += escapeRegexChar(ch);
90
+ }
91
+ out += "$";
92
+ return caseSensitivity === "sensitive" ? { $regex: out } : {
93
+ $regex: out,
94
+ $options: "i"
95
+ };
96
+ }
97
+ function escapeRegexChar(ch) {
98
+ return /[.*+?^${}()|[\]\\]/.test(ch) ? `\\${ch}` : ch;
99
+ }
100
+ //#endregion
4
101
  //#region src/query/LookupBuilder.ts
5
102
  /** Stages that are never valid inside a $lookup pipeline */
6
103
  const BLOCKED_PIPELINE_STAGES = [
@@ -191,28 +288,24 @@ var LookupBuilder = class LookupBuilder {
191
288
  const builder = new LookupBuilder(lookup.from).localField(lookup.localField).foreignField(lookup.foreignField);
192
289
  if (lookup.as) builder.as(lookup.as);
193
290
  if (lookup.single) builder.single(lookup.single);
194
- if (lookup.select) {
195
- let projection;
196
- if (typeof lookup.select === "string") {
197
- projection = {};
198
- for (const field of lookup.select.split(",").map((f) => f.trim())) if (field.startsWith("-")) projection[field.substring(1)] = 0;
199
- else projection[field] = 1;
200
- } else projection = lookup.select;
201
- const joinStage = { $match: { $expr: { $eq: [`$${lookup.foreignField}`, "$$lookupJoinVal"] } } };
202
- const existing = lookup.pipeline || [];
203
- builder.pipeline([
204
- joinStage,
205
- ...existing,
206
- { $project: projection }
207
- ]);
291
+ const stages = [];
292
+ const appendCallerStages = (raw) => {
293
+ if (raw.length === 0) return;
294
+ stages.push(...LookupBuilder.sanitizePipeline(raw));
295
+ };
296
+ const compiledWhere = lookup.where ? compileFilterToMongo(lookup.where) : void 0;
297
+ const hasWhere = !!compiledWhere && Object.keys(compiledWhere).length > 0;
298
+ if (!!(lookup.select || lookup.pipeline || hasWhere)) {
299
+ stages.push({ $match: { $expr: { $eq: [`$${lookup.foreignField}`, "$$lookupJoinVal"] } } });
300
+ if (hasWhere && compiledWhere) appendCallerStages([{ $match: compiledWhere }]);
301
+ if (lookup.pipeline) appendCallerStages(lookup.pipeline);
302
+ if (lookup.select) stages.push({ $project: compileSelectToProjection(lookup.select) });
303
+ builder.pipeline(stages);
208
304
  builder.let({
209
305
  lookupJoinVal: `$${lookup.localField}`,
210
306
  ...lookup.let || {}
211
307
  });
212
308
  builder.sanitize(false);
213
- } else if (lookup.pipeline) {
214
- builder.pipeline(lookup.pipeline);
215
- if (lookup.let) builder.let(lookup.let);
216
309
  } else if (lookup.let) builder.let(lookup.let);
217
310
  return builder.build();
218
311
  });
@@ -281,6 +374,38 @@ var LookupBuilder = class LookupBuilder {
281
374
  }
282
375
  };
283
376
  /**
377
+ * Normalize the three accepted `LookupOptions.select` shapes into a single
378
+ * Mongo `$project` map.
379
+ *
380
+ * - `string` → CSV form, leading `-` excludes (`'name,-status'`)
381
+ * - `readonly string[]` → array form, leading `-` excludes (`['-status']`)
382
+ * - `Record<string, 0|1>` → already a projection map, copied as-is
383
+ *
384
+ * The array form mirrors `repo-core`'s `LookupSpec.select` so cross-kit
385
+ * callers (`select: ['name']`) compile identically on mongokit and sqlitekit.
386
+ */
387
+ function compileSelectToProjection(select) {
388
+ if (typeof select === "string") {
389
+ const projection = {};
390
+ for (const field of select.split(",").map((f) => f.trim())) {
391
+ if (!field) continue;
392
+ if (field.startsWith("-")) projection[field.substring(1)] = 0;
393
+ else projection[field] = 1;
394
+ }
395
+ return projection;
396
+ }
397
+ if (Array.isArray(select)) {
398
+ const projection = {};
399
+ for (const field of select) {
400
+ if (!field) continue;
401
+ if (field.startsWith("-")) projection[field.substring(1)] = 0;
402
+ else projection[field] = 1;
403
+ }
404
+ return projection;
405
+ }
406
+ return { ...select };
407
+ }
408
+ /**
284
409
  * Performance Guidelines for $lookup at Scale:
285
410
  *
286
411
  * 1. **Index Requirements** (Critical for millions of records):
@@ -918,4 +1043,4 @@ async function pullFromArray(Model, id, field, value, options = {}) {
918
1043
  return update(Model, id, { $pull: { [field]: value } }, options);
919
1044
  }
920
1045
  //#endregion
921
- export { upsert as _, count as a, LookupBuilder as b, getByQuery as c, deleteById as d, deleteByQuery as f, create_exports as g, createMany as h, update_exports as i, getOrCreate as l, create as m, update as n, exists as o, delete_exports as p, updateByQuery as r, getById as s, findOneAndUpdate as t, read_exports as u, aggregate_exports as v, distinct as y };
1046
+ export { upsert as _, count as a, LookupBuilder as b, getByQuery as c, deleteById as d, deleteByQuery as f, create_exports as g, createMany as h, update_exports as i, getOrCreate as l, create as m, update as n, exists as o, delete_exports as p, updateByQuery as r, getById as s, findOneAndUpdate as t, read_exports as u, aggregate_exports as v, compileFilterToMongo as x, distinct as y };
@@ -1,5 +1,5 @@
1
- import { X as PopulateSpec, ft as SortSpec, ot as SelectSpec } from "../types-Dc2aDVUi.mjs";
2
- import { a as createFieldPreset, c as getMongooseProjection, i as configureLogger, l as createError, n as buildCrudSchemasFromMongooseSchema, o as filterResponseData, r as createMemoryCache, s as getFieldsForUser, t as buildCrudSchemasFromModel } from "../mongooseToJsonSchema-DXxjFkLB.mjs";
1
+ import { lt as SortSpec, q as PopulateSpec, rt as SelectSpec } from "../types-DXscZnLu.mjs";
2
+ import { a as createFieldPreset, c as getMongooseProjection, i as configureLogger, l as createError, n as buildCrudSchemasFromMongooseSchema, o as filterResponseData, r as createMemoryCache, s as getFieldsForUser, t as buildCrudSchemasFromModel } from "../mongooseToJsonSchema-rtkdbBhf.mjs";
3
3
 
4
4
  //#region src/utils/cache-keys.d.ts
5
5
  /**
@@ -1,6 +1,6 @@
1
- import { t as configureLogger } from "../logger-DJ7egMnd.mjs";
1
+ import { t as configureLogger } from "../logger-8-VS1za6.mjs";
2
2
  import { t as createError } from "../error-B00pI7dV.mjs";
3
3
  import { n as isObjectIdInstance, r as isValidIdForType, t as getSchemaIdType } from "../id-resolution-e4BT9zAV.mjs";
4
4
  import { a as byIdKey, c as listQueryKey, i as getMongooseProjection, l as modelPattern, n as filterResponseData, o as byQueryKey, r as getFieldsForUser, s as listPattern, t as createFieldPreset, u as versionKey } from "../field-selection-dI0m4RBs.mjs";
5
- import { n as buildCrudSchemasFromMongooseSchema, r as createMemoryCache, t as buildCrudSchemasFromModel } from "../mongooseToJsonSchema-D1sdCOmj.mjs";
5
+ import { n as buildCrudSchemasFromMongooseSchema, r as createMemoryCache, t as buildCrudSchemasFromModel } from "../mongooseToJsonSchema-B187ufOP.mjs";
6
6
  export { buildCrudSchemasFromModel, buildCrudSchemasFromMongooseSchema, byIdKey, byQueryKey, configureLogger, createError, createFieldPreset, createMemoryCache, filterResponseData, getFieldsForUser, getMongooseProjection, getSchemaIdType, isObjectIdInstance, isValidIdForType, listPattern, listQueryKey, modelPattern, versionKey };
@@ -1,12 +1,12 @@
1
- import { n as debug, r as warn } from "./logger-DJ7egMnd.mjs";
2
- import { _ as upsert, 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, y as distinct } from "./update-DEJYcgfV.mjs";
1
+ import { _ as upsert, 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-CDxPtpsk.mjs";
2
+ import { n as debug, r as warn } from "./logger-8-VS1za6.mjs";
3
3
  import { n as isDuplicateKeyError, r as parseDuplicateKeyError, t as createError } from "./error-B00pI7dV.mjs";
4
- import { t as PaginationEngine } from "./PaginationEngine-CvDFfVXT.mjs";
4
+ import { t as PaginationEngine } from "./PaginationEngine-BDaAsyPn.mjs";
5
5
  import { hasNearOperator, rewriteNearForCount } from "./query/primitives/geo.mjs";
6
6
  import { r as isValidIdForType, t as getSchemaIdType } from "./id-resolution-e4BT9zAV.mjs";
7
7
  import { a as byIdKey, c as listQueryKey, l as modelPattern, o as byQueryKey, r as getFieldsForUser, u as versionKey } from "./field-selection-dI0m4RBs.mjs";
8
- import mongoose from "mongoose";
9
8
  import { isFilter } from "@classytic/repo-core/filter";
9
+ import mongoose from "mongoose";
10
10
  import { HOOK_PRIORITY } from "@classytic/repo-core/hooks";
11
11
  import { RepositoryBase, validatePluginOrder } from "@classytic/repo-core/repository";
12
12
  import { compileUpdateSpecToMongo, isUpdateSpec } from "@classytic/repo-core/update";
@@ -728,102 +728,6 @@ function batchOperationsPlugin() {
728
728
  };
729
729
  }
730
730
  //#endregion
731
- //#region src/filter/compile.ts
732
- /**
733
- * Compile a Filter IR node to a MongoDB query object. Returns `{}`
734
- * when the node is `TRUE` (or an empty `and`) so callers can merge
735
- * with other scopes without special-casing.
736
- *
737
- * The `input` is typed as `unknown` so callers can pass either a
738
- * Filter IR node or an already-built Mongo query and we dispatch
739
- * appropriately. This keeps the portable API ergonomic for apps that
740
- * write Mongo queries directly today.
741
- */
742
- function compileFilterToMongo(input) {
743
- if (!input) return {};
744
- if (!isFilter(input)) return input;
745
- return compile(input);
746
- }
747
- function compile(filter) {
748
- switch (filter.op) {
749
- case "true": return {};
750
- case "false": return { $expr: false };
751
- case "eq": return { [filter.field]: filter.value };
752
- case "ne": return filter.value === null ? { [filter.field]: { $ne: null } } : { $and: [{ [filter.field]: { $ne: filter.value } }, { [filter.field]: { $ne: null } }] };
753
- case "gt": return { [filter.field]: { $gt: filter.value } };
754
- case "gte": return { [filter.field]: { $gte: filter.value } };
755
- case "lt": return { [filter.field]: { $lt: filter.value } };
756
- case "lte": return { [filter.field]: { $lte: filter.value } };
757
- case "in":
758
- if (filter.values.length === 0) return { $expr: false };
759
- return { [filter.field]: { $in: [...filter.values] } };
760
- case "nin":
761
- if (filter.values.length === 0) return {};
762
- return { [filter.field]: { $nin: [...filter.values] } };
763
- case "exists": return filter.exists ? { [filter.field]: { $ne: null } } : { [filter.field]: null };
764
- case "like": return { [filter.field]: likeToRegex(filter.pattern, filter.caseSensitivity) };
765
- case "regex": return { [filter.field]: { $regex: filter.pattern } };
766
- case "and": {
767
- if (filter.children.length === 0) return {};
768
- const parts = filter.children.map(compile);
769
- if (parts.length === 1) return parts[0];
770
- return { $and: parts };
771
- }
772
- case "or": {
773
- if (filter.children.length === 0) return { $expr: false };
774
- const parts = filter.children.map(compile);
775
- if (parts.length === 1) return parts[0];
776
- return { $or: parts };
777
- }
778
- case "not": return { $nor: [compile(filter.child)] };
779
- case "raw": throw new Error("mongokit/filter: `raw` is a SQL escape hatch and has no safe MongoDB translation. Pass a Mongo query object directly to the filter slot instead.");
780
- }
781
- }
782
- /**
783
- * Translate a SQL-style LIKE pattern to a MongoDB regex.
784
- *
785
- * SQL semantics:
786
- * - `%` → match zero or more characters (`.*`)
787
- * - `_` → match exactly one character (`.`)
788
- * - `\%` → literal percent (escape via backslash)
789
- * - `\_` → literal underscore
790
- * - `\\` → literal backslash
791
- * - everything else is literal — regex metacharacters get escaped.
792
- *
793
- * The naive implementation `pattern.replace(/%/g, '.*')` is wrong:
794
- * it also expands escaped `\%` into a wildcard. We walk the pattern
795
- * char-by-char, honoring backslash escapes, so `like('notes', '50\\% off')`
796
- * matches the literal string `50% off` instead of `50<anything> off`.
797
- *
798
- * Anchored with `^` / `$` so LIKE matches the whole field value, the
799
- * same way sqlitekit's `LIKE` semantically does.
800
- */
801
- function likeToRegex(pattern, caseSensitivity) {
802
- let out = "^";
803
- for (let i = 0; i < pattern.length; i++) {
804
- const ch = pattern[i];
805
- if (ch === "\\" && i + 1 < pattern.length) {
806
- const next = pattern[i + 1];
807
- if (next === "%" || next === "_" || next === "\\") {
808
- out += escapeRegexChar(next);
809
- i++;
810
- continue;
811
- }
812
- }
813
- if (ch === "%") out += ".*";
814
- else if (ch === "_") out += ".";
815
- else out += escapeRegexChar(ch);
816
- }
817
- out += "$";
818
- return caseSensitivity === "sensitive" ? { $regex: out } : {
819
- $regex: out,
820
- $options: "i"
821
- };
822
- }
823
- function escapeRegexChar(ch) {
824
- return /[.*+?^${}()|[\]\\]/.test(ch) ? `\\${ch}` : ch;
825
- }
826
- //#endregion
827
731
  //#region src/actions/aggregate-ir/normalize.ts
828
732
  /**
829
733
  * Normalize `AggRequest['groupBy']` into a readonly string array.
@@ -1626,32 +1530,6 @@ function createTxBoundRepo(outer, session) {
1626
1530
  }
1627
1531
  //#endregion
1628
1532
  //#region src/Repository.ts
1629
- /**
1630
- * Repository Pattern - Data Access Layer
1631
- *
1632
- * Event-driven, plugin-based abstraction for MongoDB operations
1633
- * Inspired by Meta & Stripe's repository patterns
1634
- *
1635
- * @example
1636
- * ```typescript
1637
- * const userRepo = new Repository(UserModel, [
1638
- * timestampPlugin(),
1639
- * softDeletePlugin(),
1640
- * ]);
1641
- *
1642
- * // Create
1643
- * const user = await userRepo.create({ name: 'John', email: 'john@example.com' });
1644
- *
1645
- * // Read with pagination
1646
- * const users = await userRepo.getAll({ page: 1, limit: 20, filters: { status: 'active' } });
1647
- *
1648
- * // Update
1649
- * const updated = await userRepo.update(user._id, { name: 'John Doe' });
1650
- *
1651
- * // Delete
1652
- * await userRepo.delete(user._id);
1653
- * ```
1654
- */
1655
1533
  function ensureLookupProjectionIncludesCursorFields(projection, sort) {
1656
1534
  if (!projection || !sort) return projection;
1657
1535
  if (!Object.values(projection).some((value) => value === 1)) return projection;
@@ -2300,7 +2178,7 @@ var Repository = class extends RepositoryBase {
2300
2178
  if (context.softDeleted) {
2301
2179
  const result = {
2302
2180
  acknowledged: true,
2303
- deletedCount: 0,
2181
+ deletedCount: context.softDeletedCount ?? 0,
2304
2182
  soft: true
2305
2183
  };
2306
2184
  await this._emitHook("after:deleteMany", {
@@ -2488,19 +2366,31 @@ var Repository = class extends RepositoryBase {
2488
2366
  * Merge policy-hook-injected filters (multi-tenant scope,
2489
2367
  * soft-delete) into the pre-aggregate `filter` slot of the request.
2490
2368
  * Plugins speak mongo-query shape (`context.query` / `context.filters`);
2491
- * we merge those with the portable `req.filter` using `$and`.
2369
+ * the caller's `req.filter` may be either Filter IR or a mongo query.
2492
2370
  *
2493
- * When the caller's filter is a Filter IR node we don't unwrap it —
2494
- * the pipeline compiler does Filter→mongo translation. Merging here
2495
- * as a mongo-shaped `$and` is safe because `compileFilterToMongo`
2496
- * passes through already-mongo queries unchanged.
2371
+ * Each part is compiled through `compileFilterToMongo` BEFORE the
2372
+ * `$and` merge — otherwise an IR node like `eq('status', 'paid')`
2373
+ * (`{ op, field, value }`) gets nested inside `$and` as a literal,
2374
+ * `compileFilterToMongo` of the merged value finds no top-level `op`
2375
+ * (so `isFilter` is false), and Mongo receives the IR object as a
2376
+ * raw query clause that matches no real document. Symptom: aggregates
2377
+ * silently return 0 rows whenever a callerpasses IR + a policy
2378
+ * plugin is installed.
2497
2379
  */
2498
2380
  _injectPolicyScopeIntoAgg(req, context) {
2499
2381
  const scopeCandidates = [];
2500
2382
  for (const candidate of [context.filters, context.query]) if (candidate && typeof candidate === "object" && Object.keys(candidate).length > 0) scopeCandidates.push(candidate);
2501
2383
  if (scopeCandidates.length === 0) return req;
2502
- const parts = [...scopeCandidates];
2503
- if (req.filter) parts.push(req.filter);
2384
+ const parts = [];
2385
+ for (const c of scopeCandidates) {
2386
+ const compiled = compileFilterToMongo(c);
2387
+ if (Object.keys(compiled).length > 0) parts.push(compiled);
2388
+ }
2389
+ if (req.filter) {
2390
+ const compiled = compileFilterToMongo(req.filter);
2391
+ if (Object.keys(compiled).length > 0) parts.push(compiled);
2392
+ }
2393
+ if (parts.length === 0) return req;
2504
2394
  const merged = parts.length === 1 ? parts[0] : { $and: parts };
2505
2395
  return {
2506
2396
  ...req,
@@ -4330,49 +4220,6 @@ function mongoOperationsPlugin() {
4330
4220
  //#endregion
4331
4221
  //#region src/plugins/multi-tenant.plugin.ts
4332
4222
  /**
4333
- * Multi-Tenant Plugin
4334
- *
4335
- * Automatically injects tenant isolation filters into all queries.
4336
- * Ensures data isolation by adding organizationId (or custom tenant field)
4337
- * to every read and write operation.
4338
- *
4339
- * Uses HOOK_PRIORITY.POLICY (100) to ensure tenant filters are injected
4340
- * BEFORE cache keys are computed (HOOK_PRIORITY.CACHE = 200).
4341
- *
4342
- * @example
4343
- * ```typescript
4344
- * // Basic — scopes every operation by organizationId from context
4345
- * const repo = new Repository(Invoice, [
4346
- * multiTenantPlugin({ tenantField: 'organizationId' }),
4347
- * ]);
4348
- *
4349
- * const invoices = await repo.getAll(
4350
- * { filters: { status: 'paid' } },
4351
- * { organizationId: 'org_123' }
4352
- * );
4353
- * // Actual query: { status: 'paid', organizationId: 'org_123' }
4354
- *
4355
- * // Super admin bypass — skip scoping based on context
4356
- * const repo = new Repository(Invoice, [
4357
- * multiTenantPlugin({
4358
- * tenantField: 'organizationId',
4359
- * skipWhen: (context) => context.role === 'superadmin',
4360
- * }),
4361
- * ]);
4362
- *
4363
- * // Admin sees all orgs
4364
- * await repo.getAll({ page: 1, limit: 10 }, { role: 'superadmin' });
4365
- *
4366
- * // Automatic context — resolve tenant from AsyncLocalStorage
4367
- * const repo = new Repository(Invoice, [
4368
- * multiTenantPlugin({
4369
- * tenantField: 'organizationId',
4370
- * resolveContext: () => asyncLocalStorage.getStore()?.tenantId,
4371
- * }),
4372
- * ]);
4373
- * ```
4374
- */
4375
- /**
4376
4223
  * True when the op's policy target already has `tenantField` set by the
4377
4224
  * caller. Used to decide whether the plugin can safely skip injecting a
4378
4225
  * tenant scope rather than throwing on a missing context.
@@ -4719,8 +4566,9 @@ function softDeletePlugin(options = {}) {
4719
4566
  ...context.query || {},
4720
4567
  ...deleteFilter
4721
4568
  };
4722
- await repo.Model.updateMany(finalQuery, { $set: { [deletedField]: /* @__PURE__ */ new Date() } }, { session: context.session });
4569
+ const updateRes = await repo.Model.updateMany(finalQuery, { $set: { [deletedField]: /* @__PURE__ */ new Date() } }, { session: context.session });
4723
4570
  context.softDeleted = true;
4571
+ context.softDeletedCount = updateRes?.modifiedCount ?? 0;
4724
4572
  }
4725
4573
  }, { priority: HOOK_PRIORITY$1.POLICY });
4726
4574
  if (addRestoreMethod) {
@@ -1,7 +1,9 @@
1
- import { B as ObjectId, F as Logger, H as OffsetPaginationResult, T as FieldPreset, X as PopulateSpec, bt as ValidationChainOptions, et as RepositoryContext, f as CascadeOptions, ft as SortSpec, l as CacheOptions, lt as SoftDeleteOptions, nt as RepositoryInstance, ot as SelectSpec, q as Plugin, u as CacheStats, xt as ValidatorDefinition } from "./types-Dc2aDVUi.mjs";
1
+ import { $ as RepositoryInstance, E as FieldPreset, P as Logger, W as Plugin, Z as RepositoryContext, _t as ValidationChainOptions, f as CascadeOptions, l as CacheOptions, lt as SortSpec, ot as SoftDeleteOptions, q as PopulateSpec, rt as SelectSpec, u as CacheStats, vt as ValidatorDefinition, z as ObjectId } from "./types-DXscZnLu.mjs";
2
2
  import mongoose from "mongoose";
3
3
  import { BulkWriteResult } from "@classytic/repo-core/repository";
4
4
  import { AsyncLocalStorage } from "node:async_hooks";
5
+ import { OffsetPaginationResult } from "@classytic/repo-core/pagination";
6
+ import { TenantConfig } from "@classytic/repo-core/tenant";
5
7
 
6
8
  //#region src/plugins/aggregate-helpers.plugin.d.ts
7
9
  /**
@@ -753,13 +755,20 @@ interface MongoOperationsMethods<TDoc> {
753
755
  }
754
756
  //#endregion
755
757
  //#region src/plugins/multi-tenant.plugin.d.ts
756
- interface MultiTenantOptions {
757
- /** Field name used for tenant isolation (default: 'organizationId') */
758
- tenantField?: string;
759
- /** Context key to read tenant ID from (default: 'organizationId') */
760
- contextKey?: string;
761
- /** Throw if tenant ID is missing from context (default: true) */
762
- required?: boolean;
758
+ /**
759
+ * Static tenant fields are picked from `@classytic/repo-core/tenant` so the
760
+ * vocabulary (`tenantField`, `contextKey`, `required`, `fieldType`) stays
761
+ * locked to the org-wide canonical source. Mongokit-specific runtime
762
+ * callbacks (`skipWhen`, `resolveContext`, `skipOperations`,
763
+ * `allowDataInjection`) extend on top — those genuinely differ from
764
+ * sqlitekit's runtime shape because mongokit's `RepositoryContext` carries
765
+ * Mongoose-specific bits the resolver may inspect.
766
+ *
767
+ * Mongokit defaults `fieldType` to `'string'` (not repo-core's `'objectId'`)
768
+ * for back-compat — pre-3.x stored tenant ids as raw strings; flipping the
769
+ * default would silently change cast behavior for existing apps.
770
+ */
771
+ interface MultiTenantOptions extends Pick<TenantConfig, 'tenantField' | 'contextKey' | 'required' | 'fieldType'> {
763
772
  /** Operations to skip tenant injection (e.g., for admin/system queries) */
764
773
  skipOperations?: string[];
765
774
  /**
@@ -784,17 +793,6 @@ interface MultiTenantOptions {
784
793
  * ```
785
794
  */
786
795
  resolveContext?: () => string | undefined;
787
- /**
788
- * How to store/query the tenant ID.
789
- *
790
- * - `'string'` (default): inject the tenant ID as-is (raw string).
791
- * - `'objectId'`: cast to `mongoose.Types.ObjectId` before injecting.
792
- *
793
- * Choose `'objectId'` when the schema declares the tenant field as
794
- * `Schema.Types.ObjectId` (e.g., Flow, accounting). This enables
795
- * `$lookup` and `.populate()` against the referenced collection.
796
- */
797
- fieldType?: 'string' | 'objectId';
798
796
  /**
799
797
  * When `true` (default), bypass the `required` throw if the operation's
800
798
  * policy target already carries the tenant field. This lets hosts that
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@classytic/mongokit",
3
- "version": "3.11.1",
3
+ "version": "3.12.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,
@@ -93,8 +93,8 @@
93
93
  },
94
94
  "homepage": "https://github.com/classytic/mongokit#readme",
95
95
  "peerDependencies": {
96
- "mongoose": ">=9.4.1",
97
- "@classytic/repo-core": ">=0.2.0"
96
+ "@classytic/repo-core": ">=0.3.0",
97
+ "mongoose": ">=9.4.1"
98
98
  },
99
99
  "engines": {
100
100
  "node": ">=22"
@@ -115,10 +115,12 @@
115
115
  "lint:fix": "biome check src/ --write",
116
116
  "format": "biome format src/ --write",
117
117
  "check": "biome ci src/ --diagnostic-level=error",
118
+ "push": "classytic-push",
118
119
  "prepublishOnly": "npm run check && npm run build && npm run typecheck && npm run typecheck:tests && npm test",
119
120
  "publish:dry": "npm publish --dry-run --access public",
120
121
  "publish:npm": "npm publish --access public",
121
- "release": "npm run check && npm run build && npm run typecheck && npm run typecheck:tests && npm test && npm publish --access public",
122
+ "release:tag": "node -e \"require('child_process').execSync('npm run push -- v'+require('./package.json').version,{stdio:'inherit'})\"",
123
+ "release": "npm run push -- main && npm run release:tag && npm publish",
122
124
  "release:patch": "npm version patch && npm run release",
123
125
  "release:minor": "npm version minor && npm run release",
124
126
  "release:major": "npm version major && npm run release"
@@ -126,7 +128,8 @@
126
128
  "devDependencies": {
127
129
  "@arethetypeswrong/cli": "^0.18.2",
128
130
  "@biomejs/biome": "^2.4.10",
129
- "@classytic/repo-core": ">=0.2.0",
131
+ "@classytic/dev-tools": "^0.2.0",
132
+ "@classytic/repo-core": "^0.3.0",
130
133
  "@types/node": "^22.0.0",
131
134
  "@vitest/coverage-v8": "^3.2.4",
132
135
  "ajv": "^8.18.0",