From ebf8b34e15bd621d335b211b67b7f28c2a49a9ce Mon Sep 17 00:00:00 2001 From: jaina heartles Date: Sun, 13 Nov 2022 23:56:58 -0800 Subject: [PATCH 1/6] Add follow table --- src/api/services/follows.zig | 142 +++++++++++++++++++++++++++++++++++ src/api/services/notes.zig | 2 +- src/main/migrations.zig | 16 ++++ 3 files changed, 159 insertions(+), 1 deletion(-) create mode 100644 src/api/services/follows.zig diff --git a/src/api/services/follows.zig b/src/api/services/follows.zig new file mode 100644 index 0000000..df76377 --- /dev/null +++ b/src/api/services/follows.zig @@ -0,0 +1,142 @@ +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, + + follower_id: Uuid, + following_id: Uuid, + + created_at: DateTime, +}; + +pub fn create(db: anytype, follower_id: Uuid, following_id: Uuid, alloc: std.mem.Allocator) !void { + if (Uuid.eql(follower_id, following_id)) return error.SelfFollow; + const now = DateTime.now(); + const id = Uuid.randv4(util.getThreadPrng()); + + db.insert("follow", .{ + .id = id, + .follower_id = follower_id, + .following_id = following_id, + .created_at = now, + }, alloc) catch |err| return switch (err) { + error.ForeignKeyViolation => error.NotFound, + error.UniqueViolation => error.NotUnique, + else => 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, + + follower_id: ?Uuid = null, + following_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 = null, +}; + +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.follower_id, follow.followee_id, follow.created_at + \\FROM follow + \\ + ); + + if (args.follower_id != null) try builder.andWhere("follow.follower_id = $1"); + if (args.following_id != null) try builder.andWhere("follow.following_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 $6 + \\ + ); + + const max_items = if (args.max_items > max_max_items) max_max_items else args.max_items; + const query_args = .{ + args.follower_id, + args.following_id, + if (args.prev) |p| p.id else null, + if (args.prev) |p| p.created_at 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, + .created_at = results[0].created_at, + }; + + next_page.prev = .{ + .id = results[results.len - 1].id, + .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, + }; +} diff --git a/src/api/services/notes.zig b/src/api/services/notes.zig index 2a2d7f0..fef125a 100644 --- a/src/api/services/notes.zig +++ b/src/api/services/notes.zig @@ -138,7 +138,7 @@ pub fn query(db: anytype, args: QueryArgs, alloc: std.mem.Allocator) !QueryResul max_items, .{ .allocator = alloc, .ignore_unused_arguments = true }, ); - errdefer util.deepFree(results); + errdefer util.deepFree(alloc, results); var next_page = args; var prev_page = args; diff --git a/src/main/migrations.zig b/src/main/migrations.zig index b0abc02..b35d4ee 100644 --- a/src/main/migrations.zig +++ b/src/main/migrations.zig @@ -189,4 +189,20 @@ const migrations: []const Migration = &.{ \\DROP TABLE community; , }, + .{ + .name = "follows", + .up = + \\CREATE TABLE follow( + \\ id UUID NOT NULL PRIMARY KEY, + \\ + \\ follower_id UUID NOT NULL, + \\ following_id UUID NOT NULL, + \\ + \\ created_at TIMESTAMPTZ NOT NULL, + \\ + \\ UNIQUE(follower_id, following_id) + \\); + , + .down = "DROP TABLE follow", + }, }; From c49f1487a7c9c871080429ecb0ff611fa27801cf Mon Sep 17 00:00:00 2001 From: jaina heartles Date: Mon, 14 Nov 2022 00:14:29 -0800 Subject: [PATCH 2/6] Add list followers api calls --- src/api/lib.zig | 57 ++++++++++++++++++++++++++++++++++++ src/api/services/follows.zig | 2 +- 2 files changed, 58 insertions(+), 1 deletion(-) diff --git a/src/api/lib.zig b/src/api/lib.zig index 98f0af2..4cf964b 100644 --- a/src/api/lib.zig +++ b/src/api/lib.zig @@ -11,6 +11,7 @@ const services = struct { const auth = @import("./services/auth.zig"); const invites = @import("./services/invites.zig"); const notes = @import("./services/notes.zig"); + const follows = @import("./services/follows.zig"); }; pub const RegistrationOptions = struct { @@ -90,6 +91,40 @@ pub const TimelineResult = struct { next_page: TimelineArgs, }; +pub const FollowQueryArgs = struct { + pub const OrderBy = services.follows.QueryArgs.OrderBy; + pub const Direction = services.follows.QueryArgs.Direction; + pub const PageDirection = services.follows.QueryArgs.PageDirection; + pub const Prev = services.follows.QueryArgs.Prev; + + max_items: usize = 20, + + order_by: OrderBy = .created_at, + + direction: Direction = .descending, + + prev: ?Prev = null, + + page_direction: PageDirection = .forward, + + fn from(args: services.follows.QueryArgs) FollowQueryArgs { + return .{ + .max_items = args.max_items, + .order_by = args.order_by, + .direction = args.direction, + .prev = args.prev, + .page_direction = args.page_direction, + }; + } +}; + +pub const FollowQueryResult = struct { + items: []services.follows.Follow, + + prev_page: FollowQueryArgs, + next_page: FollowQueryArgs, +}; + pub fn isAdminSetup(db: sql.Db) !bool { _ = services.communities.adminCommunityId(db) catch |err| switch (err) { error.NotFound => return false, @@ -405,5 +440,27 @@ fn ApiConn(comptime DbConn: type) type { .next_page = TimelineArgs.from(result.next_page), }; } + + pub fn getFollowers(self: *Self, user_id: Uuid, args: FollowQueryArgs) !FollowQueryResult { + var all_args = std.mem.zeroInit(services.follows.QueryArgs, args); + all_args.following_id = user_id; + const result = try services.follows.query(self.db, all_args, self.arena.allocator()); + return TimelineResult{ + .items = result.items, + .prev_page = FollowQueryArgs.from(result.prev_page), + .next_page = FollowQueryArgs.from(result.next_page), + }; + } + + pub fn getFollowing(self: *Self, user_id: Uuid, args: FollowQueryArgs) !FollowQueryResult { + var all_args = std.mem.zeroInit(services.follows.QueryArgs, args); + all_args.follower_id = user_id; + const result = try services.follows.query(self.db, all_args, self.arena.allocator()); + return TimelineResult{ + .items = result.items, + .prev_page = FollowQueryArgs.from(result.prev_page), + .next_page = FollowQueryArgs.from(result.next_page), + }; + } }; } diff --git a/src/api/services/follows.zig b/src/api/services/follows.zig index df76377..ff89ecc 100644 --- a/src/api/services/follows.zig +++ b/src/api/services/follows.zig @@ -60,7 +60,7 @@ pub const QueryArgs = struct { }, } = null, - page_direction: PageDirection = null, + page_direction: PageDirection = .forward, }; pub const QueryResult = struct { From 568c6cd6b6f619ae9d7f73793d0a1bf30e726291 Mon Sep 17 00:00:00 2001 From: jaina heartles Date: Mon, 14 Nov 2022 00:25:52 -0800 Subject: [PATCH 3/6] Combine pagination code --- src/main/controllers.zig | 39 +++++++++++++++++++++ src/main/controllers/communities.zig | 36 ++------------------ src/main/controllers/timelines.zig | 51 ++-------------------------- 3 files changed, 44 insertions(+), 82 deletions(-) diff --git a/src/main/controllers.zig b/src/main/controllers.zig index 902711e..7e52c49 100644 --- a/src/main/controllers.zig +++ b/src/main/controllers.zig @@ -263,3 +263,42 @@ const json_options = if (builtin.mode == .Debug) }, .string = .{ .String = .{} }, }; + +pub const helpers = struct { + pub fn paginate(community: api.Community, path: []const u8, results: anytype, res: *Response, alloc: std.mem.Allocator) !void { + var link = std.ArrayList(u8).init(alloc); + const link_writer = link.writer(); + defer link.deinit(); + + try writeLink(link_writer, community, path, results.next_page, "next"); + try link_writer.writeByte(','); + try writeLink(link_writer, community, path, results.prev_page, "prev"); + + try res.headers.put("Link", link.items); + + try res.json(.ok, results.items); + } + + fn writeLink( + writer: anytype, + community: api.Community, + path: []const u8, + params: anytype, + rel: []const u8, + ) !void { + // TODO: percent-encode + try std.fmt.format( + writer, + "<{s}://{s}/{s}?", + .{ @tagName(community.scheme), community.host, path }, + ); + + try query_utils.formatQuery(params, writer); + + try std.fmt.format( + writer, + ">; rel=\"{s}\"", + .{rel}, + ); + } +}; diff --git a/src/main/controllers/communities.zig b/src/main/controllers/communities.zig index 3aeec1f..89ed8a5 100644 --- a/src/main/controllers/communities.zig +++ b/src/main/controllers/communities.zig @@ -2,6 +2,7 @@ const std = @import("std"); const api = @import("api"); const util = @import("util"); const query_utils = @import("../query.zig"); +const controller_utils = @import("../controllers.zig").helpers; const QueryArgs = api.CommunityQueryArgs; const Uuid = util.Uuid; @@ -31,39 +32,6 @@ pub const query = struct { pub fn handler(req: anytype, res: anytype, srv: anytype) !void { const results = try srv.queryCommunities(req.query); - var link = std.ArrayList(u8).init(req.allocator); - const link_writer = link.writer(); - defer link.deinit(); - - try writeLink(link_writer, srv.community, path, results.next_page, "next"); - try link_writer.writeByte(','); - try writeLink(link_writer, srv.community, path, results.prev_page, "prev"); - - try res.headers.put("Link", link.items); - - try res.json(.ok, results.items); + try controller_utils.paginate(srv.community, path, results, res, req.allocator); } }; - -fn writeLink( - writer: anytype, - community: api.Community, - path: []const u8, - params: anytype, - rel: []const u8, -) !void { - // TODO: percent-encode - try std.fmt.format( - writer, - "<{s}://{s}/{s}?", - .{ @tagName(community.scheme), community.host, path }, - ); - - try query_utils.formatQuery(params, writer); - - try std.fmt.format( - writer, - ">; rel=\"{s}\"", - .{rel}, - ); -} diff --git a/src/main/controllers/timelines.zig b/src/main/controllers/timelines.zig index 9a68da8..6f956fc 100644 --- a/src/main/controllers/timelines.zig +++ b/src/main/controllers/timelines.zig @@ -1,6 +1,7 @@ const std = @import("std"); const api = @import("api"); const query_utils = @import("../query.zig"); +const controller_utils = @import("../controllers.zig").helpers; pub const global = struct { pub const method = .GET; @@ -10,18 +11,7 @@ pub const global = struct { pub fn handler(req: anytype, res: anytype, srv: anytype) !void { const results = try srv.globalTimeline(req.query); - - var link = std.ArrayList(u8).init(req.allocator); - const link_writer = link.writer(); - defer link.deinit(); - - try writeLink(link_writer, srv.community, path, results.next_page, "next"); - try link_writer.writeByte(','); - try writeLink(link_writer, srv.community, path, results.prev_page, "prev"); - - try res.headers.put("Link", link.items); - - try res.json(.ok, results.items); + try controller_utils.paginate(srv.community, path, results, res, req.allocator); } }; @@ -33,41 +23,6 @@ pub const local = struct { pub fn handler(req: anytype, res: anytype, srv: anytype) !void { const results = try srv.localTimeline(req.query); - - var link = std.ArrayList(u8).init(req.allocator); - const link_writer = link.writer(); - defer link.deinit(); - - try writeLink(link_writer, srv.community, path, results.next_page, "next"); - try link_writer.writeByte(','); - try writeLink(link_writer, srv.community, path, results.prev_page, "prev"); - - try res.headers.put("Link", link.items); - - try res.json(.ok, results.items); + try controller_utils.paginate(srv.community, path, results, res, req.allocator); } }; - -// TOOD: unify with communities.zig -fn writeLink( - writer: anytype, - community: api.Community, - path: []const u8, - params: anytype, - rel: []const u8, -) !void { - // TODO: percent-encode - try std.fmt.format( - writer, - "<{s}://{s}/{s}?", - .{ @tagName(community.scheme), community.host, path }, - ); - - try query_utils.formatQuery(params, writer); - - try std.fmt.format( - writer, - ">; rel=\"{s}\"", - .{rel}, - ); -} From 763327292a966b0ce00e0504c9f1a7886214594f Mon Sep 17 00:00:00 2001 From: jaina heartles Date: Mon, 14 Nov 2022 01:03:11 -0800 Subject: [PATCH 4/6] Following/follower endpoints --- src/api/lib.zig | 22 ++++++++--- src/api/services/follows.zig | 14 +++---- src/main/controllers.zig | 6 +++ src/main/controllers/users/follows.zig | 54 ++++++++++++++++++++++++++ src/main/query.zig | 7 +++- 5 files changed, 89 insertions(+), 14 deletions(-) create mode 100644 src/main/controllers/users/follows.zig diff --git a/src/api/lib.zig b/src/api/lib.zig index 4cf964b..c971f3c 100644 --- a/src/api/lib.zig +++ b/src/api/lib.zig @@ -91,7 +91,7 @@ pub const TimelineResult = struct { next_page: TimelineArgs, }; -pub const FollowQueryArgs = struct { +const FollowQueryArgs = struct { pub const OrderBy = services.follows.QueryArgs.OrderBy; pub const Direction = services.follows.QueryArgs.Direction; pub const PageDirection = services.follows.QueryArgs.PageDirection; @@ -118,13 +118,18 @@ pub const FollowQueryArgs = struct { } }; -pub const FollowQueryResult = struct { +const FollowQueryResult = struct { items: []services.follows.Follow, prev_page: FollowQueryArgs, next_page: FollowQueryArgs, }; +pub const FollowerQueryArgs = FollowQueryArgs; +pub const FollowerQueryResult = FollowQueryResult; +pub const FollowingQueryArgs = FollowQueryArgs; +pub const FollowingQueryResult = FollowQueryResult; + pub fn isAdminSetup(db: sql.Db) !bool { _ = services.communities.adminCommunityId(db) catch |err| switch (err) { error.NotFound => return false, @@ -441,26 +446,31 @@ fn ApiConn(comptime DbConn: type) type { }; } - pub fn getFollowers(self: *Self, user_id: Uuid, args: FollowQueryArgs) !FollowQueryResult { + pub fn queryFollowers(self: *Self, user_id: Uuid, args: FollowerQueryArgs) !FollowerQueryResult { var all_args = std.mem.zeroInit(services.follows.QueryArgs, args); all_args.following_id = user_id; const result = try services.follows.query(self.db, all_args, self.arena.allocator()); - return TimelineResult{ + return FollowerQueryResult{ .items = result.items, .prev_page = FollowQueryArgs.from(result.prev_page), .next_page = FollowQueryArgs.from(result.next_page), }; } - pub fn getFollowing(self: *Self, user_id: Uuid, args: FollowQueryArgs) !FollowQueryResult { + pub fn queryFollowing(self: *Self, user_id: Uuid, args: FollowingQueryArgs) !FollowingQueryResult { var all_args = std.mem.zeroInit(services.follows.QueryArgs, args); all_args.follower_id = user_id; const result = try services.follows.query(self.db, all_args, self.arena.allocator()); - return TimelineResult{ + return FollowingQueryResult{ .items = result.items, .prev_page = FollowQueryArgs.from(result.prev_page), .next_page = FollowQueryArgs.from(result.next_page), }; } + + pub fn follow(self: *Self, followee: Uuid) !void { + const result = try services.follows.create(self.db, self.user_id orelse return error.NoToken, followee, self.arena.allocator()); + defer util.deepFree(self.arena.allocator(), result); + } }; } diff --git a/src/api/services/follows.zig b/src/api/services/follows.zig index ff89ecc..e7a431a 100644 --- a/src/api/services/follows.zig +++ b/src/api/services/follows.zig @@ -19,7 +19,7 @@ pub const Follow = struct { pub fn create(db: anytype, follower_id: Uuid, following_id: Uuid, alloc: std.mem.Allocator) !void { if (Uuid.eql(follower_id, following_id)) return error.SelfFollow; const now = DateTime.now(); - const id = Uuid.randv4(util.getThreadPrng()); + const id = Uuid.randV4(util.getThreadPrng()); db.insert("follow", .{ .id = id, @@ -75,7 +75,7 @@ pub fn query(db: anytype, args: QueryArgs, alloc: std.mem.Allocator) !QueryResul defer builder.deinit(); try builder.appendSlice( - \\SELECT follow.follower_id, follow.followee_id, follow.created_at + \\SELECT follow.id, follow.follower_id, follow.following_id, follow.created_at \\FROM follow \\ ); @@ -95,7 +95,7 @@ pub fn query(db: anytype, args: QueryArgs, alloc: std.mem.Allocator) !QueryResul try builder.appendSlice( \\ \\ORDER BY follow.created_at DESC - \\LIMIT $6 + \\LIMIT $5 \\ ); @@ -104,11 +104,11 @@ pub fn query(db: anytype, args: QueryArgs, alloc: std.mem.Allocator) !QueryResul args.follower_id, args.following_id, if (args.prev) |p| p.id else null, - if (args.prev) |p| p.created_at else null, + if (args.prev) |p| p.order_val else null, max_items, }; - const results = try db.QueryRowsWithOptions( + const results = try db.queryRowsWithOptions( Follow, try builder.terminate(), query_args, @@ -124,12 +124,12 @@ pub fn query(db: anytype, args: QueryArgs, alloc: std.mem.Allocator) !QueryResul if (results.len != 0) { prev_page.prev = .{ .id = results[0].id, - .created_at = results[0].created_at, + .order_val = .{ .created_at = results[0].created_at }, }; next_page.prev = .{ .id = results[results.len - 1].id, - .created_at = results[results.len - 1].created_at, + .order_val = .{ .created_at = results[results.len - 1].created_at }, }; } // TODO: this will give incorrect links on an empty page diff --git a/src/main/controllers.zig b/src/main/controllers.zig index 7e52c49..9aac216 100644 --- a/src/main/controllers.zig +++ b/src/main/controllers.zig @@ -11,6 +11,7 @@ pub const auth = @import("./controllers/auth.zig"); pub const communities = @import("./controllers/communities.zig"); pub const invites = @import("./controllers/invites.zig"); pub const users = @import("./controllers/users.zig"); +pub const follows = @import("./controllers/users/follows.zig"); pub const notes = @import("./controllers/notes.zig"); pub const streaming = @import("./controllers/streaming.zig"); pub const timelines = @import("./controllers/timelines.zig"); @@ -45,6 +46,9 @@ const routes = .{ streaming.streaming, timelines.global, timelines.local, + follows.create, + follows.query_followers, + follows.query_following, }; pub fn Context(comptime Route: type) type { @@ -90,6 +94,8 @@ pub fn Context(comptime Route: type) type { } } + if (path_iter.next() != null) return null; + return args; } diff --git a/src/main/controllers/users/follows.zig b/src/main/controllers/users/follows.zig new file mode 100644 index 0000000..dcb36b9 --- /dev/null +++ b/src/main/controllers/users/follows.zig @@ -0,0 +1,54 @@ +const api = @import("api"); +const util = @import("util"); +const controller_utils = @import("../../controllers.zig").helpers; + +const Uuid = util.Uuid; + +pub const create = struct { + pub const method = .POST; + pub const path = "/users/:id/follow"; + + pub const Args = struct { + id: Uuid, + }; + + pub fn handler(req: anytype, res: anytype, srv: anytype) !void { + try srv.follow(req.args.id); + + try res.json(.created, .{}); + } +}; + +pub const query_followers = struct { + pub const method = .GET; + pub const path = "/users/:id/followers"; + + pub const Args = struct { + id: Uuid, + }; + + pub const Query = api.FollowingQueryArgs; + + pub fn handler(req: anytype, res: anytype, srv: anytype) !void { + const results = try srv.queryFollowers(req.args.id, req.query); + + try controller_utils.paginate(srv.community, path, results, res, req.allocator); + } +}; + +pub const query_following = struct { + pub const method = .GET; + pub const path = "/users/:id/following"; + + pub const Args = struct { + id: Uuid, + }; + + pub const Query = api.FollowerQueryArgs; + + pub fn handler(req: anytype, res: anytype, srv: anytype) !void { + const results = try srv.queryFollowing(req.args.id, req.query); + + try controller_utils.paginate(srv.community, path, results, res, req.allocator); + } +}; diff --git a/src/main/query.zig b/src/main/query.zig index 857eff0..a51a7ab 100644 --- a/src/main/query.zig +++ b/src/main/query.zig @@ -127,7 +127,11 @@ fn parse(comptime T: type, comptime prefix: []const u8, comptime name: []const u if (try parse(F, prefix ++ "." ++ name, field.name, fields)) |v| { maybe_value = v; } else if (field.default_value) |default| { - maybe_value = @ptrCast(*const F, @alignCast(@alignOf(F), default)).*; + if (comptime @sizeOf(F) != 0) { + maybe_value = @ptrCast(*const F, @alignCast(@alignOf(F), default)).*; + } else { + maybe_value = std.mem.zeroes(F); + } } if (maybe_value) |v| { @@ -139,6 +143,7 @@ fn parse(comptime T: type, comptime prefix: []const u8, comptime name: []const u if (fields_specified == 0) { return null; } else if (fields_specified != info.fields.len) { + std.log.debug("{} {s} {s}", .{ T, prefix, name }); return error.PartiallySpecifiedStruct; } else { return result; From 81c705d519156e3f0ede3677b77628973bd4b829 Mon Sep 17 00:00:00 2001 From: jaina heartles Date: Mon, 14 Nov 2022 15:00:01 -0800 Subject: [PATCH 5/6] Home timeline endpoint --- src/api/lib.zig | 13 +++++++++++++ src/api/services/notes.zig | 8 ++++++++ src/main/controllers.zig | 1 + src/main/controllers/timelines.zig | 12 ++++++++++++ 4 files changed, 34 insertions(+) diff --git a/src/api/lib.zig b/src/api/lib.zig index c971f3c..008c4ee 100644 --- a/src/api/lib.zig +++ b/src/api/lib.zig @@ -446,6 +446,19 @@ fn ApiConn(comptime DbConn: type) type { }; } + pub fn homeTimeline(self: *Self, args: TimelineArgs) !TimelineResult { + if (self.user_id == null) return error.NoToken; + + var all_args = std.mem.zeroInit(services.notes.QueryArgs, args); + all_args.followed_by = self.user_id; + const result = try services.notes.query(self.db, all_args, self.arena.allocator()); + return TimelineResult{ + .items = result.items, + .prev_page = TimelineArgs.from(result.prev_page), + .next_page = TimelineArgs.from(result.next_page), + }; + } + pub fn queryFollowers(self: *Self, user_id: Uuid, args: FollowerQueryArgs) !FollowerQueryResult { var all_args = std.mem.zeroInit(services.follows.QueryArgs, args); all_args.following_id = user_id; diff --git a/src/api/services/notes.zig b/src/api/services/notes.zig index fef125a..ad817a8 100644 --- a/src/api/services/notes.zig +++ b/src/api/services/notes.zig @@ -70,6 +70,7 @@ pub const QueryArgs = struct { created_before: ?DateTime = null, created_after: ?DateTime = null, community_id: ?Uuid = null, + followed_by: ?Uuid = null, prev: ?struct { id: Uuid, @@ -95,6 +96,12 @@ pub fn query(db: anytype, args: QueryArgs, alloc: std.mem.Allocator) !QueryResul \\ ); + if (args.followed_by != null) try builder.appendSlice( + \\ JOIN follow ON + \\ follow.follower_id = $7 AND follow.following_id = note.author_id + \\ + ); + if (args.created_before != null) try builder.andWhere("note.created_at < $1"); if (args.created_after != null) try builder.andWhere("note.created_at > $2"); if (args.prev != null) { @@ -128,6 +135,7 @@ pub fn query(db: anytype, args: QueryArgs, alloc: std.mem.Allocator) !QueryResul prev_id, args.community_id, max_items, + args.followed_by, }; }; diff --git a/src/main/controllers.zig b/src/main/controllers.zig index 9aac216..4470d75 100644 --- a/src/main/controllers.zig +++ b/src/main/controllers.zig @@ -46,6 +46,7 @@ const routes = .{ streaming.streaming, timelines.global, timelines.local, + timelines.home, follows.create, follows.query_followers, follows.query_following, diff --git a/src/main/controllers/timelines.zig b/src/main/controllers/timelines.zig index 6f956fc..2d5ace6 100644 --- a/src/main/controllers/timelines.zig +++ b/src/main/controllers/timelines.zig @@ -26,3 +26,15 @@ pub const local = struct { try controller_utils.paginate(srv.community, path, results, res, req.allocator); } }; + +pub const home = struct { + pub const method = .GET; + pub const path = "/timelines/home"; + + pub const Query = api.TimelineArgs; + + pub fn handler(req: anytype, res: anytype, srv: anytype) !void { + const results = try srv.homeTimeline(req.query); + try controller_utils.paginate(srv.community, path, results, res, req.allocator); + } +}; From 721bf7e61a5e6510fc78abc4197e82c7becfe8ca Mon Sep 17 00:00:00 2001 From: jaina heartles Date: Mon, 14 Nov 2022 20:25:59 -0800 Subject: [PATCH 6/6] Rename follower/followee --- src/api/lib.zig | 4 ++-- src/api/services/follows.zig | 26 +++++++++++++------------- src/api/services/notes.zig | 2 +- src/main/migrations.zig | 6 +++--- 4 files changed, 19 insertions(+), 19 deletions(-) diff --git a/src/api/lib.zig b/src/api/lib.zig index 008c4ee..ab19097 100644 --- a/src/api/lib.zig +++ b/src/api/lib.zig @@ -461,7 +461,7 @@ fn ApiConn(comptime DbConn: type) type { pub fn queryFollowers(self: *Self, user_id: Uuid, args: FollowerQueryArgs) !FollowerQueryResult { var all_args = std.mem.zeroInit(services.follows.QueryArgs, args); - all_args.following_id = user_id; + all_args.followee_id = user_id; const result = try services.follows.query(self.db, all_args, self.arena.allocator()); return FollowerQueryResult{ .items = result.items, @@ -472,7 +472,7 @@ fn ApiConn(comptime DbConn: type) type { pub fn queryFollowing(self: *Self, user_id: Uuid, args: FollowingQueryArgs) !FollowingQueryResult { var all_args = std.mem.zeroInit(services.follows.QueryArgs, args); - all_args.follower_id = user_id; + all_args.followed_by_id = user_id; const result = try services.follows.query(self.db, all_args, self.arena.allocator()); return FollowingQueryResult{ .items = result.items, diff --git a/src/api/services/follows.zig b/src/api/services/follows.zig index e7a431a..dc4f5c7 100644 --- a/src/api/services/follows.zig +++ b/src/api/services/follows.zig @@ -10,21 +10,21 @@ const DateTime = util.DateTime; pub const Follow = struct { id: Uuid, - follower_id: Uuid, - following_id: Uuid, + followed_by_id: Uuid, + followee_id: Uuid, created_at: DateTime, }; -pub fn create(db: anytype, follower_id: Uuid, following_id: Uuid, alloc: std.mem.Allocator) !void { - if (Uuid.eql(follower_id, following_id)) return error.SelfFollow; +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, - .follower_id = follower_id, - .following_id = following_id, + .followed_by_id = followed_by_id, + .followee_id = followee_id, .created_at = now, }, alloc) catch |err| return switch (err) { error.ForeignKeyViolation => error.NotFound, @@ -46,8 +46,8 @@ pub const QueryArgs = struct { max_items: usize = 20, - follower_id: ?Uuid = null, - following_id: ?Uuid = null, + followed_by_id: ?Uuid = null, + followee_id: ?Uuid = null, order_by: OrderBy = .created_at, @@ -75,13 +75,13 @@ pub fn query(db: anytype, args: QueryArgs, alloc: std.mem.Allocator) !QueryResul defer builder.deinit(); try builder.appendSlice( - \\SELECT follow.id, follow.follower_id, follow.following_id, follow.created_at + \\SELECT follow.id, follow.followed_by_id, follow.followee_id, follow.created_at \\FROM follow \\ ); - if (args.follower_id != null) try builder.andWhere("follow.follower_id = $1"); - if (args.following_id != null) try builder.andWhere("follow.following_id = $2"); + 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)"); @@ -101,8 +101,8 @@ pub fn query(db: anytype, args: QueryArgs, alloc: std.mem.Allocator) !QueryResul const max_items = if (args.max_items > max_max_items) max_max_items else args.max_items; const query_args = .{ - args.follower_id, - args.following_id, + 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, diff --git a/src/api/services/notes.zig b/src/api/services/notes.zig index ad817a8..086a85a 100644 --- a/src/api/services/notes.zig +++ b/src/api/services/notes.zig @@ -98,7 +98,7 @@ pub fn query(db: anytype, args: QueryArgs, alloc: std.mem.Allocator) !QueryResul if (args.followed_by != null) try builder.appendSlice( \\ JOIN follow ON - \\ follow.follower_id = $7 AND follow.following_id = note.author_id + \\ follow.followed_by_id = $7 AND follow.followee_id = note.author_id \\ ); diff --git a/src/main/migrations.zig b/src/main/migrations.zig index b35d4ee..89a63d3 100644 --- a/src/main/migrations.zig +++ b/src/main/migrations.zig @@ -195,12 +195,12 @@ const migrations: []const Migration = &.{ \\CREATE TABLE follow( \\ id UUID NOT NULL PRIMARY KEY, \\ - \\ follower_id UUID NOT NULL, - \\ following_id UUID NOT NULL, + \\ followed_by_id UUID NOT NULL, + \\ followee_id UUID NOT NULL, \\ \\ created_at TIMESTAMPTZ NOT NULL, \\ - \\ UNIQUE(follower_id, following_id) + \\ UNIQUE(followed_by_id, followee_id) \\); , .down = "DROP TABLE follow",