diff --git a/CLAUDE.md b/CLAUDE.md index 12fae48..620a2e8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -111,9 +111,36 @@ 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 +### Page Size + +The `query-resources` endpoint supports a `page_size` query parameter to control how many documents are returned per page. + +**Query Parameter:** + +- `page_size` (int, optional): Number of results per page (min: 1, max: 1000, default: 50) + +**Examples:** + +```bash +# Custom page size +GET /query/resources?v=1&type=project&page_size=20 + +# Combined with other filters +GET /query/resources?v=1&type=project&tags=active&date_field=updated_at&date_from=2025-01-01&page_size=100 +``` + +**Interaction with post-query filters:** `cel_filter` and access control checks are applied after OpenSearch returns results, so a page may return fewer than `page_size` results. + +**Implementation Details:** + +- Goa design: `design/types.go` (`Sortable` type), `design/query-svc.go` (HTTP param) +- Converter: `cmd/service/converters.go` (`payloadToCriteria()` passes `p.PageSize` to criteria) +- Domain model: `internal/domain/model/search_criteria.go` (`PageSize` field) +- OpenSearch pagination: `internal/infrastructure/opensearch/client.go` (page token generated when `len(hits) == pageSize`) +- Constants: `pkg/constants/query.go` (`DefaultPageSize`, `MaxPageSize`) + ### Date Range Filtering The query service supports filtering resources by date ranges on fields within the `data` object. @@ -154,17 +181,13 @@ GET /query/resources?v=1&type=project&tags=active&date_field=updated_at&date_fro - 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. +### CEL Filter -### Overview +The service supports Common Expression Language (CEL) filtering for post-query resource filtering. CEL filtering allows API consumers to filter resources on arbitrary data fields using a safe, non-Turing complete expression language. The filter is applied after the OpenSearch query but before access control checks. -### Implementation Details - **Location**: `internal/infrastructure/filter/cel_filter.go` **Key Components**: @@ -178,7 +201,7 @@ CEL filtering allows API consumers to filter resources on arbitrary data fields - Filters resources before access control checks - Reduces number of access control checks needed -### Available Variables in CEL Expressions +**Available Variables in CEL Expressions:** - `data` (map): Resource data object - `resource_type` (string): Resource type @@ -186,29 +209,13 @@ CEL filtering allows API consumers to filter resources on arbitrary data fields Note: `type` is a reserved word in CEL, so we use `resource_type` instead. -### Example Usage +**Example Usage:** -```go -// API call +```bash GET /query/resources?type=project&cel_filter=data.slug == "tlf" - -// Expression is evaluated against each resource after OpenSearch query -// Only matching resources proceed to access control checks -``` - -### Adding CEL Filter Tests - -When writing tests that involve resource search: - -```go -// Use MockResourceFilter for testing -mockFilter := mock.NewMockResourceFilter() - -// Pass to service constructor -service := service.NewResourceSearch(mockSearcher, mockAccessChecker, mockFilter) ``` -### Common CEL Operations +**Common CEL Operations:** - Equality: `data.status == "active"` - Comparison: `data.priority > 5` @@ -217,14 +224,13 @@ service := service.NewResourceSearch(mockSearcher, mockAccessChecker, mockFilter - List membership: `data.category in ["security", "networking"]` - Field existence: `has(data.archived)` -### Performance Considerations +**Performance Considerations:** - Compiled CEL programs are cached (100 max entries, 5-minute TTL) - Each resource evaluation has 100ms timeout - Post-query filtering means pagination may return fewer results than page size - For best performance, use specific OpenSearch criteria first, then CEL for refinement -### Important Limitations +**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 +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. diff --git a/charts/lfx-v2-query-service/Chart.yaml b/charts/lfx-v2-query-service/Chart.yaml index 6b83303..4721d00 100644 --- a/charts/lfx-v2-query-service/Chart.yaml +++ b/charts/lfx-v2-query-service/Chart.yaml @@ -5,5 +5,5 @@ apiVersion: v2 name: lfx-v2-query-service description: LFX Platform V2 Query Service chart type: application -version: 0.4.10 +version: 0.4.11 appVersion: "latest" diff --git a/cmd/service/converters.go b/cmd/service/converters.go index 37bb141..04afab6 100644 --- a/cmd/service/converters.go +++ b/cmd/service/converters.go @@ -98,7 +98,7 @@ func (s *querySvcsrvc) payloadToCriteria(ctx context.Context, p *querysvc.QueryR CelFilter: p.CelFilter, SortBy: p.Sort, PageToken: p.PageToken, - PageSize: constants.DefaultPageSize, + PageSize: p.PageSize, } switch p.Sort { case "name_asc": diff --git a/cmd/service/converters_test.go b/cmd/service/converters_test.go index 13f9a11..42caa16 100644 --- a/cmd/service/converters_test.go +++ b/cmd/service/converters_test.go @@ -35,9 +35,10 @@ func TestPayloadToCriteria(t *testing.T) { { name: "basic payload conversion", payload: &querysvc.QueryResourcesPayload{ - Name: stringPtr("test-project"), - Type: stringPtr("project"), - Tags: []string{"active", "governance"}, + Name: stringPtr("test-project"), + Type: stringPtr("project"), + Tags: []string{"active", "governance"}, + PageSize: constants.DefaultPageSize, }, expectedCriteria: model.SearchCriteria{ Name: stringPtr("test-project"), @@ -50,8 +51,9 @@ func TestPayloadToCriteria(t *testing.T) { { name: "payload with parent", payload: &querysvc.QueryResourcesPayload{ - Parent: stringPtr("parent-id"), - Name: stringPtr("child-resource"), + Parent: stringPtr("parent-id"), + Name: stringPtr("child-resource"), + PageSize: constants.DefaultPageSize, }, expectedCriteria: model.SearchCriteria{ Name: stringPtr("child-resource"), @@ -63,8 +65,9 @@ func TestPayloadToCriteria(t *testing.T) { { name: "payload with sorting - name_asc", payload: &querysvc.QueryResourcesPayload{ - Name: stringPtr("test"), - Sort: "name_asc", + Name: stringPtr("test"), + Sort: "name_asc", + PageSize: constants.DefaultPageSize, }, expectedCriteria: model.SearchCriteria{ Name: stringPtr("test"), @@ -77,8 +80,9 @@ func TestPayloadToCriteria(t *testing.T) { { name: "payload with sorting - name_desc", payload: &querysvc.QueryResourcesPayload{ - Name: stringPtr("test"), - Sort: "name_desc", + Name: stringPtr("test"), + Sort: "name_desc", + PageSize: constants.DefaultPageSize, }, expectedCriteria: model.SearchCriteria{ Name: stringPtr("test"), @@ -91,8 +95,9 @@ func TestPayloadToCriteria(t *testing.T) { { name: "payload with sorting - updated_asc", payload: &querysvc.QueryResourcesPayload{ - Name: stringPtr("test"), - Sort: "updated_asc", + Name: stringPtr("test"), + Sort: "updated_asc", + PageSize: constants.DefaultPageSize, }, expectedCriteria: model.SearchCriteria{ Name: stringPtr("test"), @@ -105,8 +110,9 @@ func TestPayloadToCriteria(t *testing.T) { { name: "payload with sorting - updated_desc", payload: &querysvc.QueryResourcesPayload{ - Name: stringPtr("test"), - Sort: "updated_desc", + Name: stringPtr("test"), + Sort: "updated_desc", + PageSize: constants.DefaultPageSize, }, expectedCriteria: model.SearchCriteria{ Name: stringPtr("test"), @@ -116,18 +122,33 @@ func TestPayloadToCriteria(t *testing.T) { }, expectedError: false, }, + { + name: "payload with explicit page_size", + payload: &querysvc.QueryResourcesPayload{ + Name: stringPtr("test"), + PageSize: 20, + }, + expectedCriteria: model.SearchCriteria{ + Name: stringPtr("test"), + PageSize: 20, + }, + expectedError: false, + }, { name: "payload with invalid page token", payload: &querysvc.QueryResourcesPayload{ Name: stringPtr("test"), PageToken: stringPtr("invalid-token"), + PageSize: constants.DefaultPageSize, }, expectedCriteria: model.SearchCriteria{}, // Will be empty due to error expectedError: true, }, { - name: "empty payload", - payload: &querysvc.QueryResourcesPayload{}, + name: "empty payload", + payload: &querysvc.QueryResourcesPayload{ + PageSize: constants.DefaultPageSize, + }, expectedCriteria: model.SearchCriteria{ PageSize: constants.DefaultPageSize, }, diff --git a/design/query-svc.go b/design/query-svc.go index b95a1d3..5b049f6 100644 --- a/design/query-svc.go +++ b/design/query-svc.go @@ -101,6 +101,7 @@ var _ = dsl.Service("query-svc", func() { dsl.Param("cel_filter") dsl.Param("sort") dsl.Param("page_token") + dsl.Param("page_size") dsl.Header("bearer_token:Authorization") dsl.Response(dsl.StatusOK, func() { dsl.Header("cache_control:Cache-Control") diff --git a/design/types.go b/design/types.go index dc73ceb..74ae892 100644 --- a/design/types.go +++ b/design/types.go @@ -28,6 +28,12 @@ var Sortable = dsl.Type("Sortable", func() { dsl.Attribute("page_token", dsl.String, "Opaque token for pagination", func() { dsl.Example("****") }) + dsl.Attribute("page_size", dsl.Int, "Number of results per page", func() { + dsl.Minimum(1) + dsl.Maximum(1000) + dsl.Default(50) + dsl.Example(20) + }) }) var Resource = dsl.Type("Resource", func() { diff --git a/gen/http/cli/lfx_v2_query_service/cli.go b/gen/http/cli/lfx_v2_query_service/cli.go index 7f34eb3..826552c 100644 --- a/gen/http/cli/lfx_v2_query_service/cli.go +++ b/gen/http/cli/lfx_v2_query_service/cli.go @@ -37,7 +37,7 @@ func UsageExamples() string { ]' --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" + + ]' --cel-filter "data.slug == \"tlf\"" --sort "updated_desc" --page-token "****" --page-size 20 --bearer-token "eyJhbGci..."` + "\n" + "" } @@ -67,6 +67,7 @@ func ParseEndpoint( querySvcQueryResourcesCelFilterFlag = querySvcQueryResourcesFlags.String("cel-filter", "", "") querySvcQueryResourcesSortFlag = querySvcQueryResourcesFlags.String("sort", "name_asc", "") querySvcQueryResourcesPageTokenFlag = querySvcQueryResourcesFlags.String("page-token", "", "") + querySvcQueryResourcesPageSizeFlag = querySvcQueryResourcesFlags.String("page-size", "50", "") querySvcQueryResourcesBearerTokenFlag = querySvcQueryResourcesFlags.String("bearer-token", "REQUIRED", "") querySvcQueryResourcesCountFlags = flag.NewFlagSet("query-resources-count", flag.ExitOnError) @@ -184,7 +185,7 @@ func ParseEndpoint( switch epn { case "query-resources": endpoint = c.QueryResources() - data, err = querysvcc.BuildQueryResourcesPayload(*querySvcQueryResourcesVersionFlag, *querySvcQueryResourcesNameFlag, *querySvcQueryResourcesParentFlag, *querySvcQueryResourcesTypeFlag, *querySvcQueryResourcesTagsFlag, *querySvcQueryResourcesTagsAllFlag, *querySvcQueryResourcesDateFieldFlag, *querySvcQueryResourcesDateFromFlag, *querySvcQueryResourcesDateToFlag, *querySvcQueryResourcesFiltersFlag, *querySvcQueryResourcesCelFilterFlag, *querySvcQueryResourcesSortFlag, *querySvcQueryResourcesPageTokenFlag, *querySvcQueryResourcesBearerTokenFlag) + data, err = querysvcc.BuildQueryResourcesPayload(*querySvcQueryResourcesVersionFlag, *querySvcQueryResourcesNameFlag, *querySvcQueryResourcesParentFlag, *querySvcQueryResourcesTypeFlag, *querySvcQueryResourcesTagsFlag, *querySvcQueryResourcesTagsAllFlag, *querySvcQueryResourcesDateFieldFlag, *querySvcQueryResourcesDateFromFlag, *querySvcQueryResourcesDateToFlag, *querySvcQueryResourcesFiltersFlag, *querySvcQueryResourcesCelFilterFlag, *querySvcQueryResourcesSortFlag, *querySvcQueryResourcesPageTokenFlag, *querySvcQueryResourcesPageSizeFlag, *querySvcQueryResourcesBearerTokenFlag) case "query-resources-count": endpoint = c.QueryResourcesCount() data, err = querysvcc.BuildQueryResourcesCountPayload(*querySvcQueryResourcesCountVersionFlag, *querySvcQueryResourcesCountNameFlag, *querySvcQueryResourcesCountParentFlag, *querySvcQueryResourcesCountTypeFlag, *querySvcQueryResourcesCountTagsFlag, *querySvcQueryResourcesCountTagsAllFlag, *querySvcQueryResourcesCountDateFieldFlag, *querySvcQueryResourcesCountDateFromFlag, *querySvcQueryResourcesCountDateToFlag, *querySvcQueryResourcesCountFiltersFlag, *querySvcQueryResourcesCountBearerTokenFlag) @@ -228,7 +229,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 -date-field STRING -date-from STRING -date-to STRING -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 -page-size INT -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: @@ -244,6 +245,7 @@ Locate resources by their type or parent, or use typeahead search to query resou -cel-filter STRING: -sort STRING: -page-token STRING: + -page-size INT: -bearer-token STRING: Example: @@ -256,7 +258,7 @@ Example: ]' --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..." + ]' --cel-filter "data.slug == \"tlf\"" --sort "updated_desc" --page-token "****" --page-size 20 --bearer-token "eyJhbGci..." `, os.Args[0]) } diff --git a/gen/http/openapi.json b/gen/http/openapi.json index 876ce75..91928d0 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":"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 +{"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":"page_size","in":"query","description":"Number of results per page","required":false,"type":"integer","default":50,"maximum":1000,"minimum":1},{"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 1ecf54f..56b6904 100644 --- a/gen/http/openapi.yaml +++ b/gen/http/openapi.yaml @@ -226,6 +226,14 @@ paths: description: Opaque token for pagination required: false type: string + - name: page_size + in: query + description: Number of results per page + required: false + type: integer + default: 50 + maximum: 1000 + minimum: 1 - name: Authorization in: header description: Token diff --git a/gen/http/openapi3.json b/gen/http/openapi3.json index 549ddb7..0f670bc 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":"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 +{"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":"****"},{"name":"page_size","in":"query","description":"Number of results per page","allowEmptyValue":true,"schema":{"type":"integer","description":"Number of results per page","default":50,"example":20,"format":"int64","minimum":1,"maximum":1000},"example":20}],"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_size":{"type":"integer","description":"Number of results per page","default":50,"example":20,"format":"int64","minimum":1,"maximum":1000},"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_size":20,"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 ed3182e..0fc028e 100644 --- a/gen/http/openapi3.yaml +++ b/gen/http/openapi3.yaml @@ -327,6 +327,19 @@ paths: description: Opaque token for pagination example: '****' example: '****' + - name: page_size + in: query + description: Number of results per page + allowEmptyValue: true + schema: + type: integer + description: Number of results per page + default: 50 + example: 20 + format: int64 + minimum: 1 + maximum: 1000 + example: 20 responses: "200": description: OK response. @@ -767,6 +780,14 @@ components: Sortable: type: object properties: + page_size: + type: integer + description: Number of results per page + default: 50 + example: 20 + format: int64 + minimum: 1 + maximum: 1000 page_token: type: string description: Opaque token for pagination @@ -782,6 +803,7 @@ components: - updated_asc - updated_desc example: + page_size: 20 page_token: '****' sort: updated_desc SuggestOrgsResponseBody: diff --git a/gen/http/query_svc/client/cli.go b/gen/http/query_svc/client/cli.go index 64ce35a..0757bb1 100644 --- a/gen/http/query_svc/client/cli.go +++ b/gen/http/query_svc/client/cli.go @@ -10,6 +10,7 @@ package client import ( "encoding/json" "fmt" + "strconv" "unicode/utf8" querysvc "github.com/linuxfoundation/lfx-v2-query-service/gen/query_svc" @@ -18,7 +19,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, querySvcQueryResourcesDateField string, querySvcQueryResourcesDateFrom string, querySvcQueryResourcesDateTo 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, querySvcQueryResourcesPageSize string, querySvcQueryResourcesBearerToken string) (*querysvc.QueryResourcesPayload, error) { var err error var version string { @@ -133,6 +134,26 @@ func BuildQueryResourcesPayload(querySvcQueryResourcesVersion string, querySvcQu pageToken = &querySvcQueryResourcesPageToken } } + var pageSize int + { + if querySvcQueryResourcesPageSize != "" { + var v int64 + v, err = strconv.ParseInt(querySvcQueryResourcesPageSize, 10, strconv.IntSize) + pageSize = int(v) + if err != nil { + return nil, fmt.Errorf("invalid value for pageSize, must be INT") + } + if pageSize < 1 { + err = goa.MergeErrors(err, goa.InvalidRangeError("page_size", pageSize, 1, true)) + } + if pageSize > 1000 { + err = goa.MergeErrors(err, goa.InvalidRangeError("page_size", pageSize, 1000, false)) + } + if err != nil { + return nil, err + } + } + } var bearerToken string { bearerToken = querySvcQueryResourcesBearerToken @@ -151,6 +172,7 @@ func BuildQueryResourcesPayload(querySvcQueryResourcesVersion string, querySvcQu v.CelFilter = celFilter v.Sort = sort v.PageToken = pageToken + v.PageSize = pageSize v.BearerToken = bearerToken return v, nil diff --git a/gen/http/query_svc/client/encode_decode.go b/gen/http/query_svc/client/encode_decode.go index 12c998a..067f708 100644 --- a/gen/http/query_svc/client/encode_decode.go +++ b/gen/http/query_svc/client/encode_decode.go @@ -10,6 +10,7 @@ package client import ( "bytes" "context" + "fmt" "io" "net/http" "net/url" @@ -86,6 +87,7 @@ func EncodeQueryResourcesRequest(encoder func(*http.Request) goahttp.Encoder) fu if p.PageToken != nil { values.Add("page_token", *p.PageToken) } + values.Add("page_size", fmt.Sprintf("%v", p.PageSize)) req.URL.RawQuery = values.Encode() return nil } diff --git a/gen/http/query_svc/server/encode_decode.go b/gen/http/query_svc/server/encode_decode.go index 5b6f712..a63464c 100644 --- a/gen/http/query_svc/server/encode_decode.go +++ b/gen/http/query_svc/server/encode_decode.go @@ -11,6 +11,7 @@ import ( "context" "errors" "net/http" + "strconv" "strings" "unicode/utf8" @@ -52,6 +53,7 @@ func DecodeQueryResourcesRequest(mux goahttp.Muxer, decoder func(*http.Request) celFilter *string sort string pageToken *string + pageSize int bearerToken string err error ) @@ -120,6 +122,24 @@ func DecodeQueryResourcesRequest(mux goahttp.Muxer, decoder func(*http.Request) if pageTokenRaw != "" { pageToken = &pageTokenRaw } + { + pageSizeRaw := qp.Get("page_size") + if pageSizeRaw == "" { + pageSize = 50 + } else { + v, err2 := strconv.ParseInt(pageSizeRaw, 10, strconv.IntSize) + if err2 != nil { + err = goa.MergeErrors(err, goa.InvalidFieldTypeError("page_size", pageSizeRaw, "integer")) + } + pageSize = int(v) + } + } + if pageSize < 1 { + err = goa.MergeErrors(err, goa.InvalidRangeError("page_size", pageSize, 1, true)) + } + if pageSize > 1000 { + err = goa.MergeErrors(err, goa.InvalidRangeError("page_size", pageSize, 1000, false)) + } bearerToken = r.Header.Get("Authorization") if bearerToken == "" { err = goa.MergeErrors(err, goa.MissingFieldError("bearer_token", "header")) @@ -127,7 +147,7 @@ func DecodeQueryResourcesRequest(mux goahttp.Muxer, decoder func(*http.Request) if err != nil { return nil, err } - payload := NewQueryResourcesPayload(version, name, parent, type_, tags, tagsAll, dateField, dateFrom, dateTo, filters, celFilter, sort, pageToken, bearerToken) + payload := NewQueryResourcesPayload(version, name, parent, type_, tags, tagsAll, dateField, dateFrom, dateTo, filters, celFilter, sort, pageToken, pageSize, 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 8354043..940a67d 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, dateField *string, dateFrom *string, dateTo *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, pageSize int, bearerToken string) *querysvc.QueryResourcesPayload { v := &querysvc.QueryResourcesPayload{} v.Version = version v.Name = name @@ -400,6 +400,7 @@ func NewQueryResourcesPayload(version string, name *string, parent *string, type v.CelFilter = celFilter v.Sort = sort v.PageToken = pageToken + v.PageSize = pageSize v.BearerToken = bearerToken return v diff --git a/gen/query_svc/service.go b/gen/query_svc/service.go index 5816264..a9d6085 100644 --- a/gen/query_svc/service.go +++ b/gen/query_svc/service.go @@ -188,6 +188,8 @@ type QueryResourcesPayload struct { Sort string // Opaque token for pagination PageToken *string + // Number of results per page + PageSize int } // QueryResourcesResult is the result type of the query-svc service diff --git a/internal/infrastructure/opensearch/client.go b/internal/infrastructure/opensearch/client.go index c1fac7a..178af2f 100644 --- a/internal/infrastructure/opensearch/client.go +++ b/internal/infrastructure/opensearch/client.go @@ -11,7 +11,6 @@ import ( "log/slog" "net/http" - "github.com/linuxfoundation/lfx-v2-query-service/pkg/constants" "github.com/linuxfoundation/lfx-v2-query-service/pkg/errors" "github.com/linuxfoundation/lfx-v2-query-service/pkg/global" "github.com/linuxfoundation/lfx-v2-query-service/pkg/paging" @@ -23,7 +22,7 @@ type httpClient struct { client *opensearchapi.Client } -func (c *httpClient) Search(ctx context.Context, index string, query []byte) (*SearchResponse, error) { +func (c *httpClient) Search(ctx context.Context, index string, query []byte, pageSize int) (*SearchResponse, error) { slog.DebugContext(ctx, "executing opensearch search", "index", index, @@ -73,7 +72,7 @@ func (c *httpClient) Search(ctx context.Context, index string, query []byte) (*S } // if the number of hits returned equals the page size, there may be more results. - if len(searchResponse.Hits.Hits) == constants.DefaultPageSize { + if pageSize > 0 && len(searchResponse.Hits.Hits) == pageSize { searchAfter := searchResponse.Hits.Hits[len(searchResponse.Hits.Hits)-1].Sort pageToken, errEncodePageToken := paging.EncodePageToken(searchAfter, global.PageTokenSecret(ctx)) if errEncodePageToken != nil { diff --git a/internal/infrastructure/opensearch/searcher.go b/internal/infrastructure/opensearch/searcher.go index b531c92..74dc068 100644 --- a/internal/infrastructure/opensearch/searcher.go +++ b/internal/infrastructure/opensearch/searcher.go @@ -40,7 +40,7 @@ type OpenSearchSearcher struct { // OpenSearchClientRetriever defines the interface for OpenSearch operations // This allows for easy mocking and testing type OpenSearchClientRetriever interface { - Search(ctx context.Context, index string, query []byte) (*SearchResponse, error) + Search(ctx context.Context, index string, query []byte, pageSize int) (*SearchResponse, error) Count(ctx context.Context, index string, query []byte) (*CountResponse, error) AggregationSearch(ctx context.Context, index string, query []byte) (*AggregationResponse, error) IsReady(ctx context.Context) error @@ -59,7 +59,7 @@ func (os *OpenSearchSearcher) QueryResources(ctx context.Context, criteria model } // Execute the search - response, err := os.client.Search(ctx, os.index, query) + response, err := os.client.Search(ctx, os.index, query, criteria.PageSize) if err != nil { return nil, fmt.Errorf("opensearch search failed: %w", err) } diff --git a/internal/infrastructure/opensearch/searcher_test.go b/internal/infrastructure/opensearch/searcher_test.go index cf148d9..1d30e50 100644 --- a/internal/infrastructure/opensearch/searcher_test.go +++ b/internal/infrastructure/opensearch/searcher_test.go @@ -22,13 +22,15 @@ type MockOpenSearchClient struct { countError error aggregationResponse *AggregationResponse aggregationError error + lastPageSize int } func NewMockOpenSearchClient() *MockOpenSearchClient { return &MockOpenSearchClient{} } -func (m *MockOpenSearchClient) Search(ctx context.Context, index string, query []byte) (*SearchResponse, error) { +func (m *MockOpenSearchClient) Search(ctx context.Context, index string, query []byte, pageSize int) (*SearchResponse, error) { + m.lastPageSize = pageSize if m.searchError != nil { return nil, m.searchError } @@ -729,6 +731,58 @@ func TestOpenSearchSearcherIntegration(t *testing.T) { }) } +func TestQueryResourcesPassesPageSizeToClient(t *testing.T) { + tests := []struct { + name string + pageSize int + expectedPageSize int + }{ + { + name: "default page size", + pageSize: 50, + expectedPageSize: 50, + }, + { + name: "custom page size", + pageSize: 20, + expectedPageSize: 20, + }, + { + name: "max page size", + pageSize: 1000, + expectedPageSize: 1000, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + assertion := assert.New(t) + + mockClient := NewMockOpenSearchClient() + mockClient.SetSearchResponse(&SearchResponse{ + Hits: Hits{ + Total: Total{Value: 0}, + Hits: []Hit{}, + }, + }) + + searcher := &OpenSearchSearcher{ + client: mockClient, + index: "test-index", + } + + ctx := context.Background() + criteria := model.SearchCriteria{ + PageSize: tc.pageSize, + } + + _, err := searcher.QueryResources(ctx, criteria) + assertion.NoError(err) + assertion.Equal(tc.expectedPageSize, mockClient.lastPageSize) + }) + } +} + func TestOpenSearchSearcherQueryResourcesCount(t *testing.T) { tests := []struct { name string diff --git a/pkg/constants/query.go b/pkg/constants/query.go index d3d7894..b7fab84 100644 --- a/pkg/constants/query.go +++ b/pkg/constants/query.go @@ -7,6 +7,8 @@ const ( // DefaultPageSize is the default number of results per page for queries DefaultPageSize = 50 + // MaxPageSize is the maximum allowed number of results per page + MaxPageSize = 1000 // DefaultBucketSize is the default size of the bucket for queries DefaultBucketSize = 100 )