@web-ts-toolkit/moo 0.54.0 → 0.56.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -20,6 +20,7 @@ Peer dependencies:
20
20
  - model-function plugin
21
21
  - new-document plugin
22
22
  - cascade-delete plugin
23
+ - persisted derived fields for ordinary MongoDB queries and indexes
23
24
  - optional Keycloak user-sync plugin through a dedicated subpath
24
25
 
25
26
  ## Quick Start
@@ -49,6 +50,7 @@ Subpath entrypoints:
49
50
  - `@web-ts-toolkit/moo/utils` — `isSchema`, `isObjectIdType`, `isReference`, and the `ReferenceShape` type
50
51
  - `@web-ts-toolkit/moo/plugins` — plugin entrypoint
51
52
  - `@web-ts-toolkit/moo/plugins/cascade-delete` — cascade-delete plugin
53
+ - `@web-ts-toolkit/moo/plugins/derived-fields` — `derivedFieldsPlugin`, `defineDerivedField`, dependency error and authoring types
52
54
  - `@web-ts-toolkit/moo/plugins/model-function` — model-function plugin
53
55
  - `@web-ts-toolkit/moo/plugins/new-document` — new-document plugin
54
56
  - `@web-ts-toolkit/moo/plugins/keycloak-user-sync` — Keycloak user-sync plugin
@@ -73,6 +75,174 @@ userSchema.plugin(cascadeDeletePlugin, {
73
75
  });
74
76
  ```
75
77
 
78
+ ## Persisted Derived Fields
79
+
80
+ Use `derived` on a stored schema path to compute a value during async document
81
+ validation and persist it with hydrated `save()` / `Model.create()`. The stored
82
+ value supports ordinary MongoDB filtering, searching, sorting, and indexes; reads
83
+ and property access do not compute or write anything.
84
+
85
+ Named root imports are canonical. `defineDerivedField<User, 'fullName'>` checks
86
+ dependency names, a readonly partial raw-model input, and the target's result type
87
+ without globally augmenting Mongoose:
88
+
89
+ ```ts
90
+ import mongoose, { Schema } from 'mongoose';
91
+ import { defineDerivedField, derivedFieldsPlugin } from '@web-ts-toolkit/moo';
92
+
93
+ type User = { firstName: string; lastName: string; fullName: string };
94
+
95
+ const userSchema = new Schema<User>(
96
+ {
97
+ firstName: { type: String, required: true },
98
+ lastName: { type: String, required: true },
99
+ fullName: {
100
+ type: String,
101
+ required: true,
102
+ index: true,
103
+ derived: defineDerivedField<User, 'fullName'>({
104
+ dependsOn: ['firstName', 'lastName'],
105
+ get: (doc) => `${doc.firstName ?? ''} ${doc.lastName ?? ''}`.trim(),
106
+ }),
107
+ },
108
+ },
109
+ { optimisticConcurrency: true },
110
+ );
111
+
112
+ // Define all paths and source-mutating middleware before registering the plugin.
113
+ userSchema.plugin(derivedFieldsPlugin);
114
+ const User = mongoose.model<User>('User', userSchema);
115
+
116
+ await mongoose.connect('mongodb://127.0.0.1:27017/app');
117
+ await User.create({ firstName: 'Ada', lastName: 'Lovelace' });
118
+ const matches = await User.find({ fullName: /^Ada/ }).sort({ fullName: 1 });
119
+ ```
120
+
121
+ The same API (including types) is available from the grouped plugins entrypoint
122
+ and the direct entrypoint:
123
+
124
+ ```ts
125
+ import {
126
+ defineDerivedField,
127
+ derivedFieldsPlugin,
128
+ DerivedFieldsDependencyError,
129
+ type DerivedFieldDescriptor,
130
+ type DerivedFieldGetter,
131
+ type DerivedFieldInput,
132
+ } from '@web-ts-toolkit/moo/plugins/derived-fields';
133
+ ```
134
+
135
+ ### Dependencies and projections
136
+
137
+ Declare every source read by `get` in `dependsOn`. Existing documents request
138
+ recomputation for a dependency modification or explicit target change, including
139
+ actual pending default writes to either:
140
+
141
+ | Recompute requested? | All dependencies available? | Result |
142
+ | -------------------- | --------------------------- | ----------------------------------------------------------------- |
143
+ | No | Either | Skip and preserve the persisted derived value. |
144
+ | Yes | Yes | Recompute and save with the source changes. |
145
+ | Yes | No | Reject before persistence, without invoking any scheduled getter. |
146
+
147
+ Mongoose can queue a non-null default for persistence on an existing document
148
+ without marking the path modified. These pending writes count as changes, including
149
+ `''`, `0`, and `false`. Defaults of `null`/`undefined` alone do not queue writes and
150
+ do not request computation. A target's pending fallback default is recomputed rather
151
+ than accepted as an arbitrary override. Mere target absence/projection omission,
152
+ without a pending write, does not request repair.
153
+
154
+ New documents compute using supplied/defaulted inputs before ordinary validation.
155
+ `dependsOn: []` is valid for creation and target-change/default-write constants.
156
+
157
+ **Available** means selected by the query or fully replaced on the hydrated
158
+ document. Selected defaults are available; selected genuine absence without a
159
+ default is `undefined`, and stored `null` is also a known value. An unselected field
160
+ is unavailable even if a default or property getter could manufacture a value. Assigning an
161
+ excluded dependency explicitly (including `null`, `undefined`, or its default
162
+ value) supplies a replacement; that availability is retained on the same document
163
+ across saves. Child-only modifications and relative `$inc()` operations do not
164
+ prove an excluded input fully available. Fetch the needed fields yourself or
165
+ explicitly replace them; the plugin never fetches omitted inputs. Applying defaults
166
+ to excluded dependencies may queue writes, but does not supply replacements: an
167
+ otherwise unrelated save then rejects before getters or persistence rather than
168
+ writing manufactured sources with stale results.
169
+
170
+ A newly inserted document retains availability of its supplied, defaulted, and
171
+ absent inputs across later saves, including when constructed with projection
172
+ metadata.
173
+
174
+ A bare function (`derived: defineDerivedField<User, 'fullName'>(get)`) is supported
175
+ as conservative shorthand. It recomputes on every async validation/save operation
176
+ and requires **every supported non-derived input** to be available, including
177
+ hidden paths and `_id`, even if the function only reads one field. Prefer explicit
178
+ descriptors for projection-friendly updates.
179
+
180
+ ### Getter inputs, results, and errors
181
+
182
+ Getters may be sync or async; return `undefined` to clear a stored target. Each
183
+ receives an isolated frozen plain input view with available supported scalars
184
+ only, no document methods/`this`, virtuals, derived targets, managed version key, or
185
+ containers. References are persisted ids rather than populated documents;
186
+ Date/ObjectId/Decimal128 values
187
+ are copied per invocation. Property getters, transforms, and automatic database
188
+ fetches are bypassed. Results are staged before assignment through normal Mongoose
189
+ setters/casting. Getters and validators must not mutate the hydrated document via
190
+ captured references; keep computations deterministic and declare their real inputs.
191
+
192
+ Missing-input preflight aggregates every affected target before any getter runs.
193
+ `DerivedFieldsDependencyError.code` is
194
+ `MOO_DERIVED_FIELDS_MISSING_DEPENDENCIES`, with frozen path-only
195
+ `issues: [{ targetPath, missingPaths }]` (no source values). The code is a stable
196
+ discriminator across separately bundled entrypoints. Getter failures propagate;
197
+ target casting and ordinary validation failures use Mongoose errors. Rejected saves
198
+ do not persist that save, but retain application assignments in memory; no
199
+ in-memory rollback is promised.
200
+
201
+ ### Lifecycle and write boundaries
202
+
203
+ - Register source-mutating pre-validation and pre-save hooks **before** this
204
+ plugin, then compile the model. Normal async validation sees the computed/cast
205
+ derived results. Earlier pre-save source/target changes after initial validation
206
+ trigger recomputation and revalidation (preserving `validateModifiedOnly`). A
207
+ normal save reuses its validation computation when inputs/results are unchanged;
208
+ separate `validate()` calls/saves are separate operations.
209
+ - Later pre-save mutations are outside the guarantee. Later validation-hook or
210
+ validator mutations are not a standalone-validation guarantee; save attempts one
211
+ revalidation and rejects an ordering error if inputs/results remain unstable.
212
+ Timestamp sources assigned in earlier pre-save hooks are covered, but a new
213
+ document's first validation can see an unset timestamp. Ordinary Mongoose change
214
+ tracking still applies (for example, mark in-place Date mutations modified).
215
+ - With schema-level or per-save `validateBeforeSave: false`, the plugin still
216
+ preflights, computes, and casts, but skips ordinary required/custom validators.
217
+ Dependency, getter, and target casting errors still reject before writing.
218
+ - Query updates/replacements and raw-driver writes bypass computation.
219
+ `insertMany()` and bulk insert paths may incidentally invoke async document
220
+ validation, but are not guaranteed all-writer support; lean/skip-validation paths
221
+ and bulk updates can bypass it. `validateSync()`, middleware bypass, and restricted
222
+ `pathsToSave` are outside this contract.
223
+ - Changing a formula or adopting the plugin for old rows requires an explicit
224
+ backfill/repair. Load all dependencies, `doc.markModified('fullName')`, then
225
+ `await doc.save()` to request recomputation even if sources are unchanged. This
226
+ is not an automatic read-time repair. Multiple writers retain ordinary Mongoose
227
+ concurrency semantics: use `optimisticConcurrency` where the source/result
228
+ invariant matters and retry version conflicts by reloading complete inputs.
229
+
230
+ ### Initial scope
231
+
232
+ Targets and dependencies must be root persisted **String, Number, Boolean, Date,
233
+ ObjectId, Decimal128, or BigInt** paths. Nested paths, subdocuments, arrays, maps,
234
+ Mixed, Union, Buffer/Binary/UUID, BSON Double/Int32, and unknown custom scalar kinds
235
+ are unsupported as targets/dependencies. Unsupported ordinary fields may coexist
236
+ but are absent from getter views. Self/derived-to-derived dependencies, unknown or
237
+ duplicate dependency names, malformed options, virtual/immutable targets, and
238
+ duplicate plugin registration throw at `schema.plugin(...)` time. Paths and
239
+ definitions are snapshotted at registration; later schema/option edits are not
240
+ discovered. Mongoose's managed `versionKey` (default `__v` or a custom name) is
241
+ rejected as a target or explicit dependency and excluded from all getter views,
242
+ including shorthand: Mongoose initializes/increments it after pre-save middleware.
243
+ With `versionKey: false`, explicitly declared version-like fields are ordinary
244
+ supported scalar targets/inputs.
245
+
76
246
  ## Model Function Plugin
77
247
 
78
248
  ```ts
package/index.d.mts CHANGED
@@ -1,5 +1,6 @@
1
1
  export { ModelDocument, ModelFunctionInstanceMethods, ModelFunctionPluginOptions, ModelFunctionStaticMethods, modelFunctionPlugin } from './plugins/model-function.mjs';
2
2
  export { CascadeDeleteDependencyMap, CascadeDeleteDocumentMethods, CascadeDeleteModelStatics, CascadeDeletePluginOptions, FilterResolver, QueryFilter, QueryOperators, cascadeDeletePlugin } from './plugins/cascade-delete.mjs';
3
+ export { DerivedFieldDefinition, DerivedFieldDependencyIssue, DerivedFieldDescriptor, DerivedFieldGetter, DerivedFieldInput, DerivedFieldsDependencyError, defineDerivedField, derivedFieldsPlugin } from './plugins/derived-fields.mjs';
3
4
  export { NewDocumentPluginOptions, newDocumentPlugin } from './plugins/new-document.mjs';
4
5
  export { SchemaOptionOverrides, uniqueEmptiableString, uniqueNullableString } from './schema.mjs';
5
6
  export { isObjectId } from './is.mjs';
package/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export { ModelDocument, ModelFunctionInstanceMethods, ModelFunctionPluginOptions, ModelFunctionStaticMethods, modelFunctionPlugin } from './plugins/model-function.js';
2
2
  export { CascadeDeleteDependencyMap, CascadeDeleteDocumentMethods, CascadeDeleteModelStatics, CascadeDeletePluginOptions, FilterResolver, QueryFilter, QueryOperators, cascadeDeletePlugin } from './plugins/cascade-delete.js';
3
+ export { DerivedFieldDefinition, DerivedFieldDependencyIssue, DerivedFieldDescriptor, DerivedFieldGetter, DerivedFieldInput, DerivedFieldsDependencyError, defineDerivedField, derivedFieldsPlugin } from './plugins/derived-fields.js';
3
4
  export { NewDocumentPluginOptions, newDocumentPlugin } from './plugins/new-document.js';
4
5
  export { SchemaOptionOverrides, uniqueEmptiableString, uniqueNullableString } from './schema.js';
5
6
  export { isObjectId } from './is.js';
package/index.js CHANGED
@@ -30,7 +30,10 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/index.ts
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
+ DerivedFieldsDependencyError: () => DerivedFieldsDependencyError,
33
34
  cascadeDeletePlugin: () => cascadeDeletePlugin,
35
+ defineDerivedField: () => defineDerivedField,
36
+ derivedFieldsPlugin: () => derivedFieldsPlugin,
34
37
  isObjectId: () => isObjectId,
35
38
  modelFunctionPlugin: () => modelFunctionPlugin,
36
39
  newDocumentPlugin: () => newDocumentPlugin,
@@ -521,6 +524,341 @@ function cascadeDeletePlugin(schema, options) {
521
524
  });
522
525
  }
523
526
 
527
+ // src/plugins/derived-fields/definitions.ts
528
+ function defineDerivedField(definition) {
529
+ return definition;
530
+ }
531
+ var DerivedFieldsDependencyError = class extends Error {
532
+ constructor(issues) {
533
+ const snapshot = Object.freeze(
534
+ issues.map(
535
+ ({ targetPath, missingPaths }) => Object.freeze({ targetPath, missingPaths: Object.freeze([...missingPaths]) })
536
+ )
537
+ );
538
+ const details = snapshot.map(
539
+ ({ targetPath, missingPaths }) => `${JSON.stringify(targetPath)} requires ${missingPaths.map((path) => JSON.stringify(path)).join(", ")}`
540
+ ).join("; ");
541
+ super(`derivedFieldsPlugin missing dependencies: ${details}`);
542
+ /** Stable discriminator, including across separately bundled entrypoints. */
543
+ this.code = "MOO_DERIVED_FIELDS_MISSING_DEPENDENCIES";
544
+ this.name = "DerivedFieldsDependencyError";
545
+ this.issues = snapshot;
546
+ }
547
+ };
548
+
549
+ // src/plugins/derived-fields/lifecycle.ts
550
+ var import_node_util = require("util");
551
+ var import_mongoose5 = require("mongoose");
552
+
553
+ // src/plugins/derived-fields/planner.ts
554
+ var hasOwn = (object, key) => Object.prototype.hasOwnProperty.call(object, key);
555
+ var isFullySelected = (document, path) => {
556
+ const projection = document.$__?.selected;
557
+ if (projection) {
558
+ if (hasOwn(projection, path) && projection[path] !== 1 && projection[path] !== true) return false;
559
+ const keys = Object.keys(projection);
560
+ if (keys.some((key) => key.startsWith(`${path}.`))) return false;
561
+ if (path !== "_id" && keys.length === 1 && projection._id === false) return true;
562
+ }
563
+ return document.isDirectSelected(path);
564
+ };
565
+ function planDerivedFields(registration, document, options = {}) {
566
+ const replacedPaths = registration.inputPaths.filter(
567
+ (path) => options.knownReplacedPaths?.has(path) || document.isDirectModified(path) && !document.$__?.primitiveAtomics?.[path]
568
+ );
569
+ const replacements = new Set(replacedPaths);
570
+ const availablePaths = registration.inputPaths.filter(
571
+ (path) => document.isNew || replacements.has(path) || isFullySelected(document, path)
572
+ );
573
+ const available = new Set(availablePaths);
574
+ const fields = registration.fields.filter(
575
+ (field) => document.isNew || field.mode === "always" || document.isDirectModified(field.targetPath) || options.pendingWritePaths?.has(field.targetPath) || field.dependsOn.some((path) => document.isModified(path) || options.pendingWritePaths?.has(path))
576
+ );
577
+ const issues = [];
578
+ for (const field of fields) {
579
+ const dependencies = field.mode === "always" ? registration.inputPaths : field.dependsOn;
580
+ const missingPaths = dependencies.filter((path) => !available.has(path));
581
+ if (missingPaths.length) issues.push({ targetPath: field.targetPath, missingPaths });
582
+ }
583
+ if (issues.length) throw new DerivedFieldsDependencyError(issues);
584
+ return Object.freeze({
585
+ fields: Object.freeze(fields),
586
+ availablePaths: Object.freeze(availablePaths),
587
+ replacedPaths: Object.freeze(replacedPaths)
588
+ });
589
+ }
590
+
591
+ // src/plugins/derived-fields/registration.ts
592
+ var supportedDerivedFieldTypes = Object.freeze([
593
+ "String",
594
+ "Number",
595
+ "Boolean",
596
+ "Date",
597
+ "ObjectId",
598
+ "Decimal128",
599
+ "BigInt"
600
+ ]);
601
+ var registrationKey = /* @__PURE__ */ Symbol.for("@web-ts-toolkit/moo/derived-fields/registration");
602
+ var hasOwn2 = (object, key) => Object.prototype.hasOwnProperty.call(object, key);
603
+ var isRootPath = (path) => path.length > 0 && !/[.\s$\0]/.test(path);
604
+ var isSupportedPath = (path, type) => isRootPath(path) && supportedDerivedFieldTypes.includes(type.instance);
605
+ var configurationError = (targetPath, reason) => {
606
+ throw new TypeError(`derivedFieldsPlugin target ${JSON.stringify(targetPath)}: ${reason}`);
607
+ };
608
+ var parseDefinition = (targetPath, value) => {
609
+ if (typeof value === "function") {
610
+ return Object.freeze({
611
+ targetPath,
612
+ mode: "always",
613
+ dependsOn: Object.freeze([]),
614
+ get: value
615
+ });
616
+ }
617
+ if (value === null || typeof value !== "object" || Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null) {
618
+ return configurationError(targetPath, "derived must be a getter function or a plain { dependsOn, get } descriptor");
619
+ }
620
+ if (Reflect.ownKeys(value).some((key) => key !== "dependsOn" && key !== "get") || !hasOwn2(value, "dependsOn") || !hasOwn2(value, "get")) {
621
+ return configurationError(targetPath, "derived descriptors require exactly dependsOn and get");
622
+ }
623
+ const descriptor = value;
624
+ if (!Array.isArray(descriptor.dependsOn) || typeof descriptor.get !== "function") {
625
+ return configurationError(targetPath, "dependsOn must be an array of root path names and get must be a function");
626
+ }
627
+ const dependencies = [];
628
+ for (const dependency of descriptor.dependsOn) {
629
+ if (typeof dependency !== "string" || !isRootPath(dependency)) {
630
+ return configurationError(
631
+ targetPath,
632
+ "dependsOn must contain only non-empty root path names (no dots, whitespace, or $)"
633
+ );
634
+ }
635
+ if (dependencies.includes(dependency)) {
636
+ return configurationError(targetPath, `duplicate dependency path ${JSON.stringify(dependency)}`);
637
+ }
638
+ dependencies.push(dependency);
639
+ }
640
+ return Object.freeze({
641
+ targetPath,
642
+ mode: "dependencies",
643
+ dependsOn: Object.freeze(dependencies),
644
+ get: descriptor.get
645
+ });
646
+ };
647
+ function registerDerivedFields(schema) {
648
+ const markedSchema = schema;
649
+ if (hasOwn2(markedSchema, registrationKey)) {
650
+ throw new TypeError("derivedFieldsPlugin is already registered on this schema");
651
+ }
652
+ const fields = [];
653
+ const versionKey = schema.options.versionKey;
654
+ const visitSchema = (current, prefix, ancestors) => {
655
+ current.eachPath((path, type) => {
656
+ const fullPath = `${prefix}${path}`;
657
+ if (!hasOwn2(type.options, "derived")) return;
658
+ if (prefix || !isSupportedPath(path, type)) {
659
+ configurationError(
660
+ fullPath,
661
+ `only root scalar persisted paths are supported (${supportedDerivedFieldTypes.join(", ")})`
662
+ );
663
+ }
664
+ if (path === versionKey) {
665
+ configurationError(
666
+ fullPath,
667
+ `Mongoose-managed version key ${JSON.stringify(versionKey)} cannot be a derived target`
668
+ );
669
+ }
670
+ if (path === "_id" || (type.$immutable ?? type.options.immutable)) {
671
+ configurationError(fullPath, "immutable targets are not supported");
672
+ }
673
+ fields.push(parseDefinition(fullPath, type.options.derived));
674
+ });
675
+ const virtuals = current.virtuals;
676
+ for (const [path, virtual] of Object.entries(virtuals)) {
677
+ if (hasOwn2(virtual.options, "derived")) {
678
+ configurationError(`${prefix}${path}`, "derived targets must be persisted schema paths, not virtuals");
679
+ }
680
+ }
681
+ if (ancestors.has(current)) return;
682
+ const nextAncestors = /* @__PURE__ */ new Set([...ancestors, current]);
683
+ const visitEmbedded = (type, path) => {
684
+ if (type.schema) {
685
+ visitSchema(type.schema, `${path}.`, nextAncestors);
686
+ return;
687
+ }
688
+ for (const member of type.schemaTypes ?? []) {
689
+ if (hasOwn2(member.options, "derived")) {
690
+ configurationError(path, "derived targets inside union containers are not supported");
691
+ }
692
+ visitEmbedded(member, path);
693
+ }
694
+ const embedded = type.getEmbeddedSchemaType?.() ?? type.caster ?? type.$embeddedSchemaType ?? type.$__schemaType;
695
+ if (!embedded || embedded === type) return;
696
+ const embeddedPath = `${path}.${type.instance === "Map" ? "$*" : "$"}`;
697
+ if (hasOwn2(embedded.options, "derived")) {
698
+ configurationError(embeddedPath, "derived targets inside containers are not supported");
699
+ }
700
+ visitEmbedded(embedded, embeddedPath);
701
+ };
702
+ current.eachPath((path, type) => visitEmbedded(type, `${prefix}${path}`));
703
+ };
704
+ visitSchema(schema, "", /* @__PURE__ */ new Set());
705
+ const targets = new Set(fields.map((field) => field.targetPath));
706
+ for (const field of fields) {
707
+ for (const dependency of field.dependsOn) {
708
+ if (dependency === versionKey) {
709
+ configurationError(
710
+ field.targetPath,
711
+ `Mongoose-managed version key ${JSON.stringify(versionKey)} cannot be a dependency`
712
+ );
713
+ }
714
+ if (targets.has(dependency)) {
715
+ configurationError(
716
+ field.targetPath,
717
+ `dependency path ${JSON.stringify(dependency)} is itself a derived target`
718
+ );
719
+ }
720
+ const type = schema.path(dependency);
721
+ if (!type) {
722
+ configurationError(field.targetPath, `unknown or non-persisted dependency path ${JSON.stringify(dependency)}`);
723
+ }
724
+ if (!isSupportedPath(dependency, type)) {
725
+ configurationError(
726
+ field.targetPath,
727
+ `dependency path ${JSON.stringify(dependency)} must be a supported root scalar persisted path`
728
+ );
729
+ }
730
+ }
731
+ }
732
+ const inputPaths = [];
733
+ schema.eachPath((path, type) => {
734
+ if (path !== versionKey && !targets.has(path) && isSupportedPath(path, type)) inputPaths.push(path);
735
+ });
736
+ const registration = Object.freeze({ fields: Object.freeze(fields), inputPaths: Object.freeze(inputPaths) });
737
+ Object.defineProperty(markedSchema, registrationKey, { value: registration });
738
+ return registration;
739
+ }
740
+
741
+ // src/plugins/derived-fields/snapshot.ts
742
+ var import_mongoose4 = require("mongoose");
743
+ function cloneDerivedScalar(value) {
744
+ if (value instanceof Date) return new Date(value.getTime());
745
+ if (value !== null && typeof value === "object") {
746
+ const bson = value;
747
+ if (bson._bsontype === "ObjectId") return new import_mongoose4.Types.ObjectId(Buffer.from(bson.id));
748
+ if (bson._bsontype === "Decimal128") return new import_mongoose4.Types.Decimal128(Buffer.from(bson.bytes));
749
+ }
750
+ return value;
751
+ }
752
+ function readDerivedScalar(document, path) {
753
+ const id = document.populated(path);
754
+ return id === void 0 ? document.get(path, void 0, { getters: false }) : id;
755
+ }
756
+ function snapshotDerivedInputs(document, paths) {
757
+ return copyDerivedSnapshot(Object.fromEntries(paths.map((path) => [path, readDerivedScalar(document, path)])));
758
+ }
759
+ function copyDerivedSnapshot(snapshot, paths = Object.keys(snapshot)) {
760
+ return Object.freeze(Object.fromEntries(paths.map((path) => [path, cloneDerivedScalar(snapshot[path])])));
761
+ }
762
+
763
+ // src/plugins/derived-fields/lifecycle.ts
764
+ var watchedPaths = (plan) => [
765
+ ...new Set(plan.fields.flatMap((field) => field.mode === "always" ? plan.availablePaths : field.dependsOn))
766
+ ];
767
+ function derivedFieldsPlugin(schema) {
768
+ const registration = registerDerivedFields(schema);
769
+ const trackedPaths = [...registration.inputPaths, ...registration.fields.map((field) => field.targetPath)];
770
+ const states = /* @__PURE__ */ new WeakMap();
771
+ const stateFor = (document) => {
772
+ let state = states.get(document);
773
+ if (!state) {
774
+ state = { replacedPaths: /* @__PURE__ */ new Set(), assignedPaths: /* @__PURE__ */ new Set(), applyingResults: false };
775
+ states.set(document, state);
776
+ }
777
+ return state;
778
+ };
779
+ for (const path of trackedPaths) {
780
+ schema.path(path).set(function derivedFieldsObserveAssignment(value, _priorValue, _schemaType, options) {
781
+ if (this instanceof import_mongoose5.Document && !this.isNew && !options?._skipMarkModified && !isFullySelected(this, path)) {
782
+ const state = stateFor(this);
783
+ if (!state.applyingResults) state.assignedPaths.add(path);
784
+ }
785
+ return value;
786
+ });
787
+ }
788
+ const planFor = (document, state) => {
789
+ for (const path of state.assignedPaths) {
790
+ if (!document.$__.validationError?.errors[path] && !document.$__.primitiveAtomics?.[path]) {
791
+ document.markModified(path);
792
+ }
793
+ }
794
+ state.assignedPaths.clear();
795
+ const pendingWritePaths = new Set(
796
+ document.isNew ? [] : trackedPaths.filter(
797
+ (path) => path !== "_id" && document.$isDefault(path) && document.$__.selected?.[path] !== 0 && document.get(path, void 0, { getters: false }) != null
798
+ )
799
+ );
800
+ const plan = planDerivedFields(registration, document, {
801
+ knownReplacedPaths: state.replacedPaths,
802
+ pendingWritePaths
803
+ });
804
+ state.replacedPaths = new Set(document.isNew ? plan.availablePaths : plan.replacedPaths);
805
+ return plan;
806
+ };
807
+ const matches = (document, plan, checkpoint) => checkpoint !== void 0 && checkpoint.saveOptions === document.$__.saveOptions && checkpoint.isNew === document.isNew && // Restoring a target to its original persisted value can clear its dirty
808
+ // flag. It was still computed; only newly scheduled fields invalidate reuse.
809
+ plan.fields.every((field) => checkpoint.targets.includes(field.targetPath)) && (0, import_node_util.isDeepStrictEqual)(checkpoint.inputs, snapshotDerivedInputs(document, Object.keys(checkpoint.inputs))) && (0, import_node_util.isDeepStrictEqual)(checkpoint.results, snapshotDerivedInputs(document, checkpoint.targets));
810
+ const compute = async (document) => {
811
+ const state = stateFor(document);
812
+ state.checkpoint = void 0;
813
+ const plan = planFor(document, state);
814
+ const inputs = snapshotDerivedInputs(document, plan.availablePaths);
815
+ const staged = [];
816
+ for (const { targetPath, get } of plan.fields) {
817
+ const result = await get(copyDerivedSnapshot(inputs));
818
+ staged.push({ path: targetPath, value: cloneDerivedScalar(result) });
819
+ }
820
+ state.applyingResults = true;
821
+ try {
822
+ for (const { path, value } of staged) document.set(path, value);
823
+ } finally {
824
+ state.applyingResults = false;
825
+ }
826
+ const validationError = document.$__.validationError;
827
+ if (staged.some(({ path }) => validationError?.errors[path]?.name === "CastError")) throw validationError;
828
+ const targets = plan.fields.map((field) => field.targetPath);
829
+ state.checkpoint = {
830
+ saveOptions: document.$__.saveOptions,
831
+ isNew: document.isNew,
832
+ targets,
833
+ inputs: copyDerivedSnapshot(inputs, watchedPaths(plan)),
834
+ results: snapshotDerivedInputs(document, targets)
835
+ };
836
+ };
837
+ schema.pre("validate", async function derivedFieldsPreValidate() {
838
+ await compute(this);
839
+ });
840
+ schema.pre("save", async function derivedFieldsPreSave() {
841
+ const state = stateFor(this);
842
+ const plan = planFor(this, state);
843
+ if (this.$__.saveOptions && matches(this, plan, state.checkpoint)) return;
844
+ const options = this.$__.saveOptions;
845
+ const validate = options && "validateBeforeSave" in options ? !!options.validateBeforeSave : schema.options.validateBeforeSave;
846
+ if (validate) {
847
+ await this.validate(
848
+ void 0,
849
+ options && "validateModifiedOnly" in options ? { validateModifiedOnly: options.validateModifiedOnly } : void 0
850
+ );
851
+ } else {
852
+ await compute(this);
853
+ }
854
+ if (!matches(this, planFor(this, state), state.checkpoint)) {
855
+ throw new Error(
856
+ "derivedFieldsPlugin inputs or targets changed after computation; register source-mutating validation hooks before the plugin and keep getters/validators free of document mutations"
857
+ );
858
+ }
859
+ });
860
+ }
861
+
524
862
  // src/plugins/new-document.ts
525
863
  var stateKey = "newDocumentPlugin";
526
864
  function newDocumentPlugin(schema, options) {
@@ -561,8 +899,8 @@ var uniqueEmptiableString = (field, options) => (
561
899
  );
562
900
 
563
901
  // src/is.ts
564
- var import_mongoose4 = require("mongoose");
565
- var { ObjectId } = import_mongoose4.Types;
902
+ var import_mongoose6 = require("mongoose");
903
+ var { ObjectId } = import_mongoose6.Types;
566
904
  var CANONICAL_OBJECT_ID_STRING = /^[0-9a-f]{24}$/;
567
905
  function isObjectId(value) {
568
906
  try {
@@ -579,7 +917,10 @@ function isObjectId(value) {
579
917
  }
580
918
  // Annotate the CommonJS export names for ESM import in node:
581
919
  0 && (module.exports = {
920
+ DerivedFieldsDependencyError,
582
921
  cascadeDeletePlugin,
922
+ defineDerivedField,
923
+ derivedFieldsPlugin,
583
924
  isObjectId,
584
925
  modelFunctionPlugin,
585
926
  newDocumentPlugin,