diff --git a/src/api/lib.zig b/src/api/lib.zig index e28bda5..f153140 100644 --- a/src/api/lib.zig +++ b/src/api/lib.zig @@ -781,13 +781,9 @@ fn ApiConn(comptime DbConn: type) type { return try self.backendDriveEntryToFrontend(entry, true); } - pub fn driveMkdir(self: *Self, path: []const u8) !DriveEntry { + pub fn driveMkdir(self: *Self, parent_path: []const u8, name: []const u8) !DriveEntry { const user_id = self.user_id orelse return error.NoToken; - var split = std.mem.splitBackwards(u8, path, "/"); - std.log.debug("{s}", .{path}); - const base = split.first(); - const dir = split.rest(); - const entry = try services.drive.create(self.db, user_id, dir, base, null, self.allocator); + const entry = try services.drive.create(self.db, user_id, parent_path, name, null, self.allocator); errdefer util.deepFree(self.allocator, entry); return try self.backendDriveEntryToFrontend(entry, true); } diff --git a/src/http/multipart.zig b/src/http/multipart.zig index 176ca7b..854b7c3 100644 --- a/src/http/multipart.zig +++ b/src/http/multipart.zig @@ -153,7 +153,7 @@ pub fn openForm(multipart_stream: anytype) MultipartForm(@TypeOf(multipart_strea fn Deserializer(comptime Result: type) type { return util.DeserializerContext(Result, MultipartFormField, struct { - pub const options = .{ .isScalar = isScalar, .embed_unions = true }; + pub const options = .{ .isScalar = isScalar }; pub fn isScalar(comptime T: type) bool { if (T == FormFile or T == ?FormFile) return true; diff --git a/src/http/urlencode.zig b/src/http/urlencode.zig index ad532e2..bd97f50 100644 --- a/src/http/urlencode.zig +++ b/src/http/urlencode.zig @@ -183,7 +183,7 @@ pub fn EncodeStruct(comptime Params: type) type { return struct { params: Params, pub fn format(v: @This(), comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void { - try formatQuery("", "", v.params, writer); + try formatQuery("", v.params, writer); } }; } @@ -221,16 +221,16 @@ fn formatScalar(comptime name: []const u8, val: anytype, writer: anytype) !void try writer.writeByte('&'); } -fn formatQuery(comptime prefix: []const u8, comptime name: []const u8, params: anytype, writer: anytype) !void { +fn formatQuery(comptime prefix: []const u8, params: anytype, writer: anytype) !void { const T = @TypeOf(params); - const eff_prefix = if (prefix.len == 0) "" else prefix ++ "."; - if (comptime isScalar(T)) return formatScalar(eff_prefix ++ name, params, writer); + if (comptime isScalar(T)) return formatScalar(prefix, params, writer); switch (@typeInfo(T)) { .Struct => { + const eff_prefix = if (prefix.len == 0) "" else prefix ++ "."; inline for (std.meta.fields(T)) |field| { const val = @field(params, field.name); - try formatQuery(eff_prefix ++ name, field.name, val, writer); + try formatQuery(eff_prefix ++ field.name, val, writer); } }, .Union => { @@ -239,12 +239,12 @@ fn formatQuery(comptime prefix: []const u8, comptime name: []const u8, params: a const tag_name = field.name; if (@as(std.meta.Tag(T), params) == tag) { const val = @field(params, tag_name); - try formatQuery(prefix, tag_name, val, writer); + try formatQuery(prefix, val, writer); } } }, .Optional => { - if (params) |p| try formatQuery(prefix, name, p, writer); + if (params) |p| try formatQuery(prefix, p, writer); }, else => @compileError("Unsupported query type"), } diff --git a/src/main/controllers/api/communities.zig b/src/main/controllers/api/communities.zig index 224e33c..161fbb0 100644 --- a/src/main/controllers/api/communities.zig +++ b/src/main/controllers/api/communities.zig @@ -1,4 +1,5 @@ const api = @import("api"); +const util = @import("util"); const controller_utils = @import("../../controllers.zig").helpers; const QueryArgs = api.CommunityQueryArgs; @@ -23,11 +24,85 @@ pub const query = struct { pub const method = .GET; pub const path = "/communities"; - pub const Query = QueryArgs; + pub const Query = struct { + const OrderBy = api.CommunityQueryArgs.OrderBy; + const Direction = api.CommunityQueryArgs.Direction; + const PageDirection = api.CommunityQueryArgs.PageDirection; + + // Max items to fetch + max_items: usize = 20, + + // Selection filters + owner_id: ?util.Uuid = null, + like: ?[]const u8 = null, + created_before: ?util.DateTime = null, + created_after: ?util.DateTime = null, + + // Ordering parameter + order_by: OrderBy = .created_at, + direction: Direction = .ascending, + + // Page start parameter + prev: ?union(OrderBy) { + name: struct { + id: util.Uuid, + name: []const u8, + }, + host: struct { + id: util.Uuid, + host: []const u8, + }, + created_at: struct { + id: util.Uuid, + created_at: util.DateTime, + }, + } = null, + + page_direction: PageDirection = .forward, + }; pub fn handler(req: anytype, res: anytype, srv: anytype) !void { - const results = try srv.queryCommunities(req.query); + const q = req.query; + const results = try srv.queryCommunities(.{ + .max_items = q.max_items, + .owner_id = q.owner_id, + .like = q.like, + .created_before = q.created_before, + .created_after = q.created_after, + .order_by = q.order_by, + .direction = q.direction, + .prev = if (q.prev) |prev| switch (prev) { + .name => |p| .{ .id = p.id, .order_val = .{ .name = p.name } }, + .host => |p| .{ .id = p.id, .order_val = .{ .host = p.host } }, + .created_at => |p| .{ .id = p.id, .order_val = .{ .created_at = p.created_at } }, + } else null, + .page_direction = q.page_direction, + }); - try controller_utils.paginate(results, res, req.allocator); + const convert = struct { + fn func(args: api.CommunityQueryArgs) Query { + return .{ + .max_items = args.max_items, + .owner_id = args.owner_id, + .like = args.like, + .created_before = args.created_before, + .created_after = args.created_after, + .order_by = args.order_by, + .direction = args.direction, + .prev = if (args.prev) |prev| switch (prev.order_val) { + .name => |v| .{ .name = .{ .id = prev.id, .name = v } }, + .host => |v| .{ .host = .{ .id = prev.id, .host = v } }, + .created_at => |v| .{ .created_at = .{ .id = prev.id, .created_at = v } }, + } else null, + .page_direction = args.page_direction, + }; + } + }.func; + + try controller_utils.paginate(.{ + .items = results.items, + .next_page = convert(results.next_page), + .prev_page = convert(results.prev_page), + }, res, req.allocator); } }; diff --git a/src/main/controllers/api/drive.zig b/src/main/controllers/api/drive.zig index 4650106..17e8a27 100644 --- a/src/main/controllers/api/drive.zig +++ b/src/main/controllers/api/drive.zig @@ -1,3 +1,4 @@ +const std = @import("std"); const api = @import("api"); const http = @import("http"); const util = @import("util"); @@ -68,7 +69,12 @@ pub const mkdir = struct { pub const Args = DriveArgs; pub fn handler(req: anytype, res: anytype, srv: anytype) !void { - const result = try srv.driveMkdir(req.args.path); + var split = std.mem.splitBackwards(u8, std.mem.trim(u8, req.args.path, "/"), "/"); + const name = split.first(); + const parent = split.rest(); + std.log.debug("{s}, {s}", .{ parent, name }); + + const result = try srv.driveMkdir(parent, name); errdefer util.deepFree(srv.allocator, result); try res.json(.created, result); diff --git a/src/main/controllers/web.zig b/src/main/controllers/web.zig index cde66a8..835e64e 100644 --- a/src/main/controllers/web.zig +++ b/src/main/controllers/web.zig @@ -18,6 +18,7 @@ pub const routes = .{ controllers.apiEndpoint(cluster.communities.create.page), controllers.apiEndpoint(cluster.communities.create.submit), controllers.apiEndpoint(drive.details), + controllers.apiEndpoint(drive.form), }; const static = struct { @@ -231,6 +232,31 @@ const user_details = struct { }; const drive = struct { + const dir_tmpl = @embedFile("./web/drive/directory.tmpl.html"); + fn servePage(req: anytype, res: anytype, srv: anytype) !void { + const info = try srv.driveGet(req.args.path); + defer util.deepFree(srv.allocator, info); + + var breadcrumbs = std.ArrayList([]const u8).init(srv.allocator); + defer breadcrumbs.deinit(); + + var iter = util.PathIter.from(req.args.path); + while (iter.next()) |p| { + std.log.debug("breadcrumb: {s}", .{p}); + try breadcrumbs.append(if (p.len != 0) p else continue); + } + + switch (info) { + .dir => |dir| try res.template(.ok, srv, dir_tmpl, .{ + .dir = dir, + .breadcrumbs = breadcrumbs.items, + .mount_path = req.mount_path, + .base_drive_path = "drive", + }), + else => unreachable, + } + } + const details = struct { pub const path = "/drive/:path*"; pub const method = .GET; @@ -239,29 +265,41 @@ const drive = struct { path: []const u8, }; - pub const dir_tmpl = @embedFile("./web/drive/directory.tmpl.html"); + pub fn handler(req: anytype, res: anytype, srv: anytype) !void { + try servePage(req, res, srv); + } + }; + + const form = struct { + pub const path = "/drive/:path*"; + pub const method = .POST; + + pub const Args = struct { + path: []const u8, + }; + + const Action = enum { + mkcol, + }; + + pub const Body = struct { + action: Action, + data: union(Action) { + mkcol: struct { + name: []const u8, + }, + }, + }; pub fn handler(req: anytype, res: anytype, srv: anytype) !void { - const info = try srv.driveGet(req.args.path); - defer util.deepFree(srv.allocator, info); + if (req.body.action != req.body.data) return error.BadRequest; + switch (req.body.data) { + .mkcol => |data| { + _ = try srv.driveMkdir(req.args.path, data.name); + // TODO - var breadcrumbs = std.ArrayList([]const u8).init(srv.allocator); - defer breadcrumbs.deinit(); - - var iter = util.PathIter.from(req.args.path); - while (iter.next()) |p| { - std.log.debug("breadcrumb: {s}", .{p}); - try breadcrumbs.append(if (p.len != 0) p else continue); - } - - switch (info) { - .dir => |dir| try res.template(.ok, srv, dir_tmpl, .{ - .dir = dir, - .breadcrumbs = breadcrumbs.items, - .mount_path = req.mount_path, - .base_drive_path = "drive", - }), - else => unreachable, + try servePage(req, res, srv); + }, } } }; diff --git a/src/main/controllers/web/drive/directory.tmpl.html b/src/main/controllers/web/drive/directory.tmpl.html index f6d59bf..5135b6a 100644 --- a/src/main/controllers/web/drive/directory.tmpl.html +++ b/src/main/controllers/web/drive/directory.tmpl.html @@ -17,6 +17,28 @@ {/for =} + +