# The acting node and elevation Every invocation of a function acts at exactly **one node** of your product's organization tree. The records it reads and writes, the mail it sends, the events it publishes, what it is metered against and what its log says are all at that node. This page says which node that is, how a function moves below it, how it manages the tree, and how it may count records it cannot read. ## Which node an invocation acts at | the invocation | acts at | |---|---| | a route inside `[authenticate]` (a session) | the home of the end user's selected membership | | a public `[[route]]` in `config/routes.toml` | the route's `org_path` | | an event trigger (`[[triggers.events]]` in `function.toml`) | the trigger's `org_path` | | a cron trigger (`[[triggers.cron]]`) | the trigger's `org_path` | | `aircloud functions invoke` | your own node (the membership you act through), or with `--at ` a node at or below it | A public route, an event trigger and a cron trigger have no end user, so they **must declare** their node: ```toml [[route]] prefix = "/api/hooks" function = "accounts" org_path = "/crm" ``` ```toml [[triggers.cron]] schedule = "0 3 * * *" org_path = "/crm/companies" [[triggers.events]] topic = "accounts.opened" org_path = "/" ``` `aircloud check` refuses an entry without one: `route.org_path_required`, `trigger.org_path_required`. `/`, the root, is a valid choice only when you write it — nothing defaults to it, so acting over every customer of the product is always visible in your repository. The generators write `org_path = "/"` on every route and trigger they add; edit it to act lower. A route to a container runs no function and declares none (`route.org_path_not_invocation`). ### Every node you name is declared A node exists only when something creates it, so every node your repository names — a public route's or a trigger's `org_path`, an `[mcp]` facet's `org_path`, an `[[authenticate.route]]`'s `at` — must be one each production activation creates, or one created at run time below a declared parent: | you may name | because | |---|---| | `/` | the tenant root always exists | | `admin_org_path`, `default_org_path`, the `[authenticate.workspaces]` parent, every `[authenticate] nodes` entry, and each of their ancestors | each production activation creates them ([Members and roles](members.md)) | | any node strictly below the `[authenticate.workspaces]` parent | workspace signup creates those at run time ([Workspaces](workspaces.md)) | `aircloud check` refuses any other node (`org.path_undeclared`), so a product never fails at run time on a node nothing created. To act at a fixed node of your own, declare it: ```toml [authenticate] nodes = ["/crm", { path = "/crm/companies", display_name = "Empresas" }] ``` An entry is the node's path, or `{ path, display_name }` to name it for people: each production release sets the declared name on the node. A node below a fixed node is not declared by it: declare that node too. A node created by a function through `nodes.create` outside the workspaces parent exists only once the function has run, so declare it in `nodes` if your repository names it. Removing a node from `nodes` never deletes it: archiving and erasing a node are your explicit acts (`aircloud org nodes archive`, `aircloud org nodes delete`). A function with no end user is not owner-scoped: it reads every customer's owned records at or below its node, and what it writes is stored at its node ([Owner and shared entities](entities.md#requests-with-no-end-user)). An event trigger receives only the events published at or below its node. ## Narrowing — only downward A function may move to a node **at or below** the one it entered at, through the guest interface `acting-node`: `current()` answers the node the next call acts at, and `narrow(path)` moves there. A node above or beside the entry node is refused (`capability.forbidden`). Narrowing grants nothing; it only shrinks what the function already reaches. ```js import { current, narrow } from "aircloud:functions/acting-node@1.0.0"; // A nightly cron declared at /crm/companies, one client at a time. for (const company of companies) { narrow(company.org_path); // every read and write that follows acts at that client's node // … } ``` Moving between nodes inside the entry node's subtree is not widening, so a sweep declared once at a parent narrows to each child in turn. ## Managing the tree from a function A function manages nodes, members, profiles, groups, denies and workspaces through the platform's tree capabilities, with the authority of its acting node, and only through the operations it declares: ```toml [org] operations = ["nodes.create", "members.invite"] ``` `nodes.create` takes `{org_path, display_name?, seed?}`: a `display_name` names the new node for people (1 to 100 characters, no control character; `org_node_name_invalid` otherwise), and `nodes.list` answers each node's `display_name`, and the acting node's as `at_display_name`, when it has one. Each name is an area and one of its operations — `nodes.create`, `nodes.archive`, `members.invite`, `members.update`, `workspaces.create` and so on, one per operation the tree capabilities offer; `aircloud check` refuses a name that is not one of them (`org.operation_unknown`), and an undeclared call is refused at run time (`capability.forbidden`). A cron declared at `/` manages the whole tree; one declared at `/acme` manages only below `/acme`. A session function manages members with the end user's own authority, never more. ## Archived nodes When a node is archived (`aircloud org nodes archive`), everything acting at it or below stops: sessions, public routes, events, crons and direct invokes are refused with `org_node_archived` until you unarchive it. ## Elevated aggregates An owned entity keeps each customer's records to that customer. Some answers need a number across all of them — "1 seat left", "you are 2nd on the waitlist" — without showing anyone else's record. A session function declares exactly the aggregate it needs in `function.toml`: ```toml [[elevate]] entity = "booking" ops = ["count", "sum", "max"] filter = ["slot"] group_by = ["status"] fields = ["seats"] scope = "subtree" ``` | key | what it says | |---|---| | `entity` | the owned entity whose owner scope the aggregate lifts | | `ops` | the aggregate functions it may run: `count`, `sum`, `avg`, `min`, `max` | | `filter` | the fields it may filter on | | `group_by` | the fields it may group by | | `fields` | the fields `sum`, `avg`, `min` and `max` may aggregate | | `scope` | `node`: the records stored at the acting node; `subtree`: the acting node and below | The function calls `aggregate-elevated` of the `flex-query` capability. That one aggregate runs over every owner's records; every other read of the same invocation stays owner-scoped, and the tree, denies and field policy still apply. **No record and no owner can ever be named**: the record id, the entity's owner field and its unique fields are refused in every role (`elevate.field_identifier`), so the answer is a number over records nobody can single out. A call outside the declaration is refused (`capability.forbidden`), each use is recorded, and it is metered. `aircloud check` also refuses an unknown entity (`elevate.entity_missing`), operation (`elevate.op_invalid`), field (`elevate.field_unknown`) or scope (`elevate.scope_invalid`). A function with no end user is not owner-scoped to begin with, so an elevation declared for it lifts nothing. --- Rendered from aircloud 0.1.12 at commit d84a0ce6dae53629277f656d96d81549df36807e.