Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ The authentication system provides JWT-based authentication with support for Hei
- Bypasses JWT validation for local development

**Authentication Configuration:**

- `AUTH_SOURCE`: Choose between "mock" or "jwt" (default: "jwt")
- `JWKS_URL`: JSON Web Key Set endpoint URL
- `JWT_AUDIENCE`: Intended audience for JWT tokens
Expand Down Expand Up @@ -186,9 +187,9 @@ go run cmd/main.go

**Authentication Configuration:**

- `AUTH_SOURCE`: Choose between "mock" or "jwt"
- `AUTH_SOURCE`: Choose between "mock" or "jwt"
- `JWKS_URL`: JSON Web Key Set endpoint URL
- `JWT_AUDIENCE`: Intended audience for JWT tokens
- `JWT_AUDIENCE`: Intended audience for JWT tokens
- `JWT_AUTH_DISABLED_MOCK_LOCAL_PRINCIPAL`: Mock principal for development (required when AUTH_SOURCE=mock)

**Server Configuration:**
Expand Down Expand Up @@ -328,10 +329,12 @@ export ORG_SEARCH_SOURCE=clearbit
The Clearbit integration supports the following search operations:

**Search by Company Name:**

- Searches for companies using their registered business name
- Falls back to domain-based search for additional data enrichment

**Search by Domain:**

- More accurate search method using company domain names
- Provides comprehensive company information

Expand Down Expand Up @@ -384,7 +387,7 @@ This project uses the [GOA Framework](https://goa.design/) for API generation. Y

#### Installing GOA Framework

Follow the [GOA installation guide](https://goa.design/docs/2-getting-started/1-installation/) to install GOA:
Follow the [GOA installation guide](https://goa.design/docs/1-goa/quickstart/) to install GOA:

```bash
go install goa.design/goa/v3/cmd/goa@latest
Expand Down
48 changes: 48 additions & 0 deletions cmd/service/converters.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ package service

import (
"context"
"fmt"
"log/slog"
"strings"

querysvc "github.com/linuxfoundation/lfx-v2-query-service/gen/query_svc"
"github.com/linuxfoundation/lfx-v2-query-service/internal/domain/model"
Expand All @@ -14,15 +16,45 @@ import (
"github.com/linuxfoundation/lfx-v2-query-service/pkg/paging"
)

// parseFilters parses filter strings in "field:value" format
// All fields are automatically prefixed with "data." to filter only within the data object
func parseFilters(filters []string) ([]model.FieldFilter, error) {
if len(filters) == 0 {
return nil, nil
}

parsed := make([]model.FieldFilter, 0, len(filters))
for _, filter := range filters {
parts := strings.SplitN(filter, ":", 2)
if len(parts) != 2 {
return nil, fmt.Errorf("invalid filter format '%s': expected 'field:value'", filter)
}
fieldName := strings.TrimSpace(parts[0])
// Automatically prefix with "data." to ensure filtering only on data fields
parsed = append(parsed, model.FieldFilter{
Field: "data." + fieldName,
Value: strings.TrimSpace(parts[1]),
})
}
return parsed, nil
}

// payloadToCriteria converts the generated payload to domain search criteria
func (s *querySvcsrvc) payloadToCriteria(ctx context.Context, p *querysvc.QueryResourcesPayload) (model.SearchCriteria, error) {
// Parse filters from "field:value" format
filters, err := parseFilters(p.Filters)
if err != nil {
slog.ErrorContext(ctx, "failed to parse filters", "error", err)
return model.SearchCriteria{}, wrapError(ctx, err)
}

criteria := model.SearchCriteria{
Name: p.Name,
Parent: p.Parent,
ResourceType: p.Type,
Tags: p.Tags,
TagsAll: p.TagsAll,
Filters: filters,
SortBy: p.Sort,
PageToken: p.PageToken,
PageSize: constants.DefaultPageSize,
Expand Down Expand Up @@ -90,9 +122,17 @@ func (s *querySvcsrvc) payloadToCountPublicCriteria(payload *querysvc.QueryResou
PublicOnly: true,
}

// Parse filters from "field:value" format
filters, err := parseFilters(payload.Filters)
if err != nil {
// Log error but continue - filters will be empty
slog.Error("failed to parse filters for count", "error", err)
}

// Set the criteria from the payload
criteria.Tags = payload.Tags
criteria.TagsAll = payload.TagsAll
criteria.Filters = filters
if payload.Name != nil {
criteria.Name = payload.Name
}
Expand All @@ -119,9 +159,17 @@ func (s *querySvcsrvc) payloadToCountAggregationCriteria(payload *querysvc.Query
GroupBy: "access_check_query.keyword",
}

// Parse filters from "field:value" format
filters, err := parseFilters(payload.Filters)
if err != nil {
// Log error but continue - filters will be empty
slog.Error("failed to parse filters for aggregation", "error", err)
}

// Set the criteria from the payload
criteria.Tags = payload.Tags
criteria.TagsAll = payload.TagsAll
criteria.Filters = filters
if payload.Name != nil {
criteria.Name = payload.Name
}
Expand Down
85 changes: 85 additions & 0 deletions cmd/service/converters_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -590,6 +590,91 @@ func TestDomainOrganizationSuggestionsToResponse(t *testing.T) {
}
}

func TestParseFilters(t *testing.T) {
tests := []struct {
name string
filters []string
expected []model.FieldFilter
expectedError bool
errorSubstring string
}{
{
name: "valid single filter - auto-prefixed with data",
filters: []string{"status:active"},
expected: []model.FieldFilter{{Field: "data.status", Value: "active"}},
expectedError: false,
},
{
name: "valid multiple filters - auto-prefixed with data",
filters: []string{"status:active", "priority:high"},
expected: []model.FieldFilter{
{Field: "data.status", Value: "active"},
{Field: "data.priority", Value: "high"},
},
expectedError: false,
},
{
name: "filter with spaces (trimmed and auto-prefixed)",
filters: []string{" status : active "},
expected: []model.FieldFilter{{Field: "data.status", Value: "active"}},
expectedError: false,
},
{
name: "filter with colon in value",
filters: []string{"url:https://example.com"},
expected: []model.FieldFilter{{Field: "data.url", Value: "https://example.com"}},
expectedError: false,
},
{
name: "invalid filter format (no colon)",
filters: []string{"invalid"},
expected: nil,
expectedError: true,
errorSubstring: "invalid filter format",
},
{
name: "invalid filter format (empty after colon)",
filters: []string{"status:"},
expected: []model.FieldFilter{{Field: "data.status", Value: ""}},
expectedError: false,
},
{
name: "empty filters array",
filters: []string{},
expected: nil,
expectedError: false,
},
{
name: "nil filters",
filters: nil,
expected: nil,
expectedError: false,
},
{
name: "nested field name (auto-prefixed)",
filters: []string{"project.id:123"},
expected: []model.FieldFilter{{Field: "data.project.id", Value: "123"}},
expectedError: false,
},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
result, err := parseFilters(tc.filters)

if tc.expectedError {
assert.Error(t, err)
if tc.errorSubstring != "" {
assert.Contains(t, err.Error(), tc.errorSubstring)
}
} else {
assert.NoError(t, err)
assert.Equal(t, tc.expected, result)
}
})
}
}

// Helper function to create string pointers
func stringPtr(s string) *string {
return &s
Expand Down
8 changes: 8 additions & 0 deletions design/query-svc.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ var _ = dsl.Service("query-svc", func() {
dsl.Attribute("tags_all", dsl.ArrayOf(dsl.String), "Tags to search with AND logic - matches resources that have all of these tags", func() {
dsl.Example([]string{"governance", "security"})
})
dsl.Attribute("filters", dsl.ArrayOf(dsl.String), "Direct field filters with term clauses on data fields - format: 'field:value' (e.g., 'status:active'). Fields are automatically prefixed with 'data.'", func() {
dsl.Example([]string{"status:active", "priority:high"})
})
dsl.Required("bearer_token", "version")
})

Expand All @@ -78,6 +81,7 @@ var _ = dsl.Service("query-svc", func() {
dsl.Param("type")
dsl.Param("tags")
dsl.Param("tags_all")
dsl.Param("filters")
dsl.Param("sort")
dsl.Param("page_token")
dsl.Header("bearer_token:Authorization")
Expand Down Expand Up @@ -120,6 +124,9 @@ var _ = dsl.Service("query-svc", func() {
dsl.Attribute("tags_all", dsl.ArrayOf(dsl.String), "Tags to search with AND logic - matches resources that have all of these tags", func() {
dsl.Example([]string{"governance", "security"})
})
dsl.Attribute("filters", dsl.ArrayOf(dsl.String), "Direct field filters with term clauses on data fields - format: 'field:value' (e.g., 'status:active'). Fields are automatically prefixed with 'data.'", func() {
dsl.Example([]string{"status:active", "priority:high"})
})
dsl.Required("bearer_token", "version")
})

Expand All @@ -144,6 +151,7 @@ var _ = dsl.Service("query-svc", func() {
dsl.Param("type")
dsl.Param("tags")
dsl.Param("tags_all")
dsl.Param("filters")
dsl.Header("bearer_token:Authorization")
dsl.Response(dsl.StatusOK, func() {
dsl.Header("cache_control:Cache-Control")
Expand Down
21 changes: 17 additions & 4 deletions gen/http/cli/lfx_v2_query_service/cli.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading