domain-toolkit
    Preparing search index...

    Class Entity<EntityState>Abstract

    An Entity is a domain object defined by who it is, not by what it holds.

    Its attributes change over their lifetime — a copy of a book gets rebound, relabelled, moved to another shelf — and it remains the same copy throughout. That thread of continuity is the entity. Equality therefore compares identity and nothing else.

    An identity is a fact about the entity, not an object of its own. What the concept demands is a stable id and identity-based equality; it says nothing about the id's shape. So id is a plain, non-blank string, the entity itself answers equals, and how an id is made is the business of a DomainIdGenerator, never of the entity or its factory.

    An identity that grows a rule of its own — a prefix someone reads, a time part, a tenant scope — is no longer a mere identity. It has structure, and structure is what a ValueObject is for. Reach for one on that day, for that id, and not before.

    Every Entity can record() a domain event into a private buffer. Nothing else. There is no bus here, no publish method, no way out.

    The only exit is AggregateRoot.pullDomainEvents, which drains this buffer along with those of every child entity the root declares. A child entity that records an event and is not reachable from its root's childEntities() will simply never have that event dispatched — silently.

    An entity may describe what happened to it, because it is the only object that knows; but the aggregate root decides what leaves the boundary, because it is the only object that knows whether the change was consistent.

    Type Parameters

    Hierarchy (View Summary)

    Index
    id: string
    abstractBase: boolean = true

    Marks a class that exists to be extended and never to be instantiated.

    abstract alone cannot do this job: it is erased at emit, and the this constraint on create is satisfied by the abstract base itself. The check is Object.hasOwn, so only the class that declares this field for itself is a base — which also makes the marker available to model authors, for an abstract intermediate of your own.

    • Build an entity of the class this was called on.

      const stock = BookStock.create("dune", {
      title: "Dune",
      barcodes: [],
      copies: [],
      }); // : BookStock

      Creation takes the entity's required properties only. Supplying an optional one does not compile:

      BookStock.create(id, { title: "Dune", barcodes: [], copies: [] });          // ok
      BookStock.create(id, { title: "Dune", barcodes: [], copies: [],
      author: "Herbert" }); // TS2353

      That refusal is the point: an entity begins life holding exactly what it cannot exist without, and everything else arrives through behaviour that means something in the domain. Read RequiredState for the ?-means-deferred convention this depends on.

      Type Parameters

      • This extends { prototype: Entity<any> }

      Parameters

      Returns This["prototype"]

    • Read state, either whole or one property at a time.

      Reading is not a write channel: the value handed back is detached from the state, so mutating it reaches nothing. The copy is one level deep — see @domain-toolkit/state for that boundary and its one deliberate limit.

      Returns EntityState

    • Read state, either whole or one property at a time.

      Reading is not a write channel: the value handed back is detached from the state, so mutating it reaches nothing. The copy is one level deep — see @domain-toolkit/state for that boundary and its one deliberate limit.

      Type Parameters

      • K extends string | number | symbol

      Parameters

      • key: K

      Returns EntityState[K]

    • Open a mutation context for the duration of operation.

      Separate from mutate because rehydration needs the context without the invariant check: AggregateRoot.replay reaches set through its handlers, and a per-event assertion would reject valid histories.

      Type Parameters

      • T

      Parameters

      • operation: () => T

      Returns T

    • Refuse a state change made outside a domain operation.

      This is what makes mutate mandatory rather than a convention: a change made outside an operation is one nothing will check and nothing can roll back.

      Parameters

      • method: string

      Returns void

    • Run one domain operation atomically: either it completes with this object's invariants intact, or nothing about it happened.

      addCopy(barcode: string): void {
      this.mutate(() => this.apply(new CopyAdded(barcode)));
      }

      On the way out it calls assertInvariants. If that throws — or if the operation itself throws — the state is restored and every event the operation recorded is discarded, so a caller who catches the error is telling the truth when they assume nothing happened.

      One limit, pinned by a test: rollback covers this object's own state and event buffer, and does not rewind events recorded by child entities during the operation. Their buffers are #private to each Entity.

      Parameters

      • operation: () => void

      Returns void

    • Assert every rule this object is responsible for.

      A no-op here, and abstract on AggregateRoot. An entity that is not an aggregate root has no invariants of its own — the rules that span a cluster belong to the root that owns the boundary — but mutate lives on this class and needs something to call.

      Returns void

    • Identity equality. Note what is not compared: none of the attributes. Two Copy instances loaded separately from the repository, one of them stale, are still the same copy.

      Identity is only meaningful between entities of the same kind: a Copy and a Member that happen to carry the same string do not refer to the same thing. "Same kind" is the same concrete class, not instanceof, so that the answer is symmetric — a.equals(b) and b.equals(a) always agree, which a subclass-tolerant check cannot promise.

      Parameters

      Returns boolean

    • Drain the buffer. Called by the aggregate root — see the class comment.

      Draining rather than copying is deliberate: an event must be dispatched exactly once. If a root is saved twice, the second save must not replay history.

      Returns readonly DomainEvent[]