const std = @import("std"); const util = @import("util"); const sql = @import("sql"); const common = @import("./common.zig"); const Uuid = util.Uuid; const DateTime = util.DateTime; pub const Note = struct { id: Uuid, author_id: Uuid, content: []const u8, created_at: DateTime, }; pub const CreateError = error{ DatabaseFailure, }; pub fn create( db: anytype, author: Uuid, content: []const u8, alloc: std.mem.Allocator, ) CreateError!Uuid { const id = Uuid.randV4(util.getThreadPrng()); db.insert("note", .{ .id = id, .author_id = author, .content = content, .created_at = DateTime.now(), }, alloc) catch return error.DatabaseFailure; return id; } pub const GetError = error{ DatabaseFailure, NotFound, }; const selectStarFromNote = std.fmt.comptimePrint( \\SELECT {s} \\FROM note \\ , .{util.comptimeJoin(",", std.meta.fieldNames(Note))}); pub fn get(db: anytype, id: Uuid, alloc: std.mem.Allocator) GetError!Note { return db.queryRow( Note, selectStarFromNote ++ \\WHERE id = $1 \\LIMIT 1 , .{id}, alloc, ) catch |err| switch (err) { error.NoRows => error.NotFound, else => error.DatabaseFailure, }; } const max_max_items = 100; pub const QueryArgs = struct { pub const PageDirection = common.PageDirection; pub const Prev = std.meta.Child(std.meta.field(@This(), .prev).field_type); max_items: usize = 20, created_before: ?DateTime = null, created_after: ?DateTime = null, prev: ?struct { id: Uuid, created_at: DateTime, } = null, page_direction: PageDirection = .forward, }; pub const QueryResult = struct { items: []Note, prev_page: QueryArgs, next_page: QueryArgs, }; pub fn query(db: anytype, args: QueryArgs, alloc: std.mem.Allocator) !QueryResult { var builder = sql.QueryBuilder.init(alloc); defer builder.deinit(); try builder.appendSlice(selectStarFromNote); if (args.created_before != null) try builder.andWhere("note.created_at < $1"); if (args.created_after != null) try builder.andWhere("note.created_at > $2"); if (args.prev != null) { try builder.andWhere("(note.created_at, note.id)"); switch (args.page_direction) { .forward => try builder.appendSlice(" < "), .backward => try builder.appendSlice(" > "), } try builder.appendSlice("($3, $4)"); } try builder.appendSlice( \\ \\ORDER BY note.created_at DESC \\LIMIT $5 \\ ); const max_items = if (args.max_items > max_max_items) max_max_items else args.max_items; const query_args = blk: { const prev_created_at = if (args.prev) |prev| @as(?DateTime, prev.created_at) else null; const prev_id = if (args.prev) |prev| @as(?Uuid, prev.id) else null; break :blk .{ args.created_before, args.created_after, prev_created_at, prev_id, max_items, }; }; var results = try db.queryWithOptions( Note, try builder.terminate(), query_args, .{ .prep_allocator = alloc, .ignore_unused_arguments = true }, ); defer results.finish(); const result_buf = try alloc.alloc(Note, args.max_items); errdefer alloc.free(result_buf); var count: usize = 0; errdefer for (result_buf[0..count]) |c| util.deepFree(alloc, c); for (result_buf) |*c| { c.* = (try results.row(alloc)) orelse break; count += 1; } var next_page = args; var prev_page = args; prev_page.page_direction = .backward; next_page.page_direction = .forward; if (count != 0) { prev_page.prev = .{ .id = result_buf[0].id, .created_at = result_buf[0].created_at, }; next_page.prev = .{ .id = result_buf[count - 1].id, .created_at = result_buf[count - 1].created_at, }; } // TODO: this will give incorrect links on an empty page return QueryResult{ .items = result_buf[0..count], .next_page = next_page, .prev_page = prev_page, }; }