@woltz/rich-domain 1.9.2 → 1.9.3

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 (122) hide show
  1. package/dist/cjs/constants.d.ts +1 -1
  2. package/dist/cjs/constants.d.ts.map +1 -1
  3. package/dist/cjs/core/base-entity.d.ts +2 -1
  4. package/dist/cjs/core/base-entity.d.ts.map +1 -1
  5. package/dist/cjs/core/base-entity.js +9 -7
  6. package/dist/cjs/core/base-entity.js.map +1 -1
  7. package/dist/cjs/core/change-tracker.d.ts +1 -0
  8. package/dist/cjs/core/change-tracker.d.ts.map +1 -1
  9. package/dist/cjs/core/change-tracker.js +24 -15
  10. package/dist/cjs/core/change-tracker.js.map +1 -1
  11. package/dist/cjs/core/domain-event.d.ts +1 -1
  12. package/dist/cjs/core/domain-event.d.ts.map +1 -1
  13. package/dist/cjs/core/domain-event.js +2 -2
  14. package/dist/cjs/core/domain-event.js.map +1 -1
  15. package/dist/cjs/core/entities.d.ts +8 -0
  16. package/dist/cjs/core/entities.d.ts.map +1 -0
  17. package/dist/cjs/core/entities.js +12 -0
  18. package/dist/cjs/core/entities.js.map +1 -0
  19. package/dist/cjs/core/index.d.ts +9 -9
  20. package/dist/cjs/core/index.d.ts.map +1 -1
  21. package/dist/cjs/core/index.js +9 -9
  22. package/dist/cjs/core/index.js.map +1 -1
  23. package/dist/cjs/repository/base-repository.d.ts +1 -1
  24. package/dist/cjs/repository/base-repository.d.ts.map +1 -1
  25. package/dist/cjs/repository/entity-schema-registry.d.ts +1 -1
  26. package/dist/cjs/repository/entity-schema-registry.d.ts.map +1 -1
  27. package/dist/cjs/repository/entity-schema-registry.js +5 -5
  28. package/dist/cjs/repository/entity-schema-registry.js.map +1 -1
  29. package/dist/cjs/types/change-tracker.d.ts +1 -1
  30. package/dist/cjs/types/change-tracker.d.ts.map +1 -1
  31. package/dist/cjs/types/domain.d.ts +1 -1
  32. package/dist/cjs/types/domain.d.ts.map +1 -1
  33. package/dist/cjs/types/event-bus.d.ts +1 -1
  34. package/dist/cjs/types/event-bus.d.ts.map +1 -1
  35. package/dist/cjs/types/unit-of-work.d.ts +2 -2
  36. package/dist/cjs/types/unit-of-work.d.ts.map +1 -1
  37. package/dist/cjs/types/utils.d.ts +1 -1
  38. package/dist/cjs/types/utils.d.ts.map +1 -1
  39. package/dist/esm/constants.d.ts +1 -1
  40. package/dist/esm/constants.d.ts.map +1 -1
  41. package/dist/esm/core/base-entity.d.ts +2 -1
  42. package/dist/esm/core/base-entity.d.ts.map +1 -1
  43. package/dist/esm/core/base-entity.js +3 -1
  44. package/dist/esm/core/base-entity.js.map +1 -1
  45. package/dist/esm/core/change-tracker.d.ts +1 -0
  46. package/dist/esm/core/change-tracker.d.ts.map +1 -1
  47. package/dist/esm/core/change-tracker.js +24 -15
  48. package/dist/esm/core/change-tracker.js.map +1 -1
  49. package/dist/esm/core/domain-event.d.ts +1 -1
  50. package/dist/esm/core/domain-event.d.ts.map +1 -1
  51. package/dist/esm/core/domain-event.js +1 -1
  52. package/dist/esm/core/domain-event.js.map +1 -1
  53. package/dist/esm/core/entities.d.ts +8 -0
  54. package/dist/esm/core/entities.d.ts.map +1 -0
  55. package/dist/esm/core/entities.js +7 -0
  56. package/dist/esm/core/entities.js.map +1 -0
  57. package/dist/esm/core/index.d.ts +9 -9
  58. package/dist/esm/core/index.d.ts.map +1 -1
  59. package/dist/esm/core/index.js +9 -9
  60. package/dist/esm/core/index.js.map +1 -1
  61. package/dist/esm/repository/base-repository.d.ts +1 -1
  62. package/dist/esm/repository/base-repository.d.ts.map +1 -1
  63. package/dist/esm/repository/entity-schema-registry.d.ts +1 -1
  64. package/dist/esm/repository/entity-schema-registry.d.ts.map +1 -1
  65. package/dist/esm/repository/entity-schema-registry.js +1 -1
  66. package/dist/esm/repository/entity-schema-registry.js.map +1 -1
  67. package/dist/esm/types/change-tracker.d.ts +1 -1
  68. package/dist/esm/types/change-tracker.d.ts.map +1 -1
  69. package/dist/esm/types/domain.d.ts +1 -1
  70. package/dist/esm/types/domain.d.ts.map +1 -1
  71. package/dist/esm/types/event-bus.d.ts +1 -1
  72. package/dist/esm/types/event-bus.d.ts.map +1 -1
  73. package/dist/esm/types/unit-of-work.d.ts +2 -2
  74. package/dist/esm/types/unit-of-work.d.ts.map +1 -1
  75. package/dist/esm/types/utils.d.ts +1 -1
  76. package/dist/esm/types/utils.d.ts.map +1 -1
  77. package/dist/tsconfig.cjs.tsbuildinfo +1 -1
  78. package/dist/tsconfig.esm.tsbuildinfo +1 -1
  79. package/dist/tsconfig.types.tsbuildinfo +1 -1
  80. package/dist/types/constants.d.ts +1 -1
  81. package/dist/types/constants.d.ts.map +1 -1
  82. package/dist/types/core/base-entity.d.ts +2 -1
  83. package/dist/types/core/base-entity.d.ts.map +1 -1
  84. package/dist/types/core/change-tracker.d.ts +1 -0
  85. package/dist/types/core/change-tracker.d.ts.map +1 -1
  86. package/dist/types/core/domain-event.d.ts +1 -1
  87. package/dist/types/core/domain-event.d.ts.map +1 -1
  88. package/dist/types/core/entities.d.ts +8 -0
  89. package/dist/types/core/entities.d.ts.map +1 -0
  90. package/dist/types/core/index.d.ts +9 -9
  91. package/dist/types/core/index.d.ts.map +1 -1
  92. package/dist/types/repository/base-repository.d.ts +1 -1
  93. package/dist/types/repository/base-repository.d.ts.map +1 -1
  94. package/dist/types/repository/entity-schema-registry.d.ts +1 -1
  95. package/dist/types/repository/entity-schema-registry.d.ts.map +1 -1
  96. package/dist/types/types/change-tracker.d.ts +1 -1
  97. package/dist/types/types/change-tracker.d.ts.map +1 -1
  98. package/dist/types/types/domain.d.ts +1 -1
  99. package/dist/types/types/domain.d.ts.map +1 -1
  100. package/dist/types/types/event-bus.d.ts +1 -1
  101. package/dist/types/types/event-bus.d.ts.map +1 -1
  102. package/dist/types/types/unit-of-work.d.ts +2 -2
  103. package/dist/types/types/unit-of-work.d.ts.map +1 -1
  104. package/dist/types/types/utils.d.ts +1 -1
  105. package/dist/types/types/utils.d.ts.map +1 -1
  106. package/package.json +68 -68
  107. package/src/constants.ts +82 -82
  108. package/src/core/aggregate-changes.ts +466 -466
  109. package/src/core/base-entity.ts +4 -1
  110. package/src/core/change-tracker.ts +30 -16
  111. package/src/core/domain-event.ts +41 -41
  112. package/src/core/{entity.ts → entities.ts} +13 -13
  113. package/src/core/index.ts +9 -9
  114. package/src/core/value-object.ts +179 -179
  115. package/src/repository/base-repository.ts +81 -81
  116. package/src/repository/entity-schema-registry.ts +1 -1
  117. package/src/types/change-tracker.ts +268 -268
  118. package/src/types/domain.ts +41 -41
  119. package/src/types/event-bus.ts +17 -17
  120. package/src/types/unit-of-work.ts +46 -46
  121. package/src/types/utils.ts +24 -24
  122. package/src/utils/helpers.ts +50 -50
@@ -14,7 +14,10 @@ import {
14
14
  } from "../types/index.js";
15
15
  import { DEFAULT_VALIDATION_CONFIG } from "../constants.js";
16
16
  import { DomainError } from "../exceptions.js";
17
- import { ChangeTracker, AggregateChanges, Id, ValueObject } from "./index";
17
+ import { AggregateChanges } from "./aggregate-changes.js";
18
+ import { ChangeTracker } from "./change-tracker.js";
19
+ import { Id } from "./id.js";
20
+ import { ValueObject } from "./value-object.js";
18
21
  import { getStaticProperty } from "../utils/helpers.js";
19
22
 
20
23
  export abstract class BaseEntity<
@@ -1,5 +1,5 @@
1
1
  import { Id } from "./id.js";
2
- import { Entity } from "./entity.js";
2
+ import { BaseEntity } from "./base-entity.js";
3
3
  import { ValueObject } from "./value-object.js";
4
4
  import { ArrayState, HistoryEntry, TrackedItem } from "../types/index.js";
5
5
  import { EntityChangeState } from "../types/change-tracker.js";
@@ -97,7 +97,7 @@ export class ChangeTracker {
97
97
 
98
98
  if (Array.isArray(value)) {
99
99
  this.captureArrayState(value, propPath, depth + 1, id, entityName);
100
- } else if (value instanceof Entity) {
100
+ } else if (value instanceof BaseEntity) {
101
101
  const nestedName = this.getEntityName(value);
102
102
  this.captureEntityState(
103
103
  value,
@@ -137,7 +137,7 @@ export class ChangeTracker {
137
137
  // Only track individual items for non-primitive arrays
138
138
  if (!isPrimitive) {
139
139
  arr.forEach((item, index) => {
140
- if (item instanceof Entity) {
140
+ if (item instanceof BaseEntity) {
141
141
  const itemPath = `${path}[${index}]`;
142
142
  this.captureEntityState(
143
143
  item,
@@ -175,7 +175,7 @@ export class ChangeTracker {
175
175
  return this.createArrayProxy(value, currentPath);
176
176
  }
177
177
 
178
- if (value instanceof Entity) {
178
+ if (value instanceof BaseEntity) {
179
179
  const nestedTracker = new ChangeTracker(
180
180
  value,
181
181
  this.getEntityName(value),
@@ -230,7 +230,10 @@ export class ChangeTracker {
230
230
 
231
231
  if (Array.isArray(newValue)) {
232
232
  this.handleArrayAssignment(currentPath, oldValue);
233
- } else if (newValue instanceof Entity || oldValue instanceof Entity) {
233
+ } else if (
234
+ (newValue instanceof BaseEntity && !this.isSelfReference(newValue)) ||
235
+ (oldValue instanceof BaseEntity && !this.isSelfReference(oldValue))
236
+ ) {
234
237
  this.handleEntityChange(currentPath, oldValue, newValue);
235
238
  }
236
239
 
@@ -277,7 +280,7 @@ export class ChangeTracker {
277
280
 
278
281
  const result = Reflect.deleteProperty(target, prop);
279
282
 
280
- if (oldValue instanceof Entity) {
283
+ if (oldValue instanceof BaseEntity && !this.isSelfReference(oldValue)) {
281
284
  this.handleEntityChange(currentPath, oldValue, undefined);
282
285
  }
283
286
 
@@ -359,7 +362,7 @@ export class ChangeTracker {
359
362
  return value.bind(target);
360
363
  }
361
364
 
362
- if (!isNaN(Number(prop)) && value instanceof Entity) {
365
+ if (!isNaN(Number(prop)) && value instanceof BaseEntity) {
363
366
  const nestedPath = `${path}[${String(prop)}]`;
364
367
  const nestedTracker = new ChangeTracker(
365
368
  value,
@@ -454,7 +457,12 @@ export class ChangeTracker {
454
457
 
455
458
  // 1:1 entity relations are tracked by analyzeEntityChanges
456
459
  if (rootTracker.trackedEntities.has(path)) continue;
457
- if (originalValue instanceof Entity || currentValue instanceof Entity) {
460
+ if (
461
+ (originalValue instanceof BaseEntity &&
462
+ !this.isSelfReference(originalValue)) ||
463
+ (currentValue instanceof BaseEntity &&
464
+ !this.isSelfReference(currentValue))
465
+ ) {
458
466
  continue;
459
467
  }
460
468
 
@@ -605,7 +613,7 @@ export class ChangeTracker {
605
613
  const relationField = propName;
606
614
 
607
615
  for (const child of value) {
608
- if (child instanceof Entity) {
616
+ if (child instanceof BaseEntity) {
609
617
  const childEntityName = this.getEntityName(child);
610
618
  changes.addCreate(
611
619
  childEntityName,
@@ -618,7 +626,7 @@ export class ChangeTracker {
618
626
  this.markNestedItemsAsCreated(child, parentDepth + 1, changes);
619
627
  }
620
628
  }
621
- } else if (value instanceof Entity) {
629
+ } else if (value instanceof BaseEntity) {
622
630
  const childEntityName = this.getEntityName(value);
623
631
  changes.addCreate(
624
632
  childEntityName,
@@ -667,8 +675,8 @@ export class ChangeTracker {
667
675
  nestedItem,
668
676
  parentDepth + 1,
669
677
  relationField,
670
- parentEntity,
671
- parentId
678
+ parentId,
679
+ parentEntity
672
680
  );
673
681
 
674
682
  this.markNestedJsonItemAsDeleted(
@@ -753,7 +761,7 @@ export class ChangeTracker {
753
761
  originalArray: any[]
754
762
  ): string | undefined {
755
763
  for (const originalItem of originalArray) {
756
- if (originalItem instanceof Entity) {
764
+ if (originalItem instanceof BaseEntity) {
757
765
  const originalJson = this.deepClone(originalItem);
758
766
  if (JSON.stringify(originalJson) === JSON.stringify(jsonItem)) {
759
767
  const key = this.getItemKey(originalItem);
@@ -783,7 +791,7 @@ export class ChangeTracker {
783
791
 
784
792
  if (Array.isArray(value)) {
785
793
  value.forEach((item, index) => {
786
- if (item instanceof Entity) {
794
+ if (item instanceof BaseEntity) {
787
795
  this.collectNestedArrays(
788
796
  item,
789
797
  `${propPath}[${index}]`,
@@ -792,7 +800,7 @@ export class ChangeTracker {
792
800
  );
793
801
  }
794
802
  });
795
- } else if (value instanceof Entity) {
803
+ } else if (value instanceof BaseEntity) {
796
804
  this.collectNestedArrays(value, propPath, allArrays, processedArrays);
797
805
  }
798
806
  }
@@ -1000,7 +1008,7 @@ export class ChangeTracker {
1000
1008
  continue;
1001
1009
  }
1002
1010
 
1003
- if (currValue instanceof Entity) {
1011
+ if (currValue instanceof BaseEntity) {
1004
1012
  continue;
1005
1013
  }
1006
1014
 
@@ -1053,6 +1061,12 @@ export class ChangeTracker {
1053
1061
  return this.rootTracker || this;
1054
1062
  }
1055
1063
 
1064
+ private isSelfReference(value: BaseEntity<any>): boolean {
1065
+ const targetId = this.getEntityId(this.target);
1066
+ const valueId = this.getEntityId(value);
1067
+ return !!targetId && !!valueId && targetId === valueId;
1068
+ }
1069
+
1056
1070
  private buildPath(prop: string): string {
1057
1071
  return this.path ? `${this.path}.${prop}` : prop;
1058
1072
  }
@@ -1,41 +1,41 @@
1
- import { IDomainEvent } from "..";
2
- import UUID from "../utils/crypto";
3
-
4
- /**
5
- * Base class for domain events
6
- */
7
- export abstract class DomainEvent<P> implements IDomainEvent<P> {
8
- public readonly eventId: string;
9
- public readonly occurredOn: Date;
10
- public readonly payload: P;
11
- static readonly queueName?: string;
12
-
13
- constructor(payload: P) {
14
- this.eventId = this.generateEventId();
15
- this.occurredOn = new Date();
16
- this.payload = payload;
17
- }
18
-
19
- /**
20
- * Get the event name (defaults to class name)
21
- */
22
- get eventName(): string {
23
- return this.constructor.name;
24
- }
25
-
26
- /**
27
- * Generate a UUID v4
28
- */
29
- private generateEventId(): string {
30
- return UUID();
31
- }
32
-
33
- toJSON() {
34
- return {
35
- eventId: this.eventId,
36
- eventName: this.eventName,
37
- occurredOn: this.occurredOn.toISOString(),
38
- payload: this.payload,
39
- };
40
- }
41
- }
1
+ import type { IDomainEvent } from "../types/domain-event.js";
2
+ import UUID from "../utils/crypto.js";
3
+
4
+ /**
5
+ * Base class for domain events
6
+ */
7
+ export abstract class DomainEvent<P> implements IDomainEvent<P> {
8
+ public readonly eventId: string;
9
+ public readonly occurredOn: Date;
10
+ public readonly payload: P;
11
+ static readonly queueName?: string;
12
+
13
+ constructor(payload: P) {
14
+ this.eventId = this.generateEventId();
15
+ this.occurredOn = new Date();
16
+ this.payload = payload;
17
+ }
18
+
19
+ /**
20
+ * Get the event name (defaults to class name)
21
+ */
22
+ get eventName(): string {
23
+ return this.constructor.name;
24
+ }
25
+
26
+ /**
27
+ * Generate a UUID v4
28
+ */
29
+ private generateEventId(): string {
30
+ return UUID();
31
+ }
32
+
33
+ toJSON() {
34
+ return {
35
+ eventId: this.eventId,
36
+ eventName: this.eventName,
37
+ occurredOn: this.occurredOn.toISOString(),
38
+ payload: this.payload,
39
+ };
40
+ }
41
+ }
@@ -1,13 +1,13 @@
1
- import { BaseEntity } from "./base-entity.js";
2
- import { BaseAggregate } from "./base-aggregate.js";
3
- import { BaseProps } from "../types/index.js";
4
-
5
- export class Entity<
6
- T extends BaseProps,
7
- TOptionalInput extends keyof T = never,
8
- > extends BaseEntity<T, TOptionalInput> {}
9
-
10
- export class Aggregate<
11
- T extends BaseProps,
12
- TOptionalInput extends keyof T = never,
13
- > extends BaseAggregate<T, TOptionalInput> {}
1
+ import { BaseEntity } from "./base-entity.js";
2
+ import { BaseAggregate } from "./base-aggregate.js";
3
+ import { BaseProps } from "../types/index.js";
4
+
5
+ export class Entity<
6
+ T extends BaseProps,
7
+ TOptionalInput extends keyof T = never,
8
+ > extends BaseEntity<T, TOptionalInput> {}
9
+
10
+ export class Aggregate<
11
+ T extends BaseProps,
12
+ TOptionalInput extends keyof T = never,
13
+ > extends BaseAggregate<T, TOptionalInput> {}
package/src/core/index.ts CHANGED
@@ -1,9 +1,9 @@
1
- export * from "./base-entity";
2
- export * from "./base-aggregate";
3
- export * from "./aggregate-changes";
4
- export * from "./change-tracker";
5
- export * from "./domain-event";
6
- export * from "./entity-changes";
7
- export * from "./entity";
8
- export * from "./id";
9
- export * from "./value-object";
1
+ export * from "./base-entity.js";
2
+ export * from "./base-aggregate.js";
3
+ export * from "./aggregate-changes.js";
4
+ export * from "./change-tracker.js";
5
+ export * from "./domain-event.js";
6
+ export * from "./entity-changes.js";
7
+ export * from "./entities.js";
8
+ export * from "./id.js";
9
+ export * from "./value-object.js";
@@ -1,179 +1,179 @@
1
- import {
2
- ValidationError,
3
- ValidationIssue,
4
- ValidationIssueCollector,
5
- } from "../validation-error.js";
6
- import {
7
- VOHooks,
8
- ValidationConfig,
9
- StandardSchema,
10
- EntityValidation,
11
- Primitive,
12
- } from "../types/index.js";
13
- import { DEFAULT_VALIDATION_CONFIG } from "../constants.js";
14
- import { DomainError } from "../exceptions.js";
15
- import { getStaticProperty } from "../utils/helpers.js";
16
-
17
- export abstract class ValueObject<T extends Primitive> {
18
- public readonly value!: T;
19
- private validationConfig: Required<ValidationConfig>;
20
- private domainHooks?: VOHooks<T, any>;
21
- private domainSchema?: StandardSchema<T>;
22
- private readonly issueCollector = new ValidationIssueCollector();
23
-
24
- protected static validation?: EntityValidation<any>;
25
- protected static hooks?: VOHooks<any, any>;
26
-
27
- constructor(value: T) {
28
- const validation = getStaticProperty<EntityValidation<T>>(
29
- this,
30
- "validation"
31
- );
32
- const hooks = getStaticProperty<VOHooks<T, any>>(this, "hooks");
33
-
34
- if (hooks?.onBeforeCreate) {
35
- hooks.onBeforeCreate(value);
36
- }
37
-
38
- this.domainHooks = hooks;
39
-
40
- if (validation?.schema) {
41
- this.domainSchema = validation.schema;
42
- }
43
-
44
- this.validationConfig = {
45
- ...DEFAULT_VALIDATION_CONFIG,
46
- ...validation?.config,
47
- };
48
-
49
- if (this.domainSchema && this.validationConfig.onCreate) {
50
- this.validateValue(value);
51
- }
52
-
53
- this.value = value;
54
-
55
- if (hooks?.rules) {
56
- this.runRulesHook();
57
- }
58
-
59
- Object.freeze(this);
60
- }
61
-
62
- /**
63
- * Add a validation issue during rules hook execution (non-throwing mode).
64
- */
65
- public addValidationIssue(path: string | string[], message: string): void {
66
- this.issueCollector.add(path, message);
67
- }
68
-
69
- private beginValidationCycle(): void {
70
- this.issueCollector.clear();
71
- }
72
-
73
- private finalizeValidation(collectedIssues: ValidationIssue[] = []): void {
74
- const existing = (this as any)._validationError as
75
- | ValidationError
76
- | undefined;
77
- const merged = ValidationError.merge(existing, collectedIssues);
78
-
79
- if (!merged) {
80
- delete (this as any)._validationError;
81
- return;
82
- }
83
-
84
- if (this.validationConfig.throwOnError) {
85
- throw merged;
86
- }
87
-
88
- (this as any)._validationError = merged;
89
- }
90
-
91
- private runRulesHook(): void {
92
- if (!this.domainHooks?.rules) return;
93
-
94
- this.beginValidationCycle();
95
- this.domainHooks.rules(this as any);
96
- this.finalizeValidation([...this.issueCollector.getIssues()]);
97
- }
98
-
99
- private validateValue(value: T): void {
100
- const schemaError = this.validateSchema(value);
101
- if (!schemaError) return;
102
-
103
- if (this.validationConfig.throwOnError) {
104
- throw schemaError;
105
- }
106
-
107
- (this as any)._validationError = schemaError;
108
- }
109
-
110
- private validateSchema(value: T): ValidationError | null {
111
- if (!this.domainSchema) return null;
112
-
113
- const result = this.domainSchema["~standard"].validate(value);
114
-
115
- if (result instanceof Promise) {
116
- throw new DomainError(
117
- "Async validation not supported in constructor. Use sync validation schema."
118
- );
119
- }
120
-
121
- if (result.issues && result.issues.length > 0) {
122
- return new ValidationError(
123
- result.issues.map((issue) => ({
124
- path: issue.path?.map((p) => this.extractPathKey(p)) || [],
125
- message: issue.message,
126
- }))
127
- );
128
- }
129
-
130
- return null;
131
- }
132
-
133
- private extractPathKey(pathSegment: unknown): string {
134
- if (pathSegment === null || pathSegment === undefined) {
135
- return "";
136
- }
137
- if (typeof pathSegment === "string" || typeof pathSegment === "number") {
138
- return String(pathSegment);
139
- }
140
- if (typeof pathSegment === "symbol") {
141
- return pathSegment.toString();
142
- }
143
- if (typeof pathSegment === "object" && "key" in pathSegment) {
144
- return String((pathSegment as { key: unknown }).key);
145
- }
146
- return String(pathSegment);
147
- }
148
-
149
- /**
150
- * Returns true if the value object has validation errors (when throwOnError is false).
151
- */
152
- get hasValidationErrors(): boolean {
153
- return !!(this as any)._validationError;
154
- }
155
-
156
- /**
157
- * Returns the validation errors (when throwOnError is false).
158
- */
159
- get validationErrors(): ValidationError | undefined {
160
- return (this as any)._validationError;
161
- }
162
-
163
- /**
164
- * Compare this ValueObject with another for equality based on their properties.
165
- */
166
- equals(other: ValueObject<T>): boolean {
167
- if (!other || !(other instanceof ValueObject)) return false;
168
- return this.value === other.value;
169
- }
170
-
171
- /**
172
- * Creates a new ValueObject with updated value.
173
- * ValueObjects are immutable, so this returns a new instance.
174
- */
175
- protected clone(value: T): this {
176
- const Constructor = this.constructor as new (value: T) => this;
177
- return new Constructor(value);
178
- }
179
- }
1
+ import {
2
+ ValidationError,
3
+ ValidationIssue,
4
+ ValidationIssueCollector,
5
+ } from "../validation-error.js";
6
+ import {
7
+ VOHooks,
8
+ ValidationConfig,
9
+ StandardSchema,
10
+ EntityValidation,
11
+ Primitive,
12
+ } from "../types/index.js";
13
+ import { DEFAULT_VALIDATION_CONFIG } from "../constants.js";
14
+ import { DomainError } from "../exceptions.js";
15
+ import { getStaticProperty } from "../utils/helpers.js";
16
+
17
+ export abstract class ValueObject<T extends Primitive> {
18
+ public readonly value!: T;
19
+ private validationConfig: Required<ValidationConfig>;
20
+ private domainHooks?: VOHooks<T, any>;
21
+ private domainSchema?: StandardSchema<T>;
22
+ private readonly issueCollector = new ValidationIssueCollector();
23
+
24
+ protected static validation?: EntityValidation<any>;
25
+ protected static hooks?: VOHooks<any, any>;
26
+
27
+ constructor(value: T) {
28
+ const validation = getStaticProperty<EntityValidation<T>>(
29
+ this,
30
+ "validation"
31
+ );
32
+ const hooks = getStaticProperty<VOHooks<T, any>>(this, "hooks");
33
+
34
+ if (hooks?.onBeforeCreate) {
35
+ hooks.onBeforeCreate(value);
36
+ }
37
+
38
+ this.domainHooks = hooks;
39
+
40
+ if (validation?.schema) {
41
+ this.domainSchema = validation.schema;
42
+ }
43
+
44
+ this.validationConfig = {
45
+ ...DEFAULT_VALIDATION_CONFIG,
46
+ ...validation?.config,
47
+ };
48
+
49
+ if (this.domainSchema && this.validationConfig.onCreate) {
50
+ this.validateValue(value);
51
+ }
52
+
53
+ this.value = value;
54
+
55
+ if (hooks?.rules) {
56
+ this.runRulesHook();
57
+ }
58
+
59
+ Object.freeze(this);
60
+ }
61
+
62
+ /**
63
+ * Add a validation issue during rules hook execution (non-throwing mode).
64
+ */
65
+ public addValidationIssue(path: string | string[], message: string): void {
66
+ this.issueCollector.add(path, message);
67
+ }
68
+
69
+ private beginValidationCycle(): void {
70
+ this.issueCollector.clear();
71
+ }
72
+
73
+ private finalizeValidation(collectedIssues: ValidationIssue[] = []): void {
74
+ const existing = (this as any)._validationError as
75
+ | ValidationError
76
+ | undefined;
77
+ const merged = ValidationError.merge(existing, collectedIssues);
78
+
79
+ if (!merged) {
80
+ delete (this as any)._validationError;
81
+ return;
82
+ }
83
+
84
+ if (this.validationConfig.throwOnError) {
85
+ throw merged;
86
+ }
87
+
88
+ (this as any)._validationError = merged;
89
+ }
90
+
91
+ private runRulesHook(): void {
92
+ if (!this.domainHooks?.rules) return;
93
+
94
+ this.beginValidationCycle();
95
+ this.domainHooks.rules(this as any);
96
+ this.finalizeValidation([...this.issueCollector.getIssues()]);
97
+ }
98
+
99
+ private validateValue(value: T): void {
100
+ const schemaError = this.validateSchema(value);
101
+ if (!schemaError) return;
102
+
103
+ if (this.validationConfig.throwOnError) {
104
+ throw schemaError;
105
+ }
106
+
107
+ (this as any)._validationError = schemaError;
108
+ }
109
+
110
+ private validateSchema(value: T): ValidationError | null {
111
+ if (!this.domainSchema) return null;
112
+
113
+ const result = this.domainSchema["~standard"].validate(value);
114
+
115
+ if (result instanceof Promise) {
116
+ throw new DomainError(
117
+ "Async validation not supported in constructor. Use sync validation schema."
118
+ );
119
+ }
120
+
121
+ if (result.issues && result.issues.length > 0) {
122
+ return new ValidationError(
123
+ result.issues.map((issue) => ({
124
+ path: issue.path?.map((p) => this.extractPathKey(p)) || [],
125
+ message: issue.message,
126
+ }))
127
+ );
128
+ }
129
+
130
+ return null;
131
+ }
132
+
133
+ private extractPathKey(pathSegment: unknown): string {
134
+ if (pathSegment === null || pathSegment === undefined) {
135
+ return "";
136
+ }
137
+ if (typeof pathSegment === "string" || typeof pathSegment === "number") {
138
+ return String(pathSegment);
139
+ }
140
+ if (typeof pathSegment === "symbol") {
141
+ return pathSegment.toString();
142
+ }
143
+ if (typeof pathSegment === "object" && "key" in pathSegment) {
144
+ return String((pathSegment as { key: unknown }).key);
145
+ }
146
+ return String(pathSegment);
147
+ }
148
+
149
+ /**
150
+ * Returns true if the value object has validation errors (when throwOnError is false).
151
+ */
152
+ get hasValidationErrors(): boolean {
153
+ return !!(this as any)._validationError;
154
+ }
155
+
156
+ /**
157
+ * Returns the validation errors (when throwOnError is false).
158
+ */
159
+ get validationErrors(): ValidationError | undefined {
160
+ return (this as any)._validationError;
161
+ }
162
+
163
+ /**
164
+ * Compare this ValueObject with another for equality based on their properties.
165
+ */
166
+ equals(other: ValueObject<T>): boolean {
167
+ if (!other || !(other instanceof ValueObject)) return false;
168
+ return this.value === other.value;
169
+ }
170
+
171
+ /**
172
+ * Creates a new ValueObject with updated value.
173
+ * ValueObjects are immutable, so this returns a new instance.
174
+ */
175
+ protected clone(value: T): this {
176
+ const Constructor = this.constructor as new (value: T) => this;
177
+ return new Constructor(value);
178
+ }
179
+ }