@woltz/rich-domain 0.2.2 → 1.1.0

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 (148) hide show
  1. package/CHANGELOG.md +23 -75
  2. package/LICENSE +20 -20
  3. package/README.md +37 -20
  4. package/dist/base-entity.d.ts +2 -2
  5. package/dist/base-entity.d.ts.map +1 -1
  6. package/dist/base-entity.js +6 -4
  7. package/dist/base-entity.js.map +1 -1
  8. package/dist/criteria.d.ts +5 -11
  9. package/dist/criteria.d.ts.map +1 -1
  10. package/dist/criteria.js +4 -3
  11. package/dist/criteria.js.map +1 -1
  12. package/dist/deep-proxy.d.ts +3 -1
  13. package/dist/deep-proxy.d.ts.map +1 -1
  14. package/dist/deep-proxy.js +116 -29
  15. package/dist/deep-proxy.js.map +1 -1
  16. package/dist/domain-event-bus.d.ts +5 -6
  17. package/dist/domain-event-bus.d.ts.map +1 -1
  18. package/dist/domain-event-bus.js +3 -11
  19. package/dist/domain-event-bus.js.map +1 -1
  20. package/dist/domain-event.d.ts +1 -31
  21. package/dist/domain-event.d.ts.map +1 -1
  22. package/dist/domain-event.js +2 -1
  23. package/dist/domain-event.js.map +1 -1
  24. package/dist/entity.d.ts +2 -2
  25. package/dist/entity.js +1 -1
  26. package/dist/exceptions.d.ts +251 -0
  27. package/dist/exceptions.d.ts.map +1 -0
  28. package/dist/exceptions.js +321 -0
  29. package/dist/exceptions.js.map +1 -0
  30. package/dist/id.d.ts +3 -3
  31. package/dist/id.d.ts.map +1 -1
  32. package/dist/id.js +15 -4
  33. package/dist/id.js.map +1 -1
  34. package/dist/index.d.ts +2 -5
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +2 -8
  37. package/dist/index.js.map +1 -1
  38. package/dist/paginated-result.d.ts.map +1 -1
  39. package/dist/paginated-result.js +12 -1
  40. package/dist/paginated-result.js.map +1 -1
  41. package/dist/repository/index.d.ts +2 -39
  42. package/dist/repository/index.d.ts.map +1 -1
  43. package/dist/repository/index.js +2 -39
  44. package/dist/repository/index.js.map +1 -1
  45. package/dist/repository/unit-of-work.d.ts +0 -11
  46. package/dist/repository/unit-of-work.d.ts.map +1 -1
  47. package/dist/repository/unit-of-work.js +0 -35
  48. package/dist/repository/unit-of-work.js.map +1 -1
  49. package/dist/types/criteria.d.ts +6 -2
  50. package/dist/types/criteria.d.ts.map +1 -1
  51. package/dist/types/criteria.js +1 -1
  52. package/dist/types/criteria.js.map +1 -1
  53. package/dist/types/domain-event.d.ts +32 -0
  54. package/dist/types/domain-event.d.ts.map +1 -0
  55. package/dist/types/domain-event.js +2 -0
  56. package/dist/types/domain-event.js.map +1 -0
  57. package/dist/types/domain.d.ts +2 -2
  58. package/dist/types/domain.d.ts.map +1 -1
  59. package/dist/types/history-tracker.d.ts +1 -1
  60. package/dist/types/history-tracker.d.ts.map +1 -1
  61. package/dist/types/index.d.ts +1 -0
  62. package/dist/types/index.d.ts.map +1 -1
  63. package/dist/types/index.js +1 -0
  64. package/dist/types/index.js.map +1 -1
  65. package/dist/value-object.d.ts +1 -1
  66. package/dist/value-object.d.ts.map +1 -1
  67. package/dist/value-object.js +2 -5
  68. package/dist/value-object.js.map +1 -1
  69. package/eslint.config.js +3 -3
  70. package/jest.config.js +1 -1
  71. package/package.json +14 -20
  72. package/src/base-entity.ts +6 -5
  73. package/src/criteria.ts +11 -11
  74. package/src/deep-proxy.ts +447 -339
  75. package/src/domain-event-bus.ts +152 -166
  76. package/src/domain-event.ts +53 -90
  77. package/src/entity.ts +16 -16
  78. package/src/exceptions.ts +435 -0
  79. package/src/id.ts +107 -94
  80. package/src/index.ts +26 -9
  81. package/src/paginated-result.ts +14 -1
  82. package/src/repository/index.ts +2 -44
  83. package/src/repository/unit-of-work.ts +1 -44
  84. package/src/types/criteria.ts +7 -2
  85. package/src/types/domain-event.ts +38 -0
  86. package/src/types/domain.ts +2 -3
  87. package/src/types/history-tracker.ts +1 -1
  88. package/src/types/index.ts +1 -0
  89. package/src/validation-error.ts +97 -97
  90. package/src/value-object.ts +3 -6
  91. package/tests/criteria.test.ts +8 -0
  92. package/tests/domain-events.test.ts +431 -445
  93. package/tests/entity-validation.test.ts +2 -2
  94. package/tests/entity.test.ts +33 -33
  95. package/tests/history-tracker.spec.ts +57 -17
  96. package/tests/id.test.ts +341 -341
  97. package/tests/repository.test.ts +8 -4
  98. package/tests/to-json.test.ts +103 -91
  99. package/tests/utils.ts +254 -151
  100. package/tests/value-object-validation.test.ts +0 -9
  101. package/tests/value-objects.test.ts +52 -52
  102. package/tsconfig.json +2 -24
  103. package/.github/workflows/ci.yml +0 -40
  104. package/.husky/commit-msg +0 -1
  105. package/.husky/pre-commit +0 -1
  106. package/.vscode/settings.json +0 -3
  107. package/commitlint.config.js +0 -23
  108. package/dist/filtering.d.ts +0 -107
  109. package/dist/filtering.d.ts.map +0 -1
  110. package/dist/filtering.js +0 -202
  111. package/dist/filtering.js.map +0 -1
  112. package/dist/ordering.d.ts +0 -93
  113. package/dist/ordering.d.ts.map +0 -1
  114. package/dist/ordering.js +0 -154
  115. package/dist/ordering.js.map +0 -1
  116. package/dist/pagination.d.ts +0 -218
  117. package/dist/pagination.d.ts.map +0 -1
  118. package/dist/pagination.js +0 -281
  119. package/dist/pagination.js.map +0 -1
  120. package/dist/repository/in-memory-repository.d.ts +0 -50
  121. package/dist/repository/in-memory-repository.d.ts.map +0 -1
  122. package/dist/repository/in-memory-repository.js +0 -93
  123. package/dist/repository/in-memory-repository.js.map +0 -1
  124. package/dist/repository/mapper.d.ts +0 -56
  125. package/dist/repository/mapper.d.ts.map +0 -1
  126. package/dist/repository/mapper.js +0 -15
  127. package/dist/repository/mapper.js.map +0 -1
  128. package/dist/repository/types.d.ts +0 -87
  129. package/dist/repository/types.d.ts.map +0 -1
  130. package/dist/repository/types.js +0 -6
  131. package/dist/repository/types.js.map +0 -1
  132. package/dist/repository.d.ts +0 -2
  133. package/dist/repository.d.ts.map +0 -1
  134. package/dist/repository.js +0 -21
  135. package/dist/repository.js.map +0 -1
  136. package/dist/specification.d.ts +0 -102
  137. package/dist/specification.d.ts.map +0 -1
  138. package/dist/specification.js +0 -187
  139. package/dist/specification.js.map +0 -1
  140. package/dist/types/repository.d.ts +0 -43
  141. package/dist/types/repository.d.ts.map +0 -1
  142. package/dist/types/repository.js +0 -2
  143. package/dist/types/repository.js.map +0 -1
  144. package/dist/types.d.ts +0 -88
  145. package/dist/types.d.ts.map +0 -1
  146. package/dist/types.js +0 -12
  147. package/dist/types.js.map +0 -1
  148. package/src/repository/in-memory-repository.ts +0 -116
@@ -70,7 +70,20 @@ export class PaginatedResult<T> {
70
70
  result = result.filter((item) => applyFilter(item, filter));
71
71
  }
72
72
 
73
- const total = result.length;
73
+ let total = result.length;
74
+
75
+ const search = criteria.getSearch();
76
+ if (search) {
77
+ result = result.filter((item) => {
78
+ return search.fields.some((field) => {
79
+ return String(getNestedValue(item, field))
80
+ .toLowerCase()
81
+ .includes(search.value.toLowerCase());
82
+ });
83
+ });
84
+
85
+ total = result.length;
86
+ }
74
87
 
75
88
  // Apply ordering
76
89
  for (const order of criteria.getOrders().reverse()) {
@@ -6,49 +6,7 @@
6
6
  export { Mapper } from "../mapper";
7
7
 
8
8
  // Base implementations
9
- export { Repository as BaseRepository } from "./base-repository";
10
- export { InMemoryRepository } from "./in-memory-repository";
9
+ export * from "./base-repository";
11
10
 
12
11
  // Unit of Work
13
- export {
14
- UnitOfWork,
15
- BaseTransactionContext,
16
- InMemoryUnitOfWork,
17
- } from "./unit-of-work";
18
-
19
- /**
20
- * QUICK START:
21
- *
22
- * 1. For Testing:
23
- * ```ts
24
- * import { InMemoryRepository } from 'rich-domain';
25
- *
26
- * const userRepo = new InMemoryRepository<User>();
27
- * await userRepo.save(user);
28
- * const found = await userRepo.findById(user.id);
29
- * ```
30
- *
31
- * 2. For Production (Prisma, TypeORM, etc):
32
- * - Extend BaseRepository
33
- * - Implement abstract methods
34
- * - Create a Mapper
35
- * - See examples/ folder for reference
36
- *
37
- * 3. With Criteria:
38
- * ```ts
39
- * const result = await userRepo.find(
40
- * Criteria.create<User>()
41
- * .whereEquals('status', 'active')
42
- * .orderByDesc('createdAt')
43
- * .paginate(1, 10)
44
- * );
45
- * ```
46
- *
47
- * 4. With Unit of Work:
48
- * ```ts
49
- * await uow.transaction(async (ctx) => {
50
- * const userRepo = uow.getRepository(UserRepository);
51
- * await userRepo.save(user);
52
- * });
53
- * ```
54
- */
12
+ export { UnitOfWork, BaseTransactionContext } from "./unit-of-work";
@@ -66,9 +66,7 @@ export abstract class UnitOfWork implements IUnitOfWork {
66
66
  /**
67
67
  * Get repository instance (cached per transaction)
68
68
  */
69
- getRepository<TRepo>(
70
- RepositoryClass: new (...args: any[]) => TRepo
71
- ): TRepo {
69
+ getRepository<TRepo>(RepositoryClass: new (...args: any[]) => TRepo): TRepo {
72
70
  const key = RepositoryClass.name;
73
71
 
74
72
  if (this.repositoryCache.has(key)) {
@@ -105,44 +103,3 @@ export abstract class BaseTransactionContext implements TransactionContext {
105
103
  this._isActive = false;
106
104
  }
107
105
  }
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
- }
@@ -1,6 +1,6 @@
1
1
  import { Primitive } from "./utils";
2
2
 
3
- export const FilterOperator = [
3
+ export const FILTER_OPERATORS = [
4
4
  "equals",
5
5
  "notEquals",
6
6
  "greaterThan",
@@ -36,7 +36,7 @@ export type PathValue<
36
36
  ? T[P]
37
37
  : never;
38
38
 
39
- export type FilterOperator = (typeof FilterOperator)[number];
39
+ export type FilterOperator = (typeof FILTER_OPERATORS)[number];
40
40
 
41
41
  export interface Filter<TField = string, TValue = unknown> {
42
42
  field: TField;
@@ -61,6 +61,11 @@ export interface Pagination {
61
61
  offset: number;
62
62
  }
63
63
 
64
+ export interface Search<T> {
65
+ fields: FieldPath<T>[];
66
+ value: string;
67
+ }
68
+
64
69
  export interface PaginationMeta {
65
70
  page: number;
66
71
  limit: number;
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Interface for all domain events
3
+ */
4
+ export interface IDomainEvent {
5
+ /**
6
+ * Unique identifier for this event occurrence
7
+ */
8
+ readonly eventId: string;
9
+
10
+ /**
11
+ * When the event occurred
12
+ */
13
+ readonly occurredOn: Date;
14
+
15
+ /**
16
+ * Name/type of the event (e.g., "UserCreated", "OrderPlaced")
17
+ */
18
+ readonly eventName: string;
19
+
20
+ /**
21
+ * ID of the aggregate that raised this event
22
+ */
23
+ readonly aggregateId: string;
24
+ }
25
+
26
+ /**
27
+ * Event handler function type
28
+ */
29
+ export type DomainEventHandler<T extends IDomainEvent = IDomainEvent> = (
30
+ event: T
31
+ ) => void | Promise<void>;
32
+
33
+ /**
34
+ * Event handler class type
35
+ */
36
+ export interface IDomainEventHandler<T extends IDomainEvent = IDomainEvent> {
37
+ handle(event: T): void | Promise<void>;
38
+ }
@@ -1,5 +1,6 @@
1
- import { StandardSchema, ValidationConfig } from "..";
1
+ import { ValidationConfig } from "..";
2
2
  import { Id } from "../id";
3
+ import { StandardSchema } from "./standard-schema";
3
4
 
4
5
  export type EntityId = string | number;
5
6
 
@@ -15,12 +16,10 @@ interface DomainValidation<T> {
15
16
  export type EntityValidation<T> = DomainValidation<T>;
16
17
  export type VOValidation<T> = DomainValidation<T>;
17
18
 
18
-
19
19
  export interface VOHooks<T, E> {
20
20
  onBeforeUpdate?: (entity: E, snapshot: T) => boolean;
21
21
  onCreate?: (entity: E) => void;
22
22
  rules?: (entity: E) => void;
23
- defaultValues?: Partial<T>;
24
23
  }
25
24
 
26
25
  // Specialized hooks for entities (with BaseProps)
@@ -1,4 +1,4 @@
1
- import { BaseProps } from "..";
1
+ import { BaseProps } from "./domain";
2
2
  import { IsArray, NonUndefined, UnwrapArray } from "./utils";
3
3
 
4
4
  export interface ChangeEvent<T> {
@@ -4,3 +4,4 @@ export * from "./history-tracker";
4
4
  export * from "./standard-schema";
5
5
  export * from "./utils";
6
6
  export * from "./unit-of-work";
7
+ export * from "./domain-event";
@@ -1,97 +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
- }
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
+ }
@@ -3,7 +3,7 @@
3
3
  // ============================================================================
4
4
 
5
5
  import { ValidationError } from "./validation-error";
6
- import { IDomainEvent } from "./domain-event";
6
+ import { IDomainEvent } from ".";
7
7
  import {
8
8
  VOHooks,
9
9
  ValidationConfig,
@@ -11,6 +11,7 @@ import {
11
11
  EntityValidation,
12
12
  } from "./types";
13
13
  import { DEFAULT_VALIDATION_CONFIG } from "./constants";
14
+ import { DomainError } from "./exceptions";
14
15
 
15
16
  // Helper to get static properties from constructor
16
17
  function getStaticProperty<T>(
@@ -50,11 +51,7 @@ export abstract class ValueObject<T> {
50
51
  ...validation?.config,
51
52
  };
52
53
 
53
- // Apply defaultValues
54
54
  let finalProps = { ...props } as T;
55
- if (hooks?.defaultValues) {
56
- finalProps = { ...hooks.defaultValues, ...props } as T;
57
- }
58
55
 
59
56
  // Validate schema on creation
60
57
  if (this.domainSchema && this.validationConfig.onCreate) {
@@ -84,7 +81,7 @@ export abstract class ValueObject<T> {
84
81
  const result = this.domainSchema["~standard"].validate(props);
85
82
 
86
83
  if (result instanceof Promise) {
87
- throw new Error(
84
+ throw new DomainError(
88
85
  "Async validation not supported in constructor. Use sync validation schema."
89
86
  );
90
87
  }
@@ -113,6 +113,14 @@ describe("Criteria", () => {
113
113
  expect(result.data.every((u) => u.status === "inactive")).toBe(true);
114
114
  });
115
115
 
116
+ it("should filter by search", () => {
117
+ const criteria = Criteria.create<TestUser>().search(["name"], "Bob");
118
+
119
+ const result = PaginatedResult.fromArray(testUsers, criteria);
120
+ expect(result.data).toHaveLength(1);
121
+ expect(result.data[0].name).toBe("Bob");
122
+ });
123
+
116
124
  it("should filter by greaterThan", () => {
117
125
  const criteria = Criteria.create<TestUser>().where(
118
126
  "age",