prefect server in zig

add blocks api (block_types, block_schemas, block_documents)

implements full blocks support with:
- block_types: create, filter, get by slug, update
- block_schemas: create, filter, get by checksum/id (server-side sha256 checksum)
- block_documents: create, filter, get, update, delete

uses std.json.Stringify streaming pattern for json serialization.
health endpoint now returns "ok" (text/plain) to match python server.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

+1800 -2
+20 -1
CLAUDE.md
··· 47 47 - POST /api/logs/ 48 48 - WS /api/events/in 49 49 - WS /api/events/out (subscribe) 50 + - POST /api/block_types/ 51 + - POST /api/block_types/filter 52 + - GET /api/block_types/slug/{slug} 53 + - PATCH /api/block_types/{id} 54 + - GET /api/block_types/slug/{slug}/block_documents 55 + - GET /api/block_types/slug/{slug}/block_documents/name/{name} 56 + - POST /api/block_schemas/ 57 + - POST /api/block_schemas/filter 58 + - GET /api/block_schemas/checksum/{checksum} 59 + - GET /api/block_schemas/{id} 60 + - POST /api/block_documents/ 61 + - POST /api/block_documents/filter 62 + - GET /api/block_documents/{id} 63 + - PATCH /api/block_documents/{id} 64 + - DELETE /api/block_documents/{id} 50 65 51 66 ## database 52 67 53 - sqlite via zqlite. tables: flow, flow_run, flow_run_state, events 68 + sqlite via zqlite. tables: flow, flow_run, flow_run_state, events, block_type, block_schema, block_document 54 69 55 70 ## architecture 56 71 ··· 85 100 2. api/events.zig parses JSON, extracts id/occurred/event/resource_id 86 101 3. messaging.publishEvent() queues to bounded channel 87 102 4. event_persister worker drains batches and writes to db 103 + 104 + ## reference 105 + 106 + python prefect server source is at ~/github.com/prefecthq/prefect - read from disk, don't fetch from github 88 107 89 108 ## roadmap 90 109
+83
scripts/test-blocks
··· 1 + #!/usr/bin/env -S uv run --script --quiet 2 + # /// script 3 + # requires-python = ">=3.12" 4 + # dependencies = ["prefect>=3.0"] 5 + # /// 6 + """ 7 + test script for prefect-server blocks API 8 + 9 + tests: 10 + 1. create and save a Secret block 11 + 2. load the block and verify value 12 + 3. create and save a JSON block 13 + 4. load and verify JSON block 14 + 5. overwrite existing block 15 + 16 + usage: 17 + PREFECT_API_URL=http://localhost:4200/api ./scripts/test-blocks 18 + """ 19 + import os 20 + 21 + from prefect.blocks.system import Secret 22 + 23 + 24 + def test_secret_block(): 25 + """test creating, saving, and loading a Secret block""" 26 + print("\n=== test_secret_block ===") 27 + 28 + secret_value = "my-super-secret-value-123" 29 + block_name = "test-secret" 30 + 31 + # create and save 32 + secret = Secret(value=secret_value) 33 + secret.save(block_name, overwrite=True) 34 + print(f"✓ saved Secret block '{block_name}'") 35 + 36 + # load and verify 37 + loaded = Secret.load(block_name) 38 + assert loaded.get() == secret_value, f"expected '{secret_value}', got '{loaded.get()}'" 39 + print(f"✓ loaded Secret block '{block_name}' with correct value") 40 + 41 + # verify it's the right type 42 + assert isinstance(loaded, Secret), f"expected Secret, got {type(loaded)}" 43 + print(f"✓ loaded block is correct type: {type(loaded).__name__}") 44 + 45 + 46 + def test_block_overwrite(): 47 + """test that overwriting a block updates the value""" 48 + print("\n=== test_block_overwrite ===") 49 + 50 + block_name = "test-overwrite" 51 + 52 + # save initial value 53 + secret1 = Secret(value="initial-value") 54 + secret1.save(block_name, overwrite=True) 55 + print(f"✓ saved initial Secret block '{block_name}'") 56 + 57 + # verify initial value 58 + loaded1 = Secret.load(block_name) 59 + assert loaded1.get() == "initial-value", f"expected 'initial-value', got '{loaded1.get()}'" 60 + print(f"✓ initial value verified") 61 + 62 + # overwrite with new value 63 + secret2 = Secret(value="updated-value") 64 + secret2.save(block_name, overwrite=True) 65 + print(f"✓ overwrote Secret block '{block_name}' with new value") 66 + 67 + # verify new value 68 + loaded2 = Secret.load(block_name) 69 + assert loaded2.get() == "updated-value", f"expected 'updated-value', got '{loaded2.get()}'" 70 + print(f"✓ updated value verified") 71 + 72 + 73 + def main(): 74 + print(f"api url: {os.environ.get('PREFECT_API_URL', '(not set)')}") 75 + 76 + test_secret_block() 77 + test_block_overwrite() 78 + 79 + print("\n=== all block tests passed ===") 80 + 81 + 82 + if __name__ == "__main__": 83 + main()
+4 -1
src/api/admin.zig
··· 11 11 } 12 12 13 13 pub fn health(r: zap.Request) !void { 14 - sendJson(r, "{\"status\":\"healthy\"}"); 14 + // match python prefect server response 15 + r.setHeader("content-type", "text/plain") catch {}; 16 + r.setHeader("access-control-allow-origin", "*") catch {}; 17 + r.sendBody("ok") catch {}; 15 18 } 16 19 17 20 pub fn csrfToken(r: zap.Request) !void {
+394
src/api/block_documents.zig
··· 1 + const std = @import("std"); 2 + const zap = @import("zap"); 3 + const mem = std.mem; 4 + const json = std.json; 5 + 6 + const db = @import("../db/sqlite.zig"); 7 + const uuid_util = @import("../utilities/uuid.zig"); 8 + const time_util = @import("../utilities/time.zig"); 9 + 10 + fn sendJson(r: zap.Request, body: []const u8) void { 11 + r.setHeader("content-type", "application/json") catch {}; 12 + r.setHeader("access-control-allow-origin", "*") catch {}; 13 + r.setHeader("access-control-allow-methods", "GET, POST, PATCH, DELETE, OPTIONS") catch {}; 14 + r.setHeader("access-control-allow-headers", "content-type, x-prefect-api-version") catch {}; 15 + r.sendBody(body) catch {}; 16 + } 17 + 18 + fn sendJsonStatus(r: zap.Request, body: []const u8, status: zap.http.StatusCode) void { 19 + r.setStatus(status); 20 + sendJson(r, body); 21 + } 22 + 23 + // Routes: 24 + // POST /block_documents/ - create 25 + // GET /block_documents/{id} - read by id 26 + // PATCH /block_documents/{id} - update 27 + // DELETE /block_documents/{id} - delete 28 + // POST /block_documents/filter - list 29 + pub fn handle(r: zap.Request) !void { 30 + const target = r.path orelse "/"; 31 + const method = r.method orelse "GET"; 32 + 33 + // strip /api prefix if present 34 + const path = if (mem.startsWith(u8, target, "/api/block_documents")) 35 + target[4..] 36 + else 37 + target; 38 + 39 + // POST /block_documents/filter 40 + if (mem.eql(u8, method, "POST") and mem.endsWith(u8, path, "/filter")) { 41 + try filter(r); 42 + return; 43 + } 44 + 45 + // POST /block_documents/ - create 46 + if (mem.eql(u8, method, "POST") and (mem.eql(u8, path, "/block_documents/") or mem.eql(u8, path, "/block_documents"))) { 47 + try create(r); 48 + return; 49 + } 50 + 51 + // GET /block_documents/{id} or PATCH /block_documents/{id} or DELETE /block_documents/{id} 52 + if (mem.startsWith(u8, path, "/block_documents/")) { 53 + const id = path["/block_documents/".len..]; 54 + if (id.len >= 32) { 55 + if (mem.eql(u8, method, "GET")) { 56 + try getById(r, id); 57 + return; 58 + } else if (mem.eql(u8, method, "PATCH")) { 59 + try update(r, id); 60 + return; 61 + } else if (mem.eql(u8, method, "DELETE")) { 62 + try delete(r, id); 63 + return; 64 + } 65 + } 66 + } 67 + 68 + sendJsonStatus(r, "{\"detail\":\"not found\"}", .not_found); 69 + } 70 + 71 + fn create(r: zap.Request) !void { 72 + var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); 73 + defer arena.deinit(); 74 + const alloc = arena.allocator(); 75 + 76 + const body = r.body orelse { 77 + sendJsonStatus(r, "{\"detail\":\"request body required\"}", .bad_request); 78 + return; 79 + }; 80 + 81 + const parsed = json.parseFromSlice(json.Value, alloc, body, .{}) catch { 82 + sendJsonStatus(r, "{\"detail\":\"invalid json\"}", .bad_request); 83 + return; 84 + }; 85 + 86 + const obj = parsed.value.object; 87 + 88 + const block_type_id = if (obj.get("block_type_id")) |v| v.string else { 89 + sendJsonStatus(r, "{\"detail\":\"block_type_id required\"}", .bad_request); 90 + return; 91 + }; 92 + 93 + const block_schema_id = if (obj.get("block_schema_id")) |v| v.string else { 94 + sendJsonStatus(r, "{\"detail\":\"block_schema_id required\"}", .bad_request); 95 + return; 96 + }; 97 + 98 + const name = if (obj.get("name")) |v| if (v == .string) v.string else null else null; 99 + const is_anonymous = if (obj.get("is_anonymous")) |v| v == .bool and v.bool else (name == null); 100 + 101 + // serialize data 102 + const data = blk: { 103 + if (obj.get("data")) |v| { 104 + var out: std.Io.Writer.Allocating = .init(alloc); 105 + var jw: json.Stringify = .{ .writer = &out.writer }; 106 + jw.write(v) catch break :blk "{}"; 107 + break :blk out.toOwnedSlice() catch "{}"; 108 + } else break :blk "{}"; 109 + }; 110 + 111 + // get block_type_name from block_type 112 + const bt = db.block_types.getById(alloc, block_type_id) catch null; 113 + const block_type_name = if (bt) |t| t.name else null; 114 + 115 + var id_buf: [36]u8 = undefined; 116 + const id = uuid_util.generate(&id_buf); 117 + 118 + db.block_documents.insert(id, name, data, is_anonymous, block_type_id, block_type_name, block_schema_id) catch |err| { 119 + if (err == error.Constraint) { 120 + sendJsonStatus(r, "{\"detail\":\"Block already exists\"}", .conflict); 121 + return; 122 + } 123 + sendJsonStatus(r, "{\"detail\":\"insert failed\"}", .internal_server_error); 124 + return; 125 + }; 126 + 127 + var ts_buf: [32]u8 = undefined; 128 + const now = time_util.timestamp(&ts_buf); 129 + 130 + const doc = db.block_documents.BlockDocumentRow{ 131 + .id = id, 132 + .created = now, 133 + .updated = now, 134 + .name = name, 135 + .data = data, 136 + .is_anonymous = is_anonymous, 137 + .block_type_id = block_type_id, 138 + .block_type_name = block_type_name, 139 + .block_schema_id = block_schema_id, 140 + }; 141 + 142 + const bs = db.block_schemas.getById(alloc, block_schema_id) catch null; 143 + try sendBlockDocumentResponse(r, alloc, doc, bt, bs, .created); 144 + } 145 + 146 + fn getById(r: zap.Request, id: []const u8) !void { 147 + var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); 148 + defer arena.deinit(); 149 + const alloc = arena.allocator(); 150 + 151 + const doc = db.block_documents.getById(alloc, id) catch null orelse { 152 + sendJsonStatus(r, "{\"detail\":\"block document not found\"}", .not_found); 153 + return; 154 + }; 155 + 156 + const bt = db.block_types.getById(alloc, doc.block_type_id) catch null; 157 + const bs = db.block_schemas.getById(alloc, doc.block_schema_id) catch null; 158 + 159 + try sendBlockDocumentResponse(r, alloc, doc, bt, bs, .ok); 160 + } 161 + 162 + fn update(r: zap.Request, id: []const u8) !void { 163 + var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); 164 + defer arena.deinit(); 165 + const alloc = arena.allocator(); 166 + 167 + const body = r.body orelse { 168 + sendJsonStatus(r, "{\"detail\":\"request body required\"}", .bad_request); 169 + return; 170 + }; 171 + 172 + const parsed = json.parseFromSlice(json.Value, alloc, body, .{}) catch { 173 + sendJsonStatus(r, "{\"detail\":\"invalid json\"}", .bad_request); 174 + return; 175 + }; 176 + 177 + const obj = parsed.value.object; 178 + 179 + // serialize data 180 + const data = blk: { 181 + if (obj.get("data")) |v| { 182 + var out: std.Io.Writer.Allocating = .init(alloc); 183 + var jw: json.Stringify = .{ .writer = &out.writer }; 184 + jw.write(v) catch break :blk "{}"; 185 + break :blk out.toOwnedSlice() catch "{}"; 186 + } else break :blk "{}"; 187 + }; 188 + 189 + const block_schema_id = if (obj.get("block_schema_id")) |v| if (v == .string) v.string else null else null; 190 + 191 + db.block_documents.update(id, data, block_schema_id) catch { 192 + sendJsonStatus(r, "{\"detail\":\"update failed\"}", .internal_server_error); 193 + return; 194 + }; 195 + 196 + r.setStatus(.no_content); 197 + r.sendBody("") catch {}; 198 + } 199 + 200 + fn delete(r: zap.Request, id: []const u8) !void { 201 + const deleted = db.block_documents.delete(id) catch { 202 + sendJsonStatus(r, "{\"detail\":\"delete failed\"}", .internal_server_error); 203 + return; 204 + }; 205 + 206 + if (!deleted) { 207 + sendJsonStatus(r, "{\"detail\":\"Block document not found\"}", .not_found); 208 + return; 209 + } 210 + 211 + r.setStatus(.no_content); 212 + r.sendBody("") catch {}; 213 + } 214 + 215 + fn filter(r: zap.Request) !void { 216 + var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); 217 + defer arena.deinit(); 218 + const alloc = arena.allocator(); 219 + 220 + const docs = db.block_documents.list(alloc, 200) catch { 221 + sendJsonStatus(r, "{\"detail\":\"database error\"}", .internal_server_error); 222 + return; 223 + }; 224 + 225 + var output: std.Io.Writer.Allocating = .init(alloc); 226 + var jw: json.Stringify = .{ .writer = &output.writer }; 227 + 228 + jw.beginArray() catch { 229 + sendJsonStatus(r, "{\"detail\":\"serialize error\"}", .internal_server_error); 230 + return; 231 + }; 232 + for (docs) |doc| { 233 + writeBlockDocument(&jw, doc) catch continue; 234 + } 235 + jw.endArray() catch {}; 236 + 237 + sendJson(r, output.toOwnedSlice() catch "[]"); 238 + } 239 + 240 + fn writeBlockDocument(jw: *json.Stringify, doc: db.block_documents.BlockDocumentRow) !void { 241 + try jw.beginObject(); 242 + 243 + try jw.objectField("id"); 244 + try jw.write(doc.id); 245 + 246 + try jw.objectField("created"); 247 + try jw.write(doc.created); 248 + 249 + try jw.objectField("updated"); 250 + try jw.write(doc.updated); 251 + 252 + try jw.objectField("name"); 253 + try jw.write(doc.name); 254 + 255 + try jw.objectField("data"); 256 + try jw.beginWriteRaw(); 257 + try jw.writer.writeAll(doc.data); 258 + jw.endWriteRaw(); 259 + 260 + try jw.objectField("is_anonymous"); 261 + try jw.write(doc.is_anonymous); 262 + 263 + try jw.objectField("block_type_id"); 264 + try jw.write(doc.block_type_id); 265 + 266 + try jw.objectField("block_schema_id"); 267 + try jw.write(doc.block_schema_id); 268 + 269 + try jw.endObject(); 270 + } 271 + 272 + fn sendBlockDocumentResponse( 273 + r: zap.Request, 274 + alloc: std.mem.Allocator, 275 + doc: db.block_documents.BlockDocumentRow, 276 + bt: ?db.block_types.BlockTypeRow, 277 + bs: ?db.block_schemas.BlockSchemaRow, 278 + status: zap.http.StatusCode, 279 + ) !void { 280 + var output: std.Io.Writer.Allocating = .init(alloc); 281 + var jw: json.Stringify = .{ .writer = &output.writer }; 282 + 283 + jw.beginObject() catch return; 284 + 285 + jw.objectField("id") catch return; 286 + jw.write(doc.id) catch return; 287 + 288 + jw.objectField("created") catch return; 289 + jw.write(doc.created) catch return; 290 + 291 + jw.objectField("updated") catch return; 292 + jw.write(doc.updated) catch return; 293 + 294 + jw.objectField("name") catch return; 295 + jw.write(doc.name) catch return; 296 + 297 + jw.objectField("data") catch return; 298 + jw.beginWriteRaw() catch return; 299 + jw.writer.writeAll(doc.data) catch return; 300 + jw.endWriteRaw(); 301 + 302 + jw.objectField("is_anonymous") catch return; 303 + jw.write(doc.is_anonymous) catch return; 304 + 305 + jw.objectField("block_type_id") catch return; 306 + jw.write(doc.block_type_id) catch return; 307 + 308 + jw.objectField("block_schema_id") catch return; 309 + jw.write(doc.block_schema_id) catch return; 310 + 311 + jw.objectField("block_type") catch return; 312 + if (bt) |t| { 313 + jw.beginObject() catch return; 314 + 315 + jw.objectField("id") catch return; 316 + jw.write(t.id) catch return; 317 + 318 + jw.objectField("created") catch return; 319 + jw.write(t.created) catch return; 320 + 321 + jw.objectField("updated") catch return; 322 + jw.write(t.updated) catch return; 323 + 324 + jw.objectField("name") catch return; 325 + jw.write(t.name) catch return; 326 + 327 + jw.objectField("slug") catch return; 328 + jw.write(t.slug) catch return; 329 + 330 + jw.objectField("logo_url") catch return; 331 + jw.write(t.logo_url) catch return; 332 + 333 + jw.objectField("documentation_url") catch return; 334 + jw.write(t.documentation_url) catch return; 335 + 336 + jw.objectField("description") catch return; 337 + jw.write(t.description) catch return; 338 + 339 + jw.objectField("code_example") catch return; 340 + jw.write(t.code_example) catch return; 341 + 342 + jw.objectField("is_protected") catch return; 343 + jw.write(t.is_protected) catch return; 344 + 345 + jw.endObject() catch return; 346 + } else { 347 + jw.write(null) catch return; 348 + } 349 + 350 + jw.objectField("block_schema") catch return; 351 + if (bs) |s| { 352 + jw.beginObject() catch return; 353 + 354 + jw.objectField("id") catch return; 355 + jw.write(s.id) catch return; 356 + 357 + jw.objectField("created") catch return; 358 + jw.write(s.created) catch return; 359 + 360 + jw.objectField("updated") catch return; 361 + jw.write(s.updated) catch return; 362 + 363 + jw.objectField("checksum") catch return; 364 + jw.write(s.checksum) catch return; 365 + 366 + jw.objectField("fields") catch return; 367 + jw.beginWriteRaw() catch return; 368 + jw.writer.writeAll(s.fields) catch return; 369 + jw.endWriteRaw(); 370 + 371 + jw.objectField("capabilities") catch return; 372 + jw.beginWriteRaw() catch return; 373 + jw.writer.writeAll(s.capabilities) catch return; 374 + jw.endWriteRaw(); 375 + 376 + jw.objectField("version") catch return; 377 + jw.write(s.version) catch return; 378 + 379 + jw.objectField("block_type_id") catch return; 380 + jw.write(s.block_type_id) catch return; 381 + 382 + jw.endObject() catch return; 383 + } else { 384 + jw.write(null) catch return; 385 + } 386 + 387 + jw.objectField("block_document_references") catch return; 388 + jw.beginObject() catch return; 389 + jw.endObject() catch return; 390 + 391 + jw.endObject() catch return; 392 + 393 + sendJsonStatus(r, output.toOwnedSlice() catch "{}", status); 394 + }
+267
src/api/block_schemas.zig
··· 1 + const std = @import("std"); 2 + const zap = @import("zap"); 3 + const mem = std.mem; 4 + const json = std.json; 5 + 6 + const db = @import("../db/sqlite.zig"); 7 + const uuid_util = @import("../utilities/uuid.zig"); 8 + const time_util = @import("../utilities/time.zig"); 9 + const crypto = std.crypto.hash.sha2; 10 + 11 + fn sendJson(r: zap.Request, body: []const u8) void { 12 + r.setHeader("content-type", "application/json") catch {}; 13 + r.setHeader("access-control-allow-origin", "*") catch {}; 14 + r.setHeader("access-control-allow-methods", "GET, POST, PATCH, DELETE, OPTIONS") catch {}; 15 + r.setHeader("access-control-allow-headers", "content-type, x-prefect-api-version") catch {}; 16 + r.sendBody(body) catch {}; 17 + } 18 + 19 + fn sendJsonStatus(r: zap.Request, body: []const u8, status: zap.http.StatusCode) void { 20 + r.setStatus(status); 21 + sendJson(r, body); 22 + } 23 + 24 + // Routes: 25 + // POST /block_schemas/ - create (idempotent) 26 + // POST /block_schemas/filter - list 27 + // GET /block_schemas/checksum/{checksum} - read by checksum 28 + // GET /block_schemas/{id} - read by id 29 + pub fn handle(r: zap.Request) !void { 30 + const target = r.path orelse "/"; 31 + const method = r.method orelse "GET"; 32 + 33 + // strip /api prefix if present 34 + const path = if (mem.startsWith(u8, target, "/api/block_schemas")) 35 + target[4..] 36 + else 37 + target; 38 + 39 + // POST /block_schemas/filter 40 + if (mem.eql(u8, method, "POST") and mem.endsWith(u8, path, "/filter")) { 41 + try filter(r); 42 + return; 43 + } 44 + 45 + // POST /block_schemas/ - create 46 + if (mem.eql(u8, method, "POST") and (mem.eql(u8, path, "/block_schemas/") or mem.eql(u8, path, "/block_schemas"))) { 47 + try create(r); 48 + return; 49 + } 50 + 51 + // GET /block_schemas/checksum/{checksum} 52 + if (mem.eql(u8, method, "GET") and mem.startsWith(u8, path, "/block_schemas/checksum/")) { 53 + const checksum = path["/block_schemas/checksum/".len..]; 54 + try getByChecksum(r, checksum); 55 + return; 56 + } 57 + 58 + // GET /block_schemas/{id} 59 + if (mem.eql(u8, method, "GET") and mem.startsWith(u8, path, "/block_schemas/")) { 60 + const id = path["/block_schemas/".len..]; 61 + if (id.len >= 32) { 62 + try getById(r, id); 63 + return; 64 + } 65 + } 66 + 67 + sendJsonStatus(r, "{\"detail\":\"not found\"}", .not_found); 68 + } 69 + 70 + fn create(r: zap.Request) !void { 71 + var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); 72 + defer arena.deinit(); 73 + const alloc = arena.allocator(); 74 + 75 + const body = r.body orelse { 76 + sendJsonStatus(r, "{\"detail\":\"request body required\"}", .bad_request); 77 + return; 78 + }; 79 + 80 + const parsed = json.parseFromSlice(json.Value, alloc, body, .{}) catch { 81 + sendJsonStatus(r, "{\"detail\":\"invalid json\"}", .bad_request); 82 + return; 83 + }; 84 + 85 + const obj = parsed.value.object; 86 + 87 + const block_type_id = if (obj.get("block_type_id")) |v| v.string else { 88 + sendJsonStatus(r, "{\"detail\":\"block_type_id required\"}", .bad_request); 89 + return; 90 + }; 91 + 92 + const version = if (obj.get("version")) |v| if (v == .string) v.string else "non-versioned" else "non-versioned"; 93 + 94 + // serialize fields and capabilities using streaming writer 95 + const fields = blk: { 96 + if (obj.get("fields")) |v| { 97 + var out: std.Io.Writer.Allocating = .init(alloc); 98 + var jw: json.Stringify = .{ .writer = &out.writer }; 99 + jw.write(v) catch break :blk "{}"; 100 + break :blk out.toOwnedSlice() catch "{}"; 101 + } else break :blk "{}"; 102 + }; 103 + 104 + const capabilities = blk: { 105 + if (obj.get("capabilities")) |v| { 106 + var out: std.Io.Writer.Allocating = .init(alloc); 107 + var jw: json.Stringify = .{ .writer = &out.writer }; 108 + jw.write(v) catch break :blk "[]"; 109 + break :blk out.toOwnedSlice() catch "[]"; 110 + } else break :blk "[]"; 111 + }; 112 + 113 + // compute checksum from fields (sha256) 114 + var checksum_buf: [71]u8 = undefined; // "sha256:" (7) + 64 hex chars 115 + const checksum = blk: { 116 + var hasher = crypto.Sha256.init(.{}); 117 + hasher.update(fields); 118 + const hash = hasher.finalResult(); 119 + const hex = std.fmt.bytesToHex(hash, .lower); 120 + @memcpy(checksum_buf[0..7], "sha256:"); 121 + @memcpy(checksum_buf[7..71], &hex); 122 + break :blk checksum_buf[0..71]; 123 + }; 124 + 125 + // check if schema already exists (idempotent) 126 + if (db.block_schemas.getByChecksum(alloc, checksum, version) catch null) |existing| { 127 + try sendBlockSchemaResponse(r, alloc, existing, .ok); 128 + return; 129 + } 130 + 131 + var id_buf: [36]u8 = undefined; 132 + const id = uuid_util.generate(&id_buf); 133 + 134 + db.block_schemas.insert(id, checksum, fields, capabilities, version, block_type_id) catch { 135 + sendJsonStatus(r, "{\"detail\":\"insert failed\"}", .internal_server_error); 136 + return; 137 + }; 138 + 139 + var ts_buf: [32]u8 = undefined; 140 + const now = time_util.timestamp(&ts_buf); 141 + 142 + const schema = db.block_schemas.BlockSchemaRow{ 143 + .id = id, 144 + .created = now, 145 + .updated = now, 146 + .checksum = checksum, 147 + .fields = fields, 148 + .capabilities = capabilities, 149 + .version = version, 150 + .block_type_id = block_type_id, 151 + }; 152 + 153 + try sendBlockSchemaResponse(r, alloc, schema, .created); 154 + } 155 + 156 + fn getByChecksum(r: zap.Request, checksum: []const u8) !void { 157 + var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); 158 + defer arena.deinit(); 159 + const alloc = arena.allocator(); 160 + 161 + // check for version query param (simplified - just look for ?version=) 162 + var version: ?[]const u8 = null; 163 + if (mem.indexOf(u8, checksum, "?version=")) |idx| { 164 + version = checksum[idx + "?version=".len ..]; 165 + const actual_checksum = checksum[0..idx]; 166 + const schema = db.block_schemas.getByChecksum(alloc, actual_checksum, version) catch null orelse { 167 + sendJsonStatus(r, "{\"detail\":\"block schema not found\"}", .not_found); 168 + return; 169 + }; 170 + try sendBlockSchemaResponse(r, alloc, schema, .ok); 171 + return; 172 + } 173 + 174 + const schema = db.block_schemas.getByChecksum(alloc, checksum, null) catch null orelse { 175 + sendJsonStatus(r, "{\"detail\":\"block schema not found\"}", .not_found); 176 + return; 177 + }; 178 + 179 + try sendBlockSchemaResponse(r, alloc, schema, .ok); 180 + } 181 + 182 + fn getById(r: zap.Request, id: []const u8) !void { 183 + var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); 184 + defer arena.deinit(); 185 + const alloc = arena.allocator(); 186 + 187 + const schema = db.block_schemas.getById(alloc, id) catch null orelse { 188 + sendJsonStatus(r, "{\"detail\":\"block schema not found\"}", .not_found); 189 + return; 190 + }; 191 + 192 + try sendBlockSchemaResponse(r, alloc, schema, .ok); 193 + } 194 + 195 + fn filter(r: zap.Request) !void { 196 + var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); 197 + defer arena.deinit(); 198 + const alloc = arena.allocator(); 199 + 200 + const schemas = db.block_schemas.list(alloc, 200) catch { 201 + sendJsonStatus(r, "{\"detail\":\"database error\"}", .internal_server_error); 202 + return; 203 + }; 204 + 205 + var output: std.Io.Writer.Allocating = .init(alloc); 206 + var jw: json.Stringify = .{ .writer = &output.writer }; 207 + 208 + jw.beginArray() catch { 209 + sendJsonStatus(r, "{\"detail\":\"serialize error\"}", .internal_server_error); 210 + return; 211 + }; 212 + for (schemas) |s| { 213 + writeBlockSchema(&jw, s) catch continue; 214 + } 215 + jw.endArray() catch {}; 216 + 217 + sendJson(r, output.toOwnedSlice() catch "[]"); 218 + } 219 + 220 + fn writeBlockSchema(jw: *json.Stringify, s: db.block_schemas.BlockSchemaRow) !void { 221 + try jw.beginObject(); 222 + 223 + try jw.objectField("id"); 224 + try jw.write(s.id); 225 + 226 + try jw.objectField("created"); 227 + try jw.write(s.created); 228 + 229 + try jw.objectField("updated"); 230 + try jw.write(s.updated); 231 + 232 + try jw.objectField("checksum"); 233 + try jw.write(s.checksum); 234 + 235 + try jw.objectField("fields"); 236 + try jw.beginWriteRaw(); 237 + try jw.writer.writeAll(s.fields); 238 + jw.endWriteRaw(); 239 + 240 + try jw.objectField("capabilities"); 241 + try jw.beginWriteRaw(); 242 + try jw.writer.writeAll(s.capabilities); 243 + jw.endWriteRaw(); 244 + 245 + try jw.objectField("version"); 246 + try jw.write(s.version); 247 + 248 + try jw.objectField("block_type_id"); 249 + try jw.write(s.block_type_id); 250 + 251 + try jw.objectField("block_type"); 252 + try jw.write(null); 253 + 254 + try jw.endObject(); 255 + } 256 + 257 + fn sendBlockSchemaResponse(r: zap.Request, alloc: std.mem.Allocator, schema: db.block_schemas.BlockSchemaRow, status: zap.http.StatusCode) !void { 258 + var output: std.Io.Writer.Allocating = .init(alloc); 259 + var jw: json.Stringify = .{ .writer = &output.writer }; 260 + 261 + writeBlockSchema(&jw, schema) catch { 262 + sendJsonStatus(r, "{\"detail\":\"serialize error\"}", .internal_server_error); 263 + return; 264 + }; 265 + 266 + sendJsonStatus(r, output.toOwnedSlice() catch "{}", status); 267 + }
+442
src/api/block_types.zig
··· 1 + const std = @import("std"); 2 + const zap = @import("zap"); 3 + const mem = std.mem; 4 + const json = std.json; 5 + 6 + const db = @import("../db/sqlite.zig"); 7 + const uuid_util = @import("../utilities/uuid.zig"); 8 + const time_util = @import("../utilities/time.zig"); 9 + 10 + // JSON output types 11 + const BlockTypeJson = struct { 12 + id: []const u8, 13 + created: []const u8, 14 + updated: []const u8, 15 + name: []const u8, 16 + slug: []const u8, 17 + logo_url: ?[]const u8 = null, 18 + documentation_url: ?[]const u8 = null, 19 + description: ?[]const u8 = null, 20 + code_example: ?[]const u8 = null, 21 + is_protected: bool = false, 22 + 23 + fn fromRow(bt: db.block_types.BlockTypeRow) BlockTypeJson { 24 + return .{ 25 + .id = bt.id, 26 + .created = bt.created, 27 + .updated = bt.updated, 28 + .name = bt.name, 29 + .slug = bt.slug, 30 + .logo_url = bt.logo_url, 31 + .documentation_url = bt.documentation_url, 32 + .description = bt.description, 33 + .code_example = bt.code_example, 34 + .is_protected = bt.is_protected, 35 + }; 36 + } 37 + }; 38 + 39 + fn sendJson(r: zap.Request, body: []const u8) void { 40 + r.setHeader("content-type", "application/json") catch {}; 41 + r.setHeader("access-control-allow-origin", "*") catch {}; 42 + r.setHeader("access-control-allow-methods", "GET, POST, PATCH, DELETE, OPTIONS") catch {}; 43 + r.setHeader("access-control-allow-headers", "content-type, x-prefect-api-version") catch {}; 44 + r.sendBody(body) catch {}; 45 + } 46 + 47 + fn sendJsonStatus(r: zap.Request, body: []const u8, status: zap.http.StatusCode) void { 48 + r.setStatus(status); 49 + sendJson(r, body); 50 + } 51 + 52 + // Routes: 53 + // POST /block_types/ - create 54 + // POST /block_types/filter - list 55 + // GET /block_types/slug/{slug} - read by slug 56 + // PATCH /block_types/{id} - update 57 + // GET /block_types/slug/{slug}/block_documents - list documents for type 58 + // GET /block_types/slug/{slug}/block_documents/name/{name} - get document by name 59 + pub fn handle(r: zap.Request) !void { 60 + const target = r.path orelse "/"; 61 + const method = r.method orelse "GET"; 62 + 63 + // strip /api prefix if present 64 + const path = if (mem.startsWith(u8, target, "/api/block_types")) 65 + target[4..] 66 + else 67 + target; 68 + 69 + // POST /block_types/filter 70 + if (mem.eql(u8, method, "POST") and mem.endsWith(u8, path, "/filter")) { 71 + try filter(r); 72 + return; 73 + } 74 + 75 + // POST /block_types/ - create 76 + if (mem.eql(u8, method, "POST") and (mem.eql(u8, path, "/block_types/") or mem.eql(u8, path, "/block_types"))) { 77 + try create(r); 78 + return; 79 + } 80 + 81 + // GET /block_types/slug/{slug}/block_documents/name/{name} 82 + if (mem.eql(u8, method, "GET") and mem.indexOf(u8, path, "/block_documents/name/") != null) { 83 + try getDocumentByName(r, path); 84 + return; 85 + } 86 + 87 + // GET /block_types/slug/{slug}/block_documents 88 + if (mem.eql(u8, method, "GET") and mem.endsWith(u8, path, "/block_documents")) { 89 + try listDocumentsForType(r, path); 90 + return; 91 + } 92 + 93 + // GET /block_types/slug/{slug} 94 + if (mem.eql(u8, method, "GET") and mem.startsWith(u8, path, "/block_types/slug/")) { 95 + const slug = path["/block_types/slug/".len..]; 96 + try getBySlug(r, slug); 97 + return; 98 + } 99 + 100 + // PATCH /block_types/{id} 101 + if (mem.eql(u8, method, "PATCH") and mem.startsWith(u8, path, "/block_types/")) { 102 + const id = path["/block_types/".len..]; 103 + if (id.len >= 32) { 104 + try update(r, id); 105 + return; 106 + } 107 + } 108 + 109 + sendJsonStatus(r, "{\"detail\":\"not found\"}", .not_found); 110 + } 111 + 112 + fn create(r: zap.Request) !void { 113 + var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); 114 + defer arena.deinit(); 115 + const alloc = arena.allocator(); 116 + 117 + const body = r.body orelse { 118 + sendJsonStatus(r, "{\"detail\":\"request body required\"}", .bad_request); 119 + return; 120 + }; 121 + 122 + const parsed = json.parseFromSlice(json.Value, alloc, body, .{}) catch { 123 + sendJsonStatus(r, "{\"detail\":\"invalid json\"}", .bad_request); 124 + return; 125 + }; 126 + 127 + const obj = parsed.value.object; 128 + 129 + const name = if (obj.get("name")) |v| v.string else { 130 + sendJsonStatus(r, "{\"detail\":\"name required\"}", .bad_request); 131 + return; 132 + }; 133 + 134 + const slug = if (obj.get("slug")) |v| v.string else { 135 + sendJsonStatus(r, "{\"detail\":\"slug required\"}", .bad_request); 136 + return; 137 + }; 138 + 139 + // forbid Prefect- prefix 140 + if (mem.startsWith(u8, name, "Prefect")) { 141 + sendJsonStatus(r, "{\"detail\":\"block type names starting with 'Prefect' are reserved\"}", .forbidden); 142 + return; 143 + } 144 + 145 + const logo_url = if (obj.get("logo_url")) |v| if (v == .string) v.string else null else null; 146 + const documentation_url = if (obj.get("documentation_url")) |v| if (v == .string) v.string else null else null; 147 + const description = if (obj.get("description")) |v| if (v == .string) v.string else null else null; 148 + const code_example = if (obj.get("code_example")) |v| if (v == .string) v.string else null else null; 149 + const is_protected = if (obj.get("is_protected")) |v| v == .bool and v.bool else false; 150 + 151 + var id_buf: [36]u8 = undefined; 152 + const id = uuid_util.generate(&id_buf); 153 + 154 + db.block_types.insert(id, name, slug, logo_url, documentation_url, description, code_example, is_protected) catch |err| { 155 + if (err == error.SQLiteConstraint) { 156 + sendJsonStatus(r, "{\"detail\":\"block type with this slug already exists\"}", .conflict); 157 + return; 158 + } 159 + sendJsonStatus(r, "{\"detail\":\"insert failed\"}", .internal_server_error); 160 + return; 161 + }; 162 + 163 + var ts_buf: [32]u8 = undefined; 164 + const now = time_util.timestamp(&ts_buf); 165 + 166 + const bt = BlockTypeJson{ 167 + .id = id, 168 + .created = now, 169 + .updated = now, 170 + .name = name, 171 + .slug = slug, 172 + .logo_url = logo_url, 173 + .documentation_url = documentation_url, 174 + .description = description, 175 + .code_example = code_example, 176 + .is_protected = is_protected, 177 + }; 178 + 179 + var output: std.Io.Writer.Allocating = .init(alloc); 180 + var jw: json.Stringify = .{ .writer = &output.writer }; 181 + jw.write(bt) catch { 182 + sendJsonStatus(r, "{\"detail\":\"serialize error\"}", .internal_server_error); 183 + return; 184 + }; 185 + 186 + sendJsonStatus(r, output.toOwnedSlice() catch "", .created); 187 + } 188 + 189 + fn getBySlug(r: zap.Request, slug: []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 bt = db.block_types.getBySlug(alloc, slug) catch null orelse { 195 + sendJsonStatus(r, "{\"detail\":\"block type not found\"}", .not_found); 196 + return; 197 + }; 198 + 199 + var output: std.Io.Writer.Allocating = .init(alloc); 200 + var jw: json.Stringify = .{ .writer = &output.writer }; 201 + jw.write(BlockTypeJson.fromRow(bt)) catch { 202 + sendJsonStatus(r, "{\"detail\":\"serialize error\"}", .internal_server_error); 203 + return; 204 + }; 205 + 206 + sendJson(r, output.toOwnedSlice() catch ""); 207 + } 208 + 209 + fn update(r: zap.Request, id: []const u8) !void { 210 + var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); 211 + defer arena.deinit(); 212 + const alloc = arena.allocator(); 213 + 214 + const body = r.body orelse { 215 + sendJsonStatus(r, "{\"detail\":\"request body required\"}", .bad_request); 216 + return; 217 + }; 218 + 219 + const parsed = json.parseFromSlice(json.Value, alloc, body, .{}) catch { 220 + sendJsonStatus(r, "{\"detail\":\"invalid json\"}", .bad_request); 221 + return; 222 + }; 223 + 224 + const obj = parsed.value.object; 225 + 226 + const logo_url = if (obj.get("logo_url")) |v| if (v == .string) v.string else null else null; 227 + const documentation_url = if (obj.get("documentation_url")) |v| if (v == .string) v.string else null else null; 228 + const description = if (obj.get("description")) |v| if (v == .string) v.string else null else null; 229 + const code_example = if (obj.get("code_example")) |v| if (v == .string) v.string else null else null; 230 + 231 + db.block_types.update(id, logo_url, documentation_url, description, code_example) catch { 232 + sendJsonStatus(r, "{\"detail\":\"update failed\"}", .internal_server_error); 233 + return; 234 + }; 235 + 236 + r.setStatus(.no_content); 237 + r.sendBody("") catch {}; 238 + } 239 + 240 + fn filter(r: zap.Request) !void { 241 + var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); 242 + defer arena.deinit(); 243 + const alloc = arena.allocator(); 244 + 245 + const types = db.block_types.list(alloc, 200) catch { 246 + sendJsonStatus(r, "{\"detail\":\"database error\"}", .internal_server_error); 247 + return; 248 + }; 249 + 250 + var output: std.Io.Writer.Allocating = .init(alloc); 251 + var jw: json.Stringify = .{ .writer = &output.writer }; 252 + 253 + jw.beginArray() catch { 254 + sendJsonStatus(r, "{\"detail\":\"serialize error\"}", .internal_server_error); 255 + return; 256 + }; 257 + for (types) |bt| { 258 + jw.write(BlockTypeJson.fromRow(bt)) catch continue; 259 + } 260 + jw.endArray() catch {}; 261 + 262 + sendJson(r, output.toOwnedSlice() catch "[]"); 263 + } 264 + 265 + fn getDocumentByName(r: zap.Request, path: []const u8) !void { 266 + var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); 267 + defer arena.deinit(); 268 + const alloc = arena.allocator(); 269 + 270 + // parse /block_types/slug/{slug}/block_documents/name/{name} 271 + const slug_start = "/block_types/slug/".len; 272 + const docs_idx = mem.indexOf(u8, path, "/block_documents/name/") orelse { 273 + sendJsonStatus(r, "{\"detail\":\"invalid path\"}", .bad_request); 274 + return; 275 + }; 276 + const slug = path[slug_start..docs_idx]; 277 + const name = path[docs_idx + "/block_documents/name/".len ..]; 278 + 279 + const doc = db.block_documents.getByTypeSlugAndName(alloc, slug, name) catch null orelse { 280 + sendJsonStatus(r, "{\"detail\":\"block document not found\"}", .not_found); 281 + return; 282 + }; 283 + 284 + // get associated block type and schema for full response 285 + const bt = db.block_types.getById(alloc, doc.block_type_id) catch null; 286 + const bs = db.block_schemas.getById(alloc, doc.block_schema_id) catch null; 287 + 288 + try sendBlockDocumentResponse(r, alloc, doc, bt, bs, .ok); 289 + } 290 + 291 + fn listDocumentsForType(r: zap.Request, path: []const u8) !void { 292 + var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); 293 + defer arena.deinit(); 294 + const alloc = arena.allocator(); 295 + 296 + // parse /block_types/slug/{slug}/block_documents 297 + const slug_start = "/block_types/slug/".len; 298 + const docs_idx = mem.indexOf(u8, path, "/block_documents") orelse { 299 + sendJsonStatus(r, "{\"detail\":\"invalid path\"}", .bad_request); 300 + return; 301 + }; 302 + const slug = path[slug_start..docs_idx]; 303 + 304 + const docs = db.block_documents.listByTypeSlug(alloc, slug, 200) catch { 305 + sendJsonStatus(r, "{\"detail\":\"database error\"}", .internal_server_error); 306 + return; 307 + }; 308 + 309 + var output: std.Io.Writer.Allocating = .init(alloc); 310 + var jw: json.Stringify = .{ .writer = &output.writer }; 311 + 312 + jw.beginArray() catch { 313 + sendJsonStatus(r, "{\"detail\":\"serialize error\"}", .internal_server_error); 314 + return; 315 + }; 316 + for (docs) |doc| { 317 + jw.beginObject() catch continue; 318 + 319 + jw.objectField("id") catch continue; 320 + jw.write(doc.id) catch continue; 321 + 322 + jw.objectField("created") catch continue; 323 + jw.write(doc.created) catch continue; 324 + 325 + jw.objectField("updated") catch continue; 326 + jw.write(doc.updated) catch continue; 327 + 328 + jw.objectField("name") catch continue; 329 + jw.write(doc.name) catch continue; 330 + 331 + jw.objectField("data") catch continue; 332 + jw.beginWriteRaw() catch continue; 333 + jw.writer.writeAll(doc.data) catch continue; 334 + jw.endWriteRaw(); 335 + 336 + jw.objectField("is_anonymous") catch continue; 337 + jw.write(doc.is_anonymous) catch continue; 338 + 339 + jw.objectField("block_type_id") catch continue; 340 + jw.write(doc.block_type_id) catch continue; 341 + 342 + jw.objectField("block_schema_id") catch continue; 343 + jw.write(doc.block_schema_id) catch continue; 344 + 345 + jw.endObject() catch continue; 346 + } 347 + jw.endArray() catch {}; 348 + 349 + sendJson(r, output.toOwnedSlice() catch "[]"); 350 + } 351 + 352 + fn sendBlockDocumentResponse( 353 + r: zap.Request, 354 + alloc: std.mem.Allocator, 355 + doc: db.block_documents.BlockDocumentRow, 356 + bt: ?db.block_types.BlockTypeRow, 357 + bs: ?db.block_schemas.BlockSchemaRow, 358 + status: zap.http.StatusCode, 359 + ) !void { 360 + var output: std.Io.Writer.Allocating = .init(alloc); 361 + var jw: json.Stringify = .{ .writer = &output.writer }; 362 + 363 + jw.beginObject() catch return; 364 + 365 + jw.objectField("id") catch return; 366 + jw.write(doc.id) catch return; 367 + 368 + jw.objectField("created") catch return; 369 + jw.write(doc.created) catch return; 370 + 371 + jw.objectField("updated") catch return; 372 + jw.write(doc.updated) catch return; 373 + 374 + jw.objectField("name") catch return; 375 + jw.write(doc.name) catch return; 376 + 377 + jw.objectField("data") catch return; 378 + jw.beginWriteRaw() catch return; 379 + jw.writer.writeAll(doc.data) catch return; 380 + jw.endWriteRaw(); 381 + 382 + jw.objectField("is_anonymous") catch return; 383 + jw.write(doc.is_anonymous) catch return; 384 + 385 + jw.objectField("block_type_id") catch return; 386 + jw.write(doc.block_type_id) catch return; 387 + 388 + jw.objectField("block_schema_id") catch return; 389 + jw.write(doc.block_schema_id) catch return; 390 + 391 + jw.objectField("block_type") catch return; 392 + if (bt) |t| { 393 + jw.write(BlockTypeJson.fromRow(t)) catch return; 394 + } else { 395 + jw.write(null) catch return; 396 + } 397 + 398 + jw.objectField("block_schema") catch return; 399 + if (bs) |s| { 400 + jw.beginObject() catch return; 401 + 402 + jw.objectField("id") catch return; 403 + jw.write(s.id) catch return; 404 + 405 + jw.objectField("created") catch return; 406 + jw.write(s.created) catch return; 407 + 408 + jw.objectField("updated") catch return; 409 + jw.write(s.updated) catch return; 410 + 411 + jw.objectField("checksum") catch return; 412 + jw.write(s.checksum) catch return; 413 + 414 + jw.objectField("fields") catch return; 415 + jw.beginWriteRaw() catch return; 416 + jw.writer.writeAll(s.fields) catch return; 417 + jw.endWriteRaw(); 418 + 419 + jw.objectField("capabilities") catch return; 420 + jw.beginWriteRaw() catch return; 421 + jw.writer.writeAll(s.capabilities) catch return; 422 + jw.endWriteRaw(); 423 + 424 + jw.objectField("version") catch return; 425 + jw.write(s.version) catch return; 426 + 427 + jw.objectField("block_type_id") catch return; 428 + jw.write(s.block_type_id) catch return; 429 + 430 + jw.endObject() catch return; 431 + } else { 432 + jw.write(null) catch return; 433 + } 434 + 435 + jw.objectField("block_document_references") catch return; 436 + jw.beginObject() catch return; 437 + jw.endObject() catch return; 438 + 439 + jw.endObject() catch return; 440 + 441 + sendJsonStatus(r, output.toOwnedSlice() catch "{}", status); 442 + }
+9
src/api/routes.zig
··· 7 7 pub const flow_runs = @import("flow_runs.zig"); 8 8 pub const task_runs = @import("task_runs.zig"); 9 9 pub const logs = @import("logs.zig"); 10 + pub const block_types = @import("block_types.zig"); 11 + pub const block_schemas = @import("block_schemas.zig"); 12 + pub const block_documents = @import("block_documents.zig"); 10 13 11 14 pub fn handle(r: zap.Request) !void { 12 15 const target = r.path orelse "/"; ··· 33 36 try flow_runs.handle(r); 34 37 } else if (std.mem.startsWith(u8, target, "/api/task_runs") or std.mem.startsWith(u8, target, "/task_runs")) { 35 38 try task_runs.handle(r); 39 + } else if (std.mem.startsWith(u8, target, "/api/block_types") or std.mem.startsWith(u8, target, "/block_types")) { 40 + try block_types.handle(r); 41 + } else if (std.mem.startsWith(u8, target, "/api/block_schemas") or std.mem.startsWith(u8, target, "/block_schemas")) { 42 + try block_schemas.handle(r); 43 + } else if (std.mem.startsWith(u8, target, "/api/block_documents") or std.mem.startsWith(u8, target, "/block_documents")) { 44 + try block_documents.handle(r); 36 45 } else { 37 46 try sendNotFound(r); 38 47 }
+218
src/db/block_documents.zig
··· 1 + const std = @import("std"); 2 + const Allocator = std.mem.Allocator; 3 + 4 + const sqlite = @import("sqlite.zig"); 5 + const log = @import("../logging.zig"); 6 + 7 + pub const BlockDocumentRow = struct { 8 + id: []const u8, 9 + created: []const u8, 10 + updated: []const u8, 11 + name: ?[]const u8, 12 + data: []const u8, 13 + is_anonymous: bool, 14 + block_type_id: []const u8, 15 + block_type_name: ?[]const u8, 16 + block_schema_id: []const u8, 17 + }; 18 + 19 + pub fn insert( 20 + id: []const u8, 21 + name: ?[]const u8, 22 + data: []const u8, 23 + is_anonymous: bool, 24 + block_type_id: []const u8, 25 + block_type_name: ?[]const u8, 26 + block_schema_id: []const u8, 27 + ) !void { 28 + sqlite.mutex.lock(); 29 + defer sqlite.mutex.unlock(); 30 + 31 + sqlite.conn.exec( 32 + \\INSERT INTO block_document (id, name, data, is_anonymous, block_type_id, block_type_name, block_schema_id) 33 + \\VALUES (?, ?, ?, ?, ?, ?, ?) 34 + , .{ 35 + id, 36 + name, 37 + data, 38 + @as(i32, if (is_anonymous) 1 else 0), 39 + block_type_id, 40 + block_type_name, 41 + block_schema_id, 42 + }) catch |err| { 43 + log.err("database", "insert block_document error: {}", .{err}); 44 + return err; 45 + }; 46 + } 47 + 48 + pub fn getById(alloc: Allocator, id: []const u8) !?BlockDocumentRow { 49 + sqlite.mutex.lock(); 50 + defer sqlite.mutex.unlock(); 51 + 52 + const row = sqlite.conn.row( 53 + \\SELECT id, created, updated, name, data, is_anonymous, 54 + \\ block_type_id, block_type_name, block_schema_id 55 + \\FROM block_document WHERE id = ? 56 + , .{id}) catch return null; 57 + 58 + if (row) |r| { 59 + defer r.deinit(); 60 + return BlockDocumentRow{ 61 + .id = try alloc.dupe(u8, r.text(0)), 62 + .created = try alloc.dupe(u8, r.text(1)), 63 + .updated = try alloc.dupe(u8, r.text(2)), 64 + .name = if (r.text(3).len > 0) try alloc.dupe(u8, r.text(3)) else null, 65 + .data = try alloc.dupe(u8, r.text(4)), 66 + .is_anonymous = r.int(5) != 0, 67 + .block_type_id = try alloc.dupe(u8, r.text(6)), 68 + .block_type_name = if (r.text(7).len > 0) try alloc.dupe(u8, r.text(7)) else null, 69 + .block_schema_id = try alloc.dupe(u8, r.text(8)), 70 + }; 71 + } 72 + return null; 73 + } 74 + 75 + pub fn getByTypeSlugAndName( 76 + alloc: Allocator, 77 + block_type_slug: []const u8, 78 + name: []const u8, 79 + ) !?BlockDocumentRow { 80 + sqlite.mutex.lock(); 81 + defer sqlite.mutex.unlock(); 82 + 83 + const row = sqlite.conn.row( 84 + \\SELECT bd.id, bd.created, bd.updated, bd.name, bd.data, bd.is_anonymous, 85 + \\ bd.block_type_id, bd.block_type_name, bd.block_schema_id 86 + \\FROM block_document bd 87 + \\JOIN block_type bt ON bd.block_type_id = bt.id 88 + \\WHERE bt.slug = ? AND bd.name = ? 89 + , .{ block_type_slug, name }) catch return null; 90 + 91 + if (row) |r| { 92 + defer r.deinit(); 93 + return BlockDocumentRow{ 94 + .id = try alloc.dupe(u8, r.text(0)), 95 + .created = try alloc.dupe(u8, r.text(1)), 96 + .updated = try alloc.dupe(u8, r.text(2)), 97 + .name = if (r.text(3).len > 0) try alloc.dupe(u8, r.text(3)) else null, 98 + .data = try alloc.dupe(u8, r.text(4)), 99 + .is_anonymous = r.int(5) != 0, 100 + .block_type_id = try alloc.dupe(u8, r.text(6)), 101 + .block_type_name = if (r.text(7).len > 0) try alloc.dupe(u8, r.text(7)) else null, 102 + .block_schema_id = try alloc.dupe(u8, r.text(8)), 103 + }; 104 + } 105 + return null; 106 + } 107 + 108 + pub fn update( 109 + id: []const u8, 110 + data: []const u8, 111 + block_schema_id: ?[]const u8, 112 + ) !void { 113 + sqlite.mutex.lock(); 114 + defer sqlite.mutex.unlock(); 115 + 116 + if (block_schema_id) |schema_id| { 117 + sqlite.conn.exec( 118 + \\UPDATE block_document SET data = ?, block_schema_id = ?, updated = datetime('now') 119 + \\WHERE id = ? 120 + , .{ data, schema_id, id }) catch |err| { 121 + log.err("database", "update block_document error: {}", .{err}); 122 + return err; 123 + }; 124 + } else { 125 + sqlite.conn.exec( 126 + \\UPDATE block_document SET data = ?, updated = datetime('now') 127 + \\WHERE id = ? 128 + , .{ data, id }) catch |err| { 129 + log.err("database", "update block_document error: {}", .{err}); 130 + return err; 131 + }; 132 + } 133 + } 134 + 135 + pub fn list(alloc: Allocator, limit: usize) ![]BlockDocumentRow { 136 + sqlite.mutex.lock(); 137 + defer sqlite.mutex.unlock(); 138 + 139 + var results = std.ArrayListUnmanaged(BlockDocumentRow){}; 140 + errdefer results.deinit(alloc); 141 + 142 + var rows = sqlite.conn.rows( 143 + \\SELECT id, created, updated, name, data, is_anonymous, 144 + \\ block_type_id, block_type_name, block_schema_id 145 + \\FROM block_document ORDER BY created DESC LIMIT ? 146 + , .{@as(i64, @intCast(limit))}) catch |err| { 147 + log.err("database", "list block_documents error: {}", .{err}); 148 + return err; 149 + }; 150 + defer rows.deinit(); 151 + 152 + while (rows.next()) |r| { 153 + try results.append(alloc, .{ 154 + .id = try alloc.dupe(u8, r.text(0)), 155 + .created = try alloc.dupe(u8, r.text(1)), 156 + .updated = try alloc.dupe(u8, r.text(2)), 157 + .name = if (r.text(3).len > 0) try alloc.dupe(u8, r.text(3)) else null, 158 + .data = try alloc.dupe(u8, r.text(4)), 159 + .is_anonymous = r.int(5) != 0, 160 + .block_type_id = try alloc.dupe(u8, r.text(6)), 161 + .block_type_name = if (r.text(7).len > 0) try alloc.dupe(u8, r.text(7)) else null, 162 + .block_schema_id = try alloc.dupe(u8, r.text(8)), 163 + }); 164 + } 165 + 166 + return results.toOwnedSlice(alloc); 167 + } 168 + 169 + pub fn delete(id: []const u8) !bool { 170 + sqlite.mutex.lock(); 171 + defer sqlite.mutex.unlock(); 172 + 173 + sqlite.conn.exec( 174 + \\DELETE FROM block_document WHERE id = ? 175 + , .{id}) catch |err| { 176 + log.err("database", "delete block_document error: {}", .{err}); 177 + return err; 178 + }; 179 + 180 + return sqlite.conn.changes() > 0; 181 + } 182 + 183 + pub fn listByTypeSlug(alloc: Allocator, block_type_slug: []const u8, limit: usize) ![]BlockDocumentRow { 184 + sqlite.mutex.lock(); 185 + defer sqlite.mutex.unlock(); 186 + 187 + var results = std.ArrayListUnmanaged(BlockDocumentRow){}; 188 + errdefer results.deinit(alloc); 189 + 190 + var rows = sqlite.conn.rows( 191 + \\SELECT bd.id, bd.created, bd.updated, bd.name, bd.data, bd.is_anonymous, 192 + \\ bd.block_type_id, bd.block_type_name, bd.block_schema_id 193 + \\FROM block_document bd 194 + \\JOIN block_type bt ON bd.block_type_id = bt.id 195 + \\WHERE bt.slug = ? 196 + \\ORDER BY bd.created DESC LIMIT ? 197 + , .{ block_type_slug, @as(i64, @intCast(limit)) }) catch |err| { 198 + log.err("database", "list block_documents by type error: {}", .{err}); 199 + return err; 200 + }; 201 + defer rows.deinit(); 202 + 203 + while (rows.next()) |r| { 204 + try results.append(alloc, .{ 205 + .id = try alloc.dupe(u8, r.text(0)), 206 + .created = try alloc.dupe(u8, r.text(1)), 207 + .updated = try alloc.dupe(u8, r.text(2)), 208 + .name = if (r.text(3).len > 0) try alloc.dupe(u8, r.text(3)) else null, 209 + .data = try alloc.dupe(u8, r.text(4)), 210 + .is_anonymous = r.int(5) != 0, 211 + .block_type_id = try alloc.dupe(u8, r.text(6)), 212 + .block_type_name = if (r.text(7).len > 0) try alloc.dupe(u8, r.text(7)) else null, 213 + .block_schema_id = try alloc.dupe(u8, r.text(8)), 214 + }); 215 + } 216 + 217 + return results.toOwnedSlice(alloc); 218 + }
+128
src/db/block_schemas.zig
··· 1 + const std = @import("std"); 2 + const Allocator = std.mem.Allocator; 3 + 4 + const sqlite = @import("sqlite.zig"); 5 + const log = @import("../logging.zig"); 6 + 7 + pub const BlockSchemaRow = struct { 8 + id: []const u8, 9 + created: []const u8, 10 + updated: []const u8, 11 + checksum: []const u8, 12 + fields: []const u8, 13 + capabilities: []const u8, 14 + version: []const u8, 15 + block_type_id: []const u8, 16 + }; 17 + 18 + pub fn insert( 19 + id: []const u8, 20 + checksum: []const u8, 21 + fields: []const u8, 22 + capabilities: []const u8, 23 + version: []const u8, 24 + block_type_id: []const u8, 25 + ) !void { 26 + sqlite.mutex.lock(); 27 + defer sqlite.mutex.unlock(); 28 + 29 + sqlite.conn.exec( 30 + \\INSERT INTO block_schema (id, checksum, fields, capabilities, version, block_type_id) 31 + \\VALUES (?, ?, ?, ?, ?, ?) 32 + , .{ id, checksum, fields, capabilities, version, block_type_id }) catch |err| { 33 + log.err("database", "insert block_schema error: {}", .{err}); 34 + return err; 35 + }; 36 + } 37 + 38 + pub fn getByChecksum( 39 + alloc: Allocator, 40 + checksum: []const u8, 41 + version: ?[]const u8, 42 + ) !?BlockSchemaRow { 43 + sqlite.mutex.lock(); 44 + defer sqlite.mutex.unlock(); 45 + 46 + const row = if (version) |v| 47 + sqlite.conn.row( 48 + \\SELECT id, created, updated, checksum, fields, capabilities, version, block_type_id 49 + \\FROM block_schema WHERE checksum = ? AND version = ? 50 + , .{ checksum, v }) catch return null 51 + else 52 + sqlite.conn.row( 53 + \\SELECT id, created, updated, checksum, fields, capabilities, version, block_type_id 54 + \\FROM block_schema WHERE checksum = ? 55 + , .{checksum}) catch return null; 56 + 57 + if (row) |r| { 58 + defer r.deinit(); 59 + return BlockSchemaRow{ 60 + .id = try alloc.dupe(u8, r.text(0)), 61 + .created = try alloc.dupe(u8, r.text(1)), 62 + .updated = try alloc.dupe(u8, r.text(2)), 63 + .checksum = try alloc.dupe(u8, r.text(3)), 64 + .fields = try alloc.dupe(u8, r.text(4)), 65 + .capabilities = try alloc.dupe(u8, r.text(5)), 66 + .version = try alloc.dupe(u8, r.text(6)), 67 + .block_type_id = try alloc.dupe(u8, r.text(7)), 68 + }; 69 + } 70 + return null; 71 + } 72 + 73 + pub fn getById(alloc: Allocator, id: []const u8) !?BlockSchemaRow { 74 + sqlite.mutex.lock(); 75 + defer sqlite.mutex.unlock(); 76 + 77 + const row = sqlite.conn.row( 78 + \\SELECT id, created, updated, checksum, fields, capabilities, version, block_type_id 79 + \\FROM block_schema WHERE id = ? 80 + , .{id}) catch return null; 81 + 82 + if (row) |r| { 83 + defer r.deinit(); 84 + return BlockSchemaRow{ 85 + .id = try alloc.dupe(u8, r.text(0)), 86 + .created = try alloc.dupe(u8, r.text(1)), 87 + .updated = try alloc.dupe(u8, r.text(2)), 88 + .checksum = try alloc.dupe(u8, r.text(3)), 89 + .fields = try alloc.dupe(u8, r.text(4)), 90 + .capabilities = try alloc.dupe(u8, r.text(5)), 91 + .version = try alloc.dupe(u8, r.text(6)), 92 + .block_type_id = try alloc.dupe(u8, r.text(7)), 93 + }; 94 + } 95 + return null; 96 + } 97 + 98 + pub fn list(alloc: Allocator, limit: usize) ![]BlockSchemaRow { 99 + sqlite.mutex.lock(); 100 + defer sqlite.mutex.unlock(); 101 + 102 + var results = std.ArrayListUnmanaged(BlockSchemaRow){}; 103 + errdefer results.deinit(alloc); 104 + 105 + var rows = sqlite.conn.rows( 106 + \\SELECT id, created, updated, checksum, fields, capabilities, version, block_type_id 107 + \\FROM block_schema ORDER BY created DESC LIMIT ? 108 + , .{@as(i64, @intCast(limit))}) catch |err| { 109 + log.err("database", "list block_schemas error: {}", .{err}); 110 + return err; 111 + }; 112 + defer rows.deinit(); 113 + 114 + while (rows.next()) |r| { 115 + try results.append(alloc, .{ 116 + .id = try alloc.dupe(u8, r.text(0)), 117 + .created = try alloc.dupe(u8, r.text(1)), 118 + .updated = try alloc.dupe(u8, r.text(2)), 119 + .checksum = try alloc.dupe(u8, r.text(3)), 120 + .fields = try alloc.dupe(u8, r.text(4)), 121 + .capabilities = try alloc.dupe(u8, r.text(5)), 122 + .version = try alloc.dupe(u8, r.text(6)), 123 + .block_type_id = try alloc.dupe(u8, r.text(7)), 124 + }); 125 + } 126 + 127 + return results.toOwnedSlice(alloc); 128 + }
+161
src/db/block_types.zig
··· 1 + const std = @import("std"); 2 + const Allocator = std.mem.Allocator; 3 + 4 + const sqlite = @import("sqlite.zig"); 5 + const log = @import("../logging.zig"); 6 + 7 + pub const BlockTypeRow = struct { 8 + id: []const u8, 9 + created: []const u8, 10 + updated: []const u8, 11 + name: []const u8, 12 + slug: []const u8, 13 + logo_url: ?[]const u8, 14 + documentation_url: ?[]const u8, 15 + description: ?[]const u8, 16 + code_example: ?[]const u8, 17 + is_protected: bool, 18 + }; 19 + 20 + pub fn insert( 21 + id: []const u8, 22 + name: []const u8, 23 + slug: []const u8, 24 + logo_url: ?[]const u8, 25 + documentation_url: ?[]const u8, 26 + description: ?[]const u8, 27 + code_example: ?[]const u8, 28 + is_protected: bool, 29 + ) !void { 30 + sqlite.mutex.lock(); 31 + defer sqlite.mutex.unlock(); 32 + 33 + sqlite.conn.exec( 34 + \\INSERT INTO block_type (id, name, slug, logo_url, documentation_url, description, code_example, is_protected) 35 + \\VALUES (?, ?, ?, ?, ?, ?, ?, ?) 36 + , .{ 37 + id, 38 + name, 39 + slug, 40 + logo_url, 41 + documentation_url, 42 + description, 43 + code_example, 44 + @as(i32, if (is_protected) 1 else 0), 45 + }) catch |err| { 46 + log.err("database", "insert block_type error: {}", .{err}); 47 + return err; 48 + }; 49 + } 50 + 51 + pub fn getBySlug(alloc: Allocator, slug: []const u8) !?BlockTypeRow { 52 + sqlite.mutex.lock(); 53 + defer sqlite.mutex.unlock(); 54 + 55 + const row = sqlite.conn.row( 56 + \\SELECT id, created, updated, name, slug, logo_url, documentation_url, 57 + \\ description, code_example, is_protected 58 + \\FROM block_type WHERE slug = ? 59 + , .{slug}) catch return null; 60 + 61 + if (row) |r| { 62 + defer r.deinit(); 63 + return BlockTypeRow{ 64 + .id = try alloc.dupe(u8, r.text(0)), 65 + .created = try alloc.dupe(u8, r.text(1)), 66 + .updated = try alloc.dupe(u8, r.text(2)), 67 + .name = try alloc.dupe(u8, r.text(3)), 68 + .slug = try alloc.dupe(u8, r.text(4)), 69 + .logo_url = if (r.text(5).len > 0) try alloc.dupe(u8, r.text(5)) else null, 70 + .documentation_url = if (r.text(6).len > 0) try alloc.dupe(u8, r.text(6)) else null, 71 + .description = if (r.text(7).len > 0) try alloc.dupe(u8, r.text(7)) else null, 72 + .code_example = if (r.text(8).len > 0) try alloc.dupe(u8, r.text(8)) else null, 73 + .is_protected = r.int(9) != 0, 74 + }; 75 + } 76 + return null; 77 + } 78 + 79 + pub fn getById(alloc: Allocator, id: []const u8) !?BlockTypeRow { 80 + sqlite.mutex.lock(); 81 + defer sqlite.mutex.unlock(); 82 + 83 + const row = sqlite.conn.row( 84 + \\SELECT id, created, updated, name, slug, logo_url, documentation_url, 85 + \\ description, code_example, is_protected 86 + \\FROM block_type WHERE id = ? 87 + , .{id}) catch return null; 88 + 89 + if (row) |r| { 90 + defer r.deinit(); 91 + return BlockTypeRow{ 92 + .id = try alloc.dupe(u8, r.text(0)), 93 + .created = try alloc.dupe(u8, r.text(1)), 94 + .updated = try alloc.dupe(u8, r.text(2)), 95 + .name = try alloc.dupe(u8, r.text(3)), 96 + .slug = try alloc.dupe(u8, r.text(4)), 97 + .logo_url = if (r.text(5).len > 0) try alloc.dupe(u8, r.text(5)) else null, 98 + .documentation_url = if (r.text(6).len > 0) try alloc.dupe(u8, r.text(6)) else null, 99 + .description = if (r.text(7).len > 0) try alloc.dupe(u8, r.text(7)) else null, 100 + .code_example = if (r.text(8).len > 0) try alloc.dupe(u8, r.text(8)) else null, 101 + .is_protected = r.int(9) != 0, 102 + }; 103 + } 104 + return null; 105 + } 106 + 107 + pub fn update( 108 + id: []const u8, 109 + logo_url: ?[]const u8, 110 + documentation_url: ?[]const u8, 111 + description: ?[]const u8, 112 + code_example: ?[]const u8, 113 + ) !void { 114 + sqlite.mutex.lock(); 115 + defer sqlite.mutex.unlock(); 116 + 117 + sqlite.conn.exec( 118 + \\UPDATE block_type SET 119 + \\ logo_url = ?, documentation_url = ?, description = ?, code_example = ?, 120 + \\ updated = datetime('now') 121 + \\WHERE id = ? 122 + , .{ logo_url, documentation_url, description, code_example, id }) catch |err| { 123 + log.err("database", "update block_type error: {}", .{err}); 124 + return err; 125 + }; 126 + } 127 + 128 + pub fn list(alloc: Allocator, limit: usize) ![]BlockTypeRow { 129 + sqlite.mutex.lock(); 130 + defer sqlite.mutex.unlock(); 131 + 132 + var results = std.ArrayListUnmanaged(BlockTypeRow){}; 133 + errdefer results.deinit(alloc); 134 + 135 + var rows = sqlite.conn.rows( 136 + \\SELECT id, created, updated, name, slug, logo_url, documentation_url, 137 + \\ description, code_example, is_protected 138 + \\FROM block_type ORDER BY created DESC LIMIT ? 139 + , .{@as(i64, @intCast(limit))}) catch |err| { 140 + log.err("database", "list block_types error: {}", .{err}); 141 + return err; 142 + }; 143 + defer rows.deinit(); 144 + 145 + while (rows.next()) |r| { 146 + try results.append(alloc, .{ 147 + .id = try alloc.dupe(u8, r.text(0)), 148 + .created = try alloc.dupe(u8, r.text(1)), 149 + .updated = try alloc.dupe(u8, r.text(2)), 150 + .name = try alloc.dupe(u8, r.text(3)), 151 + .slug = try alloc.dupe(u8, r.text(4)), 152 + .logo_url = if (r.text(5).len > 0) try alloc.dupe(u8, r.text(5)) else null, 153 + .documentation_url = if (r.text(6).len > 0) try alloc.dupe(u8, r.text(6)) else null, 154 + .description = if (r.text(7).len > 0) try alloc.dupe(u8, r.text(7)) else null, 155 + .code_example = if (r.text(8).len > 0) try alloc.dupe(u8, r.text(8)) else null, 156 + .is_protected = r.int(9) != 0, 157 + }); 158 + } 159 + 160 + return results.toOwnedSlice(alloc); 161 + }
+74
src/db/sqlite.zig
··· 8 8 pub const flow_runs = @import("flow_runs.zig"); 9 9 pub const task_runs = @import("task_runs.zig"); 10 10 pub const events = @import("events.zig"); 11 + pub const block_types = @import("block_types.zig"); 12 + pub const block_schemas = @import("block_schemas.zig"); 13 + pub const block_documents = @import("block_documents.zig"); 11 14 12 15 // re-export types for compatibility 13 16 pub const FlowRow = flows.FlowRow; ··· 169 172 return err; 170 173 }; 171 174 175 + // block tables 176 + conn.execNoArgs( 177 + \\CREATE TABLE IF NOT EXISTS block_type ( 178 + \\ id TEXT PRIMARY KEY, 179 + \\ created TEXT DEFAULT (datetime('now')), 180 + \\ updated TEXT DEFAULT (datetime('now')), 181 + \\ name TEXT NOT NULL, 182 + \\ slug TEXT NOT NULL UNIQUE, 183 + \\ logo_url TEXT, 184 + \\ documentation_url TEXT, 185 + \\ description TEXT, 186 + \\ code_example TEXT, 187 + \\ is_protected INTEGER DEFAULT 0 188 + \\) 189 + ) catch |err| { 190 + log.err("database", "failed to create block_type table: {}", .{err}); 191 + return err; 192 + }; 193 + 194 + conn.execNoArgs( 195 + \\CREATE TABLE IF NOT EXISTS block_schema ( 196 + \\ id TEXT PRIMARY KEY, 197 + \\ created TEXT DEFAULT (datetime('now')), 198 + \\ updated TEXT DEFAULT (datetime('now')), 199 + \\ checksum TEXT NOT NULL, 200 + \\ fields TEXT NOT NULL DEFAULT '{}', 201 + \\ capabilities TEXT NOT NULL DEFAULT '[]', 202 + \\ version TEXT NOT NULL DEFAULT '1', 203 + \\ block_type_id TEXT NOT NULL REFERENCES block_type(id) ON DELETE CASCADE, 204 + \\ UNIQUE(checksum, version) 205 + \\) 206 + ) catch |err| { 207 + log.err("database", "failed to create block_schema table: {}", .{err}); 208 + return err; 209 + }; 210 + 211 + conn.execNoArgs( 212 + \\CREATE TABLE IF NOT EXISTS block_document ( 213 + \\ id TEXT PRIMARY KEY, 214 + \\ created TEXT DEFAULT (datetime('now')), 215 + \\ updated TEXT DEFAULT (datetime('now')), 216 + \\ name TEXT, 217 + \\ data TEXT NOT NULL DEFAULT '{}', 218 + \\ is_anonymous INTEGER DEFAULT 0, 219 + \\ block_type_id TEXT NOT NULL REFERENCES block_type(id), 220 + \\ block_type_name TEXT, 221 + \\ block_schema_id TEXT NOT NULL REFERENCES block_schema(id), 222 + \\ UNIQUE(block_type_id, name) 223 + \\) 224 + ) catch |err| { 225 + log.err("database", "failed to create block_document table: {}", .{err}); 226 + return err; 227 + }; 228 + 172 229 // indexes 173 230 conn.execNoArgs( 174 231 "CREATE INDEX IF NOT EXISTS ix_flow_run__state_type ON flow_run(state_type)", ··· 196 253 ) catch {}; 197 254 conn.execNoArgs( 198 255 "CREATE INDEX IF NOT EXISTS ix_events__occurred_id ON events(occurred, id)", 256 + ) catch {}; 257 + 258 + // block indexes 259 + conn.execNoArgs( 260 + "CREATE INDEX IF NOT EXISTS ix_block_schema__block_type_id ON block_schema(block_type_id)", 261 + ) catch {}; 262 + conn.execNoArgs( 263 + "CREATE INDEX IF NOT EXISTS ix_block_schema__checksum ON block_schema(checksum)", 264 + ) catch {}; 265 + conn.execNoArgs( 266 + "CREATE INDEX IF NOT EXISTS ix_block_document__block_type_id ON block_document(block_type_id)", 267 + ) catch {}; 268 + conn.execNoArgs( 269 + "CREATE INDEX IF NOT EXISTS ix_block_document__block_schema_id ON block_document(block_schema_id)", 270 + ) catch {}; 271 + conn.execNoArgs( 272 + "CREATE INDEX IF NOT EXISTS ix_block_document__name ON block_document(name)", 199 273 ) catch {}; 200 274 }