II Business Logic

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

OperationTypeScopeWhat it does
transittransitionentryMerges 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.
observeobservationentryReturns the object, or null.
enumerateobservationentriesReturns the list the query selects.
streamobservationstreamReturns the selection as a stream.
assignassignmentchangesetSets the properties given in the input, once the manifest declares that input.
terminateassignmentchangesetMarks the object deleted.
ensureeffectentryReturns 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:

EventPublished when
createdan object is created
updatedan existing object is changed
deletedan object is marked deleted
syncany 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.

Search
Search the documentation