II Business Logic

Describe State

The shop takes orders. An order has a customer, a list of items, a total in minor units, and a status that starts as pending. Describing that is the entity section of the component’s manifest.

§An entity

# components/store.orders/manifest.toa.yaml
name: orders
namespace: store

entity:
  properties:
    customer:
      type: string
    items:
      type: array
      items:
        type: object
        properties:
          product: { type: string }
          quantity: { type: integer, minimum: 1 }
        required: [product, quantity]
    total:
      type: integer
      minimum: 0
    status:
      type: string
      enum: [pending, approved, cancelled]
  required: [customer, total, status]
  blank:
    items: []
    status: pending
  • properties are JSON Schema definitions, one per property.
  • required names the properties every stored order has.
  • blank is what an order holds before anything is written to it. It need not be a whole valid order, but each value must fit the property it names.

store.orders is the component Create a Component writes; this is its entity, with items not required and empty in blank. An order is created by an operation that forwards to the inherited transit. Declare it below the entity:

# components/store.orders/manifest.toa.yaml
operations:
  create:
    forward: transit
    query: false
    input:
      type: object
      properties:
        customer: { type: string }
        items: { type: array }
        total: { type: integer }
      required: [customer, total]

Restart the application and create an order:

$ npm run restart
$ npx toa call store.orders.create "{ input: { customer: '8d1f0c2e5a7b4c3d9e6f1a2b3c4d5e6f', total: 1500 } }"
{
  id: '5b1ad5d8a4c54e1dbb2a4a0b6e1f0c11',
  customer: '8d1f0c2e5a7b4c3d9e6f1a2b3c4d5e6f',
  items: [],
  total: 1500,
  status: 'pending',
  VERSION: 1,
  CREATED: 1764000000000,
  UPDATED: 1764000000000,
  DELETED: null,
  REGION: 0
}

items and status came from blank; the properties in capitals are written by the runtime.

Every write is checked against the schema, whichever operation makes it. A state that does not fit is not stored and the call fails with an exception:

$ npx toa call store.orders.create "{ input: { customer: '8d1f0c2e5a7b4c3d9e6f1a2b3c4d5e6f', total: -1 } }"

A rule about one property’s shape or range belongs here. A rule about a change — “an approved order’s total never grows” — is a guard; a rule about who may do what and when is an operation.

§Name and namespace

name and namespace are the component’s address: other components call store.orders, and its events are store.orders.<event>. Each is a letter followed by up to 31 letters and digits. The namespace system is reserved.

A namespace groups components that make no sense apart: store.orders and store.catalog are the shop’s own, billing.wallets and billing.ledger are its money. It is not the name of the application. A component any application could use — notifications, accounts — has none: its manifest has no namespace line, it is called as notifications.send, and its events are notifications.<event>. Its id, which the runtime prints in a log or an exception, is default.notifications.

A manifest that states no name takes both from its directory: components/store.orders is orders in store, and components/notifications is notifications with no namespace.

§One definition for a property

Operations usually repeat parts of the entity schema in their input. YAML anchors keep one definition:

# components/store.orders/manifest.toa.yaml
entity:
  properties:
    total: &total
      type: integer
      minimum: 0

operations:
  create:
    input:
      type: object
      properties:
        total: *total

§System properties

Every stored object carries properties you do not declare:

PropertyWhat it holds
idThe object’s identity: 32 hexadecimal characters, assigned at creation.
VERSIONA counter that grows with every write.
CREATED, UPDATEDWhen it was created and last written, in milliseconds since the epoch.
DELETEDWhen it was deleted, or null.
REGIONWhich region wrote it; 0 unless the application runs in several.

Read them, filter and sort by them in a query, index them. Do not declare them in properties or name them in blank; only id may be declared. A manifest that does is refused:

System property 'DELETED' cannot be overridden

Two more exist only on the state a transition receives and are never stored: TRAILERS (Announce a Change) and DISCARD (Refuse a Request). Neither may be declared.

§Identity

An operation never generates an id: it is assigned when the object is created.

§An object identified by something else

An associated entity has no id of its own: its id is the id of another entity.

A customer keeps money in the shop: each customer has one wallet, and the customer’s id is the id of the wallet. Create components/billing.wallets and declare its entity associated:

# components/billing.wallets/manifest.toa.yaml
name: wallets
namespace: billing

entity:
  associated: true
  properties:
    balance:
      type: integer
      minimum: 0
  required: [balance]
  blank:
    balance: 0

Now any id names a wallet. Reading one that was never written answers its blank, and the first write creates it. There is no “create a wallet” step and no “wallet not found”:

$ npm run restart
$ npx toa call billing.wallets.observe "{ query: { id: '8d1f0c2e5a7b4c3d9e6f1a2b3c4d5e6f' } }"
{ id: '8d1f0c2e5a7b4c3d9e6f1a2b3c4d5e6f', balance: 0, VERSION: 0 }

Nothing has to create a wallet when a customer signs up, and everything else kept about a customer can be kept the same way, under the same id; Keep Data About a Customer without Creating It reasons it out. The wallet’s operations are written where they are needed: debit in Refuse a Request, and the whole of it in A Wallet and Its Ledger.

§Identifiers of another shape

To use identifiers that are not 32 hexadecimal characters, declare id yourself. The shop’s catalog keeps a product under its SKU; create components/store.catalog:

# components/store.catalog/manifest.toa.yaml
name: catalog
namespace: store

entity:
  associated: true
  properties:
    id:
      type: string
      pattern: ^[A-Z]{3}-[0-9]{4}$
    title:
      type: string
    price:
      type: integer
      minimum: 0

operations:
  transit:
    concurrency: retry
    input:
      type: object
      properties:
        title: { type: string }
        price: { type: integer, minimum: 0 }

The inherited transit is there once the manifest declares it, and takes the input declared for it:

$ npm run restart
$ npx toa call store.catalog.transit "{ query: { id: 'TEA-0042' }, input: { title: 'Green tea', price: 450 } }"

§Moments

A property that holds a point in time is declared with a format. It is then stored as a date, so that an index sorts, compares and expires by it:

entity:
  properties:
    paid: { type: string, format: date-time }        # an ISO 8601 string in your code
    expires: { type: integer, format: epoch-millis } # what context.now() answers

Your code reads and writes the value as declared. Three things to know:

  • epoch-millis is a moment, not a duration. A timeout or an interval declared with it would be stored as a date in 1970.
  • Only a property of the object itself is stored as a date. A date-time nested in an object or in an array is validated and stored as the string it is.
  • Adding the format to a property that already holds data needs a migration that converts what is stored.

§A component without state

A component that keeps nothing declares no entity. It still has operations: computations and effects (see Write Operations).

§Storage

storage names where the state is kept. It defaults to MongoDB and is rarely stated:

entity:
  storage: mongodb

storage: ~ keeps nothing: writes are validated and dropped, and no query finds anything. Use it for a component under test. Other storages are in Connectors.

§The whole manifest

KeyDeclaresWhere
name, namespaceThe component’s address.above
entityIts state: properties, required, blank, associated, storage.above
operationsWhat it does.Write Operations
prototypeWhat it inherits.Use the Inherited Operations
receiversWhich events it reacts to.React to an Event
bindings, bridgeHow it is called and what runs its code.Connectors
configuration, exposition, realtime, storages, stash, state, cadence, continuity, telemetry, …The extensions it uses.Extensions
version, files, ignore, buildHow it is versioned and built.Upgrades, Deployment

Events and guards are files, not manifest keys: see Announce a Change and Protect an Invariant. A manifest with an unknown key is refused when it is read.

To see the manifest as it is understood — with inherited operations merged in and each operation’s type and scope as read from the code:

$ npx toa export manifest --path components/store.orders
Search
Search the documentation