prefect server in zig
at main 369 lines 12 kB view raw
1const std = @import("std"); 2const zap = @import("zap"); 3const db = @import("../db/sqlite.zig"); 4const json_util = @import("../utilities/json.zig"); 5const uuid_util = @import("../utilities/uuid.zig"); 6const time_util = @import("../utilities/time.zig"); 7const logging = @import("../logging.zig"); 8 9/// Serialize a std.json.Value to a string using Stringify 10fn serializeJsonValue(alloc: std.mem.Allocator, value: std.json.Value) ![]const u8 { 11 var output: std.io.Writer.Allocating = .init(alloc); 12 errdefer output.deinit(); 13 var jw: std.json.Stringify = .{ .writer = &output.writer }; 14 15 try jw.write(value); 16 17 return output.toOwnedSlice(); 18} 19 20pub fn handle(r: zap.Request) !void { 21 const target = r.path orelse "/"; 22 const method = r.method orelse "GET"; 23 24 // POST /automations/filter - list/filter automations 25 if (std.mem.endsWith(u8, target, "/filter") and std.mem.eql(u8, method, "POST")) { 26 try filterAutomations(r); 27 return; 28 } 29 30 // POST /automations/count - count automations 31 if (std.mem.endsWith(u8, target, "/count") and std.mem.eql(u8, method, "POST")) { 32 try countAutomations(r); 33 return; 34 } 35 36 // Extract ID from path for single-resource operations 37 const id = extractId(target); 38 39 if (id) |automation_id| { 40 // GET /automations/{id} - get single automation 41 if (std.mem.eql(u8, method, "GET")) { 42 try getAutomation(r, automation_id); 43 return; 44 } 45 // DELETE /automations/{id} - delete automation 46 if (std.mem.eql(u8, method, "DELETE")) { 47 try deleteAutomation(r, automation_id); 48 return; 49 } 50 // PATCH /automations/{id} - partial update (enabled only) 51 if (std.mem.eql(u8, method, "PATCH")) { 52 try patchAutomation(r, automation_id); 53 return; 54 } 55 } 56 57 // POST /automations/ - create automation 58 if (std.mem.eql(u8, method, "POST")) { 59 try createAutomation(r); 60 return; 61 } 62 63 json_util.sendStatus(r, "{\"detail\":\"not found\"}", .not_found); 64} 65 66fn extractId(target: []const u8) ?[]const u8 { 67 // Handle /api/automations/{id} or /automations/{id} 68 const prefix1 = "/api/automations/"; 69 const prefix2 = "/automations/"; 70 71 var rest: []const u8 = undefined; 72 if (std.mem.startsWith(u8, target, prefix1)) { 73 rest = target[prefix1.len..]; 74 } else if (std.mem.startsWith(u8, target, prefix2)) { 75 rest = target[prefix2.len..]; 76 } else { 77 return null; 78 } 79 80 // Check if it's an ID (not a sub-path like "filter" or "count") 81 if (rest.len == 0) return null; 82 if (std.mem.eql(u8, rest, "filter") or std.mem.eql(u8, rest, "count")) return null; 83 84 // Remove trailing slash if present 85 if (rest.len > 0 and rest[rest.len - 1] == '/') { 86 rest = rest[0 .. rest.len - 1]; 87 } 88 89 return if (rest.len > 0) rest else null; 90} 91 92fn createAutomation(r: zap.Request) !void { 93 const body = r.body orelse { 94 json_util.sendStatus(r, "{\"detail\":\"missing body\"}", .bad_request); 95 return; 96 }; 97 98 var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); 99 defer arena.deinit(); 100 const alloc = arena.allocator(); 101 102 const parsed = std.json.parseFromSlice(std.json.Value, alloc, body, .{}) catch { 103 json_util.sendStatus(r, "{\"detail\":\"invalid json\"}", .bad_request); 104 return; 105 }; 106 107 if (parsed.value != .object) { 108 json_util.sendStatus(r, "{\"detail\":\"expected object\"}", .bad_request); 109 return; 110 } 111 112 const obj = parsed.value.object; 113 114 // Required fields 115 const name = if (obj.get("name")) |v| (if (v == .string) v.string else null) else null; 116 const trigger = obj.get("trigger"); 117 const actions = obj.get("actions"); 118 119 if (name == null or trigger == null or actions == null) { 120 json_util.sendStatus(r, "{\"detail\":\"name, trigger, and actions are required\"}", .bad_request); 121 return; 122 } 123 124 // Optional fields 125 const description = if (obj.get("description")) |v| (if (v == .string) v.string else "") else ""; 126 const enabled = if (obj.get("enabled")) |v| (if (v == .bool) v.bool else true) else true; 127 128 // Serialize JSON fields 129 const tags_json = if (obj.get("tags")) |v| 130 serializeJsonValue(alloc, v) catch "[]" 131 else 132 "[]"; 133 const trigger_json = serializeJsonValue(alloc, trigger.?) catch { 134 json_util.sendStatus(r, "{\"detail\":\"invalid trigger\"}", .bad_request); 135 return; 136 }; 137 const actions_json = serializeJsonValue(alloc, actions.?) catch { 138 json_util.sendStatus(r, "{\"detail\":\"invalid actions\"}", .bad_request); 139 return; 140 }; 141 const actions_on_trigger_json = if (obj.get("actions_on_trigger")) |v| 142 serializeJsonValue(alloc, v) catch "[]" 143 else 144 "[]"; 145 const actions_on_resolve_json = if (obj.get("actions_on_resolve")) |v| 146 serializeJsonValue(alloc, v) catch "[]" 147 else 148 "[]"; 149 150 // Generate ID and timestamp 151 var id_buf: [36]u8 = undefined; 152 const id = uuid_util.generate(&id_buf); 153 var ts_buf: [32]u8 = undefined; 154 const created = time_util.timestamp(&ts_buf); 155 156 db.automations.insert( 157 id, 158 name.?, 159 description, 160 enabled, 161 tags_json, 162 trigger_json, 163 actions_json, 164 actions_on_trigger_json, 165 actions_on_resolve_json, 166 created, 167 ) catch { 168 json_util.sendStatus(r, "{\"detail\":\"insert failed\"}", .internal_server_error); 169 return; 170 }; 171 172 // Fetch and return created automation 173 const row = db.automations.getById(alloc, id) catch { 174 json_util.sendStatus(r, "{\"detail\":\"fetch failed\"}", .internal_server_error); 175 return; 176 }; 177 178 if (row) |automation| { 179 const resp = buildAutomationJson(alloc, automation) catch { 180 json_util.sendStatus(r, "{\"detail\":\"serialize error\"}", .internal_server_error); 181 return; 182 }; 183 json_util.sendStatus(r, resp, .created); 184 } else { 185 json_util.sendStatus(r, "{\"detail\":\"automation not found after insert\"}", .internal_server_error); 186 } 187} 188 189fn getAutomation(r: zap.Request, id: []const u8) !void { 190 var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); 191 defer arena.deinit(); 192 const alloc = arena.allocator(); 193 194 const row = db.automations.getById(alloc, id) catch { 195 json_util.sendStatus(r, "{\"detail\":\"database error\"}", .internal_server_error); 196 return; 197 }; 198 199 if (row) |automation| { 200 const resp = buildAutomationJson(alloc, automation) catch { 201 json_util.sendStatus(r, "{\"detail\":\"serialize error\"}", .internal_server_error); 202 return; 203 }; 204 json_util.sendStatus(r, resp, .ok); 205 } else { 206 json_util.sendStatus(r, "{\"detail\":\"automation not found\"}", .not_found); 207 } 208} 209 210fn deleteAutomation(r: zap.Request, id: []const u8) !void { 211 const deleted = db.automations.deleteById(id) catch { 212 json_util.sendStatus(r, "{\"detail\":\"database error\"}", .internal_server_error); 213 return; 214 }; 215 216 if (deleted) { 217 json_util.sendStatus(r, "", .no_content); 218 } else { 219 json_util.sendStatus(r, "{\"detail\":\"automation not found\"}", .not_found); 220 } 221} 222 223fn patchAutomation(r: zap.Request, id: []const u8) !void { 224 const body = r.body orelse { 225 json_util.sendStatus(r, "{\"detail\":\"missing body\"}", .bad_request); 226 return; 227 }; 228 229 var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); 230 defer arena.deinit(); 231 const alloc = arena.allocator(); 232 233 const parsed = std.json.parseFromSlice(std.json.Value, alloc, body, .{}) catch { 234 json_util.sendStatus(r, "{\"detail\":\"invalid json\"}", .bad_request); 235 return; 236 }; 237 238 if (parsed.value != .object) { 239 json_util.sendStatus(r, "{\"detail\":\"expected object\"}", .bad_request); 240 return; 241 } 242 243 const obj = parsed.value.object; 244 245 // Only enabled field is supported for PATCH 246 const enabled = if (obj.get("enabled")) |v| (if (v == .bool) v.bool else null) else null; 247 248 if (enabled == null) { 249 json_util.sendStatus(r, "{\"detail\":\"enabled field required\"}", .bad_request); 250 return; 251 } 252 253 var ts_buf: [32]u8 = undefined; 254 const updated = time_util.timestamp(&ts_buf); 255 256 const success = db.automations.updateEnabled(id, enabled.?, updated) catch { 257 json_util.sendStatus(r, "{\"detail\":\"database error\"}", .internal_server_error); 258 return; 259 }; 260 261 if (success) { 262 json_util.sendStatus(r, "", .no_content); 263 } else { 264 json_util.sendStatus(r, "{\"detail\":\"automation not found\"}", .not_found); 265 } 266} 267 268fn filterAutomations(r: zap.Request) !void { 269 var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); 270 defer arena.deinit(); 271 const alloc = arena.allocator(); 272 273 // Parse body for limit/offset 274 var limit: usize = 200; 275 var offset: usize = 0; 276 277 if (r.body) |body| { 278 const parsed = std.json.parseFromSlice(std.json.Value, alloc, body, .{}) catch null; 279 if (parsed) |p| { 280 if (p.value == .object) { 281 const obj = p.value.object; 282 if (obj.get("limit")) |v| { 283 if (v == .integer) limit = @intCast(@max(1, @min(10000, v.integer))); 284 } 285 if (obj.get("offset")) |v| { 286 if (v == .integer) offset = @intCast(@max(0, v.integer)); 287 } 288 } 289 } 290 } 291 292 const rows = db.automations.list(alloc, limit, offset) catch { 293 json_util.sendStatus(r, "{\"detail\":\"database error\"}", .internal_server_error); 294 return; 295 }; 296 297 // Build JSON array 298 var json_buf = std.ArrayListUnmanaged(u8){}; 299 const writer = json_buf.writer(alloc); 300 301 try writer.writeByte('['); 302 for (rows, 0..) |automation, i| { 303 if (i > 0) try writer.writeByte(','); 304 const item_json = buildAutomationJson(alloc, automation) catch continue; 305 try writer.writeAll(item_json); 306 } 307 try writer.writeByte(']'); 308 309 json_util.sendStatus(r, json_buf.items, .ok); 310} 311 312fn countAutomations(r: zap.Request) !void { 313 const cnt = db.automations.count() catch 0; 314 315 var buf: [32]u8 = undefined; 316 const count_str = std.fmt.bufPrint(&buf, "{d}", .{cnt}) catch "0"; 317 318 json_util.sendStatus(r, count_str, .ok); 319} 320 321fn buildAutomationJson(alloc: std.mem.Allocator, automation: db.automations.AutomationRow) ![]const u8 { 322 var output: std.io.Writer.Allocating = .init(alloc); 323 errdefer output.deinit(); 324 var jw: std.json.Stringify = .{ .writer = &output.writer }; 325 326 try jw.beginObject(); 327 try jw.objectField("id"); 328 try jw.write(automation.id); 329 try jw.objectField("created"); 330 try jw.write(automation.created); 331 try jw.objectField("updated"); 332 try jw.write(automation.updated); 333 try jw.objectField("name"); 334 try jw.write(automation.name); 335 try jw.objectField("description"); 336 try jw.write(automation.description); 337 try jw.objectField("enabled"); 338 try jw.write(automation.enabled); 339 340 // raw JSON passthrough for pre-serialized fields 341 try jw.objectField("tags"); 342 try jw.beginWriteRaw(); 343 try jw.writer.writeAll(automation.tags); 344 jw.endWriteRaw(); 345 346 try jw.objectField("trigger"); 347 try jw.beginWriteRaw(); 348 try jw.writer.writeAll(automation.trigger); 349 jw.endWriteRaw(); 350 351 try jw.objectField("actions"); 352 try jw.beginWriteRaw(); 353 try jw.writer.writeAll(automation.actions); 354 jw.endWriteRaw(); 355 356 try jw.objectField("actions_on_trigger"); 357 try jw.beginWriteRaw(); 358 try jw.writer.writeAll(automation.actions_on_trigger); 359 jw.endWriteRaw(); 360 361 try jw.objectField("actions_on_resolve"); 362 try jw.beginWriteRaw(); 363 try jw.writer.writeAll(automation.actions_on_resolve); 364 jw.endWriteRaw(); 365 366 try jw.endObject(); 367 368 return output.toOwnedSlice(); 369}