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 CHANGED
@@ -1,20 +1,53 @@
1
1
  <div align="center">
2
2
 
3
- # archstone
3
+ <br />
4
4
 
5
- **TypeScript architecture foundation for backend services.**
6
- Stop re-implementing DDD boilerplate. Focus on your domain.
5
+ # Archstone
7
6
 
8
- [![npm](https://img.shields.io/npm/v/archstone?style=flat-square&color=black)](https://www.npmjs.com/package/archstone)
9
- [![license](https://img.shields.io/badge/license-MIT-black?style=flat-square)](./LICENSE)
10
- [![typescript](https://img.shields.io/badge/TypeScript-5-black?style=flat-square&logo=typescript&logoColor=white)](https://www.typescriptlang.org/)
11
- [![bun](https://img.shields.io/badge/Bun-runtime-black?style=flat-square&logo=bun)](https://bun.sh)
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
+ [![npm version](https://img.shields.io/npm/v/archstone?style=for-the-badge&logo=npm&color=CB3837&logoColor=white)](https://www.npmjs.com/package/archstone)
14
+ [![license](https://img.shields.io/badge/license-MIT-22C55E?style=for-the-badge)](./LICENSE)
15
+ [![typescript](https://img.shields.io/badge/TypeScript-5-3178C6?style=for-the-badge&logo=typescript&logoColor=white)](https://www.typescriptlang.org/)
16
+ [![bun](https://img.shields.io/badge/Bun-ready-F9F1E1?style=for-the-badge&logo=bun&logoColor=black)](https://bun.sh)
17
+
18
+ <br />
12
19
 
13
20
  </div>
14
21
 
15
22
  ---
16
23
 
17
- Archstone gives you the structural pieces of **Domain-Driven Design** and **Clean Architecture** — entities, value objects, aggregates, domain events, use cases, and repository contracts — so every project starts from a solid, consistent foundation.
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
- ## At a Glance
60
+ > Zero runtime dependencies. Pure TypeScript.
28
61
 
29
- | Building block | What it does |
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 — handle errors without throwing
66
+ ### `Either`stop throwing, start returning
42
67
 
43
68
  ```ts
44
69
  import { Either, left, right } from 'archstone/core'
45
70
 
46
- type Result = Either<UserNotFoundError, User>
71
+ type FindUserResult = Either<UserNotFoundError, User>
47
72
 
48
- async function findUser(id: string): Promise<Result> {
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
- ### Entity & AggregateRoot — model your domain
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
- ### ValueObject — equality by value
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
- a.equals(b) // true
141
+
142
+ a.equals(b) // ✅ true — compared by value, not reference
109
143
  ```
110
144
 
111
- ### WatchedList — track collection changes
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
- tags.getNewItems() // [newTag]
125
- tags.getRemovedItems() // [existingTag]
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 a handler
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 persisting the aggregate
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
- ### Repository Contracts — keep infra out of your domain
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
- // Compose the interface you need
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
- // Or only what you need
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
- archstone/core → Either, ValueObject, UniqueEntityId, WatchedList, Optional
162
- archstone/domain → all domain exports
163
- archstone/domain/enterprise Entity, AggregateRoot, DomainEvent, DomainEvents, EventHandler
164
- archstone/domain/application UseCase, UseCaseError, repository contracts
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
- ## Layer Architecture
212
+ ---
213
+
214
+ ## Architecture
168
215
 
169
216
  ```
170
217
  src/
171
- ├── core/ # Zero domain knowledge — pure utilities
172
- │ ├── either.ts
173
- │ ├── value-object.ts
174
- │ ├── unique-entity-id.ts
175
- │ ├── watched-list.ts
176
- │ └── types/optional.ts
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/ # Pure domain model — no framework deps
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/ # Use cases & repository contracts
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.3",
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
- "prepare": "husky || true"
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
+ ```