@woltz/rich-domain 1.2.0 → 1.2.1

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 (105) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/dist/aggregate-changes.d.ts +164 -0
  3. package/dist/aggregate-changes.d.ts.map +1 -0
  4. package/dist/aggregate-changes.js +281 -0
  5. package/dist/aggregate-changes.js.map +1 -0
  6. package/dist/base-entity.d.ts +32 -8
  7. package/dist/base-entity.d.ts.map +1 -1
  8. package/dist/base-entity.js +117 -86
  9. package/dist/base-entity.js.map +1 -1
  10. package/dist/criteria.d.ts +3 -3
  11. package/dist/criteria.d.ts.map +1 -1
  12. package/dist/criteria.js.map +1 -1
  13. package/dist/crypto.d.ts +3 -0
  14. package/dist/crypto.d.ts.map +1 -0
  15. package/dist/crypto.js +29 -0
  16. package/dist/crypto.js.map +1 -0
  17. package/dist/entity-changes.d.ts +84 -0
  18. package/dist/entity-changes.d.ts.map +1 -0
  19. package/dist/entity-changes.js +135 -0
  20. package/dist/entity-changes.js.map +1 -0
  21. package/dist/entity-schema-registry.d.ts +148 -0
  22. package/dist/entity-schema-registry.d.ts.map +1 -0
  23. package/dist/entity-schema-registry.js +219 -0
  24. package/dist/entity-schema-registry.js.map +1 -0
  25. package/dist/history-tracker.d.ts +97 -0
  26. package/dist/history-tracker.d.ts.map +1 -0
  27. package/dist/history-tracker.js +805 -0
  28. package/dist/history-tracker.js.map +1 -0
  29. package/dist/id.d.ts +11 -10
  30. package/dist/id.d.ts.map +1 -1
  31. package/dist/id.js +4 -28
  32. package/dist/id.js.map +1 -1
  33. package/dist/index.d.ts +1 -0
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js +1 -0
  36. package/dist/index.js.map +1 -1
  37. package/dist/mapper.d.ts +1 -1
  38. package/dist/mapper.d.ts.map +1 -1
  39. package/dist/mapper.js.map +1 -1
  40. package/dist/repository/base-repository.d.ts +6 -32
  41. package/dist/repository/base-repository.d.ts.map +1 -1
  42. package/dist/repository/base-repository.js +0 -27
  43. package/dist/repository/base-repository.js.map +1 -1
  44. package/dist/repository/unit-of-work.d.ts +0 -25
  45. package/dist/repository/unit-of-work.d.ts.map +1 -1
  46. package/dist/repository/unit-of-work.js +0 -25
  47. package/dist/repository/unit-of-work.js.map +1 -1
  48. package/dist/types/change-tracker.d.ts +186 -0
  49. package/dist/types/change-tracker.d.ts.map +1 -0
  50. package/dist/types/change-tracker.js +2 -0
  51. package/dist/types/change-tracker.js.map +1 -0
  52. package/dist/types/criteria.d.ts +5 -1
  53. package/dist/types/criteria.d.ts.map +1 -1
  54. package/dist/types/history-tracker.d.ts +11 -0
  55. package/dist/types/history-tracker.d.ts.map +1 -1
  56. package/dist/types/utils.d.ts +0 -1
  57. package/dist/types/utils.d.ts.map +1 -1
  58. package/dist/validation-error.d.ts.map +1 -1
  59. package/dist/validation-error.js +0 -3
  60. package/dist/validation-error.js.map +1 -1
  61. package/dist/value-object.d.ts +57 -8
  62. package/dist/value-object.d.ts.map +1 -1
  63. package/dist/value-object.js +49 -21
  64. package/dist/value-object.js.map +1 -1
  65. package/package.json +2 -1
  66. package/src/aggregate-changes.ts +335 -0
  67. package/src/base-entity.ts +140 -100
  68. package/src/criteria.ts +2 -1
  69. package/src/crypto.ts +31 -0
  70. package/src/entity-changes.ts +151 -0
  71. package/src/entity-schema-registry.ts +275 -0
  72. package/src/history-tracker.ts +1114 -0
  73. package/src/id.ts +17 -26
  74. package/src/index.ts +1 -0
  75. package/src/mapper.ts +4 -1
  76. package/src/repository/base-repository.ts +6 -37
  77. package/src/repository/unit-of-work.ts +0 -25
  78. package/src/types/change-tracker.ts +221 -0
  79. package/src/types/criteria.ts +6 -1
  80. package/src/types/history-tracker.ts +13 -0
  81. package/src/types/utils.ts +0 -9
  82. package/src/validation-error.ts +0 -4
  83. package/src/value-object.ts +84 -23
  84. package/tests/aggregate-changes.test.ts +284 -0
  85. package/tests/criteria.test.ts +122 -161
  86. package/tests/entity-equality.test.ts +38 -61
  87. package/tests/entity-schema-registry.test.ts +382 -0
  88. package/tests/entity-validation.test.ts +7 -94
  89. package/tests/history-tracker.spec.ts +349 -617
  90. package/tests/id.test.ts +41 -44
  91. package/tests/load-test/data.json +346041 -0
  92. package/tests/load-test/entities.ts +97 -0
  93. package/tests/load-test/generate-data.ts +81 -0
  94. package/tests/load-test/lead-to-domain.mapper.ts +24 -0
  95. package/tests/load-test/load.test.ts +38 -0
  96. package/tests/repository.test.ts +30 -54
  97. package/tests/to-json.test.ts +14 -18
  98. package/tests/utils.ts +138 -102
  99. package/tests/value-objects.test.ts +57 -29
  100. package/dist/deep-proxy.d.ts +0 -36
  101. package/dist/deep-proxy.d.ts.map +0 -1
  102. package/dist/deep-proxy.js +0 -384
  103. package/dist/deep-proxy.js.map +0 -1
  104. package/src/deep-proxy.ts +0 -447
  105. package/tests/entity.test.ts +0 -33
package/src/id.ts CHANGED
@@ -2,42 +2,33 @@
2
2
  // Id Class - Smart Identity Management
3
3
  // ============================================================================
4
4
 
5
- function randomUUID(): string {
6
- // If we are in the browser, use the browser's crypto API
7
- // @ts-expect-error - window.crypto is not defined in the browser
8
- if (typeof window !== "undefined" && window.crypto) {
9
- // @ts-expect-error - window.crypto is not defined in the browser
10
- return window.crypto.randomUUID();
11
- }
12
- // If we are in the server, use the crypto library
13
- // eslint-disable-next-line @typescript-eslint/no-require-imports
14
- const crypto = require("crypto");
15
-
16
- return crypto.randomUUID();
17
- }
5
+ import UUID from "./crypto";
18
6
 
19
7
  export class Id {
20
8
  private readonly _value: string;
21
9
  private readonly _isNew: boolean;
22
10
 
11
+
23
12
  /**
24
13
  * Create a new Id
25
14
  * @param value - Optional existing ID value. If not provided, generates a new UUID.
26
- *
27
- * @example
28
- * // New entity (generates UUID)
29
- * const newId = new Id();
30
- * newuser.isNew() // true
31
- *
32
- * // Existing entity (uses provided ID)
33
- * const existingId = new Id("550e8400-e29b-41d4-a716-446655440000");
34
- * existinguser.isNew() // false
35
- */
36
- constructor(value?: string) {
15
+ *
16
+ * @example
17
+ * // New entity (generates UUID)
18
+ * const newId = new Id();
19
+ * newuser.isNew() // true
20
+ *
21
+ * // Existing entity (uses provided ID)
22
+ * const existingId = new Id("550e8400-e29b-41d4-a716-446655440000");
23
+ * existinguser.isNew() // false
24
+ */
25
+ constructor(value: string, isNew?: boolean);
26
+ constructor(value?: string);
27
+ constructor(value?: string, isNew?: boolean) {
37
28
  if (value !== undefined) {
38
29
  // ID was provided - this is an existing entity
39
30
  this._value = value;
40
- this._isNew = false;
31
+ this._isNew = isNew ?? false;
41
32
  } else {
42
33
  // No ID provided - generate new one, this is a new entity
43
34
  this._value = this.generateUUID();
@@ -88,7 +79,7 @@ export class Id {
88
79
  */
89
80
  private generateUUID(): string {
90
81
  // Simple UUID v4 implementation
91
- return randomUUID();
82
+ return UUID();
92
83
  }
93
84
 
94
85
  /**
package/src/index.ts CHANGED
@@ -7,6 +7,7 @@ export { Id } from "./id";
7
7
  export { Entity, Aggregate } from "./entity";
8
8
  export { ValueObject } from "./value-object";
9
9
  export { Mapper } from "./mapper";
10
+ export { EntitySchemaRegistry } from "./entity-schema-registry";
10
11
 
11
12
  export * from "./validation-error";
12
13
 
package/src/mapper.ts CHANGED
@@ -1,3 +1,6 @@
1
1
  export abstract class Mapper<Input, Output> {
2
- public abstract build(input: Input, ...args: unknown[]): Output;
2
+ public abstract build(
3
+ input: Input,
4
+ ...args: unknown[]
5
+ ): Output | Promise<Output>;
3
6
  }
@@ -1,57 +1,26 @@
1
- // ============================================================================
2
- // Base Repository - Abstract implementation with common logic
3
- // ============================================================================
4
-
5
1
  import type { Aggregate } from "../entity";
6
2
  import type { Criteria } from "../criteria";
7
3
  import { PaginatedResult } from "../paginated-result";
8
4
  import { Mapper } from "../mapper";
9
5
 
10
- /**
11
- * Abstract base repository
12
- * Implements common logic, delegates persistence to subclasses
13
- *
14
- * @example
15
- * ```ts
16
- * class UserRepository extends BaseRepository<User, PrismaUser> {
17
- * constructor(prisma: PrismaClient) {
18
- * super(new UserMapper());
19
- * this.prisma = prisma;
20
- * }
21
- *
22
- * protected async insertOne(data: PrismaUser): Promise<PrismaUser> {
23
- * return this.prisma.user.create({ data });
24
- * }
25
- *
26
- * protected async updateOne(id: string, data: PrismaUser): Promise<PrismaUser> {
27
- * return this.prisma.user.update({ where: { id }, data });
28
- * }
29
- *
30
- * // ... implement other abstract methods
31
- * }
32
- * ```
33
- */
34
-
35
6
  export abstract class ReadRepository<Agg extends Aggregate<any>> {
36
- abstract find(criteria: Criteria<Agg>): Promise<PaginatedResult<Agg>>;
7
+ abstract find(criteria?: Criteria<Agg>): Promise<PaginatedResult<Agg>>;
37
8
  abstract findById(id: string): Promise<Agg | null>;
38
- abstract count(criteria: Criteria<Agg>): Promise<number>;
9
+ abstract count(criteria?: Criteria<Agg>): Promise<number>;
39
10
  abstract exists(id: string): Promise<boolean>;
40
11
  }
41
12
 
42
13
  export abstract class WriteRepository<Agg extends Aggregate<any>> {
43
- abstract create(entity: Agg): Promise<void>;
44
- abstract update(entity: Agg): Promise<void>;
14
+ abstract save(entity: Agg): Promise<void>;
45
15
  abstract delete(entity: Agg): Promise<void>;
46
16
  }
47
17
 
48
18
  export abstract class WriteAndRead<Agg extends Aggregate<any>> {
49
- abstract find(criteria: Criteria<Agg>): Promise<PaginatedResult<Agg>>;
19
+ abstract find(criteria?: Criteria<Agg>): Promise<PaginatedResult<Agg>>;
50
20
  abstract findById(id: string): Promise<Agg | null>;
51
- abstract create(entity: Agg): Promise<void>;
52
- abstract update(entity: Agg): Promise<void>;
21
+ abstract save(entity: Agg): Promise<void>;
53
22
  abstract delete(entity: Agg): Promise<void>;
54
- abstract count(criteria: Criteria<Agg>): Promise<number>;
23
+ abstract count(criteria?: Criteria<Agg>): Promise<number>;
55
24
  abstract exists(id: string): Promise<boolean>;
56
25
  }
57
26
 
@@ -7,31 +7,6 @@ import type { IUnitOfWork, TransactionContext } from "../types";
7
7
  /**
8
8
  * Abstract Unit of Work
9
9
  * Provides transaction management across multiple repositories
10
- *
11
- * @example
12
- * ```ts
13
- * // Using transaction helper (recommended)
14
- * await uow.transaction(async (ctx) => {
15
- * const userRepo = uow.getRepository(UserRepository);
16
- * const orderRepo = uow.getRepository(OrderRepository);
17
- *
18
- * await userRepo.save(user);
19
- * await orderRepo.save(order);
20
- *
21
- * // Auto-commits on success, rolls back on error
22
- * });
23
- *
24
- * // Manual control
25
- * const ctx = await uow.begin();
26
- * try {
27
- * await userRepo.save(user);
28
- * await orderRepo.save(order);
29
- * await ctx.commit();
30
- * } catch (error) {
31
- * await ctx.rollback();
32
- * throw error;
33
- * }
34
- * ```
35
10
  */
36
11
  export abstract class UnitOfWork implements IUnitOfWork {
37
12
  protected currentContext: TransactionContext | null = null;
@@ -0,0 +1,221 @@
1
+ import { Entity } from "../entity";
2
+ import { ValueObject } from "../value-object";
3
+
4
+ /**
5
+ * Base operation with common information.
6
+ */
7
+ export interface BaseOperation {
8
+ /** Entity name in the domain (e.g., 'User', 'Post', 'Comment') */
9
+ entity: string;
10
+ /** Depth in the aggregate tree (0 = root, 1 = direct children, etc.) */
11
+ depth: number;
12
+ }
13
+
14
+ /**
15
+ * Create operation.
16
+ */
17
+ export interface CreateOperation<T = any> extends BaseOperation {
18
+ type: "create";
19
+ /** Entity data to be created */
20
+ data: T;
21
+ /** Parent ID (for FK) */
22
+ parentId?: string;
23
+ /** Parent entity name */
24
+ parentEntity?: string;
25
+ }
26
+
27
+ /**
28
+ * Update operation.
29
+ */
30
+ export interface UpdateOperation<T = any> extends BaseOperation {
31
+ type: "update";
32
+ /** Entity ID */
33
+ id: string;
34
+ /** Current entity instance */
35
+ data: T;
36
+ /** Only fields that have changed */
37
+ changedFields: Record<string, any>;
38
+ }
39
+
40
+ /**
41
+ * Delete operation.
42
+ */
43
+ export interface DeleteOperation<T = any> extends BaseOperation {
44
+ type: "delete";
45
+ /** Entity ID to be deleted */
46
+ id: string;
47
+ /** Entity data (for reference) */
48
+ data: T;
49
+ }
50
+
51
+ /**
52
+ * Union of all possible operations.
53
+ */
54
+ export type Operation<T = any> =
55
+ | CreateOperation<T>
56
+ | UpdateOperation<T>
57
+ | DeleteOperation<T>;
58
+
59
+ /**
60
+ * Item for batch creation.
61
+ */
62
+ export interface BatchCreateItem<T = any> {
63
+ /** Entity data */
64
+ data: T;
65
+ /** Parent ID (for FK) */
66
+ parentId?: string;
67
+ }
68
+
69
+ /**
70
+ * Item for batch update.
71
+ */
72
+ export interface BatchUpdateItem {
73
+ /** Entity ID */
74
+ id: string;
75
+ /** Fields that have changed */
76
+ changedFields: Record<string, any>;
77
+ }
78
+
79
+ /**
80
+ * Grouped and ordered operations for batch execution.
81
+ */
82
+ export interface BatchOperations {
83
+ /**
84
+ * Deletes grouped by entity, ordered by depth descending (leaf → root).
85
+ */
86
+ deletes: Array<{
87
+ entity: string;
88
+ depth: number;
89
+ ids: string[];
90
+ }>;
91
+
92
+ /**
93
+ * Creates grouped by entity, ordered by depth ascending (root → leaf).
94
+ */
95
+ creates: Array<{
96
+ entity: string;
97
+ depth: number;
98
+ items: BatchCreateItem[];
99
+ }>;
100
+
101
+ /**
102
+ * Updates grouped by entity.
103
+ */
104
+ updates: Array<{
105
+ entity: string;
106
+ items: BatchUpdateItem[];
107
+ }>;
108
+ }
109
+
110
+ /**
111
+ * Changes detected in a collection (1:N).
112
+ */
113
+ export interface CollectionChanges<T = any> {
114
+ /** Created items */
115
+ created: T[];
116
+ /** Updated items with their changes */
117
+ updated: Array<{
118
+ entity: T;
119
+ changes: Record<string, { from: any; to: any }>;
120
+ }>;
121
+ /** Deleted items */
122
+ deleted: T[];
123
+ }
124
+
125
+ /**
126
+ * Possible states for a 1:1 relationship.
127
+ */
128
+ export type EntityChangeState =
129
+ | "created" // null → Entity
130
+ | "updated" // Entity(id:1) → Entity(id:1) with changes
131
+ | "deleted" // Entity → null
132
+ | "replaced" // Entity(id:1) → Entity(id:2)
133
+ | "unchanged"; // No changes
134
+
135
+ /**
136
+ * Change in a 1:1 entity relationship.
137
+ */
138
+ export interface EntityChange<T = any> {
139
+ /** State of the change */
140
+ state: EntityChangeState;
141
+ /** Current entity (null if deleted) */
142
+ current: T | null;
143
+ /** Previous entity (null if created) */
144
+ previous: T | null;
145
+ /** Field changes (if state === 'updated') */
146
+ changes?: Record<string, { from: any; to: any }>;
147
+ }
148
+
149
+ /**
150
+ * Change in a primitive field.
151
+ */
152
+ export interface FieldChange<T = any> {
153
+ from: T;
154
+ to: T;
155
+ }
156
+
157
+ /**
158
+ * Extracts the props type from an Entity or ValueObject.
159
+ */
160
+ export type ExtractProps<T> = T extends Entity<infer P>
161
+ ? P
162
+ : T extends ValueObject<infer P>
163
+ ? P
164
+ : never;
165
+
166
+ /**
167
+ * Keys of primitive properties (not Entity, ValueObject or Array).
168
+ */
169
+ export type PrimitiveKeys<T> = {
170
+ [K in keyof T]: T[K] extends
171
+ | Entity<any>
172
+ | ValueObject<any>
173
+ | Array<any>
174
+ | undefined
175
+ ? never
176
+ : K;
177
+ }[keyof T];
178
+
179
+ /**
180
+ * Keys of collections (arrays).
181
+ */
182
+ export type CollectionKeys<T> = {
183
+ [K in keyof T]: T[K] extends Array<any> ? K : never;
184
+ }[keyof T];
185
+
186
+ /**
187
+ * Keys of single entities (1:1).
188
+ */
189
+ export type SingleEntityKeys<T> = {
190
+ [K in keyof T]: T[K] extends Entity<any> | ValueObject<any> | null | undefined
191
+ ? T[K] extends Array<any>
192
+ ? never
193
+ : K
194
+ : never;
195
+ }[keyof T];
196
+
197
+ /**
198
+ * Change history entry.
199
+ */
200
+ export interface HistoryEntry {
201
+ path: string;
202
+ previousValue: any;
203
+ currentValue: any;
204
+ timestamp: number;
205
+ }
206
+
207
+ /**
208
+ * Metadata from a tracked entity/VO.
209
+ */
210
+ export interface TrackedEntityMetadata {
211
+ /** Entity name */
212
+ entityName: string;
213
+ /** Depth in the tree */
214
+ depth: number;
215
+ /** Parent ID */
216
+ parentId?: string;
217
+ /** Parent entity name */
218
+ parentEntity?: string;
219
+ /** Path in the object (e.g., 'posts[0].comments[1]') */
220
+ path: string;
221
+ }
@@ -118,6 +118,11 @@ export interface Order {
118
118
  direction: OrderDirection;
119
119
  }
120
120
 
121
+ export type TypedOrder<T> = {
122
+ field: FieldPath<T>;
123
+ direction: OrderDirection;
124
+ };
125
+
121
126
  export interface Pagination {
122
127
  page: number;
123
128
  limit: number;
@@ -153,5 +158,5 @@ export type FieldPath<T> = T extends Primitive
153
158
  ? U extends Primitive
154
159
  ? K
155
160
  : K | `${K}.${FieldPath<U>}`
156
- : K | `${K}.${FieldPath<NonNullable<T[K]>>}`;
161
+ : `${K}.${FieldPath<NonNullable<T[K]>>}`;
157
162
  }[ExcludeBuiltInKeys<T> & string];
@@ -1,3 +1,4 @@
1
+ import { TrackedEntityMetadata } from "./change-tracker";
1
2
  import { BaseProps } from "./domain";
2
3
  import { IsArray, NonUndefined, UnwrapArray } from "./utils";
3
4
 
@@ -7,6 +8,18 @@ export interface ChangeEvent<T> {
7
8
  path: string;
8
9
  }
9
10
 
11
+ export interface TrackedItem {
12
+ entity: any;
13
+ metadata: TrackedEntityMetadata;
14
+ originalState: any;
15
+ }
16
+
17
+ export interface ArrayState {
18
+ cloned: any[];
19
+ original: any[];
20
+ metadata: TrackedEntityMetadata;
21
+ }
22
+
10
23
  export interface ArrayChangeEvent<T> {
11
24
  toCreate: T[];
12
25
  toUpdate: T[];
@@ -14,16 +14,7 @@ export type DeepJsonResult<T> = {
14
14
  : T[K];
15
15
  };
16
16
 
17
- export type DeepKeyOf<T, K extends keyof T = keyof T> = K extends string
18
- ? T[K] extends Primitive
19
- ? K
20
- : T[K] extends object
21
- ? `${K}` | `${K}.${DeepKeyOf<T[K]>}`
22
- : never
23
- : never;
24
-
25
17
  export type Primitive = string | number | boolean | Date | null | undefined;
26
-
27
18
  export type UnwrapArray<T> = T extends Array<infer U> ? U : never;
28
19
  export type IsArray<T> = T extends Array<any> ? true : false;
29
20
  export type NonUndefined<T> = T extends undefined ? never : T;
@@ -1,7 +1,3 @@
1
- // ============================================================================
2
- // Validation Error - Domain Validation Errors
3
- // ============================================================================
4
-
5
1
  export interface ValidationIssue {
6
2
  path: string[];
7
3
  message: string;
@@ -1,7 +1,3 @@
1
- // ============================================================================
2
- // Value Object - Immutable Domain Objects
3
- // ============================================================================
4
-
5
1
  import { ValidationError } from "./validation-error";
6
2
  import { IDomainEvent } from ".";
7
3
  import {
@@ -21,6 +17,11 @@ function getStaticProperty<T>(
21
17
  return instance.constructor[propertyName];
22
18
  }
23
19
 
20
+ /**
21
+ * Identity key type for a Value Object. Can be a single key or an array of keys (composite key).
22
+ */
23
+ export type IdentityKeyDefinition<T> = (keyof T)[] | keyof T;
24
+
24
25
  export abstract class ValueObject<T> {
25
26
  protected readonly props!: T;
26
27
  private validationConfig: Required<ValidationConfig>;
@@ -28,12 +29,29 @@ export abstract class ValueObject<T> {
28
29
  private domainSchema?: StandardSchema<T>;
29
30
  private domainEvents: IDomainEvent[] = [];
30
31
 
31
- // Static properties that subclasses can override
32
32
  protected static validation?: EntityValidation<any>;
33
33
  protected static hooks?: VOHooks<any, any>;
34
34
 
35
+ /**
36
+ * Identity key for identification in collections.
37
+ * Used by HistoryTracker to track changes in arrays of Value Objects.
38
+ *
39
+ * @example
40
+ * ```typescript
41
+ * // Simple key
42
+ * class TagReference extends ValueObject<{ tagId: string }> {
43
+ * static readonly identityKey = 'tagId';
44
+ * }
45
+ *
46
+ * // Composite key
47
+ * class Like extends ValueObject<{ postId: string; userId: string }> {
48
+ * static readonly identityKey = ['postId', 'userId'];
49
+ * }
50
+ * ```
51
+ */
52
+ protected static identityKey?: IdentityKeyDefinition<any>;
53
+
35
54
  constructor(props: T) {
36
- // Get static configuration from subclass
37
55
  const validation = getStaticProperty<EntityValidation<T>>(
38
56
  this,
39
57
  "validation"
@@ -53,23 +71,18 @@ export abstract class ValueObject<T> {
53
71
 
54
72
  let finalProps = { ...props } as T;
55
73
 
56
- // Validate schema on creation
57
74
  if (this.domainSchema && this.validationConfig.onCreate) {
58
75
  this.validateProps(finalProps);
59
76
  }
60
77
 
61
- // Set props (not frozen yet) so rules can access them
62
78
  (this as any).props = finalProps;
63
79
 
64
- // Execute rules (custom validations) - after props is set but before freezing
65
80
  if (hooks?.rules) {
66
81
  hooks.rules(this as any);
67
82
  }
68
83
 
69
- // Now freeze the props for immutability
70
84
  Object.freeze(this.props);
71
85
 
72
- // Hook onCreate
73
86
  if (hooks?.onCreate) {
74
87
  hooks.onCreate(this as any);
75
88
  }
@@ -98,7 +111,6 @@ export abstract class ValueObject<T> {
98
111
  throw validationError;
99
112
  }
100
113
 
101
- // If not throwing, store error for later retrieval
102
114
  (this as any)._validationError = validationError;
103
115
  }
104
116
  }
@@ -107,63 +119,112 @@ export abstract class ValueObject<T> {
107
119
  if (pathSegment === null || pathSegment === undefined) {
108
120
  return "";
109
121
  }
110
- // Handle PropertyKey (string | number | symbol)
111
122
  if (typeof pathSegment === "string" || typeof pathSegment === "number") {
112
123
  return String(pathSegment);
113
124
  }
114
125
  if (typeof pathSegment === "symbol") {
115
126
  return pathSegment.toString();
116
127
  }
117
- // Handle object with 'key' property (Zod's PathSegment)
118
128
  if (typeof pathSegment === "object" && "key" in pathSegment) {
119
129
  return String((pathSegment as { key: unknown }).key);
120
130
  }
121
- // Fallback
122
131
  return String(pathSegment);
123
132
  }
124
133
 
125
134
  /**
126
- * Check if value object has validation errors (when throwOnError is false)
135
+ * Returns true if the value object has validation errors (when throwOnError is false).
127
136
  */
128
137
  get hasValidationErrors(): boolean {
129
138
  return !!(this as any)._validationError;
130
139
  }
131
140
 
132
141
  /**
133
- * Get validation errors (when throwOnError is false)
142
+ * Returns the validation errors (when throwOnError is false).
134
143
  */
135
144
  get validationErrors(): ValidationError | undefined {
136
145
  return (this as any)._validationError;
137
146
  }
138
147
 
148
+ /**
149
+ * Compare this ValueObject with another for equality based on their properties.
150
+ */
139
151
  equals(other: ValueObject<T>): boolean {
140
152
  if (!other || !(other instanceof ValueObject)) return false;
141
153
  return JSON.stringify(this.props) === JSON.stringify(other.props);
142
154
  }
143
155
 
144
156
  /**
145
- * Add a domain event to this value object
157
+ * Returns the identity key for this Value Object.
158
+ * Used for identification in collections when identityKey is set.
159
+ *
160
+ * @returns String with the identity key or null if not defined
161
+ *
162
+ * @example
163
+ * ```typescript
164
+ * const like = new Like({ postId: 'p1', userId: 'u1' });
165
+ * like.getIdentityKey(); // 'p1:u1'
166
+ *
167
+ * const tag = new TagReference({ tagId: 'tag-123' });
168
+ * tag.getIdentityKey(); // 'tag-123'
169
+ * ```
170
+ */
171
+ getIdentityKey(): string | null {
172
+ const keyDef = getStaticProperty<IdentityKeyDefinition<T>>(
173
+ this,
174
+ "identityKey"
175
+ );
176
+
177
+ if (!keyDef) {
178
+ return null;
179
+ }
180
+
181
+ if (Array.isArray(keyDef)) {
182
+ return keyDef.map((k) => String(this.props[k])).join(":");
183
+ }
184
+
185
+ return String(this.props[keyDef]);
186
+ }
187
+
188
+ /**
189
+ * Returns true if this Value Object has an identity key defined.
190
+ */
191
+ hasIdentityKey(): boolean {
192
+ return (
193
+ getStaticProperty<IdentityKeyDefinition<T>>(this, "identityKey") !==
194
+ undefined
195
+ );
196
+ }
197
+
198
+ /**
199
+ * Returns the identity key definition (if any).
200
+ */
201
+ static getIdentityKeyDefinition<P>(): IdentityKeyDefinition<P> | undefined {
202
+ return (this as any).identityKey;
203
+ }
204
+
205
+ /**
206
+ * Adds a domain event to this value object.
146
207
  */
147
208
  protected addDomainEvent(event: IDomainEvent): void {
148
209
  this.domainEvents.push(event);
149
210
  }
150
211
 
151
212
  /**
152
- * Get all uncommitted domain events
213
+ * Returns all uncommitted domain events.
153
214
  */
154
215
  getUncommittedEvents(): IDomainEvent[] {
155
216
  return [...this.domainEvents];
156
217
  }
157
218
 
158
219
  /**
159
- * Clear all domain events (call after publishing)
220
+ * Clears all domain events (call after publishing).
160
221
  */
161
222
  clearEvents(): void {
162
223
  this.domainEvents = [];
163
224
  }
164
225
 
165
226
  /**
166
- * Check if value object has uncommitted events
227
+ * Returns true if the value object has uncommitted events.
167
228
  */
168
229
  hasUncommittedEvents(): boolean {
169
230
  return this.domainEvents.length > 0;
@@ -174,8 +235,8 @@ export abstract class ValueObject<T> {
174
235
  }
175
236
 
176
237
  /**
177
- * Create a new ValueObject with updated properties
178
- * Since ValueObjects are immutable, this returns a new instance
238
+ * Creates a new ValueObject with updated properties.
239
+ * ValueObjects are immutable, so this returns a new instance.
179
240
  */
180
241
  protected clone(updates: Partial<T>): this {
181
242
  const Constructor = this.constructor as new (props: T) => this;