scripting / scripting-5Markdown ↓

Calling CoCoCo

Normal and Integration scripts reach CoCoCo through the global ctx: run a GraphQL query, look up a device, read a config value, send a notification. The ctx reference lists every function.

Two results: ok and result

A function that has to wait for something, such as a database, a device or another service, returns two values. When ok is true, result is the value; when it is false, result is the error message. The reference shows these functions returning ok, result.

This is the most common mistake in a script: binding one value binds ok, not the data. Always write local ok, result = … and check ok before using result. Passing such a call straight into another function passes both values, so bind them first.

Functions that don't wait, such as ctx.log.info, return their value directly.

-- Service-reaching calls are async, so they return TWO values: an ok flag and
-- the result. Binding one value binds the FLAG, not the data.
local ok, result = ctx.graphql.query([[
  query ListCustomers($first: Int!) {
    listCustomers(first: $first) {
      edges { node { id name email isActive } }
      totalCount
    }
  }
]], { first = 5 })

if not ok then error(result) end

-- `ok` covers transport and authorization. GraphQL-level errors arrive inside
-- the payload, so they need their own check.
if result.errors then error(json.encode(result.errors)) end

-- `data` is optional and has to be bound before it is indexed. It is absent
-- when the request was rejected outright rather than partially failing — an
-- errors block whose path names a field can otherwise read like a problem with
-- that field, when nothing ran at all.
local data = result.data
if data == nil then error("GraphQL returned no data") end

local customers = {}
for _, edge in ipairs(data.listCustomers.edges) do
  table.insert(customers, { name = edge.node.name, email = edge.node.email })
end

return { total = data.listCustomers.totalCount, customers = customers }

GraphQL results are checked

When a query is written directly in the script, ctx.graphql.query is checked against CoCoCo's GraphQL schema before the script runs: the fields you select, the variables you pass and the shape of the result. The editor completes fields as you type. A GraphQL null arrives as nil, so check a field before you use it. Checking doesn't run the query or change who may run it: the query runs with the permissions of whoever, or whatever, runs the script.

Objects

Some functions return an object rather than data: ctx.device.get returns a device, ctx.template.get a template, ctx.ml.get a model. Getting the object doesn't look anything up yet, so a name that doesn't exist fails when you first call one of the object's methods, and it raises an error there rather than returning a false ok. Methods are called with a colon (printer:http(…)), and those that wait return ok, result like any other call.

-- ctx.device.get is a CONSTRUCTOR, not a service call: it is synchronous and
-- returns ONE value — the handle. There is no ok flag to bind, and binding two
-- names here would put the handle in the first and nil in the second.
--
-- Resolution is LAZY: building the handle does not look the device up, so an
-- unknown name fails later, when a method runs — and it raises there rather
-- than returning a false ok. Wrap the whole use if the name is not trusted.
local available, printer = pcall(function() return ctx.device.get("my-printer") end)
if not available then return { available = false } end

-- The handle's METHODS are the async half, and they take the handle as self —
-- so they are called with a colon. These do return (ok, result).
local ok, resp = printer:http({ method = "GET", path = "/api/status" })
if not ok then return { available = true, ok = false, error = resp } end

-- `id()` confirms which device the name actually bound to — async like the
-- rest, so it too returns two values.
local idOk, deviceId = printer:id()

-- The same handle also speaks SQL where the device exposes a database, so one
-- resolution serves both protocols — that is the point of holding a handle
-- rather than passing the name to every call.
local sqlOk, rows = printer:sql({ query = "SELECT 1", bindings = {} })

return {
  available = true,
  ok = true,
  status = resp.status,
  deviceId = idOk and deviceId or nil,
  sqlReachable = sqlOk,
}

Records a script keeps: ctx.store

ctx.store keeps named records that belong to whatever is running: a custom app's own records, or an integration's. Other scripts have no store. A script can't reach another app's records, whatever name it asks for. ctx.dataContainer is the older name for the same functions.

-- ctx.store belongs to whatever is running: here a custom app, so the records
-- are that app's. The owner is fixed when the run is built, so a script cannot
-- reach another app's records whatever name it asks for.
local name = "sync-state"

-- Every ctx.store call waits on the database, so each returns TWO values.
-- `put` answers with the stored RECORD (name, data, timestamps, …).
local ok, saved = ctx.store.put(name, { cursor = 42, label = "nightly" })
if not ok then error(saved) end

-- `get` returns that record, or nil when the name was never written. A missing
-- record is a SUCCESSFUL read: ok stays true and the value is nil, so test the
-- value — not the flag — to decide "missing".
local readOk, found = ctx.store.get(name)
if not readOk then error(found) end

local cursor = nil
if found ~= nil then cursor = found.data.cursor end

-- `delete` is safe to call without checking first: deleting a missing record
-- is not an error.
local deleteOk, problem = ctx.store.delete(name)
if not deleteOk then error(problem) end

-- Proving the round trip means reading back AFTER the delete too.
local afterOk, after = ctx.store.get(name)
if not afterOk then error(after) end

return {
  name = saved.name,
  cursor = cursor,
  goneAfterDelete = after == nil,
}

In an integration

An integration uses the same ctx names, narrowed to the integration: Integrations describes how.

Names or ids

ctx.device.get accepts a device's id (dev_…) or its name ("my-printer"). ctx.notify.send accepts a group id (ngr_…), a recipient id (nrc_…) or a group name. Looking something up by name checks your permissions, just as an id does.

Published platform manual · revision 6c9dff4f344a