2022-09-08 07:52:23 +00:00
|
|
|
const std = @import("std");
|
|
|
|
const util = @import("util");
|
2022-10-02 05:18:24 +00:00
|
|
|
const sql = @import("sql");
|
2022-11-12 12:39:49 +00:00
|
|
|
const common = @import("./common.zig");
|
2022-09-08 07:52:23 +00:00
|
|
|
|
|
|
|
const Uuid = util.Uuid;
|
|
|
|
const DateTime = util.DateTime;
|
|
|
|
|
|
|
|
pub const Note = struct {
|
|
|
|
id: Uuid,
|
|
|
|
|
|
|
|
author_id: Uuid,
|
|
|
|
content: []const u8,
|
|
|
|
created_at: DateTime,
|
|
|
|
};
|
|
|
|
|
2022-10-02 05:18:24 +00:00
|
|
|
pub const CreateError = error{
|
|
|
|
DatabaseFailure,
|
2022-09-08 07:52:23 +00:00
|
|
|
};
|
|
|
|
pub fn create(
|
|
|
|
db: anytype,
|
|
|
|
author: Uuid,
|
|
|
|
content: []const u8,
|
2022-10-02 05:18:24 +00:00
|
|
|
alloc: std.mem.Allocator,
|
|
|
|
) CreateError!Uuid {
|
2022-10-08 20:47:54 +00:00
|
|
|
const id = Uuid.randV4(util.getThreadPrng());
|
2022-09-08 07:52:23 +00:00
|
|
|
|
2022-10-02 05:18:24 +00:00
|
|
|
db.insert("note", .{
|
2022-09-08 07:52:23 +00:00
|
|
|
.id = id,
|
|
|
|
.author_id = author,
|
|
|
|
.content = content,
|
|
|
|
.created_at = DateTime.now(),
|
2022-10-02 05:18:24 +00:00
|
|
|
}, alloc) catch return error.DatabaseFailure;
|
2022-09-08 07:52:23 +00:00
|
|
|
|
|
|
|
return id;
|
|
|
|
}
|
|
|
|
|
2022-10-02 05:18:24 +00:00
|
|
|
pub const GetError = error{
|
|
|
|
DatabaseFailure,
|
|
|
|
NotFound,
|
|
|
|
};
|
2022-10-08 07:51:22 +00:00
|
|
|
const selectStarFromNote = std.fmt.comptimePrint(
|
|
|
|
\\SELECT {s}
|
|
|
|
\\FROM note
|
|
|
|
\\
|
|
|
|
, .{util.comptimeJoin(",", std.meta.fieldNames(Note))});
|
2022-10-02 05:18:24 +00:00
|
|
|
pub fn get(db: anytype, id: Uuid, alloc: std.mem.Allocator) GetError!Note {
|
|
|
|
return db.queryRow(
|
|
|
|
Note,
|
2022-10-08 07:51:22 +00:00
|
|
|
selectStarFromNote ++
|
2022-10-02 05:18:24 +00:00
|
|
|
\\WHERE id = $1
|
|
|
|
\\LIMIT 1
|
|
|
|
,
|
2022-09-08 07:52:23 +00:00
|
|
|
.{id},
|
|
|
|
alloc,
|
2022-10-02 05:18:24 +00:00
|
|
|
) catch |err| switch (err) {
|
|
|
|
error.NoRows => error.NotFound,
|
|
|
|
else => error.DatabaseFailure,
|
2022-09-08 07:52:23 +00:00
|
|
|
};
|
|
|
|
}
|
2022-11-12 12:39:49 +00:00
|
|
|
|
|
|
|
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,
|
|
|
|
};
|
|
|
|
}
|