appview/pulls: add search endpoint #709

merged
opened by boltless.me targeting master from boltless.me/core: feat/search
Changed files
+105 -2
appview
db
pages
templates
repo
pulls
pulls
state
+61
appview/db/pulls.go
···
return GetPullsWithLimit(e, 0, filters...)
}
+
func GetPullIDs(e Execer, opts models.PullSearchOptions) ([]int64, error) {
+
var ids []int64
+
+
var filters []filter
+
filters = append(filters, FilterEq("state", opts.State))
+
if opts.RepoAt != "" {
+
filters = append(filters, FilterEq("repo_at", opts.RepoAt))
+
}
+
+
var conditions []string
+
var args []any
+
+
for _, filter := range filters {
+
conditions = append(conditions, filter.Condition())
+
args = append(args, filter.Arg()...)
+
}
+
+
whereClause := ""
+
if conditions != nil {
+
whereClause = " where " + strings.Join(conditions, " and ")
+
}
+
pageClause := ""
+
if opts.Page.Limit != 0 {
+
pageClause = fmt.Sprintf(
+
" limit %d offset %d ",
+
opts.Page.Limit,
+
opts.Page.Offset,
+
)
+
}
+
+
query := fmt.Sprintf(
+
`
+
select
+
id
+
from
+
pulls
+
%s
+
%s`,
+
whereClause,
+
pageClause,
+
)
+
args = append(args, opts.Page.Limit, opts.Page.Offset)
+
rows, err := e.Query(query, args...)
+
if err != nil {
+
return nil, err
+
}
+
defer rows.Close()
+
+
for rows.Next() {
+
var id int64
+
err := rows.Scan(&id)
+
if err != nil {
+
return nil, err
+
}
+
+
ids = append(ids, id)
+
}
+
+
return ids, nil
+
}
+
func GetPull(e Execer, repoAt syntax.ATURI, pullId int) (*models.Pull, error) {
pulls, err := GetPullsWithLimit(e, 1, FilterEq("repo_at", repoAt), FilterEq("pull_id", pullId))
if err != nil {
+1
appview/pages/pages.go
···
Pulls []*models.Pull
Active string
FilteringBy models.PullState
+
FilterQuery string
Stacks map[string]models.Stack
Pipelines map[string]models.Pipeline
LabelDefs map[string]*models.LabelDefinition
+7
appview/pages/templates/repo/pulls/pulls.html
···
{{ i "ban" "w-4 h-4" }}
<span>{{ .RepoInfo.Stats.PullCount.Closed }} closed</span>
</a>
+
<form class="flex gap-4" method="GET">
+
<input type="hidden" name="state" value="{{ .FilteringBy.String }}">
+
<input class="" type="text" name="q" value="{{ .FilterQuery }}">
+
<button class="btn" type="submit">
+
search
+
</button>
+
</form>
</div>
<a
href="/{{ .RepoInfo.FullName }}/pulls/new"
+35 -2
appview/pulls/pulls.go
···
"tangled.org/core/api/tangled"
"tangled.org/core/appview/config"
"tangled.org/core/appview/db"
+
pulls_indexer "tangled.org/core/appview/indexer/pulls"
"tangled.org/core/appview/models"
"tangled.org/core/appview/notify"
"tangled.org/core/appview/oauth"
···
enforcer *rbac.Enforcer
logger *slog.Logger
validator *validator.Validator
+
indexer *pulls_indexer.Indexer
}
func New(
···
notifier notify.Notifier,
enforcer *rbac.Enforcer,
validator *validator.Validator,
+
indexer *pulls_indexer.Indexer,
logger *slog.Logger,
) *Pulls {
return &Pulls{
···
enforcer: enforcer,
logger: logger,
validator: validator,
+
indexer: indexer,
}
}
···
}
func (s *Pulls) RepoPulls(w http.ResponseWriter, r *http.Request) {
+
l := s.logger.With("handler", "RepoPulls")
+
user := s.oauth.GetUser(r)
params := r.URL.Query()
···
return
}
+
keyword := params.Get("q")
+
+
var ids []int64
+
searchOpts := models.PullSearchOptions{
+
Keyword: keyword,
+
RepoAt: f.RepoAt().String(),
+
State: state,
+
// Page: page,
+
}
+
l.Debug("searching with", "searchOpts", searchOpts)
+
if keyword != "" {
+
res, err := s.indexer.Search(r.Context(), searchOpts)
+
if err != nil {
+
l.Error("failed to search for pulls", "err", err)
+
return
+
}
+
l.Debug("searched pulls with indexer", "res.Hits", res.Hits)
+
ids = res.Hits
+
} else {
+
ids, err = db.GetPullIDs(s.db, searchOpts)
+
if err != nil {
+
l.Error("failed to get all pull ids", "err", err)
+
return
+
}
+
l.Debug("indexed all pulls from the db", "ids", ids)
+
}
+
pulls, err := db.GetPulls(
s.db,
-
db.FilterEq("repo_at", f.RepoAt()),
-
db.FilterEq("state", state),
+
db.FilterIn("id", ids),
)
if err != nil {
log.Println("failed to get pulls", err)
···
Pulls: pulls,
LabelDefs: defs,
FilteringBy: state,
+
FilterQuery: keyword,
Stacks: stacks,
Pipelines: m,
})
+1
appview/state/router.go
···
s.notifier,
s.enforcer,
s.validator,
+
s.indexer.Pulls,
log.SubLogger(s.logger, "pulls"),
)
return pulls.Router(mw)