2022-09-07 23:14:52 +00:00
const std = @import ( " std " ) ;
const builtin = @import ( " builtin " ) ;
const util = @import ( " util " ) ;
const models = @import ( " ../db/models.zig " ) ;
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-08 06:56:29 +00:00
owner_id : ? Uuid ,
2022-09-07 23:14:52 +00:00
host : [ ] const u8 ,
name : [ ] const u8 ,
scheme : Scheme ,
2022-09-10 04:02:51 +00:00
created_at : DateTime ,
2022-09-07 23:14:52 +00:00
} ;
2022-09-10 04:02:51 +00:00
fn freeCommunity ( alloc : std . mem . Allocator , c : Community ) void {
alloc . free ( c . host ) ;
alloc . free ( c . name ) ;
}
2022-09-07 23:14:52 +00:00
pub fn create ( db : anytype , origin : [ ] const u8 , name : ? [ ] const u8 ) CreateError ! Community {
const scheme_len = firstIndexOf ( 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 ( firstIndexOf ( host , ':' ) ! = null and builtin . mode ! = . Debug ) return error . InvalidOrigin ;
// community cannot be hosted on a path
if ( firstIndexOf ( host , '/' ) ! = null ) return error . InvalidOrigin ;
// 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-08 06:56:29 +00:00
. owner_id = null ,
2022-09-07 23:14:52 +00:00
. host = host ,
. name = name orelse host ,
. scheme = scheme ,
2022-09-10 04:02:51 +00:00
. created_at = DateTime . now ( ) ,
2022-09-07 23:14:52 +00:00
} ;
2022-09-15 01:12:07 +00:00
if ( ( try db . queryRow ( & . { 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
fn firstIndexOf ( str : [ ] const u8 , ch : u8 ) ? usize {
2022-09-07 23:14:52 +00:00
for ( str ) | c , i | {
if ( c = = ch ) return i ;
}
return null ;
}
2022-09-08 05:10:58 +00:00
pub fn getByHost ( db : anytype , host : [ ] const u8 , alloc : std . mem . Allocator ) ! Community {
2022-09-15 01:12:07 +00:00
const result = ( try db . queryRow ( & . { Uuid , ? Uuid , [ ] const u8 , [ ] const u8 , Scheme , DateTime } , " SELECT id, owner_id, host, name, scheme, created_at FROM community WHERE host = $1 " , . { host } , alloc ) ) orelse return error . NotFound ;
2022-09-08 05:10:58 +00:00
return Community {
. id = result [ 0 ] ,
2022-09-08 06:56:29 +00:00
. owner_id = result [ 1 ] ,
. host = result [ 2 ] ,
. name = result [ 3 ] ,
. scheme = result [ 4 ] ,
2022-09-10 04:02:51 +00:00
. created_at = result [ 5 ] ,
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 ( " \n ORDER 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 ( " \n LIMIT $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 ;
errdefer for ( result_buf [ 0 . . count ] ) | c | freeCommunity ( alloc , c ) ;
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 ] ;
}