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 LoginResponse = services.auth.LoginResult;
|
||||||
|
|
||||||
|
pub const ProfileField = services.actors.ProfileField;
|
||||||
pub const UserResponse = struct {
|
pub const UserResponse = struct {
|
||||||
id: Uuid,
|
id: Uuid,
|
||||||
|
|
||||||
username: []const u8,
|
username: []const u8,
|
||||||
host: []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,
|
created_at: DateTime,
|
||||||
|
updated_at: DateTime,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const NoteResponse = struct {
|
pub const NoteResponse = struct {
|
||||||
|
@ -429,6 +441,7 @@ fn ApiConn(comptime DbConn: type) type {
|
||||||
|
|
||||||
pub fn getUser(self: *Self, user_id: Uuid) !UserResponse {
|
pub fn getUser(self: *Self, user_id: Uuid) !UserResponse {
|
||||||
const user = try services.actors.get(self.db, user_id, self.allocator);
|
const user = try services.actors.get(self.db, user_id, self.allocator);
|
||||||
|
errdefer util.deepFree(self.allocator, user);
|
||||||
|
|
||||||
if (self.user_id == null) {
|
if (self.user_id == null) {
|
||||||
if (!Uuid.eql(self.community.id, user.community_id)) return error.NotFound;
|
if (!Uuid.eql(self.community.id, user.community_id)) return error.NotFound;
|
||||||
|
@ -436,9 +449,22 @@ fn ApiConn(comptime DbConn: type) type {
|
||||||
|
|
||||||
return UserResponse{
|
return UserResponse{
|
||||||
.id = user.id,
|
.id = user.id,
|
||||||
|
|
||||||
.username = user.username,
|
.username = user.username,
|
||||||
.host = user.host,
|
.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,
|
.created_at = user.created_at,
|
||||||
|
.updated_at = user.updated_at,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,7 +17,23 @@ pub const ActorDetailed = struct {
|
||||||
id: Uuid,
|
id: Uuid,
|
||||||
username: []const u8,
|
username: []const u8,
|
||||||
host: []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,
|
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{
|
pub const LookupError = error{
|
||||||
|
@ -90,15 +106,33 @@ pub fn create(
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub const ProfileField = struct {
|
||||||
|
key: []const u8,
|
||||||
|
value: []const u8,
|
||||||
|
};
|
||||||
|
|
||||||
pub const Actor = struct {
|
pub const Actor = struct {
|
||||||
id: Uuid,
|
id: Uuid,
|
||||||
|
|
||||||
username: []const u8,
|
username: []const u8,
|
||||||
host: []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,
|
community_id: Uuid,
|
||||||
|
|
||||||
created_at: DateTime,
|
created_at: DateTime,
|
||||||
|
updated_at: DateTime,
|
||||||
|
|
||||||
|
pub const sql_serialize = struct {
|
||||||
|
pub const profile_fields = .json;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const GetError = error{ NotFound, DatabaseFailure };
|
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.id,
|
||||||
\\ actor.username,
|
\\ actor.username,
|
||||||
\\ community.host,
|
\\ community.host,
|
||||||
|
\\ actor.display_name,
|
||||||
|
\\ actor.bio,
|
||||||
|
\\ actor.avatar_file_id,
|
||||||
|
\\ actor.header_file_id,
|
||||||
|
\\ actor.profile_fields,
|
||||||
\\ actor.community_id,
|
\\ actor.community_id,
|
||||||
\\ actor.created_at
|
\\ actor.created_at,
|
||||||
|
\\ actor.updated_at
|
||||||
\\FROM actor JOIN community
|
\\FROM actor JOIN community
|
||||||
\\ ON actor.community_id = community.id
|
\\ ON actor.community_id = community.id
|
||||||
\\WHERE actor.id = $1
|
\\WHERE actor.id = $1
|
||||||
|
@ -123,3 +163,6 @@ pub fn get(db: anytype, id: Uuid, alloc: std.mem.Allocator) GetError!Actor {
|
||||||
else => error.DatabaseFailure,
|
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(communities.query),
|
||||||
controllers.apiEndpoint(invites.create),
|
controllers.apiEndpoint(invites.create),
|
||||||
controllers.apiEndpoint(users.create),
|
controllers.apiEndpoint(users.create),
|
||||||
|
controllers.apiEndpoint(users.get),
|
||||||
controllers.apiEndpoint(notes.create),
|
controllers.apiEndpoint(notes.create),
|
||||||
controllers.apiEndpoint(notes.get),
|
controllers.apiEndpoint(notes.get),
|
||||||
//controllers.apiEndpoint(streaming.streaming),
|
//controllers.apiEndpoint(streaming.streaming),
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
const api = @import("api");
|
const util = @import("util");
|
||||||
|
|
||||||
pub const create = struct {
|
pub const create = struct {
|
||||||
pub const method = .POST;
|
pub const method = .POST;
|
||||||
|
@ -24,3 +24,19 @@ pub const create = struct {
|
||||||
try res.json(.created, user);
|
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 = "",
|
.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;
|
if (names.len == 0) return Ptr;
|
||||||
|
|
||||||
const T = std.meta.Child(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..]);
|
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;
|
if (names.len == 0) return ptr;
|
||||||
|
|
||||||
return fieldPtr(&@field(ptr.*, names[0]), names[1..]);
|
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.
|
// Represents a set of results.
|
||||||
// row() must be called until it returns null, or the query may not complete
|
// row() must be called until it returns null, or the query may not complete
|
||||||
// Must be deallocated by a call to finish()
|
// Must be deallocated by a call to finish()
|
||||||
pub fn Results(comptime T: type) type {
|
pub fn Results(comptime T: type) type {
|
||||||
// would normally make this a declaration of the struct, but it causes the compiler to crash
|
// 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,
|
T,
|
||||||
&.{},
|
&.{},
|
||||||
util.serialize.default_options,
|
util.serialize.default_options,
|
||||||
|
@ -223,11 +254,35 @@ pub fn Results(comptime T: type) type {
|
||||||
//const F = @TypeOf(@field(result, f.name));
|
//const F = @TypeOf(@field(result, f.name));
|
||||||
const F = std.meta.Child(FieldPtr(*@TypeOf(result), f));
|
const F = std.meta.Child(FieldPtr(*@TypeOf(result), f));
|
||||||
const ptr = fieldPtr(&result, f);
|
const ptr = fieldPtr(&result, f);
|
||||||
const name = util.comptimeJoin(".", f);
|
const name = comptime 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 });
|
const mode = comptime if (@hasDecl(T, "sql_serialize")) blk: {
|
||||||
return err;
|
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;
|
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"),
|
else => @compileError("Many and C-style pointers not supported by deepfree"),
|
||||||
},
|
},
|
||||||
.Optional => if (val) |v| deepFree(alloc, v) else {},
|
.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| {
|
.Union => |union_info| inline for (union_info.fields) |field| {
|
||||||
const tag = @field(std.meta.Tag(T), field.name);
|
const tag = @field(std.meta.Tag(T), field.name);
|
||||||
if (@as(std.meta.Tag(T), val) == tag) {
|
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.
|
/// Clones a struct/array/slice/etc and all its submembers.
|
||||||
/// Assumes that there are no self-refrential pointers within and that
|
/// Assumes that there are no self-refrential pointers within and that
|
||||||
/// every pointer should be followed.
|
/// 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);
|
const T = @TypeOf(val);
|
||||||
var result: T = undefined;
|
var result: T = undefined;
|
||||||
switch (@typeInfo(T)) {
|
switch (@typeInfo(T)) {
|
||||||
|
|
Loading…
Reference in a new issue