snapshot of https://github.com/cortezaproject/corteza.git (2024.9.x, 2026-09-18; blobs > 131072B omitted)
Some checks failed
SonarQube / SonarQube (push) Failing after 5m28s
E2e test on main branch push / server and client test (map[name:admin port:8080]) (push) Failing after 3m33s
Server and lib tests / Library test (js) (push) Failing after 1m36s
Server and lib tests / Library test (vue) (push) Successful in 2m24s
Server and lib tests / Web client test (admin) (push) Successful in 2m52s
Server and lib tests / Web client test (compose) (push) Successful in 3m23s
Server and lib tests / Web client test (discovery) (push) Successful in 2m49s
Server and lib tests / Web client test (one) (push) Successful in 2m52s
Server and lib tests / Web client test (privacy) (push) Successful in 2m44s
Server and lib tests / Web client test (reporter) (push) Successful in 2m56s
Server and lib tests / Web client test (workflow) (push) Successful in 3m0s
Server and lib tests / Server tests (push) Failing after 4m55s
E2e test on main branch push / notify-failure (push) Failing after 9s
Server and lib tests / Send matrix testing notification (push) Successful in 2s
Mark stale issues and pull requests / stale (push) Successful in 40s

This commit is contained in:
upstream-mirror
2026-09-18 01:22:23 +05:00
commit 7e385fc126
8391 changed files with 1766236 additions and 0 deletions

View File

@@ -0,0 +1,446 @@
package rest
import (
"encoding/json"
"fmt"
"html"
"reflect"
"sort"
"strings"
"time"
"github.com/microcosm-cc/bluemonday"
"github.com/spf13/cast"
)
type (
// corteza discovery results
cdResults struct {
Total struct {
Value int `json:"value"`
TotalOp string `json:"op"`
} `json:"-"`
Hits []cdHit `json:"hits"`
TotalHits int `json:"total_hits"`
Aggregations []cdAggregation `json:"aggregations"`
pagination
// Context ldCtx `json:"@context"`
}
pagination struct {
Size int `json:"size"`
From int `json:"from"`
TotalResults int `json:"total_results"`
}
cdHit struct {
Type string `json:"type"`
Value map[string]interface{} `json:"value"`
}
cdAggregation struct {
Resource string `json:"resource"`
Name string `json:"name"`
Hits int `json:"hits"`
ResourceName []cdAggregationHits `json:"resource_name"`
}
cdAggregationHits struct {
Name string `json:"name"`
Label string `json:"label"`
Hits int `json:"hits"`
}
// ldCtx map[string]interface{}
createdBy struct {
UserID uint64 `json:"userID,string"`
Email string `json:"email,omitempty"`
Name string `json:"name,omitempty"`
Username string `json:"username,omitempty"`
Handle string `json:"handle,omitempty"`
}
)
// conv converts results from the backend into corteza-discovery (jsonld-ish) format
func conv(sr *esSearchResponse, aggregation *esSearchResponse, noHits bool, moduleMeta map[string][]string, nsHandleMap map[string]nsMeta, mHandleMap map[string]mMeta, page pagination) (out *cdResults, err error) {
if sr == nil {
return
}
out = &cdResults{}
out.Total.Value = sr.Hits.Total.Value
out.Total.TotalOp = sr.Hits.Total.Relation
// Pagination
out.TotalResults = sr.Hits.Total.Value
out.From = page.From
out.Size = page.Size
// Aggregation
out.Aggregations = []cdAggregation{}
nsTotalHits := make(map[string]cdAggregationHits)
mTotalHits := make(map[string]cdAggregationHits)
aggsRes := sr.Aggregations
if aggregation != nil {
aggsRes = aggregation.Aggregations
}
// for _, bucket := range aggsRes.Resource.Buckets {
// bucketName := getResourceName(bucket.Key)
// if bucketName == "User" {
// continue
// }
//
// for _, subBucket := range bucket.ResourceName.Buckets {
// resourceName := subBucket.Key
// if bucketName == "Namespace" {
// if val, is := nsTotalHits[resourceName]; is {
// val.Hits += subBucket.DocCount
// nsTotalHits[resourceName] = val
// } else {
// nsTotalHits[resourceName] = cdAggregationHits{
// Name: resourceName,
// Label: nsHandleMap[resourceName],
// Hits: subBucket.DocCount,
// }
// }
// }
//
// if bucketName == "Module" {
// if val, is := mTotalHits[resourceName]; is {
// val.Hits += subBucket.DocCount
// mTotalHits[resourceName] = val
// } else {
// mTotalHits[resourceName] = cdAggregationHits{
// Name: resourceName,
// Label: mHandleMap[resourceName],
// Hits: subBucket.DocCount,
// }
// }
// }
// }
//
// // Namespace total aggs hit counts
// for _, nsBucket := range bucket.Namespaces.Buckets {
// resourceName := nsBucket.Key
// if val, is := nsTotalHits[resourceName]; is {
// val.Hits += nsBucket.DocCount
// nsTotalHits[resourceName] = val
// } else {
// nsTotalHits[resourceName] = cdAggregationHits{
// Name: resourceName,
// Label: nsHandleMap[resourceName],
// Hits: nsBucket.DocCount,
// }
// }
// }
//
// // Module total aggs hit counts
// for _, mBucket := range bucket.Modules.Buckets {
// resourceName := mBucket.Key
// if val, is := mTotalHits[resourceName]; is {
// val.Hits += mBucket.DocCount
// mTotalHits[resourceName] = val
//
// } else {
// mTotalHits[resourceName] = cdAggregationHits{
// Name: resourceName,
// Label: mHandleMap[resourceName],
// Hits: mBucket.DocCount,
// }
// }
// }
// }
nsAggregation := cdAggregation{
Name: "Namespace",
Resource: "compose:namespace",
Hits: 0,
ResourceName: []cdAggregationHits{},
}
for _, bucket := range aggsRes.Namespace.Namespace.Buckets {
resourceName := bucket.Key
if val, is := nsTotalHits[resourceName]; is {
val.Hits += bucket.DocCount
nsTotalHits[resourceName] = val
} else {
var name string
if nsHandleMap != nil {
name = nsHandleMap[resourceName].Name
}
if len(name) > 0 {
nsTotalHits[resourceName] = cdAggregationHits{
Name: name,
Label: resourceName,
Hits: bucket.DocCount,
}
}
}
}
for _, nsHits := range nsTotalHits {
nsAggregation.Hits += nsHits.Hits
nsAggregation.ResourceName = append(nsAggregation.ResourceName, nsHits)
}
if len(nsAggregation.ResourceName) > 0 {
sort.Slice(nsAggregation.ResourceName, func(i, j int) bool {
return nsAggregation.ResourceName[i].Name < nsAggregation.ResourceName[j].Name
})
out.Aggregations = append(out.Aggregations, nsAggregation)
}
mAggregation := cdAggregation{
Name: "Module",
Resource: "compose:module",
Hits: 0,
ResourceName: []cdAggregationHits{},
}
for _, bucket := range aggsRes.Module.Module.Buckets {
resourceName := bucket.Key
if val, is := mTotalHits[resourceName]; is {
val.Hits += bucket.DocCount
mTotalHits[resourceName] = val
} else {
var name string
if mHandleMap != nil {
name = mHandleMap[resourceName].Name
}
mTotalHits[resourceName] = cdAggregationHits{
Name: name,
Label: resourceName,
Hits: bucket.DocCount,
}
}
}
for _, mHits := range mTotalHits {
mAggregation.Hits += mHits.Hits
mAggregation.ResourceName = append(mAggregation.ResourceName, mHits)
}
if len(mAggregation.ResourceName) > 0 {
sort.Slice(mAggregation.ResourceName, func(i, j int) bool {
return mAggregation.ResourceName[i].Name < mAggregation.ResourceName[j].Name
})
out.Aggregations = append(out.Aggregations, mAggregation)
}
// if !noHits {
hits:
for _, h := range sr.Hits.Hits {
aux := map[string]interface{}{}
if err = json.Unmarshal(h.Source, &aux); err != nil {
return
}
resType := cast.ToString(aux["resourceType"])
delete(aux, "resourceType")
switch resType {
case "system:user":
aux["id"] = aux["userID"]
delete(aux, "userID")
case "compose:record":
// @todo: Remove below line and find proper solution for searsia as value needs to be in json
ssVal := make(map[string]interface{})
// @todo refactor record related values
type (
created struct {
At *time.Time `json:"at,omitempty"`
By string `json:"by,omitempty"`
}
record struct {
Created struct {
At *time.Time `json:"at,omitempty"`
By *createdBy `json:"by,omitempty"`
} `json:"created"`
Module struct {
Name string `json:"name"`
Handle string `json:"handle"`
ModuleId uint64 `json:"moduleId,string"`
} `json:"module"`
Namespace struct {
Name string `json:"name"`
Handle string `json:"handle"`
NamespaceId uint64 `json:"namespaceId,string"`
} `json:"namespace"`
Values map[string]interface{} `json:"values"`
ValueLabels map[string]string `json:"valueLabels"`
}
)
var r record
if err = json.Unmarshal(h.Source, &r); err != nil {
return
}
type valueJson struct {
Name string `json:"name"`
Label string `json:"label"`
Value interface{} `json:"value"`
}
key := fmt.Sprintf("%d-%d", r.Namespace.NamespaceId, r.Module.ModuleId)
var (
slice []valueJson
uc = created{
At: r.Created.At,
By: getCreatedBy(r.Created.By),
}
)
if val, is := moduleMeta[key]; is {
for _, f := range val {
slice = append(slice, valueJson{
Name: f,
Label: r.ValueLabels[f],
Value: sanitize(r.Values[f]),
})
if vv, ok := r.Values[f].([]interface{}); ok {
if len(vv) > 0 {
ssVal[f] = sanitize(vv[0])
}
}
}
} else {
for k, v := range r.Values {
// @todo hardcoded value
sanitizedVal := sanitize(v)
if len(slice) < 5 && sanitizedVal != nil {
slice = append(slice, valueJson{
Name: k,
Label: r.ValueLabels[k],
Value: sanitizedVal,
})
if vv, ok := v.([]interface{}); ok {
if len(vv) > 0 {
ssVal[k] = sanitize(vv[0])
}
}
}
}
}
matching_fields := make(map[string]any)
for key, value := range h.Highlight {
newKey := strings.TrimPrefix(key, "values.")
matching_fields[newKey] = value
}
aux["created"] = uc
aux["customValues"] = ssVal
aux["values"] = slice
aux["matching_fields"] = matching_fields
aux["@id"] = aux["_id"]
delete(aux, "_id")
delete(aux, "valueLabels")
case "compose:namespace":
aux["@id"] = aux["_id"]
delete(aux, "_id")
delete(aux, "Namespace")
case "compose:module":
aux["@id"] = aux["_id"]
delete(aux, "_id")
delete(aux, "Namespace")
delete(aux, "Module")
default:
continue hits
}
out.Hits = append(out.Hits, cdHit{
Type: resType,
Value: aux,
})
}
out.TotalHits = len(out.Hits)
// }
return
}
// @todo use RBAC resource stringify
// getResourceName return name of resource based on resource type
func getResourceName(resType string) string {
switch resType {
case "system:user":
return "User"
case "compose:record":
return "Record"
case "compose:namespace":
return "Namespace"
case "compose:module":
return "Module"
default:
return "Resource"
}
}
func getCreatedBy(user *createdBy) string {
if user == nil {
return ""
}
if len(user.Name) > 0 {
return user.Name
} else if len(user.Username) > 0 {
return user.Username
} else if len(user.Email) > 0 {
return user.Email
} else if user.UserID > 0 {
return fmt.Sprintf("%d", user.UserID)
}
return ""
}
func sanitize(v interface{}) interface{} {
if v == nil {
return v
}
switch v.(type) {
case string:
v = richText(v.(string))
case []interface{}:
vv := reflect.ValueOf(v)
for i, val := range v.([]interface{}) {
// Wee need to do a failsafe here because we're working with reflect values
// In case sanitize returns nil, we'll get a runtime panic
sanitized := sanitize(val)
var v reflect.Value
// Since we're working with types, we need the zero value of interface{} over nil
if sanitized == nil {
v = reflect.Zero(reflect.TypeOf((*any)(nil)).Elem())
} else {
v = reflect.ValueOf(sanitized)
}
vv.Index(i).Set(v)
}
default:
return v
}
return v
}
// RichText assures safe HTML content
func richText(in string) string {
// use standard html escaping policy
p := bluemonday.StrictPolicy()
sanitized := p.Sanitize(in)
// handle escaped strings and unescape them
// all the dangerous chars should have been stripped
// by now
return html.UnescapeString(sanitized)
}

View File

@@ -0,0 +1,645 @@
package rest
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io/ioutil"
"os"
"strconv"
"strings"
"github.com/davecgh/go-spew/spew"
"github.com/elastic/go-elasticsearch/v7"
"github.com/elastic/go-elasticsearch/v7/esapi"
"github.com/go-chi/jwtauth"
"go.uber.org/zap"
)
type (
// EsSearchAggrTerms is aggregations parameter for es search api.
EsSearchAggrTerms map[string]esSearchAggr
EsSearchNestedAggrTerms map[string]esSearchNestedAggr
EsSearchHighlight struct {
RequireFieldMatch bool `json:"require_field_match"`
Fields map[string]highlightSetting `json:"fields"`
}
esSearchParamsIndex struct {
Prefix struct {
Index struct {
Value string `json:"value"`
} `json:"_index"`
} `json:"prefix"`
}
esSimpleQueryString struct {
Wrap struct {
Query string `json:"query"`
} `json:"simple_query_string"`
}
esDisMax struct {
TieBreaker float64 `json:"tie_breaker,omitempty"`
Boost float64 `json:"boost,omitempty"`
Queries []interface{} `json:"queries,omitempty"`
}
esDisMaxWrap struct {
Wrap esDisMax `json:"dis_max,omitempty"`
}
esNestedDisMax struct {
Wrap struct {
IgnoreUnmapped bool `json:"ignore_unmapped"`
Path string `json:"path,omitempty"`
Query esDisMaxWrap `json:"query,omitempty"`
} `json:"nested,omitempty"`
}
esMultiMatch struct {
Wrap struct {
Query string `json:"query"`
Type string `json:"type"`
// Operator string `json:"operator"`
Fields []string `json:"fields"`
MinimumShouldMatch string `json:"minimum_should_match"`
} `json:"multi_match"`
}
VectorValue struct {
Vector []float32 `json:"vector"`
K int `json:"k"`
Filter []interface{} `json:"filter,omitempty"`
}
esSearchParams struct {
Query struct {
Bool *struct {
// query context
Must []interface{} `json:"must,omitempty"`
Should []interface{} `json:"should,omitempty"`
// filter context
Filter []interface{} `json:"filter,omitempty"`
MustNot []interface{} `json:"must_not,omitempty"`
} `json:"bool,omitempty"`
} `json:"query"`
Aggregations EsSearchNestedAggrTerms `json:"aggs,omitempty"`
Source SourceFilter `json:"_source,omitempty"`
MinScore float32 `json:"min_score,omitempty"`
Highlight EsSearchHighlight `json:"highlight,omitempty"`
}
esSearchAggrTerm struct {
Field string `json:"field,omitempty"`
Size int `json:"size,omitempty"`
}
esSearchAggrComposite struct {
Sources interface{} `json:"sources"` // it can be esSearchAggrTerm,.. (Histogram, Date histogram, GeoTile grid)
Size int `json:"size,omitempty"`
}
SourceFilter struct {
Excludes []string `json:"excludes"` //columns to exclude on the query response
}
esSearchNestedAggs struct {
Path string `json:"path,omitempty"`
}
esSearchAggr struct {
Nested esSearchNestedAggs `json:"nested,omitempty"`
Terms esSearchAggrTerm `json:"terms,omitempty"`
Aggregations EsSearchAggrTerms `json:"aggs,omitempty"`
// Composite *esSearchAggrComposite `json:"composite"`
}
esSearchNestedAggrTerm struct {
Terms esSearchAggrTerm `json:"terms,omitempty"`
}
esSearchNestedAggr struct {
Nested esSearchNestedAggs `json:"nested,omitempty"`
Aggregations map[string]esSearchNestedAggrTerm `json:"aggs,omitempty"`
// Composite *esSearchAggrComposite `json:"composite"`
}
esSearchResponse struct {
Took int `json:"took"`
TimedOut bool `json:"timed_out"`
Hits esSearchHits `json:"hits"`
Aggregations esSearchAggregations `json:"aggregations"`
}
esSearchTotal struct {
Value int `json:"value"`
Relation string `json:"relation"`
}
esSearchHits struct {
Total esSearchTotal `json:"total"`
Hits []*esSearchHit `json:"hits"`
}
esSearchHit struct {
Index string `json:"_index"`
ID string `json:"_id"`
Source json.RawMessage `json:"_source"`
Highlight map[string]any `json:"highlight,omitempty"`
}
esSearchAggregations struct {
Resource struct {
DocCountErrorUpperBound int `json:"-"`
SumOtherDocCount int `json:"-"`
Buckets []struct {
Key string `json:"key"`
DocCount int `json:"doc_count"`
ResourceName struct {
DocCountErrorUpperBound int `json:"-"`
SumOtherDocCount int `json:"-"`
Buckets []struct {
Key string `json:"key"`
DocCount int `json:"doc_count"`
} `json:"buckets"`
} `json:"resourceName"`
Namespaces struct {
DocCountErrorUpperBound int `json:"-"`
SumOtherDocCount int `json:"-"`
Buckets []struct {
Key string `json:"key"`
DocCount int `json:"doc_count"`
} `json:"buckets"`
} `json:"namespaces"`
Modules struct {
DocCountErrorUpperBound int `json:"-"`
SumOtherDocCount int `json:"-"`
Buckets []struct {
Key string `json:"key"`
DocCount int `json:"doc_count"`
} `json:"buckets"`
} `json:"modules"`
} `json:"buckets"`
} `json:"resource"`
Module struct {
DocCount int `json:"doc_count"`
Module struct {
DocCountErrorUpperBound int `json:"doc_count_error_upper_bound"`
SumOtherDocCount int `json:"sum_other_doc_count"`
Buckets []struct {
Key string `json:"key"`
DocCount int `json:"doc_count"`
} `json:"buckets"`
} `json:"module"`
} `json:"module"`
Namespace struct {
DocCount int `json:"doc_count"`
Namespace struct {
DocCountErrorUpperBound int `json:"doc_count_error_upper_bound"`
SumOtherDocCount int `json:"sum_other_doc_count"`
Buckets []struct {
Key string `json:"key"`
DocCount int `json:"doc_count"`
} `json:"buckets"`
} `json:"namespace"`
} `json:"namespace"`
}
searchParams struct {
title string
query string
moduleAggs []string
namespaceAggs []string
dumpRaw bool
from int
size int
resourceType []string
aggOnly bool
mAggOnly bool
allowedRoles map[interface{}]bool
searchMode string
}
highlightSetting struct {
PreTags []string `json:"pre_tags"`
PostTags []string `json:"post_tags"`
NumberOfFragments int `json:"number_of_fragments"`
}
)
var (
rtIndexMapping = map[string]string{
"compose:module": "compose-modules",
"compose:record": "compose-records-*",
"compose:namespace": "compose-namespaces",
"system:user": "system-users",
}
)
func esSearch(ctx context.Context, log *zap.Logger, esc *elasticsearch.Client, p searchParams) (sr *esSearchResponse, page pagination, err error) {
var (
buf bytes.Buffer
roles []string
userID uint64
_, claims, _ = jwtauth.FromContext(ctx)
allowedRoleExist = false
)
if _, has := claims["roles"]; has {
if rr, is := claims["roles"].([]interface{}); is {
for _, role := range rr {
if _, ok := p.allowedRoles[role]; ok {
allowedRoleExist = true
}
roles = append(roles, fmt.Sprintf("%s", role))
}
}
}
if _, has := claims["sub"]; has {
if sub, is := claims["sub"].(string); is {
userID, _ = extractFromSubClaim(sub)
}
}
noQ := len(p.query) == 0
noNSFilter := len(p.namespaceAggs) == 0
// noMFilter := len(p.moduleAggs) == 0
sqs := esSimpleQueryString{}
sqs.Wrap.Query = strings.TrimSpace(strings.ToLower(p.query))
query := &esSearchParams{}
index := esSearchParamsIndex{}
if query.Query.Bool == nil {
query.Query.Bool = &struct {
Must []interface{} `json:"must,omitempty"`
Should []interface{} `json:"should,omitempty"`
Filter []interface{} `json:"filter,omitempty"`
MustNot []interface{} `json:"must_not,omitempty"`
}{}
}
// Decide what indexes we can use
if userID == 0 {
// Missing, invalid, expired access token (JWT)
index.Prefix.Index.Value = "corteza-public-"
} else {
// Authenticated user
index.Prefix.Index.Value = "corteza-private-"
if !allowedRoleExist {
// Skip all documents that do not have baring roles in to allow list
query.Query.Bool.Filter = append(query.Query.Bool.Filter, map[string]map[string]interface{}{
"terms": {"security.allowedRoles": roles},
})
// Skip all documents that have baring roles in to deny list
query.Query.Bool.MustNot = append(query.Query.Bool.MustNot, map[string]map[string]interface{}{
"terms": {"security.deniedRoles": roles},
})
}
}
// Aggregations V1.0
// if len(p.aggregations) > 0 {
// query.Aggregations = make(map[string]esSearchAggr)
//
// for _, a := range p.aggregations {
// query.Aggregations[a] = esSearchAggr{esSearchAggrTerm{Field: a + ".keyword"}}
// }
// }
// Search string filter
if !noQ {
applyTraditionalSearch(query, index, sqs.Wrap.Query)
}
// add the highlight for the matched fields.
query.Highlight = EsSearchHighlight{
RequireFieldMatch: false,
Fields: map[string]highlightSetting{
"values.*": {
PreTags: []string{"*"},
PostTags: []string{"*"},
NumberOfFragments: 0,
},
},
}
var (
mm = esMultiMatch{}
mdd esNestedDisMax
nsdd esNestedDisMax
)
for _, mAggs := range p.moduleAggs {
mm.Wrap.Query = mAggs
mm.Wrap.Type = "cross_fields"
mm.Wrap.MinimumShouldMatch = "100%"
mm.Wrap.Fields = []string{"module.name"}
// query.Query.Bool.Must = append(query.Query.Bool.Must, mm)
// query.Query.DisMax.Queries = append(query.Query.DisMax.Queries, mm)
// dd.Wrap.Queries = append(dd.Wrap.Queries, mm)
mdd.Wrap.Path = "module"
nsdd.Wrap.IgnoreUnmapped = true
mdd.Wrap.Query.Wrap.Queries = append(mdd.Wrap.Query.Wrap.Queries, mm)
}
if len(mdd.Wrap.Query.Wrap.Queries) > 0 {
query.Query.Bool.Must = append(query.Query.Bool.Must, mdd)
}
// no need now since we are adding below as filter
// if p.aggOnly {
for _, nAggs := range p.namespaceAggs {
mm.Wrap.Query = nAggs
mm.Wrap.Type = "cross_fields"
mm.Wrap.MinimumShouldMatch = "100%"
mm.Wrap.Fields = []string{"namespace.name"}
// query.Query.Bool.Must = append(query.Query.Bool.Must, mm)
// query.Query.DisMax.Queries = append(query.Query.DisMax.Queries, mm)
// dd.Wrap.Queries = append(dd.Wrap.Queries, mm)
nsdd.Wrap.Path = "namespace"
nsdd.Wrap.IgnoreUnmapped = true
nsdd.Wrap.Query.Wrap.Queries = append(nsdd.Wrap.Query.Wrap.Queries, mm)
}
// }
if len(nsdd.Wrap.Query.Wrap.Queries) > 0 {
query.Query.Bool.Must = append(query.Query.Bool.Must, nsdd)
}
// if !p.aggOnly && !noNSFilter {
// nsf := make(map[string]interface{})
// nsf["terms"] = map[string][]string{
// "namespace.name.keyword": p.namespaceAggs,
// }
// query.Query.Bool.Filter = append(query.Query.Bool.Filter, nsf)
// }
// Aggregations V1.0 Improved
// if len(p.aggregations) > 0 {
// for _, a := range p.aggregations {
// if len(a) > 0 {
// sqs = esSimpleQueryString{}
// sqs.Wrap.Query = a
// query.Query.Bool.Must = append(query.Query.Bool.Must, sqs)
// }
// }
// }
// if noQ == 0 && len(p.moduleAggs) == 0 && len(p.namespaceAggs) == 0 {
// query.Query.DisMax.Queries = append(query.Query.DisMax.Queries, index)
// }
query.Aggregations = make(map[string]esSearchNestedAggr)
query.Aggregations["namespace"] = esSearchNestedAggr{
Nested: esSearchNestedAggs{Path: "namespace"},
Aggregations: map[string]esSearchNestedAggrTerm{
"namespace": {
Terms: esSearchAggrTerm{
// Field: "namespace.name.keyword",
Field: "namespace.handle",
Size: 999,
},
},
},
}
query.Source = SourceFilter{
Excludes: []string{"vectorsValue"},
}
if !noQ || !noNSFilter {
query.Aggregations["module"] = esSearchNestedAggr{
Nested: esSearchNestedAggs{Path: "module"},
Aggregations: map[string]esSearchNestedAggrTerm{
"module": {
Terms: esSearchAggrTerm{
// Field: "module.name.keyword",
Field: "module.handle",
Size: 999,
},
},
},
}
}
// query.Aggregations["resource"] = esSearchAggr{
// Terms: esSearchAggrTerm{
// Field: "resourceType.keyword",
// Size: 999,
// },
// Aggregations: EsSearchAggrTerms{
// "resourceName": esSearchAggr{
// Terms: esSearchAggrTerm{
// Field: "name.keyword",
// Size: 999,
// },
// },
// "modules": esSearchAggr{
// Terms: esSearchAggrTerm{
// Field: "module.name.keyword",
// Size: 999,
// },
// },
// "namespaces": esSearchAggr{
// Terms: esSearchAggrTerm{
// Field: "namespace.name.keyword",
// Size: 999,
// },
// },
// },
// }
// Aggregations V2.0
// if len(p.aggregations) > 0 {
// query.Aggregations = (Aggregations{}).encodeTerms(p.aggregations)
// }
// spew.Dump("query: ", query)
if err = json.NewEncoder(&buf).Encode(query); err != nil {
err = fmt.Errorf("could not encode query: %q", err)
return
}
spew.Dump("buf: ", buf)
log.Debug("searching ",
zap.String("for", p.title),
// zap.String("open search body", buf.String()),
)
// Why set size to 999? default value for size is 10,
// so we needed to set value till we add (@todo) pagination to search result
if p.from > 0 && p.from >= p.size {
p.size = p.from + 100
}
if p.size == 0 {
p.size = 999
}
page.From = p.from
page.Size = p.size
// In case of requesting specific resource types, use only the appropriate indexes
indexes := []string{}
if len(p.resourceType) > 0 {
for _, rt := range p.resourceType {
if _, ok := rtIndexMapping[rt]; !ok {
err = fmt.Errorf("invalid resource type: %s", rt)
return
}
indexes = append(indexes, fmt.Sprintf("%s%s", index.Prefix.Index.Value, rtIndexMapping[rt]))
}
}
sReqArgs := []func(*esapi.SearchRequest){
esc.Search.WithContext(ctx),
esc.Search.WithBody(&buf),
esc.Search.WithTrackTotalHits(true),
// esc.Search.WithScroll(),
esc.Search.WithSize(p.size),
esc.Search.WithFrom(p.from),
esc.Search.WithIndex(indexes...),
// esc.Search.WithExplain(true), // debug
}
if p.dumpRaw {
sReqArgs = append(
sReqArgs,
esc.Search.WithSourceExcludes("security"),
esc.Search.WithPretty(),
)
}
// Perform the search request.
res, err := esc.Search(sReqArgs...)
if err != nil {
return
}
if err = validElasticResponse(res, err); err != nil {
err = fmt.Errorf("invalid search response: %w", err)
return
}
defer res.Body.Close()
if p.dumpRaw {
// Copy body buf and then restore it
bodyBytes, _ := ioutil.ReadAll(res.Body)
res.Body = ioutil.NopCloser(bytes.NewBuffer(bodyBytes))
os.Stdout.Write(bodyBytes)
}
sr = &esSearchResponse{}
if err = json.NewDecoder(res.Body).Decode(sr); err != nil {
return
}
// Print the response status, number of results, and request duration.
log.Debug("search completed",
zap.String("for", p.title),
zap.String("query", sqs.Wrap.Query),
zap.String("indexPrefix", index.Prefix.Index.Value),
zap.Int("from", p.from),
zap.Int("size", p.size),
zap.String("status", res.Status()),
zap.Int("took", sr.Took),
zap.Bool("timedOut", sr.TimedOut),
zap.Int("hits", sr.Hits.Total.Value),
zap.String("hitsRelation", sr.Hits.Total.Relation),
zap.Int("namespaceAggs", len(sr.Aggregations.Namespace.Namespace.Buckets)),
zap.Int("moduleAggs", len(sr.Aggregations.Module.Module.Buckets)),
)
return
}
// @todo move this to es service
func validElasticResponse(res *esapi.Response, err error) error {
if err != nil {
return fmt.Errorf("failed to get response from search backend: %w", err)
}
if res.IsError() {
defer res.Body.Close()
var rsp struct {
Error struct {
Type string
Reason string
}
}
if err := json.NewDecoder(res.Body).Decode(&rsp); err != nil {
return fmt.Errorf("could not parse response body: %w", err)
} else {
return fmt.Errorf("search backend responded with an error: %s (type: %s, status: %s)", rsp.Error.Reason, rsp.Error.Type, res.Status())
}
}
return nil
}
func extractFromSubClaim(sub string) (userID uint64, rr []uint64) {
parts := strings.Split(sub, " ")
rr = make([]uint64, len(parts)-1)
for p := range parts {
id, _ := strconv.ParseUint(parts[p], 10, 64)
if p == 0 {
userID = id
} else {
rr[p-1] = id
}
}
return
}
func applyTraditionalSearch(query *esSearchParams, index interface{}, searchQuery string) {
query.Query.Bool.Must = []interface{}{index}
query.Query.Bool.Must = append(query.Query.Bool.Must, buildWildcardQuery(searchQuery))
}
func buildWildcardQuery(searchQuery string) map[string]interface{} {
return map[string]interface{}{
"wildcard": map[string]interface{}{
"catch_all": map[string]interface{}{
"value": fmt.Sprintf("*%s*", searchQuery),
},
},
}
}
func buildKNNQuery(vector interface{}) map[string]interface{} {
return map[string]interface{}{
"knn": map[string]interface{}{
"vectorsValue": map[string]interface{}{
"vector": vector,
"k": 10,
},
},
}
}
func buildMultiMatchQuery(searchQuery string) map[string]interface{} {
return map[string]interface{}{
"multi_match": map[string]interface{}{
"query": searchQuery,
"fields": []string{"catch_all"},
"boost": 1.0,
},
}
}

View File

@@ -0,0 +1,49 @@
package handlers
import (
"context"
"github.com/cortezaproject/corteza/extra/server-discovery/searcher/rest/request"
"github.com/cortezaproject/corteza/server/pkg/api"
"github.com/go-chi/chi/v5"
"net/http"
)
type (
// SearchAPI Internal API interface
SearchAPI interface {
SearchResources(context.Context, *request.SearchResources) (interface{}, error)
}
// Search HTTP API interface
Search struct {
SearchResources func(http.ResponseWriter, *http.Request)
}
)
func NewSearch(h SearchAPI) *Search {
return &Search{
SearchResources: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewSearchListResources()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.SearchResources(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
}
}
func (h Search) MountRoutes(r chi.Router, middlewares ...func(http.Handler) http.Handler) {
r.Group(func(r chi.Router) {
r.Use(middlewares...)
r.Get("/", h.SearchResources)
})
}

View File

@@ -0,0 +1,139 @@
package request
import (
"net/http"
"github.com/cortezaproject/corteza/server/pkg/payload"
)
type (
SearchResources struct {
Q string
From int
Size int
Rt []string
NamespaceAggs []string
ModuleAggs []string
DumpRaw string
}
)
// NewSearchListResources request
func NewSearchListResources() *SearchResources {
return &SearchResources{}
}
// Auditable returns all auditable/loggable parameters
func (r SearchResources) Auditable() map[string]interface{} {
return map[string]interface{}{
"q": r.Q,
"from": r.From,
"size": r.Size,
"namespaceAggs": r.NamespaceAggs,
"resourceTypes": r.Rt,
"moduleAggs": r.ModuleAggs,
"dump": r.DumpRaw,
}
}
func (r SearchResources) GetQuery() string {
return r.Q
}
func (r SearchResources) GetSize() int {
return r.Size
}
func (r SearchResources) GetFrom() int {
return r.From
}
func (r SearchResources) GetResourceTypes() []string {
return r.Rt
}
func (r SearchResources) GetNamespaceAggs() []string {
return r.NamespaceAggs
}
func (r SearchResources) GetModuleAggs() []string {
return r.ModuleAggs
}
func (r SearchResources) GetDumpRaw() string {
return r.DumpRaw
}
// Fill processes request and fills internal variables
func (r *SearchResources) Fill(req *http.Request) (err error) {
{
// GET params
tmp := req.URL.Query()
if val, ok := tmp["q"]; ok && len(val) > 0 {
r.Q, err = val[0], nil
if err != nil {
return err
}
}
if val, ok := tmp["size"]; ok && len(val) > 0 {
r.Size, err = payload.ParseInt(val[0]), nil
if err != nil {
return err
}
}
if val, ok := tmp["from"]; ok && len(val) > 0 {
r.From, err = payload.ParseInt(val[0]), nil
if err != nil {
return err
}
}
if val, ok := tmp["resourceTypes[]"]; ok {
r.Rt, err = val, nil
if err != nil {
return err
}
} else if val, ok = tmp["resourceTypes"]; ok {
r.Rt, err = val, nil
if err != nil {
return err
}
}
if val, ok := tmp["namespaceAggs[]"]; ok {
r.NamespaceAggs, err = val, nil
if err != nil {
return err
}
} else if val, ok = tmp["namespaceAggs"]; ok {
r.NamespaceAggs, err = val, nil
if err != nil {
return err
}
}
if val, ok := tmp["moduleAggs[]"]; ok {
r.ModuleAggs, err = val, nil
if err != nil {
return err
}
} else if val, ok = tmp["moduleAggs"]; ok {
r.ModuleAggs, err = val, nil
if err != nil {
return err
}
}
if val, ok := tmp["dump"]; ok && len(val) > 0 {
r.DumpRaw, err = val[0], nil
if err != nil {
return err
}
}
}
return err
}

View File

@@ -0,0 +1,21 @@
package rest
import (
"github.com/cortezaproject/corteza/extra/server-discovery/pkg/auth"
"github.com/cortezaproject/corteza/extra/server-discovery/searcher/rest/handlers"
"github.com/go-chi/chi/v5"
"go.uber.org/zap"
)
func MountRoutes(log *zap.Logger) func(r chi.Router) {
return func(r chi.Router) {
r.Group(func(r chi.Router) {
search := Search()
r.Use(auth.HttpTokenValidator("discovery"))
// traditional search endpoint for Traditional and vectorsearch
handlers.NewSearch(search).MountRoutes(r)
})
}
}

View File

@@ -0,0 +1,336 @@
package rest
import (
"context"
"encoding/json"
"fmt"
"net/http"
"github.com/cortezaproject/corteza/extra/server-discovery/searcher"
"github.com/cortezaproject/corteza/extra/server-discovery/searcher/rest/request"
types2 "github.com/cortezaproject/corteza/server/compose/types"
)
type (
search struct{}
cResponse struct {
Response struct {
Set []struct {
NamespaceID uint64 `json:",string"`
Slug string `json:"slug"`
Name string `json:"name"`
ModuleID uint64 `json:",string"`
Handle string `json:"handle"`
Config types2.ModuleConfig `json:"config"`
} `json:"set,omitempty"`
} `json:"response,omitempty"`
}
moduleMeta struct {
Discovery ModuleMeta `json:"discovery"`
}
ModuleMeta struct {
Public struct {
Result []Result `json:"result"`
} `json:"public"`
Private struct {
Result []Result `json:"result"`
} `json:"private"`
Protected struct {
Result []Result `json:"result"`
} `json:"protected"`
}
Result struct {
Lang string `json:"lang"`
Fields []string `json:"fields"`
// @todo? TBD? excludeModuleFields, includeModuleFields <- if passed filter module field accordingly.
}
nsMeta struct {
Name string
Handle string
}
mMeta struct {
Name string
Handle string
}
)
func Search() *search {
return &search{}
}
func (s search) SearchResources(ctx context.Context, r *request.SearchResources) (out interface{}, err error) {
var (
log = searcher.DefaultLogger
allowedRoles = searcher.DefaultConfig.Searcher.AllowedRole
searchString = r.GetQuery()
size = r.GetSize()
from = r.GetFrom()
namespaceAggs = r.GetNamespaceAggs()
moduleAggs = r.GetModuleAggs()
resourceTypes = r.GetResourceTypes()
validDumpRaw = r.GetDumpRaw() != ""
page pagination
results *esSearchResponse
aggregation *esSearchResponse
nsAggregation *esSearchResponse
mAggregation *esSearchResponse
nsReq *http.Request
nsRes *http.Response
mReq *http.Request
mRes *http.Response
nsResponse cResponse
mResponse cResponse
moduleMap = make(map[string][]string)
nsHandleMap = make(map[string]nsMeta)
mHandleMap = make(map[string]mMeta)
)
esc := searcher.DefaultEsClient
results, page, err = esSearch(ctx, log, esc, searchParams{
title: "results",
query: searchString,
resourceType: resourceTypes,
from: from,
size: size,
moduleAggs: moduleAggs,
namespaceAggs: namespaceAggs,
dumpRaw: validDumpRaw,
allowedRoles: allowedRoles,
})
if err != nil {
return nil, fmt.Errorf("could not execute search: %w", err)
}
if len(searchString) == 0 {
aggregation, _, err = esSearch(ctx, log, esc, searchParams{
title: "aggregation",
// size: size,
dumpRaw: validDumpRaw,
namespaceAggs: namespaceAggs,
aggOnly: true,
allowedRoles: allowedRoles,
})
if err != nil {
return nil, fmt.Errorf("could not execute aggregation search: %w", err)
}
}
// append all namespace agg with counts no matter what
nsAggregation, _, err = esSearch(ctx, log, esc, searchParams{
title: "nsAggregation",
// size: size,
dumpRaw: validDumpRaw,
aggOnly: true,
allowedRoles: allowedRoles,
})
if err != nil {
return nil, fmt.Errorf("could not execute namespace aggregation search: %w", err)
}
if len(searchString) == 0 {
if aggregation != nil && nsAggregation != nil {
aggregation.Aggregations.Namespace = nsAggregation.Aggregations.Namespace
}
} else {
if results != nil && nsAggregation != nil {
nsMap := make(map[string]struct {
Key string `json:"key"`
DocCount int `json:"doc_count"`
})
for _, bucket := range results.Aggregations.Namespace.Namespace.Buckets {
nsMap[bucket.Key] = bucket
}
var buckets []struct {
Key string `json:"key"`
DocCount int `json:"doc_count"`
}
for _, bucket := range nsAggregation.Aggregations.Namespace.Namespace.Buckets {
val, ok := nsMap[bucket.Key]
if ok {
val.DocCount = nsMap[bucket.Key].DocCount
} else {
val.Key = bucket.Key
val.DocCount = 0
}
buckets = append(buckets, val)
}
results.Aggregations.Namespace.Namespace.Buckets = buckets
}
}
// append namespace agg response which are not in es response
if results != nil && len(namespaceAggs) > 0 {
nsMap := make(map[string]struct {
Key string `json:"key"`
DocCount int `json:"doc_count"`
})
var bb []struct {
Key string `json:"key"`
DocCount int `json:"doc_count"`
}
for _, b := range results.Aggregations.Namespace.Namespace.Buckets {
nsMap = map[string]struct {
Key string `json:"key"`
DocCount int `json:"doc_count"`
}{
b.Key: b,
}
bb = append(bb, b)
}
for _, agg := range namespaceAggs {
if _, ok := nsMap[agg]; !ok {
nsMap = map[string]struct {
Key string `json:"key"`
DocCount int `json:"doc_count"`
}{
agg: {Key: agg, DocCount: 0},
}
bb = append(bb, struct {
Key string `json:"key"`
DocCount int `json:"doc_count"`
}{Key: agg, DocCount: 0})
}
}
if len(bb) > 0 {
results.Aggregations.Namespace.Namespace.Buckets = bb
}
}
mAggregation, _, err = esSearch(ctx, log, esc, searchParams{
title: "mAggregation",
// size: size,
dumpRaw: validDumpRaw,
query: searchString,
namespaceAggs: namespaceAggs,
aggOnly: true,
mAggOnly: true,
allowedRoles: allowedRoles,
})
if err != nil {
return nil, fmt.Errorf("could not execute module aggregation search: %w", err)
}
if len(searchString) > 0 {
if results != nil && mAggregation != nil {
results.Aggregations.Module = mAggregation.Aggregations.Module
}
}
// append module agg response which are not in es response
if results != nil && len(moduleAggs) > 0 {
mMap := make(map[string]struct {
Key string `json:"key"`
DocCount int `json:"doc_count"`
})
var bb []struct {
Key string `json:"key"`
DocCount int `json:"doc_count"`
}
for _, b := range results.Aggregations.Module.Module.Buckets {
mMap = map[string]struct {
Key string `json:"key"`
DocCount int `json:"doc_count"`
}{
b.Key: b,
}
bb = append(bb, b)
}
for _, agg := range moduleAggs {
if _, ok := mMap[agg]; !ok {
mMap = map[string]struct {
Key string `json:"key"`
DocCount int `json:"doc_count"`
}{
agg: {Key: agg, DocCount: 0},
}
bb = append(bb, struct {
Key string `json:"key"`
DocCount int `json:"doc_count"`
}{Key: agg, DocCount: 0})
}
}
if len(bb) > 0 {
results.Aggregations.Module.Module.Buckets = bb
}
}
noHits := len(searchString) == 0 && len(moduleAggs) == 0 && len(namespaceAggs) == 0
// if !noHits {
// @todo only fetch module from result but that requires another loop to fetch module Id from es response
// TEMP fix, I have solution use elastic for the same but different index
nsReq, err = searcher.DefaultApiClient.Namespaces()
if err != nil {
return nil, fmt.Errorf("failed to prepare namespace request: %w", err)
} else {
if nsRes, err = searcher.DefaultApiClient.HttpClient().Do(nsReq.WithContext(ctx)); err != nil {
return nil, fmt.Errorf("failed to send namespace request: %w", err)
}
if nsRes.StatusCode != http.StatusOK {
fmt.Println("err: ", err)
return nil, fmt.Errorf("request resulted in an unexpected status: %s: %w", err)
}
if err = json.NewDecoder(nsRes.Body).Decode(&nsResponse); err != nil {
return nil, fmt.Errorf("failed to decode namespace response: %w", err)
}
if err = nsRes.Body.Close(); err != nil {
return nil, fmt.Errorf("failed to close namespace response body: %w", err)
}
for _, s := range nsResponse.Response.Set {
// Get the module handles for aggs response
nsHandleMap[s.Slug] = nsMeta{
Name: s.Name,
Handle: s.Slug,
}
if mReq, err = searcher.DefaultApiClient.Modules(s.NamespaceID); err != nil {
return nil, fmt.Errorf("failed to prepare module meta request: %w", err)
}
if mRes, err = searcher.DefaultApiClient.HttpClient().Do(mReq.WithContext(ctx)); err != nil {
return nil, fmt.Errorf("failed to send module request: %w", err)
}
if mRes.StatusCode != http.StatusOK {
return nil, fmt.Errorf("request resulted in an unexpected status: %s: %w", err)
}
if err = json.NewDecoder(mRes.Body).Decode(&mResponse); err != nil {
return nil, fmt.Errorf("failed to decode response: %w", err)
}
if err = mRes.Body.Close(); err != nil {
return nil, fmt.Errorf("failed to close response body: %w", err)
}
for _, m := range mResponse.Response.Set {
// Get the module handles for aggs response
mHandleMap[m.Handle] = mMeta{
Name: m.Name,
Handle: m.Slug,
}
var (
key = fmt.Sprintf("%d-%d", s.NamespaceID, m.ModuleID)
)
if len(m.Config.Discovery.Private.Result) > 0 && len(m.Config.Discovery.Private.Result[0].Fields) > 0 {
moduleMap[key] = m.Config.Discovery.Private.Result[0].Fields
}
}
}
}
// }
return conv(results, aggregation, noHits, moduleMap, nsHandleMap, mHandleMap, page)
}