@woltz/rich-domain 1.2.0 → 1.2.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 (143) hide show
  1. package/CHANGELOG.md +58 -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 +86 -93
  9. package/dist/base-entity.js.map +1 -1
  10. package/dist/change-tracker.d.ts +97 -0
  11. package/dist/change-tracker.d.ts.map +1 -0
  12. package/dist/change-tracker.js +758 -0
  13. package/dist/change-tracker.js.map +1 -0
  14. package/dist/constants.d.ts +7 -1
  15. package/dist/constants.d.ts.map +1 -1
  16. package/dist/constants.js +65 -0
  17. package/dist/constants.js.map +1 -1
  18. package/dist/criteria.d.ts +3 -3
  19. package/dist/criteria.d.ts.map +1 -1
  20. package/dist/criteria.js +6 -4
  21. package/dist/criteria.js.map +1 -1
  22. package/dist/crypto.d.ts +3 -0
  23. package/dist/crypto.d.ts.map +1 -0
  24. package/dist/crypto.js +29 -0
  25. package/dist/crypto.js.map +1 -0
  26. package/dist/domain-event.d.ts.map +1 -1
  27. package/dist/domain-event.js +0 -3
  28. package/dist/domain-event.js.map +1 -1
  29. package/dist/entity-changes.d.ts +84 -0
  30. package/dist/entity-changes.d.ts.map +1 -0
  31. package/dist/entity-changes.js +131 -0
  32. package/dist/entity-changes.js.map +1 -0
  33. package/dist/entity-schema-registry.d.ts +148 -0
  34. package/dist/entity-schema-registry.d.ts.map +1 -0
  35. package/dist/entity-schema-registry.js +213 -0
  36. package/dist/entity-schema-registry.js.map +1 -0
  37. package/dist/entity.d.ts +0 -6
  38. package/dist/entity.d.ts.map +1 -1
  39. package/dist/entity.js +0 -9
  40. package/dist/entity.js.map +1 -1
  41. package/dist/id.d.ts +11 -10
  42. package/dist/id.d.ts.map +1 -1
  43. package/dist/id.js +4 -28
  44. package/dist/id.js.map +1 -1
  45. package/dist/index.d.ts +9 -5
  46. package/dist/index.d.ts.map +1 -1
  47. package/dist/index.js +8 -11
  48. package/dist/index.js.map +1 -1
  49. package/dist/mapper.d.ts +1 -1
  50. package/dist/mapper.d.ts.map +1 -1
  51. package/dist/mapper.js.map +1 -1
  52. package/dist/paginated-result.d.ts.map +1 -1
  53. package/dist/paginated-result.js +0 -15
  54. package/dist/paginated-result.js.map +1 -1
  55. package/dist/repository/base-repository.d.ts +7 -33
  56. package/dist/repository/base-repository.d.ts.map +1 -1
  57. package/dist/repository/base-repository.js +0 -27
  58. package/dist/repository/base-repository.js.map +1 -1
  59. package/dist/repository/index.d.ts.map +1 -1
  60. package/dist/repository/index.js +0 -6
  61. package/dist/repository/index.js.map +1 -1
  62. package/dist/repository/unit-of-work.d.ts +0 -25
  63. package/dist/repository/unit-of-work.d.ts.map +1 -1
  64. package/dist/repository/unit-of-work.js +0 -28
  65. package/dist/repository/unit-of-work.js.map +1 -1
  66. package/dist/types/change-tracker.d.ts +196 -0
  67. package/dist/types/change-tracker.d.ts.map +1 -0
  68. package/dist/types/change-tracker.js +2 -0
  69. package/dist/types/change-tracker.js.map +1 -0
  70. package/dist/types/criteria.d.ts +5 -1
  71. package/dist/types/criteria.d.ts.map +1 -1
  72. package/dist/types/domain.d.ts +4 -6
  73. package/dist/types/domain.d.ts.map +1 -1
  74. package/dist/types/index.d.ts +1 -1
  75. package/dist/types/index.d.ts.map +1 -1
  76. package/dist/types/index.js +1 -1
  77. package/dist/types/index.js.map +1 -1
  78. package/dist/types/utils.d.ts +0 -1
  79. package/dist/types/utils.d.ts.map +1 -1
  80. package/dist/utils/criteria-operator-validation.d.ts +1 -0
  81. package/dist/utils/criteria-operator-validation.d.ts.map +1 -1
  82. package/dist/utils/criteria-operator-validation.js +39 -17
  83. package/dist/utils/criteria-operator-validation.js.map +1 -1
  84. package/dist/validation-error.d.ts.map +1 -1
  85. package/dist/validation-error.js +1 -6
  86. package/dist/validation-error.js.map +1 -1
  87. package/dist/value-object.d.ts +57 -8
  88. package/dist/value-object.d.ts.map +1 -1
  89. package/dist/value-object.js +49 -22
  90. package/dist/value-object.js.map +1 -1
  91. package/package.json +2 -1
  92. package/src/aggregate-changes.ts +335 -0
  93. package/src/base-entity.ts +102 -109
  94. package/src/change-tracker.ts +1062 -0
  95. package/src/constants.ts +75 -1
  96. package/src/criteria.ts +11 -4
  97. package/src/crypto.ts +31 -0
  98. package/src/domain-event.ts +0 -4
  99. package/src/entity-changes.ts +146 -0
  100. package/src/entity-schema-registry.ts +255 -0
  101. package/src/entity.ts +0 -11
  102. package/src/id.ts +17 -26
  103. package/src/index.ts +15 -19
  104. package/src/mapper.ts +4 -1
  105. package/src/paginated-result.ts +0 -21
  106. package/src/repository/base-repository.ts +7 -38
  107. package/src/repository/index.ts +0 -9
  108. package/src/repository/unit-of-work.ts +0 -29
  109. package/src/types/change-tracker.ts +233 -0
  110. package/src/types/criteria.ts +6 -1
  111. package/src/types/domain.ts +4 -8
  112. package/src/types/index.ts +1 -1
  113. package/src/types/utils.ts +0 -9
  114. package/src/utils/criteria-operator-validation.ts +57 -19
  115. package/src/validation-error.ts +1 -7
  116. package/src/value-object.ts +84 -24
  117. package/tests/aggregate-changes.test.ts +284 -0
  118. package/tests/criteria.test.ts +122 -161
  119. package/tests/entity-equality.test.ts +38 -61
  120. package/tests/entity-schema-registry.test.ts +382 -0
  121. package/tests/entity-validation.test.ts +7 -94
  122. package/tests/history-tracker.spec.ts +349 -617
  123. package/tests/id.test.ts +41 -44
  124. package/tests/load-test/data.json +346041 -0
  125. package/tests/load-test/entities.ts +97 -0
  126. package/tests/load-test/generate-data.ts +81 -0
  127. package/tests/load-test/lead-to-domain.mapper.ts +24 -0
  128. package/tests/load-test/load.test.ts +38 -0
  129. package/tests/repository.test.ts +30 -54
  130. package/tests/to-json.test.ts +14 -18
  131. package/tests/utils.ts +138 -102
  132. package/tests/value-objects.test.ts +57 -29
  133. package/dist/deep-proxy.d.ts +0 -36
  134. package/dist/deep-proxy.d.ts.map +0 -1
  135. package/dist/deep-proxy.js +0 -384
  136. package/dist/deep-proxy.js.map +0 -1
  137. package/dist/types/history-tracker.d.ts +0 -36
  138. package/dist/types/history-tracker.d.ts.map +0 -1
  139. package/dist/types/history-tracker.js +0 -2
  140. package/dist/types/history-tracker.js.map +0 -1
  141. package/src/deep-proxy.ts +0 -447
  142. package/src/types/history-tracker.ts +0 -45
  143. package/tests/entity.test.ts +0 -33
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
  }
@@ -2,10 +2,6 @@ import { Id } from "./id";
2
2
  import type { Criteria } from "./criteria";
3
3
  import type { Pagination, PaginationMeta, Filter } from "./types";
4
4
 
5
- // ============================================================================
6
- // Type Utilities
7
- // ============================================================================
8
-
9
5
  /**
10
6
  * Infers the JSON result type from T
11
7
  * - If T has toJson(), returns its return type
@@ -21,10 +17,6 @@ export type PaginatedJsonResult<T> = {
21
17
  meta: PaginationMeta;
22
18
  };
23
19
 
24
- // ============================================================================
25
- // PaginatedResult Class
26
- // ============================================================================
27
-
28
20
  export class PaginatedResult<T> {
29
21
  constructor(
30
22
  public readonly data: T[],
@@ -78,13 +70,11 @@ export class PaginatedResult<T> {
78
70
  total = result.length;
79
71
  }
80
72
 
81
- // Apply filters
82
73
  for (const filter of criteria.getFilters()) {
83
74
  result = result.filter((item) => applyFilter(item, filter));
84
75
  total = result.length;
85
76
  }
86
77
 
87
- // Apply ordering
88
78
  for (const order of criteria.getOrders().reverse()) {
89
79
  result.sort((a, b) => {
90
80
  const aVal = getNestedValue(a, order.field);
@@ -98,7 +88,6 @@ export class PaginatedResult<T> {
98
88
  });
99
89
  }
100
90
 
101
- // Apply pagination
102
91
  const pagination = criteria.getPagination();
103
92
  if (pagination && !criteria.hasSearch()) {
104
93
  result = result.slice(
@@ -108,7 +97,6 @@ export class PaginatedResult<T> {
108
97
  return PaginatedResult.create(result, pagination, total);
109
98
  }
110
99
 
111
- // No pagination - return all with default meta
112
100
  return PaginatedResult.create(
113
101
  result,
114
102
  { page: 1, limit: result.length, offset: 0 },
@@ -140,20 +128,16 @@ export class PaginatedResult<T> {
140
128
  private deepSerialize(obj: any): any {
141
129
  if (obj === null || obj === undefined) return obj;
142
130
 
143
- // Id → string
144
131
  if (obj instanceof Id) return obj.value;
145
132
 
146
- // Arrays → map recursively
147
133
  if (Array.isArray(obj)) {
148
134
  return obj.map((item) => this.deepSerialize(item));
149
135
  }
150
136
 
151
- // Objects with toJson() method (Entity/Aggregate/ValueObject)
152
137
  if (obj && typeof obj.toJson === "function") {
153
138
  return obj.toJson();
154
139
  }
155
140
 
156
- // Plain objects → serialize properties recursively
157
141
  if (typeof obj === "object") {
158
142
  const result: any = {};
159
143
  for (const key in obj) {
@@ -164,7 +148,6 @@ export class PaginatedResult<T> {
164
148
  return result;
165
149
  }
166
150
 
167
- // Primitives
168
151
  return obj;
169
152
  }
170
153
 
@@ -190,10 +173,6 @@ export class PaginatedResult<T> {
190
173
  }
191
174
  }
192
175
 
193
- // ============================================================================
194
- // Helper Functions (moved from criteria.ts)
195
- // ============================================================================
196
-
197
176
  function applyFilter<T>(item: T, filter: Filter): boolean {
198
177
  const value = getNestedValue(item, filter.field);
199
178
 
@@ -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
 
@@ -60,5 +29,5 @@ export abstract class Repository<
60
29
  > extends WriteAndRead<TDomain> {
61
30
  protected abstract readonly mapperToDomain: Mapper<unknown, TDomain>;
62
31
  protected abstract readonly mapperToPersistence: Mapper<TDomain, unknown>;
63
- abstract get model(): any;
32
+ protected abstract get model(): any;
64
33
  }
@@ -1,12 +1,3 @@
1
- // ============================================================================
2
- // Repository Module - Clean exports
3
- // ============================================================================
4
-
5
- // Mapper
6
1
  export { Mapper } from "../mapper";
7
-
8
- // Base implementations
9
2
  export * from "./base-repository";
10
-
11
- // Unit of Work
12
3
  export { UnitOfWork, BaseTransactionContext } from "./unit-of-work";
@@ -1,37 +1,8 @@
1
- // ============================================================================
2
- // Unit of Work - Simple transaction management
3
- // ============================================================================
4
-
5
1
  import type { IUnitOfWork, TransactionContext } from "../types";
6
2
 
7
3
  /**
8
4
  * Abstract Unit of Work
9
5
  * 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
6
  */
36
7
  export abstract class UnitOfWork implements IUnitOfWork {
37
8
  protected currentContext: TransactionContext | null = null;
@@ -0,0 +1,233 @@
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
+ }
222
+
223
+ export interface TrackedItem {
224
+ entity: any;
225
+ metadata: TrackedEntityMetadata;
226
+ originalState: any;
227
+ }
228
+
229
+ export interface ArrayState {
230
+ cloned: any[];
231
+ original: any[];
232
+ metadata: TrackedEntityMetadata;
233
+ }
@@ -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,9 +1,6 @@
1
- import { ValidationConfig } from "..";
2
1
  import { Id } from "../id";
3
2
  import { StandardSchema } from "./standard-schema";
4
3
 
5
- export type EntityId = string | number;
6
-
7
4
  export interface BaseProps {
8
5
  id: Id;
9
6
  }
@@ -22,15 +19,14 @@ export interface VOHooks<T, E> {
22
19
  rules?: (entity: E) => void;
23
20
  }
24
21
 
25
- // Specialized hooks for entities (with BaseProps)
26
22
  export interface EntityHooks<T extends BaseProps, E> {
27
23
  onBeforeUpdate?: (entity: E, snapshot: T) => boolean;
28
24
  onCreate?: (entity: E) => void;
29
25
  rules?: (entity: E) => void;
30
26
  }
31
27
 
32
- export interface EntityConstructor<T extends BaseProps, E> {
33
- new (props: T): E;
34
- validation?: DomainValidation<T>;
35
- hooks?: EntityHooks<T, E>;
28
+ export interface ValidationConfig {
29
+ onCreate?: boolean;
30
+ onUpdate?: boolean;
31
+ throwOnError?: boolean;
36
32
  }
@@ -1,7 +1,7 @@
1
1
  export * from "./criteria";
2
2
  export * from "./domain";
3
- export * from "./history-tracker";
4
3
  export * from "./standard-schema";
5
4
  export * from "./utils";
6
5
  export * from "./unit-of-work";
7
6
  export * from "./domain-event";
7
+ export * from "./change-tracker";
@@ -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;
@@ -8,28 +8,71 @@ import {
8
8
  StringOperators,
9
9
  } from "../types";
10
10
 
11
- export function isValidOperatorForType(
11
+ const FORCE_STRING_OPERATORS = new Set(["contains", "startsWith", "endsWith"]);
12
+
13
+ export function sanitizeFieldValue(
12
14
  value: unknown,
13
15
  operator: FilterOperator
14
- ): boolean {
15
- // Handle null/undefined
16
+ ): unknown {
16
17
  if (value === null || value === undefined) {
17
- return ["isNull", "isNotNull", "equals", "notEquals"].includes(operator);
18
+ return value;
19
+ }
20
+
21
+ if (Array.isArray(value)) {
22
+ return value.map((item) => sanitizeFieldValue(item, operator));
23
+ }
24
+
25
+ if (value instanceof Date) {
26
+ return value;
27
+ }
28
+
29
+ const stringValue = String(value).trim();
30
+
31
+ if (stringValue === "") {
32
+ return "";
33
+ }
34
+
35
+ if (operator && FORCE_STRING_OPERATORS.has(operator)) {
36
+ return stringValue;
37
+ }
38
+
39
+ if (stringValue === "true" || stringValue === "false") {
40
+ return stringValue === "true";
41
+ }
42
+
43
+ const numberValue = Number(stringValue);
44
+ if (!Number.isNaN(numberValue)) {
45
+ return numberValue;
46
+ }
47
+
48
+ const dateObj = new Date(stringValue);
49
+ if (!Number.isNaN(dateObj.getTime())) {
50
+ return dateObj;
18
51
  }
19
52
 
20
- // Special case: between operator with array [min, max]
21
- if (operator === "between" && Array.isArray(value) && value.length === 2) {
22
- // Validate based on the type of the first element
23
- const elementType = typeof value[0];
24
- if (elementType === "number" || value[0] instanceof Date) {
53
+ return stringValue;
54
+ }
55
+
56
+ export function isValidOperatorForType(
57
+ value: unknown,
58
+ operator: FilterOperator
59
+ ): boolean {
60
+ const sanitizedValue = sanitizeFieldValue(value, operator);
61
+
62
+ if (
63
+ operator === "between" &&
64
+ Array.isArray(sanitizedValue) &&
65
+ sanitizedValue.length === 2
66
+ ) {
67
+ const elementType = typeof sanitizedValue[0];
68
+ if (elementType === "number" || sanitizedValue[0] instanceof Date) {
25
69
  return true;
26
70
  }
27
71
  return false;
28
72
  }
29
73
 
30
- const valueType = typeof value;
74
+ const valueType = typeof sanitizedValue;
31
75
 
32
- // String operators
33
76
  if (valueType === "string") {
34
77
  const validOps: StringOperators[] = [
35
78
  "equals",
@@ -45,7 +88,6 @@ export function isValidOperatorForType(
45
88
  return validOps.includes(operator as StringOperators);
46
89
  }
47
90
 
48
- // Number operators
49
91
  if (valueType === "number") {
50
92
  const validOps: NumberOperators[] = [
51
93
  "equals",
@@ -63,7 +105,6 @@ export function isValidOperatorForType(
63
105
  return validOps.includes(operator as NumberOperators);
64
106
  }
65
107
 
66
- // Boolean operators
67
108
  if (valueType === "boolean") {
68
109
  const validOps: BooleanOperators[] = [
69
110
  "equals",
@@ -74,8 +115,7 @@ export function isValidOperatorForType(
74
115
  return validOps.includes(operator as BooleanOperators);
75
116
  }
76
117
 
77
- // Date operators
78
- if (value instanceof Date) {
118
+ if (sanitizedValue instanceof Date) {
79
119
  const validOps: DateOperators[] = [
80
120
  "equals",
81
121
  "notEquals",
@@ -92,13 +132,11 @@ export function isValidOperatorForType(
92
132
  return validOps.includes(operator as DateOperators);
93
133
  }
94
134
 
95
- // Array operators
96
- if (Array.isArray(value)) {
135
+ if (Array.isArray(sanitizedValue)) {
97
136
  const validOps: ArrayOperators[] = ["in", "notIn", "isNull", "isNotNull"];
98
137
  return validOps.includes(operator as ArrayOperators);
99
138
  }
100
139
 
101
- // For unknown types, allow all operators
102
140
  return true;
103
141
  }
104
142
 
@@ -168,4 +206,4 @@ export function getValidOperatorsForType(value: unknown): FilterOperator[] {
168
206
 
169
207
  export function isOperator(value: string): value is FilterOperator {
170
208
  return FILTER_OPERATORS.includes(value as FilterOperator);
171
- }
209
+ }
@@ -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;
@@ -9,7 +5,7 @@ export interface ValidationIssue {
9
5
 
10
6
  export class ValidationError extends Error {
11
7
  public readonly issues: ValidationIssue[];
12
- public readonly __isValidationError = true; // Brand for identification
8
+ public readonly __isValidationError = true;
13
9
 
14
10
  constructor(issues: ValidationIssue[], message?: string) {
15
11
  const errorMessage =
@@ -18,7 +14,6 @@ export class ValidationError extends Error {
18
14
  this.name = 'ValidationError';
19
15
  this.issues = issues;
20
16
 
21
- // Maintain proper stack trace
22
17
  if (Error.captureStackTrace) {
23
18
  Error.captureStackTrace(this, ValidationError);
24
19
  }
@@ -31,7 +26,6 @@ export class ValidationError extends Error {
31
26
  if (error instanceof ValidationError) {
32
27
  return true;
33
28
  }
34
- // Check by duck typing for cross-module compatibility
35
29
  return (
36
30
  error instanceof Error &&
37
31
  error.name === 'ValidationError' &&