forked from tangled.org/core
this repo has no description
at master 8.2 kB view raw
1package db 2 3import ( 4 "database/sql" 5 "errors" 6 "fmt" 7 "log" 8 "slices" 9 "strings" 10 "time" 11 12 "github.com/bluesky-social/indigo/atproto/syntax" 13 "tangled.org/core/appview/models" 14) 15 16func AddStar(e Execer, star *models.Star) error { 17 query := `insert or ignore into stars (starred_by_did, repo_at, rkey) values (?, ?, ?)` 18 _, err := e.Exec( 19 query, 20 star.StarredByDid, 21 star.RepoAt.String(), 22 star.Rkey, 23 ) 24 return err 25} 26 27// Get a star record 28func GetStar(e Execer, starredByDid string, repoAt syntax.ATURI) (*models.Star, error) { 29 query := ` 30 select starred_by_did, repo_at, created, rkey 31 from stars 32 where starred_by_did = ? and repo_at = ?` 33 row := e.QueryRow(query, starredByDid, repoAt) 34 35 var star models.Star 36 var created string 37 err := row.Scan(&star.StarredByDid, &star.RepoAt, &created, &star.Rkey) 38 if err != nil { 39 return nil, err 40 } 41 42 createdAtTime, err := time.Parse(time.RFC3339, created) 43 if err != nil { 44 log.Println("unable to determine followed at time") 45 star.Created = time.Now() 46 } else { 47 star.Created = createdAtTime 48 } 49 50 return &star, nil 51} 52 53// Remove a star 54func DeleteStar(e Execer, starredByDid string, repoAt syntax.ATURI) error { 55 _, err := e.Exec(`delete from stars where starred_by_did = ? and repo_at = ?`, starredByDid, repoAt) 56 return err 57} 58 59// Remove a star 60func DeleteStarByRkey(e Execer, starredByDid string, rkey string) error { 61 _, err := e.Exec(`delete from stars where starred_by_did = ? and rkey = ?`, starredByDid, rkey) 62 return err 63} 64 65func GetStarCount(e Execer, repoAt syntax.ATURI) (int, error) { 66 stars := 0 67 err := e.QueryRow( 68 `select count(starred_by_did) from stars where repo_at = ?`, repoAt).Scan(&stars) 69 if err != nil { 70 return 0, err 71 } 72 return stars, nil 73} 74 75// getStarStatuses returns a map of repo URIs to star status for a given user 76// This is an internal helper function to avoid N+1 queries 77func getStarStatuses(e Execer, userDid string, repoAts []syntax.ATURI) (map[string]bool, error) { 78 if len(repoAts) == 0 || userDid == "" { 79 return make(map[string]bool), nil 80 } 81 82 placeholders := make([]string, len(repoAts)) 83 args := make([]any, len(repoAts)+1) 84 args[0] = userDid 85 86 for i, repoAt := range repoAts { 87 placeholders[i] = "?" 88 args[i+1] = repoAt.String() 89 } 90 91 query := fmt.Sprintf(` 92 SELECT repo_at 93 FROM stars 94 WHERE starred_by_did = ? AND repo_at IN (%s) 95 `, strings.Join(placeholders, ",")) 96 97 rows, err := e.Query(query, args...) 98 if err != nil { 99 return nil, err 100 } 101 defer rows.Close() 102 103 result := make(map[string]bool) 104 // Initialize all repos as not starred 105 for _, repoAt := range repoAts { 106 result[repoAt.String()] = false 107 } 108 109 // Mark starred repos as true 110 for rows.Next() { 111 var repoAt string 112 if err := rows.Scan(&repoAt); err != nil { 113 return nil, err 114 } 115 result[repoAt] = true 116 } 117 118 return result, nil 119} 120 121func GetStarStatus(e Execer, userDid string, repoAt syntax.ATURI) bool { 122 statuses, err := getStarStatuses(e, userDid, []syntax.ATURI{repoAt}) 123 if err != nil { 124 return false 125 } 126 return statuses[repoAt.String()] 127} 128 129// GetStarStatuses returns a map of repo URIs to star status for a given user 130func GetStarStatuses(e Execer, userDid string, repoAts []syntax.ATURI) (map[string]bool, error) { 131 return getStarStatuses(e, userDid, repoAts) 132} 133func GetStars(e Execer, limit int, filters ...filter) ([]models.Star, error) { 134 var conditions []string 135 var args []any 136 for _, filter := range filters { 137 conditions = append(conditions, filter.Condition()) 138 args = append(args, filter.Arg()...) 139 } 140 141 whereClause := "" 142 if conditions != nil { 143 whereClause = " where " + strings.Join(conditions, " and ") 144 } 145 146 limitClause := "" 147 if limit != 0 { 148 limitClause = fmt.Sprintf(" limit %d", limit) 149 } 150 151 repoQuery := fmt.Sprintf( 152 `select starred_by_did, repo_at, created, rkey 153 from stars 154 %s 155 order by created desc 156 %s`, 157 whereClause, 158 limitClause, 159 ) 160 rows, err := e.Query(repoQuery, args...) 161 if err != nil { 162 return nil, err 163 } 164 165 starMap := make(map[string][]models.Star) 166 for rows.Next() { 167 var star models.Star 168 var created string 169 err := rows.Scan(&star.StarredByDid, &star.RepoAt, &created, &star.Rkey) 170 if err != nil { 171 return nil, err 172 } 173 174 star.Created = time.Now() 175 if t, err := time.Parse(time.RFC3339, created); err == nil { 176 star.Created = t 177 } 178 179 repoAt := string(star.RepoAt) 180 starMap[repoAt] = append(starMap[repoAt], star) 181 } 182 183 // populate *Repo in each star 184 args = make([]any, len(starMap)) 185 i := 0 186 for r := range starMap { 187 args[i] = r 188 i++ 189 } 190 191 if len(args) == 0 { 192 return nil, nil 193 } 194 195 repos, err := GetRepos(e, 0, FilterIn("at_uri", args)) 196 if err != nil { 197 return nil, err 198 } 199 200 for _, r := range repos { 201 if stars, ok := starMap[string(r.RepoAt())]; ok { 202 for i := range stars { 203 stars[i].Repo = &r 204 } 205 } 206 } 207 208 var stars []models.Star 209 for _, s := range starMap { 210 stars = append(stars, s...) 211 } 212 213 slices.SortFunc(stars, func(a, b models.Star) int { 214 if a.Created.After(b.Created) { 215 return -1 216 } 217 if b.Created.After(a.Created) { 218 return 1 219 } 220 return 0 221 }) 222 223 return stars, nil 224} 225 226func CountStars(e Execer, filters ...filter) (int64, error) { 227 var conditions []string 228 var args []any 229 for _, filter := range filters { 230 conditions = append(conditions, filter.Condition()) 231 args = append(args, filter.Arg()...) 232 } 233 234 whereClause := "" 235 if conditions != nil { 236 whereClause = " where " + strings.Join(conditions, " and ") 237 } 238 239 repoQuery := fmt.Sprintf(`select count(1) from stars %s`, whereClause) 240 var count int64 241 err := e.QueryRow(repoQuery, args...).Scan(&count) 242 243 if !errors.Is(err, sql.ErrNoRows) && err != nil { 244 return 0, err 245 } 246 247 return count, nil 248} 249 250func GetAllStars(e Execer, limit int) ([]models.Star, error) { 251 var stars []models.Star 252 253 rows, err := e.Query(` 254 select 255 s.starred_by_did, 256 s.repo_at, 257 s.rkey, 258 s.created, 259 r.did, 260 r.name, 261 r.knot, 262 r.rkey, 263 r.created 264 from stars s 265 join repos r on s.repo_at = r.at_uri 266 `) 267 268 if err != nil { 269 return nil, err 270 } 271 defer rows.Close() 272 273 for rows.Next() { 274 var star models.Star 275 var repo models.Repo 276 var starCreatedAt, repoCreatedAt string 277 278 if err := rows.Scan( 279 &star.StarredByDid, 280 &star.RepoAt, 281 &star.Rkey, 282 &starCreatedAt, 283 &repo.Did, 284 &repo.Name, 285 &repo.Knot, 286 &repo.Rkey, 287 &repoCreatedAt, 288 ); err != nil { 289 return nil, err 290 } 291 292 star.Created, err = time.Parse(time.RFC3339, starCreatedAt) 293 if err != nil { 294 star.Created = time.Now() 295 } 296 repo.Created, err = time.Parse(time.RFC3339, repoCreatedAt) 297 if err != nil { 298 repo.Created = time.Now() 299 } 300 star.Repo = &repo 301 302 stars = append(stars, star) 303 } 304 305 if err := rows.Err(); err != nil { 306 return nil, err 307 } 308 309 return stars, nil 310} 311 312// GetTopStarredReposLastWeek returns the top 8 most starred repositories from the last week 313func GetTopStarredReposLastWeek(e Execer) ([]models.Repo, error) { 314 // first, get the top repo URIs by star count from the last week 315 query := ` 316 with recent_starred_repos as ( 317 select distinct repo_at 318 from stars 319 where created >= datetime('now', '-7 days') 320 ), 321 repo_star_counts as ( 322 select 323 s.repo_at, 324 count(*) as stars_gained_last_week 325 from stars s 326 join recent_starred_repos rsr on s.repo_at = rsr.repo_at 327 where s.created >= datetime('now', '-7 days') 328 group by s.repo_at 329 ) 330 select rsc.repo_at 331 from repo_star_counts rsc 332 order by rsc.stars_gained_last_week desc 333 limit 8 334 ` 335 336 rows, err := e.Query(query) 337 if err != nil { 338 return nil, err 339 } 340 defer rows.Close() 341 342 var repoUris []string 343 for rows.Next() { 344 var repoUri string 345 err := rows.Scan(&repoUri) 346 if err != nil { 347 return nil, err 348 } 349 repoUris = append(repoUris, repoUri) 350 } 351 352 if err := rows.Err(); err != nil { 353 return nil, err 354 } 355 356 if len(repoUris) == 0 { 357 return []models.Repo{}, nil 358 } 359 360 // get full repo data 361 repos, err := GetRepos(e, 0, FilterIn("at_uri", repoUris)) 362 if err != nil { 363 return nil, err 364 } 365 366 // sort repos by the original trending order 367 repoMap := make(map[string]models.Repo) 368 for _, repo := range repos { 369 repoMap[repo.RepoAt().String()] = repo 370 } 371 372 orderedRepos := make([]models.Repo, 0, len(repoUris)) 373 for _, uri := range repoUris { 374 if repo, exists := repoMap[uri]; exists { 375 orderedRepos = append(orderedRepos, repo) 376 } 377 } 378 379 return orderedRepos, nil 380}