const std = @import("std"); const zap = @import("zap"); const db = @import("../db/sqlite.zig"); const json_util = @import("../utilities/json.zig"); const uuid_util = @import("../utilities/uuid.zig"); const time_util = @import("../utilities/time.zig"); const logging = @import("../logging.zig"); /// Serialize a std.json.Value to a string using Stringify fn serializeJsonValue(alloc: std.mem.Allocator, value: std.json.Value) ![]const u8 { var output: std.io.Writer.Allocating = .init(alloc); errdefer output.deinit(); var jw: std.json.Stringify = .{ .writer = &output.writer }; try jw.write(value); return output.toOwnedSlice(); } pub fn handle(r: zap.Request) !void { const target = r.path orelse "/"; const method = r.method orelse "GET"; // POST /automations/filter - list/filter automations if (std.mem.endsWith(u8, target, "/filter") and std.mem.eql(u8, method, "POST")) { try filterAutomations(r); return; } // POST /automations/count - count automations if (std.mem.endsWith(u8, target, "/count") and std.mem.eql(u8, method, "POST")) { try countAutomations(r); return; } // Extract ID from path for single-resource operations const id = extractId(target); if (id) |automation_id| { // GET /automations/{id} - get single automation if (std.mem.eql(u8, method, "GET")) { try getAutomation(r, automation_id); return; } // DELETE /automations/{id} - delete automation if (std.mem.eql(u8, method, "DELETE")) { try deleteAutomation(r, automation_id); return; } // PATCH /automations/{id} - partial update (enabled only) if (std.mem.eql(u8, method, "PATCH")) { try patchAutomation(r, automation_id); return; } } // POST /automations/ - create automation if (std.mem.eql(u8, method, "POST")) { try createAutomation(r); return; } json_util.sendStatus(r, "{\"detail\":\"not found\"}", .not_found); } fn extractId(target: []const u8) ?[]const u8 { // Handle /api/automations/{id} or /automations/{id} const prefix1 = "/api/automations/"; const prefix2 = "/automations/"; var rest: []const u8 = undefined; if (std.mem.startsWith(u8, target, prefix1)) { rest = target[prefix1.len..]; } else if (std.mem.startsWith(u8, target, prefix2)) { rest = target[prefix2.len..]; } else { return null; } // Check if it's an ID (not a sub-path like "filter" or "count") if (rest.len == 0) return null; if (std.mem.eql(u8, rest, "filter") or std.mem.eql(u8, rest, "count")) return null; // Remove trailing slash if present if (rest.len > 0 and rest[rest.len - 1] == '/') { rest = rest[0 .. rest.len - 1]; } return if (rest.len > 0) rest else null; } fn createAutomation(r: zap.Request) !void { const body = r.body orelse { json_util.sendStatus(r, "{\"detail\":\"missing body\"}", .bad_request); return; }; var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); const alloc = arena.allocator(); const parsed = std.json.parseFromSlice(std.json.Value, alloc, body, .{}) catch { json_util.sendStatus(r, "{\"detail\":\"invalid json\"}", .bad_request); return; }; if (parsed.value != .object) { json_util.sendStatus(r, "{\"detail\":\"expected object\"}", .bad_request); return; } const obj = parsed.value.object; // Required fields const name = if (obj.get("name")) |v| (if (v == .string) v.string else null) else null; const trigger = obj.get("trigger"); const actions = obj.get("actions"); if (name == null or trigger == null or actions == null) { json_util.sendStatus(r, "{\"detail\":\"name, trigger, and actions are required\"}", .bad_request); return; } // Optional fields const description = if (obj.get("description")) |v| (if (v == .string) v.string else "") else ""; const enabled = if (obj.get("enabled")) |v| (if (v == .bool) v.bool else true) else true; // Serialize JSON fields const tags_json = if (obj.get("tags")) |v| serializeJsonValue(alloc, v) catch "[]" else "[]"; const trigger_json = serializeJsonValue(alloc, trigger.?) catch { json_util.sendStatus(r, "{\"detail\":\"invalid trigger\"}", .bad_request); return; }; const actions_json = serializeJsonValue(alloc, actions.?) catch { json_util.sendStatus(r, "{\"detail\":\"invalid actions\"}", .bad_request); return; }; const actions_on_trigger_json = if (obj.get("actions_on_trigger")) |v| serializeJsonValue(alloc, v) catch "[]" else "[]"; const actions_on_resolve_json = if (obj.get("actions_on_resolve")) |v| serializeJsonValue(alloc, v) catch "[]" else "[]"; // Generate ID and timestamp var id_buf: [36]u8 = undefined; const id = uuid_util.generate(&id_buf); var ts_buf: [32]u8 = undefined; const created = time_util.timestamp(&ts_buf); db.automations.insert( id, name.?, description, enabled, tags_json, trigger_json, actions_json, actions_on_trigger_json, actions_on_resolve_json, created, ) catch { json_util.sendStatus(r, "{\"detail\":\"insert failed\"}", .internal_server_error); return; }; // Fetch and return created automation const row = db.automations.getById(alloc, id) catch { json_util.sendStatus(r, "{\"detail\":\"fetch failed\"}", .internal_server_error); return; }; if (row) |automation| { const resp = buildAutomationJson(alloc, automation) catch { json_util.sendStatus(r, "{\"detail\":\"serialize error\"}", .internal_server_error); return; }; json_util.sendStatus(r, resp, .created); } else { json_util.sendStatus(r, "{\"detail\":\"automation not found after insert\"}", .internal_server_error); } } fn getAutomation(r: zap.Request, id: []const u8) !void { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); const alloc = arena.allocator(); const row = db.automations.getById(alloc, id) catch { json_util.sendStatus(r, "{\"detail\":\"database error\"}", .internal_server_error); return; }; if (row) |automation| { const resp = buildAutomationJson(alloc, automation) catch { json_util.sendStatus(r, "{\"detail\":\"serialize error\"}", .internal_server_error); return; }; json_util.sendStatus(r, resp, .ok); } else { json_util.sendStatus(r, "{\"detail\":\"automation not found\"}", .not_found); } } fn deleteAutomation(r: zap.Request, id: []const u8) !void { const deleted = db.automations.deleteById(id) catch { json_util.sendStatus(r, "{\"detail\":\"database error\"}", .internal_server_error); return; }; if (deleted) { json_util.sendStatus(r, "", .no_content); } else { json_util.sendStatus(r, "{\"detail\":\"automation not found\"}", .not_found); } } fn patchAutomation(r: zap.Request, id: []const u8) !void { const body = r.body orelse { json_util.sendStatus(r, "{\"detail\":\"missing body\"}", .bad_request); return; }; var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); const alloc = arena.allocator(); const parsed = std.json.parseFromSlice(std.json.Value, alloc, body, .{}) catch { json_util.sendStatus(r, "{\"detail\":\"invalid json\"}", .bad_request); return; }; if (parsed.value != .object) { json_util.sendStatus(r, "{\"detail\":\"expected object\"}", .bad_request); return; } const obj = parsed.value.object; // Only enabled field is supported for PATCH const enabled = if (obj.get("enabled")) |v| (if (v == .bool) v.bool else null) else null; if (enabled == null) { json_util.sendStatus(r, "{\"detail\":\"enabled field required\"}", .bad_request); return; } var ts_buf: [32]u8 = undefined; const updated = time_util.timestamp(&ts_buf); const success = db.automations.updateEnabled(id, enabled.?, updated) catch { json_util.sendStatus(r, "{\"detail\":\"database error\"}", .internal_server_error); return; }; if (success) { json_util.sendStatus(r, "", .no_content); } else { json_util.sendStatus(r, "{\"detail\":\"automation not found\"}", .not_found); } } fn filterAutomations(r: zap.Request) !void { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); const alloc = arena.allocator(); // Parse body for limit/offset var limit: usize = 200; var offset: usize = 0; if (r.body) |body| { const parsed = std.json.parseFromSlice(std.json.Value, alloc, body, .{}) catch null; if (parsed) |p| { if (p.value == .object) { const obj = p.value.object; if (obj.get("limit")) |v| { if (v == .integer) limit = @intCast(@max(1, @min(10000, v.integer))); } if (obj.get("offset")) |v| { if (v == .integer) offset = @intCast(@max(0, v.integer)); } } } } const rows = db.automations.list(alloc, limit, offset) catch { json_util.sendStatus(r, "{\"detail\":\"database error\"}", .internal_server_error); return; }; // Build JSON array var json_buf = std.ArrayListUnmanaged(u8){}; const writer = json_buf.writer(alloc); try writer.writeByte('['); for (rows, 0..) |automation, i| { if (i > 0) try writer.writeByte(','); const item_json = buildAutomationJson(alloc, automation) catch continue; try writer.writeAll(item_json); } try writer.writeByte(']'); json_util.sendStatus(r, json_buf.items, .ok); } fn countAutomations(r: zap.Request) !void { const cnt = db.automations.count() catch 0; var buf: [32]u8 = undefined; const count_str = std.fmt.bufPrint(&buf, "{d}", .{cnt}) catch "0"; json_util.sendStatus(r, count_str, .ok); } fn buildAutomationJson(alloc: std.mem.Allocator, automation: db.automations.AutomationRow) ![]const u8 { var output: std.io.Writer.Allocating = .init(alloc); errdefer output.deinit(); var jw: std.json.Stringify = .{ .writer = &output.writer }; try jw.beginObject(); try jw.objectField("id"); try jw.write(automation.id); try jw.objectField("created"); try jw.write(automation.created); try jw.objectField("updated"); try jw.write(automation.updated); try jw.objectField("name"); try jw.write(automation.name); try jw.objectField("description"); try jw.write(automation.description); try jw.objectField("enabled"); try jw.write(automation.enabled); // raw JSON passthrough for pre-serialized fields try jw.objectField("tags"); try jw.beginWriteRaw(); try jw.writer.writeAll(automation.tags); jw.endWriteRaw(); try jw.objectField("trigger"); try jw.beginWriteRaw(); try jw.writer.writeAll(automation.trigger); jw.endWriteRaw(); try jw.objectField("actions"); try jw.beginWriteRaw(); try jw.writer.writeAll(automation.actions); jw.endWriteRaw(); try jw.objectField("actions_on_trigger"); try jw.beginWriteRaw(); try jw.writer.writeAll(automation.actions_on_trigger); jw.endWriteRaw(); try jw.objectField("actions_on_resolve"); try jw.beginWriteRaw(); try jw.writer.writeAll(automation.actions_on_resolve); jw.endWriteRaw(); try jw.endObject(); return output.toOwnedSlice(); }