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