archstone 1.3.0 → 1.3.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 CHANGED
@@ -224,9 +224,9 @@ export interface AuditRepository extends Creatable<AuditLog> {}
224
224
  | Import | Contents |
225
225
  |---|---|
226
226
  | `archstone` | Everything |
227
- | `archstone/core` | `Either`, `ValueObject`, `UniqueEntityId`, `WatchedList`, `Optional`, `EventHandler` |
227
+ | `archstone/core` | `Either`, `ValueObject`, `UniqueEntityId`, `WatchedList`, `Optional`, `DomainEvent`, `DomainEvents`, `EventHandler` |
228
228
  | `archstone/domain` | All domain exports |
229
- | `archstone/domain/enterprise` | `Entity`, `AggregateRoot`, `DomainEvent`, `DomainEvents` |
229
+ | `archstone/domain/enterprise` | `Entity`, `AggregateRoot` |
230
230
  | `archstone/domain/application` | `UseCase`, `UseCaseError`, repository contracts |
231
231
 
232
232
  All sub-paths share type declarations via a common chunk — mixing imports from multiple sub-paths is fully type-safe with no duplicate declaration conflicts.
@@ -242,18 +242,18 @@ src/
242
242
  │ ├── value-object.ts # Value equality base class
243
243
  │ ├── unique-entity-id.ts # UUID v7 identity wrapper
244
244
  │ ├── watched-list.ts # Change-tracked collection
245
+ │ ├── events/
246
+ │ │ ├── domain-event.ts # Marker interface for all domain events
247
+ │ │ ├── domain-events.ts # Central registry and dispatcher (singleton)
248
+ │ │ └── event-handler.ts # Generic handler interface EventHandler<T>
245
249
  │ └── types/
246
250
  │ └── optional.ts # Optional<T, K> helper type
247
251
 
248
252
  └── domain/
249
253
  ├── enterprise/ # Pure domain model — zero framework dependencies
250
- ├── entities/
251
- ├── entity.ts
252
- └── aggregate-root.ts
253
- │ └── events/
254
- │ ├── domain-event.ts
255
- │ ├── domain-events.ts
256
- │ └── event-handler.ts
254
+ └── entities/
255
+ ├── entity.ts
256
+ └── aggregate-root.ts
257
257
 
258
258
  └── application/ # Orchestration — use cases & repository contracts
259
259
  ├── use-cases/
@@ -269,6 +269,73 @@ src/
269
269
 
270
270
  ---
271
271
 
272
+ ## Technology Stack
273
+
274
+ | | |
275
+ |---|---|
276
+ | **Language** | TypeScript 5+ |
277
+ | **Runtime / Package Manager** | [Bun](https://bun.sh) (required) |
278
+ | **Test Framework** | `bun:test` (built-in) |
279
+ | **Build Tool** | [bunup](https://github.com/nicepkg/bunup) |
280
+ | **Linter / Formatter** | [Biome](https://biomejs.dev) via [Ultracite](https://ultracite.dev) |
281
+ | **Dependencies** | None (zero runtime dependencies) |
282
+
283
+ ---
284
+
285
+ ## Development Workflow
286
+
287
+ Requirements: **Bun >= 1.0**
288
+
289
+ ```bash
290
+ bun install # install dev dependencies
291
+ bun test # run tests
292
+ bun run build # compile to dist/
293
+ bun x ultracite fix # lint + format
294
+ ```
295
+
296
+ Branch naming: `feat/<name>`, `fix/<name>`, `docs/<name>`, `chore/<name>`
297
+
298
+ Every commit must pass `bun test` and `bun x ultracite fix` before pushing.
299
+
300
+ ---
301
+
302
+ ## Coding Standards
303
+
304
+ - **Error handling:** never throw inside use cases — always return `left(error)` with `Either`
305
+ - **Imports:** always import from a layer's index (`archstone/core`, `archstone/domain/enterprise`), never from deep paths
306
+ - **Layer boundaries:** inner layers never import outer ones — `core` has zero domain knowledge, `enterprise` never imports `application`
307
+ - **Factories:** always provide a static `create()` factory on entities and value objects — never expose constructors directly
308
+ - **Style:** no semicolons, 2-space indent, double quotes (enforced by Biome via Ultracite)
309
+ - **Commits:** [Conventional Commits](https://www.conventionalcommits.org/) — `feat`, `fix`, `chore`, `docs`, `refactor`, `test`
310
+
311
+ ---
312
+
313
+ ## Testing
314
+
315
+ Framework: `bun:test`. Test files are `*.spec.ts` co-located with the source they test.
316
+
317
+ ```ts
318
+ import { test, expect } from "bun:test"
319
+
320
+ test("example", () => {
321
+ expect(1).toBe(1)
322
+ })
323
+ ```
324
+
325
+ Use **in-memory repository implementations** for use case tests — never couple tests to a real database. Isolate domain event state between tests:
326
+
327
+ ```ts
328
+ import { DomainEvents } from "archstone/core"
329
+ import { beforeEach } from "bun:test"
330
+
331
+ beforeEach(() => {
332
+ DomainEvents.clearHandlers()
333
+ DomainEvents.clearMarkedAggregates()
334
+ })
335
+ ```
336
+
337
+ ---
338
+
272
339
  ## Agent Skills — new in v1.1.0
273
340
 
274
341
  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.
@@ -297,7 +364,7 @@ cp -r node_modules/archstone/skills/use-archstone .claude/skills/
297
364
 
298
365
  <div align="center">
299
366
 
300
- **Built with care for the TypeScript community.**
367
+ **Built with ❤️ for the TypeScript community.**
301
368
 
302
369
  [Contributing](./CONTRIBUTING.md) · [Code of Conduct](./CODE_OF_CONDUCT.md) · [MIT License](./LICENSE)
303
370
 
@@ -1,2 +1,2 @@
1
- import { DomainEvent, DomainEvents, Either, EventHandler1 as EventHandler, Optional, UniqueEntityId, ValueObject, WatchedList, left, right } from "../shared/chunk-z9br464b.js";
1
+ import { DomainEvent, DomainEvents, Either, EventHandler1 as EventHandler, Optional, UniqueEntityId, ValueObject, WatchedList, left, right } from "../shared/chunk-ghvxv2w6.js";
2
2
  export { right, left, WatchedList, ValueObject, UniqueEntityId, Optional, EventHandler, Either, DomainEvents, DomainEvent };
@@ -1,2 +1,2 @@
1
1
  // @bun
2
- import{c as a,d as b,e as c,f as d,g as e,h as f}from"../shared/chunk-8hx7pxm3.js";export{b as right,a as left,f as WatchedList,e as ValueObject,d as UniqueEntityId,c as DomainEvents};
2
+ import{c as a,d as b,e as c,f as d,g as e,h as f}from"../shared/chunk-k4yvnajk.js";export{b as right,a as left,f as WatchedList,e as ValueObject,d as UniqueEntityId,c as DomainEvents};
@@ -1,2 +1,2 @@
1
- import { Creatable, Deletable, Findable, Repository, Saveable, UseCase, UseCaseError } from "../../shared/chunk-z9br464b.js";
1
+ import { Creatable, Deletable, Findable, Repository, Saveable, UseCase, UseCaseError } from "../../shared/chunk-ghvxv2w6.js";
2
2
  export { UseCaseError, UseCase, Saveable, Repository, Findable, Deletable, Creatable };
@@ -1,2 +1,2 @@
1
- import { AggregateRoot, DomainEvent, DomainEvents, Entity, EventHandler } from "../../shared/chunk-z9br464b.js";
1
+ import { AggregateRoot, DomainEvent, DomainEvents, Entity, EventHandler } from "../../shared/chunk-ghvxv2w6.js";
2
2
  export { EventHandler, Entity, DomainEvents, DomainEvent, AggregateRoot };
@@ -1,2 +1,2 @@
1
1
  // @bun
2
- import{a as b,b as c}from"../../shared/chunk-k39ksk62.js";import{e as a}from"../../shared/chunk-8hx7pxm3.js";export{b as Entity,a as DomainEvents,c as AggregateRoot};
2
+ import{a as b,b as c}from"../../shared/chunk-ph10w7xb.js";import{e as a}from"../../shared/chunk-k4yvnajk.js";export{b as Entity,a as DomainEvents,c as AggregateRoot};
@@ -1,2 +1,2 @@
1
- import { AggregateRoot, Creatable, Deletable, DomainEvent, DomainEvents, Entity, EventHandler, Findable, Repository, Saveable, UseCase, UseCaseError } from "../shared/chunk-z9br464b.js";
1
+ import { AggregateRoot, Creatable, Deletable, DomainEvent, DomainEvents, Entity, EventHandler, Findable, Repository, Saveable, UseCase, UseCaseError } from "../shared/chunk-ghvxv2w6.js";
2
2
  export { UseCaseError, UseCase, Saveable, Repository, Findable, EventHandler, Entity, DomainEvents, DomainEvent, Deletable, Creatable, AggregateRoot };
@@ -1,2 +1,2 @@
1
1
  // @bun
2
- import"../shared/chunk-x296z7h1.js";import"../shared/chunk-wsjyhnpq.js";import{a as b,b as c}from"../shared/chunk-k39ksk62.js";import{e as a}from"../shared/chunk-8hx7pxm3.js";export{b as Entity,a as DomainEvents,c as AggregateRoot};
2
+ import"../shared/chunk-x296z7h1.js";import"../shared/chunk-wsjyhnpq.js";import{a as b,b as c}from"../shared/chunk-ph10w7xb.js";import{e as a}from"../shared/chunk-k4yvnajk.js";export{b as Entity,a as DomainEvents,c as AggregateRoot};
package/dist/index.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- import { AggregateRoot, Creatable, Deletable, DomainEvent, DomainEvents, Either, Entity, Findable, Optional, Repository, Saveable, UniqueEntityId, UseCase, UseCaseError, ValueObject, WatchedList, left, right } from "./shared/chunk-z9br464b.js";
1
+ import { AggregateRoot, Creatable, Deletable, DomainEvent, DomainEvents, Either, Entity, Findable, Optional, Repository, Saveable, UniqueEntityId, UseCase, UseCaseError, ValueObject, WatchedList, left, right } from "./shared/chunk-ghvxv2w6.js";
2
2
  export { right, left, WatchedList, ValueObject, UseCaseError, UseCase, UniqueEntityId, Saveable, Repository, Optional, Findable, Entity, Either, DomainEvents, DomainEvent, Deletable, Creatable, AggregateRoot };
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
1
  // @bun
2
- import"./shared/chunk-x296z7h1.js";import"./shared/chunk-wsjyhnpq.js";import{a as b,b as c}from"./shared/chunk-k39ksk62.js";import{c as f,d as m,e as p,f as t,g as x,h as a}from"./shared/chunk-8hx7pxm3.js";export{m as right,f as left,a as WatchedList,x as ValueObject,t as UniqueEntityId,b as Entity,p as DomainEvents,c as AggregateRoot};
2
+ import"./shared/chunk-x296z7h1.js";import"./shared/chunk-wsjyhnpq.js";import{a as b,b as c}from"./shared/chunk-ph10w7xb.js";import{c as f,d as m,e as p,f as t,g as x,h as a}from"./shared/chunk-k4yvnajk.js";export{m as right,f as left,a as WatchedList,x as ValueObject,t as UniqueEntityId,b as Entity,p as DomainEvents,c as AggregateRoot};
@@ -346,6 +346,15 @@ declare class DomainEventsImplementation {
346
346
  private readonly handlersMap;
347
347
  private readonly markedAggregates;
348
348
  /**
349
+ * Controls whether event dispatching is active.
350
+ *
351
+ * Set to `false` in tests that construct aggregates but do not want
352
+ * side-effects to run, without having to clear and re-register handlers.
353
+ *
354
+ * @default true
355
+ */
356
+ shouldRun: boolean;
357
+ /**
349
358
  * Marks an aggregate root to have its events dispatched.
350
359
  * Called automatically by {@link AggregateRoot.addDomainEvent}.
351
360
  *
@@ -0,0 +1,2 @@
1
+ // @bun
2
+ class H{value;constructor(x){this.value=x}isRight(){return!1}isLeft(){return!0}}class V{value;constructor(x){this.value=x}isRight(){return!0}isLeft(){return!1}}var B=(x)=>new H(x),F=(x)=>new V(x);class W{handlersMap=new Map;markedAggregates=new Set;shouldRun=!0;markAggregateForDispatch(x){if(!this.findMarkedAggregateByID(x.id))this.markedAggregates.add(x)}dispatchEventsForAggregate(x){let E=this.findMarkedAggregateByID(x);if(E)this.dispatchAggregateEvents(E),E.clearEvents(),this.removeAggregateFromMarkedDispatchList(E)}register(x,E){if(!this.handlersMap.has(E))this.handlersMap.set(E,[]);this.handlersMap.get(E)?.push(x)}clearHandlers(){this.handlersMap.clear()}clearMarkedAggregates(){this.markedAggregates.clear()}dispatchAggregateEvents(x){for(let E of x.domainEvents)this.dispatch(E)}removeAggregateFromMarkedDispatchList(x){this.markedAggregates.delete(x)}findMarkedAggregateByID(x){return[...this.markedAggregates].find((E)=>E.id.equals(x))}dispatch(x){if(!this.shouldRun)return;let E=this.handlersMap.get(x.constructor.name)??[];for(let O of E)O(x)}}var G=new W;var{randomUUIDv7:J}=globalThis.Bun;class f{value;constructor(x){this.value=x??J()}toValue(){return this.value}toString(){return this.value}equals(x){return x.toValue()===this.value}}class z{props;constructor(x){this.props=x}equals(x){return JSON.stringify(this.props)===JSON.stringify(x.props)}}class A{currentItems;initial;new;removed;constructor(x){this.currentItems=x??[],this.initial=x??[],this.new=[],this.removed=[]}getItems(){return this.currentItems}getNewItems(){return this.new}getRemovedItems(){return this.removed}exists(x){return this.isCurrentItem(x)}add(x){if(this.isRemovedItem(x))this.removeFromRemoved(x);if(!(this.isNewItem(x)||this.wasAddedInitially(x)))this.new.push(x);if(!this.isCurrentItem(x))this.currentItems.push(x)}remove(x){if(this.removeFromCurrent(x),this.isNewItem(x)){this.removeFromNew(x);return}if(!this.isRemovedItem(x))this.removed.push(x)}update(x){this.new=x.filter((E)=>!this.getItems().some((O)=>this.compareItems(E,O))),this.removed=this.getItems().filter((E)=>!x.some((O)=>this.compareItems(E,O))),this.currentItems=x}isCurrentItem(x){return this.currentItems.some((E)=>this.compareItems(x,E))}isNewItem(x){return this.new.some((E)=>this.compareItems(x,E))}isRemovedItem(x){return this.removed.some((E)=>this.compareItems(x,E))}removeFromNew(x){this.new=this.new.filter((E)=>!this.compareItems(E,x))}removeFromCurrent(x){this.currentItems=this.currentItems.filter((E)=>!this.compareItems(x,E))}removeFromRemoved(x){this.removed=this.removed.filter((E)=>!this.compareItems(x,E))}wasAddedInitially(x){return this.initial.some((E)=>this.compareItems(x,E))}}export{B as c,F as d,G as e,f,z as g,A as h};
@@ -1,2 +1,2 @@
1
1
  // @bun
2
- import{e as j,f as k}from"./chunk-8hx7pxm3.js";class R{_id;props;get id(){return this._id}constructor(A,B){this._id=B??new k,this.props=A}equals(A){if(A===this)return!0;return A.id.equals(this._id)}}class z extends R{_domainEvents=new Set;get domainEvents(){return Array.from(this._domainEvents)}addDomainEvent(A){this._domainEvents.add(A),j.markAggregateForDispatch(this)}clearEvents(){this._domainEvents.clear()}}export{R as a,z as b};
2
+ import{e as j,f as k}from"./chunk-k4yvnajk.js";class R{_id;props;get id(){return this._id}constructor(A,B){this._id=B??new k,this.props=A}equals(A){if(A===this)return!0;return A.id.equals(this._id)}}class z extends R{_domainEvents=new Set;get domainEvents(){return Array.from(this._domainEvents)}addDomainEvent(A){this._domainEvents.add(A),j.markAggregateForDispatch(this)}clearEvents(){this._domainEvents.clear()}}export{R as a,z as b};
package/package.json CHANGED
@@ -1,7 +1,7 @@
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.3.0",
4
+ "version": "1.3.1",
5
5
  "type": "module",
6
6
  "private": false,
7
7
  "files": [
@@ -1,2 +0,0 @@
1
- // @bun
2
- class H{value;constructor(x){this.value=x}isRight(){return!1}isLeft(){return!0}}class V{value;constructor(x){this.value=x}isRight(){return!0}isLeft(){return!1}}var B=(x)=>new H(x),F=(x)=>new V(x);class W{handlersMap=new Map;markedAggregates=new Set;markAggregateForDispatch(x){if(!this.findMarkedAggregateByID(x.id))this.markedAggregates.add(x)}dispatchEventsForAggregate(x){let E=this.findMarkedAggregateByID(x);if(E)this.dispatchAggregateEvents(E),E.clearEvents(),this.removeAggregateFromMarkedDispatchList(E)}register(x,E){if(!this.handlersMap.has(E))this.handlersMap.set(E,[]);this.handlersMap.get(E)?.push(x)}clearHandlers(){this.handlersMap.clear()}clearMarkedAggregates(){this.markedAggregates.clear()}dispatchAggregateEvents(x){for(let E of x.domainEvents)this.dispatch(E)}removeAggregateFromMarkedDispatchList(x){this.markedAggregates.delete(x)}findMarkedAggregateByID(x){return[...this.markedAggregates].find((E)=>E.id.equals(x))}dispatch(x){let E=this.handlersMap.get(x.constructor.name)??[];for(let O of E)O(x)}}var G=new W;var{randomUUIDv7:J}=globalThis.Bun;class f{value;constructor(x){this.value=x??J()}toValue(){return this.value}toString(){return this.value}equals(x){return x.toValue()===this.value}}class z{props;constructor(x){this.props=x}equals(x){return JSON.stringify(this.props)===JSON.stringify(x.props)}}class A{currentItems;initial;new;removed;constructor(x){this.currentItems=x??[],this.initial=x??[],this.new=[],this.removed=[]}getItems(){return this.currentItems}getNewItems(){return this.new}getRemovedItems(){return this.removed}exists(x){return this.isCurrentItem(x)}add(x){if(this.isRemovedItem(x))this.removeFromRemoved(x);if(!(this.isNewItem(x)||this.wasAddedInitially(x)))this.new.push(x);if(!this.isCurrentItem(x))this.currentItems.push(x)}remove(x){if(this.removeFromCurrent(x),this.isNewItem(x)){this.removeFromNew(x);return}if(!this.isRemovedItem(x))this.removed.push(x)}update(x){this.new=x.filter((E)=>!this.getItems().some((O)=>this.compareItems(E,O))),this.removed=this.getItems().filter((E)=>!x.some((O)=>this.compareItems(E,O))),this.currentItems=x}isCurrentItem(x){return this.currentItems.some((E)=>this.compareItems(x,E))}isNewItem(x){return this.new.some((E)=>this.compareItems(x,E))}isRemovedItem(x){return this.removed.some((E)=>this.compareItems(x,E))}removeFromNew(x){this.new=this.new.filter((E)=>!this.compareItems(E,x))}removeFromCurrent(x){this.currentItems=this.currentItems.filter((E)=>!this.compareItems(x,E))}removeFromRemoved(x){this.removed=this.removed.filter((E)=>!this.compareItems(x,E))}wasAddedInitially(x){return this.initial.some((E)=>this.compareItems(x,E))}}export{B as c,F as d,G as e,f,z as g,A as h};