@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
package/src/index.ts ADDED
@@ -0,0 +1,33 @@
1
+ // ============================================================================
2
+ // Rich Domain Library - Main Exports
3
+ // ============================================================================
4
+
5
+ // Core Classes
6
+ export { Id } from "./id";
7
+ export { BaseEntity } from "./base-entity";
8
+ export { Entity, Aggregate } from "./entity";
9
+ export { ValueObject } from "./value-object";
10
+
11
+ export * from "./validation-error";
12
+
13
+ // Domain Events
14
+ export * from "./domain-event";
15
+
16
+ export * from "./domain-event-bus";
17
+
18
+ // Criteria & Repository
19
+ export * from "./criteria";
20
+ export * from "./paginated-result";
21
+
22
+ // Repository
23
+ export * from "./repository";
24
+ // Backward compatibility - re-export InMemoryRepository at top level
25
+ export { InMemoryRepository } from "./repository";
26
+
27
+ // Types
28
+ export * from "./types";
29
+
30
+ export * from "./constants";
31
+
32
+ // Internal (for advanced usage)
33
+ export { DeepProxy } from "./deep-proxy";
@@ -0,0 +1,274 @@
1
+ // ============================================================================
2
+ // PaginatedResult - Container for paginated data with deep serialization
3
+ // ============================================================================
4
+
5
+ import { Id } from "./id";
6
+ import type { Criteria } from "./criteria";
7
+ import type { Pagination, PaginationMeta, Filter } from "./types";
8
+
9
+ // ============================================================================
10
+ // Type Utilities
11
+ // ============================================================================
12
+
13
+ /**
14
+ * Infers the JSON result type from T
15
+ * - If T has toJson(), returns its return type
16
+ * - Otherwise returns T as-is
17
+ */
18
+ type InferJsonResult<T> = T extends { toJson(): infer R } ? R : T;
19
+
20
+ /**
21
+ * Type for the serialized result of PaginatedResult.toJSON()
22
+ */
23
+ export type PaginatedJsonResult<T> = {
24
+ data: InferJsonResult<T>[];
25
+ meta: PaginationMeta;
26
+ };
27
+
28
+ // ============================================================================
29
+ // PaginatedResult Class
30
+ // ============================================================================
31
+
32
+ export class PaginatedResult<T> {
33
+ constructor(
34
+ public readonly data: T[],
35
+ public readonly meta: PaginationMeta
36
+ ) {}
37
+
38
+ /**
39
+ * Creates a PaginatedResult with calculated metadata
40
+ */
41
+ static create<T>(
42
+ data: T[],
43
+ pagination: Pagination,
44
+ total: number
45
+ ): PaginatedResult<T> {
46
+ const meta = this.createMeta(pagination, total);
47
+ return new PaginatedResult(data, meta);
48
+ }
49
+
50
+ /**
51
+ * Creates pagination metadata from total count
52
+ */
53
+ static createMeta(pagination: Pagination, total: number): PaginationMeta {
54
+ const totalPages = Math.ceil(total / pagination.limit);
55
+
56
+ return {
57
+ page: pagination.page,
58
+ limit: pagination.limit,
59
+ total,
60
+ totalPages,
61
+ hasNext: pagination.page < totalPages,
62
+ hasPrevious: pagination.page > 1,
63
+ };
64
+ }
65
+
66
+ /**
67
+ * Applies criteria to an in-memory array (useful for testing)
68
+ */
69
+ static fromArray<T>(
70
+ items: T[],
71
+ criteria: Criteria<T>
72
+ ): PaginatedResult<T> {
73
+ let result = [...items];
74
+
75
+ // Apply filters
76
+ for (const filter of criteria.getFilters()) {
77
+ result = result.filter((item) => applyFilter(item, filter));
78
+ }
79
+
80
+ const total = result.length;
81
+
82
+ // Apply ordering
83
+ for (const order of criteria.getOrders().reverse()) {
84
+ result.sort((a, b) => {
85
+ const aVal = getNestedValue(a, order.field);
86
+ const bVal = getNestedValue(b, order.field);
87
+
88
+ let comparison = 0;
89
+ if (aVal < bVal) comparison = -1;
90
+ if (aVal > bVal) comparison = 1;
91
+
92
+ return order.direction === "desc" ? -comparison : comparison;
93
+ });
94
+ }
95
+
96
+ // Apply pagination
97
+ const pagination = criteria.getPagination();
98
+ if (pagination) {
99
+ result = result.slice(
100
+ pagination.offset,
101
+ pagination.offset + pagination.limit
102
+ );
103
+ return PaginatedResult.create(result, pagination, total);
104
+ }
105
+
106
+ // No pagination - return all with default meta
107
+ return PaginatedResult.create(
108
+ result,
109
+ { page: 1, limit: result.length, offset: 0 },
110
+ total
111
+ );
112
+ }
113
+
114
+ /**
115
+ * Converts the result to JSON, deeply serializing all entities/aggregates/value objects
116
+ * - Entities/Aggregates → calls toJson() recursively
117
+ * - Value Objects → calls toJson()
118
+ * - Id → converts to string
119
+ * - Arrays → maps recursively
120
+ * - Plain objects → serializes properties recursively
121
+ * - Primitives → returns as-is
122
+ */
123
+ toJSON(): PaginatedJsonResult<T> {
124
+ return {
125
+ data: this.data.map((item) =>
126
+ this.deepSerialize(item)
127
+ ) as InferJsonResult<T>[],
128
+ meta: this.meta,
129
+ };
130
+ }
131
+
132
+ /**
133
+ * Deep serialization logic (similar to BaseEntity.deepToJson)
134
+ */
135
+ private deepSerialize(obj: any): any {
136
+ if (obj === null || obj === undefined) return obj;
137
+
138
+ // Id → string
139
+ if (obj instanceof Id) return obj.value;
140
+
141
+ // Arrays → map recursively
142
+ if (Array.isArray(obj)) {
143
+ return obj.map((item) => this.deepSerialize(item));
144
+ }
145
+
146
+ // Objects with toJson() method (Entity/Aggregate/ValueObject)
147
+ if (obj && typeof obj.toJson === "function") {
148
+ return obj.toJson();
149
+ }
150
+
151
+ // Plain objects → serialize properties recursively
152
+ if (typeof obj === "object") {
153
+ const result: any = {};
154
+ for (const key in obj) {
155
+ if (obj.hasOwnProperty(key)) {
156
+ result[key] = this.deepSerialize(obj[key]);
157
+ }
158
+ }
159
+ return result;
160
+ }
161
+
162
+ // Primitives
163
+ return obj;
164
+ }
165
+
166
+ /**
167
+ * Transform each item in the result using a mapper function
168
+ */
169
+ map<U>(fn: (item: T) => U): PaginatedResult<U> {
170
+ return new PaginatedResult(this.data.map(fn), this.meta);
171
+ }
172
+
173
+ /**
174
+ * Check if result has no data
175
+ */
176
+ get isEmpty(): boolean {
177
+ return this.data.length === 0;
178
+ }
179
+
180
+ /**
181
+ * Check if there are more pages available
182
+ */
183
+ get hasMore(): boolean {
184
+ return this.meta.hasNext;
185
+ }
186
+ }
187
+
188
+ // ============================================================================
189
+ // Helper Functions (moved from criteria.ts)
190
+ // ============================================================================
191
+
192
+ function applyFilter<T>(item: T, filter: Filter): boolean {
193
+ const value = getNestedValue(item, filter.field);
194
+
195
+ const isValueDate = value instanceof Date;
196
+
197
+ const parseValue = (v: any) => {
198
+ if (isValueDate && typeof v === "string") return new Date(v);
199
+ if (isValueDate && typeof v === "number") return new Date(v);
200
+ return v;
201
+ };
202
+
203
+ switch (filter.operator) {
204
+ case "equals":
205
+ return value === filter.value;
206
+
207
+ case "notEquals":
208
+ return value !== filter.value;
209
+
210
+ case "greaterThan": {
211
+ const compareTo = parseValue(filter.value);
212
+ return isValueDate ? value > compareTo : value > (filter.value as any);
213
+ }
214
+
215
+ case "greaterThanOrEqual": {
216
+ const compareTo = parseValue(filter.value);
217
+ return isValueDate ? value >= compareTo : value >= (filter.value as any);
218
+ }
219
+
220
+ case "lessThan":
221
+ const lt = parseValue(filter.value);
222
+ return isValueDate ? value < lt : value < (filter.value as any);
223
+
224
+ case "lessThanOrEqual":
225
+ const lte = parseValue(filter.value);
226
+ return isValueDate ? value <= lte : value <= (filter.value as any);
227
+
228
+ case "contains":
229
+ return String(value)
230
+ .toLowerCase()
231
+ .includes(String(filter.value).toLowerCase());
232
+
233
+ case "startsWith":
234
+ return String(value)
235
+ .toLowerCase()
236
+ .startsWith(String(filter.value).toLowerCase());
237
+
238
+ case "endsWith":
239
+ return String(value)
240
+ .toLowerCase()
241
+ .endsWith(String(filter.value).toLowerCase());
242
+
243
+ case "in":
244
+ return Array.isArray(filter.value) && filter.value.includes(value);
245
+
246
+ case "notIn":
247
+ return Array.isArray(filter.value) && !filter.value.includes(value);
248
+
249
+ case "between":
250
+ if (Array.isArray(filter.value) && filter.value.length === 2) {
251
+ const [min, max] = filter.value.map(parseValue);
252
+ return isValueDate
253
+ ? value >= min && value <= max
254
+ : value >= filter.value[0] && value <= filter.value[1];
255
+ }
256
+ return false;
257
+
258
+ case "isNull":
259
+ return value === null || value === undefined;
260
+
261
+ case "isNotNull":
262
+ return value !== null && value !== undefined;
263
+
264
+ default:
265
+ return true;
266
+ }
267
+ }
268
+
269
+ function getNestedValue(obj: any, path: string): any {
270
+ return path.split(".").reduce((current, key) => {
271
+ if (current === null || current === undefined) return undefined;
272
+ return current[key];
273
+ }, obj);
274
+ }
@@ -0,0 +1,152 @@
1
+ // ============================================================================
2
+ // Base Repository - Abstract implementation with common logic
3
+ // ============================================================================
4
+
5
+ import type { Id } from "../id";
6
+ import type { Aggregate } from "../entity";
7
+ import type { Criteria } from "../criteria";
8
+ import { PaginatedResult } from "../paginated-result";
9
+ import type { IRepository } from "../types";
10
+ import type { IMapper } from "./mapper";
11
+
12
+ /**
13
+ * Abstract base repository
14
+ * Implements common logic, delegates persistence to subclasses
15
+ *
16
+ * @example
17
+ * ```ts
18
+ * class UserRepository extends BaseRepository<User, PrismaUser> {
19
+ * constructor(prisma: PrismaClient) {
20
+ * super(new UserMapper());
21
+ * this.prisma = prisma;
22
+ * }
23
+ *
24
+ * protected async insertOne(data: PrismaUser): Promise<PrismaUser> {
25
+ * return this.prisma.user.create({ data });
26
+ * }
27
+ *
28
+ * protected async updateOne(id: string, data: PrismaUser): Promise<PrismaUser> {
29
+ * return this.prisma.user.update({ where: { id }, data });
30
+ * }
31
+ *
32
+ * // ... implement other abstract methods
33
+ * }
34
+ * ```
35
+ */
36
+ export abstract class BaseRepository<
37
+ TDomain extends Aggregate<any>,
38
+ TPersistence = any
39
+ > implements IRepository<TDomain>
40
+ {
41
+ constructor(protected readonly mapper: IMapper<TDomain, TPersistence>) {}
42
+
43
+ // ============================================================================
44
+ // Abstract methods - Must be implemented by subclasses
45
+ // ============================================================================
46
+
47
+ /**
48
+ * Insert new record in database
49
+ */
50
+ protected abstract insertOne(data: TPersistence): Promise<TPersistence>;
51
+
52
+ /**
53
+ * Update existing record in database
54
+ */
55
+ protected abstract updateOne(
56
+ id: string,
57
+ data: TPersistence
58
+ ): Promise<TPersistence>;
59
+
60
+ /**
61
+ * Delete record from database
62
+ */
63
+ protected abstract deleteOne(id: string): Promise<void>;
64
+
65
+ /**
66
+ * Find record by ID in database
67
+ */
68
+ protected abstract findOneById(id: string): Promise<TDomain | null>;
69
+
70
+ /**
71
+ * Find all records in database (no filtering)
72
+ */
73
+ protected abstract findMany(): Promise<TDomain[]>;
74
+
75
+ /**
76
+ * Apply criteria to query (filtering, ordering, pagination)
77
+ * Returns [data, total]
78
+ */
79
+ protected abstract applyCriteria(
80
+ criteria: Criteria<TDomain>
81
+ ): Promise<PaginatedResult<TDomain>>;
82
+
83
+ /**
84
+ * Count records matching criteria
85
+ */
86
+ protected abstract countByCriteria(
87
+ criteria?: Criteria<TDomain>
88
+ ): Promise<number>;
89
+
90
+ /**
91
+ * Check if record exists by ID
92
+ */
93
+ protected abstract existsById(id: string): Promise<boolean>;
94
+
95
+ // ============================================================================
96
+ // Public API - Implemented using abstract methods
97
+ // ============================================================================
98
+
99
+ async findById(id: Id): Promise<TDomain | null> {
100
+ const domain = await this.findOneById(id.value);
101
+ if (!domain) return null;
102
+ return domain;
103
+ }
104
+
105
+ async find(criteria: Criteria<TDomain>): Promise<PaginatedResult<TDomain>> {
106
+ return await this.applyCriteria(criteria);
107
+ }
108
+
109
+ async findAll(criteria?: Criteria<TDomain>): Promise<TDomain[]> {
110
+ if (criteria) {
111
+ const result = await this.find(criteria);
112
+ return result.data;
113
+ }
114
+
115
+ const domains = await this.findMany();
116
+ return domains;
117
+ }
118
+
119
+ async findOne(criteria: Criteria<TDomain>): Promise<TDomain | null> {
120
+ // Limit to 1 result
121
+ const limitedCriteria = criteria.clone().limit(1);
122
+ const result = await this.find(limitedCriteria);
123
+
124
+ return result.data.length > 0 ? result.data[0] : null;
125
+ }
126
+
127
+ async save(aggregate: TDomain): Promise<void> {
128
+ const persistence = this.mapper.toPersistence(aggregate);
129
+
130
+ if (aggregate.isNew) {
131
+ await this.insertOne(persistence);
132
+ } else {
133
+ await this.updateOne(aggregate.id.value, persistence);
134
+ }
135
+ }
136
+
137
+ async delete(aggregate: TDomain): Promise<void> {
138
+ await this.deleteOne(aggregate.id.value);
139
+ }
140
+
141
+ async deleteById(id: Id): Promise<void> {
142
+ await this.deleteOne(id.value);
143
+ }
144
+
145
+ async exists(id: Id): Promise<boolean> {
146
+ return this.existsById(id.value);
147
+ }
148
+
149
+ async count(criteria?: Criteria<TDomain>): Promise<number> {
150
+ return this.countByCriteria(criteria);
151
+ }
152
+ }
@@ -0,0 +1,104 @@
1
+ // ============================================================================
2
+ // In-Memory Repository - Perfect for testing
3
+ // ============================================================================
4
+
5
+ import type { Id } from "../id";
6
+ import type { Aggregate } from "../entity";
7
+ import type { Criteria } from "../criteria";
8
+ import { PaginatedResult } from "../paginated-result";
9
+ import type { IRepository } from "../types";
10
+
11
+ /**
12
+ * In-memory repository implementation
13
+ * Perfect for unit tests and prototyping
14
+ *
15
+ * @example
16
+ * ```ts
17
+ * const userRepo = new InMemoryRepository<User>();
18
+ *
19
+ * await userRepo.save(user);
20
+ * const found = await userRepo.findById(user.id);
21
+ * const active = await userRepo.find(
22
+ * Criteria.create<User>().whereEquals('status', 'active')
23
+ * );
24
+ * ```
25
+ */
26
+ export class InMemoryRepository<TDomain extends Aggregate<any>>
27
+ implements IRepository<TDomain>
28
+ {
29
+ protected items: Map<string, TDomain> = new Map();
30
+
31
+ async findById(id: Id): Promise<TDomain | null> {
32
+ return this.items.get(id.value) || null;
33
+ }
34
+
35
+ async find(criteria: Criteria<TDomain>): Promise<PaginatedResult<TDomain>> {
36
+ const allItems = Array.from(this.items.values());
37
+ return PaginatedResult.fromArray(allItems, criteria);
38
+ }
39
+
40
+ async findAll(criteria?: Criteria<TDomain>): Promise<TDomain[]> {
41
+ if (criteria) {
42
+ const result = await this.find(criteria);
43
+ return result.data;
44
+ }
45
+
46
+ return Array.from(this.items.values());
47
+ }
48
+
49
+ async findOne(criteria: Criteria<TDomain>): Promise<TDomain | null> {
50
+ const result = await this.find(criteria.clone().limit(1));
51
+ return result.data.length > 0 ? result.data[0] : null;
52
+ }
53
+
54
+ async save(aggregate: TDomain): Promise<void> {
55
+ this.items.set(aggregate.id.value, aggregate);
56
+ }
57
+
58
+ async saveMany(aggregates: TDomain[]): Promise<void> {
59
+ for (const aggregate of aggregates) {
60
+ await this.save(aggregate);
61
+ }
62
+ }
63
+
64
+ async delete(aggregate: TDomain): Promise<void> {
65
+ this.items.delete(aggregate.id.value);
66
+ }
67
+
68
+ async deleteById(id: Id): Promise<void> {
69
+ this.items.delete(id.value);
70
+ }
71
+
72
+ async exists(id: Id): Promise<boolean> {
73
+ return this.items.has(id.value);
74
+ }
75
+
76
+ async count(criteria?: Criteria<TDomain>): Promise<number> {
77
+ if (criteria) {
78
+ const result = await this.find(criteria);
79
+ return result.meta.total;
80
+ }
81
+ return this.items.size;
82
+ }
83
+
84
+ /**
85
+ * Clear all items (useful for test cleanup)
86
+ */
87
+ clear(): void {
88
+ this.items.clear();
89
+ }
90
+
91
+ /**
92
+ * Get all items as array (useful for debugging)
93
+ */
94
+ getAll(): TDomain[] {
95
+ return Array.from(this.items.values());
96
+ }
97
+
98
+ /**
99
+ * Get items count
100
+ */
101
+ size(): number {
102
+ return this.items.size;
103
+ }
104
+ }
@@ -0,0 +1,55 @@
1
+ // ============================================================================
2
+ // Repository Module - Clean exports
3
+ // ============================================================================
4
+
5
+ // Mapper
6
+ export { BaseMapper } from "./mapper";
7
+ export type { IMapper } from "./mapper";
8
+
9
+ // Base implementations
10
+ export { BaseRepository } from "./base-repository";
11
+ export { InMemoryRepository } from "./in-memory-repository";
12
+
13
+ // Unit of Work
14
+ export {
15
+ UnitOfWork,
16
+ BaseTransactionContext,
17
+ InMemoryUnitOfWork,
18
+ } from "./unit-of-work";
19
+
20
+ /**
21
+ * QUICK START:
22
+ *
23
+ * 1. For Testing:
24
+ * ```ts
25
+ * import { InMemoryRepository } from 'rich-domain';
26
+ *
27
+ * const userRepo = new InMemoryRepository<User>();
28
+ * await userRepo.save(user);
29
+ * const found = await userRepo.findById(user.id);
30
+ * ```
31
+ *
32
+ * 2. For Production (Prisma, TypeORM, etc):
33
+ * - Extend BaseRepository
34
+ * - Implement abstract methods
35
+ * - Create a Mapper
36
+ * - See examples/ folder for reference
37
+ *
38
+ * 3. With Criteria:
39
+ * ```ts
40
+ * const result = await userRepo.find(
41
+ * Criteria.create<User>()
42
+ * .whereEquals('status', 'active')
43
+ * .orderByDesc('createdAt')
44
+ * .paginate(1, 10)
45
+ * );
46
+ * ```
47
+ *
48
+ * 4. With Unit of Work:
49
+ * ```ts
50
+ * await uow.transaction(async (ctx) => {
51
+ * const userRepo = uow.getRepository(UserRepository);
52
+ * await userRepo.save(user);
53
+ * });
54
+ * ```
55
+ */
@@ -0,0 +1,74 @@
1
+ // ============================================================================
2
+ // Mapper - Domain ↔ Persistence
3
+ // ============================================================================
4
+
5
+ import type { Aggregate } from "../entity";
6
+
7
+ /**
8
+ * Mapper interface for converting between Domain and Persistence models
9
+ *
10
+ * @template TDomain - Domain aggregate/entity
11
+ * @template TPersistence - Database model (Prisma, TypeORM, etc.)
12
+ *
13
+ * @example
14
+ * ```ts
15
+ * class UserMapper implements IMapper<User, PrismaUser> {
16
+ * toDomain(persistence: PrismaUser): User {
17
+ * return new User({
18
+ * id: Id.from(persistence.id),
19
+ * name: persistence.name,
20
+ * email: persistence.email,
21
+ * });
22
+ * }
23
+ *
24
+ * toPersistence(domain: User): PrismaUser {
25
+ * return {
26
+ * id: domain.id.value,
27
+ * name: domain.props.name,
28
+ * email: domain.props.email,
29
+ * };
30
+ * }
31
+ * }
32
+ * ```
33
+ */
34
+ export interface IMapper<TDomain extends Aggregate<any>, TPersistence = any> {
35
+ /**
36
+ * Convert from persistence model to domain aggregate
37
+ */
38
+ toDomain(persistence: TPersistence): TDomain;
39
+
40
+ /**
41
+ * Convert from domain aggregate to persistence model
42
+ */
43
+ toPersistence(domain: TDomain): TPersistence;
44
+
45
+ /**
46
+ * Convert array of persistence models to domain aggregates
47
+ */
48
+ toDomainList?(persistence: TPersistence[]): TDomain[];
49
+
50
+ /**
51
+ * Convert array of domain aggregates to persistence models
52
+ */
53
+ toPersistenceList?(domain: TDomain[]): TPersistence[];
54
+ }
55
+
56
+ /**
57
+ * Base mapper with default array implementations
58
+ */
59
+ export abstract class BaseMapper<
60
+ TDomain extends Aggregate<any>,
61
+ TPersistence = any
62
+ > implements IMapper<TDomain, TPersistence>
63
+ {
64
+ abstract toDomain(persistence: TPersistence): TDomain;
65
+ abstract toPersistence(domain: TDomain): TPersistence;
66
+
67
+ toDomainList(persistence: TPersistence[]): TDomain[] {
68
+ return persistence.map((p) => this.toDomain(p));
69
+ }
70
+
71
+ toPersistenceList(domain: TDomain[]): TPersistence[] {
72
+ return domain.map((d) => this.toPersistence(d));
73
+ }
74
+ }