olivetin/service/internal/api/api_search_hints.go

169 lines
4.9 KiB
Go

package api
import (
"sort"
apiv1 "github.com/OliveTin/OliveTin/gen/olivetin/api/v1"
acl "github.com/OliveTin/OliveTin/internal/acl"
authpublic "github.com/OliveTin/OliveTin/internal/auth/authpublic"
"github.com/OliveTin/OliveTin/internal/entities"
executor "github.com/OliveTin/OliveTin/internal/executor"
"github.com/OliveTin/OliveTin/internal/tpl"
)
const (
maxSearchHintActions = 100
maxSearchHintEntitiesPerType = 50
)
// buildSearchHints returns lightweight search index data for Init.
// Dashboards are not included; clients index Init.root_dashboard_entries.
// Visibility matches action/entity view ACL; omitted entirely when login is required.
func (api *oliveTinAPI) buildSearchHints(user *authpublic.AuthenticatedUser) *apiv1.SearchHints {
return &apiv1.SearchHints{
Entities: api.buildEntitySearchHints(user),
Actions: api.buildActionSearchHints(user),
}
}
func (api *oliveTinAPI) buildEntitySearchHints(user *authpublic.AuthenticatedUser) []*apiv1.EntitySearchHint {
hints := entities.ListSearchHints()
out := make([]*apiv1.EntitySearchHint, 0, len(hints))
for _, hint := range hints {
if allowedHint := api.entitySearchHintIfAllowed(user, hint); allowedHint != nil {
out = append(out, allowedHint)
}
}
sortEntitySearchHints(out)
return capEntitySearchHintsPerType(out, maxSearchHintEntitiesPerType)
}
func (api *oliveTinAPI) entitySearchHintIfAllowed(user *authpublic.AuthenticatedUser, hint entities.SearchHint) *apiv1.EntitySearchHint {
if hint.UniqueKey == "" || hint.Type == "" {
return nil
}
if !api.userCanViewEntityType(user, hint.Type) {
return nil
}
return &apiv1.EntitySearchHint{
Title: hint.Title,
Type: hint.Type,
UniqueKey: hint.UniqueKey,
}
}
func sortEntitySearchHints(hints []*apiv1.EntitySearchHint) {
sort.SliceStable(hints, func(leftIndex, rightIndex int) bool {
if hints[leftIndex].Type != hints[rightIndex].Type {
return hints[leftIndex].Type < hints[rightIndex].Type
}
if hints[leftIndex].UniqueKey != hints[rightIndex].UniqueKey {
return hints[leftIndex].UniqueKey < hints[rightIndex].UniqueKey
}
return hints[leftIndex].Title < hints[rightIndex].Title
})
}
func capEntitySearchHintsPerType(hints []*apiv1.EntitySearchHint, perType int) []*apiv1.EntitySearchHint {
if perType < 1 || len(hints) == 0 {
return hints
}
counts := make(map[string]int)
out := make([]*apiv1.EntitySearchHint, 0, len(hints))
for _, hint := range hints {
if counts[hint.Type] >= perType {
continue
}
counts[hint.Type]++
out = append(out, hint)
}
return out
}
func (api *oliveTinAPI) buildActionSearchHints(user *authpublic.AuthenticatedUser) []*apiv1.ActionSearchHint {
candidates := api.collectViewableActionBindings(user)
sortActionSearchCandidates(candidates)
if len(candidates) > maxSearchHintActions {
candidates = candidates[:maxSearchHintActions]
}
out := make([]*apiv1.ActionSearchHint, 0, len(candidates))
for _, candidate := range candidates {
out = append(out, &apiv1.ActionSearchHint{
Title: candidate.title,
BindingId: candidate.bindingID,
})
}
return out
}
type actionSearchCandidate struct {
title string
bindingID string
hasEntity bool
}
func (api *oliveTinAPI) collectViewableActionBindings(user *authpublic.AuthenticatedUser) []actionSearchCandidate {
api.executor.MapActionBindingsLock.RLock()
defer api.executor.MapActionBindingsLock.RUnlock()
candidates := make([]actionSearchCandidate, 0)
for _, binding := range api.executor.MapActionBindings {
if candidate, ok := actionSearchCandidateFromBinding(api, user, binding); ok {
candidates = append(candidates, candidate)
}
}
return candidates
}
func actionSearchCandidateFromBinding(api *oliveTinAPI, user *authpublic.AuthenticatedUser, binding *executor.ActionBinding) (actionSearchCandidate, bool) {
if !isSearchableActionBinding(binding) {
return actionSearchCandidate{}, false
}
if !acl.IsAllowedView(api.cfg, user, binding.Action) {
return actionSearchCandidate{}, false
}
if !api.bindingEntityTypeAllowed(user, binding) {
return actionSearchCandidate{}, false
}
return actionSearchCandidate{
title: tpl.ParseTemplateOfActionBeforeExec(binding.Action.Title, binding.Entity),
bindingID: binding.ID,
hasEntity: binding.Entity != nil,
}, true
}
func isSearchableActionBinding(binding *executor.ActionBinding) bool {
return binding != nil && binding.Action != nil && binding.ID != "" && !binding.Action.Hidden
}
func sortActionSearchCandidates(candidates []actionSearchCandidate) {
sort.SliceStable(candidates, func(leftIndex, rightIndex int) bool {
if candidates[leftIndex].hasEntity != candidates[rightIndex].hasEntity {
return !candidates[leftIndex].hasEntity
}
if candidates[leftIndex].title != candidates[rightIndex].title {
return candidates[leftIndex].title < candidates[rightIndex].title
}
return candidates[leftIndex].bindingID < candidates[rightIndex].bindingID
})
}