@classytic/mongokit 3.28.0 → 3.29.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/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2024 Classytic (Sadman Chowdhury)
3
+ Copyright (c) 2024 Classytic LLC
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -18,7 +18,10 @@ Requirements:
18
18
 
19
19
  - Node.js `>=22`
20
20
  - Mongoose `>=9.4.1`
21
- - `@classytic/repo-core >=0.7.0`
21
+ - `@classytic/repo-core >=0.19.0`
22
+ - `mongodb-memory-server >=10.4.3` — **optional**, needed only by the
23
+ `@classytic/mongokit/testkit` subpath (declared as an optional peer, so it is
24
+ never installed for consumers that don't use it)
22
25
 
23
26
  ## Quick Start
24
27
 
@@ -430,3 +433,10 @@ All accept `{ replset?, dbName?, uri? }`.
430
433
  ## License
431
434
 
432
435
  MIT
436
+
437
+
438
+ ## Trademark
439
+
440
+ The code is MIT-licensed. **"Classytic", "arc", and the logos are trademarks of
441
+ Classytic LLC** and are **not** licensed under MIT — see [TRADEMARK.md](TRADEMARK.md).
442
+ Forks must be renamed; the license covers the code, not the brand.
package/TRADEMARK.md ADDED
@@ -0,0 +1,22 @@
1
+ # Trademark Notice
2
+
3
+ The **source code** in this package is licensed under the MIT License (see
4
+ [LICENSE](LICENSE)). Trademarks are **not** part of that license.
5
+
6
+ **"Classytic", "arc", the Classytic and arc logos**, and related names and marks
7
+ are trademarks of **Classytic LLC** (Wyoming, USA). The MIT License covers the
8
+ code only — it grants **no right** to use Classytic LLC's names, logos, or brand.
9
+
10
+ ## You may
11
+ - use, fork, modify, and redistribute the **code** under the MIT License;
12
+ - state truthfully that your product "uses" or "is built on" arc / Classytic
13
+ software, without implying endorsement.
14
+
15
+ ## You may not (without written permission from Classytic LLC)
16
+ - use "Classytic", "arc", or confusingly similar names as the name of your own
17
+ product, package, company, or service;
18
+ - use the Classytic / arc logos or brand styling;
19
+ - imply sponsorship, affiliation, or endorsement by Classytic LLC.
20
+
21
+ Forks or redistributions must be **renamed** so they are not confused with the
22
+ official project. Permission / questions: **trademark@classytic.com**.
@@ -1,5 +1,5 @@
1
1
  import { i as isValidIdForType, t as getSchemaIdType } from "./id-resolution-BqDrk0S2.mjs";
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";
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-CI2NdAW6.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
5
  import { t as PaginationEngine } from "./PaginationEngine-CczbvCAq.mjs";
@@ -317,6 +317,122 @@ function applyExecutionHints(aggregation, hints) {
317
317
  if (hints.indexHint !== void 0) aggregation.option({ hint: hints.indexHint });
318
318
  }
319
319
  //#endregion
320
+ //#region src/filter/cast-schema.ts
321
+ /** Operators whose operand is a single scalar to cast. */
322
+ const SCALAR_OPS = /* @__PURE__ */ new Set([
323
+ "$eq",
324
+ "$ne",
325
+ "$gt",
326
+ "$gte",
327
+ "$lt",
328
+ "$lte"
329
+ ]);
330
+ /** Operators whose operand is an array of scalars to cast element-wise. */
331
+ const ARRAY_OPS = /* @__PURE__ */ new Set(["$in", "$nin"]);
332
+ /** Logical operators whose operand is an ARRAY of sub-filters. */
333
+ const LOGICAL_ARRAY_OPS = /* @__PURE__ */ new Set([
334
+ "$and",
335
+ "$or",
336
+ "$nor"
337
+ ]);
338
+ /** Logical operators whose operand is a SINGLE nested sub-filter. */
339
+ const LOGICAL_OBJECT_OPS = /* @__PURE__ */ new Set(["$not"]);
340
+ function isPlainObject(value) {
341
+ return value !== null && typeof value === "object" && !Array.isArray(value) && !(value instanceof Date);
342
+ }
343
+ /**
344
+ * Cast one value through a schema path. Returns the original value when the
345
+ * path is unknown, the value is nullish, or the cast throws — never raises.
346
+ */
347
+ function castValue(schema, field, value) {
348
+ if (value === null || value === void 0) return value;
349
+ if (typeof value === "object" && !(value instanceof Date)) return value;
350
+ let schemaType;
351
+ try {
352
+ schemaType = schema.path(field);
353
+ } catch {
354
+ return value;
355
+ }
356
+ if (!schemaType || typeof schemaType.cast !== "function") return value;
357
+ try {
358
+ return schemaType.cast(value);
359
+ } catch {
360
+ return value;
361
+ }
362
+ }
363
+ /**
364
+ * Cast the operand(s) of a field-level operator object, leaving operators we
365
+ * don't understand (`$regex`, `$exists`, `$size`, `$elemMatch`, …) alone.
366
+ */
367
+ function castOperatorObject(schema, field, ops) {
368
+ let changed = false;
369
+ const out = {};
370
+ for (const [op, operand] of Object.entries(ops)) if (SCALAR_OPS.has(op)) {
371
+ const next = castValue(schema, field, operand);
372
+ out[op] = next;
373
+ if (next !== operand) changed = true;
374
+ } else if (ARRAY_OPS.has(op) && Array.isArray(operand)) {
375
+ const arr = operand.map((entry) => castValue(schema, field, entry));
376
+ out[op] = arr;
377
+ if (arr.some((v, i) => v !== operand[i])) changed = true;
378
+ } else out[op] = operand;
379
+ return changed ? out : ops;
380
+ }
381
+ /**
382
+ * Cast a record-shape Mongo filter against a Mongoose schema so it is safe to
383
+ * hand to a `$match` stage. Recurses `$and` / `$or` / `$nor` / `$not`, since a
384
+ * policy/tenant scope conjoined with a caller filter nests one level down.
385
+ *
386
+ * Filter IR nodes and non-objects pass through untouched — IR values are built
387
+ * in code (already correctly typed) and carry no schema paths to resolve.
388
+ *
389
+ * @example
390
+ * ```ts
391
+ * // organizationId is an ObjectId column; arc supplies the scope id as a string
392
+ * castFilterToSchema({ organizationId: '6a6a…' }, Order.schema)
393
+ * // → { organizationId: ObjectId('6a6a…') }
394
+ * ```
395
+ */
396
+ function castFilterToSchema(filter, schema) {
397
+ if (!isPlainObject(filter)) return filter;
398
+ if (typeof filter.op === "string") return filter;
399
+ let changed = false;
400
+ const out = {};
401
+ for (const [key, value] of Object.entries(filter)) {
402
+ if (LOGICAL_ARRAY_OPS.has(key) && Array.isArray(value)) {
403
+ const arr = value.map((entry) => castFilterToSchema(entry, schema));
404
+ out[key] = arr;
405
+ if (arr.some((v, i) => v !== value[i])) changed = true;
406
+ continue;
407
+ }
408
+ if (LOGICAL_OBJECT_OPS.has(key) && isPlainObject(value)) {
409
+ const nested = castFilterToSchema(value, schema);
410
+ out[key] = nested;
411
+ if (nested !== value) changed = true;
412
+ continue;
413
+ }
414
+ if (key.startsWith("$")) {
415
+ out[key] = value;
416
+ continue;
417
+ }
418
+ if (isPlainObject(value)) {
419
+ const keys = Object.keys(value);
420
+ if (keys.length > 0 && keys.every((k) => k.startsWith("$"))) {
421
+ const nextOps = castOperatorObject(schema, key, value);
422
+ out[key] = nextOps;
423
+ if (nextOps !== value) changed = true;
424
+ continue;
425
+ }
426
+ out[key] = value;
427
+ continue;
428
+ }
429
+ const next = castValue(schema, key, value);
430
+ out[key] = next;
431
+ if (next !== value) changed = true;
432
+ }
433
+ return changed ? out : filter;
434
+ }
435
+ //#endregion
320
436
  //#region src/actions/aggregate-ir/normalize.ts
321
437
  /**
322
438
  * AggRequest → input normalization (mongokit binding).
@@ -580,14 +696,14 @@ function compileMeasure(measure) {
580
696
  }
581
697
  //#endregion
582
698
  //#region src/actions/aggregate-ir/pipeline.ts
583
- function buildAggPipeline(req) {
699
+ function buildAggPipeline(req, schema) {
584
700
  validateMeasures$1(req.measures);
585
701
  const groupCols = normalizeGroupBy(req.groupBy);
586
702
  const bucketAliases = req.dateBuckets ? Object.keys(req.dateBuckets) : [];
587
703
  validateBucketAliases(bucketAliases, groupCols, req.measures);
588
704
  const stages = [];
589
705
  const aliasSet = new Set((req.lookups ?? []).map((l) => l.as ?? l.from));
590
- const { base: baseFilter, joined: joinedFilter } = splitFilterByAlias(req.filter, aliasSet);
706
+ const { base: baseFilter, joined: joinedFilter } = splitFilterByAlias(schema !== void 0 && req.filter !== void 0 ? castFilterToSchema(req.filter, schema) : req.filter, aliasSet);
591
707
  if (baseFilter !== void 0) {
592
708
  const match = compileFilterToMongo(baseFilter);
593
709
  if (Object.keys(match).length > 0) stages.push({ $match: match });
@@ -869,7 +985,7 @@ async function countAggGroups(Model, req, options = {}) {
869
985
  const hasDottedGroupBy = groupCols.some((f) => f.includes("."));
870
986
  const hasDateBuckets = !!req.dateBuckets && Object.keys(req.dateBuckets).length > 0;
871
987
  if (req.having !== void 0 || req.lookups !== void 0 && req.lookups.length > 0 || hasDottedGroupBy || hasDateBuckets) {
872
- const { pipeline, prePaginationIndex } = buildAggPipeline(req);
988
+ const { pipeline, prePaginationIndex } = buildAggPipeline(req, Model.schema);
873
989
  const finalPipeline = [...pipeline.slice(0, prePaginationIndex), { $count: "n" }];
874
990
  const aggregation = Model.aggregate(finalPipeline);
875
991
  if (session) aggregation.session(session);
@@ -877,7 +993,7 @@ async function countAggGroups(Model, req, options = {}) {
877
993
  const [row] = await aggregation.exec();
878
994
  return row?.n ?? 0;
879
995
  }
880
- const match = req.filter ? compileFilterToMongo(req.filter) : {};
996
+ const match = req.filter ? compileFilterToMongo(castFilterToSchema(req.filter, Model.schema)) : {};
881
997
  if (groupCols.length === 0) {
882
998
  const pipeline = [];
883
999
  if (Object.keys(match).length > 0) pipeline.push({ $match: match });
@@ -906,7 +1022,7 @@ async function countAggGroups(Model, req, options = {}) {
906
1022
  //#region src/actions/aggregate-ir/execute.ts
907
1023
  async function executeAgg(Model, req, options = {}) {
908
1024
  const session = options.session;
909
- const { pipeline } = buildAggPipeline(req);
1025
+ const { pipeline } = buildAggPipeline(req, Model.schema);
910
1026
  const aggregation = Model.aggregate(pipeline);
911
1027
  if (session) aggregation.session(session);
912
1028
  applyExecutionHints(aggregation, req.executionHints);
@@ -3746,7 +3862,7 @@ var Repository = class extends RepositoryBase {
3746
3862
  if (!finalReq.sort || Object.keys(finalReq.sort).length === 0) throw new Error("mongokit/aggregatePaginate: keyset pagination requires `sort` — the cursor anchors on the sort-key tuple");
3747
3863
  const cursor = finalReq.after ? decodeAggCursor$1(finalReq.after) : void 0;
3748
3864
  const keysetMatch = cursor ? buildKeysetPredicate(finalReq.sort, cursor) : void 0;
3749
- const { pipeline, prePaginationIndex } = buildAggPipeline(finalReq);
3865
+ const { pipeline, prePaginationIndex } = buildAggPipeline(finalReq, this.Model.schema);
3750
3866
  const headStages = pipeline.slice(0, prePaginationIndex);
3751
3867
  const tailStages = [];
3752
3868
  if (keysetMatch) tailStages.push(keysetMatch);
@@ -1,5 +1,5 @@
1
1
  import { t as __exportAll } from "../rolldown-runtime-D7D4PA-g.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-1Lq9OMD2.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-CI2NdAW6.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 { n as Repository } from "../Repository-BFBTAMlA.mjs";
1
+ import { n as Repository } from "../Repository-DThvFCaV.mjs";
2
2
  import { n as createMongooseAdapter } from "../adapter-CLvUHiTW.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-BFBTAMlA.mjs";
2
- import { a as methodRegistryPlugin, d as cachePlugin, f as batchOperationsPlugin, g as auditLogPlugin, i as multiTenantPlugin, m as auditTrailPlugin, n as softDeletePlugin, o as customIdPlugin, t as timestampPlugin } from "./timestamp.plugin-ennbatTW.mjs";
1
+ import { n as Repository } from "./Repository-DThvFCaV.mjs";
2
+ import { a as methodRegistryPlugin, d as cachePlugin, f as batchOperationsPlugin, g as auditLogPlugin, i as multiTenantPlugin, m as auditTrailPlugin, n as softDeletePlugin, o as customIdPlugin, t as timestampPlugin } from "./timestamp.plugin-CFL6LlYZ.mjs";
3
3
  //#region src/create-repository.ts
4
4
  function isLoggerAudit(value) {
5
5
  const v = value;
package/dist/index.d.mts CHANGED
@@ -8,7 +8,7 @@ import { A as ensureAuditTrailReady, C as AuditTrailMethods, D as AuditTrailQuer
8
8
  import { t as PaginationEngine } from "./PaginationEngine-CBS1XGzp.mjs";
9
9
  import { a as Repository, i as HOOK_PRIORITY, n as CreateRepositoryConfig, o as TransitionMachine, r as createRepository, s as AggregationBuilder, t as AuditConfig } from "./create-repository-D4ZU94b3.mjs";
10
10
  import { a as SoftDeleteFilterMode, c as ValidationChainOptions, i as Logger, l as ValidatorDefinition, n as CascadeRelation, o as SoftDeleteOptions, r as FieldPreset, s as SoftDeleteRepository, t as CascadeOptions } from "./plugin-options-vFufQZMZ.mjs";
11
- import { A as ImmutableStatesPluginOptions, B as AppendOnlyPluginOptions, C as LeasePluginOptions, D as IdempotencyStorePluginOptions, E as IdempotencyStoreMethods, F as cascadePlugin, H as AggregateHelpersMethods, I as BatchOperationsMethods, L as BulkWriteResult, M as fieldFilterPlugin, N as ElasticSearchOptions, O as idempotencyStorePlugin, P as elasticSearchPlugin, R as batchOperationsPlugin, S as LeaseMethods, T as IdempotencyClaim, U as aggregateHelpersPlugin, V as appendOnlyPlugin, _ as observabilityPlugin, a as uniqueField, c as TenantContext, d as SubdocumentMethods, f as subdocumentPlugin, g as OperationMetric, h as ObservabilityOptions, i as requireField, j as immutableStatesPlugin, k as ImmutableClaimView, l as TenantStore, m as softDeletePlugin, n as blockIf, o as validationChainPlugin, p as SoftDeleteMethods, r as immutableField, s as timestampPlugin, t as autoInject, u as createTenantContext, v as MongoOperationsMethods, w as leasePlugin, x as methodRegistryPlugin, y as mongoOperationsPlugin, z as auditLogPlugin } from "./validation-chain.plugin-Cm0Ttnd8.mjs";
11
+ import { A as IdempotencyStorePluginOptions, B as AppendOnlyPluginOptions, C as LeasePluginOptions, D as immutableStatesPlugin, E as ImmutableStatesPluginOptions, F as cascadePlugin, H as AggregateHelpersMethods, I as BatchOperationsMethods, L as BulkWriteResult, M as fieldFilterPlugin, N as ElasticSearchOptions, O as IdempotencyClaim, P as elasticSearchPlugin, R as batchOperationsPlugin, S as LeaseMethods, T as ImmutableClaimView, U as aggregateHelpersPlugin, V as appendOnlyPlugin, _ as observabilityPlugin, a as uniqueField, c as TenantContext, d as SubdocumentMethods, f as subdocumentPlugin, g as OperationMetric, h as ObservabilityOptions, i as requireField, j as idempotencyStorePlugin, k as IdempotencyStoreMethods, l as TenantStore, m as softDeletePlugin, n as blockIf, o as validationChainPlugin, p as SoftDeleteMethods, r as immutableField, s as timestampPlugin, t as autoInject, u as createTenantContext, v as MongoOperationsMethods, w as leasePlugin, x as methodRegistryPlugin, y as mongoOperationsPlugin, z as auditLogPlugin } from "./validation-chain.plugin-BLecaKa0.mjs";
12
12
  import { n as SchemaPathsLike, t as FieldType$1 } from "./coercion-BjdPX3YF.mjs";
13
13
  import { n as SchemaIndexes } from "./indexes-DIjMpYES.mjs";
14
14
  import { _ as isDuplicateKeyError, a as configureLogger, c as idVariants, f as createFieldPreset, g as createError, h as getMongooseProjection, i as createMemoryCache, m as getFieldsForUser, n as buildCrudSchemasFromModel, p as filterResponseData, r as buildCrudSchemasFromMongooseSchema, t as toPlain, v as parseDuplicateKeyError } from "./to-plain-DkF5px0R.mjs";
package/dist/index.mjs CHANGED
@@ -1,14 +1,14 @@
1
1
  import { n as idVariants } from "./id-resolution-BqDrk0S2.mjs";
2
- import { a as supportsTransactions, c as ALL_OPERATIONS, d as READ_OPERATIONS, f as operationsByPolicyKey, i as isTransactionUnsupported, l as MUTATING_OPERATIONS, m as createMongoPurgePort, n as Repository, o as withTransaction, p as MONGOKIT_CAPABILITIES, r as batchTransaction, s as AggregationBuilder, t as HOOK_PRIORITY, u as OP_REGISTRY } from "./Repository-BFBTAMlA.mjs";
3
- import { b as LookupBuilder, x as compileFilterToMongo } from "./update-1Lq9OMD2.mjs";
2
+ import { a as supportsTransactions, c as ALL_OPERATIONS, d as READ_OPERATIONS, f as operationsByPolicyKey, i as isTransactionUnsupported, l as MUTATING_OPERATIONS, m as createMongoPurgePort, n as Repository, o as withTransaction, p as MONGOKIT_CAPABILITIES, r as batchTransaction, s as AggregationBuilder, t as HOOK_PRIORITY, u as OP_REGISTRY } from "./Repository-DThvFCaV.mjs";
3
+ import { b as LookupBuilder, x as compileFilterToMongo } from "./update-CI2NdAW6.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, g as auditLogPlugin, h as ensureAuditTrailReady, 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-ennbatTW.mjs";
7
+ import { a as methodRegistryPlugin, c as getNextSequence, d as cachePlugin, f as batchOperationsPlugin, g as auditLogPlugin, h as ensureAuditTrailReady, 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-CFL6LlYZ.mjs";
8
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-2DMwkYVz.mjs";
11
- import { _ as cascadePlugin, a as uniqueField, c as subdocumentPlugin, d as mongoOperationsPlugin, f as leasePlugin, g as elasticSearchPlugin, h as fieldFilterPlugin, i as requireField, l as observabilityPlugin, m as immutableStatesPlugin, n as blockIf, o as validationChainPlugin, p as idempotencyStorePlugin, r as immutableField, s as createTenantContext, t as autoInject, u as MONGOKIT_PLUGIN_NAMES, v as appendOnlyPlugin, y as aggregateHelpersPlugin } from "./validation-chain.plugin-k9joH2qw.mjs";
10
+ import { t as createRepository } from "./create-repository-uhKt_oUT.mjs";
11
+ import { _ as cascadePlugin, a as uniqueField, c as subdocumentPlugin, d as mongoOperationsPlugin, f as leasePlugin, g as elasticSearchPlugin, h as fieldFilterPlugin, i as requireField, l as observabilityPlugin, m as idempotencyStorePlugin, n as blockIf, o as validationChainPlugin, p as immutableStatesPlugin, r as immutableField, s as createTenantContext, t as autoInject, u as MONGOKIT_PLUGIN_NAMES, v as appendOnlyPlugin, y as aggregateHelpersPlugin } from "./validation-chain.plugin-CwOcUJC1.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,6 +1,6 @@
1
1
  import { S as RepositoryPluginName, b as MONGOKIT_PLUGIN_NAMES, l as Plugin, x as MongoKitPluginName } from "../repository-DCHRWRXw.mjs";
2
2
  import { A as ensureAuditTrailReady, C as AuditTrailMethods, D as AuditTrailQueryOptions, E as AuditTrailQuery, O as EnsureAuditTrailReadyOptions, S as AuditQueryResult, T as AuditTrailOptions, _ as RepositoryCachePluginOptions, a as DateSequentialIdOptions, b as AuditOperation, c as SequentialIdOptions, d as getNextSequence, f as prefixedId, g as RepositoryCacheHandle, h as CacheOptions, i as CustomIdOptions, k as auditTrailPlugin, l as customIdPlugin, m as CacheAdapter, o as IdGenerator, p as sequentialId, r as multiTenantPlugin, s as PrefixedIdOptions, t as MultiTenantOptions, u as dateSequentialId, v as cachePlugin, w as AuditTrailMode, x as AuditQueryOptions, y as AuditEntry } from "../multi-tenant.plugin-DVyVGREb.mjs";
3
- import { A as ImmutableStatesPluginOptions, B as AppendOnlyPluginOptions, C as LeasePluginOptions, D as IdempotencyStorePluginOptions, E as IdempotencyStoreMethods, F as cascadePlugin, H as AggregateHelpersMethods, I as BatchOperationsMethods, M as fieldFilterPlugin, N as ElasticSearchOptions, O as idempotencyStorePlugin, P as elasticSearchPlugin, R as batchOperationsPlugin, S as LeaseMethods, T as IdempotencyClaim, U as aggregateHelpersPlugin, V as appendOnlyPlugin, _ as observabilityPlugin, a as uniqueField, b as MethodRegistryRepository, c as TenantContext, d as SubdocumentMethods, f as subdocumentPlugin, g as OperationMetric, h as ObservabilityOptions, i as requireField, j as immutableStatesPlugin, k as ImmutableClaimView, l as TenantStore, m as softDeletePlugin, n as blockIf, o as validationChainPlugin, p as SoftDeleteMethods, r as immutableField, s as timestampPlugin, t as autoInject, u as createTenantContext, v as MongoOperationsMethods, w as leasePlugin, x as methodRegistryPlugin, y as mongoOperationsPlugin, z as auditLogPlugin } from "../validation-chain.plugin-Cm0Ttnd8.mjs";
3
+ import { A as IdempotencyStorePluginOptions, B as AppendOnlyPluginOptions, C as LeasePluginOptions, D as immutableStatesPlugin, E as ImmutableStatesPluginOptions, F as cascadePlugin, H as AggregateHelpersMethods, I as BatchOperationsMethods, M as fieldFilterPlugin, N as ElasticSearchOptions, O as IdempotencyClaim, P as elasticSearchPlugin, R as batchOperationsPlugin, S as LeaseMethods, T as ImmutableClaimView, U as aggregateHelpersPlugin, V as appendOnlyPlugin, _ as observabilityPlugin, a as uniqueField, b as MethodRegistryRepository, c as TenantContext, d as SubdocumentMethods, f as subdocumentPlugin, g as OperationMetric, h as ObservabilityOptions, i as requireField, j as idempotencyStorePlugin, k as IdempotencyStoreMethods, l as TenantStore, m as softDeletePlugin, n as blockIf, o as validationChainPlugin, p as SoftDeleteMethods, r as immutableField, s as timestampPlugin, t as autoInject, u as createTenantContext, v as MongoOperationsMethods, w as leasePlugin, x as methodRegistryPlugin, y as mongoOperationsPlugin, z as auditLogPlugin } from "../validation-chain.plugin-BLecaKa0.mjs";
4
4
  import { ChangeLogStore } from "@classytic/repo-core/sync";
5
5
  //#region src/plugins/change-log.plugin.d.ts
6
6
  interface ChangeLogPluginOptions {
@@ -1,5 +1,5 @@
1
- import { a as methodRegistryPlugin, c as getNextSequence, d as cachePlugin, f as batchOperationsPlugin, g as auditLogPlugin, h as ensureAuditTrailReady, 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-ennbatTW.mjs";
2
- import { _ as cascadePlugin, a as uniqueField, c as subdocumentPlugin, d as mongoOperationsPlugin, f as leasePlugin, g as elasticSearchPlugin, h as fieldFilterPlugin, i as requireField, l as observabilityPlugin, m as immutableStatesPlugin, n as blockIf, o as validationChainPlugin, p as idempotencyStorePlugin, r as immutableField, s as createTenantContext, t as autoInject, u as MONGOKIT_PLUGIN_NAMES, v as appendOnlyPlugin, y as aggregateHelpersPlugin } from "../validation-chain.plugin-k9joH2qw.mjs";
1
+ import { a as methodRegistryPlugin, c as getNextSequence, d as cachePlugin, f as batchOperationsPlugin, g as auditLogPlugin, h as ensureAuditTrailReady, 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-CFL6LlYZ.mjs";
2
+ import { _ as cascadePlugin, a as uniqueField, c as subdocumentPlugin, d as mongoOperationsPlugin, f as leasePlugin, g as elasticSearchPlugin, h as fieldFilterPlugin, i as requireField, l as observabilityPlugin, m as idempotencyStorePlugin, n as blockIf, o as validationChainPlugin, p as immutableStatesPlugin, r as immutableField, s as createTenantContext, t as autoInject, u as MONGOKIT_PLUGIN_NAMES, v as appendOnlyPlugin, y as aggregateHelpersPlugin } from "../validation-chain.plugin-CwOcUJC1.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-2DMwkYVz.mjs";
1
+ import { t as createRepository } from "../create-repository-uhKt_oUT.mjs";
2
2
  import { createConnection } from "mongoose";
3
3
  //#region src/testkit/index.ts
4
4
  function idempotentStop(server) {
@@ -1,4 +1,4 @@
1
- import { c as ALL_OPERATIONS, t as HOOK_PRIORITY, u as OP_REGISTRY } from "./Repository-BFBTAMlA.mjs";
1
+ import { c as ALL_OPERATIONS, t as HOOK_PRIORITY, u as OP_REGISTRY } from "./Repository-DThvFCaV.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,7 +1,7 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.mjs";
2
2
  import { n as warn } from "./logger-K7dPmu2U.mjs";
3
3
  import { t as createError } from "./error-BuByZwKU.mjs";
4
- import { isFilter } from "@classytic/repo-core/filter";
4
+ import { coerceFilterDates, isFilter } from "@classytic/repo-core/filter";
5
5
  //#region src/filter/compile.ts
6
6
  /**
7
7
  * Compile a Filter IR node to a MongoDB query object. Returns `{}`
@@ -34,47 +34,41 @@ const SHORTHAND_OPS = /* @__PURE__ */ new Set([
34
34
  "regex",
35
35
  "mod"
36
36
  ]);
37
- /** Range operators whose string values should be coerced to Date. */
38
- const RANGE_OPS = /* @__PURE__ */ new Set([
39
- "gt",
40
- "gte",
41
- "lt",
42
- "lte"
37
+ /** Logical operators whose operand is an array of sub-queries to recurse into. */
38
+ const LOGICAL_ARRAY_OPS = /* @__PURE__ */ new Set([
39
+ "$and",
40
+ "$or",
41
+ "$nor"
43
42
  ]);
44
- /** ISO-8601 date-only or datetime string pattern. */
45
- const ISO_DATE_RE = /^\d{4}-\d{2}-\d{2}/;
46
- /**
47
- * Coerce a string value to a Date when it looks like an ISO-8601 date.
48
- * Needed because MongoDB won't match a BSON Date field against a string
49
- * value even when using range operators — BSON type comparison treats
50
- * Date (type 9) and String (type 2) as distinct, so `{ createdAt: {
51
- * $gte: "2026-05-01" } }` never matches a Date-typed field.
52
- */
53
- function tryCoerceDate(value) {
54
- if (typeof value === "string" && ISO_DATE_RE.test(value)) {
55
- const d = new Date(value);
56
- return Number.isNaN(d.getTime()) ? value : d;
57
- }
58
- return value;
59
- }
60
43
  /**
61
- * Expand operator shorthand values in a plain Mongo-style query object.
44
+ * Expand operator shorthand values in a plain Mongo-style query object —
45
+ * `$`-prefixing is the ONE Mongo-dialect concern this function owns. Date
46
+ * coercion is delegated to `coerceFilterDates` (repo-core) so the ISO
47
+ * pattern has a single source of truth shared with the URL-parsing boundary.
48
+ *
62
49
  * Only expands nested objects whose keys are ALL in `SHORTHAND_OPS` — so
63
50
  * real nested documents (e.g. `address: { city: 'Dhaka' }`) pass through
64
51
  * unchanged, while `createdAt: { gte: '2026-04-01', lte: '2026-05-01' }`
65
- * becomes `createdAt: { $gte: Date(2026-04-01), $lte: Date(2026-05-01) }`.
66
- * String values on range operators are coerced to Date when they match the
67
- * ISO-8601 pattern, so BSON Date fields in MongoDB are compared correctly.
52
+ * becomes `createdAt: { $gte: '2026-04-01', $lte: '2026-05-01' }` for the
53
+ * coercion pass to type.
54
+ *
55
+ * Recurses `$and` / `$or` / `$nor` because a policy-scope merge conjoins the
56
+ * caller filter under `$and`, and un-prefixed shorthand nested there would
57
+ * otherwise reach `$match` as a literal field named `gte`.
68
58
  */
69
59
  function expandShorthands(obj) {
70
60
  const out = {};
71
61
  for (const [key, value] of Object.entries(obj)) {
62
+ if (LOGICAL_ARRAY_OPS.has(key) && Array.isArray(value)) {
63
+ out[key] = value.map((entry) => entry !== null && typeof entry === "object" && !Array.isArray(entry) ? expandShorthands(entry) : entry);
64
+ continue;
65
+ }
72
66
  if (value !== null && typeof value === "object" && !Array.isArray(value)) {
73
67
  const nested = value;
74
68
  const keys = Object.keys(nested);
75
69
  if (keys.length > 0 && keys.every((k) => !k.startsWith("$") && SHORTHAND_OPS.has(k))) {
76
70
  const expanded = {};
77
- for (const [op, opVal] of Object.entries(nested)) expanded[`$${op}`] = RANGE_OPS.has(op) ? tryCoerceDate(opVal) : opVal;
71
+ for (const [op, opVal] of Object.entries(nested)) expanded[`$${op}`] = opVal;
78
72
  out[key] = expanded;
79
73
  continue;
80
74
  }
@@ -85,7 +79,7 @@ function expandShorthands(obj) {
85
79
  }
86
80
  function compileFilterToMongo(input) {
87
81
  if (!input) return {};
88
- if (!isFilter(input)) return expandShorthands(input);
82
+ if (!isFilter(input)) return coerceFilterDates(expandShorthands(input));
89
83
  return compile(input);
90
84
  }
91
85
  function compile(filter) {
@@ -185,47 +185,6 @@ declare function elasticSearchPlugin(options: ElasticSearchOptions): Plugin;
185
185
  */
186
186
  declare function fieldFilterPlugin(fieldPreset: FieldPreset): Plugin;
187
187
  //#endregion
188
- //#region src/plugins/immutable-states.plugin.d.ts
189
- /** The slice of a `before:claim` context `allowClaim` predicates see. */
190
- interface ImmutableClaimView {
191
- id: unknown;
192
- transition: {
193
- from?: unknown | readonly unknown[];
194
- to?: unknown;
195
- field?: string;
196
- where?: Record<string, unknown>;
197
- };
198
- /** Operator-form patch (`{ $set: {...}, ... }`). */
199
- data: Record<string, unknown> | undefined;
200
- }
201
- interface ImmutableStatesPluginOptions {
202
- /** States in which the document is frozen (e.g. `['posted']`). */
203
- states: readonly string[];
204
- /** State field on the doc — dotted paths supported. Default `'state'`. */
205
- field?: string | undefined;
206
- /**
207
- * Context flag the OWNING ENGINE's verbs set to pass the guard
208
- * (private handshake — see module doc). Default `'_immutableInternal'`.
209
- */
210
- internalFlag?: string | undefined;
211
- /** Tenant field used to scope state lookups (mirrors the repo's scope). */
212
- tenantField?: string | undefined;
213
- /**
214
- * Exempt specific claim shapes that legitimately touch frozen rows
215
- * (audit-trail stamps like ledger's reverse-mark). Return `true` to
216
- * allow. Runs only after the claim was determined to reach a frozen
217
- * state — keep the predicate a strict fingerprint.
218
- */
219
- allowClaim?: ((view: ImmutableClaimView) => boolean) | undefined;
220
- /** Domain error to throw. Default: 403 `IMMUTABLE_STATE_VIOLATION`. */
221
- errorFactory?: ((ctx: {
222
- id: unknown;
223
- operation: string;
224
- state?: string | undefined;
225
- }) => Error) | undefined;
226
- }
227
- declare function immutableStatesPlugin(options: ImmutableStatesPluginOptions): Plugin;
228
- //#endregion
229
188
  //#region src/plugins/idempotency-store.plugin.d.ts
230
189
  interface IdempotencyStorePluginOptions {
231
190
  /** Field carrying the deterministic operation key. @default `'key'` */
@@ -362,6 +321,47 @@ interface IdempotencyStoreMethods<TProgress = Record<string, unknown>, TResult =
362
321
  }
363
322
  declare function idempotencyStorePlugin(options?: IdempotencyStorePluginOptions): Plugin;
364
323
  //#endregion
324
+ //#region src/plugins/immutable-states.plugin.d.ts
325
+ /** The slice of a `before:claim` context `allowClaim` predicates see. */
326
+ interface ImmutableClaimView {
327
+ id: unknown;
328
+ transition: {
329
+ from?: unknown | readonly unknown[];
330
+ to?: unknown;
331
+ field?: string;
332
+ where?: Record<string, unknown>;
333
+ };
334
+ /** Operator-form patch (`{ $set: {...}, ... }`). */
335
+ data: Record<string, unknown> | undefined;
336
+ }
337
+ interface ImmutableStatesPluginOptions {
338
+ /** States in which the document is frozen (e.g. `['posted']`). */
339
+ states: readonly string[];
340
+ /** State field on the doc — dotted paths supported. Default `'state'`. */
341
+ field?: string | undefined;
342
+ /**
343
+ * Context flag the OWNING ENGINE's verbs set to pass the guard
344
+ * (private handshake — see module doc). Default `'_immutableInternal'`.
345
+ */
346
+ internalFlag?: string | undefined;
347
+ /** Tenant field used to scope state lookups (mirrors the repo's scope). */
348
+ tenantField?: string | undefined;
349
+ /**
350
+ * Exempt specific claim shapes that legitimately touch frozen rows
351
+ * (audit-trail stamps like ledger's reverse-mark). Return `true` to
352
+ * allow. Runs only after the claim was determined to reach a frozen
353
+ * state — keep the predicate a strict fingerprint.
354
+ */
355
+ allowClaim?: ((view: ImmutableClaimView) => boolean) | undefined;
356
+ /** Domain error to throw. Default: 403 `IMMUTABLE_STATE_VIOLATION`. */
357
+ errorFactory?: ((ctx: {
358
+ id: unknown;
359
+ operation: string;
360
+ state?: string | undefined;
361
+ }) => Error) | undefined;
362
+ }
363
+ declare function immutableStatesPlugin(options: ImmutableStatesPluginOptions): Plugin;
364
+ //#endregion
365
365
  //#region src/plugins/lease.plugin.d.ts
366
366
  interface LeasePluginOptions {
367
367
  /** Field carrying the row's status. @default `'status'` */
@@ -958,4 +958,4 @@ declare function immutableField(field: string): ValidatorDefinition;
958
958
  */
959
959
  declare function uniqueField(field: string, errorMessage?: string): ValidatorDefinition;
960
960
  //#endregion
961
- export { ImmutableStatesPluginOptions as A, AppendOnlyPluginOptions as B, LeasePluginOptions as C, IdempotencyStorePluginOptions as D, IdempotencyStoreMethods as E, cascadePlugin as F, AggregateHelpersMethods as H, BatchOperationsMethods as I, BulkWriteResult as L, fieldFilterPlugin as M, ElasticSearchOptions as N, idempotencyStorePlugin as O, elasticSearchPlugin as P, batchOperationsPlugin as R, LeaseMethods as S, IdempotencyClaim as T, aggregateHelpersPlugin as U, appendOnlyPlugin as V, observabilityPlugin as _, uniqueField as a, MethodRegistryRepository as b, TenantContext as c, SubdocumentMethods as d, subdocumentPlugin as f, OperationMetric as g, ObservabilityOptions as h, requireField as i, immutableStatesPlugin as j, ImmutableClaimView as k, TenantStore as l, softDeletePlugin as m, blockIf as n, validationChainPlugin as o, SoftDeleteMethods as p, immutableField as r, timestampPlugin as s, autoInject as t, createTenantContext as u, MongoOperationsMethods as v, leasePlugin as w, methodRegistryPlugin as x, mongoOperationsPlugin as y, auditLogPlugin as z };
961
+ export { IdempotencyStorePluginOptions as A, AppendOnlyPluginOptions as B, LeasePluginOptions as C, immutableStatesPlugin as D, ImmutableStatesPluginOptions as E, cascadePlugin as F, AggregateHelpersMethods as H, BatchOperationsMethods as I, BulkWriteResult as L, fieldFilterPlugin as M, ElasticSearchOptions as N, IdempotencyClaim as O, elasticSearchPlugin as P, batchOperationsPlugin as R, LeaseMethods as S, ImmutableClaimView as T, aggregateHelpersPlugin as U, appendOnlyPlugin as V, observabilityPlugin as _, uniqueField as a, MethodRegistryRepository as b, TenantContext as c, SubdocumentMethods as d, subdocumentPlugin as f, OperationMetric as g, ObservabilityOptions as h, requireField as i, idempotencyStorePlugin as j, IdempotencyStoreMethods as k, TenantStore as l, softDeletePlugin as m, blockIf as n, validationChainPlugin as o, SoftDeleteMethods as p, immutableField as r, timestampPlugin as s, autoInject as t, createTenantContext as u, MongoOperationsMethods as v, leasePlugin as w, methodRegistryPlugin as x, mongoOperationsPlugin as y, auditLogPlugin as z };
@@ -1,5 +1,5 @@
1
- import { c as ALL_OPERATIONS, u as OP_REGISTRY } from "./Repository-BFBTAMlA.mjs";
2
- import { _ as upsert } from "./update-1Lq9OMD2.mjs";
1
+ import { c as ALL_OPERATIONS, u as OP_REGISTRY } from "./Repository-DThvFCaV.mjs";
2
+ import { _ as upsert } from "./update-CI2NdAW6.mjs";
3
3
  import { n as warn } from "./logger-K7dPmu2U.mjs";
4
4
  import { t as createError } from "./error-BuByZwKU.mjs";
5
5
  import { r as getFieldsForUser } from "./field-selection-FSQFLpsU.mjs";
@@ -476,140 +476,6 @@ function fieldFilterPlugin(fieldPreset) {
476
476
  };
477
477
  }
478
478
  //#endregion
479
- //#region src/plugins/immutable-states.plugin.ts
480
- /** Ops this plugin has an explicit strategy for (see module doc). */
481
- const STRATEGY_OPS = /* @__PURE__ */ new Set([
482
- "create",
483
- "createMany",
484
- "update",
485
- "delete",
486
- "claimVersion",
487
- "restore",
488
- "findOneAndUpdate",
489
- "updateMany",
490
- "deleteMany",
491
- "bulkWrite",
492
- "claim"
493
- ]);
494
- function immutableStatesPlugin(options) {
495
- const field = options.field ?? "state";
496
- const internalFlag = options.internalFlag ?? "_immutableInternal";
497
- const frozen = new Set(options.states);
498
- const throwViolation = (ctx) => {
499
- if (options.errorFactory) throw options.errorFactory(ctx);
500
- throw createError(403, `Document ${String(ctx.id)} is in immutable state${ctx.state !== void 0 ? ` '${ctx.state}'` : ""} — '${ctx.operation}' is not permitted. Frozen rows change only through the owning engine's transition verbs.`, {
501
- code: "IMMUTABLE_STATE_VIOLATION",
502
- meta: { ...ctx }
503
- });
504
- };
505
- return {
506
- name: "immutableStates",
507
- apply(repo) {
508
- const unhandled = Object.entries(OP_REGISTRY).filter(([op, meta]) => meta.mutates && !STRATEGY_OPS.has(op)).map(([op]) => op);
509
- if (unhandled.length > 0) throw createError(500, `immutableStatesPlugin has no strategy for mutating op(s): ${unhandled.join(", ")}. Mongokit added write ops this plugin predates — extend the plugin before using it.`, {
510
- code: "IMMUTABLE_STATES_UNHANDLED_OPS",
511
- meta: { unhandled }
512
- });
513
- const Model = repo.Model;
514
- const getState = (doc) => field.split(".").reduce((acc, key) => acc?.[key], doc);
515
- const scoped = (base, ctx) => {
516
- const q = { ...base };
517
- if (options.tenantField && ctx.query && options.tenantField in ctx.query) q[options.tenantField] = ctx.query[options.tenantField];
518
- return q;
519
- };
520
- /** Raw-Model read — never re-enters the repo hook pipeline. */
521
- const readStateById = async (ctx) => {
522
- if (ctx.id === void 0 || ctx.id === null) return void 0;
523
- const doc = await Model.findOne(scoped({ _id: ctx.id }, ctx)).select({ [field]: 1 }).lean();
524
- return doc ? getState(doc) : void 0;
525
- };
526
- const guardById = (op) => async (ctx) => {
527
- if (ctx[internalFlag]) return;
528
- const state = await readStateById(ctx);
529
- if (state !== void 0 && frozen.has(state)) throwViolation({
530
- id: ctx.id,
531
- operation: op,
532
- state
533
- });
534
- };
535
- for (const op of [
536
- "update",
537
- "delete",
538
- "claimVersion",
539
- "restore"
540
- ]) repo.on(`before:${op}`, guardById(op));
541
- repo.on("before:findOneAndUpdate", async (ctx) => {
542
- if (ctx[internalFlag]) return;
543
- const filter = ctx.query ?? {};
544
- const doc = await Model.findOne(filter).select({
545
- [field]: 1,
546
- _id: 1
547
- }).lean();
548
- if (!doc) return;
549
- const state = getState(doc);
550
- if (state !== void 0 && frozen.has(state)) throwViolation({
551
- id: doc._id,
552
- operation: "findOneAndUpdate",
553
- state
554
- });
555
- });
556
- const guardMany = (op) => async (ctx) => {
557
- if (ctx[internalFlag]) return;
558
- const filter = ctx.query ?? {};
559
- const hit = await Model.exists({ $and: [filter, { [field]: { $in: [...frozen] } }] });
560
- if (hit) throwViolation({
561
- id: hit._id,
562
- operation: op,
563
- state: void 0
564
- });
565
- };
566
- repo.on("before:updateMany", guardMany("updateMany"));
567
- repo.on("before:deleteMany", guardMany("deleteMany"));
568
- repo.on("before:bulkWrite", (ctx) => {
569
- if (ctx[internalFlag]) return;
570
- throwViolation({
571
- id: void 0,
572
- operation: "bulkWrite",
573
- state: void 0
574
- });
575
- });
576
- repo.on("before:claim", async (rawCtx) => {
577
- const ctx = rawCtx;
578
- if (ctx[internalFlag]) return;
579
- const t = ctx.transition;
580
- if (!t) return;
581
- if ((t.field ?? "status") === field) {
582
- const fromSpec = t.from;
583
- if (!(Array.isArray(fromSpec) ? fromSpec.some((f) => frozen.has(String(f))) : frozen.has(String(fromSpec)))) return;
584
- if (options.allowClaim?.({
585
- id: ctx.id,
586
- transition: t,
587
- data: ctx.data
588
- })) return;
589
- throwViolation({
590
- id: ctx.id,
591
- operation: "claim",
592
- state: void 0
593
- });
594
- }
595
- const state = await readStateById(ctx);
596
- if (state !== void 0 && frozen.has(state)) {
597
- if (options.allowClaim?.({
598
- id: ctx.id,
599
- transition: t,
600
- data: ctx.data
601
- })) return;
602
- throwViolation({
603
- id: ctx.id,
604
- operation: "claim",
605
- state
606
- });
607
- }
608
- });
609
- }
610
- };
611
- }
612
- //#endregion
613
479
  //#region src/plugins/idempotency-store.plugin.ts
614
480
  function randomToken() {
615
481
  const bytes = /* @__PURE__ */ new Uint8Array(16);
@@ -757,6 +623,140 @@ function idempotencyStorePlugin(options = {}) {
757
623
  };
758
624
  }
759
625
  //#endregion
626
+ //#region src/plugins/immutable-states.plugin.ts
627
+ /** Ops this plugin has an explicit strategy for (see module doc). */
628
+ const STRATEGY_OPS = /* @__PURE__ */ new Set([
629
+ "create",
630
+ "createMany",
631
+ "update",
632
+ "delete",
633
+ "claimVersion",
634
+ "restore",
635
+ "findOneAndUpdate",
636
+ "updateMany",
637
+ "deleteMany",
638
+ "bulkWrite",
639
+ "claim"
640
+ ]);
641
+ function immutableStatesPlugin(options) {
642
+ const field = options.field ?? "state";
643
+ const internalFlag = options.internalFlag ?? "_immutableInternal";
644
+ const frozen = new Set(options.states);
645
+ const throwViolation = (ctx) => {
646
+ if (options.errorFactory) throw options.errorFactory(ctx);
647
+ throw createError(403, `Document ${String(ctx.id)} is in immutable state${ctx.state !== void 0 ? ` '${ctx.state}'` : ""} — '${ctx.operation}' is not permitted. Frozen rows change only through the owning engine's transition verbs.`, {
648
+ code: "IMMUTABLE_STATE_VIOLATION",
649
+ meta: { ...ctx }
650
+ });
651
+ };
652
+ return {
653
+ name: "immutableStates",
654
+ apply(repo) {
655
+ const unhandled = Object.entries(OP_REGISTRY).filter(([op, meta]) => meta.mutates && !STRATEGY_OPS.has(op)).map(([op]) => op);
656
+ if (unhandled.length > 0) throw createError(500, `immutableStatesPlugin has no strategy for mutating op(s): ${unhandled.join(", ")}. Mongokit added write ops this plugin predates — extend the plugin before using it.`, {
657
+ code: "IMMUTABLE_STATES_UNHANDLED_OPS",
658
+ meta: { unhandled }
659
+ });
660
+ const Model = repo.Model;
661
+ const getState = (doc) => field.split(".").reduce((acc, key) => acc?.[key], doc);
662
+ const scoped = (base, ctx) => {
663
+ const q = { ...base };
664
+ if (options.tenantField && ctx.query && options.tenantField in ctx.query) q[options.tenantField] = ctx.query[options.tenantField];
665
+ return q;
666
+ };
667
+ /** Raw-Model read — never re-enters the repo hook pipeline. */
668
+ const readStateById = async (ctx) => {
669
+ if (ctx.id === void 0 || ctx.id === null) return void 0;
670
+ const doc = await Model.findOne(scoped({ _id: ctx.id }, ctx)).select({ [field]: 1 }).lean();
671
+ return doc ? getState(doc) : void 0;
672
+ };
673
+ const guardById = (op) => async (ctx) => {
674
+ if (ctx[internalFlag]) return;
675
+ const state = await readStateById(ctx);
676
+ if (state !== void 0 && frozen.has(state)) throwViolation({
677
+ id: ctx.id,
678
+ operation: op,
679
+ state
680
+ });
681
+ };
682
+ for (const op of [
683
+ "update",
684
+ "delete",
685
+ "claimVersion",
686
+ "restore"
687
+ ]) repo.on(`before:${op}`, guardById(op));
688
+ repo.on("before:findOneAndUpdate", async (ctx) => {
689
+ if (ctx[internalFlag]) return;
690
+ const filter = ctx.query ?? {};
691
+ const doc = await Model.findOne(filter).select({
692
+ [field]: 1,
693
+ _id: 1
694
+ }).lean();
695
+ if (!doc) return;
696
+ const state = getState(doc);
697
+ if (state !== void 0 && frozen.has(state)) throwViolation({
698
+ id: doc._id,
699
+ operation: "findOneAndUpdate",
700
+ state
701
+ });
702
+ });
703
+ const guardMany = (op) => async (ctx) => {
704
+ if (ctx[internalFlag]) return;
705
+ const filter = ctx.query ?? {};
706
+ const hit = await Model.exists({ $and: [filter, { [field]: { $in: [...frozen] } }] });
707
+ if (hit) throwViolation({
708
+ id: hit._id,
709
+ operation: op,
710
+ state: void 0
711
+ });
712
+ };
713
+ repo.on("before:updateMany", guardMany("updateMany"));
714
+ repo.on("before:deleteMany", guardMany("deleteMany"));
715
+ repo.on("before:bulkWrite", (ctx) => {
716
+ if (ctx[internalFlag]) return;
717
+ throwViolation({
718
+ id: void 0,
719
+ operation: "bulkWrite",
720
+ state: void 0
721
+ });
722
+ });
723
+ repo.on("before:claim", async (rawCtx) => {
724
+ const ctx = rawCtx;
725
+ if (ctx[internalFlag]) return;
726
+ const t = ctx.transition;
727
+ if (!t) return;
728
+ if ((t.field ?? "status") === field) {
729
+ const fromSpec = t.from;
730
+ if (!(Array.isArray(fromSpec) ? fromSpec.some((f) => frozen.has(String(f))) : frozen.has(String(fromSpec)))) return;
731
+ if (options.allowClaim?.({
732
+ id: ctx.id,
733
+ transition: t,
734
+ data: ctx.data
735
+ })) return;
736
+ throwViolation({
737
+ id: ctx.id,
738
+ operation: "claim",
739
+ state: void 0
740
+ });
741
+ }
742
+ const state = await readStateById(ctx);
743
+ if (state !== void 0 && frozen.has(state)) {
744
+ if (options.allowClaim?.({
745
+ id: ctx.id,
746
+ transition: t,
747
+ data: ctx.data
748
+ })) return;
749
+ throwViolation({
750
+ id: ctx.id,
751
+ operation: "claim",
752
+ state
753
+ });
754
+ }
755
+ });
756
+ }
757
+ };
758
+ }
759
+ //#endregion
760
760
  //#region src/plugins/lease.plugin.ts
761
761
  function leasePlugin(options = {}) {
762
762
  const statusField = options.statusField ?? "status";
@@ -1451,4 +1451,4 @@ function uniqueField(field, errorMessage) {
1451
1451
  };
1452
1452
  }
1453
1453
  //#endregion
1454
- export { cascadePlugin as _, uniqueField as a, subdocumentPlugin as c, mongoOperationsPlugin as d, leasePlugin as f, elasticSearchPlugin as g, fieldFilterPlugin as h, requireField as i, observabilityPlugin as l, immutableStatesPlugin as m, blockIf as n, validationChainPlugin as o, idempotencyStorePlugin as p, immutableField as r, createTenantContext as s, autoInject as t, MONGOKIT_PLUGIN_NAMES as u, appendOnlyPlugin as v, aggregateHelpersPlugin as y };
1454
+ export { cascadePlugin as _, uniqueField as a, subdocumentPlugin as c, mongoOperationsPlugin as d, leasePlugin as f, elasticSearchPlugin as g, fieldFilterPlugin as h, requireField as i, observabilityPlugin as l, idempotencyStorePlugin as m, blockIf as n, validationChainPlugin as o, immutableStatesPlugin as p, immutableField as r, createTenantContext as s, autoInject as t, MONGOKIT_PLUGIN_NAMES as u, appendOnlyPlugin as v, aggregateHelpersPlugin as y };
package/package.json CHANGED
@@ -1,180 +1,187 @@
1
1
  {
2
- "name": "@classytic/mongokit",
3
- "version": "3.28.0",
4
- "description": "Production-grade MongoDB repositories - smart pagination, events, and plugins. Built on @classytic/repo-core.",
5
- "type": "module",
6
- "sideEffects": false,
7
- "main": "./dist/index.mjs",
8
- "module": "./dist/index.mjs",
9
- "types": "./dist/index.d.mts",
10
- "exports": {
11
- ".": {
12
- "types": "./dist/index.d.mts",
13
- "default": "./dist/index.mjs"
14
- },
15
- "./pagination": {
16
- "types": "./dist/pagination/PaginationEngine.d.mts",
17
- "default": "./dist/pagination/PaginationEngine.mjs"
18
- },
19
- "./plugins": {
20
- "types": "./dist/plugins/index.d.mts",
21
- "default": "./dist/plugins/index.mjs"
22
- },
23
- "./utils": {
24
- "types": "./dist/utils/index.d.mts",
25
- "default": "./dist/utils/index.mjs"
26
- },
27
- "./actions": {
28
- "types": "./dist/actions/index.d.mts",
29
- "default": "./dist/actions/index.mjs"
30
- },
31
- "./ai": {
32
- "types": "./dist/ai/index.d.mts",
33
- "default": "./dist/ai/index.mjs"
34
- },
35
- "./adapter": {
36
- "types": "./dist/adapter/index.d.mts",
37
- "default": "./dist/adapter/index.mjs"
38
- },
39
- "./better-auth": {
40
- "types": "./dist/better-auth/index.d.mts",
41
- "default": "./dist/better-auth/index.mjs"
42
- },
43
- "./lock": {
44
- "types": "./dist/lock/index.d.mts",
45
- "default": "./dist/lock/index.mjs"
46
- },
47
- "./usage": {
48
- "types": "./dist/usage/index.d.mts",
49
- "default": "./dist/usage/index.mjs"
50
- },
51
- "./query/primitives/geo": {
52
- "types": "./dist/query/primitives/geo.d.mts",
53
- "default": "./dist/query/primitives/geo.mjs"
54
- },
55
- "./query/primitives/coercion": {
56
- "types": "./dist/query/primitives/coercion.d.mts",
57
- "default": "./dist/query/primitives/coercion.mjs"
58
- },
59
- "./query/primitives/indexes": {
60
- "types": "./dist/query/primitives/indexes.d.mts",
61
- "default": "./dist/query/primitives/indexes.mjs"
62
- },
63
- "./testkit": {
64
- "types": "./dist/testkit/index.d.mts",
65
- "default": "./dist/testkit/index.mjs"
66
- }
67
- },
68
- "files": [
69
- "dist",
70
- "README.md",
71
- "LICENSE"
72
- ],
73
- "keywords": [
74
- "mongodb",
75
- "mongoose",
76
- "repository",
77
- "repository-pattern",
78
- "data-access",
79
- "pagination",
80
- "cursor-pagination",
81
- "infinite-scroll",
82
- "offset-pagination",
83
- "keyset-pagination",
84
- "caching",
85
- "redis",
86
- "cache",
87
- "multi-tenant",
88
- "multi-tenancy",
89
- "vector-search",
90
- "embeddings",
91
- "atlas-search",
92
- "observability",
93
- "event-driven",
94
- "plugin-based",
95
- "zero-dependencies",
96
- "express",
97
- "fastify",
98
- "nestjs",
99
- "nextjs",
100
- "typescript"
101
- ],
102
- "author": "Classytic <classytic.dev@gmail.com> (https://github.com/classytic)",
103
- "contributors": [
104
- "Sadman Chowdhury (https://github.com/siam923)"
105
- ],
106
- "license": "MIT",
107
- "repository": {
108
- "type": "git",
109
- "url": "git+https://github.com/classytic/mongokit.git"
110
- },
111
- "bugs": {
112
- "url": "https://github.com/classytic/mongokit/issues"
113
- },
114
- "homepage": "https://github.com/classytic/mongokit#readme",
115
- "peerDependencies": {
116
- "@classytic/repo-core": ">=0.17.0",
117
- "mongoose": ">=9.4.1"
118
- },
119
- "engines": {
120
- "node": ">=22"
121
- },
122
- "publishConfig": {
123
- "access": "public"
124
- },
125
- "scripts": {
126
- "build": "tsdown",
127
- "dev": "tsdown --watch",
128
- "test": "vitest run --project unit --project integration",
129
- "test:unit": "vitest run --project unit",
130
- "test:integration": "vitest run --project integration",
131
- "test:e2e": "vitest run --project e2e",
132
- "test:all": "vitest run --project unit --project integration --project e2e",
133
- "test:watch": "vitest --project unit --project integration",
134
- "test:coverage": "vitest run --coverage",
135
- "typecheck": "tsc --noEmit",
136
- "typecheck:tests": "tsc --noEmit -p tsconfig.tests.json",
137
- "lint": "biome check src/",
138
- "lint:fix": "biome check src/ --write",
139
- "lint:package": "publint && attw --pack . --profile esm-only",
140
- "format": "biome format src/ --write",
141
- "check": "biome ci src/ --diagnostic-level=error",
142
- "check:dead-code": "knip",
143
- "ci": "npm run check && npm run typecheck && npm run typecheck:tests && npm run build && npm run lint:package && npm run check:dead-code && npm test",
144
- "push": "classytic-push",
145
- "prepublishOnly": "npm run ci",
146
- "publish:dry": "npm publish --dry-run --access public",
147
- "publish:npm": "npm publish --access public",
148
- "release:tag": "node -e \"require('child_process').execSync('npm run push -- v'+require('./package.json').version,{stdio:'inherit'})\"",
149
- "release": "npm run push -- main && npm run release:tag && npm publish --access public",
150
- "release:patch": "npm version patch && npm run release",
151
- "release:minor": "npm version minor && npm run release",
152
- "release:major": "npm version major && npm run release"
153
- },
154
- "devDependencies": {
155
- "@arethetypeswrong/cli": "^0.18.2",
156
- "@better-auth/api-key": "^1.6.22",
157
- "@better-auth/mongo-adapter": "^1.6.22",
158
- "@biomejs/biome": "^2.4.10",
159
- "@classytic/dev-tools": "^0.2.0",
160
- "@classytic/repo-core": "^0.17.0",
161
- "@types/node": "^22.0.0",
162
- "@vitest/coverage-v8": "^4.1.4",
163
- "ajv": "^8.18.0",
164
- "ajv-formats": "^3.0.1",
165
- "better-auth": "^1.6.23",
166
- "dotenv": "^17.4.2",
167
- "knip": "^6.3.0",
168
- "mongodb": "7.2.0",
169
- "mongodb-memory-server": "^10.4.3",
170
- "mongoose": "^9.4.1",
171
- "publint": "^0.3.18",
172
- "tsdown": "^0.22.5",
173
- "typescript": "~5.9.0",
174
- "vitest": "^4.1.4"
175
- },
176
- "overrides": {
177
- "mongodb": "7.2.0",
178
- "bson": "7.2.0"
179
- }
2
+ "name": "@classytic/mongokit",
3
+ "version": "3.29.0",
4
+ "description": "Production-grade MongoDB repositories - smart pagination, events, and plugins. Built on @classytic/repo-core.",
5
+ "type": "module",
6
+ "sideEffects": false,
7
+ "main": "./dist/index.mjs",
8
+ "module": "./dist/index.mjs",
9
+ "types": "./dist/index.d.mts",
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/index.d.mts",
13
+ "default": "./dist/index.mjs"
14
+ },
15
+ "./pagination": {
16
+ "types": "./dist/pagination/PaginationEngine.d.mts",
17
+ "default": "./dist/pagination/PaginationEngine.mjs"
18
+ },
19
+ "./plugins": {
20
+ "types": "./dist/plugins/index.d.mts",
21
+ "default": "./dist/plugins/index.mjs"
22
+ },
23
+ "./utils": {
24
+ "types": "./dist/utils/index.d.mts",
25
+ "default": "./dist/utils/index.mjs"
26
+ },
27
+ "./actions": {
28
+ "types": "./dist/actions/index.d.mts",
29
+ "default": "./dist/actions/index.mjs"
30
+ },
31
+ "./ai": {
32
+ "types": "./dist/ai/index.d.mts",
33
+ "default": "./dist/ai/index.mjs"
34
+ },
35
+ "./adapter": {
36
+ "types": "./dist/adapter/index.d.mts",
37
+ "default": "./dist/adapter/index.mjs"
38
+ },
39
+ "./better-auth": {
40
+ "types": "./dist/better-auth/index.d.mts",
41
+ "default": "./dist/better-auth/index.mjs"
42
+ },
43
+ "./lock": {
44
+ "types": "./dist/lock/index.d.mts",
45
+ "default": "./dist/lock/index.mjs"
46
+ },
47
+ "./usage": {
48
+ "types": "./dist/usage/index.d.mts",
49
+ "default": "./dist/usage/index.mjs"
50
+ },
51
+ "./query/primitives/geo": {
52
+ "types": "./dist/query/primitives/geo.d.mts",
53
+ "default": "./dist/query/primitives/geo.mjs"
54
+ },
55
+ "./query/primitives/coercion": {
56
+ "types": "./dist/query/primitives/coercion.d.mts",
57
+ "default": "./dist/query/primitives/coercion.mjs"
58
+ },
59
+ "./query/primitives/indexes": {
60
+ "types": "./dist/query/primitives/indexes.d.mts",
61
+ "default": "./dist/query/primitives/indexes.mjs"
62
+ },
63
+ "./testkit": {
64
+ "types": "./dist/testkit/index.d.mts",
65
+ "default": "./dist/testkit/index.mjs"
66
+ }
67
+ },
68
+ "files": [
69
+ "dist",
70
+ "README.md",
71
+ "LICENSE",
72
+ "TRADEMARK.md"
73
+ ],
74
+ "keywords": [
75
+ "mongodb",
76
+ "mongoose",
77
+ "repository",
78
+ "repository-pattern",
79
+ "data-access",
80
+ "pagination",
81
+ "cursor-pagination",
82
+ "infinite-scroll",
83
+ "offset-pagination",
84
+ "keyset-pagination",
85
+ "caching",
86
+ "redis",
87
+ "cache",
88
+ "multi-tenant",
89
+ "multi-tenancy",
90
+ "vector-search",
91
+ "embeddings",
92
+ "atlas-search",
93
+ "observability",
94
+ "event-driven",
95
+ "plugin-based",
96
+ "zero-dependencies",
97
+ "express",
98
+ "fastify",
99
+ "nestjs",
100
+ "nextjs",
101
+ "typescript"
102
+ ],
103
+ "author": "Classytic <classytic.dev@gmail.com> (https://github.com/classytic)",
104
+ "contributors": [
105
+ "Sadman Chowdhury (https://github.com/siam923)"
106
+ ],
107
+ "license": "MIT",
108
+ "repository": {
109
+ "type": "git",
110
+ "url": "git+https://github.com/classytic/mongokit.git"
111
+ },
112
+ "bugs": {
113
+ "url": "https://github.com/classytic/mongokit/issues"
114
+ },
115
+ "homepage": "https://github.com/classytic/mongokit#readme",
116
+ "peerDependencies": {
117
+ "@classytic/repo-core": ">=0.19.0",
118
+ "mongodb-memory-server": ">=10.4.3",
119
+ "mongoose": ">=9.4.1"
120
+ },
121
+ "peerDependenciesMeta": {
122
+ "mongodb-memory-server": {
123
+ "optional": true
124
+ }
125
+ },
126
+ "engines": {
127
+ "node": ">=22"
128
+ },
129
+ "publishConfig": {
130
+ "access": "public"
131
+ },
132
+ "scripts": {
133
+ "build": "tsdown",
134
+ "dev": "tsdown --watch",
135
+ "test": "vitest run --project unit --project integration",
136
+ "test:unit": "vitest run --project unit",
137
+ "test:integration": "vitest run --project integration",
138
+ "test:e2e": "vitest run --project e2e",
139
+ "test:all": "vitest run --project unit --project integration --project e2e",
140
+ "test:watch": "vitest --project unit --project integration",
141
+ "test:coverage": "vitest run --coverage",
142
+ "typecheck": "tsc --noEmit",
143
+ "typecheck:tests": "tsc --noEmit -p tsconfig.tests.json",
144
+ "lint": "biome check src/",
145
+ "lint:fix": "biome check src/ --write",
146
+ "lint:package": "publint && attw --pack . --profile esm-only",
147
+ "format": "biome format src/ --write",
148
+ "check": "biome ci src/ --diagnostic-level=error",
149
+ "check:dead-code": "knip",
150
+ "ci": "npm run check && npm run typecheck && npm run typecheck:tests && npm run build && npm run lint:package && npm run check:dead-code && npm test",
151
+ "push": "classytic-push",
152
+ "prepublishOnly": "npm run ci",
153
+ "publish:dry": "npm publish --dry-run --access public",
154
+ "publish:npm": "npm publish --access public",
155
+ "release:tag": "node -e \"require('child_process').execSync('npm run push -- v'+require('./package.json').version,{stdio:'inherit'})\"",
156
+ "release": "npm run push -- main && npm run release:tag && npm publish --access public",
157
+ "release:patch": "npm version patch && npm run release",
158
+ "release:minor": "npm version minor && npm run release",
159
+ "release:major": "npm version major && npm run release"
160
+ },
161
+ "devDependencies": {
162
+ "@arethetypeswrong/cli": "^0.18.2",
163
+ "@better-auth/api-key": "^1.6.22",
164
+ "@better-auth/mongo-adapter": "^1.6.22",
165
+ "@biomejs/biome": "^2.4.10",
166
+ "@classytic/dev-tools": "^0.2.0",
167
+ "@classytic/repo-core": "^0.19.0",
168
+ "@types/node": "^22.0.0",
169
+ "@vitest/coverage-v8": "^4.1.4",
170
+ "ajv": "^8.18.0",
171
+ "ajv-formats": "^3.0.1",
172
+ "better-auth": "^1.6.23",
173
+ "dotenv": "^17.4.2",
174
+ "knip": "^6.3.0",
175
+ "mongodb": "7.2.0",
176
+ "mongodb-memory-server": "^10.4.3",
177
+ "mongoose": "^9.4.1",
178
+ "publint": "^0.3.18",
179
+ "tsdown": "^0.22.5",
180
+ "typescript": "~5.9.0",
181
+ "vitest": "^4.1.4"
182
+ },
183
+ "overrides": {
184
+ "mongodb": "7.2.0",
185
+ "bson": "7.2.0"
186
+ }
180
187
  }