archstone 1.0.3 → 1.1.0
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 +118 -48
- package/package.json +5 -5
- package/skills/archstone/SKILL.md +63 -0
- package/skills/archstone/references/conventions.md +23 -0
- package/skills/archstone/references/domain-events.md +78 -0
- package/skills/archstone/references/entity.md +75 -0
- package/skills/archstone/references/imports.md +13 -0
- package/skills/archstone/references/layers.md +34 -0
- package/skills/archstone/references/repository.md +65 -0
- package/skills/archstone/references/testing.md +50 -0
- package/skills/archstone/references/use-case.md +59 -0
- package/skills/archstone/references/value-object.md +57 -0
package/README.md
CHANGED
|
@@ -1,20 +1,53 @@
|
|
|
1
1
|
<div align="center">
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
<br />
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
Stop re-implementing DDD boilerplate. Focus on your domain.
|
|
5
|
+
# Archstone
|
|
7
6
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
7
|
+
### The TypeScript foundation for serious backend services.
|
|
8
|
+
|
|
9
|
+
Build on Domain-Driven Design and Clean Architecture — without writing the same boilerplate on every project.
|
|
10
|
+
|
|
11
|
+
<br />
|
|
12
|
+
|
|
13
|
+
[](https://www.npmjs.com/package/archstone)
|
|
14
|
+
[](./LICENSE)
|
|
15
|
+
[](https://www.typescriptlang.org/)
|
|
16
|
+
[](https://bun.sh)
|
|
17
|
+
|
|
18
|
+
<br />
|
|
12
19
|
|
|
13
20
|
</div>
|
|
14
21
|
|
|
15
22
|
---
|
|
16
23
|
|
|
17
|
-
|
|
24
|
+
## Why archstone?
|
|
25
|
+
|
|
26
|
+
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.
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
// ❌ Before — scattered, inconsistent, no error contract
|
|
30
|
+
class User { id: string }
|
|
31
|
+
function createUser() { throw new Error('not found') }
|
|
32
|
+
|
|
33
|
+
// ✅ After — structured, predictable, type-safe
|
|
34
|
+
class User extends AggregateRoot<UserProps> { ... }
|
|
35
|
+
async function createUser(): Promise<Either<NotFoundError, User>> { ... }
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## Features
|
|
41
|
+
|
|
42
|
+
- **`Either`** — functional error handling; use cases never throw
|
|
43
|
+
- **`Entity` / `AggregateRoot`** — identity-based domain objects with built-in event support
|
|
44
|
+
- **`ValueObject`** — equality by value, not reference
|
|
45
|
+
- **`UniqueEntityId`** — UUID v7 identity, consistent across your entire domain
|
|
46
|
+
- **`WatchedList`** — track additions and removals in collections without overwriting persistence
|
|
47
|
+
- **`UseCase`** — typed contract for application logic
|
|
48
|
+
- **Repository contracts** — define your interface in the domain; implement in infrastructure
|
|
49
|
+
|
|
50
|
+
---
|
|
18
51
|
|
|
19
52
|
## Install
|
|
20
53
|
|
|
@@ -24,43 +57,39 @@ bun add archstone
|
|
|
24
57
|
npm install archstone
|
|
25
58
|
```
|
|
26
59
|
|
|
27
|
-
|
|
60
|
+
> Zero runtime dependencies. Pure TypeScript.
|
|
28
61
|
|
|
29
|
-
|
|
30
|
-
|---|---|
|
|
31
|
-
| `Entity` / `AggregateRoot` | Identity-based domain objects; aggregates raise domain events |
|
|
32
|
-
| `ValueObject` | Equality by value, not reference |
|
|
33
|
-
| `UniqueEntityId` | UUID v7 identity wrapper |
|
|
34
|
-
| `WatchedList` | Tracks additions and removals in a collection |
|
|
35
|
-
| `Either` | Functional error handling — no throwing in use cases |
|
|
36
|
-
| `UseCase` | Contract for application use cases returning `Either` |
|
|
37
|
-
| `Repository` | CRUD interface contracts — implementations live in infra |
|
|
62
|
+
---
|
|
38
63
|
|
|
39
64
|
## Usage
|
|
40
65
|
|
|
41
|
-
### Either —
|
|
66
|
+
### `Either` — stop throwing, start returning
|
|
42
67
|
|
|
43
68
|
```ts
|
|
44
69
|
import { Either, left, right } from 'archstone/core'
|
|
45
70
|
|
|
46
|
-
type
|
|
71
|
+
type FindUserResult = Either<UserNotFoundError, User>
|
|
47
72
|
|
|
48
|
-
async function findUser(id: string): Promise<
|
|
73
|
+
async function findUser(id: string): Promise<FindUserResult> {
|
|
49
74
|
const user = await repo.findById(id)
|
|
75
|
+
|
|
50
76
|
if (!user) return left(new UserNotFoundError(id))
|
|
51
77
|
return right(user)
|
|
52
78
|
}
|
|
53
79
|
|
|
80
|
+
// The caller always handles both cases — no surprises
|
|
54
81
|
const result = await findUser('123')
|
|
55
82
|
|
|
56
83
|
if (result.isLeft()) {
|
|
57
84
|
console.error(result.value) // UserNotFoundError
|
|
58
85
|
} else {
|
|
59
|
-
console.log(result.value) // User
|
|
86
|
+
console.log(result.value) // User ✓
|
|
60
87
|
}
|
|
61
88
|
```
|
|
62
89
|
|
|
63
|
-
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
### `Entity` & `AggregateRoot` — model your domain
|
|
64
93
|
|
|
65
94
|
```ts
|
|
66
95
|
import { AggregateRoot } from 'archstone/domain/enterprise'
|
|
@@ -81,13 +110,17 @@ class Order extends AggregateRoot<OrderProps> {
|
|
|
81
110
|
...props,
|
|
82
111
|
createdAt: props.createdAt ?? new Date(),
|
|
83
112
|
})
|
|
113
|
+
|
|
114
|
+
// Raise domain events from inside the aggregate
|
|
84
115
|
order.addDomainEvent(new OrderCreatedEvent(order))
|
|
85
116
|
return order
|
|
86
117
|
}
|
|
87
118
|
}
|
|
88
119
|
```
|
|
89
120
|
|
|
90
|
-
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
### `ValueObject` — equality that makes sense
|
|
91
124
|
|
|
92
125
|
```ts
|
|
93
126
|
import { ValueObject } from 'archstone/core'
|
|
@@ -105,10 +138,13 @@ class Email extends ValueObject<EmailProps> {
|
|
|
105
138
|
|
|
106
139
|
const a = Email.create('user@example.com')
|
|
107
140
|
const b = Email.create('user@example.com')
|
|
108
|
-
|
|
141
|
+
|
|
142
|
+
a.equals(b) // ✅ true — compared by value, not reference
|
|
109
143
|
```
|
|
110
144
|
|
|
111
|
-
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
### `WatchedList` — persist only what changed
|
|
112
148
|
|
|
113
149
|
```ts
|
|
114
150
|
import { WatchedList } from 'archstone/core'
|
|
@@ -121,62 +157,74 @@ const tags = new TagList([existingTag])
|
|
|
121
157
|
tags.add(newTag)
|
|
122
158
|
tags.remove(existingTag)
|
|
123
159
|
|
|
124
|
-
|
|
125
|
-
tags.
|
|
160
|
+
// Send only the diff to your repository — not the whole list
|
|
161
|
+
tags.getNewItems() // → [newTag]
|
|
162
|
+
tags.getRemovedItems() // → [existingTag]
|
|
126
163
|
```
|
|
127
164
|
|
|
165
|
+
---
|
|
166
|
+
|
|
128
167
|
### Domain Events — decouple side effects
|
|
129
168
|
|
|
130
169
|
```ts
|
|
131
170
|
import { DomainEvents } from 'archstone/domain/enterprise'
|
|
132
171
|
|
|
133
|
-
// Register
|
|
172
|
+
// Register handlers anywhere in your infrastructure layer
|
|
134
173
|
DomainEvents.register(
|
|
135
174
|
(event) => sendWelcomeEmail(event as UserCreatedEvent),
|
|
136
175
|
UserCreatedEvent.name,
|
|
137
176
|
)
|
|
138
177
|
|
|
139
|
-
// Dispatch after
|
|
178
|
+
// Dispatch after persistence — events stay inside the aggregate until then
|
|
140
179
|
await userRepository.create(user)
|
|
141
180
|
DomainEvents.dispatchEventsForAggregate(user.id)
|
|
142
181
|
```
|
|
143
182
|
|
|
144
|
-
|
|
183
|
+
---
|
|
184
|
+
|
|
185
|
+
### Repository Contracts — keep infrastructure out of your domain
|
|
145
186
|
|
|
146
187
|
```ts
|
|
147
188
|
import { Repository, Creatable } from 'archstone/domain/application'
|
|
148
189
|
|
|
149
|
-
//
|
|
190
|
+
// Define your contract in the application layer
|
|
150
191
|
export interface UserRepository extends Repository<User> {
|
|
151
192
|
findByEmail(email: string): Promise<User | null>
|
|
152
193
|
}
|
|
153
194
|
|
|
154
|
-
//
|
|
195
|
+
// Compose only what you need
|
|
155
196
|
export interface AuditRepository extends Creatable<AuditLog> {}
|
|
197
|
+
|
|
198
|
+
// Implement anywhere in infrastructure — domain stays clean
|
|
156
199
|
```
|
|
157
200
|
|
|
201
|
+
---
|
|
202
|
+
|
|
158
203
|
## Package Exports
|
|
159
204
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
archstone/
|
|
163
|
-
archstone/domain
|
|
164
|
-
archstone/domain/
|
|
165
|
-
|
|
205
|
+
| Import | Contents |
|
|
206
|
+
|---|---|
|
|
207
|
+
| `archstone/core` | `Either`, `ValueObject`, `UniqueEntityId`, `WatchedList`, `Optional` |
|
|
208
|
+
| `archstone/domain` | All domain exports |
|
|
209
|
+
| `archstone/domain/enterprise` | `Entity`, `AggregateRoot`, `DomainEvent`, `DomainEvents`, `EventHandler` |
|
|
210
|
+
| `archstone/domain/application` | `UseCase`, `UseCaseError`, repository contracts |
|
|
166
211
|
|
|
167
|
-
|
|
212
|
+
---
|
|
213
|
+
|
|
214
|
+
## Architecture
|
|
168
215
|
|
|
169
216
|
```
|
|
170
217
|
src/
|
|
171
|
-
├── core/
|
|
172
|
-
│ ├── either.ts
|
|
173
|
-
│ ├── value-object.ts
|
|
174
|
-
│ ├── unique-entity-id.ts
|
|
175
|
-
│ ├── watched-list.ts
|
|
176
|
-
│ └── types/
|
|
218
|
+
├── core/ # Zero domain knowledge — pure language utilities
|
|
219
|
+
│ ├── either.ts # Left / Right functional result type
|
|
220
|
+
│ ├── value-object.ts # Value equality base class
|
|
221
|
+
│ ├── unique-entity-id.ts # UUID v7 identity wrapper
|
|
222
|
+
│ ├── watched-list.ts # Change-tracked collection
|
|
223
|
+
│ └── types/
|
|
224
|
+
│ └── optional.ts # Optional<T, K> helper type
|
|
177
225
|
│
|
|
178
226
|
└── domain/
|
|
179
|
-
├── enterprise/
|
|
227
|
+
├── enterprise/ # Pure domain model — zero framework dependencies
|
|
180
228
|
│ ├── entities/
|
|
181
229
|
│ │ ├── entity.ts
|
|
182
230
|
│ │ └── aggregate-root.ts
|
|
@@ -185,7 +233,7 @@ src/
|
|
|
185
233
|
│ ├── domain-events.ts
|
|
186
234
|
│ └── event-handler.ts
|
|
187
235
|
│
|
|
188
|
-
└── application/
|
|
236
|
+
└── application/ # Orchestration — use cases & repository contracts
|
|
189
237
|
├── use-cases/
|
|
190
238
|
│ ├── use-case.ts
|
|
191
239
|
│ └── use-case.error.ts
|
|
@@ -199,8 +247,30 @@ src/
|
|
|
199
247
|
|
|
200
248
|
---
|
|
201
249
|
|
|
250
|
+
## Agent Skills
|
|
251
|
+
|
|
252
|
+
Archstone ships with a built-in skill for AI coding agents — giving them full knowledge of DDD conventions, layer boundaries, and usage patterns so you don't have to explain them in every project.
|
|
253
|
+
|
|
254
|
+
**Install with Claude Code:**
|
|
255
|
+
|
|
256
|
+
```bash
|
|
257
|
+
bun x skills add joao-coimbra/archstone
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
**Or copy from the installed package:**
|
|
261
|
+
|
|
262
|
+
```bash
|
|
263
|
+
cp -r node_modules/archstone/skills/archstone .claude/skills/
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
The skill covers: entities, aggregates, value objects, use cases, repository contracts, domain events, Either error handling, and testing patterns — all with examples and common-mistake callouts.
|
|
267
|
+
|
|
268
|
+
---
|
|
269
|
+
|
|
202
270
|
<div align="center">
|
|
203
271
|
|
|
272
|
+
**Built with care for the TypeScript community.**
|
|
273
|
+
|
|
204
274
|
[Contributing](./CONTRIBUTING.md) · [Code of Conduct](./CODE_OF_CONDUCT.md) · [MIT License](./LICENSE)
|
|
205
275
|
|
|
206
276
|
</div>
|
package/package.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
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
|
|
4
|
+
"version": "1.1.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": false,
|
|
7
7
|
"files": [
|
|
8
|
-
"dist"
|
|
8
|
+
"dist",
|
|
9
|
+
"skills"
|
|
9
10
|
],
|
|
10
11
|
"module": "./dist/index.js",
|
|
11
12
|
"types": "./dist/index.d.ts",
|
|
@@ -73,11 +74,10 @@
|
|
|
73
74
|
"build": "bunup",
|
|
74
75
|
"dev": "bunup --watch",
|
|
75
76
|
"test": "bun test",
|
|
76
|
-
"lint": "biome check src/",
|
|
77
|
-
"prepublishOnly": "bun run build",
|
|
78
77
|
"check": "ultracite check",
|
|
79
78
|
"fix": "ultracite fix",
|
|
80
|
-
"
|
|
79
|
+
"prepublishOnly": "bun run build",
|
|
80
|
+
"prepare": "bunx husky"
|
|
81
81
|
},
|
|
82
82
|
"devDependencies": {
|
|
83
83
|
"@biomejs/biome": "2.4.5",
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: archstone
|
|
3
|
+
description: 'Apply Archstone DDD and Clean Architecture conventions. Use when developers: (1) Create domain entities, aggregates, or value objects, (2) Write application use cases, (3) Define repository contracts, (4) Work with domain events or event handlers, (5) Ask about Either, UniqueEntityId, WatchedList, or UseCaseError. Triggers on: "entity", "aggregate", "value object", "use case", "repository", "domain event", "Either", "UniqueEntityId", "archstone".'
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
## Critical: Follow Archstone Conventions
|
|
7
|
+
|
|
8
|
+
Everything you know about DDD patterns may differ from how Archstone implements them. Always follow the rules below — do not apply generic DDD patterns that contradict them.
|
|
9
|
+
|
|
10
|
+
When working with Archstone:
|
|
11
|
+
|
|
12
|
+
1. Check `node_modules/archstone/` for the installed version
|
|
13
|
+
2. All entities must extend `Entity<Props>` or `AggregateRoot<Props>` — never use plain classes
|
|
14
|
+
3. All value objects must extend `ValueObject<Props>`
|
|
15
|
+
4. Use cases must implement `UseCase<Input, Output>` and **never throw** — always return `Either`
|
|
16
|
+
5. Repository contracts are interfaces only — implementations belong in infrastructure
|
|
17
|
+
6. Domain events are raised inside aggregates and dispatched after persistence — never before
|
|
18
|
+
7. Use `UniqueEntityId` for all entity identifiers — never a plain `string`
|
|
19
|
+
8. The left side of `Either` must `implement UseCaseError` — not `extend Error`
|
|
20
|
+
|
|
21
|
+
If you are unsure about a pattern, check [Conventions Reference](references/conventions.md) before writing code.
|
|
22
|
+
|
|
23
|
+
## Layer Boundaries
|
|
24
|
+
|
|
25
|
+
Always respect the layer rules. Never place infrastructure code in `domain/`, and never import concrete implementations into use cases.
|
|
26
|
+
|
|
27
|
+
See [Layer Rules](references/layers.md) for details.
|
|
28
|
+
|
|
29
|
+
## Entities & Aggregates
|
|
30
|
+
|
|
31
|
+
Use a static `create()` factory. Pass `id` as the second constructor argument. Use `Optional<T, K>` for auto-generated fields. See [Entity Patterns](references/entity.md).
|
|
32
|
+
|
|
33
|
+
## Value Objects
|
|
34
|
+
|
|
35
|
+
Use a static `create()` factory with validation. Value object `create()` may throw — wrap calls inside use cases with `try/catch` and return `left()`. See [Value Object Patterns](references/value-object.md).
|
|
36
|
+
|
|
37
|
+
## Use Cases
|
|
38
|
+
|
|
39
|
+
Implement `UseCase<Input, Output>`. Return `left()` for errors, `right()` for success. Error classes must `implement UseCaseError`. See [Use Case Patterns](references/use-case.md).
|
|
40
|
+
|
|
41
|
+
## Repository Contracts
|
|
42
|
+
|
|
43
|
+
Define as interfaces only. Use `Repository<T>` or compose `Findable`, `Creatable`, `Saveable`, `Deletable`. Note: `findById` takes `string` — pass `entity.id.toValue()`. See [Repository Patterns](references/repository.md).
|
|
44
|
+
|
|
45
|
+
## Domain Events
|
|
46
|
+
|
|
47
|
+
Raise inside the aggregate via `addDomainEvent()`. Dispatch after persistence via `DomainEvents.dispatchEventsForAggregate(aggregate.id)`. Define handlers as classes implementing `EventHandler`. See [Domain Event Patterns](references/domain-events.md).
|
|
48
|
+
|
|
49
|
+
## Testing
|
|
50
|
+
|
|
51
|
+
Use `bun:test`. Co-locate test files as `*.spec.ts`. Use in-memory repository implementations. See [Testing Patterns](references/testing.md).
|
|
52
|
+
|
|
53
|
+
## References
|
|
54
|
+
|
|
55
|
+
- [Conventions](references/conventions.md) — quick rules summary
|
|
56
|
+
- [Imports](references/imports.md) — import paths for all exports
|
|
57
|
+
- [Layers](references/layers.md) — layer boundary rules
|
|
58
|
+
- [Entity Patterns](references/entity.md) — Entity and AggregateRoot examples
|
|
59
|
+
- [Value Object Patterns](references/value-object.md) — ValueObject examples
|
|
60
|
+
- [Use Case Patterns](references/use-case.md) — UseCase and Either examples
|
|
61
|
+
- [Repository Patterns](references/repository.md) — repository interface and in-memory examples
|
|
62
|
+
- [Domain Event Patterns](references/domain-events.md) — EventHandler and dispatch examples
|
|
63
|
+
- [Testing Patterns](references/testing.md) — bun:test and in-memory repo examples
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Archstone Conventions — Quick Reference
|
|
2
|
+
|
|
3
|
+
## Always
|
|
4
|
+
|
|
5
|
+
- Extend `Entity<Props>` or `AggregateRoot<Props>` for domain entities
|
|
6
|
+
- Extend `ValueObject<Props>` for value objects
|
|
7
|
+
- Use `UniqueEntityId` for all entity identities — never `string`
|
|
8
|
+
- Use static `create()` factory — never instantiate directly
|
|
9
|
+
- Return `Either<UseCaseError, Value>` from use cases — never throw
|
|
10
|
+
- Define repositories as interfaces only
|
|
11
|
+
- Raise domain events inside aggregates via `addDomainEvent()`
|
|
12
|
+
- Dispatch domain events **after** persistence
|
|
13
|
+
|
|
14
|
+
## Never
|
|
15
|
+
|
|
16
|
+
- Never throw inside a use case — use `left()`
|
|
17
|
+
- Never use `extends Error` for use case errors — use `implements UseCaseError`
|
|
18
|
+
- Never place repository implementations in `domain/`
|
|
19
|
+
- Never import concrete classes into use cases
|
|
20
|
+
- Never dispatch domain events before persistence
|
|
21
|
+
- Never call `clearEvents()` manually — `dispatchEventsForAggregate` handles it
|
|
22
|
+
- Never pass `UniqueEntityId` to `findById()` — it takes `string`; use `.toValue()`
|
|
23
|
+
- Never use `===` to compare value objects — use `.equals()`
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# Domain Event Patterns
|
|
2
|
+
|
|
3
|
+
## Rules
|
|
4
|
+
|
|
5
|
+
- Raise events inside the aggregate via `this.addDomainEvent()`
|
|
6
|
+
- Dispatch **after** successful persistence — never before
|
|
7
|
+
- Define handlers as classes implementing `EventHandler` with `setupSubscriptions(): void`
|
|
8
|
+
- Register handlers in the infrastructure composition root before the first request
|
|
9
|
+
- Dispatch via `DomainEvents.dispatchEventsForAggregate(aggregate.id)` — argument is `UniqueEntityId`
|
|
10
|
+
- `clearEvents()` is called internally by `dispatchEventsForAggregate` — do not call manually
|
|
11
|
+
- Test isolation: call `DomainEvents.clearHandlers()` and `DomainEvents.clearMarkedAggregates()` in `beforeEach`
|
|
12
|
+
|
|
13
|
+
## Raising Events (inside aggregate)
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
class User extends AggregateRoot<UserProps> {
|
|
17
|
+
static create(props: Optional<UserProps, 'createdAt'>, id?: UniqueEntityId): User {
|
|
18
|
+
const user = new User(
|
|
19
|
+
{ ...props, createdAt: props.createdAt ?? new Date() },
|
|
20
|
+
id ?? new UniqueEntityId(),
|
|
21
|
+
)
|
|
22
|
+
user.addDomainEvent(new UserCreatedEvent(user))
|
|
23
|
+
return user
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Defining a Handler
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
import type { EventHandler } from 'archstone/domain/enterprise'
|
|
32
|
+
import { DomainEvents } from 'archstone/domain/enterprise'
|
|
33
|
+
|
|
34
|
+
class OnUserCreated implements EventHandler {
|
|
35
|
+
constructor(private readonly mailer: Mailer) {}
|
|
36
|
+
|
|
37
|
+
setupSubscriptions(): void {
|
|
38
|
+
DomainEvents.register(
|
|
39
|
+
(event) => this.handle(event as UserCreatedEvent),
|
|
40
|
+
UserCreatedEvent.name,
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
private async handle(event: UserCreatedEvent): Promise<void> {
|
|
45
|
+
await this.mailer.send(event.user.email.value)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Dispatching (in repository, after persistence)
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
async create(user: User): Promise<void> {
|
|
54
|
+
await this.db.insert(user)
|
|
55
|
+
DomainEvents.dispatchEventsForAggregate(user.id) // UniqueEntityId, not string
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Composition Root Registration
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
// Called once at app startup — in infrastructure, never in domain
|
|
63
|
+
new OnUserCreated(mailer).setupSubscriptions()
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Common Mistakes
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
// ❌ dispatching before persisting
|
|
70
|
+
DomainEvents.dispatchEventsForAggregate(user.id)
|
|
71
|
+
await this.db.insert(user) // wrong order
|
|
72
|
+
|
|
73
|
+
// ❌ passing string
|
|
74
|
+
DomainEvents.dispatchEventsForAggregate(user.id.toValue()) // use UniqueEntityId
|
|
75
|
+
|
|
76
|
+
// ❌ calling clearEvents() manually
|
|
77
|
+
user.clearEvents() // dispatchEventsForAggregate handles this
|
|
78
|
+
```
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# Entity & AggregateRoot Patterns
|
|
2
|
+
|
|
3
|
+
## Rules
|
|
4
|
+
|
|
5
|
+
- Extend `Entity<Props>` for identity-only entities
|
|
6
|
+
- Extend `AggregateRoot<Props>` for entities that raise domain events
|
|
7
|
+
- Always use a static `create()` factory — constructor stays `protected`
|
|
8
|
+
- Include `id` as an optional field in `Props` and pass it as the second constructor argument
|
|
9
|
+
- Use `Optional<T, K>` for auto-generated fields (e.g. `'id' | 'createdAt'`)
|
|
10
|
+
- Use `UniqueEntityId` — never a plain `string`
|
|
11
|
+
|
|
12
|
+
## Entity Example
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
import { Entity } from 'archstone/domain/enterprise'
|
|
16
|
+
import { UniqueEntityId, type Optional } from 'archstone/core'
|
|
17
|
+
|
|
18
|
+
interface ProductProps {
|
|
19
|
+
name: string
|
|
20
|
+
price: number
|
|
21
|
+
createdAt: Date
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
class Product extends Entity<ProductProps> {
|
|
25
|
+
get name() { return this.props.name }
|
|
26
|
+
get price() { return this.props.price }
|
|
27
|
+
|
|
28
|
+
static create(props: Optional<ProductProps, 'createdAt'>, id?: UniqueEntityId): Product {
|
|
29
|
+
return new Product(
|
|
30
|
+
{ ...props, createdAt: props.createdAt ?? new Date() },
|
|
31
|
+
id ?? new UniqueEntityId(),
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## AggregateRoot Example
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
import { AggregateRoot } from 'archstone/domain/enterprise'
|
|
41
|
+
import { UniqueEntityId, type Optional } from 'archstone/core'
|
|
42
|
+
|
|
43
|
+
interface OrderProps {
|
|
44
|
+
customerId: UniqueEntityId
|
|
45
|
+
total: number
|
|
46
|
+
createdAt: Date
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
class Order extends AggregateRoot<OrderProps> {
|
|
50
|
+
get customerId() { return this.props.customerId }
|
|
51
|
+
get total() { return this.props.total }
|
|
52
|
+
|
|
53
|
+
static create(props: Optional<OrderProps, 'createdAt'>, id?: UniqueEntityId): Order {
|
|
54
|
+
const order = new Order(
|
|
55
|
+
{ ...props, createdAt: props.createdAt ?? new Date() },
|
|
56
|
+
id ?? new UniqueEntityId(),
|
|
57
|
+
)
|
|
58
|
+
order.addDomainEvent(new OrderCreatedEvent(order))
|
|
59
|
+
return order
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Common Mistakes
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
// ❌ plain class
|
|
68
|
+
class Order { id: string }
|
|
69
|
+
|
|
70
|
+
// ❌ id not passed as second constructor arg
|
|
71
|
+
return new Order(props) // id goes as second arg: new Order(props, id)
|
|
72
|
+
|
|
73
|
+
// ❌ string id
|
|
74
|
+
interface OrderProps { id: string } // use UniqueEntityId
|
|
75
|
+
```
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Archstone Import Reference
|
|
2
|
+
|
|
3
|
+
| What | Import path |
|
|
4
|
+
|---|---|
|
|
5
|
+
| `Either`, `left`, `right` | `archstone/core` |
|
|
6
|
+
| `ValueObject` | `archstone/core` |
|
|
7
|
+
| `UniqueEntityId` | `archstone/core` |
|
|
8
|
+
| `WatchedList` | `archstone/core` |
|
|
9
|
+
| `Optional` | `archstone/core` |
|
|
10
|
+
| `Entity`, `AggregateRoot` | `archstone/domain/enterprise` |
|
|
11
|
+
| `DomainEvents`, `EventHandler` | `archstone/domain/enterprise` |
|
|
12
|
+
| `UseCase`, `UseCaseError` | `archstone/domain/application` |
|
|
13
|
+
| `Repository`, `Findable`, `Creatable`, `Saveable`, `Deletable` | `archstone/domain/application` |
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Layer Rules
|
|
2
|
+
|
|
3
|
+
## core/
|
|
4
|
+
|
|
5
|
+
Zero domain knowledge. Contains only pure language utilities.
|
|
6
|
+
|
|
7
|
+
Exports: `Either`, `left`, `right`, `ValueObject`, `UniqueEntityId`, `WatchedList`, `Optional`
|
|
8
|
+
|
|
9
|
+
## domain/enterprise/
|
|
10
|
+
|
|
11
|
+
Pure domain model. No framework or infrastructure dependencies.
|
|
12
|
+
|
|
13
|
+
Contains: entities, aggregates, value objects, domain events, event handlers.
|
|
14
|
+
|
|
15
|
+
## domain/application/
|
|
16
|
+
|
|
17
|
+
Orchestration layer. Use cases and repository contracts only.
|
|
18
|
+
|
|
19
|
+
Contains: use case interfaces, use case error contracts, repository interfaces.
|
|
20
|
+
|
|
21
|
+
## Infrastructure (outside domain/)
|
|
22
|
+
|
|
23
|
+
Database adapters, HTTP handlers, ORMs, email services — all live here.
|
|
24
|
+
|
|
25
|
+
Repository implementations belong here, not in `domain/`.
|
|
26
|
+
|
|
27
|
+
## Rule
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
core/ ← no deps
|
|
31
|
+
domain/enterprise/ ← imports from core/ only
|
|
32
|
+
domain/application/ ← imports from core/ and domain/enterprise/
|
|
33
|
+
infrastructure/ ← imports from all layers, implements domain/application/ contracts
|
|
34
|
+
```
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Repository Patterns
|
|
2
|
+
|
|
3
|
+
## Rules
|
|
4
|
+
|
|
5
|
+
- Contracts are **interfaces only** — never place implementations in `domain/`
|
|
6
|
+
- Extend `Repository<T>` for full CRUD, or compose granular interfaces:
|
|
7
|
+
- `Findable<T>` — `findById(id: string)` — takes `string`, not `UniqueEntityId`
|
|
8
|
+
- `Creatable<T>` — `create(entity: T)`
|
|
9
|
+
- `Saveable<T>` — `save(entity: T)`
|
|
10
|
+
- `Deletable<T>` — `delete(entity: T)` — takes full entity, not an id
|
|
11
|
+
- Import via barrel: `archstone/domain/application`
|
|
12
|
+
- Implementations belong in infrastructure
|
|
13
|
+
- Inject as the **interface type** in use cases
|
|
14
|
+
|
|
15
|
+
## Contract Example
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import type { Repository, Creatable } from 'archstone/domain/application'
|
|
19
|
+
|
|
20
|
+
// Full CRUD + custom method
|
|
21
|
+
export interface UserRepository extends Repository<User> {
|
|
22
|
+
findByEmail(email: string): Promise<User | null>
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Compose only what you need
|
|
26
|
+
export interface AuditRepository extends Creatable<AuditLog> {}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## In-Memory Implementation (for tests)
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
export class InMemoryUserRepository implements UserRepository {
|
|
33
|
+
public items: User[] = []
|
|
34
|
+
|
|
35
|
+
async findById(id: string) {
|
|
36
|
+
return this.items.find(u => u.id.toValue() === id) ?? null
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async findByEmail(email: string) {
|
|
40
|
+
return this.items.find(u => u.email.value === email) ?? null
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async create(user: User) { this.items.push(user) }
|
|
44
|
+
|
|
45
|
+
async save(user: User) {
|
|
46
|
+
const i = this.items.findIndex(u => u.id.equals(user.id))
|
|
47
|
+
if (i >= 0) this.items[i] = user
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async delete(user: User) {
|
|
51
|
+
this.items = this.items.filter(u => !u.id.equals(user.id))
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Common Mistakes
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
// ❌ concrete class in use case
|
|
60
|
+
constructor(private repo: PrismaUserRepository) {}
|
|
61
|
+
|
|
62
|
+
// ❌ UniqueEntityId to findById
|
|
63
|
+
await repo.findById(user.id) // wrong
|
|
64
|
+
await repo.findById(user.id.toValue()) // correct
|
|
65
|
+
```
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Testing Patterns
|
|
2
|
+
|
|
3
|
+
## Rules
|
|
4
|
+
|
|
5
|
+
- Use `bun:test` — import `test`, `expect`, `beforeEach` from `bun:test`
|
|
6
|
+
- Test files: `*.spec.ts` co-located with the source file they test
|
|
7
|
+
- Use in-memory repository implementations for use case tests — never mock databases
|
|
8
|
+
|
|
9
|
+
## Use Case Test Example
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import { test, expect, beforeEach } from 'bun:test'
|
|
13
|
+
import { GetUserUseCase } from './get-user'
|
|
14
|
+
import { InMemoryUserRepository } from '@/test/repositories/in-memory-user-repository'
|
|
15
|
+
|
|
16
|
+
let repo: InMemoryUserRepository
|
|
17
|
+
let useCase: GetUserUseCase
|
|
18
|
+
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
repo = new InMemoryUserRepository()
|
|
21
|
+
useCase = new GetUserUseCase(repo)
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
test('returns user when found', async () => {
|
|
25
|
+
const user = User.create({ name: 'João' })
|
|
26
|
+
await repo.create(user)
|
|
27
|
+
|
|
28
|
+
const result = await useCase.execute({ userId: user.id.toValue() })
|
|
29
|
+
|
|
30
|
+
expect(result.isRight()).toBe(true)
|
|
31
|
+
expect(result.value).toEqual(user)
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
test('returns error when user not found', async () => {
|
|
35
|
+
const result = await useCase.execute({ userId: 'non-existent' })
|
|
36
|
+
|
|
37
|
+
expect(result.isLeft()).toBe(true)
|
|
38
|
+
})
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Domain Event Isolation
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
import { DomainEvents } from 'archstone/domain/enterprise'
|
|
45
|
+
|
|
46
|
+
beforeEach(() => {
|
|
47
|
+
DomainEvents.clearHandlers()
|
|
48
|
+
DomainEvents.clearMarkedAggregates()
|
|
49
|
+
})
|
|
50
|
+
```
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# UseCase + Either Patterns
|
|
2
|
+
|
|
3
|
+
## Rules
|
|
4
|
+
|
|
5
|
+
- Implement `UseCase<Input, Output>`
|
|
6
|
+
- Output is always `Either<UseCaseError, Value>` — **never throw**
|
|
7
|
+
- Error classes must `implement UseCaseError` (requires `message: string`) — not `extends Error`
|
|
8
|
+
- Wrap value object construction in `try/catch` and return `left()`
|
|
9
|
+
- Inject repositories via constructor typed as the **interface**
|
|
10
|
+
|
|
11
|
+
## Example
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import type { UseCase, UseCaseError } from 'archstone/domain/application'
|
|
15
|
+
import { Either, left, right } from 'archstone/core'
|
|
16
|
+
|
|
17
|
+
class UserNotFoundError implements UseCaseError {
|
|
18
|
+
message = 'User not found.'
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
class InvalidEmailError implements UseCaseError {
|
|
22
|
+
message = 'Invalid email address.'
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
type Input = { userId: string; newEmail: string }
|
|
26
|
+
type Output = Either<UserNotFoundError | InvalidEmailError, User>
|
|
27
|
+
|
|
28
|
+
class UpdateUserEmailUseCase implements UseCase<Input, Output> {
|
|
29
|
+
constructor(private readonly repo: UserRepository) {} // interface, not concrete
|
|
30
|
+
|
|
31
|
+
async execute({ userId, newEmail }: Input): Promise<Output> {
|
|
32
|
+
const user = await this.repo.findById(userId)
|
|
33
|
+
if (!user) return left(new UserNotFoundError())
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
const email = Email.create(newEmail)
|
|
37
|
+
user.updateEmail(email)
|
|
38
|
+
} catch {
|
|
39
|
+
return left(new InvalidEmailError())
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
await this.repo.save(user)
|
|
43
|
+
return right(user)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Common Mistakes
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
// ❌ extends Error
|
|
52
|
+
class UserNotFoundError extends Error {} // implement UseCaseError instead
|
|
53
|
+
|
|
54
|
+
// ❌ throwing
|
|
55
|
+
if (!user) throw new Error('not found') // return left(new UserNotFoundError())
|
|
56
|
+
|
|
57
|
+
// ❌ concrete repo
|
|
58
|
+
constructor(private repo: PrismaUserRepository) {} // use the interface
|
|
59
|
+
```
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# ValueObject Patterns
|
|
2
|
+
|
|
3
|
+
## Rules
|
|
4
|
+
|
|
5
|
+
- Extend `ValueObject<Props>`
|
|
6
|
+
- Static `create()` factory with validation — may throw on invalid input (intentional)
|
|
7
|
+
- Never mutate `this.props` — return a new instance instead
|
|
8
|
+
- Compare with `.equals()` — never `===`
|
|
9
|
+
|
|
10
|
+
## Example
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
import { ValueObject } from 'archstone/core'
|
|
14
|
+
|
|
15
|
+
interface EmailProps { value: string }
|
|
16
|
+
|
|
17
|
+
class Email extends ValueObject<EmailProps> {
|
|
18
|
+
get value() { return this.props.value }
|
|
19
|
+
|
|
20
|
+
static create(raw: string): Email {
|
|
21
|
+
if (!raw.includes('@')) throw new Error('Invalid email address')
|
|
22
|
+
return new Email({ value: raw.toLowerCase().trim() })
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
withDomain(domain: string): Email {
|
|
26
|
+
const [local] = this.props.value.split('@')
|
|
27
|
+
return new Email({ value: `${local}@${domain}` })
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const a = Email.create('user@example.com')
|
|
32
|
+
const b = Email.create('user@example.com')
|
|
33
|
+
a.equals(b) // true
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Handling throws in use cases
|
|
37
|
+
|
|
38
|
+
Since `create()` may throw, wrap it inside use cases:
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
try {
|
|
42
|
+
const email = Email.create(rawEmail)
|
|
43
|
+
user.updateEmail(email)
|
|
44
|
+
} catch {
|
|
45
|
+
return left(new InvalidEmailError())
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Common Mistakes
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
// ❌ comparing with ===
|
|
53
|
+
if (user.email === other.email) {} // use .equals()
|
|
54
|
+
|
|
55
|
+
// ❌ mutating props
|
|
56
|
+
this.props.value = newValue // return new Email({ value: newValue })
|
|
57
|
+
```
|