From 9f0cac0ed39a1b77a9d4d6e01ca367e37b81ad4c Mon Sep 17 00:00:00 2001 From: jaina heartles Date: Tue, 6 Dec 2022 23:12:11 -0800 Subject: [PATCH] Add user profiles --- src/api/lib.zig | 26 +++++++++++ src/api/services/actors.zig | 45 ++++++++++++++++++- src/main/controllers/api.zig | 1 + src/main/controllers/api/users.zig | 18 +++++++- src/main/migrations.zig | 19 ++++++++ src/sql/lib.zig | 71 ++++++++++++++++++++++++++---- src/util/lib.zig | 14 +++++- 7 files changed, 182 insertions(+), 12 deletions(-) diff --git a/src/api/lib.zig b/src/api/lib.zig index 0aa3f6a..9a6a5d9 100644 --- a/src/api/lib.zig +++ b/src/api/lib.zig @@ -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, }; } diff --git a/src/api/services/actors.zig b/src/api/services/actors.zig index 30dd6d2..448bb98 100644 --- a/src/api/services/actors.zig +++ b/src/api/services/actors.zig @@ -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 {} diff --git a/src/main/controllers/api.zig b/src/main/controllers/api.zig index b767e5d..0bb8feb 100644 --- a/src/main/controllers/api.zig +++ b/src/main/controllers/api.zig @@ -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), diff --git a/src/main/controllers/api/users.zig b/src/main/controllers/api/users.zig index fdc683e..be18e81 100644 --- a/src/main/controllers/api/users.zig +++ b/src/main/controllers/api/users.zig @@ -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); + } +}; diff --git a/src/main/migrations.zig b/src/main/migrations.zig index 86ecaed..1f0886a 100644 --- a/src/main/migrations.zig +++ b/src/main/migrations.zig @@ -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; + , + }, }; diff --git a/src/sql/lib.zig b/src/sql/lib.zig index ba583f2..149e58a 100644 --- a/src/sql/lib.zig +++ b/src/sql/lib.zig @@ -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; } diff --git a/src/util/lib.zig b/src/util/lib.zig index fb2bee0..02bde83 100644 --- a/src/util/lib.zig +++ b/src/util/lib.zig @@ -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)) {