Skip to content

Actions: full CRUD management commands for the CLI - #43

Open
mateuscardosodeveloper wants to merge 5 commits into
masterfrom
feat/action-management-commands
Open

mateuscardosodeveloper wants to merge 5 commits into
masterfrom
feat/action-management-commands

Conversation

@mateuscardosodeveloper

Copy link
Copy Markdown
Collaborator

Summary

Adds a complete action-* command family so an Action's full lifecycle is drivable from tagoio, following the conventions PR #41 (devices) and PR #39 (files) established. Actions could previously only be created or edited through the web admin, which broke any scripted workflow that provisions automation alongside the devices and analyses it already provisions.

Seven commands: action-list, action-info, action-create, action-edit, action-enable, action-disable, action-delete. All carry the family conventions: --json with parseable error codes, --silent for non-interactive callers, an interactive picker when the ID is omitted, and confirmation on the destructive path.

Trigger and target coverage

action-create exposes typed flags for six of the seven trigger families and the four common targets. --trigger-json and --action-json cover the shapes flat flags cannot express: condition_geofence, whose value is a nested geometry object, plus targets like queue-sqs and the Twilio and SendGrid variants.

Two shapes are reachable only through --trigger-json, and both are documented in --help with a worked example: targeting every device carrying a tag instead of one device id, and pairing a firing trigger with an unlock trigger.

API constraints encoded

Three rules live in action-builders.ts that are absent from the SDK types and from the docs. They were taken from profile/export/services/actions-export.ts, where the export path learned them, and each is now verified against the live API:

  • an empty trigger value must omit the key rather than send ""
  • an empty second_value must omit the key
  • a resource (tag_key) trigger must never carry unlock

Without these, action-create fails in production with an opaque API error that no mocked unit test would catch.

Notes on the SDK

actions.create resolves { action: "<id>" }, not the { device_id } shape devices.create uses; reading the wrong key yields undefined ids in --json. The SDK also types last_triggered as ExpireTimeOption ("never" | Date) and models no mqtt_topic trigger member, so both are handled explicitly rather than cast away.

Test plan

  • 920 unit tests pass (npm test); commands/actions at 97.6% statements, 90.2% branch, 100% functions
  • npx tsc --noEmit clean, npm run linter 0 errors, oxfmt --check clean
  • Man snapshot regenerated; diff reviewed as purely additive (seven new .SS entries, no removals, no Header leak). Regression guard extended with action-list
  • 73 functional cases against a live profile, all seven trigger families and all five targets created and read back
  • Live: empty --trigger-value accepted by the API (normalization rule 1); resource trigger stored without unlock (rule 3)
  • Live: --header "Authorization=Bearer abc=123" round-trips with the inner = intact
  • Live: action-info --json output feeds back into action-create --trigger-json unmodified
  • Live: action-delete cancel leaves the action and exits 0; -y and --silent remove it
  • Live: action-disable then action-enable round-trip, sending only { active }
  • Manual: 16 actions inspected in the web admin to confirm each trigger and target renders as configured
  • All test actions removed; the profile is back to its original two actions

Risk (CIA)

Likelihood: 🟢 Low | Impact: 🟡 Medium | Exposure: 🟢 Low

Warning

Impact is Medium because action-delete permanently removes an automation and action-edit replaces trigger and target as whole values. Deletion confirms unless -y or --silent is explicit, and a declined prompt exits 0 without calling the API. The whole-value replacement is deliberate: the API overwrites those fields rather than merging them, so a partial edit would silently drop fields the CLI does not model. Every other command in the family is additive or read-only.

Interactive autocomplete over the profile's actions, mirroring
pickEntityIDFromTagoIO so the action-* commands share the prompt UX of
the device and entity families.

Requests 200 items, the Scale plan ceiling for Actions, so the list can
never be silently truncated.
Pure flag-to-payload construction shared by action-create and
action-edit: six trigger families through typed flags, plus
--trigger-json and --action-json for the shapes flat flags cannot
express.

Encodes three API constraints that are absent from the SDK types and
from the docs, taken from profile/export/services/actions-export.ts:

  - an empty trigger value must omit the key rather than send ""
  - an empty second_value must omit the key
  - a resource (tag_key) trigger must never carry unlock

Rejects --trigger-unlock as the only trigger: unlock marks the condition
that re-arms an action, so on its own it leaves nothing to fire on. The
API accepts that payload but the web UI then renders an empty
variable/value row.

The SDK types 'value' as required on the condition-trigger member and
models no mqtt_topic member at all, so both assembly sites need a cast.
Read-only surface: list with name, tag and active filters, and info
rendering the nested trigger and action as indented JSON.

Both normalize last_triggered through mapLastTriggered. The SDK types it
as ExpireTimeOption ("never" | Date) and the API really does return the
string, which mapDate would call toLocaleDateString on. The field is also
absent for actions that never fired, so it is normalized to "never"
rather than dropped from --json output.

action-info renders its human view entirely on stderr. console.table
writes to stdout, which would pollute the stream reserved for --json.
action-list keeps console.table because there the table is the command's
data output, matching device-list and entity-list.

Tag filtering is built locally instead of reusing repeatableTags from
device-list: that helper is private, typed to DeviceQuery, and loops to
<= maxRows, which appends a trailing empty tag entry.
Completes the action lifecycle. Validation lives in the builders, so an
invalid invocation fails before any request.

action-create reads the new id from the SDK's { action } response rather
than { id }: actions.create does not follow the { device_id } shape
devices.create uses, and reading the wrong key yields undefined ids in
--json output.

action-edit replaces trigger and action as whole values, because the API
overwrites those fields rather than merging them; a partial edit would
silently drop whatever the CLI does not model. action-enable and
action-disable reuse its applyActionEdit so every edit-shaped command
goes through one path, and send only { active }.

action-delete confirms unless -y or --silent, and a declined prompt
leaves the action untouched and exits 0.
Wires the family into the program, which also puts it in the man page
through buildProgram. Regenerates the snapshot and extends the
command-surface regression guard with action-list.

The --help text carries the two behaviours that are invisible from the
flags alone: how value_type is inferred, and that trigger and target are
replaced whole on edit, with the action-info --json round-trip spelled
out. It also documents the two shapes only --trigger-json can express,
targeting devices by tag and pairing a firing trigger with an unlock.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant