Call Operations and Select State
Approving an order charges the customer’s wallet, and the back office lists a customer’s pending orders, newest first. Both are calls: one operation asking another to do something, and saying which state to do it to.
§A call
approve is a transition of store.orders, the component Create a Component writes; billing.wallets.debit is the operation Refuse a Request writes, which takes an amount and a ref,
what the money is for, and the whole approve is
there too. A customer’s wallet is kept under the customer’s id.
// components/store.orders/operations/approve.ts
const reply = await context.remote.billing.wallets.debit({
query: { id: entry.customer },
input: { amount: entry.total, ref: entry.id }
}) The argument is a request. Its two main parts are kept apart: input is what the operation is
asked to do, query is which state it does it to. The same request from the command line:
$ npx toa call billing.wallets.debit "{ query: { id: '8d1f0c2e5a7b4c3d9e6f1a2b3c4d5e6f' }, input: { amount: 1500, ref: 'order-1' } }" { balance: 3500 } context.remote.<namespace>.<name>.<operation>(request) | An operation of another component. |
context.local.<operation>(request) | An operation of the same component. |
Both return a promise of the reply: the output, or an Error the operation returned
(Refuse a Request). Neither needs to know where the
other component runs. A component without a namespace is called as context.remote.<name>.<operation>: context.remote.notifications.send(request).
A local call is a real call, not a function call: the request is validated, state is retrieved and stored, and events are published, as for a call from outside.
The request may be left out for an operation that takes no input and no query.
§Input
input must fit the operation’s input schema. A request that does not fit throws an exception
at the call, before anything is sent.
§Query
A query selects state within the operation’s scope.
§One object
{ query: { id: '5b1ad5d8a4c54e1dbb2a4a0b6e1f0c11' } } A query that selects nothing is an exception for a transition or an assignment, and the
operation does not run. An observation over entry runs nevertheless, and the inherited observe answers null:
$ npx toa call store.orders.observe "{ query: { id: '00000000000000000000000000000000' } }" null A query may also select one object by criteria; the first match in sort order is taken:
{ query: { criteria: 'customer==8d1f0c2e5a7b4c3d9e6f1a2b3c4d5e6f;status==pending', sort: ['CREATED:desc'] } } Criteria, sort and projection name a property inside an object by the path to it: delivery.city==Porto. See Properties inside an object.
§Several objects
$ npx toa call store.orders.enumerate "{ query: { criteria: 'status==pending;total>=1000', sort: ['CREATED:desc'], limit: 10 } }" [
{ id: '0c9e3b1f6a2d4e8f9b7c5d3a1e2f4b6c', total: 4200, status: 'pending', /* … */ },
{ id: '3f7a9d2c1b4e4a6f8c5d0e1b2a3c4d5e', total: 1800, status: 'pending', /* … */ }
] From an operation:
const orders = await context.remote.store.orders.enumerate({
query: {
criteria: 'status==pending;total>=1000',
sort: ['total:desc', 'id:asc'],
omit: 20,
limit: 10
}
}) §Query properties
| Property | Meaning |
|---|---|
id | One object by identity. |
ids | Several objects by identity. |
criteria | A filter over properties, in RSQL. |
search | A full-text search; needs a text index. |
sort | A list of property, property:asc or property:desc. |
omit, limit | Skip the first objects of the selection, and take at most so many. |
sample | Take so many objects at random from the selection. |
projection | The properties the operation is given; id always is. Observations only. |
version | The VERSION the object must be at. Not for observations. |
deleted | true to select deleted objects as well. |
token, stop | Continuing a stream; see Read a Large Collection. |
§Criteria
Criteria are written in RSQL:
status==pending, status!=pending | equal, not equal |
total>1000, total>=1000, total<1000, total<=1000 | comparison |
status=in=(pending,approved), status=out=(cancelled) | one of, none of |
a==1;b==2 | and |
a==1,b==2 | or |
(a==1,b==2);c==3 | grouping |
Values are read as the type the property is declared with. note==null matches objects where
the property is absent or null; quote a value to mean the text: note=="null".
Criteria can name system properties: CREATED>1764000000000.
Add an index for every query the application makes often.
§What each operation accepts
omitandlimitare accepted by an operation overentries— an observation, a transition or an effect. Such a call requiresidsorlimit, and alimitis at least1.omitrequiressort: pages of an unordered selection repeat and miss objects.- A stream accepts
limitand atokeninstead ofomit. projectionis for observations only: a transition writes back what it read, so it reads all of it.- An operation declared
query: falseaccepts no query; one declaredquery: truerequires one. An operation of a component without an entity accepts none.
A query that breaks one of these rules is an exception, like an input that does not fit.
§Creating
A transition called without a query creates an object: it receives a blank one with a fresh id, and what it leaves is stored as new. Written that way, create of store.orders is this
file and this declaration, in place of what the component has for it:
// components/store.orders/operations/create.ts
import type { CreateInput, State } from '../types/index.d.ts'
export async function transition (input: CreateInput, entry: State) {
entry.customer = input.customer
entry.items = input.items
entry.total = input.total
return { id: entry.id }
} # components/store.orders/manifest.toa.yaml
operations:
create:
query: false
input:
type: object
properties:
customer: { type: string }
items: { type: array }
total: { type: integer, minimum: 0 }
required: [customer, items, total] $ npm run restart
$ npx toa call store.orders.create "{ input: { customer: '8d1f0c2e5a7b4c3d9e6f1a2b3c4d5e6f', items: [{ product: 'p1', quantity: 2 }], total: 1800 } }" { id: '3f7a9d2c1b4e4a6f8c5d0e1b2a3c4d5e' } query: false makes it create-only. Without it, the same operation would also change an
existing order when called with a query — which is what the inherited transit does, and rarely
what an operation with rules wants.
For an associated entity there is no
creation: a query by id always selects an object, written or not.
§Changing only what you have seen
Every write increments the object’s VERSION. A caller that read an object and wants its change
applied only if nothing changed since names the version it saw:
const reply = await context.remote.store.orders.approve({
query: { id: order.id, version: order.VERSION }
}) If the object is at another version, the call fails with an exception — over HTTP, 412. This
is the caller’s check; concurrency on the operation is about
changes that happen while it runs.
§Deleted objects
Nothing is removed from storage. The inherited terminate sets DELETED, and from then on
queries do not find the object. deleted: true in a query finds it again — to show it, or to
restore it with a transition that sets DELETED to null:
// components/store.orders/operations/restore.ts
import type { State } from '../types/index.d.ts'
export async function transition (input: null, entry: State) {
entry.DELETED = null
} # components/store.orders/manifest.toa.yaml
operations:
restore:
concurrency: retry
query: true $ npm run restart
$ npx toa call store.orders.restore "{ query: { id: '3f7a9d2c1b4e4a6f8c5d0e1b2a3c4d5e', deleted: true } }" §Choosing what comes back
output in a request names the properties of the reply the caller wants; the rest is not sent:
$ npx toa call store.orders.enumerate "{ query: { criteria: 'status==pending', limit: 100 }, output: [id, total] }" [
{ id: '0c9e3b1f6a2d4e8f9b7c5d3a1e2f4b6c', total: 4200 },
{ id: '3f7a9d2c1b4e4a6f8c5d0e1b2a3c4d5e', total: 1800 }
] It applies to each object of a reply that is a list. Unlike projection, it works for any
operation, and it trims what the operation returned rather than what it was given.
§The whole request
| Property | Meaning | Where |
|---|---|---|
input | The operation’s arguments. | above |
query | Which state it works on. | above |
output | The properties of the reply to send back. | above |
entity | State handed to an effect instead of a query: the matching object is found or created. | Use the Inherited Operations |
task | true hands the call over to be run later, without waiting for it. | Do Work in the Background |
id | The identity of the call, for operations declared once. | Handle Repeated Calls |
instance | The process a stateful operation is called on. | Keep Something in a Process |
Calls in the model explains why input and query are separate.