archstone 1.0.2 → 1.0.4
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/README.md +152 -93
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,94 +1,97 @@
|
|
|
1
|
-
|
|
1
|
+
<div align="center">
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
[](./LICENSE)
|
|
5
|
-
[](https://www.typescriptlang.org/)
|
|
6
|
-
[](https://bun.sh)
|
|
3
|
+
# Archstone
|
|
7
4
|
|
|
8
|
-
|
|
5
|
+
### The TypeScript foundation for serious backend services.
|
|
9
6
|
|
|
10
|
-
|
|
7
|
+
Build on Domain-Driven Design and Clean Architecture — without writing the same boilerplate on every project.
|
|
11
8
|
|
|
12
|
-
|
|
9
|
+
<br />
|
|
13
10
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
```
|
|
11
|
+
[](https://www.npmjs.com/package/archstone)
|
|
12
|
+
[](./LICENSE)
|
|
13
|
+
[](https://www.typescriptlang.org/)
|
|
14
|
+
[](https://bun.sh)
|
|
19
15
|
|
|
20
|
-
|
|
16
|
+
<br />
|
|
21
17
|
|
|
22
|
-
|
|
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 |
|
|
18
|
+
</div>
|
|
28
19
|
|
|
29
|
-
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Why archstone?
|
|
30
23
|
|
|
24
|
+
Every backend project in DDD needs the same structural pieces — and most teams rewrite them from scratch each time. Archstone gives you a **battle-tested, zero-dependency set of base classes and contracts** so you can skip the boilerplate and go straight to modeling your domain.
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
// ❌ Before — scattered, inconsistent, no error contract
|
|
28
|
+
class User { id: string }
|
|
29
|
+
function createUser() { throw new Error('not found') }
|
|
30
|
+
|
|
31
|
+
// ✅ After — structured, predictable, type-safe
|
|
32
|
+
class User extends AggregateRoot<UserProps> { ... }
|
|
33
|
+
async function createUser(): Promise<Either<NotFoundError, User>> { ... }
|
|
31
34
|
```
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
│ ├── use-case.ts # UseCase<Input, Output> interface
|
|
54
|
-
│ └── use-case.error.ts # Base error type for use case failures
|
|
55
|
-
└── repositories/
|
|
56
|
-
├── repository.ts # Full CRUD contract (composed)
|
|
57
|
-
├── findabe.ts # Findable<T>
|
|
58
|
-
├── creatable.ts # Creatable<T>
|
|
59
|
-
├── saveble.ts # Saveable<T>
|
|
60
|
-
└── deletable.ts # Deletable<T>
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Features
|
|
39
|
+
|
|
40
|
+
- **`Either`** — functional error handling; use cases never throw
|
|
41
|
+
- **`Entity` / `AggregateRoot`** — identity-based domain objects with built-in event support
|
|
42
|
+
- **`ValueObject`** — equality by value, not reference
|
|
43
|
+
- **`UniqueEntityId`** — UUID v7 identity, consistent across your entire domain
|
|
44
|
+
- **`WatchedList`** — track additions and removals in collections without overwriting persistence
|
|
45
|
+
- **`UseCase`** — typed contract for application logic
|
|
46
|
+
- **Repository contracts** — define your interface in the domain; implement in infrastructure
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## Install
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
bun add archstone
|
|
54
|
+
# or
|
|
55
|
+
npm install archstone
|
|
61
56
|
```
|
|
62
57
|
|
|
63
|
-
|
|
58
|
+
> Zero runtime dependencies. Pure TypeScript.
|
|
64
59
|
|
|
65
|
-
|
|
60
|
+
---
|
|
66
61
|
|
|
67
|
-
|
|
62
|
+
## Usage
|
|
63
|
+
|
|
64
|
+
### `Either` — stop throwing, start returning
|
|
68
65
|
|
|
69
66
|
```ts
|
|
70
|
-
import { Either, left, right } from
|
|
67
|
+
import { Either, left, right } from 'archstone/core'
|
|
71
68
|
|
|
72
|
-
type
|
|
69
|
+
type FindUserResult = Either<UserNotFoundError, User>
|
|
73
70
|
|
|
74
|
-
async function findUser(id: string): Promise<
|
|
71
|
+
async function findUser(id: string): Promise<FindUserResult> {
|
|
75
72
|
const user = await repo.findById(id)
|
|
73
|
+
|
|
76
74
|
if (!user) return left(new UserNotFoundError(id))
|
|
77
75
|
return right(user)
|
|
78
76
|
}
|
|
79
77
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
78
|
+
// The caller always handles both cases — no surprises
|
|
79
|
+
const result = await findUser('123')
|
|
80
|
+
|
|
81
|
+
if (result.isLeft()) {
|
|
82
|
+
console.error(result.value) // UserNotFoundError
|
|
83
|
+
} else {
|
|
84
|
+
console.log(result.value) // User ✓
|
|
85
|
+
}
|
|
83
86
|
```
|
|
84
87
|
|
|
85
|
-
|
|
88
|
+
---
|
|
86
89
|
|
|
87
|
-
|
|
90
|
+
### `Entity` & `AggregateRoot` — model your domain
|
|
88
91
|
|
|
89
92
|
```ts
|
|
90
|
-
import { AggregateRoot } from
|
|
91
|
-
import { UniqueEntityId, Optional } from
|
|
93
|
+
import { AggregateRoot } from 'archstone/domain/enterprise'
|
|
94
|
+
import { UniqueEntityId, Optional } from 'archstone/core'
|
|
92
95
|
|
|
93
96
|
interface OrderProps {
|
|
94
97
|
customerId: UniqueEntityId
|
|
@@ -98,24 +101,27 @@ interface OrderProps {
|
|
|
98
101
|
|
|
99
102
|
class Order extends AggregateRoot<OrderProps> {
|
|
100
103
|
get customerId() { return this.props.customerId }
|
|
101
|
-
get total()
|
|
104
|
+
get total() { return this.props.total }
|
|
102
105
|
|
|
103
|
-
static create(props: Optional<OrderProps,
|
|
104
|
-
const order = new Order(
|
|
105
|
-
|
|
106
|
-
|
|
106
|
+
static create(props: Optional<OrderProps, 'createdAt'>): Order {
|
|
107
|
+
const order = new Order({
|
|
108
|
+
...props,
|
|
109
|
+
createdAt: props.createdAt ?? new Date(),
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
// Raise domain events from inside the aggregate
|
|
107
113
|
order.addDomainEvent(new OrderCreatedEvent(order))
|
|
108
114
|
return order
|
|
109
115
|
}
|
|
110
116
|
}
|
|
111
117
|
```
|
|
112
118
|
|
|
113
|
-
|
|
119
|
+
---
|
|
114
120
|
|
|
115
|
-
|
|
121
|
+
### `ValueObject` — equality that makes sense
|
|
116
122
|
|
|
117
123
|
```ts
|
|
118
|
-
import { ValueObject } from
|
|
124
|
+
import { ValueObject } from 'archstone/core'
|
|
119
125
|
|
|
120
126
|
interface EmailProps { value: string }
|
|
121
127
|
|
|
@@ -123,18 +129,23 @@ class Email extends ValueObject<EmailProps> {
|
|
|
123
129
|
get value() { return this.props.value }
|
|
124
130
|
|
|
125
131
|
static create(raw: string): Email {
|
|
126
|
-
if (!raw.includes(
|
|
132
|
+
if (!raw.includes('@')) throw new Error('Invalid email')
|
|
127
133
|
return new Email({ value: raw.toLowerCase() })
|
|
128
134
|
}
|
|
129
135
|
}
|
|
136
|
+
|
|
137
|
+
const a = Email.create('user@example.com')
|
|
138
|
+
const b = Email.create('user@example.com')
|
|
139
|
+
|
|
140
|
+
a.equals(b) // ✅ true — compared by value, not reference
|
|
130
141
|
```
|
|
131
142
|
|
|
132
|
-
|
|
143
|
+
---
|
|
133
144
|
|
|
134
|
-
|
|
145
|
+
### `WatchedList` — persist only what changed
|
|
135
146
|
|
|
136
147
|
```ts
|
|
137
|
-
import { WatchedList } from
|
|
148
|
+
import { WatchedList } from 'archstone/core'
|
|
138
149
|
|
|
139
150
|
class TagList extends WatchedList<Tag> {
|
|
140
151
|
compareItems(a: Tag, b: Tag) { return a.id.equals(b.id) }
|
|
@@ -144,52 +155,100 @@ const tags = new TagList([existingTag])
|
|
|
144
155
|
tags.add(newTag)
|
|
145
156
|
tags.remove(existingTag)
|
|
146
157
|
|
|
147
|
-
|
|
148
|
-
tags.
|
|
158
|
+
// Send only the diff to your repository — not the whole list
|
|
159
|
+
tags.getNewItems() // → [newTag]
|
|
160
|
+
tags.getRemovedItems() // → [existingTag]
|
|
149
161
|
```
|
|
150
162
|
|
|
151
|
-
|
|
163
|
+
---
|
|
152
164
|
|
|
153
|
-
|
|
165
|
+
### Domain Events — decouple side effects
|
|
154
166
|
|
|
155
167
|
```ts
|
|
156
|
-
import { DomainEvents } from
|
|
168
|
+
import { DomainEvents } from 'archstone/domain/enterprise'
|
|
157
169
|
|
|
158
|
-
//
|
|
170
|
+
// Register handlers anywhere in your infrastructure layer
|
|
159
171
|
DomainEvents.register(
|
|
160
172
|
(event) => sendWelcomeEmail(event as UserCreatedEvent),
|
|
161
173
|
UserCreatedEvent.name,
|
|
162
174
|
)
|
|
163
175
|
|
|
164
|
-
//
|
|
176
|
+
// Dispatch after persistence — events stay inside the aggregate until then
|
|
165
177
|
await userRepository.create(user)
|
|
166
178
|
DomainEvents.dispatchEventsForAggregate(user.id)
|
|
167
179
|
```
|
|
168
180
|
|
|
169
|
-
|
|
181
|
+
---
|
|
170
182
|
|
|
171
|
-
|
|
183
|
+
### Repository Contracts — keep infrastructure out of your domain
|
|
172
184
|
|
|
173
185
|
```ts
|
|
174
|
-
import { Repository, Creatable } from
|
|
186
|
+
import { Repository, Creatable } from 'archstone/domain/application'
|
|
175
187
|
|
|
176
|
-
// application
|
|
188
|
+
// Define your contract in the application layer
|
|
177
189
|
export interface UserRepository extends Repository<User> {
|
|
178
190
|
findByEmail(email: string): Promise<User | null>
|
|
179
191
|
}
|
|
180
192
|
|
|
181
|
-
//
|
|
193
|
+
// Compose only what you need
|
|
182
194
|
export interface AuditRepository extends Creatable<AuditLog> {}
|
|
195
|
+
|
|
196
|
+
// Implement anywhere in infrastructure — domain stays clean
|
|
183
197
|
```
|
|
184
198
|
|
|
185
|
-
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
## Package Exports
|
|
202
|
+
|
|
203
|
+
| Import | Contents |
|
|
204
|
+
|---|---|
|
|
205
|
+
| `archstone/core` | `Either`, `ValueObject`, `UniqueEntityId`, `WatchedList`, `Optional` |
|
|
206
|
+
| `archstone/domain` | All domain exports |
|
|
207
|
+
| `archstone/domain/enterprise` | `Entity`, `AggregateRoot`, `DomainEvent`, `DomainEvents`, `EventHandler` |
|
|
208
|
+
| `archstone/domain/application` | `UseCase`, `UseCaseError`, repository contracts |
|
|
209
|
+
|
|
210
|
+
---
|
|
211
|
+
|
|
212
|
+
## Architecture
|
|
213
|
+
|
|
214
|
+
```
|
|
215
|
+
src/
|
|
216
|
+
├── core/ # Zero domain knowledge — pure language utilities
|
|
217
|
+
│ ├── either.ts # Left / Right functional result type
|
|
218
|
+
│ ├── value-object.ts # Value equality base class
|
|
219
|
+
│ ├── unique-entity-id.ts # UUID v7 identity wrapper
|
|
220
|
+
│ ├── watched-list.ts # Change-tracked collection
|
|
221
|
+
│ └── types/
|
|
222
|
+
│ └── optional.ts # Optional<T, K> helper type
|
|
223
|
+
│
|
|
224
|
+
└── domain/
|
|
225
|
+
├── enterprise/ # Pure domain model — zero framework dependencies
|
|
226
|
+
│ ├── entities/
|
|
227
|
+
│ │ ├── entity.ts
|
|
228
|
+
│ │ └── aggregate-root.ts
|
|
229
|
+
│ └── events/
|
|
230
|
+
│ ├── domain-event.ts
|
|
231
|
+
│ ├── domain-events.ts
|
|
232
|
+
│ └── event-handler.ts
|
|
233
|
+
│
|
|
234
|
+
└── application/ # Orchestration — use cases & repository contracts
|
|
235
|
+
├── use-cases/
|
|
236
|
+
│ ├── use-case.ts
|
|
237
|
+
│ └── use-case.error.ts
|
|
238
|
+
└── repositories/
|
|
239
|
+
├── repository.ts
|
|
240
|
+
├── findable.ts
|
|
241
|
+
├── creatable.ts
|
|
242
|
+
├── saveable.ts
|
|
243
|
+
└── deletable.ts
|
|
244
|
+
```
|
|
186
245
|
|
|
187
|
-
|
|
246
|
+
---
|
|
188
247
|
|
|
189
|
-
|
|
248
|
+
<div align="center">
|
|
190
249
|
|
|
191
|
-
|
|
250
|
+
**Built with care for the TypeScript community.**
|
|
192
251
|
|
|
193
|
-
|
|
252
|
+
[Contributing](./CONTRIBUTING.md) · [Code of Conduct](./CODE_OF_CONDUCT.md) · [MIT License](./LICENSE)
|
|
194
253
|
|
|
195
|
-
|
|
254
|
+
</div>
|
package/package.json
CHANGED