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 getRandom = @import("../api.zig").getRandom;
|
|
|
|
|
|
|
|
const Uuid = util.Uuid;
|
2022-09-10 04:02:51 +00:00
|
|
|
const DateTime = util.DateTime;
|
2022-09-07 23:14:52 +00:00
|
|
|
|
|
|
|
const CreateError = error{
|
|
|
|
InvalidOrigin,
|
|
|
|
UnsupportedScheme,
|
|
|
|
CommunityExists,
|
2022-09-15 01:12:07 +00:00
|
|
|
} || anyerror; // TODO
|
2022-09-07 23:14:52 +00:00
|
|
|
|
|
|
|
pub const Scheme = enum {
|
|
|
|
https,
|
|
|
|
http,
|
|
|
|
|
|
|
|
pub fn jsonStringify(s: Scheme, _: std.json.StringifyOptions, writer: anytype) !void {
|
|
|
|
return std.fmt.format(writer, "\"{s}\"", .{@tagName(s)});
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
pub const Community = struct {
|
|
|
|
id: Uuid,
|
|
|
|
|
2022-09-29 21:52:01 +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 Kind = enum {
|
|
|
|
admin,
|
|
|
|
local,
|
|
|
|
|
|
|
|
pub fn jsonStringify(val: Kind, _: std.json.StringifyOptions, writer: anytype) !void {
|
|
|
|
return std.fmt.format(writer, "\"{s}\"", .{@tagName(val)});
|
|
|
|
}
|
|
|
|
};
|
2022-09-10 04:02:51 +00:00
|
|
|
|
2022-09-29 21:52:01 +00:00
|
|
|
pub const CreateOptions = struct {
|
|
|
|
name: ?[]const u8 = null,
|
|
|
|
kind: Kind = .local,
|
|
|
|
};
|
|
|
|
|
|
|
|
pub fn create(db: anytype, origin: []const u8, owner: Uuid, options: CreateOptions) CreateError!Community {
|
|
|
|
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];
|
|
|
|
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.
|
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;
|
|
|
|
|
|
|
|
const id = Uuid.randV4(getRandom());
|
|
|
|
|
|
|
|
const community = Community{
|
|
|
|
.id = id,
|
2022-09-29 21:52:01 +00:00
|
|
|
.owner_id = owner,
|
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,
|
2022-09-10 04:02:51 +00:00
|
|
|
.created_at = DateTime.now(),
|
2022-09-07 23:14:52 +00:00
|
|
|
};
|
|
|
|
|
2022-09-29 21:52:01 +00:00
|
|
|
if ((try db.queryRow(std.meta.Tuple(&.{Uuid}), "SELECT id FROM community WHERE host = $1", .{host}, null)) != null) {
|
2022-09-07 23:14:52 +00:00
|
|
|
return error.CommunityExists;
|
|
|
|
}
|
|
|
|
|
2022-09-08 02:01:24 +00:00
|
|
|
try db.insert("community", community);
|
2022-09-07 23:14:52 +00:00
|
|
|
|
|
|
|
return community;
|
|
|
|
}
|
|
|
|
|
2022-09-08 05:10:58 +00:00
|
|
|
pub fn getByHost(db: anytype, host: []const u8, alloc: std.mem.Allocator) !Community {
|
2022-09-29 21:52:01 +00:00
|
|
|
return (try db.queryRow(
|
|
|
|
Community,
|
|
|
|
std.fmt.comptimePrint("SELECT {s} FROM community WHERE host = $1", .{comptime sql.fieldList(Community)}),
|
|
|
|
.{host},
|
|
|
|
alloc,
|
|
|
|
)) orelse return error.NotFound;
|
2022-09-08 05:10:58 +00:00
|
|
|
}
|
2022-09-08 06:56:29 +00:00
|
|
|
|
|
|
|
pub fn transferOwnership(db: anytype, community_id: Uuid, new_owner: Uuid) !void {
|
2022-09-15 01:12:07 +00:00
|
|
|
try db.exec("UPDATE community SET owner_id = $1 WHERE id = $2", .{ new_owner, community_id }, null);
|
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,
|
|
|
|
};
|
|
|
|
|
|
|
|
// 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,
|
|
|
|
direction: enum {
|
|
|
|
ascending,
|
|
|
|
descending,
|
|
|
|
} = .ascending,
|
|
|
|
|
|
|
|
// 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
|
|
|
|
page_direction: enum {
|
|
|
|
forward,
|
|
|
|
backward,
|
|
|
|
} = .forward,
|
|
|
|
};
|
|
|
|
|
|
|
|
const Builder = struct {
|
|
|
|
array: std.ArrayList(u8),
|
|
|
|
where_clauses_appended: usize = 0,
|
|
|
|
|
|
|
|
pub fn init(alloc: std.mem.Allocator) Builder {
|
|
|
|
return Builder{ .array = std.ArrayList(u8).init(alloc) };
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn deinit(self: *const Builder) void {
|
|
|
|
self.array.deinit();
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn andWhere(self: *Builder, clause: []const u8) !void {
|
|
|
|
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;
|
|
|
|
pub fn query(db: anytype, args: QueryArgs, alloc: std.mem.Allocator) ![]Community {
|
|
|
|
var builder = Builder.init(alloc);
|
|
|
|
defer builder.deinit();
|
|
|
|
|
|
|
|
try builder.array.appendSlice(
|
|
|
|
\\SELECT id, owner_id, host, name, scheme, created_at
|
|
|
|
\\FROM 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;
|
|
|
|
|
|
|
|
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)");
|
|
|
|
}
|
|
|
|
_ = try builder.array.appendSlice("\nORDER BY ");
|
|
|
|
_ = try builder.array.appendSlice(@tagName(args.order_by));
|
|
|
|
_ = try builder.array.appendSlice(", id ");
|
|
|
|
_ = try builder.array.appendSlice(switch (args.direction) {
|
|
|
|
.ascending => "ASC",
|
|
|
|
.descending => "DESC",
|
|
|
|
});
|
|
|
|
|
|
|
|
_ = 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-09-15 01:12:07 +00:00
|
|
|
var results = try db.query(
|
2022-09-10 04:02:51 +00:00
|
|
|
&.{ Uuid, ?Uuid, []const u8, []const u8, Scheme, DateTime },
|
|
|
|
builder.array.items,
|
|
|
|
query_args,
|
|
|
|
);
|
|
|
|
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| {
|
|
|
|
const row = results.row(alloc) orelse break;
|
|
|
|
c.* = .{
|
|
|
|
.id = row[0],
|
|
|
|
.owner_id = row[1],
|
|
|
|
.host = row[2],
|
|
|
|
.name = row[3],
|
|
|
|
.scheme = row[4],
|
|
|
|
.created_at = row[5],
|
|
|
|
};
|
|
|
|
|
|
|
|
count += 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (results.err) |err| return err;
|
|
|
|
|
|
|
|
return result_buf[0..count];
|
|
|
|
}
|
2022-09-29 21:52:01 +00:00
|
|
|
|
|
|
|
pub fn adminCommunityId(db: anytype) !Uuid {
|
|
|
|
const row = (try db.queryRow(
|
|
|
|
std.meta.Tuple(&.{Uuid}),
|
|
|
|
"SELECT id FROM community WHERE kind = 'admin' LIMIT 1",
|
|
|
|
.{},
|
|
|
|
null,
|
|
|
|
)) orelse return error.NotFound;
|
|
|
|
|
|
|
|
return row[0];
|
|
|
|
}
|