@shirudo/ddd-kit 3.0.0-rc.5 → 3.0.0-rc.7

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.
@@ -703,6 +703,37 @@ function shadowMutators(obj, typeName, methods) {
703
703
  }
704
704
  return true;
705
705
  }
706
+ const VALUE_OBJECT_CLASS = Symbol.for("@shirudo/ddd-kit/value-object-class/v1");
707
+ function recordValueObjectClass(instance, valueObjectClass) {
708
+ Object.defineProperty(instance, VALUE_OBJECT_CLASS, {
709
+ value: valueObjectClass,
710
+ enumerable: false,
711
+ writable: false,
712
+ configurable: false
713
+ });
714
+ }
715
+ function isValueObjectInstance(value) {
716
+ if (value === null || typeof value !== "object") return false;
717
+ const record = Reflect.getOwnPropertyDescriptor(value, VALUE_OBJECT_CLASS);
718
+ if (record === void 0 || typeof record.value !== "function" || record.enumerable !== false || record.writable !== false || record.configurable !== false) return false;
719
+ const props = Reflect.getOwnPropertyDescriptor(value, "props");
720
+ return props !== void 0 && isSealedProps(props.value);
721
+ }
722
+ function isSealedProps(props) {
723
+ if (typeof props !== "object" || props === null) return false;
724
+ return Object.isFrozen(props) || builtInTagWithoutInvokingAccessors(props) === "[object RegExp]";
725
+ }
726
+ function looksLikeValueObject(instance) {
727
+ if (Object.hasOwn(instance, VALUE_OBJECT_CLASS)) return true;
728
+ const props = Reflect.getOwnPropertyDescriptor(instance, "props");
729
+ return props !== void 0 && isSealedProps(props.value);
730
+ }
731
+ function openValueObjectKeys(instance) {
732
+ return Reflect.ownKeys(instance).filter((key) => key !== "props" && key !== VALUE_OBJECT_CLASS);
733
+ }
734
+ function rejectValueObjectAsInput(input, entry) {
735
+ if (isValueObjectInstance(input)) throw new TypeError(`${entry} does not accept a value object as its input: nest the value object under a key, or pass its props`);
736
+ }
706
737
  /**
707
738
  * Deep freezes an object and all its nested properties recursively, then
708
739
  * returns it. Iterates both string-keyed and symbol-keyed own properties
@@ -809,11 +840,12 @@ function freezeDeep(obj, walk) {
809
840
  * `deepEqual` DOES consider symbol keys) and shared references / cycles
810
841
  * keep their identity across Map boundaries. Function values throw,
811
842
  * preserving `vo()`'s documented data-not-behaviour gate. Built-ins without
812
- * immutable value semantics throw a descriptive `TypeError`. Custom class
813
- * instances and subclasses of built-ins are rejected because cloning them
814
- * without invoking their
815
- * constructor can silently lose private or non-enumerable state. Map keys
816
- * and Set members must be primitive because their equality is
843
+ * immutable value semantics throw a descriptive `TypeError`. A kit
844
+ * `ValueObject` instance is admitted by reference (see
845
+ * `VALUE_OBJECT_CLASS`). Every other custom class instance and every
846
+ * subclass of a built-in is rejected because cloning it without invoking
847
+ * its constructor can silently lose private or non-enumerable state. Map
848
+ * keys and Set members must be primitive because their equality is
817
849
  * identity-based and object identity cannot survive defensive cloning.
818
850
  * Accessor properties are rejected without invoking them. Admitted atomic
819
851
  * built-ins (Date, RegExp and primitive wrappers) delegate to
@@ -828,7 +860,7 @@ function cloneForVo(value, visited) {
828
860
  if (ArrayBuffer.isView(obj)) throwUnsupportedValueSemantics(builtInTagWithoutInvokingAccessors(obj) ?? "[object ArrayBuffer view]");
829
861
  if (visited.has(obj)) return visited.get(obj);
830
862
  if (Array.isArray(obj)) {
831
- if (!hasIntrinsicPrototypeChain(obj, "Array")) throwUnsupportedClassInstance();
863
+ if (!hasIntrinsicPrototypeChain(obj, "Array")) throwUnsupportedClassInstance(obj);
832
864
  const clone = new Array(obj.length);
833
865
  visited.set(obj, clone);
834
866
  for (const key of Reflect.ownKeys(obj)) {
@@ -844,7 +876,7 @@ function cloneForVo(value, visited) {
844
876
  }
845
877
  const tag = builtInTagWithoutInvokingAccessors(obj);
846
878
  if (tag !== void 0) {
847
- if (!hasIntrinsicPrototypeChain(obj)) throwUnsupportedClassInstance();
879
+ if (!hasIntrinsicPrototypeChain(obj)) throwUnsupportedClassInstance(obj);
848
880
  if (tag === "[object Map]") {
849
881
  const clone = /* @__PURE__ */ new Map();
850
882
  visited.set(obj, clone);
@@ -874,7 +906,14 @@ function cloneForVo(value, visited) {
874
906
  return builtInClone;
875
907
  }
876
908
  const prototype = Object.getPrototypeOf(obj);
877
- if (prototype !== null && (!isIntrinsicConstructorPrototype(prototype, "Object") || Object.getPrototypeOf(prototype) !== null)) throwUnsupportedClassInstance();
909
+ if (prototype !== null && (!isIntrinsicConstructorPrototype(prototype, "Object") || Object.getPrototypeOf(prototype) !== null)) {
910
+ if (isValueObjectInstance(obj)) {
911
+ const openKeys = openValueObjectKeys(obj);
912
+ if (openKeys.length > 0) throwOpenValueObjectFields(openKeys);
913
+ return obj;
914
+ }
915
+ throwUnsupportedClassInstance(obj);
916
+ }
878
917
  const clone = Object.create(prototype === null ? null : Object.prototype);
879
918
  visited.set(obj, clone);
880
919
  for (const key of Reflect.ownKeys(obj)) {
@@ -891,8 +930,12 @@ function cloneForVo(value, visited) {
891
930
  }
892
931
  return clone;
893
932
  }
894
- function throwUnsupportedClassInstance() {
895
- throw new TypeError("vo() cannot clone custom class instances: Value Objects are plain data");
933
+ function throwUnsupportedClassInstance(instance) {
934
+ const valueObjectHint = looksLikeValueObject(instance) ? ". A value object is recognized only when a copy of this kit version built it and its props are frozen" : "";
935
+ throw new TypeError(`vo() cannot clone custom class instances: Value Objects are plain data${valueObjectHint}`);
936
+ }
937
+ function throwOpenValueObjectFields(keys) {
938
+ throw new TypeError(`vo() cannot nest a value object with own fields outside props (${keys.map(String).join(", ")}): keep the state of a value object in props`);
896
939
  }
897
940
  function throwUnsupportedAccessorProperty() {
898
941
  throw new TypeError("vo() cannot clone accessor properties: Value Objects are plain data");
@@ -910,9 +953,12 @@ function isPrimitiveValue(value) {
910
953
  * The input is first deep-cloned, then the clone is frozen, so calling
911
954
  * `vo(input)` never freezes the caller's own object graph as a
912
955
  * side-effect. Mutating the input afterwards does not bleed into the VO.
913
- * Symbol-keyed properties are preserved (matching `voEquals`); function
914
- * values and custom class instances are rejected (Value Objects are plain
915
- * data, not behaviour-bearing object graphs). Inputs must be trusted and
956
+ * Symbol-keyed properties are preserved (matching `voEquals`). A kit
957
+ * `ValueObject` instance nested in the input is kept by reference and
958
+ * frozen in place; it must keep all of its state in `props`. A value
959
+ * object as the input itself is rejected.
960
+ * Function values and every other custom class instance are rejected
961
+ * (Value Objects are plain data, not behaviour-bearing object graphs). Inputs must be trusted and
916
962
  * Proxy-free: ECMAScript provides no portable way to identify a transparent
917
963
  * Proxy without potentially executing its traps, so `vo()` is not a sandbox
918
964
  * for hostile in-process objects. Built-ins that cannot provide immutable,
@@ -927,6 +973,7 @@ function isPrimitiveValue(value) {
927
973
  * ```
928
974
  */
929
975
  function vo(t) {
976
+ rejectValueObjectAsInput(t, "vo()");
930
977
  return deepFreeze(cloneForVo(t, /* @__PURE__ */ new WeakMap()));
931
978
  }
932
979
  /**
@@ -967,6 +1014,11 @@ function voEquals(a, b) {
967
1014
  * Useful for comparing value objects that contain metadata or optional fields
968
1015
  * that should not affect equality comparison.
969
1016
  *
1017
+ * The walk enters a nested `ValueObject` instance like any other object,
1018
+ * so inside it the path continues with `props`; `ignoreKeys: ["props"]`
1019
+ * empties every nested value object. The key under which the kit records
1020
+ * the class of the instance is never ignored.
1021
+ *
970
1022
  * @param a - First value object
971
1023
  * @param b - Second value object
972
1024
  * @param options - Options specifying which keys to ignore during comparison
@@ -1000,7 +1052,15 @@ function voEquals(a, b) {
1000
1052
  * ```
1001
1053
  */
1002
1054
  function voEqualsExcept(a, b, options) {
1003
- return deepEqualExcept(a, b, options);
1055
+ return deepEqualExcept(a, b, keepValueObjectClass(options));
1056
+ }
1057
+ function keepValueObjectClass(options) {
1058
+ const { ignoreKeys, ignoreKeyPredicate } = options;
1059
+ return {
1060
+ ...options,
1061
+ ignoreKeys: ignoreKeys?.filter((key) => key !== VALUE_OBJECT_CLASS),
1062
+ ignoreKeyPredicate: ignoreKeyPredicate && ((key, path) => key !== VALUE_OBJECT_CLASS && ignoreKeyPredicate(key, path))
1063
+ };
1004
1064
  }
1005
1065
  /**
1006
1066
  * Creates a value object with optional validation.
@@ -1049,7 +1109,11 @@ function describeValue(value) {
1049
1109
  }
1050
1110
  /**
1051
1111
  * Abstract base class for creating Value Objects.
1052
- * Value Objects are immutable and defined by their properties.
1112
+ * Value Objects are immutable and defined by their properties. A value
1113
+ * object can hold other value objects in its props. Every instance
1114
+ * records its class under an own symbol key, so `equals` compares a
1115
+ * nested value object by class and props and does not call its `equals`
1116
+ * method.
1053
1117
  *
1054
1118
  * @template T - The shape of the value object's properties
1055
1119
  */
@@ -1076,8 +1140,10 @@ var ValueObject = class {
1076
1140
  * ```
1077
1141
  */
1078
1142
  constructor(props) {
1143
+ rejectValueObjectAsInput(props, "new ValueObject()");
1079
1144
  this.validate(props);
1080
1145
  this.props = deepFreeze(cloneForVo(props, /* @__PURE__ */ new WeakMap()));
1146
+ recordValueObjectClass(this, this.constructor);
1081
1147
  }
1082
1148
  /**
1083
1149
  * Optional validation hook that can be overridden by subclasses.
@@ -1621,5 +1687,5 @@ function isDispatchTrackingOutbox(outbox) {
1621
1687
  }
1622
1688
 
1623
1689
  //#endregion
1624
- export { isWeakMap as A, voWithValidation as C, findPropertyDescriptor as D, deepEqual as E, isBuiltInObject as O, voEqualsExcept as S, deepOmit as T, stampCooperativeBrand as _, createDomainEvent as a, vo as b, createUncommittedDomainEvent as c, isUncommittedDomainEvent as d, mergeMetadata as f, hasCooperativeBrand as g, SnapshotTimeValidationError as h, copyMetadata as i, isIntrinsicConstructorPrototype as k, defaultDomainEventFactory as l, DomainEventValidationError as m, adoptRecordedDomainEvent as n, createDomainEventFactory as o, recordDomainEvent as p, adoptUncommittedDomainEvent as r, createDomainEventFromFacts as s, isDispatchTrackingOutbox as t, isRecordedDomainEvent as u, ValueObject as v, deepEqualExcept as w, voEquals as x, deepFreeze as y };
1690
+ export { isBuiltInObject as A, voWithValidation as C, builtInTagWithoutInvokingAccessors as D, deepEqual as E, isWeakMap as M, findPropertyDescriptor as O, voEqualsExcept as S, deepOmit as T, stampCooperativeBrand as _, createDomainEvent as a, vo as b, createUncommittedDomainEvent as c, isUncommittedDomainEvent as d, mergeMetadata as f, hasCooperativeBrand as g, SnapshotTimeValidationError as h, copyMetadata as i, isIntrinsicConstructorPrototype as j, hasIntrinsicPrototypeChain as k, defaultDomainEventFactory as l, DomainEventValidationError as m, adoptRecordedDomainEvent as n, createDomainEventFactory as o, recordDomainEvent as p, adoptUncommittedDomainEvent as r, createDomainEventFromFacts as s, isDispatchTrackingOutbox as t, isRecordedDomainEvent as u, ValueObject as v, deepEqualExcept as w, voEquals as x, deepFreeze as y };
1625
1691
  //# sourceMappingURL=ports.js.map