From e3e270449c3fc636cf09154f746305a1737d16cc Mon Sep 17 00:00:00 2001 From: Andras Toth <4157749+tothandras@users.noreply.github.com> Date: Wed, 11 Mar 2026 10:30:32 +0100 Subject: [PATCH 1/4] chore: filter play --- Makefile | 2 +- .../packages/aip/src/llmcost/operations.tsp | 15 +- api/v3/api.gen.go | 942 +++++++++++------- api/v3/api.go | 19 + api/v3/handlers/llmcost/list_overrides.go | 10 +- api/v3/handlers/llmcost/list_prices.go | 21 +- api/v3/openapi.yaml | 117 ++- api/v3/server/server.go | 6 +- 8 files changed, 738 insertions(+), 394 deletions(-) diff --git a/Makefile b/Makefile index c4aec88a2f..562ccd026a 100644 --- a/Makefile +++ b/Makefile @@ -29,7 +29,7 @@ generate-javascript-sdk: ## Generate JavaScript SDK $(MAKE) -C api/client/javascript generate .PHONY: gen-api -gen-api: update-openapi generate-javascript-sdk ## Generate API and SDKs +gen-api: update-openapi # generate-javascript-sdk ## Generate API and SDKs $(call print-target) .PHONY: generate-all diff --git a/api/spec/packages/aip/src/llmcost/operations.tsp b/api/spec/packages/aip/src/llmcost/operations.tsp index b46afaf399..52f022eae6 100644 --- a/api/spec/packages/aip/src/llmcost/operations.tsp +++ b/api/spec/packages/aip/src/llmcost/operations.tsp @@ -17,24 +17,32 @@ namespace LLMCost; * TODO: This is a temporary solution to support the filter API. */ @friendlyName("FilterSingleString") +@useRef("../../../../common/definitions/aip_filters.yaml#/components/schemas/StringFieldFilter") model FilterSingleString { /** * The field must match the provided value. */ - @extension("x-omitempty", true) eq?: string; + /** + * aasd + */ + oeq?: string; + /** * The field must not match the provided value. */ - @extension("x-omitempty", true) neq?: string; /** * The field must contain the provided value. */ - @extension("x-omitempty", true) contains?: string; + + /** + * asd + */ + ocontains?: string; } /** @@ -44,6 +52,7 @@ model FilterSingleString { @friendlyName("ListLLMCostPricesParamsFilter") model ListPricesParamsFilter { /** Filter by provider. e.g. ?filter[provider][eq]=openai */ + @extension("x-go-type", "FilterString") provider?: FilterSingleString; /** Filter by model ID. e.g. ?filter[model_id][eq]=gpt-4 */ diff --git a/api/v3/api.gen.go b/api/v3/api.gen.go index 703cabebcf..38fe5a878e 100644 --- a/api/v3/api.gen.go +++ b/api/v3/api.gen.go @@ -2260,19 +2260,6 @@ type FeatureCostQueryRow_Dimensions struct { AdditionalProperties map[string]string `json:"-"` } -// FilterSingleString A filter for a single string field. -// TODO: This is a temporary solution to support the filter API. -type FilterSingleString struct { - // Contains The field must contain the provided value. - Contains *string `json:"contains,omitempty"` - - // Eq The field must match the provided value. - Eq *string `json:"eq,omitempty"` - - // Neq The field must not match the provided value. - Neq *string `json:"neq,omitempty"` -} - // ForbiddenError defines model for ForbiddenError. type ForbiddenError struct { Detail interface{} `json:"detail"` @@ -2504,16 +2491,16 @@ type ListCustomersParamsFilter struct { // ListLLMCostPricesParamsFilter Filter options for listing LLM cost prices. type ListLLMCostPricesParamsFilter struct { // Currency Filter by currency code. e.g. ?filter[currency][eq]=USD - Currency *FilterSingleString `json:"currency,omitempty"` + Currency *StringFieldFilter `json:"currency,omitempty"` // ModelId Filter by model ID. e.g. ?filter[model_id][eq]=gpt-4 - ModelId *FilterSingleString `json:"model_id,omitempty"` + ModelId *StringFieldFilter `json:"model_id,omitempty"` // ModelName Filter by model name. e.g. ?filter[model_name][contains]=gpt - ModelName *FilterSingleString `json:"model_name,omitempty"` + ModelName *StringFieldFilter `json:"model_name,omitempty"` // Provider Filter by provider. e.g. ?filter[provider][eq]=openai - Provider *FilterSingleString `json:"provider,omitempty"` + Provider *StringFieldFilter `json:"provider,omitempty"` } // Meter A meter is a configuration that defines how to match and aggregate events. @@ -2812,6 +2799,44 @@ type ResourceKey = string // JSONPath notation may be used to specify a sub-attribute (eg: 'foo.bar desc'). type SortQuery = string +// StringFieldContainsFilter Filters on the given string field value by fuzzy match. +type StringFieldContainsFilter struct { + Contains string `json:"contains"` +} + +// StringFieldEqualsFilter Filters on the given string field value by exact match. +type StringFieldEqualsFilter struct { + union json.RawMessage +} + +// StringFieldEqualsFilter0 defines model for . +type StringFieldEqualsFilter0 = string + +// StringFieldEqualsFilter1 defines model for . +type StringFieldEqualsFilter1 struct { + Eq string `json:"eq"` +} + +// StringFieldFilter Filters on the given string field value by either exact or fuzzy match. +type StringFieldFilter struct { + union json.RawMessage +} + +// StringFieldNEQFilter Filters on the given string field value by exact match inequality. +type StringFieldNEQFilter struct { + Neq string `json:"neq"` +} + +// StringFieldOContainsFilter Returns entities that fuzzy-match any of the comma-delimited phrases in the filter string. +type StringFieldOContainsFilter struct { + Ocontains string `json:"ocontains"` +} + +// StringFieldOEQFilter Returns entities that exact match any of the comma-delimited phrases in the filter string. +type StringFieldOEQFilter struct { + Oeq string `json:"oeq"` +} + // SubscriptionPagePaginatedResponse Page paginated response. type SubscriptionPagePaginatedResponse struct { Data []BillingSubscription `json:"data"` @@ -4353,6 +4378,208 @@ func (t *InvalidParameters_Item) UnmarshalJSON(b []byte) error { return err } +// AsStringFieldEqualsFilter0 returns the union data inside the StringFieldEqualsFilter as a StringFieldEqualsFilter0 +func (t StringFieldEqualsFilter) AsStringFieldEqualsFilter0() (StringFieldEqualsFilter0, error) { + var body StringFieldEqualsFilter0 + err := json.Unmarshal(t.union, &body) + return body, err +} + +// FromStringFieldEqualsFilter0 overwrites any union data inside the StringFieldEqualsFilter as the provided StringFieldEqualsFilter0 +func (t *StringFieldEqualsFilter) FromStringFieldEqualsFilter0(v StringFieldEqualsFilter0) error { + b, err := json.Marshal(v) + t.union = b + return err +} + +// MergeStringFieldEqualsFilter0 performs a merge with any union data inside the StringFieldEqualsFilter, using the provided StringFieldEqualsFilter0 +func (t *StringFieldEqualsFilter) MergeStringFieldEqualsFilter0(v StringFieldEqualsFilter0) error { + b, err := json.Marshal(v) + if err != nil { + return err + } + + merged, err := runtime.JSONMerge(t.union, b) + t.union = merged + return err +} + +// AsStringFieldEqualsFilter1 returns the union data inside the StringFieldEqualsFilter as a StringFieldEqualsFilter1 +func (t StringFieldEqualsFilter) AsStringFieldEqualsFilter1() (StringFieldEqualsFilter1, error) { + var body StringFieldEqualsFilter1 + err := json.Unmarshal(t.union, &body) + return body, err +} + +// FromStringFieldEqualsFilter1 overwrites any union data inside the StringFieldEqualsFilter as the provided StringFieldEqualsFilter1 +func (t *StringFieldEqualsFilter) FromStringFieldEqualsFilter1(v StringFieldEqualsFilter1) error { + b, err := json.Marshal(v) + t.union = b + return err +} + +// MergeStringFieldEqualsFilter1 performs a merge with any union data inside the StringFieldEqualsFilter, using the provided StringFieldEqualsFilter1 +func (t *StringFieldEqualsFilter) MergeStringFieldEqualsFilter1(v StringFieldEqualsFilter1) error { + b, err := json.Marshal(v) + if err != nil { + return err + } + + merged, err := runtime.JSONMerge(t.union, b) + t.union = merged + return err +} + +func (t StringFieldEqualsFilter) MarshalJSON() ([]byte, error) { + b, err := t.union.MarshalJSON() + return b, err +} + +func (t *StringFieldEqualsFilter) UnmarshalJSON(b []byte) error { + err := t.union.UnmarshalJSON(b) + return err +} + +// AsStringFieldEqualsFilter returns the union data inside the StringFieldFilter as a StringFieldEqualsFilter +func (t StringFieldFilter) AsStringFieldEqualsFilter() (StringFieldEqualsFilter, error) { + var body StringFieldEqualsFilter + err := json.Unmarshal(t.union, &body) + return body, err +} + +// FromStringFieldEqualsFilter overwrites any union data inside the StringFieldFilter as the provided StringFieldEqualsFilter +func (t *StringFieldFilter) FromStringFieldEqualsFilter(v StringFieldEqualsFilter) error { + b, err := json.Marshal(v) + t.union = b + return err +} + +// MergeStringFieldEqualsFilter performs a merge with any union data inside the StringFieldFilter, using the provided StringFieldEqualsFilter +func (t *StringFieldFilter) MergeStringFieldEqualsFilter(v StringFieldEqualsFilter) error { + b, err := json.Marshal(v) + if err != nil { + return err + } + + merged, err := runtime.JSONMerge(t.union, b) + t.union = merged + return err +} + +// AsStringFieldContainsFilter returns the union data inside the StringFieldFilter as a StringFieldContainsFilter +func (t StringFieldFilter) AsStringFieldContainsFilter() (StringFieldContainsFilter, error) { + var body StringFieldContainsFilter + err := json.Unmarshal(t.union, &body) + return body, err +} + +// FromStringFieldContainsFilter overwrites any union data inside the StringFieldFilter as the provided StringFieldContainsFilter +func (t *StringFieldFilter) FromStringFieldContainsFilter(v StringFieldContainsFilter) error { + b, err := json.Marshal(v) + t.union = b + return err +} + +// MergeStringFieldContainsFilter performs a merge with any union data inside the StringFieldFilter, using the provided StringFieldContainsFilter +func (t *StringFieldFilter) MergeStringFieldContainsFilter(v StringFieldContainsFilter) error { + b, err := json.Marshal(v) + if err != nil { + return err + } + + merged, err := runtime.JSONMerge(t.union, b) + t.union = merged + return err +} + +// AsStringFieldOContainsFilter returns the union data inside the StringFieldFilter as a StringFieldOContainsFilter +func (t StringFieldFilter) AsStringFieldOContainsFilter() (StringFieldOContainsFilter, error) { + var body StringFieldOContainsFilter + err := json.Unmarshal(t.union, &body) + return body, err +} + +// FromStringFieldOContainsFilter overwrites any union data inside the StringFieldFilter as the provided StringFieldOContainsFilter +func (t *StringFieldFilter) FromStringFieldOContainsFilter(v StringFieldOContainsFilter) error { + b, err := json.Marshal(v) + t.union = b + return err +} + +// MergeStringFieldOContainsFilter performs a merge with any union data inside the StringFieldFilter, using the provided StringFieldOContainsFilter +func (t *StringFieldFilter) MergeStringFieldOContainsFilter(v StringFieldOContainsFilter) error { + b, err := json.Marshal(v) + if err != nil { + return err + } + + merged, err := runtime.JSONMerge(t.union, b) + t.union = merged + return err +} + +// AsStringFieldOEQFilter returns the union data inside the StringFieldFilter as a StringFieldOEQFilter +func (t StringFieldFilter) AsStringFieldOEQFilter() (StringFieldOEQFilter, error) { + var body StringFieldOEQFilter + err := json.Unmarshal(t.union, &body) + return body, err +} + +// FromStringFieldOEQFilter overwrites any union data inside the StringFieldFilter as the provided StringFieldOEQFilter +func (t *StringFieldFilter) FromStringFieldOEQFilter(v StringFieldOEQFilter) error { + b, err := json.Marshal(v) + t.union = b + return err +} + +// MergeStringFieldOEQFilter performs a merge with any union data inside the StringFieldFilter, using the provided StringFieldOEQFilter +func (t *StringFieldFilter) MergeStringFieldOEQFilter(v StringFieldOEQFilter) error { + b, err := json.Marshal(v) + if err != nil { + return err + } + + merged, err := runtime.JSONMerge(t.union, b) + t.union = merged + return err +} + +// AsStringFieldNEQFilter returns the union data inside the StringFieldFilter as a StringFieldNEQFilter +func (t StringFieldFilter) AsStringFieldNEQFilter() (StringFieldNEQFilter, error) { + var body StringFieldNEQFilter + err := json.Unmarshal(t.union, &body) + return body, err +} + +// FromStringFieldNEQFilter overwrites any union data inside the StringFieldFilter as the provided StringFieldNEQFilter +func (t *StringFieldFilter) FromStringFieldNEQFilter(v StringFieldNEQFilter) error { + b, err := json.Marshal(v) + t.union = b + return err +} + +// MergeStringFieldNEQFilter performs a merge with any union data inside the StringFieldFilter, using the provided StringFieldNEQFilter +func (t *StringFieldFilter) MergeStringFieldNEQFilter(v StringFieldNEQFilter) error { + b, err := json.Marshal(v) + if err != nil { + return err + } + + merged, err := runtime.JSONMerge(t.union, b) + t.union = merged + return err +} + +func (t StringFieldFilter) MarshalJSON() ([]byte, error) { + b, err := t.union.MarshalJSON() + return b, err +} + +func (t *StringFieldFilter) UnmarshalJSON(b []byte) error { + err := t.union.UnmarshalJSON(b) + return err +} + // ServerInterface represents all server handlers. type ServerInterface interface { // List apps @@ -6168,344 +6395,349 @@ func HandlerWithOptions(si ServerInterface, options ChiServerOptions) http.Handl // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", + "AP//HScheYsYAgA=", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/api/v3/api.go b/api/v3/api.go index a90249418c..345f1e2ad7 100644 --- a/api/v3/api.go +++ b/api/v3/api.go @@ -1,2 +1,21 @@ //go:generate go tool github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen --config=codegen.yaml ./openapi.yaml package v3 + +// FilterSingleString A filter for a single string field. +// TODO: This is a temporary solution to support the filter API. +type FilterString struct { + // Contains The field must contain the provided value. + Contains *string `json:"contains,omitempty"` + + // Eq The field must match the provided value. + Eq *string `json:"eq,omitempty"` + + // Neq The field must not match the provided value. + Neq *string `json:"neq,omitempty"` + + // Ocontains asd + Ocontains *string `json:"ocontains,omitempty"` + + // Oeq aasd + Oeq *string `json:"oeq,omitempty"` +} diff --git a/api/v3/handlers/llmcost/list_overrides.go b/api/v3/handlers/llmcost/list_overrides.go index 18e3aabcff..72bc9dd9ce 100644 --- a/api/v3/handlers/llmcost/list_overrides.go +++ b/api/v3/handlers/llmcost/list_overrides.go @@ -53,11 +53,11 @@ func (h *handler) ListOverrides() ListOverridesHandler { // Filters if params.Filter != nil { - provider, err := filterSingleStringToDomain(params.Filter.Provider) - if err != nil { - return req, err - } - req.Provider = provider + // provider, err := filterSingleStringToDomain(params.Filter.Provider) + // if err != nil { + // return req, err + // } + // req.Provider = provider modelID, err := filterSingleStringToDomain(params.Filter.ModelId) if err != nil { diff --git a/api/v3/handlers/llmcost/list_prices.go b/api/v3/handlers/llmcost/list_prices.go index 4c2c913726..b8cab2fdad 100644 --- a/api/v3/handlers/llmcost/list_prices.go +++ b/api/v3/handlers/llmcost/list_prices.go @@ -2,7 +2,9 @@ package llmcost import ( "context" + "encoding/json" "fmt" + "log/slog" "net/http" "github.com/samber/lo" @@ -71,13 +73,22 @@ func (h *handler) ListPrices() ListPricesHandler { req.Order = sort.Order.ToSortxOrder() } + j, err := json.Marshal(params.Filter) + if err != nil { + return req, err + } + + // query := r.URL.Query() + // filter := query.Get("filter") + + slog.Info("params.Filter", "filter", string(j)) // Filters if params.Filter != nil { - provider, err := filterSingleStringToDomain(params.Filter.Provider) - if err != nil { - return req, err - } - req.Provider = provider + // provider, err := filterSingleStringToDomain(params.Filter.Provider) + // if err != nil { + // return req, err + // } + // req.Provider = provider modelID, err := filterSingleStringToDomain(params.Filter.ModelId) if err != nil { diff --git a/api/v3/openapi.yaml b/api/v3/openapi.yaml index dff68d9d1b..1e896cadf4 100644 --- a/api/v3/openapi.yaml +++ b/api/v3/openapi.yaml @@ -3741,24 +3741,6 @@ components: type: string description: The dimensions the value is aggregated over. description: A row in the result of a feature cost query. - FilterSingleString: - type: object - properties: - eq: - type: string - description: The field must match the provided value. - x-omitempty: true - neq: - type: string - description: The field must not match the provided value. - x-omitempty: true - contains: - type: string - description: The field must contain the provided value. - x-omitempty: true - description: |- - A filter for a single string field. - TODO: This is a temporary solution to support the filter API. ISO8601Duration: type: string pattern: ^P(?:\d+(?:\.\d+)?Y)?(?:\d+(?:\.\d+)?M)?(?:\d+(?:\.\d+)?W)?(?:\d+(?:\.\d+)?D)?(?:T(?:\d+(?:\.\d+)?H)?(?:\d+(?:\.\d+)?M)?(?:\d+(?:\.\d+)?S)?)?$ @@ -3971,19 +3953,20 @@ components: properties: provider: allOf: - - $ref: '#/components/schemas/FilterSingleString' + - $ref: '#/components/schemas/StringFieldFilter' description: Filter by provider. e.g. ?filter[provider][eq]=openai + x-go-type: FilterString model_id: allOf: - - $ref: '#/components/schemas/FilterSingleString' + - $ref: '#/components/schemas/StringFieldFilter' description: Filter by model ID. e.g. ?filter[model_id][eq]=gpt-4 model_name: allOf: - - $ref: '#/components/schemas/FilterSingleString' + - $ref: '#/components/schemas/StringFieldFilter' description: Filter by model name. e.g. ?filter[model_name][contains]=gpt currency: allOf: - - $ref: '#/components/schemas/FilterSingleString' + - $ref: '#/components/schemas/StringFieldFilter' description: Filter by currency code. e.g. ?filter[currency][eq]=USD description: Filter options for listing LLM cost prices. Meter: @@ -5025,6 +5008,96 @@ components: example: kong:trace:1234567890 detail: example: Gone + StringFieldEqualsFilter: + title: StringFieldEqualsFilter + description: Filters on the given string field value by exact match. + oneOf: + - type: string + - type: object + title: StringFieldEqualsComparison + additionalProperties: false + properties: + eq: + type: string + required: + - eq + x-examples: + example-1: equals-some-value + example-2: + eq: some-value + StringFieldContainsFilter: + title: StringFieldContainsFilter + description: Filters on the given string field value by fuzzy match. + type: object + additionalProperties: false + properties: + contains: + type: string + required: + - contains + x-examples: + example-1: + contains: some-value + StringFieldOContainsFilter: + title: StringFieldOContainsFilter + description: Returns entities that fuzzy-match any of the comma-delimited phrases in the filter string. + type: object + additionalProperties: false + properties: + ocontains: + type: string + required: + - ocontains + x-examples: + example-1: + ocontains: this-value,or-that-value + StringFieldOEQFilter: + title: StringFieldOEQFilter + description: Returns entities that exact match any of the comma-delimited phrases in the filter string. + type: object + additionalProperties: false + properties: + oeq: + type: string + required: + - oeq + x-examples: + example-1: + oeq: some-value,some-other-value + StringFieldNEQFilter: + title: StringFieldNEQFilter + description: Filters on the given string field value by exact match inequality. + type: object + additionalProperties: false + properties: + neq: + type: string + required: + - neq + x-examples: + example-1: + neq: not-this-value + StringFieldFilter: + title: StringFieldFilter + description: Filters on the given string field value by either exact or fuzzy match. + oneOf: + - $ref: '#/components/schemas/StringFieldEqualsFilter' + - $ref: '#/components/schemas/StringFieldContainsFilter' + - $ref: '#/components/schemas/StringFieldOContainsFilter' + - $ref: '#/components/schemas/StringFieldOEQFilter' + - $ref: '#/components/schemas/StringFieldNEQFilter' + x-examples: + example-1: equals-some-value + example-2: + eq: some-value + example-3: + contains: some-value + example-4: + ocontains: some-potential,value + example-5: + oeq: some-potential,value + example-6: + neq: not-this-value ConflictError: allOf: - $ref: '#/components/schemas/BaseError' diff --git a/api/v3/server/server.go b/api/v3/server/server.go index 562a5b4c27..eb45e6441b 100644 --- a/api/v3/server/server.go +++ b/api/v3/server/server.go @@ -251,9 +251,9 @@ func (s *Server) RegisterRoutes(r chi.Router) error { validationMiddleware := oasmiddleware.ValidateRequest(validationRouter, oasmiddleware.ValidateRequestOption{ RouteNotFoundHook: oasmiddleware.OasRouteNotFoundErrorHook, - RouteValidationErrorHook: func(err error, w http.ResponseWriter, r *http.Request) bool { - return oasmiddleware.OasValidationErrorHook(r.Context(), err, w, r) - }, + // RouteValidationErrorHook: func(err error, w http.ResponseWriter, r *http.Request) bool { + // return oasmiddleware.OasValidationErrorHook(r.Context(), err, w, r) + // }, FilterOptions: &openapi3filter.Options{ // No-op auth: auth is handled by other middleware. AuthenticationFunc: openapi3filter.NoopAuthenticationFunc, From f4ca53841175a2dde92a21f5e6a388611566ca12 Mon Sep 17 00:00:00 2001 From: Andras Toth <4157749+tothandras@users.noreply.github.com> Date: Wed, 11 Mar 2026 21:40:12 +0100 Subject: [PATCH 2/4] feat: WIP --- .../packages/aip/src/llmcost/operations.tsp | 13 +- api/v3/api.gen.go | 949 +++++++----------- api/v3/api.go | 18 +- api/v3/handlers/llmcost/convert.go | 2 +- api/v3/handlers/llmcost/list_overrides.go | 22 +- api/v3/handlers/llmcost/list_prices.go | 29 +- api/v3/oasmiddleware/decoder.go | 25 - api/v3/oasmiddleware/error.go | 164 +-- api/v3/oasmiddleware/hook.go | 101 +- api/v3/oasmiddleware/router.go | 60 -- api/v3/oasmiddleware/validator.go | 218 ++-- api/v3/openapi.yaml | 134 +-- api/v3/server/server.go | 28 +- api/v3/spec.go | 7 + go.mod | 24 +- go.sum | 57 +- 16 files changed, 751 insertions(+), 1100 deletions(-) delete mode 100644 api/v3/oasmiddleware/decoder.go delete mode 100644 api/v3/oasmiddleware/router.go create mode 100644 api/v3/spec.go diff --git a/api/spec/packages/aip/src/llmcost/operations.tsp b/api/spec/packages/aip/src/llmcost/operations.tsp index 52f022eae6..8ec2897235 100644 --- a/api/spec/packages/aip/src/llmcost/operations.tsp +++ b/api/spec/packages/aip/src/llmcost/operations.tsp @@ -17,7 +17,7 @@ namespace LLMCost; * TODO: This is a temporary solution to support the filter API. */ @friendlyName("FilterSingleString") -@useRef("../../../../common/definitions/aip_filters.yaml#/components/schemas/StringFieldFilter") +// @useRef("../../../../common/definitions/aip_filters.yaml#/components/schemas/StringFieldFilter") model FilterSingleString { /** * The field must match the provided value. @@ -53,16 +53,19 @@ model FilterSingleString { model ListPricesParamsFilter { /** Filter by provider. e.g. ?filter[provider][eq]=openai */ @extension("x-go-type", "FilterString") - provider?: FilterSingleString; + provider?: string | FilterSingleString; /** Filter by model ID. e.g. ?filter[model_id][eq]=gpt-4 */ - model_id?: FilterSingleString; + @extension("x-go-type", "FilterString") + model_id?: string | FilterSingleString; /** Filter by model name. e.g. ?filter[model_name][contains]=gpt */ - model_name?: FilterSingleString; + @extension("x-go-type", "FilterString") + model_name?: string | FilterSingleString; /** Filter by currency code. e.g. ?filter[currency][eq]=USD */ - currency?: FilterSingleString; + @extension("x-go-type", "FilterString") + currency?: string | FilterSingleString; } /** diff --git a/api/v3/api.gen.go b/api/v3/api.gen.go index 38fe5a878e..5f1cd59196 100644 --- a/api/v3/api.gen.go +++ b/api/v3/api.gen.go @@ -2260,6 +2260,25 @@ type FeatureCostQueryRow_Dimensions struct { AdditionalProperties map[string]string `json:"-"` } +// FilterSingleString A filter for a single string field. +// TODO: This is a temporary solution to support the filter API. +type FilterSingleString struct { + // Contains The field must contain the provided value. + Contains *string `json:"contains,omitempty"` + + // Eq The field must match the provided value. + Eq *string `json:"eq,omitempty"` + + // Neq The field must not match the provided value. + Neq *string `json:"neq,omitempty"` + + // Ocontains asd + Ocontains *string `json:"ocontains,omitempty"` + + // Oeq aasd + Oeq *string `json:"oeq,omitempty"` +} + // ForbiddenError defines model for ForbiddenError. type ForbiddenError struct { Detail interface{} `json:"detail"` @@ -2491,16 +2510,16 @@ type ListCustomersParamsFilter struct { // ListLLMCostPricesParamsFilter Filter options for listing LLM cost prices. type ListLLMCostPricesParamsFilter struct { // Currency Filter by currency code. e.g. ?filter[currency][eq]=USD - Currency *StringFieldFilter `json:"currency,omitempty"` + Currency *FilterString `json:"currency,omitempty"` // ModelId Filter by model ID. e.g. ?filter[model_id][eq]=gpt-4 - ModelId *StringFieldFilter `json:"model_id,omitempty"` + ModelId *FilterString `json:"model_id,omitempty"` // ModelName Filter by model name. e.g. ?filter[model_name][contains]=gpt - ModelName *StringFieldFilter `json:"model_name,omitempty"` + ModelName *FilterString `json:"model_name,omitempty"` // Provider Filter by provider. e.g. ?filter[provider][eq]=openai - Provider *StringFieldFilter `json:"provider,omitempty"` + Provider *FilterString `json:"provider,omitempty"` } // Meter A meter is a configuration that defines how to match and aggregate events. @@ -2799,44 +2818,6 @@ type ResourceKey = string // JSONPath notation may be used to specify a sub-attribute (eg: 'foo.bar desc'). type SortQuery = string -// StringFieldContainsFilter Filters on the given string field value by fuzzy match. -type StringFieldContainsFilter struct { - Contains string `json:"contains"` -} - -// StringFieldEqualsFilter Filters on the given string field value by exact match. -type StringFieldEqualsFilter struct { - union json.RawMessage -} - -// StringFieldEqualsFilter0 defines model for . -type StringFieldEqualsFilter0 = string - -// StringFieldEqualsFilter1 defines model for . -type StringFieldEqualsFilter1 struct { - Eq string `json:"eq"` -} - -// StringFieldFilter Filters on the given string field value by either exact or fuzzy match. -type StringFieldFilter struct { - union json.RawMessage -} - -// StringFieldNEQFilter Filters on the given string field value by exact match inequality. -type StringFieldNEQFilter struct { - Neq string `json:"neq"` -} - -// StringFieldOContainsFilter Returns entities that fuzzy-match any of the comma-delimited phrases in the filter string. -type StringFieldOContainsFilter struct { - Ocontains string `json:"ocontains"` -} - -// StringFieldOEQFilter Returns entities that exact match any of the comma-delimited phrases in the filter string. -type StringFieldOEQFilter struct { - Oeq string `json:"oeq"` -} - // SubscriptionPagePaginatedResponse Page paginated response. type SubscriptionPagePaginatedResponse struct { Data []BillingSubscription `json:"data"` @@ -4378,208 +4359,6 @@ func (t *InvalidParameters_Item) UnmarshalJSON(b []byte) error { return err } -// AsStringFieldEqualsFilter0 returns the union data inside the StringFieldEqualsFilter as a StringFieldEqualsFilter0 -func (t StringFieldEqualsFilter) AsStringFieldEqualsFilter0() (StringFieldEqualsFilter0, error) { - var body StringFieldEqualsFilter0 - err := json.Unmarshal(t.union, &body) - return body, err -} - -// FromStringFieldEqualsFilter0 overwrites any union data inside the StringFieldEqualsFilter as the provided StringFieldEqualsFilter0 -func (t *StringFieldEqualsFilter) FromStringFieldEqualsFilter0(v StringFieldEqualsFilter0) error { - b, err := json.Marshal(v) - t.union = b - return err -} - -// MergeStringFieldEqualsFilter0 performs a merge with any union data inside the StringFieldEqualsFilter, using the provided StringFieldEqualsFilter0 -func (t *StringFieldEqualsFilter) MergeStringFieldEqualsFilter0(v StringFieldEqualsFilter0) error { - b, err := json.Marshal(v) - if err != nil { - return err - } - - merged, err := runtime.JSONMerge(t.union, b) - t.union = merged - return err -} - -// AsStringFieldEqualsFilter1 returns the union data inside the StringFieldEqualsFilter as a StringFieldEqualsFilter1 -func (t StringFieldEqualsFilter) AsStringFieldEqualsFilter1() (StringFieldEqualsFilter1, error) { - var body StringFieldEqualsFilter1 - err := json.Unmarshal(t.union, &body) - return body, err -} - -// FromStringFieldEqualsFilter1 overwrites any union data inside the StringFieldEqualsFilter as the provided StringFieldEqualsFilter1 -func (t *StringFieldEqualsFilter) FromStringFieldEqualsFilter1(v StringFieldEqualsFilter1) error { - b, err := json.Marshal(v) - t.union = b - return err -} - -// MergeStringFieldEqualsFilter1 performs a merge with any union data inside the StringFieldEqualsFilter, using the provided StringFieldEqualsFilter1 -func (t *StringFieldEqualsFilter) MergeStringFieldEqualsFilter1(v StringFieldEqualsFilter1) error { - b, err := json.Marshal(v) - if err != nil { - return err - } - - merged, err := runtime.JSONMerge(t.union, b) - t.union = merged - return err -} - -func (t StringFieldEqualsFilter) MarshalJSON() ([]byte, error) { - b, err := t.union.MarshalJSON() - return b, err -} - -func (t *StringFieldEqualsFilter) UnmarshalJSON(b []byte) error { - err := t.union.UnmarshalJSON(b) - return err -} - -// AsStringFieldEqualsFilter returns the union data inside the StringFieldFilter as a StringFieldEqualsFilter -func (t StringFieldFilter) AsStringFieldEqualsFilter() (StringFieldEqualsFilter, error) { - var body StringFieldEqualsFilter - err := json.Unmarshal(t.union, &body) - return body, err -} - -// FromStringFieldEqualsFilter overwrites any union data inside the StringFieldFilter as the provided StringFieldEqualsFilter -func (t *StringFieldFilter) FromStringFieldEqualsFilter(v StringFieldEqualsFilter) error { - b, err := json.Marshal(v) - t.union = b - return err -} - -// MergeStringFieldEqualsFilter performs a merge with any union data inside the StringFieldFilter, using the provided StringFieldEqualsFilter -func (t *StringFieldFilter) MergeStringFieldEqualsFilter(v StringFieldEqualsFilter) error { - b, err := json.Marshal(v) - if err != nil { - return err - } - - merged, err := runtime.JSONMerge(t.union, b) - t.union = merged - return err -} - -// AsStringFieldContainsFilter returns the union data inside the StringFieldFilter as a StringFieldContainsFilter -func (t StringFieldFilter) AsStringFieldContainsFilter() (StringFieldContainsFilter, error) { - var body StringFieldContainsFilter - err := json.Unmarshal(t.union, &body) - return body, err -} - -// FromStringFieldContainsFilter overwrites any union data inside the StringFieldFilter as the provided StringFieldContainsFilter -func (t *StringFieldFilter) FromStringFieldContainsFilter(v StringFieldContainsFilter) error { - b, err := json.Marshal(v) - t.union = b - return err -} - -// MergeStringFieldContainsFilter performs a merge with any union data inside the StringFieldFilter, using the provided StringFieldContainsFilter -func (t *StringFieldFilter) MergeStringFieldContainsFilter(v StringFieldContainsFilter) error { - b, err := json.Marshal(v) - if err != nil { - return err - } - - merged, err := runtime.JSONMerge(t.union, b) - t.union = merged - return err -} - -// AsStringFieldOContainsFilter returns the union data inside the StringFieldFilter as a StringFieldOContainsFilter -func (t StringFieldFilter) AsStringFieldOContainsFilter() (StringFieldOContainsFilter, error) { - var body StringFieldOContainsFilter - err := json.Unmarshal(t.union, &body) - return body, err -} - -// FromStringFieldOContainsFilter overwrites any union data inside the StringFieldFilter as the provided StringFieldOContainsFilter -func (t *StringFieldFilter) FromStringFieldOContainsFilter(v StringFieldOContainsFilter) error { - b, err := json.Marshal(v) - t.union = b - return err -} - -// MergeStringFieldOContainsFilter performs a merge with any union data inside the StringFieldFilter, using the provided StringFieldOContainsFilter -func (t *StringFieldFilter) MergeStringFieldOContainsFilter(v StringFieldOContainsFilter) error { - b, err := json.Marshal(v) - if err != nil { - return err - } - - merged, err := runtime.JSONMerge(t.union, b) - t.union = merged - return err -} - -// AsStringFieldOEQFilter returns the union data inside the StringFieldFilter as a StringFieldOEQFilter -func (t StringFieldFilter) AsStringFieldOEQFilter() (StringFieldOEQFilter, error) { - var body StringFieldOEQFilter - err := json.Unmarshal(t.union, &body) - return body, err -} - -// FromStringFieldOEQFilter overwrites any union data inside the StringFieldFilter as the provided StringFieldOEQFilter -func (t *StringFieldFilter) FromStringFieldOEQFilter(v StringFieldOEQFilter) error { - b, err := json.Marshal(v) - t.union = b - return err -} - -// MergeStringFieldOEQFilter performs a merge with any union data inside the StringFieldFilter, using the provided StringFieldOEQFilter -func (t *StringFieldFilter) MergeStringFieldOEQFilter(v StringFieldOEQFilter) error { - b, err := json.Marshal(v) - if err != nil { - return err - } - - merged, err := runtime.JSONMerge(t.union, b) - t.union = merged - return err -} - -// AsStringFieldNEQFilter returns the union data inside the StringFieldFilter as a StringFieldNEQFilter -func (t StringFieldFilter) AsStringFieldNEQFilter() (StringFieldNEQFilter, error) { - var body StringFieldNEQFilter - err := json.Unmarshal(t.union, &body) - return body, err -} - -// FromStringFieldNEQFilter overwrites any union data inside the StringFieldFilter as the provided StringFieldNEQFilter -func (t *StringFieldFilter) FromStringFieldNEQFilter(v StringFieldNEQFilter) error { - b, err := json.Marshal(v) - t.union = b - return err -} - -// MergeStringFieldNEQFilter performs a merge with any union data inside the StringFieldFilter, using the provided StringFieldNEQFilter -func (t *StringFieldFilter) MergeStringFieldNEQFilter(v StringFieldNEQFilter) error { - b, err := json.Marshal(v) - if err != nil { - return err - } - - merged, err := runtime.JSONMerge(t.union, b) - t.union = merged - return err -} - -func (t StringFieldFilter) MarshalJSON() ([]byte, error) { - b, err := t.union.MarshalJSON() - return b, err -} - -func (t *StringFieldFilter) UnmarshalJSON(b []byte) error { - err := t.union.UnmarshalJSON(b) - return err -} - // ServerInterface represents all server handlers. type ServerInterface interface { // List apps @@ -6395,349 +6174,345 @@ func HandlerWithOptions(si ServerInterface, options ChiServerOptions) http.Handl // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", - "AP//HScheYsYAgA=", + "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", + "avlYUgcSAgA=", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/api/v3/api.go b/api/v3/api.go index 345f1e2ad7..673c602c36 100644 --- a/api/v3/api.go +++ b/api/v3/api.go @@ -1,8 +1,11 @@ //go:generate go tool github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen --config=codegen.yaml ./openapi.yaml package v3 -// FilterSingleString A filter for a single string field. -// TODO: This is a temporary solution to support the filter API. +import ( + "net/http" +) + +// FilterString A filter for a string field. type FilterString struct { // Contains The field must contain the provided value. Contains *string `json:"contains,omitempty"` @@ -19,3 +22,14 @@ type FilterString struct { // Oeq aasd Oeq *string `json:"oeq,omitempty"` } + +func (f *FilterString) ParseEq(name string, r *http.Request) { + if f == nil { + f = &FilterString{} + } + query := r.URL.Query() + eq := query.Get(name) + if eq != "" { + f.Eq = &eq + } +} diff --git a/api/v3/handlers/llmcost/convert.go b/api/v3/handlers/llmcost/convert.go index b4d902b7ca..68d1454d64 100644 --- a/api/v3/handlers/llmcost/convert.go +++ b/api/v3/handlers/llmcost/convert.go @@ -203,7 +203,7 @@ func validPriceSortField(field string) bool { // filterSingleStringToDomain converts an API FilterSingleString to the domain StringFilter. // Returns nil if the input is nil or empty. -func filterSingleStringToDomain(f *api.FilterSingleString) (*filters.StringFilter, error) { +func filterSingleStringToDomain(f *api.FilterString) (*filters.StringFilter, error) { if f == nil { return nil, nil } diff --git a/api/v3/handlers/llmcost/list_overrides.go b/api/v3/handlers/llmcost/list_overrides.go index 72bc9dd9ce..1074b44445 100644 --- a/api/v3/handlers/llmcost/list_overrides.go +++ b/api/v3/handlers/llmcost/list_overrides.go @@ -2,7 +2,9 @@ package llmcost import ( "context" + "encoding/json" "fmt" + "log/slog" "net/http" "github.com/samber/lo" @@ -53,24 +55,28 @@ func (h *handler) ListOverrides() ListOverridesHandler { // Filters if params.Filter != nil { - // provider, err := filterSingleStringToDomain(params.Filter.Provider) - // if err != nil { - // return req, err - // } - // req.Provider = provider + params.Filter.Provider.ParseEq("filter[provider]", r) + provider, err := filterSingleStringToDomain(params.Filter.Provider) + if err != nil { + return req, err + } + req.Provider = provider + params.Filter.ModelId.ParseEq("filter[model_id]", r) modelID, err := filterSingleStringToDomain(params.Filter.ModelId) if err != nil { return req, err } req.ModelID = modelID + params.Filter.ModelName.ParseEq("filter[model_name]", r) modelName, err := filterSingleStringToDomain(params.Filter.ModelName) if err != nil { return req, err } req.ModelName = modelName + params.Filter.Currency.ParseEq("filter[currency]", r) currency, err := filterSingleStringToDomain(params.Filter.Currency) if err != nil { return req, err @@ -78,6 +84,12 @@ func (h *handler) ListOverrides() ListOverridesHandler { req.Currency = currency } + j, err := json.Marshal(req) + if err != nil { + return req, err + } + slog.Info("req", "req", string(j)) + return req, nil }, func(ctx context.Context, request ListOverridesRequest) (ListOverridesResponse, error) { diff --git a/api/v3/handlers/llmcost/list_prices.go b/api/v3/handlers/llmcost/list_prices.go index b8cab2fdad..a18fb08782 100644 --- a/api/v3/handlers/llmcost/list_prices.go +++ b/api/v3/handlers/llmcost/list_prices.go @@ -73,35 +73,30 @@ func (h *handler) ListPrices() ListPricesHandler { req.Order = sort.Order.ToSortxOrder() } - j, err := json.Marshal(params.Filter) - if err != nil { - return req, err - } - - // query := r.URL.Query() - // filter := query.Get("filter") - - slog.Info("params.Filter", "filter", string(j)) // Filters if params.Filter != nil { - // provider, err := filterSingleStringToDomain(params.Filter.Provider) - // if err != nil { - // return req, err - // } - // req.Provider = provider + params.Filter.Provider.ParseEq("filter[provider]", r) + provider, err := filterSingleStringToDomain(params.Filter.Provider) + if err != nil { + return req, err + } + req.Provider = provider + params.Filter.ModelId.ParseEq("filter[model_id]", r) modelID, err := filterSingleStringToDomain(params.Filter.ModelId) if err != nil { return req, err } req.ModelID = modelID + params.Filter.ModelName.ParseEq("filter[model_name]", r) modelName, err := filterSingleStringToDomain(params.Filter.ModelName) if err != nil { return req, err } req.ModelName = modelName + params.Filter.Currency.ParseEq("filter[currency]", r) currency, err := filterSingleStringToDomain(params.Filter.Currency) if err != nil { return req, err @@ -109,6 +104,12 @@ func (h *handler) ListPrices() ListPricesHandler { req.Currency = currency } + j, err := json.Marshal(req) + if err != nil { + return req, err + } + slog.Info("req", "req", string(j)) + return req, nil }, func(ctx context.Context, request ListPricesRequest) (ListPricesResponse, error) { diff --git a/api/v3/oasmiddleware/decoder.go b/api/v3/oasmiddleware/decoder.go deleted file mode 100644 index 60e11b895d..0000000000 --- a/api/v3/oasmiddleware/decoder.go +++ /dev/null @@ -1,25 +0,0 @@ -package oasmiddleware - -import ( - "encoding/json" - "io" - "net/http" - - "github.com/getkin/kin-openapi/openapi3" - "github.com/getkin/kin-openapi/openapi3filter" -) - -// ref: https://github.com/getkin/kin-openapi/blob/994d4f01c1e8dd613805668a7c10b568547f7789/openapi3filter/req_resp_decoder.go#L1031-L1047 - -// JsonBodyDecoder is meant to be used with openapi3filter.RegisterBodyDecoder -// to register a decoder for a custom vendor type like "application/konnect.foo+json" -func JsonBodyDecoder(body io.Reader, _ http.Header, _ *openapi3.SchemaRef, _ openapi3filter.EncodingFn) (any, error) { - var value any - dec := json.NewDecoder(body) - dec.UseNumber() - if err := dec.Decode(&value); err != nil { - return nil, &openapi3filter.ParseError{Kind: openapi3filter.KindInvalidFormat, Cause: err} - } - - return value, nil -} diff --git a/api/v3/oasmiddleware/error.go b/api/v3/oasmiddleware/error.go index be0dc0d1db..20d6a32c1a 100644 --- a/api/v3/oasmiddleware/error.go +++ b/api/v3/oasmiddleware/error.go @@ -1,12 +1,9 @@ package oasmiddleware import ( - "errors" - "fmt" "strings" - "github.com/getkin/kin-openapi/openapi3" - "github.com/getkin/kin-openapi/openapi3filter" + validatorerrors "github.com/pb33f/libopenapi-validator/errors" "github.com/openmeterio/openmeter/api/v3/apierrors" ) @@ -18,126 +15,75 @@ var oasRuleToAip = map[string]string{ "maxItems": "max_items", } -func ToAipError(me openapi3.MultiError) []apierrors.InvalidParameter { - return aipMapper(me, nil) -} - -func aipMapper(me openapi3.MultiError, parent *apierrors.InvalidParameter) []apierrors.InvalidParameter { +// ToAipErrorFromLibopenapi converts libopenapi ValidationErrors to AIP InvalidParameters. +func ToAipErrorFromLibopenapi(errs []*validatorerrors.ValidationError) []apierrors.InvalidParameter { var ipErrs []apierrors.InvalidParameter - for _, err := range me { - var i *apierrors.InvalidParameter - if parent != nil { - i = parent - } else { - i = &apierrors.InvalidParameter{} + for _, ve := range errs { + if ve == nil { + continue } - switch err := err.(type) { - case *openapi3.SchemaError: - i.Reason = err.Reason - ipErrs = append(ipErrs, invalidParamFromSchemaError(err, i)) - case *openapi3filter.RequestError: - if err.Parameter != nil { - if err.Parameter.Name != "" { - i.Field = err.Parameter.Name - } - if err.Parameter.In != "" { - i.Source = apierrors.ToInvalid(err.Parameter.In) - } - if err.Parameter.Required { - i.Rule = "required" - } - } - i.Reason = err.Reason - if err.Reason == "" || err.RequestBody != nil { - i.Reason = err.Error() - } - - if err, ok := err.Err.(openapi3.MultiError); ok { - ipErrs = append(ipErrs, aipMapper(err, i)...) - continue + ip := apierrors.InvalidParameter{ + Field: ve.ParameterName, + Reason: ve.Reason, + Rule: ruleFromValidationError(ve), + Source: sourceFromValidationError(ve), + } + if ip.Field == "" && len(ve.SchemaValidationErrors) > 0 { + // Use field path from schema errors if no parameter name + sve := ve.SchemaValidationErrors[0] + if sve.FieldName != "" { + ip.Field = sve.FieldName + } else if sve.FieldPath != "" { + ip.Field = strings.TrimPrefix(strings.TrimPrefix(sve.FieldPath, "$."), "body.") } - - if err, ok := err.Err.(*openapi3.SchemaError); ok { - i.Choices = make([]string, 0) - if err.SchemaField == "enum" { - i.Rule = "enum" - for _, v := range err.Schema.Enum { - i.Choices = append(i.Choices, fmt.Sprintf("%v", v)) - } - i.Reason = fmt.Sprintf("must be one of: [%s]", strings.Join(i.Choices, ",")) - } else if err.SchemaField == "oneOf" { - ipErrs = append(ipErrs, collectFromSchemaError(err)...) - continue + } + if len(ve.SchemaValidationErrors) > 0 { + // Extract enum choices from schema validation errors if applicable + for _, sve := range ve.SchemaValidationErrors { + if sve.Reason != "" && ip.Reason == "" { + ip.Reason = sve.Reason } } - ipErrs = append(ipErrs, *i) } + ipErrs = append(ipErrs, ip) } return ipErrs } -// collectFromSchemaError looks at schemaErr.Origin. If there are deeper -// child errors (via unwrapOriginError), it returns those. Otherwise, it -// returns a single InvalidParameter built from schemaErr itself. -func collectFromSchemaError(se *openapi3.SchemaError) []apierrors.InvalidParameter { - childParams := unwrapOriginError(se) - if len(childParams) == 0 { - return []apierrors.InvalidParameter{ - invalidParamFromSchemaError(se, nil), - } +func ruleFromValidationError(ve *validatorerrors.ValidationError) string { + if r, ok := oasRuleToAip[ve.ValidationSubType]; ok { + return r } - return childParams -} - -// unwrapOriginError traverses schemaErr.Origin (which may be a wrapped multiErrorForOneOf) -// and returns a flat slice of InvalidParameter entries for each underlying *SchemaError. -func unwrapOriginError(schemaErr *openapi3.SchemaError) []apierrors.InvalidParameter { - if schemaErr == nil || schemaErr.Origin == nil { - return nil + if ve.ValidationSubType != "" { + return ve.ValidationSubType } - - // 1) First, try to pull out a MultiError (or multiErrorForOneOf) from the wrapper chain. - var me openapi3.MultiError - if errors.As(schemaErr.Origin, &me) { - var result []apierrors.InvalidParameter - for _, subErr := range me { - var subSE *openapi3.SchemaError - if errors.As(subErr, &subSE) { - result = append(result, collectFromSchemaError(subSE)...) - } - } - return result + if ve.ValidationType != "" { + return ve.ValidationType } - - // 2) If there are no multi-errors and Origin wraps another *SchemaError somewhere in its chain, dive into that. - var innerSE *openapi3.SchemaError - if errors.As(schemaErr.Origin, &innerSE) { - return collectFromSchemaError(innerSE) - } - - // 3) If we reach here, Origin was neither a nested *SchemaError nor a MultiError. - return nil + return "" } -func invalidParamFromSchemaError( - schemaErr *openapi3.SchemaError, - parent *apierrors.InvalidParameter, -) apierrors.InvalidParameter { - var ip *apierrors.InvalidParameter - if parent != nil { - ip = parent - } else { - ip = &apierrors.InvalidParameter{ - Reason: schemaErr.Reason, - } - } - if rule, ok := oasRuleToAip[schemaErr.SchemaField]; ok { - ip.Rule = rule - } else { - ip.Rule = schemaErr.SchemaField +func sourceFromValidationError(ve *validatorerrors.ValidationError) apierrors.InvalidParameterSource { + // libopenapi uses: path, query, header, cookie for parameter validation + switch strings.ToLower(ve.ValidationType) { + case "path": + return apierrors.InvalidParamSourcePath + case "query": + return apierrors.InvalidParamSourceQuery + case "header": + return apierrors.InvalidParamSourceHeader + case "requestbody", "schema": + return apierrors.InvalidParamSourceBody } - if path := schemaErr.JSONPointer(); len(path) > 0 { - ip.Field = strings.Join(path, ".") + switch strings.ToLower(ve.ValidationSubType) { + case "path": + return apierrors.InvalidParamSourcePath + case "query": + return apierrors.InvalidParamSourceQuery + case "header": + return apierrors.InvalidParamSourceHeader + case "requestbody", "schema": + return apierrors.InvalidParamSourceBody } - return *ip + return apierrors.InvalidParamSourceBody } diff --git a/api/v3/oasmiddleware/hook.go b/api/v3/oasmiddleware/hook.go index 480356a9f0..b2cda8d5ad 100644 --- a/api/v3/oasmiddleware/hook.go +++ b/api/v3/oasmiddleware/hook.go @@ -5,16 +5,13 @@ import ( "errors" "net/http" - "github.com/getkin/kin-openapi/openapi3" - "github.com/getkin/kin-openapi/openapi3filter" - "github.com/openmeterio/openmeter/api/v3/apierrors" ) var ErrRouteNotFound = errors.New("route not found") -// OasRouteNotFoundErrorHook handles the error when a route is not found in a validation -// router. This will stop the request lifecycle and return an AIP compliant 404 response +// OasRouteNotFoundErrorHook handles the error when a route is not found in validation. +// It stops the request lifecycle and returns an AIP-compliant 404 response. func OasRouteNotFoundErrorHook(err error, w http.ResponseWriter, r *http.Request) bool { if err != nil { apierrors. @@ -25,82 +22,38 @@ func OasRouteNotFoundErrorHook(err error, w http.ResponseWriter, r *http.Request return false } -// OasValidationErrorHook handles the error when a request is not matching the -// OAS spec definition for a given route in the validation router. -// This will stop the request lifecycle and return an AIP compliant 400 response +// OasValidationErrorHook handles the error when a request does not match the OAS spec. +// It stops the request lifecycle and returns an AIP-compliant 400 or 404 response. func OasValidationErrorHook(ctx context.Context, err error, w http.ResponseWriter, r *http.Request) bool { - switch err := err.(type) { - case nil: + if err == nil { return false - case openapi3.MultiError: - invalidParams := ToAipError(err) - sourcePath := false - for _, v := range invalidParams { - if v.Source == apierrors.InvalidParamSourcePath { - sourcePath = true - break - } - } - if sourcePath { - apierrors. - NewNotFoundError(ctx, err, "entity"). - HandleAPIError(w, r) - } else { - apierrors. - NewBadRequestError(ctx, SanitizeSensitiveFieldValues(err), invalidParams). - HandleAPIError(w, r) - } + } + + var ve *LibopenapiValidationErrors + if !errors.As(err, &ve) { + apierrors. + NewBadRequestError(ctx, err, nil). + HandleAPIError(w, r) return true - case *openapi3filter.RequestError: - if err.Parameter != nil && err.Parameter.In == "path" { - apierrors. - NewNotFoundError(ctx, err, "entity"). - HandleAPIError(w, r) - return true - } } - apierrors. - NewBadRequestError(ctx, err, nil). - HandleAPIError(w, r) - return true -} -func SanitizeSensitiveFieldValues(err error) error { - switch err := err.(type) { - case nil: - return nil - case openapi3.MultiError: - sanitizedMultiErr := make(openapi3.MultiError, 0) - for _, vErr := range err { - sanitizedMultiErr = append(sanitizedMultiErr, SanitizeSensitiveFieldValues(vErr)) + invalidParams := ToAipErrorFromLibopenapi(ve.Errors) + sourcePath := false + for _, v := range invalidParams { + if v.Source == apierrors.InvalidParamSourcePath { + sourcePath = true + break } - return sanitizedMultiErr - case *openapi3filter.RequestError: - err.Err = SanitizeSensitiveFieldValues(err.Err) - return err - case *openapi3.SchemaError: - if err.Schema != nil && err.Schema.Extensions != nil { - xSensitive, ok := err.Schema.Extensions["x-sensitive"] - if ok && isSensitive(xSensitive) { - err.Value = "********" - } - } - return err - default: - return err } -} -func isSensitive(sensitive any) bool { - switch v := sensitive.(type) { - case string: - if v == "true" { - return true - } - return false - case bool: - return v - default: - return false + if sourcePath { + apierrors. + NewNotFoundError(ctx, err, "entity"). + HandleAPIError(w, r) + } else { + apierrors. + NewBadRequestError(ctx, err, invalidParams). + HandleAPIError(w, r) } + return true } diff --git a/api/v3/oasmiddleware/router.go b/api/v3/oasmiddleware/router.go deleted file mode 100644 index 98de3d5320..0000000000 --- a/api/v3/oasmiddleware/router.go +++ /dev/null @@ -1,60 +0,0 @@ -package oasmiddleware - -import ( - "context" - - "github.com/getkin/kin-openapi/openapi3" - "github.com/getkin/kin-openapi/routers" - "github.com/getkin/kin-openapi/routers/gorillamux" -) - -// ValidationRouterOpts represents the options to be passed to the validation router for processing the requests. -type ValidationRouterOpts struct { - // DeleteServers removes the `Servers` property from the parsed OAS spec to be used within test or even at runtime. - // If you want to use it at runtime please read the doc for `ServerPrefix` first. - DeleteServers bool - // ServerPrefix adds a server entry with the desired prefix - // eg: the service expose domain.com/foo/v0/entity . Your spec path are defined at the /entity level and then - // /foo/v0 is part of the server entry in the OAS spec. If no prefix is provided, the validation router will either - // take the whole server entry like domain.com/foo/v0/entity to validate or if `DeleteServers` is to true it will - // only validate `/entity` - ServerPrefix string -} - -// NewValidationRouter creates a validation router to be injected in the middlewares -// to validate requests or responses. In a case of a bad spec it returns an error -func NewValidationRouter(ctx context.Context, doc *openapi3.T, opts *ValidationRouterOpts) (routers.Router, error) { - if opts == nil { - opts = &ValidationRouterOpts{ - DeleteServers: true, - } - } - - if opts.DeleteServers { - doc.Servers = nil - - for key, pathItem := range doc.Paths.Map() { - pathItem.Servers = nil - doc.Paths.Set(key, pathItem) - } - } - - if opts.ServerPrefix != "" { - doc.Servers = openapi3.Servers{ - &openapi3.Server{ - URL: opts.ServerPrefix, - }, - } - } - - if err := doc.Validate(ctx); err != nil { - return nil, err - } - - validationRouter, err := gorillamux.NewRouter(doc) - if err != nil { - return nil, err - } - - return validationRouter, err -} diff --git a/api/v3/oasmiddleware/validator.go b/api/v3/oasmiddleware/validator.go index 71d811c1f0..26ced23b58 100644 --- a/api/v3/oasmiddleware/validator.go +++ b/api/v3/oasmiddleware/validator.go @@ -2,55 +2,149 @@ package oasmiddleware import ( "bytes" + "encoding/json" + "errors" + "fmt" "io" "net/http" + "strings" - "github.com/getkin/kin-openapi/openapi3filter" - "github.com/getkin/kin-openapi/routers" + "github.com/pb33f/libopenapi" + validator "github.com/pb33f/libopenapi-validator" + validatorerrors "github.com/pb33f/libopenapi-validator/errors" + "gopkg.in/yaml.v3" ) type ( - RequestNotFoundHookFunc = func(error, http.ResponseWriter, *http.Request) bool - RequestValidationErrorFunc = func(error, http.ResponseWriter, *http.Request) bool + RequestNotFoundHookFunc = func(error, http.ResponseWriter, *http.Request) bool + RequestValidationErrorFunc = func(error, http.ResponseWriter, *http.Request) bool ResponseValidationFunc = func(error, *http.Request) ) -// ValidateRequestOption provides the hook functions and the openapi3filter -// option to be passed in to the underlying library +// ValidateRequestOption provides the hook functions for the validation middleware. type ValidateRequestOption struct { - // RouteNotFoundHook is called when the route is not found at the spec level - // if the hook returns `true` the request flow is stopped + // RouteNotFoundHook is called when the route is not found at the spec level. + // If the hook returns `true` the request flow is stopped. RouteNotFoundHook RequestNotFoundHookFunc // RouteValidationErrorHook is called when the route parameters or body are - // not validated. if the hook returns `true` the request flow is stopped + // not validated. If the hook returns `true` the request flow is stopped. RouteValidationErrorHook RequestValidationErrorFunc - // FilterOptions are the openapi3filter option to pass to the underlying lib - FilterOptions *openapi3filter.Options } -// ValidateRequest is the middleware to be used to validate the request to the spec -// passed in for the validation router -func ValidateRequest(validationRouter routers.Router, opts ValidateRequestOption) func(h http.Handler) http.Handler { +// ValidateResponseOption provides the hook function for response validation. +type ValidateResponseOption struct { + // ResponseValidationErrorHook is called when the route response body is not validated. + ResponseValidationErrorHook ResponseValidationFunc +} + +// NewValidator creates a libopenapi-validator from the given spec bytes and base URL. +// The baseURL is set as the server URL for path matching (e.g. /api/v3). +func NewValidator(specBytes []byte, baseURL string) (validator.Validator, error) { + patched, err := patchSpecServers(specBytes, baseURL) + if err != nil { + return nil, err + } + + document, err := libopenapi.NewDocument(patched) + if err != nil { + return nil, err + } + + v, errs := validator.NewValidator(document) + if len(errs) > 0 { + return nil, errors.Join(errs...) + } + return v, nil +} + +// patchSpecServers modifies the OpenAPI spec to set the servers URL for path matching. +func patchSpecServers(specBytes []byte, baseURL string) ([]byte, error) { + var spec map[string]any + if err := json.Unmarshal(specBytes, &spec); err != nil { + if err := yaml.Unmarshal(specBytes, &spec); err != nil { + return nil, fmt.Errorf("parse spec: %w", err) + } + } + + spec["servers"] = []map[string]any{ + {"url": baseURL}, + } + + return json.Marshal(spec) +} + +// filterQueryParamErrors removes validation errors for multi-level nested deepObject +// query params. libopenapi-validator has a known issue: it works for objects with +// depth of one but fails for nested objects (e.g. filter[provider][eq]=x). +// See https://github.com/pb33f/libopenapi-validator/issues/83 +func filterQueryParamErrors(errs []*validatorerrors.ValidationError) []*validatorerrors.ValidationError { + if len(errs) == 0 { + return errs + } + var filtered []*validatorerrors.ValidationError + for _, e := range errs { + if e == nil { + continue + } + // Only skip errors for object-type query params that failed schema validation + // (e.g. "The query parameter 'filter' is defined as an object, however it + // failed to pass a schema validation"). Simple query params are still validated. + msg := strings.ToLower(e.Message) + reason := strings.ToLower(e.Reason) + isQueryParam := strings.Contains(msg, "query parameter") || strings.Contains(reason, "query parameter") + isObjectSchema := strings.Contains(msg, "object") && strings.Contains(msg, "schema validation") || + strings.Contains(reason, "object") && strings.Contains(reason, "schema validation") + if isQueryParam && isObjectSchema { + continue + } + filtered = append(filtered, e) + } + return filtered +} + +// isRouteNotFound returns true if any validation error indicates path or operation not found. +func isRouteNotFound(errs []*validatorerrors.ValidationError) bool { + for _, e := range errs { + if e != nil && (e.IsPathMissingError() || e.IsOperationMissingError()) { + return true + } + } + return false +} + +// LibopenapiValidationErrors wraps libopenapi validation errors for use in hooks. +type LibopenapiValidationErrors struct { + Errors []*validatorerrors.ValidationError +} + +func (e *LibopenapiValidationErrors) Error() string { + if len(e.Errors) == 0 { + return "validation failed" + } + return e.Errors[0].Error() +} + +// ValidateRequest is the middleware to validate the request against the OpenAPI spec. +// Validation errors for multi-level nested deepObject query params are filtered out +// due to libopenapi-validator issue #83 (works for depth 1, fails for nested objects). +func ValidateRequest(v validator.Validator, opts ValidateRequestOption) func(h http.Handler) http.Handler { return func(h http.Handler) http.Handler { fn := func(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() skipServe := false - route, pathParams, err := validationRouter.FindRoute(r.WithContext(ctx)) - if err != nil { - if opts.RouteNotFoundHook != nil { - skipServe = opts.RouteNotFoundHook(err, w, r) - } - } else { - requestValidationInput := &openapi3filter.RequestValidationInput{ - Request: r, - PathParams: pathParams, - Route: route, - Options: opts.FilterOptions, + valid, validationErrors := v.ValidateHttpRequest(r) + if !valid { + // Filter out multi-level nested deepObject query param errors (issue #83). + validationErrors = filterQueryParamErrors(validationErrors) + if len(validationErrors) == 0 { + valid = true } - if err := openapi3filter.ValidateRequest(ctx, requestValidationInput); err != nil { - if opts.RouteValidationErrorHook != nil { - skipServe = opts.RouteValidationErrorHook(err, w, r) - } + } + if !valid { + err := &LibopenapiValidationErrors{Errors: validationErrors} + if opts.RouteNotFoundHook != nil && isRouteNotFound(validationErrors) { + skipServe = opts.RouteNotFoundHook(err, w, r) + } else if opts.RouteValidationErrorHook != nil { + skipServe = opts.RouteValidationErrorHook(err, w, r) } } if !skipServe { @@ -61,59 +155,27 @@ func ValidateRequest(validationRouter routers.Router, opts ValidateRequestOption } } -// ValidateResponseOption provides the hook function and the openapi3filter -// option to be passed in to the underlying library -type ValidateResponseOption struct { - // ResponseValidationErrorHook is called when the route response body is not validated - ResponseValidationErrorHook ResponseValidationFunc - // FilterOptions are the openapi3filter option to pass to the underlying lib - FilterOptions *openapi3filter.Options -} - -// ValidateResponse is the middleware to be used to validate the response to the spec -// passed in for the validation router -func ValidateResponse(validationRouter routers.Router, opts ValidateResponseOption) func(h http.Handler) http.Handler { +// ValidateResponse is the middleware to validate the response against the OpenAPI spec. +func ValidateResponse(v validator.Validator, opts ValidateResponseOption) func(h http.Handler) http.Handler { return func(h http.Handler) http.Handler { fn := func(w http.ResponseWriter, r *http.Request) { - var err error - - route, pathParams, err := validationRouter.FindRoute(r) - - if err != nil { - h.ServeHTTP(w, r) - if opts.ResponseValidationErrorHook != nil { - opts.ResponseValidationErrorHook(err, r) - } - } else { - // need to wrap std lib response to access the body - rww := NewResponseWriterWrapper(w) + rww := NewResponseWriterWrapper(w) + h.ServeHTTP(rww, r) - h.ServeHTTP(rww, r) + b := new(bytes.Buffer) + if _, err := b.ReadFrom(rww.Body()); err != nil { + return + } - b := new(bytes.Buffer) - _, err := b.ReadFrom(rww.Body()) - if err != nil { - return - } - bodyReader := bytes.NewReader(b.Bytes()) - - responseValidationInput := &openapi3filter.ResponseValidationInput{ - RequestValidationInput: &openapi3filter.RequestValidationInput{ - Request: r, - PathParams: pathParams, - Route: route, - Options: opts.FilterOptions, - }, - Header: rww.Header(), - Body: io.NopCloser(bodyReader), - Status: *rww.StatusCode(), - } + resp := &http.Response{ + StatusCode: *rww.StatusCode(), + Header: rww.Header(), + Body: io.NopCloser(bytes.NewReader(b.Bytes())), + } - if err := openapi3filter.ValidateResponse(r.Context(), responseValidationInput); err != nil { - if opts.ResponseValidationErrorHook != nil { - opts.ResponseValidationErrorHook(err, r) - } - } + valid, validationErrors := v.ValidateHttpResponse(r, resp) + if !valid && opts.ResponseValidationErrorHook != nil { + opts.ResponseValidationErrorHook(&LibopenapiValidationErrors{Errors: validationErrors}, r) } } return http.HandlerFunc(fn) diff --git a/api/v3/openapi.yaml b/api/v3/openapi.yaml index 1e896cadf4..9b371fd08b 100644 --- a/api/v3/openapi.yaml +++ b/api/v3/openapi.yaml @@ -3741,6 +3741,27 @@ components: type: string description: The dimensions the value is aggregated over. description: A row in the result of a feature cost query. + FilterSingleString: + type: object + properties: + eq: + type: string + description: The field must match the provided value. + oeq: + type: string + description: aasd + neq: + type: string + description: The field must not match the provided value. + contains: + type: string + description: The field must contain the provided value. + ocontains: + type: string + description: asd + description: |- + A filter for a single string field. + TODO: This is a temporary solution to support the filter API. ISO8601Duration: type: string pattern: ^P(?:\d+(?:\.\d+)?Y)?(?:\d+(?:\.\d+)?M)?(?:\d+(?:\.\d+)?W)?(?:\d+(?:\.\d+)?D)?(?:T(?:\d+(?:\.\d+)?H)?(?:\d+(?:\.\d+)?M)?(?:\d+(?:\.\d+)?S)?)?$ @@ -3952,22 +3973,29 @@ components: type: object properties: provider: - allOf: - - $ref: '#/components/schemas/StringFieldFilter' + anyOf: + - type: string + - $ref: '#/components/schemas/FilterSingleString' description: Filter by provider. e.g. ?filter[provider][eq]=openai x-go-type: FilterString model_id: - allOf: - - $ref: '#/components/schemas/StringFieldFilter' + anyOf: + - type: string + - $ref: '#/components/schemas/FilterSingleString' description: Filter by model ID. e.g. ?filter[model_id][eq]=gpt-4 + x-go-type: FilterString model_name: - allOf: - - $ref: '#/components/schemas/StringFieldFilter' + anyOf: + - type: string + - $ref: '#/components/schemas/FilterSingleString' description: Filter by model name. e.g. ?filter[model_name][contains]=gpt + x-go-type: FilterString currency: - allOf: - - $ref: '#/components/schemas/StringFieldFilter' + anyOf: + - type: string + - $ref: '#/components/schemas/FilterSingleString' description: Filter by currency code. e.g. ?filter[currency][eq]=USD + x-go-type: FilterString description: Filter options for listing LLM cost prices. Meter: type: object @@ -5008,96 +5036,6 @@ components: example: kong:trace:1234567890 detail: example: Gone - StringFieldEqualsFilter: - title: StringFieldEqualsFilter - description: Filters on the given string field value by exact match. - oneOf: - - type: string - - type: object - title: StringFieldEqualsComparison - additionalProperties: false - properties: - eq: - type: string - required: - - eq - x-examples: - example-1: equals-some-value - example-2: - eq: some-value - StringFieldContainsFilter: - title: StringFieldContainsFilter - description: Filters on the given string field value by fuzzy match. - type: object - additionalProperties: false - properties: - contains: - type: string - required: - - contains - x-examples: - example-1: - contains: some-value - StringFieldOContainsFilter: - title: StringFieldOContainsFilter - description: Returns entities that fuzzy-match any of the comma-delimited phrases in the filter string. - type: object - additionalProperties: false - properties: - ocontains: - type: string - required: - - ocontains - x-examples: - example-1: - ocontains: this-value,or-that-value - StringFieldOEQFilter: - title: StringFieldOEQFilter - description: Returns entities that exact match any of the comma-delimited phrases in the filter string. - type: object - additionalProperties: false - properties: - oeq: - type: string - required: - - oeq - x-examples: - example-1: - oeq: some-value,some-other-value - StringFieldNEQFilter: - title: StringFieldNEQFilter - description: Filters on the given string field value by exact match inequality. - type: object - additionalProperties: false - properties: - neq: - type: string - required: - - neq - x-examples: - example-1: - neq: not-this-value - StringFieldFilter: - title: StringFieldFilter - description: Filters on the given string field value by either exact or fuzzy match. - oneOf: - - $ref: '#/components/schemas/StringFieldEqualsFilter' - - $ref: '#/components/schemas/StringFieldContainsFilter' - - $ref: '#/components/schemas/StringFieldOContainsFilter' - - $ref: '#/components/schemas/StringFieldOEQFilter' - - $ref: '#/components/schemas/StringFieldNEQFilter' - x-examples: - example-1: equals-some-value - example-2: - eq: some-value - example-3: - contains: some-value - example-4: - ocontains: some-potential,value - example-5: - oeq: some-potential,value - example-6: - neq: not-this-value ConflictError: allOf: - $ref: '#/components/schemas/BaseError' diff --git a/api/v3/server/server.go b/api/v3/server/server.go index eb45e6441b..0c876b71ef 100644 --- a/api/v3/server/server.go +++ b/api/v3/server/server.go @@ -8,7 +8,6 @@ import ( "net/http" "github.com/getkin/kin-openapi/openapi3" - "github.com/getkin/kin-openapi/openapi3filter" "github.com/go-chi/chi/v5" "github.com/samber/lo" @@ -237,27 +236,20 @@ func NewServer(config *Config) (*Server, error) { } func (s *Server) RegisterRoutes(r chi.Router) error { - validationRouter, err := oasmiddleware.NewValidationRouter( - context.Background(), - s.swagger, - &oasmiddleware.ValidationRouterOpts{ - DeleteServers: true, - ServerPrefix: s.BaseURL, - }, - ) + specBytes, err := api.GetSpecBytes() + if err != nil { + return fmt.Errorf("get spec bytes: %w", err) + } + + validationValidator, err := oasmiddleware.NewValidator(specBytes, s.BaseURL) if err != nil { - return fmt.Errorf("create validation router: %w", err) + return fmt.Errorf("create validation validator: %w", err) } - validationMiddleware := oasmiddleware.ValidateRequest(validationRouter, oasmiddleware.ValidateRequestOption{ + validationMiddleware := oasmiddleware.ValidateRequest(validationValidator, oasmiddleware.ValidateRequestOption{ RouteNotFoundHook: oasmiddleware.OasRouteNotFoundErrorHook, - // RouteValidationErrorHook: func(err error, w http.ResponseWriter, r *http.Request) bool { - // return oasmiddleware.OasValidationErrorHook(r.Context(), err, w, r) - // }, - FilterOptions: &openapi3filter.Options{ - // No-op auth: auth is handled by other middleware. - AuthenticationFunc: openapi3filter.NoopAuthenticationFunc, - MultiError: true, + RouteValidationErrorHook: func(err error, w http.ResponseWriter, r *http.Request) bool { + return oasmiddleware.OasValidationErrorHook(r.Context(), err, w, r) }, }) diff --git a/api/v3/spec.go b/api/v3/spec.go new file mode 100644 index 0000000000..fbd3d58ad0 --- /dev/null +++ b/api/v3/spec.go @@ -0,0 +1,7 @@ +package v3 + +// GetSpecBytes returns the raw embedded OpenAPI specification bytes. +// Used by libopenapi for request/response validation. +func GetSpecBytes() ([]byte, error) { + return rawSpec() +} diff --git a/go.mod b/go.mod index ab15a854ab..ef0be9589e 100644 --- a/go.mod +++ b/go.mod @@ -46,6 +46,8 @@ require ( github.com/oklog/run v1.1.1-0.20240127200640-eee6e044b77c github.com/oklog/ulid/v2 v2.1.1 github.com/oliveagle/jsonpath v0.1.0 + github.com/pb33f/libopenapi v0.34.2 + github.com/pb33f/libopenapi-validator v0.13.1 github.com/peterbourgon/ctxdata/v4 v4.0.0 github.com/peterldowns/pgtestdb v0.1.1 github.com/prometheus/client_golang v1.23.2 @@ -102,7 +104,7 @@ require ( dario.cat/mergo v1.0.2 // indirect filippo.io/edwards25519 v1.1.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/storage/azdatalake v1.4.1 // indirect - github.com/BurntSushi/toml v1.5.0 // indirect + github.com/BurntSushi/toml v1.6.0 // indirect github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp v1.5.3 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0 // indirect @@ -112,6 +114,7 @@ require ( github.com/authzed/authzed-go v1.4.1 // indirect github.com/authzed/grpcutil v0.0.0-20250221190651-1985b19b35b8 // indirect github.com/awalterschulze/goderive v0.5.1 // indirect + github.com/basgys/goxml2json v1.1.1-0.20231018121955-e66ee54ceaad // indirect github.com/bhmj/xpression v0.9.4 // indirect github.com/bitfield/gotestdox v0.2.2 // indirect github.com/bmatcuk/doublestar v1.3.4 // indirect @@ -137,6 +140,7 @@ require ( github.com/go-git/go-git/v5 v5.16.2 // indirect github.com/go-jose/go-jose/v4 v4.1.3 // indirect github.com/go-openapi/jsonreference v0.20.2 // indirect + github.com/go-openapi/swag/jsonname v0.25.4 // indirect github.com/gofrs/uuid/v5 v5.3.2 // indirect github.com/google/gnostic-models v0.7.0 // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect @@ -159,6 +163,8 @@ require ( github.com/oapi-codegen/oapi-codegen/v2 v2.6.0 // indirect github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect + github.com/pb33f/jsonpath v0.8.1 // indirect + github.com/pb33f/ordered-map/v2 v2.3.0 // indirect github.com/pgvector/pgvector-go v0.3.0 // indirect github.com/pinecone-io/go-pinecone v1.1.1 // indirect github.com/pjbgf/sha1cd v0.4.0 // indirect @@ -169,6 +175,7 @@ require ( github.com/questdb/go-questdb-client/v3 v3.2.0 // indirect github.com/redpanda-data/connect/v4 v4.61.0 // indirect github.com/samber/slog-common v0.19.0 // indirect + github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect github.com/skeema/knownhosts v1.3.1 // indirect github.com/speakeasy-api/jsonpath v0.6.0 // indirect github.com/spiffe/go-spiffe/v2 v2.6.0 // indirect @@ -193,8 +200,9 @@ require ( go.opentelemetry.io/contrib/detectors/gcp v1.38.0 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/net v0.48.0 // indirect - golang.org/x/telemetry v0.0.0-20251203150158-8fff8a5912fc // indirect + go.yaml.in/yaml/v4 v4.0.0-rc.4 // indirect + golang.org/x/net v0.49.0 // indirect + golang.org/x/telemetry v0.0.0-20260109210033-bd525da824e2 // indirect gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect @@ -341,7 +349,7 @@ require ( github.com/go-logr/logr v1.4.3 github.com/go-logr/stdr v1.2.2 // indirect github.com/go-openapi/inflect v0.21.0 // indirect - github.com/go-openapi/jsonpointer v0.21.0 // indirect + github.com/go-openapi/jsonpointer v0.22.4 // indirect github.com/go-openapi/swag v0.23.0 // indirect github.com/go-sourcemap/sourcemap v2.1.4+incompatible // indirect github.com/go-sql-driver/mysql v1.9.3 // indirect @@ -510,14 +518,14 @@ require ( go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.1 // indirect golang.org/x/crypto v0.47.0 // indirect - golang.org/x/mod v0.31.0 // indirect + golang.org/x/mod v0.32.0 // indirect golang.org/x/oauth2 v0.34.0 // indirect golang.org/x/sync v0.19.0 golang.org/x/sys v0.40.0 // indirect golang.org/x/term v0.39.0 // indirect - golang.org/x/text v0.33.0 + golang.org/x/text v0.34.0 golang.org/x/time v0.12.0 // indirect - golang.org/x/tools v0.40.0 // indirect + golang.org/x/tools v0.41.0 // indirect golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect google.golang.org/api v0.247.0 // indirect google.golang.org/genproto v0.0.0-20250707201910-8d1bb00bc6a7 // indirect @@ -526,7 +534,7 @@ require ( gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect + gopkg.in/yaml.v3 v3.0.1 k8s.io/klog/v2 v2.130.1 modernc.org/libc v1.66.3 // indirect modernc.org/mathutil v1.7.1 // indirect diff --git a/go.sum b/go.sum index f50d201d07..5a143fd6f6 100644 --- a/go.sum +++ b/go.sum @@ -741,8 +741,8 @@ github.com/AzureAD/microsoft-authentication-library-for-go v1.0.0/go.mod h1:kgDm github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 h1:oygO0locgZJe7PpYPXT5A29ZkwJaPqcva7BVeemZOZs= github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= -github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= +github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/ClickHouse/ch-go v0.70.0 h1:/0lJpiSXxg/7IaJi7TOkKAOHrx0z0OiSMU475EJNAwM= github.com/ClickHouse/ch-go v0.70.0/go.mod h1:gk6B9UqB7UtvTNVruztrh6k85SlrIZiCCSfQFIxKU3s= @@ -977,6 +977,8 @@ github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuP github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= +github.com/basgys/goxml2json v1.1.1-0.20231018121955-e66ee54ceaad h1:3swAvbzgfaI6nKuDDU7BiKfZRdF+h2ZwKgMHd8Ha4t8= +github.com/basgys/goxml2json v1.1.1-0.20231018121955-e66ee54ceaad/go.mod h1:9+nBLYNWkvPcq9ep0owWUsPTLgL9ZXTsZWcCSVGGLJ0= github.com/beanstalkd/go-beanstalk v0.2.0 h1:6UOJugnu47uNB2jJO/lxyDgeD1Yds7owYi1USELqexA= github.com/beanstalkd/go-beanstalk v0.2.0/go.mod h1:/G8YTyChOtpOArwLTQPY1CHB+i212+av35bkPXXj56Y= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= @@ -992,6 +994,8 @@ github.com/bitfield/gotestdox v0.2.2 h1:x6RcPAbBbErKLnapz1QeAlf3ospg8efBsedU93CD github.com/bitfield/gotestdox v0.2.2/go.mod h1:D+gwtS0urjBrzguAkTM2wodsTQYFHdpx8eqRJ3N+9pY= github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932 h1:mXoPYz/Ul5HYEDvkta6I8/rnYM5gSdSV2tJ6XbZuEtY= github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932/go.mod h1:NOuUCSz6Q9T7+igc/hlvDOUdtWKryOrtFyIVABv/p7k= +github.com/bitly/go-simplejson v0.5.1 h1:xgwPbetQScXt1gh9BmoJ6j9JMr3TElvuIyjR8pgdoow= +github.com/bitly/go-simplejson v0.5.1/go.mod h1:YOPVLzCfwK14b4Sff3oP1AmGhI9T9Vsg84etUnlyp+Q= github.com/bits-and-blooms/bitset v1.22.0 h1:Tquv9S8+SGaS3EhyA+up3FXzmkhxPGjQQCkcs2uw7w4= github.com/bits-and-blooms/bitset v1.22.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= @@ -1344,13 +1348,17 @@ github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW github.com/go-openapi/inflect v0.21.0 h1:FoBjBTQEcbg2cJUWX6uwL9OyIW8eqc9k4KhN4lfbeYk= github.com/go-openapi/inflect v0.21.0/go.mod h1:INezMuUu7SJQc2AyR3WO0DqqYUJSj8Kb4hBd7WtjlAw= github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= -github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= -github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= +github.com/go-openapi/jsonpointer v0.22.4 h1:dZtK82WlNpVLDW2jlA1YCiVJFVqkED1MegOUy9kR5T4= +github.com/go-openapi/jsonpointer v0.22.4/go.mod h1:elX9+UgznpFhgBuaMQ7iu4lvvX1nvNsesQ3oxmYTw80= github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= +github.com/go-openapi/swag/jsonname v0.25.4 h1:bZH0+MsS03MbnwBXYhuTttMOqk+5KcQ9869Vye1bNHI= +github.com/go-openapi/swag/jsonname v0.25.4/go.mod h1:GPVEk9CWVhNvWhZgrnvRA6utbAltopbKwDu8mXNUMag= +github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls= +github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54= github.com/go-pdf/fpdf v0.5.0/go.mod h1:HzcnA+A23uwogo0tp9yU+l3V+KXhiESpt1PMayhOh5M= github.com/go-pdf/fpdf v0.6.0/go.mod h1:HzcnA+A23uwogo0tp9yU+l3V+KXhiESpt1PMayhOh5M= github.com/go-pg/pg/v10 v10.11.0 h1:CMKJqLgTrfpE/aOVeLdybezR2om071Vh38OLZjsyMI0= @@ -1394,8 +1402,8 @@ github.com/gocql/gocql v1.7.0/go.mod h1:vnlvXyFZeLBF0Wy+RS8hrOdbn0UWsWtdg07XJnFx github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 h1:ZpnhV/YsD2/4cESfV5+Hoeu/iUR3ruzNvZ+yQfO03a0= github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E= -github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0= +github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw= +github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0= github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA= github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= @@ -2045,6 +2053,14 @@ github.com/parquet-go/parquet-go v0.25.1/go.mod h1:AXBuotO1XiBtcqJb/FKFyjBG4aqa3 github.com/paulmach/orb v0.12.0 h1:z+zOwjmG3MyEEqzv92UN49Lg1JFYx0L9GpGKNVDKk1s= github.com/paulmach/orb v0.12.0/go.mod h1:5mULz1xQfs3bmQm63QEJA6lNGujuRafwA5S/EnuLaLU= github.com/paulmach/protoscan v0.2.1/go.mod h1:SpcSwydNLrxUGSDvXvO0P7g7AuhJ7lcKfDlhJCDw2gY= +github.com/pb33f/jsonpath v0.8.1 h1:84C6QRyx6HcSm6PZnsMpcqYot3IsZ+m0n95+0NbBbvs= +github.com/pb33f/jsonpath v0.8.1/go.mod h1:zBV5LJW4OQOPatmQE2QdKpGQJvhDTlE5IEj6ASaRNTo= +github.com/pb33f/libopenapi v0.34.2 h1:ValgPCDIVSC1IzPY7rY6GPOslCzaAWEml40IuFGZXOc= +github.com/pb33f/libopenapi v0.34.2/go.mod h1:YOP20KzYe3mhE5301aQzJtzQ9MnvhABBGO7RMttA4V4= +github.com/pb33f/libopenapi-validator v0.13.1 h1:KJimsXewLMIcM0O/wmfBswYJqZwQCkp37IVGfACp868= +github.com/pb33f/libopenapi-validator v0.13.1/go.mod h1:YZQRDh+8xap/H0GM0cJsBrqqT+XLlMivA/qwqRLiidQ= +github.com/pb33f/ordered-map/v2 v2.3.0 h1:k2OhVEQkhTCQMhAicQ3Z6iInzoZNQ7L9MVomwKBZ5WQ= +github.com/pb33f/ordered-map/v2 v2.3.0/go.mod h1:oe5ue+6ZNhy7QN9cPZvPA23Hx0vMHnNVeMg4fGdCANw= github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o= github.com/pborman/getopt v0.0.0-20180729010549-6fdd0a2c7117/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o= github.com/pebbe/zmq4 v1.4.0 h1:gO5P92Ayl8GXpPZdYcD62Cwbq0slSBVVQRIXwGSJ6eQ= @@ -2184,6 +2200,8 @@ github.com/samber/slog-common v0.19.0 h1:fNcZb8B2uOLooeYwFpAlKjkQTUafdjfqKcwcC89 github.com/samber/slog-common v0.19.0/go.mod h1:dTz+YOU76aH007YUU0DffsXNsGFQRQllPQh9XyNoA3M= github.com/samber/slog-multi v1.7.0 h1:GKhbkxU3ujkyMsefkuz4qvE6EcgtSuqjFisPnfdzVLI= github.com/samber/slog-multi v1.7.0/go.mod h1:qTqzmKdPpT0h4PFsTN5rYRgLwom1v+fNGuIrl1Xnnts= +github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ= +github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/secure-systems-lab/go-securesystemslib v0.4.0 h1:b23VGrQhTA8cN2CbBw7/FulN9fTtqYUdS5+Oxzt+DUE= github.com/secure-systems-lab/go-securesystemslib v0.4.0/go.mod h1:FGBZgq2tXWICsxWQW1msNf49F0Pf2Op5Htayx335Qbs= @@ -2540,6 +2558,8 @@ go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +go.yaml.in/yaml/v4 v4.0.0-rc.4 h1:UP4+v6fFrBIb1l934bDl//mmnoIZEDK0idg1+AIvX5U= +go.yaml.in/yaml/v4 v4.0.0-rc.4/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0= gocloud.dev v0.26.0/go.mod h1:mkUgejbnbLotorqDyvedJO20XcZNTynmSeVSQS9btVg= golang.org/x/crypto v0.0.0-20180723164146-c126467f60eb/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= @@ -2567,6 +2587,7 @@ golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= +golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.20.0/go.mod h1:Xwo95rrVNIoSMx9wa1JroENMToLWn3RNVrTBpLHgZPQ= @@ -2636,8 +2657,8 @@ golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI= -golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg= +golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c= +golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -2709,11 +2730,12 @@ golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.13.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= -golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= -golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= +golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= +golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -2872,6 +2894,7 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= @@ -2879,8 +2902,8 @@ golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= -golang.org/x/telemetry v0.0.0-20251203150158-8fff8a5912fc h1:bH6xUXay0AIFMElXG2rQ4uiE+7ncwtiOdPfYK1NK2XA= -golang.org/x/telemetry v0.0.0-20251203150158-8fff8a5912fc/go.mod h1:hKdjCMrbv9skySur+Nek8Hd0uJ0GuxJIoIX2payrIdQ= +golang.org/x/telemetry v0.0.0-20260109210033-bd525da824e2 h1:O1cMQHRfwNpDfDJerqRoE2oD+AFlyid87D40L/OkkJo= +golang.org/x/telemetry v0.0.0-20260109210033-bd525da824e2/go.mod h1:b7fPSJ0pKZ3ccUh8gnTONJxhn3c/PS6tyzQvyqw4iA8= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -2891,6 +2914,7 @@ golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o= golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= @@ -2913,12 +2937,13 @@ golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= -golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= -golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= +golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= +golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -3005,8 +3030,8 @@ golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= -golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA= -golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc= +golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc= +golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg= golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= From 1edb5762322a2c706cf44638ddfa30160fcb4807 Mon Sep 17 00:00:00 2001 From: mark-vass-konghq Date: Mon, 16 Mar 2026 16:03:35 +0100 Subject: [PATCH 3/4] feat(api): add basic implmentation and apply it to llmcost prices --- api/v3/filters/filter.go | 34 ++- api/v3/handlers/llmcost/list_prices.go | 130 +++++------ api/v3/request/aip.go | 293 +++++++++++++++++++++++++ api/v3/request/aip_filter.go | 230 +++++++++++++++++++ api/v3/request/aip_pagination.go | 146 ++++++++++++ api/v3/request/aip_sort.go | 58 +++++ api/v3/request/cursor.go | 146 ++++++++++++ go.mod | 14 +- go.sum | 67 +++--- 9 files changed, 1015 insertions(+), 103 deletions(-) create mode 100644 api/v3/request/aip.go create mode 100644 api/v3/request/aip_filter.go create mode 100644 api/v3/request/aip_pagination.go create mode 100644 api/v3/request/aip_sort.go create mode 100644 api/v3/request/cursor.go diff --git a/api/v3/filters/filter.go b/api/v3/filters/filter.go index fc2d0b2fb4..39691323b6 100644 --- a/api/v3/filters/filter.go +++ b/api/v3/filters/filter.go @@ -3,7 +3,6 @@ package filters import "errors" // StringFilter represents a filter operation on a string field. -// Exactly one of Eq, Neq, or Contains should be set. type StringFilter struct { // Eq requires the field to match the provided value exactly (case-insensitive). Eq *string `json:"eq,omitempty"` @@ -11,13 +10,43 @@ type StringFilter struct { // Neq requires the field to not match the provided value (case-insensitive). Neq *string `json:"neq,omitempty"` + // Gt requires the field to be greater than the provided value. + Gt *string `json:"gt,omitempty"` + + // Gte requires the field to be greater than or equal to the provided value. + Gte *string `json:"gte,omitempty"` + + // Lt requires the field to be less than the provided value. + Lt *string `json:"lt,omitempty"` + + // Lte requires the field to be less than or equal to the provided value. + Lte *string `json:"lte,omitempty"` + // Contains requires the field to contain the provided value (case-insensitive). Contains *string `json:"contains,omitempty"` + + // Oeq requires the field to match any of the provided comma-separated values (case-insensitive). + Oeq *string `json:"oeq,omitempty"` + + // Ocontains requires the field to contain any of the provided comma-separated values (case-insensitive). + Ocontains *string `json:"ocontains,omitempty"` + + // Exists requires the field to be present (true) or absent (false). + Exists *bool `json:"exists,omitempty"` } // IsEmpty returns true if no filter operator is set. func (f StringFilter) IsEmpty() bool { - return f.Eq == nil && f.Neq == nil && f.Contains == nil + return f.Eq == nil && + f.Neq == nil && + f.Gt == nil && + f.Gte == nil && + f.Lt == nil && + f.Lte == nil && + f.Contains == nil && + f.Oeq == nil && + f.Ocontains == nil && + f.Exists == nil } // Validate validates the filter. @@ -26,7 +55,6 @@ func (f StringFilter) Validate() error { return nil } - // Check for mutually exclusive filters if f.Eq != nil && f.Neq != nil { return errors.New("eq and neq cannot be set at the same time") } diff --git a/api/v3/handlers/llmcost/list_prices.go b/api/v3/handlers/llmcost/list_prices.go index a18fb08782..0f3b2da21e 100644 --- a/api/v3/handlers/llmcost/list_prices.go +++ b/api/v3/handlers/llmcost/list_prices.go @@ -2,9 +2,7 @@ package llmcost import ( "context" - "encoding/json" "fmt" - "log/slog" "net/http" "github.com/samber/lo" @@ -26,89 +24,79 @@ type ( ListPricesHandler = httptransport.HandlerWithArgs[ListPricesRequest, ListPricesResponse, ListPricesParams] ) +var listPricesAuthorizedFilters = map[string]request.AIPFilterOption{ + "provider": { + Filters: []request.QueryFilterOp{ + request.QueryFilterEQ, + request.QueryFilterNEQ, + request.QueryFilterContains, + request.QueryFilterOrContains, + }, + }, + "model_id": { + Filters: []request.QueryFilterOp{ + request.QueryFilterEQ, + request.QueryFilterNEQ, + request.QueryFilterContains, + }, + }, + "model_name": { + Filters: []request.QueryFilterOp{ + request.QueryFilterEQ, + request.QueryFilterNEQ, + request.QueryFilterContains, + }, + }, + "currency": { + Filters: []request.QueryFilterOp{ + request.QueryFilterEQ, + request.QueryFilterNEQ, + request.QueryFilterContains, + }, + }, +} + +var listPricesAuthorizedSorts = []string{ + "id", "provider.id", "model.id", "effective_from", "effective_to", +} + + func (h *handler) ListPrices() ListPricesHandler { return httptransport.NewHandlerWithArgs( - func(ctx context.Context, r *http.Request, params ListPricesParams) (ListPricesRequest, error) { + func(ctx context.Context, r *http.Request, _ ListPricesParams) (ListPricesRequest, error) { ns, err := h.resolveNamespace(ctx) if err != nil { return ListPricesRequest{}, err } - req := ListPricesRequest{ - Namespace: ns, - } - - // Pagination - req.Page = pagination.NewPage(1, 20) - - if params.Page != nil { - req.Page = pagination.NewPage( - lo.FromPtrOr(params.Page.Number, 1), - lo.FromPtrOr(params.Page.Size, 20), - ) - - if err := req.Page.Validate(); err != nil { - return req, apierrors.NewBadRequestError(ctx, err, apierrors.InvalidParameters{ - {Field: "page", Reason: err.Error(), Source: apierrors.InvalidParamSourceQuery}, - }) - } + attrs, err := request.GetAipAttributes(r, + request.WithDefaultPageSizeDefault(20), + request.WithMaxPageSize(100), + request.WithAuthorizedSorts(listPricesAuthorizedSorts), + request.WithAuthorizedFilters(listPricesAuthorizedFilters), + ) + if err != nil { + return ListPricesRequest{}, err } - // Sort - if params.Sort != nil { - sort, err := request.ParseSortBy(*params.Sort) - if err != nil { - return req, apierrors.NewBadRequestError(ctx, err, apierrors.InvalidParameters{ - {Field: "sort", Reason: err.Error(), Source: apierrors.InvalidParamSourceQuery}, - }) - } - - if !validPriceSortField(sort.Field) { - return req, apierrors.NewBadRequestError(ctx, fmt.Errorf("unsupported sort field: %s", sort.Field), apierrors.InvalidParameters{ - {Field: "sort", Reason: fmt.Sprintf("unsupported sort field %q, must be one of: id, provider.id, model.id, effective_from, effective_to", sort.Field), Source: apierrors.InvalidParamSourceQuery}, - }) - } - - req.OrderBy = sort.Field - req.Order = sort.Order.ToSortxOrder() + pageNumber := attrs.Pagination.Number + if pageNumber < 1 { + pageNumber = 1 } - // Filters - if params.Filter != nil { - params.Filter.Provider.ParseEq("filter[provider]", r) - provider, err := filterSingleStringToDomain(params.Filter.Provider) - if err != nil { - return req, err - } - req.Provider = provider - - params.Filter.ModelId.ParseEq("filter[model_id]", r) - modelID, err := filterSingleStringToDomain(params.Filter.ModelId) - if err != nil { - return req, err - } - req.ModelID = modelID - - params.Filter.ModelName.ParseEq("filter[model_name]", r) - modelName, err := filterSingleStringToDomain(params.Filter.ModelName) - if err != nil { - return req, err - } - req.ModelName = modelName - - params.Filter.Currency.ParseEq("filter[currency]", r) - currency, err := filterSingleStringToDomain(params.Filter.Currency) - if err != nil { - return req, err - } - req.Currency = currency + req := ListPricesRequest{ + Namespace: ns, + Page: pagination.NewPage(pageNumber, attrs.Pagination.Size), + Provider: request.FilterStringFromAip(attrs.Filters, "provider"), + ModelID: request.FilterStringFromAip(attrs.Filters, "model_id"), + ModelName: request.FilterStringFromAip(attrs.Filters, "model_name"), + Currency: request.FilterStringFromAip(attrs.Filters, "currency"), } - j, err := json.Marshal(req) - if err != nil { - return req, err + if len(attrs.Sorts) > 0 { + req.OrderBy = attrs.Sorts[0].Field + req.Order = attrs.Sorts[0].Order.ToSortxOrder() } - slog.Info("req", "req", string(j)) return req, nil }, diff --git a/api/v3/request/aip.go b/api/v3/request/aip.go new file mode 100644 index 0000000000..03ce8547c8 --- /dev/null +++ b/api/v3/request/aip.go @@ -0,0 +1,293 @@ +package request + +import ( + "net/http" + "strings" + + "github.com/openmeterio/openmeter/api/v3/filters" +) + +// GetAipAttributes return the AipAttributes found in the request query string +// if the parser is set to Strict using `AipSetStrictMode` is apierrors out +// with a `BaseApiError` +func GetAipAttributes(r *http.Request, opts ...AipParseOption) (*AipAttributes, error) { + a := &AipAttributes{} + + conf := newConfig() + for _, v := range opts { + v(conf) + } + + queryValues := r.URL.Query() + + pagination, err := extractPagination(r.Context(), queryValues, conf) + if err != nil { + return nil, err + } + a.Pagination = pagination + + filters, err := extractFilter(r.Context(), queryValues, conf) + if err != nil { + return nil, err + } + a.Filters = filters + + sort, sortErr := extractSort(queryValues, conf) + if sortErr != nil { + return nil, sortErr + } + + a.Sorts = sort + + return a, nil +} + +// RemapAipAttributes remaps the filters and sorts to another name +// this is used when API is not inlined with the database entities +func RemapAipAttributes(attrs *AipAttributes, mappedAttributes map[string]string) { + if attrs.Filters != nil { + for k, f := range attrs.Filters { + if _, ok := mappedAttributes[f.Name]; ok { + attrs.Filters[k].Name = mappedAttributes[f.Name] + continue + } + parts := strings.SplitN(f.Name, ".", 2) // allow filters[known_custom_field.unknown_key] + if _, ok := mappedAttributes[parts[0]]; ok { + if len(parts) == 2 { + attrs.Filters[k].Name = mappedAttributes[parts[0]] + "." + parts[1] + } else { + attrs.Filters[k].Name = mappedAttributes[f.Name] + } + } + } + } + if attrs.Sorts != nil { + for k, s := range attrs.Sorts { + if _, ok := mappedAttributes[s.Field]; ok { + attrs.Sorts[k].Field = mappedAttributes[s.Field] + continue + } + parts := strings.SplitN(s.Field, ".", 2) // allow filters[known_custom_field.unknown_key] + if _, ok := mappedAttributes[parts[0]]; ok { + if len(parts) == 2 { + attrs.Sorts[k].Field = mappedAttributes[parts[0]] + "." + parts[1] + } else { + attrs.Sorts[k].Field = mappedAttributes[s.Field] + } + } + } + } +} + +type AipAttributes struct { + Pagination Pagination + Filters []QueryFilter + Sorts []SortBy +} + +type paginationKind int + +const ( + paginationKindOffset paginationKind = iota + paginationKindCursor +) +const ( + defaultPaginationMaxSize = 100 +) + +type config struct { + strictMode bool + defaultPageSize int + maxPageSize int + paginationKind paginationKind + cursorValidateUUIDs bool + cursorCipherKey string + defaultSort *defaultSort + authorizedFilters AuthorizedFilters + authorizedSorts []string + authorizedDotSorts []string +} + +func newConfig() *config { + return &config{ + maxPageSize: defaultPaginationMaxSize, + cursorValidateUUIDs: false, + cursorCipherKey: DefaultCipherKey, + strictMode: false, + defaultPageSize: DefaultPaginationSize, + paginationKind: paginationKindOffset, + } +} + +type AipParseOption func(*config) + +// WithAipStrictMode sets the parser a Strict, which means when some fallbackable +// arguments like page[size] or page[number] are invalid, the parser will return +// a 400 baseApiError instead of processing the request with default pagination size. +func WithAipStrictMode() AipParseOption { + return func(c *config) { + c.strictMode = true + } +} + +// WithCursorPagination sets the AIP request parser to only take the cursor AIP +// attributes in consideration and will ignore other kinds of paginations +func WithCursorPagination() AipParseOption { + return func(c *config) { + c.paginationKind = paginationKindCursor + } +} + +// WithCursorPagination sets the AIP request parser to only take the offset AIP +// attributes in consideration and will ignore other kinds of paginations. +// +// This is the default parser behavior +func WithOffsetPagination() AipParseOption { + return func(c *config) { + c.paginationKind = paginationKindOffset + } +} + +// WithDefaultPageSizeDefault sets the AIP request parser default page size. +// This value is used when the client is not setting the page[size] querystring +// or when the page[size] attribute is not valid and the parser is not using +// strict mode +// +// Default value is 20 +func WithDefaultPageSizeDefault(value int) AipParseOption { + return func(c *config) { + c.defaultPageSize = value + } +} + +// WithDefaultSort sets the default sort parameter if none is declared +// in the incoming request +func WithDefaultSort(field string, order SortOrder) AipParseOption { + return func(c *config) { + c.defaultSort = &defaultSort{ + field: field, + order: order, + } + } +} + +// WithCursorValidateUUIDs makes the AIP request parser to validate every UUID +// passed within a cursor in page[before] or page[after]. +func WithCursorValidateUUIDs() AipParseOption { + return func(c *config) { + c.cursorValidateUUIDs = true + } +} + +// WithCursorCipherKey sets the cipher key used with the cursor pagination encoding +// and decoding methods +// +// by default the aip request parser uses the request.DefaultCipherKey value +func WithCursorCipherKey(key string) AipParseOption { + return func(c *config) { + c.cursorCipherKey = key + } +} + +// WithAuthorizedFilters defines the set of filters that the parser should parse +// other filters are ignored +// +// by default the parser takes all the filters that are passed to it +// Use the DotFilter parameter for filters that have unknown sub-attributes (filters[labels.key_1]=true) +func WithAuthorizedFilters(fields map[string]AIPFilterOption) AipParseOption { + return func(c *config) { + c.authorizedFilters = fields + } +} + +// WithAuthorizedSorts defines the set of sorts that the parser should parse +// other filters are ignored +// +// by default the parser takes all the sorts that are passed to it +// do not use dot notation (field.subfield) with this method, use WithAuthorizedSorts instead. +func WithAuthorizedSorts(fields []string) AipParseOption { + return func(c *config) { + c.authorizedSorts = fields + } +} + +// WithAuthorizedDotSorts is equivalent to WithAuthorizedSorts but allows +// sorting on user-defined sub-attributes. +// +// examples: +// "foo" allows ?sort=foo.bar or ?sort=foo.baz. +// "foo.bar" only allows ?sort=foo.bar. +// "foo" rejects ?sort=foo because it doesn't have a sub-attribute. +func WithAuthorizedDotSorts(fields []string) AipParseOption { + return func(c *config) { + c.authorizedDotSorts = fields + } +} + +// WithMaxPageSize defines the maximum size of the pagination +// +// by default the parser sets it to 100 +func WithMaxPageSize(size int) AipParseOption { + return func(c *config) { + c.maxPageSize = size + } +} + +// ValidationFunc represents the validation function on a field. If it returns +// false it means the validation hasn't passed. The string return parameter is +// used as error message in the apierror +type ValidationFunc func(field, value string) error + +// AuthorizedFilters reprensents the map of fields that are authorized to be +// filtered on +type AuthorizedFilters map[string]AIPFilterOption + +// AIPFilterOption defines the list of available filters for a giving field +// and its optional validation function +type AIPFilterOption struct { + Filters []QueryFilterOp + ValidationFunc ValidationFunc + DotFilter bool +} + +// FilterStringFromAip extracts a *filters.StringFilter for the named field from AIP query filters. +// Returns nil if no matching filters are found. +func FilterStringFromAip(queryFilters []QueryFilter, field string) *filters.StringFilter { + var f filters.StringFilter + found := false + for _, qf := range queryFilters { + if qf.Name != field { + continue + } + found = true + v := qf.Value + switch qf.Filter { + case QueryFilterEQ: + f.Eq = &v + case QueryFilterNEQ: + f.Neq = &v + case QueryFilterGT: + f.Gt = &v + case QueryFilterGTE: + f.Gte = &v + case QueryFilterLT: + f.Lt = &v + case QueryFilterLTE: + f.Lte = &v + case QueryFilterContains: + f.Contains = &v + case QueryFilterOrEQ: + f.Oeq = &v + case QueryFilterOrContains: + f.Ocontains = &v + case QueryFilterExists: + t := true + f.Exists = &t + } + } + if !found || f.IsEmpty() { + return nil + } + return &f +} + diff --git a/api/v3/request/aip_filter.go b/api/v3/request/aip_filter.go new file mode 100644 index 0000000000..e02ed0fe91 --- /dev/null +++ b/api/v3/request/aip_filter.go @@ -0,0 +1,230 @@ +package request + +import ( + "context" + "errors" + "fmt" + "net/url" + "slices" + "strings" + + "github.com/openmeterio/openmeter/api/v3/apierrors" +) + +// ErrReturnEmptySet this error is used as underlying error as a signal to HandleAPIError that it should return an empty set. +var ErrReturnEmptySet = errors.New("should return an empty set") + +var ( + filterMap = map[string]QueryFilterOp{ + "oeq": QueryFilterOrEQ, + "eq": QueryFilterEQ, + "neq": QueryFilterNEQ, + "gt": QueryFilterGT, + "gte": QueryFilterGTE, + "lt": QueryFilterLT, + "lte": QueryFilterLTE, + "contains": QueryFilterContains, + "ocontains": QueryFilterOrContains, + "exists": QueryFilterExists, + } + ErrUnallowedFilterColumn = errors.New("unallowed filtering column") + ErrUnallowedFilterMethod = errors.New("unallowed filtering method") +) + +func filterName(value QueryFilterOp) string { + for k, v := range filterMap { + if v == value { + return k + } + } + return "" +} + +const ( + FilterQuery = "filter" + + // filter[field][eq] + QueryFilterEQ QueryFilterOp = iota + // filter[field][neq] + QueryFilterNEQ + // filter[field][gt] + QueryFilterGT + // filter[field][gte] + QueryFilterGTE + // filter[field][lt] + QueryFilterLT + // filter[field][lte] + QueryFilterLTE + // filter[field][contains] + QueryFilterContains + // filter[field] + QueryFilterExists + // filter[field][oeq] + QueryFilterOrEQ + // filter[field][ocontains] + QueryFilterOrContains +) + +var ( + // lookup to only focus filter[foo] and not filterfoo[bar] + prefixLookup = FilterQuery + "[" +) + +// QueryFilter column filter +type QueryFilter struct { + Name string + Path *string + Value string + Values []string + Filter QueryFilterOp +} + +type QueryFilterOp int + +func extractFilter(ctx context.Context, qs url.Values, c *config) ([]QueryFilter, *apierrors.BaseAPIError) { + var out []QueryFilter + + for i, v := range qs { + if !strings.HasPrefix(i, prefixLookup) { + continue + } + for _, filter := range v { + o, err := parseFilterQs(ctx, filter, i) + if err != nil { + return nil, err + } + + // no field name provided is an invalid query filter + if o.Name == "" { + continue + } + + // if there is value that means we're falling back on + // EXIST query filter + if filter == "" { + o.Filter = QueryFilterExists + } + + o.Value = filter + + checkFilters := c.authorizedFilters != nil + var ok bool + var filters AIPFilterOption + + if checkFilters && strings.ContainsRune(o.Name, '.') { + parts := strings.SplitN(o.Name, ".", 2) // allow filters[known_custom_field.unknown_key] + filters, ok = c.authorizedFilters[parts[0]] + if !ok { + filters, ok = c.authorizedFilters[o.Name] // specific case where only 1 field is allowed + } + ok = ok && filters.DotFilter + } else if checkFilters { + filters, ok = c.authorizedFilters[o.Name] + ok = ok && !filters.DotFilter // forbid using whole field for dot filters + } + + if checkFilters { + if !ok { + if c.strictMode { + return nil, apierrors.NewBadRequestError(ctx, ErrUnallowedFilterMethod, + apierrors.InvalidParameters{ + apierrors.InvalidParameter{ + Field: o.Name, + Reason: "unauthorized filter", + Source: apierrors.InvalidParamSourceQuery, + Rule: "unknown_property", + }, + }) + } + continue + } + if !slices.Contains(filters.Filters, o.Filter) { + if c.strictMode { + return nil, apierrors.NewBadRequestError(ctx, ErrUnallowedFilterColumn, + apierrors.InvalidParameters{ + apierrors.InvalidParameter{ + Field: filterName(o.Filter), + Reason: "unauthorized filter on column", + Source: apierrors.InvalidParamSourceQuery, + Rule: "unknown_property", + }, + }) + } + continue + } + if filters.ValidationFunc != nil { + if err := filters.ValidationFunc(o.Name, o.Value); err != nil { + if errors.Is(err, ErrReturnEmptySet) { + // for errors in uuid format, we want to handle it by returning an empty list. + return nil, apierrors.NewEmptySetResponse(ctx, c.paginationKind == paginationKindCursor) + } + return nil, apierrors.NewBadRequestError(ctx, ErrUnallowedFilterColumn, + apierrors.InvalidParameters{ + apierrors.InvalidParameter{ + Field: filter, + Reason: err.Error(), + Source: apierrors.InvalidParamSourceQuery, + Rule: "unauthorized filter on column", + }, + }) + } + } + } + + if o.Filter == QueryFilterOrEQ || o.Filter == QueryFilterOrContains { + o.Values = parseMultipleStringValues(o.Value) + } + out = append(out, o) + } + } + + return out, nil +} + +func parseMultipleStringValues(strValue string) []string { + var out []string + for _, v := range strings.Split(strValue, ",") { + out = append(out, strings.TrimSpace(v)) + } + return out +} + +func parseFilterQs(ctx context.Context, filter, qs string) (QueryFilter, *apierrors.BaseAPIError) { + o := QueryFilter{} + i := strings.IndexRune(qs, '[') + if i == -1 { + return o, nil + } + + endFirst := strings.IndexRune(qs, ']') + if endFirst == -1 { + return o, nil + } + o.Filter = QueryFilterEQ + o.Name = qs[i+1 : endFirst] + + qsRest := qs[endFirst+1:] + + if len(qsRest) > 0 { + start := strings.IndexRune(qsRest, '[') + end := strings.IndexRune(qsRest, ']') + op := qsRest[start+1 : end] + if len(op) > 0 { + if queryOp, ok := filterMap[op]; ok { + o.Filter = queryOp + } else { + return QueryFilter{}, apierrors.NewBadRequestError(ctx, ErrUnallowedFilterColumn, + apierrors.InvalidParameters{ + apierrors.InvalidParameter{ + Field: filter, + Reason: fmt.Sprintf("invalid operation '%s' on filter", op), + Source: apierrors.InvalidParamSourceQuery, + Rule: "unauthorized filter on column", + }, + }) + } + } + } + + return o, nil +} diff --git a/api/v3/request/aip_pagination.go b/api/v3/request/aip_pagination.go new file mode 100644 index 0000000000..0bd8c4b563 --- /dev/null +++ b/api/v3/request/aip_pagination.go @@ -0,0 +1,146 @@ +package request + +import ( + "context" + "errors" + "fmt" + "net/url" + "strconv" + + "github.com/openmeterio/openmeter/api/v3/apierrors" +) + +const ( + PageNumberQuery = "page[number]" + + // offset pagination specific + PageSizeQuery = "page[size]" + + // cursor pagination specific + PageBeforeQuery = "page[before]" + PageAfterQuery = "page[after]" + + DefaultPaginationNumber = 1 + DefaultPaginationSize = 20 +) + +var ( + ErrCursorUndefined = errors.New("at least before or after cursor need to be defined") + ErrCursorRange = errors.New("range pagination not supported, both before and after cursor were defined") +) + +type Pagination struct { + Size int + Number int + Offset int + Limit int + After *Cursor + Before *Cursor +} + +func extractPagination(ctx context.Context, qs url.Values, c *config) (Pagination, *apierrors.BaseAPIError) { + p := Pagination{ + Size: c.defaultPageSize, + } + + if qs.Has(PageSizeQuery) { + strPageSize := qs.Get(PageSizeQuery) + pageSize, err := strconv.ParseInt(strPageSize, 10, 16) + if err != nil { + if c.strictMode || pageSize < 0 { + return p, apierrors.NewBadRequestError(ctx, err, + apierrors.InvalidParameters{ + apierrors.InvalidParameter{ + Field: PageSizeQuery, + Reason: "unable to parse query field", + Source: apierrors.InvalidParamSourceQuery, + Rule: "page size should be a positive integer", + }, + }) + } else { + pageSize = int64(c.defaultPageSize) + } + } + if pageSize < 1 { + pageSize = DefaultPaginationSize + } + p.Size = int(pageSize) + } + + if p.Size > c.maxPageSize { + p.Size = c.maxPageSize + } + + if c.paginationKind == paginationKindOffset { + p.Number = DefaultPaginationNumber + + if qs.Has(PageNumberQuery) { + strPageNumber := qs.Get(PageNumberQuery) + pageNumber, err := strconv.ParseInt(strPageNumber, 10, 16) + if err != nil { + if c.strictMode || pageNumber < 0 { + return p, apierrors.NewBadRequestError(ctx, err, + apierrors.InvalidParameters{ + apierrors.InvalidParameter{ + Field: PageNumberQuery, + Reason: "unable to parse query field", + Source: apierrors.InvalidParamSourceQuery, + Rule: "page number should be a positive integer", + }, + }) + } + } + if pageNumber < 1 { + pageNumber = DefaultPaginationNumber + } + p.Number = int(pageNumber) + } + + var coef int + coef = int(p.Number) - 1 + if coef < 0 { + coef = 0 + } + p.Offset = coef * p.Size + p.Limit = p.Size + } else if c.paginationKind == paginationKindCursor { + if qs.Has(PageBeforeQuery) && qs.Has(PageAfterQuery) { + return p, apierrors.NewBadRequestError(ctx, ErrCursorRange, + apierrors.InvalidParameters{ + apierrors.InvalidParameter{ + Source: apierrors.InvalidParamSourceQuery, + Reason: "api doesn't support range pagination", + }, + }) + } + + if qs.Has(PageBeforeQuery) { + b, err := decodeCursorAfterQueryUnescape(c.cursorCipherKey, qs.Get(PageBeforeQuery), c.cursorValidateUUIDs) + if err != nil { + return p, apierrors.NewBadRequestError(ctx, err, + apierrors.InvalidParameters{ + apierrors.InvalidParameter{ + Source: apierrors.InvalidParamSourceQuery, + Reason: fmt.Sprintf("unable to parse %s cursor", PageBeforeQuery), + }, + }) + } + p.Before = b + } + if qs.Has(PageAfterQuery) { + a, err := decodeCursorAfterQueryUnescape(c.cursorCipherKey, qs.Get(PageAfterQuery), c.cursorValidateUUIDs) + if err != nil { + return p, apierrors.NewBadRequestError(ctx, err, + apierrors.InvalidParameters{ + apierrors.InvalidParameter{ + Source: apierrors.InvalidParamSourceQuery, + Reason: fmt.Sprintf("unable to parse %s cursor", PageAfterQuery), + }, + }) + } + p.After = a + } + } + + return p, nil +} diff --git a/api/v3/request/aip_sort.go b/api/v3/request/aip_sort.go new file mode 100644 index 0000000000..8b4ffeb7c0 --- /dev/null +++ b/api/v3/request/aip_sort.go @@ -0,0 +1,58 @@ +package request + +import ( + "net/url" + "slices" + "strings" +) + +const SortQuery = "sort" + +type defaultSort struct { + field string + order SortOrder +} + +func extractSort(qs url.Values, c *config) ([]SortBy, error) { + if !qs.Has(SortQuery) { + if c.defaultSort == nil { + return nil, nil + } + return []SortBy{{Field: c.defaultSort.field, Order: c.defaultSort.order}}, nil + } + + segments := strings.Split(qs.Get(SortQuery), ",") + out := make([]SortBy, 0, len(segments)) + for _, v := range segments { + parts := strings.Fields(strings.TrimSpace(v)) + if len(parts) == 0 { + continue + } + sortBy := SortBy{Field: parts[0], Order: SortOrderAsc} + if len(parts) > 1 { + order := SortOrder(parts[1]) + if order == SortOrderAsc || order == SortOrderDesc { + sortBy.Order = order + } + } + if isAuthorizedSort(sortBy.Field, c) { + out = append(out, sortBy) + } + } + return out, nil +} + +func isAuthorizedSort(field string, c *config) bool { + checkSorts := len(c.authorizedSorts) != 0 + checkDotSorts := len(c.authorizedDotSorts) != 0 + switch { + case !checkDotSorts && !checkSorts: + return true + case checkDotSorts && strings.ContainsRune(field, '.'): + parts := strings.SplitN(field, ".", 2) + return slices.Contains(c.authorizedDotSorts, parts[0]) || slices.Contains(c.authorizedDotSorts, field) + case checkSorts: + return slices.Contains(c.authorizedSorts, field) + } + return false +} diff --git a/api/v3/request/cursor.go b/api/v3/request/cursor.go new file mode 100644 index 0000000000..cd95089ea2 --- /dev/null +++ b/api/v3/request/cursor.go @@ -0,0 +1,146 @@ +package request + +import ( + "encoding/base64" + "errors" + "net/url" + "strings" + + "github.com/google/uuid" +) + +// ErrInvalidCursor is used when a cursor does not conform to the expected format. +var ErrInvalidCursor = errors.New("invalid pagination cursor provided") + +const ( + DefaultCipherKey = "Oh hai there! I was originally written in Koko, the super awesome control plane for Kong Gateway! AND VOILA" + cursorVersion = "1" +) + +// Cursor is a representation of an object's ID, as a cursor should +// be opaque and that its format should not be relied upon. +// +// A cursor is used to implement keyset pagination within a database. +// +// Under the hood, this uses a simple XOR cipher. +type Cursor struct{ encoded, decoded, version string } + +// String implements fmt.Stringer & returns the encoded cursor representation of the ID. +func (c *Cursor) String() string { return c.encoded } + +// ID decodes the provided cursor (during instantiation) and returns its representation +// (usually set to a UUID or [UUID:]UUID). If there is no cursor, an empty string is returned. +func (c *Cursor) ID() string { + if c == nil { + return "" + } + return c.decoded +} + +// xorText is a simple XOR cipher implementation. +// Read more: https://en.wikipedia.org/wiki/XOR_cipher +func xorText(cipherKey, input string) string { + var output string + keyLen := len(cipherKey) + for i := range input { + output += string(input[i] ^ cipherKey[i%keyLen]) + } + return output +} + +// EncodeCursor instantiates a new Cursor from an object's ID. A cursor ID can +// be one or more UUIDs separated by the colon char. +// +// You must only provide an ASCII string. If UTF-8 is used (e.g.: graphics), decoding will +// not function properly. There are no error checks for this due to performance reasons. +// +// ErrInvalidCursor be returned when the provided ID is empty. +func EncodeCursor(cipherKey, id string) (*Cursor, error) { + if id == "" { + return nil, ErrInvalidCursor + } + + c := Cursor{ + encoded: xorText(cipherKey, id), + decoded: id, + version: cursorVersion, + } + + // Inject the version into the XOR'ed string. + versionIdx := getVersionIdx(c.encoded) + c.encoded = c.encoded[:versionIdx] + c.version + c.encoded[versionIdx:] + + // Base64 encoding for ASCII compatibility. + c.encoded = base64.StdEncoding.EncodeToString([]byte(c.encoded)) + + // we need to pass the base64 encoded string through QueryEscape as the encoded string will contain + // certain characters which are not valid in a URL parser. + // Example : when the id input is `01960a8d-eccd-72c0-935e-a87370362c2a` + // the base64 encoded string is `f1kZXlEIGBBFABEGRQ1+EhRRMV4ZXEcMSghWVl9bSRNBQApGFQ==` + // The + char in the above string is not valid in a URL parser. + c.encoded = url.QueryEscape(c.encoded) + + return &c, nil +} + +// DecodeCursor instantiates a new Cursor from an encoded cursor value with query escaped value. +// +// Returns ErrInvalidCursor when an invalid cursor is provided (and optionally +// validates the decoded value as a UUID when validateAsUUID is true). +func DecodeCursor(cipherKey, cursor string, validateAsUUID bool) (*Cursor, error) { + if cursor == "" { + return nil, ErrInvalidCursor + } + + unescapeCursor, err := url.QueryUnescape(cursor) + if err != nil { + return nil, ErrInvalidCursor + } + + return decodeCursorAfterQueryUnescape(cipherKey, unescapeCursor, validateAsUUID) +} + +// decodeCursorAfterQueryUnescape instantiates a new Cursor from already unescaped cursor value. +// This function is needed because GetAipAttributes function already +// unescapes the query param so we need not unescape it again. +// +// Returns ErrInvalidCursor when an invalid cursor is provided (and optionally +// validates the decoded value as a UUID when validateAsUUID is true). +func decodeCursorAfterQueryUnescape(cipherKey, cursor string, validateAsUUID bool) (*Cursor, error) { + if cursor == "" { + return nil, ErrInvalidCursor + } + + // All cursors should always be base64 encoded (see store.EncodeCursor). + v, err := base64.StdEncoding.DecodeString(cursor) + if err != nil { + return nil, ErrInvalidCursor + } + + c := Cursor{encoded: cursor, decoded: string(v)} + + // We're adding a single-character version ID to the cursor, in case we ever change this + // implementation. As such, we need to account for it & remove it before XOR'ing the input. + versionIdx := getVersionIdx(c.decoded[1:]) + if c.version = string(c.decoded[versionIdx]); c.version != cursorVersion { + return nil, ErrInvalidCursor + } + c.decoded = xorText(cipherKey, c.decoded[:versionIdx]+c.decoded[versionIdx+1:]) + + if validateAsUUID { + ids := strings.Split(c.decoded, ":") + for _, id := range ids { + if _, err := uuid.Parse(id); err != nil { + return nil, ErrInvalidCursor + } + } + } + + return &c, err +} + +// getVersionIdx returns the index where the single character is, representing the version of this implementation. +func getVersionIdx(input string) int { + // The version is injected halfway in the string to not introduce too much predictability. + return int(float64(len(input) / 2)) //nolint:gomnd +} diff --git a/go.mod b/go.mod index ef0be9589e..546127a9b3 100644 --- a/go.mod +++ b/go.mod @@ -39,7 +39,7 @@ require ( github.com/invopop/gobl v0.306.0 github.com/jackc/pgx/v5 v5.8.0 github.com/lmittmann/tint v1.1.2 - github.com/mitchellh/mapstructure v1.5.0 + github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c github.com/oapi-codegen/nethttp-middleware v1.1.2 github.com/oapi-codegen/nullable v1.1.0 github.com/oapi-codegen/runtime v1.2.0 @@ -141,11 +141,14 @@ require ( github.com/go-jose/go-jose/v4 v4.1.3 // indirect github.com/go-openapi/jsonreference v0.20.2 // indirect github.com/go-openapi/swag/jsonname v0.25.4 // indirect + github.com/go-pg/pg/v10 v10.11.1 // indirect github.com/gofrs/uuid/v5 v5.3.2 // indirect github.com/google/gnostic-models v0.7.0 // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/googleapis/go-sql-spanner v1.16.0 // indirect github.com/hamba/avro/v2 v2.29.0 // indirect + github.com/hashicorp/go-hclog v1.6.3 // indirect + github.com/hashicorp/golang-lru v1.0.2 // indirect github.com/huandu/go-clone v1.7.3 // indirect github.com/invopop/validation v0.8.0 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect @@ -169,13 +172,15 @@ require ( github.com/pinecone-io/go-pinecone v1.1.1 // indirect github.com/pjbgf/sha1cd v0.4.0 // indirect github.com/pkoukk/tiktoken-go v0.1.7 // indirect - github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect + github.com/planetscale/vtprotobuf v0.6.1-0.20250313105119-ba97887b0a25 // indirect github.com/prometheus/otlptranslator v1.0.0 // indirect github.com/qdrant/go-client v1.14.1 // indirect github.com/questdb/go-questdb-client/v3 v3.2.0 // indirect github.com/redpanda-data/connect/v4 v4.61.0 // indirect github.com/samber/slog-common v0.19.0 // indirect github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect + github.com/secure-systems-lab/go-securesystemslib v0.9.0 // indirect + github.com/shirou/gopsutil/v4 v4.25.8-0.20250809033336-ffcdc2b7662f // indirect github.com/skeema/knownhosts v1.3.1 // indirect github.com/speakeasy-api/jsonpath v0.6.0 // indirect github.com/spiffe/go-spiffe/v2 v2.6.0 // indirect @@ -187,6 +192,7 @@ require ( github.com/tmc/langchaingo v0.1.13 // indirect github.com/twmb/franz-go/pkg/kadm v1.16.0 // indirect github.com/twmb/franz-go/pkg/sr v1.4.0 // indirect + github.com/uptrace/bun v1.1.17 // indirect github.com/woodsbury/decimal128 v1.3.0 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect github.com/zclconf/go-cty-yaml v1.1.0 // indirect @@ -198,6 +204,8 @@ require ( go.mongodb.org/mongo-driver/v2 v2.2.2 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib/detectors/gcp v1.38.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.38.0 // indirect + go.uber.org/mock v0.6.0 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect go.yaml.in/yaml/v4 v4.0.0-rc.4 // indirect @@ -206,6 +214,8 @@ require ( gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect + gorm.io/driver/postgres v1.5.5 // indirect + gorm.io/gorm v1.25.12 // indirect gotest.tools/gotestsum v1.13.0 // indirect k8s.io/apiextensions-apiserver v0.35.0 // indirect k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 // indirect diff --git a/go.sum b/go.sum index 5a143fd6f6..3f1df2dd9c 100644 --- a/go.sum +++ b/go.sum @@ -1258,6 +1258,7 @@ github.com/evanphx/json-patch v0.5.2 h1:xVCHIVMUu1wtM/VkR9jVZ45N3FhZfYMMYGorLCR8 github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ= github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU= github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM= +github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= @@ -1361,8 +1362,8 @@ github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6 github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54= github.com/go-pdf/fpdf v0.5.0/go.mod h1:HzcnA+A23uwogo0tp9yU+l3V+KXhiESpt1PMayhOh5M= github.com/go-pdf/fpdf v0.6.0/go.mod h1:HzcnA+A23uwogo0tp9yU+l3V+KXhiESpt1PMayhOh5M= -github.com/go-pg/pg/v10 v10.11.0 h1:CMKJqLgTrfpE/aOVeLdybezR2om071Vh38OLZjsyMI0= -github.com/go-pg/pg/v10 v10.11.0/go.mod h1:4BpHRoxE61y4Onpof3x1a2SQvi9c+q1dJnrNdMjsroA= +github.com/go-pg/pg/v10 v10.11.1 h1:vYwbFpqoMpTDphnzIPshPPepdy3VpzD8qo29OFKp4vo= +github.com/go-pg/pg/v10 v10.11.1/go.mod h1:ExJWndhDNNftBdw1Ow83xqpSf4WMSJK8urmXD5VXS1I= github.com/go-pg/zerochecker v0.2.0 h1:pp7f72c3DobMWOb2ErtZsnrPaSvHd2W4o9//8HtF4mU= github.com/go-pg/zerochecker v0.2.0/go.mod h1:NJZ4wKL0NmTtz0GKCoJ8kym6Xn/EQzXRl2OnAe7MmDo= github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= @@ -1624,8 +1625,8 @@ github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= -github.com/hashicorp/go-hclog v1.6.2 h1:NOtoftovWkDheyUM/8JW3QMiXyxJK3uHRK7wV04nD2I= -github.com/hashicorp/go-hclog v1.6.2/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= +github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= +github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc= github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-msgpack v0.5.5 h1:i9R9JSrqIz0QVLz3sz+i3YJdT7TTSLcfLLzJi9aZTuI= @@ -1642,8 +1643,8 @@ github.com/hashicorp/go-version v1.8.0 h1:KAkNb1HAiZd1ukkxDFGmokVZe1Xy9HG6NUp+bP github.com/hashicorp/go-version v1.8.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= -github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c= +github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/hashicorp/golang-lru/arc/v2 v2.0.7 h1:QxkVTxwColcduO+LP7eJO56r2hFiG8zEbfAAzRv52KQ= github.com/hashicorp/golang-lru/arc/v2 v2.0.7/go.mod h1:Pe7gBlGdc8clY5LJ0LpJXMt5AmgmWNH1g+oFFVUHOEc= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= @@ -1877,12 +1878,15 @@ github.com/matoous/go-nanoid/v2 v2.1.0 h1:P64+dmq21hhWdtvZfEAofnvJULaRR1Yib0+PnU github.com/matoous/go-nanoid/v2 v2.1.0/go.mod h1:KlbGNQ+FhrUNIHUxZdL63t7tl4LaPkZNpUULS8H4uVM= github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-ieproxy v0.0.1/go.mod h1:pYabZ6IHcRpFh7vIaLfK7rdcWgFEb3SFJ6/gNWuh88E= github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= @@ -1915,8 +1919,8 @@ github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQ github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= github.com/mitchellh/mapstructure v1.3.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= -github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c h1:cqn374mizHuIWj+OSJCajGr/phAmuMug9qIX3l9CflE= +github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/moby/buildkit v0.14.1 h1:2epLCZTkn4CikdImtsLtIa++7DzCimrrZCT1sway+oI= github.com/moby/buildkit v0.14.1/go.mod h1:1XssG7cAqv5Bz1xcGMxJL123iCv5TYN4Z/qf647gfuk= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= @@ -2018,6 +2022,7 @@ github.com/oliveagle/jsonpath v0.1.0/go.mod h1:eqOVx5Vwu4gd2mmMZvVZsgIqNSaW3xxRT github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.10.2/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.14.2/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= @@ -2027,6 +2032,7 @@ github.com/onsi/ginkgo/v2 v2.27.2/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zw github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/gomega v1.10.3/go.mod h1:V9xEwhxec5O8UDM77eCW8vLymOMltsqPVYWrpDsH8xc= github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A= @@ -2109,8 +2115,8 @@ github.com/pkg/sftp v1.13.9 h1:4NGkvGudBL7GteO3m6qnaQ4pC0Kvf0onSVc9gR3EWBw= github.com/pkg/sftp v1.13.9/go.mod h1:OBN7bVXdstkFFN/gdnHPUb5TE8eb8G1Rp9wCItqjkkA= github.com/pkoukk/tiktoken-go v0.1.7 h1:qOBHXX4PHtvIvmOtyg1EeKlwFRiMKAcoMp4Q+bLQDmw= github.com/pkoukk/tiktoken-go v0.1.7/go.mod h1:9NiV+i9mJKGj1rYOT+njbv+ZwA/zJxYdewGl6qVatpg= -github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= -github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= +github.com/planetscale/vtprotobuf v0.6.1-0.20250313105119-ba97887b0a25 h1:S1hI5JiKP7883xBzZAr1ydcxrKNSVNm7+3+JwjxZEsg= +github.com/planetscale/vtprotobuf v0.6.1-0.20250313105119-ba97887b0a25/go.mod h1:ZQntvDG8TkPgljxtA0R9frDoND4QORU1VXz015N5Ks4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -2203,8 +2209,8 @@ github.com/samber/slog-multi v1.7.0/go.mod h1:qTqzmKdPpT0h4PFsTN5rYRgLwom1v+fNGu github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ= github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= -github.com/secure-systems-lab/go-securesystemslib v0.4.0 h1:b23VGrQhTA8cN2CbBw7/FulN9fTtqYUdS5+Oxzt+DUE= -github.com/secure-systems-lab/go-securesystemslib v0.4.0/go.mod h1:FGBZgq2tXWICsxWQW1msNf49F0Pf2Op5Htayx335Qbs= +github.com/secure-systems-lab/go-securesystemslib v0.9.0 h1:rf1HIbL64nUpEIZnjLZ3mcNEL9NBPB0iuVjyxvq3LZc= +github.com/secure-systems-lab/go-securesystemslib v0.9.0/go.mod h1:DVHKMcZ+V4/woA/peqr+L0joiRXbPpQ042GgJckkFgw= github.com/segmentio/asm v1.2.1 h1:DTNbBqs57ioxAD4PrArqftgypG4/qNpXoJx8TVXxPR0= github.com/segmentio/asm v1.2.1/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= github.com/segmentio/ksuid v1.0.4 h1:sBo2BdShXjmcugAMwjugoGUdUV0pcxY5mW4xKRn3v4c= @@ -2216,9 +2222,8 @@ github.com/serialx/hashring v0.0.0-20200727003509-22c0c7ab6b1b h1:h+3JX2VoWTFuyQ github.com/serialx/hashring v0.0.0-20200727003509-22c0c7ab6b1b/go.mod h1:/yeG0My1xr/u+HZrFQ1tOQQQQrOawfyMUH13ai5brBc= github.com/shibumi/go-pathspec v1.3.0 h1:QUyMZhFo0Md5B8zV8x2tesohbb5kfbpTi9rBnKh5dkI= github.com/shibumi/go-pathspec v1.3.0/go.mod h1:Xutfslp817l2I1cZvgcfeMQJG5QnU2lh5tVaaMCl3jE= -github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI= -github.com/shirou/gopsutil/v4 v4.25.6 h1:kLysI2JsKorfaFPcYmcJqbzROzsBWEOAtw6A7dIfqXs= -github.com/shirou/gopsutil/v4 v4.25.6/go.mod h1:PfybzyydfZcN+JMMjkF6Zb8Mq1A/VcogFFg7hj50W9c= +github.com/shirou/gopsutil/v4 v4.25.8-0.20250809033336-ffcdc2b7662f h1:S+PHRM3lk96X0/cGEGUukqltzkX/ekUx0F9DoCGK1G0= +github.com/shirou/gopsutil/v4 v4.25.8-0.20250809033336-ffcdc2b7662f/go.mod h1:4f4j4w8HLMPWEFs3BO2UBBLigKAaWYwkSkbIt/6Q4Ss= github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= @@ -2284,6 +2289,7 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5 github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= github.com/stretchr/testify v1.7.5/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= @@ -2358,8 +2364,8 @@ github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVM github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= -github.com/uptrace/bun v1.1.12 h1:sOjDVHxNTuM6dNGaba0wUuz7KvDE1BmNu9Gqs2gJSXQ= -github.com/uptrace/bun v1.1.12/go.mod h1:NPG6JGULBeQ9IU6yHp7YGELRa5Agmd7ATZdz4tGZ6z0= +github.com/uptrace/bun v1.1.17 h1:qxBaEIo0hC/8O3O6GrMDKxqyT+mw5/s0Pn/n6xjyGIk= +github.com/uptrace/bun v1.1.17/go.mod h1:hATAzivtTIRsSJR4B8AXR+uABqnQxr3myKDKEf5iQ9U= github.com/uptrace/bun/dialect/pgdialect v1.1.12 h1:m/CM1UfOkoBTglGO5CUTKnIKKOApOYxkcP2qn0F9tJk= github.com/uptrace/bun/dialect/pgdialect v1.1.12/go.mod h1:Ij6WIxQILxLlL2frUBxUBOZJtLElD2QQNDcu/PWDHTc= github.com/uptrace/bun/driver/pgdriver v1.1.12 h1:3rRWB1GK0psTJrHwxzNfEij2MLibggiLdTqjTtfHc1w= @@ -2368,6 +2374,7 @@ github.com/urfave/cli/v2 v2.27.7 h1:bH59vdhbjLv3LAvIu6gd0usJHgoTTPhCFib8qqOwXYU= github.com/urfave/cli/v2 v2.27.7/go.mod h1:CyNAG/xg+iAOg0N4MPGZqVmv2rCoP267496AOXUZjA4= github.com/vmihailenco/bufpool v0.1.11 h1:gOq2WmBrq0i2yW5QJ16ykccQ4wH9UyEsgLm6czKAd94= github.com/vmihailenco/bufpool v0.1.11/go.mod h1:AFf/MOy3l2CFTKbxwt0mp2MwnqjNEs5H/UxrkA5jxTQ= +github.com/vmihailenco/msgpack/v5 v5.3.4/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc= github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc= github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= @@ -2488,12 +2495,10 @@ go.opentelemetry.io/otel/exporters/jaeger v1.17.0 h1:D7UpUy2Xc2wsi1Ras6V40q806WM go.opentelemetry.io/otel/exporters/jaeger v1.17.0/go.mod h1:nPCqOnEH9rNLKqH/+rrUjiMzHJdV1BlpKcTwRTyKkKI= go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.15.0 h1:W+m0g+/6v3pa5PgVf2xoFMi5YtNR06WtS7ve5pcvLtM= go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.15.0/go.mod h1:JM31r0GGZ/GU94mX8hN4D8v6e40aFlUECSQ48HaLgHM= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric v0.42.0 h1:ZtfnDL+tUrs1F0Pzfwbg2d59Gru9NCH3bgSHBM6LDwU= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric v0.42.0/go.mod h1:hG4Fj/y8TR/tlEDREo8tWstl9fO9gcFkn4xrx0Io8xU= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.39.0 h1:cEf8jF6WbuGQWUVcqgyWtTR0kOOAWY1DYZ+UhvdmQPw= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.39.0/go.mod h1:k1lzV5n5U3HkGvTCJHraTAGJ7MqsgL1wrGwTj1Isfiw= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v0.42.0 h1:wNMDy/LVGLj2h3p6zg4d0gypKfWKSWI14E1C4smOgl8= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v0.42.0/go.mod h1:YfbDdXAAkemWJK3H/DshvlrxqFB2rtW4rY6ky/3x/H0= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.38.0 h1:Oe2z/BCg5q7k4iXC3cqJxKYg0ieRiOqF0cecFYdPTwk= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.38.0/go.mod h1:ZQM5lAJpOsKnYagGg/zV2krVqTtaVdYdDkhMoX6Oalg= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 h1:f0cb2XPmrqn4XMy9PNliTgRKJgS5WcL/u0/WRYGz4t0= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0/go.mod h1:vnakAaFckOMiMtOIhFI2MNH4FYrZzXCYxmb1LlhoGz8= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0 h1:in9O8ESIOlwJAEGTkkf34DesGRAc/Pn8qJ7k3r/42LM= @@ -2537,8 +2542,8 @@ go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= -go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU= -go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= +go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= +go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= @@ -2563,6 +2568,7 @@ go.yaml.in/yaml/v4 v4.0.0-rc.4/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfP gocloud.dev v0.26.0/go.mod h1:mkUgejbnbLotorqDyvedJO20XcZNTynmSeVSQS9btVg= golang.org/x/crypto v0.0.0-20180723164146-c126467f60eb/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190320223903-b7391e95e576/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -2583,6 +2589,7 @@ golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29/go.mod h1:IxCIyHEi3zRg3s0 golang.org/x/crypto v0.0.0-20220511200225-c6db032c6c88/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU= golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= @@ -2690,6 +2697,7 @@ golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81R golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200904194848-62affa334b73/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= @@ -2722,6 +2730,7 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug golang.org/x/net v0.0.0-20220909164309-bea034e7d591/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/net v0.0.0-20221012135044-0b7e1fb9d458/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/net v0.0.0-20221014081412-f15817d10f9b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= +golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= @@ -2826,6 +2835,7 @@ golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200828194041-157a740278f4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -2859,6 +2869,7 @@ golang.org/x/sys v0.0.0-20210816183151-1e6c022a8912/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210917161153-d61c044b1678/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -2907,6 +2918,7 @@ golang.org/x/telemetry v0.0.0-20260109210033-bd525da824e2/go.mod h1:b7fPSJ0pKZ3c golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ= @@ -3358,6 +3370,7 @@ gopkg.in/cenkalti/backoff.v1 v1.1.0/go.mod h1:J6Vskwqd+OMVJl8C33mmtxTBs2gyzfv7UD gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= @@ -3393,10 +3406,10 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gorm.io/driver/postgres v1.5.4 h1:Iyrp9Meh3GmbSuyIAGyjkN+n9K+GHX9b9MqsTL4EJCo= -gorm.io/driver/postgres v1.5.4/go.mod h1:Bgo89+h0CRcdA33Y6frlaHHVuTdOf87pmyzwW9C/BH0= -gorm.io/gorm v1.25.11 h1:/Wfyg1B/je1hnDx3sMkX+gAlxrlZpn6X0BXRlwXlvHg= -gorm.io/gorm v1.25.11/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ= +gorm.io/driver/postgres v1.5.5 h1:r1VBTQQrOAlUux3JI9V7rdxVWBPPnzxa315qNJUzmjI= +gorm.io/driver/postgres v1.5.5/go.mod h1:Bgo89+h0CRcdA33Y6frlaHHVuTdOf87pmyzwW9C/BH0= +gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8= +gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ= gotest.tools/gotestsum v1.13.0 h1:+Lh454O9mu9AMG1APV4o0y7oDYKyik/3kBOiCqiEpRo= gotest.tools/gotestsum v1.13.0/go.mod h1:7f0NS5hFb0dWr4NtcsAsF0y1kzjEFfAil0HiBQJE03Q= gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= From c91a4c6f366134a8e74d68ac6830ac6f03b2a6fd Mon Sep 17 00:00:00 2001 From: mark-vass-konghq Date: Tue, 17 Mar 2026 11:10:38 +0100 Subject: [PATCH 4/4] chore: refactor and adding tests --- Makefile | 2 +- .../packages/aip/src/llmcost/operations.tsp | 4 +- api/v3/api.gen.go | 297 +++++++++--------- api/v3/handlers/llmcost/list_prices.go | 3 +- api/v3/oasmiddleware/validator.go | 4 +- api/v3/openapi.yaml | 4 +- api/v3/request/aip.go | 120 ++++--- api/v3/request/aip_filter.go | 274 ++++++++-------- api/v3/request/aip_filter_test.go | 270 ++++++++++++++++ api/v3/request/aip_pagination.go | 184 ++++++----- api/v3/request/aip_sort.go | 13 +- api/v3/request/aip_sort_test.go | 167 ++++++++++ api/v3/request/aip_test.go | 251 +++++++++++++++ api/v3/request/cursor.go | 7 +- api/v3/request/cursor_test.go | 139 ++++++++ 15 files changed, 1298 insertions(+), 441 deletions(-) create mode 100644 api/v3/request/aip_filter_test.go create mode 100644 api/v3/request/aip_sort_test.go create mode 100644 api/v3/request/aip_test.go create mode 100644 api/v3/request/cursor_test.go diff --git a/Makefile b/Makefile index 562ccd026a..c4aec88a2f 100644 --- a/Makefile +++ b/Makefile @@ -29,7 +29,7 @@ generate-javascript-sdk: ## Generate JavaScript SDK $(MAKE) -C api/client/javascript generate .PHONY: gen-api -gen-api: update-openapi # generate-javascript-sdk ## Generate API and SDKs +gen-api: update-openapi generate-javascript-sdk ## Generate API and SDKs $(call print-target) .PHONY: generate-all diff --git a/api/spec/packages/aip/src/llmcost/operations.tsp b/api/spec/packages/aip/src/llmcost/operations.tsp index 8ec2897235..d21303fefc 100644 --- a/api/spec/packages/aip/src/llmcost/operations.tsp +++ b/api/spec/packages/aip/src/llmcost/operations.tsp @@ -25,7 +25,7 @@ model FilterSingleString { eq?: string; /** - * aasd + * The field must match the provided value. This is an "override" filter that can be used when the default `eq` filter is not sufficient, e.g. when filtering by `model_name` where the name can have multiple variants for the same model ID. */ oeq?: string; @@ -40,7 +40,7 @@ model FilterSingleString { contains?: string; /** - * asd + * The field must contain the provided value. This is an "override" filter that can be used when the default `contains` filter is not sufficient. */ ocontains?: string; } diff --git a/api/v3/api.gen.go b/api/v3/api.gen.go index 5f1cd59196..ae8bd34aa6 100644 --- a/api/v3/api.gen.go +++ b/api/v3/api.gen.go @@ -2272,10 +2272,10 @@ type FilterSingleString struct { // Neq The field must not match the provided value. Neq *string `json:"neq,omitempty"` - // Ocontains asd + // Ocontains The field must contain the provided value. This is an "override" filter that can be used when the default `contains` filter is not sufficient. Ocontains *string `json:"ocontains,omitempty"` - // Oeq aasd + // Oeq The field must match the provided value. This is an "override" filter that can be used when the default `eq` filter is not sufficient, e.g. when filtering by `model_name` where the name can have multiple variants for the same model ID. Oeq *string `json:"oeq,omitempty"` } @@ -6367,152 +6367,153 @@ var swaggerSpec = []string{ "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", - "avlYUgcSAgA=", + "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", + "JYRhQbmsY0d2bs5v/ncAAAD//8v90rF9EwIA", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/api/v3/handlers/llmcost/list_prices.go b/api/v3/handlers/llmcost/list_prices.go index 0f3b2da21e..6d3b641bac 100644 --- a/api/v3/handlers/llmcost/list_prices.go +++ b/api/v3/handlers/llmcost/list_prices.go @@ -60,7 +60,6 @@ var listPricesAuthorizedSorts = []string{ "id", "provider.id", "model.id", "effective_from", "effective_to", } - func (h *handler) ListPrices() ListPricesHandler { return httptransport.NewHandlerWithArgs( func(ctx context.Context, r *http.Request, _ ListPricesParams) (ListPricesRequest, error) { @@ -70,7 +69,7 @@ func (h *handler) ListPrices() ListPricesHandler { } attrs, err := request.GetAipAttributes(r, - request.WithDefaultPageSizeDefault(20), + request.WithDefaultPageSize(20), request.WithMaxPageSize(100), request.WithAuthorizedSorts(listPricesAuthorizedSorts), request.WithAuthorizedFilters(listPricesAuthorizedFilters), diff --git a/api/v3/oasmiddleware/validator.go b/api/v3/oasmiddleware/validator.go index 26ced23b58..453aec2adf 100644 --- a/api/v3/oasmiddleware/validator.go +++ b/api/v3/oasmiddleware/validator.go @@ -16,8 +16,8 @@ import ( ) type ( - RequestNotFoundHookFunc = func(error, http.ResponseWriter, *http.Request) bool - RequestValidationErrorFunc = func(error, http.ResponseWriter, *http.Request) bool + RequestNotFoundHookFunc = func(error, http.ResponseWriter, *http.Request) bool + RequestValidationErrorFunc = func(error, http.ResponseWriter, *http.Request) bool ResponseValidationFunc = func(error, *http.Request) ) diff --git a/api/v3/openapi.yaml b/api/v3/openapi.yaml index 9b371fd08b..6ef8e1aa9d 100644 --- a/api/v3/openapi.yaml +++ b/api/v3/openapi.yaml @@ -3749,7 +3749,7 @@ components: description: The field must match the provided value. oeq: type: string - description: aasd + description: The field must match the provided value. This is an "override" filter that can be used when the default `eq` filter is not sufficient, e.g. when filtering by `model_name` where the name can have multiple variants for the same model ID. neq: type: string description: The field must not match the provided value. @@ -3758,7 +3758,7 @@ components: description: The field must contain the provided value. ocontains: type: string - description: asd + description: The field must contain the provided value. This is an "override" filter that can be used when the default `contains` filter is not sufficient. description: |- A filter for a single string field. TODO: This is a temporary solution to support the filter API. diff --git a/api/v3/request/aip.go b/api/v3/request/aip.go index 03ce8547c8..bd5e0c8763 100644 --- a/api/v3/request/aip.go +++ b/api/v3/request/aip.go @@ -4,19 +4,19 @@ import ( "net/http" "strings" + "github.com/samber/lo" + "github.com/openmeterio/openmeter/api/v3/filters" ) -// GetAipAttributes return the AipAttributes found in the request query string -// if the parser is set to Strict using `AipSetStrictMode` is apierrors out -// with a `BaseApiError` +// GetAipAttributes returns the AipAttributes parsed from the request query string. +// If strict mode is enabled via WithAipStrictMode and an invalid parameter is +// encountered, it returns a *apierrors.BaseAPIError. func GetAipAttributes(r *http.Request, opts ...AipParseOption) (*AipAttributes, error) { a := &AipAttributes{} conf := newConfig() - for _, v := range opts { - v(conf) - } + lo.ForEach(opts, func(v AipParseOption, _ int) { v(conf) }) queryValues := r.URL.Query() @@ -45,38 +45,26 @@ func GetAipAttributes(r *http.Request, opts ...AipParseOption) (*AipAttributes, // RemapAipAttributes remaps the filters and sorts to another name // this is used when API is not inlined with the database entities func RemapAipAttributes(attrs *AipAttributes, mappedAttributes map[string]string) { - if attrs.Filters != nil { - for k, f := range attrs.Filters { - if _, ok := mappedAttributes[f.Name]; ok { - attrs.Filters[k].Name = mappedAttributes[f.Name] - continue - } - parts := strings.SplitN(f.Name, ".", 2) // allow filters[known_custom_field.unknown_key] - if _, ok := mappedAttributes[parts[0]]; ok { - if len(parts) == 2 { - attrs.Filters[k].Name = mappedAttributes[parts[0]] + "." + parts[1] - } else { - attrs.Filters[k].Name = mappedAttributes[f.Name] - } - } - } + lo.ForEach(attrs.Filters, func(f QueryFilter, k int) { + attrs.Filters[k].Name = remapName(f.Name, mappedAttributes) + }) + lo.ForEach(attrs.Sorts, func(s SortBy, k int) { + attrs.Sorts[k].Field = remapName(s.Field, mappedAttributes) + }) +} + +// remapName remaps a field name using the provided mapping. +// Supports dot-notation: "labels.env" is remapped as "mapped_labels.env" if "labels" is in the map. +func remapName(name string, mappedAttributes map[string]string) string { + if mapped, ok := mappedAttributes[name]; ok { + return mapped } - if attrs.Sorts != nil { - for k, s := range attrs.Sorts { - if _, ok := mappedAttributes[s.Field]; ok { - attrs.Sorts[k].Field = mappedAttributes[s.Field] - continue - } - parts := strings.SplitN(s.Field, ".", 2) // allow filters[known_custom_field.unknown_key] - if _, ok := mappedAttributes[parts[0]]; ok { - if len(parts) == 2 { - attrs.Sorts[k].Field = mappedAttributes[parts[0]] + "." + parts[1] - } else { - attrs.Sorts[k].Field = mappedAttributes[s.Field] - } - } - } + parts := strings.SplitN(name, ".", 2) // allow known_custom_field.unknown_key + mapped, ok := mappedAttributes[parts[0]] + if ok && len(parts) == 2 { + return mapped + "." + parts[1] } + return name } type AipAttributes struct { @@ -91,6 +79,7 @@ const ( paginationKindOffset paginationKind = iota paginationKindCursor ) + const ( defaultPaginationMaxSize = 100 ) @@ -138,25 +127,28 @@ func WithCursorPagination() AipParseOption { } } -// WithCursorPagination sets the AIP request parser to only take the offset AIP -// attributes in consideration and will ignore other kinds of paginations. +// WithOffsetPagination sets the AIP request parser to only take the offset AIP +// attributes (page[number], page[size]) in consideration and will ignore other +// kinds of paginations. // -// This is the default parser behavior +// This is the default parser behavior. func WithOffsetPagination() AipParseOption { return func(c *config) { c.paginationKind = paginationKindOffset } } -// WithDefaultPageSizeDefault sets the AIP request parser default page size. +// WithDefaultPageSize sets the AIP request parser default page size. // This value is used when the client is not setting the page[size] querystring // or when the page[size] attribute is not valid and the parser is not using -// strict mode +// strict mode. Non-positive values are ignored. // -// Default value is 20 -func WithDefaultPageSizeDefault(value int) AipParseOption { +// Default value is 20. +func WithDefaultPageSize(value int) AipParseOption { return func(c *config) { - c.defaultPageSize = value + if value > 0 { + c.defaultPageSize = value + } } } @@ -200,11 +192,11 @@ func WithAuthorizedFilters(fields map[string]AIPFilterOption) AipParseOption { } } -// WithAuthorizedSorts defines the set of sorts that the parser should parse -// other filters are ignored +// WithAuthorizedSorts defines the set of allowed sort fields. Sorts on any +// field not in the provided list are ignored. // -// by default the parser takes all the sorts that are passed to it -// do not use dot notation (field.subfield) with this method, use WithAuthorizedSorts instead. +// By default the parser accepts all sort fields. +// Do not use dot notation (field.subfield) with this method; use WithAuthorizedDotSorts instead. func WithAuthorizedSorts(fields []string) AipParseOption { return func(c *config) { c.authorizedSorts = fields @@ -224,18 +216,20 @@ func WithAuthorizedDotSorts(fields []string) AipParseOption { } } -// WithMaxPageSize defines the maximum size of the pagination +// WithMaxPageSize defines the maximum size of the pagination. +// Non-positive values are ignored. // -// by default the parser sets it to 100 +// Default value is 100. func WithMaxPageSize(size int) AipParseOption { return func(c *config) { - c.maxPageSize = size + if size > 0 { + c.maxPageSize = size + } } } -// ValidationFunc represents the validation function on a field. If it returns -// false it means the validation hasn't passed. The string return parameter is -// used as error message in the apierror +// ValidationFunc is a field-level validation callback. A non-nil error return +// indicates validation failure; the error message is included in the API error response. type ValidationFunc func(field, value string) error // AuthorizedFilters reprensents the map of fields that are authorized to be @@ -253,13 +247,13 @@ type AIPFilterOption struct { // FilterStringFromAip extracts a *filters.StringFilter for the named field from AIP query filters. // Returns nil if no matching filters are found. func FilterStringFromAip(queryFilters []QueryFilter, field string) *filters.StringFilter { + matching := lo.Filter(queryFilters, func(qf QueryFilter, _ int) bool { return qf.Name == field }) + if len(matching) == 0 { + return nil + } + var f filters.StringFilter - found := false - for _, qf := range queryFilters { - if qf.Name != field { - continue - } - found = true + lo.ForEach(matching, func(qf QueryFilter, _ int) { v := qf.Value switch qf.Filter { case QueryFilterEQ: @@ -284,10 +278,10 @@ func FilterStringFromAip(queryFilters []QueryFilter, field string) *filters.Stri t := true f.Exists = &t } - } - if !found || f.IsEmpty() { + }) + + if f.IsEmpty() { return nil } return &f } - diff --git a/api/v3/request/aip_filter.go b/api/v3/request/aip_filter.go index e02ed0fe91..e9c33f720a 100644 --- a/api/v3/request/aip_filter.go +++ b/api/v3/request/aip_filter.go @@ -8,6 +8,8 @@ import ( "slices" "strings" + "github.com/samber/lo" + "github.com/openmeterio/openmeter/api/v3/apierrors" ) @@ -32,12 +34,8 @@ var ( ) func filterName(value QueryFilterOp) string { - for k, v := range filterMap { - if v == value { - return k - } - } - return "" + key, _ := lo.FindKey(filterMap, value) + return key } const ( @@ -65,10 +63,8 @@ const ( QueryFilterOrContains ) -var ( - // lookup to only focus filter[foo] and not filterfoo[bar] - prefixLookup = FilterQuery + "[" -) +// lookup to only focus filter[foo] and not filterfoo[bar] +var prefixLookup = FilterQuery + "[" // QueryFilter column filter type QueryFilter struct { @@ -82,111 +78,130 @@ type QueryFilter struct { type QueryFilterOp int func extractFilter(ctx context.Context, qs url.Values, c *config) ([]QueryFilter, *apierrors.BaseAPIError) { - var out []QueryFilter + type filterEntry struct{ key, value string } - for i, v := range qs { - if !strings.HasPrefix(i, prefixLookup) { - continue + entries := lo.FlatMap(lo.Entries(qs), func(e lo.Entry[string, []string], _ int) []filterEntry { + if !strings.HasPrefix(e.Key, prefixLookup) { + return nil } - for _, filter := range v { - o, err := parseFilterQs(ctx, filter, i) - if err != nil { - return nil, err - } - - // no field name provided is an invalid query filter - if o.Name == "" { - continue - } - - // if there is value that means we're falling back on - // EXIST query filter - if filter == "" { - o.Filter = QueryFilterExists - } - - o.Value = filter - - checkFilters := c.authorizedFilters != nil - var ok bool - var filters AIPFilterOption - - if checkFilters && strings.ContainsRune(o.Name, '.') { - parts := strings.SplitN(o.Name, ".", 2) // allow filters[known_custom_field.unknown_key] - filters, ok = c.authorizedFilters[parts[0]] - if !ok { - filters, ok = c.authorizedFilters[o.Name] // specific case where only 1 field is allowed - } - ok = ok && filters.DotFilter - } else if checkFilters { - filters, ok = c.authorizedFilters[o.Name] - ok = ok && !filters.DotFilter // forbid using whole field for dot filters - } - - if checkFilters { - if !ok { - if c.strictMode { - return nil, apierrors.NewBadRequestError(ctx, ErrUnallowedFilterMethod, - apierrors.InvalidParameters{ - apierrors.InvalidParameter{ - Field: o.Name, - Reason: "unauthorized filter", - Source: apierrors.InvalidParamSourceQuery, - Rule: "unknown_property", - }, - }) - } - continue - } - if !slices.Contains(filters.Filters, o.Filter) { - if c.strictMode { - return nil, apierrors.NewBadRequestError(ctx, ErrUnallowedFilterColumn, - apierrors.InvalidParameters{ - apierrors.InvalidParameter{ - Field: filterName(o.Filter), - Reason: "unauthorized filter on column", - Source: apierrors.InvalidParamSourceQuery, - Rule: "unknown_property", - }, - }) - } - continue - } - if filters.ValidationFunc != nil { - if err := filters.ValidationFunc(o.Name, o.Value); err != nil { - if errors.Is(err, ErrReturnEmptySet) { - // for errors in uuid format, we want to handle it by returning an empty list. - return nil, apierrors.NewEmptySetResponse(ctx, c.paginationKind == paginationKindCursor) - } - return nil, apierrors.NewBadRequestError(ctx, ErrUnallowedFilterColumn, - apierrors.InvalidParameters{ - apierrors.InvalidParameter{ - Field: filter, - Reason: err.Error(), - Source: apierrors.InvalidParamSourceQuery, - Rule: "unauthorized filter on column", - }, - }) - } - } - } - - if o.Filter == QueryFilterOrEQ || o.Filter == QueryFilterOrContains { - o.Values = parseMultipleStringValues(o.Value) - } - out = append(out, o) + return lo.Map(e.Value, func(v string, _ int) filterEntry { return filterEntry{e.Key, v} }) + }) + + var out []QueryFilter + for _, e := range entries { + qf, skip, err := processFilter(ctx, e.value, e.key, c) + if err != nil { + return nil, err + } + if !skip { + out = append(out, qf) } } - return out, nil } -func parseMultipleStringValues(strValue string) []string { - var out []string - for _, v := range strings.Split(strValue, ",") { - out = append(out, strings.TrimSpace(v)) +func processFilter(ctx context.Context, filter, key string, c *config) (QueryFilter, bool, *apierrors.BaseAPIError) { + o, err := parseFilterQs(ctx, filter, key) + if err != nil { + return QueryFilter{}, false, err + } + if o.Name == "" { + return QueryFilter{}, true, nil + } + if filter == "" { + o.Filter = QueryFilterExists + } + o.Value = filter + + skip, apiErr := checkFilterAuthorization(ctx, o, filter, c) + if apiErr != nil { + return QueryFilter{}, false, apiErr + } + if skip { + return QueryFilter{}, true, nil + } + + if o.Filter == QueryFilterOrEQ || o.Filter == QueryFilterOrContains { + o.Values = parseMultipleStringValues(o.Value) + } + return o, false, nil +} + +func resolveAuthorizedFilter(name string, authorizedFilters AuthorizedFilters) (AIPFilterOption, bool) { + if !strings.ContainsRune(name, '.') { + opt, ok := authorizedFilters[name] + return opt, ok && !opt.DotFilter + } + parts := strings.SplitN(name, ".", 2) // allow filters[known_custom_field.unknown_key] + if opt, ok := authorizedFilters[parts[0]]; ok && opt.DotFilter { + return opt, true + } + opt, ok := authorizedFilters[name] // specific case where only 1 field is allowed + return opt, ok && opt.DotFilter +} + +func checkFilterAuthorization(ctx context.Context, o QueryFilter, rawFilter string, c *config) (bool, *apierrors.BaseAPIError) { + if c.authorizedFilters == nil { + return false, nil + } + + authorizedOpt, ok := resolveAuthorizedFilter(o.Name, c.authorizedFilters) + if !ok && c.strictMode { + return false, apierrors.NewBadRequestError(ctx, ErrUnallowedFilterMethod, + apierrors.InvalidParameters{ + apierrors.InvalidParameter{ + Field: o.Name, + Reason: "unauthorized filter", + Source: apierrors.InvalidParamSourceQuery, + Rule: "unknown_property", + }, + }) + } + if !ok { + return true, nil + } + + filterAllowed := slices.Contains(authorizedOpt.Filters, o.Filter) + if !filterAllowed && c.strictMode { + return false, apierrors.NewBadRequestError(ctx, ErrUnallowedFilterColumn, + apierrors.InvalidParameters{ + apierrors.InvalidParameter{ + Field: filterName(o.Filter), + Reason: "unauthorized filter on column", + Source: apierrors.InvalidParamSourceQuery, + Rule: "unknown_property", + }, + }) + } + if !filterAllowed { + return true, nil + } + + if authorizedOpt.ValidationFunc == nil { + return false, nil + } + + err := authorizedOpt.ValidationFunc(o.Name, o.Value) + if err == nil { + return false, nil + } + if errors.Is(err, ErrReturnEmptySet) { + // for errors in uuid format, we want to handle it by returning an empty list. + return false, apierrors.NewEmptySetResponse(ctx, c.paginationKind == paginationKindCursor) } - return out + return false, apierrors.NewBadRequestError(ctx, ErrUnallowedFilterColumn, + apierrors.InvalidParameters{ + apierrors.InvalidParameter{ + Field: rawFilter, + Reason: err.Error(), + Source: apierrors.InvalidParamSourceQuery, + Rule: "unauthorized filter on column", + }, + }) +} + +func parseMultipleStringValues(strValue string) []string { + return lo.Map(strings.Split(strValue, ","), func(v string, _ int) string { return strings.TrimSpace(v) }) } func parseFilterQs(ctx context.Context, filter, qs string) (QueryFilter, *apierrors.BaseAPIError) { @@ -204,27 +219,34 @@ func parseFilterQs(ctx context.Context, filter, qs string) (QueryFilter, *apierr o.Name = qs[i+1 : endFirst] qsRest := qs[endFirst+1:] + if len(qsRest) == 0 { + return o, nil + } - if len(qsRest) > 0 { - start := strings.IndexRune(qsRest, '[') - end := strings.IndexRune(qsRest, ']') - op := qsRest[start+1 : end] - if len(op) > 0 { - if queryOp, ok := filterMap[op]; ok { - o.Filter = queryOp - } else { - return QueryFilter{}, apierrors.NewBadRequestError(ctx, ErrUnallowedFilterColumn, - apierrors.InvalidParameters{ - apierrors.InvalidParameter{ - Field: filter, - Reason: fmt.Sprintf("invalid operation '%s' on filter", op), - Source: apierrors.InvalidParamSourceQuery, - Rule: "unauthorized filter on column", - }, - }) - } - } + start := strings.IndexRune(qsRest, '[') + end := strings.IndexRune(qsRest, ']') + if start == -1 || end == -1 || end <= start { + return o, nil + } + + op := qsRest[start+1 : end] + if len(op) == 0 { + return o, nil + } + + queryOp, ok := filterMap[op] + if !ok { + return QueryFilter{}, apierrors.NewBadRequestError(ctx, ErrUnallowedFilterColumn, + apierrors.InvalidParameters{ + apierrors.InvalidParameter{ + Field: filter, + Reason: fmt.Sprintf("invalid operation '%s' on filter", op), + Source: apierrors.InvalidParamSourceQuery, + Rule: "unauthorized filter on column", + }, + }) } + o.Filter = queryOp return o, nil } diff --git a/api/v3/request/aip_filter_test.go b/api/v3/request/aip_filter_test.go new file mode 100644 index 0000000000..a2c13d494d --- /dev/null +++ b/api/v3/request/aip_filter_test.go @@ -0,0 +1,270 @@ +package request_test + +import ( + "errors" + "net/http" + "net/http/httptest" + "net/url" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/openmeterio/openmeter/api/v3/apierrors" + "github.com/openmeterio/openmeter/api/v3/request" +) + +// newFilterRequest builds an HTTP GET request with the given query params. +func newFilterRequest(t *testing.T, params url.Values) *http.Request { + t.Helper() + r := httptest.NewRequest(http.MethodGet, "/", nil) + r.URL.RawQuery = params.Encode() + return r +} + +func TestExtractFilter_NoParams(t *testing.T) { + t.Run("no query params returns empty filters", func(t *testing.T) { + r := httptest.NewRequest(http.MethodGet, "/", nil) + attrs, err := request.GetAipAttributes(r) + require.NoError(t, err) + assert.Empty(t, attrs.Filters) + }) + + t.Run("non-filter params do not produce filters", func(t *testing.T) { + params := url.Values{"page[size]": {"10"}, "sort": {"name"}} + attrs, err := request.GetAipAttributes(newFilterRequest(t, params)) + require.NoError(t, err) + assert.Empty(t, attrs.Filters) + }) + + t.Run("filter prefix without brackets is ignored", func(t *testing.T) { + params := url.Values{"filtername": {"foo"}} + attrs, err := request.GetAipAttributes(newFilterRequest(t, params)) + require.NoError(t, err) + assert.Empty(t, attrs.Filters) + }) +} + +func TestExtractFilter_Operations(t *testing.T) { + cases := []struct { + op string + param string + wantFilter request.QueryFilterOp + }{ + {"eq", "filter[name][eq]", request.QueryFilterEQ}, + {"neq", "filter[name][neq]", request.QueryFilterNEQ}, + {"gt", "filter[name][gt]", request.QueryFilterGT}, + {"gte", "filter[name][gte]", request.QueryFilterGTE}, + {"lt", "filter[name][lt]", request.QueryFilterLT}, + {"lte", "filter[name][lte]", request.QueryFilterLTE}, + {"contains", "filter[name][contains]", request.QueryFilterContains}, + {"oeq", "filter[name][oeq]", request.QueryFilterOrEQ}, + {"ocontains", "filter[name][ocontains]", request.QueryFilterOrContains}, + } + + for _, tc := range cases { + t.Run("parses "+tc.op+" operator", func(t *testing.T) { + params := url.Values{tc.param: {"testvalue"}} + attrs, err := request.GetAipAttributes(newFilterRequest(t, params)) + require.NoError(t, err) + require.Len(t, attrs.Filters, 1) + assert.Equal(t, "name", attrs.Filters[0].Name) + assert.Equal(t, "testvalue", attrs.Filters[0].Value) + assert.Equal(t, tc.wantFilter, attrs.Filters[0].Filter) + }) + } + + t.Run("no op bracket defaults to eq", func(t *testing.T) { + params := url.Values{"filter[name]": {"testvalue"}} + attrs, err := request.GetAipAttributes(newFilterRequest(t, params)) + require.NoError(t, err) + require.Len(t, attrs.Filters, 1) + assert.Equal(t, request.QueryFilterEQ, attrs.Filters[0].Filter) + }) + + t.Run("empty value on plain filter is exists", func(t *testing.T) { + params := url.Values{"filter[name]": {""}} + attrs, err := request.GetAipAttributes(newFilterRequest(t, params)) + require.NoError(t, err) + require.Len(t, attrs.Filters, 1) + assert.Equal(t, request.QueryFilterExists, attrs.Filters[0].Filter) + }) + + t.Run("explicit exists operator", func(t *testing.T) { + params := url.Values{"filter[name][exists]": {"1"}} + attrs, err := request.GetAipAttributes(newFilterRequest(t, params)) + require.NoError(t, err) + require.Len(t, attrs.Filters, 1) + assert.Equal(t, request.QueryFilterExists, attrs.Filters[0].Filter) + }) + + t.Run("invalid operation returns error", func(t *testing.T) { + params := url.Values{"filter[name][bogus]": {"foo"}} + _, err := request.GetAipAttributes(newFilterRequest(t, params)) + require.Error(t, err) + }) +} + +func TestExtractFilter_MultiValueOps(t *testing.T) { + t.Run("oeq splits comma-separated values", func(t *testing.T) { + params := url.Values{"filter[name][oeq]": {"a,b,c"}} + attrs, err := request.GetAipAttributes(newFilterRequest(t, params)) + require.NoError(t, err) + require.Len(t, attrs.Filters, 1) + assert.Equal(t, []string{"a", "b", "c"}, attrs.Filters[0].Values) + }) + + t.Run("oeq trims whitespace from values", func(t *testing.T) { + params := url.Values{"filter[name][oeq]": {"a, b , c"}} + attrs, err := request.GetAipAttributes(newFilterRequest(t, params)) + require.NoError(t, err) + require.Len(t, attrs.Filters, 1) + assert.Equal(t, []string{"a", "b", "c"}, attrs.Filters[0].Values) + }) + + t.Run("ocontains splits comma-separated values", func(t *testing.T) { + params := url.Values{"filter[name][ocontains]": {"foo,bar"}} + attrs, err := request.GetAipAttributes(newFilterRequest(t, params)) + require.NoError(t, err) + require.Len(t, attrs.Filters, 1) + assert.Equal(t, []string{"foo", "bar"}, attrs.Filters[0].Values) + }) +} + +func TestExtractFilter_MultipleFilters(t *testing.T) { + t.Run("multiple different fields", func(t *testing.T) { + params := url.Values{ + "filter[name][eq]": {"foo"}, + "filter[status][eq]": {"active"}, + } + attrs, err := request.GetAipAttributes(newFilterRequest(t, params)) + require.NoError(t, err) + assert.Len(t, attrs.Filters, 2) + }) +} + +func TestExtractFilter_AuthorizedFilters(t *testing.T) { + authorized := request.AuthorizedFilters{ + "name": {Filters: []request.QueryFilterOp{request.QueryFilterEQ, request.QueryFilterContains}}, + } + + t.Run("authorized field and op passes through", func(t *testing.T) { + params := url.Values{"filter[name][eq]": {"foo"}} + attrs, err := request.GetAipAttributes(newFilterRequest(t, params), request.WithAuthorizedFilters(authorized)) + require.NoError(t, err) + require.Len(t, attrs.Filters, 1) + assert.Equal(t, "name", attrs.Filters[0].Name) + }) + + t.Run("unauthorized field silently ignored in non-strict mode", func(t *testing.T) { + params := url.Values{"filter[unknown][eq]": {"foo"}} + attrs, err := request.GetAipAttributes(newFilterRequest(t, params), request.WithAuthorizedFilters(authorized)) + require.NoError(t, err) + assert.Empty(t, attrs.Filters) + }) + + t.Run("unauthorized field returns 400 in strict mode", func(t *testing.T) { + params := url.Values{"filter[unknown][eq]": {"foo"}} + _, err := request.GetAipAttributes(newFilterRequest(t, params), + request.WithAuthorizedFilters(authorized), + request.WithAipStrictMode(), + ) + require.Error(t, err) + var apiErr *apierrors.BaseAPIError + require.True(t, errors.As(err, &apiErr)) + assert.Equal(t, http.StatusBadRequest, apiErr.Status) + }) + + t.Run("unauthorized op silently ignored in non-strict mode", func(t *testing.T) { + params := url.Values{"filter[name][gt]": {"foo"}} // gt not in authorized ops + attrs, err := request.GetAipAttributes(newFilterRequest(t, params), request.WithAuthorizedFilters(authorized)) + require.NoError(t, err) + assert.Empty(t, attrs.Filters) + }) + + t.Run("unauthorized op returns 400 in strict mode", func(t *testing.T) { + params := url.Values{"filter[name][gt]": {"foo"}} + _, err := request.GetAipAttributes(newFilterRequest(t, params), + request.WithAuthorizedFilters(authorized), + request.WithAipStrictMode(), + ) + require.Error(t, err) + var apiErr *apierrors.BaseAPIError + require.True(t, errors.As(err, &apiErr)) + assert.Equal(t, http.StatusBadRequest, apiErr.Status) + }) +} + +func TestExtractFilter_DotFilters(t *testing.T) { + authorized := request.AuthorizedFilters{ + "labels": { + Filters: []request.QueryFilterOp{request.QueryFilterEQ}, + DotFilter: true, + }, + } + + t.Run("dot sub-attribute passes for DotFilter field", func(t *testing.T) { + params := url.Values{"filter[labels.env][eq]": {"prod"}} + attrs, err := request.GetAipAttributes(newFilterRequest(t, params), request.WithAuthorizedFilters(authorized)) + require.NoError(t, err) + require.Len(t, attrs.Filters, 1) + assert.Equal(t, "labels.env", attrs.Filters[0].Name) + assert.Equal(t, "prod", attrs.Filters[0].Value) + }) + + t.Run("bare field name rejected for DotFilter-only field", func(t *testing.T) { + params := url.Values{"filter[labels][eq]": {"prod"}} + attrs, err := request.GetAipAttributes(newFilterRequest(t, params), request.WithAuthorizedFilters(authorized)) + require.NoError(t, err) + assert.Empty(t, attrs.Filters) + }) +} + +func TestExtractFilter_ValidationFunc(t *testing.T) { + errCustom := errors.New("custom validation error") + authorized := request.AuthorizedFilters{ + "id": { + Filters: []request.QueryFilterOp{request.QueryFilterEQ}, + ValidationFunc: func(_, value string) error { + if value == "bad" { + return errCustom + } + return nil + }, + }, + "uuid_id": { + Filters: []request.QueryFilterOp{request.QueryFilterEQ}, + ValidationFunc: func(_, value string) error { + if value == "not-a-uuid" { + return request.ErrReturnEmptySet + } + return nil + }, + }, + } + + t.Run("valid value passes through", func(t *testing.T) { + params := url.Values{"filter[id][eq]": {"good-value"}} + attrs, err := request.GetAipAttributes(newFilterRequest(t, params), request.WithAuthorizedFilters(authorized)) + require.NoError(t, err) + require.Len(t, attrs.Filters, 1) + }) + + t.Run("validation error returns 400", func(t *testing.T) { + params := url.Values{"filter[id][eq]": {"bad"}} + _, err := request.GetAipAttributes(newFilterRequest(t, params), request.WithAuthorizedFilters(authorized)) + require.Error(t, err) + var apiErr *apierrors.BaseAPIError + require.True(t, errors.As(err, &apiErr)) + assert.Equal(t, http.StatusBadRequest, apiErr.Status) + }) + + t.Run("ErrReturnEmptySet returns 200 empty-set response", func(t *testing.T) { + params := url.Values{"filter[uuid_id][eq]": {"not-a-uuid"}} + _, err := request.GetAipAttributes(newFilterRequest(t, params), request.WithAuthorizedFilters(authorized)) + require.Error(t, err) + var apiErr *apierrors.BaseAPIError + require.True(t, errors.As(err, &apiErr)) + assert.Equal(t, http.StatusOK, apiErr.Status) + }) +} diff --git a/api/v3/request/aip_pagination.go b/api/v3/request/aip_pagination.go index 0bd8c4b563..a1497b0bbd 100644 --- a/api/v3/request/aip_pagination.go +++ b/api/v3/request/aip_pagination.go @@ -39,73 +39,27 @@ type Pagination struct { } func extractPagination(ctx context.Context, qs url.Values, c *config) (Pagination, *apierrors.BaseAPIError) { - p := Pagination{ - Size: c.defaultPageSize, + size, apiErr := parsePageSize(ctx, qs, c) + if apiErr != nil { + return Pagination{}, apiErr } - if qs.Has(PageSizeQuery) { - strPageSize := qs.Get(PageSizeQuery) - pageSize, err := strconv.ParseInt(strPageSize, 10, 16) - if err != nil { - if c.strictMode || pageSize < 0 { - return p, apierrors.NewBadRequestError(ctx, err, - apierrors.InvalidParameters{ - apierrors.InvalidParameter{ - Field: PageSizeQuery, - Reason: "unable to parse query field", - Source: apierrors.InvalidParamSourceQuery, - Rule: "page size should be a positive integer", - }, - }) - } else { - pageSize = int64(c.defaultPageSize) - } - } - if pageSize < 1 { - pageSize = DefaultPaginationSize - } - p.Size = int(pageSize) - } - - if p.Size > c.maxPageSize { - p.Size = c.maxPageSize - } - - if c.paginationKind == paginationKindOffset { - p.Number = DefaultPaginationNumber - - if qs.Has(PageNumberQuery) { - strPageNumber := qs.Get(PageNumberQuery) - pageNumber, err := strconv.ParseInt(strPageNumber, 10, 16) - if err != nil { - if c.strictMode || pageNumber < 0 { - return p, apierrors.NewBadRequestError(ctx, err, - apierrors.InvalidParameters{ - apierrors.InvalidParameter{ - Field: PageNumberQuery, - Reason: "unable to parse query field", - Source: apierrors.InvalidParamSourceQuery, - Rule: "page number should be a positive integer", - }, - }) - } - } - if pageNumber < 1 { - pageNumber = DefaultPaginationNumber - } - p.Number = int(pageNumber) - } + p := Pagination{Size: min(size, c.maxPageSize)} - var coef int - coef = int(p.Number) - 1 - if coef < 0 { - coef = 0 + switch c.paginationKind { + case paginationKindOffset: + number, apiErr := parsePageNumber(ctx, qs, c) + if apiErr != nil { + return Pagination{}, apiErr } + p.Number = number + coef := max(p.Number-1, 0) p.Offset = coef * p.Size p.Limit = p.Size - } else if c.paginationKind == paginationKindCursor { + + case paginationKindCursor: if qs.Has(PageBeforeQuery) && qs.Has(PageAfterQuery) { - return p, apierrors.NewBadRequestError(ctx, ErrCursorRange, + return Pagination{}, apierrors.NewBadRequestError(ctx, ErrCursorRange, apierrors.InvalidParameters{ apierrors.InvalidParameter{ Source: apierrors.InvalidParamSourceQuery, @@ -114,33 +68,95 @@ func extractPagination(ctx context.Context, qs url.Values, c *config) (Paginatio }) } - if qs.Has(PageBeforeQuery) { - b, err := decodeCursorAfterQueryUnescape(c.cursorCipherKey, qs.Get(PageBeforeQuery), c.cursorValidateUUIDs) - if err != nil { - return p, apierrors.NewBadRequestError(ctx, err, - apierrors.InvalidParameters{ - apierrors.InvalidParameter{ - Source: apierrors.InvalidParamSourceQuery, - Reason: fmt.Sprintf("unable to parse %s cursor", PageBeforeQuery), - }, - }) - } - p.Before = b + before, apiErr := parseCursorParam(ctx, qs, PageBeforeQuery, c.cursorCipherKey, c.cursorValidateUUIDs) + if apiErr != nil { + return Pagination{}, apiErr } - if qs.Has(PageAfterQuery) { - a, err := decodeCursorAfterQueryUnescape(c.cursorCipherKey, qs.Get(PageAfterQuery), c.cursorValidateUUIDs) - if err != nil { - return p, apierrors.NewBadRequestError(ctx, err, - apierrors.InvalidParameters{ - apierrors.InvalidParameter{ - Source: apierrors.InvalidParamSourceQuery, - Reason: fmt.Sprintf("unable to parse %s cursor", PageAfterQuery), - }, - }) - } - p.After = a + p.Before = before + + after, apiErr := parseCursorParam(ctx, qs, PageAfterQuery, c.cursorCipherKey, c.cursorValidateUUIDs) + if apiErr != nil { + return Pagination{}, apiErr } + p.After = after } return p, nil } + +func parsePageSize(ctx context.Context, qs url.Values, c *config) (int, *apierrors.BaseAPIError) { + if !qs.Has(PageSizeQuery) { + return c.defaultPageSize, nil + } + + pageSize, err := strconv.ParseInt(qs.Get(PageSizeQuery), 10, 16) + if err != nil && (c.strictMode || pageSize < 0) { + return 0, apierrors.NewBadRequestError(ctx, err, + apierrors.InvalidParameters{ + apierrors.InvalidParameter{ + Field: PageSizeQuery, + Reason: "unable to parse query field", + Source: apierrors.InvalidParamSourceQuery, + Rule: "page size should be a positive integer", + }, + }) + } + if err != nil { + return c.defaultPageSize, nil + } + if pageSize < 1 { + return DefaultPaginationSize, nil + } + return int(pageSize), nil +} + +func parsePageNumber(ctx context.Context, qs url.Values, c *config) (int, *apierrors.BaseAPIError) { + if !qs.Has(PageNumberQuery) { + return DefaultPaginationNumber, nil + } + + pageNumber, err := strconv.ParseInt(qs.Get(PageNumberQuery), 10, 16) + if err != nil && c.strictMode { + return 0, apierrors.NewBadRequestError(ctx, err, + apierrors.InvalidParameters{ + apierrors.InvalidParameter{ + Field: PageNumberQuery, + Reason: "unable to parse query field", + Source: apierrors.InvalidParamSourceQuery, + Rule: "page number should be a positive integer", + }, + }) + } + if err == nil && pageNumber < 0 { + return 0, apierrors.NewBadRequestError(ctx, err, + apierrors.InvalidParameters{ + apierrors.InvalidParameter{ + Field: PageNumberQuery, + Reason: "unable to parse query field", + Source: apierrors.InvalidParamSourceQuery, + Rule: "page number should be a positive integer", + }, + }) + } + if err != nil || pageNumber < 1 { + return DefaultPaginationNumber, nil + } + return int(pageNumber), nil +} + +func parseCursorParam(ctx context.Context, qs url.Values, key, cipherKey string, validateUUIDs bool) (*Cursor, *apierrors.BaseAPIError) { + if !qs.Has(key) { + return nil, nil + } + cursor, err := decodeCursorAfterQueryUnescape(cipherKey, qs.Get(key), validateUUIDs) + if err != nil { + return nil, apierrors.NewBadRequestError(ctx, err, + apierrors.InvalidParameters{ + apierrors.InvalidParameter{ + Source: apierrors.InvalidParamSourceQuery, + Reason: fmt.Sprintf("unable to parse %s cursor", key), + }, + }) + } + return cursor, nil +} diff --git a/api/v3/request/aip_sort.go b/api/v3/request/aip_sort.go index 8b4ffeb7c0..a007afdea7 100644 --- a/api/v3/request/aip_sort.go +++ b/api/v3/request/aip_sort.go @@ -4,6 +4,8 @@ import ( "net/url" "slices" "strings" + + "github.com/samber/lo" ) const SortQuery = "sort" @@ -22,11 +24,10 @@ func extractSort(qs url.Values, c *config) ([]SortBy, error) { } segments := strings.Split(qs.Get(SortQuery), ",") - out := make([]SortBy, 0, len(segments)) - for _, v := range segments { + out := lo.FilterMap(segments, func(v string, _ int) (SortBy, bool) { parts := strings.Fields(strings.TrimSpace(v)) if len(parts) == 0 { - continue + return SortBy{}, false } sortBy := SortBy{Field: parts[0], Order: SortOrderAsc} if len(parts) > 1 { @@ -35,10 +36,8 @@ func extractSort(qs url.Values, c *config) ([]SortBy, error) { sortBy.Order = order } } - if isAuthorizedSort(sortBy.Field, c) { - out = append(out, sortBy) - } - } + return sortBy, isAuthorizedSort(sortBy.Field, c) + }) return out, nil } diff --git a/api/v3/request/aip_sort_test.go b/api/v3/request/aip_sort_test.go new file mode 100644 index 0000000000..7e0ec1ff62 --- /dev/null +++ b/api/v3/request/aip_sort_test.go @@ -0,0 +1,167 @@ +package request_test + +import ( + "net/http" + "net/http/httptest" + "net/url" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/openmeterio/openmeter/api/v3/request" +) + +func newSortRequest(t *testing.T, sortValue string) *http.Request { + t.Helper() + r := httptest.NewRequest(http.MethodGet, "/", nil) + r.URL.RawQuery = url.Values{"sort": {sortValue}}.Encode() + return r +} + +func TestExtractSort_NoParam(t *testing.T) { + t.Run("no sort param returns nil", func(t *testing.T) { + r := httptest.NewRequest(http.MethodGet, "/", nil) + attrs, err := request.GetAipAttributes(r) + require.NoError(t, err) + assert.Nil(t, attrs.Sorts) + }) + + t.Run("no sort param with default sort returns default", func(t *testing.T) { + r := httptest.NewRequest(http.MethodGet, "/", nil) + attrs, err := request.GetAipAttributes(r, request.WithDefaultSort("created_at", request.SortOrderDesc)) + require.NoError(t, err) + require.Len(t, attrs.Sorts, 1) + assert.Equal(t, "created_at", attrs.Sorts[0].Field) + assert.Equal(t, request.SortOrderDesc, attrs.Sorts[0].Order) + }) + + t.Run("explicit sort param overrides default", func(t *testing.T) { + attrs, err := request.GetAipAttributes( + newSortRequest(t, "name"), + request.WithDefaultSort("created_at", request.SortOrderDesc), + ) + require.NoError(t, err) + require.Len(t, attrs.Sorts, 1) + assert.Equal(t, "name", attrs.Sorts[0].Field) + }) +} + +func TestExtractSort_SingleField(t *testing.T) { + t.Run("field only defaults to asc", func(t *testing.T) { + attrs, err := request.GetAipAttributes(newSortRequest(t, "name")) + require.NoError(t, err) + require.Len(t, attrs.Sorts, 1) + assert.Equal(t, "name", attrs.Sorts[0].Field) + assert.Equal(t, request.SortOrderAsc, attrs.Sorts[0].Order) + }) + + t.Run("field with asc order", func(t *testing.T) { + attrs, err := request.GetAipAttributes(newSortRequest(t, "name asc")) + require.NoError(t, err) + require.Len(t, attrs.Sorts, 1) + assert.Equal(t, request.SortOrderAsc, attrs.Sorts[0].Order) + }) + + t.Run("field with desc order", func(t *testing.T) { + attrs, err := request.GetAipAttributes(newSortRequest(t, "name desc")) + require.NoError(t, err) + require.Len(t, attrs.Sorts, 1) + assert.Equal(t, "name", attrs.Sorts[0].Field) + assert.Equal(t, request.SortOrderDesc, attrs.Sorts[0].Order) + }) + + t.Run("unknown order string defaults to asc", func(t *testing.T) { + attrs, err := request.GetAipAttributes(newSortRequest(t, "name invalid_order")) + require.NoError(t, err) + require.Len(t, attrs.Sorts, 1) + assert.Equal(t, request.SortOrderAsc, attrs.Sorts[0].Order) + }) +} + +func TestExtractSort_MultipleFields(t *testing.T) { + t.Run("two comma-separated fields", func(t *testing.T) { + attrs, err := request.GetAipAttributes(newSortRequest(t, "name,created_at")) + require.NoError(t, err) + require.Len(t, attrs.Sorts, 2) + assert.Equal(t, "name", attrs.Sorts[0].Field) + assert.Equal(t, request.SortOrderAsc, attrs.Sorts[0].Order) + assert.Equal(t, "created_at", attrs.Sorts[1].Field) + assert.Equal(t, request.SortOrderAsc, attrs.Sorts[1].Order) + }) + + t.Run("mixed order on multiple fields", func(t *testing.T) { + attrs, err := request.GetAipAttributes(newSortRequest(t, "name asc,created_at desc")) + require.NoError(t, err) + require.Len(t, attrs.Sorts, 2) + assert.Equal(t, request.SortOrderAsc, attrs.Sorts[0].Order) + assert.Equal(t, request.SortOrderDesc, attrs.Sorts[1].Order) + }) + + t.Run("empty segment in comma list is ignored", func(t *testing.T) { + attrs, err := request.GetAipAttributes(newSortRequest(t, "name,,created_at")) + require.NoError(t, err) + assert.Len(t, attrs.Sorts, 2) + }) +} + +func TestExtractSort_AuthorizedSorts(t *testing.T) { + t.Run("authorized field passes through", func(t *testing.T) { + attrs, err := request.GetAipAttributes( + newSortRequest(t, "name"), + request.WithAuthorizedSorts([]string{"name", "created_at"}), + ) + require.NoError(t, err) + require.Len(t, attrs.Sorts, 1) + assert.Equal(t, "name", attrs.Sorts[0].Field) + }) + + t.Run("unauthorized field is silently ignored", func(t *testing.T) { + attrs, err := request.GetAipAttributes( + newSortRequest(t, "unknown_field"), + request.WithAuthorizedSorts([]string{"name"}), + ) + require.NoError(t, err) + assert.Empty(t, attrs.Sorts) + }) + + t.Run("only authorized fields pass through from multi-field sort", func(t *testing.T) { + attrs, err := request.GetAipAttributes( + newSortRequest(t, "name,unknown_field"), + request.WithAuthorizedSorts([]string{"name"}), + ) + require.NoError(t, err) + require.Len(t, attrs.Sorts, 1) + assert.Equal(t, "name", attrs.Sorts[0].Field) + }) +} + +func TestExtractSort_AuthorizedDotSorts(t *testing.T) { + t.Run("dot sub-attribute authorized by prefix", func(t *testing.T) { + attrs, err := request.GetAipAttributes( + newSortRequest(t, "labels.env"), + request.WithAuthorizedDotSorts([]string{"labels"}), + ) + require.NoError(t, err) + require.Len(t, attrs.Sorts, 1) + assert.Equal(t, "labels.env", attrs.Sorts[0].Field) + }) + + t.Run("exact dot field authorized", func(t *testing.T) { + attrs, err := request.GetAipAttributes( + newSortRequest(t, "labels.env"), + request.WithAuthorizedDotSorts([]string{"labels.env"}), + ) + require.NoError(t, err) + require.Len(t, attrs.Sorts, 1) + }) + + t.Run("unauthorized dot prefix is ignored", func(t *testing.T) { + attrs, err := request.GetAipAttributes( + newSortRequest(t, "other.key"), + request.WithAuthorizedDotSorts([]string{"labels"}), + ) + require.NoError(t, err) + assert.Empty(t, attrs.Sorts) + }) +} diff --git a/api/v3/request/aip_test.go b/api/v3/request/aip_test.go new file mode 100644 index 0000000000..bfcf85b170 --- /dev/null +++ b/api/v3/request/aip_test.go @@ -0,0 +1,251 @@ +package request_test + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/openmeterio/openmeter/api/v3/filters" + "github.com/openmeterio/openmeter/api/v3/request" +) + +func TestGetAipAttributes_Pagination(t *testing.T) { + t.Run("defaults applied when no params", func(t *testing.T) { + r := httptest.NewRequest(http.MethodGet, "/", nil) + attrs, err := request.GetAipAttributes(r) + require.NoError(t, err) + assert.Equal(t, request.DefaultPaginationSize, attrs.Pagination.Size) + assert.Equal(t, request.DefaultPaginationNumber, attrs.Pagination.Number) + }) + + t.Run("page size and number parsed", func(t *testing.T) { + r := httptest.NewRequest(http.MethodGet, "/?page%5Bsize%5D=5&page%5Bnumber%5D=3", nil) + attrs, err := request.GetAipAttributes(r) + require.NoError(t, err) + assert.Equal(t, 5, attrs.Pagination.Size) + assert.Equal(t, 3, attrs.Pagination.Number) + }) + + t.Run("page size capped at max", func(t *testing.T) { + r := httptest.NewRequest(http.MethodGet, "/?page%5Bsize%5D=999", nil) + attrs, err := request.GetAipAttributes(r, request.WithMaxPageSize(50)) + require.NoError(t, err) + assert.Equal(t, 50, attrs.Pagination.Size) + }) +} + +func TestGetAipAttributes_Combined(t *testing.T) { + t.Run("pagination filter and sort all parsed together", func(t *testing.T) { + r := httptest.NewRequest(http.MethodGet, "/", nil) + q := r.URL.Query() + q.Set("page[size]", "5") + q.Set("page[number]", "2") + q.Set("filter[name][eq]", "foo") + q.Set("sort", "name desc") + r.URL.RawQuery = q.Encode() + + attrs, err := request.GetAipAttributes(r) + require.NoError(t, err) + assert.Equal(t, 5, attrs.Pagination.Size) + assert.Equal(t, 2, attrs.Pagination.Number) + require.Len(t, attrs.Filters, 1) + assert.Equal(t, "name", attrs.Filters[0].Name) + require.Len(t, attrs.Sorts, 1) + assert.Equal(t, "name", attrs.Sorts[0].Field) + assert.Equal(t, request.SortOrderDesc, attrs.Sorts[0].Order) + }) +} + +func TestRemapAipAttributes(t *testing.T) { + t.Run("remaps a filter field name", func(t *testing.T) { + attrs := &request.AipAttributes{ + Filters: []request.QueryFilter{ + {Name: "api_name", Value: "foo", Filter: request.QueryFilterEQ}, + }, + } + request.RemapAipAttributes(attrs, map[string]string{"api_name": "db_name"}) + assert.Equal(t, "db_name", attrs.Filters[0].Name) + }) + + t.Run("unmapped filter field is unchanged", func(t *testing.T) { + attrs := &request.AipAttributes{ + Filters: []request.QueryFilter{ + {Name: "other", Value: "foo", Filter: request.QueryFilterEQ}, + }, + } + request.RemapAipAttributes(attrs, map[string]string{"api_name": "db_name"}) + assert.Equal(t, "other", attrs.Filters[0].Name) + }) + + t.Run("remaps dot filter preserving sub-attribute", func(t *testing.T) { + attrs := &request.AipAttributes{ + Filters: []request.QueryFilter{ + {Name: "labels.env", Value: "prod", Filter: request.QueryFilterEQ}, + }, + } + request.RemapAipAttributes(attrs, map[string]string{"labels": "metadata"}) + assert.Equal(t, "metadata.env", attrs.Filters[0].Name) + }) + + t.Run("remaps a sort field name", func(t *testing.T) { + attrs := &request.AipAttributes{ + Sorts: []request.SortBy{ + {Field: "api_name", Order: request.SortOrderAsc}, + }, + } + request.RemapAipAttributes(attrs, map[string]string{"api_name": "db_name"}) + assert.Equal(t, "db_name", attrs.Sorts[0].Field) + }) + + t.Run("unmapped sort field is unchanged", func(t *testing.T) { + attrs := &request.AipAttributes{ + Sorts: []request.SortBy{ + {Field: "other", Order: request.SortOrderAsc}, + }, + } + request.RemapAipAttributes(attrs, map[string]string{"api_name": "db_name"}) + assert.Equal(t, "other", attrs.Sorts[0].Field) + }) + + t.Run("remaps dot sort preserving sub-attribute", func(t *testing.T) { + attrs := &request.AipAttributes{ + Sorts: []request.SortBy{ + {Field: "labels.env", Order: request.SortOrderDesc}, + }, + } + request.RemapAipAttributes(attrs, map[string]string{"labels": "metadata"}) + assert.Equal(t, "metadata.env", attrs.Sorts[0].Field) + }) + + t.Run("nil filters and sorts are no-ops", func(t *testing.T) { + attrs := &request.AipAttributes{} + request.RemapAipAttributes(attrs, map[string]string{"api_name": "db_name"}) + assert.Nil(t, attrs.Filters) + assert.Nil(t, attrs.Sorts) + }) +} + +func TestFilterStringFromAip(t *testing.T) { + t.Run("returns nil when filter list is empty", func(t *testing.T) { + assert.Nil(t, request.FilterStringFromAip(nil, "name")) + }) + + t.Run("returns nil when no filter matches the field", func(t *testing.T) { + f := request.FilterStringFromAip([]request.QueryFilter{ + {Name: "other", Value: "foo", Filter: request.QueryFilterEQ}, + }, "name") + assert.Nil(t, f) + }) + + t.Run("only processes the matching field", func(t *testing.T) { + f := request.FilterStringFromAip([]request.QueryFilter{ + {Name: "name", Value: "foo", Filter: request.QueryFilterEQ}, + {Name: "other", Value: "bar", Filter: request.QueryFilterNEQ}, + }, "name") + require.NotNil(t, f) + require.NotNil(t, f.Eq) + assert.Equal(t, "foo", *f.Eq) + assert.Nil(t, f.Neq) + }) + + filterOpCases := []struct { + name string + filterOp request.QueryFilterOp + value string + checkFunc func(t *testing.T, f *filters.StringFilter) + }{ + { + name: "eq", filterOp: request.QueryFilterEQ, value: "v", + checkFunc: func(t *testing.T, f *filters.StringFilter) { + t.Helper() + require.NotNil(t, f.Eq) + assert.Equal(t, "v", *f.Eq) + }, + }, + { + name: "neq", filterOp: request.QueryFilterNEQ, value: "v", + checkFunc: func(t *testing.T, f *filters.StringFilter) { + t.Helper() + require.NotNil(t, f.Neq) + assert.Equal(t, "v", *f.Neq) + }, + }, + { + name: "gt", filterOp: request.QueryFilterGT, value: "v", + checkFunc: func(t *testing.T, f *filters.StringFilter) { + t.Helper() + require.NotNil(t, f.Gt) + assert.Equal(t, "v", *f.Gt) + }, + }, + { + name: "gte", filterOp: request.QueryFilterGTE, value: "v", + checkFunc: func(t *testing.T, f *filters.StringFilter) { + t.Helper() + require.NotNil(t, f.Gte) + assert.Equal(t, "v", *f.Gte) + }, + }, + { + name: "lt", filterOp: request.QueryFilterLT, value: "v", + checkFunc: func(t *testing.T, f *filters.StringFilter) { + t.Helper() + require.NotNil(t, f.Lt) + assert.Equal(t, "v", *f.Lt) + }, + }, + { + name: "lte", filterOp: request.QueryFilterLTE, value: "v", + checkFunc: func(t *testing.T, f *filters.StringFilter) { + t.Helper() + require.NotNil(t, f.Lte) + assert.Equal(t, "v", *f.Lte) + }, + }, + { + name: "contains", filterOp: request.QueryFilterContains, value: "v", + checkFunc: func(t *testing.T, f *filters.StringFilter) { + t.Helper() + require.NotNil(t, f.Contains) + assert.Equal(t, "v", *f.Contains) + }, + }, + { + name: "oeq", filterOp: request.QueryFilterOrEQ, value: "v", + checkFunc: func(t *testing.T, f *filters.StringFilter) { + t.Helper() + require.NotNil(t, f.Oeq) + assert.Equal(t, "v", *f.Oeq) + }, + }, + { + name: "ocontains", filterOp: request.QueryFilterOrContains, value: "v", + checkFunc: func(t *testing.T, f *filters.StringFilter) { + t.Helper() + require.NotNil(t, f.Ocontains) + assert.Equal(t, "v", *f.Ocontains) + }, + }, + { + name: "exists", filterOp: request.QueryFilterExists, + checkFunc: func(t *testing.T, f *filters.StringFilter) { + t.Helper() + require.NotNil(t, f.Exists) + assert.True(t, *f.Exists) + }, + }, + } + + for _, tc := range filterOpCases { + t.Run("maps "+tc.name+" to StringFilter", func(t *testing.T) { + f := request.FilterStringFromAip([]request.QueryFilter{ + {Name: "field", Value: tc.value, Filter: tc.filterOp}, + }, "field") + require.NotNil(t, f) + tc.checkFunc(t, f) + }) + } +} diff --git a/api/v3/request/cursor.go b/api/v3/request/cursor.go index cd95089ea2..f81506c2fb 100644 --- a/api/v3/request/cursor.go +++ b/api/v3/request/cursor.go @@ -7,6 +7,7 @@ import ( "strings" "github.com/google/uuid" + "github.com/samber/lo" ) // ErrInvalidCursor is used when a cursor does not conform to the expected format. @@ -129,10 +130,8 @@ func decodeCursorAfterQueryUnescape(cipherKey, cursor string, validateAsUUID boo if validateAsUUID { ids := strings.Split(c.decoded, ":") - for _, id := range ids { - if _, err := uuid.Parse(id); err != nil { - return nil, ErrInvalidCursor - } + if !lo.EveryBy(ids, func(id string) bool { _, err := uuid.Parse(id); return err == nil }) { + return nil, ErrInvalidCursor } } diff --git a/api/v3/request/cursor_test.go b/api/v3/request/cursor_test.go new file mode 100644 index 0000000000..390317a079 --- /dev/null +++ b/api/v3/request/cursor_test.go @@ -0,0 +1,139 @@ +package request_test + +import ( + "errors" + "testing" + + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/openmeterio/openmeter/api/v3/request" +) + +func TestEncodeCursor(t *testing.T) { + t.Run("empty id returns ErrInvalidCursor", func(t *testing.T) { + _, err := request.EncodeCursor(request.DefaultCipherKey, "") + require.Error(t, err) + assert.True(t, errors.Is(err, request.ErrInvalidCursor)) + }) + + t.Run("non-empty id returns non-empty encoded string", func(t *testing.T) { + c, err := request.EncodeCursor(request.DefaultCipherKey, uuid.New().String()) + require.NoError(t, err) + assert.NotEmpty(t, c.String()) + }) + + t.Run("encoded value differs from original id", func(t *testing.T) { + id := uuid.New().String() + c, err := request.EncodeCursor(request.DefaultCipherKey, id) + require.NoError(t, err) + assert.NotEqual(t, id, c.String()) + }) +} + +func TestDecodeCursor(t *testing.T) { + t.Run("empty cursor returns ErrInvalidCursor", func(t *testing.T) { + _, err := request.DecodeCursor(request.DefaultCipherKey, "", false) + require.Error(t, err) + assert.True(t, errors.Is(err, request.ErrInvalidCursor)) + }) + + t.Run("non-base64 input returns ErrInvalidCursor", func(t *testing.T) { + _, err := request.DecodeCursor(request.DefaultCipherKey, "not-valid-base64!!!", false) + require.Error(t, err) + assert.True(t, errors.Is(err, request.ErrInvalidCursor)) + }) + + t.Run("valid base64 with wrong content returns ErrInvalidCursor", func(t *testing.T) { + // base64("hello") — valid base64 but not a valid cursor + _, err := request.DecodeCursor(request.DefaultCipherKey, "aGVsbG8=", false) + require.Error(t, err) + assert.True(t, errors.Is(err, request.ErrInvalidCursor)) + }) +} + +func TestCursorRoundtrip(t *testing.T) { + t.Run("single uuid roundtrip", func(t *testing.T) { + id := uuid.New().String() + encoded, err := request.EncodeCursor(request.DefaultCipherKey, id) + require.NoError(t, err) + + decoded, err := request.DecodeCursor(request.DefaultCipherKey, encoded.String(), false) + require.NoError(t, err) + assert.Equal(t, id, decoded.ID()) + }) + + t.Run("compound uuid roundtrip", func(t *testing.T) { + id := uuid.New().String() + ":" + uuid.New().String() + encoded, err := request.EncodeCursor(request.DefaultCipherKey, id) + require.NoError(t, err) + + decoded, err := request.DecodeCursor(request.DefaultCipherKey, encoded.String(), false) + require.NoError(t, err) + assert.Equal(t, id, decoded.ID()) + }) + + t.Run("custom cipher key roundtrip", func(t *testing.T) { + const key = "custom-cipher-key-long-enough-for-testing-purposes" + id := uuid.New().String() + encoded, err := request.EncodeCursor(key, id) + require.NoError(t, err) + + decoded, err := request.DecodeCursor(key, encoded.String(), false) + require.NoError(t, err) + assert.Equal(t, id, decoded.ID()) + }) + + t.Run("wrong cipher key fails to decode correctly but does not error without uuid validation", func(t *testing.T) { + id := uuid.New().String() + encoded, err := request.EncodeCursor(request.DefaultCipherKey, id) + require.NoError(t, err) + + decoded, err := request.DecodeCursor("wrong-key-that-is-long-enough-for-xor-cipher-test", encoded.String(), false) + // decodes without error but ID will differ + require.NoError(t, err) + assert.NotEqual(t, id, decoded.ID()) + }) +} + +func TestCursorUUIDValidation(t *testing.T) { + t.Run("valid uuid passes validation", func(t *testing.T) { + id := uuid.New().String() + encoded, err := request.EncodeCursor(request.DefaultCipherKey, id) + require.NoError(t, err) + + decoded, err := request.DecodeCursor(request.DefaultCipherKey, encoded.String(), true) + require.NoError(t, err) + assert.Equal(t, id, decoded.ID()) + }) + + t.Run("non-uuid value fails validation", func(t *testing.T) { + encoded, err := request.EncodeCursor(request.DefaultCipherKey, "not-a-uuid") + require.NoError(t, err) + + _, err = request.DecodeCursor(request.DefaultCipherKey, encoded.String(), true) + require.Error(t, err) + assert.True(t, errors.Is(err, request.ErrInvalidCursor)) + }) + + t.Run("compound valid uuids pass validation", func(t *testing.T) { + id := uuid.New().String() + ":" + uuid.New().String() + encoded, err := request.EncodeCursor(request.DefaultCipherKey, id) + require.NoError(t, err) + + decoded, err := request.DecodeCursor(request.DefaultCipherKey, encoded.String(), true) + require.NoError(t, err) + assert.Equal(t, id, decoded.ID()) + }) + + t.Run("compound with one invalid part fails validation", func(t *testing.T) { + id := uuid.New().String() + ":not-a-uuid" + encoded, err := request.EncodeCursor(request.DefaultCipherKey, id) + require.NoError(t, err) + + _, err = request.DecodeCursor(request.DefaultCipherKey, encoded.String(), true) + require.Error(t, err) + assert.True(t, errors.Is(err, request.ErrInvalidCursor)) + }) +}