@classytic/mongokit 3.19.0 → 3.20.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 (48) hide show
  1. package/dist/{PaginationEngine-BcvaXBQ-.mjs → PaginationEngine-BT1ywu9k.mjs} +327 -6
  2. package/dist/{PaginationEngine-4SjzfBQE.d.mts → PaginationEngine-DYpOD2wY.d.mts} +1 -2
  3. package/dist/{Repository-B01VW_fU.mjs → Repository-dbbgOGjK.mjs} +85 -11
  4. package/dist/actions/index.d.mts +1 -1
  5. package/dist/actions/index.mjs +2 -2
  6. package/dist/adapter/index.d.mts +10 -2
  7. package/dist/adapter/index.mjs +1 -1
  8. package/dist/{adapter-BSxhT0Ip.mjs → adapter-CW1EL0fF.mjs} +10 -1
  9. package/dist/ai/index.d.mts +1 -2
  10. package/dist/ai/index.mjs +3 -2
  11. package/dist/better-auth/index.d.mts +0 -1
  12. package/dist/better-auth/index.mjs +2 -2
  13. package/dist/{create-repository-K0yft2ht.d.mts → create-repository-0TkQhcv-.d.mts} +107 -13
  14. package/dist/{create-repository-D5YX0wGs.mjs → create-repository-CJATiAKX.mjs} +2 -2
  15. package/dist/{id-resolution-CnN0YA8t.mjs → id-resolution-P2OzcyRl.mjs} +10 -5
  16. package/dist/{index-BgSBwlWu.d.mts → index-Cg3SWugF.d.mts} +1 -3
  17. package/dist/index.d.mts +9 -10
  18. package/dist/index.mjs +51 -13
  19. package/dist/lock/index.d.mts +0 -1
  20. package/dist/{mongooseToJsonSchema-Bz47aMfH.d.mts → mongooseToJsonSchema-D2CyAMIC.d.mts} +9 -5
  21. package/dist/{mongooseToJsonSchema-ChtXSrLT.mjs → mongooseToJsonSchema-khdzVo8x.mjs} +1 -1
  22. package/dist/{multi-tenant.plugin-D6lw3BsI.d.mts → multi-tenant.plugin-CTsWz0XK.d.mts} +1 -2
  23. package/dist/pagination/PaginationEngine.d.mts +1 -1
  24. package/dist/pagination/PaginationEngine.mjs +1 -1
  25. package/dist/plugins/index.d.mts +3 -4
  26. package/dist/plugins/index.mjs +2 -2
  27. package/dist/query/primitives/coercion.d.mts +1 -1
  28. package/dist/query/primitives/coercion.mjs +1 -1
  29. package/dist/query/primitives/geo.d.mts +1 -1
  30. package/dist/query/primitives/indexes.d.mts +1 -1
  31. package/dist/testkit/index.d.mts +1 -2
  32. package/dist/testkit/index.mjs +1 -1
  33. package/dist/{timestamp.plugin-B1A9U-P7.mjs → timestamp.plugin-nG9Z1JEk.mjs} +4 -4
  34. package/dist/{types-BA8oXjFe.d.mts → types-DNFMofHy.d.mts} +51 -22
  35. package/dist/{update-CvCMv5Cl.mjs → update-1Lq9OMD2.mjs} +5 -5
  36. package/dist/utils/index.d.mts +1 -1
  37. package/dist/utils/index.mjs +5 -5
  38. package/dist/{validation-chain.plugin-DHZbpb4t.mjs → validation-chain.plugin-Dn_wZYLA.mjs} +6 -6
  39. package/dist/{validation-chain.plugin-CYOPfFGF.d.mts → validation-chain.plugin-buy3uvyk.d.mts} +1 -2
  40. package/package.json +8 -7
  41. package/dist/cursor-_SbWVAn-.mjs +0 -254
  42. package/dist/sort-Ca1BPfvE.mjs +0 -72
  43. /package/dist/{coercion-BxxHyLjP.d.mts → coercion-BjdPX3YF.d.mts} +0 -0
  44. /package/dist/{error-BTeepSNL.mjs → error-BuByZwKU.mjs} +0 -0
  45. /package/dist/{field-selection-d1qdRiMr.mjs → field-selection-FSQFLpsU.mjs} +0 -0
  46. /package/dist/{indexes-3eHBWNZ2.d.mts → indexes-DIjMpYES.d.mts} +0 -0
  47. /package/dist/{logger-B8wf-Mtb.mjs → logger-K7dPmu2U.mjs} +0 -0
  48. /package/dist/{chunk-CfYAbeIz.mjs → rolldown-runtime-D7D4PA-g.mjs} +0 -0
@@ -1,7 +1,258 @@
1
- import { n as warn } from "./logger-B8wf-Mtb.mjs";
2
- import { t as createError } from "./error-BTeepSNL.mjs";
3
- import { n as encodeCursor, r as resolveCursorFilter } from "./cursor-_SbWVAn-.mjs";
4
- import { r as validateKeysetSort, t as getPrimaryField } from "./sort-Ca1BPfvE.mjs";
1
+ import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.mjs";
2
+ import { n as warn } from "./logger-K7dPmu2U.mjs";
3
+ import { t as createError } from "./error-BuByZwKU.mjs";
4
+ import mongoose from "mongoose";
5
+ //#region src/pagination/utils/filter.ts
6
+ /**
7
+ * Builds MongoDB filter for keyset pagination
8
+ * Creates compound $or condition for proper cursor-based filtering
9
+ *
10
+ * @param baseFilters - Existing query filters
11
+ * @param sort - Normalized sort specification
12
+ * @param cursorValue - Primary field value from cursor
13
+ * @param cursorId - _id value from cursor
14
+ * @returns MongoDB filter with keyset condition
15
+ *
16
+ * @example
17
+ * buildKeysetFilter(
18
+ * { status: 'active' },
19
+ * { createdAt: -1, _id: -1 },
20
+ * new Date('2024-01-01'),
21
+ * new ObjectId('...')
22
+ * )
23
+ * // Returns:
24
+ * // {
25
+ * // status: 'active',
26
+ * // $or: [
27
+ * // { createdAt: { $lt: Date('2024-01-01') } },
28
+ * // { createdAt: Date('2024-01-01'), _id: { $lt: ObjectId('...') } }
29
+ * // ]
30
+ * // }
31
+ */
32
+ function buildKeysetFilter(baseFilters, sort, cursorValue, cursorId, cursorValues) {
33
+ const sortFields = Object.keys(sort).filter((k) => k !== "_id");
34
+ if (sortFields.length <= 1 && !cursorValues) {
35
+ const primaryField = sortFields[0] || "_id";
36
+ const direction = sort[primaryField];
37
+ const operator = direction === 1 ? "$gt" : "$lt";
38
+ if (cursorValue === null || cursorValue === void 0) if (direction === 1) return {
39
+ ...baseFilters,
40
+ $or: [{
41
+ [primaryField]: null,
42
+ _id: { $gt: cursorId }
43
+ }, { [primaryField]: { $ne: null } }]
44
+ };
45
+ else return {
46
+ ...baseFilters,
47
+ [primaryField]: null,
48
+ _id: { $lt: cursorId }
49
+ };
50
+ return {
51
+ ...baseFilters,
52
+ $or: [{ [primaryField]: { [operator]: cursorValue } }, {
53
+ [primaryField]: cursorValue,
54
+ _id: { [operator]: cursorId }
55
+ }]
56
+ };
57
+ }
58
+ const values = cursorValues || { [sortFields[0]]: cursorValue };
59
+ const allFields = [...sortFields, "_id"];
60
+ const allValues = {
61
+ ...values,
62
+ _id: cursorId
63
+ };
64
+ const orConditions = [];
65
+ for (let i = 0; i < allFields.length; i++) {
66
+ const field = allFields[i];
67
+ const operator = (sort[field] ?? sort[sortFields[0]]) === 1 ? "$gt" : "$lt";
68
+ const condition = {};
69
+ for (let j = 0; j < i; j++) condition[allFields[j]] = allValues[allFields[j]];
70
+ condition[field] = { [operator]: allValues[field] };
71
+ orConditions.push(condition);
72
+ }
73
+ return {
74
+ ...baseFilters,
75
+ $or: orConditions
76
+ };
77
+ }
78
+ //#endregion
79
+ //#region src/pagination/utils/cursor.ts
80
+ /**
81
+ * Cursor Utilities
82
+ *
83
+ * Encoding and decoding of cursor tokens for keyset pagination.
84
+ * Cursors are base64-encoded JSON containing position data and metadata.
85
+ */
86
+ var cursor_exports = /* @__PURE__ */ __exportAll({
87
+ decodeCursor: () => decodeCursor,
88
+ encodeCursor: () => encodeCursor,
89
+ resolveCursorFilter: () => resolveCursorFilter,
90
+ validateCursorSort: () => validateCursorSort,
91
+ validateCursorVersion: () => validateCursorVersion
92
+ });
93
+ /**
94
+ * Encodes document values and sort metadata into a base64 cursor token
95
+ *
96
+ * @param doc - Document to extract cursor values from
97
+ * @param primaryField - Primary sort field name
98
+ * @param sort - Normalized sort specification
99
+ * @param version - Cursor version for forward compatibility
100
+ * @returns Base64-encoded cursor token
101
+ */
102
+ function encodeCursor(doc, primaryField, sort, version = 1) {
103
+ const primaryValue = doc[primaryField];
104
+ const idValue = doc._id;
105
+ const sortFields = Object.keys(sort).filter((k) => k !== "_id");
106
+ const vals = {};
107
+ const types = {};
108
+ for (const field of sortFields) {
109
+ vals[field] = serializeValue(doc[field]);
110
+ types[field] = getValueType(doc[field]);
111
+ }
112
+ const payload = {
113
+ v: serializeValue(primaryValue),
114
+ t: getValueType(primaryValue),
115
+ id: serializeValue(idValue),
116
+ idType: getValueType(idValue),
117
+ sort,
118
+ ver: version,
119
+ ...sortFields.length > 1 && {
120
+ vals,
121
+ types
122
+ }
123
+ };
124
+ return Buffer.from(JSON.stringify(payload)).toString("base64");
125
+ }
126
+ /**
127
+ * Decodes a cursor token back into document values and sort metadata
128
+ *
129
+ * @param token - Base64-encoded cursor token
130
+ * @returns Decoded cursor data
131
+ * @throws Error if token is invalid or malformed
132
+ */
133
+ function decodeCursor(token) {
134
+ let json;
135
+ try {
136
+ json = Buffer.from(token, "base64").toString("utf-8");
137
+ } catch {
138
+ throw new Error("Invalid cursor token: not valid base64");
139
+ }
140
+ let payload;
141
+ try {
142
+ payload = JSON.parse(json);
143
+ } catch {
144
+ throw new Error("Invalid cursor token: not valid JSON");
145
+ }
146
+ if (!payload || typeof payload !== "object" || !("v" in payload) || !("t" in payload) || !("id" in payload) || !("idType" in payload) || !payload.sort || typeof payload.sort !== "object" || typeof payload.ver !== "number") throw new Error("Invalid cursor token: malformed payload structure");
147
+ const VALID_TYPES = [
148
+ "date",
149
+ "objectid",
150
+ "boolean",
151
+ "number",
152
+ "string",
153
+ "null",
154
+ "unknown"
155
+ ];
156
+ if (!VALID_TYPES.includes(payload.t) || !VALID_TYPES.includes(payload.idType)) throw new Error("Invalid cursor token: unrecognized value type");
157
+ try {
158
+ let values;
159
+ if (payload.vals && payload.types) {
160
+ values = {};
161
+ for (const [field, serialized] of Object.entries(payload.vals)) values[field] = rehydrateValue(serialized, payload.types[field]);
162
+ }
163
+ return {
164
+ value: rehydrateValue(payload.v, payload.t),
165
+ id: rehydrateValue(payload.id, payload.idType),
166
+ sort: payload.sort,
167
+ version: payload.ver,
168
+ ...values && { values }
169
+ };
170
+ } catch {
171
+ throw new Error("Invalid cursor token: failed to rehydrate values");
172
+ }
173
+ }
174
+ /**
175
+ * Validates that cursor sort matches current query sort
176
+ *
177
+ * @param cursorSort - Sort specification from cursor
178
+ * @param currentSort - Sort specification from query
179
+ * @throws Error if sorts don't match
180
+ */
181
+ function validateCursorSort(cursorSort, currentSort) {
182
+ if (JSON.stringify(cursorSort) !== JSON.stringify(currentSort)) throw new Error("Cursor sort does not match current query sort");
183
+ }
184
+ /**
185
+ * Validates cursor version against the server's expected range.
186
+ *
187
+ * - Cursors newer than `expectedVersion` are rejected (client ahead of server).
188
+ * - Cursors older than `minVersion` are rejected (client cached a cursor from
189
+ * a pre-breaking-change deploy). Default `minVersion = 1` keeps the legacy
190
+ * "accept anything <= expected" behavior; bump it when you ship a breaking
191
+ * cursor format change so old clients restart pagination cleanly instead of
192
+ * silently paginating from the wrong position.
193
+ */
194
+ function validateCursorVersion(cursorVersion, expectedVersion, minVersion = 1) {
195
+ if (cursorVersion > expectedVersion) throw new Error(`Cursor version ${cursorVersion} is newer than expected version ${expectedVersion}. Please upgrade.`);
196
+ if (cursorVersion < minVersion) throw new Error(`Cursor version ${cursorVersion} is older than minimum supported ${minVersion}. Pagination must restart.`);
197
+ }
198
+ /**
199
+ * Serializes a value for cursor storage
200
+ */
201
+ function serializeValue(value) {
202
+ if (value === null || value === void 0) return null;
203
+ if (value instanceof Date) return value.toISOString();
204
+ if (value instanceof mongoose.Types.ObjectId) return value.toString();
205
+ return value;
206
+ }
207
+ /**
208
+ * Gets the type identifier for a value
209
+ */
210
+ function getValueType(value) {
211
+ if (value === null || value === void 0) return "null";
212
+ if (value instanceof Date) return "date";
213
+ if (value instanceof mongoose.Types.ObjectId) return "objectid";
214
+ if (typeof value === "boolean") return "boolean";
215
+ if (typeof value === "number") return "number";
216
+ if (typeof value === "string") return "string";
217
+ return "unknown";
218
+ }
219
+ /**
220
+ * Rehydrates a serialized value back to its original type
221
+ */
222
+ function rehydrateValue(serialized, type) {
223
+ if (type === "null" || serialized === null) return null;
224
+ switch (type) {
225
+ case "date": return new Date(serialized);
226
+ case "objectid": return new mongoose.Types.ObjectId(serialized);
227
+ case "boolean": return serialized === true || serialized === "true";
228
+ case "number": return Number(serialized);
229
+ default: return serialized;
230
+ }
231
+ }
232
+ /**
233
+ * Resolves cursor token into MongoDB query filters.
234
+ * Shared by PaginationEngine.stream() and Repository.lookupPopulate() keyset path.
235
+ *
236
+ * Handles:
237
+ * - Plain 24-char hex ObjectId strings (fallback cursor)
238
+ * - Base64-encoded cursor tokens (standard cursor)
239
+ * - Cursor version and sort validation
240
+ */
241
+ function resolveCursorFilter(after, sort, cursorVersion, baseFilters = {}, minCursorVersion = 1) {
242
+ if (/^[a-f0-9]{24}$/i.test(after)) {
243
+ const objectId = new mongoose.Types.ObjectId(after);
244
+ const idOperator = (sort._id || -1) === 1 ? "$gt" : "$lt";
245
+ return {
246
+ ...baseFilters,
247
+ _id: { [idOperator]: objectId }
248
+ };
249
+ }
250
+ const cursor = decodeCursor(after);
251
+ validateCursorVersion(cursor.version, cursorVersion, minCursorVersion);
252
+ validateCursorSort(cursor.sort, sort);
253
+ return buildKeysetFilter(baseFilters, sort, cursor.value, cursor.id, cursor.values);
254
+ }
255
+ //#endregion
5
256
  //#region src/pagination/utils/index-hint.ts
6
257
  /**
7
258
  * Read a Mongoose schema's declared indexes, defensively.
@@ -131,6 +382,76 @@ function calculateTotalPages(total, limit) {
131
382
  return Math.ceil(total / limit);
132
383
  }
133
384
  //#endregion
385
+ //#region src/pagination/utils/sort.ts
386
+ var sort_exports = /* @__PURE__ */ __exportAll({
387
+ getPrimaryField: () => getPrimaryField,
388
+ normalizeSort: () => normalizeSort,
389
+ validateKeysetSort: () => validateKeysetSort
390
+ });
391
+ /**
392
+ * Normalizes sort object to ensure stable key order
393
+ * Primary fields first, _id last (not alphabetical)
394
+ *
395
+ * @param sort - Sort specification
396
+ * @returns Normalized sort with stable key order
397
+ */
398
+ function normalizeSort(sort) {
399
+ const normalized = {};
400
+ Object.keys(sort).forEach((key) => {
401
+ if (key !== "_id") normalized[key] = sort[key];
402
+ });
403
+ if (sort._id !== void 0) normalized._id = sort._id;
404
+ return normalized;
405
+ }
406
+ /**
407
+ * Validates and normalizes sort for keyset pagination.
408
+ * Auto-adds `_id` tie-breaker if needed; ensures `_id` direction matches the
409
+ * primary field.
410
+ *
411
+ * **Known limitation (MongoDB mixed-type sort):** keyset pagination across a
412
+ * field that can hold both typed values (Date, Number) and `null`/`undefined`
413
+ * is lossy — `$lt/$gt` don't cross the type boundary in BSON comparison, so
414
+ * some docs at the null/non-null boundary are unreachable. If your sort field
415
+ * is nullable, either:
416
+ * - sort by `_id` alone (always safe), or
417
+ * - use `allowedPrimaryFields` to lock keyset sort to fields your schema
418
+ * guarantees non-null (see `PaginationConfig.strictKeysetSortFields`).
419
+ *
420
+ * @param sort - Sort specification
421
+ * @param allowedPrimaryFields - Optional allowlist of primary (non-_id) sort
422
+ * field names. When set, rejects sorts whose primary field isn't listed.
423
+ * `_id` is always allowed.
424
+ * @returns Validated and normalized sort
425
+ * @throws Error if sort is invalid for keyset pagination
426
+ */
427
+ function validateKeysetSort(sort, allowedPrimaryFields) {
428
+ const keys = Object.keys(sort);
429
+ if (keys.length === 0) throw new Error("Keyset pagination requires at least one sort field");
430
+ if (keys.length === 1 && keys[0] === "_id") return normalizeSort(sort);
431
+ for (const key of keys) if (sort[key] !== 1 && sort[key] !== -1) throw new Error(`Invalid sort direction for "${key}": must be 1 or -1, got ${sort[key]}`);
432
+ const nonIdKeys = keys.filter((k) => k !== "_id");
433
+ const primaryDirection = sort[nonIdKeys[0]];
434
+ if (allowedPrimaryFields && allowedPrimaryFields.length > 0) {
435
+ for (const key of nonIdKeys) if (!allowedPrimaryFields.includes(key)) throw new Error(`Keyset sort field "${key}" is not in the strictKeysetSortFields allowlist. Allowed: ${allowedPrimaryFields.join(", ")}. (See PaginationConfig.strictKeysetSortFields — protects against lossy null/non-null keyset boundaries.)`);
436
+ }
437
+ for (const key of nonIdKeys) if (sort[key] !== primaryDirection) throw new Error("All sort fields must share the same direction for keyset pagination");
438
+ if (keys.includes("_id") && sort._id !== primaryDirection) throw new Error("_id direction must match primary field direction");
439
+ if (!keys.includes("_id")) return normalizeSort({
440
+ ...sort,
441
+ _id: primaryDirection
442
+ });
443
+ return normalizeSort(sort);
444
+ }
445
+ /**
446
+ * Extracts primary sort field (first non-_id field)
447
+ *
448
+ * @param sort - Sort specification
449
+ * @returns Primary field name
450
+ */
451
+ function getPrimaryField(sort) {
452
+ return Object.keys(sort).find((k) => k !== "_id") || "_id";
453
+ }
454
+ //#endregion
134
455
  //#region src/pagination/PaginationEngine.ts
135
456
  /**
136
457
  * Strip a trailing `_id` tiebreaker from a normalized sort spec.
@@ -152,7 +473,7 @@ function stripTrailingIdTiebreaker(sort) {
152
473
  }
153
474
  function ensureKeysetSelectIncludesCursorFields(select, sort) {
154
475
  if (!select) return select;
155
- const requiredFields = new Set([...Object.keys(sort), "_id"]);
476
+ const requiredFields = /* @__PURE__ */ new Set([...Object.keys(sort), "_id"]);
156
477
  if (typeof select === "string") {
157
478
  const fields = select.split(/[,\s]+/).map((field) => field.trim()).filter(Boolean);
158
479
  if (fields.length > 0 && fields.every((field) => field.startsWith("-"))) return select;
@@ -390,4 +711,4 @@ var PaginationEngine = class {
390
711
  }
391
712
  };
392
713
  //#endregion
393
- export { PaginationEngine as t };
714
+ export { sort_exports as n, cursor_exports as r, PaginationEngine as t };
@@ -1,7 +1,6 @@
1
- import { A as KeysetPaginationOptions, U as PaginationConfig, V as OffsetPaginationOptions, i as AnyDocument, n as AggregatePaginationOptions } from "./types-BA8oXjFe.mjs";
1
+ import { A as KeysetPaginationOptions, U as PaginationConfig, V as OffsetPaginationOptions, i as AnyDocument, n as AggregatePaginationOptions } from "./types-DNFMofHy.mjs";
2
2
  import { Model } from "mongoose";
3
3
  import { AggregatePaginationResult, KeysetPaginationResult, OffsetPaginationResult } from "@classytic/repo-core/pagination";
4
-
5
4
  //#region src/pagination/PaginationEngine.d.ts
6
5
  /**
7
6
  * Internal pagination config with required values
@@ -1,10 +1,10 @@
1
- import { i as isValidIdForType, t as getSchemaIdType } from "./id-resolution-CnN0YA8t.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-CvCMv5Cl.mjs";
3
- import { n as warn } from "./logger-B8wf-Mtb.mjs";
4
- import { n as isDuplicateKeyError, r as parseDuplicateKeyError, t as createError } from "./error-BTeepSNL.mjs";
5
- import { t as PaginationEngine } from "./PaginationEngine-BcvaXBQ-.mjs";
1
+ import { i as isValidIdForType, t as getSchemaIdType } from "./id-resolution-P2OzcyRl.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";
3
+ import { n as warn } from "./logger-K7dPmu2U.mjs";
4
+ import { n as isDuplicateKeyError, r as parseDuplicateKeyError, t as createError } from "./error-BuByZwKU.mjs";
5
+ import { t as PaginationEngine } from "./PaginationEngine-BT1ywu9k.mjs";
6
6
  import { hasNearOperator, rewriteNearForCount } from "./query/primitives/geo.mjs";
7
- import { RepositoryBase, runChunkedPurge, throwIfAborted, validatePluginOrder, withRetry } from "@classytic/repo-core/repository";
7
+ import { RepositoryBase, runChunkedArchive, runChunkedPurge, throwIfAborted, validatePluginOrder, withRetry } from "@classytic/repo-core/repository";
8
8
  import mongoose from "mongoose";
9
9
  import { isFilter } from "@classytic/repo-core/filter";
10
10
  import { HOOK_PRIORITY } from "@classytic/repo-core/hooks";
@@ -97,6 +97,7 @@ const MONGOKIT_CAPABILITIES = {
97
97
  getOrCreate: true,
98
98
  countAndExists: true,
99
99
  purgeByField: true,
100
+ archiveByFilter: true,
100
101
  arrayOperators: true,
101
102
  regexFilter: true,
102
103
  changeStreams: true,
@@ -617,7 +618,7 @@ function validateTopN(topN, groupCols, bucketAliases, measures) {
617
618
  if (!Number.isInteger(topN.limit) || topN.limit <= 0) throw new Error(`mongokit/aggregate: topN.limit must be a positive integer — got ${String(topN.limit)}`);
618
619
  if (!topN.sortBy || Object.keys(topN.sortBy).length === 0) throw new Error("mongokit/aggregate: topN.sortBy must declare at least one ranking field");
619
620
  const partitionList = Array.isArray(topN.partitionBy) ? topN.partitionBy : [topN.partitionBy];
620
- const validKeys = new Set([
621
+ const validKeys = /* @__PURE__ */ new Set([
621
622
  ...groupCols,
622
623
  ...bucketAliases,
623
624
  ...Object.keys(measures)
@@ -913,6 +914,30 @@ function buildKeysetPredicate(sort, cursor) {
913
914
  return { $match: branches.length === 1 ? branches[0] : { $or: branches } };
914
915
  }
915
916
  //#endregion
917
+ //#region src/actions/archive.ts
918
+ /**
919
+ * Build an `ArchivePort` bound to a repository + a pre-compiled Mongo
920
+ * filter (the repository compiles Filter IR before calling this).
921
+ */
922
+ function createMongoArchivePort(repo, filter, session) {
923
+ return {
924
+ async readChunk(limit) {
925
+ return await repo.Model.find(filter).sort({ _id: 1 }).limit(limit).session(session ?? null).lean().exec();
926
+ },
927
+ async deleteChunk(docs) {
928
+ const ids = docs.map((doc) => doc._id);
929
+ return (await repo.deleteMany({
930
+ ...filter,
931
+ _id: { $in: ids }
932
+ }, {
933
+ session,
934
+ bypassTenant: true,
935
+ mode: "hard"
936
+ })).deletedCount;
937
+ }
938
+ };
939
+ }
940
+ //#endregion
916
941
  //#region src/query/AggregationBuilder.ts
917
942
  /**
918
943
  * Normalize SortSpec to MongoDB's strict format (1 | -1)
@@ -1831,7 +1856,7 @@ var Repository = class extends RepositoryBase {
1831
1856
  _wireStrictQueryStripDiagnostic(enabled) {
1832
1857
  if (!enabled) return;
1833
1858
  const schema = this.Model.schema;
1834
- const declaredPaths = new Set(["_id"]);
1859
+ const declaredPaths = /* @__PURE__ */ new Set(["_id"]);
1835
1860
  for (const path of Object.keys(schema.paths)) {
1836
1861
  declaredPaths.add(path);
1837
1862
  const segments = path.split(".");
@@ -2090,6 +2115,40 @@ var Repository = class extends RepositoryBase {
2090
2115
  });
2091
2116
  }
2092
2117
  /**
2118
+ * Batch point-read — the `$in` counterpart to {@link getById}. Fetches every
2119
+ * doc whose id (the repo's `idField`, default `_id`) is in `ids` in ONE query,
2120
+ * routed through `findAll` so tenant + soft-delete scoping and hooks apply
2121
+ * exactly as a single read would. Returns a Map keyed by the stringified id;
2122
+ * ids are de-duplicated and ids with no matching doc are simply absent (no
2123
+ * throw — mirrors `getById(..., { throwOnNotFound: false })`). Structurally
2124
+ * INVALID ids (wrong ObjectId shape, non-numeric for a Number id field, ...)
2125
+ * are dropped the same way — an id that cannot exist in the collection is
2126
+ * unambiguously a miss, and one malformed id must never poison the rest of
2127
+ * the batch. Lean by default (findAll semantics) — pass `{ lean: false }`
2128
+ * for hydrated docs.
2129
+ *
2130
+ * The N+1 killer for hosts that resolve many ids in one tick (order-line
2131
+ * snapshotting, availability matrices, dashboard row enrichment):
2132
+ *
2133
+ * @example
2134
+ * const byId = await repo.getByIds(offerIds, ctx);
2135
+ * const doc = byId.get(offerId); // O(1); undefined if not found
2136
+ *
2137
+ * Sizing: there is no library-imposed cap — the bounds are MongoDB's (the
2138
+ * `$in` array must fit the 16 MB query document; `$in` on an indexed field
2139
+ * is n point-lookups). Keep batches ≤ ~10k ids per call and chunk beyond
2140
+ * that. Don't exclude the id field via `select` — result keys derive from
2141
+ * it.
2142
+ */
2143
+ async getByIds(ids, options = {}) {
2144
+ const field = options.idField ?? this.idField;
2145
+ const idType = getSchemaIdType(this.Model.schema, field);
2146
+ const unique = [...new Set(ids.map((id) => String(id)))].filter((id) => isValidIdForType(id, idType));
2147
+ if (unique.length === 0) return /* @__PURE__ */ new Map();
2148
+ const docs = await this.findAll({ [field]: { $in: unique } }, options);
2149
+ return new Map(docs.map((d) => [String(d[field]), d]));
2150
+ }
2151
+ /**
2093
2152
  * Get single document by query
2094
2153
  */
2095
2154
  async getByQuery(query, options = {}) {
@@ -3246,6 +3305,21 @@ var Repository = class extends RepositoryBase {
3246
3305
  return runChunkedPurge(strategy, options, createMongoPurgePort(this, field, value, options.session));
3247
3306
  }
3248
3307
  /**
3308
+ * Chunked cold-storage extraction — see `StandardRepo.archiveByFilter`
3309
+ * for the cross-kit contract (write-before-delete, at-least-once,
3310
+ * duplicate-tolerant sinks). Mongokit composes the kit-agnostic
3311
+ * `runChunkedArchive` orchestrator with `createMongoArchivePort`
3312
+ * (`_id`-ordered lean reads + plugin-routed deleteMany removal). See
3313
+ * [actions/archive.ts](./actions/archive.ts) for the port.
3314
+ *
3315
+ * Accepts Filter IR or a Mongo-shaped filter record — compiled once via
3316
+ * `compileFilterToMongo`, same dual-dialect rule as every other verb.
3317
+ */
3318
+ async archiveByFilter(filter, sink, options = {}) {
3319
+ const compiled = compileFilterToMongo(filter);
3320
+ return runChunkedArchive(options, sink, createMongoArchivePort(this, compiled, options.session));
3321
+ }
3322
+ /**
3249
3323
  * Kit-native aggregation pipeline — takes a MongoDB stage array and
3250
3324
  * returns the raw pipeline output. Use this for `$lookup`, `$unwind`,
3251
3325
  * `$facet`, `$graphLookup`, and any other mongo-specific power
@@ -3614,9 +3688,9 @@ var Repository = class extends RepositoryBase {
3614
3688
  };
3615
3689
  if (isKeyset && sort) {
3616
3690
  const parsedSort = this._parseSort(sort);
3617
- const { validateKeysetSort } = await import("./sort-Ca1BPfvE.mjs").then((n) => n.n);
3618
- const { encodeCursor, resolveCursorFilter } = await import("./cursor-_SbWVAn-.mjs").then((n) => n.t);
3619
- const { getPrimaryField } = await import("./sort-Ca1BPfvE.mjs").then((n) => n.n);
3691
+ const { validateKeysetSort } = await import("./PaginationEngine-BT1ywu9k.mjs").then((n) => n.n);
3692
+ const { encodeCursor, resolveCursorFilter } = await import("./PaginationEngine-BT1ywu9k.mjs").then((n) => n.r);
3693
+ const { getPrimaryField } = await import("./PaginationEngine-BT1ywu9k.mjs").then((n) => n.n);
3620
3694
  const normalizedSort = validateKeysetSort(parsedSort, this._pagination.config.strictKeysetSortFields);
3621
3695
  const cursorVersion = this._pagination.config.cursorVersion ?? 1;
3622
3696
  const minCursorVersion = this._pagination.config.minCursorVersion ?? 1;
@@ -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-BgSBwlWu.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-Cg3SWugF.mjs";
2
2
  export { aggregate_d_exports as aggregate, create_d_exports as create, delete_d_exports as deleteActions, read_d_exports as read, update_d_exports as update };
@@ -1,5 +1,5 @@
1
- import { t as __exportAll } from "../chunk-CfYAbeIz.mjs";
2
- import { g as create_exports, i as update_exports, p as delete_exports, u as read_exports, v as aggregate_exports } from "../update-CvCMv5Cl.mjs";
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";
3
3
  //#region src/actions/index.ts
4
4
  var actions_exports = /* @__PURE__ */ __exportAll({
5
5
  aggregate: () => aggregate_exports,
@@ -2,7 +2,6 @@ import { StandardRepo } from "@classytic/repo-core/repository";
2
2
  import { Document, Model } from "mongoose";
3
3
  import { SchemaBuilderOptions, SchemaGenerator } from "@classytic/repo-core/schema";
4
4
  import { AdapterRepositoryInput, AdapterSchemaContext, AdapterValidationResult, DataAdapter, OpenApiSchemas, RepositoryLike, SchemaMetadata } from "@classytic/repo-core/adapter";
5
-
6
5
  //#region src/adapter/mongoose-adapter.d.ts
7
6
  /**
8
7
  * Options for creating a Mongoose adapter.
@@ -53,7 +52,7 @@ interface MongooseAdapterOptions<TDoc = unknown> {
53
52
  * `@classytic/repo-core/adapter`.
54
53
  */
55
54
  declare class MongooseAdapter<TDoc = unknown> implements DataAdapter<TDoc> {
56
- readonly type: "mongoose";
55
+ readonly type: 'mongoose';
57
56
  readonly name: string;
58
57
  readonly model: Model<TDoc>;
59
58
  readonly repository: RepositoryLike<TDoc>;
@@ -95,6 +94,15 @@ declare class MongooseAdapter<TDoc = unknown> implements DataAdapter<TDoc> {
95
94
  * find a no-op success path.
96
95
  */
97
96
  validate(_data: unknown): AdapterValidationResult;
97
+ /**
98
+ * No-op — mongokit's per-call resources (`watch()` change streams,
99
+ * `cursor()` iterators) are released by their own `AbortSignal` /
100
+ * end-of-iteration, so the adapter holds nothing long-lived. Provided
101
+ * so hosts can call `adapter.close()` uniformly across every kit. Per
102
+ * the `DataAdapter.close` ownership rule the host owns the mongoose
103
+ * connection (`mongoose.connect(...)`) and closes it itself.
104
+ */
105
+ close(): Promise<void>;
98
106
  /**
99
107
  * Extract relation metadata from Mongoose ref paths.
100
108
  *
@@ -1,2 +1,2 @@
1
- import { n as createMongooseAdapter, r as isMongooseModel, t as MongooseAdapter } from "../adapter-BSxhT0Ip.mjs";
1
+ import { n as createMongooseAdapter, r as isMongooseModel, t as MongooseAdapter } from "../adapter-CW1EL0fF.mjs";
2
2
  export { MongooseAdapter, createMongooseAdapter, isMongooseModel };
@@ -1,4 +1,4 @@
1
- import { n as warn } from "./logger-B8wf-Mtb.mjs";
1
+ import { n as warn } from "./logger-K7dPmu2U.mjs";
2
2
  import { mergeFieldRuleConstraints } from "@classytic/repo-core/schema";
3
3
  import { asRepositoryLike, isRepository } from "@classytic/repo-core/adapter";
4
4
  //#region src/adapter/types.ts
@@ -160,6 +160,15 @@ var MongooseAdapter = class {
160
160
  return { valid: true };
161
161
  }
162
162
  /**
163
+ * No-op — mongokit's per-call resources (`watch()` change streams,
164
+ * `cursor()` iterators) are released by their own `AbortSignal` /
165
+ * end-of-iteration, so the adapter holds nothing long-lived. Provided
166
+ * so hosts can call `adapter.close()` uniformly across every kit. Per
167
+ * the `DataAdapter.close` ownership rule the host owns the mongoose
168
+ * connection (`mongoose.connect(...)`) and closes it itself.
169
+ */
170
+ async close() {}
171
+ /**
163
172
  * Extract relation metadata from Mongoose ref paths.
164
173
  *
165
174
  * Cardinality rules:
@@ -1,6 +1,5 @@
1
- import { G as Plugin } from "../types-BA8oXjFe.mjs";
1
+ import { G as Plugin } from "../types-DNFMofHy.mjs";
2
2
  import { PipelineStage } from "mongoose";
3
-
4
3
  //#region src/ai/types.d.ts
5
4
  /** Supported similarity metrics for vector search */
6
5
  type SimilarityMetric = 'cosine' | 'euclidean' | 'dotProduct';
package/dist/ai/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { n as warn } from "../logger-B8wf-Mtb.mjs";
1
+ import { n as warn } from "../logger-K7dPmu2U.mjs";
2
2
  //#region src/ai/url-policy.ts
3
3
  /**
4
4
  * URL policy helpers for the vector plugin media pipeline.
@@ -277,7 +277,8 @@ function vectorPlugin(options) {
277
277
  if (Array.isArray(params.query)) queryVector = params.query;
278
278
  else {
279
279
  if (!embedFn) throw new Error("[mongokit] Non-vector queries require embedFn in vectorPlugin options");
280
- queryVector = await embedFn(toEmbeddingInput(params.query));
280
+ const input = toEmbeddingInput(params.query);
281
+ queryVector = await embedFn(input);
281
282
  }
282
283
  if (queryVector.length !== field.dimensions) throw new Error(`[mongokit] Query vector has ${queryVector.length} dimensions, expected ${field.dimensions}`);
283
284
  const pipeline = buildVectorSearchPipeline(field, queryVector, params);
@@ -1,6 +1,5 @@
1
1
  import { DataAdapter, RepositoryLike } from "@classytic/repo-core/adapter";
2
2
  import { BetterAuthPluginKey, BetterAuthPluginKey as BetterAuthPluginKey$1 } from "@classytic/repo-core/better-auth";
3
-
4
3
  //#region src/better-auth/index.d.ts
5
4
  /**
6
5
  * Minimal structural type for the Mongoose surface this module touches.
@@ -1,5 +1,5 @@
1
- import { n as Repository } from "../Repository-B01VW_fU.mjs";
2
- import { n as createMongooseAdapter } from "../adapter-BSxhT0Ip.mjs";
1
+ import { n as Repository } from "../Repository-dbbgOGjK.mjs";
2
+ import { n as createMongooseAdapter } from "../adapter-CW1EL0fF.mjs";
3
3
  import { pluralizeBetterAuthCollection, resolveBetterAuthCollections } from "@classytic/repo-core/better-auth";
4
4
  //#region src/better-auth/index.ts
5
5
  /**