Basic reacts

This commit is contained in:
jaina heartles 2022-07-19 00:07:01 -07:00
parent 07019cb090
commit 718da7b408
5 changed files with 120 additions and 21 deletions

View file

@ -50,8 +50,12 @@ fn reify(comptime T: type, id: Uuid, val: CreateInfo(T)) T {
return result; return result;
} }
pub const UserContext = struct { pub const ApiContext = struct {
user: models.User, user_context: struct {
user: models.User,
},
alloc: std.mem.Allocator,
}; };
pub const NoteCreate = struct { pub const NoteCreate = struct {
@ -69,26 +73,30 @@ pub const ApiServer = struct {
}; };
} }
pub fn makeUserContext(self: *ApiServer, token: []const u8, alloc: std.mem.Allocator) !UserContext { pub fn makeApiContext(self: *ApiServer, token: []const u8, alloc: std.mem.Allocator) !ApiContext {
if (token.len == 0) return error.InvalidToken; if (token.len == 0) return error.InvalidToken;
const user_handle = token; const user_handle = token;
const user = (try self.db.getBy(models.User, .handle, user_handle, alloc)) orelse return error.InvalidToken; const user = (try self.db.getBy(models.User, .handle, user_handle, alloc)) orelse return error.InvalidToken;
return UserContext{ return ApiContext{
.user = user, .user_context = .{
.user = user,
},
.alloc = alloc,
}; };
} }
pub fn createNoteUser(self: *ApiServer, info: NoteCreate, ctx: UserContext) !models.Note { pub fn createNoteUser(self: *ApiServer, info: NoteCreate, ctx: ApiContext) !models.Note {
const id = Uuid.randV4(self.prng.random()); const id = Uuid.randV4(self.prng.random());
// TODO: check for dupes // TODO: check for dupes
std.debug.print("user {s} making a note\n", .{ctx.user.handle}); std.debug.print("user {s} making a note\n", .{ctx.user_context.user.handle});
const note = models.Note{ const note = models.Note{
.id = id, .id = id,
.author_id = ctx.user.id, .author_id = ctx.user_context.user.id,
.content = info.content, .content = info.content,
.created_at = DateTime.now(), .created_at = DateTime.now(),
@ -118,4 +126,12 @@ pub const ApiServer = struct {
pub fn getUser(self: *ApiServer, id: Uuid, alloc: std.mem.Allocator) !?models.User { pub fn getUser(self: *ApiServer, id: Uuid, alloc: std.mem.Allocator) !?models.User {
return self.db.getBy(models.User, .id, id, alloc); return self.db.getBy(models.User, .id, id, alloc);
} }
pub fn react(self: *ApiServer, note_id: Uuid, ctx: ApiContext) !void {
try self.db.insert(models.Reaction, .{ .note_id = note_id, .reactor_id = ctx.user_context.user.id });
}
pub fn listReacts(self: *ApiServer, note_id: Uuid, ctx: ApiContext) ![]models.Reaction {
return try self.db.getWhereEq(models.Reaction, .note_id, note_id, ctx.alloc);
}
}; };

View file

@ -48,12 +48,12 @@ const utils = struct {
std.json.parseFree(@TypeOf(value), value, .{ .allocator = alloc }); std.json.parseFree(@TypeOf(value), value, .{ .allocator = alloc });
} }
fn getUserContext(srv: *RequestServer, ctx: *http.server.Context) !api.UserContext { fn getApiContext(srv: *RequestServer, ctx: *http.server.Context) !api.ApiContext {
const header = ctx.request.headers.get("authorization") orelse "(null)"; const header = ctx.request.headers.get("authorization") orelse "(null)";
const token = header[("bearer ").len..]; const token = header[("bearer ").len..];
return try srv.api.makeUserContext(token, srv.alloc); return try srv.api.makeApiContext(token, srv.alloc);
// TODO: defer api.free(srv.alloc, user_ctx); // TODO: defer api.free(srv.alloc, user_ctx);
} }
}; };
@ -62,8 +62,8 @@ const RequestServer = root.RequestServer;
const RouteArgs = http.RouteArgs; const RouteArgs = http.RouteArgs;
pub fn createNote(srv: *RequestServer, ctx: *http.server.Context, _: RouteArgs) !void { pub fn createNote(srv: *RequestServer, ctx: *http.server.Context, _: RouteArgs) !void {
const user_context = try utils.getUserContext(srv, ctx); const user_context = try utils.getApiContext(srv, ctx);
// TODO: defer free usercontext // TODO: defer free ApiContext
const info = try utils.parseRequestBody(api.NoteCreate, ctx, srv.alloc); const info = try utils.parseRequestBody(api.NoteCreate, ctx, srv.alloc);
defer utils.freeRequestBody(info, srv.alloc); defer utils.freeRequestBody(info, srv.alloc);
@ -102,11 +102,33 @@ pub fn getUser(srv: *RequestServer, ctx: *http.server.Context, args: RouteArgs)
try utils.respondJson(ctx, .ok, user, srv.alloc); try utils.respondJson(ctx, .ok, user, srv.alloc);
} }
pub fn react(srv: *RequestServer, ctx: *http.server.Context, args: RouteArgs) !void {
const user_context = try utils.getApiContext(srv, ctx);
// TODO: defer free ApiContext
const note_id = args.get("id") orelse return error.NotFound;
const id = Uuid.parse(note_id) catch return utils.respondJson(ctx, .bad_request, .{ .@"error" = "Invalid UUID" }, srv.alloc);
try srv.api.react(id, user_context);
try utils.respondJson(ctx, .created, .{}, srv.alloc);
}
pub fn listReacts(srv: *RequestServer, ctx: *http.server.Context, args: RouteArgs) !void {
const user_context = try utils.getApiContext(srv, ctx);
// TODO: defer free ApiContext
const note_id = args.get("id") orelse return error.NotFound;
const id = Uuid.parse(note_id) catch return utils.respondJson(ctx, .bad_request, .{ .@"error" = "Invalid UUID" }, srv.alloc);
const reacts = try srv.api.listReacts(id, user_context);
try utils.respondJson(ctx, .ok, .{ .items = reacts }, srv.alloc);
}
pub fn authenticate(srv: *RequestServer, ctx: *http.server.Context, _: RouteArgs) !void { pub fn authenticate(srv: *RequestServer, ctx: *http.server.Context, _: RouteArgs) !void {
const user_ctx = try utils.getUserContext(srv, ctx); const user_ctx = try utils.getApiContext(srv, ctx);
// TODO: defer api.free(srv.alloc, user_ctx); // TODO: defer api.free(srv.alloc, user_ctx);
try utils.respondJson(ctx, .ok, user_ctx, srv.alloc); try utils.respondJson(ctx, .ok, user_ctx.user_context, srv.alloc);
} }
pub fn healthcheck(srv: *RequestServer, ctx: *http.server.Context, _: RouteArgs) !void { pub fn healthcheck(srv: *RequestServer, ctx: *http.server.Context, _: RouteArgs) !void {

View file

@ -4,11 +4,13 @@ const models = @import("./models.zig");
const Uuid = @import("util").Uuid; const Uuid = @import("util").Uuid;
const String = []const u8; const String = []const u8;
const comptimePrint = std.fmt.comptimePrint;
fn tableName(comptime T: type) String { fn tableName(comptime T: type) String {
return switch (T) { return switch (T) {
models.Note => "note", models.Note => "note",
models.User => "user", models.User => "user",
models.Reaction => "reaction",
else => unreachable, else => unreachable,
}; };
} }
@ -19,7 +21,7 @@ fn join(comptime vals: anytype, comptime joiner: String) String {
var result: String = ""; var result: String = "";
for (vals) |v| { for (vals) |v| {
result = std.fmt.comptimePrint("{s}{s}{s}", .{ result, joiner, v }); result = comptimePrint("{s}{s}{s}", .{ result, joiner, v });
} }
return result[joiner.len..]; return result[joiner.len..];
@ -30,14 +32,20 @@ const Query = struct {
select: []const String, select: []const String,
from: String, from: String,
where: String = "id = ?", where: String = "id = ?",
order_by: ?[]const String = null,
group_by: ?[]const String = null,
limit: ?usize = null, limit: ?usize = null,
offset: ?usize = null,
pub fn str(comptime self: Query) String { pub fn str(comptime self: Query) String {
comptime { comptime {
const limit_expr = if (self.limit == null) "" else std.fmt.comptimePrint(" LIMIT {}", .{self.limit}); const order_expr = if (self.order_by == null) "" else comptimePrint(" ORDER BY {s}", .{join(self.order_by.?, ", ")});
return std.fmt.comptimePrint( const group_expr = if (self.group_by == null) "" else comptimePrint(" GROUP BY {s}", .{join(self.group_by.?, ", ")});
"SELECT {s} FROM {s} WHERE {s}{s};", const limit_expr = if (self.limit == null) "" else comptimePrint(" LIMIT {}", .{self.limit});
.{ join(self.select, ", "), self.from, self.where, limit_expr }, const offset_expr = if (self.offset == null) "" else comptimePrint(" OFFSET {}", .{self.offset});
return comptimePrint(
"SELECT {s} FROM {s} WHERE {s}{s}{s}{s}{s};",
.{ join(self.select, ", "), self.from, self.where, order_expr, group_expr, limit_expr, offset_expr },
); );
} }
} }
@ -50,12 +58,12 @@ const Insert = struct {
pub fn str(comptime self: Insert) String { pub fn str(comptime self: Insert) String {
comptime { comptime {
const row = std.fmt.comptimePrint( const row = comptimePrint(
"({s})", "({s})",
.{join(.{"?"} ** self.columns.len, ", ")}, .{join(.{"?"} ** self.columns.len, ", ")},
); );
return std.fmt.comptimePrint( return comptimePrint(
"INSERT INTO {s} ({s}) VALUES {s};", "INSERT INTO {s} ({s}) VALUES {s};",
.{ self.into, join(self.columns, ", "), join(.{row} ** self.count, ", ") }, .{ self.into, join(self.columns, ", "), join(.{row} ** self.count, ", ") },
); );
@ -108,6 +116,17 @@ pub const Database = struct {
\\ \\
\\ FOREIGN KEY (author_id) REFERENCES user(id) \\ FOREIGN KEY (author_id) REFERENCES user(id)
\\) STRICT; \\) STRICT;
,
\\CREATE TABLE IF NOT EXISTS
\\reaction(
\\ reactor_id TEXT NOT NULL,
\\ note_id TEXT NOT NULL,
\\
\\ FOREIGN KEY(reactor_id) REFERENCES user(id),
\\ FOREIGN KEY(note_id) REFERENCES note(id),
\\
\\ PRIMARY KEY(reactor_id, note_id)
\\) STRICT;
}; };
pub fn init() !Database { pub fn init() !Database {
@ -160,6 +179,41 @@ pub const Database = struct {
return result; return result;
} }
pub fn getWhereEq(
self: *Database,
comptime T: type,
comptime field: std.meta.FieldEnum(T),
val: std.meta.fieldInfo(T, field).field_type,
alloc: std.mem.Allocator,
) ![]T {
const field_name = std.meta.fieldInfo(T, field).name;
const fields = comptime fieldsExcept(T, &.{field_name});
const q = comptime (Query{
.select = fields,
.from = tableName(T),
.where = field_name ++ " = ?",
}).str();
var stmt = try self.db.prepare(q);
defer stmt.finalize();
try stmt.bind(1, val);
var results = std.ArrayList(T).init(alloc);
while (try stmt.step()) |row| {
var item: T = undefined;
@field(item, field_name) = val;
inline for (fields) |f, i| {
@field(item, f) = row.getAlloc(@TypeOf(@field(item, f)), i, alloc) catch unreachable;
}
try results.append(item);
}
return results.toOwnedSlice();
}
pub fn countWhereEq( pub fn countWhereEq(
self: *Database, self: *Database,
comptime T: type, comptime T: type,

View file

@ -20,6 +20,8 @@ const router = Router{
Route.new(.GET, "/notes/:id", c.getNote), Route.new(.GET, "/notes/:id", c.getNote),
Route.new(.POST, "/notes", c.createNote), Route.new(.POST, "/notes", c.createNote),
Route.new(.GET, "/notes/:id/reacts", c.listReacts),
Route.new(.POST, "/notes/:id/reacts", c.react),
Route.new(.GET, "/users/:id", c.getUser), Route.new(.GET, "/users/:id", c.getUser),
Route.new(.POST, "/users", c.createUser), Route.new(.POST, "/users", c.createUser),
}, },

View file

@ -16,3 +16,8 @@ pub const User = struct {
id: Uuid, id: Uuid,
handle: []const u8, handle: []const u8,
}; };
pub const Reaction = struct {
reactor_id: Uuid,
note_id: Uuid,
};