fediglam/src/api/services/communities.zig

378 lines
11 KiB
Zig
Raw Normal View History

2022-09-07 23:14:52 +00:00
const std = @import("std");
const builtin = @import("builtin");
const util = @import("util");
2022-09-29 21:52:01 +00:00
const sql = @import("sql");
2022-09-07 23:14:52 +00:00
const Uuid = util.Uuid;
2022-09-10 04:02:51 +00:00
const DateTime = util.DateTime;
2022-09-07 23:14:52 +00:00
2022-10-12 05:48:08 +00:00
pub const Community = struct {
pub const Kind = enum {
admin,
local,
2022-09-07 23:14:52 +00:00
2022-10-12 05:48:08 +00:00
pub const jsonStringify = util.jsonSerializeEnumAsString;
};
2022-10-02 05:18:24 +00:00
2022-10-12 05:48:08 +00:00
pub const Scheme = enum {
https,
http,
2022-10-02 05:18:24 +00:00
2022-10-12 05:48:08 +00:00
pub const jsonStringify = util.jsonSerializeEnumAsString;
};
2022-09-07 23:14:52 +00:00
id: Uuid,
2022-10-04 02:41:59 +00:00
owner_id: ?Uuid,
2022-09-07 23:14:52 +00:00
host: []const u8,
name: []const u8,
scheme: Scheme,
2022-09-29 21:52:01 +00:00
kind: Kind,
2022-09-10 04:02:51 +00:00
created_at: DateTime,
2022-09-07 23:14:52 +00:00
};
2022-09-29 21:52:01 +00:00
pub const CreateOptions = struct {
name: ?[]const u8 = null,
2022-10-12 05:48:08 +00:00
kind: Community.Kind = .local,
2022-09-29 21:52:01 +00:00
};
2022-10-02 05:18:24 +00:00
pub const CreateError = error{
DatabaseFailure,
UnsupportedScheme,
InvalidOrigin,
CommunityExists,
};
2022-10-04 02:41:59 +00:00
pub fn create(db: anytype, origin: []const u8, options: CreateOptions, alloc: std.mem.Allocator) CreateError!Uuid {
2022-09-29 21:52:01 +00:00
const scheme_len = std.mem.indexOfScalar(u8, origin, ':') orelse return error.InvalidOrigin;
2022-09-07 23:14:52 +00:00
const scheme_str = origin[0..scheme_len];
2022-10-12 05:48:08 +00:00
const scheme = std.meta.stringToEnum(Community.Scheme, scheme_str) orelse return error.UnsupportedScheme;
2022-09-07 23:14:52 +00:00
// host must be in the format "{scheme}://{host}"
if (origin.len <= scheme_len + ("://").len or
origin[scheme_len] != ':' or
origin[scheme_len + 1] != '/' or
origin[scheme_len + 2] != '/') return error.InvalidOrigin;
const host = origin[scheme_len + 3 ..];
// community cannot use non-default ports (except for testing)
// NOTE: Do not add, say localhost and localhost:80 or bugs may happen.
// Avoid using non-default ports unless a test can't be conducted without it.
2022-09-29 21:52:01 +00:00
if (std.mem.indexOfScalar(u8, host, ':') != null and builtin.mode != .Debug) return error.InvalidOrigin;
2022-09-07 23:14:52 +00:00
// community cannot be hosted on a path
2022-09-29 21:52:01 +00:00
if (std.mem.indexOfScalar(u8, host, '/') != null) return error.InvalidOrigin;
2022-09-07 23:14:52 +00:00
// Require TLS on production builds
if (scheme != .https and builtin.mode != .Debug) return error.UnsupportedScheme;
2022-10-08 20:47:54 +00:00
const id = Uuid.randV4(util.getThreadPrng());
2022-09-07 23:14:52 +00:00
2022-10-02 05:18:24 +00:00
// TODO: wrap this in TX
if (db.queryRow(
std.meta.Tuple(&.{Uuid}),
"SELECT id FROM community WHERE host = $1",
.{host},
alloc,
)) |_| {
return error.CommunityExists;
} else |err| switch (err) {
error.NoRows => {},
else => return error.DatabaseFailure,
}
2022-10-04 02:41:59 +00:00
db.insert("community", .{
2022-09-07 23:14:52 +00:00
.id = id,
2022-10-04 02:41:59 +00:00
.owner_id = null,
2022-09-07 23:14:52 +00:00
.host = host,
2022-09-29 21:52:01 +00:00
.name = options.name orelse host,
2022-09-07 23:14:52 +00:00
.scheme = scheme,
2022-09-29 21:52:01 +00:00
.kind = options.kind,
.created_at = DateTime.now(),
2022-10-04 02:41:59 +00:00
}, alloc) catch return error.DatabaseFailure;
2022-09-07 23:14:52 +00:00
2022-10-02 05:18:24 +00:00
return id;
2022-09-07 23:14:52 +00:00
}
2022-10-02 05:18:24 +00:00
pub const GetError = error{
NotFound,
DatabaseFailure,
};
2022-10-04 02:41:59 +00:00
fn getWhere(
db: anytype,
comptime where: []const u8,
args: anytype,
alloc: std.mem.Allocator,
) GetError!Community {
2022-10-02 05:18:24 +00:00
return db.queryRow(
2022-09-29 21:52:01 +00:00
Community,
2022-10-02 05:18:24 +00:00
std.fmt.comptimePrint(
\\SELECT {s}
\\FROM community
2022-10-04 02:41:59 +00:00
\\WHERE {s}
2022-10-02 05:18:24 +00:00
\\LIMIT 1
,
2022-10-04 02:41:59 +00:00
.{
comptime util.comptimeJoin(",", std.meta.fieldNames(Community)),
where,
},
2022-10-02 05:18:24 +00:00
),
2022-10-04 02:41:59 +00:00
args,
2022-09-29 21:52:01 +00:00
alloc,
2022-10-02 05:18:24 +00:00
) catch |err| switch (err) {
error.NoRows => error.NotFound,
else => error.DatabaseFailure,
};
2022-09-08 05:10:58 +00:00
}
2022-09-08 06:56:29 +00:00
2022-10-04 02:41:59 +00:00
pub fn get(db: anytype, id: Uuid, alloc: std.mem.Allocator) GetError!Community {
return getWhere(db, "id = $1", .{id}, alloc);
}
pub fn getByHost(db: anytype, host: []const u8, alloc: std.mem.Allocator) GetError!Community {
return getWhere(db, "host = $1", .{host}, alloc);
}
2022-09-08 06:56:29 +00:00
pub fn transferOwnership(db: anytype, community_id: Uuid, new_owner: Uuid) !void {
2022-10-02 05:18:24 +00:00
// TODO: check that this actually found/updated the row (needs update to sql lib)
db.exec(
"UPDATE community SET owner_id = $1 WHERE id = $2",
.{ new_owner, community_id },
null,
) catch return error.DatabaseFailure;
2022-09-08 06:56:29 +00:00
}
2022-09-10 04:02:51 +00:00
pub const QueryArgs = struct {
pub const OrderBy = enum {
name,
host,
created_at,
2022-10-08 07:51:22 +00:00
pub const jsonStringify = util.jsonSerializeEnumAsString;
2022-09-10 04:02:51 +00:00
};
2022-10-11 04:49:36 +00:00
pub const Direction = enum {
ascending,
descending,
pub const jsonStringify = util.jsonSerializeEnumAsString;
};
pub const PageDirection = enum {
forward,
backward,
pub const jsonStringify = util.jsonSerializeEnumAsString;
};
pub const Prev = std.meta.Child(std.meta.fieldInfo(QueryArgs, .prev).field_type);
2022-10-11 05:19:58 +00:00
pub const OrderVal = std.meta.fieldInfo(Prev, .order_val).field_type;
2022-10-11 04:49:36 +00:00
2022-09-10 04:02:51 +00:00
// Max items to fetch
max_items: usize = 20,
// Selection filters
owner_id: ?Uuid = null, // searches for communities owned by this user
like: ?[]const u8 = null, // searches for communities with host or name LIKE '%?%'
created_before: ?DateTime = null,
created_after: ?DateTime = null,
// Ordering parameter
order_by: OrderBy = .created_at,
2022-10-11 04:49:36 +00:00
direction: Direction = .ascending,
2022-09-10 04:02:51 +00:00
// Page start parameter
// This struct is a reference to the last value scanned
// If prev is present, then prev.order_val must have the same tag as order_by
// "prev" here refers to it being the previous value returned. It may be that
// prev refers to the item directly after the results you are about to recieve,
// if you are querying the previous page.
prev: ?struct {
id: Uuid,
order_val: union(OrderBy) {
name: []const u8,
host: []const u8,
created_at: DateTime,
},
} = null,
// What direction to scan the page window
// If "forward", then "prev" is interpreted as the item directly before the items
// to query, in the direction of "direction" above. If "backward", then the opposite
2022-10-11 04:49:36 +00:00
page_direction: PageDirection = .forward,
2022-09-10 04:02:51 +00:00
};
2022-10-11 05:19:58 +00:00
pub const QueryResult = struct {
items: []const Community,
prev_page: QueryArgs,
next_page: QueryArgs,
};
2022-10-02 05:18:24 +00:00
const QueryBuilder = struct {
2022-09-10 04:02:51 +00:00
array: std.ArrayList(u8),
where_clauses_appended: usize = 0,
2022-10-02 05:18:24 +00:00
pub fn init(alloc: std.mem.Allocator) QueryBuilder {
return QueryBuilder{ .array = std.ArrayList(u8).init(alloc) };
2022-09-10 04:02:51 +00:00
}
2022-10-02 05:18:24 +00:00
pub fn deinit(self: *const QueryBuilder) void {
2022-09-10 04:02:51 +00:00
self.array.deinit();
}
2022-10-02 05:18:24 +00:00
pub fn andWhere(self: *QueryBuilder, clause: []const u8) !void {
2022-09-10 04:02:51 +00:00
if (self.where_clauses_appended == 0) {
try self.array.appendSlice("WHERE ");
} else {
try self.array.appendSlice(" AND ");
}
try self.array.appendSlice(clause);
self.where_clauses_appended += 1;
}
};
const max_max_items = 100;
2022-10-02 05:18:24 +00:00
pub const QueryError = error{
PageArgMismatch,
DatabaseError,
};
// Retrieves up to `args.max_items` Community entries matching the given query
// arguments.
// `args.max_items` is only a request, and fewer entries may be returned.
2022-10-11 05:19:58 +00:00
pub fn query(db: anytype, args: QueryArgs, alloc: std.mem.Allocator) !QueryResult {
2022-10-02 05:18:24 +00:00
var builder = QueryBuilder.init(alloc);
2022-09-10 04:02:51 +00:00
defer builder.deinit();
try builder.array.appendSlice(
2022-10-02 05:18:24 +00:00
std.fmt.comptimePrint(
\\SELECT {s}
\\FROM community
\\
2022-10-11 04:49:36 +00:00
, .{comptime util.comptimeJoin(",", std.meta.fieldNames(Community))}),
2022-09-10 04:02:51 +00:00
);
2022-10-02 05:18:24 +00:00
2022-09-10 04:02:51 +00:00
const max_items = if (args.max_items > max_max_items) max_max_items else args.max_items;
if (args.owner_id != null) try builder.andWhere("owner_id = $1");
2022-10-12 02:28:36 +00:00
if (args.like != null) try builder.andWhere("(host LIKE ('%' || $2 || '%') OR name LIKE ('%' || $2 || '%'))");
2022-09-10 04:02:51 +00:00
if (args.created_before != null) try builder.andWhere("created_at < $3");
if (args.created_after != null) try builder.andWhere("created_at > $4");
if (args.prev) |prev| {
if (prev.order_val != args.order_by) return error.PageArgMismatch;
try builder.andWhere(switch (args.order_by) {
.name => "(name, id)",
.host => "(host, id)",
.created_at => "(created_at, id)",
});
_ = try builder.array.appendSlice(switch (args.direction) {
.ascending => switch (args.page_direction) {
.forward => " > ",
.backward => " < ",
},
.descending => switch (args.page_direction) {
.forward => " < ",
.backward => " > ",
},
});
_ = try builder.array.appendSlice("($5, $6)");
}
const direction_string = switch (args.direction) {
.ascending => " ASC ",
.descending => " DESC ",
};
2022-09-10 04:02:51 +00:00
_ = try builder.array.appendSlice("\nORDER BY ");
_ = try builder.array.appendSlice(@tagName(args.order_by));
_ = try builder.array.appendSlice(direction_string);
2022-09-10 04:02:51 +00:00
_ = try builder.array.appendSlice(", id ");
_ = try builder.array.appendSlice(direction_string);
2022-09-10 04:02:51 +00:00
_ = try builder.array.appendSlice("\nLIMIT $7");
const query_args = .{
args.owner_id,
args.like,
args.created_before,
args.created_after,
if (args.prev) |prev| prev.order_val else null,
if (args.prev) |prev| prev.id else null,
max_items,
};
2022-10-11 04:49:36 +00:00
try builder.array.append(0);
var results = try db.queryWithOptions(
Community,
std.meta.assumeSentinel(builder.array.items, 0),
2022-09-10 04:02:51 +00:00
query_args,
2022-10-11 04:49:36 +00:00
.{ .prep_allocator = alloc, .ignore_unused_arguments = true },
2022-09-10 04:02:51 +00:00
);
defer results.finish();
const result_buf = try alloc.alloc(Community, args.max_items);
errdefer alloc.free(result_buf);
var count: usize = 0;
2022-09-29 21:52:01 +00:00
errdefer for (result_buf[0..count]) |c| util.deepFree(alloc, c);
2022-09-10 04:02:51 +00:00
for (result_buf) |*c| {
2022-10-11 04:49:36 +00:00
c.* = (try results.row(alloc)) orelse break;
2022-09-10 04:02:51 +00:00
count += 1;
}
2022-10-11 05:19:58 +00:00
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,
.order_val = getOrderVal(result_buf[0], args.order_by),
};
next_page.prev = .{
.id = result_buf[count - 1].id,
.order_val = getOrderVal(result_buf[count - 1], args.order_by),
};
}
2022-10-12 02:28:36 +00:00
// TODO: This will give incorrect links on an empty page
2022-10-11 05:19:58 +00:00
return QueryResult{
.items = result_buf[0..count],
.next_page = next_page,
.prev_page = prev_page,
};
}
fn getOrderVal(community: Community, order_val: QueryArgs.OrderBy) QueryArgs.OrderVal {
return switch (order_val) {
.name => .{ .name = community.name },
.host => .{ .host = community.host },
.created_at => .{ .created_at = community.created_at },
};
2022-09-10 04:02:51 +00:00
}
2022-09-29 21:52:01 +00:00
pub fn adminCommunityId(db: anytype) !Uuid {
2022-10-02 05:18:24 +00:00
const row = db.queryRow(
2022-09-29 21:52:01 +00:00
std.meta.Tuple(&.{Uuid}),
"SELECT id FROM community WHERE kind = 'admin' LIMIT 1",
2022-10-02 05:18:24 +00:00
{},
2022-09-29 21:52:01 +00:00
null,
2022-10-02 05:18:24 +00:00
) catch |err| return switch (err) {
error.NoRows => error.NotFound,
else => error.DatabaseFailure,
};
2022-09-29 21:52:01 +00:00
return row[0];
}