Basic reacts
This commit is contained in:
parent
07019cb090
commit
718da7b408
5 changed files with 120 additions and 21 deletions
|
@ -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);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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),
|
||||||
},
|
},
|
||||||
|
|
|
@ -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,
|
||||||
|
};
|
||||||
|
|
Loading…
Reference in a new issue