Add user profiles

This commit is contained in:
jaina heartles 2022-12-06 23:12:11 -08:00
parent 03a5112036
commit 9f0cac0ed3
7 changed files with 182 additions and 12 deletions

View File

@ -41,13 +41,25 @@ pub const InviteOptions = struct {
pub const LoginResponse = services.auth.LoginResult;
pub const ProfileField = services.actors.ProfileField;
pub const UserResponse = struct {
id: Uuid,
username: []const u8,
host: []const u8,
display_name: ?[]const u8,
bio: []const u8,
avatar_file_id: ?Uuid,
header_file_id: ?Uuid,
profile_fields: []const ProfileField,
community_id: Uuid,
created_at: DateTime,
updated_at: DateTime,
};
pub const NoteResponse = struct {
@ -429,6 +441,7 @@ fn ApiConn(comptime DbConn: type) type {
pub fn getUser(self: *Self, user_id: Uuid) !UserResponse {
const user = try services.actors.get(self.db, user_id, self.allocator);
errdefer util.deepFree(self.allocator, user);
if (self.user_id == null) {
if (!Uuid.eql(self.community.id, user.community_id)) return error.NotFound;
@ -436,9 +449,22 @@ fn ApiConn(comptime DbConn: type) type {
return UserResponse{
.id = user.id,
.username = user.username,
.host = user.host,
.display_name = user.display_name,
.bio = user.bio,
.avatar_file_id = user.avatar_file_id,
.header_file_id = user.header_file_id,
.profile_fields = user.profile_fields,
.community_id = user.community_id,
.created_at = user.created_at,
.updated_at = user.updated_at,
};
}

View File

@ -17,7 +17,23 @@ pub const ActorDetailed = struct {
id: Uuid,
username: []const u8,
host: []const u8,
display_name: ?[]const u8,
bio: []const u8,
avatar_file_id: ?Uuid,
header_file_id: ?Uuid,
profile_fields: ProfileField,
created_at: DateTime,
updated_at: DateTime,
};
pub const Profile = struct {
display_name: ?[]const u8,
bio: []const u8,
avatar_file_id: ?Uuid,
header_file_id: ?Uuid,
profile_fields: ProfileField,
};
pub const LookupError = error{
@ -90,15 +106,33 @@ pub fn create(
return id;
}
pub const ProfileField = struct {
key: []const u8,
value: []const u8,
};
pub const Actor = struct {
id: Uuid,
username: []const u8,
host: []const u8,
display_name: ?[]const u8,
bio: []const u8,
avatar_file_id: ?Uuid,
header_file_id: ?Uuid,
profile_fields: []const ProfileField,
community_id: Uuid,
created_at: DateTime,
updated_at: DateTime,
pub const sql_serialize = struct {
pub const profile_fields = .json;
};
};
pub const GetError = error{ NotFound, DatabaseFailure };
@ -109,8 +143,14 @@ pub fn get(db: anytype, id: Uuid, alloc: std.mem.Allocator) GetError!Actor {
\\ actor.id,
\\ actor.username,
\\ community.host,
\\ actor.display_name,
\\ actor.bio,
\\ actor.avatar_file_id,
\\ actor.header_file_id,
\\ actor.profile_fields,
\\ actor.community_id,
\\ actor.created_at
\\ actor.created_at,
\\ actor.updated_at
\\FROM actor JOIN community
\\ ON actor.community_id = community.id
\\WHERE actor.id = $1
@ -123,3 +163,6 @@ pub fn get(db: anytype, id: Uuid, alloc: std.mem.Allocator) GetError!Actor {
else => error.DatabaseFailure,
};
}
pub const max_fields = 32;
//pub fn update(db: anytype, id: Uuid, new: Partial(Profile), alloc: std.mem.Allocator) !Actor {}

View File

@ -17,6 +17,7 @@ pub const routes = .{
controllers.apiEndpoint(communities.query),
controllers.apiEndpoint(invites.create),
controllers.apiEndpoint(users.create),
controllers.apiEndpoint(users.get),
controllers.apiEndpoint(notes.create),
controllers.apiEndpoint(notes.get),
//controllers.apiEndpoint(streaming.streaming),

View File

@ -1,4 +1,4 @@
const api = @import("api");
const util = @import("util");
pub const create = struct {
pub const method = .POST;
@ -24,3 +24,19 @@ pub const create = struct {
try res.json(.created, user);
}
};
pub const get = struct {
pub const method = .GET;
pub const path = "/users/:id";
pub const Args = struct {
id: util.Uuid,
};
pub fn handler(req: anytype, res: anytype, srv: anytype) !void {
const result = try srv.getUser(req.args.id);
defer util.deepFree(srv.allocator, result);
try res.json(.ok, result);
}
};

View File

@ -343,4 +343,23 @@ const migrations: []const Migration = &.{
,
.down = "",
},
.{
.name = "user profiles",
.up =
\\ALTER TABLE actor ADD COLUMN bio TEXT NOT NULL DEFAULT '';
\\ALTER TABLE actor ADD COLUMN display_name TEXT;
\\ALTER TABLE actor ADD COLUMN avatar_file_id UUID REFERENCES file_upload(id);
\\ALTER TABLE actor ADD COLUMN header_file_id UUID REFERENCES file_upload(id);
\\ALTER TABLE actor ADD COLUMN profile_fields JSON DEFAULT '[]';
\\ALTER TABLE actor ADD COLUMN updated_at TIMESTAMPTZ DEFAULT 0;
,
.down =
\\ALTER TABLE actor DROP COLUMN bio;
\\ALTER TABLE actor DROP COLUMN display_name;
\\ALTER TABLE actor DROP COLUMN avatar_file_id;
\\ALTER TABLE actor DROP COLUMN header_file_id;
\\ALTER TABLE actor DROP COLUMN profile_fields;
\\ALTER TABLE actor DROP COLUMN updated_at;
,
},
};

View File

@ -140,7 +140,8 @@ const RawResults = union(Engine) {
}
};
fn FieldPtr(comptime Ptr: type, comptime names: []const []const u8) type {
const FieldRef = []const []const u8;
fn FieldPtr(comptime Ptr: type, comptime names: FieldRef) type {
if (names.len == 0) return Ptr;
const T = std.meta.Child(Ptr);
@ -152,18 +153,48 @@ fn FieldPtr(comptime Ptr: type, comptime names: []const []const u8) type {
return FieldPtr(*field.field_type, names[1..]);
}
fn fieldPtr(ptr: anytype, comptime names: []const []const u8) FieldPtr(@TypeOf(ptr), names) {
fn fieldPtr(ptr: anytype, comptime names: FieldRef) FieldPtr(@TypeOf(ptr), names) {
if (names.len == 0) return ptr;
return fieldPtr(&@field(ptr.*, names[0]), names[1..]);
}
fn getRecursiveFieldList(comptime T: type, comptime prefix: FieldRef, comptime options: anytype) []const FieldRef {
comptime {
if (std.meta.trait.is(.Union)(T) and prefix.len == 0 and options.embed_unions) {
@compileError("Cannot embed a union into nothing");
}
if (options.isScalar(T)) return &.{prefix};
if (std.meta.trait.is(.Optional)(T)) return getRecursiveFieldList(std.meta.Child(T), prefix, options);
const eff_prefix: FieldRef = if (std.meta.trait.is(.Union)(T) and options.embed_unions)
prefix[0 .. prefix.len - 1]
else
prefix;
var fields: []const FieldRef = &.{};
for (std.meta.fields(T)) |f| {
const new_prefix = eff_prefix ++ &[_][]const u8{f.name};
if (@hasDecl(T, "sql_serialize") and @hasDecl(T.sql_serialize, f.name) and @field(T.sql_serialize, f.name) == .json) {
fields = fields ++ &[_]FieldRef{new_prefix};
} else {
const F = f.field_type;
fields = fields ++ getRecursiveFieldList(F, new_prefix, options);
}
}
return fields;
}
}
// Represents a set of results.
// row() must be called until it returns null, or the query may not complete
// Must be deallocated by a call to finish()
pub fn Results(comptime T: type) type {
// would normally make this a declaration of the struct, but it causes the compiler to crash
const fields = if (T == void) .{} else util.serialize.getRecursiveFieldList(
const fields = if (T == void) .{} else getRecursiveFieldList(
T,
&.{},
util.serialize.default_options,
@ -223,11 +254,35 @@ pub fn Results(comptime T: type) type {
//const F = @TypeOf(@field(result, f.name));
const F = std.meta.Child(FieldPtr(*@TypeOf(result), f));
const ptr = fieldPtr(&result, f);
const name = util.comptimeJoin(".", f);
ptr.* = row_val.get(F, self.column_indices[i], alloc) catch |err| {
std.log.err("SQL: Error getting column {s} of type {}", .{ name, F });
return err;
};
const name = comptime util.comptimeJoin(".", f);
const mode = comptime if (@hasDecl(T, "sql_serialize")) blk: {
if (@hasDecl(T.sql_serialize, name)) {
break :blk @field(T.sql_serialize, name);
}
break :blk .default;
} else .default;
switch (mode) {
.default => ptr.* = row_val.get(F, self.column_indices[i], alloc) catch |err| {
std.log.err("SQL: Error getting column {s} of type {}", .{ name, F });
return err;
},
.json => {
const str = row_val.get([]const u8, self.column_indices[i], alloc) catch |err| {
std.log.err("SQL: Error getting column {s} of type {}", .{ name, F });
return err;
};
const a = alloc orelse return error.AllocatorRequired;
defer a.free(str);
var ts = std.json.TokenStream.init(str);
ptr.* = std.json.parse(F, &ts, .{ .allocator = a }) catch |err| {
std.log.err("SQL: Error parsing columns {s} of type {}: {}", .{ name, F, err });
return error.ResultTypeMismatch;
};
},
else => @compileError("unknown mode"),
}
fields_allocated += 1;
}

View File

@ -95,7 +95,17 @@ pub fn deepFree(alloc: ?std.mem.Allocator, val: anytype) void {
else => @compileError("Many and C-style pointers not supported by deepfree"),
},
.Optional => if (val) |v| deepFree(alloc, v) else {},
.Struct => |struct_info| inline for (struct_info.fields) |field| deepFree(alloc, @field(val, field.name)),
.Struct => |struct_info| inline for (struct_info.fields) |field| {
const v = @field(val, field.name);
const should_free = if (field.default_value) |opaque_ptr| blk: {
const aligned = if (field.alignment != 0) @alignCast(field.alignment, opaque_ptr) else opaque_ptr;
const ptr = @ptrCast(*const field.field_type, aligned);
if (std.meta.eql(v, ptr.*)) break :blk false;
break :blk true;
} else true;
if (should_free) deepFree(alloc, @field(val, field.name));
},
.Union => |union_info| inline for (union_info.fields) |field| {
const tag = @field(std.meta.Tag(T), field.name);
if (@as(std.meta.Tag(T), val) == tag) {
@ -114,7 +124,7 @@ pub fn deepFree(alloc: ?std.mem.Allocator, val: anytype) void {
/// Clones a struct/array/slice/etc and all its submembers.
/// Assumes that there are no self-refrential pointers within and that
/// every pointer should be followed.
pub fn deepClone(alloc: std.mem.Allocator, val: anytype) !@TypeOf(val) {
pub fn deepClone(alloc: std.mem.Allocator, val: anytype) std.mem.Allocator.Error!@TypeOf(val) {
const T = @TypeOf(val);
var result: T = undefined;
switch (@typeInfo(T)) {