Add user profiles
This commit is contained in:
parent
03a5112036
commit
9f0cac0ed3
7 changed files with 182 additions and 12 deletions
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {}
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
,
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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| {
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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)) {
|
||||
|
|
Loading…
Reference in a new issue