From 21c3598f73922104af55609205241da14aab8470 Mon Sep 17 00:00:00 2001 From: Aditya <60684641+0x0elliot@users.noreply.github.com> Date: Fri, 26 Jun 2026 16:11:09 +0530 Subject: [PATCH] feat: add prefix search for datastore keys via list_cache ?search param --- db-connector.go | 67 +++++++++++++++++++++++++++++++++++++++++++++++++ shared.go | 14 +++++++++++ 2 files changed, 81 insertions(+) diff --git a/db-connector.go b/db-connector.go index a790db56..24dad359 100755 --- a/db-connector.go +++ b/db-connector.go @@ -17336,6 +17336,73 @@ func GetAllCacheKeys(ctx context.Context, orgId string, category string, max int return cacheKeys, cursor, nil } +// GetCacheKeysByPrefix returns datastore keys within a category whose Key starts +// with the given prefix (case-insensitive). +// +// org_cache is a write-heavy kind, so we deliberately avoid adding a composite +// index just to serve occasional admin searches - that would tax every write +// for a rare read. Instead we page through the category (reusing GetAllCacheKeys, +// which is cached) and filter by prefix in Go, bounded by scanCap. Matching the +// Key field in Go also lets us be case-insensitive, which a Datastore range +// filter could not do. +func GetCacheKeysByPrefix(ctx context.Context, orgId string, category string, prefix string, max int, inputcursor string) ([]CacheKeyData, string, error) { + if os.Getenv("SHUFFLE_SWARM_CONFIG") == "run" || project.Environment == "worker" { + return []CacheKeyData{}, "", errors.New("Not available in worker mode") + } + + if strings.ToLower(category) == "default" { + category = "" + } + + if max > 1000 { + max = 1000 + } + if max <= 0 { + max = 50 + } + + category = strings.ReplaceAll(strings.ToLower(category), " ", "_") + lowerPrefix := strings.ToLower(prefix) + + matched := []CacheKeyData{} + scanCap := 5000 + scanned := 0 + cursor := "" + + for scanned < scanCap { + batch, newCursor, err := GetAllCacheKeys(ctx, orgId, category, 1000, cursor) + if err != nil { + return matched, "", err + } + + if len(batch) == 0 { + break + } + + for _, k := range batch { + scanned += 1 + if strings.HasPrefix(strings.ToLower(k.Key), lowerPrefix) { + matched = append(matched, k) + if len(matched) >= max { + return matched, "", nil + } + } + } + + if newCursor == "" || newCursor == cursor { + break + } + + cursor = newCursor + } + + if scanned >= scanCap { + log.Printf("[WARNING] Prefix search hit scan cap (%d) for category '%s'. Results may be incomplete for very large categories.", scanCap, category) + } + + return matched, "", nil +} + func GetAllDeals(ctx context.Context, orgId string) ([]ResellerDeal, error) { nameKey := "reseller_deal" cacheKey := fmt.Sprintf("%s_%s", nameKey, orgId) diff --git a/shared.go b/shared.go index 87e3a8e3..c285c97b 100644 --- a/shared.go +++ b/shared.go @@ -20991,6 +20991,20 @@ func HandleListCacheKeys(resp http.ResponseWriter, request *http.Request) { keys = []CacheKeyData{ *cacheItem, } + } else if searchList, searchOk := request.URL.Query()["search"]; searchOk && len(searchList) > 0 && searchList[0] != "" { + // Prefix search within a category (e.g. DataGrid "starts with"). + // Require a real category so the scan stays bounded - never an org-wide scan. + if len(category) == 0 || category == "default" { + log.Printf("[WARNING] Prefix search attempted without a category. Returning 400.") + resp.WriteHeader(400) + resp.Write([]byte(`{"success": false, "reason": "A category is required to search keys"}`)) + return + } + + keys, newCursor, err = GetCacheKeysByPrefix(ctx, org.Id, category, searchList[0], maxAmount, cursor) + if err != nil { + isSuccess = false + } } else { keys, newCursor, err = GetAllCacheKeys(ctx, org.Id, category, maxAmount, cursor) if err != nil {