╭─────────────╮ │scroll to top│ ╰─────────────╯ ╭ event.zig ────────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
590 .issue = .{ 591 .title = "Login form clears password on validation error", 592 .description = "Submitting an invalid email address resets the password field. Preserve the field value and show an inline validation message.", 593 .tags = "bug,priority-low,ui", 594 }, 595 }, 596 }, 597 .{ 598 .id = events_to_consume[0].id, 599 .event = .{ 600 .issue = .{ 601 .title = "Login form clears password on validation error", 602 .description = "Submitting an invalid email address resets the password field. Preserve the field value and show an inline validation message.", 603 .tags = "bug,priority-medium,ui", 604 }, 605 }, 606 }, 607 }; 608 609 // 610 // insert issues as commits in the repo on different branches 611 // 612 613 { 614 var json: std.Io.Writer.Allocating = .init(allocator); 615 defer json.deinit(); 616 617 for (events_to_consume3, 0..) |event, i| { 618 json.clearRetainingCapacity(); 619 620 try std.json.Stringify.value(event, .{}, &json.writer); 621 622 // commit the event into a special branch 623 _ = try repo.commitAtRef( 624 io, 625 allocator, 626 .{ .parent_oids = &.{merge_oid}, .message = json.written() }, 627 null, 628 switch (i) { 629 0 => evt.events_ref, 630 1 => other_events_ref, 631 else => unreachable, 632 }, 633 ); 634 } 635 } 636 637 // 638 // check out the events branch and merge the other events branch into it 639 // 640 641 { 642 var result = try repo.switchDir(io, allocator, .{ .target = .{ .ref = evt.events_ref } }); 643 defer result.deinit(); 644 645 var merge = try repo.merge(io, allocator, .{ .kind = .full, .action = .{ .new = .{ .source = &.{.{ .ref = other_events_ref }} } } }, null); 646 defer merge.deinit(); 647 648 try std.testing.expect(.success == merge.result); 649 } 650 651 // 652 // consume events into the database 653 // 654 655 try std.testing.expectError(error.MergeConflict, evt.consume(repo_opts, io, allocator, &repo, evt.events_ref)); 656 } 657 658 test "user and repo" { 659 const io = std.testing.io; 660 const allocator = std.testing.allocator; 661 const temp_dir_name = "temp-event-user-and-repo"; 662 663 // create the temp dir 664 const cwd = std.Io.Dir.cwd(); 665 var temp_dir_or_err = cwd.openDir(io, temp_dir_name, .{}); 666 if (temp_dir_or_err) |*temp_dir| { 667 temp_dir.close(io); 668 try cwd.deleteTree(io, temp_dir_name); 669 } else |_| {} 670 var temp_dir = try cwd.createDirPathOpen(io, temp_dir_name, .{}); 671 defer cwd.deleteTree(io, temp_dir_name) catch {}; 672 defer temp_dir.close(io); 673 674 const cwd_path = try std.process.currentPathAlloc(io, allocator); 675 defer allocator.free(cwd_path); 676 677 const work_path = try std.fs.path.join(allocator, &.{ cwd_path, temp_dir_name }); 678 defer allocator.free(work_path); 679 680 const repo_opts: rp.RepoOpts(.xit) = .{ .is_test = true }; 681 const Repo = rp.Repo(.xit, repo_opts); 682 var repo = try Repo.init(io, allocator, .{ .path = work_path }); 683 defer repo.deinit(io, allocator); 684 685 var arena = std.heap.ArenaAllocator.init(allocator); 686 defer arena.deinit(); 687 688 // 689 // define test events 690 // 691 692 var prng = std.Random.DefaultPrng.init(std.testing.random_seed); 693 const user_event_id = evt.EventWithId.randomId(prng.random()); 694 const repo_event_id = evt.EventWithId.randomId(prng.random()); 695 696 var first_password_hash_buf: [evt.User.password_hash_max_len]u8 = undefined; 697 const first_password_hash = try evt.User.hashPassword("correct horse battery staple", &first_password_hash_buf, io); 698 699 var second_password_hash_buf: [evt.User.password_hash_max_len]u8 = undefined; 700 const second_password_hash = try evt.User.hashPassword("Tr0ub4dor&3", &second_password_hash_buf, io); 701 702 const events_to_consume = [_]evt.EventWithId{ 703 .{ 704 .id = std.fmt.bytesToHex(user_event_id, .lower), 705 .event = .{ 706 .user = .{ 707 .name = "alice", 708 .display_name = "Alice Example", 709 .email = "alice@example.test", 710 .password_hash = first_password_hash, 711 }, 712 }, 713 }, 714 // this event edits the previous one because it has the same id 715 .{ 716 .id = std.fmt.bytesToHex(user_event_id, .lower), 717 .event = .{ 718 .user = .{ 719 .name = "alice", 720 .display_name = "Alice Example", 721 .email = "alice@example.test", 722 .password_hash = second_password_hash, 723 }, 724 }, 725 }, 726 .{ 727 .id = std.fmt.bytesToHex(repo_event_id, .lower), 728 .event = .{ 729 .repo = .{ 730 .user_id = &user_event_id, 731 .name = "ziglings", 732 .description = "Learn the Zig programming language by fixing tiny broken programs", 733 .enable_issue = true, 734 }, 735 }, 736 }, 737 }; 738 739 // commit and consume the seed events 740 try evt.commitAndConsume(repo_opts, io, allocator, &repo, evt.events_ref, &events_to_consume); 741 742 { 743 const haxy_moment = try evt.currentMoment(repo_opts, &repo); 744 745 // get the map of users 746 const event_id_to_user_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "event-id->user")) orelse return error.NotFound; 747 const event_id_to_user = try Repo.DB.HashMap(.read_only).init(event_id_to_user_cursor); 748 749 // get the user out of the map that was edited 750 const user_cursor = try event_id_to_user.getCursor(hash.hashInt(repo_opts.hash, &user_event_id)) orelse return error.NotFound; 751 const user_map = try Repo.DB.HashMap(.read_only).init(user_cursor); 752 const user_event = try evt.read(evt.User, Repo.DB, repo_opts.hash, &arena, user_map); 753 754 // the password was correctly edited 755 try std.testing.expectEqualStrings(events_to_consume[1].event.user.?.password_hash, user_event.password_hash); 756 757 // get the map of repos 758 const event_id_to_repo_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "event-id->repo")) orelse return error.NotFound; 759 const event_id_to_repo = try Repo.DB.HashMap(.read_only).init(event_id_to_repo_cursor); 760 761 // get the repo out of the map 762 const repo_cursor = try event_id_to_repo.getCursor(hash.hashInt(repo_opts.hash, &repo_event_id)) orelse return error.NotFound; 763 const repo_map = try Repo.DB.HashMap(.read_only).init(repo_cursor); 764 const repo_event = try evt.read(evt.Repo, Repo.DB, repo_opts.hash, &arena, repo_map); 765 766 try std.testing.expectEqualSlices(u8, &user_event_id, repo_event.user_id); 767 try std.testing.expectEqualStrings("ziglings", repo_event.name); 768 769 // get the repos created by the user 770 const user_id_to_repo_id_set_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "user-id->repo-id-set")) orelse return error.NotFound; 771 const user_id_to_repo_id_set = try Repo.DB.HashMap(.read_only).init(user_id_to_repo_id_set_cursor); 772 773 const user_repos_cursor = try user_id_to_repo_id_set.getCursor(hash.hashInt(repo_opts.hash, &user_event_id)) orelse return error.NotFound; 774 const user_repos = try Repo.DB.SortedSet(.read_only).init(user_repos_cursor); 775 776 try std.testing.expectEqual(1, try user_repos.count()); 777 778 // the set is keyed by orderKey([created-ts][event-id]) 779 const order_key = evt.orderKey(repo_event.created_ts, &repo_event_id); 780 try std.testing.expect(try user_repos.contains(&order_key)); 781 } 782 783 // 784 // remove the repo 785 // 786 787 const events_to_consume2 = [_]evt.EventWithId{ 788 .{ 789 .id = std.fmt.bytesToHex(repo_event_id, .lower), 790 .event = .{ .repo = null }, 791 }, 792 }; 793 794 // commit and consume the removal 795 try evt.commitAndConsume(repo_opts, io, allocator, &repo, evt.events_ref, &events_to_consume2); 796 797 { 798 const haxy_moment = try evt.currentMoment(repo_opts, &repo); 799 800 // get the map of repos 801 const event_id_to_repo_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "event-id->repo")) orelse return error.NotFound; 802 const event_id_to_repo = try Repo.DB.HashMap(.read_only).init(event_id_to_repo_cursor); 803 804 try std.testing.expect(null == try event_id_to_repo.getCursor(hash.hashInt(repo_opts.hash, &repo_event_id))); 805 806 // get the repos created by the user 807 const user_id_to_repo_id_set_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "user-id->repo-id-set")) orelse return error.NotFound; 808 const user_id_to_repo_id_set = try Repo.DB.HashMap(.read_only).init(user_id_to_repo_id_set_cursor); 809 810 const user_repos_cursor = try user_id_to_repo_id_set.getCursor(hash.hashInt(repo_opts.hash, &user_event_id)) orelse return error.NotFound; 811 const user_repos = try Repo.DB.SortedSet(.read_only).init(user_repos_cursor); 812 813 // removing the repo emptied the user's set 814 try std.testing.expectEqual(0, try user_repos.count()); 815 } 816 817 // 818 // remove the user 819 // 820 821 const events_to_consume3 = [_]evt.EventWithId{ 822 .{ 823 .id = std.fmt.bytesToHex(user_event_id, .lower), 824 .event = .{ .user = null }, 825 }, 826 }; 827 828 // commit and consume the removal 829 try evt.commitAndConsume(repo_opts, io, allocator, &repo, evt.events_ref, &events_to_consume3); 830 831 { 832 const haxy_moment = try evt.currentMoment(repo_opts, &repo); 833 834 // get the map of users 835 const event_id_to_user_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "event-id->user")) orelse return error.NotFound; 836 const event_id_to_user = try Repo.DB.HashMap(.read_only).init(event_id_to_user_cursor); 837 838 try std.testing.expect(null == try event_id_to_user.getCursor(hash.hashInt(repo_opts.hash, &user_event_id))); 839 } 840 } 841 842 test "repos and users paginate in creation order" { 843 const io = std.testing.io; 844 const allocator = std.testing.allocator; 845 const temp_dir_name = "temp-event-order"; 846 847 const cwd = std.Io.Dir.cwd(); 848 var temp_dir_or_err = cwd.openDir(io, temp_dir_name, .{}); 849 if (temp_dir_or_err) |*temp_dir| { 850 temp_dir.close(io); 851 try cwd.deleteTree(io, temp_dir_name); 852 } else |_| {} 853 var temp_dir = try cwd.createDirPathOpen(io, temp_dir_name, .{}); 854 defer cwd.deleteTree(io, temp_dir_name) catch {}; 855 defer temp_dir.close(io); 856 857 const cwd_path = try std.process.currentPathAlloc(io, allocator); 858 defer allocator.free(cwd_path); 859 const work_path = try std.fs.path.join(allocator, &.{ cwd_path, temp_dir_name }); 860 defer allocator.free(work_path); 861 862 const repo_opts: rp.RepoOpts(.xit) = .{ .is_test = true }; 863 const Repo = rp.Repo(.xit, repo_opts); 864 var repo = try Repo.init(io, allocator, .{ .path = work_path }); 865 defer repo.deinit(io, allocator); 866 867 var arena = std.heap.ArenaAllocator.init(allocator); 868 defer arena.deinit(); 869 870 var prng = std.Random.DefaultPrng.init(std.testing.random_seed); 871 const user_id = evt.EventWithId.randomId(prng.random()); 872 var repo_ids: [4][evt.event_id_size]u8 = undefined; 873 for (&repo_ids) |*id| id.* = evt.EventWithId.randomId(prng.random()); 874 875 var pw_buf: [evt.User.password_hash_max_len]u8 = undefined; 876 const pw = try evt.User.hashPassword("pw", &pw_buf, io); 877 878 // asserts the repo-id-set holds exactly `expected` ids in that order 879 const Check = struct { 880 fn order( 881 comptime DB: type, 882 comptime hash_kind: hash.HashKind, 883 moment: DB.HashMap(.read_only), 884 expected: []const *const [evt.event_id_size]u8, 885 ) !void { 886 const cursor = try moment.getCursor(hash.hashInt(hash_kind, "repo-id-set")) orelse return error.NotFound; 887 const set = try DB.SortedSet(.read_only).init(cursor); 888 try std.testing.expectEqual(expected.len, try set.count()); 889 for (expected, 0..) |id, i| { 890 const kv = (try set.getIndexKeyValuePair(@intCast(i))) orelse return error.NotFound; 891 // the key is orderKey ([timestamp][event-id]); its trailing bytes are the id 892 var order_key: [@sizeOf(u64) + evt.event_id_size]u8 = undefined; 893 _ = try kv.key_cursor.readBytes(&order_key); 894 try std.testing.expectEqualSlices(u8, id, order_key[@sizeOf(u64)..]); 895 } 896 } 897 }; 898 899 // one user, then four repos, each with its own creation timestamp (user@100, 900 // repos@101..104) 901 const events = [_]evt.EventWithId{ 902 .{ .id = std.fmt.bytesToHex(user_id, .lower), .timestamp = 100, .event = .{ .user = .{ .name = "alice", .display_name = "Alice", .email = "alice@example.test", .password_hash = pw } } }, 903 .{ .id = std.fmt.bytesToHex(repo_ids[0], .lower), .timestamp = 101, .event = .{ .repo = .{ .user_id = &user_id, .name = "repo0", .description = "d0", .enable_issue = true } } }, 904 .{ .id = std.fmt.bytesToHex(repo_ids[1], .lower), .timestamp = 102, .event = .{ .repo = .{ .user_id = &user_id, .name = "repo1", .description = "d1", .enable_issue = true } } }, 905 .{ .id = std.fmt.bytesToHex(repo_ids[2], .lower), .timestamp = 103, .event = .{ .repo = .{ .user_id = &user_id, .name = "repo2", .description = "d2", .enable_issue = true } } }, 906 .{ .id = std.fmt.bytesToHex(repo_ids[3], .lower), .timestamp = 104, .event = .{ .repo = .{ .user_id = &user_id, .name = "repo3", .description = "d3", .enable_issue = true } } }, 907 }; 908 try evt.commitAndConsume(repo_opts, io, allocator, &repo, evt.events_ref, &events); 909 910 { 911 const moment = try evt.currentMoment(repo_opts, &repo); 912 try Check.order(Repo.DB, repo_opts.hash, moment, &.{ &repo_ids[0], &repo_ids[1], &repo_ids[2], &repo_ids[3] }); 913 914 // the single user shows up in its own ordered set 915 const ucur = try moment.getCursor(hash.hashInt(repo_opts.hash, "user-id-set")) orelse return error.NotFound; 916 const uset = try Repo.DB.SortedSet(.read_only).init(ucur); 917 try std.testing.expectEqual(1, try uset.count()); 918 } 919 920 // delete repo1 -> dense order (no tombstone) 921 try evt.commitAndConsume(repo_opts, io, allocator, &repo, evt.events_ref, &[_]evt.EventWithId{ 922 .{ .id = std.fmt.bytesToHex(repo_ids[1], .lower), .timestamp = 200, .event = .{ .repo = null } }, 923 }); 924 { 925 const moment = try evt.currentMoment(repo_opts, &repo); 926 try Check.order(Repo.DB, repo_opts.hash, moment, &.{ &repo_ids[0], &repo_ids[2], &repo_ids[3] }); 927 } 928 929 // update repo0 at a later timestamp -> keeps its original slot 930 try evt.commitAndConsume(repo_opts, io, allocator, &repo, evt.events_ref, &[_]evt.EventWithId{ 931 .{ .id = std.fmt.bytesToHex(repo_ids[0], .lower), .timestamp = 300, .event = .{ .repo = .{ .user_id = &user_id, .name = "repo0", .description = "updated", .enable_issue = true } } }, 932 }); 933 { 934 const moment = try evt.currentMoment(repo_opts, &repo); 935 try Check.order(Repo.DB, repo_opts.hash, moment, &.{ &repo_ids[0], &repo_ids[2], &repo_ids[3] }); 936 937 // the value really was updated 938 const e2r_cur = try moment.getCursor(hash.hashInt(repo_opts.hash, "event-id->repo")) orelse return error.NotFound; 939 const e2r = try Repo.DB.HashMap(.read_only).init(e2r_cur); 940 const rc = try e2r.getCursor(hash.hashInt(repo_opts.hash, &repo_ids[0])) orelse return error.NotFound; 941 const rm = try Repo.DB.HashMap(.read_only).init(rc); 942 const re = try evt.read(evt.Repo, Repo.DB, repo_opts.hash, &arena, rm); 943 try std.testing.expectEqualStrings("updated", re.description); 944 } 945 } 946