@classytic/mongokit 3.24.0 → 3.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/README.md +12 -2
  2. package/dist/{PaginationEngine-BnZ24ehC.d.mts → PaginationEngine-CBS1XGzp.d.mts} +2 -1
  3. package/dist/{Repository-ClOraFjT.mjs → Repository-BFBTAMlA.mjs} +177 -54
  4. package/dist/actions/index.d.mts +1 -1
  5. package/dist/adapter/index.d.mts +1 -1
  6. package/dist/adapter/index.mjs +1 -1
  7. package/dist/{adapter-BWgJ_Jdc.mjs → adapter-CLvUHiTW.mjs} +1 -1
  8. package/dist/ai/index.d.mts +1 -1
  9. package/dist/better-auth/index.mjs +2 -2
  10. package/dist/core-62oo3N1C.d.mts +50 -0
  11. package/dist/{create-repository-CVupgeRG.mjs → create-repository-2DMwkYVz.mjs} +2 -2
  12. package/dist/{create-repository-Cn1LkvL7.d.mts → create-repository-D4ZU94b3.d.mts} +33 -5
  13. package/dist/{id-resolution-P2OzcyRl.mjs → id-resolution-BqDrk0S2.mjs} +12 -1
  14. package/dist/{index-DLQ6pjal.d.mts → index-DceInCTX.d.mts} +3 -1
  15. package/dist/index.d.mts +70 -213
  16. package/dist/index.mjs +1071 -922
  17. package/dist/{mongooseToJsonSchema-CVnrKFDq.mjs → mongooseToJsonSchema-CByt1VhQ.mjs} +1 -1
  18. package/dist/{multi-tenant.plugin-BjXXuc_g.d.mts → multi-tenant.plugin-DVyVGREb.d.mts} +117 -18
  19. package/dist/operations-C8IcCHF9.d.mts +463 -0
  20. package/dist/pagination/PaginationEngine.d.mts +1 -1
  21. package/dist/pagination-jSUdrACK.d.mts +158 -0
  22. package/dist/plugin-options-vFufQZMZ.d.mts +125 -0
  23. package/dist/plugins/index.d.mts +4 -4
  24. package/dist/plugins/index.mjs +3 -3
  25. package/dist/query/primitives/coercion.mjs +1 -1
  26. package/dist/query/primitives/geo.d.mts +1 -1
  27. package/dist/repository-DCHRWRXw.d.mts +511 -0
  28. package/dist/testkit/index.d.mts +1 -1
  29. package/dist/testkit/index.mjs +1 -1
  30. package/dist/{timestamp.plugin-BhSX0Lu_.mjs → timestamp.plugin-ennbatTW.mjs} +192 -46
  31. package/dist/{to-plain-g8-gsVxZ.d.mts → to-plain-DkF5px0R.d.mts} +12 -16
  32. package/dist/type-utils-CLzRFtIA.d.mts +163 -0
  33. package/dist/utils/index.d.mts +2 -2
  34. package/dist/utils/index.mjs +3 -3
  35. package/dist/{validation-chain.plugin-CFJbr964.d.mts → validation-chain.plugin-Cm0Ttnd8.d.mts} +140 -2
  36. package/dist/{validation-chain.plugin-Dtt4tO7R.mjs → validation-chain.plugin-k9joH2qw.mjs} +149 -2
  37. package/package.json +7 -5
  38. package/dist/types-BtMuhoX5.d.mts +0 -1830
package/README.md CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  [![npm version](https://badge.fury.io/js/@classytic%2Fmongokit.svg)](https://www.npmjs.com/package/@classytic/mongokit)
4
4
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
+ [![Sponsor](https://img.shields.io/github/sponsors/classytic?style=flat-square&label=Sponsor&logo=GitHub&color=EA4AAA)](https://github.com/sponsors/classytic)
5
6
 
6
7
  Production-ready repository layer for Mongoose apps: tenant-safe CRUD, soft delete, pagination, hooks, retries, transactions, change streams, and plugins.
7
8
 
@@ -212,12 +213,21 @@ const parser = new QueryParser({
212
213
  searchMode: 'auto',
213
214
  });
214
215
 
215
- const parsed = parser.parse(req.query);
216
+ const parsed = parser.parse(req.query); // throws HttpError(400) on invalid/blocked input
216
217
  const result = await invoiceRepo.getAll(parsed);
217
218
  ```
218
219
 
219
220
  Supports common URL operators such as `_gt`, `_gte`, `_lt`, `_lte`, `_in`, `_nin`, `_regex`, geo filters, populate, and schema-aware coercion.
220
221
 
222
+ The parser is **fail-closed by default** (`invalidInput: 'throw'`): blocked
223
+ operators, disallowed fields, malformed values, and pathological regex input
224
+ raise HTTP 400 (`code: 'INVALID_QUERY_INPUT'`) instead of being silently
225
+ dropped — dropping a request's *only* filter would broaden the query to every
226
+ record in tenant scope. Literal-semantics input (`search`, `like`/`contains`)
227
+ always escapes rather than rejects, so `?search=c++` never 400s. Trusted
228
+ migration/compat tooling can opt into the legacy warn-and-drop behavior with
229
+ `invalidInput: 'drop'`.
230
+
221
231
  ## Built-In Plugins
222
232
 
223
233
  | Plugin | Purpose |
@@ -228,7 +238,7 @@ Supports common URL operators such as `_gt`, `_gte`, `_lt`, `_lte`, `_in`, `_nin
228
238
  | `timestampPlugin` | `createdAt` / `updatedAt` management |
229
239
  | `batchOperationsPlugin` | `bulkWrite` and batch helpers |
230
240
  | `cachePlugin` | Pluggable read-through cache |
231
- | `auditLogPlugin` / `auditTrailPlugin` | Operational and compliance audit trails |
241
+ | `auditLogPlugin` / `auditTrailPlugin` | Audit trails — `auditTrailPlugin({ mode: 'transactional' })` gives session-joined entries that are atomic inside `withTransaction` (compliance-grade); the default `'best-effort'` mode is fire-and-forget observability. Call `ensureAuditTrailReady()` at boot so collection/index creation doesn't happen inside the first transaction |
232
242
  | `observabilityPlugin` | Metrics hook points |
233
243
  | `cascadePlugin` | Hook-routed cascade delete |
234
244
  | `customIdPlugin` | Prefixed or sequential public IDs |
@@ -1,4 +1,5 @@
1
- import { A as KeysetPaginationOptions, U as PaginationConfig, V as OffsetPaginationOptions, i as AnyDocument, n as AggregatePaginationOptions } from "./types-BtMuhoX5.mjs";
1
+ import { t as AnyDocument } from "./core-62oo3N1C.mjs";
2
+ import { c as PaginationConfig, o as KeysetPaginationOptions, s as OffsetPaginationOptions, t as AggregatePaginationOptions } from "./pagination-jSUdrACK.mjs";
2
3
  import { Model } from "mongoose";
3
4
  import { AggregatePaginationResult, KeysetPaginationResult, OffsetPaginationResult } from "@classytic/repo-core/pagination";
4
5
  //#region src/pagination/PaginationEngine.d.ts
@@ -1,4 +1,4 @@
1
- import { i as isValidIdForType, t as getSchemaIdType } from "./id-resolution-P2OzcyRl.mjs";
1
+ import { i as isValidIdForType, t as getSchemaIdType } from "./id-resolution-BqDrk0S2.mjs";
2
2
  import { a as count, b as LookupBuilder, c as getByQuery, d as deleteById, f as deleteByQuery, h as createMany, l as getOrCreate, m as create, n as update, o as exists, r as updateByQuery, s as getById, t as findOneAndUpdate, x as compileFilterToMongo, y as distinct } from "./update-1Lq9OMD2.mjs";
3
3
  import { n as warn } from "./logger-K7dPmu2U.mjs";
4
4
  import { n as isDuplicateKeyError, r as parseDuplicateKeyError, t as createError } from "./error-BuByZwKU.mjs";
@@ -16,20 +16,56 @@ import { decodeAggCursor, encodeAggCursor, isKeysetMode, normalizeGroupBy, valid
16
16
  * predicate the purge targets.
17
17
  */
18
18
  function createMongoPurgePort(repo, field, value, session) {
19
- const filter = { [field]: value };
19
+ return createKeysetPurgePort(repo, { [field]: value }, session);
20
+ }
21
+ /**
22
+ * Build a `PurgePort` bound to a repository + a pre-compiled Mongo filter
23
+ * (the repository compiles Filter IR via `compileFilterToMongo` before
24
+ * calling this) — the range/filter-scoped sibling of
25
+ * {@link createMongoPurgePort}.
26
+ *
27
+ * The per-strategy chunk logic is IDENTICAL to the equality-bound port; the
28
+ * only difference is the base predicate — an arbitrary compiled filter (a
29
+ * `civilDate` window, a retention cutoff, a compound cohort) instead of
30
+ * `{ [field]: value }`. The narrowed write is re-asserted on
31
+ * `{ _id: { $in: ids }, ...filter }`, the same defense the equality port
32
+ * uses, so a row that leaves the matching set between the id select and the
33
+ * write is never touched.
34
+ *
35
+ * Mirrors `createMongoArchivePort`, which likewise binds a pre-compiled
36
+ * filter rather than a `field/value` pair.
37
+ */
38
+ function createMongoPurgePortFromFilter(repo, filter, session) {
39
+ return createKeysetPurgePort(repo, filter, session);
40
+ }
41
+ /**
42
+ * Shared keyset-progressed port both public factories delegate to. The
43
+ * closure's `cursor` is the port's single-run progression state.
44
+ */
45
+ function createKeysetPurgePort(repo, baseFilter, session) {
20
46
  const baseOpts = {
21
47
  session,
22
48
  bypassTenant: true
23
49
  };
50
+ let cursor = null;
51
+ const selectFilter = () => cursor == null ? baseFilter : {
52
+ ...baseFilter,
53
+ _id: { $gt: cursor }
54
+ };
24
55
  return { async purgeChunk(strategy, limit) {
25
56
  if (strategy.type === "anonymize") {
26
- if (Object.values(strategy.fields).some((v) => typeof v === "function")) return purgeAnonymizeFunctional(repo, filter, strategy.fields, limit, session);
57
+ if (Object.values(strategy.fields).some((v) => typeof v === "function")) {
58
+ const { processed, lastId } = await purgeAnonymizeFunctional(repo, selectFilter(), strategy.fields, limit, session);
59
+ if (lastId != null) cursor = lastId;
60
+ return processed;
61
+ }
27
62
  }
28
- const idDocs = await repo.Model.find(filter, { _id: 1 }).limit(limit).session(session ?? null).lean().exec();
63
+ const idDocs = await repo.Model.find(selectFilter(), { _id: 1 }).sort({ _id: 1 }).limit(limit).session(session ?? null).lean().exec();
29
64
  if (idDocs.length === 0) return 0;
65
+ const ids = idDocs.map((d) => d._id);
30
66
  const chunkFilter = {
31
- _id: { $in: idDocs.map((d) => d._id) },
32
- [field]: value
67
+ ...baseFilter,
68
+ _id: { $in: ids }
33
69
  };
34
70
  switch (strategy.type) {
35
71
  case "hard":
@@ -37,17 +73,19 @@ function createMongoPurgePort(repo, field, value, session) {
37
73
  ...baseOpts,
38
74
  mode: "hard"
39
75
  });
40
- return idDocs.length;
76
+ break;
41
77
  case "soft":
42
78
  await repo.updateMany(chunkFilter, { $set: {
43
79
  [strategy.deletedField ?? "deleted"]: true,
44
80
  [strategy.deletedAtField ?? "deletedAt"]: /* @__PURE__ */ new Date()
45
81
  } }, baseOpts);
46
- return idDocs.length;
82
+ break;
47
83
  case "anonymize":
48
84
  await repo.updateMany(chunkFilter, { $set: strategy.fields }, baseOpts);
49
- return idDocs.length;
85
+ break;
50
86
  }
87
+ cursor = ids[ids.length - 1];
88
+ return idDocs.length;
51
89
  } };
52
90
  }
53
91
  /**
@@ -56,13 +94,20 @@ function createMongoPurgePort(repo, field, value, session) {
56
94
  * heterogeneous `updateOne` ops. Two round-trips per chunk regardless
57
95
  * of N — vs the N+1 of per-doc `update()` fan-out.
58
96
  *
97
+ * Selection is `_id`-ascending against the caller's keyset-narrowed
98
+ * filter; returns the chunk's last `_id` so the port can advance its
99
+ * cursor after the write commits.
100
+ *
59
101
  * Mongo's `bulkWrite` is ordered: false → independent failures don't
60
102
  * abort the batch. For idempotent anonymize (same row anonymized twice
61
103
  * is the same write), this matches the at-least-once contract.
62
104
  */
63
- async function purgeAnonymizeFunctional(repo, filter, fields, limit, session) {
64
- const docs = await repo.Model.find(filter).limit(limit).session(session ?? null).lean().exec();
65
- if (docs.length === 0) return 0;
105
+ async function purgeAnonymizeFunctional(repo, selectFilter, fields, limit, session) {
106
+ const docs = await repo.Model.find(selectFilter).sort({ _id: 1 }).limit(limit).session(session ?? null).lean().exec();
107
+ if (docs.length === 0) return {
108
+ processed: 0,
109
+ lastId: null
110
+ };
66
111
  const operations = docs.map((doc) => {
67
112
  const set = {};
68
113
  for (const [k, v] of Object.entries(fields)) set[k] = typeof v === "function" ? v(doc) : v;
@@ -75,7 +120,10 @@ async function purgeAnonymizeFunctional(repo, filter, fields, limit, session) {
75
120
  ordered: false,
76
121
  session: session ?? void 0
77
122
  });
78
- return docs.length;
123
+ return {
124
+ processed: docs.length,
125
+ lastId: docs[docs.length - 1]?._id ?? null
126
+ };
79
127
  }
80
128
  //#endregion
81
129
  //#region src/capabilities.ts
@@ -97,6 +145,7 @@ const MONGOKIT_CAPABILITIES = {
97
145
  getOrCreate: true,
98
146
  countAndExists: true,
99
147
  purgeByField: true,
148
+ purgeByFilter: true,
100
149
  archiveByFilter: true,
101
150
  arrayOperators: true,
102
151
  regexFilter: true,
@@ -543,7 +592,7 @@ function buildAggPipeline(req) {
543
592
  const match = compileFilterToMongo(baseFilter);
544
593
  if (Object.keys(match).length > 0) stages.push({ $match: match });
545
594
  }
546
- if (req.lookups && req.lookups.length > 0) appendLookupStages(stages, req.lookups);
595
+ if (req.lookups && req.lookups.length > 0) appendLookupStages$1(stages, req.lookups);
547
596
  if (joinedFilter !== void 0) {
548
597
  const match = compileFilterToMongo(joinedFilter);
549
598
  if (Object.keys(match).length > 0) stages.push({ $match: match });
@@ -698,7 +747,7 @@ function validateBucketAliases(bucketAliases, groupCols, measures) {
698
747
  function safeIdKey(field) {
699
748
  return field.includes(".") ? field.replace(/\./g, "__") : field;
700
749
  }
701
- function appendLookupStages(stages, lookups) {
750
+ function appendLookupStages$1(stages, lookups) {
702
751
  const asLookupOptions = lookups;
703
752
  stages.push(...LookupBuilder.multiple(asLookupOptions));
704
753
  for (const lookup of lookups) if (lookup.single) {
@@ -1470,6 +1519,59 @@ var AggregationBuilder = class AggregationBuilder {
1470
1519
  * ```
1471
1520
  */
1472
1521
  //#endregion
1522
+ //#region src/repository/lookup-populate.ts
1523
+ /**
1524
+ * Inclusion projections must carry the keyset sort fields (plus `_id`) or
1525
+ * cursor encoding has no values to serialize. No-op for exclusion
1526
+ * projections and cursor-less calls.
1527
+ */
1528
+ function ensureLookupProjectionIncludesCursorFields(projection, sort) {
1529
+ if (!projection || !sort) return projection;
1530
+ if (!Object.values(projection).some((value) => value === 1)) return projection;
1531
+ const nextProjection = { ...projection };
1532
+ for (const field of [...Object.keys(sort), "_id"]) nextProjection[field] = 1;
1533
+ return nextProjection;
1534
+ }
1535
+ /**
1536
+ * Build the `$project` map from a caller's select spec (string / array /
1537
+ * inclusion-exclusion record). For inclusion projections, every lookup's
1538
+ * `as` field is auto-included so `$project` doesn't strip the joined data.
1539
+ * Returns a fresh object (callers may add fields for cursor encoding).
1540
+ */
1541
+ function buildLookupProjection(selectSpec, lookups) {
1542
+ if (!selectSpec) return void 0;
1543
+ let projection;
1544
+ if (typeof selectSpec === "string") {
1545
+ projection = {};
1546
+ for (const field of selectSpec.split(",").map((f) => f.trim())) if (field.startsWith("-")) projection[field.substring(1)] = 0;
1547
+ else projection[field] = 1;
1548
+ } else if (Array.isArray(selectSpec)) {
1549
+ projection = {};
1550
+ for (const field of selectSpec) if (field.startsWith("-")) projection[field.substring(1)] = 0;
1551
+ else projection[field] = 1;
1552
+ } else projection = { ...selectSpec };
1553
+ if (Object.values(projection).some((v) => v === 1)) for (const lookup of lookups) {
1554
+ const asField = lookup.as || lookup.from;
1555
+ if (!(asField in projection)) projection[asField] = 1;
1556
+ }
1557
+ return projection;
1558
+ }
1559
+ /**
1560
+ * Append the shared pipeline tail: `$lookup` stages, `$ifNull` coalescing
1561
+ * for `single` lookups, and the final `$project` (with cursor fields kept
1562
+ * when a keyset sort is active). Mutates `pipeline` in place — it is the
1563
+ * tail-builder both the keyset and offset modes share.
1564
+ */
1565
+ function appendLookupStages(pipeline, lookups, projection, keysetSort) {
1566
+ pipeline.push(...LookupBuilder.multiple(lookups));
1567
+ for (const lookup of lookups) if (lookup.single) {
1568
+ const asField = lookup.as || lookup.from;
1569
+ pipeline.push({ $addFields: { [asField]: { $ifNull: [`$${asField}`, null] } } });
1570
+ }
1571
+ const finalProjection = ensureLookupProjectionIncludesCursorFields(projection, keysetSort);
1572
+ if (finalProjection) pipeline.push({ $project: finalProjection });
1573
+ }
1574
+ //#endregion
1473
1575
  //#region src/tx-bound.ts
1474
1576
  /**
1475
1577
  * Map of session-aware method name → positional index of the `{ session }`
@@ -1678,15 +1780,35 @@ function isTransactionUnsupported(error) {
1678
1780
  const message = (error.message || "").toLowerCase();
1679
1781
  return message.includes("transaction numbers are only allowed on a replica set member") || message.includes("transaction is not supported") || message.includes("does not support retryable writes");
1680
1782
  }
1783
+ /**
1784
+ * PROACTIVELY report whether a connection's deployment supports multi-document
1785
+ * transactions — the read-side companion to {@link isTransactionUnsupported}
1786
+ * (which classifies a FAILED attempt). Single source of truth for the topology
1787
+ * check so callers don't hand-roll `client.topology.description.type` probing.
1788
+ *
1789
+ * Returns `false` ONLY when the topology is positively a standalone (`'Single'`)
1790
+ * server; an unknown/undefined topology (not yet connected, non-standard
1791
+ * connection object) returns `true` — optimistic, because a genuine failure is
1792
+ * still caught reactively by `isTransactionUnsupported`. Lets a caller skip a
1793
+ * doomed transaction attempt on standalone dev Mongo rather than starting one
1794
+ * just to catch the error.
1795
+ *
1796
+ * Accepts a Mongoose `Connection` (or anything exposing `getClient()`/`client`
1797
+ * with a driver `topology`), so it stays decoupled from mongoose types.
1798
+ */
1799
+ function supportsTransactions(connection) {
1800
+ const probe = connection;
1801
+ if (!probe) return true;
1802
+ let client;
1803
+ try {
1804
+ client = probe.getClient?.() ?? probe.client ?? void 0;
1805
+ } catch {
1806
+ return true;
1807
+ }
1808
+ return client?.topology?.description?.type !== "Single";
1809
+ }
1681
1810
  //#endregion
1682
1811
  //#region src/Repository.ts
1683
- function ensureLookupProjectionIncludesCursorFields(projection, sort) {
1684
- if (!projection || !sort) return projection;
1685
- if (!Object.values(projection).some((value) => value === 1)) return projection;
1686
- const nextProjection = { ...projection };
1687
- for (const field of [...Object.keys(sort), "_id"]) nextProjection[field] = 1;
1688
- return nextProjection;
1689
- }
1690
1812
  /**
1691
1813
  * Validates that an update patch is homogeneous — either all top-level
1692
1814
  * keys are `$`-prefixed Mongo operators OR none are. Mixed shapes (e.g.
@@ -3395,6 +3517,24 @@ var Repository = class extends RepositoryBase {
3395
3517
  return runChunkedPurge(strategy, options, createMongoPurgePort(this, field, value, options.session));
3396
3518
  }
3397
3519
  /**
3520
+ * Range/filter-scoped purge — see `StandardRepo.purgeByFilter` for the
3521
+ * cross-kit contract. The compliance primitive for "purge/anonymize a
3522
+ * slice across a RANGE while retaining measures" (redact a PII dimension
3523
+ * across a `civilDate` window, hard-delete past a retention cutoff): the
3524
+ * base predicate is an arbitrary compiled filter, not `field = value`.
3525
+ *
3526
+ * Composes the kit-agnostic `runChunkedPurge` orchestrator (loop, signal,
3527
+ * progress, result envelope) with `createMongoPurgePortFromFilter` (the
3528
+ * filter-bound sibling of `createMongoPurgePort`). Accepts Filter IR or a
3529
+ * Mongo-shaped filter record — compiled once via `compileFilterToMongo`,
3530
+ * the same dual-dialect rule as `archiveByFilter` and every other verb.
3531
+ * See [actions/purge.ts](./actions/purge.ts) for the port.
3532
+ */
3533
+ async purgeByFilter(filter, strategy, options = {}) {
3534
+ const compiled = compileFilterToMongo(filter);
3535
+ return runChunkedPurge(strategy, options, createMongoPurgePortFromFilter(this, compiled, options.session));
3536
+ }
3537
+ /**
3398
3538
  * Chunked cold-storage extraction — see `StandardRepo.archiveByFilter`
3399
3539
  * for the cross-kit contract (write-before-delete, at-least-once,
3400
3540
  * duplicate-tolerant sinks). Mongokit composes the kit-agnostic
@@ -3423,6 +3563,15 @@ var Repository = class extends RepositoryBase {
3423
3563
  * Routes through the hook system for policy enforcement
3424
3564
  * (multi-tenant, soft-delete).
3425
3565
  *
3566
+ * The pipeline parameter also accepts `readonly unknown[]` (3.25.0):
3567
+ * real-world stage arrays are assembled from exported fragment builders
3568
+ * and spreads whose element type degrades to `unknown` — mongoose's
3569
+ * `PipelineStage` union can't be satisfied without an `as` at every
3570
+ * such call site. Runtime validation is MongoDB's job either way; the
3571
+ * single documented widening lives HERE instead of N host-side casts
3572
+ * (spine PACKAGE_RULES §7.1). Literal arrays still get full
3573
+ * `PipelineStage` contextual checking via the first union member.
3574
+ *
3426
3575
  * @param pipeline - MongoDB aggregation stage array
3427
3576
  * @param options - Aggregation options including governance controls
3428
3577
  */
@@ -3748,34 +3897,8 @@ var Repository = class extends RepositoryBase {
3748
3897
  const pageFromContext = context.page ?? options.page;
3749
3898
  const isKeyset = !!after || !pageFromContext && !!sort;
3750
3899
  const countStrategy = context.countStrategy ?? options.countStrategy ?? "exact";
3751
- const selectSpec = context.select ?? options.select;
3752
- let projection;
3753
- if (selectSpec) {
3754
- if (typeof selectSpec === "string") {
3755
- projection = {};
3756
- for (const field of selectSpec.split(",").map((f) => f.trim())) if (field.startsWith("-")) projection[field.substring(1)] = 0;
3757
- else projection[field] = 1;
3758
- } else if (Array.isArray(selectSpec)) {
3759
- projection = {};
3760
- for (const field of selectSpec) if (field.startsWith("-")) projection[field.substring(1)] = 0;
3761
- else projection[field] = 1;
3762
- } else projection = { ...selectSpec };
3763
- if (projection) {
3764
- if (Object.values(projection).some((v) => v === 1)) for (const lookup of lookups) {
3765
- const asField = lookup.as || lookup.from;
3766
- if (!(asField in projection)) projection[asField] = 1;
3767
- }
3768
- }
3769
- }
3770
- const appendLookupStages = (pipeline) => {
3771
- pipeline.push(...LookupBuilder.multiple(lookups));
3772
- for (const lookup of lookups) if (lookup.single) {
3773
- const asField = lookup.as || lookup.from;
3774
- pipeline.push({ $addFields: { [asField]: { $ifNull: [`$${asField}`, null] } } });
3775
- }
3776
- const finalProjection = ensureLookupProjectionIncludesCursorFields(projection, isKeyset && sort ? this._parseSort(sort) : void 0);
3777
- if (finalProjection) pipeline.push({ $project: finalProjection });
3778
- };
3900
+ const projection = buildLookupProjection(context.select ?? options.select, lookups);
3901
+ const appendLookupStages$2 = (pipeline) => appendLookupStages(pipeline, lookups, projection, isKeyset && sort ? this._parseSort(sort) : void 0);
3779
3902
  if (isKeyset && sort) {
3780
3903
  const parsedSort = this._parseSort(sort);
3781
3904
  const { validateKeysetSort } = await import("./PaginationEngine-CczbvCAq.mjs").then((n) => n.n);
@@ -3794,7 +3917,7 @@ var Repository = class extends RepositoryBase {
3794
3917
  if (Object.keys(matchFilters).length > 0) pipeline.push({ $match: matchFilters });
3795
3918
  pipeline.push({ $sort: normalizedSort });
3796
3919
  pipeline.push({ $limit: limit + 1 });
3797
- appendLookupStages(pipeline);
3920
+ appendLookupStages$2(pipeline);
3798
3921
  const aggregation = this.Model.aggregate(pipeline).session(session || null);
3799
3922
  if (collation) aggregation.collation(collation);
3800
3923
  if (readPref) aggregation.read(readPref);
@@ -3823,7 +3946,7 @@ var Repository = class extends RepositoryBase {
3823
3946
  if (sort) dataPipeline.push({ $sort: this._parseSort(sort) });
3824
3947
  if (countStrategy === "none") {
3825
3948
  dataPipeline.push({ $skip: skip }, { $limit: limit + 1 });
3826
- appendLookupStages(dataPipeline);
3949
+ appendLookupStages$2(dataPipeline);
3827
3950
  const aggregation = this.Model.aggregate(dataPipeline).session(session || null);
3828
3951
  if (collation) aggregation.collation(collation);
3829
3952
  if (readPref) aggregation.read(readPref);
@@ -3847,7 +3970,7 @@ var Repository = class extends RepositoryBase {
3847
3970
  return result;
3848
3971
  }
3849
3972
  dataPipeline.push({ $skip: skip }, { $limit: limit });
3850
- appendLookupStages(dataPipeline);
3973
+ appendLookupStages$2(dataPipeline);
3851
3974
  const countPipeline = [];
3852
3975
  if (filters && Object.keys(filters).length > 0) countPipeline.push({ $match: filters });
3853
3976
  countPipeline.push({ $count: "total" });
@@ -4180,4 +4303,4 @@ var Repository = class extends RepositoryBase {
4180
4303
  }
4181
4304
  };
4182
4305
  //#endregion
4183
- export { withTransaction as a, MUTATING_OPERATIONS as c, operationsByPolicyKey as d, MONGOKIT_CAPABILITIES as f, isTransactionUnsupported as i, OP_REGISTRY as l, Repository as n, AggregationBuilder as o, createMongoPurgePort as p, batchTransaction as r, ALL_OPERATIONS as s, HOOK_PRIORITY$1 as t, READ_OPERATIONS as u };
4306
+ export { supportsTransactions as a, ALL_OPERATIONS as c, READ_OPERATIONS as d, operationsByPolicyKey as f, isTransactionUnsupported as i, MUTATING_OPERATIONS as l, createMongoPurgePort as m, Repository as n, withTransaction as o, MONGOKIT_CAPABILITIES as p, batchTransaction as r, AggregationBuilder as s, HOOK_PRIORITY$1 as t, OP_REGISTRY as u };
@@ -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-DLQ6pjal.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-DceInCTX.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 };
@@ -59,7 +59,7 @@ interface MongooseAdapterOptions<TDoc = unknown> {
59
59
  * `@classytic/repo-core/adapter`.
60
60
  */
61
61
  declare class MongooseAdapter<TDoc = unknown> implements DataAdapter<TDoc> {
62
- readonly type: 'mongoose';
62
+ readonly type: "mongoose";
63
63
  readonly name: string;
64
64
  readonly model: Model<TDoc>;
65
65
  readonly repository: RepositoryLike<TDoc>;
@@ -1,2 +1,2 @@
1
- import { n as createMongooseAdapter, r as isMongooseModel, t as MongooseAdapter } from "../adapter-BWgJ_Jdc.mjs";
1
+ import { n as createMongooseAdapter, r as isMongooseModel, t as MongooseAdapter } from "../adapter-CLvUHiTW.mjs";
2
2
  export { MongooseAdapter, createMongooseAdapter, isMongooseModel };
@@ -1,5 +1,5 @@
1
1
  import { n as warn } from "./logger-K7dPmu2U.mjs";
2
- import { t as buildCrudSchemasFromModel } from "./mongooseToJsonSchema-CVnrKFDq.mjs";
2
+ import { t as buildCrudSchemasFromModel } from "./mongooseToJsonSchema-CByt1VhQ.mjs";
3
3
  import { matchesRecordFilter } from "@classytic/repo-core/filter";
4
4
  import { mergeFieldRuleConstraints } from "@classytic/repo-core/schema";
5
5
  import { asRepositoryLike, isRepository } from "@classytic/repo-core/adapter";
@@ -1,4 +1,4 @@
1
- import { G as Plugin } from "../types-BtMuhoX5.mjs";
1
+ import { l as Plugin } from "../repository-DCHRWRXw.mjs";
2
2
  import { PipelineStage } from "mongoose";
3
3
  //#region src/ai/types.d.ts
4
4
  /** Supported similarity metrics for vector search */
@@ -1,5 +1,5 @@
1
- import { n as Repository } from "../Repository-ClOraFjT.mjs";
2
- import { n as createMongooseAdapter } from "../adapter-BWgJ_Jdc.mjs";
1
+ import { n as Repository } from "../Repository-BFBTAMlA.mjs";
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
5
5
  /**
@@ -0,0 +1,50 @@
1
+ import { Document, Model, PopulateOptions, Types } from "mongoose";
2
+ //#region src/types/core.d.ts
3
+ /**
4
+ * Core query/document primitives shared across every mongokit module —
5
+ * sort/select/populate/filter specs and the mongoose-facing aliases.
6
+ *
7
+ * Also the home of the `softRequired` Mongoose declaration merge (this file
8
+ * is loaded by every module, so the augmentation is always in the program).
9
+ */
10
+ /**
11
+ * Global declaration merge: adds `softRequired` to Mongoose's SchemaTypeOptions
12
+ * so consumers can annotate paths type-safely without importing anything.
13
+ *
14
+ * A path with `softRequired: true` keeps its DB-level `required: true`
15
+ * invariant but is excluded from the auto-generated CRUD body `required[]`
16
+ * array (see buildCrudSchemasFromModel).
17
+ */
18
+ declare module 'mongoose' {
19
+ interface SchemaTypeOptions<T, EnforcedDocType = any, THydratedDocumentType = HydratedDocument<EnforcedDocType>> {
20
+ /** mongokit: omit from auto-generated CRUD body `required` array. */
21
+ softRequired?: boolean;
22
+ }
23
+ }
24
+ /** Read Preference Type for replica sets */
25
+ type ReadPreferenceType = 'primary' | 'primaryPreferred' | 'secondary' | 'secondaryPreferred' | 'nearest' | (string & {});
26
+ /** Re-export mongoose ObjectId */
27
+ type ObjectId = Types.ObjectId;
28
+ /** Generic document type */
29
+ type AnyDocument = Document & Record<string, unknown>;
30
+ /** Generic model type */
31
+ type AnyModel = Model<AnyDocument>;
32
+ /** Sort direction */
33
+ type SortDirection = 1 | -1;
34
+ /** Sort specification for MongoDB queries */
35
+ type SortSpec = Record<string, SortDirection>;
36
+ /** Populate specification */
37
+ type PopulateSpec = string | string[] | PopulateOptions | PopulateOptions[];
38
+ /**
39
+ * Select specification. `readonly string[]` widens the legacy `string[]`
40
+ * form to also accept the immutable variant repo-core's contract uses
41
+ * (`LookupPopulateOptions.select: readonly string[] | Record<string, 0
42
+ * | 1>`), so `Repository<TDoc>` assigns to `StandardRepo<TDoc>` without
43
+ * TS2322 on the `select` field. Any existing mutable-array caller stays
44
+ * compatible — `string[]` is assignable to `readonly string[]`.
45
+ */
46
+ type SelectSpec = string | readonly string[] | Record<string, 0 | 1>;
47
+ /** Filter query type for MongoDB queries (compatible with Mongoose 8 & 9) */
48
+ type FilterQuery<_T = unknown> = Record<string, unknown>;
49
+ //#endregion
50
+ export { PopulateSpec as a, SortDirection as c, ObjectId as i, SortSpec as l, AnyModel as n, ReadPreferenceType as o, FilterQuery as r, SelectSpec as s, AnyDocument as t };
@@ -1,5 +1,5 @@
1
- import { n as Repository } from "./Repository-ClOraFjT.mjs";
2
- import { a as methodRegistryPlugin, d as cachePlugin, f as batchOperationsPlugin, h as auditLogPlugin, i as multiTenantPlugin, m as auditTrailPlugin, n as softDeletePlugin, o as customIdPlugin, t as timestampPlugin } from "./timestamp.plugin-BhSX0Lu_.mjs";
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";
3
3
  //#region src/create-repository.ts
4
4
  function isLoggerAudit(value) {
5
5
  const v = value;
@@ -1,12 +1,16 @@
1
- import { At as LookupOptions, B as ObjectId, H as OperationOptions, J as PopulateSpec, M as LookupPopulateOptions, N as LookupPopulateResult, P as Middleware, Q as RepositoryContext, U as PaginationConfig, X as ReadOptions, Y as PrioritizedHook, Z as ReadPreferenceType, _ as DeleteManyResult, at as SessionOptions, c as CacheableOptions, d as CollationOptions, ft as UpdateManyResult, it as SelectSpec, j as Logger, kt as LookupBuilder, n as AggregatePaginationOptions, nt as RepositoryOptions, p as CreateOptions, pt as UpdateOptions, q as PluginType, st as SoftDeleteOptions, t as AggregateOptions, ut as SortSpec$1, v as DeleteResult, w as FindOneAndUpdateOptions, xt as WithTransactionOptions } from "./types-BtMuhoX5.mjs";
2
- import { _ as RepositoryCachePluginOptions, i as CustomIdOptions, t as MultiTenantOptions, w as AuditTrailOptions } from "./multi-tenant.plugin-BjXXuc_g.mjs";
3
- import { t as PaginationEngine } from "./PaginationEngine-BnZ24ehC.mjs";
1
+ import { _ as WithTransactionOptions, a as DeleteManyResult, f as OperationOptions, g as UpdateOptions, h as UpdateManyResult, i as CreateOptions, l as LookupPopulateOptions, m as SessionOptions, o as DeleteResult, p as ReadOptions, r as CacheableOptions, s as FindOneAndUpdateOptions, t as AggregateOptions, u as LookupPopulateResult, v as LookupBuilder, y as LookupOptions } from "./operations-C8IcCHF9.mjs";
2
+ import { a as PopulateSpec, i as ObjectId, l as SortSpec$1, o as ReadPreferenceType, s as SelectSpec } from "./core-62oo3N1C.mjs";
3
+ import { c as PaginationConfig, r as CollationOptions, t as AggregatePaginationOptions } from "./pagination-jSUdrACK.mjs";
4
+ import { _ as RepositoryOptions, d as PluginType, f as PrioritizedHook, o as Middleware, p as RepositoryContext } from "./repository-DCHRWRXw.mjs";
5
+ import { T as AuditTrailOptions, _ as RepositoryCachePluginOptions, i as CustomIdOptions, t as MultiTenantOptions } from "./multi-tenant.plugin-DVyVGREb.mjs";
6
+ import { t as PaginationEngine } from "./PaginationEngine-CBS1XGzp.mjs";
7
+ import { i as Logger, o as SoftDeleteOptions } from "./plugin-options-vFufQZMZ.mjs";
4
8
  import { AggPaginationRequest, AggRequest, AggResult, ArchiveOptions, ArchiveResult, ArchiveSink, ChangeEvent, FilterInput, KeysetAggPaginationResult, QueryOptions, RepoCapabilities, RepositoryBase, TenantPurgeOptions, TenantPurgeResult, TenantPurgeStrategy, WatchOptions } from "@classytic/repo-core/repository";
5
9
  import { ClientSession, Expression, Model, PipelineStage, PopulateOptions } from "mongoose";
6
10
  import { Filter } from "@classytic/repo-core/filter";
7
11
  import { UpdateInput } from "@classytic/repo-core/update";
8
- import { AggregatePaginationResult, KeysetPaginationResult, OffsetPaginationResult, OffsetPaginationResultCore } from "@classytic/repo-core/pagination";
9
12
  import { HttpError } from "@classytic/repo-core/errors";
13
+ import { AggregatePaginationResult, KeysetPaginationResult, OffsetPaginationResult, OffsetPaginationResultCore } from "@classytic/repo-core/pagination";
10
14
  //#region src/query/AggregationBuilder.d.ts
11
15
  /** Options for $vectorSearch stage (Atlas only) */
12
16
  interface VectorSearchOptions {
@@ -1458,6 +1462,21 @@ declare class Repository<TDoc = unknown> extends RepositoryBase {
1458
1462
  * the port implementation.
1459
1463
  */
1460
1464
  purgeByField(field: string, value: unknown, strategy: TenantPurgeStrategy, options?: TenantPurgeOptions): Promise<TenantPurgeResult>;
1465
+ /**
1466
+ * Range/filter-scoped purge — see `StandardRepo.purgeByFilter` for the
1467
+ * cross-kit contract. The compliance primitive for "purge/anonymize a
1468
+ * slice across a RANGE while retaining measures" (redact a PII dimension
1469
+ * across a `civilDate` window, hard-delete past a retention cutoff): the
1470
+ * base predicate is an arbitrary compiled filter, not `field = value`.
1471
+ *
1472
+ * Composes the kit-agnostic `runChunkedPurge` orchestrator (loop, signal,
1473
+ * progress, result envelope) with `createMongoPurgePortFromFilter` (the
1474
+ * filter-bound sibling of `createMongoPurgePort`). Accepts Filter IR or a
1475
+ * Mongo-shaped filter record — compiled once via `compileFilterToMongo`,
1476
+ * the same dual-dialect rule as `archiveByFilter` and every other verb.
1477
+ * See [actions/purge.ts](./actions/purge.ts) for the port.
1478
+ */
1479
+ purgeByFilter(filter: Record<string, unknown> | Filter, strategy: TenantPurgeStrategy, options?: TenantPurgeOptions): Promise<TenantPurgeResult>;
1461
1480
  /**
1462
1481
  * Chunked cold-storage extraction — see `StandardRepo.archiveByFilter`
1463
1482
  * for the cross-kit contract (write-before-delete, at-least-once,
@@ -1486,10 +1505,19 @@ declare class Repository<TDoc = unknown> extends RepositoryBase {
1486
1505
  * Routes through the hook system for policy enforcement
1487
1506
  * (multi-tenant, soft-delete).
1488
1507
  *
1508
+ * The pipeline parameter also accepts `readonly unknown[]` (3.25.0):
1509
+ * real-world stage arrays are assembled from exported fragment builders
1510
+ * and spreads whose element type degrades to `unknown` — mongoose's
1511
+ * `PipelineStage` union can't be satisfied without an `as` at every
1512
+ * such call site. Runtime validation is MongoDB's job either way; the
1513
+ * single documented widening lives HERE instead of N host-side casts
1514
+ * (spine PACKAGE_RULES §7.1). Literal arrays still get full
1515
+ * `PipelineStage` contextual checking via the first union member.
1516
+ *
1489
1517
  * @param pipeline - MongoDB aggregation stage array
1490
1518
  * @param options - Aggregation options including governance controls
1491
1519
  */
1492
- aggregatePipeline<TResult = unknown>(pipeline: PipelineStage[], options?: AggregateOptions): Promise<TResult[]>;
1520
+ aggregatePipeline<TResult = unknown>(pipeline: PipelineStage[] | readonly unknown[], options?: AggregateOptions): Promise<TResult[]>;
1493
1521
  /**
1494
1522
  * Paginated kit-native aggregation pipeline. Same mongo-specific
1495
1523
  * scope as `aggregatePipeline` — reach for this when you need
@@ -15,6 +15,17 @@ import mongoose from "mongoose";
15
15
  * Design: no `this`, no Model, no DB calls. Only reads `schema.paths._id.instance`.
16
16
  */
17
17
  /**
18
+ * Null-tolerant `id → ObjectId` coercion — the single seam for optional
19
+ * reference/audit-stamp fields (`approvedBy`, `submittedBy`, …) where an
20
+ * absent actor is a legitimate `null`, not an error. Invalid non-empty
21
+ * strings still throw (mongoose's own constructor validation) — this helper
22
+ * only absorbs the absent case, never bad input. Hosts were hand-rolling
23
+ * this identically at every approval-hook write; import it instead.
24
+ */
25
+ function toObjectId(id) {
26
+ return id ? new mongoose.Types.ObjectId(id) : null;
27
+ }
28
+ /**
18
29
  * Check if a Mongoose SchemaType `instance` string is an ObjectId variant.
19
30
  * Mongoose 9.x uses `'ObjectId'`, older versions use `'ObjectID'`. This
20
31
  * is the single source of truth — use it instead of inline
@@ -103,4 +114,4 @@ function idVariants(id) {
103
114
  return [id];
104
115
  }
105
116
  //#endregion
106
- export { isValidIdForType as i, idVariants as n, isObjectIdInstance as r, getSchemaIdType as t };
117
+ export { toObjectId as a, isValidIdForType as i, idVariants as n, isObjectIdInstance as r, getSchemaIdType as t };
@@ -1,4 +1,6 @@
1
- import { At as LookupOptions, B as ObjectId, H as OperationOptions, I as MinMaxResult, J as PopulateSpec, T as GroupResult, Z as ReadPreferenceType, ft as UpdateManyResult, ht as UpdateWithValidationResult, i as AnyDocument, it as SelectSpec, p as CreateOptions, pt as UpdateOptions, ut as SortSpec, v as DeleteResult, w as FindOneAndUpdateOptions } from "./types-BtMuhoX5.mjs";
1
+ import { c as GroupResult, d as MinMaxResult, f as OperationOptions, g as UpdateOptions, h as UpdateManyResult, i as CreateOptions, o as DeleteResult, s as FindOneAndUpdateOptions, y as LookupOptions } from "./operations-C8IcCHF9.mjs";
2
+ import { a as PopulateSpec, i as ObjectId, l as SortSpec, o as ReadPreferenceType, s as SelectSpec, t as AnyDocument } from "./core-62oo3N1C.mjs";
3
+ import { p as UpdateWithValidationResult } from "./type-utils-CLzRFtIA.mjs";
2
4
  import { Model, PipelineStage } from "mongoose";
3
5
  declare namespace aggregate_d_exports {
4
6
  export { aggregate, aggregatePaginate, average, countBy, distinct, facet, groupBy, lookup, minMax, sum, unwind };