Add support for Community query

This commit is contained in:
jaina heartles 2022-10-10 21:49:36 -07:00
parent add17c0a0a
commit 195b201564
6 changed files with 139 additions and 34 deletions

View File

@ -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];
}

View File

@ -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;

View File

@ -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);
}
};

View File

@ -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)),
},
};

View File

@ -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)),
}

View File

@ -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,