I First Steps

Run It and Make a Change

The shop wants a greeting of its own, and notes that can be ticked off. Both are changes to the two components toa create wrote, and making them shows what a component is made of.

The application is running, as Create an Application leaves it.

§Change what an operation answers

The greeting comes from one file:

// components/hello/operations/greet.ts
import type { GreetInput } from '../types/index.d.ts'

export async function computation (input: GreetInput) {
  return { greeting: `Hello, ${input.name ?? 'world'}!` }
}

Toa runs JavaScript and TypeScript alike: a .js and a .ts file are loaded the same way, with no build step. Every example is TypeScript, and it is what a production system is best written in: a mistyped property, or a call that no longer fits another component’s contract, is found by the editor and in CI, not at run time.

This is an operation. The file’s name is the operation’s name, greet. The name of the function says what kind of operation it is: a computation takes an input and returns a result, and touches no stored state. What it returns is what the caller gets.

Change the text:

// components/hello/operations/greet.ts
import type { GreetInput } from '../types/index.d.ts'

export async function computation (input: GreetInput) {
  return { greeting: `Welcome to the store, ${input.name ?? 'stranger'}!` }
}

A running process does not pick up edits. Restart:

$ npm run restart

§Call the operation

$ npx toa call hello.greet "{ input: { name: 'Toa' } }"
{ greeting: 'Welcome to the store, Toa!' }

This is a call, and it is how anything reaches an operation: the command line here, another component, a test. A call names an operation and carries a request; the runtime finds a process that runs the component, hands the request to the function, and brings back what it returned.

  • The first argument is <component>.<operation>.
  • The second is the request, written as YAML or JSON. input is the argument of the operation.

Nothing in it is HTTP. toa is the project’s own command line, installed with its packages, which is why it is run with npx.

§The manifest

What the code cannot say is in the component’s manifest:

# components/hello/manifest.toa.yaml
name: hello

operations:
  greet:
    input:
      type: object
      properties:
        name: { type: string, maxLength: 64 }
    output:
      type: object
      properties:
        greeting: { type: string }
      required: [greeting]

input and output are JSON Schemas of what greet takes and returns. A call that does not fit is refused before the function runs:

$ npx toa call hello.greet "{ input: { name: 'a name of more than sixty-four characters, which the schema does not allow' } }"
RequestContractException {
  code: 202,
  message: 'must NOT have more than 64 characters',
  …
}

GreetInput, the type the function’s parameter has, is generated from input. npm run env writes it, and npm run restart runs that first.

§Why it answers over HTTP

A component serves no HTTP itself. The same manifest goes on to say which of its operations appear as HTTP resources:

# components/hello/manifest.toa.yaml
exposition:
  /:
    anonymous: true
    io:output: true
    GET:
      endpoint: greet
      query:
        parameters: [name]

The gateway, part of the sys process, reads this and serves GET /hello/. A request to it becomes the call above: the name parameter of the URL is the name of the input, and the reply is the response.

$ curl 'http://localhost:8000/hello/?name=Toa'
{"greeting":"Welcome to the store, Toa!"}

Expose It over HTTP explains exposition.

§Change what is stored

notes has no code at all:

# components/notes/manifest.toa.yaml
name: notes

entity:
  properties:
    text: { type: string, maxLength: 1024 }
  required: [text]

operations:
  create:
    forward: transit
    query: false
    input:
      type: object
      properties:
        text: { type: string, maxLength: 1024 }
      required: [text]

exposition:
  /:
    anonymous: true
    io:output: [id, text]
    GET: enumerate
    POST: create
    /:id:
      GET: observe

entity is a JSON Schema of one note. A component that has one stores objects of it, and inherits the operations that read and write them:

OperationDoes
observeReturns one object.
enumerateReturns a list of objects.
transitCreates an object, or changes one, from its input.
assignSets some properties of an object.
terminateDeletes an object.

create is declared here and has no file: forward: transit runs the inherited transit, and query: false says a call never selects an existing note, so create only creates. Use the Inherited Operations describes all of them.

A note should be ticked off when it is done. Add the property, what a new note holds for it, and what a client may set:

# components/notes/manifest.toa.yaml
name: notes

entity:
  properties:
    text: { type: string, maxLength: 1024 }
    done: { type: boolean }
  required: [text, done]
  blank:
    done: false

operations:
  create:
    forward: transit
    query: false
    input:
      type: object
      properties:
        text: { type: string, maxLength: 1024 }
      required: [text]
  assign:
    input:
      type: object
      properties:
        done: { type: boolean }
      additionalProperties: false

exposition:
  /:
    anonymous: true
    io:output: [id, text, done]
    GET: enumerate
    POST: create
    /:id:
      GET: observe
      PATCH: assign
  • blank is what an object holds before anything is written to it: a new note is not done.
  • The inherited assign takes no input until the manifest says which properties it may set. Here it is done and nothing else, so a note’s text cannot be changed through it.
  • io:output names the properties a response carries, and PATCH: assign adds the route.
$ npm run restart

Create a note, then tick it off:

$ curl http://localhost:8000/notes/ 
    -H 'content-type: application/json' 
    -d '{"text":"Buy milk"}'
{"id":"01a115eaddd7717ba992e9bc4d95b29c","done":false,"text":"Buy milk"}
$ curl -X PATCH http://localhost:8000/notes/01a115eaddd7717ba992e9bc4d95b29c/ 
    -H 'content-type: application/json' 
    -d '{"done":true}'
{"done":true,"text":"Buy milk","id":"01a115eaddd7717ba992e9bc4d95b29c"}

The inherited operations are called like any other. query selects the state an operation works on:

$ npx toa call notes.enumerate "{ query: { criteria: 'done==true', limit: 10 } }"
[
  {
    VERSION: 2,
    DELETED: null,
    REGION: 0,
    done: true,
    CREATED: 1791369076184,
    UPDATED: 1791369134211,
    text: 'Buy milk',
    id: '01a115eaddd7717ba992e9bc4d95b29c'
  }
]

The runtime assigned the id and keeps the properties in capitals; VERSION grew with the change. Everything a request can hold is in Call Operations and Select State.

§A scenario now fails

$ npm run features
AssertionError [ERR_ASSERTION]: Response is missing 'greeting: Hello, Toa!'
    …
    -greeting: Welcome to the store, Toa!

The application came with a scenario for the greeting, and the greeting changed. Change what the scenario expects:

# features/hello.feature
Feature: Hello

  Scenario: Greeting by name
    When the request is sent:
      """
      GET /hello/?name=Toa HTTP/1.1
      accept: application/yaml
      """
    Then the response is received:
      """
      200 OK

      greeting: Welcome to the store, Toa!
      """
$ npm run features
2 scenarios (2 passed)
8 steps (8 passed)

Write a Test explains scenarios and adds one.

§What a restart does

npm run restart is two steps, and the first can be run alone:

$ npm run env   # toa types, toa env --dev, toa map
It runsWhich writes
toa typesThe types of every component, from its manifest: components/*/types.
toa env --dev.env: the settings a process needs, taken from the Context, with the local credentials.
toa map.map.json: what each component provides, and the version of its sources.

A process is started with the map and held to it. One started after an edit, without a new map, is refused:

'default.hello' is composed at a version the component map does not state. Run `toa map`.

.env and .map.json are generated; .gitignore leaves them out.

§More

Search
Search the documentation