╭ changed ────╮ │src/event.zig│ ╰─────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1676 // │ │1677 // reading from xitdb │ │1678 // │ │1679 + │ │1680 +// read just `Fields`, a subset of `T.Record`, from the record with event id │ │1681 +// `id`, or null when there is none │ │1682 +pub fn readRecordSubset( │ │1683 + comptime T: type, │ │1684 + comptime Fields: type, │ │1685 + comptime DB: type, │ │1686 + comptime hash_kind: hash.HashKind, │ │1687 + haxy_moment: DB.HashMap(.read_only), │ │1688 + arena: *std.heap.ArenaAllocator, │ │1689 + id: []const u8, │ │1690 +) !?Fields { │ │1691 + comptime assertFieldSubset(Fields, T.Record); │ │1692 + const records_cursor = try haxy_moment.getCursor(hash.hashInt(hash_kind, T.record_map_key)) orelse return null;│ │1693 + const records = try DB.HashMap(.read_only).init(records_cursor); │ │1694 + const record_cursor = try records.getCursor(hash.hashInt(hash_kind, id)) orelse return null; │ │1695 + return try read(Fields, DB, hash_kind, arena, try DB.HashMap(.read_only).init(record_cursor)); │ │1696 +} │ │1697 │ │1698 +fn assertFieldSubset(comptime Sub: type, comptime Full: type) void { │ │1699 + for (@typeInfo(Sub).@"struct".fields) |field| { │ │1700 + if (!@hasField(Full, field.name)) @compileError(@typeName(Full) ++ " has no field " ++ field.name); │ │1701 + const FullField = @FieldType(Full, field.name); │ │1702 + if (@typeInfo(field.type) == .@"struct") { │ │1703 + assertFieldSubset(field.type, FullField); │ │1704 + } else if (field.type != FullField) { │ │1705 + @compileError(@typeName(Full) ++ "." ++ field.name ++ " is a " ++ @typeName(FullField)); │ │1706 + } │ │1707 + } │ │1708 +} │ │1709 + │ │1710 pub fn read( │ │1711 comptime T: type, │ │1712 comptime DB: type, │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ──────────────╮ │src/ui/Repo/Comment.zig│ ╰───────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │117 const comment = (try evt.Comment.readById(DB, hash_kind, haxy_moment, arena, id_bytes)) orelse return null; │ │118 const parent_author: ?ui.Author = if (!std.mem.eql(u8, &comment.event.parent_id, &comment.event.thread_id)) blk: { │ │119 const parent_bytes = idBytes(&comment.event.parent_id) orelse break :blk null; │ │120 - const parent = (try evt.Comment.readById(DB, hash_kind, haxy_moment, arena, &parent_bytes)) orelse break :blk null; │ │120 + const parent = (try evt.readRecordSubset(evt.Comment, struct { author_email: ?[]const u8 }, DB, hash_kind, haxy_moment, arena, &parent_bytes)) orelse break :blk null;│ │121 break :blk try ui.Author.initFromEmail(admin_moment, arena, parent.author_email); │ │122 } else null; │ │123 return .{ │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ─────────────╮ │src/ui/Repo/Events.zig│ ╰──────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │238 fn formatId(aa: std.mem.Allocator, id: *const [evt.event_id_size]u8) ![]const u8 { │ │239 const hex = std.fmt.bytesToHex(id.*, .lower); │ │240 return try aa.dupe(u8, &hex); │ │241 -} │ │242 - │ │243 -fn readRecord( │ │244 - comptime T: type, │ │245 - comptime hash_kind: hash.HashKind, │ │246 - arena: *std.heap.ArenaAllocator, │ │247 - haxy_moment: evt.EventDB(hash_kind).HashMap(.read_only), │ │248 - id: *const [evt.event_id_size]u8, │ │249 -) !?T.Record { │ │250 - const DB = evt.EventDB(hash_kind); │ │251 - const records_cursor = try haxy_moment.getCursor(hash.hashInt(hash_kind, T.record_map_key)) orelse return null;│ │252 - const records = try DB.HashMap(.read_only).init(records_cursor); │ │253 - const record_cursor = try records.getCursor(hash.hashInt(hash_kind, id)) orelse return null; │ │254 - const record = try DB.HashMap(.read_only).init(record_cursor); │ │255 - return try evt.read(T.Record, DB, hash_kind, arena, record); │ │241 } │ │242 │ │243 const EventMeta = struct { │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │265 .patchrev => evt.PatchRev, │ │266 .patch => evt.Patch, │ │267 }; │ │283 - const record = (try readRecord(T, hash_kind, arena, haxy_moment, id)) orelse return error.NotFound; │ │284 - break :blk .{ .updated_order = record.updated_order, .removed = record.removed }; │ │268 + break :blk (try evt.readRecordSubset(T, EventMeta, evt.EventDB(hash_kind), hash_kind, haxy_moment, arena, id)) orelse return error.NotFound;│ │269 }, │ │270 }; │ │271 } │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────╮ │281 kind: evt.EventKind, │ │282 id: *const [evt.event_id_size]u8, │ │283 ) !?Item { │ │284 + const DB = evt.EventDB(hash_kind); │ │285 + // the parts of a record a row shows │ │286 + const Authored = struct { author_email: ?[]const u8 }; │ │287 + const Owned = struct { event: struct { user_id: []const u8 } };│ │288 var item: Item = .{ │ │289 .id = try formatId(arena.allocator(), id), │ │290 .kind = kind, │ ╰────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │292 }; │ │293 switch (kind) { │ │294 .user => { │ │307 - const record = (try readRecord(evt.User, hash_kind, arena, haxy_moment, id)) orelse return null; │ │295 + const record = (try evt.readRecordSubset(evt.User, struct { event: struct { email: []const u8 } }, DB, hash_kind, haxy_moment, arena, id)) orelse return null;│ │296 item.author = try ui.Author.initFromEmail(admin_moment, arena, record.event.email); │ │297 }, │ │298 .repo => { │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │311 - const record = (try readRecord(evt.Repo, hash_kind, arena, haxy_moment, id)) orelse return null; │ │299 + const record = (try evt.readRecordSubset(evt.Repo, Owned, DB, hash_kind, haxy_moment, arena, id)) orelse return null;│ │300 if (admin_moment) |moment| { │ │301 if (try evt.User.readById(evt.AdminDB, evt.admin_repo_opts.hash, moment, arena, record.event.user_id)) |user| │ │302 item.author = .{ .user_name = user.event.name }; │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │303 } │ │304 }, │ │305 .fork => { │ │318 - const record = (try readRecord(evt.Fork, hash_kind, arena, haxy_moment, id)) orelse return null; │ │306 + const record = (try evt.readRecordSubset(evt.Fork, Owned, DB, hash_kind, haxy_moment, arena, id)) orelse return null;│ │307 if (admin_moment) |moment| { │ │308 if (try evt.User.readById(evt.AdminDB, evt.admin_repo_opts.hash, moment, arena, record.event.user_id)) |user| │ │309 item.author = .{ .user_name = user.event.name }; │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │310 } │ │311 }, │ │312 .issue => { │ │325 - const record = (try readRecord(evt.Issue, hash_kind, arena, haxy_moment, id)) orelse return null; │ │313 + const record = (try evt.readRecordSubset(evt.Issue, Authored, DB, hash_kind, haxy_moment, arena, id)) orelse return null;│ │314 item.author = try ui.Author.initFromEmail(admin_moment, arena, record.author_email); │ │315 if (!include_view_url) return item; │ │316 const route = ui.RoutablePage.repoThreadCommentsRoute(.issue, identity, item.id, 0) orelse return error.RouteTooLong; │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │317 item.view_url = try route.toUrl(arena); │ │318 }, │ │319 .discuss => { │ │332 - const record = (try readRecord(evt.Discussion, hash_kind, arena, haxy_moment, id)) orelse return null; │ │320 + const record = (try evt.readRecordSubset(evt.Discussion, Authored, DB, hash_kind, haxy_moment, arena, id)) orelse return null;│ │321 item.author = try ui.Author.initFromEmail(admin_moment, arena, record.author_email); │ │322 if (!include_view_url) return item; │ │323 const route = ui.RoutablePage.repoThreadCommentsRoute(.discuss, identity, item.id, 0) orelse return error.RouteTooLong; │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯