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