diff --git a/CLAUDE.md b/CLAUDE.md index 8ec7750..12fae48 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -111,6 +111,50 @@ Environment variables control implementation selection: - Integration tests can switch between real and mock implementations - Test files follow `*_test.go` pattern alongside implementation files +<<<<<<< HEAD +## API Features + +### Date Range Filtering + +The query service supports filtering resources by date ranges on fields within the `data` object. + +**Query Parameters:** + +- `date_field` (string, optional): Date field to filter on (automatically prefixed with `"data."`) +- `date_from` (string, optional): Start date (inclusive, gte operator) +- `date_to` (string, optional): End date (inclusive, lte operator) + +**Supported Date Formats:** + +1. **ISO 8601 datetime**: `2025-01-10T15:30:00Z` (time used as provided) +2. **Date-only**: `2025-01-10` (converted to start/end of day UTC) + - `date_from` → `2025-01-10T00:00:00Z` (start of day) + - `date_to` → `2025-01-10T23:59:59Z` (end of day) + +**Examples:** + +```bash +# Date range with date-only format +GET /query/resources?v=1&date_field=updated_at&date_from=2025-01-10&date_to=2025-01-28 + +# Date range with ISO 8601 format +GET /query/resources?v=1&date_field=created_at&date_from=2025-01-10T15:30:00Z&date_to=2025-01-28T18:45:00Z + +# Open-ended range (only start date) +GET /query/resources?v=1&date_field=created_at&date_from=2025-01-01 + +# Combined with other filters +GET /query/resources?v=1&type=project&tags=active&date_field=updated_at&date_from=2025-01-01&date_to=2025-03-31 +``` + +**Implementation Details:** + +- Date parsing logic: `cmd/service/converters.go` (`parseDateFilter()` function) +- Domain model: `internal/domain/model/search_criteria.go` (DateField, DateFrom, DateTo) +- OpenSearch query: `internal/infrastructure/opensearch/template.go` (range query with gte/lte) +- API design: `design/query-svc.go` (Goa design specification) +- Test coverage: `cmd/service/converters_test.go` (17 comprehensive test cases) +======= ## CEL Filter Feature The service supports Common Expression Language (CEL) filtering for post-query resource filtering. @@ -183,3 +227,4 @@ service := service.NewResourceSearch(mockSearcher, mockAccessChecker, mockFilter ### Important Limitations **Pagination**: CEL filters apply only to results from each OpenSearch page. If the target resource is not in the first page of OpenSearch results, it won't be found even if it matches the CEL filter. Always use specific primary search criteria (`type`, `name`, `parent`) to narrow OpenSearch results first. +>>>>>>> 3e45fc4d33aba656a5abe1c3df0d3f2bd0fd6be7 diff --git a/README.md b/README.md index e0564da..030a64f 100644 --- a/README.md +++ b/README.md @@ -214,9 +214,17 @@ Authorization: Bearer - `name`: Resource name or alias (supports typeahead search) - `type`: Resource type to filter by - `parent`: Parent resource for hierarchical queries +<<<<<<< HEAD +- `tags`: Array of tags to filter by (OR logic - matches resources with any of these tags) +- `tags_all`: Array of tags to filter by (AND logic - matches resources that have all of these tags) +- `date_field`: Date field to filter on (within data object) - used with date_from and/or date_to +- `date_from`: Start date (inclusive). Format: ISO 8601 datetime or date-only (YYYY-MM-DD). Date-only uses start of day UTC +- `date_to`: End date (inclusive). Format: ISO 8601 datetime or date-only (YYYY-MM-DD). Date-only uses end of day UTC +======= - `tags`: Array of tags to filter by (OR logic) - `tags_all`: Array of tags where all must match (AND logic) - `cel_filter`: CEL expression for advanced post-query filtering (see [CEL Filter](#cel-filter) section) +>>>>>>> 3e45fc4d33aba656a5abe1c3df0d3f2bd0fd6be7 - `sort`: Sort order (name_asc, name_desc, updated_asc, updated_desc) - `page_token`: Pagination token - `v`: API version (required) @@ -241,6 +249,46 @@ Authorization: Bearer } ``` +<<<<<<< HEAD +**Date Range Filtering Examples:** + +Filter resources updated between two dates (date-only format): + +```bash +GET /query/resources?v=1&date_field=updated_at&date_from=2025-01-10&date_to=2025-01-28 +Authorization: Bearer +``` + +Filter resources with precise datetime filtering (ISO 8601 format): + +```bash +GET /query/resources?v=1&date_field=created_at&date_from=2025-01-10T15:30:00Z&date_to=2025-01-28T18:45:00Z +Authorization: Bearer +``` + +Filter resources created after a specific date (open-ended range): + +```bash +GET /query/resources?v=1&date_field=created_at&date_from=2025-01-01 +Authorization: Bearer +``` + +Combine date filtering with other parameters: + +```bash +GET /query/resources?v=1&type=project&tags=active&date_field=updated_at&date_from=2025-01-01&date_to=2025-03-31 +Authorization: Bearer +``` + +**Date Format Notes:** + +- **ISO 8601 datetime format**: `2025-01-10T15:30:00Z` (time is used as provided) +- **Date-only format**: `2025-01-10` (automatically converted to start/end of day UTC) + - For `date_from`: Converts to `2025-01-10T00:00:00Z` (start of day) + - For `date_to`: Converts to `2025-01-10T23:59:59Z` (end of day) +- All dates are inclusive (uses `gte` and `lte` operators) +- The `date_field` parameter is automatically prefixed with `"data."` to scope to the resource's data object +======= #### CEL Filter The `cel_filter` query parameter enables advanced filtering of search results using Common Expression Language (CEL). CEL is a non-Turing complete expression language designed for safe, fast evaluation of expressions in performance-critical applications. @@ -353,6 +401,7 @@ Invalid CEL expressions return a 400 Bad Request with details: "error": "filter expression failed: ERROR: :1:6: Syntax error: mismatched input 'invalid' expecting {'[', '{', '(', '.', '-', '!', 'true', 'false', 'null', NUM_FLOAT, NUM_INT, NUM_UINT, STRING, BYTES, IDENTIFIER}" } ``` +>>>>>>> 3e45fc4d33aba656a5abe1c3df0d3f2bd0fd6be7 #### Organization Search API diff --git a/cmd/service/converters.go b/cmd/service/converters.go index 16f3cd6..37bb141 100644 --- a/cmd/service/converters.go +++ b/cmd/service/converters.go @@ -7,6 +7,7 @@ import ( "context" "fmt" "log/slog" + "time" "strings" querysvc "github.com/linuxfoundation/lfx-v2-query-service/gen/query_svc" @@ -16,6 +17,42 @@ import ( "github.com/linuxfoundation/lfx-v2-query-service/pkg/paging" ) +// parseDateFilter parses a date string in ISO 8601 datetime or date-only format +// and returns it normalized for OpenSearch range queries. +// Date-only format (YYYY-MM-DD) is converted to: +// - Start of day (00:00:00 UTC) for date_from +// - End of day (23:59:59 UTC) for date_to +func parseDateFilter(dateStr string, isEndDate bool) (string, error) { + if dateStr == "" { + return "", nil + } + + // Try parsing as ISO 8601 datetime first (e.g., 2025-01-10T15:30:00Z) + t, err := time.Parse(time.RFC3339, dateStr) + if err == nil { + // Already in datetime format, return as-is + return t.Format(time.RFC3339), nil + } + + // Try parsing as date-only (e.g., 2025-01-10) + t, err = time.Parse("2006-01-02", dateStr) + if err != nil { + return "", fmt.Errorf("invalid date format '%s': must be ISO 8601 datetime (2006-01-02T15:04:05Z) or date-only (2006-01-02)", dateStr) + } + + // Convert date-only to datetime + if isEndDate { + // For end dates, use end of day (23:59:59 UTC) + // Note: Using 23:59:59 instead of 23:59:59.999 for simplicity and OpenSearch compatibility + t = time.Date(t.Year(), t.Month(), t.Day(), 23, 59, 59, 0, time.UTC) + } else { + // For start dates, use start of day (00:00:00 UTC) + t = time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, time.UTC) + } + + return t.Format(time.RFC3339), nil +} + // 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) { @@ -91,6 +128,40 @@ func (s *querySvcsrvc) payloadToCriteria(ctx context.Context, p *querysvc.QueryR ) } + // Validate date filtering parameters + if (p.DateFrom != nil || p.DateTo != nil) && p.DateField == nil { + err := fmt.Errorf("date_field is required when using date_from or date_to") + slog.ErrorContext(ctx, "invalid date filter parameters", "error", err) + return criteria, wrapError(ctx, err) + } + + // Handle date filtering parameters + if p.DateField != nil { + // Auto-prefix with "data." to scope to data object + prefixedField := "data." + *p.DateField + criteria.DateField = &prefixedField + + // Parse and normalize date_from + if p.DateFrom != nil { + normalizedFrom, err := parseDateFilter(*p.DateFrom, false) + if err != nil { + slog.ErrorContext(ctx, "invalid date_from format", "error", err, "date_from", *p.DateFrom) + return criteria, wrapError(ctx, err) + } + criteria.DateFrom = &normalizedFrom + } + + // Parse and normalize date_to + if p.DateTo != nil { + normalizedTo, err := parseDateFilter(*p.DateTo, true) + if err != nil { + slog.ErrorContext(ctx, "invalid date_to format", "error", err, "date_to", *p.DateTo) + return criteria, wrapError(ctx, err) + } + criteria.DateTo = &normalizedTo + } + } + return criteria, nil } @@ -146,6 +217,36 @@ func (s *querySvcsrvc) payloadToCountPublicCriteria(payload *querysvc.QueryResou criteria.ParentRef = payload.Parent } + // Validate date filtering parameters + if (payload.DateFrom != nil || payload.DateTo != nil) && payload.DateField == nil { + return criteria, fmt.Errorf("date_field is required when using date_from or date_to") + } + + // Handle date filtering parameters + if payload.DateField != nil { + // Auto-prefix with "data." to scope to data object + prefixedField := "data." + *payload.DateField + criteria.DateField = &prefixedField + + // Parse and normalize date_from + if payload.DateFrom != nil { + normalizedFrom, err := parseDateFilter(*payload.DateFrom, false) + if err != nil { + return criteria, fmt.Errorf("invalid date_from: %w", err) + } + criteria.DateFrom = &normalizedFrom + } + + // Parse and normalize date_to + if payload.DateTo != nil { + normalizedTo, err := parseDateFilter(*payload.DateTo, true) + if err != nil { + return criteria, fmt.Errorf("invalid date_to: %w", err) + } + criteria.DateTo = &normalizedTo + } + } + return criteria, nil } @@ -182,6 +283,36 @@ func (s *querySvcsrvc) payloadToCountAggregationCriteria(payload *querysvc.Query criteria.ParentRef = payload.Parent } + // Validate date filtering parameters + if (payload.DateFrom != nil || payload.DateTo != nil) && payload.DateField == nil { + return criteria, fmt.Errorf("date_field is required when using date_from or date_to") + } + + // Handle date filtering parameters + if payload.DateField != nil { + // Auto-prefix with "data." to scope to data object + prefixedField := "data." + *payload.DateField + criteria.DateField = &prefixedField + + // Parse and normalize date_from + if payload.DateFrom != nil { + normalizedFrom, err := parseDateFilter(*payload.DateFrom, false) + if err != nil { + return criteria, fmt.Errorf("invalid date_from: %w", err) + } + criteria.DateFrom = &normalizedFrom + } + + // Parse and normalize date_to + if payload.DateTo != nil { + normalizedTo, err := parseDateFilter(*payload.DateTo, true) + if err != nil { + return criteria, fmt.Errorf("invalid date_to: %w", err) + } + criteria.DateTo = &normalizedTo + } + } + return criteria, nil } diff --git a/cmd/service/converters_test.go b/cmd/service/converters_test.go index db61be9..13f9a11 100644 --- a/cmd/service/converters_test.go +++ b/cmd/service/converters_test.go @@ -590,6 +590,103 @@ func TestDomainOrganizationSuggestionsToResponse(t *testing.T) { } } +func TestParseDateFilter(t *testing.T) { + tests := []struct { + name string + dateStr string + isEndDate bool + expected string + expectError bool + }{ + { + name: "ISO 8601 datetime format (start date)", + dateStr: "2025-01-10T15:30:00Z", + isEndDate: false, + expected: "2025-01-10T15:30:00Z", + expectError: false, + }, + { + name: "ISO 8601 datetime format (end date)", + dateStr: "2025-01-28T23:59:59Z", + isEndDate: true, + expected: "2025-01-28T23:59:59Z", + expectError: false, + }, + { + name: "date-only format converted to start of day", + dateStr: "2025-01-10", + isEndDate: false, + expected: "2025-01-10T00:00:00Z", + expectError: false, + }, + { + name: "date-only format converted to end of day", + dateStr: "2025-01-28", + isEndDate: true, + expected: "2025-01-28T23:59:59Z", + expectError: false, + }, + { + name: "empty string returns empty", + dateStr: "", + isEndDate: false, + expected: "", + expectError: false, + }, + { + name: "invalid date format", + dateStr: "01/10/2025", + isEndDate: false, + expected: "", + expectError: true, + }, + { + name: "invalid date string", + dateStr: "not-a-date", + isEndDate: false, + expected: "", + expectError: true, + }, + { + name: "partial datetime (missing timezone)", + dateStr: "2025-01-10T15:30:00", + isEndDate: false, + expected: "", + expectError: true, + }, + { + name: "date-only with different year", + dateStr: "2024-12-31", + isEndDate: true, + expected: "2024-12-31T23:59:59Z", + expectError: false, + }, + { + name: "ISO 8601 with milliseconds (truncated to seconds)", + dateStr: "2025-01-10T15:30:00.123Z", + isEndDate: false, + expected: "2025-01-10T15:30:00Z", + expectError: false, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + // Execute + result, err := parseDateFilter(tc.dateStr, tc.isEndDate) + + // Verify + if tc.expectError { + assert.Error(t, err) + assert.Contains(t, err.Error(), "invalid date format") + } else { + assert.NoError(t, err) + assert.Equal(t, tc.expected, result) + } + }) + } +} + func TestParseFilters(t *testing.T) { tests := []struct { name string @@ -633,10 +730,10 @@ func TestParseFilters(t *testing.T) { errorSubstring: "invalid filter format", }, { - name: "invalid filter format (empty after colon)", - filters: []string{"status:"}, - expected: []model.FieldFilter{{Field: "data.status", Value: ""}}, - expectedError: false, + name: "invalid filter format (empty after colon)", + filters: []string{"status:"}, + expected: []model.FieldFilter{{Field: "data.status", Value: ""}}, + expectedError: false, }, { name: "invalid filter format (empty field name)", @@ -689,6 +786,157 @@ func TestParseFilters(t *testing.T) { } } +func TestPayloadToCriteriaWithDateFilters(t *testing.T) { + // Setup service for testing + mockResourceSearcher := mock.NewMockResourceSearcher() + mockAccessChecker := mock.NewMockAccessControlChecker() + mockOrgSearcher := mock.NewMockOrganizationSearcher() + mockAuth := mock.NewMockAuthService() + service := NewQuerySvc(mockResourceSearcher, mockAccessChecker, mock.NewMockResourceFilter(), mockOrgSearcher, mockAuth) + svc := service.(*querySvcsrvc) + + tests := []struct { + name string + payload *querysvc.QueryResourcesPayload + expectError bool + checkCriteria func(*testing.T, model.SearchCriteria) + }{ + { + name: "date range with ISO 8601 format", + payload: &querysvc.QueryResourcesPayload{ + DateField: stringPtr("updated_at"), + DateFrom: stringPtr("2025-01-10T00:00:00Z"), + DateTo: stringPtr("2025-01-28T23:59:59Z"), + }, + expectError: false, + checkCriteria: func(t *testing.T, c model.SearchCriteria) { + assert.NotNil(t, c.DateField) + assert.Equal(t, "data.updated_at", *c.DateField) + assert.NotNil(t, c.DateFrom) + assert.Equal(t, "2025-01-10T00:00:00Z", *c.DateFrom) + assert.NotNil(t, c.DateTo) + assert.Equal(t, "2025-01-28T23:59:59Z", *c.DateTo) + }, + }, + { + name: "date range with date-only format", + payload: &querysvc.QueryResourcesPayload{ + DateField: stringPtr("created_at"), + DateFrom: stringPtr("2025-01-10"), + DateTo: stringPtr("2025-01-28"), + }, + expectError: false, + checkCriteria: func(t *testing.T, c model.SearchCriteria) { + assert.NotNil(t, c.DateField) + assert.Equal(t, "data.created_at", *c.DateField) + assert.NotNil(t, c.DateFrom) + assert.Equal(t, "2025-01-10T00:00:00Z", *c.DateFrom) + assert.NotNil(t, c.DateTo) + assert.Equal(t, "2025-01-28T23:59:59Z", *c.DateTo) + }, + }, + { + name: "date range with only date_from", + payload: &querysvc.QueryResourcesPayload{ + DateField: stringPtr("updated_at"), + DateFrom: stringPtr("2025-01-10"), + }, + expectError: false, + checkCriteria: func(t *testing.T, c model.SearchCriteria) { + assert.NotNil(t, c.DateField) + assert.Equal(t, "data.updated_at", *c.DateField) + assert.NotNil(t, c.DateFrom) + assert.Equal(t, "2025-01-10T00:00:00Z", *c.DateFrom) + assert.Nil(t, c.DateTo) + }, + }, + { + name: "date range with only date_to", + payload: &querysvc.QueryResourcesPayload{ + DateField: stringPtr("updated_at"), + DateTo: stringPtr("2025-01-28"), + }, + expectError: false, + checkCriteria: func(t *testing.T, c model.SearchCriteria) { + assert.NotNil(t, c.DateField) + assert.Equal(t, "data.updated_at", *c.DateField) + assert.Nil(t, c.DateFrom) + assert.NotNil(t, c.DateTo) + assert.Equal(t, "2025-01-28T23:59:59Z", *c.DateTo) + }, + }, + { + name: "invalid date_from format", + payload: &querysvc.QueryResourcesPayload{ + DateField: stringPtr("updated_at"), + DateFrom: stringPtr("invalid-date"), + }, + expectError: true, + }, + { + name: "invalid date_to format", + payload: &querysvc.QueryResourcesPayload{ + DateField: stringPtr("updated_at"), + DateTo: stringPtr("01/28/2025"), + }, + expectError: true, + }, + { + name: "no date filtering (nil date_field)", + payload: &querysvc.QueryResourcesPayload{ + Name: stringPtr("test"), + }, + expectError: false, + checkCriteria: func(t *testing.T, c model.SearchCriteria) { + assert.Nil(t, c.DateField) + assert.Nil(t, c.DateFrom) + assert.Nil(t, c.DateTo) + }, + }, + { + name: "date_from without date_field (should error)", + payload: &querysvc.QueryResourcesPayload{ + DateFrom: stringPtr("2025-01-10"), + }, + expectError: true, + }, + { + name: "date_to without date_field (should error)", + payload: &querysvc.QueryResourcesPayload{ + DateTo: stringPtr("2025-01-28"), + }, + expectError: true, + }, + { + name: "date_from and date_to without date_field (should error)", + payload: &querysvc.QueryResourcesPayload{ + DateFrom: stringPtr("2025-01-10"), + DateTo: stringPtr("2025-01-28"), + }, + expectError: true, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + ctx := context.Background() + + // Execute + result, err := svc.payloadToCriteria(ctx, tc.payload) + + // Verify + if tc.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + if tc.checkCriteria != nil { + tc.checkCriteria(t, result) + } + } + }) + } +} + // Helper function to create string pointers func stringPtr(s string) *string { return &s diff --git a/design/query-svc.go b/design/query-svc.go index f229cb8..b95a1d3 100644 --- a/design/query-svc.go +++ b/design/query-svc.go @@ -56,6 +56,15 @@ 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("date_field", dsl.String, "Date field to filter on (within data object) - used with date_from and/or date_to. Supports ISO 8601 datetime (2006-01-02T15:04:05Z) or date-only (2006-01-02, assumes UTC)", func() { + dsl.Example("updated_at") + }) + dsl.Attribute("date_from", dsl.String, "Start date (inclusive). Format: ISO 8601 datetime or date-only. Date-only uses start of day UTC. Requires date_field.", func() { + dsl.Example("2025-01-10") + }) + dsl.Attribute("date_to", dsl.String, "End date (inclusive). Format: ISO 8601 datetime or date-only. Date-only uses end of day UTC. Requires date_field.", func() { + dsl.Example("2025-01-28") + }) 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"}) }) @@ -85,6 +94,9 @@ var _ = dsl.Service("query-svc", func() { dsl.Param("type") dsl.Param("tags") dsl.Param("tags_all") + dsl.Param("date_field") + dsl.Param("date_from") + dsl.Param("date_to") dsl.Param("filters") dsl.Param("cel_filter") dsl.Param("sort") @@ -129,6 +141,15 @@ 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("date_field", dsl.String, "Date field to filter on (within data object) - used with date_from and/or date_to. Supports ISO 8601 datetime (2006-01-02T15:04:05Z) or date-only (2006-01-02, assumes UTC)", func() { + dsl.Example("updated_at") + }) + dsl.Attribute("date_from", dsl.String, "Start date (inclusive). Format: ISO 8601 datetime or date-only. Date-only uses start of day UTC. Requires date_field.", func() { + dsl.Example("2025-01-10") + }) + dsl.Attribute("date_to", dsl.String, "End date (inclusive). Format: ISO 8601 datetime or date-only. Date-only uses end of day UTC. Requires date_field.", func() { + dsl.Example("2025-01-28") + }) 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"}) }) @@ -156,6 +177,9 @@ var _ = dsl.Service("query-svc", func() { dsl.Param("type") dsl.Param("tags") dsl.Param("tags_all") + dsl.Param("date_field") + dsl.Param("date_from") + dsl.Param("date_to") dsl.Param("filters") dsl.Header("bearer_token:Authorization") dsl.Response(dsl.StatusOK, func() { diff --git a/gen/http/cli/lfx_v2_query_service/cli.go b/gen/http/cli/lfx_v2_query_service/cli.go index f32a4cd..7f34eb3 100644 --- a/gen/http/cli/lfx_v2_query_service/cli.go +++ b/gen/http/cli/lfx_v2_query_service/cli.go @@ -34,7 +34,7 @@ func UsageExamples() string { ]' --tags-all '[ "governance", "security" - ]' --filters '[ + ]' --date-field "updated_at" --date-from "2025-01-10" --date-to "2025-01-28" --filters '[ "status:active", "priority:high" ]' --cel-filter "data.slug == \"tlf\"" --sort "updated_desc" --page-token "****" --bearer-token "eyJhbGci..."` + "\n" + @@ -60,6 +60,9 @@ func ParseEndpoint( querySvcQueryResourcesTypeFlag = querySvcQueryResourcesFlags.String("type", "", "") querySvcQueryResourcesTagsFlag = querySvcQueryResourcesFlags.String("tags", "", "") querySvcQueryResourcesTagsAllFlag = querySvcQueryResourcesFlags.String("tags-all", "", "") + querySvcQueryResourcesDateFieldFlag = querySvcQueryResourcesFlags.String("date-field", "", "") + querySvcQueryResourcesDateFromFlag = querySvcQueryResourcesFlags.String("date-from", "", "") + querySvcQueryResourcesDateToFlag = querySvcQueryResourcesFlags.String("date-to", "", "") querySvcQueryResourcesFiltersFlag = querySvcQueryResourcesFlags.String("filters", "", "") querySvcQueryResourcesCelFilterFlag = querySvcQueryResourcesFlags.String("cel-filter", "", "") querySvcQueryResourcesSortFlag = querySvcQueryResourcesFlags.String("sort", "name_asc", "") @@ -73,6 +76,9 @@ func ParseEndpoint( querySvcQueryResourcesCountTypeFlag = querySvcQueryResourcesCountFlags.String("type", "", "") querySvcQueryResourcesCountTagsFlag = querySvcQueryResourcesCountFlags.String("tags", "", "") querySvcQueryResourcesCountTagsAllFlag = querySvcQueryResourcesCountFlags.String("tags-all", "", "") + querySvcQueryResourcesCountDateFieldFlag = querySvcQueryResourcesCountFlags.String("date-field", "", "") + querySvcQueryResourcesCountDateFromFlag = querySvcQueryResourcesCountFlags.String("date-from", "", "") + querySvcQueryResourcesCountDateToFlag = querySvcQueryResourcesCountFlags.String("date-to", "", "") querySvcQueryResourcesCountFiltersFlag = querySvcQueryResourcesCountFlags.String("filters", "", "") querySvcQueryResourcesCountBearerTokenFlag = querySvcQueryResourcesCountFlags.String("bearer-token", "REQUIRED", "") @@ -178,10 +184,10 @@ func ParseEndpoint( switch epn { case "query-resources": endpoint = c.QueryResources() - data, err = querysvcc.BuildQueryResourcesPayload(*querySvcQueryResourcesVersionFlag, *querySvcQueryResourcesNameFlag, *querySvcQueryResourcesParentFlag, *querySvcQueryResourcesTypeFlag, *querySvcQueryResourcesTagsFlag, *querySvcQueryResourcesTagsAllFlag, *querySvcQueryResourcesFiltersFlag, *querySvcQueryResourcesCelFilterFlag, *querySvcQueryResourcesSortFlag, *querySvcQueryResourcesPageTokenFlag, *querySvcQueryResourcesBearerTokenFlag) + data, err = querysvcc.BuildQueryResourcesPayload(*querySvcQueryResourcesVersionFlag, *querySvcQueryResourcesNameFlag, *querySvcQueryResourcesParentFlag, *querySvcQueryResourcesTypeFlag, *querySvcQueryResourcesTagsFlag, *querySvcQueryResourcesTagsAllFlag, *querySvcQueryResourcesDateFieldFlag, *querySvcQueryResourcesDateFromFlag, *querySvcQueryResourcesDateToFlag, *querySvcQueryResourcesFiltersFlag, *querySvcQueryResourcesCelFilterFlag, *querySvcQueryResourcesSortFlag, *querySvcQueryResourcesPageTokenFlag, *querySvcQueryResourcesBearerTokenFlag) case "query-resources-count": endpoint = c.QueryResourcesCount() - data, err = querysvcc.BuildQueryResourcesCountPayload(*querySvcQueryResourcesCountVersionFlag, *querySvcQueryResourcesCountNameFlag, *querySvcQueryResourcesCountParentFlag, *querySvcQueryResourcesCountTypeFlag, *querySvcQueryResourcesCountTagsFlag, *querySvcQueryResourcesCountTagsAllFlag, *querySvcQueryResourcesCountFiltersFlag, *querySvcQueryResourcesCountBearerTokenFlag) + data, err = querysvcc.BuildQueryResourcesCountPayload(*querySvcQueryResourcesCountVersionFlag, *querySvcQueryResourcesCountNameFlag, *querySvcQueryResourcesCountParentFlag, *querySvcQueryResourcesCountTypeFlag, *querySvcQueryResourcesCountTagsFlag, *querySvcQueryResourcesCountTagsAllFlag, *querySvcQueryResourcesCountDateFieldFlag, *querySvcQueryResourcesCountDateFromFlag, *querySvcQueryResourcesCountDateToFlag, *querySvcQueryResourcesCountFiltersFlag, *querySvcQueryResourcesCountBearerTokenFlag) case "query-orgs": endpoint = c.QueryOrgs() data, err = querysvcc.BuildQueryOrgsPayload(*querySvcQueryOrgsVersionFlag, *querySvcQueryOrgsNameFlag, *querySvcQueryOrgsDomainFlag, *querySvcQueryOrgsBearerTokenFlag) @@ -222,7 +228,7 @@ Additional help: `, os.Args[0]) } func querySvcQueryResourcesUsage() { - fmt.Fprintf(os.Stderr, `%[1]s [flags] query-svc query-resources -version STRING -name STRING -parent STRING -type STRING -tags JSON -tags-all JSON -filters JSON -cel-filter STRING -sort STRING -page-token STRING -bearer-token STRING + fmt.Fprintf(os.Stderr, `%[1]s [flags] query-svc query-resources -version STRING -name STRING -parent STRING -type STRING -tags JSON -tags-all JSON -date-field STRING -date-from STRING -date-to STRING -filters JSON -cel-filter STRING -sort STRING -page-token STRING -bearer-token STRING Locate resources by their type or parent, or use typeahead search to query resources by a display name or similar alias. -version STRING: @@ -231,6 +237,9 @@ Locate resources by their type or parent, or use typeahead search to query resou -type STRING: -tags JSON: -tags-all JSON: + -date-field STRING: + -date-from STRING: + -date-to STRING: -filters JSON: -cel-filter STRING: -sort STRING: @@ -244,7 +253,7 @@ Example: ]' --tags-all '[ "governance", "security" - ]' --filters '[ + ]' --date-field "updated_at" --date-from "2025-01-10" --date-to "2025-01-28" --filters '[ "status:active", "priority:high" ]' --cel-filter "data.slug == \"tlf\"" --sort "updated_desc" --page-token "****" --bearer-token "eyJhbGci..." @@ -252,7 +261,7 @@ Example: } func querySvcQueryResourcesCountUsage() { - fmt.Fprintf(os.Stderr, `%[1]s [flags] query-svc query-resources-count -version STRING -name STRING -parent STRING -type STRING -tags JSON -tags-all JSON -filters JSON -bearer-token STRING + fmt.Fprintf(os.Stderr, `%[1]s [flags] query-svc query-resources-count -version STRING -name STRING -parent STRING -type STRING -tags JSON -tags-all JSON -date-field STRING -date-from STRING -date-to STRING -filters JSON -bearer-token STRING Count matching resources by query. -version STRING: @@ -261,6 +270,9 @@ Count matching resources by query. -type STRING: -tags JSON: -tags-all JSON: + -date-field STRING: + -date-from STRING: + -date-to STRING: -filters JSON: -bearer-token STRING: @@ -271,7 +283,7 @@ Example: ]' --tags-all '[ "governance", "security" - ]' --filters '[ + ]' --date-field "updated_at" --date-from "2025-01-10" --date-to "2025-01-28" --filters '[ "status:active", "priority:high" ]' --bearer-token "eyJhbGci..." diff --git a/gen/http/openapi.json b/gen/http/openapi.json index 829f457..876ce75 100644 --- a/gen/http/openapi.json +++ b/gen/http/openapi.json @@ -1 +1 @@ -{"swagger":"2.0","info":{"title":"LFX V2 - Query Service","description":"Query indexed resources","version":"0.0.1"},"host":"localhost:80","consumes":["application/json","application/xml","application/gob"],"produces":["application/json","application/xml","application/gob"],"paths":{"/query/orgs":{"get":{"tags":["query-svc"],"summary":"query-orgs query-svc","description":"Locate a single organization by name or domain.","operationId":"query-svc#query-orgs","parameters":[{"name":"v","in":"query","description":"Version of the API","required":true,"type":"string","enum":["1"]},{"name":"name","in":"query","description":"Organization name","required":false,"type":"string","minLength":1},{"name":"domain","in":"query","description":"Organization domain or website URL","required":false,"type":"string","pattern":"^[a-zA-Z0-9][a-zA-Z0-9-_.]*[a-zA-Z0-9]*\\.[a-zA-Z]{2,}$"},{"name":"Authorization","in":"header","description":"Token","required":true,"type":"string"}],"responses":{"200":{"description":"OK response.","schema":{"$ref":"#/definitions/Organization"}},"400":{"description":"Bad Request response.","schema":{"$ref":"#/definitions/BadRequestError","required":["message"]}},"404":{"description":"Not Found response.","schema":{"$ref":"#/definitions/NotFoundError","required":["message"]}},"500":{"description":"Internal Server Error response.","schema":{"$ref":"#/definitions/InternalServerError","required":["message"]}},"503":{"description":"Service Unavailable response.","schema":{"$ref":"#/definitions/ServiceUnavailableError","required":["message"]}}},"schemes":["http"],"security":[{"jwt_header_Authorization":[]}]}},"/query/orgs/suggest":{"get":{"tags":["query-svc"],"summary":"suggest-orgs query-svc","description":"Get organization suggestions for typeahead search based on a query.","operationId":"query-svc#suggest-orgs","parameters":[{"name":"v","in":"query","description":"Version of the API","required":true,"type":"string","enum":["1"]},{"name":"query","in":"query","description":"Search query for organization suggestions","required":true,"type":"string","minLength":1},{"name":"Authorization","in":"header","description":"Token","required":true,"type":"string"}],"responses":{"200":{"description":"OK response.","schema":{"$ref":"#/definitions/QuerySvcSuggestOrgsResponseBody","required":["suggestions"]}},"400":{"description":"Bad Request response.","schema":{"$ref":"#/definitions/BadRequestError","required":["message"]}},"500":{"description":"Internal Server Error response.","schema":{"$ref":"#/definitions/InternalServerError","required":["message"]}},"503":{"description":"Service Unavailable response.","schema":{"$ref":"#/definitions/ServiceUnavailableError","required":["message"]}}},"schemes":["http"],"security":[{"jwt_header_Authorization":[]}]}},"/query/resources":{"get":{"tags":["query-svc"],"summary":"query-resources query-svc","description":"Locate resources by their type or parent, or use typeahead search to query resources by a display name or similar alias.","operationId":"query-svc#query-resources","parameters":[{"name":"v","in":"query","description":"Version of the API","required":true,"type":"string","enum":["1"]},{"name":"name","in":"query","description":"Resource name or alias; supports typeahead","required":false,"type":"string","minLength":1},{"name":"parent","in":"query","description":"Parent (for navigation; varies by object type)","required":false,"type":"string","pattern":"^[a-zA-Z]+:[a-zA-Z0-9_-]+$"},{"name":"type","in":"query","description":"Resource type to search","required":false,"type":"string"},{"name":"tags","in":"query","description":"Tags to search with OR logic - matches resources with any of these tags","required":false,"type":"array","items":{"type":"string"},"collectionFormat":"multi"},{"name":"tags_all","in":"query","description":"Tags to search with AND logic - matches resources that have all of these tags","required":false,"type":"array","items":{"type":"string"},"collectionFormat":"multi"},{"name":"filters","in":"query","description":"Direct field filters with term clauses on data fields - format: 'field:value' (e.g., 'status:active'). Fields are automatically prefixed with 'data.'","required":false,"type":"array","items":{"type":"string"},"collectionFormat":"multi"},{"name":"cel_filter","in":"query","description":"CEL expression to filter results on resource data fields. Available variables: data (map), resource_type (string), id (string)","required":false,"type":"string","maxLength":1000},{"name":"sort","in":"query","description":"Sort order for results","required":false,"type":"string","default":"name_asc","enum":["name_asc","name_desc","updated_asc","updated_desc"]},{"name":"page_token","in":"query","description":"Opaque token for pagination","required":false,"type":"string"},{"name":"Authorization","in":"header","description":"Token","required":true,"type":"string"}],"responses":{"200":{"description":"OK response.","schema":{"$ref":"#/definitions/QuerySvcQueryResourcesResponseBody","required":["resources"]},"headers":{"Cache-Control":{"description":"Cache control header","type":"string"}}},"400":{"description":"Bad Request response.","schema":{"$ref":"#/definitions/BadRequestError","required":["message"]}},"500":{"description":"Internal Server Error response.","schema":{"$ref":"#/definitions/InternalServerError","required":["message"]}},"503":{"description":"Service Unavailable response.","schema":{"$ref":"#/definitions/ServiceUnavailableError","required":["message"]}}},"schemes":["http"],"security":[{"jwt_header_Authorization":[]}]}},"/query/resources/count":{"get":{"tags":["query-svc"],"summary":"query-resources-count query-svc","description":"Count matching resources by query.","operationId":"query-svc#query-resources-count","parameters":[{"name":"v","in":"query","description":"Version of the API","required":true,"type":"string","enum":["1"]},{"name":"name","in":"query","description":"Resource name or alias; supports typeahead","required":false,"type":"string","minLength":1},{"name":"parent","in":"query","description":"Parent (for navigation; varies by object type)","required":false,"type":"string"},{"name":"type","in":"query","description":"Resource type to search","required":false,"type":"string"},{"name":"tags","in":"query","description":"Tags to search with OR logic - matches resources with any of these tags","required":false,"type":"array","items":{"type":"string"},"collectionFormat":"multi"},{"name":"tags_all","in":"query","description":"Tags to search with AND logic - matches resources that have all of these tags","required":false,"type":"array","items":{"type":"string"},"collectionFormat":"multi"},{"name":"filters","in":"query","description":"Direct field filters with term clauses on data fields - format: 'field:value' (e.g., 'status:active'). Fields are automatically prefixed with 'data.'","required":false,"type":"array","items":{"type":"string"},"collectionFormat":"multi"},{"name":"Authorization","in":"header","description":"JWT token issued by Heimdall","required":true,"type":"string"}],"responses":{"200":{"description":"OK response.","schema":{"$ref":"#/definitions/QuerySvcQueryResourcesCountResponseBody","required":["count","has_more"]},"headers":{"Cache-Control":{"description":"Cache control header","type":"string"}}},"400":{"description":"Bad Request response.","schema":{"$ref":"#/definitions/BadRequestError","required":["message"]}},"500":{"description":"Internal Server Error response.","schema":{"$ref":"#/definitions/InternalServerError","required":["message"]}},"503":{"description":"Service Unavailable response.","schema":{"$ref":"#/definitions/ServiceUnavailableError","required":["message"]}}},"schemes":["http"],"security":[{"jwt_header_Authorization":[]}]}}},"definitions":{"BadRequestError":{"title":"BadRequestError","type":"object","properties":{"message":{"type":"string","description":"Error message","example":"The request was invalid."}},"description":"Bad request","example":{"message":"The request was invalid."},"required":["message"]},"InternalServerError":{"title":"InternalServerError","type":"object","properties":{"message":{"type":"string","description":"Error message","example":"An internal server error occurred."}},"description":"Internal server error","example":{"message":"An internal server error occurred."},"required":["message"]},"NotFoundError":{"title":"NotFoundError","type":"object","properties":{"message":{"type":"string","description":"Error message","example":"The requested resource was not found."}},"description":"Not found","example":{"message":"The requested resource was not found."},"required":["message"]},"Organization":{"title":"Organization","type":"object","properties":{"domain":{"type":"string","description":"Organization domain","example":"linuxfoundation.org"},"employees":{"type":"string","description":"Employee count or range","example":"100-499"},"industry":{"type":"string","description":"Organization industry classification","example":"Non-Profit"},"name":{"type":"string","description":"Organization name","example":"Linux Foundation"},"sector":{"type":"string","description":"Business sector classification","example":"Technology"}},"example":{"domain":"linuxfoundation.org","employees":"100-499","industry":"Non-Profit","name":"Linux Foundation","sector":"Technology"}},"OrganizationSuggestion":{"title":"OrganizationSuggestion","type":"object","properties":{"domain":{"type":"string","description":"Organization domain","example":"linuxfoundation.org"},"logo":{"type":"string","description":"Organization logo URL","example":"https://example.com/logo.png"},"name":{"type":"string","description":"Organization name","example":"Linux Foundation"}},"description":"An organization suggestion for the search.","example":{"domain":"linuxfoundation.org","logo":"https://example.com/logo.png","name":"Linux Foundation"},"required":["name","domain"]},"QuerySvcQueryResourcesCountResponseBody":{"title":"QuerySvcQueryResourcesCountResponseBody","type":"object","properties":{"count":{"type":"integer","description":"Count of resources found","example":1234,"format":"int64"},"has_more":{"type":"boolean","description":"True if count is not guaranteed to be exhaustive: client should request a narrower query","example":false}},"example":{"count":1234,"has_more":false},"required":["count","has_more"]},"QuerySvcQueryResourcesResponseBody":{"title":"QuerySvcQueryResourcesResponseBody","type":"object","properties":{"page_token":{"type":"string","description":"Opaque token if more results are available","example":"****"},"resources":{"type":"array","items":{"$ref":"#/definitions/Resource"},"description":"Resources found","example":[{"data":{"id":"123","name":"My committee","description":"a committee"},"id":"123","type":"committee"},{"data":{"id":"123","name":"My committee","description":"a committee"},"id":"123","type":"committee"},{"data":{"id":"123","name":"My committee","description":"a committee"},"id":"123","type":"committee"},{"data":{"id":"123","name":"My committee","description":"a committee"},"id":"123","type":"committee"}]}},"example":{"page_token":"****","resources":[{"data":{"id":"123","name":"My committee","description":"a committee"},"id":"123","type":"committee"},{"data":{"id":"123","name":"My committee","description":"a committee"},"id":"123","type":"committee"},{"data":{"id":"123","name":"My committee","description":"a committee"},"id":"123","type":"committee"},{"data":{"id":"123","name":"My committee","description":"a committee"},"id":"123","type":"committee"}]},"required":["resources"]},"QuerySvcSuggestOrgsResponseBody":{"title":"QuerySvcSuggestOrgsResponseBody","type":"object","properties":{"suggestions":{"type":"array","items":{"$ref":"#/definitions/OrganizationSuggestion"},"description":"Organization suggestions","example":[{"domain":"linuxfoundation.org","logo":"https://example.com/logo.png","name":"Linux Foundation"},{"domain":"linuxfoundation.org","logo":"https://example.com/logo.png","name":"Linux Foundation"},{"domain":"linuxfoundation.org","logo":"https://example.com/logo.png","name":"Linux Foundation"}]}},"example":{"suggestions":[{"domain":"linuxfoundation.org","logo":"https://example.com/logo.png","name":"Linux Foundation"},{"domain":"linuxfoundation.org","logo":"https://example.com/logo.png","name":"Linux Foundation"},{"domain":"linuxfoundation.org","logo":"https://example.com/logo.png","name":"Linux Foundation"},{"domain":"linuxfoundation.org","logo":"https://example.com/logo.png","name":"Linux Foundation"}]},"required":["suggestions"]},"Resource":{"title":"Resource","type":"object","properties":{"data":{"description":"Resource data snapshot","example":{"id":"123","name":"My committee","description":"a committee"}},"id":{"type":"string","description":"Resource ID (within its resource collection)","example":"123"},"type":{"type":"string","description":"Resource type","example":"committee"}},"description":"A resource is a universal representation of an LFX API resource for indexing.","example":{"data":{"id":"123","name":"My committee","description":"a committee"},"id":"123","type":"committee"}},"ServiceUnavailableError":{"title":"ServiceUnavailableError","type":"object","properties":{"message":{"type":"string","description":"Error message","example":"The service is unavailable."}},"description":"Service unavailable","example":{"message":"The service is unavailable."},"required":["message"]}},"securityDefinitions":{"jwt_header_Authorization":{"type":"apiKey","description":"Heimdall authorization","name":"Authorization","in":"header"}}} \ No newline at end of file +{"swagger":"2.0","info":{"title":"LFX V2 - Query Service","description":"Query indexed resources","version":"0.0.1"},"host":"localhost:80","consumes":["application/json","application/xml","application/gob"],"produces":["application/json","application/xml","application/gob"],"paths":{"/query/orgs":{"get":{"tags":["query-svc"],"summary":"query-orgs query-svc","description":"Locate a single organization by name or domain.","operationId":"query-svc#query-orgs","parameters":[{"name":"v","in":"query","description":"Version of the API","required":true,"type":"string","enum":["1"]},{"name":"name","in":"query","description":"Organization name","required":false,"type":"string","minLength":1},{"name":"domain","in":"query","description":"Organization domain or website URL","required":false,"type":"string","pattern":"^[a-zA-Z0-9][a-zA-Z0-9-_.]*[a-zA-Z0-9]*\\.[a-zA-Z]{2,}$"},{"name":"Authorization","in":"header","description":"Token","required":true,"type":"string"}],"responses":{"200":{"description":"OK response.","schema":{"$ref":"#/definitions/Organization"}},"400":{"description":"Bad Request response.","schema":{"$ref":"#/definitions/BadRequestError","required":["message"]}},"404":{"description":"Not Found response.","schema":{"$ref":"#/definitions/NotFoundError","required":["message"]}},"500":{"description":"Internal Server Error response.","schema":{"$ref":"#/definitions/InternalServerError","required":["message"]}},"503":{"description":"Service Unavailable response.","schema":{"$ref":"#/definitions/ServiceUnavailableError","required":["message"]}}},"schemes":["http"],"security":[{"jwt_header_Authorization":[]}]}},"/query/orgs/suggest":{"get":{"tags":["query-svc"],"summary":"suggest-orgs query-svc","description":"Get organization suggestions for typeahead search based on a query.","operationId":"query-svc#suggest-orgs","parameters":[{"name":"v","in":"query","description":"Version of the API","required":true,"type":"string","enum":["1"]},{"name":"query","in":"query","description":"Search query for organization suggestions","required":true,"type":"string","minLength":1},{"name":"Authorization","in":"header","description":"Token","required":true,"type":"string"}],"responses":{"200":{"description":"OK response.","schema":{"$ref":"#/definitions/QuerySvcSuggestOrgsResponseBody","required":["suggestions"]}},"400":{"description":"Bad Request response.","schema":{"$ref":"#/definitions/BadRequestError","required":["message"]}},"500":{"description":"Internal Server Error response.","schema":{"$ref":"#/definitions/InternalServerError","required":["message"]}},"503":{"description":"Service Unavailable response.","schema":{"$ref":"#/definitions/ServiceUnavailableError","required":["message"]}}},"schemes":["http"],"security":[{"jwt_header_Authorization":[]}]}},"/query/resources":{"get":{"tags":["query-svc"],"summary":"query-resources query-svc","description":"Locate resources by their type or parent, or use typeahead search to query resources by a display name or similar alias.","operationId":"query-svc#query-resources","parameters":[{"name":"v","in":"query","description":"Version of the API","required":true,"type":"string","enum":["1"]},{"name":"name","in":"query","description":"Resource name or alias; supports typeahead","required":false,"type":"string","minLength":1},{"name":"parent","in":"query","description":"Parent (for navigation; varies by object type)","required":false,"type":"string","pattern":"^[a-zA-Z]+:[a-zA-Z0-9_-]+$"},{"name":"type","in":"query","description":"Resource type to search","required":false,"type":"string"},{"name":"tags","in":"query","description":"Tags to search with OR logic - matches resources with any of these tags","required":false,"type":"array","items":{"type":"string"},"collectionFormat":"multi"},{"name":"tags_all","in":"query","description":"Tags to search with AND logic - matches resources that have all of these tags","required":false,"type":"array","items":{"type":"string"},"collectionFormat":"multi"},{"name":"date_field","in":"query","description":"Date field to filter on (within data object) - used with date_from and/or date_to. Supports ISO 8601 datetime (2006-01-02T15:04:05Z) or date-only (2006-01-02, assumes UTC)","required":false,"type":"string"},{"name":"date_from","in":"query","description":"Start date (inclusive). Format: ISO 8601 datetime or date-only. Date-only uses start of day UTC. Requires date_field.","required":false,"type":"string"},{"name":"date_to","in":"query","description":"End date (inclusive). Format: ISO 8601 datetime or date-only. Date-only uses end of day UTC. Requires date_field.","required":false,"type":"string"},{"name":"filters","in":"query","description":"Direct field filters with term clauses on data fields - format: 'field:value' (e.g., 'status:active'). Fields are automatically prefixed with 'data.'","required":false,"type":"array","items":{"type":"string"},"collectionFormat":"multi"},{"name":"cel_filter","in":"query","description":"CEL expression to filter results on resource data fields. Available variables: data (map), resource_type (string), id (string)","required":false,"type":"string","maxLength":1000},{"name":"sort","in":"query","description":"Sort order for results","required":false,"type":"string","default":"name_asc","enum":["name_asc","name_desc","updated_asc","updated_desc"]},{"name":"page_token","in":"query","description":"Opaque token for pagination","required":false,"type":"string"},{"name":"Authorization","in":"header","description":"Token","required":true,"type":"string"}],"responses":{"200":{"description":"OK response.","schema":{"$ref":"#/definitions/QuerySvcQueryResourcesResponseBody","required":["resources"]},"headers":{"Cache-Control":{"description":"Cache control header","type":"string"}}},"400":{"description":"Bad Request response.","schema":{"$ref":"#/definitions/BadRequestError","required":["message"]}},"500":{"description":"Internal Server Error response.","schema":{"$ref":"#/definitions/InternalServerError","required":["message"]}},"503":{"description":"Service Unavailable response.","schema":{"$ref":"#/definitions/ServiceUnavailableError","required":["message"]}}},"schemes":["http"],"security":[{"jwt_header_Authorization":[]}]}},"/query/resources/count":{"get":{"tags":["query-svc"],"summary":"query-resources-count query-svc","description":"Count matching resources by query.","operationId":"query-svc#query-resources-count","parameters":[{"name":"v","in":"query","description":"Version of the API","required":true,"type":"string","enum":["1"]},{"name":"name","in":"query","description":"Resource name or alias; supports typeahead","required":false,"type":"string","minLength":1},{"name":"parent","in":"query","description":"Parent (for navigation; varies by object type)","required":false,"type":"string"},{"name":"type","in":"query","description":"Resource type to search","required":false,"type":"string"},{"name":"tags","in":"query","description":"Tags to search with OR logic - matches resources with any of these tags","required":false,"type":"array","items":{"type":"string"},"collectionFormat":"multi"},{"name":"tags_all","in":"query","description":"Tags to search with AND logic - matches resources that have all of these tags","required":false,"type":"array","items":{"type":"string"},"collectionFormat":"multi"},{"name":"date_field","in":"query","description":"Date field to filter on (within data object) - used with date_from and/or date_to. Supports ISO 8601 datetime (2006-01-02T15:04:05Z) or date-only (2006-01-02, assumes UTC)","required":false,"type":"string"},{"name":"date_from","in":"query","description":"Start date (inclusive). Format: ISO 8601 datetime or date-only. Date-only uses start of day UTC. Requires date_field.","required":false,"type":"string"},{"name":"date_to","in":"query","description":"End date (inclusive). Format: ISO 8601 datetime or date-only. Date-only uses end of day UTC. Requires date_field.","required":false,"type":"string"},{"name":"filters","in":"query","description":"Direct field filters with term clauses on data fields - format: 'field:value' (e.g., 'status:active'). Fields are automatically prefixed with 'data.'","required":false,"type":"array","items":{"type":"string"},"collectionFormat":"multi"},{"name":"Authorization","in":"header","description":"JWT token issued by Heimdall","required":true,"type":"string"}],"responses":{"200":{"description":"OK response.","schema":{"$ref":"#/definitions/QuerySvcQueryResourcesCountResponseBody","required":["count","has_more"]},"headers":{"Cache-Control":{"description":"Cache control header","type":"string"}}},"400":{"description":"Bad Request response.","schema":{"$ref":"#/definitions/BadRequestError","required":["message"]}},"500":{"description":"Internal Server Error response.","schema":{"$ref":"#/definitions/InternalServerError","required":["message"]}},"503":{"description":"Service Unavailable response.","schema":{"$ref":"#/definitions/ServiceUnavailableError","required":["message"]}}},"schemes":["http"],"security":[{"jwt_header_Authorization":[]}]}}},"definitions":{"BadRequestError":{"title":"BadRequestError","type":"object","properties":{"message":{"type":"string","description":"Error message","example":"The request was invalid."}},"description":"Bad request","example":{"message":"The request was invalid."},"required":["message"]},"InternalServerError":{"title":"InternalServerError","type":"object","properties":{"message":{"type":"string","description":"Error message","example":"An internal server error occurred."}},"description":"Internal server error","example":{"message":"An internal server error occurred."},"required":["message"]},"NotFoundError":{"title":"NotFoundError","type":"object","properties":{"message":{"type":"string","description":"Error message","example":"The requested resource was not found."}},"description":"Not found","example":{"message":"The requested resource was not found."},"required":["message"]},"Organization":{"title":"Organization","type":"object","properties":{"domain":{"type":"string","description":"Organization domain","example":"linuxfoundation.org"},"employees":{"type":"string","description":"Employee count or range","example":"100-499"},"industry":{"type":"string","description":"Organization industry classification","example":"Non-Profit"},"name":{"type":"string","description":"Organization name","example":"Linux Foundation"},"sector":{"type":"string","description":"Business sector classification","example":"Technology"}},"example":{"domain":"linuxfoundation.org","employees":"100-499","industry":"Non-Profit","name":"Linux Foundation","sector":"Technology"}},"OrganizationSuggestion":{"title":"OrganizationSuggestion","type":"object","properties":{"domain":{"type":"string","description":"Organization domain","example":"linuxfoundation.org"},"logo":{"type":"string","description":"Organization logo URL","example":"https://example.com/logo.png"},"name":{"type":"string","description":"Organization name","example":"Linux Foundation"}},"description":"An organization suggestion for the search.","example":{"domain":"linuxfoundation.org","logo":"https://example.com/logo.png","name":"Linux Foundation"},"required":["name","domain"]},"QuerySvcQueryResourcesCountResponseBody":{"title":"QuerySvcQueryResourcesCountResponseBody","type":"object","properties":{"count":{"type":"integer","description":"Count of resources found","example":1234,"format":"int64"},"has_more":{"type":"boolean","description":"True if count is not guaranteed to be exhaustive: client should request a narrower query","example":false}},"example":{"count":1234,"has_more":false},"required":["count","has_more"]},"QuerySvcQueryResourcesResponseBody":{"title":"QuerySvcQueryResourcesResponseBody","type":"object","properties":{"page_token":{"type":"string","description":"Opaque token if more results are available","example":"****"},"resources":{"type":"array","items":{"$ref":"#/definitions/Resource"},"description":"Resources found","example":[{"data":{"id":"123","name":"My committee","description":"a committee"},"id":"123","type":"committee"},{"data":{"id":"123","name":"My committee","description":"a committee"},"id":"123","type":"committee"},{"data":{"id":"123","name":"My committee","description":"a committee"},"id":"123","type":"committee"},{"data":{"id":"123","name":"My committee","description":"a committee"},"id":"123","type":"committee"}]}},"example":{"page_token":"****","resources":[{"data":{"id":"123","name":"My committee","description":"a committee"},"id":"123","type":"committee"},{"data":{"id":"123","name":"My committee","description":"a committee"},"id":"123","type":"committee"},{"data":{"id":"123","name":"My committee","description":"a committee"},"id":"123","type":"committee"},{"data":{"id":"123","name":"My committee","description":"a committee"},"id":"123","type":"committee"}]},"required":["resources"]},"QuerySvcSuggestOrgsResponseBody":{"title":"QuerySvcSuggestOrgsResponseBody","type":"object","properties":{"suggestions":{"type":"array","items":{"$ref":"#/definitions/OrganizationSuggestion"},"description":"Organization suggestions","example":[{"domain":"linuxfoundation.org","logo":"https://example.com/logo.png","name":"Linux Foundation"},{"domain":"linuxfoundation.org","logo":"https://example.com/logo.png","name":"Linux Foundation"},{"domain":"linuxfoundation.org","logo":"https://example.com/logo.png","name":"Linux Foundation"}]}},"example":{"suggestions":[{"domain":"linuxfoundation.org","logo":"https://example.com/logo.png","name":"Linux Foundation"},{"domain":"linuxfoundation.org","logo":"https://example.com/logo.png","name":"Linux Foundation"},{"domain":"linuxfoundation.org","logo":"https://example.com/logo.png","name":"Linux Foundation"},{"domain":"linuxfoundation.org","logo":"https://example.com/logo.png","name":"Linux Foundation"}]},"required":["suggestions"]},"Resource":{"title":"Resource","type":"object","properties":{"data":{"description":"Resource data snapshot","example":{"id":"123","name":"My committee","description":"a committee"}},"id":{"type":"string","description":"Resource ID (within its resource collection)","example":"123"},"type":{"type":"string","description":"Resource type","example":"committee"}},"description":"A resource is a universal representation of an LFX API resource for indexing.","example":{"data":{"id":"123","name":"My committee","description":"a committee"},"id":"123","type":"committee"}},"ServiceUnavailableError":{"title":"ServiceUnavailableError","type":"object","properties":{"message":{"type":"string","description":"Error message","example":"The service is unavailable."}},"description":"Service unavailable","example":{"message":"The service is unavailable."},"required":["message"]}},"securityDefinitions":{"jwt_header_Authorization":{"type":"apiKey","description":"Heimdall authorization","name":"Authorization","in":"header"}}} \ No newline at end of file diff --git a/gen/http/openapi.yaml b/gen/http/openapi.yaml index 9faf0d3..1ecf54f 100644 --- a/gen/http/openapi.yaml +++ b/gen/http/openapi.yaml @@ -181,6 +181,21 @@ paths: items: type: string collectionFormat: multi + - name: date_field + in: query + description: Date field to filter on (within data object) - used with date_from and/or date_to. Supports ISO 8601 datetime (2006-01-02T15:04:05Z) or date-only (2006-01-02, assumes UTC) + required: false + type: string + - name: date_from + in: query + description: 'Start date (inclusive). Format: ISO 8601 datetime or date-only. Date-only uses start of day UTC. Requires date_field.' + required: false + type: string + - name: date_to + in: query + description: 'End date (inclusive). Format: ISO 8601 datetime or date-only. Date-only uses end of day UTC. Requires date_field.' + required: false + type: string - name: filters in: query description: 'Direct field filters with term clauses on data fields - format: ''field:value'' (e.g., ''status:active''). Fields are automatically prefixed with ''data.''' @@ -296,6 +311,21 @@ paths: items: type: string collectionFormat: multi + - name: date_field + in: query + description: Date field to filter on (within data object) - used with date_from and/or date_to. Supports ISO 8601 datetime (2006-01-02T15:04:05Z) or date-only (2006-01-02, assumes UTC) + required: false + type: string + - name: date_from + in: query + description: 'Start date (inclusive). Format: ISO 8601 datetime or date-only. Date-only uses start of day UTC. Requires date_field.' + required: false + type: string + - name: date_to + in: query + description: 'End date (inclusive). Format: ISO 8601 datetime or date-only. Date-only uses end of day UTC. Requires date_field.' + required: false + type: string - name: filters in: query description: 'Direct field filters with term clauses on data fields - format: ''field:value'' (e.g., ''status:active''). Fields are automatically prefixed with ''data.''' diff --git a/gen/http/openapi3.json b/gen/http/openapi3.json index 71f1c72..549ddb7 100644 --- a/gen/http/openapi3.json +++ b/gen/http/openapi3.json @@ -1 +1 @@ -{"openapi":"3.0.3","info":{"title":"LFX V2 - Query Service","description":"Query indexed resources","version":"0.0.1"},"servers":[{"url":"http://localhost:80","description":"Default server for lfx-v2-query-service"}],"paths":{"/query/orgs":{"get":{"tags":["query-svc"],"summary":"query-orgs query-svc","description":"Locate a single organization by name or domain.","operationId":"query-svc#query-orgs","parameters":[{"name":"v","in":"query","description":"Version of the API","allowEmptyValue":true,"required":true,"schema":{"type":"string","description":"Version of the API","example":"1","enum":["1"]},"example":"1"},{"name":"name","in":"query","description":"Organization name","allowEmptyValue":true,"schema":{"type":"string","description":"Organization name","example":"The Linux Foundation","minLength":1},"example":"The Linux Foundation"},{"name":"domain","in":"query","description":"Organization domain or website URL","allowEmptyValue":true,"schema":{"type":"string","description":"Organization domain or website URL","example":"linuxfoundation.org","pattern":"^[a-zA-Z0-9][a-zA-Z0-9-_.]*[a-zA-Z0-9]*\\.[a-zA-Z]{2,}$"},"example":"linuxfoundation.org"}],"responses":{"200":{"description":"OK response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Organization"},"example":{"domain":"linuxfoundation.org","employees":"100-499","industry":"Non-Profit","name":"Linux Foundation","sector":"Technology"}}}},"400":{"description":"BadRequest: Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BadRequestError"},"example":{"message":"The request was invalid."}}}},"404":{"description":"NotFound: Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NotFoundError"},"example":{"message":"The requested resource was not found."}}}},"500":{"description":"InternalServerError: Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InternalServerError"},"example":{"message":"An internal server error occurred."}}}},"503":{"description":"ServiceUnavailable: Service unavailable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServiceUnavailableError"},"example":{"message":"The service is unavailable."}}}}},"security":[{"jwt_header_Authorization":[]}]}},"/query/orgs/suggest":{"get":{"tags":["query-svc"],"summary":"suggest-orgs query-svc","description":"Get organization suggestions for typeahead search based on a query.","operationId":"query-svc#suggest-orgs","parameters":[{"name":"v","in":"query","description":"Version of the API","allowEmptyValue":true,"required":true,"schema":{"type":"string","description":"Version of the API","example":"1","enum":["1"]},"example":"1"},{"name":"query","in":"query","description":"Search query for organization suggestions","allowEmptyValue":true,"required":true,"schema":{"type":"string","description":"Search query for organization suggestions","example":"linux","minLength":1},"example":"linux"}],"responses":{"200":{"description":"OK response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuggestOrgsResponseBody"},"example":{"suggestions":[{"domain":"linuxfoundation.org","logo":"https://example.com/logo.png","name":"Linux Foundation"},{"domain":"linuxfoundation.org","logo":"https://example.com/logo.png","name":"Linux Foundation"},{"domain":"linuxfoundation.org","logo":"https://example.com/logo.png","name":"Linux Foundation"}]}}}},"400":{"description":"BadRequest: Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BadRequestError"},"example":{"message":"The request was invalid."}}}},"500":{"description":"InternalServerError: Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InternalServerError"},"example":{"message":"An internal server error occurred."}}}},"503":{"description":"ServiceUnavailable: Service unavailable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServiceUnavailableError"},"example":{"message":"The service is unavailable."}}}}},"security":[{"jwt_header_Authorization":[]}]}},"/query/resources":{"get":{"tags":["query-svc"],"summary":"query-resources query-svc","description":"Locate resources by their type or parent, or use typeahead search to query resources by a display name or similar alias.","operationId":"query-svc#query-resources","parameters":[{"name":"v","in":"query","description":"Version of the API","allowEmptyValue":true,"required":true,"schema":{"type":"string","description":"Version of the API","example":"1","enum":["1"]},"example":"1"},{"name":"name","in":"query","description":"Resource name or alias; supports typeahead","allowEmptyValue":true,"schema":{"type":"string","description":"Resource name or alias; supports typeahead","example":"gov board","minLength":1},"example":"gov board"},{"name":"parent","in":"query","description":"Parent (for navigation; varies by object type)","allowEmptyValue":true,"schema":{"type":"string","description":"Parent (for navigation; varies by object type)","example":"project:123","pattern":"^[a-zA-Z]+:[a-zA-Z0-9_-]+$"},"example":"project:123"},{"name":"type","in":"query","description":"Resource type to search","allowEmptyValue":true,"schema":{"type":"string","description":"Resource type to search","example":"committee"},"example":"committee"},{"name":"tags","in":"query","description":"Tags to search with OR logic - matches resources with any of these tags","allowEmptyValue":true,"schema":{"type":"array","items":{"type":"string","example":"Animi aspernatur."},"description":"Tags to search with OR logic - matches resources with any of these tags","example":["active","public"]},"example":["active","public"]},{"name":"tags_all","in":"query","description":"Tags to search with AND logic - matches resources that have all of these tags","allowEmptyValue":true,"schema":{"type":"array","items":{"type":"string","example":"Ex itaque."},"description":"Tags to search with AND logic - matches resources that have all of these tags","example":["governance","security"]},"example":["governance","security"]},{"name":"filters","in":"query","description":"Direct field filters with term clauses on data fields - format: 'field:value' (e.g., 'status:active'). Fields are automatically prefixed with 'data.'","allowEmptyValue":true,"schema":{"type":"array","items":{"type":"string","example":"Sint commodi."},"description":"Direct field filters with term clauses on data fields - format: 'field:value' (e.g., 'status:active'). Fields are automatically prefixed with 'data.'","example":["status:active","priority:high"]},"example":["status:active","priority:high"]},{"name":"cel_filter","in":"query","description":"CEL expression to filter results on resource data fields. Available variables: data (map), resource_type (string), id (string)","allowEmptyValue":true,"schema":{"type":"string","description":"CEL expression to filter results on resource data fields. Available variables: data (map), resource_type (string), id (string)","example":"data.slug == \"tlf\"","maxLength":1000},"example":"data.slug == \"tlf\""},{"name":"sort","in":"query","description":"Sort order for results","allowEmptyValue":true,"schema":{"type":"string","description":"Sort order for results","default":"name_asc","example":"updated_desc","enum":["name_asc","name_desc","updated_asc","updated_desc"]},"example":"updated_desc"},{"name":"page_token","in":"query","description":"Opaque token for pagination","allowEmptyValue":true,"schema":{"type":"string","description":"Opaque token for pagination","example":"****"},"example":"****"}],"responses":{"200":{"description":"OK response.","headers":{"Cache-Control":{"description":"Cache control header","schema":{"type":"string","description":"Cache control header","example":"public, max-age=300"},"example":"public, max-age=300"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/QueryResourcesResponseBody"},"example":{"page_token":"****","resources":[{"data":{"id":"123","name":"My committee","description":"a committee"},"id":"123","type":"committee"},{"data":{"id":"123","name":"My committee","description":"a committee"},"id":"123","type":"committee"},{"data":{"id":"123","name":"My committee","description":"a committee"},"id":"123","type":"committee"}]}}}},"400":{"description":"BadRequest: Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BadRequestError"},"example":{"message":"The request was invalid."}}}},"500":{"description":"InternalServerError: Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InternalServerError"},"example":{"message":"An internal server error occurred."}}}},"503":{"description":"ServiceUnavailable: Service unavailable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServiceUnavailableError"},"example":{"message":"The service is unavailable."}}}}},"security":[{"jwt_header_Authorization":[]}]}},"/query/resources/count":{"get":{"tags":["query-svc"],"summary":"query-resources-count query-svc","description":"Count matching resources by query.","operationId":"query-svc#query-resources-count","parameters":[{"name":"v","in":"query","description":"Version of the API","allowEmptyValue":true,"required":true,"schema":{"type":"string","description":"Version of the API","example":"1","enum":["1"]},"example":"1"},{"name":"name","in":"query","description":"Resource name or alias; supports typeahead","allowEmptyValue":true,"schema":{"type":"string","description":"Resource name or alias; supports typeahead","example":"gov board","minLength":1},"example":"gov board"},{"name":"parent","in":"query","description":"Parent (for navigation; varies by object type)","allowEmptyValue":true,"schema":{"type":"string","description":"Parent (for navigation; varies by object type)","example":"project:123"},"example":"project:123"},{"name":"type","in":"query","description":"Resource type to search","allowEmptyValue":true,"schema":{"type":"string","description":"Resource type to search","example":"committee"},"example":"committee"},{"name":"tags","in":"query","description":"Tags to search with OR logic - matches resources with any of these tags","allowEmptyValue":true,"schema":{"type":"array","items":{"type":"string","example":"Labore aperiam libero ipsam et ullam."},"description":"Tags to search with OR logic - matches resources with any of these tags","example":["active","public"]},"example":["active","public"]},{"name":"tags_all","in":"query","description":"Tags to search with AND logic - matches resources that have all of these tags","allowEmptyValue":true,"schema":{"type":"array","items":{"type":"string","example":"Doloribus voluptatem ipsa optio."},"description":"Tags to search with AND logic - matches resources that have all of these tags","example":["governance","security"]},"example":["governance","security"]},{"name":"filters","in":"query","description":"Direct field filters with term clauses on data fields - format: 'field:value' (e.g., 'status:active'). Fields are automatically prefixed with 'data.'","allowEmptyValue":true,"schema":{"type":"array","items":{"type":"string","example":"Nobis corporis aperiam consectetur temporibus voluptatem vitae."},"description":"Direct field filters with term clauses on data fields - format: 'field:value' (e.g., 'status:active'). Fields are automatically prefixed with 'data.'","example":["status:active","priority:high"]},"example":["status:active","priority:high"]}],"responses":{"200":{"description":"OK response.","headers":{"Cache-Control":{"description":"Cache control header","schema":{"type":"string","description":"Cache control header","example":"public, max-age=300"},"example":"public, max-age=300"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/QueryResourcesCountResponseBody"},"example":{"count":1234,"has_more":false}}}},"400":{"description":"BadRequest: Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BadRequestError"},"example":{"message":"The request was invalid."}}}},"500":{"description":"InternalServerError: Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InternalServerError"},"example":{"message":"An internal server error occurred."}}}},"503":{"description":"ServiceUnavailable: Service unavailable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServiceUnavailableError"},"example":{"message":"The service is unavailable."}}}}},"security":[{"jwt_header_Authorization":[]}]}}},"components":{"schemas":{"BadRequestError":{"type":"object","properties":{"message":{"type":"string","description":"Error message","example":"The request was invalid."}},"example":{"message":"The request was invalid."},"required":["message"]},"InternalServerError":{"type":"object","properties":{"message":{"type":"string","description":"Error message","example":"An internal server error occurred."}},"example":{"message":"An internal server error occurred."},"required":["message"]},"NotFoundError":{"type":"object","properties":{"message":{"type":"string","description":"Error message","example":"The requested resource was not found."}},"example":{"message":"The requested resource was not found."},"required":["message"]},"Organization":{"type":"object","properties":{"domain":{"type":"string","description":"Organization domain","example":"linuxfoundation.org"},"employees":{"type":"string","description":"Employee count or range","example":"100-499"},"industry":{"type":"string","description":"Organization industry classification","example":"Non-Profit"},"name":{"type":"string","description":"Organization name","example":"Linux Foundation"},"sector":{"type":"string","description":"Business sector classification","example":"Technology"}},"description":"An organization is a universal representation of an LFX API organization.","example":{"domain":"linuxfoundation.org","employees":"100-499","industry":"Non-Profit","name":"Linux Foundation","sector":"Technology"}},"OrganizationSuggestion":{"type":"object","properties":{"domain":{"type":"string","description":"Organization domain","example":"linuxfoundation.org"},"logo":{"type":"string","description":"Organization logo URL","example":"https://example.com/logo.png"},"name":{"type":"string","description":"Organization name","example":"Linux Foundation"}},"description":"An organization suggestion for the search.","example":{"domain":"linuxfoundation.org","logo":"https://example.com/logo.png","name":"Linux Foundation"},"required":["name","domain"]},"QueryResourcesCountResponseBody":{"type":"object","properties":{"count":{"type":"integer","description":"Count of resources found","example":1234,"format":"int64"},"has_more":{"type":"boolean","description":"True if count is not guaranteed to be exhaustive: client should request a narrower query","example":false}},"example":{"count":1234,"has_more":false},"required":["count","has_more"]},"QueryResourcesResponseBody":{"type":"object","properties":{"page_token":{"type":"string","description":"Opaque token if more results are available","example":"****"},"resources":{"type":"array","items":{"$ref":"#/components/schemas/Resource"},"description":"Resources found","example":[{"data":{"id":"123","name":"My committee","description":"a committee"},"id":"123","type":"committee"},{"data":{"id":"123","name":"My committee","description":"a committee"},"id":"123","type":"committee"},{"data":{"id":"123","name":"My committee","description":"a committee"},"id":"123","type":"committee"},{"data":{"id":"123","name":"My committee","description":"a committee"},"id":"123","type":"committee"}]}},"example":{"page_token":"****","resources":[{"data":{"id":"123","name":"My committee","description":"a committee"},"id":"123","type":"committee"},{"data":{"id":"123","name":"My committee","description":"a committee"},"id":"123","type":"committee"},{"data":{"id":"123","name":"My committee","description":"a committee"},"id":"123","type":"committee"},{"data":{"id":"123","name":"My committee","description":"a committee"},"id":"123","type":"committee"}]},"required":["resources"]},"Resource":{"type":"object","properties":{"data":{"description":"Resource data snapshot","example":{"id":"123","name":"My committee","description":"a committee"}},"id":{"type":"string","description":"Resource ID (within its resource collection)","example":"123"},"type":{"type":"string","description":"Resource type","example":"committee"}},"description":"A resource is a universal representation of an LFX API resource for indexing.","example":{"data":{"id":"123","name":"My committee","description":"a committee"},"id":"123","type":"committee"}},"ServiceUnavailableError":{"type":"object","properties":{"message":{"type":"string","description":"Error message","example":"The service is unavailable."}},"example":{"message":"The service is unavailable."},"required":["message"]},"Sortable":{"type":"object","properties":{"page_token":{"type":"string","description":"Opaque token for pagination","example":"****"},"sort":{"type":"string","description":"Sort order for results","default":"name_asc","example":"updated_desc","enum":["name_asc","name_desc","updated_asc","updated_desc"]}},"example":{"page_token":"****","sort":"updated_desc"}},"SuggestOrgsResponseBody":{"type":"object","properties":{"suggestions":{"type":"array","items":{"$ref":"#/components/schemas/OrganizationSuggestion"},"description":"Organization suggestions","example":[{"domain":"linuxfoundation.org","logo":"https://example.com/logo.png","name":"Linux Foundation"},{"domain":"linuxfoundation.org","logo":"https://example.com/logo.png","name":"Linux Foundation"}]}},"example":{"suggestions":[{"domain":"linuxfoundation.org","logo":"https://example.com/logo.png","name":"Linux Foundation"},{"domain":"linuxfoundation.org","logo":"https://example.com/logo.png","name":"Linux Foundation"},{"domain":"linuxfoundation.org","logo":"https://example.com/logo.png","name":"Linux Foundation"}]},"required":["suggestions"]}},"securitySchemes":{"jwt_header_Authorization":{"type":"http","description":"Heimdall authorization","scheme":"bearer"}}},"tags":[{"name":"query-svc","description":"The query service provides resource and user queries."}]} \ No newline at end of file +{"openapi":"3.0.3","info":{"title":"LFX V2 - Query Service","description":"Query indexed resources","version":"0.0.1"},"servers":[{"url":"http://localhost:80","description":"Default server for lfx-v2-query-service"}],"paths":{"/query/orgs":{"get":{"tags":["query-svc"],"summary":"query-orgs query-svc","description":"Locate a single organization by name or domain.","operationId":"query-svc#query-orgs","parameters":[{"name":"v","in":"query","description":"Version of the API","allowEmptyValue":true,"required":true,"schema":{"type":"string","description":"Version of the API","example":"1","enum":["1"]},"example":"1"},{"name":"name","in":"query","description":"Organization name","allowEmptyValue":true,"schema":{"type":"string","description":"Organization name","example":"The Linux Foundation","minLength":1},"example":"The Linux Foundation"},{"name":"domain","in":"query","description":"Organization domain or website URL","allowEmptyValue":true,"schema":{"type":"string","description":"Organization domain or website URL","example":"linuxfoundation.org","pattern":"^[a-zA-Z0-9][a-zA-Z0-9-_.]*[a-zA-Z0-9]*\\.[a-zA-Z]{2,}$"},"example":"linuxfoundation.org"}],"responses":{"200":{"description":"OK response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Organization"},"example":{"domain":"linuxfoundation.org","employees":"100-499","industry":"Non-Profit","name":"Linux Foundation","sector":"Technology"}}}},"400":{"description":"BadRequest: Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BadRequestError"},"example":{"message":"The request was invalid."}}}},"404":{"description":"NotFound: Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NotFoundError"},"example":{"message":"The requested resource was not found."}}}},"500":{"description":"InternalServerError: Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InternalServerError"},"example":{"message":"An internal server error occurred."}}}},"503":{"description":"ServiceUnavailable: Service unavailable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServiceUnavailableError"},"example":{"message":"The service is unavailable."}}}}},"security":[{"jwt_header_Authorization":[]}]}},"/query/orgs/suggest":{"get":{"tags":["query-svc"],"summary":"suggest-orgs query-svc","description":"Get organization suggestions for typeahead search based on a query.","operationId":"query-svc#suggest-orgs","parameters":[{"name":"v","in":"query","description":"Version of the API","allowEmptyValue":true,"required":true,"schema":{"type":"string","description":"Version of the API","example":"1","enum":["1"]},"example":"1"},{"name":"query","in":"query","description":"Search query for organization suggestions","allowEmptyValue":true,"required":true,"schema":{"type":"string","description":"Search query for organization suggestions","example":"linux","minLength":1},"example":"linux"}],"responses":{"200":{"description":"OK response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuggestOrgsResponseBody"},"example":{"suggestions":[{"domain":"linuxfoundation.org","logo":"https://example.com/logo.png","name":"Linux Foundation"},{"domain":"linuxfoundation.org","logo":"https://example.com/logo.png","name":"Linux Foundation"},{"domain":"linuxfoundation.org","logo":"https://example.com/logo.png","name":"Linux Foundation"}]}}}},"400":{"description":"BadRequest: Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BadRequestError"},"example":{"message":"The request was invalid."}}}},"500":{"description":"InternalServerError: Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InternalServerError"},"example":{"message":"An internal server error occurred."}}}},"503":{"description":"ServiceUnavailable: Service unavailable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServiceUnavailableError"},"example":{"message":"The service is unavailable."}}}}},"security":[{"jwt_header_Authorization":[]}]}},"/query/resources":{"get":{"tags":["query-svc"],"summary":"query-resources query-svc","description":"Locate resources by their type or parent, or use typeahead search to query resources by a display name or similar alias.","operationId":"query-svc#query-resources","parameters":[{"name":"v","in":"query","description":"Version of the API","allowEmptyValue":true,"required":true,"schema":{"type":"string","description":"Version of the API","example":"1","enum":["1"]},"example":"1"},{"name":"name","in":"query","description":"Resource name or alias; supports typeahead","allowEmptyValue":true,"schema":{"type":"string","description":"Resource name or alias; supports typeahead","example":"gov board","minLength":1},"example":"gov board"},{"name":"parent","in":"query","description":"Parent (for navigation; varies by object type)","allowEmptyValue":true,"schema":{"type":"string","description":"Parent (for navigation; varies by object type)","example":"project:123","pattern":"^[a-zA-Z]+:[a-zA-Z0-9_-]+$"},"example":"project:123"},{"name":"type","in":"query","description":"Resource type to search","allowEmptyValue":true,"schema":{"type":"string","description":"Resource type to search","example":"committee"},"example":"committee"},{"name":"tags","in":"query","description":"Tags to search with OR logic - matches resources with any of these tags","allowEmptyValue":true,"schema":{"type":"array","items":{"type":"string","example":"Animi aspernatur."},"description":"Tags to search with OR logic - matches resources with any of these tags","example":["active","public"]},"example":["active","public"]},{"name":"tags_all","in":"query","description":"Tags to search with AND logic - matches resources that have all of these tags","allowEmptyValue":true,"schema":{"type":"array","items":{"type":"string","example":"Ex itaque."},"description":"Tags to search with AND logic - matches resources that have all of these tags","example":["governance","security"]},"example":["governance","security"]},{"name":"date_field","in":"query","description":"Date field to filter on (within data object) - used with date_from and/or date_to. Supports ISO 8601 datetime (2006-01-02T15:04:05Z) or date-only (2006-01-02, assumes UTC)","allowEmptyValue":true,"schema":{"type":"string","description":"Date field to filter on (within data object) - used with date_from and/or date_to. Supports ISO 8601 datetime (2006-01-02T15:04:05Z) or date-only (2006-01-02, assumes UTC)","example":"updated_at"},"example":"updated_at"},{"name":"date_from","in":"query","description":"Start date (inclusive). Format: ISO 8601 datetime or date-only. Date-only uses start of day UTC. Requires date_field.","allowEmptyValue":true,"schema":{"type":"string","description":"Start date (inclusive). Format: ISO 8601 datetime or date-only. Date-only uses start of day UTC. Requires date_field.","example":"2025-01-10"},"example":"2025-01-10"},{"name":"date_to","in":"query","description":"End date (inclusive). Format: ISO 8601 datetime or date-only. Date-only uses end of day UTC. Requires date_field.","allowEmptyValue":true,"schema":{"type":"string","description":"End date (inclusive). Format: ISO 8601 datetime or date-only. Date-only uses end of day UTC. Requires date_field.","example":"2025-01-28"},"example":"2025-01-28"},{"name":"filters","in":"query","description":"Direct field filters with term clauses on data fields - format: 'field:value' (e.g., 'status:active'). Fields are automatically prefixed with 'data.'","allowEmptyValue":true,"schema":{"type":"array","items":{"type":"string","example":"Sint commodi."},"description":"Direct field filters with term clauses on data fields - format: 'field:value' (e.g., 'status:active'). Fields are automatically prefixed with 'data.'","example":["status:active","priority:high"]},"example":["status:active","priority:high"]},{"name":"cel_filter","in":"query","description":"CEL expression to filter results on resource data fields. Available variables: data (map), resource_type (string), id (string)","allowEmptyValue":true,"schema":{"type":"string","description":"CEL expression to filter results on resource data fields. Available variables: data (map), resource_type (string), id (string)","example":"data.slug == \"tlf\"","maxLength":1000},"example":"data.slug == \"tlf\""},{"name":"sort","in":"query","description":"Sort order for results","allowEmptyValue":true,"schema":{"type":"string","description":"Sort order for results","default":"name_asc","example":"updated_desc","enum":["name_asc","name_desc","updated_asc","updated_desc"]},"example":"updated_desc"},{"name":"page_token","in":"query","description":"Opaque token for pagination","allowEmptyValue":true,"schema":{"type":"string","description":"Opaque token for pagination","example":"****"},"example":"****"}],"responses":{"200":{"description":"OK response.","headers":{"Cache-Control":{"description":"Cache control header","schema":{"type":"string","description":"Cache control header","example":"public, max-age=300"},"example":"public, max-age=300"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/QueryResourcesResponseBody"},"example":{"page_token":"****","resources":[{"data":{"id":"123","name":"My committee","description":"a committee"},"id":"123","type":"committee"},{"data":{"id":"123","name":"My committee","description":"a committee"},"id":"123","type":"committee"},{"data":{"id":"123","name":"My committee","description":"a committee"},"id":"123","type":"committee"}]}}}},"400":{"description":"BadRequest: Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BadRequestError"},"example":{"message":"The request was invalid."}}}},"500":{"description":"InternalServerError: Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InternalServerError"},"example":{"message":"An internal server error occurred."}}}},"503":{"description":"ServiceUnavailable: Service unavailable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServiceUnavailableError"},"example":{"message":"The service is unavailable."}}}}},"security":[{"jwt_header_Authorization":[]}]}},"/query/resources/count":{"get":{"tags":["query-svc"],"summary":"query-resources-count query-svc","description":"Count matching resources by query.","operationId":"query-svc#query-resources-count","parameters":[{"name":"v","in":"query","description":"Version of the API","allowEmptyValue":true,"required":true,"schema":{"type":"string","description":"Version of the API","example":"1","enum":["1"]},"example":"1"},{"name":"name","in":"query","description":"Resource name or alias; supports typeahead","allowEmptyValue":true,"schema":{"type":"string","description":"Resource name or alias; supports typeahead","example":"gov board","minLength":1},"example":"gov board"},{"name":"parent","in":"query","description":"Parent (for navigation; varies by object type)","allowEmptyValue":true,"schema":{"type":"string","description":"Parent (for navigation; varies by object type)","example":"project:123"},"example":"project:123"},{"name":"type","in":"query","description":"Resource type to search","allowEmptyValue":true,"schema":{"type":"string","description":"Resource type to search","example":"committee"},"example":"committee"},{"name":"tags","in":"query","description":"Tags to search with OR logic - matches resources with any of these tags","allowEmptyValue":true,"schema":{"type":"array","items":{"type":"string","example":"Labore aperiam libero ipsam et ullam."},"description":"Tags to search with OR logic - matches resources with any of these tags","example":["active","public"]},"example":["active","public"]},{"name":"tags_all","in":"query","description":"Tags to search with AND logic - matches resources that have all of these tags","allowEmptyValue":true,"schema":{"type":"array","items":{"type":"string","example":"Doloribus voluptatem ipsa optio."},"description":"Tags to search with AND logic - matches resources that have all of these tags","example":["governance","security"]},"example":["governance","security"]},{"name":"date_field","in":"query","description":"Date field to filter on (within data object) - used with date_from and/or date_to. Supports ISO 8601 datetime (2006-01-02T15:04:05Z) or date-only (2006-01-02, assumes UTC)","allowEmptyValue":true,"schema":{"type":"string","description":"Date field to filter on (within data object) - used with date_from and/or date_to. Supports ISO 8601 datetime (2006-01-02T15:04:05Z) or date-only (2006-01-02, assumes UTC)","example":"updated_at"},"example":"updated_at"},{"name":"date_from","in":"query","description":"Start date (inclusive). Format: ISO 8601 datetime or date-only. Date-only uses start of day UTC. Requires date_field.","allowEmptyValue":true,"schema":{"type":"string","description":"Start date (inclusive). Format: ISO 8601 datetime or date-only. Date-only uses start of day UTC. Requires date_field.","example":"2025-01-10"},"example":"2025-01-10"},{"name":"date_to","in":"query","description":"End date (inclusive). Format: ISO 8601 datetime or date-only. Date-only uses end of day UTC. Requires date_field.","allowEmptyValue":true,"schema":{"type":"string","description":"End date (inclusive). Format: ISO 8601 datetime or date-only. Date-only uses end of day UTC. Requires date_field.","example":"2025-01-28"},"example":"2025-01-28"},{"name":"filters","in":"query","description":"Direct field filters with term clauses on data fields - format: 'field:value' (e.g., 'status:active'). Fields are automatically prefixed with 'data.'","allowEmptyValue":true,"schema":{"type":"array","items":{"type":"string","example":"Nobis corporis aperiam consectetur temporibus voluptatem vitae."},"description":"Direct field filters with term clauses on data fields - format: 'field:value' (e.g., 'status:active'). Fields are automatically prefixed with 'data.'","example":["status:active","priority:high"]},"example":["status:active","priority:high"]}],"responses":{"200":{"description":"OK response.","headers":{"Cache-Control":{"description":"Cache control header","schema":{"type":"string","description":"Cache control header","example":"public, max-age=300"},"example":"public, max-age=300"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/QueryResourcesCountResponseBody"},"example":{"count":1234,"has_more":false}}}},"400":{"description":"BadRequest: Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BadRequestError"},"example":{"message":"The request was invalid."}}}},"500":{"description":"InternalServerError: Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InternalServerError"},"example":{"message":"An internal server error occurred."}}}},"503":{"description":"ServiceUnavailable: Service unavailable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServiceUnavailableError"},"example":{"message":"The service is unavailable."}}}}},"security":[{"jwt_header_Authorization":[]}]}}},"components":{"schemas":{"BadRequestError":{"type":"object","properties":{"message":{"type":"string","description":"Error message","example":"The request was invalid."}},"example":{"message":"The request was invalid."},"required":["message"]},"InternalServerError":{"type":"object","properties":{"message":{"type":"string","description":"Error message","example":"An internal server error occurred."}},"example":{"message":"An internal server error occurred."},"required":["message"]},"NotFoundError":{"type":"object","properties":{"message":{"type":"string","description":"Error message","example":"The requested resource was not found."}},"example":{"message":"The requested resource was not found."},"required":["message"]},"Organization":{"type":"object","properties":{"domain":{"type":"string","description":"Organization domain","example":"linuxfoundation.org"},"employees":{"type":"string","description":"Employee count or range","example":"100-499"},"industry":{"type":"string","description":"Organization industry classification","example":"Non-Profit"},"name":{"type":"string","description":"Organization name","example":"Linux Foundation"},"sector":{"type":"string","description":"Business sector classification","example":"Technology"}},"description":"An organization is a universal representation of an LFX API organization.","example":{"domain":"linuxfoundation.org","employees":"100-499","industry":"Non-Profit","name":"Linux Foundation","sector":"Technology"}},"OrganizationSuggestion":{"type":"object","properties":{"domain":{"type":"string","description":"Organization domain","example":"linuxfoundation.org"},"logo":{"type":"string","description":"Organization logo URL","example":"https://example.com/logo.png"},"name":{"type":"string","description":"Organization name","example":"Linux Foundation"}},"description":"An organization suggestion for the search.","example":{"domain":"linuxfoundation.org","logo":"https://example.com/logo.png","name":"Linux Foundation"},"required":["name","domain"]},"QueryResourcesCountResponseBody":{"type":"object","properties":{"count":{"type":"integer","description":"Count of resources found","example":1234,"format":"int64"},"has_more":{"type":"boolean","description":"True if count is not guaranteed to be exhaustive: client should request a narrower query","example":false}},"example":{"count":1234,"has_more":false},"required":["count","has_more"]},"QueryResourcesResponseBody":{"type":"object","properties":{"page_token":{"type":"string","description":"Opaque token if more results are available","example":"****"},"resources":{"type":"array","items":{"$ref":"#/components/schemas/Resource"},"description":"Resources found","example":[{"data":{"id":"123","name":"My committee","description":"a committee"},"id":"123","type":"committee"},{"data":{"id":"123","name":"My committee","description":"a committee"},"id":"123","type":"committee"},{"data":{"id":"123","name":"My committee","description":"a committee"},"id":"123","type":"committee"},{"data":{"id":"123","name":"My committee","description":"a committee"},"id":"123","type":"committee"}]}},"example":{"page_token":"****","resources":[{"data":{"id":"123","name":"My committee","description":"a committee"},"id":"123","type":"committee"},{"data":{"id":"123","name":"My committee","description":"a committee"},"id":"123","type":"committee"},{"data":{"id":"123","name":"My committee","description":"a committee"},"id":"123","type":"committee"},{"data":{"id":"123","name":"My committee","description":"a committee"},"id":"123","type":"committee"}]},"required":["resources"]},"Resource":{"type":"object","properties":{"data":{"description":"Resource data snapshot","example":{"id":"123","name":"My committee","description":"a committee"}},"id":{"type":"string","description":"Resource ID (within its resource collection)","example":"123"},"type":{"type":"string","description":"Resource type","example":"committee"}},"description":"A resource is a universal representation of an LFX API resource for indexing.","example":{"data":{"id":"123","name":"My committee","description":"a committee"},"id":"123","type":"committee"}},"ServiceUnavailableError":{"type":"object","properties":{"message":{"type":"string","description":"Error message","example":"The service is unavailable."}},"example":{"message":"The service is unavailable."},"required":["message"]},"Sortable":{"type":"object","properties":{"page_token":{"type":"string","description":"Opaque token for pagination","example":"****"},"sort":{"type":"string","description":"Sort order for results","default":"name_asc","example":"updated_desc","enum":["name_asc","name_desc","updated_asc","updated_desc"]}},"example":{"page_token":"****","sort":"updated_desc"}},"SuggestOrgsResponseBody":{"type":"object","properties":{"suggestions":{"type":"array","items":{"$ref":"#/components/schemas/OrganizationSuggestion"},"description":"Organization suggestions","example":[{"domain":"linuxfoundation.org","logo":"https://example.com/logo.png","name":"Linux Foundation"},{"domain":"linuxfoundation.org","logo":"https://example.com/logo.png","name":"Linux Foundation"}]}},"example":{"suggestions":[{"domain":"linuxfoundation.org","logo":"https://example.com/logo.png","name":"Linux Foundation"},{"domain":"linuxfoundation.org","logo":"https://example.com/logo.png","name":"Linux Foundation"},{"domain":"linuxfoundation.org","logo":"https://example.com/logo.png","name":"Linux Foundation"}]},"required":["suggestions"]}},"securitySchemes":{"jwt_header_Authorization":{"type":"http","description":"Heimdall authorization","scheme":"bearer"}}},"tags":[{"name":"query-svc","description":"The query service provides resource and user queries."}]} \ No newline at end of file diff --git a/gen/http/openapi3.yaml b/gen/http/openapi3.yaml index 22e9082..ed3182e 100644 --- a/gen/http/openapi3.yaml +++ b/gen/http/openapi3.yaml @@ -250,6 +250,33 @@ paths: example: - governance - security + - name: date_field + in: query + description: Date field to filter on (within data object) - used with date_from and/or date_to. Supports ISO 8601 datetime (2006-01-02T15:04:05Z) or date-only (2006-01-02, assumes UTC) + allowEmptyValue: true + schema: + type: string + description: Date field to filter on (within data object) - used with date_from and/or date_to. Supports ISO 8601 datetime (2006-01-02T15:04:05Z) or date-only (2006-01-02, assumes UTC) + example: updated_at + example: updated_at + - name: date_from + in: query + description: 'Start date (inclusive). Format: ISO 8601 datetime or date-only. Date-only uses start of day UTC. Requires date_field.' + allowEmptyValue: true + schema: + type: string + description: 'Start date (inclusive). Format: ISO 8601 datetime or date-only. Date-only uses start of day UTC. Requires date_field.' + example: "2025-01-10" + example: "2025-01-10" + - name: date_to + in: query + description: 'End date (inclusive). Format: ISO 8601 datetime or date-only. Date-only uses end of day UTC. Requires date_field.' + allowEmptyValue: true + schema: + type: string + description: 'End date (inclusive). Format: ISO 8601 datetime or date-only. Date-only uses end of day UTC. Requires date_field.' + example: "2025-01-28" + example: "2025-01-28" - name: filters in: query description: 'Direct field filters with term clauses on data fields - format: ''field:value'' (e.g., ''status:active''). Fields are automatically prefixed with ''data.''' @@ -442,6 +469,33 @@ paths: example: - governance - security + - name: date_field + in: query + description: Date field to filter on (within data object) - used with date_from and/or date_to. Supports ISO 8601 datetime (2006-01-02T15:04:05Z) or date-only (2006-01-02, assumes UTC) + allowEmptyValue: true + schema: + type: string + description: Date field to filter on (within data object) - used with date_from and/or date_to. Supports ISO 8601 datetime (2006-01-02T15:04:05Z) or date-only (2006-01-02, assumes UTC) + example: updated_at + example: updated_at + - name: date_from + in: query + description: 'Start date (inclusive). Format: ISO 8601 datetime or date-only. Date-only uses start of day UTC. Requires date_field.' + allowEmptyValue: true + schema: + type: string + description: 'Start date (inclusive). Format: ISO 8601 datetime or date-only. Date-only uses start of day UTC. Requires date_field.' + example: "2025-01-10" + example: "2025-01-10" + - name: date_to + in: query + description: 'End date (inclusive). Format: ISO 8601 datetime or date-only. Date-only uses end of day UTC. Requires date_field.' + allowEmptyValue: true + schema: + type: string + description: 'End date (inclusive). Format: ISO 8601 datetime or date-only. Date-only uses end of day UTC. Requires date_field.' + example: "2025-01-28" + example: "2025-01-28" - name: filters in: query description: 'Direct field filters with term clauses on data fields - format: ''field:value'' (e.g., ''status:active''). Fields are automatically prefixed with ''data.''' diff --git a/gen/http/query_svc/client/cli.go b/gen/http/query_svc/client/cli.go index 3cd78e5..64ce35a 100644 --- a/gen/http/query_svc/client/cli.go +++ b/gen/http/query_svc/client/cli.go @@ -18,7 +18,7 @@ import ( // BuildQueryResourcesPayload builds the payload for the query-svc // query-resources endpoint from CLI flags. -func BuildQueryResourcesPayload(querySvcQueryResourcesVersion string, querySvcQueryResourcesName string, querySvcQueryResourcesParent string, querySvcQueryResourcesType string, querySvcQueryResourcesTags string, querySvcQueryResourcesTagsAll string, querySvcQueryResourcesFilters string, querySvcQueryResourcesCelFilter string, querySvcQueryResourcesSort string, querySvcQueryResourcesPageToken string, querySvcQueryResourcesBearerToken string) (*querysvc.QueryResourcesPayload, error) { +func BuildQueryResourcesPayload(querySvcQueryResourcesVersion string, querySvcQueryResourcesName string, querySvcQueryResourcesParent string, querySvcQueryResourcesType string, querySvcQueryResourcesTags string, querySvcQueryResourcesTagsAll string, querySvcQueryResourcesDateField string, querySvcQueryResourcesDateFrom string, querySvcQueryResourcesDateTo string, querySvcQueryResourcesFilters string, querySvcQueryResourcesCelFilter string, querySvcQueryResourcesSort string, querySvcQueryResourcesPageToken string, querySvcQueryResourcesBearerToken string) (*querysvc.QueryResourcesPayload, error) { var err error var version string { @@ -76,6 +76,24 @@ func BuildQueryResourcesPayload(querySvcQueryResourcesVersion string, querySvcQu } } } + var dateField *string + { + if querySvcQueryResourcesDateField != "" { + dateField = &querySvcQueryResourcesDateField + } + } + var dateFrom *string + { + if querySvcQueryResourcesDateFrom != "" { + dateFrom = &querySvcQueryResourcesDateFrom + } + } + var dateTo *string + { + if querySvcQueryResourcesDateTo != "" { + dateTo = &querySvcQueryResourcesDateTo + } + } var filters []string { if querySvcQueryResourcesFilters != "" { @@ -126,6 +144,9 @@ func BuildQueryResourcesPayload(querySvcQueryResourcesVersion string, querySvcQu v.Type = type_ v.Tags = tags v.TagsAll = tagsAll + v.DateField = dateField + v.DateFrom = dateFrom + v.DateTo = dateTo v.Filters = filters v.CelFilter = celFilter v.Sort = sort @@ -137,7 +158,7 @@ func BuildQueryResourcesPayload(querySvcQueryResourcesVersion string, querySvcQu // BuildQueryResourcesCountPayload builds the payload for the query-svc // query-resources-count endpoint from CLI flags. -func BuildQueryResourcesCountPayload(querySvcQueryResourcesCountVersion string, querySvcQueryResourcesCountName string, querySvcQueryResourcesCountParent string, querySvcQueryResourcesCountType string, querySvcQueryResourcesCountTags string, querySvcQueryResourcesCountTagsAll string, querySvcQueryResourcesCountFilters string, querySvcQueryResourcesCountBearerToken string) (*querysvc.QueryResourcesCountPayload, error) { +func BuildQueryResourcesCountPayload(querySvcQueryResourcesCountVersion string, querySvcQueryResourcesCountName string, querySvcQueryResourcesCountParent string, querySvcQueryResourcesCountType string, querySvcQueryResourcesCountTags string, querySvcQueryResourcesCountTagsAll string, querySvcQueryResourcesCountDateField string, querySvcQueryResourcesCountDateFrom string, querySvcQueryResourcesCountDateTo string, querySvcQueryResourcesCountFilters string, querySvcQueryResourcesCountBearerToken string) (*querysvc.QueryResourcesCountPayload, error) { var err error var version string { @@ -191,6 +212,24 @@ func BuildQueryResourcesCountPayload(querySvcQueryResourcesCountVersion string, } } } + var dateField *string + { + if querySvcQueryResourcesCountDateField != "" { + dateField = &querySvcQueryResourcesCountDateField + } + } + var dateFrom *string + { + if querySvcQueryResourcesCountDateFrom != "" { + dateFrom = &querySvcQueryResourcesCountDateFrom + } + } + var dateTo *string + { + if querySvcQueryResourcesCountDateTo != "" { + dateTo = &querySvcQueryResourcesCountDateTo + } + } var filters []string { if querySvcQueryResourcesCountFilters != "" { @@ -211,6 +250,9 @@ func BuildQueryResourcesCountPayload(querySvcQueryResourcesCountVersion string, v.Type = type_ v.Tags = tags v.TagsAll = tagsAll + v.DateField = dateField + v.DateFrom = dateFrom + v.DateTo = dateTo v.Filters = filters v.BearerToken = bearerToken diff --git a/gen/http/query_svc/client/encode_decode.go b/gen/http/query_svc/client/encode_decode.go index 10013fc..12c998a 100644 --- a/gen/http/query_svc/client/encode_decode.go +++ b/gen/http/query_svc/client/encode_decode.go @@ -67,6 +67,15 @@ func EncodeQueryResourcesRequest(encoder func(*http.Request) goahttp.Encoder) fu for _, value := range p.TagsAll { values.Add("tags_all", value) } + if p.DateField != nil { + values.Add("date_field", *p.DateField) + } + if p.DateFrom != nil { + values.Add("date_from", *p.DateFrom) + } + if p.DateTo != nil { + values.Add("date_to", *p.DateTo) + } for _, value := range p.Filters { values.Add("filters", value) } @@ -225,6 +234,15 @@ func EncodeQueryResourcesCountRequest(encoder func(*http.Request) goahttp.Encode for _, value := range p.TagsAll { values.Add("tags_all", value) } + if p.DateField != nil { + values.Add("date_field", *p.DateField) + } + if p.DateFrom != nil { + values.Add("date_from", *p.DateFrom) + } + if p.DateTo != nil { + values.Add("date_to", *p.DateTo) + } for _, value := range p.Filters { values.Add("filters", value) } diff --git a/gen/http/query_svc/server/encode_decode.go b/gen/http/query_svc/server/encode_decode.go index a16f1ce..5b6f712 100644 --- a/gen/http/query_svc/server/encode_decode.go +++ b/gen/http/query_svc/server/encode_decode.go @@ -45,6 +45,9 @@ func DecodeQueryResourcesRequest(mux goahttp.Muxer, decoder func(*http.Request) type_ *string tags []string tagsAll []string + dateField *string + dateFrom *string + dateTo *string filters []string celFilter *string sort string @@ -82,6 +85,18 @@ func DecodeQueryResourcesRequest(mux goahttp.Muxer, decoder func(*http.Request) } tags = qp["tags"] tagsAll = qp["tags_all"] + dateFieldRaw := qp.Get("date_field") + if dateFieldRaw != "" { + dateField = &dateFieldRaw + } + dateFromRaw := qp.Get("date_from") + if dateFromRaw != "" { + dateFrom = &dateFromRaw + } + dateToRaw := qp.Get("date_to") + if dateToRaw != "" { + dateTo = &dateToRaw + } filters = qp["filters"] celFilterRaw := qp.Get("cel_filter") if celFilterRaw != "" { @@ -112,7 +127,7 @@ func DecodeQueryResourcesRequest(mux goahttp.Muxer, decoder func(*http.Request) if err != nil { return nil, err } - payload := NewQueryResourcesPayload(version, name, parent, type_, tags, tagsAll, filters, celFilter, sort, pageToken, bearerToken) + payload := NewQueryResourcesPayload(version, name, parent, type_, tags, tagsAll, dateField, dateFrom, dateTo, filters, celFilter, sort, pageToken, bearerToken) if strings.Contains(payload.BearerToken, " ") { // Remove authorization scheme prefix (e.g. "Bearer") cred := strings.SplitN(payload.BearerToken, " ", 2)[1] @@ -204,6 +219,9 @@ func DecodeQueryResourcesCountRequest(mux goahttp.Muxer, decoder func(*http.Requ type_ *string tags []string tagsAll []string + dateField *string + dateFrom *string + dateTo *string filters []string bearerToken string err error @@ -235,6 +253,18 @@ func DecodeQueryResourcesCountRequest(mux goahttp.Muxer, decoder func(*http.Requ } tags = qp["tags"] tagsAll = qp["tags_all"] + dateFieldRaw := qp.Get("date_field") + if dateFieldRaw != "" { + dateField = &dateFieldRaw + } + dateFromRaw := qp.Get("date_from") + if dateFromRaw != "" { + dateFrom = &dateFromRaw + } + dateToRaw := qp.Get("date_to") + if dateToRaw != "" { + dateTo = &dateToRaw + } filters = qp["filters"] bearerToken = r.Header.Get("Authorization") if bearerToken == "" { @@ -243,7 +273,7 @@ func DecodeQueryResourcesCountRequest(mux goahttp.Muxer, decoder func(*http.Requ if err != nil { return nil, err } - payload := NewQueryResourcesCountPayload(version, name, parent, type_, tags, tagsAll, filters, bearerToken) + payload := NewQueryResourcesCountPayload(version, name, parent, type_, tags, tagsAll, dateField, dateFrom, dateTo, filters, bearerToken) if strings.Contains(payload.BearerToken, " ") { // Remove authorization scheme prefix (e.g. "Bearer") cred := strings.SplitN(payload.BearerToken, " ", 2)[1] diff --git a/gen/http/query_svc/server/types.go b/gen/http/query_svc/server/types.go index 9f8a7c8..8354043 100644 --- a/gen/http/query_svc/server/types.go +++ b/gen/http/query_svc/server/types.go @@ -385,7 +385,7 @@ func NewReadyzNotReadyResponseBody(res *goa.ServiceError) *ReadyzNotReadyRespons // NewQueryResourcesPayload builds a query-svc service query-resources endpoint // payload. -func NewQueryResourcesPayload(version string, name *string, parent *string, type_ *string, tags []string, tagsAll []string, filters []string, celFilter *string, sort string, pageToken *string, bearerToken string) *querysvc.QueryResourcesPayload { +func NewQueryResourcesPayload(version string, name *string, parent *string, type_ *string, tags []string, tagsAll []string, dateField *string, dateFrom *string, dateTo *string, filters []string, celFilter *string, sort string, pageToken *string, bearerToken string) *querysvc.QueryResourcesPayload { v := &querysvc.QueryResourcesPayload{} v.Version = version v.Name = name @@ -393,6 +393,9 @@ func NewQueryResourcesPayload(version string, name *string, parent *string, type v.Type = type_ v.Tags = tags v.TagsAll = tagsAll + v.DateField = dateField + v.DateFrom = dateFrom + v.DateTo = dateTo v.Filters = filters v.CelFilter = celFilter v.Sort = sort @@ -404,7 +407,7 @@ func NewQueryResourcesPayload(version string, name *string, parent *string, type // NewQueryResourcesCountPayload builds a query-svc service // query-resources-count endpoint payload. -func NewQueryResourcesCountPayload(version string, name *string, parent *string, type_ *string, tags []string, tagsAll []string, filters []string, bearerToken string) *querysvc.QueryResourcesCountPayload { +func NewQueryResourcesCountPayload(version string, name *string, parent *string, type_ *string, tags []string, tagsAll []string, dateField *string, dateFrom *string, dateTo *string, filters []string, bearerToken string) *querysvc.QueryResourcesCountPayload { v := &querysvc.QueryResourcesCountPayload{} v.Version = version v.Name = name @@ -412,6 +415,9 @@ func NewQueryResourcesCountPayload(version string, name *string, parent *string, v.Type = type_ v.Tags = tags v.TagsAll = tagsAll + v.DateField = dateField + v.DateFrom = dateFrom + v.DateTo = dateTo v.Filters = filters v.BearerToken = bearerToken diff --git a/gen/query_svc/service.go b/gen/query_svc/service.go index 2b65e84..5816264 100644 --- a/gen/query_svc/service.go +++ b/gen/query_svc/service.go @@ -122,6 +122,16 @@ type QueryResourcesCountPayload struct { Tags []string // Tags to search with AND logic - matches resources that have all of these tags TagsAll []string + // Date field to filter on (within data object) - used with date_from and/or + // date_to. Supports ISO 8601 datetime (2006-01-02T15:04:05Z) or date-only + // (2006-01-02, assumes UTC) + DateField *string + // Start date (inclusive). Format: ISO 8601 datetime or date-only. Date-only + // uses start of day UTC. Requires date_field. + DateFrom *string + // End date (inclusive). Format: ISO 8601 datetime or date-only. Date-only uses + // end of day UTC. Requires date_field. + DateTo *string // Direct field filters with term clauses on data fields - format: // 'field:value' (e.g., 'status:active'). Fields are automatically prefixed // with 'data.' @@ -157,6 +167,16 @@ type QueryResourcesPayload struct { Tags []string // Tags to search with AND logic - matches resources that have all of these tags TagsAll []string + // Date field to filter on (within data object) - used with date_from and/or + // date_to. Supports ISO 8601 datetime (2006-01-02T15:04:05Z) or date-only + // (2006-01-02, assumes UTC) + DateField *string + // Start date (inclusive). Format: ISO 8601 datetime or date-only. Date-only + // uses start of day UTC. Requires date_field. + DateFrom *string + // End date (inclusive). Format: ISO 8601 datetime or date-only. Date-only uses + // end of day UTC. Requires date_field. + DateTo *string // Direct field filters with term clauses on data fields - format: // 'field:value' (e.g., 'status:active'). Fields are automatically prefixed // with 'data.' diff --git a/internal/domain/model/search_criteria.go b/internal/domain/model/search_criteria.go index a35911c..a46e656 100644 --- a/internal/domain/model/search_criteria.go +++ b/internal/domain/model/search_criteria.go @@ -47,6 +47,12 @@ type SearchCriteria struct { GroupBy string // GroupBySize indicates the size of the group by GroupBySize int + // DateField is the field to filter by date range (auto-prefixed with "data.") + DateField *string + // DateFrom is the start date for range filter (inclusive, ISO 8601 or date-only) + DateFrom *string + // DateTo is the end date for range filter (inclusive, ISO 8601 or date-only) + DateTo *string } // SearchResult contains the results of a resource search diff --git a/internal/infrastructure/opensearch/template.go b/internal/infrastructure/opensearch/template.go index c07a79f..9829d4a 100644 --- a/internal/infrastructure/opensearch/template.go +++ b/internal/infrastructure/opensearch/template.go @@ -63,6 +63,21 @@ const queryResourceSource = `{ } {{- end }} {{- end }} + {{- if and .DateField (or .DateFrom .DateTo) }}, + { + "range": { + {{ .DateField | quote }}: { + {{- if .DateFrom }} + "gte": {{ .DateFrom | quote }} + {{- end }} + {{- if and .DateFrom .DateTo }},{{- end }} + {{- if .DateTo }} + "lte": {{ .DateTo | quote }} + {{- end }} + } + } + } + {{- end }} {{- if .Filters }} {{- range .Filters }}, {