const std = @import("std"); const zap = @import("zap"); const mem = std.mem; const json = std.json; const db = @import("../db/sqlite.zig"); const uuid_util = @import("../utilities/uuid.zig"); const time_util = @import("../utilities/time.zig"); fn sendJson(r: zap.Request, body: []const u8) void { r.setHeader("content-type", "application/json") catch {}; r.setHeader("access-control-allow-origin", "*") catch {}; r.setHeader("access-control-allow-methods", "GET, POST, PATCH, DELETE, OPTIONS") catch {}; r.setHeader("access-control-allow-headers", "content-type, x-prefect-api-version") catch {}; r.sendBody(body) catch {}; } fn sendJsonStatus(r: zap.Request, body: []const u8, status: zap.http.StatusCode) void { r.setStatus(status); sendJson(r, body); } // Routes: // POST /block_documents/ - create // GET /block_documents/{id} - read by id // PATCH /block_documents/{id} - update // DELETE /block_documents/{id} - delete // POST /block_documents/filter - list pub fn handle(r: zap.Request) !void { const target = r.path orelse "/"; const method = r.method orelse "GET"; // strip /api prefix if present const path = if (mem.startsWith(u8, target, "/api/block_documents")) target[4..] else target; // POST /block_documents/filter if (mem.eql(u8, method, "POST") and mem.endsWith(u8, path, "/filter")) { try filter(r); return; } // POST /block_documents/ - create if (mem.eql(u8, method, "POST") and (mem.eql(u8, path, "/block_documents/") or mem.eql(u8, path, "/block_documents"))) { try create(r); return; } // GET /block_documents/{id} or PATCH /block_documents/{id} or DELETE /block_documents/{id} if (mem.startsWith(u8, path, "/block_documents/")) { const id = path["/block_documents/".len..]; if (id.len >= 32) { if (mem.eql(u8, method, "GET")) { try getById(r, id); return; } else if (mem.eql(u8, method, "PATCH")) { try update(r, id); return; } else if (mem.eql(u8, method, "DELETE")) { try delete(r, id); return; } } } sendJsonStatus(r, "{\"detail\":\"not found\"}", .not_found); } fn create(r: zap.Request) !void { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); const alloc = arena.allocator(); const body = r.body orelse { sendJsonStatus(r, "{\"detail\":\"request body required\"}", .bad_request); return; }; const parsed = json.parseFromSlice(json.Value, alloc, body, .{}) catch { sendJsonStatus(r, "{\"detail\":\"invalid json\"}", .bad_request); return; }; const obj = parsed.value.object; const block_type_id = if (obj.get("block_type_id")) |v| v.string else { sendJsonStatus(r, "{\"detail\":\"block_type_id required\"}", .bad_request); return; }; const block_schema_id = if (obj.get("block_schema_id")) |v| v.string else { sendJsonStatus(r, "{\"detail\":\"block_schema_id required\"}", .bad_request); return; }; const name = if (obj.get("name")) |v| if (v == .string) v.string else null else null; const is_anonymous = if (obj.get("is_anonymous")) |v| v == .bool and v.bool else (name == null); // serialize data const data = blk: { if (obj.get("data")) |v| { var out: std.Io.Writer.Allocating = .init(alloc); var jw: json.Stringify = .{ .writer = &out.writer }; jw.write(v) catch break :blk "{}"; break :blk out.toOwnedSlice() catch "{}"; } else break :blk "{}"; }; // get block_type_name from block_type const bt = db.block_types.getById(alloc, block_type_id) catch null; const block_type_name = if (bt) |t| t.name else null; var id_buf: [36]u8 = undefined; const id = uuid_util.generate(&id_buf); db.block_documents.insert(id, name, data, is_anonymous, block_type_id, block_type_name, block_schema_id) catch |err| { if (err == error.Constraint) { sendJsonStatus(r, "{\"detail\":\"Block already exists\"}", .conflict); return; } sendJsonStatus(r, "{\"detail\":\"insert failed\"}", .internal_server_error); return; }; var ts_buf: [32]u8 = undefined; const now = time_util.timestamp(&ts_buf); const doc = db.block_documents.BlockDocumentRow{ .id = id, .created = now, .updated = now, .name = name, .data = data, .is_anonymous = is_anonymous, .block_type_id = block_type_id, .block_type_name = block_type_name, .block_schema_id = block_schema_id, }; const bs = db.block_schemas.getById(alloc, block_schema_id) catch null; try sendBlockDocumentResponse(r, alloc, doc, bt, bs, .created); } fn getById(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 doc = db.block_documents.getById(alloc, id) catch null orelse { sendJsonStatus(r, "{\"detail\":\"block document not found\"}", .not_found); return; }; const bt = db.block_types.getById(alloc, doc.block_type_id) catch null; const bs = db.block_schemas.getById(alloc, doc.block_schema_id) catch null; try sendBlockDocumentResponse(r, alloc, doc, bt, bs, .ok); } fn update(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 body = r.body orelse { sendJsonStatus(r, "{\"detail\":\"request body required\"}", .bad_request); return; }; const parsed = json.parseFromSlice(json.Value, alloc, body, .{}) catch { sendJsonStatus(r, "{\"detail\":\"invalid json\"}", .bad_request); return; }; const obj = parsed.value.object; // merge_existing_data defaults to true (matching python behavior) const merge_existing_data = if (obj.get("merge_existing_data")) |v| (v == .bool and v.bool) else true; // get the new data from request (if provided) const new_data_value = obj.get("data"); // determine final data to store const data = blk: { if (new_data_value) |new_val| { if (merge_existing_data) { // fetch current document to merge with const current = db.block_documents.getById(alloc, id) catch null orelse { sendJsonStatus(r, "{\"detail\":\"Block document not found\"}", .not_found); return; }; // parse current data const current_parsed = json.parseFromSlice(json.Value, alloc, current.data, .{}) catch { // if current data isn't valid json, just use new data var out: std.Io.Writer.Allocating = .init(alloc); var jw: json.Stringify = .{ .writer = &out.writer }; jw.write(new_val) catch break :blk "{}"; break :blk out.toOwnedSlice() catch "{}"; }; defer current_parsed.deinit(); // merge: overlay new data onto current data // modify the current object in place with new values if (current_parsed.value == .object and new_val == .object) { var current_obj = current_parsed.value.object; var it = new_val.object.iterator(); while (it.next()) |entry| { current_obj.put(entry.key_ptr.*, entry.value_ptr.*) catch continue; } // serialize merged result var out: std.Io.Writer.Allocating = .init(alloc); var jw: json.Stringify = .{ .writer = &out.writer }; jw.write(json.Value{ .object = current_obj }) catch break :blk "{}"; break :blk out.toOwnedSlice() catch "{}"; } // fallback: just use new data if types don't match var out: std.Io.Writer.Allocating = .init(alloc); var jw: json.Stringify = .{ .writer = &out.writer }; jw.write(new_val) catch break :blk "{}"; break :blk out.toOwnedSlice() catch "{}"; } else { // no merge - just use new data directly var out: std.Io.Writer.Allocating = .init(alloc); var jw: json.Stringify = .{ .writer = &out.writer }; jw.write(new_val) catch break :blk "{}"; break :blk out.toOwnedSlice() catch "{}"; } } else { // no data provided - don't update data field break :blk null; } }; const block_schema_id = if (obj.get("block_schema_id")) |v| if (v == .string) v.string else null else null; // only update if we have something to update if (data == null and block_schema_id == null) { r.setStatus(.no_content); r.sendBody("") catch {}; return; } db.block_documents.update(id, data orelse "{}", block_schema_id) catch { sendJsonStatus(r, "{\"detail\":\"update failed\"}", .internal_server_error); return; }; r.setStatus(.no_content); r.sendBody("") catch {}; } fn delete(r: zap.Request, id: []const u8) !void { const deleted = db.block_documents.delete(id) catch { sendJsonStatus(r, "{\"detail\":\"delete failed\"}", .internal_server_error); return; }; if (!deleted) { sendJsonStatus(r, "{\"detail\":\"Block document not found\"}", .not_found); return; } r.setStatus(.no_content); r.sendBody("") catch {}; } fn filter(r: zap.Request) !void { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); const alloc = arena.allocator(); const docs = db.block_documents.list(alloc, 200) catch { sendJsonStatus(r, "{\"detail\":\"database error\"}", .internal_server_error); return; }; var output: std.Io.Writer.Allocating = .init(alloc); var jw: json.Stringify = .{ .writer = &output.writer }; jw.beginArray() catch { sendJsonStatus(r, "{\"detail\":\"serialize error\"}", .internal_server_error); return; }; for (docs) |doc| { writeBlockDocument(&jw, doc) catch continue; } jw.endArray() catch {}; sendJson(r, output.toOwnedSlice() catch "[]"); } fn writeBlockDocument(jw: *json.Stringify, doc: db.block_documents.BlockDocumentRow) !void { try jw.beginObject(); try jw.objectField("id"); try jw.write(doc.id); try jw.objectField("created"); try jw.write(doc.created); try jw.objectField("updated"); try jw.write(doc.updated); try jw.objectField("name"); try jw.write(doc.name); try jw.objectField("data"); try jw.beginWriteRaw(); try jw.writer.writeAll(doc.data); jw.endWriteRaw(); try jw.objectField("is_anonymous"); try jw.write(doc.is_anonymous); try jw.objectField("block_type_id"); try jw.write(doc.block_type_id); try jw.objectField("block_schema_id"); try jw.write(doc.block_schema_id); try jw.endObject(); } fn sendBlockDocumentResponse( r: zap.Request, alloc: std.mem.Allocator, doc: db.block_documents.BlockDocumentRow, bt: ?db.block_types.BlockTypeRow, bs: ?db.block_schemas.BlockSchemaRow, status: zap.http.StatusCode, ) !void { var output: std.Io.Writer.Allocating = .init(alloc); var jw: json.Stringify = .{ .writer = &output.writer }; jw.beginObject() catch return; jw.objectField("id") catch return; jw.write(doc.id) catch return; jw.objectField("created") catch return; jw.write(doc.created) catch return; jw.objectField("updated") catch return; jw.write(doc.updated) catch return; jw.objectField("name") catch return; jw.write(doc.name) catch return; jw.objectField("data") catch return; jw.beginWriteRaw() catch return; jw.writer.writeAll(doc.data) catch return; jw.endWriteRaw(); jw.objectField("is_anonymous") catch return; jw.write(doc.is_anonymous) catch return; jw.objectField("block_type_id") catch return; jw.write(doc.block_type_id) catch return; jw.objectField("block_schema_id") catch return; jw.write(doc.block_schema_id) catch return; jw.objectField("block_type") catch return; if (bt) |t| { jw.beginObject() catch return; jw.objectField("id") catch return; jw.write(t.id) catch return; jw.objectField("created") catch return; jw.write(t.created) catch return; jw.objectField("updated") catch return; jw.write(t.updated) catch return; jw.objectField("name") catch return; jw.write(t.name) catch return; jw.objectField("slug") catch return; jw.write(t.slug) catch return; jw.objectField("logo_url") catch return; jw.write(t.logo_url) catch return; jw.objectField("documentation_url") catch return; jw.write(t.documentation_url) catch return; jw.objectField("description") catch return; jw.write(t.description) catch return; jw.objectField("code_example") catch return; jw.write(t.code_example) catch return; jw.objectField("is_protected") catch return; jw.write(t.is_protected) catch return; jw.endObject() catch return; } else { jw.write(null) catch return; } jw.objectField("block_schema") catch return; if (bs) |s| { jw.beginObject() catch return; jw.objectField("id") catch return; jw.write(s.id) catch return; jw.objectField("created") catch return; jw.write(s.created) catch return; jw.objectField("updated") catch return; jw.write(s.updated) catch return; jw.objectField("checksum") catch return; jw.write(s.checksum) catch return; jw.objectField("fields") catch return; jw.beginWriteRaw() catch return; jw.writer.writeAll(s.fields) catch return; jw.endWriteRaw(); jw.objectField("capabilities") catch return; jw.beginWriteRaw() catch return; jw.writer.writeAll(s.capabilities) catch return; jw.endWriteRaw(); jw.objectField("version") catch return; jw.write(s.version) catch return; jw.objectField("block_type_id") catch return; jw.write(s.block_type_id) catch return; jw.endObject() catch return; } else { jw.write(null) catch return; } jw.objectField("block_document_references") catch return; jw.beginObject() catch return; jw.endObject() catch return; jw.endObject() catch return; sendJsonStatus(r, output.toOwnedSlice() catch "{}", status); }