const std = @import("std"); const util = @import("util"); const sql = @import("sql"); const common = @import("./common.zig"); const Uuid = util.Uuid; const DateTime = util.DateTime; pub const Follow = struct { id: Uuid, followed_by_id: Uuid, followee_id: Uuid, created_at: DateTime, }; pub fn create(db: anytype, followed_by_id: Uuid, followee_id: Uuid, alloc: std.mem.Allocator) !void { if (Uuid.eql(followed_by_id, followee_id)) return error.SelfFollow; const now = DateTime.now(); const id = Uuid.randV4(util.getThreadPrng()); db.insert("follow", .{ .id = id, .followed_by_id = followed_by_id, .followee_id = followee_id, .created_at = now, }, alloc) catch |err| return switch (err) { error.ForeignKeyViolation => error.NotFound, error.UniqueViolation => error.NotUnique, else => error.DatabaseFailure, }; } pub fn delete(db: anytype, followed_by_id: Uuid, followee_id: Uuid, alloc: std.mem.Allocator) !void { // TODO: Measure count and report success db.exec( \\DELETE FROM follow \\WHERE followed_by_id = $1 AND followee_id = $2 , .{ followed_by_id, followee_id }, alloc, ) catch return error.DatabaseFailure; } const max_max_items = 100; pub const QueryArgs = struct { pub const Direction = common.Direction; pub const PageDirection = common.PageDirection; pub const Prev = std.meta.Child(std.meta.fieldInfo(@This(), .prev).field_type); pub const OrderBy = enum { created_at, }; max_items: usize = 20, followed_by_id: ?Uuid = null, followee_id: ?Uuid = null, order_by: OrderBy = .created_at, direction: Direction = .descending, prev: ?struct { id: Uuid, order_val: union(OrderBy) { created_at: DateTime, }, } = null, page_direction: PageDirection = .forward, }; pub const QueryResult = struct { items: []Follow, prev_page: QueryArgs, next_page: QueryArgs, }; pub fn query(db: anytype, args: QueryArgs, alloc: std.mem.Allocator) !QueryResult { var builder = sql.QueryBuilder.init(alloc); defer builder.deinit(); try builder.appendSlice( \\SELECT follow.id, follow.followed_by_id, follow.followee_id, follow.created_at \\FROM follow \\ ); if (args.followed_by_id != null) try builder.andWhere("follow.followed_by_id = $1"); if (args.followee_id != null) try builder.andWhere("follow.followee_id = $2"); if (args.prev != null) { try builder.andWhere("(follow.id, follow.created_at)"); switch (args.page_direction) { .forward => try builder.appendSlice(" < "), .backward => try builder.appendSlice(" > "), } try builder.appendSlice("($3, $4)"); } try builder.appendSlice( \\ \\ORDER BY follow.created_at DESC \\LIMIT $5 \\ ); const max_items = if (args.max_items > max_max_items) max_max_items else args.max_items; const query_args = .{ args.followed_by_id, args.followee_id, if (args.prev) |p| p.id else null, if (args.prev) |p| p.order_val else null, max_items, }; const results = try db.queryRowsWithOptions( Follow, try builder.terminate(), query_args, max_items, .{ .allocator = alloc, .ignore_unused_arguments = true }, ); errdefer util.deepFree(alloc, results); var next_page = args; var prev_page = args; prev_page.page_direction = .backward; next_page.page_direction = .forward; if (results.len != 0) { prev_page.prev = .{ .id = results[0].id, .order_val = .{ .created_at = results[0].created_at }, }; next_page.prev = .{ .id = results[results.len - 1].id, .order_val = .{ .created_at = results[results.len - 1].created_at }, }; } // TODO: this will give incorrect links on an empty page return QueryResult{ .items = results, .next_page = next_page, .prev_page = prev_page, }; }