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.
inputis 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:
| Operation | Does |
|---|---|
observe | Returns one object. |
enumerate | Returns a list of objects. |
transit | Creates an object, or changes one, from its input. |
assign | Sets some properties of an object. |
terminate | Deletes 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 blankis what an object holds before anything is written to it: a new note is not done.- The inherited
assigntakes no input until the manifest says which properties it may set. Here it isdoneand nothing else, so a note’s text cannot be changed through it. io:outputnames the properties a response carries, andPATCH: assignadds 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 runs | Which writes |
|---|---|
toa types | The 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
- Every command and flag: The CLI.
- Kinds of operations: Write Operations.
- The entity schema,
blankand the properties in capitals: Describe State.