appview: add issue search endpoint #496

merged
opened by boltless.me targeting master from boltless.me/core: feat/search
Changed files
+121 -40
appview
db
issues
pages
templates
repo
issues
+84 -37
appview/db/issues.go
··· 3 3 import ( 4 4 "database/sql" 5 5 "fmt" 6 + "strconv" 6 7 "strings" 7 8 "time" 8 9 9 10 "github.com/bluesky-social/indigo/atproto/syntax" 10 11 "tangled.sh/tangled.sh/core/api/tangled" 12 + "tangled.sh/tangled.sh/core/appview/models" 11 13 "tangled.sh/tangled.sh/core/appview/pagination" 12 14 ) 13 15 ··· 162 164 return issues, nil 163 165 } 164 166 165 - func GetIssuesPaginated(e Execer, repoAt syntax.ATURI, isOpen bool, page pagination.Page) ([]Issue, error) { 166 - var issues []Issue 167 + // GetIssueIDs gets list of all existing issue's IDs 168 + func GetIssueIDs(e Execer, opts models.IssueSearchOptions) ([]int64, error) { 169 + var ids []int64 170 + 171 + var filters []filter 167 172 openValue := 0 168 - if isOpen { 173 + if opts.IsOpen { 169 174 openValue = 1 170 175 } 176 + filters = append(filters, FilterEq("open", openValue)) 177 + if opts.RepoAt != "" { 178 + filters = append(filters, FilterEq("repo_at", opts.RepoAt)) 179 + } 171 180 172 - rows, err := e.Query( 181 + var conditions []string 182 + var args []any 183 + 184 + for _, filter := range filters { 185 + conditions = append(conditions, filter.Condition()) 186 + args = append(args, filter.Arg()...) 187 + } 188 + 189 + whereClause := "" 190 + if conditions != nil { 191 + whereClause = " where " + strings.Join(conditions, " and ") 192 + } 193 + query := fmt.Sprintf( 173 194 ` 174 - with numbered_issue as ( 175 - select 176 - i.id, 177 - i.owner_did, 178 - i.rkey, 179 - i.issue_id, 180 - i.created, 181 - i.title, 182 - i.body, 183 - i.open, 184 - count(c.id) as comment_count, 185 - row_number() over (order by i.created desc) as row_num 186 - from 187 - issues i 188 - left join 189 - comments c on i.repo_at = c.repo_at and i.issue_id = c.issue_id 190 - where 191 - i.repo_at = ? and i.open = ? 192 - group by 193 - i.id, i.owner_did, i.issue_id, i.created, i.title, i.body, i.open 194 - ) 195 195 select 196 - id, 197 - owner_did, 198 - rkey, 199 - issue_id, 200 - created, 201 - title, 202 - body, 203 - open, 204 - comment_count 196 + id 205 197 from 206 - numbered_issue 198 + issues 199 + %s 200 + limit ? offset ?`, 201 + whereClause, 202 + ) 203 + args = append(args, opts.Page.Limit, opts.Page.Offset) 204 + rows, err := e.Query(query, args...) 205 + if err != nil { 206 + return nil, err 207 + } 208 + defer rows.Close() 209 + 210 + for rows.Next() { 211 + var id int64 212 + err := rows.Scan(&id) 213 + if err != nil { 214 + return nil, err 215 + } 216 + 217 + ids = append(ids, id) 218 + } 219 + 220 + return ids, nil 221 + } 222 + 223 + // GetIssuesByIDs gets list of issues from given IDs 224 + func GetIssuesByIDs(e Execer, ids []int64) ([]Issue, error) { 225 + var issues []Issue 226 + 227 + // HACK: would be better to create "?,?,?,..." or use something like sqlx 228 + idStrings := make([]string, len(ids)) 229 + for i, id := range ids { 230 + idStrings[i] = strconv.FormatInt(id, 10) 231 + } 232 + idList := strings.Join(idStrings, ",") 233 + query := fmt.Sprintf( 234 + ` 235 + select 236 + i.id, 237 + i.owner_did, 238 + i.rkey, 239 + i.issue_id, 240 + i.created, 241 + i.title, 242 + i.body, 243 + i.open, 244 + count(c.id) as comment_count 245 + from 246 + issues i 247 + left join 248 + comments c on i.repo_at = c.repo_at and i.issue_id = c.issue_id 207 249 where 208 - row_num between ? and ?`, 209 - repoAt, openValue, page.Offset+1, page.Offset+page.Limit) 250 + i.id in (%s) 251 + group by 252 + i.id 253 + order by i.created desc`, 254 + idList, 255 + ) 256 + rows, err := e.Query(query) 210 257 if err != nil { 211 258 return nil, err 212 259 }
+28 -1
appview/issues/issues.go
··· 19 19 "tangled.sh/tangled.sh/core/appview/config" 20 20 "tangled.sh/tangled.sh/core/appview/db" 21 21 issues_indexer "tangled.sh/tangled.sh/core/appview/indexer/issues" 22 + "tangled.sh/tangled.sh/core/appview/models" 22 23 "tangled.sh/tangled.sh/core/appview/notify" 23 24 "tangled.sh/tangled.sh/core/appview/oauth" 24 25 "tangled.sh/tangled.sh/core/appview/pages" ··· 608 609 return 609 610 } 610 611 611 - issues, err := db.GetIssuesPaginated(rp.db, f.RepoAt(), isOpen, page) 612 + keyword := params.Get("q") 613 + 614 + var ids []int64 615 + searchOpts := models.IssueSearchOptions{ 616 + Keyword: keyword, 617 + RepoAt: f.RepoAt().String(), 618 + IsOpen: isOpen, 619 + Page: page, 620 + } 621 + if keyword != "" { 622 + res, err := rp.indexer.Search(r.Context(), searchOpts) 623 + if err != nil { 624 + log.Println("failed to search for issues", err) 625 + return 626 + } 627 + log.Println("searched issues:", res.Hits) 628 + ids = res.Hits 629 + } else { 630 + ids, err = db.GetIssueIDs(rp.db, searchOpts) 631 + if err != nil { 632 + log.Println("failed to search for issues", err) 633 + return 634 + } 635 + } 636 + 637 + issues, err := db.GetIssuesByIDs(rp.db, ids) 612 638 if err != nil { 613 639 log.Println("failed to get issues", err) 614 640 rp.pages.Notice(w, "issues", "Failed to load issues. Try again later.") ··· 620 646 RepoInfo: f.RepoInfo(user), 621 647 Issues: issues, 622 648 FilteringByOpen: isOpen, 649 + FilterQuery: keyword, 623 650 Page: page, 624 651 }) 625 652 }
+9 -2
appview/pages/templates/repo/issues/issues.html
··· 24 24 {{ i "ban" "w-4 h-4" }} 25 25 <span>{{ .RepoInfo.Stats.IssueCount.Closed }} closed</span> 26 26 </a> 27 + <form class="flex gap-4" method="GET"> 28 + <input type="hidden" name="state" value="{{ if .FilteringByOpen }}open{{ else }}closed{{ end }}"> 29 + <input class="" type="text" name="q" value="{{ .FilterQuery }}"> 30 + <button class="btn" type="submit"> 31 + search 32 + </button> 33 + </form> 27 34 </div> 28 35 <a 29 36 href="/{{ .RepoInfo.FullName }}/issues/new" ··· 100 107 <a 101 108 class="btn flex items-center gap-2 no-underline hover:no-underline dark:text-white dark:hover:bg-gray-700" 102 109 hx-boost="true" 103 - href = "/{{ $.RepoInfo.FullName }}/issues?state={{ $currentState }}&offset={{ $prev.Offset }}&limit={{ $prev.Limit }}" 110 + href = "/{{ $.RepoInfo.FullName }}/issues?state={{ $currentState }}&q={{ .FilterQuery }}&offset={{ $prev.Offset }}&limit={{ $prev.Limit }}" 104 111 > 105 112 {{ i "chevron-left" "w-4 h-4" }} 106 113 previous ··· 114 121 <a 115 122 class="btn flex items-center gap-2 no-underline hover:no-underline dark:text-white dark:hover:bg-gray-700" 116 123 hx-boost="true" 117 - href = "/{{ $.RepoInfo.FullName }}/issues?state={{ $currentState }}&offset={{ $next.Offset }}&limit={{ $next.Limit }}" 124 + href = "/{{ $.RepoInfo.FullName }}/issues?state={{ $currentState }}&q={{ .FilterQuery }}&offset={{ $next.Offset }}&limit={{ $next.Limit }}" 118 125 > 119 126 next 120 127 {{ i "chevron-right" "w-4 h-4" }}