@woltz/rich-domain 0.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 (178) hide show
  1. package/.github/workflows/ci.yml +40 -0
  2. package/.husky/commit-msg +1 -0
  3. package/.husky/pre-commit +1 -0
  4. package/.versionrc.json +21 -0
  5. package/.vscode/settings.json +3 -0
  6. package/CHANGELOG.md +81 -0
  7. package/LICENSE +21 -0
  8. package/README.md +712 -0
  9. package/commitlint.config.js +23 -0
  10. package/dist/base-entity.d.ts +67 -0
  11. package/dist/base-entity.d.ts.map +1 -0
  12. package/dist/base-entity.js +309 -0
  13. package/dist/base-entity.js.map +1 -0
  14. package/dist/constants.d.ts +3 -0
  15. package/dist/constants.d.ts.map +1 -0
  16. package/dist/constants.js +6 -0
  17. package/dist/constants.js.map +1 -0
  18. package/dist/criteria.d.ts +60 -0
  19. package/dist/criteria.d.ts.map +1 -0
  20. package/dist/criteria.js +214 -0
  21. package/dist/criteria.js.map +1 -0
  22. package/dist/deep-proxy.d.ts +34 -0
  23. package/dist/deep-proxy.d.ts.map +1 -0
  24. package/dist/deep-proxy.js +297 -0
  25. package/dist/deep-proxy.js.map +1 -0
  26. package/dist/domain-event-bus.d.ts +57 -0
  27. package/dist/domain-event-bus.d.ts.map +1 -0
  28. package/dist/domain-event-bus.js +112 -0
  29. package/dist/domain-event-bus.js.map +1 -0
  30. package/dist/domain-event.d.ts +55 -0
  31. package/dist/domain-event.d.ts.map +1 -0
  32. package/dist/domain-event.js +42 -0
  33. package/dist/domain-event.js.map +1 -0
  34. package/dist/entity.d.ts +13 -0
  35. package/dist/entity.d.ts.map +1 -0
  36. package/dist/entity.js +15 -0
  37. package/dist/entity.js.map +1 -0
  38. package/dist/filtering.d.ts +107 -0
  39. package/dist/filtering.d.ts.map +1 -0
  40. package/dist/filtering.js +202 -0
  41. package/dist/filtering.js.map +1 -0
  42. package/dist/id.d.ts +51 -0
  43. package/dist/id.d.ts.map +1 -0
  44. package/dist/id.js +84 -0
  45. package/dist/id.js.map +1 -0
  46. package/dist/index.d.ts +15 -0
  47. package/dist/index.d.ts.map +1 -0
  48. package/dist/index.js +25 -0
  49. package/dist/index.js.map +1 -0
  50. package/dist/ordering.d.ts +93 -0
  51. package/dist/ordering.d.ts.map +1 -0
  52. package/dist/ordering.js +154 -0
  53. package/dist/ordering.js.map +1 -0
  54. package/dist/paginated-result.d.ts +62 -0
  55. package/dist/paginated-result.d.ts.map +1 -0
  56. package/dist/paginated-result.js +201 -0
  57. package/dist/paginated-result.js.map +1 -0
  58. package/dist/pagination.d.ts +218 -0
  59. package/dist/pagination.d.ts.map +1 -0
  60. package/dist/pagination.js +281 -0
  61. package/dist/pagination.js.map +1 -0
  62. package/dist/repository/base-repository.d.ts +77 -0
  63. package/dist/repository/base-repository.d.ts.map +1 -0
  64. package/dist/repository/base-repository.js +80 -0
  65. package/dist/repository/base-repository.js.map +1 -0
  66. package/dist/repository/in-memory-repository.d.ts +46 -0
  67. package/dist/repository/in-memory-repository.d.ts.map +1 -0
  68. package/dist/repository/in-memory-repository.js +85 -0
  69. package/dist/repository/in-memory-repository.js.map +1 -0
  70. package/dist/repository/index.d.ts +42 -0
  71. package/dist/repository/index.d.ts.map +1 -0
  72. package/dist/repository/index.js +47 -0
  73. package/dist/repository/index.js.map +1 -0
  74. package/dist/repository/mapper.d.ts +56 -0
  75. package/dist/repository/mapper.d.ts.map +1 -0
  76. package/dist/repository/mapper.js +15 -0
  77. package/dist/repository/mapper.js.map +1 -0
  78. package/dist/repository/types.d.ts +87 -0
  79. package/dist/repository/types.d.ts.map +1 -0
  80. package/dist/repository/types.js +6 -0
  81. package/dist/repository/types.js.map +1 -0
  82. package/dist/repository/unit-of-work.d.ts +70 -0
  83. package/dist/repository/unit-of-work.d.ts.map +1 -0
  84. package/dist/repository/unit-of-work.js +122 -0
  85. package/dist/repository/unit-of-work.js.map +1 -0
  86. package/dist/repository.d.ts +2 -0
  87. package/dist/repository.d.ts.map +1 -0
  88. package/dist/repository.js +21 -0
  89. package/dist/repository.js.map +1 -0
  90. package/dist/specification.d.ts +102 -0
  91. package/dist/specification.d.ts.map +1 -0
  92. package/dist/specification.js +187 -0
  93. package/dist/specification.js.map +1 -0
  94. package/dist/types/criteria.d.ts +35 -0
  95. package/dist/types/criteria.d.ts.map +1 -0
  96. package/dist/types/criteria.js +17 -0
  97. package/dist/types/criteria.js.map +1 -0
  98. package/dist/types/domain.d.ts +30 -0
  99. package/dist/types/domain.d.ts.map +1 -0
  100. package/dist/types/domain.js +2 -0
  101. package/dist/types/domain.js.map +1 -0
  102. package/dist/types/history-tracker.d.ts +36 -0
  103. package/dist/types/history-tracker.d.ts.map +1 -0
  104. package/dist/types/history-tracker.js +2 -0
  105. package/dist/types/history-tracker.js.map +1 -0
  106. package/dist/types/index.d.ts +8 -0
  107. package/dist/types/index.d.ts.map +1 -0
  108. package/dist/types/index.js +8 -0
  109. package/dist/types/index.js.map +1 -0
  110. package/dist/types/repository.d.ts +43 -0
  111. package/dist/types/repository.d.ts.map +1 -0
  112. package/dist/types/repository.js +2 -0
  113. package/dist/types/repository.js.map +1 -0
  114. package/dist/types/standard-schema.d.ts +15 -0
  115. package/dist/types/standard-schema.d.ts.map +1 -0
  116. package/dist/types/standard-schema.js +2 -0
  117. package/dist/types/standard-schema.js.map +1 -0
  118. package/dist/types/unit-of-work.d.ts +39 -0
  119. package/dist/types/unit-of-work.d.ts.map +1 -0
  120. package/dist/types/unit-of-work.js +2 -0
  121. package/dist/types/unit-of-work.js.map +1 -0
  122. package/dist/types/utils.d.ts +14 -0
  123. package/dist/types/utils.d.ts.map +1 -0
  124. package/dist/types/utils.js +2 -0
  125. package/dist/types/utils.js.map +1 -0
  126. package/dist/types.d.ts +88 -0
  127. package/dist/types.d.ts.map +1 -0
  128. package/dist/types.js +12 -0
  129. package/dist/types.js.map +1 -0
  130. package/dist/validation-error.d.ts +42 -0
  131. package/dist/validation-error.d.ts.map +1 -0
  132. package/dist/validation-error.js +73 -0
  133. package/dist/validation-error.js.map +1 -0
  134. package/dist/value-object.d.ts +47 -0
  135. package/dist/value-object.d.ts.map +1 -0
  136. package/dist/value-object.js +136 -0
  137. package/dist/value-object.js.map +1 -0
  138. package/eslint.config.js +51 -0
  139. package/jest.config.js +21 -0
  140. package/package.json +58 -0
  141. package/src/base-entity.ts +401 -0
  142. package/src/constants.ts +7 -0
  143. package/src/criteria.ts +291 -0
  144. package/src/deep-proxy.ts +339 -0
  145. package/src/domain-event-bus.ts +166 -0
  146. package/src/domain-event.ts +90 -0
  147. package/src/entity.ts +16 -0
  148. package/src/id.ts +94 -0
  149. package/src/index.ts +33 -0
  150. package/src/paginated-result.ts +274 -0
  151. package/src/repository/base-repository.ts +152 -0
  152. package/src/repository/in-memory-repository.ts +104 -0
  153. package/src/repository/index.ts +55 -0
  154. package/src/repository/mapper.ts +74 -0
  155. package/src/repository/unit-of-work.ts +148 -0
  156. package/src/types/criteria.ts +79 -0
  157. package/src/types/domain.ts +37 -0
  158. package/src/types/history-tracker.ts +45 -0
  159. package/src/types/index.ts +7 -0
  160. package/src/types/repository.ts +51 -0
  161. package/src/types/standard-schema.ts +19 -0
  162. package/src/types/unit-of-work.ts +46 -0
  163. package/src/types/utils.ts +29 -0
  164. package/src/validation-error.ts +97 -0
  165. package/src/value-object.ts +187 -0
  166. package/tests/criteria.test.ts +432 -0
  167. package/tests/domain-events.test.ts +445 -0
  168. package/tests/entity-equality.test.ts +487 -0
  169. package/tests/entity-validation.test.ts +339 -0
  170. package/tests/entity.test.ts +33 -0
  171. package/tests/history-tracker.spec.ts +667 -0
  172. package/tests/id.test.ts +341 -0
  173. package/tests/repository.test.ts +641 -0
  174. package/tests/to-json.test.ts +91 -0
  175. package/tests/utils.ts +151 -0
  176. package/tests/value-object-validation.test.ts +228 -0
  177. package/tests/value-objects.test.ts +52 -0
  178. package/tsconfig.json +31 -0
@@ -0,0 +1,148 @@
1
+ // ============================================================================
2
+ // Unit of Work - Simple transaction management
3
+ // ============================================================================
4
+
5
+ import type { IUnitOfWork, TransactionContext } from "../types";
6
+
7
+ /**
8
+ * Abstract Unit of Work
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
+ */
36
+ export abstract class UnitOfWork implements IUnitOfWork {
37
+ protected currentContext: TransactionContext | null = null;
38
+ protected repositoryCache: Map<string, any> = new Map();
39
+
40
+ abstract begin(): Promise<TransactionContext>;
41
+
42
+ /**
43
+ * Execute work within a transaction
44
+ * Auto-commits on success, rolls back on error
45
+ */
46
+ async transaction<T>(
47
+ work: (ctx: TransactionContext) => Promise<T>
48
+ ): Promise<T> {
49
+ const ctx = await this.begin();
50
+
51
+ try {
52
+ const result = await work(ctx);
53
+ await ctx.commit();
54
+ return result;
55
+ } catch (error) {
56
+ if (ctx.isActive()) {
57
+ await ctx.rollback();
58
+ }
59
+ throw error;
60
+ } finally {
61
+ this.currentContext = null;
62
+ this.repositoryCache.clear();
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Get repository instance (cached per transaction)
68
+ */
69
+ getRepository<TRepo>(
70
+ RepositoryClass: new (...args: any[]) => TRepo
71
+ ): TRepo {
72
+ const key = RepositoryClass.name;
73
+
74
+ if (this.repositoryCache.has(key)) {
75
+ return this.repositoryCache.get(key);
76
+ }
77
+
78
+ const repo = this.createRepository(RepositoryClass);
79
+ this.repositoryCache.set(key, repo);
80
+ return repo;
81
+ }
82
+
83
+ /**
84
+ * Create repository instance - implement in subclass
85
+ */
86
+ protected abstract createRepository<TRepo>(
87
+ RepositoryClass: new (...args: any[]) => TRepo
88
+ ): TRepo;
89
+ }
90
+
91
+ /**
92
+ * Base Transaction Context
93
+ */
94
+ export abstract class BaseTransactionContext implements TransactionContext {
95
+ protected _isActive = true;
96
+
97
+ abstract commit(): Promise<void>;
98
+ abstract rollback(): Promise<void>;
99
+
100
+ isActive(): boolean {
101
+ return this._isActive;
102
+ }
103
+
104
+ protected markInactive(): void {
105
+ this._isActive = false;
106
+ }
107
+ }
108
+
109
+ /**
110
+ * In-Memory Unit of Work (for testing)
111
+ */
112
+ export class InMemoryUnitOfWork extends UnitOfWork {
113
+ private committed = false;
114
+ private rolledBack = false;
115
+
116
+ async begin(): Promise<TransactionContext> {
117
+ this.currentContext = new InMemoryTransactionContext();
118
+ this.committed = false;
119
+ this.rolledBack = false;
120
+ return this.currentContext;
121
+ }
122
+
123
+ protected createRepository<TRepo>(
124
+ RepositoryClass: new (...args: any[]) => TRepo
125
+ ): TRepo {
126
+ // For in-memory, just create a new instance
127
+ // In real implementation, pass transaction client
128
+ return new RepositoryClass();
129
+ }
130
+
131
+ isCommitted(): boolean {
132
+ return this.committed;
133
+ }
134
+
135
+ isRolledBack(): boolean {
136
+ return this.rolledBack;
137
+ }
138
+ }
139
+
140
+ class InMemoryTransactionContext extends BaseTransactionContext {
141
+ async commit(): Promise<void> {
142
+ this.markInactive();
143
+ }
144
+
145
+ async rollback(): Promise<void> {
146
+ this.markInactive();
147
+ }
148
+ }
@@ -0,0 +1,79 @@
1
+ import { Primitive } from "./utils";
2
+
3
+ export const FilterOperator = [
4
+ "equals",
5
+ "notEquals",
6
+ "greaterThan",
7
+ "greaterThanOrEqual",
8
+ "lessThan",
9
+ "lessThanOrEqual",
10
+ "contains",
11
+ "startsWith",
12
+ "endsWith",
13
+ "in",
14
+ "notIn",
15
+ "between",
16
+ "isNull",
17
+ "isNotNull",
18
+ ] as const;
19
+
20
+ export type FilterValueFor<T> =
21
+ | T // equals, notEquals
22
+ | (T extends number | Date
23
+ ? [T, T] // between
24
+ : never)
25
+ | T[] // in, notIn
26
+ | null;
27
+
28
+ export type PathValue<
29
+ T,
30
+ P extends string
31
+ > = P extends `${infer K}.${infer Rest}`
32
+ ? K extends keyof T
33
+ ? PathValue<T[K], Rest>
34
+ : never
35
+ : P extends keyof T
36
+ ? T[P]
37
+ : never;
38
+
39
+ export type FilterOperator = (typeof FilterOperator)[number];
40
+
41
+ export interface Filter<TField = string, TValue = unknown> {
42
+ field: TField;
43
+ operator: FilterOperator;
44
+ value: TValue;
45
+ }
46
+
47
+ export type TypedFilter<T> = {
48
+ [K in FieldPath<T>]: Filter<K, FilterValueFor<PathValue<T, K>>>;
49
+ }[FieldPath<T>];
50
+
51
+ export type OrderDirection = "asc" | "desc";
52
+
53
+ export interface Order {
54
+ field: string;
55
+ direction: OrderDirection;
56
+ }
57
+
58
+ export interface Pagination {
59
+ page: number;
60
+ limit: number;
61
+ offset: number;
62
+ }
63
+
64
+ export interface PaginationMeta {
65
+ page: number;
66
+ limit: number;
67
+ total: number;
68
+ totalPages: number;
69
+ hasNext: boolean;
70
+ hasPrevious: boolean;
71
+ }
72
+
73
+ export type FieldPath<T> = {
74
+ [K in keyof T & string]: T[K] extends Primitive
75
+ ? K
76
+ : T[K] extends Array<infer U>
77
+ ? K | `${K}.${FieldPath<U>}`
78
+ : K | `${K}.${FieldPath<T[K]>}`;
79
+ }[keyof T & string];
@@ -0,0 +1,37 @@
1
+ import { StandardSchema, ValidationConfig } from "..";
2
+ import { Id } from "../id";
3
+
4
+ export type EntityId = string | number;
5
+
6
+ export interface BaseProps {
7
+ id: Id;
8
+ }
9
+
10
+ interface DomainValidation<T> {
11
+ schema: StandardSchema<T>;
12
+ config?: ValidationConfig;
13
+ }
14
+
15
+ export type EntityValidation<T> = DomainValidation<T>;
16
+ export type VOValidation<T> = DomainValidation<T>;
17
+
18
+
19
+ export interface VOHooks<T, E> {
20
+ onBeforeUpdate?: (entity: E, snapshot: T) => boolean;
21
+ onCreate?: (entity: E) => void;
22
+ rules?: (entity: E) => void;
23
+ defaultValues?: Partial<T>;
24
+ }
25
+
26
+ // Specialized hooks for entities (with BaseProps)
27
+ export interface EntityHooks<T extends BaseProps, E> {
28
+ onBeforeUpdate?: (entity: E, snapshot: T) => boolean;
29
+ onCreate?: (entity: E) => void;
30
+ rules?: (entity: E) => void;
31
+ }
32
+
33
+ export interface EntityConstructor<T extends BaseProps, E> {
34
+ new (props: T): E;
35
+ validation?: DomainValidation<T>;
36
+ hooks?: EntityHooks<T, E>;
37
+ }
@@ -0,0 +1,45 @@
1
+ import { BaseProps } from "..";
2
+ import { IsArray, NonUndefined, UnwrapArray } from "./utils";
3
+
4
+ export interface ChangeEvent<T> {
5
+ previous: T | undefined;
6
+ current: T;
7
+ path: string;
8
+ }
9
+
10
+ export interface ArrayChangeEvent<T> {
11
+ toCreate: T[];
12
+ toUpdate: T[];
13
+ toDelete: T[];
14
+ path: string;
15
+ }
16
+
17
+ export type PropertySubscriber<T> = (event: ChangeEvent<T>) => void;
18
+ export type ArraySubscriber<T> = (event: ArrayChangeEvent<T>) => void;
19
+
20
+ export interface PropertySubscription<T> {
21
+ onChange: PropertySubscriber<T>;
22
+ }
23
+
24
+ export interface ArraySubscription<T> {
25
+ onChange: ArraySubscriber<T>;
26
+ }
27
+
28
+ export type SubscriptionConfig<T extends BaseProps> = {
29
+ [K in keyof T]?: IsArray<NonUndefined<T[K]>> extends true
30
+ ? ArraySubscription<UnwrapArray<NonUndefined<T[K]>>>
31
+ : PropertySubscription<NonUndefined<T[K]>>;
32
+ };
33
+
34
+ export interface ValidationConfig {
35
+ onCreate?: boolean;
36
+ onUpdate?: boolean;
37
+ throwOnError?: boolean;
38
+ }
39
+
40
+ export interface HistoryEntry {
41
+ path: string;
42
+ previousValue: any;
43
+ currentValue: any;
44
+ timestamp: number;
45
+ }
@@ -0,0 +1,7 @@
1
+ export * from "./criteria";
2
+ export * from "./domain";
3
+ export * from "./history-tracker";
4
+ export * from "./standard-schema";
5
+ export * from "./utils";
6
+ export * from "./repository";
7
+ export * from "./unit-of-work";
@@ -0,0 +1,51 @@
1
+ import { Criteria } from "../criteria";
2
+ import { Aggregate } from "../entity";
3
+ import { Id } from "../id";
4
+ import { PaginatedResult } from "../paginated-result";
5
+
6
+ export interface IRepository<TDomain extends Aggregate<any>> {
7
+ /**
8
+ * Find by ID
9
+ */
10
+ findById(id: Id): Promise<TDomain | null>;
11
+
12
+ /**
13
+ * Find using criteria (filtering, ordering, pagination)
14
+ */
15
+ find(criteria: Criteria<TDomain>): Promise<PaginatedResult<TDomain>>;
16
+
17
+ /**
18
+ * Find all (with optional criteria)
19
+ */
20
+ findAll(criteria?: Criteria<TDomain>): Promise<TDomain[]>;
21
+
22
+ /**
23
+ * Find one (first matching criteria)
24
+ */
25
+ findOne(criteria: Criteria<TDomain>): Promise<TDomain | null>;
26
+
27
+ /**
28
+ * Save (insert or update based on aggregate.isNew)
29
+ */
30
+ save(aggregate: TDomain): Promise<void>;
31
+
32
+ /**
33
+ * Delete aggregate
34
+ */
35
+ delete(aggregate: TDomain): Promise<void>;
36
+
37
+ /**
38
+ * Delete by ID
39
+ */
40
+ deleteById(id: Id): Promise<void>;
41
+
42
+ /**
43
+ * Check if exists
44
+ */
45
+ exists(id: Id): Promise<boolean>;
46
+
47
+ /**
48
+ * Count matching criteria
49
+ */
50
+ count(criteria?: Criteria<TDomain>): Promise<number>;
51
+ }
@@ -0,0 +1,19 @@
1
+ export interface StandardSchemaIssue {
2
+ message: string;
3
+ path?: ReadonlyArray<unknown>;
4
+ }
5
+
6
+ export interface StandardSchemaResult<T> {
7
+ value?: T;
8
+ issues?: ReadonlyArray<StandardSchemaIssue>;
9
+ }
10
+
11
+ export interface StandardSchemaProps<T> {
12
+ validate: (
13
+ value: unknown
14
+ ) => StandardSchemaResult<T> | Promise<StandardSchemaResult<T>>;
15
+ }
16
+
17
+ export interface StandardSchema<T = unknown> {
18
+ "~standard": StandardSchemaProps<T>;
19
+ }
@@ -0,0 +1,46 @@
1
+ import { Aggregate } from "../entity";
2
+ import { IRepository } from "./repository";
3
+
4
+ /**
5
+ * Transaction context for Unit of Work
6
+ */
7
+ export interface TransactionContext {
8
+ /**
9
+ * Commit all changes
10
+ */
11
+ commit(): Promise<void>;
12
+
13
+ /**
14
+ * Rollback all changes
15
+ */
16
+ rollback(): Promise<void>;
17
+
18
+ /**
19
+ * Check if transaction is active
20
+ */
21
+ isActive(): boolean;
22
+ }
23
+
24
+ /**
25
+ * Unit of Work interface
26
+ * Manages transactions across multiple repositories
27
+ */
28
+ export interface IUnitOfWork {
29
+ /**
30
+ * Start a new transaction
31
+ */
32
+ begin(): Promise<TransactionContext>;
33
+
34
+ /**
35
+ * Execute work within a transaction
36
+ * Auto-commits on success, rolls back on error
37
+ */
38
+ transaction<T>(work: (ctx: TransactionContext) => Promise<T>): Promise<T>;
39
+
40
+ /**
41
+ * Get repository within transaction context
42
+ */
43
+ getRepository<TDomain extends Aggregate<any>>(
44
+ repository: new (...args: any[]) => IRepository<TDomain>
45
+ ): IRepository<TDomain>;
46
+ }
@@ -0,0 +1,29 @@
1
+ import { Id } from "../id";
2
+
3
+ export type DeepJsonResult<T> = {
4
+ [K in keyof T]: T[K] extends Id
5
+ ? string
6
+ : T[K] extends { toJson(): infer U }
7
+ ? U
8
+ : T[K] extends Array<infer U>
9
+ ? U extends { toJson(): infer V }
10
+ ? V[]
11
+ : U extends Id
12
+ ? string[]
13
+ : U[]
14
+ : T[K];
15
+ };
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
+ export type Primitive = string | number | boolean | Date | null | undefined;
26
+
27
+ export type UnwrapArray<T> = T extends Array<infer U> ? U : never;
28
+ export type IsArray<T> = T extends Array<any> ? true : false;
29
+ export type NonUndefined<T> = T extends undefined ? never : T;
@@ -0,0 +1,97 @@
1
+ // ============================================================================
2
+ // Validation Error - Domain Validation Errors
3
+ // ============================================================================
4
+
5
+ export interface ValidationIssue {
6
+ path: string[];
7
+ message: string;
8
+ }
9
+
10
+ export class ValidationError extends Error {
11
+ public readonly issues: ValidationIssue[];
12
+ public readonly __isValidationError = true; // Brand for identification
13
+
14
+ constructor(issues: ValidationIssue[], message?: string) {
15
+ const errorMessage =
16
+ message || `Validation failed: ${issues.map(i => i.message).join(', ')}`;
17
+ super(errorMessage);
18
+ this.name = 'ValidationError';
19
+ this.issues = issues;
20
+
21
+ // Maintain proper stack trace
22
+ if (Error.captureStackTrace) {
23
+ Error.captureStackTrace(this, ValidationError);
24
+ }
25
+ }
26
+
27
+ /**
28
+ * Check if an error is a ValidationError (works across module boundaries)
29
+ */
30
+ static isValidationError(error: unknown): error is ValidationError {
31
+ if (error instanceof ValidationError) {
32
+ return true;
33
+ }
34
+ // Check by duck typing for cross-module compatibility
35
+ return (
36
+ error instanceof Error &&
37
+ error.name === 'ValidationError' &&
38
+ 'issues' in error &&
39
+ Array.isArray((error as any).issues)
40
+ );
41
+ }
42
+
43
+ /**
44
+ * Get all error messages as a simple array
45
+ */
46
+ getMessages(): string[] {
47
+ return this.issues.map(i => i.message);
48
+ }
49
+
50
+ /**
51
+ * Get errors for a specific field path
52
+ */
53
+ getErrorsForPath(path: string): ValidationIssue[] {
54
+ return this.issues.filter(i => i.path.join('.') === path);
55
+ }
56
+
57
+ /**
58
+ * Check if a specific path has errors
59
+ */
60
+ hasErrorsForPath(path: string): boolean {
61
+ return this.getErrorsForPath(path).length > 0;
62
+ }
63
+
64
+ /**
65
+ * Convert to a plain object for serialization
66
+ */
67
+ toJSON(): { name: string; message: string; issues: ValidationIssue[] } {
68
+ return {
69
+ name: this.name,
70
+ message: this.message,
71
+ issues: this.issues,
72
+ };
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Helper to create a single validation issue
78
+ */
79
+ export function createValidationIssue(
80
+ path: string | string[],
81
+ message: string
82
+ ): ValidationIssue {
83
+ return {
84
+ path: Array.isArray(path) ? path : path.split('.'),
85
+ message,
86
+ };
87
+ }
88
+
89
+ /**
90
+ * Helper to throw a validation error with a single issue
91
+ */
92
+ export function throwValidationError(
93
+ path: string | string[],
94
+ message: string
95
+ ): never {
96
+ throw new ValidationError([createValidationIssue(path, message)]);
97
+ }