archstone 1.0.0 → 1.0.2
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/LICENSE +21 -0
- package/README.md +39 -17
- package/dist/core/index.js +6 -111
- package/dist/domain/index.js +1 -0
- package/dist/index.d.ts +637 -0
- package/dist/index.js +27 -0
- package/dist/shared/chunk-833sxwt1.js +112 -0
- package/dist/shared/chunk-x296z7h1.js +1 -0
- package/package.json +37 -1
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 João Henrique Benatti Coimbra
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,10 +1,32 @@
|
|
|
1
1
|
# archstone
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/archstone)
|
|
4
|
+
[](./LICENSE)
|
|
5
|
+
[](https://www.typescriptlang.org/)
|
|
6
|
+
[](https://bun.sh)
|
|
7
|
+
|
|
8
|
+
> TypeScript architecture foundation for backend services based on Domain-Driven Design (DDD) and Clean Architecture.
|
|
4
9
|
|
|
5
10
|
Archstone provides the core building blocks — entities, value objects, aggregates, domain events, use cases, and repository contracts — so you can focus on your domain logic instead of re-implementing the same structural patterns across every project.
|
|
6
11
|
|
|
7
|
-
##
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
bun add archstone
|
|
16
|
+
# or
|
|
17
|
+
npm install archstone
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Packages
|
|
21
|
+
|
|
22
|
+
| Import path | Contents |
|
|
23
|
+
|---|---|
|
|
24
|
+
| `archstone/core` | `Either`, `ValueObject`, `UniqueEntityId`, `WatchedList`, `Optional` |
|
|
25
|
+
| `archstone/domain` | All domain exports |
|
|
26
|
+
| `archstone/domain/enterprise` | `Entity`, `AggregateRoot`, `DomainEvent`, `DomainEvents`, `EventHandler` |
|
|
27
|
+
| `archstone/domain/application` | `UseCase`, `UseCaseError`, repository contracts |
|
|
28
|
+
|
|
29
|
+
## Architecture
|
|
8
30
|
|
|
9
31
|
```
|
|
10
32
|
src/
|
|
@@ -45,7 +67,7 @@ src/
|
|
|
45
67
|
Use cases never throw. They return an `Either<Error, Value>` — left for failure, right for success.
|
|
46
68
|
|
|
47
69
|
```ts
|
|
48
|
-
import { Either, left, right } from "
|
|
70
|
+
import { Either, left, right } from "archstone/core"
|
|
49
71
|
|
|
50
72
|
type Result = Either<UserNotFoundError, User>
|
|
51
73
|
|
|
@@ -65,9 +87,8 @@ else console.log(result.value) // User
|
|
|
65
87
|
Entities are defined by identity. Aggregates extend that with domain event support.
|
|
66
88
|
|
|
67
89
|
```ts
|
|
68
|
-
import { AggregateRoot } from "
|
|
69
|
-
import { UniqueEntityId } from "
|
|
70
|
-
import { Optional } from "@/core/types/optional"
|
|
90
|
+
import { AggregateRoot } from "archstone/domain/enterprise"
|
|
91
|
+
import { UniqueEntityId, Optional } from "archstone/core"
|
|
71
92
|
|
|
72
93
|
interface OrderProps {
|
|
73
94
|
customerId: UniqueEntityId
|
|
@@ -94,7 +115,7 @@ class Order extends AggregateRoot<OrderProps> {
|
|
|
94
115
|
Value objects are equal by their properties, not by reference.
|
|
95
116
|
|
|
96
117
|
```ts
|
|
97
|
-
import { ValueObject } from "
|
|
118
|
+
import { ValueObject } from "archstone/core"
|
|
98
119
|
|
|
99
120
|
interface EmailProps { value: string }
|
|
100
121
|
|
|
@@ -113,6 +134,8 @@ class Email extends ValueObject<EmailProps> {
|
|
|
113
134
|
Track additions and removals in a collection without rewriting the whole thing on save.
|
|
114
135
|
|
|
115
136
|
```ts
|
|
137
|
+
import { WatchedList } from "archstone/core"
|
|
138
|
+
|
|
116
139
|
class TagList extends WatchedList<Tag> {
|
|
117
140
|
compareItems(a: Tag, b: Tag) { return a.id.equals(b.id) }
|
|
118
141
|
}
|
|
@@ -130,6 +153,8 @@ tags.getRemovedItems() // [existingTag]
|
|
|
130
153
|
Events are raised inside aggregates and dispatched by the infrastructure layer after successful persistence.
|
|
131
154
|
|
|
132
155
|
```ts
|
|
156
|
+
import { DomainEvents } from "archstone/domain/enterprise"
|
|
157
|
+
|
|
133
158
|
// register a handler
|
|
134
159
|
DomainEvents.register(
|
|
135
160
|
(event) => sendWelcomeEmail(event as UserCreatedEvent),
|
|
@@ -146,6 +171,8 @@ DomainEvents.dispatchEventsForAggregate(user.id)
|
|
|
146
171
|
Repositories are defined as interfaces in the application layer. Implementations live in infrastructure.
|
|
147
172
|
|
|
148
173
|
```ts
|
|
174
|
+
import { Repository, Creatable } from "archstone/domain/application"
|
|
175
|
+
|
|
149
176
|
// application/repositories/user-repository.ts
|
|
150
177
|
export interface UserRepository extends Repository<User> {
|
|
151
178
|
findByEmail(email: string): Promise<User | null>
|
|
@@ -155,19 +182,14 @@ export interface UserRepository extends Repository<User> {
|
|
|
155
182
|
export interface AuditRepository extends Creatable<AuditLog> {}
|
|
156
183
|
```
|
|
157
184
|
|
|
158
|
-
##
|
|
185
|
+
## Contributing
|
|
159
186
|
|
|
160
|
-
|
|
161
|
-
bun install
|
|
162
|
-
bun test
|
|
163
|
-
```
|
|
187
|
+
Contributions are welcome! Please read [CONTRIBUTING.md](./CONTRIBUTING.md) first.
|
|
164
188
|
|
|
165
|
-
##
|
|
189
|
+
## Code of Conduct
|
|
166
190
|
|
|
167
|
-
|
|
168
|
-
- TypeScript 5 with strict mode
|
|
169
|
-
- Zero runtime dependencies
|
|
191
|
+
This project follows the [Contributor Covenant](./CODE_OF_CONDUCT.md).
|
|
170
192
|
|
|
171
193
|
## License
|
|
172
194
|
|
|
173
|
-
MIT
|
|
195
|
+
MIT — see [LICENSE](./LICENSE).
|
package/dist/core/index.js
CHANGED
|
@@ -1,118 +1,13 @@
|
|
|
1
1
|
// @bun
|
|
2
|
+
import {
|
|
3
|
+
ValueObject,
|
|
4
|
+
WatchedList,
|
|
5
|
+
left,
|
|
6
|
+
right
|
|
7
|
+
} from "../shared/chunk-833sxwt1.js";
|
|
2
8
|
import {
|
|
3
9
|
UniqueEntityId
|
|
4
10
|
} from "../shared/chunk-wzqmg7ms.js";
|
|
5
|
-
|
|
6
|
-
// src/core/either.ts
|
|
7
|
-
class Left {
|
|
8
|
-
value;
|
|
9
|
-
constructor(value) {
|
|
10
|
-
this.value = value;
|
|
11
|
-
}
|
|
12
|
-
isRight() {
|
|
13
|
-
return false;
|
|
14
|
-
}
|
|
15
|
-
isLeft() {
|
|
16
|
-
return true;
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
class Right {
|
|
21
|
-
value;
|
|
22
|
-
constructor(value) {
|
|
23
|
-
this.value = value;
|
|
24
|
-
}
|
|
25
|
-
isRight() {
|
|
26
|
-
return true;
|
|
27
|
-
}
|
|
28
|
-
isLeft() {
|
|
29
|
-
return false;
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
var left = (value) => new Left(value);
|
|
33
|
-
var right = (value) => new Right(value);
|
|
34
|
-
// src/core/value-object.ts
|
|
35
|
-
class ValueObject {
|
|
36
|
-
props;
|
|
37
|
-
constructor(props) {
|
|
38
|
-
this.props = props;
|
|
39
|
-
}
|
|
40
|
-
equals(other) {
|
|
41
|
-
return JSON.stringify(this.props) === JSON.stringify(other.props);
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
// src/core/watched-list.ts
|
|
45
|
-
class WatchedList {
|
|
46
|
-
currentItems;
|
|
47
|
-
initial;
|
|
48
|
-
new;
|
|
49
|
-
removed;
|
|
50
|
-
constructor(initialItems) {
|
|
51
|
-
this.currentItems = initialItems ?? [];
|
|
52
|
-
this.initial = initialItems ?? [];
|
|
53
|
-
this.new = [];
|
|
54
|
-
this.removed = [];
|
|
55
|
-
}
|
|
56
|
-
getItems() {
|
|
57
|
-
return this.currentItems;
|
|
58
|
-
}
|
|
59
|
-
getNewItems() {
|
|
60
|
-
return this.new;
|
|
61
|
-
}
|
|
62
|
-
getRemovedItems() {
|
|
63
|
-
return this.removed;
|
|
64
|
-
}
|
|
65
|
-
exists(item) {
|
|
66
|
-
return this.isCurrentItem(item);
|
|
67
|
-
}
|
|
68
|
-
add(item) {
|
|
69
|
-
if (this.isRemovedItem(item)) {
|
|
70
|
-
this.removeFromRemoved(item);
|
|
71
|
-
}
|
|
72
|
-
if (!(this.isNewItem(item) || this.wasAddedInitially(item))) {
|
|
73
|
-
this.new.push(item);
|
|
74
|
-
}
|
|
75
|
-
if (!this.isCurrentItem(item)) {
|
|
76
|
-
this.currentItems.push(item);
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
remove(item) {
|
|
80
|
-
this.removeFromCurrent(item);
|
|
81
|
-
if (this.isNewItem(item)) {
|
|
82
|
-
this.removeFromNew(item);
|
|
83
|
-
return;
|
|
84
|
-
}
|
|
85
|
-
if (!this.isRemovedItem(item)) {
|
|
86
|
-
this.removed.push(item);
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
update(items) {
|
|
90
|
-
this.new = items.filter((a) => !this.getItems().some((b) => this.compareItems(a, b)));
|
|
91
|
-
this.removed = this.getItems().filter((a) => !items.some((b) => this.compareItems(a, b)));
|
|
92
|
-
this.currentItems = items;
|
|
93
|
-
}
|
|
94
|
-
isCurrentItem(item) {
|
|
95
|
-
return this.currentItems.some((v) => this.compareItems(item, v));
|
|
96
|
-
}
|
|
97
|
-
isNewItem(item) {
|
|
98
|
-
return this.new.some((v) => this.compareItems(item, v));
|
|
99
|
-
}
|
|
100
|
-
isRemovedItem(item) {
|
|
101
|
-
return this.removed.some((v) => this.compareItems(item, v));
|
|
102
|
-
}
|
|
103
|
-
removeFromNew(item) {
|
|
104
|
-
this.new = this.new.filter((v) => !this.compareItems(v, item));
|
|
105
|
-
}
|
|
106
|
-
removeFromCurrent(item) {
|
|
107
|
-
this.currentItems = this.currentItems.filter((v) => !this.compareItems(item, v));
|
|
108
|
-
}
|
|
109
|
-
removeFromRemoved(item) {
|
|
110
|
-
this.removed = this.removed.filter((v) => !this.compareItems(item, v));
|
|
111
|
-
}
|
|
112
|
-
wasAddedInitially(item) {
|
|
113
|
-
return this.initial.some((v) => this.compareItems(item, v));
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
11
|
export {
|
|
117
12
|
right,
|
|
118
13
|
left,
|
package/dist/domain/index.js
CHANGED
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,637 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Represents the left side of an {@link Either} — conventionally used for
|
|
3
|
+
* failure or error values.
|
|
4
|
+
*
|
|
5
|
+
* @template L - The type of the failure value
|
|
6
|
+
* @template R - The type of the success value
|
|
7
|
+
*/
|
|
8
|
+
declare class Left<
|
|
9
|
+
L,
|
|
10
|
+
R
|
|
11
|
+
> {
|
|
12
|
+
readonly value: L;
|
|
13
|
+
constructor(value: L);
|
|
14
|
+
isRight(): this is Right<L, R>;
|
|
15
|
+
isLeft(): this is Left<L, R>;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Represents the right side of an {@link Either} — conventionally used for
|
|
19
|
+
* success values.
|
|
20
|
+
*
|
|
21
|
+
* @template L - The type of the failure value
|
|
22
|
+
* @template R - The type of the success value
|
|
23
|
+
*/
|
|
24
|
+
declare class Right<
|
|
25
|
+
L,
|
|
26
|
+
R
|
|
27
|
+
> {
|
|
28
|
+
readonly value: R;
|
|
29
|
+
constructor(value: R);
|
|
30
|
+
isRight(): this is Right<L, R>;
|
|
31
|
+
isLeft(): this is Left<L, R>;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* A discriminated union that represents a value of one of two possible types.
|
|
35
|
+
*
|
|
36
|
+
* Commonly used as a type-safe alternative to throwing exceptions — the left
|
|
37
|
+
* side carries an error, and the right side carries a success value.
|
|
38
|
+
*
|
|
39
|
+
* Use the {@link left} and {@link right} helper functions to construct values,
|
|
40
|
+
* and `isLeft()` / `isRight()` to narrow the type.
|
|
41
|
+
*
|
|
42
|
+
* @template L - The type of the failure value
|
|
43
|
+
* @template R - The type of the success value
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* ```ts
|
|
47
|
+
* type FindUserResult = Either<NotFoundError, User>
|
|
48
|
+
*
|
|
49
|
+
* function findUser(id: string): FindUserResult {
|
|
50
|
+
* const user = db.find(id)
|
|
51
|
+
* if (!user) return left(new NotFoundError("User", id))
|
|
52
|
+
* return right(user)
|
|
53
|
+
* }
|
|
54
|
+
*
|
|
55
|
+
* const result = findUser("123")
|
|
56
|
+
*
|
|
57
|
+
* if (result.isLeft()) {
|
|
58
|
+
* console.error(result.value) // NotFoundError
|
|
59
|
+
* } else {
|
|
60
|
+
* console.log(result.value) // User
|
|
61
|
+
* }
|
|
62
|
+
* ```
|
|
63
|
+
*/
|
|
64
|
+
type Either<
|
|
65
|
+
L,
|
|
66
|
+
R
|
|
67
|
+
> = Left<L, R> | Right<L, R>;
|
|
68
|
+
/**
|
|
69
|
+
* Constructs a {@link Left} value, representing a failure.
|
|
70
|
+
*
|
|
71
|
+
* @param value - The error or failure value
|
|
72
|
+
*/
|
|
73
|
+
declare const left: <
|
|
74
|
+
L,
|
|
75
|
+
R
|
|
76
|
+
>(value: L) => Either<L, R>;
|
|
77
|
+
/**
|
|
78
|
+
* Constructs a {@link Right} value, representing a success.
|
|
79
|
+
*
|
|
80
|
+
* @param value - The success value
|
|
81
|
+
*/
|
|
82
|
+
declare const right: <
|
|
83
|
+
L,
|
|
84
|
+
R
|
|
85
|
+
>(value: R) => Either<L, R>;
|
|
86
|
+
/**
|
|
87
|
+
* Make some property optional on type
|
|
88
|
+
*
|
|
89
|
+
* @example
|
|
90
|
+
* ```typescript
|
|
91
|
+
* type Post {
|
|
92
|
+
* id: string;
|
|
93
|
+
* name: string;
|
|
94
|
+
* email: string;
|
|
95
|
+
* }
|
|
96
|
+
*
|
|
97
|
+
* Optional<Post, 'id' | 'email'>
|
|
98
|
+
* ```
|
|
99
|
+
*/
|
|
100
|
+
type Optional<
|
|
101
|
+
T,
|
|
102
|
+
K extends keyof T
|
|
103
|
+
> = Pick<Partial<T>, K> & Omit<T, K>;
|
|
104
|
+
/**
|
|
105
|
+
* Represents a unique identifier for a domain entity.
|
|
106
|
+
*
|
|
107
|
+
* Wraps a UUID v7 string, which is time-sortable and ideal for database
|
|
108
|
+
* indexing. A new identifier is generated automatically if no value is provided.
|
|
109
|
+
*
|
|
110
|
+
* @example
|
|
111
|
+
* ```ts
|
|
112
|
+
* // auto-generated
|
|
113
|
+
* const id = new UniqueEntityId()
|
|
114
|
+
*
|
|
115
|
+
* // from existing value (e.g. reconstructing from database)
|
|
116
|
+
* const id = new UniqueEntityId("0195d810-5b3e-7000-8e3e-1a2b3c4d5e6f")
|
|
117
|
+
* ```
|
|
118
|
+
*/
|
|
119
|
+
declare class UniqueEntityId {
|
|
120
|
+
private readonly value;
|
|
121
|
+
constructor(value?: string);
|
|
122
|
+
/**
|
|
123
|
+
* Returns the identifier as a primitive string.
|
|
124
|
+
* Useful for serialization and database persistence.
|
|
125
|
+
*/
|
|
126
|
+
toValue(): string;
|
|
127
|
+
/**
|
|
128
|
+
* Returns the string representation of the identifier.
|
|
129
|
+
*/
|
|
130
|
+
toString(): string;
|
|
131
|
+
/**
|
|
132
|
+
* Compares this identifier with another by value equality.
|
|
133
|
+
*
|
|
134
|
+
* @param id - The identifier to compare against
|
|
135
|
+
* @returns `true` if both identifiers have the same value
|
|
136
|
+
*/
|
|
137
|
+
equals(id: UniqueEntityId): boolean;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Base class for all value objects in the domain.
|
|
141
|
+
*
|
|
142
|
+
* A value object is an object defined entirely by its attributes — it has no
|
|
143
|
+
* identity. Two value objects are considered equal if all their properties
|
|
144
|
+
* are deeply equal, regardless of reference.
|
|
145
|
+
*
|
|
146
|
+
* Value objects should be immutable. Avoid mutating `props` directly;
|
|
147
|
+
* instead, create a new instance with the updated values.
|
|
148
|
+
*
|
|
149
|
+
* All value objects must implement a static `create` factory method to
|
|
150
|
+
* encapsulate construction and validation logic, keeping the constructor
|
|
151
|
+
* protected from external instantiation.
|
|
152
|
+
*
|
|
153
|
+
* @template Props - The shape of the value object's properties
|
|
154
|
+
*
|
|
155
|
+
* @example
|
|
156
|
+
* ```ts
|
|
157
|
+
* interface EmailProps {
|
|
158
|
+
* value: string
|
|
159
|
+
* }
|
|
160
|
+
*
|
|
161
|
+
* class Email extends ValueObject<EmailProps> {
|
|
162
|
+
* get value() { return this.props.value }
|
|
163
|
+
*
|
|
164
|
+
* static create(email: string): Email {
|
|
165
|
+
* if (!email.includes("@")) {
|
|
166
|
+
* throw new ValidationError("Invalid email address.")
|
|
167
|
+
* }
|
|
168
|
+
*
|
|
169
|
+
* return new Email({ value: email })
|
|
170
|
+
* }
|
|
171
|
+
* }
|
|
172
|
+
* ```
|
|
173
|
+
*/
|
|
174
|
+
declare abstract class ValueObject<Props> {
|
|
175
|
+
protected props: Props;
|
|
176
|
+
protected constructor(props: Props);
|
|
177
|
+
/**
|
|
178
|
+
* Compares this value object with another by deep equality of their properties.
|
|
179
|
+
*
|
|
180
|
+
* @param other - The value object to compare against
|
|
181
|
+
* @returns `true` if both value objects have deeply equal properties
|
|
182
|
+
*/
|
|
183
|
+
equals(other: ValueObject<Props>): boolean;
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Tracks additions and removals of items in a collection, enabling
|
|
187
|
+
* efficient persistence of only what changed.
|
|
188
|
+
*
|
|
189
|
+
* Commonly used inside aggregate roots to manage one-to-many relationships
|
|
190
|
+
* without rewriting the entire collection on every save — only new and
|
|
191
|
+
* removed items are persisted.
|
|
192
|
+
*
|
|
193
|
+
* Subclasses must implement {@link compareItems} to define equality between items.
|
|
194
|
+
*
|
|
195
|
+
* @template T - The type of items in the list
|
|
196
|
+
*
|
|
197
|
+
* @example
|
|
198
|
+
* ```ts
|
|
199
|
+
* class TagList extends WatchedList<Tag> {
|
|
200
|
+
* compareItems(a: Tag, b: Tag): boolean {
|
|
201
|
+
* return a.id.equals(b.id)
|
|
202
|
+
* }
|
|
203
|
+
*
|
|
204
|
+
* static create(tags: Tag[]): TagList {
|
|
205
|
+
* return new TagList(tags)
|
|
206
|
+
* }
|
|
207
|
+
* }
|
|
208
|
+
*
|
|
209
|
+
* const tags = TagList.create([existingTag])
|
|
210
|
+
*
|
|
211
|
+
* tags.add(newTag) // tracked as new
|
|
212
|
+
* tags.remove(oldTag) // tracked as removed
|
|
213
|
+
*
|
|
214
|
+
* tags.getNewItems() // [newTag]
|
|
215
|
+
* tags.getRemovedItems() // [oldTag]
|
|
216
|
+
* ```
|
|
217
|
+
*/
|
|
218
|
+
declare abstract class WatchedList<T> {
|
|
219
|
+
protected currentItems: T[];
|
|
220
|
+
protected initial: T[];
|
|
221
|
+
protected new: T[];
|
|
222
|
+
protected removed: T[];
|
|
223
|
+
constructor(initialItems?: T[]);
|
|
224
|
+
/**
|
|
225
|
+
* Returns all current items in the list.
|
|
226
|
+
*/
|
|
227
|
+
getItems(): T[];
|
|
228
|
+
/**
|
|
229
|
+
* Returns items that were added since the list was created.
|
|
230
|
+
*/
|
|
231
|
+
getNewItems(): T[];
|
|
232
|
+
/**
|
|
233
|
+
* Returns items that were removed since the list was created.
|
|
234
|
+
*/
|
|
235
|
+
getRemovedItems(): T[];
|
|
236
|
+
/**
|
|
237
|
+
* Returns whether the given item exists in the current list.
|
|
238
|
+
*
|
|
239
|
+
* @param item - The item to check
|
|
240
|
+
*/
|
|
241
|
+
exists(item: T): boolean;
|
|
242
|
+
/**
|
|
243
|
+
* Adds an item to the list, tracking it as new if it wasn't in the initial set.
|
|
244
|
+
* If the item was previously removed, it is restored.
|
|
245
|
+
*
|
|
246
|
+
* @param item - The item to add
|
|
247
|
+
*/
|
|
248
|
+
add(item: T): void;
|
|
249
|
+
/**
|
|
250
|
+
* Removes an item from the list, tracking it as removed if it was in the initial set.
|
|
251
|
+
*
|
|
252
|
+
* @param item - The item to remove
|
|
253
|
+
*/
|
|
254
|
+
remove(item: T): void;
|
|
255
|
+
/**
|
|
256
|
+
* Replaces the entire list with a new set of items, automatically
|
|
257
|
+
* computing what was added and what was removed.
|
|
258
|
+
*
|
|
259
|
+
* @param items - The new set of items
|
|
260
|
+
*/
|
|
261
|
+
update(items: T[]): void;
|
|
262
|
+
private isCurrentItem;
|
|
263
|
+
private isNewItem;
|
|
264
|
+
private isRemovedItem;
|
|
265
|
+
private removeFromNew;
|
|
266
|
+
private removeFromCurrent;
|
|
267
|
+
private removeFromRemoved;
|
|
268
|
+
private wasAddedInitially;
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Contract for entities that can be created.
|
|
272
|
+
*
|
|
273
|
+
* @template T - The entity type
|
|
274
|
+
*/
|
|
275
|
+
interface Creatable<T> {
|
|
276
|
+
/**
|
|
277
|
+
* Persists a new entity for the first time.
|
|
278
|
+
*
|
|
279
|
+
* @param entity - The entity to create
|
|
280
|
+
*/
|
|
281
|
+
create(entity: T): Promise<void>;
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Contract for entities that can be deleted.
|
|
285
|
+
*
|
|
286
|
+
* @template T - The entity type
|
|
287
|
+
*/
|
|
288
|
+
interface Deletable<T> {
|
|
289
|
+
/**
|
|
290
|
+
* Deletes an entity from the repository.
|
|
291
|
+
*
|
|
292
|
+
* @param entity - The entity to delete
|
|
293
|
+
*/
|
|
294
|
+
delete(entity: T): Promise<void>;
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Contract for entities that can be retrieved by their unique identifier.
|
|
298
|
+
*
|
|
299
|
+
* @template T - The entity type
|
|
300
|
+
*/
|
|
301
|
+
interface Findable<T> {
|
|
302
|
+
/**
|
|
303
|
+
* Finds an entity by its unique identifier.
|
|
304
|
+
*
|
|
305
|
+
* @param id - The unique identifier of the entity
|
|
306
|
+
* @returns The entity if found, `null` otherwise
|
|
307
|
+
*/
|
|
308
|
+
findById(id: string): Promise<T | null>;
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Contract for entities that can be persisted.
|
|
312
|
+
*
|
|
313
|
+
* @template T - The entity type
|
|
314
|
+
*/
|
|
315
|
+
interface Saveable<T> {
|
|
316
|
+
/**
|
|
317
|
+
* Persists an existing entity.
|
|
318
|
+
*
|
|
319
|
+
* @param entity - The entity to save
|
|
320
|
+
*/
|
|
321
|
+
save(entity: T): Promise<void>;
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* Full CRUD repository contract, composing all segregated interfaces.
|
|
325
|
+
*
|
|
326
|
+
* Use this when the repository needs to support all operations. For more
|
|
327
|
+
* constrained repositories, prefer composing only the interfaces you need.
|
|
328
|
+
*
|
|
329
|
+
* @template T - The entity type
|
|
330
|
+
*
|
|
331
|
+
* @example
|
|
332
|
+
* ```ts
|
|
333
|
+
* // full CRUD
|
|
334
|
+
* interface UserRepository extends Repository<User> {
|
|
335
|
+
* findByEmail(email: string): Promise<User | null>
|
|
336
|
+
* }
|
|
337
|
+
*
|
|
338
|
+
* // read-only
|
|
339
|
+
* interface ReportRepository extends Findable<Report> {
|
|
340
|
+
* findByPeriod(start: Date, end: Date): Promise<Report[]>
|
|
341
|
+
* }
|
|
342
|
+
*
|
|
343
|
+
* // append-only
|
|
344
|
+
* interface AuditRepository extends Creatable<AuditLog> {}
|
|
345
|
+
* ```
|
|
346
|
+
*/
|
|
347
|
+
interface Repository<T> extends Findable<T>, Saveable<T>, Creatable<T>, Deletable<T> {}
|
|
348
|
+
/**
|
|
349
|
+
* Base contract for all use case errors.
|
|
350
|
+
*
|
|
351
|
+
* Implement this interface to define semantic, domain-aware errors
|
|
352
|
+
* that can be returned as the left side of an {@link Either}.
|
|
353
|
+
*
|
|
354
|
+
* @example
|
|
355
|
+
* ```ts
|
|
356
|
+
* class UserNotFoundError implements UseCaseError {
|
|
357
|
+
* message = "User not found."
|
|
358
|
+
* }
|
|
359
|
+
*
|
|
360
|
+
* type FindUserResult = Either<UserNotFoundError, User>
|
|
361
|
+
* ```
|
|
362
|
+
*/
|
|
363
|
+
interface UseCaseError {
|
|
364
|
+
message: string;
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* Represents the expected output shape of any use case.
|
|
368
|
+
* Always an {@link Either} — left for errors, right for success.
|
|
369
|
+
*/
|
|
370
|
+
type UseCaseOutput = Either<UseCaseError | never, unknown | null>;
|
|
371
|
+
/**
|
|
372
|
+
* Base contract for all application use cases.
|
|
373
|
+
*
|
|
374
|
+
* A use case orchestrates domain logic for a single application operation.
|
|
375
|
+
* It receives a typed input, interacts with the domain, and returns an
|
|
376
|
+
* {@link Either} — never throwing exceptions directly.
|
|
377
|
+
*
|
|
378
|
+
* The left side carries a {@link UseCaseError} on failure.
|
|
379
|
+
* The right side carries the success value.
|
|
380
|
+
*
|
|
381
|
+
* @template Input - The shape of the data required to execute the use case
|
|
382
|
+
* @template Output - The expected {@link Either} output, constrained to {@link UseCaseOutput}
|
|
383
|
+
*
|
|
384
|
+
* @example
|
|
385
|
+
* ```ts
|
|
386
|
+
* type Input = { userId: string }
|
|
387
|
+
* type Output = Either<UserNotFoundError, User>
|
|
388
|
+
*
|
|
389
|
+
* class GetUserUseCase implements UseCase<Input, Output> {
|
|
390
|
+
* constructor(private userRepository: UserRepository) {}
|
|
391
|
+
*
|
|
392
|
+
* async execute({ userId }: Input): Promise<Output> {
|
|
393
|
+
* const user = await this.userRepository.findById(userId)
|
|
394
|
+
* if (!user) return left(new UserNotFoundError(userId))
|
|
395
|
+
* return right(user)
|
|
396
|
+
* }
|
|
397
|
+
* }
|
|
398
|
+
* ```
|
|
399
|
+
*/
|
|
400
|
+
interface UseCase<
|
|
401
|
+
Input,
|
|
402
|
+
Output extends UseCaseOutput
|
|
403
|
+
> {
|
|
404
|
+
execute(input: Input): Promise<Output>;
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
407
|
+
* Base contract for all domain events.
|
|
408
|
+
*
|
|
409
|
+
* A domain event represents something meaningful that happened in the domain.
|
|
410
|
+
* Events are raised by aggregate roots and dispatched after successful persistence,
|
|
411
|
+
* enabling decoupled side effects such as notifications, projections, and integrations.
|
|
412
|
+
*
|
|
413
|
+
* @example
|
|
414
|
+
* ```ts
|
|
415
|
+
* class UserCreatedEvent implements DomainEvent {
|
|
416
|
+
* occurredAt = new Date()
|
|
417
|
+
*
|
|
418
|
+
* constructor(private readonly user: User) {}
|
|
419
|
+
*
|
|
420
|
+
* getAggregateId(): UniqueEntityId {
|
|
421
|
+
* return this.user.id
|
|
422
|
+
* }
|
|
423
|
+
* }
|
|
424
|
+
* ```
|
|
425
|
+
*/
|
|
426
|
+
interface DomainEvent {
|
|
427
|
+
/**
|
|
428
|
+
* Returns the identifier of the aggregate root that raised this event.
|
|
429
|
+
*/
|
|
430
|
+
getAggregateId(): UniqueEntityId;
|
|
431
|
+
/**
|
|
432
|
+
* The date and time when the event occurred.
|
|
433
|
+
*/
|
|
434
|
+
occurredAt: Date;
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* Callback function invoked when a domain event is dispatched.
|
|
438
|
+
*/
|
|
439
|
+
type DomainEventCallback = (event: DomainEvent) => void;
|
|
440
|
+
/**
|
|
441
|
+
* Central registry and dispatcher for domain events.
|
|
442
|
+
*
|
|
443
|
+
* Aggregates register themselves for dispatch after raising events.
|
|
444
|
+
* The infrastructure layer is responsible for calling {@link dispatchEventsForAggregate}
|
|
445
|
+
* after successfully persisting an aggregate, ensuring events are only
|
|
446
|
+
* dispatched after a successful transaction.
|
|
447
|
+
*
|
|
448
|
+
* @example
|
|
449
|
+
* ```ts
|
|
450
|
+
* // register a handler
|
|
451
|
+
* DomainEvents.register(
|
|
452
|
+
* (event) => sendWelcomeEmail(event as UserCreatedEvent),
|
|
453
|
+
* UserCreatedEvent.name,
|
|
454
|
+
* )
|
|
455
|
+
*
|
|
456
|
+
* // dispatch after persistence
|
|
457
|
+
* await userRepository.create(user)
|
|
458
|
+
* DomainEvents.dispatchEventsForAggregate(user.id)
|
|
459
|
+
* ```
|
|
460
|
+
*/
|
|
461
|
+
declare class DomainEventsImplementation {
|
|
462
|
+
private readonly handlersMap;
|
|
463
|
+
private readonly markedAggregates;
|
|
464
|
+
/**
|
|
465
|
+
* Marks an aggregate root to have its events dispatched.
|
|
466
|
+
* Called automatically by {@link AggregateRoot.addDomainEvent}.
|
|
467
|
+
*
|
|
468
|
+
* @param aggregate - The aggregate to mark for dispatch
|
|
469
|
+
*/
|
|
470
|
+
markAggregateForDispatch(aggregate: AggregateRoot<unknown>): void;
|
|
471
|
+
/**
|
|
472
|
+
* Dispatches all pending events for the aggregate with the given id,
|
|
473
|
+
* clears its events, and removes it from the dispatch list.
|
|
474
|
+
*
|
|
475
|
+
* Should be called by the infrastructure layer after persisting the aggregate.
|
|
476
|
+
*
|
|
477
|
+
* @param id - The identifier of the aggregate to dispatch events for
|
|
478
|
+
*/
|
|
479
|
+
dispatchEventsForAggregate(id: UniqueEntityId): void;
|
|
480
|
+
/**
|
|
481
|
+
* Registers an event handler for a given event class name.
|
|
482
|
+
*
|
|
483
|
+
* @param callback - The function to invoke when the event is dispatched
|
|
484
|
+
* @param eventClassName - The name of the event class to listen for
|
|
485
|
+
*/
|
|
486
|
+
register(callback: DomainEventCallback, eventClassName: string): void;
|
|
487
|
+
/**
|
|
488
|
+
* Removes all registered event handlers.
|
|
489
|
+
* Useful for test isolation.
|
|
490
|
+
*/
|
|
491
|
+
clearHandlers(): void;
|
|
492
|
+
/**
|
|
493
|
+
* Removes all aggregates from the dispatch list.
|
|
494
|
+
* Useful for test isolation.
|
|
495
|
+
*/
|
|
496
|
+
clearMarkedAggregates(): void;
|
|
497
|
+
private dispatchAggregateEvents;
|
|
498
|
+
private removeAggregateFromMarkedDispatchList;
|
|
499
|
+
private findMarkedAggregateByID;
|
|
500
|
+
private dispatch;
|
|
501
|
+
}
|
|
502
|
+
/**
|
|
503
|
+
* Singleton instance of the domain events registry.
|
|
504
|
+
* Use this to register handlers and dispatch events across the application.
|
|
505
|
+
*/
|
|
506
|
+
declare const DomainEvents: DomainEventsImplementation;
|
|
507
|
+
/**
|
|
508
|
+
* Base contract for all event handlers.
|
|
509
|
+
*
|
|
510
|
+
* An event handler is responsible for subscribing to domain events
|
|
511
|
+
* and executing side effects in response — such as sending emails,
|
|
512
|
+
* updating read models, or triggering external integrations.
|
|
513
|
+
*
|
|
514
|
+
* All handlers must implement {@link setupSubscriptions} to register
|
|
515
|
+
* their callbacks in the {@link DomainEvents} registry.
|
|
516
|
+
*
|
|
517
|
+
* @example
|
|
518
|
+
* ```ts
|
|
519
|
+
* class OnUserCreated implements EventHandler {
|
|
520
|
+
* constructor(private readonly mailer: Mailer) {}
|
|
521
|
+
*
|
|
522
|
+
* setupSubscriptions(): void {
|
|
523
|
+
* DomainEvents.register(
|
|
524
|
+
* (event) => this.sendWelcomeEmail(event as UserCreatedEvent),
|
|
525
|
+
* UserCreatedEvent.name,
|
|
526
|
+
* )
|
|
527
|
+
* }
|
|
528
|
+
*
|
|
529
|
+
* private async sendWelcomeEmail(event: UserCreatedEvent): Promise<void> {
|
|
530
|
+
* await this.mailer.send(event.user.email)
|
|
531
|
+
* }
|
|
532
|
+
* }
|
|
533
|
+
* ```
|
|
534
|
+
*/
|
|
535
|
+
interface EventHandler {
|
|
536
|
+
/**
|
|
537
|
+
* Registers all event subscriptions for this handler.
|
|
538
|
+
* Should be called once during application bootstrap.
|
|
539
|
+
*/
|
|
540
|
+
setupSubscriptions(): void;
|
|
541
|
+
}
|
|
542
|
+
/**
|
|
543
|
+
* Base class for all domain entities.
|
|
544
|
+
*
|
|
545
|
+
* An entity is an object defined by its identity rather than its attributes.
|
|
546
|
+
* Two entities are considered equal if they share the same `id`, regardless
|
|
547
|
+
* of their other properties.
|
|
548
|
+
*
|
|
549
|
+
* All entities must implement a static `create` factory method to encapsulate
|
|
550
|
+
* construction logic and keep the constructor protected.
|
|
551
|
+
*
|
|
552
|
+
* @template Props - The shape of the entity's properties
|
|
553
|
+
*
|
|
554
|
+
* @example
|
|
555
|
+
* ```ts
|
|
556
|
+
* interface UserProps {
|
|
557
|
+
* id: UniqueEntityId
|
|
558
|
+
* name: string
|
|
559
|
+
* email: string
|
|
560
|
+
* createdAt: Date
|
|
561
|
+
* }
|
|
562
|
+
*
|
|
563
|
+
* class User extends Entity<UserProps> {
|
|
564
|
+
* get name() { return this.props.name }
|
|
565
|
+
* get email() { return this.props.email }
|
|
566
|
+
*
|
|
567
|
+
* static create(props: Optional<UserProps, "id" | "createdAt">): User {
|
|
568
|
+
* return new User(
|
|
569
|
+
* { ...props, createdAt: props.createdAt ?? new Date() },
|
|
570
|
+
* props.id ?? new UniqueEntityId(),
|
|
571
|
+
* )
|
|
572
|
+
* }
|
|
573
|
+
* }
|
|
574
|
+
* ```
|
|
575
|
+
*/
|
|
576
|
+
declare abstract class Entity<Props> {
|
|
577
|
+
private readonly _id;
|
|
578
|
+
protected props: Props;
|
|
579
|
+
get id(): UniqueEntityId;
|
|
580
|
+
protected constructor(props: Props, id?: UniqueEntityId);
|
|
581
|
+
/**
|
|
582
|
+
* Compares this entity with another by identity.
|
|
583
|
+
*
|
|
584
|
+
* @param entity - The entity to compare against
|
|
585
|
+
* @returns `true` if both entities share the same `id`
|
|
586
|
+
*/
|
|
587
|
+
equals(entity: Entity<unknown>): boolean;
|
|
588
|
+
}
|
|
589
|
+
/**
|
|
590
|
+
* Base class for all aggregate roots in the domain.
|
|
591
|
+
*
|
|
592
|
+
* An aggregate root is the entry point to an aggregate — a cluster of domain
|
|
593
|
+
* objects that are treated as a single unit. It is responsible for enforcing
|
|
594
|
+
* invariants and coordinating domain events within its boundary.
|
|
595
|
+
*
|
|
596
|
+
* Domain events are collected internally and dispatched after the aggregate
|
|
597
|
+
* is persisted, ensuring side effects only occur after a successful transaction.
|
|
598
|
+
*
|
|
599
|
+
* @template Props - The shape of the aggregate's properties
|
|
600
|
+
*
|
|
601
|
+
* @example
|
|
602
|
+
* ```ts
|
|
603
|
+
* class User extends AggregateRoot<UserProps> {
|
|
604
|
+
* static create(props: Optional<UserProps, "id" | "createdAt">): User {
|
|
605
|
+
* const user = new User(
|
|
606
|
+
* { ...props, createdAt: props.createdAt ?? new Date() },
|
|
607
|
+
* props.id ?? new UniqueEntityId(),
|
|
608
|
+
* )
|
|
609
|
+
*
|
|
610
|
+
* user.addDomainEvent(new UserCreatedEvent(user))
|
|
611
|
+
*
|
|
612
|
+
* return user
|
|
613
|
+
* }
|
|
614
|
+
* }
|
|
615
|
+
* ```
|
|
616
|
+
*/
|
|
617
|
+
declare abstract class AggregateRoot<Props> extends Entity<Props> {
|
|
618
|
+
private readonly _domainEvents;
|
|
619
|
+
/**
|
|
620
|
+
* Returns all domain events that have been raised by this aggregate
|
|
621
|
+
* and are pending dispatch.
|
|
622
|
+
*/
|
|
623
|
+
get domainEvents(): DomainEvent[];
|
|
624
|
+
/**
|
|
625
|
+
* Registers a domain event to be dispatched after the aggregate is persisted.
|
|
626
|
+
* Automatically marks this aggregate for dispatch in the {@link DomainEvents} registry.
|
|
627
|
+
*
|
|
628
|
+
* @param domainEvent - The domain event to register
|
|
629
|
+
*/
|
|
630
|
+
protected addDomainEvent(domainEvent: DomainEvent): void;
|
|
631
|
+
/**
|
|
632
|
+
* Clears all pending domain events from this aggregate.
|
|
633
|
+
* Should be called by the infrastructure layer after events are dispatched.
|
|
634
|
+
*/
|
|
635
|
+
clearEvents(): void;
|
|
636
|
+
}
|
|
637
|
+
export { right, left, WatchedList, ValueObject, UseCaseError, UseCase, UniqueEntityId, Saveable, Repository, Optional, Findable, EventHandler, Entity, Either, DomainEvents, DomainEvent, Deletable, Creatable, AggregateRoot };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
import {
|
|
3
|
+
ValueObject,
|
|
4
|
+
WatchedList,
|
|
5
|
+
left,
|
|
6
|
+
right
|
|
7
|
+
} from "./shared/chunk-833sxwt1.js";
|
|
8
|
+
import"./shared/chunk-x296z7h1.js";
|
|
9
|
+
import"./shared/chunk-wsjyhnpq.js";
|
|
10
|
+
import {
|
|
11
|
+
AggregateRoot,
|
|
12
|
+
DomainEvents,
|
|
13
|
+
Entity
|
|
14
|
+
} from "./shared/chunk-9cq8r162.js";
|
|
15
|
+
import {
|
|
16
|
+
UniqueEntityId
|
|
17
|
+
} from "./shared/chunk-wzqmg7ms.js";
|
|
18
|
+
export {
|
|
19
|
+
right,
|
|
20
|
+
left,
|
|
21
|
+
WatchedList,
|
|
22
|
+
ValueObject,
|
|
23
|
+
UniqueEntityId,
|
|
24
|
+
Entity,
|
|
25
|
+
DomainEvents,
|
|
26
|
+
AggregateRoot
|
|
27
|
+
};
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// src/core/either.ts
|
|
3
|
+
class Left {
|
|
4
|
+
value;
|
|
5
|
+
constructor(value) {
|
|
6
|
+
this.value = value;
|
|
7
|
+
}
|
|
8
|
+
isRight() {
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
isLeft() {
|
|
12
|
+
return true;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
class Right {
|
|
17
|
+
value;
|
|
18
|
+
constructor(value) {
|
|
19
|
+
this.value = value;
|
|
20
|
+
}
|
|
21
|
+
isRight() {
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
isLeft() {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
var left = (value) => new Left(value);
|
|
29
|
+
var right = (value) => new Right(value);
|
|
30
|
+
// src/core/value-object.ts
|
|
31
|
+
class ValueObject {
|
|
32
|
+
props;
|
|
33
|
+
constructor(props) {
|
|
34
|
+
this.props = props;
|
|
35
|
+
}
|
|
36
|
+
equals(other) {
|
|
37
|
+
return JSON.stringify(this.props) === JSON.stringify(other.props);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
// src/core/watched-list.ts
|
|
41
|
+
class WatchedList {
|
|
42
|
+
currentItems;
|
|
43
|
+
initial;
|
|
44
|
+
new;
|
|
45
|
+
removed;
|
|
46
|
+
constructor(initialItems) {
|
|
47
|
+
this.currentItems = initialItems ?? [];
|
|
48
|
+
this.initial = initialItems ?? [];
|
|
49
|
+
this.new = [];
|
|
50
|
+
this.removed = [];
|
|
51
|
+
}
|
|
52
|
+
getItems() {
|
|
53
|
+
return this.currentItems;
|
|
54
|
+
}
|
|
55
|
+
getNewItems() {
|
|
56
|
+
return this.new;
|
|
57
|
+
}
|
|
58
|
+
getRemovedItems() {
|
|
59
|
+
return this.removed;
|
|
60
|
+
}
|
|
61
|
+
exists(item) {
|
|
62
|
+
return this.isCurrentItem(item);
|
|
63
|
+
}
|
|
64
|
+
add(item) {
|
|
65
|
+
if (this.isRemovedItem(item)) {
|
|
66
|
+
this.removeFromRemoved(item);
|
|
67
|
+
}
|
|
68
|
+
if (!(this.isNewItem(item) || this.wasAddedInitially(item))) {
|
|
69
|
+
this.new.push(item);
|
|
70
|
+
}
|
|
71
|
+
if (!this.isCurrentItem(item)) {
|
|
72
|
+
this.currentItems.push(item);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
remove(item) {
|
|
76
|
+
this.removeFromCurrent(item);
|
|
77
|
+
if (this.isNewItem(item)) {
|
|
78
|
+
this.removeFromNew(item);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (!this.isRemovedItem(item)) {
|
|
82
|
+
this.removed.push(item);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
update(items) {
|
|
86
|
+
this.new = items.filter((a) => !this.getItems().some((b) => this.compareItems(a, b)));
|
|
87
|
+
this.removed = this.getItems().filter((a) => !items.some((b) => this.compareItems(a, b)));
|
|
88
|
+
this.currentItems = items;
|
|
89
|
+
}
|
|
90
|
+
isCurrentItem(item) {
|
|
91
|
+
return this.currentItems.some((v) => this.compareItems(item, v));
|
|
92
|
+
}
|
|
93
|
+
isNewItem(item) {
|
|
94
|
+
return this.new.some((v) => this.compareItems(item, v));
|
|
95
|
+
}
|
|
96
|
+
isRemovedItem(item) {
|
|
97
|
+
return this.removed.some((v) => this.compareItems(item, v));
|
|
98
|
+
}
|
|
99
|
+
removeFromNew(item) {
|
|
100
|
+
this.new = this.new.filter((v) => !this.compareItems(v, item));
|
|
101
|
+
}
|
|
102
|
+
removeFromCurrent(item) {
|
|
103
|
+
this.currentItems = this.currentItems.filter((v) => !this.compareItems(item, v));
|
|
104
|
+
}
|
|
105
|
+
removeFromRemoved(item) {
|
|
106
|
+
this.removed = this.removed.filter((v) => !this.compareItems(item, v));
|
|
107
|
+
}
|
|
108
|
+
wasAddedInitially(item) {
|
|
109
|
+
return this.initial.some((v) => this.compareItems(item, v));
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
export { left, right, ValueObject, WatchedList };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
// @bun
|
package/package.json
CHANGED
|
@@ -1,12 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "archstone",
|
|
3
|
-
"
|
|
3
|
+
"description": "TypeScript architecture foundation for backend services based on Domain-Driven Design and Clean Architecture",
|
|
4
|
+
"version": "1.0.2",
|
|
4
5
|
"type": "module",
|
|
5
6
|
"private": false,
|
|
6
7
|
"files": [
|
|
7
8
|
"dist"
|
|
8
9
|
],
|
|
10
|
+
"module": "./dist/index.js",
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
9
12
|
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"import": {
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"default": "./dist/index.js"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
10
19
|
"./core": {
|
|
11
20
|
"import": {
|
|
12
21
|
"types": "./dist/core/index.d.ts",
|
|
@@ -33,6 +42,33 @@
|
|
|
33
42
|
},
|
|
34
43
|
"./package.json": "./package.json"
|
|
35
44
|
},
|
|
45
|
+
"keywords": [
|
|
46
|
+
"ddd",
|
|
47
|
+
"domain-driven-design",
|
|
48
|
+
"clean-architecture",
|
|
49
|
+
"typescript",
|
|
50
|
+
"entity",
|
|
51
|
+
"aggregate",
|
|
52
|
+
"value-object",
|
|
53
|
+
"either",
|
|
54
|
+
"domain-events",
|
|
55
|
+
"repository",
|
|
56
|
+
"use-case",
|
|
57
|
+
"bun"
|
|
58
|
+
],
|
|
59
|
+
"author": {
|
|
60
|
+
"name": "João Henrique Benatti Coimbra",
|
|
61
|
+
"url": "https://github.com/joao-coimbra"
|
|
62
|
+
},
|
|
63
|
+
"license": "MIT",
|
|
64
|
+
"homepage": "https://github.com/joao-coimbra/archstone#readme",
|
|
65
|
+
"repository": {
|
|
66
|
+
"type": "git",
|
|
67
|
+
"url": "git+https://github.com/joao-coimbra/archstone.git"
|
|
68
|
+
},
|
|
69
|
+
"bugs": {
|
|
70
|
+
"url": "https://github.com/joao-coimbra/archstone/issues"
|
|
71
|
+
},
|
|
36
72
|
"scripts": {
|
|
37
73
|
"build": "bunup",
|
|
38
74
|
"dev": "bunup --watch",
|