@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.
- package/.github/workflows/ci.yml +40 -0
- package/.husky/commit-msg +1 -0
- package/.husky/pre-commit +1 -0
- package/.versionrc.json +21 -0
- package/.vscode/settings.json +3 -0
- package/CHANGELOG.md +81 -0
- package/LICENSE +21 -0
- package/README.md +712 -0
- package/commitlint.config.js +23 -0
- package/dist/base-entity.d.ts +67 -0
- package/dist/base-entity.d.ts.map +1 -0
- package/dist/base-entity.js +309 -0
- package/dist/base-entity.js.map +1 -0
- package/dist/constants.d.ts +3 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +6 -0
- package/dist/constants.js.map +1 -0
- package/dist/criteria.d.ts +60 -0
- package/dist/criteria.d.ts.map +1 -0
- package/dist/criteria.js +214 -0
- package/dist/criteria.js.map +1 -0
- package/dist/deep-proxy.d.ts +34 -0
- package/dist/deep-proxy.d.ts.map +1 -0
- package/dist/deep-proxy.js +297 -0
- package/dist/deep-proxy.js.map +1 -0
- package/dist/domain-event-bus.d.ts +57 -0
- package/dist/domain-event-bus.d.ts.map +1 -0
- package/dist/domain-event-bus.js +112 -0
- package/dist/domain-event-bus.js.map +1 -0
- package/dist/domain-event.d.ts +55 -0
- package/dist/domain-event.d.ts.map +1 -0
- package/dist/domain-event.js +42 -0
- package/dist/domain-event.js.map +1 -0
- package/dist/entity.d.ts +13 -0
- package/dist/entity.d.ts.map +1 -0
- package/dist/entity.js +15 -0
- package/dist/entity.js.map +1 -0
- package/dist/filtering.d.ts +107 -0
- package/dist/filtering.d.ts.map +1 -0
- package/dist/filtering.js +202 -0
- package/dist/filtering.js.map +1 -0
- package/dist/id.d.ts +51 -0
- package/dist/id.d.ts.map +1 -0
- package/dist/id.js +84 -0
- package/dist/id.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +25 -0
- package/dist/index.js.map +1 -0
- package/dist/ordering.d.ts +93 -0
- package/dist/ordering.d.ts.map +1 -0
- package/dist/ordering.js +154 -0
- package/dist/ordering.js.map +1 -0
- package/dist/paginated-result.d.ts +62 -0
- package/dist/paginated-result.d.ts.map +1 -0
- package/dist/paginated-result.js +201 -0
- package/dist/paginated-result.js.map +1 -0
- package/dist/pagination.d.ts +218 -0
- package/dist/pagination.d.ts.map +1 -0
- package/dist/pagination.js +281 -0
- package/dist/pagination.js.map +1 -0
- package/dist/repository/base-repository.d.ts +77 -0
- package/dist/repository/base-repository.d.ts.map +1 -0
- package/dist/repository/base-repository.js +80 -0
- package/dist/repository/base-repository.js.map +1 -0
- package/dist/repository/in-memory-repository.d.ts +46 -0
- package/dist/repository/in-memory-repository.d.ts.map +1 -0
- package/dist/repository/in-memory-repository.js +85 -0
- package/dist/repository/in-memory-repository.js.map +1 -0
- package/dist/repository/index.d.ts +42 -0
- package/dist/repository/index.d.ts.map +1 -0
- package/dist/repository/index.js +47 -0
- package/dist/repository/index.js.map +1 -0
- package/dist/repository/mapper.d.ts +56 -0
- package/dist/repository/mapper.d.ts.map +1 -0
- package/dist/repository/mapper.js +15 -0
- package/dist/repository/mapper.js.map +1 -0
- package/dist/repository/types.d.ts +87 -0
- package/dist/repository/types.d.ts.map +1 -0
- package/dist/repository/types.js +6 -0
- package/dist/repository/types.js.map +1 -0
- package/dist/repository/unit-of-work.d.ts +70 -0
- package/dist/repository/unit-of-work.d.ts.map +1 -0
- package/dist/repository/unit-of-work.js +122 -0
- package/dist/repository/unit-of-work.js.map +1 -0
- package/dist/repository.d.ts +2 -0
- package/dist/repository.d.ts.map +1 -0
- package/dist/repository.js +21 -0
- package/dist/repository.js.map +1 -0
- package/dist/specification.d.ts +102 -0
- package/dist/specification.d.ts.map +1 -0
- package/dist/specification.js +187 -0
- package/dist/specification.js.map +1 -0
- package/dist/types/criteria.d.ts +35 -0
- package/dist/types/criteria.d.ts.map +1 -0
- package/dist/types/criteria.js +17 -0
- package/dist/types/criteria.js.map +1 -0
- package/dist/types/domain.d.ts +30 -0
- package/dist/types/domain.d.ts.map +1 -0
- package/dist/types/domain.js +2 -0
- package/dist/types/domain.js.map +1 -0
- package/dist/types/history-tracker.d.ts +36 -0
- package/dist/types/history-tracker.d.ts.map +1 -0
- package/dist/types/history-tracker.js +2 -0
- package/dist/types/history-tracker.js.map +1 -0
- package/dist/types/index.d.ts +8 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +8 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/repository.d.ts +43 -0
- package/dist/types/repository.d.ts.map +1 -0
- package/dist/types/repository.js +2 -0
- package/dist/types/repository.js.map +1 -0
- package/dist/types/standard-schema.d.ts +15 -0
- package/dist/types/standard-schema.d.ts.map +1 -0
- package/dist/types/standard-schema.js +2 -0
- package/dist/types/standard-schema.js.map +1 -0
- package/dist/types/unit-of-work.d.ts +39 -0
- package/dist/types/unit-of-work.d.ts.map +1 -0
- package/dist/types/unit-of-work.js +2 -0
- package/dist/types/unit-of-work.js.map +1 -0
- package/dist/types/utils.d.ts +14 -0
- package/dist/types/utils.d.ts.map +1 -0
- package/dist/types/utils.js +2 -0
- package/dist/types/utils.js.map +1 -0
- package/dist/types.d.ts +88 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +12 -0
- package/dist/types.js.map +1 -0
- package/dist/validation-error.d.ts +42 -0
- package/dist/validation-error.d.ts.map +1 -0
- package/dist/validation-error.js +73 -0
- package/dist/validation-error.js.map +1 -0
- package/dist/value-object.d.ts +47 -0
- package/dist/value-object.d.ts.map +1 -0
- package/dist/value-object.js +136 -0
- package/dist/value-object.js.map +1 -0
- package/eslint.config.js +51 -0
- package/jest.config.js +21 -0
- package/package.json +58 -0
- package/src/base-entity.ts +401 -0
- package/src/constants.ts +7 -0
- package/src/criteria.ts +291 -0
- package/src/deep-proxy.ts +339 -0
- package/src/domain-event-bus.ts +166 -0
- package/src/domain-event.ts +90 -0
- package/src/entity.ts +16 -0
- package/src/id.ts +94 -0
- package/src/index.ts +33 -0
- package/src/paginated-result.ts +274 -0
- package/src/repository/base-repository.ts +152 -0
- package/src/repository/in-memory-repository.ts +104 -0
- package/src/repository/index.ts +55 -0
- package/src/repository/mapper.ts +74 -0
- package/src/repository/unit-of-work.ts +148 -0
- package/src/types/criteria.ts +79 -0
- package/src/types/domain.ts +37 -0
- package/src/types/history-tracker.ts +45 -0
- package/src/types/index.ts +7 -0
- package/src/types/repository.ts +51 -0
- package/src/types/standard-schema.ts +19 -0
- package/src/types/unit-of-work.ts +46 -0
- package/src/types/utils.ts +29 -0
- package/src/validation-error.ts +97 -0
- package/src/value-object.ts +187 -0
- package/tests/criteria.test.ts +432 -0
- package/tests/domain-events.test.ts +445 -0
- package/tests/entity-equality.test.ts +487 -0
- package/tests/entity-validation.test.ts +339 -0
- package/tests/entity.test.ts +33 -0
- package/tests/history-tracker.spec.ts +667 -0
- package/tests/id.test.ts +341 -0
- package/tests/repository.test.ts +641 -0
- package/tests/to-json.test.ts +91 -0
- package/tests/utils.ts +151 -0
- package/tests/value-object-validation.test.ts +228 -0
- package/tests/value-objects.test.ts +52 -0
- package/tsconfig.json +31 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { Id } from "../src";
|
|
2
|
+
import { Post, User, Address, Comment } 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
|
+
likes: 5,
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
const json = post.toJson();
|
|
14
|
+
expect(json).toEqual({
|
|
15
|
+
id: "1",
|
|
16
|
+
title: "First Post",
|
|
17
|
+
content: "Hello World",
|
|
18
|
+
likes: 5,
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("should convert nested entities to JSON", () => {
|
|
23
|
+
const user = new User({
|
|
24
|
+
id: new Id("1"),
|
|
25
|
+
name: "John Doe",
|
|
26
|
+
email: "john@example.com",
|
|
27
|
+
posts: [
|
|
28
|
+
new Post({
|
|
29
|
+
id: new Id("1"),
|
|
30
|
+
title: "Post 1",
|
|
31
|
+
content: "Content 1",
|
|
32
|
+
likes: 0,
|
|
33
|
+
}),
|
|
34
|
+
new Post({
|
|
35
|
+
id: new Id("2"),
|
|
36
|
+
title: "Post 2",
|
|
37
|
+
content: "Content 2",
|
|
38
|
+
likes: 5,
|
|
39
|
+
}),
|
|
40
|
+
],
|
|
41
|
+
address: new Address({
|
|
42
|
+
street: "Main St",
|
|
43
|
+
city: "NYC",
|
|
44
|
+
zipCode: "10001",
|
|
45
|
+
}),
|
|
46
|
+
comments: [
|
|
47
|
+
new Comment({
|
|
48
|
+
id: new Id("1"),
|
|
49
|
+
text: "Great post!",
|
|
50
|
+
author: "Alice",
|
|
51
|
+
}),
|
|
52
|
+
],
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const json = user.toJson();
|
|
56
|
+
|
|
57
|
+
expect(json.id).toBe("1");
|
|
58
|
+
expect(json.name).toBe("John Doe");
|
|
59
|
+
expect(json.posts).toHaveLength(2);
|
|
60
|
+
expect(json.posts[0].title).toBe("Post 1");
|
|
61
|
+
expect(json.address.city).toBe("NYC");
|
|
62
|
+
expect(json.comments[0].text).toBe("Great post!");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("should handle deeply nested structures", () => {
|
|
66
|
+
const user = new User({
|
|
67
|
+
id: new Id("1"),
|
|
68
|
+
name: "John Doe",
|
|
69
|
+
email: "john@example.com",
|
|
70
|
+
posts: [
|
|
71
|
+
new Post({
|
|
72
|
+
id: new Id("1"),
|
|
73
|
+
title: "Post 1",
|
|
74
|
+
content: "Content 1",
|
|
75
|
+
likes: 0,
|
|
76
|
+
}),
|
|
77
|
+
],
|
|
78
|
+
address: new Address({
|
|
79
|
+
street: "Main St",
|
|
80
|
+
city: "NYC",
|
|
81
|
+
zipCode: "10001",
|
|
82
|
+
}),
|
|
83
|
+
comments: [],
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const json = user.toJson();
|
|
87
|
+
expect(typeof json).toBe("object");
|
|
88
|
+
expect(Array.isArray(json.posts)).toBe(true);
|
|
89
|
+
expect(json.posts[0].id).toBe("1");
|
|
90
|
+
});
|
|
91
|
+
});
|
package/tests/utils.ts
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// Test Entities & Value Objects
|
|
3
|
+
// ============================================================================
|
|
4
|
+
|
|
5
|
+
import { Aggregate, Entity, Id, ValueObject } from "../src";
|
|
6
|
+
|
|
7
|
+
interface AddressProps {
|
|
8
|
+
street: string;
|
|
9
|
+
city: string;
|
|
10
|
+
zipCode: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
class Address extends ValueObject<AddressProps> {
|
|
14
|
+
get street(): string {
|
|
15
|
+
return this.props.street;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
get city(): string {
|
|
19
|
+
return this.props.city;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
get zipCode(): string {
|
|
23
|
+
return this.props.zipCode;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface PostProps {
|
|
28
|
+
id: Id;
|
|
29
|
+
title: string;
|
|
30
|
+
content: string;
|
|
31
|
+
likes: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
class Post extends Entity<PostProps> {
|
|
35
|
+
get title(): string {
|
|
36
|
+
return this.props.title;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
set title(value: string) {
|
|
40
|
+
this.props.title = value;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
get content(): string {
|
|
44
|
+
return this.props.content;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
set content(value: string) {
|
|
48
|
+
this.props.content = value;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
get likes(): number {
|
|
52
|
+
return this.props.likes;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
set likes(value: number) {
|
|
56
|
+
this.props.likes = value;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
interface CommentProps {
|
|
61
|
+
id: Id;
|
|
62
|
+
text: string;
|
|
63
|
+
author: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
class Comment extends Entity<CommentProps> {
|
|
67
|
+
get text(): string {
|
|
68
|
+
return this.props.text;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
set text(value: string) {
|
|
72
|
+
this.props.text = value;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
get author(): string {
|
|
76
|
+
return this.props.author;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
interface UserProps {
|
|
81
|
+
id: Id;
|
|
82
|
+
name: string;
|
|
83
|
+
email: string;
|
|
84
|
+
posts: Post[];
|
|
85
|
+
address: Address;
|
|
86
|
+
comments: Comment[];
|
|
87
|
+
extra?: {
|
|
88
|
+
age: number;
|
|
89
|
+
height: number;
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
class User extends Aggregate<UserProps> {
|
|
94
|
+
get name(): string {
|
|
95
|
+
return this.props.name;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
set name(value: string) {
|
|
99
|
+
this.props.name = value;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
get email(): string {
|
|
103
|
+
return this.props.email;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
get posts(): Post[] {
|
|
107
|
+
return this.props.posts;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
set posts(value: Post[]) {
|
|
111
|
+
this.props.posts = value;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
get address(): Address {
|
|
115
|
+
return this.props.address;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
set address(value: Address) {
|
|
119
|
+
this.props.address = value;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
get comments(): Comment[] {
|
|
123
|
+
return this.props.comments;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
set comments(value: Comment[]) {
|
|
127
|
+
this.props.comments = value;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
public addPost(post: Post) {
|
|
131
|
+
this.props.posts.push(post);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
public addManyPosts(posts: Post[]) {
|
|
135
|
+
this.props.posts.push(...posts);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
public removePostById(id: string) {
|
|
139
|
+
this.props.posts = this.props.posts.filter((post) => post.id.value !== id);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
public changeEmail(email: string) {
|
|
143
|
+
this.props.email = email;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
public changeExtra(extra: { age: number; height: number }) {
|
|
147
|
+
this.props.extra = extra;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export { User, Post, Comment, Address };
|
|
@@ -0,0 +1,228 @@
|
|
|
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
|
+
defaultValues: {
|
|
58
|
+
currency: "USD",
|
|
59
|
+
},
|
|
60
|
+
rules: (money) => {
|
|
61
|
+
if (money.amount > 1000000) {
|
|
62
|
+
throwValidationError("amount", "Amount cannot exceed 1,000,000");
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
get amount(): number {
|
|
68
|
+
return this.props.amount;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
get currency(): string {
|
|
72
|
+
return this.props.currency;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
add(other: Money): Money {
|
|
76
|
+
if (this.currency !== other.currency) {
|
|
77
|
+
throw new Error("Cannot add money with different currencies");
|
|
78
|
+
}
|
|
79
|
+
return this.clone({ amount: this.amount + other.amount });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ============================================================================
|
|
84
|
+
// Test Value Object with throwOnError: false
|
|
85
|
+
// ============================================================================
|
|
86
|
+
|
|
87
|
+
class EmailSafe extends ValueObject<EmailProps> {
|
|
88
|
+
protected static validation: VOValidation<EmailProps> = {
|
|
89
|
+
schema: emailSchema,
|
|
90
|
+
config: {
|
|
91
|
+
onCreate: true,
|
|
92
|
+
throwOnError: false,
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
get value(): string {
|
|
97
|
+
return this.props.value;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ============================================================================
|
|
102
|
+
// Tests
|
|
103
|
+
// ============================================================================
|
|
104
|
+
|
|
105
|
+
describe("ValueObject with Validation", () => {
|
|
106
|
+
describe("Email ValueObject", () => {
|
|
107
|
+
it("should create email with valid value", () => {
|
|
108
|
+
const email = new Email({ value: "test@example.com" });
|
|
109
|
+
expect(email.value).toBe("test@example.com");
|
|
110
|
+
expect(email.hasValidationErrors).toBe(false);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("should throw on invalid email format", () => {
|
|
114
|
+
expect(() => {
|
|
115
|
+
new Email({ value: "invalid-email" });
|
|
116
|
+
}).toThrow(ValidationError);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("should have correct error message", () => {
|
|
120
|
+
try {
|
|
121
|
+
new Email({ value: "invalid" });
|
|
122
|
+
} catch (error) {
|
|
123
|
+
expect(ValidationError.isValidationError(error)).toBe(true);
|
|
124
|
+
if (ValidationError.isValidationError(error)) {
|
|
125
|
+
expect(error.getMessages()).toContain("Invalid email format");
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
describe("Money ValueObject with Hooks", () => {
|
|
132
|
+
it("should create money with valid data", () => {
|
|
133
|
+
const money = new Money({ amount: 100, currency: "USD" });
|
|
134
|
+
expect(money.amount).toBe(100);
|
|
135
|
+
expect(money.currency).toBe("USD");
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("should apply default currency", () => {
|
|
139
|
+
const money = new Money({ amount: 50 } as any);
|
|
140
|
+
expect(money.amount).toBe(50);
|
|
141
|
+
expect(money.currency).toBe("USD");
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("should throw on negative amount", () => {
|
|
145
|
+
expect(() => {
|
|
146
|
+
new Money({ amount: -10, currency: "USD" });
|
|
147
|
+
}).toThrow(ValidationError);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("should throw on invalid currency code", () => {
|
|
151
|
+
expect(() => {
|
|
152
|
+
new Money({ amount: 100, currency: "US" });
|
|
153
|
+
}).toThrow(ValidationError);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it("should throw on custom rule violation (amount > 1M)", () => {
|
|
157
|
+
expect(() => {
|
|
158
|
+
new Money({ amount: 1000001, currency: "USD" });
|
|
159
|
+
}).toThrow(ValidationError);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it("should add money with same currency", () => {
|
|
163
|
+
const m1 = new Money({ amount: 100, currency: "USD" });
|
|
164
|
+
const m2 = new Money({ amount: 50, currency: "USD" });
|
|
165
|
+
const result = m1.add(m2);
|
|
166
|
+
|
|
167
|
+
expect(result.amount).toBe(150);
|
|
168
|
+
expect(result.currency).toBe("USD");
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("should throw when adding different currencies", () => {
|
|
172
|
+
const m1 = new Money({ amount: 100, currency: "USD" });
|
|
173
|
+
const m2 = new Money({ amount: 50, currency: "EUR" });
|
|
174
|
+
|
|
175
|
+
expect(() => m1.add(m2)).toThrow(
|
|
176
|
+
"Cannot add money with different currencies"
|
|
177
|
+
);
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
describe("Email ValueObject with throwOnError: false", () => {
|
|
182
|
+
it("should not throw on invalid email", () => {
|
|
183
|
+
const email = new EmailSafe({ value: "invalid" });
|
|
184
|
+
expect(email.hasValidationErrors).toBe(true);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("should store validation errors", () => {
|
|
188
|
+
const email = new EmailSafe({ value: "not-an-email" });
|
|
189
|
+
|
|
190
|
+
expect(email.validationErrors).toBeDefined();
|
|
191
|
+
expect(email.validationErrors?.getMessages()).toContain(
|
|
192
|
+
"Invalid email format"
|
|
193
|
+
);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it("should not have errors for valid email", () => {
|
|
197
|
+
const email = new EmailSafe({ value: "valid@example.com" });
|
|
198
|
+
|
|
199
|
+
expect(email.hasValidationErrors).toBe(false);
|
|
200
|
+
expect(email.validationErrors).toBeUndefined();
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
describe("ValueObject Immutability", () => {
|
|
205
|
+
it("should remain immutable with validation", () => {
|
|
206
|
+
const email = new Email({ value: "test@example.com" });
|
|
207
|
+
|
|
208
|
+
expect(() => {
|
|
209
|
+
(email as any).props.value = "changed@example.com";
|
|
210
|
+
}).toThrow();
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it("should create new instance when adding (immutability)", () => {
|
|
214
|
+
const m1 = new Money({ amount: 100, currency: "USD" });
|
|
215
|
+
const m2 = new Money({ amount: 50, currency: "USD" });
|
|
216
|
+
const result = m1.add(m2);
|
|
217
|
+
|
|
218
|
+
// Original instances unchanged
|
|
219
|
+
expect(m1.amount).toBe(100);
|
|
220
|
+
expect(m2.amount).toBe(50);
|
|
221
|
+
|
|
222
|
+
// New instance created
|
|
223
|
+
expect(result.amount).toBe(150);
|
|
224
|
+
expect(result).not.toBe(m1);
|
|
225
|
+
expect(result).not.toBe(m2);
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
});
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { Address } from "./utils";
|
|
2
|
+
|
|
3
|
+
describe("Value Object", () => {
|
|
4
|
+
it("should create immutable value object", () => {
|
|
5
|
+
const address = new Address({
|
|
6
|
+
street: "Main St",
|
|
7
|
+
city: "NYC",
|
|
8
|
+
zipCode: "10001",
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
expect(address.street).toBe("Main St");
|
|
12
|
+
expect(address.city).toBe("NYC");
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("should compare value objects by value", () => {
|
|
16
|
+
const address1 = new Address({
|
|
17
|
+
street: "Main St",
|
|
18
|
+
city: "NYC",
|
|
19
|
+
zipCode: "10001",
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const address2 = new Address({
|
|
23
|
+
street: "Main St",
|
|
24
|
+
city: "NYC",
|
|
25
|
+
zipCode: "10001",
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const address3 = new Address({
|
|
29
|
+
street: "Broadway",
|
|
30
|
+
city: "NYC",
|
|
31
|
+
zipCode: "10001",
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
expect(address1.equals(address2)).toBe(true);
|
|
35
|
+
expect(address1.equals(address3)).toBe(false);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("should convert value object to JSON", () => {
|
|
39
|
+
const address = new Address({
|
|
40
|
+
street: "Main St",
|
|
41
|
+
city: "NYC",
|
|
42
|
+
zipCode: "10001",
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const json = address.toJson();
|
|
46
|
+
expect(json).toEqual({
|
|
47
|
+
street: "Main St",
|
|
48
|
+
city: "NYC",
|
|
49
|
+
zipCode: "10001",
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
});
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "esnext",
|
|
5
|
+
"lib": ["ES2020"],
|
|
6
|
+
"declaration": true,
|
|
7
|
+
"declarationMap": true,
|
|
8
|
+
"sourceMap": true,
|
|
9
|
+
"outDir": "./dist",
|
|
10
|
+
"rootDir": "./src",
|
|
11
|
+
"strict": true,
|
|
12
|
+
"noImplicitAny": true,
|
|
13
|
+
"strictNullChecks": true,
|
|
14
|
+
"strictFunctionTypes": true,
|
|
15
|
+
"strictBindCallApply": true,
|
|
16
|
+
"strictPropertyInitialization": true,
|
|
17
|
+
"noImplicitThis": true,
|
|
18
|
+
"alwaysStrict": true,
|
|
19
|
+
"noUnusedLocals": false,
|
|
20
|
+
"noUnusedParameters": false,
|
|
21
|
+
"noImplicitReturns": true,
|
|
22
|
+
"noFallthroughCasesInSwitch": true,
|
|
23
|
+
"moduleResolution": "bundler",
|
|
24
|
+
"esModuleInterop": true,
|
|
25
|
+
"skipLibCheck": true,
|
|
26
|
+
"forceConsistentCasingInFileNames": true,
|
|
27
|
+
"resolveJsonModule": true
|
|
28
|
+
},
|
|
29
|
+
"include": ["src/**/*"],
|
|
30
|
+
"exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts"]
|
|
31
|
+
}
|