api_search_hints.go 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170
  1. package api
  2. import (
  3. "sort"
  4. apiv1 "github.com/OliveTin/OliveTin/gen/olivetin/api/v1"
  5. acl "github.com/OliveTin/OliveTin/internal/acl"
  6. authpublic "github.com/OliveTin/OliveTin/internal/auth/authpublic"
  7. "github.com/OliveTin/OliveTin/internal/entities"
  8. executor "github.com/OliveTin/OliveTin/internal/executor"
  9. "github.com/OliveTin/OliveTin/internal/tpl"
  10. )
  11. const (
  12. maxSearchHintActions = 100
  13. maxSearchHintEntitiesPerType = 50
  14. )
  15. // buildSearchHints returns lightweight search index data for Init.
  16. // Dashboards are not included; clients index Init.root_dashboard_entries.
  17. // Visibility matches action/entity view ACL; omitted entirely when login is required.
  18. func (api *oliveTinAPI) buildSearchHints(user *authpublic.AuthenticatedUser) *apiv1.SearchHints {
  19. return &apiv1.SearchHints{
  20. Entities: api.buildEntitySearchHints(user),
  21. Actions: api.buildActionSearchHints(user),
  22. }
  23. }
  24. func (api *oliveTinAPI) buildEntitySearchHints(user *authpublic.AuthenticatedUser) []*apiv1.EntitySearchHint {
  25. hintsByType := make(map[string][]*apiv1.EntitySearchHint)
  26. for _, hint := range entities.ListSearchHints() {
  27. if allowedHint := api.entitySearchHintIfAllowed(user, hint); allowedHint != nil {
  28. hintsByType[allowedHint.Type] = appendBoundedEntitySearchHints(
  29. hintsByType[allowedHint.Type],
  30. allowedHint,
  31. maxSearchHintEntitiesPerType,
  32. )
  33. }
  34. }
  35. entityTypes := make([]string, 0, len(hintsByType))
  36. for entityType := range hintsByType {
  37. entityTypes = append(entityTypes, entityType)
  38. }
  39. sort.Strings(entityTypes)
  40. out := make([]*apiv1.EntitySearchHint, 0, len(entityTypes)*maxSearchHintEntitiesPerType)
  41. for _, entityType := range entityTypes {
  42. out = append(out, hintsByType[entityType]...)
  43. }
  44. return out
  45. }
  46. func appendBoundedEntitySearchHints(hints []*apiv1.EntitySearchHint, hint *apiv1.EntitySearchHint, limit int) []*apiv1.EntitySearchHint {
  47. hints = append(hints, hint)
  48. sortEntitySearchHints(hints)
  49. if len(hints) > limit {
  50. hints = hints[:limit]
  51. }
  52. return hints
  53. }
  54. func (api *oliveTinAPI) entitySearchHintIfAllowed(user *authpublic.AuthenticatedUser, hint entities.SearchHint) *apiv1.EntitySearchHint {
  55. if hint.UniqueKey == "" || hint.Type == "" {
  56. return nil
  57. }
  58. if !api.userCanViewEntityType(user, hint.Type) {
  59. return nil
  60. }
  61. return &apiv1.EntitySearchHint{
  62. Title: hint.Title,
  63. Type: hint.Type,
  64. UniqueKey: hint.UniqueKey,
  65. }
  66. }
  67. func sortEntitySearchHints(hints []*apiv1.EntitySearchHint) {
  68. sort.SliceStable(hints, func(leftIndex, rightIndex int) bool {
  69. if hints[leftIndex].Type != hints[rightIndex].Type {
  70. return hints[leftIndex].Type < hints[rightIndex].Type
  71. }
  72. if hints[leftIndex].UniqueKey != hints[rightIndex].UniqueKey {
  73. return hints[leftIndex].UniqueKey < hints[rightIndex].UniqueKey
  74. }
  75. return hints[leftIndex].Title < hints[rightIndex].Title
  76. })
  77. }
  78. func (api *oliveTinAPI) buildActionSearchHints(user *authpublic.AuthenticatedUser) []*apiv1.ActionSearchHint {
  79. candidates := make([]actionSearchCandidate, 0, maxSearchHintActions)
  80. api.executor.MapActionBindingsLock.RLock()
  81. for _, binding := range api.executor.MapActionBindings {
  82. if candidate, ok := actionSearchCandidateFromBinding(api, user, binding); ok {
  83. candidates = appendBoundedActionSearchCandidates(candidates, candidate, maxSearchHintActions)
  84. }
  85. }
  86. api.executor.MapActionBindingsLock.RUnlock()
  87. out := make([]*apiv1.ActionSearchHint, 0, len(candidates))
  88. for _, candidate := range candidates {
  89. out = append(out, &apiv1.ActionSearchHint{
  90. Title: candidate.title,
  91. BindingId: candidate.bindingID,
  92. })
  93. }
  94. return out
  95. }
  96. func appendBoundedActionSearchCandidates(candidates []actionSearchCandidate, candidate actionSearchCandidate, limit int) []actionSearchCandidate {
  97. candidates = append(candidates, candidate)
  98. sortActionSearchCandidates(candidates)
  99. if len(candidates) > limit {
  100. candidates = candidates[:limit]
  101. }
  102. return candidates
  103. }
  104. type actionSearchCandidate struct {
  105. title string
  106. bindingID string
  107. hasEntity bool
  108. }
  109. func actionSearchCandidateFromBinding(api *oliveTinAPI, user *authpublic.AuthenticatedUser, binding *executor.ActionBinding) (actionSearchCandidate, bool) {
  110. if !isSearchableActionBinding(binding) {
  111. return actionSearchCandidate{}, false
  112. }
  113. if !acl.IsAllowedView(api.cfg, user, binding.Action) {
  114. return actionSearchCandidate{}, false
  115. }
  116. if !api.bindingEntityTypeAllowed(user, binding) {
  117. return actionSearchCandidate{}, false
  118. }
  119. return actionSearchCandidate{
  120. title: tpl.ParseTemplateOfActionBeforeExec(binding.Action.Title, binding.Entity),
  121. bindingID: binding.ID,
  122. hasEntity: binding.Entity != nil,
  123. }, true
  124. }
  125. func isSearchableActionBinding(binding *executor.ActionBinding) bool {
  126. return binding != nil && binding.Action != nil && binding.ID != "" && !binding.Action.Hidden
  127. }
  128. func sortActionSearchCandidates(candidates []actionSearchCandidate) {
  129. sort.SliceStable(candidates, func(leftIndex, rightIndex int) bool {
  130. if candidates[leftIndex].hasEntity != candidates[rightIndex].hasEntity {
  131. return !candidates[leftIndex].hasEntity
  132. }
  133. if candidates[leftIndex].title != candidates[rightIndex].title {
  134. return candidates[leftIndex].title < candidates[rightIndex].title
  135. }
  136. return candidates[leftIndex].bindingID < candidates[rightIndex].bindingID
  137. })
  138. }