diff --git a/src/api/services/communities.zig b/src/api/services/communities.zig index 8d2bab3..7ceebc4 100644 --- a/src/api/services/communities.zig +++ b/src/api/services/communities.zig @@ -153,6 +153,22 @@ pub const QueryArgs = struct { pub const jsonStringify = util.jsonSerializeEnumAsString; }; + pub const Direction = enum { + ascending, + descending, + + pub const jsonStringify = util.jsonSerializeEnumAsString; + }; + + pub const PageDirection = enum { + forward, + backward, + + pub const jsonStringify = util.jsonSerializeEnumAsString; + }; + + pub const Prev = std.meta.Child(std.meta.fieldInfo(QueryArgs, .prev).field_type); + // Max items to fetch max_items: usize = 20, @@ -164,12 +180,7 @@ pub const QueryArgs = struct { // Ordering parameter order_by: OrderBy = .created_at, - direction: enum { - ascending, - descending, - - pub const jsonStringify = util.jsonSerializeEnumAsString; - } = .ascending, + direction: Direction = .ascending, // Page start parameter // This struct is a reference to the last value scanned @@ -189,12 +200,7 @@ pub const QueryArgs = struct { // What direction to scan the page window // If "forward", then "prev" is interpreted as the item directly before the items // to query, in the direction of "direction" above. If "backward", then the opposite - page_direction: enum { - forward, - backward, - - pub const jsonStringify = util.jsonSerializeEnumAsString; - } = .forward, + page_direction: PageDirection = .forward, }; const QueryBuilder = struct { @@ -240,7 +246,7 @@ pub fn query(db: anytype, args: QueryArgs, alloc: std.mem.Allocator) ![]Communit \\SELECT {s} \\FROM community \\ - , .{util.comptimeJoin(",", std.meta.fieldNames(Community))}), + , .{comptime util.comptimeJoin(",", std.meta.fieldNames(Community))}), ); const max_items = if (args.max_items > max_max_items) max_max_items else args.max_items; @@ -290,10 +296,13 @@ pub fn query(db: anytype, args: QueryArgs, alloc: std.mem.Allocator) ![]Communit max_items, }; - var results = try db.query( - &.{ Uuid, ?Uuid, []const u8, []const u8, Scheme, DateTime }, - builder.array.items, + try builder.array.append(0); + + var results = try db.queryWithOptions( + Community, + std.meta.assumeSentinel(builder.array.items, 0), query_args, + .{ .prep_allocator = alloc, .ignore_unused_arguments = true }, ); defer results.finish(); @@ -304,21 +313,11 @@ pub fn query(db: anytype, args: QueryArgs, alloc: std.mem.Allocator) ![]Communit errdefer for (result_buf[0..count]) |c| util.deepFree(alloc, c); for (result_buf) |*c| { - const row = results.row(alloc) orelse break; - c.* = .{ - .id = row[0], - .owner_id = row[1], - .host = row[2], - .name = row[3], - .scheme = row[4], - .created_at = row[5], - }; + c.* = (try results.row(alloc)) orelse break; count += 1; } - if (results.err) |err| return err; - return result_buf[0..count]; } diff --git a/src/main/controllers.zig b/src/main/controllers.zig index a963521..c17a9b4 100644 --- a/src/main/controllers.zig +++ b/src/main/controllers.zig @@ -29,10 +29,11 @@ const routes = .{ auth.login, auth.verify_login, communities.create, + communities.query, invites.create, users.create, notes.create, - //notes.get, + notes.get, }; pub fn Context(comptime Route: type) type { @@ -66,7 +67,8 @@ pub fn Context(comptime Route: type) type { inline while (comptime route_iter.next()) |route_segment| { const path_segment = path_iter.next() orelse return null; if (route_segment[0] == ':') { - @field(args, route_segment[1..]) = path_segment; + const A = @TypeOf(@field(args, route_segment[1..])); + @field(args, route_segment[1..]) = parseArg(A, path_segment) catch return null; } else { if (!std.ascii.eqlIgnoreCase(route_segment, path_segment)) return null; } @@ -75,6 +77,13 @@ pub fn Context(comptime Route: type) type { return args; } + fn parseArg(comptime T: type, segment: []const u8) !T { + if (T == []const u8) return segment; + if (comptime std.meta.trait.hasFn("parse")(T)) return T.parse(segment); + + @compileError("Unsupported Type " ++ @typeName(T)); + } + pub fn matchAndHandle(api_source: *api.ApiSource, ctx: http.server.Context, alloc: std.mem.Allocator) bool { const req = ctx.request; if (req.method != Route.method) return false; diff --git a/src/main/controllers/communities.zig b/src/main/controllers/communities.zig index e108ce8..21eb453 100644 --- a/src/main/controllers/communities.zig +++ b/src/main/controllers/communities.zig @@ -1,4 +1,9 @@ const api = @import("api"); +const util = @import("util"); + +const QueryArgs = api.CommunityQueryArgs; +const Uuid = util.Uuid; +const DateTime = util.DateTime; pub const create = struct { pub const method = .POST; @@ -14,3 +19,82 @@ pub const create = struct { try res.json(.created, invite); } }; + +pub const query = struct { + pub const method = .GET; + pub const path = "/communities"; + + // NOTE: This has to match QueryArgs + // TODO: Support union fields in query strings natively, so we don't + // have to keep these in sync + pub const Query = struct { + const OrderBy = QueryArgs.OrderBy; + const Direction = QueryArgs.Direction; + const PageDirection = QueryArgs.PageDirection; + + // Max items to fetch + max_items: usize = 20, + + // Selection filters + owner_id: ?Uuid = null, + like: ?[]const u8 = null, + created_before: ?DateTime = null, + created_after: ?DateTime = null, + + // Ordering parameter + order_by: OrderBy = .created_at, + direction: Direction = .ascending, + + // the `prev` struct has a slightly different format to QueryArgs + prev: struct { + id: ?Uuid = null, + + // Only one of these can be present, and must match order_by above + name: ?[]const u8 = null, + host: ?[]const u8 = null, + created_at: ?DateTime = null, + } = .{}, + + // What direction to scan the page window + page_direction: PageDirection = .forward, + }; + + pub fn handler(req: anytype, res: anytype, srv: anytype) !void { + const q = req.query; + const query_matches = if (q.prev.id) |_| switch (q.order_by) { + .name => q.prev.name != null and q.prev.host == null and q.prev.created_at == null, + .host => q.prev.name == null and q.prev.host != null and q.prev.created_at == null, + .created_at => q.prev.name == null and q.prev.host == null and q.prev.created_at != null, + } else (q.prev.name == null and q.prev.host == null and q.prev.created_at == null); + + if (!query_matches) return res.err(.bad_request, "prev.* parameters do not match", {}); + + const prev_arg: ?QueryArgs.Prev = if (q.prev.id) |id| .{ + .id = id, + .order_val = switch (q.order_by) { + .name => .{ .name = q.prev.name.? }, + .host => .{ .host = q.prev.host.? }, + .created_at => .{ .created_at = q.prev.created_at.? }, + }, + } else null; + + const query_args = QueryArgs{ + .max_items = q.max_items, + .owner_id = q.owner_id, + .like = q.like, + .created_before = q.created_before, + .created_after = q.created_after, + + .order_by = q.order_by, + .direction = q.direction, + + .prev = prev_arg, + + .page_direction = q.page_direction, + }; + + const results = try srv.queryCommunities(query_args); + + try res.json(.ok, results); + } +}; diff --git a/src/sql/engines/common.zig b/src/sql/engines/common.zig index 849e470..67fef2f 100644 --- a/src/sql/engines/common.zig +++ b/src/sql/engines/common.zig @@ -89,6 +89,13 @@ pub fn prepareParamText(arena: *std.heap.ArenaAllocator, val: anytype) !?[:0]con .Enum => return @tagName(val), .Optional => if (val) |v| try prepareParamText(arena, v) else null, .Int => try std.fmt.allocPrintZ(arena.allocator(), "{}", .{val}), + .Union => loop: inline for (std.meta.fields(T)) |field| { + // Have to do this in a roundabout way to satisfy comptime checker + const Tag = std.meta.Tag(T); + const tag = @field(Tag, field.name); + + if (val == tag) break :loop try prepareParamText(arena, @field(val, field.name)); + } else unreachable, else => @compileError("Unsupported Type " ++ @typeName(T)), }, }; diff --git a/src/sql/engines/sqlite.zig b/src/sql/engines/sqlite.zig index b884301..b9fabe4 100644 --- a/src/sql/engines/sqlite.zig +++ b/src/sql/engines/sqlite.zig @@ -139,10 +139,14 @@ pub const Db = struct { const T = @TypeOf(val); switch (@typeInfo(T)) { - .Struct, - .Union, - .Opaque, - => { + .Union => inline for (std.meta.fields(T)) |field| { + const Tag = std.meta.Tag(T); + const tag = @field(Tag, field.name); + + if (val == tag) return try self.bindArgument(stmt, idx, @field(val, field.name)); + } else unreachable, + + .Struct => { const arr = if (@hasDecl(T, "toCharArray")) val.toCharArray() else if (@hasDecl(T, "toCharArrayZ")) @@ -164,7 +168,7 @@ pub const Db = struct { return if (val) |v| self.bindArgument(stmt, idx, v) else self.bindNull(stmt, idx); }, .Null => return self.bindNull(stmt, idx), - .Int => return self.bindInt(stmt, idx, val), + .Int => return self.bindInt(stmt, idx, std.math.cast(i64, val) orelse unreachable), .Float => return self.bindFloat(stmt, idx, val), else => @compileError("Unable to serialize type " ++ @typeName(T)), } diff --git a/src/util/DateTime.zig b/src/util/DateTime.zig index b0fad4c..05854cc 100644 --- a/src/util/DateTime.zig +++ b/src/util/DateTime.zig @@ -21,6 +21,8 @@ pub fn parse(str: []const u8) !DateTime { error.UnknownFormat; } +pub const JsonParseAs = []const u8; + pub fn add(self: DateTime, duration: Duration) DateTime { return DateTime{ .seconds_since_epoch = self.seconds_since_epoch + duration.seconds,