prefect server in zig
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}