Add support for Community query
This commit is contained in:
parent
add17c0a0a
commit
195b201564
|
@ -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];
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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)),
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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)),
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in New Issue