Create a Component
The shop keeps orders. An order has a customer, items, a total and a status, and it may be
approved only while it is pending. That is a component of its own, beside the two toa create wrote.
§Describe an order
A component is a directory under components with a manifest:
# 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, items, total, status]
blank:
status: pending entity is a JSON Schema of one order. blank is what a new order holds before anything is
written to it.
namespace groups components that make no sense apart. Orders will be joined by the catalog
they are made from and the vouchers they are paid with, and none of those is of use without the
others: that is store. A component any application could use — the hello and notes that toa create wrote, a customer’s account, a sender of mail — has no namespace. The namespace is
not the application’s name, and most applications have several.
The directory is named <namespace>.<name>, or <name> for a component without a namespace.
That is already a component: nothing lists the components, the directories are the list. Ask the runtime what it sees:
$ npx toa export manifest --path components/store.orders operations:
assign:
type: assignment
scope: changeset
enumerate:
type: observation
scope: entries
observe:
type: observation
scope: entry
terminate:
type: assignment
scope: changeset
# … The output is the whole manifest as the runtime reads it. These operations are in it though you declared none: every component with an entity inherits them.
| Operation | Does |
|---|---|
observe | Returns one order. |
enumerate | Returns a list of orders. |
assign | Sets the properties of an order the manifest lets it set. |
terminate | Deletes an order. |
transit | Creates an order or changes one from its input. Available once you declare it. |
Use the Inherited Operations describes each of them.
§Create orders without code
Declare an operation that forwards to the inherited transit and say what it accepts:
# 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, minimum: 0 }
required: [customer, items, total] forward: transit—createruns the inherited implementation; there is no file to write.query: false— a call never selects an existing order, socreateonly creates.input— a JSON Schema of the argument. A call that does not fit is refused before anything runs.
§Add a rule
An order is approved only while it is pending. An operation with a rule is a file in the
component’s operations directory, named after the operation:
// components/store.orders/operations/approve.ts
import type { State } from '../types/index.d.ts'
export async function transition (input: null, entry: State) {
if (entry.status !== 'pending')
return new Error('NOT_PENDING')
entry.status = 'approved'
return entry
} State is the type of an order as a transition receives it. It is generated from the manifest
by npm run env, which npm run restart runs; until then the editor does not know it. approve declares no input, so its input is null. The function receives the order as entry, changes it, and returns what the caller gets. It does not load the order or save it: the
runtime does both. To refuse, return an Error whose message is a code.
The name of the function, transition, says that the operation changes state; the name of the
second parameter, entry, that it works on one order. The manifest adds what the code cannot say:
# components/store.orders/manifest.toa.yaml
operations:
create:
# …
approve:
concurrency: retry
query: true
errors: [NOT_PENDING] concurrency: retry— if the order was changed by someone else whileapprovewas running, run it again on the fresh order. A transition that takes a query has to declare it.query: true— a call must select an order.errors— every code the operation may return. A code that is not listed is refused instead of reaching the caller.
A mistake in the manifest is refused by every command that reads it, which says what is wrong:
must have required property 'concurrency' §Run it
$ npm run restart Create an order:
$ npx toa call store.orders.create "{ input: { customer: 'c1', items: [{ product: 'kettle', quantity: 1 }], total: 4200 } }" {
id: '01a115ec138e72048edc54e519ad736a',
VERSION: 1,
DELETED: null,
REGION: 0,
status: 'pending',
CREATED: 1791369200167,
UPDATED: 1791369200167,
customer: 'c1',
items: [ { product: 'kettle', quantity: 1 } ],
total: 4200
} The first argument is <namespace>.<name>.<operation>. input is the argument of the
operation. The runtime assigned the id, took status from blank and added the properties in
capitals, which are its own.
Approve it. query selects the order the operation works on:
$ npx toa call store.orders.approve "{ query: { id: '01a115ec138e72048edc54e519ad736a' } }" {
VERSION: 2,
status: 'approved',
// …
} Approve it again, and the rule answers:
CodedError: NOT_PENDING
at … {
code: 'NOT_PENDING'
} A returned error is a reply like any other: it is printed, and the command exits with 0.
A request that does not fit the operation is not a reply. It is an exception: printed to stderr,
with exit code 1.
$ npx toa call store.orders.create "{ input: { customer: 'c1' } }" RequestContractException {
code: 202,
message: "must have required property 'total'",
…
} §What to read when you need more
- The schema,
blank, identifiers and the properties in capitals: Describe State. - Kinds of operations and everything an operation may declare: Write Operations.
- Errors: Refuse a Request.
- Everything a request can hold: Call Operations and Select State.