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"); const ascii = std.ascii; /// check if string starts with prefix, ignoring case fn startsWithIgnoreCase(haystack: []const u8, prefix: []const u8) bool { if (haystack.len < prefix.len) return false; for (haystack[0..prefix.len], prefix) |h, p| { if (ascii.toLower(h) != ascii.toLower(p)) return false; } return true; } // JSON output types const BlockTypeJson = struct { id: []const u8, created: []const u8, updated: []const u8, name: []const u8, slug: []const u8, logo_url: ?[]const u8 = null, documentation_url: ?[]const u8 = null, description: ?[]const u8 = null, code_example: ?[]const u8 = null, is_protected: bool = false, fn fromRow(bt: db.block_types.BlockTypeRow) BlockTypeJson { return .{ .id = bt.id, .created = bt.created, .updated = bt.updated, .name = bt.name, .slug = bt.slug, .logo_url = bt.logo_url, .documentation_url = bt.documentation_url, .description = bt.description, .code_example = bt.code_example, .is_protected = bt.is_protected, }; } }; 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_types/ - create // POST /block_types/filter - list // GET /block_types/slug/{slug} - read by slug // PATCH /block_types/{id} - update // GET /block_types/slug/{slug}/block_documents - list documents for type // GET /block_types/slug/{slug}/block_documents/name/{name} - get document by name 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_types")) target[4..] else target; // POST /block_types/filter if (mem.eql(u8, method, "POST") and mem.endsWith(u8, path, "/filter")) { try filter(r); return; } // POST /block_types/ - create if (mem.eql(u8, method, "POST") and (mem.eql(u8, path, "/block_types/") or mem.eql(u8, path, "/block_types"))) { try create(r); return; } // GET /block_types/slug/{slug}/block_documents/name/{name} if (mem.eql(u8, method, "GET") and mem.indexOf(u8, path, "/block_documents/name/") != null) { try getDocumentByName(r, path); return; } // GET /block_types/slug/{slug}/block_documents if (mem.eql(u8, method, "GET") and mem.endsWith(u8, path, "/block_documents")) { try listDocumentsForType(r, path); return; } // GET /block_types/slug/{slug} if (mem.eql(u8, method, "GET") and mem.startsWith(u8, path, "/block_types/slug/")) { const slug = path["/block_types/slug/".len..]; try getBySlug(r, slug); return; } // PATCH /block_types/{id} if (mem.eql(u8, method, "PATCH") and mem.startsWith(u8, path, "/block_types/")) { const id = path["/block_types/".len..]; if (id.len >= 32) { try update(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 name = if (obj.get("name")) |v| v.string else { sendJsonStatus(r, "{\"detail\":\"name required\"}", .bad_request); return; }; const slug = if (obj.get("slug")) |v| v.string else { sendJsonStatus(r, "{\"detail\":\"slug required\"}", .bad_request); return; }; // forbid Prefect- prefix (case-insensitive, matching python behavior) if (startsWithIgnoreCase(name, "prefect")) { sendJsonStatus(r, "{\"detail\":\"Block type names beginning with 'Prefect' are reserved.\"}", .forbidden); return; } const logo_url = if (obj.get("logo_url")) |v| if (v == .string) v.string else null else null; const documentation_url = if (obj.get("documentation_url")) |v| if (v == .string) v.string else null else null; const description = if (obj.get("description")) |v| if (v == .string) v.string else null else null; const code_example = if (obj.get("code_example")) |v| if (v == .string) v.string else null else null; const is_protected = if (obj.get("is_protected")) |v| v == .bool and v.bool else false; var id_buf: [36]u8 = undefined; const id = uuid_util.generate(&id_buf); db.block_types.insert(id, name, slug, logo_url, documentation_url, description, code_example, is_protected) catch |err| { if (err == error.SQLiteConstraint) { sendJsonStatus(r, "{\"detail\":\"block type with this slug 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 bt = BlockTypeJson{ .id = id, .created = now, .updated = now, .name = name, .slug = slug, .logo_url = logo_url, .documentation_url = documentation_url, .description = description, .code_example = code_example, .is_protected = is_protected, }; var output: std.Io.Writer.Allocating = .init(alloc); var jw: json.Stringify = .{ .writer = &output.writer }; jw.write(bt) catch { sendJsonStatus(r, "{\"detail\":\"serialize error\"}", .internal_server_error); return; }; sendJsonStatus(r, output.toOwnedSlice() catch "", .created); } fn getBySlug(r: zap.Request, slug: []const u8) !void { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); const alloc = arena.allocator(); const bt = db.block_types.getBySlug(alloc, slug) catch null orelse { sendJsonStatus(r, "{\"detail\":\"block type not found\"}", .not_found); return; }; var output: std.Io.Writer.Allocating = .init(alloc); var jw: json.Stringify = .{ .writer = &output.writer }; jw.write(BlockTypeJson.fromRow(bt)) catch { sendJsonStatus(r, "{\"detail\":\"serialize error\"}", .internal_server_error); return; }; sendJson(r, output.toOwnedSlice() catch ""); } 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; const logo_url = if (obj.get("logo_url")) |v| if (v == .string) v.string else null else null; const documentation_url = if (obj.get("documentation_url")) |v| if (v == .string) v.string else null else null; const description = if (obj.get("description")) |v| if (v == .string) v.string else null else null; const code_example = if (obj.get("code_example")) |v| if (v == .string) v.string else null else null; db.block_types.update(id, logo_url, documentation_url, description, code_example) catch { sendJsonStatus(r, "{\"detail\":\"update failed\"}", .internal_server_error); 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 types = db.block_types.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 (types) |bt| { jw.write(BlockTypeJson.fromRow(bt)) catch continue; } jw.endArray() catch {}; sendJson(r, output.toOwnedSlice() catch "[]"); } fn getDocumentByName(r: zap.Request, path: []const u8) !void { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); const alloc = arena.allocator(); // parse /block_types/slug/{slug}/block_documents/name/{name} const slug_start = "/block_types/slug/".len; const docs_idx = mem.indexOf(u8, path, "/block_documents/name/") orelse { sendJsonStatus(r, "{\"detail\":\"invalid path\"}", .bad_request); return; }; const slug = path[slug_start..docs_idx]; const name = path[docs_idx + "/block_documents/name/".len ..]; const doc = db.block_documents.getByTypeSlugAndName(alloc, slug, name) catch null orelse { sendJsonStatus(r, "{\"detail\":\"block document not found\"}", .not_found); return; }; // get associated block type and schema for full response 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 listDocumentsForType(r: zap.Request, path: []const u8) !void { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); const alloc = arena.allocator(); // parse /block_types/slug/{slug}/block_documents const slug_start = "/block_types/slug/".len; const docs_idx = mem.indexOf(u8, path, "/block_documents") orelse { sendJsonStatus(r, "{\"detail\":\"invalid path\"}", .bad_request); return; }; const slug = path[slug_start..docs_idx]; const docs = db.block_documents.listByTypeSlug(alloc, slug, 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| { jw.beginObject() catch continue; jw.objectField("id") catch continue; jw.write(doc.id) catch continue; jw.objectField("created") catch continue; jw.write(doc.created) catch continue; jw.objectField("updated") catch continue; jw.write(doc.updated) catch continue; jw.objectField("name") catch continue; jw.write(doc.name) catch continue; jw.objectField("data") catch continue; jw.beginWriteRaw() catch continue; jw.writer.writeAll(doc.data) catch continue; jw.endWriteRaw(); jw.objectField("is_anonymous") catch continue; jw.write(doc.is_anonymous) catch continue; jw.objectField("block_type_id") catch continue; jw.write(doc.block_type_id) catch continue; jw.objectField("block_schema_id") catch continue; jw.write(doc.block_schema_id) catch continue; jw.endObject() catch continue; } jw.endArray() catch {}; sendJson(r, output.toOwnedSlice() catch "[]"); } 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.write(BlockTypeJson.fromRow(t)) 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); }