@woltz/rich-domain 1.9.0 → 1.9.2

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 (144) hide show
  1. package/dist/cjs/core/aggregate-changes.d.ts +14 -0
  2. package/dist/cjs/core/aggregate-changes.d.ts.map +1 -1
  3. package/dist/cjs/core/aggregate-changes.js +18 -0
  4. package/dist/cjs/core/aggregate-changes.js.map +1 -1
  5. package/dist/cjs/core/base-entity.d.ts +2 -0
  6. package/dist/cjs/core/base-entity.d.ts.map +1 -1
  7. package/dist/cjs/core/base-entity.js +39 -41
  8. package/dist/cjs/core/base-entity.js.map +1 -1
  9. package/dist/cjs/core/change-tracker.d.ts +8 -0
  10. package/dist/cjs/core/change-tracker.d.ts.map +1 -1
  11. package/dist/cjs/core/change-tracker.js +36 -6
  12. package/dist/cjs/core/change-tracker.js.map +1 -1
  13. package/dist/cjs/core/domain-event.d.ts +3 -0
  14. package/dist/cjs/core/domain-event.d.ts.map +1 -1
  15. package/dist/cjs/core/domain-event.js +8 -1
  16. package/dist/cjs/core/domain-event.js.map +1 -1
  17. package/dist/cjs/core/value-object.d.ts.map +1 -1
  18. package/dist/cjs/core/value-object.js +3 -5
  19. package/dist/cjs/core/value-object.js.map +1 -1
  20. package/dist/cjs/criteria.d.ts +1 -1
  21. package/dist/cjs/index.d.ts +1 -1
  22. package/dist/cjs/index.d.ts.map +1 -1
  23. package/dist/cjs/repository/entity-schema-registry.d.ts +56 -3
  24. package/dist/cjs/repository/entity-schema-registry.d.ts.map +1 -1
  25. package/dist/cjs/repository/entity-schema-registry.js +61 -6
  26. package/dist/cjs/repository/entity-schema-registry.js.map +1 -1
  27. package/dist/cjs/types/index.d.ts +1 -0
  28. package/dist/cjs/types/index.d.ts.map +1 -1
  29. package/dist/cjs/types/index.js +1 -0
  30. package/dist/cjs/types/index.js.map +1 -1
  31. package/dist/cjs/types/outbox-store.d.ts +91 -0
  32. package/dist/cjs/types/outbox-store.d.ts.map +1 -0
  33. package/dist/cjs/types/outbox-store.js +3 -0
  34. package/dist/cjs/types/outbox-store.js.map +1 -0
  35. package/dist/cjs/utils/helpers.d.ts +1 -0
  36. package/dist/cjs/utils/helpers.d.ts.map +1 -1
  37. package/dist/cjs/utils/helpers.js +4 -0
  38. package/dist/cjs/utils/helpers.js.map +1 -1
  39. package/dist/esm/core/aggregate-changes.d.ts +14 -0
  40. package/dist/esm/core/aggregate-changes.d.ts.map +1 -1
  41. package/dist/esm/core/aggregate-changes.js +18 -0
  42. package/dist/esm/core/aggregate-changes.js.map +1 -1
  43. package/dist/esm/core/base-entity.d.ts +2 -0
  44. package/dist/esm/core/base-entity.d.ts.map +1 -1
  45. package/dist/esm/core/base-entity.js +37 -39
  46. package/dist/esm/core/base-entity.js.map +1 -1
  47. package/dist/esm/core/change-tracker.d.ts +8 -0
  48. package/dist/esm/core/change-tracker.d.ts.map +1 -1
  49. package/dist/esm/core/change-tracker.js +36 -6
  50. package/dist/esm/core/change-tracker.js.map +1 -1
  51. package/dist/esm/core/domain-event.d.ts +3 -0
  52. package/dist/esm/core/domain-event.d.ts.map +1 -1
  53. package/dist/esm/core/domain-event.js +5 -1
  54. package/dist/esm/core/domain-event.js.map +1 -1
  55. package/dist/esm/core/value-object.d.ts.map +1 -1
  56. package/dist/esm/core/value-object.js +1 -3
  57. package/dist/esm/core/value-object.js.map +1 -1
  58. package/dist/esm/criteria.d.ts +1 -1
  59. package/dist/esm/index.d.ts +1 -1
  60. package/dist/esm/index.d.ts.map +1 -1
  61. package/dist/esm/repository/entity-schema-registry.d.ts +56 -3
  62. package/dist/esm/repository/entity-schema-registry.d.ts.map +1 -1
  63. package/dist/esm/repository/entity-schema-registry.js +61 -6
  64. package/dist/esm/repository/entity-schema-registry.js.map +1 -1
  65. package/dist/esm/types/index.d.ts +1 -0
  66. package/dist/esm/types/index.d.ts.map +1 -1
  67. package/dist/esm/types/index.js +1 -0
  68. package/dist/esm/types/index.js.map +1 -1
  69. package/dist/esm/types/outbox-store.d.ts +91 -0
  70. package/dist/esm/types/outbox-store.d.ts.map +1 -0
  71. package/dist/esm/types/outbox-store.js +2 -0
  72. package/dist/esm/types/outbox-store.js.map +1 -0
  73. package/dist/esm/utils/helpers.d.ts +1 -0
  74. package/dist/esm/utils/helpers.d.ts.map +1 -1
  75. package/dist/esm/utils/helpers.js +3 -0
  76. package/dist/esm/utils/helpers.js.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/core/aggregate-changes.d.ts +14 -0
  81. package/dist/types/core/aggregate-changes.d.ts.map +1 -1
  82. package/dist/types/core/base-entity.d.ts +2 -0
  83. package/dist/types/core/base-entity.d.ts.map +1 -1
  84. package/dist/types/core/change-tracker.d.ts +8 -0
  85. package/dist/types/core/change-tracker.d.ts.map +1 -1
  86. package/dist/types/core/domain-event.d.ts +3 -0
  87. package/dist/types/core/domain-event.d.ts.map +1 -1
  88. package/dist/types/core/value-object.d.ts.map +1 -1
  89. package/dist/types/criteria.d.ts +1 -1
  90. package/dist/types/index.d.ts +1 -1
  91. package/dist/types/index.d.ts.map +1 -1
  92. package/dist/types/repository/entity-schema-registry.d.ts +56 -3
  93. package/dist/types/repository/entity-schema-registry.d.ts.map +1 -1
  94. package/dist/types/types/index.d.ts +1 -0
  95. package/dist/types/types/index.d.ts.map +1 -1
  96. package/dist/types/types/outbox-store.d.ts +91 -0
  97. package/dist/types/types/outbox-store.d.ts.map +1 -0
  98. package/dist/types/utils/helpers.d.ts +1 -0
  99. package/dist/types/utils/helpers.d.ts.map +1 -1
  100. package/package.json +68 -67
  101. package/src/constants.ts +82 -0
  102. package/src/core/aggregate-changes.ts +466 -0
  103. package/src/core/base-aggregate.ts +76 -0
  104. package/src/core/base-entity.ts +552 -0
  105. package/src/core/change-tracker.ts +1327 -0
  106. package/src/core/domain-event.ts +41 -0
  107. package/src/core/entity-changes.ts +146 -0
  108. package/src/core/entity.ts +13 -0
  109. package/src/core/id.ts +124 -0
  110. package/src/core/index.ts +9 -0
  111. package/src/core/value-object.ts +179 -0
  112. package/src/criteria.ts +574 -0
  113. package/src/exceptions.ts +549 -0
  114. package/src/index.ts +74 -0
  115. package/src/repository/base-repository.ts +81 -0
  116. package/src/repository/entity-schema-registry.ts +620 -0
  117. package/src/repository/index.ts +5 -0
  118. package/src/repository/mapper.ts +7 -0
  119. package/src/repository/paginated-result.ts +251 -0
  120. package/src/repository/unit-of-work.ts +76 -0
  121. package/src/types/change-tracker.ts +268 -0
  122. package/src/types/criteria.ts +197 -0
  123. package/src/types/domain-event.ts +29 -0
  124. package/src/types/domain.ts +41 -0
  125. package/src/types/event-bus.ts +17 -0
  126. package/src/types/index.ts +9 -0
  127. package/src/types/outbox-store.ts +97 -0
  128. package/src/types/standard-schema.ts +19 -0
  129. package/src/types/unit-of-work.ts +46 -0
  130. package/src/types/utils.ts +24 -0
  131. package/src/utils/criteria-operator-validation.ts +209 -0
  132. package/src/utils/crypto.ts +31 -0
  133. package/src/utils/helpers.ts +50 -0
  134. package/src/validation-error.ts +219 -0
  135. package/dist/cjs/t.d.ts +0 -2
  136. package/dist/cjs/t.d.ts.map +0 -1
  137. package/dist/cjs/t.js +0 -96
  138. package/dist/cjs/t.js.map +0 -1
  139. package/dist/esm/t.d.ts +0 -2
  140. package/dist/esm/t.d.ts.map +0 -1
  141. package/dist/esm/t.js +0 -94
  142. package/dist/esm/t.js.map +0 -1
  143. package/dist/types/t.d.ts +0 -2
  144. package/dist/types/t.d.ts.map +0 -1
@@ -0,0 +1,552 @@
1
+ import {
2
+ ValidationError,
3
+ ValidationIssue,
4
+ ValidationIssueCollector,
5
+ } from "../validation-error.js";
6
+ import {
7
+ BaseProps,
8
+ HistoryEntry,
9
+ DeepJsonResult,
10
+ EntityHooks,
11
+ ValidationConfig,
12
+ StandardSchema,
13
+ EntityValidation,
14
+ } from "../types/index.js";
15
+ import { DEFAULT_VALIDATION_CONFIG } from "../constants.js";
16
+ import { DomainError } from "../exceptions.js";
17
+ import { ChangeTracker, AggregateChanges, Id, ValueObject } from "./index";
18
+ import { getStaticProperty } from "../utils/helpers.js";
19
+
20
+ export abstract class BaseEntity<
21
+ T extends BaseProps,
22
+ TOptionalInput extends keyof T = never,
23
+ > {
24
+ private _props: T;
25
+ private tracker: ChangeTracker;
26
+ private proxiedProps: T;
27
+ private snapshot: T | null = null;
28
+ private validationConfig: Required<ValidationConfig>;
29
+ private entityHooks?: EntityHooks<T, any>;
30
+ private entitySchema?: StandardSchema<T>;
31
+ private readonly issueCollector = new ValidationIssueCollector();
32
+
33
+ protected static validation?: EntityValidation<any>;
34
+ protected static hooks?: EntityHooks<any, any>;
35
+
36
+ constructor(
37
+ props: Omit<T, TOptionalInput | "id"> &
38
+ Partial<Pick<T, TOptionalInput>> & { id?: Id }
39
+ ) {
40
+ const validation = getStaticProperty<EntityValidation<T>>(
41
+ this,
42
+ "validation"
43
+ );
44
+
45
+ const hooks = getStaticProperty<EntityHooks<T, any>>(this, "hooks");
46
+
47
+ if (!props.id) {
48
+ props.id = new Id();
49
+ }
50
+
51
+ if (hooks?.onBeforeCreate) {
52
+ hooks.onBeforeCreate(props as T);
53
+ }
54
+
55
+ this.entityHooks = hooks;
56
+
57
+ if (validation?.schema) {
58
+ this.entitySchema = validation.schema;
59
+ }
60
+
61
+ this.validationConfig = {
62
+ ...DEFAULT_VALIDATION_CONFIG,
63
+ ...validation?.config,
64
+ };
65
+
66
+ let finalProps = { ...props } as T;
67
+
68
+ if (this.entitySchema && this.validationConfig.onCreate) {
69
+ this.validateProps(finalProps);
70
+ }
71
+
72
+ this._props = finalProps;
73
+ this.tracker = new ChangeTracker(this._props, this.constructor.name);
74
+
75
+ if (this.validationConfig.onUpdate) {
76
+ this.setupUpdateValidation();
77
+ }
78
+
79
+ this.proxiedProps = this.tracker.createProxy();
80
+
81
+ if (hooks?.rules) {
82
+ this.runRulesHook();
83
+ }
84
+
85
+ if (hooks?.onCreate) {
86
+ hooks.onCreate(this as any);
87
+ }
88
+
89
+ this.takeSnapshot();
90
+ }
91
+
92
+ /**
93
+ * Add a validation issue during rules hook execution (non-throwing mode).
94
+ */
95
+ public addValidationIssue(path: string | string[], message: string): void {
96
+ this.issueCollector.add(path, message);
97
+ }
98
+
99
+ private beginValidationCycle(): void {
100
+ this.issueCollector.clear();
101
+ }
102
+
103
+ private finalizeValidation(collectedIssues: ValidationIssue[] = []): void {
104
+ const existing = (this as any)._validationError as
105
+ | ValidationError
106
+ | undefined;
107
+ const merged = ValidationError.merge(existing, collectedIssues, {
108
+ entityName: this.constructor.name,
109
+ });
110
+
111
+ if (!merged) {
112
+ delete (this as any)._validationError;
113
+ return;
114
+ }
115
+
116
+ if (this.validationConfig.throwOnError) {
117
+ throw merged;
118
+ }
119
+
120
+ (this as any)._validationError = merged;
121
+ }
122
+
123
+ private runRulesHook(): void {
124
+ if (!this.entityHooks?.rules) return;
125
+
126
+ this.beginValidationCycle();
127
+ this.entityHooks.rules(this as any);
128
+ this.finalizeValidation([...this.issueCollector.getIssues()]);
129
+ }
130
+
131
+ private validateProps(props: T): void {
132
+ const schemaError = this.validateSchema(props);
133
+ if (!schemaError) return;
134
+
135
+ if (this.validationConfig.throwOnError) {
136
+ throw schemaError;
137
+ }
138
+
139
+ (this as any)._validationError = schemaError;
140
+ }
141
+
142
+ private validateSchema(props: T): ValidationError | null {
143
+ if (!this.entitySchema) return null;
144
+
145
+ const result = this.entitySchema["~standard"].validate(props);
146
+
147
+ if (result instanceof Promise) {
148
+ throw new DomainError(
149
+ "Async validation not supported in constructor. Use sync validation schema."
150
+ );
151
+ }
152
+
153
+ if (result.issues && result.issues.length > 0) {
154
+ return new ValidationError(
155
+ result.issues.map((issue) => ({
156
+ path: issue.path?.map((p) => this.extractPathKey(p)) || [],
157
+ message: issue.message,
158
+ })),
159
+ {
160
+ entityName: this.constructor.name,
161
+ }
162
+ );
163
+ }
164
+
165
+ return null;
166
+ }
167
+
168
+ private handleValidationFailure(issues: ValidationIssue[]): void {
169
+ if (issues.length === 0) {
170
+ this.clearValidationError();
171
+ return;
172
+ }
173
+
174
+ const error = ValidationError.fromIssues(issues, {
175
+ entityName: this.constructor.name,
176
+ });
177
+
178
+ if (this.validationConfig.throwOnError) {
179
+ throw error;
180
+ }
181
+
182
+ (this as any)._validationError = error;
183
+ }
184
+
185
+ private clearValidationError(): void {
186
+ delete (this as any)._validationError;
187
+ }
188
+
189
+ /**
190
+ * Validates the full current props (schema + rules). Used on updates when
191
+ * throwOnError is false so validationErrors reflects every invalid field.
192
+ */
193
+ private collectCurrentValidationIssues(): ValidationIssue[] {
194
+ const issues: ValidationIssue[] = [];
195
+
196
+ const schemaError = this.entitySchema
197
+ ? this.validateSchema(this._props)
198
+ : null;
199
+ if (schemaError) {
200
+ issues.push(...schemaError.issues);
201
+ }
202
+
203
+ if (this.entityHooks?.rules) {
204
+ this.beginValidationCycle();
205
+ this.entityHooks.rules(this as any);
206
+ issues.push(...this.issueCollector.getIssues());
207
+ }
208
+
209
+ return issues;
210
+ }
211
+
212
+ /**
213
+ * @returns true when the entity has no validation issues after refresh
214
+ */
215
+ private refreshValidationStateFromCurrentProps(): boolean {
216
+ const issues = this.collectCurrentValidationIssues();
217
+
218
+ if (issues.length === 0) {
219
+ this.clearValidationError();
220
+ return true;
221
+ }
222
+
223
+ this.handleValidationFailure(issues);
224
+ return false;
225
+ }
226
+
227
+ /**
228
+ * When true, failed schema/rules updates keep the mutated value and refresh
229
+ * validationErrors (dirty / form mode). Requires throwOnError: false and
230
+ * persistInvalidMutations: true.
231
+ */
232
+ private shouldPersistInvalidMutation(): boolean {
233
+ return (
234
+ !this.validationConfig.throwOnError &&
235
+ this.validationConfig.persistInvalidMutations
236
+ );
237
+ }
238
+
239
+ private extractPathKey(pathSegment: unknown): string {
240
+ if (pathSegment === null || pathSegment === undefined) {
241
+ return "";
242
+ }
243
+ if (typeof pathSegment === "string" || typeof pathSegment === "number") {
244
+ return String(pathSegment);
245
+ }
246
+ if (typeof pathSegment === "symbol") {
247
+ return pathSegment.toString();
248
+ }
249
+ if (typeof pathSegment === "object" && "key" in pathSegment) {
250
+ return String((pathSegment as { key: unknown }).key);
251
+ }
252
+ return String(pathSegment);
253
+ }
254
+
255
+ /**
256
+ * Setup validation that runs on every property change.
257
+ * Uses the ChangeTracker's onChangeValidator callback.
258
+ */
259
+ private setupUpdateValidation(): void {
260
+ const self = this;
261
+
262
+ this.tracker.setOnChangeValidator((path, newValue) => {
263
+ const originalValue = this.getValueAtPath(self._props, path);
264
+ this.setValueAtPath(self._props, path, newValue);
265
+
266
+ try {
267
+ if (
268
+ !self.validationConfig.persistInvalidMutations &&
269
+ (self as any)._validationError
270
+ ) {
271
+ this.setValueAtPath(self._props, path, originalValue);
272
+ return false;
273
+ }
274
+
275
+ if (self.entityHooks?.onBeforeUpdate && self.snapshot) {
276
+ const shouldContinue = self.entityHooks.onBeforeUpdate(
277
+ self as any,
278
+ self.snapshot
279
+ );
280
+ if (!shouldContinue) {
281
+ this.setValueAtPath(self._props, path, originalValue);
282
+ return false;
283
+ }
284
+ }
285
+
286
+ if (!self.validationConfig.throwOnError) {
287
+ const isValid = self.refreshValidationStateFromCurrentProps();
288
+ if (!isValid) {
289
+ if (!self.shouldPersistInvalidMutation()) {
290
+ this.setValueAtPath(self._props, path, originalValue);
291
+ }
292
+ return self.shouldPersistInvalidMutation();
293
+ }
294
+
295
+ this.setValueAtPath(self._props, path, originalValue);
296
+ return true;
297
+ }
298
+
299
+ const schemaError = self.entitySchema
300
+ ? self.validateSchema(self._props)
301
+ : null;
302
+
303
+ if (schemaError) {
304
+ this.setValueAtPath(self._props, path, originalValue);
305
+ throw schemaError;
306
+ }
307
+
308
+ if (self.entityHooks?.rules) {
309
+ self.beginValidationCycle();
310
+ self.entityHooks.rules(self as any);
311
+ const collected = [...self.issueCollector.getIssues()];
312
+
313
+ if (collected.length > 0) {
314
+ this.setValueAtPath(self._props, path, originalValue);
315
+ throw ValidationError.fromIssues(collected, {
316
+ entityName: self.constructor.name,
317
+ });
318
+ }
319
+ }
320
+
321
+ self.clearValidationError();
322
+ this.setValueAtPath(self._props, path, originalValue);
323
+ return true;
324
+ } catch (error) {
325
+ this.setValueAtPath(self._props, path, originalValue);
326
+ throw error;
327
+ }
328
+ });
329
+ }
330
+
331
+ private takeSnapshot(): void {
332
+ this.snapshot = this.deepCloneProps(this._props);
333
+ }
334
+
335
+ private deepCloneProps(obj: any, seen: WeakSet<object> = new WeakSet()): any {
336
+ if (obj === null || obj === undefined) return obj;
337
+ if (typeof obj !== "object") return obj;
338
+ if (obj instanceof Id) return obj;
339
+ if (obj instanceof Date) return new Date(obj.getTime());
340
+
341
+ if (seen.has(obj)) {
342
+ return obj;
343
+ }
344
+
345
+ if (obj instanceof BaseEntity) {
346
+ return obj;
347
+ }
348
+
349
+ if (
350
+ obj.constructor &&
351
+ obj.constructor.name !== "Object" &&
352
+ obj.constructor.name !== "Array"
353
+ ) {
354
+ if (
355
+ typeof obj.toJSON === "function" &&
356
+ typeof obj.equals === "function"
357
+ ) {
358
+ return obj;
359
+ }
360
+ }
361
+
362
+ seen.add(obj);
363
+
364
+ if (Array.isArray(obj)) {
365
+ return obj.map((item) => this.deepCloneProps(item, seen));
366
+ }
367
+
368
+ if (obj.constructor === Object) {
369
+ try {
370
+ return structuredClone(obj);
371
+ } catch {
372
+ const cloned: any = {};
373
+ for (const key in obj) {
374
+ if (Object.prototype.hasOwnProperty.call(obj, key)) {
375
+ cloned[key] = this.deepCloneProps(obj[key], seen);
376
+ }
377
+ }
378
+ return cloned;
379
+ }
380
+ }
381
+
382
+ return obj;
383
+ }
384
+
385
+ get id(): Id {
386
+ return this._props.id;
387
+ }
388
+
389
+ public isNew(): boolean {
390
+ return this._props.id.isNew();
391
+ }
392
+
393
+ /**
394
+ * Check equality with another entity by comparing IDs
395
+ */
396
+ equals(other: BaseEntity<T> | Id | string): boolean {
397
+ if (!other) {
398
+ return false;
399
+ }
400
+
401
+ if (other instanceof BaseEntity) {
402
+ return this.id.equals(other.id);
403
+ }
404
+
405
+ if (other instanceof Id) {
406
+ return this.id.equals(other);
407
+ }
408
+
409
+ if (typeof other === "string") {
410
+ return this.id.equals(other);
411
+ }
412
+
413
+ return false;
414
+ }
415
+
416
+ public get props(): T {
417
+ return this.proxiedProps;
418
+ }
419
+
420
+ /**
421
+ * Check if entity has validation errors (when throwOnError is false)
422
+ */
423
+ get hasValidationErrors(): boolean {
424
+ return !!(this as any)._validationError;
425
+ }
426
+
427
+ /**
428
+ * Get validation errors (when throwOnError is false)
429
+ */
430
+ get validationErrors(): ValidationError | undefined {
431
+ return (this as any)._validationError;
432
+ }
433
+
434
+ /**
435
+ * Returns all detected changes as AggregateChanges.
436
+ *
437
+ * @example
438
+ * ```typescript
439
+ * const changes = user.getChanges();
440
+ * const batch = changes.toBatchOperations();
441
+ *
442
+ * for (const del of batch.deletes) { ... }
443
+ * for (const create of batch.creates) { ... }
444
+ * for (const upd of batch.updates) { ... }
445
+ * ```
446
+ */
447
+ getChanges<TEntityMap = Record<string, any>>(): AggregateChanges<TEntityMap> {
448
+ return this.tracker.getChanges<TEntityMap>();
449
+ }
450
+
451
+ /**
452
+ * Returns the change history (for debugging).
453
+ */
454
+ getHistory(): HistoryEntry[] {
455
+ return this.tracker.getHistory();
456
+ }
457
+
458
+ /**
459
+ * Clears history and marks entity as "clean".
460
+ * Recursively marks all nested entities as clean.
461
+ */
462
+ markAsClean(): void {
463
+ this.tracker.markAsClean();
464
+ this.takeSnapshot();
465
+ this.forEachNestedEntity((entity) => entity.markAsClean());
466
+ }
467
+
468
+ /**
469
+ * Clears history, marks entity as "clean" and marks the Id as not new.
470
+ * Recursively marks all nested entities as persisted.
471
+ * Call this after successfully persisting to the database.
472
+ */
473
+ markAsPersisted(): void {
474
+ this.tracker.markAsClean();
475
+ this.takeSnapshot();
476
+ this.id.markAsNotNew();
477
+ this.forEachNestedEntity((entity) => entity.markAsPersisted());
478
+ }
479
+
480
+ /**
481
+ * Iterates over all nested entities (direct children only) and executes a callback.
482
+ * This includes entities in arrays and single entity properties.
483
+ */
484
+ private forEachNestedEntity(
485
+ callback: (entity: BaseEntity<any>) => void
486
+ ): void {
487
+ for (const value of Object.values(this._props)) {
488
+ if (value instanceof BaseEntity) {
489
+ callback(value);
490
+ } else if (Array.isArray(value)) {
491
+ for (const item of value) {
492
+ if (item instanceof BaseEntity) {
493
+ callback(item);
494
+ }
495
+ }
496
+ }
497
+ }
498
+ }
499
+
500
+ toJSON(): DeepJsonResult<T> {
501
+ return this.deepToJson(this._props) as DeepJsonResult<T>;
502
+ }
503
+
504
+ private deepToJson(obj: any): any {
505
+ if (obj === null || obj === undefined) return obj;
506
+ if (obj instanceof Id) return obj.value;
507
+ if (obj instanceof ValueObject) return obj.value;
508
+ if (obj instanceof Date) return obj.toISOString();
509
+ if (Array.isArray(obj)) return obj.map((item) => this.deepToJson(item));
510
+ if (obj instanceof BaseEntity) return obj.toJSON();
511
+ if (obj && typeof obj.toJSON === "function") return obj.toJSON();
512
+ if (typeof obj === "object") {
513
+ const result: any = {};
514
+ for (const key in obj) {
515
+ if (obj.hasOwnProperty(key)) result[key] = this.deepToJson(obj[key]);
516
+ }
517
+ return result;
518
+ }
519
+ return obj;
520
+ }
521
+
522
+ private setValueAtPath(obj: any, path: string, value: any): void {
523
+ if (!path) return;
524
+
525
+ const parts = path.split(/[.\[\]]+/).filter(Boolean);
526
+ let current = obj;
527
+
528
+ for (let i = 0; i < parts.length - 1; i++) {
529
+ const part = parts[i];
530
+ if (current[part] === null || current[part] === undefined) {
531
+ current[part] = {};
532
+ }
533
+ current = current[part];
534
+ }
535
+
536
+ current[parts[parts.length - 1]!] = value;
537
+ }
538
+
539
+ private getValueAtPath(obj: any, path: string): any {
540
+ if (!path) return obj;
541
+
542
+ const parts = path.split(/[.\[\]]+/).filter(Boolean);
543
+ let current = obj;
544
+
545
+ for (const part of parts) {
546
+ if (current === null || current === undefined) return undefined;
547
+ current = current[part];
548
+ }
549
+
550
+ return current;
551
+ }
552
+ }