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