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