@web-ts-toolkit/moo 0.55.0 → 0.57.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/plugins/index.mjs CHANGED
@@ -480,6 +480,341 @@ function cascadeDeletePlugin(schema, options) {
480
480
  });
481
481
  }
482
482
 
483
+ // src/plugins/derived-fields/definitions.ts
484
+ function defineDerivedField(definition) {
485
+ return definition;
486
+ }
487
+ var DerivedFieldsDependencyError = class extends Error {
488
+ constructor(issues) {
489
+ const snapshot = Object.freeze(
490
+ issues.map(
491
+ ({ targetPath, missingPaths }) => Object.freeze({ targetPath, missingPaths: Object.freeze([...missingPaths]) })
492
+ )
493
+ );
494
+ const details = snapshot.map(
495
+ ({ targetPath, missingPaths }) => `${JSON.stringify(targetPath)} requires ${missingPaths.map((path) => JSON.stringify(path)).join(", ")}`
496
+ ).join("; ");
497
+ super(`derivedFieldsPlugin missing dependencies: ${details}`);
498
+ /** Stable discriminator, including across separately bundled entrypoints. */
499
+ this.code = "MOO_DERIVED_FIELDS_MISSING_DEPENDENCIES";
500
+ this.name = "DerivedFieldsDependencyError";
501
+ this.issues = snapshot;
502
+ }
503
+ };
504
+
505
+ // src/plugins/derived-fields/lifecycle.ts
506
+ import { isDeepStrictEqual } from "util";
507
+ import { Document } from "mongoose";
508
+
509
+ // src/plugins/derived-fields/planner.ts
510
+ var hasOwn = (object, key) => Object.prototype.hasOwnProperty.call(object, key);
511
+ var isFullySelected = (document, path) => {
512
+ const projection = document.$__?.selected;
513
+ if (projection) {
514
+ if (hasOwn(projection, path) && projection[path] !== 1 && projection[path] !== true) return false;
515
+ const keys = Object.keys(projection);
516
+ if (keys.some((key) => key.startsWith(`${path}.`))) return false;
517
+ if (path !== "_id" && keys.length === 1 && projection._id === false) return true;
518
+ }
519
+ return document.isDirectSelected(path);
520
+ };
521
+ function planDerivedFields(registration, document, options = {}) {
522
+ const replacedPaths = registration.inputPaths.filter(
523
+ (path) => options.knownReplacedPaths?.has(path) || document.isDirectModified(path) && !document.$__?.primitiveAtomics?.[path]
524
+ );
525
+ const replacements = new Set(replacedPaths);
526
+ const availablePaths = registration.inputPaths.filter(
527
+ (path) => document.isNew || replacements.has(path) || isFullySelected(document, path)
528
+ );
529
+ const available = new Set(availablePaths);
530
+ const fields = registration.fields.filter(
531
+ (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))
532
+ );
533
+ const issues = [];
534
+ for (const field of fields) {
535
+ const dependencies = field.mode === "always" ? registration.inputPaths : field.dependsOn;
536
+ const missingPaths = dependencies.filter((path) => !available.has(path));
537
+ if (missingPaths.length) issues.push({ targetPath: field.targetPath, missingPaths });
538
+ }
539
+ if (issues.length) throw new DerivedFieldsDependencyError(issues);
540
+ return Object.freeze({
541
+ fields: Object.freeze(fields),
542
+ availablePaths: Object.freeze(availablePaths),
543
+ replacedPaths: Object.freeze(replacedPaths)
544
+ });
545
+ }
546
+
547
+ // src/plugins/derived-fields/registration.ts
548
+ var supportedDerivedFieldTypes = Object.freeze([
549
+ "String",
550
+ "Number",
551
+ "Boolean",
552
+ "Date",
553
+ "ObjectId",
554
+ "Decimal128",
555
+ "BigInt"
556
+ ]);
557
+ var registrationKey = /* @__PURE__ */ Symbol.for("@web-ts-toolkit/moo/derived-fields/registration");
558
+ var hasOwn2 = (object, key) => Object.prototype.hasOwnProperty.call(object, key);
559
+ var isRootPath = (path) => path.length > 0 && !/[.\s$\0]/.test(path);
560
+ var isSupportedPath = (path, type) => isRootPath(path) && supportedDerivedFieldTypes.includes(type.instance);
561
+ var configurationError = (targetPath, reason) => {
562
+ throw new TypeError(`derivedFieldsPlugin target ${JSON.stringify(targetPath)}: ${reason}`);
563
+ };
564
+ var parseDefinition = (targetPath, value) => {
565
+ if (typeof value === "function") {
566
+ return Object.freeze({
567
+ targetPath,
568
+ mode: "always",
569
+ dependsOn: Object.freeze([]),
570
+ get: value
571
+ });
572
+ }
573
+ if (value === null || typeof value !== "object" || Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null) {
574
+ return configurationError(targetPath, "derived must be a getter function or a plain { dependsOn, get } descriptor");
575
+ }
576
+ if (Reflect.ownKeys(value).some((key) => key !== "dependsOn" && key !== "get") || !hasOwn2(value, "dependsOn") || !hasOwn2(value, "get")) {
577
+ return configurationError(targetPath, "derived descriptors require exactly dependsOn and get");
578
+ }
579
+ const descriptor = value;
580
+ if (!Array.isArray(descriptor.dependsOn) || typeof descriptor.get !== "function") {
581
+ return configurationError(targetPath, "dependsOn must be an array of root path names and get must be a function");
582
+ }
583
+ const dependencies = [];
584
+ for (const dependency of descriptor.dependsOn) {
585
+ if (typeof dependency !== "string" || !isRootPath(dependency)) {
586
+ return configurationError(
587
+ targetPath,
588
+ "dependsOn must contain only non-empty root path names (no dots, whitespace, or $)"
589
+ );
590
+ }
591
+ if (dependencies.includes(dependency)) {
592
+ return configurationError(targetPath, `duplicate dependency path ${JSON.stringify(dependency)}`);
593
+ }
594
+ dependencies.push(dependency);
595
+ }
596
+ return Object.freeze({
597
+ targetPath,
598
+ mode: "dependencies",
599
+ dependsOn: Object.freeze(dependencies),
600
+ get: descriptor.get
601
+ });
602
+ };
603
+ function registerDerivedFields(schema) {
604
+ const markedSchema = schema;
605
+ if (hasOwn2(markedSchema, registrationKey)) {
606
+ throw new TypeError("derivedFieldsPlugin is already registered on this schema");
607
+ }
608
+ const fields = [];
609
+ const versionKey = schema.options.versionKey;
610
+ const visitSchema = (current, prefix, ancestors) => {
611
+ current.eachPath((path, type) => {
612
+ const fullPath = `${prefix}${path}`;
613
+ if (!hasOwn2(type.options, "derived")) return;
614
+ if (prefix || !isSupportedPath(path, type)) {
615
+ configurationError(
616
+ fullPath,
617
+ `only root scalar persisted paths are supported (${supportedDerivedFieldTypes.join(", ")})`
618
+ );
619
+ }
620
+ if (path === versionKey) {
621
+ configurationError(
622
+ fullPath,
623
+ `Mongoose-managed version key ${JSON.stringify(versionKey)} cannot be a derived target`
624
+ );
625
+ }
626
+ if (path === "_id" || (type.$immutable ?? type.options.immutable)) {
627
+ configurationError(fullPath, "immutable targets are not supported");
628
+ }
629
+ fields.push(parseDefinition(fullPath, type.options.derived));
630
+ });
631
+ const virtuals = current.virtuals;
632
+ for (const [path, virtual] of Object.entries(virtuals)) {
633
+ if (hasOwn2(virtual.options, "derived")) {
634
+ configurationError(`${prefix}${path}`, "derived targets must be persisted schema paths, not virtuals");
635
+ }
636
+ }
637
+ if (ancestors.has(current)) return;
638
+ const nextAncestors = /* @__PURE__ */ new Set([...ancestors, current]);
639
+ const visitEmbedded = (type, path) => {
640
+ if (type.schema) {
641
+ visitSchema(type.schema, `${path}.`, nextAncestors);
642
+ return;
643
+ }
644
+ for (const member of type.schemaTypes ?? []) {
645
+ if (hasOwn2(member.options, "derived")) {
646
+ configurationError(path, "derived targets inside union containers are not supported");
647
+ }
648
+ visitEmbedded(member, path);
649
+ }
650
+ const embedded = type.getEmbeddedSchemaType?.() ?? type.caster ?? type.$embeddedSchemaType ?? type.$__schemaType;
651
+ if (!embedded || embedded === type) return;
652
+ const embeddedPath = `${path}.${type.instance === "Map" ? "$*" : "$"}`;
653
+ if (hasOwn2(embedded.options, "derived")) {
654
+ configurationError(embeddedPath, "derived targets inside containers are not supported");
655
+ }
656
+ visitEmbedded(embedded, embeddedPath);
657
+ };
658
+ current.eachPath((path, type) => visitEmbedded(type, `${prefix}${path}`));
659
+ };
660
+ visitSchema(schema, "", /* @__PURE__ */ new Set());
661
+ const targets = new Set(fields.map((field) => field.targetPath));
662
+ for (const field of fields) {
663
+ for (const dependency of field.dependsOn) {
664
+ if (dependency === versionKey) {
665
+ configurationError(
666
+ field.targetPath,
667
+ `Mongoose-managed version key ${JSON.stringify(versionKey)} cannot be a dependency`
668
+ );
669
+ }
670
+ if (targets.has(dependency)) {
671
+ configurationError(
672
+ field.targetPath,
673
+ `dependency path ${JSON.stringify(dependency)} is itself a derived target`
674
+ );
675
+ }
676
+ const type = schema.path(dependency);
677
+ if (!type) {
678
+ configurationError(field.targetPath, `unknown or non-persisted dependency path ${JSON.stringify(dependency)}`);
679
+ }
680
+ if (!isSupportedPath(dependency, type)) {
681
+ configurationError(
682
+ field.targetPath,
683
+ `dependency path ${JSON.stringify(dependency)} must be a supported root scalar persisted path`
684
+ );
685
+ }
686
+ }
687
+ }
688
+ const inputPaths = [];
689
+ schema.eachPath((path, type) => {
690
+ if (path !== versionKey && !targets.has(path) && isSupportedPath(path, type)) inputPaths.push(path);
691
+ });
692
+ const registration = Object.freeze({ fields: Object.freeze(fields), inputPaths: Object.freeze(inputPaths) });
693
+ Object.defineProperty(markedSchema, registrationKey, { value: registration });
694
+ return registration;
695
+ }
696
+
697
+ // src/plugins/derived-fields/snapshot.ts
698
+ import { Types } from "mongoose";
699
+ function cloneDerivedScalar(value) {
700
+ if (value instanceof Date) return new Date(value.getTime());
701
+ if (value !== null && typeof value === "object") {
702
+ const bson = value;
703
+ if (bson._bsontype === "ObjectId") return new Types.ObjectId(Buffer.from(bson.id));
704
+ if (bson._bsontype === "Decimal128") return new Types.Decimal128(Buffer.from(bson.bytes));
705
+ }
706
+ return value;
707
+ }
708
+ function readDerivedScalar(document, path) {
709
+ const id = document.populated(path);
710
+ return id === void 0 ? document.get(path, void 0, { getters: false }) : id;
711
+ }
712
+ function snapshotDerivedInputs(document, paths) {
713
+ return copyDerivedSnapshot(Object.fromEntries(paths.map((path) => [path, readDerivedScalar(document, path)])));
714
+ }
715
+ function copyDerivedSnapshot(snapshot, paths = Object.keys(snapshot)) {
716
+ return Object.freeze(Object.fromEntries(paths.map((path) => [path, cloneDerivedScalar(snapshot[path])])));
717
+ }
718
+
719
+ // src/plugins/derived-fields/lifecycle.ts
720
+ var watchedPaths = (plan) => [
721
+ ...new Set(plan.fields.flatMap((field) => field.mode === "always" ? plan.availablePaths : field.dependsOn))
722
+ ];
723
+ function derivedFieldsPlugin(schema) {
724
+ const registration = registerDerivedFields(schema);
725
+ const trackedPaths = [...registration.inputPaths, ...registration.fields.map((field) => field.targetPath)];
726
+ const states = /* @__PURE__ */ new WeakMap();
727
+ const stateFor = (document) => {
728
+ let state = states.get(document);
729
+ if (!state) {
730
+ state = { replacedPaths: /* @__PURE__ */ new Set(), assignedPaths: /* @__PURE__ */ new Set(), applyingResults: false };
731
+ states.set(document, state);
732
+ }
733
+ return state;
734
+ };
735
+ for (const path of trackedPaths) {
736
+ schema.path(path).set(function derivedFieldsObserveAssignment(value, _priorValue, _schemaType, options) {
737
+ if (this instanceof Document && !this.isNew && !options?._skipMarkModified && !isFullySelected(this, path)) {
738
+ const state = stateFor(this);
739
+ if (!state.applyingResults) state.assignedPaths.add(path);
740
+ }
741
+ return value;
742
+ });
743
+ }
744
+ const planFor = (document, state) => {
745
+ for (const path of state.assignedPaths) {
746
+ if (!document.$__.validationError?.errors[path] && !document.$__.primitiveAtomics?.[path]) {
747
+ document.markModified(path);
748
+ }
749
+ }
750
+ state.assignedPaths.clear();
751
+ const pendingWritePaths = new Set(
752
+ document.isNew ? [] : trackedPaths.filter(
753
+ (path) => path !== "_id" && document.$isDefault(path) && document.$__.selected?.[path] !== 0 && document.get(path, void 0, { getters: false }) != null
754
+ )
755
+ );
756
+ const plan = planDerivedFields(registration, document, {
757
+ knownReplacedPaths: state.replacedPaths,
758
+ pendingWritePaths
759
+ });
760
+ state.replacedPaths = new Set(document.isNew ? plan.availablePaths : plan.replacedPaths);
761
+ return plan;
762
+ };
763
+ 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
764
+ // flag. It was still computed; only newly scheduled fields invalidate reuse.
765
+ plan.fields.every((field) => checkpoint.targets.includes(field.targetPath)) && isDeepStrictEqual(checkpoint.inputs, snapshotDerivedInputs(document, Object.keys(checkpoint.inputs))) && isDeepStrictEqual(checkpoint.results, snapshotDerivedInputs(document, checkpoint.targets));
766
+ const compute = async (document) => {
767
+ const state = stateFor(document);
768
+ state.checkpoint = void 0;
769
+ const plan = planFor(document, state);
770
+ const inputs = snapshotDerivedInputs(document, plan.availablePaths);
771
+ const staged = [];
772
+ for (const { targetPath, get } of plan.fields) {
773
+ const result = await get(copyDerivedSnapshot(inputs));
774
+ staged.push({ path: targetPath, value: cloneDerivedScalar(result) });
775
+ }
776
+ state.applyingResults = true;
777
+ try {
778
+ for (const { path, value } of staged) document.set(path, value);
779
+ } finally {
780
+ state.applyingResults = false;
781
+ }
782
+ const validationError = document.$__.validationError;
783
+ if (staged.some(({ path }) => validationError?.errors[path]?.name === "CastError")) throw validationError;
784
+ const targets = plan.fields.map((field) => field.targetPath);
785
+ state.checkpoint = {
786
+ saveOptions: document.$__.saveOptions,
787
+ isNew: document.isNew,
788
+ targets,
789
+ inputs: copyDerivedSnapshot(inputs, watchedPaths(plan)),
790
+ results: snapshotDerivedInputs(document, targets)
791
+ };
792
+ };
793
+ schema.pre("validate", async function derivedFieldsPreValidate() {
794
+ await compute(this);
795
+ });
796
+ schema.pre("save", async function derivedFieldsPreSave() {
797
+ const state = stateFor(this);
798
+ const plan = planFor(this, state);
799
+ if (this.$__.saveOptions && matches(this, plan, state.checkpoint)) return;
800
+ const options = this.$__.saveOptions;
801
+ const validate = options && "validateBeforeSave" in options ? !!options.validateBeforeSave : schema.options.validateBeforeSave;
802
+ if (validate) {
803
+ await this.validate(
804
+ void 0,
805
+ options && "validateModifiedOnly" in options ? { validateModifiedOnly: options.validateModifiedOnly } : void 0
806
+ );
807
+ } else {
808
+ await compute(this);
809
+ }
810
+ if (!matches(this, planFor(this, state), state.checkpoint)) {
811
+ throw new Error(
812
+ "derivedFieldsPlugin inputs or targets changed after computation; register source-mutating validation hooks before the plugin and keep getters/validators free of document mutations"
813
+ );
814
+ }
815
+ });
816
+ }
817
+
483
818
  // src/plugins/new-document.ts
484
819
  var stateKey = "newDocumentPlugin";
485
820
  function newDocumentPlugin(schema, options) {
@@ -493,7 +828,10 @@ function newDocumentPlugin(schema, options) {
493
828
  });
494
829
  }
495
830
  export {
831
+ DerivedFieldsDependencyError,
496
832
  cascadeDeletePlugin,
833
+ defineDerivedField,
834
+ derivedFieldsPlugin,
497
835
  modelFunctionPlugin,
498
836
  newDocumentPlugin
499
837
  };