@woltz/rich-domain 1.2.4 → 1.3.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.
- package/dist/aggregate-changes.d.ts +56 -14
- package/dist/aggregate-changes.d.ts.map +1 -1
- package/dist/aggregate-changes.js +103 -23
- package/dist/aggregate-changes.js.map +1 -1
- package/dist/base-entity.d.ts +1 -1
- package/dist/base-entity.d.ts.map +1 -1
- package/dist/base-entity.js +28 -13
- package/dist/base-entity.js.map +1 -1
- package/dist/change-tracker.d.ts +2 -1
- package/dist/change-tracker.d.ts.map +1 -1
- package/dist/change-tracker.js +61 -35
- package/dist/change-tracker.js.map +1 -1
- package/dist/criteria.d.ts +7 -15
- package/dist/criteria.d.ts.map +1 -1
- package/dist/criteria.js +105 -81
- package/dist/criteria.js.map +1 -1
- package/dist/domain-event-bus.js +4 -4
- package/dist/domain-event-bus.js.map +1 -1
- package/dist/domain-event.js +3 -0
- package/dist/domain-event.js.map +1 -1
- package/dist/entity-changes.js +1 -0
- package/dist/entity-changes.js.map +1 -1
- package/dist/entity-schema-registry.d.ts +137 -3
- package/dist/entity-schema-registry.d.ts.map +1 -1
- package/dist/entity-schema-registry.js +160 -7
- package/dist/entity-schema-registry.js.map +1 -1
- package/dist/exceptions.js +26 -1
- package/dist/exceptions.js.map +1 -1
- package/dist/id.js +2 -0
- package/dist/id.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/paginated-result.d.ts +4 -4
- package/dist/paginated-result.d.ts.map +1 -1
- package/dist/paginated-result.js +14 -19
- package/dist/paginated-result.js.map +1 -1
- package/dist/repository/unit-of-work.js +3 -7
- package/dist/repository/unit-of-work.js.map +1 -1
- package/dist/types/change-tracker.d.ts +30 -0
- package/dist/types/change-tracker.d.ts.map +1 -1
- package/dist/types/criteria.d.ts +1 -4
- package/dist/types/criteria.d.ts.map +1 -1
- package/dist/types/domain.d.ts +2 -1
- package/dist/types/domain.d.ts.map +1 -1
- package/dist/types/utils.d.ts +2 -2
- package/dist/utils/helpers.d.ts +1 -0
- package/dist/utils/helpers.d.ts.map +1 -1
- package/dist/utils/helpers.js +23 -0
- package/dist/utils/helpers.js.map +1 -1
- package/dist/validation-error.d.ts +15 -1
- package/dist/validation-error.d.ts.map +1 -1
- package/dist/validation-error.js +46 -3
- package/dist/validation-error.js.map +1 -1
- package/dist/value-object.d.ts +1 -1
- package/dist/value-object.d.ts.map +1 -1
- package/dist/value-object.js +30 -2
- package/dist/value-object.js.map +1 -1
- package/package.json +17 -3
- package/src/aggregate-changes.ts +133 -24
- package/src/base-entity.ts +22 -11
- package/src/change-tracker.ts +113 -54
- package/src/criteria.ts +151 -109
- package/src/entity-schema-registry.ts +256 -6
- package/src/index.ts +1 -1
- package/src/paginated-result.ts +21 -29
- package/src/types/change-tracker.ts +31 -0
- package/src/types/criteria.ts +1 -4
- package/src/types/domain.ts +2 -1
- package/src/types/utils.ts +2 -2
- package/src/utils/helpers.ts +28 -0
- package/src/validation-error.ts +54 -4
- package/src/value-object.ts +6 -1
- package/.versionrc.json +0 -21
- package/CHANGELOG.md +0 -163
- package/tests/aggregate-changes.test.ts +0 -284
- package/tests/criteria.test.ts +0 -716
- package/tests/depth/deep-tracking.test.ts +0 -554
- package/tests/domain-events.test.ts +0 -431
- package/tests/entity-equality.test.ts +0 -464
- package/tests/entity-schema-registry.test.ts +0 -382
- package/tests/entity-validation.test.ts +0 -252
- package/tests/history-tracker.spec.ts +0 -439
- package/tests/id.test.ts +0 -338
- package/tests/load-test/data.json +0 -347211
- package/tests/load-test/entities.ts +0 -97
- package/tests/load-test/generate-data.ts +0 -81
- package/tests/load-test/lead-to-domain.mapper.ts +0 -24
- package/tests/load-test/load.test.ts +0 -38
- package/tests/repository.test.ts +0 -635
- package/tests/to-json.test.ts +0 -99
- package/tests/utils.ts +0 -290
- package/tests/value-object-validation.test.ts +0 -219
- package/tests/value-objects.test.ts +0 -80
- package/tsconfig.json +0 -9
package/tests/to-json.test.ts
DELETED
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
import { Entity, Id } from "../src";
|
|
2
|
-
import { Post, User, Address } from "./utils";
|
|
3
|
-
|
|
4
|
-
describe("toJson Functionality", () => {
|
|
5
|
-
it("should convert simple entity to JSON", () => {
|
|
6
|
-
const post = new Post({
|
|
7
|
-
id: new Id("1"),
|
|
8
|
-
title: "First Post",
|
|
9
|
-
content: "Hello World",
|
|
10
|
-
published: false,
|
|
11
|
-
comments: [],
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
const json = post.toJson();
|
|
15
|
-
expect(json).toEqual({
|
|
16
|
-
id: "1",
|
|
17
|
-
title: "First Post",
|
|
18
|
-
content: "Hello World",
|
|
19
|
-
published: false,
|
|
20
|
-
comments: [],
|
|
21
|
-
});
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
it("should convert nested entities to JSON", () => {
|
|
25
|
-
const user = new User({
|
|
26
|
-
id: new Id("1"),
|
|
27
|
-
name: "John Doe",
|
|
28
|
-
email: "john@example.com",
|
|
29
|
-
posts: [
|
|
30
|
-
new Post({
|
|
31
|
-
id: new Id("1"),
|
|
32
|
-
title: "Post 1",
|
|
33
|
-
content: "Content 1",
|
|
34
|
-
published: false,
|
|
35
|
-
comments: [],
|
|
36
|
-
}),
|
|
37
|
-
new Post({
|
|
38
|
-
id: new Id("2"),
|
|
39
|
-
title: "Post 2",
|
|
40
|
-
content: "Content 2",
|
|
41
|
-
published: false,
|
|
42
|
-
comments: [],
|
|
43
|
-
}),
|
|
44
|
-
],
|
|
45
|
-
address: new Address({
|
|
46
|
-
street: "Main St",
|
|
47
|
-
city: "NYC",
|
|
48
|
-
}),
|
|
49
|
-
tags: [],
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
const json = user.toJson();
|
|
53
|
-
|
|
54
|
-
expect(json.id).toBe("1");
|
|
55
|
-
expect(json.name).toBe("John Doe");
|
|
56
|
-
expect(json.posts).toHaveLength(2);
|
|
57
|
-
expect(json.posts[0].title).toBe("Post 1");
|
|
58
|
-
expect(json.address?.city).toBe("NYC");
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
it("should handle deeply nested structures", () => {
|
|
62
|
-
const user = new User({
|
|
63
|
-
id: new Id("1"),
|
|
64
|
-
name: "John Doe",
|
|
65
|
-
email: "john@example.com",
|
|
66
|
-
posts: [
|
|
67
|
-
new Post({
|
|
68
|
-
id: new Id("1"),
|
|
69
|
-
title: "Post 1",
|
|
70
|
-
content: "Content 1",
|
|
71
|
-
published: false,
|
|
72
|
-
comments: [],
|
|
73
|
-
}),
|
|
74
|
-
],
|
|
75
|
-
address: new Address({
|
|
76
|
-
street: "Main St",
|
|
77
|
-
city: "NYC",
|
|
78
|
-
}),
|
|
79
|
-
tags: [],
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
const json = user.toJson();
|
|
83
|
-
expect(typeof json).toBe("object");
|
|
84
|
-
expect(Array.isArray(json.posts)).toBe(true);
|
|
85
|
-
expect(json.posts[0].id).toBe("1");
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
it("should serialize date correctly", () => {
|
|
89
|
-
class Test extends Entity<{ id: Id; createdAt: Date }> {}
|
|
90
|
-
|
|
91
|
-
const test = new Test({
|
|
92
|
-
id: new Id("1"),
|
|
93
|
-
createdAt: new Date(),
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
const json = test.toJson();
|
|
97
|
-
expect(json.createdAt).toBe(test.props.createdAt.toISOString());
|
|
98
|
-
});
|
|
99
|
-
});
|
package/tests/utils.ts
DELETED
|
@@ -1,290 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
Aggregate,
|
|
3
|
-
Criteria,
|
|
4
|
-
Entity,
|
|
5
|
-
Id,
|
|
6
|
-
Mapper,
|
|
7
|
-
PaginatedResult,
|
|
8
|
-
Repository,
|
|
9
|
-
UnitOfWork,
|
|
10
|
-
ValueObject,
|
|
11
|
-
} from "../src";
|
|
12
|
-
|
|
13
|
-
export class Like extends ValueObject<{
|
|
14
|
-
postId: string;
|
|
15
|
-
userId: string;
|
|
16
|
-
createdAt: Date;
|
|
17
|
-
}> {
|
|
18
|
-
static readonly identityKey = ["postId", "userId"];
|
|
19
|
-
|
|
20
|
-
get postId() {
|
|
21
|
-
return this.props.postId;
|
|
22
|
-
}
|
|
23
|
-
get userId() {
|
|
24
|
-
return this.props.userId;
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export class Address extends Entity<{
|
|
29
|
-
id: Id;
|
|
30
|
-
street: string;
|
|
31
|
-
city: string;
|
|
32
|
-
}> {
|
|
33
|
-
get street() {
|
|
34
|
-
return this.props.street;
|
|
35
|
-
}
|
|
36
|
-
get city() {
|
|
37
|
-
return this.props.city;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
changeStreet(street: string) {
|
|
41
|
-
this.props.street = street;
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export class TagReference extends ValueObject<{ tagId: string; name: string }> {
|
|
46
|
-
static readonly identityKey = "tagId";
|
|
47
|
-
|
|
48
|
-
get tagId() {
|
|
49
|
-
return this.props.tagId;
|
|
50
|
-
}
|
|
51
|
-
get name() {
|
|
52
|
-
return this.props.name;
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export class Comment extends Entity<{
|
|
57
|
-
id: Id;
|
|
58
|
-
text: string;
|
|
59
|
-
authorId: string;
|
|
60
|
-
likes: Like[];
|
|
61
|
-
}> {
|
|
62
|
-
get text() {
|
|
63
|
-
return this.props.text;
|
|
64
|
-
}
|
|
65
|
-
get authorId() {
|
|
66
|
-
return this.props.authorId;
|
|
67
|
-
}
|
|
68
|
-
get likes() {
|
|
69
|
-
return this.props.likes;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
changeText(text: string) {
|
|
73
|
-
this.props.text = text;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
addLike(like: Like) {
|
|
77
|
-
this.props.likes.push(like);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
removeLike(postId: string, userId: string) {
|
|
81
|
-
this.props.likes = this.props.likes.filter(
|
|
82
|
-
(l) => !(l.postId === postId && l.userId === userId)
|
|
83
|
-
);
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
export class Post extends Entity<{
|
|
87
|
-
id: Id;
|
|
88
|
-
title: string;
|
|
89
|
-
content: string;
|
|
90
|
-
published: boolean;
|
|
91
|
-
comments: Comment[];
|
|
92
|
-
}> {
|
|
93
|
-
get title() {
|
|
94
|
-
return this.props.title;
|
|
95
|
-
}
|
|
96
|
-
get content() {
|
|
97
|
-
return this.props.content;
|
|
98
|
-
}
|
|
99
|
-
get published() {
|
|
100
|
-
return this.props.published;
|
|
101
|
-
}
|
|
102
|
-
get comments() {
|
|
103
|
-
return this.props.comments;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
set comments(comments: Comment[]) {
|
|
107
|
-
this.props.comments = comments;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
changeTitle(title: string) {
|
|
111
|
-
this.props.title = title;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
publish() {
|
|
115
|
-
this.props.published = true;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
addComment(comment: Comment) {
|
|
119
|
-
this.props.comments.push(comment);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
removeComment(commentId: Id) {
|
|
123
|
-
this.props.comments = this.props.comments.filter(
|
|
124
|
-
(c) => c.id.value !== commentId.value
|
|
125
|
-
);
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
export class User extends Entity<{
|
|
130
|
-
id: Id;
|
|
131
|
-
name: string;
|
|
132
|
-
email: string;
|
|
133
|
-
address: Address | null;
|
|
134
|
-
posts: Post[];
|
|
135
|
-
tags: TagReference[];
|
|
136
|
-
}> {
|
|
137
|
-
get name() {
|
|
138
|
-
return this.props.name;
|
|
139
|
-
}
|
|
140
|
-
get email() {
|
|
141
|
-
return this.props.email;
|
|
142
|
-
}
|
|
143
|
-
get address() {
|
|
144
|
-
return this.props.address;
|
|
145
|
-
}
|
|
146
|
-
get posts() {
|
|
147
|
-
return this.props.posts;
|
|
148
|
-
}
|
|
149
|
-
get tags() {
|
|
150
|
-
return this.props.tags;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
set posts(posts: Post[]) {
|
|
154
|
-
this.props.posts = posts;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
changeName(name: string) {
|
|
158
|
-
this.props.name = name;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
changeEmail(email: string) {
|
|
162
|
-
this.props.email = email;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
setAddress(address: Address) {
|
|
166
|
-
this.props.address = address;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
removeAddress() {
|
|
170
|
-
this.props.address = null;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
addPost(post: Post) {
|
|
174
|
-
this.props.posts.push(post);
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
removePost(postId: Id) {
|
|
178
|
-
this.props.posts = this.props.posts.filter(
|
|
179
|
-
(p) => p.id.value !== postId.value
|
|
180
|
-
);
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
addTag(tag: TagReference) {
|
|
184
|
-
this.props.tags.push(tag);
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
addManyTags(tags: TagReference[]) {
|
|
188
|
-
this.props.tags.push(...tags);
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
addManyPosts(posts: Post[]) {
|
|
192
|
-
this.props.posts.push(...posts);
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
removeTag(tagId: string) {
|
|
196
|
-
this.props.tags = this.props.tags.filter((t) => t.tagId !== tagId);
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
public getTypedChanges() {
|
|
200
|
-
type UserEntities = {
|
|
201
|
-
User: User;
|
|
202
|
-
Post: Post;
|
|
203
|
-
Comment: Comment;
|
|
204
|
-
Address: Address;
|
|
205
|
-
TagReference: TagReference;
|
|
206
|
-
Like: Like;
|
|
207
|
-
};
|
|
208
|
-
return this.getChanges<UserEntities>();
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
export class InMemoryRepository<
|
|
213
|
-
TDomain extends Aggregate<any>
|
|
214
|
-
> extends Repository<TDomain> {
|
|
215
|
-
protected items: Map<string, TDomain> = new Map();
|
|
216
|
-
readonly uow: UnitOfWork;
|
|
217
|
-
constructor(
|
|
218
|
-
protected readonly mapperToDomain: Mapper<unknown, TDomain>,
|
|
219
|
-
protected readonly mapperToPersistence: Mapper<TDomain, unknown>
|
|
220
|
-
) {
|
|
221
|
-
super();
|
|
222
|
-
this.uow = {} as UnitOfWork;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
get model(): any {
|
|
226
|
-
// your database table name
|
|
227
|
-
return "inMemory";
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
async findById(id: string): Promise<TDomain | null> {
|
|
231
|
-
return this.items.get(id) || null;
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
async find(criteria: Criteria<TDomain>): Promise<PaginatedResult<TDomain>> {
|
|
235
|
-
const allItems = Array.from(this.items.values());
|
|
236
|
-
return PaginatedResult.fromArray(allItems, criteria);
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
async findAll(criteria?: Criteria<TDomain>): Promise<TDomain[]> {
|
|
240
|
-
if (criteria) {
|
|
241
|
-
const result = await this.find(criteria);
|
|
242
|
-
return result.data;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
return Array.from(this.items.values());
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
async findOne(criteria: Criteria<TDomain>): Promise<TDomain | null> {
|
|
249
|
-
const result = await this.find(criteria.clone().limit(1));
|
|
250
|
-
return result.data.length > 0 ? result.data[0] : null;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
async save(aggregate: TDomain): Promise<void> {
|
|
254
|
-
this.items.set(aggregate.id.value, aggregate);
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
async createMany(aggregates: TDomain[]): Promise<void> {
|
|
258
|
-
for (const aggregate of aggregates) {
|
|
259
|
-
await this.save(aggregate);
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
async delete(aggregate: TDomain): Promise<void> {
|
|
264
|
-
this.items.delete(aggregate.id.value);
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
async exists(id: string): Promise<boolean> {
|
|
268
|
-
return this.items.has(id);
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
async count(criteria?: Criteria<TDomain>): Promise<number> {
|
|
272
|
-
if (criteria) {
|
|
273
|
-
const result = await this.find(criteria);
|
|
274
|
-
return result.meta.total;
|
|
275
|
-
}
|
|
276
|
-
return this.items.size;
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
clear(): void {
|
|
280
|
-
this.items.clear();
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
getAll(): TDomain[] {
|
|
284
|
-
return Array.from(this.items.values());
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
size(): number {
|
|
288
|
-
return this.items.size;
|
|
289
|
-
}
|
|
290
|
-
}
|
|
@@ -1,219 +0,0 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
import { ValueObject, ValidationError, throwValidationError } from "../src";
|
|
3
|
-
import { VOHooks, VOValidation } from "../src/types";
|
|
4
|
-
|
|
5
|
-
// ============================================================================
|
|
6
|
-
// Test Value Objects with Validation
|
|
7
|
-
// ============================================================================
|
|
8
|
-
|
|
9
|
-
interface EmailProps {
|
|
10
|
-
value: string;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
const emailSchema = z.object({
|
|
14
|
-
value: z.string().email("Invalid email format"),
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
class Email extends ValueObject<EmailProps> {
|
|
18
|
-
protected static validation: VOValidation<EmailProps> = {
|
|
19
|
-
schema: emailSchema,
|
|
20
|
-
config: {
|
|
21
|
-
onCreate: true,
|
|
22
|
-
throwOnError: true,
|
|
23
|
-
},
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
get value(): string {
|
|
27
|
-
return this.props.value;
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
// ============================================================================
|
|
32
|
-
// Test Value Object with Default Values and Hooks
|
|
33
|
-
// ============================================================================
|
|
34
|
-
|
|
35
|
-
interface MoneyProps {
|
|
36
|
-
amount: number;
|
|
37
|
-
currency: string;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const moneySchema = z.object({
|
|
41
|
-
amount: z.number().min(0, "Amount must be non-negative"),
|
|
42
|
-
currency: z
|
|
43
|
-
.string()
|
|
44
|
-
.length(3, "Currency must be 3 characters (e.g., USD, EUR)"),
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
class Money extends ValueObject<MoneyProps> {
|
|
48
|
-
protected static validation: VOValidation<MoneyProps> = {
|
|
49
|
-
schema: moneySchema,
|
|
50
|
-
config: {
|
|
51
|
-
onCreate: true,
|
|
52
|
-
throwOnError: true,
|
|
53
|
-
},
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
protected static hooks: VOHooks<MoneyProps, Money> = {
|
|
57
|
-
rules: (money) => {
|
|
58
|
-
if (money.amount > 1000000) {
|
|
59
|
-
throwValidationError("amount", "Amount cannot exceed 1,000,000");
|
|
60
|
-
}
|
|
61
|
-
},
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
get amount(): number {
|
|
65
|
-
return this.props.amount;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
get currency(): string {
|
|
69
|
-
return this.props.currency;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
add(other: Money): Money {
|
|
73
|
-
if (this.currency !== other.currency) {
|
|
74
|
-
throw new Error("Cannot add money with different currencies");
|
|
75
|
-
}
|
|
76
|
-
return this.clone({ amount: this.amount + other.amount });
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// ============================================================================
|
|
81
|
-
// Test Value Object with throwOnError: false
|
|
82
|
-
// ============================================================================
|
|
83
|
-
|
|
84
|
-
class EmailSafe extends ValueObject<EmailProps> {
|
|
85
|
-
protected static validation: VOValidation<EmailProps> = {
|
|
86
|
-
schema: emailSchema,
|
|
87
|
-
config: {
|
|
88
|
-
onCreate: true,
|
|
89
|
-
throwOnError: false,
|
|
90
|
-
},
|
|
91
|
-
};
|
|
92
|
-
|
|
93
|
-
get value(): string {
|
|
94
|
-
return this.props.value;
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
// ============================================================================
|
|
99
|
-
// Tests
|
|
100
|
-
// ============================================================================
|
|
101
|
-
|
|
102
|
-
describe("ValueObject with Validation", () => {
|
|
103
|
-
describe("Email ValueObject", () => {
|
|
104
|
-
it("should create email with valid value", () => {
|
|
105
|
-
const email = new Email({ value: "test@example.com" });
|
|
106
|
-
expect(email.value).toBe("test@example.com");
|
|
107
|
-
expect(email.hasValidationErrors).toBe(false);
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
it("should throw on invalid email format", () => {
|
|
111
|
-
expect(() => {
|
|
112
|
-
new Email({ value: "invalid-email" });
|
|
113
|
-
}).toThrow(ValidationError);
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
it("should have correct error message", () => {
|
|
117
|
-
try {
|
|
118
|
-
new Email({ value: "invalid" });
|
|
119
|
-
} catch (error) {
|
|
120
|
-
expect(ValidationError.isValidationError(error)).toBe(true);
|
|
121
|
-
if (ValidationError.isValidationError(error)) {
|
|
122
|
-
expect(error.getMessages()).toContain("Invalid email format");
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
});
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
describe("Money ValueObject with Hooks", () => {
|
|
129
|
-
it("should create money with valid data", () => {
|
|
130
|
-
const money = new Money({ amount: 100, currency: "USD" });
|
|
131
|
-
expect(money.amount).toBe(100);
|
|
132
|
-
expect(money.currency).toBe("USD");
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
it("should throw on negative amount", () => {
|
|
136
|
-
expect(() => {
|
|
137
|
-
new Money({ amount: -10, currency: "USD" });
|
|
138
|
-
}).toThrow(ValidationError);
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
it("should throw on invalid currency code", () => {
|
|
142
|
-
expect(() => {
|
|
143
|
-
new Money({ amount: 100, currency: "US" });
|
|
144
|
-
}).toThrow(ValidationError);
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
it("should throw on custom rule violation (amount > 1M)", () => {
|
|
148
|
-
expect(() => {
|
|
149
|
-
new Money({ amount: 1000001, currency: "USD" });
|
|
150
|
-
}).toThrow(ValidationError);
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
it("should add money with same currency", () => {
|
|
154
|
-
const m1 = new Money({ amount: 100, currency: "USD" });
|
|
155
|
-
const m2 = new Money({ amount: 50, currency: "USD" });
|
|
156
|
-
const result = m1.add(m2);
|
|
157
|
-
|
|
158
|
-
expect(result.amount).toBe(150);
|
|
159
|
-
expect(result.currency).toBe("USD");
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
it("should throw when adding different currencies", () => {
|
|
163
|
-
const m1 = new Money({ amount: 100, currency: "USD" });
|
|
164
|
-
const m2 = new Money({ amount: 50, currency: "EUR" });
|
|
165
|
-
|
|
166
|
-
expect(() => m1.add(m2)).toThrow(
|
|
167
|
-
"Cannot add money with different currencies"
|
|
168
|
-
);
|
|
169
|
-
});
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
describe("Email ValueObject with throwOnError: false", () => {
|
|
173
|
-
it("should not throw on invalid email", () => {
|
|
174
|
-
const email = new EmailSafe({ value: "invalid" });
|
|
175
|
-
expect(email.hasValidationErrors).toBe(true);
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
it("should store validation errors", () => {
|
|
179
|
-
const email = new EmailSafe({ value: "not-an-email" });
|
|
180
|
-
|
|
181
|
-
expect(email.validationErrors).toBeDefined();
|
|
182
|
-
expect(email.validationErrors?.getMessages()).toContain(
|
|
183
|
-
"Invalid email format"
|
|
184
|
-
);
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
it("should not have errors for valid email", () => {
|
|
188
|
-
const email = new EmailSafe({ value: "valid@example.com" });
|
|
189
|
-
|
|
190
|
-
expect(email.hasValidationErrors).toBe(false);
|
|
191
|
-
expect(email.validationErrors).toBeUndefined();
|
|
192
|
-
});
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
describe("ValueObject Immutability", () => {
|
|
196
|
-
it("should remain immutable with validation", () => {
|
|
197
|
-
const email = new Email({ value: "test@example.com" });
|
|
198
|
-
|
|
199
|
-
expect(() => {
|
|
200
|
-
(email as any).props.value = "changed@example.com";
|
|
201
|
-
}).toThrow();
|
|
202
|
-
});
|
|
203
|
-
|
|
204
|
-
it("should create new instance when adding (immutability)", () => {
|
|
205
|
-
const m1 = new Money({ amount: 100, currency: "USD" });
|
|
206
|
-
const m2 = new Money({ amount: 50, currency: "USD" });
|
|
207
|
-
const result = m1.add(m2);
|
|
208
|
-
|
|
209
|
-
// Original instances unchanged
|
|
210
|
-
expect(m1.amount).toBe(100);
|
|
211
|
-
expect(m2.amount).toBe(50);
|
|
212
|
-
|
|
213
|
-
// New instance created
|
|
214
|
-
expect(result.amount).toBe(150);
|
|
215
|
-
expect(result).not.toBe(m1);
|
|
216
|
-
expect(result).not.toBe(m2);
|
|
217
|
-
});
|
|
218
|
-
});
|
|
219
|
-
});
|
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
import { TagReference } from "./utils";
|
|
2
|
-
|
|
3
|
-
describe("Value Object", () => {
|
|
4
|
-
it("should create immutable value object", () => {
|
|
5
|
-
const tag = new TagReference({
|
|
6
|
-
tagId: "1",
|
|
7
|
-
name: "Tag 1",
|
|
8
|
-
});
|
|
9
|
-
|
|
10
|
-
expect(tag.tagId).toBe("1");
|
|
11
|
-
expect(tag.name).toBe("Tag 1");
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
it("should compare value objects by value", () => {
|
|
15
|
-
const tag1 = new TagReference({
|
|
16
|
-
tagId: "1",
|
|
17
|
-
name: "Tag 1",
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
const tag2 = new TagReference({
|
|
21
|
-
tagId: "1",
|
|
22
|
-
name: "Tag 1",
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
const tag3 = new TagReference({
|
|
26
|
-
tagId: "2",
|
|
27
|
-
name: "Tag 2",
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
expect(tag1.equals(tag2)).toBe(true);
|
|
31
|
-
expect(tag1.equals(tag3)).toBe(false);
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
it("should convert value object to JSON", () => {
|
|
35
|
-
const tag = new TagReference({
|
|
36
|
-
name: "Tag 1",
|
|
37
|
-
tagId: "1",
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
const json = tag.toJson();
|
|
41
|
-
expect(json).toEqual({
|
|
42
|
-
name: "Tag 1",
|
|
43
|
-
tagId: "1",
|
|
44
|
-
});
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
describe("Value Object", () => {
|
|
48
|
-
it("should be immutable", () => {
|
|
49
|
-
const tag = new TagReference({
|
|
50
|
-
tagId: "1",
|
|
51
|
-
name: "Tag 1",
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
expect(tag.tagId).toBe("1");
|
|
55
|
-
expect(() => {
|
|
56
|
-
(tag as any).props.tagId = "2";
|
|
57
|
-
}).toThrow();
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
it("should compare by value", () => {
|
|
61
|
-
const tag1 = new TagReference({
|
|
62
|
-
tagId: "1",
|
|
63
|
-
name: "Tag 1",
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
const tag2 = new TagReference({
|
|
67
|
-
tagId: "1",
|
|
68
|
-
name: "Tag 1",
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
const tag3 = new TagReference({
|
|
72
|
-
tagId: "2",
|
|
73
|
-
name: "Tag 2",
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
expect(tag1.equals(tag2)).toBe(true);
|
|
77
|
-
expect(tag1.equals(tag3)).toBe(false);
|
|
78
|
-
});
|
|
79
|
-
});
|
|
80
|
-
});
|