archstone 1.0.4 → 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,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.
@@ -245,6 +247,26 @@ src/
245
247
 
246
248
  ---
247
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
+
248
270
  <div align="center">
249
271
 
250
272
  **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.0.4",
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
+ ```