I First Steps

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.

OperationDoes
observeReturns one order.
enumerateReturns a list of orders.
assignSets the properties of an order the manifest lets it set.
terminateDeletes an order.
transitCreates 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 — create runs the inherited implementation; there is no file to write.
  • query: false — a call never selects an existing order, so create only 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 while approve was 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

Search
Search the documentation