278 lines
8.7 KiB
Zig
278 lines
8.7 KiB
Zig
const std = @import("std");
|
|
const builtin = @import("builtin");
|
|
const util = @import("util");
|
|
const sql = @import("sql");
|
|
const actors = @import("./actors.zig");
|
|
const types = @import("./types.zig");
|
|
|
|
const Uuid = util.Uuid;
|
|
const DateTime = util.DateTime;
|
|
const Community = types.communities.Community;
|
|
const Scheme = types.communities.Scheme;
|
|
const CreateOptions = types.communities.CreateOptions;
|
|
const QueryArgs = types.communities.QueryArgs;
|
|
const QueryResult = types.communities.QueryResult;
|
|
|
|
pub const CreateError = error{
|
|
UnsupportedScheme,
|
|
InvalidOrigin,
|
|
CommunityExists,
|
|
} || sql.DatabaseError;
|
|
|
|
pub fn create(db: anytype, origin: []const u8, options: CreateOptions, alloc: std.mem.Allocator) CreateError!Uuid {
|
|
const scheme_len = std.mem.indexOfScalar(u8, origin, ':') orelse return error.InvalidOrigin;
|
|
const scheme_str = origin[0..scheme_len];
|
|
const scheme = std.meta.stringToEnum(Scheme, scheme_str) orelse return error.UnsupportedScheme;
|
|
|
|
// 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.
|
|
if (std.mem.indexOfScalar(u8, host, ':') != null and builtin.mode != .Debug) return error.InvalidOrigin;
|
|
|
|
// community cannot be hosted on a path
|
|
if (std.mem.indexOfScalar(u8, host, '/') != null) return error.InvalidOrigin;
|
|
|
|
// Require TLS on production builds
|
|
if (scheme != .https and builtin.mode != .Debug) return error.UnsupportedScheme;
|
|
|
|
const id = Uuid.randV4(util.getThreadPrng());
|
|
|
|
// TODO: wrap this in TX
|
|
var tx = try db.beginOrSavepoint();
|
|
errdefer tx.rollback();
|
|
if (tx.queryRow(
|
|
std.meta.Tuple(&.{Uuid}),
|
|
"SELECT id FROM community WHERE host = $1",
|
|
.{host},
|
|
alloc,
|
|
)) |_| {
|
|
return error.CommunityExists;
|
|
} else |err| switch (err) {
|
|
error.NoRows => {},
|
|
else => |e| return e,
|
|
}
|
|
|
|
const name = options.name orelse host;
|
|
try tx.insert("community", .{
|
|
.id = id,
|
|
.owner_id = null,
|
|
.host = host,
|
|
.name = name,
|
|
.scheme = scheme,
|
|
.kind = options.kind,
|
|
.created_at = DateTime.now(),
|
|
}, alloc);
|
|
|
|
if (options.kind == .local) {
|
|
const actor_id = actors.create(tx, "community.actor", id, true, alloc) catch |err| switch (err) {
|
|
error.UsernameContainsInvalidChar,
|
|
error.UsernameTooLong,
|
|
error.UsernameEmpty,
|
|
error.UsernameTaken,
|
|
=> unreachable,
|
|
else => @panic("TODO"),
|
|
};
|
|
try tx.exec(
|
|
\\UPDATE community
|
|
\\SET community_actor_id = $1
|
|
\\WHERE id = $2
|
|
, .{ actor_id, id }, alloc);
|
|
}
|
|
|
|
try tx.commitOrRelease();
|
|
return id;
|
|
}
|
|
|
|
pub const GetError = error{
|
|
NotFound,
|
|
DatabaseFailure,
|
|
};
|
|
|
|
fn getWhere(
|
|
db: anytype,
|
|
comptime where: []const u8,
|
|
args: anytype,
|
|
alloc: std.mem.Allocator,
|
|
) GetError!Community {
|
|
return db.queryRow(
|
|
Community,
|
|
std.fmt.comptimePrint(
|
|
\\SELECT {s}
|
|
\\FROM community
|
|
\\WHERE {s}
|
|
\\LIMIT 1
|
|
,
|
|
.{
|
|
comptime util.comptimeJoin(",", std.meta.fieldNames(Community)),
|
|
where,
|
|
},
|
|
),
|
|
args,
|
|
alloc,
|
|
) catch |err| switch (err) {
|
|
error.NoRows => error.NotFound,
|
|
else => error.DatabaseFailure,
|
|
};
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
pub fn transferOwnership(db: anytype, community_id: Uuid, new_owner: Uuid) !void {
|
|
// 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;
|
|
}
|
|
|
|
const max_max_items = 100;
|
|
|
|
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.
|
|
pub fn query(db: anytype, args: QueryArgs, alloc: std.mem.Allocator) !QueryResult {
|
|
var builder = sql.QueryBuilder.init(alloc);
|
|
defer builder.deinit();
|
|
|
|
try builder.array.appendSlice(
|
|
std.fmt.comptimePrint(
|
|
\\SELECT {s}
|
|
\\FROM community
|
|
\\
|
|
, .{comptime util.comptimeJoin(",", std.meta.fieldNames(Community))}),
|
|
);
|
|
|
|
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");
|
|
if (args.like != null) try builder.andWhere("(host LIKE ('%' || $2 || '%') OR name LIKE ('%' || $2 || '%'))");
|
|
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;
|
|
|
|
switch (args.order_by) {
|
|
.name => try builder.andWhere("(name, id)"),
|
|
.host => try builder.andWhere("(host, id)"),
|
|
.created_at => try builder.andWhere("(created_at, id)"),
|
|
}
|
|
switch (args.direction) {
|
|
.ascending => switch (args.page_direction) {
|
|
.forward => try builder.appendSlice(" > "),
|
|
.backward => try builder.appendSlice(" < "),
|
|
},
|
|
.descending => switch (args.page_direction) {
|
|
.forward => try builder.appendSlice(" < "),
|
|
.backward => try builder.appendSlice(" > "),
|
|
},
|
|
}
|
|
|
|
_ = try builder.array.appendSlice("($5, $6)");
|
|
}
|
|
|
|
const direction_string = switch (args.direction) {
|
|
.ascending => " ASC ",
|
|
.descending => " DESC ",
|
|
};
|
|
_ = try builder.array.appendSlice("\nORDER BY ");
|
|
_ = try builder.array.appendSlice(@tagName(args.order_by));
|
|
_ = try builder.array.appendSlice(direction_string);
|
|
_ = try builder.array.appendSlice(", id ");
|
|
_ = try builder.array.appendSlice(direction_string);
|
|
|
|
_ = try builder.array.appendSlice("\nLIMIT $7");
|
|
|
|
const query_args = blk: {
|
|
const ord_val =
|
|
if (args.prev) |prev| @as(?QueryArgs.OrderVal, prev.order_val) else null;
|
|
const id =
|
|
if (args.prev) |prev| @as(?Uuid, prev.id) else null;
|
|
break :blk .{
|
|
args.owner_id,
|
|
args.like,
|
|
args.created_before,
|
|
args.created_after,
|
|
ord_val,
|
|
id,
|
|
max_items,
|
|
};
|
|
};
|
|
|
|
try builder.array.append(0);
|
|
|
|
var results = try db.queryRowsWithOptions(
|
|
Community,
|
|
std.meta.assumeSentinel(builder.array.items, 0),
|
|
query_args,
|
|
max_items,
|
|
.{ .allocator = alloc, .ignore_unused_arguments = true },
|
|
);
|
|
errdefer util.deepFree(alloc, results);
|
|
|
|
var next_page = args;
|
|
var prev_page = args;
|
|
prev_page.page_direction = .backward;
|
|
next_page.page_direction = .forward;
|
|
if (results.len != 0) {
|
|
prev_page.prev = .{
|
|
.id = results[0].id,
|
|
.order_val = getOrderVal(results[0], args.order_by),
|
|
};
|
|
|
|
next_page.prev = .{
|
|
.id = results[results.len - 1].id,
|
|
.order_val = getOrderVal(results[results.len - 1], args.order_by),
|
|
};
|
|
}
|
|
// TODO: This will give incorrect links on an empty page
|
|
|
|
return QueryResult{
|
|
.items = results,
|
|
|
|
.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 },
|
|
};
|
|
}
|
|
|
|
pub fn adminCommunityId(db: anytype) !Uuid {
|
|
const row = db.queryRow(
|
|
std.meta.Tuple(&.{Uuid}),
|
|
"SELECT id FROM community WHERE kind = 'admin' LIMIT 1",
|
|
{},
|
|
null,
|
|
) catch |err| return switch (err) {
|
|
error.NoRows => error.NotFound,
|
|
else => error.DatabaseFailure,
|
|
};
|
|
|
|
return row[0];
|
|
}
|