@classytic/mongokit 3.44.0 → 3.46.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/README.md +23 -3
  2. package/dist/{PaginationEngine-C9iS0U5l.mjs → PaginationEngine-BRlLTQQz.mjs} +1 -1
  3. package/dist/{PaginationEngine-D17AthKw.d.mts → PaginationEngine-BWb-c47a.d.mts} +1 -1
  4. package/dist/{Repository-B6oL6OMv.d.mts → Repository-DJuRmBNx.d.mts} +16 -7
  5. package/dist/{Repository-eMRYZ_2Y.mjs → Repository-DheA7oW-.mjs} +135 -29
  6. package/dist/actions/index.d.mts +1 -1
  7. package/dist/actions/index.mjs +2 -2
  8. package/dist/{actions-DExyXMTy.mjs → actions-DKQx9ToJ.mjs} +1 -1
  9. package/dist/ai/index.d.mts +1 -1
  10. package/dist/better-auth/index.d.mts +5 -2
  11. package/dist/better-auth/index.mjs +10 -20
  12. package/dist/{create-repository-D7MQpIZo.mjs → create-repository-C9DHHcl7.mjs} +3 -3
  13. package/dist/{create-repository-BOFUYj1l.d.mts → create-repository-DG-gwOf0.d.mts} +5 -5
  14. package/dist/{index-8QUEsOFI.d.mts → index-5mhQ63_B.d.mts} +2 -2
  15. package/dist/index.d.mts +19 -18
  16. package/dist/index.mjs +10 -10
  17. package/dist/kernel-conformance/index.d.mts +44 -3
  18. package/dist/kernel-conformance/index.mjs +43 -4
  19. package/dist/{method-registry.plugin-CGAepsBU.mjs → method-registry.plugin-BeJfQBJV.mjs} +2 -2
  20. package/dist/{multi-tenant.plugin-DfeDMiI8.d.mts → multi-tenant.plugin-BTPtCLNK.d.mts} +2 -2
  21. package/dist/{operations-Dw9OFV0V.d.mts → operations-D6XsHx4e.d.mts} +6 -7
  22. package/dist/outbox/index.d.mts +2 -2
  23. package/dist/outbox/index.mjs +2 -2
  24. package/dist/pagination/PaginationEngine.d.mts +1 -1
  25. package/dist/pagination/PaginationEngine.mjs +1 -1
  26. package/dist/pagination/utils/limits.d.mts +1 -1
  27. package/dist/{pagination-DAUg5zOY.d.mts → pagination-BAM9cA4N.d.mts} +5 -7
  28. package/dist/{plugin-options-Bpcuz9kS.d.mts → plugin-options-DrNGWV5b.d.mts} +2 -2
  29. package/dist/plugins/index.d.mts +4 -4
  30. package/dist/plugins/index.mjs +4 -4
  31. package/dist/{query-defaults-aUoKlEbh.mjs → query-defaults-DwvWUjZU.mjs} +16 -51
  32. package/dist/read-model/index.d.mts +1 -1
  33. package/dist/{repository-DzVIwhU2.d.mts → repository-B0aFVgP2.d.mts} +1 -1
  34. package/dist/{retention-purge-DqcYvRnG.d.mts → retention-purge-CCd_VXUf.d.mts} +4 -1
  35. package/dist/sync/index.d.mts +25 -9
  36. package/dist/sync/index.mjs +122 -12
  37. package/dist/testkit/index.d.mts +2 -2
  38. package/dist/testkit/index.mjs +1 -1
  39. package/dist/{timestamp.plugin-Bz_zL9PX.mjs → timestamp.plugin-B-IJrlYb.mjs} +1 -1
  40. package/dist/{to-plain-PO5r1Qoy.d.mts → to-plain-Do-8jTKC.d.mts} +2 -2
  41. package/dist/{transaction-C2PFxfIv.mjs → transaction-C6Oj941P.mjs} +14 -12
  42. package/dist/{update-DcfmNbJe.mjs → update-CYaJMu6j.mjs} +2 -4
  43. package/dist/utils/index.d.mts +1 -1
  44. package/dist/{validation-chain.plugin-B3gZCZj4.d.mts → validation-chain.plugin-COYrUdd8.d.mts} +19 -4
  45. package/dist/{validation-chain.plugin-DNY6MIF6.mjs → validation-chain.plugin-DJplVJ12.mjs} +10 -7
  46. package/package.json +11 -11
  47. package/skills/mongokit/references/plugins-tenancy.md +1 -0
  48. package/skills/mongokit/references/transactions-outbox.md +3 -1
@@ -1,4 +1,4 @@
1
- import { o as withTransaction } from "../transaction-C2PFxfIv.mjs";
1
+ import { o as withTransaction } from "../transaction-C6Oj941P.mjs";
2
2
  import { Schema, Types } from "mongoose";
3
3
  import { CursorExpiredError } from "@classytic/repo-core/sync";
4
4
  // src/sync/index.ts
@@ -394,6 +394,45 @@ function createChangeLogStore(models, storeOptions = {}) {
394
394
  scope: id.scope,
395
395
  docId: id.docId
396
396
  });
397
+ /**
398
+ * The entry a reader of one document ends on, which retention keeps. A tombstone applies
399
+ * unconditionally (`changeLogPlugin` writes version 0); an upsert only above the version held. So:
400
+ * the last tombstone, unless upserts follow it, then the highest of those (the first on a tie).
401
+ * Not simply the last cursor: branch stock records a lower version after a higher one.
402
+ */
403
+ const winnerOf = (rows) => {
404
+ let held = null;
405
+ for (const row of [...rows].sort((a, b) => a.seq - b.seq)) if (row.op === "delete") held = {
406
+ seq: row.seq,
407
+ version: Number.NEGATIVE_INFINITY
408
+ };
409
+ else if (!held || row.version > held.version) held = {
410
+ seq: row.seq,
411
+ version: row.version
412
+ };
413
+ return held?.seq ?? 0;
414
+ };
415
+ const winnerSeq = async (filter) => {
416
+ const tomb = await entries.findOne({
417
+ ...filter,
418
+ op: "delete"
419
+ }).sort({ seq: -1 }).select({ seq: 1 }).lean().exec();
420
+ const bound = filter.seq ?? {};
421
+ return (await entries.findOne(tomb?.seq === void 0 ? {
422
+ ...filter,
423
+ op: "upsert"
424
+ } : {
425
+ ...filter,
426
+ op: "upsert",
427
+ seq: {
428
+ ...bound,
429
+ $gt: tomb.seq
430
+ }
431
+ }).sort({
432
+ version: -1,
433
+ seq: 1
434
+ }).select({ seq: 1 }).lean().exec())?.seq ?? tomb?.seq ?? 0;
435
+ };
397
436
  const toEntry = (row) => ({
398
437
  scope: row.scope,
399
438
  docId: row.docId,
@@ -506,11 +545,11 @@ function createChangeLogStore(models, storeOptions = {}) {
506
545
  const groups = entries.aggregate([
507
546
  { $group: {
508
547
  _id: docKey,
509
- latest: { $max: "$seq" },
548
+ top: { $max: "$seq" },
510
549
  n: { $sum: 1 }
511
550
  } },
512
551
  { $match: { n: { $gt: 1 } } },
513
- { $project: { latest: 1 } }
552
+ { $project: { top: 1 } }
514
553
  ]).allowDiskUse(true).cursor({ batchSize });
515
554
  let removed = 0;
516
555
  let ops = [];
@@ -520,9 +559,17 @@ function createChangeLogStore(models, storeOptions = {}) {
520
559
  ops = [];
521
560
  };
522
561
  for await (const g of groups) {
562
+ const doc = docOf(g._id);
563
+ const keep = await winnerSeq({
564
+ ...doc,
565
+ seq: { $lte: g.top }
566
+ });
523
567
  ops.push({ deleteMany: { filter: {
524
- ...docOf(g._id),
525
- seq: { $lt: g.latest }
568
+ ...doc,
569
+ seq: {
570
+ $lte: g.top,
571
+ $ne: keep
572
+ }
526
573
  } } });
527
574
  if (ops.length >= batchSize) await flush();
528
575
  }
@@ -533,6 +580,66 @@ function createChangeLogStore(models, storeOptions = {}) {
533
580
  const seq = await horizonFor(options);
534
581
  return seq > 0 ? toCursor(seq) : "";
535
582
  },
583
+ async snapshot(cursor, options = {}) {
584
+ const { upTo: bound, ...view } = options;
585
+ const limit = view.limit ?? DEFAULT_LIMIT;
586
+ const upTo = bound === void 0 ? await horizonFor(view) : fromCursor(bound);
587
+ const after = fromCursor(cursor);
588
+ const upToCursor = upTo > 0 ? toCursor(upTo) : "";
589
+ if (after >= upTo) return {
590
+ changes: [],
591
+ cursor,
592
+ hasMore: false,
593
+ upTo: upToCursor
594
+ };
595
+ const rows = await entries.find({
596
+ seq: {
597
+ $gt: after,
598
+ $lte: upTo
599
+ },
600
+ ...viewOf(view)
601
+ }).sort({ seq: 1 }).limit(limit + 1).lean().exec();
602
+ const page = rows.slice(0, limit);
603
+ const latest = /* @__PURE__ */ new Map();
604
+ if (page.length > 0) {
605
+ const keyOf = (r) => JSON.stringify([
606
+ typeof r.tenantId === "string" ? r.tenantId : null,
607
+ r.scope,
608
+ r.docId
609
+ ]);
610
+ const docs = new Map(page.map((r) => [keyOf(r), r]));
611
+ const groups = await entries.aggregate([{ $match: {
612
+ seq: { $lte: upTo },
613
+ $or: [...docs.values()].map((r) => docOf({
614
+ tenantId: typeof r.tenantId === "string" ? r.tenantId : null,
615
+ scope: r.scope,
616
+ docId: r.docId
617
+ }))
618
+ } }, { $group: {
619
+ _id: docKey,
620
+ rows: { $push: {
621
+ seq: "$seq",
622
+ version: "$version",
623
+ op: "$op"
624
+ } }
625
+ } }]).allowDiskUse(true).exec();
626
+ for (const g of groups) latest.set(keyOf(g._id), winnerOf(g.rows));
627
+ const changes = page.filter((r) => latest.get(keyOf(r)) === r.seq && r.op === "upsert").map(toEntry);
628
+ const last = page[page.length - 1];
629
+ return {
630
+ changes,
631
+ cursor: toCursor(last.seq),
632
+ hasMore: rows.length > limit,
633
+ upTo: upToCursor
634
+ };
635
+ }
636
+ return {
637
+ changes: [],
638
+ cursor,
639
+ hasMore: false,
640
+ upTo: upToCursor
641
+ };
642
+ },
536
643
  async prune(before, options = {}) {
537
644
  if (!(before instanceof Date) || Number.isNaN(before.getTime())) throw new Error("[mongokit:sync] prune(before) needs a valid Date");
538
645
  const batchSize = options.batchSize ?? 1e3;
@@ -569,20 +676,23 @@ function createChangeLogStore(models, storeOptions = {}) {
569
676
  };
570
677
  for await (const g of groups) {
571
678
  const doc = docOf(g._id);
572
- const latest = await topSeq(doc);
573
- let deletedMax;
574
- if (latest > g.oldMax) deletedMax = g.oldMax;
575
- else if (g.n > 1) deletedMax = await topSeq({
679
+ const keep = await winnerSeq(doc);
680
+ const deletedMax = await topSeq({
576
681
  ...doc,
577
682
  at: old,
578
- seq: { $lt: latest }
683
+ seq: {
684
+ $lte: g.oldMax,
685
+ $ne: keep
686
+ }
579
687
  });
580
- else continue;
581
688
  if (deletedMax <= 0) continue;
582
689
  ops.push({ deleteMany: { filter: {
583
690
  ...doc,
584
691
  at: old,
585
- seq: { $lte: deletedMax }
692
+ seq: {
693
+ $lte: deletedMax,
694
+ $ne: keep
695
+ }
586
696
  } } });
587
697
  batchMax = Math.max(batchMax, deletedMax);
588
698
  const key = JSON.stringify([doc.tenantId, doc.scope]);
@@ -1,5 +1,5 @@
1
- import { n as CreateRepositoryConfig } from "../create-repository-BOFUYj1l.mjs";
2
- import { r as Repository } from "../Repository-B6oL6OMv.mjs";
1
+ import { n as CreateRepositoryConfig } from "../create-repository-DG-gwOf0.mjs";
2
+ import { r as Repository } from "../Repository-DJuRmBNx.mjs";
3
3
  import { Connection, Model, Schema } from "mongoose";
4
4
  import { MongoMemoryReplSet, MongoMemoryServer } from "mongodb-memory-server";
5
5
  /**
@@ -1,4 +1,4 @@
1
- import { t as createRepository } from "../create-repository-D7MQpIZo.mjs";
1
+ import { t as createRepository } from "../create-repository-C9DHHcl7.mjs";
2
2
  import { createConnection } from "mongoose";
3
3
  import { rmSync } from "node:fs";
4
4
  import { mkdir, mkdtemp, readFile, readdir, rm, stat } from "node:fs/promises";
@@ -1,4 +1,4 @@
1
- import { a as ALL_OPERATIONS, s as OP_REGISTRY, t as HOOK_PRIORITY } from "./Repository-eMRYZ_2Y.mjs";
1
+ import { a as ALL_OPERATIONS, s as OP_REGISTRY, t as HOOK_PRIORITY } from "./Repository-DheA7oW-.mjs";
2
2
  import { n as warn } from "./logger-hbWOlj_X.mjs";
3
3
  import { n as declareCollectionScope } from "./join-scope-C1gOtZ59.mjs";
4
4
  import { throwIfAborted, withRetry } from "@classytic/repo-core/repository";
@@ -1,5 +1,5 @@
1
- import { y as UserContext } from "./repository-DzVIwhU2.mjs";
2
- import { r as FieldPreset } from "./plugin-options-Bpcuz9kS.mjs";
1
+ import { y as UserContext } from "./repository-B0aFVgP2.mjs";
2
+ import { r as FieldPreset } from "./plugin-options-DrNGWV5b.mjs";
3
3
  import mongoose, { Schema } from "mongoose";
4
4
  import { CacheAdapter } from "@classytic/repo-core/cache";
5
5
  import { HttpError } from "@classytic/repo-core/errors";
@@ -1,4 +1,4 @@
1
- import { d as setTransactionDeadline } from "./query-defaults-aUoKlEbh.mjs";
1
+ import { u as setTransactionDeadline } from "./query-defaults-DwvWUjZU.mjs";
2
2
  import { t as createError } from "./error-DjdmPLWO.mjs";
3
3
  import mongoose from "mongoose";
4
4
  import { setTimeout as setTimeout$1 } from "node:timers/promises";
@@ -494,18 +494,17 @@ function timeoutError(cause, csot) {
494
494
  }
495
495
  /**
496
496
  * The driver's convenient-transaction algorithm (start → callback → commit, label-based retry,
497
- * jittered backoff, a deadline), run here so the driver never attaches a session `timeoutContext`:
498
- * with one, every legacy bulk write (`insertMany`, `bulkWrite`) in the transaction is refused once
499
- * the client carries `timeoutMS` (driver <= 7.7: the bulk path re-resolves the inherited client
500
- * `timeoutMS` as a per-op one). Each operation keeps its own per-op CSOT budget instead.
501
- * Deadline: `transactionOptions.timeoutMS`, else the client's `timeoutMS`, else 120 s (as the driver).
502
- * Pinned by `tests/integration/csot-transaction-matrix.test.ts`.
497
+ * jittered backoff, a deadline), run here and nowhere else: the driver's own `timeoutMS` / CSOT is a
498
+ * no-op since 7.8, so the budget is mongokit's. `transactionOptions.timeoutMS` is that budget, enforced
499
+ * by mongokit alone: every mongokit operation in the transaction is sent with `maxTimeMS` = what
500
+ * remains of it, the commit is bounded by `maxCommitTimeMS`, and the retry loop stops at the deadline
501
+ * (a spent budget before the commit aborts, so the outcome is known). Without it the deadline is 120 s
502
+ * (as the driver) and operations carry only their own bound. The client's `timeoutMS` is not read.
503
+ * Pinned by `tests/integration/transaction-budget-matrix.test.ts`.
503
504
  */
504
505
  async function runTransaction(session, callback, transactionOptions = {}) {
505
506
  assertSessionSurface(session, DRIVEN);
506
- const { timeoutMS: explicitTimeout, ...startOptions } = transactionOptions;
507
- const clientTimeout = session.timeoutMS;
508
- const timeoutMS = explicitTimeout ?? clientTimeout;
507
+ const { timeoutMS, ...startOptions } = transactionOptions;
509
508
  const csot = timeoutMS != null;
510
509
  const deadline = performance.now() + (timeoutMS ?? MAX_TRANSACTION_TIMEOUT_MS);
511
510
  let lastError;
@@ -517,7 +516,10 @@ async function runTransaction(session, callback, transactionOptions = {}) {
517
516
  if (performance.now() + backoff >= deadline) throw timeoutError(lastError, csot);
518
517
  await setTimeout$1(backoff);
519
518
  }
520
- session.startTransaction(startOptions);
519
+ session.startTransaction(csot && startOptions.maxCommitTimeMS === void 0 ? {
520
+ ...startOptions,
521
+ maxCommitTimeMS: Math.max(1, Math.floor(deadline - performance.now()))
522
+ } : startOptions);
521
523
  let result;
522
524
  try {
523
525
  result = await callback(session);
@@ -539,7 +541,7 @@ async function runTransaction(session, callback, transactionOptions = {}) {
539
541
  throw late;
540
542
  }
541
543
  try {
542
- await session.commitTransaction(csot ? { timeoutMS: Math.max(1, Math.floor(left)) } : void 0);
544
+ await session.commitTransaction();
543
545
  return result;
544
546
  } catch (err) {
545
547
  lastError = err;
@@ -1,7 +1,7 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-D8jQpDG7.mjs";
2
2
  import { t as createError } from "./error-DjdmPLWO.mjs";
3
3
  import { n as warn } from "./logger-hbWOlj_X.mjs";
4
- import { i as applyToQuery } from "./query-defaults-aUoKlEbh.mjs";
4
+ import { r as applyToQuery } from "./query-defaults-DwvWUjZU.mjs";
5
5
  import { coerceFilterDates, isFilter } from "@classytic/repo-core/filter";
6
6
  // src/actions/create.ts
7
7
  var create_exports = /* @__PURE__ */ __exportAll({
@@ -17,8 +17,6 @@ var create_exports = /* @__PURE__ */ __exportAll({
17
17
  async function create(Model, data, options = {}) {
18
18
  const document = new Model(data);
19
19
  const saveOptions = { session: options.session };
20
- const bound = options.queryOptions?.maxTimeMS;
21
- if (bound !== void 0) Object.assign(saveOptions, { timeoutMS: bound });
22
20
  const wc = options.queryOptions?.writeConcern;
23
21
  if (wc) Object.assign(saveOptions, { writeConcern: {
24
22
  ...wc.w !== void 0 ? { w: wc.w } : {},
@@ -71,7 +69,7 @@ async function createMany(Model, dataArray, options = {}) {
71
69
  const wc = options.queryOptions?.writeConcern;
72
70
  if (wc) Object.assign(insertOptions, { writeConcern: wc });
73
71
  const bound = options.queryOptions?.maxTimeMS;
74
- if (bound !== void 0) Object.assign(insertOptions, { timeoutMS: bound });
72
+ if (bound !== void 0) Object.assign(insertOptions, { maxTimeMS: bound });
75
73
  return await Model.insertMany(dataArray, insertOptions);
76
74
  } catch (err) {
77
75
  const inserted = err.insertedDocs;
@@ -1,4 +1,4 @@
1
- import { _ as getMongooseProjection, a as createMemoryCache, b as createError, c as IdType, d as isObjectIdInstance, f as isValidIdForType, g as getFieldsForUser, h as filterResponseData, i as buildCrudSchemasFromMongooseSchema, l as getSchemaIdType, m as createFieldPreset, n as CrudSchemasFramework, o as LoggerConfig, p as toObjectId, r as buildCrudSchemasFromModel, s as configureLogger, t as toPlain, v as CreateErrorOptions, y as ParseDuplicateKeyOptions } from "../to-plain-PO5r1Qoy.mjs";
1
+ import { _ as getMongooseProjection, a as createMemoryCache, b as createError, c as IdType, d as isObjectIdInstance, f as isValidIdForType, g as getFieldsForUser, h as filterResponseData, i as buildCrudSchemasFromMongooseSchema, l as getSchemaIdType, m as createFieldPreset, n as CrudSchemasFramework, o as LoggerConfig, p as toObjectId, r as buildCrudSchemasFromModel, s as configureLogger, t as toPlain, v as CreateErrorOptions, y as ParseDuplicateKeyOptions } from "../to-plain-Do-8jTKC.mjs";
2
2
  import { ClientSession, Model } from "mongoose";
3
3
  /**
4
4
  * Default slice for `$in` fan-out. Larger than `runChunkedPurge`'s 1000 on purpose:
@@ -1,7 +1,7 @@
1
- import { A as ObjectId, F as SortSpec, N as SelectSpec, j as PopulateSpec } from "./pagination-DAUg5zOY.mjs";
2
- import { h as RepositoryInstance, l as Plugin, p as RepositoryContext } from "./repository-DzVIwhU2.mjs";
3
- import { n as RetentionPurgeEvent, r as RetentionPurgeRequest } from "./retention-purge-DqcYvRnG.mjs";
4
- import { c as ValidationChainOptions, i as Logger, l as ValidatorDefinition, o as SoftDeleteOptions, r as FieldPreset, t as CascadeOptions } from "./plugin-options-Bpcuz9kS.mjs";
1
+ import { A as ObjectId, F as SortSpec, N as SelectSpec, j as PopulateSpec } from "./pagination-BAM9cA4N.mjs";
2
+ import { h as RepositoryInstance, l as Plugin, p as RepositoryContext } from "./repository-B0aFVgP2.mjs";
3
+ import { n as RetentionPurgeEvent, r as RetentionPurgeRequest } from "./retention-purge-CCd_VXUf.mjs";
4
+ import { c as ValidationChainOptions, i as Logger, l as ValidatorDefinition, o as SoftDeleteOptions, r as FieldPreset, t as CascadeOptions } from "./plugin-options-DrNGWV5b.mjs";
5
5
  import { BulkWriteResult } from "@classytic/repo-core/repository";
6
6
  import { OffsetPaginationResult } from "@classytic/repo-core/pagination";
7
7
  import { AsyncLocalStorage } from "node:async_hooks";
@@ -112,6 +112,13 @@ interface AppendOnlyPluginOptions {
112
112
  * refused (403 `RETENTION_PURGE_DISABLED`).
113
113
  */
114
114
  allowPurge?: boolean | undefined;
115
+ /**
116
+ * Allow `archiveByFilter(filter, sink, { retentionPurge })` to delete the rows its
117
+ * sink acknowledged, and nothing else: a retentionPurge delete without the run's
118
+ * attestation is still refused. Audited via `after:append-only-purge` with
119
+ * `archived: true`. Implied by `allowPurge`. Default `false`.
120
+ */
121
+ allowArchive?: boolean | undefined;
115
122
  }
116
123
  /** Payload of `after:append-only-purge`: one per successful purge call. */
117
124
  type AppendOnlyPurgeEvent = RetentionPurgeEvent;
@@ -127,6 +134,8 @@ interface BulkWriteOptions {
127
134
  comment?: unknown;
128
135
  let?: Record<string, unknown>;
129
136
  ignoreUndefined?: boolean;
137
+ /** Server-side time bound for the write. */
138
+ maxTimeMS?: number;
130
139
  /** Plugin context (tenant id, actor, ...) - spread into the op context. */
131
140
  [key: string]: unknown;
132
141
  }
@@ -358,6 +367,12 @@ interface ImmutableStatesPluginOptions {
358
367
  * option is refused (403 `RETENTION_PURGE_DISABLED`).
359
368
  */
360
369
  allowPurge?: boolean | undefined;
370
+ /**
371
+ * Allow `archiveByFilter(filter, sink, { retentionPurge })` to delete frozen rows its
372
+ * sink acknowledged, and nothing else (see `appendOnlyPlugin`'s `allowArchive`).
373
+ * Implied by `allowPurge`. Default `false`.
374
+ */
375
+ allowArchive?: boolean | undefined;
361
376
  /** Domain error to throw. Default: 403 `IMMUTABLE_STATE_VIOLATION`. */
362
377
  errorFactory?: ((ctx: {
363
378
  id: unknown;
@@ -1,9 +1,9 @@
1
1
  import { t as createError } from "./error-DjdmPLWO.mjs";
2
- import { a as ALL_OPERATIONS, f as readRetentionPurge, p as retentionPurgeEvent, s as OP_REGISTRY } from "./Repository-eMRYZ_2Y.mjs";
2
+ import { a as ALL_OPERATIONS, f as readRetentionPurge, p as retentionPurgeEvent, s as OP_REGISTRY } from "./Repository-DheA7oW-.mjs";
3
3
  import { n as collectIds, r as idChunks, t as DEFAULT_ID_CHUNK } from "./id-chunks-DH-RAZWG.mjs";
4
- import { w as upsert } from "./update-DcfmNbJe.mjs";
4
+ import { w as upsert } from "./update-CYaJMu6j.mjs";
5
5
  import { n as warn } from "./logger-hbWOlj_X.mjs";
6
- import { c as tenantContextKeysOf, o as forwardScope } from "./timestamp.plugin-Bz_zL9PX.mjs";
6
+ import { c as tenantContextKeysOf, o as forwardScope } from "./timestamp.plugin-B-IJrlYb.mjs";
7
7
  import { r as getFieldsForUser } from "./field-selection-DPFaFw9Y.mjs";
8
8
  import mongoose from "mongoose";
9
9
  import { HOOK_PRIORITY } from "@classytic/repo-core/hooks";
@@ -125,6 +125,7 @@ function aggregateHelpersPlugin() {
125
125
  * applies). The same option is honoured by `immutableStatesPlugin`, so a
126
126
  * repository with both plugins purges through one call. Prefer it over
127
127
  * `bypassAppendOnly` for retention: it is delete-only and carries a reason.
128
+ * `allowArchive: true` admits only `archiveByFilter`'s attested chunk deletes.
128
129
  *
129
130
  * `skipPlugins` is deliberately IGNORED — same policy as tenant scope
130
131
  * and soft delete: integrity fences don't have a convenience bypass.
@@ -154,7 +155,8 @@ function appendOnlyPlugin(options = {}) {
154
155
  const modelName = repo.Model?.modelName ?? "model";
155
156
  const purgeGuard = {
156
157
  plugin: "appendOnlyPlugin",
157
- allowPurge: options.allowPurge === true
158
+ allowPurge: options.allowPurge === true,
159
+ allowArchive: options.allowArchive === true
158
160
  };
159
161
  const priority = options.priority ?? HOOK_PRIORITY.POLICY;
160
162
  for (const [op, meta] of Object.entries(OP_REGISTRY)) {
@@ -183,7 +185,7 @@ function appendOnlyPlugin(options = {}) {
183
185
  });
184
186
  }, { priority });
185
187
  }
186
- if (options.allowPurge) for (const op of ["delete", "deleteMany"]) repo.on(`after:${op}`, async (payload) => {
188
+ if (options.allowPurge || options.allowArchive) for (const op of ["delete", "deleteMany"]) repo.on(`after:${op}`, async (payload) => {
187
189
  const event = retentionPurgeEvent(op, payload.context, payload.result);
188
190
  if (event) await repo.emitAsync("after:append-only-purge", event);
189
191
  });
@@ -827,7 +829,8 @@ function immutableStatesPlugin(options) {
827
829
  };
828
830
  const purgeGuard = {
829
831
  plugin: "immutableStatesPlugin",
830
- allowPurge: options.allowPurge === true
832
+ allowPurge: options.allowPurge === true,
833
+ allowArchive: options.allowArchive === true
831
834
  };
832
835
  const purgeRequest = (op, ctx) => readRetentionPurge(ctx, op, purgeGuard);
833
836
  for (const op of [
@@ -842,7 +845,7 @@ function immutableStatesPlugin(options) {
842
845
  await guardDelete(ctx);
843
846
  });
844
847
  const emitPurge = async (operation, context, result) => {
845
- if (!options.allowPurge || context[internalFlag]) return;
848
+ if (!(options.allowPurge || options.allowArchive) || context[internalFlag]) return;
846
849
  const base = retentionPurgeEvent(operation, context, result);
847
850
  if (!base) return;
848
851
  const event = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@classytic/mongokit",
3
- "version": "3.44.0",
3
+ "version": "3.46.0",
4
4
  "description": "Production-grade MongoDB repositories - smart pagination, events, and plugins. Built on @classytic/repo-core.",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -152,9 +152,9 @@
152
152
  },
153
153
  "homepage": "https://github.com/classytic/mongokit#readme",
154
154
  "peerDependencies": {
155
- "@classytic/repo-core": ">=0.37.0",
155
+ "@classytic/repo-core": ">=0.38.0",
156
156
  "mongodb-memory-server": ">=11.3.0",
157
- "mongoose": ">=9.11.4"
157
+ "mongoose": ">=9.12.0"
158
158
  },
159
159
  "peerDependenciesMeta": {
160
160
  "mongodb-memory-server": {
@@ -202,25 +202,25 @@
202
202
  },
203
203
  "devDependencies": {
204
204
  "@arethetypeswrong/cli": "^0.18.5",
205
- "@better-auth/api-key": "^1.7.15",
206
- "@better-auth/mongo-adapter": "^1.7.15",
207
- "@biomejs/biome": "^2.6.4",
205
+ "@better-auth/api-key": "^1.7.18",
206
+ "@better-auth/mongo-adapter": "^1.7.18",
207
+ "@biomejs/biome": "^2.6.5",
208
208
  "@classytic/dev-tools": "^0.2.1",
209
- "@classytic/repo-core": "^0.37.0",
209
+ "@classytic/repo-core": "^0.38.0",
210
210
  "@types/node": "^26.6.6",
211
211
  "@vitest/coverage-v8": "^5.1.0",
212
212
  "ajv": "^8.20.0",
213
213
  "ajv-formats": "^3.0.1",
214
- "better-auth": "^1.7.15",
214
+ "better-auth": "^1.7.18",
215
215
  "dotenv": "^18.0.9",
216
216
  "fast-check": "^4.10.2",
217
217
  "knip": "^6.44.1",
218
- "mongodb": "~7.7.0",
218
+ "mongodb": "~7.8.0",
219
219
  "mongodb-memory-server": "^11.3.0",
220
- "mongoose": "^9.11.4",
220
+ "mongoose": "^9.12.0",
221
221
  "publint": "^0.3.25",
222
222
  "ts-api": "npm:typescript@~5.9.3",
223
- "tsdown": "^0.23.4",
223
+ "tsdown": "^0.23.5",
224
224
  "typescript": "~7.0.2",
225
225
  "vitest": "^5.1.0"
226
226
  }
@@ -37,6 +37,7 @@ Retention purge (append-only or frozen rows): build `appendOnlyPlugin` and/or `i
37
37
  `purgeByFilter(filter, { type: 'hard' }, { retentionPurge: { reason, actor } })` (or `deleteMany(filter, { mode: 'hard', retentionPurge })`).
38
38
  Each delete call emits `after:append-only-purge` / `after:immutable-states-purge` with `deletedCount` and `session`. Listeners are awaited and a
39
39
  throwing one fails the purge; write the audit row with `{ session: event.session }` so it commits or aborts with the delete. Ordinary deletes stay refused.
40
+ Archive (move to cold storage) on those repos: `allowArchive: true`, then `archiveByFilter(filter, sink, { retentionPurge })`; only chunks the sink acknowledged are deleted (event `archived: true`).
40
41
  Function-form anonymize writes through the guarded `updateMany` (frozen / append-only rows refused); `immutableStatesPlugin` reads state in the call's session.
41
42
 
42
43
  `leasePlugin` (after `methodRegistryPlugin`): `lease({ filter, leaseFor, leasedBy, now })` claims a pending row or a dead lease (a row
@@ -77,12 +77,14 @@ Resolution order: per call, then repository (`queryDefaults` option), then deplo
77
77
  import { assertQueryDefaultsConfigured, configureAggregateDefaults, configureQueryDefaults } from '@classytic/mongokit';
78
78
  configureQueryDefaults({ maxTimeMS: 15000, readConcern: 'majority', comment: 'api' });
79
79
  configureAggregateDefaults({ maxTimeMs: 30000, allowDiskUse: true });
80
- assertQueryDefaultsConfigured(mongoose.connection); // boot gate: throws without maxTimeMS or a client timeoutMS
80
+ assertQueryDefaultsConfigured(); // boot gate: throws without a deployment maxTimeMS
81
81
  await repo.getAll({ mode: 'offset', page: 1 }, { comment: 'orders.list', maxTimeMS: 5000, hint: { organizationId: 1, createdAt: -1 } });
82
82
  ```
83
83
 
84
84
  Inside a transaction a default read or write concern is dropped (the transaction owns it); a per-call one is passed through.
85
85
 
86
+ The bound is the server-side `maxTimeMS`. The driver's `timeoutMS` (CSOT) is a no-op since 7.8, so never rely on it. A single `create()` (Mongoose `save()`) forwards no `maxTimeMS` and is not server-bounded; `createMany`, updates, deletes, `bulkWrite`, reads and aggregates are. `withTransaction({ transactionOptions: { timeoutMS } })` is mongokit's own budget for the whole transaction: each operation gets `maxTimeMS` = what remains, the commit gets `maxCommitTimeMS`.
87
+
86
88
  ## Verify indexes
87
89
 
88
90
  ```ts