local worker_mod = require("assay.engine.workflow.worker")
local M = {}
local function trim_slash(s) return (s or ""):gsub("/+$", "") end
local function url_encode(s)
return (tostring(s):gsub("([^A-Za-z0-9%-_.~])", function(ch)
return string.format("%%%02X", string.byte(ch))
end))
end
function M.client(opts)
opts = opts or {}
local engine_url = trim_slash(opts.engine_url or env.get("ASSAY_ENGINE_URL") or "")
if engine_url == "" then
error("assay.engine.workflow: engine_url required (or set ASSAY_ENGINE_URL)")
end
local api_key = opts.api_key or env.get("ASSAY_ADMIN_KEY")
local session_cookie = opts.session_cookie
local function build_headers()
local h = { ["Content-Type"] = "application/json" }
if api_key and api_key ~= "" then
h["Authorization"] = "Bearer " .. api_key
end
if session_cookie and session_cookie ~= "" then
h["Cookie"] = "assay_session=" .. session_cookie
end
return h
end
local BASE = "/api/v1/engine/workflow"
local function api_call(method, path, body)
local url = engine_url .. BASE .. path
local opts2 = { headers = build_headers() }
if method == "GET" then return http.get(url, opts2)
elseif method == "POST" then return http.post(url, body, opts2)
elseif method == "PATCH" then return http.patch(url, body, opts2)
elseif method == "PUT" then return http.put(url, body, opts2)
elseif method == "DELETE" then return http.delete(url, opts2)
else error("engine.workflow: unsupported method: " .. method)
end
end
local client = {
_engine_url = engine_url,
_api = api_call,
_workflows = {},
_activities = {},
_worker_id = nil,
}
local function expect(resp, status, fn_name)
if type(status) == "table" then
for _, s in ipairs(status) do
if resp.status == s then return end
end
error(fn_name .. ": HTTP " .. resp.status .. ": " .. (resp.body or ""))
end
if resp.status ~= status then
error(fn_name .. ": HTTP " .. resp.status .. ": " .. (resp.body or ""))
end
end
function client:start(start_opts)
local body = {
workflow_type = start_opts.workflow_type,
workflow_id = start_opts.workflow_id,
namespace = start_opts.namespace,
input = start_opts.input,
task_queue = start_opts.task_queue or "default",
search_attributes = start_opts.search_attributes,
}
local resp = api_call("POST", "/workflows", body)
expect(resp, 201, "engine.workflow.start")
return json.parse(resp.body)
end
function client:signal(workflow_id, signal_name, payload)
local body = payload and { payload = payload } or {}
local resp = api_call("POST",
"/workflows/" .. url_encode(workflow_id) .. "/signal/" .. url_encode(signal_name), body)
expect(resp, 200, "engine.workflow.signal")
end
function client:describe(workflow_id)
local resp = api_call("GET", "/workflows/" .. url_encode(workflow_id))
expect(resp, 200, "engine.workflow.describe")
return json.parse(resp.body)
end
function client:cancel(workflow_id)
local resp = api_call("POST", "/workflows/" .. url_encode(workflow_id) .. "/cancel")
expect(resp, 200, "engine.workflow.cancel")
end
function client:terminate(workflow_id, reason)
local body = reason and { reason = reason } or {}
local resp = api_call("POST",
"/workflows/" .. url_encode(workflow_id) .. "/terminate", body)
expect(resp, 200, "engine.workflow.terminate")
end
function client:list(qopts)
qopts = qopts or {}
local parts = {}
if qopts.namespace then parts[#parts + 1] = "namespace=" .. url_encode(qopts.namespace) end
if qopts.status then parts[#parts + 1] = "status=" .. url_encode(qopts.status) end
if qopts.type then parts[#parts + 1] = "type=" .. url_encode(qopts.type) end
if qopts.search_attrs then
parts[#parts + 1] = "search_attrs=" .. url_encode(json.encode(qopts.search_attrs))
end
if qopts.limit then parts[#parts + 1] = "limit=" .. tostring(qopts.limit) end
if qopts.offset then parts[#parts + 1] = "offset=" .. tostring(qopts.offset) end
local path = "/workflows"
if #parts > 0 then path = path .. "?" .. table.concat(parts, "&") end
local resp = api_call("GET", path)
expect(resp, 200, "engine.workflow.list")
return json.parse(resp.body)
end
function client:get_events(workflow_id)
local resp = api_call("GET", "/workflows/" .. url_encode(workflow_id) .. "/events")
expect(resp, 200, "engine.workflow.get_events")
return json.parse(resp.body)
end
function client:get_state(workflow_id, name)
local path = "/workflows/" .. url_encode(workflow_id) .. "/state"
if name then path = path .. "/" .. url_encode(name) end
local resp = api_call("GET", path)
if resp.status == 404 then return nil end
expect(resp, 200, "engine.workflow.get_state")
return json.parse(resp.body)
end
function client:list_children(workflow_id)
local resp = api_call("GET", "/workflows/" .. url_encode(workflow_id) .. "/children")
expect(resp, 200, "engine.workflow.list_children")
return json.parse(resp.body)
end
function client:continue_as_new(workflow_id, input)
local resp = api_call("POST",
"/workflows/" .. url_encode(workflow_id) .. "/continue-as-new", { input = input })
expect(resp, 201, "engine.workflow.continue_as_new")
return json.parse(resp.body)
end
client.schedules = {}
function client.schedules:create(sopts)
if not sopts or not sopts.name or not sopts.workflow_type or not sopts.cron_expr then
error("engine.workflow.schedules.create: name, workflow_type, cron_expr required")
end
local resp = api_call("POST", "/schedules", sopts)
expect(resp, 201, "engine.workflow.schedules.create")
return json.parse(resp.body)
end
function client.schedules:list(sopts)
local ns = (sopts and sopts.namespace) or "main"
local resp = api_call("GET", "/schedules?namespace=" .. url_encode(ns))
expect(resp, 200, "engine.workflow.schedules.list")
return json.parse(resp.body)
end
function client.schedules:describe(name, sopts)
local ns = (sopts and sopts.namespace) or "main"
local resp = api_call("GET",
"/schedules/" .. url_encode(name) .. "?namespace=" .. url_encode(ns))
if resp.status == 404 then return nil end
expect(resp, 200, "engine.workflow.schedules.describe")
return json.parse(resp.body)
end
function client.schedules:patch(name, patch, sopts)
local ns = (sopts and sopts.namespace) or "main"
local resp = api_call("PATCH",
"/schedules/" .. url_encode(name) .. "?namespace=" .. url_encode(ns), patch or {})
expect(resp, 200, "engine.workflow.schedules.patch")
return json.parse(resp.body)
end
function client.schedules:pause(name, sopts)
local ns = (sopts and sopts.namespace) or "main"
local resp = api_call("POST",
"/schedules/" .. url_encode(name) .. "/pause?namespace=" .. url_encode(ns))
expect(resp, 200, "engine.workflow.schedules.pause")
return json.parse(resp.body)
end
function client.schedules:resume(name, sopts)
local ns = (sopts and sopts.namespace) or "main"
local resp = api_call("POST",
"/schedules/" .. url_encode(name) .. "/resume?namespace=" .. url_encode(ns))
expect(resp, 200, "engine.workflow.schedules.resume")
return json.parse(resp.body)
end
function client.schedules:delete(name, sopts)
local ns = (sopts and sopts.namespace) or "main"
local resp = api_call("DELETE",
"/schedules/" .. url_encode(name) .. "?namespace=" .. url_encode(ns))
expect(resp, 200, "engine.workflow.schedules.delete")
end
client.namespaces = {}
function client.namespaces:create(name)
local resp = api_call("POST", "/namespaces", { name = name })
expect(resp, { 200, 201 }, "engine.workflow.namespaces.create")
end
function client.namespaces:list()
local resp = api_call("GET", "/namespaces")
expect(resp, 200, "engine.workflow.namespaces.list")
return json.parse(resp.body)
end
function client.namespaces:stats(name)
local resp = api_call("GET", "/namespaces/" .. url_encode(name))
expect(resp, 200, "engine.workflow.namespaces.stats")
return json.parse(resp.body)
end
function client.namespaces:describe(name) return client.namespaces:stats(name) end
function client.namespaces:delete(name)
local resp = api_call("DELETE", "/namespaces/" .. url_encode(name))
expect(resp, 200, "engine.workflow.namespaces.delete")
end
client.workers = {}
function client.workers:list(wopts)
local ns = (wopts and wopts.namespace) or "main"
local resp = api_call("GET", "/workers?namespace=" .. url_encode(ns))
expect(resp, 200, "engine.workflow.workers.list")
return json.parse(resp.body)
end
client.queues = {}
function client.queues:stats(qopts)
local ns = (qopts and qopts.namespace) or "main"
local resp = api_call("GET", "/queues?namespace=" .. url_encode(ns))
expect(resp, 200, "engine.workflow.queues.stats")
return json.parse(resp.body)
end
function client:register_workflow(name, handler) self._workflows[name] = handler end
function client:register_activity(name, handler) self._activities[name] = handler end
function client:listen(lopts) return worker_mod.listen(self, lopts) end
return client
end
return M