a mini social media app for small communities

fix user searching, future-proof User/Post.from_row, and add user/post counts to about

Changed files
+84 -27
src
database
entity
templates
util
+14 -3
src/database/post.v
··· 164 164 // todo: levenshtein distance, query options/filters (user:beep, !excluded-text, 165 165 // etc) 166 166 pub fn (app &DatabaseAccess) search_for_posts(query string, limit int, offset int) []PostSearchResult { 167 - queried_posts := app.db.exec_param_many('SELECT * FROM search_for_posts($1, $2, $3)', [query, limit.str(), offset.str()]) or { 167 + queried_posts := app.db.exec_param_many_result('SELECT * FROM search_for_posts($1, $2, $3)', [query, limit.str(), offset.str()]) or { 168 168 eprintln('search_for_posts error in app.db.error: ${err}') 169 + pg.Result{} 170 + } 171 + posts := queried_posts.rows.map(fn [queried_posts] (it pg.Row) Post { 172 + return Post.from_row(queried_posts, it) 173 + }) 174 + return PostSearchResult.from_post_list(app, posts) 175 + } 176 + 177 + // get_post_count gets the number of posts in the database. 178 + pub fn (app &DatabaseAccess) get_post_count() int { 179 + n := app.db.exec('SELECT COUNT(id) FROM "Post"') or { 180 + eprintln('get_post_count error in app.db.error: ${err}') 169 181 [] 170 182 } 171 - posts := queried_posts.map(|it| Post.from_row(it)) 172 - return PostSearchResult.from_post_list(app, posts) 183 + return if n.len == 0 { 0 } else { util.or_throw(n[0].vals[0]).int() } 173 184 }
+15 -3
src/database/user.v
··· 2 2 3 3 import entity { User, Notification, Like, LikeCache, Post } 4 4 import util 5 + import db.pg 5 6 6 7 // new_user creates a new user and returns their struct after creation. 7 8 pub fn (app &DatabaseAccess) new_user(user User) ?User { ··· 216 217 // search_for_users searches for posts matching the given query. 217 218 // todo: query options/filters, such as created-after:<date>, created-before:<date>, etc 218 219 pub fn (app &DatabaseAccess) search_for_users(query string, limit int, offset int) []User { 219 - queried_users := app.db.exec_param_many('SELECT * FROM search_for_users($1, $2, $3)', [query, limit.str(), offset.str()]) or { 220 + queried_users := app.db.exec_param_many_result('SELECT * FROM search_for_users($1, $2, $3)', [query, limit.str(), offset.str()]) or { 220 221 eprintln('search_for_users error in app.db.error: ${err}') 222 + pg.Result{} 223 + } 224 + users := queried_users.rows.map(fn [queried_users] (it pg.Row) User { 225 + return User.from_row(queried_users, it) 226 + }) 227 + return users 228 + } 229 + 230 + // get_user_count gets the number of registered users in the database. 231 + pub fn (app &DatabaseAccess) get_user_count() int { 232 + n := app.db.exec('SELECT COUNT(id) FROM "User"') or { 233 + eprintln('get_user_count error in app.db.error: ${err}') 221 234 [] 222 235 } 223 - users := queried_users.map(|it| User.from_row(it)) 224 - return users 236 + return if n.len == 0 { 0 } else { util.or_throw(n[0].vals[0]).int() } 225 237 }
+17 -9
src/entity/post.v
··· 21 21 // Post.from_row creates a post object from the given database row. 22 22 // see src/database/post.v#search_for_posts for usage. 23 23 @[inline] 24 - pub fn Post.from_row(row pg.Row) Post { 24 + pub fn Post.from_row(res pg.Result, row pg.Row) Post { 25 + // curry some arguments for cleanliness 26 + c := fn [res, row] (key string) ?string { 27 + return util.get_row_col(res, row, key) 28 + } 29 + ct := fn [res, row] (key string) string { 30 + return util.get_row_col_or_throw(res, row, key) 31 + } 32 + 25 33 // this throws a cgen error when put in Post{} 26 34 //todo: report this 27 - posted_at := time.parse(util.or_throw[string](row.vals[6])) or { panic(err) } 35 + posted_at := time.parse(ct('posted_at')) or { panic(err) } 28 36 29 37 return Post{ 30 - id: util.or_throw[string](row.vals[0]).int() 31 - author_id: util.or_throw[string](row.vals[1]).int() 32 - replying_to: if row.vals[2] == none { ?int(none) } else { 33 - util.map_or_throw[string, int](row.vals[2], |it| it.int()) 38 + id: ct('id').int() 39 + author_id: ct('author_id').int() 40 + replying_to: if c('replying_to') == none { none } else { 41 + util.map_or_throw[string, int](ct('replying_to'), |it| it.int()) 34 42 } 35 - title: util.or_throw[string](row.vals[3]) 36 - body: util.or_throw[string](row.vals[4]) 37 - pinned: util.map_or_throw[string, bool](row.vals[5], |it| it.bool()) 43 + title: ct('title') 44 + body: ct('body') 45 + pinned: util.map_or_throw[string, bool](ct('pinned'), |it| it.bool()) 38 46 posted_at: posted_at 39 47 } 40 48 }
+19 -11
src/entity/user.v
··· 44 44 // User.from_row creates a user object from the given database row. 45 45 // see src/database/user.v#search_for_users for usage. 46 46 @[inline] 47 - pub fn User.from_row(row pg.Row) User { 47 + pub fn User.from_row(res pg.Result, row pg.Row) User { 48 + // curry some arguments for cleanliness 49 + c := fn [res, row] (key string) ?string { 50 + return util.get_row_col(res, row, key) 51 + } 52 + ct := fn [res, row] (key string) string { 53 + return util.get_row_col_or_throw(res, row, key) 54 + } 55 + 48 56 // this throws a cgen error when put in User{} 49 57 //todo: report this 50 - created_at := time.parse(util.or_throw[string](row.vals[10])) or { panic(err) } 58 + created_at := time.parse(ct('created_at')) or { panic(err) } 51 59 52 60 return User{ 53 - id: util.or_throw[string](row.vals[0]).int() 54 - username: util.or_throw[string](row.vals[1]) 55 - nickname: if row.vals[2] == none { ?string(none) } else { 56 - util.or_throw[string](row.vals[2]) 61 + id: ct('id').int() 62 + username: ct('username') 63 + nickname: if c('nickname') == none { none } else { 64 + ct('nickname') 57 65 } 58 66 password: 'haha lol, nope' 59 67 password_salt: 'haha lol, nope' 60 - muted: util.map_or_throw[string, bool](row.vals[5], |it| it.bool()) 61 - admin: util.map_or_throw[string, bool](row.vals[6], |it| it.bool()) 62 - theme: util.or_throw[string](row.vals[7]) 63 - bio: util.or_throw[string](row.vals[8]) 64 - pronouns: util.or_throw[string](row.vals[9]) 68 + muted: util.map_or_throw[string, bool](util.get_row_col(res, row, 'muted'), |it| it.bool()) 69 + admin: util.map_or_throw[string, bool](util.get_row_col(res, row, 'admin'), |it| it.bool()) 70 + theme: ct('theme') 71 + bio: ct('bio') 72 + pronouns: ct('pronouns') 65 73 created_at: created_at 66 74 } 67 75 }
+6 -1
src/templates/about.html
··· 11 11 @if app.config.instance.source != '' 12 12 <p>source: <a href="@{app.config.instance.source}">@{app.config.instance.source}</a></p> 13 13 @end 14 + 15 + <br> 16 + 17 + <p>users: @{app.get_user_count()}</p> 18 + <p>posts: @{app.get_post_count()}</p> 14 19 </div> 15 20 16 21 <script> 17 22 document.getElementById('built_at').innerText = new Date(@{app.built_at} * 1000).toLocaleString() 18 23 </script> 19 24 20 - @include 'partial/footer.html' 25 + @include 'partial/footer.html'
+13
src/util/row.v
··· 1 + module util 2 + 3 + import db.pg 4 + 5 + @[inline] 6 + pub fn get_row_col(res pg.Result, row pg.Row, key string) ?string { 7 + return row.vals[res.cols[key]] 8 + } 9 + 10 + @[inline] 11 + pub fn get_row_col_or_throw(res pg.Result, row pg.Row, key string) string { 12 + return util.or_throw(row.vals[res.cols[key]]) 13 + }