archstone 1.0.4 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +43 -8
- 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,5 +1,7 @@
|
|
|
1
1
|
<div align="center">
|
|
2
2
|
|
|
3
|
+
<br />
|
|
4
|
+
|
|
3
5
|
# Archstone
|
|
4
6
|
|
|
5
7
|
### The TypeScript foundation for serious backend services.
|
|
@@ -15,11 +17,15 @@ Build on Domain-Driven Design and Clean Architecture — without writing the sam
|
|
|
15
17
|
|
|
16
18
|
<br />
|
|
17
19
|
|
|
20
|
+
[Quick Start](#install) · [Why Archstone](#why-archstone) · [Usage](#usage) · [Agent Skills](#agent-skills-new-in-v110) · [Architecture](#architecture) · [Contributing](./CONTRIBUTING.md)
|
|
21
|
+
|
|
22
|
+
<br />
|
|
23
|
+
|
|
18
24
|
</div>
|
|
19
25
|
|
|
20
26
|
---
|
|
21
27
|
|
|
22
|
-
## Why
|
|
28
|
+
## Why Archstone?
|
|
23
29
|
|
|
24
30
|
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
31
|
|
|
@@ -37,13 +43,16 @@ async function createUser(): Promise<Either<NotFoundError, User>> { ... }
|
|
|
37
43
|
|
|
38
44
|
## Features
|
|
39
45
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
46
|
+
| | |
|
|
47
|
+
|---|---|
|
|
48
|
+
| **`Either`** | Functional error handling — use cases never throw |
|
|
49
|
+
| **`Entity` / `AggregateRoot`** | Identity-based domain objects with built-in event support |
|
|
50
|
+
| **`ValueObject`** | Equality by value, not reference |
|
|
51
|
+
| **`UniqueEntityId`** | UUID v7 identity, consistent across your entire domain |
|
|
52
|
+
| **`WatchedList`** | Track additions and removals in collections without overwriting persistence |
|
|
53
|
+
| **`UseCase`** | Typed contract for application logic that always returns `Either` |
|
|
54
|
+
| **Repository contracts** | Define your interface in the domain — implement anywhere in infrastructure |
|
|
55
|
+
| **Agent Skills** | Built-in AI skill so your coding agent knows every DDD convention |
|
|
47
56
|
|
|
48
57
|
---
|
|
49
58
|
|
|
@@ -245,6 +254,32 @@ src/
|
|
|
245
254
|
|
|
246
255
|
---
|
|
247
256
|
|
|
257
|
+
## Agent Skills — new in v1.1.0
|
|
258
|
+
|
|
259
|
+
Archstone ships with a built-in skill for AI coding agents. Once installed, your agent understands every DDD convention, layer boundary, and usage pattern — without you ever having to explain them.
|
|
260
|
+
|
|
261
|
+
**The skill covers:**
|
|
262
|
+
- Entities, aggregates, and value objects
|
|
263
|
+
- Use cases with `Either` error handling
|
|
264
|
+
- Repository contracts and in-memory implementations
|
|
265
|
+
- Domain events — raising, dispatching, and handling
|
|
266
|
+
- Testing patterns with `bun:test` and in-memory repos
|
|
267
|
+
- Common mistakes and how to avoid them
|
|
268
|
+
|
|
269
|
+
**Install with Claude Code:**
|
|
270
|
+
|
|
271
|
+
```bash
|
|
272
|
+
bun x skills add joao-coimbra/archstone
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
**Or copy from the installed package:**
|
|
276
|
+
|
|
277
|
+
```bash
|
|
278
|
+
cp -r node_modules/archstone/skills/archstone .claude/skills/
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
---
|
|
282
|
+
|
|
248
283
|
<div align="center">
|
|
249
284
|
|
|
250
285
|
**Built with care for the TypeScript community.**
|
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.
|
|
4
|
+
"version": "1.1.1",
|
|
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
|
+
```
|