Use the Inherited Operations
Staff add a note to an order, list orders, look one up, delete one. None of that carries a business rule, and none of it needs a file: a component that declares an entity already has these operations. It inherits them from the generic prototype.
§The operations
| Operation | Type | Scope | What it does |
|---|---|---|---|
transit | transition | entry | Merges the input into the object and returns it. Creates one when called without a query. There once the manifest declares it, or forwards to it. |
observe | observation | entry | Returns the object, or null. |
enumerate | observation | entries | Returns the list the query selects. |
stream | observation | stream | Returns the selection as a stream. |
assign | assignment | changeset | Sets the properties given in the input, once the manifest declares that input. |
terminate | assignment | changeset | Marks the object deleted. |
ensure | effect | entry | Returns the object matching the request’s entity, creating it if there is none. |
The orders are those of store.orders, the component Create a Component writes. An inherited operation takes no input
until the manifest declares one. For the note, add a property to the entity and say that assign may set it:
# components/store.orders/manifest.toa.yaml
entity:
properties:
# …
note: { type: string, maxLength: 256 }
operations:
assign:
input:
type: object
properties:
note: { type: string, maxLength: 256 }
additionalProperties: false $ npm run restart
$ npx toa call store.orders.observe "{ query: { id: '5b1ad5d8a4c54e1dbb2a4a0b6e1f0c11' } }"
$ npx toa call store.orders.enumerate "{ query: { criteria: 'status==pending', sort: ['CREATED:desc'], limit: 50 } }"
$ npx toa call store.orders.assign "{ query: { id: '5b1ad5d8a4c54e1dbb2a4a0b6e1f0c11' }, input: { note: 'gift' } }"
$ npx toa call store.orders.terminate "{ query: { id: '5b1ad5d8a4c54e1dbb2a4a0b6e1f0c11' } }" From an operation, as a call:
const order = await context.remote.store.orders.observe({ query: { id } })
const pending = await context.remote.store.orders.enumerate({
query: { criteria: 'status==pending', sort: ['CREATED:desc'], limit: 50 }
})
await context.remote.store.orders.assign({ query: { id }, input: { note: 'gift' } })
await context.remote.store.orders.terminate({ query: { id } }) The entity schema still applies: transit and assign cannot store an invalid object, and guards apply to them as to any operation.
§Constraining an inherited operation
Declare an inherited operation in the manifest to say what it accepts; the declaration is merged
with the inherited one. transit declared with an input accepts exactly that from any caller:
# components/store.orders/manifest.toa.yaml
operations:
transit:
concurrency: retry
input:
type: object
properties:
note: { type: string, maxLength: 256 }
additionalProperties: false transit must be declared with a concurrency before it can change existing objects.
§Forwarding
The shop needs “add a note” as an operation of its own, with a narrow contract, and still no
code. forward gives an inherited implementation another name:
# components/store.orders/manifest.toa.yaml
operations:
add:
forward: transit
query: false
input:
type: object
properties:
customer: { type: string }
items: { type: array }
total: { type: integer, minimum: 0 }
required: [customer, items, total]
annotate:
forward: assign
query: true
input:
type: object
properties:
note: { type: string, maxLength: 256 }
additionalProperties: false $ npm run restart
$ npx toa call store.orders.annotate "{ query: { id: '5b1ad5d8a4c54e1dbb2a4a0b6e1f0c11' }, input: { note: 'gift' } }" add creates an order from exactly these properties, annotate sets a note on an existing one,
and neither needed a file. Most write operations that carry no rules are forwards with a narrow
input.
§Replacing an inherited operation
Deleting an order must also cancel it. A file in operations with the name of an inherited
operation replaces its implementation:
// components/store.orders/operations/terminate.ts
import type { Context, Entity } from '../types/index.d.ts'
export async function assignment (input: null, changeset: Partial<Entity>, context: Context) {
changeset.status = 'cancelled'
changeset.DELETED = context.now()
} Deletion is a write of DELETED, which is all terminate is.
§The inherited events
Every component with an entity also announces its changes, with nothing declared:
| Event | Published when |
|---|---|
created | an object is created |
updated | an existing object is changed |
deleted | an object is marked deleted |
sync | any of the above |
Each carries the object’s new state. Another component can receive store.orders.created without store.orders declaring anything. To
change what one of them means, write an event of the same name.
§Going without
prototype: null inherits nothing: no operations, no events. A component that declares no
entity has no prototype to begin with.
prototype: null §A prototype of your own
Several components of the shop keep an audit trail the same way. Put what they share — entity properties, operations, events — in a component directory and name it by path:
# components/store.orders/manifest.toa.yaml
prototype: ../prototypes/auditable The component inherits the prototype’s entity properties, operations, events, extension declarations and migrations, and the prototype in turn inherits from its own — the generic one unless it says otherwise. What the component declares itself wins over what it inherits.
A prototype that declares migrations must have a name.
The Prototype in the model explains the idea.