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 propertiesare JSON Schema definitions, one per property.requirednames the properties every stored order has.blankis 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:
| Property | What it holds |
|---|---|
id | The object’s identity: 32 hexadecimal characters, assigned at creation. |
VERSION | A counter that grows with every write. |
CREATED, UPDATED | When it was created and last written, in milliseconds since the epoch. |
DELETED | When it was deleted, or null. |
REGION | Which 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-millisis 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-timenested 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
| Key | Declares | Where |
|---|---|---|
name, namespace | The component’s address. | above |
entity | Its state: properties, required, blank, associated, storage. | above |
operations | What it does. | Write Operations |
prototype | What it inherits. | Use the Inherited Operations |
receivers | Which events it reacts to. | React to an Event |
bindings, bridge | How 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, build | How 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