diff --git a/api/spec/packages/aip-client-javascript/src/funcs/planAddons.ts b/api/spec/packages/aip-client-javascript/src/funcs/planAddons.ts index 3bdbdf8f89..28cc0266df 100644 --- a/api/spec/packages/aip-client-javascript/src/funcs/planAddons.ts +++ b/api/spec/packages/aip-client-javascript/src/funcs/planAddons.ts @@ -22,6 +22,8 @@ export function listPlanAddons( ): Promise> { const searchParams = toURLSearchParams({ page: req.page, + sort: encodeSort(req.sort), + filter: req.filter, }) const path = encodePath('openmeter/plans/{planId}/addons', { planId: req.planId, diff --git a/api/spec/packages/aip-client-javascript/src/index.ts b/api/spec/packages/aip-client-javascript/src/index.ts index ea535f596f..d62e0eca45 100644 --- a/api/spec/packages/aip-client-javascript/src/index.ts +++ b/api/spec/packages/aip-client-javascript/src/index.ts @@ -208,6 +208,7 @@ export type { ListSubscriptionsParamsFilter, ListFeatureParamsFilter, ListAddonsParamsFilter, + ListPlanAddonsParamsFilter, CreateCreditGrantTaxConfig, CreditGrantTaxConfig, TaxConfig, diff --git a/api/spec/packages/aip-client-javascript/src/models/operations/planAddons.ts b/api/spec/packages/aip-client-javascript/src/models/operations/planAddons.ts index b845d47f4b..c4a5a9bea7 100644 --- a/api/spec/packages/aip-client-javascript/src/models/operations/planAddons.ts +++ b/api/spec/packages/aip-client-javascript/src/models/operations/planAddons.ts @@ -2,14 +2,29 @@ import { z } from 'zod' import * as schemas from '../schemas.js' import type { CreatePlanAddonRequest as CreatePlanAddonRequestBody, + ListPlanAddonsParamsFilter, PlanAddon, PlanAddonPagePaginatedResponse, + SortQueryInput, UpsertPlanAddonRequest, } from '../types.js' export interface ListPlanAddonsQuery { /** Determines which page of the collection to retrieve. */ page?: { size?: number; number?: number } + /** + * Sort plan add-ons returned in the response. Supported sort attributes are: + * + * - `id` (default) + * - `created_at` + * - `updated_at` + * + * The `asc` suffix is optional as the default sort order is ascending. The `desc` + * suffix is used to specify a descending order. + */ + sort?: SortQueryInput + /** Filter plan add-ons returned in the response. */ + filter?: ListPlanAddonsParamsFilter } export type ListPlanAddonsRequest = ListPlanAddonsQuery & { planId: string } diff --git a/api/spec/packages/aip-client-javascript/src/models/schemas.ts b/api/spec/packages/aip-client-javascript/src/models/schemas.ts index 90a3dd4a99..d081e0a0c9 100644 --- a/api/spec/packages/aip-client-javascript/src/models/schemas.ts +++ b/api/spec/packages/aip-client-javascript/src/models/schemas.ts @@ -3107,6 +3107,17 @@ export const listAddonsParamsFilter = z }) .describe('Filter options for listing add-ons.') +export const listPlanAddonsParamsFilter = z + .object({ + id: ulidFieldFilter.optional(), + plan_key: stringFieldFilter.optional(), + addon_id: ulidFieldFilter.optional(), + addon_key: stringFieldFilter.optional(), + addon_name: stringFieldFilter.optional(), + plan_currency: stringFieldFilter.optional(), + }) + .describe('Filter options for listing plan add-ons.') + export const createCreditGrantTaxConfig = z .object({ behavior: taxBehavior.optional(), @@ -5954,6 +5965,8 @@ export const listPlanAddonsQueryParams = z.object({ }) .optional() .describe('Determines which page of the collection to retrieve.'), + sort: sortQuery.optional(), + filter: listPlanAddonsParamsFilter.optional(), }) export const listPlanAddonsResponse = z.object({ diff --git a/api/spec/packages/aip-client-javascript/src/models/types.ts b/api/spec/packages/aip-client-javascript/src/models/types.ts index 0a7172c7d3..7146c4ef8b 100644 --- a/api/spec/packages/aip-client-javascript/src/models/types.ts +++ b/api/spec/packages/aip-client-javascript/src/models/types.ts @@ -2554,6 +2554,68 @@ export interface ListAddonsParamsFilter { currency?: string | { eq?: string; oeq?: string[]; neq?: string } } +/** Filter options for listing plan add-ons. */ +export interface ListPlanAddonsParamsFilter { + id?: string | { eq?: string; oeq?: string[]; neq?: string } + plan_key?: + | string + | { + eq?: string + neq?: string + contains?: string + ocontains?: string[] + oeq?: string[] + gt?: string + gte?: string + lt?: string + lte?: string + exists?: boolean + } + addon_id?: string | { eq?: string; oeq?: string[]; neq?: string } + addon_key?: + | string + | { + eq?: string + neq?: string + contains?: string + ocontains?: string[] + oeq?: string[] + gt?: string + gte?: string + lt?: string + lte?: string + exists?: boolean + } + addon_name?: + | string + | { + eq?: string + neq?: string + contains?: string + ocontains?: string[] + oeq?: string[] + gt?: string + gte?: string + lt?: string + lte?: string + exists?: boolean + } + plan_currency?: + | string + | { + eq?: string + neq?: string + contains?: string + ocontains?: string[] + oeq?: string[] + gt?: string + gte?: string + lt?: string + lte?: string + exists?: boolean + } +} + /** * Tax configuration for a credit grant. * diff --git a/api/spec/packages/aip/src/productcatalog/operations.tsp b/api/spec/packages/aip/src/productcatalog/operations.tsp index b182f1cc77..4e4158c6cd 100644 --- a/api/spec/packages/aip/src/productcatalog/operations.tsp +++ b/api/spec/packages/aip/src/productcatalog/operations.tsp @@ -274,6 +274,25 @@ interface AddonOperations { | Common.NotFound; } +/** + * Filter options for listing plan add-ons. + */ +@friendlyName("ListPlanAddonsParamsFilter") +model ListPlanAddonsParamsFilter { + #suppress "@openmeter/api-spec-aip/doc-decorator" "shared model" + id?: Common.ULIDFieldFilter; + #suppress "@openmeter/api-spec-aip/doc-decorator" "shared model" + plan_key?: Common.StringFieldFilter; + #suppress "@openmeter/api-spec-aip/doc-decorator" "shared model" + addon_id?: Common.ULIDFieldFilter; + #suppress "@openmeter/api-spec-aip/doc-decorator" "shared model" + addon_key?: Common.StringFieldFilter; + #suppress "@openmeter/api-spec-aip/doc-decorator" "shared model" + addon_name?: Common.StringFieldFilter; + #suppress "@openmeter/api-spec-aip/doc-decorator" "shared model" + plan_currency?: Common.StringFieldFilter; +} + interface PlanAddonOperations { /** * List add-ons associated with a plan. @@ -282,7 +301,30 @@ interface PlanAddonOperations { @operationId("list-plan-addons") @summary("List add-ons for plan") @extension(Shared.UnstableExtension, true) - listPlanAddons(@path planId: Shared.ULID, ...Common.PagePaginationQuery): + @extension(Shared.InternalExtension, true) + listPlanAddons( + @path planId: Shared.ULID, + ...Common.PagePaginationQuery, + + /** + * Sort plan add-ons returned in the response. Supported sort attributes are: + * + * - `id` (default) + * - `created_at` + * - `updated_at` + * + * The `asc` suffix is optional as the default sort order is ascending. The `desc` + * suffix is used to specify a descending order. + */ + @query(#{ name: "sort" }) + sort?: Common.SortQuery, + + /** + * Filter plan add-ons returned in the response. + */ + @query(#{ style: "deepObject", explode: true }) + filter?: ListPlanAddonsParamsFilter, + ): | Shared.PagePaginatedResponse | Common.ErrorResponses | Common.NotFound; diff --git a/api/v3/api.gen.go b/api/v3/api.gen.go index 67dd3bd7d1..4e9b2f3afc 100644 --- a/api/v3/api.gen.go +++ b/api/v3/api.gen.go @@ -5832,6 +5832,33 @@ type ListMetersParamsFilter struct { Name *StringFieldFilter `json:"name,omitempty"` } +// ListPlanAddonsParamsFilter Filter options for listing plan add-ons. +type ListPlanAddonsParamsFilter struct { + // AddonId Filters on the given ULID field value by exact match. All properties are + // optional; provide exactly one to specify the comparison. + AddonId *ULIDFieldFilter `json:"addon_id,omitempty"` + + // AddonKey Filters on the given string field value by either exact or fuzzy match. All + // properties are optional; provide exactly one to specify the comparison. + AddonKey *StringFieldFilter `json:"addon_key,omitempty"` + + // AddonName Filters on the given string field value by either exact or fuzzy match. All + // properties are optional; provide exactly one to specify the comparison. + AddonName *StringFieldFilter `json:"addon_name,omitempty"` + + // Id Filters on the given ULID field value by exact match. All properties are + // optional; provide exactly one to specify the comparison. + Id *ULIDFieldFilter `json:"id,omitempty"` + + // PlanCurrency Filters on the given string field value by either exact or fuzzy match. All + // properties are optional; provide exactly one to specify the comparison. + PlanCurrency *StringFieldFilter `json:"plan_currency,omitempty"` + + // PlanKey Filters on the given string field value by either exact or fuzzy match. All + // properties are optional; provide exactly one to specify the comparison. + PlanKey *StringFieldFilter `json:"plan_key,omitempty"` +} + // ListPlansParamsFilter Filter options for listing plans. type ListPlansParamsFilter struct { // Currency Filters on the given string field value by exact match. All properties are @@ -6897,6 +6924,19 @@ type ListPlansParams struct { type ListPlanAddonsParams struct { // Page Determines which page of the collection to retrieve. Page *PagePaginationQuery `json:"page,omitempty"` + + // Sort Sort plan add-ons returned in the response. Supported sort attributes are: + // + // - `id` (default) + // - `created_at` + // - `updated_at` + // + // The `asc` suffix is optional as the default sort order is ascending. The `desc` + // suffix is used to specify a descending order. + Sort *SortQuery `form:"sort,omitempty" json:"sort,omitempty"` + + // Filter Filter plan add-ons returned in the response. + Filter *ListPlanAddonsParamsFilter `json:"filter,omitempty"` } // ListBillingProfilesParams defines parameters for ListBillingProfiles. @@ -10850,6 +10890,22 @@ func (siw *ServerInterfaceWrapper) ListPlanAddons(w http.ResponseWriter, r *http return } + // ------------- Optional query parameter "sort" ------------- + + err = runtime.BindQueryParameterWithOptions("form", false, false, "sort", r.URL.Query(), ¶ms.Sort, runtime.BindQueryParameterOptions{Type: "string", Format: ""}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "sort", Err: err}) + return + } + + // ------------- Optional query parameter "filter" ------------- + + err = filters.Parse(r.URL.Query(), ¶ms.Filter) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "filter", Err: err}) + return + } + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { siw.Handler.ListPlanAddons(w, r, planId, params) })) @@ -12441,219 +12497,220 @@ var swaggerSpec = []string{ "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", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/api/v3/handlers/plans/planaddons/lists.go b/api/v3/handlers/plans/planaddons/lists.go index c561dad197..0e6170b8f3 100644 --- a/api/v3/handlers/plans/planaddons/lists.go +++ b/api/v3/handlers/plans/planaddons/lists.go @@ -8,8 +8,11 @@ import ( api "github.com/openmeterio/openmeter/api/v3" "github.com/openmeterio/openmeter/api/v3/apierrors" + "github.com/openmeterio/openmeter/api/v3/filters" + "github.com/openmeterio/openmeter/api/v3/request" "github.com/openmeterio/openmeter/api/v3/response" "github.com/openmeterio/openmeter/openmeter/productcatalog/planaddon" + "github.com/openmeterio/openmeter/pkg/filter" "github.com/openmeterio/openmeter/pkg/framework/commonhttp" "github.com/openmeterio/openmeter/pkg/framework/transport/httptransport" "github.com/openmeterio/openmeter/pkg/pagination" @@ -51,11 +54,75 @@ func (h *handler) ListPlanAddons() ListPlanAddonsHandler { }) } - return ListPlanAddonsRequest{ + req := ListPlanAddonsRequest{ Namespaces: []string{ns}, - PlanIDs: []string{params.PlanID}, - Page: page, - }, nil + // Enforce the plan scope from the path parameter. + PlanID: &filter.FilterULID{FilterString: filter.FilterString{Eq: lo.ToPtr(params.PlanID)}}, + Page: page, + } + + if params.Params.Filter != nil { + id, err := filters.FromAPIFilterULID(params.Params.Filter.Id) + if err != nil { + return ListPlanAddonsRequest{}, apierrors.NewBadRequestError(ctx, err, apierrors.InvalidParameters{ + {Field: "filter[id]", Reason: err.Error(), Source: apierrors.InvalidParamSourceQuery}, + }) + } + req.ID = id + + planKey, err := filters.FromAPIFilterString(params.Params.Filter.PlanKey) + if err != nil { + return ListPlanAddonsRequest{}, apierrors.NewBadRequestError(ctx, err, apierrors.InvalidParameters{ + {Field: "filter[plan_key]", Reason: err.Error(), Source: apierrors.InvalidParamSourceQuery}, + }) + } + req.PlanKey = planKey + + addonID, err := filters.FromAPIFilterULID(params.Params.Filter.AddonId) + if err != nil { + return ListPlanAddonsRequest{}, apierrors.NewBadRequestError(ctx, err, apierrors.InvalidParameters{ + {Field: "filter[addon_id]", Reason: err.Error(), Source: apierrors.InvalidParamSourceQuery}, + }) + } + req.AddonID = addonID + + addonKey, err := filters.FromAPIFilterString(params.Params.Filter.AddonKey) + if err != nil { + return ListPlanAddonsRequest{}, apierrors.NewBadRequestError(ctx, err, apierrors.InvalidParameters{ + {Field: "filter[addon_key]", Reason: err.Error(), Source: apierrors.InvalidParamSourceQuery}, + }) + } + req.AddonKey = addonKey + + addonName, err := filters.FromAPIFilterString(params.Params.Filter.AddonName) + if err != nil { + return ListPlanAddonsRequest{}, apierrors.NewBadRequestError(ctx, err, apierrors.InvalidParameters{ + {Field: "filter[addon_name]", Reason: err.Error(), Source: apierrors.InvalidParamSourceQuery}, + }) + } + req.AddonName = addonName + + planCurrency, err := filters.FromAPIFilterString(params.Params.Filter.PlanCurrency) + if err != nil { + return ListPlanAddonsRequest{}, apierrors.NewBadRequestError(ctx, err, apierrors.InvalidParameters{ + {Field: "filter[plan_currency]", Reason: err.Error(), Source: apierrors.InvalidParamSourceQuery}, + }) + } + req.PlanCurrency = planCurrency + } + + if params.Params.Sort != nil { + sort, err := request.ParseSortBy(*params.Params.Sort) + if err != nil { + return ListPlanAddonsRequest{}, apierrors.NewBadRequestError(ctx, err, apierrors.InvalidParameters{ + {Field: "sort", Reason: err.Error(), Source: apierrors.InvalidParamSourceQuery}, + }) + } + req.OrderBy = planaddon.OrderBy(sort.Field) + req.Order = sort.Order.ToSortxOrder() + } + + return req, nil }, func(ctx context.Context, req ListPlanAddonsRequest) (ListPlanAddonsResponse, error) { result, err := h.addonService.ListPlanAddons(ctx, req) diff --git a/api/v3/openapi.yaml b/api/v3/openapi.yaml index 87a5c18ad3..e20b9ca05c 100644 --- a/api/v3/openapi.yaml +++ b/api/v3/openapi.yaml @@ -2212,6 +2212,29 @@ paths: schema: $ref: '#/components/schemas/ULID' - $ref: '#/components/parameters/PagePaginationQuery' + - name: sort + in: query + required: false + description: |- + Sort plan add-ons returned in the response. Supported sort attributes are: + + - `id` (default) + - `created_at` + - `updated_at` + + The `asc` suffix is optional as the default sort order is ascending. The `desc` + suffix is used to specify a descending order. + schema: + $ref: '#/components/schemas/SortQuery' + explode: false + style: form + - name: filter + in: query + required: false + description: Filter plan add-ons returned in the response. + schema: + $ref: '#/components/schemas/ListPlanAddonsParamsFilter' + style: deepObject responses: '200': description: Page paginated response. @@ -2229,6 +2252,7 @@ paths: $ref: '#/components/responses/NotFound' tags: - OpenMeter Product Catalog + x-internal: true x-unstable: true post: operationId: create-plan-addon @@ -9952,6 +9976,23 @@ components: description: Filter meters by name. additionalProperties: false description: Filter options for listing meters. + ListPlanAddonsParamsFilter: + type: object + properties: + id: + $ref: '#/components/schemas/ULIDFieldFilter' + plan_key: + $ref: '#/components/schemas/StringFieldFilter' + addon_id: + $ref: '#/components/schemas/ULIDFieldFilter' + addon_key: + $ref: '#/components/schemas/StringFieldFilter' + addon_name: + $ref: '#/components/schemas/StringFieldFilter' + plan_currency: + $ref: '#/components/schemas/StringFieldFilter' + additionalProperties: false + description: Filter options for listing plan add-ons. ListPlansParamsFilter: type: object properties: diff --git a/openmeter/productcatalog/planaddon/adapter/adapter_test.go b/openmeter/productcatalog/planaddon/adapter/adapter_test.go index b4ba692811..60317416dd 100644 --- a/openmeter/productcatalog/planaddon/adapter/adapter_test.go +++ b/openmeter/productcatalog/planaddon/adapter/adapter_test.go @@ -18,6 +18,7 @@ import ( "github.com/openmeterio/openmeter/openmeter/productcatalog/planaddon" pctestutils "github.com/openmeterio/openmeter/openmeter/productcatalog/testutils" "github.com/openmeterio/openmeter/pkg/datetime" + "github.com/openmeterio/openmeter/pkg/filter" "github.com/openmeterio/openmeter/pkg/models" "github.com/openmeterio/openmeter/pkg/pagination" ) @@ -25,7 +26,7 @@ import ( var MonthPeriod = datetime.NewISODuration(0, 1, 0, 0, 0, 0, 0) func TestPostgresAdapter(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithCancel(t.Context()) defer cancel() env := pctestutils.NewTestEnv(t) @@ -338,7 +339,7 @@ func TestPostgresAdapter(t *testing.T) { t.Run("ById", func(t *testing.T) { listPlanAddons, err := env.PlanAddonRepository.ListPlanAddons(ctx, planaddon.ListPlanAddonsInput{ Namespaces: []string{namespace}, - IDs: []string{planAddon.ID}, + ID: &filter.FilterULID{FilterString: filter.FilterString{Eq: lo.ToPtr(planAddon.ID)}}, }) assert.NoErrorf(t, err, "listing plan add-on assignment by id must not fail") @@ -350,8 +351,8 @@ func TestPostgresAdapter(t *testing.T) { t.Run("ByResourceKey", func(t *testing.T) { listPlanAddons, err := env.PlanAddonRepository.ListPlanAddons(ctx, planaddon.ListPlanAddonsInput{ Namespaces: []string{namespace}, - PlanKeys: []string{planV1.Key}, - AddonKeys: []string{addonV1.Key}, + PlanKey: &filter.FilterString{Eq: lo.ToPtr(planV1.Key)}, + AddonKey: &filter.FilterString{Eq: lo.ToPtr(addonV1.Key)}, }) assert.NoErrorf(t, err, "listing plan add-on assignment by plan and add-on keys must not fail") @@ -359,6 +360,64 @@ func TestPostgresAdapter(t *testing.T) { planaddon.AssertPlanAddonEqual(t, *planAddon, listPlanAddons.Items[0]) }) + + t.Run("ByPlanID", func(t *testing.T) { + listPlanAddons, err := env.PlanAddonRepository.ListPlanAddons(ctx, planaddon.ListPlanAddonsInput{ + Namespaces: []string{namespace}, + PlanID: &filter.FilterULID{FilterString: filter.FilterString{Eq: lo.ToPtr(planV1.ID)}}, + }) + assert.NoErrorf(t, err, "listing plan add-on assignment by plan id must not fail") + + require.Lenf(t, listPlanAddons.Items, 1, "plan add-on assignments must not be empty") + + planaddon.AssertPlanAddonEqual(t, *planAddon, listPlanAddons.Items[0]) + }) + + t.Run("ByAddonID", func(t *testing.T) { + listPlanAddons, err := env.PlanAddonRepository.ListPlanAddons(ctx, planaddon.ListPlanAddonsInput{ + Namespaces: []string{namespace}, + AddonID: &filter.FilterULID{FilterString: filter.FilterString{Eq: lo.ToPtr(addonV1.ID)}}, + }) + assert.NoErrorf(t, err, "listing plan add-on assignment by addon id must not fail") + + require.Lenf(t, listPlanAddons.Items, 1, "plan add-on assignments must not be empty") + + planaddon.AssertPlanAddonEqual(t, *planAddon, listPlanAddons.Items[0]) + }) + + t.Run("ByAddonName", func(t *testing.T) { + listPlanAddons, err := env.PlanAddonRepository.ListPlanAddons(ctx, planaddon.ListPlanAddonsInput{ + Namespaces: []string{namespace}, + AddonName: &filter.FilterString{Contains: lo.ToPtr("Addon")}, + }) + assert.NoErrorf(t, err, "listing plan add-on assignment by addon name must not fail") + + require.Lenf(t, listPlanAddons.Items, 1, "plan add-on assignments must not be empty") + + planaddon.AssertPlanAddonEqual(t, *planAddon, listPlanAddons.Items[0]) + }) + + t.Run("ByCurrency", func(t *testing.T) { + listPlanAddons, err := env.PlanAddonRepository.ListPlanAddons(ctx, planaddon.ListPlanAddonsInput{ + Namespaces: []string{namespace}, + PlanCurrency: &filter.FilterString{Eq: lo.ToPtr("USD")}, + }) + assert.NoErrorf(t, err, "listing plan add-on assignment by currency must not fail") + + require.Lenf(t, listPlanAddons.Items, 1, "plan add-on assignments must not be empty") + + planaddon.AssertPlanAddonEqual(t, *planAddon, listPlanAddons.Items[0]) + }) + + t.Run("NoMatch", func(t *testing.T) { + listPlanAddons, err := env.PlanAddonRepository.ListPlanAddons(ctx, planaddon.ListPlanAddonsInput{ + Namespaces: []string{namespace}, + PlanCurrency: &filter.FilterString{Eq: lo.ToPtr("EUR")}, + }) + assert.NoErrorf(t, err, "listing plan add-on assignments with non-matching currency must not fail") + + require.Lenf(t, listPlanAddons.Items, 0, "plan add-on assignments must be empty for non-matching currency") + }) }) t.Run("Update", func(t *testing.T) { diff --git a/openmeter/productcatalog/planaddon/adapter/planaddon.go b/openmeter/productcatalog/planaddon/adapter/planaddon.go index f22ebbbff0..5333dcd16b 100644 --- a/openmeter/productcatalog/planaddon/adapter/planaddon.go +++ b/openmeter/productcatalog/planaddon/adapter/planaddon.go @@ -12,6 +12,7 @@ import ( "github.com/openmeterio/openmeter/openmeter/productcatalog/addon" "github.com/openmeterio/openmeter/openmeter/productcatalog/planaddon" "github.com/openmeterio/openmeter/pkg/clock" + "github.com/openmeterio/openmeter/pkg/filter" "github.com/openmeterio/openmeter/pkg/framework/entutils" "github.com/openmeterio/openmeter/pkg/models" "github.com/openmeterio/openmeter/pkg/pagination" @@ -30,77 +31,45 @@ func (a *adapter) ListPlanAddons(ctx context.Context, params planaddon.ListPlanA query = query.Where(planaddondb.NamespaceIn(params.Namespaces...)) } - var orFilters []predicate.PlanAddon - if len(params.IDs) > 0 { - orFilters = append(orFilters, planaddondb.IDIn(params.IDs...)) - } - - // Plan predicates - - var planOrFilters []predicate.Plan + // Assignment-level filters (AND semantics) + query = filter.ApplyToQuery(query, params.ID, planaddondb.FieldID) - if len(params.PlanIDs) > 0 { - planOrFilters = append(planOrFilters, plandb.IDIn(params.PlanIDs...)) - } + // Plan-side filters applied via HasPlanWith (all AND) + var planPreds []predicate.Plan - if len(params.PlanKeys) > 0 { - planOrFilters = append(planOrFilters, plandb.KeyIn(params.PlanKeys...)) - } + planPreds = filter.ApplyToPredicate(planPreds, params.PlanID, plandb.FieldID) + planPreds = filter.ApplyToPredicate(planPreds, params.PlanKey, plandb.FieldKey) + planPreds = filter.ApplyToPredicate(planPreds, params.PlanCurrency, plandb.FieldCurrency) if len(params.PlanKeyVersions) > 0 { - var planKeyVersionFilters []predicate.Plan - + var planKeyVersionPreds []predicate.Plan for key, version := range params.PlanKeyVersions { - planOrFilters = append(planOrFilters, plandb.And(plandb.Key(key), plandb.VersionIn(version...))) + planKeyVersionPreds = append(planKeyVersionPreds, plandb.And(plandb.Key(key), plandb.VersionIn(version...))) } - - planOrFilters = append(planOrFilters, plandb.Or(planKeyVersionFilters...)) - } - - if len(params.Currencies) > 0 { - planOrFilters = append(planOrFilters, plandb.CurrencyIn(params.Currencies...)) + planPreds = append(planPreds, plandb.Or(planKeyVersionPreds...)) } - if len(planOrFilters) > 0 { - orFilters = append(orFilters, planaddondb.HasPlanWith( - plandb.Or(planOrFilters...), - )) + if len(planPreds) > 0 { + query = query.Where(planaddondb.HasPlanWith(planPreds...)) } - // Addon predicates - - var addonOrFilters []predicate.Addon + // Addon-side filters applied via HasAddonWith (all AND) + var addonPreds []predicate.Addon - if len(params.AddonIDs) > 0 { - addonOrFilters = append(addonOrFilters, addondb.IDIn(params.AddonIDs...)) - } + addonPreds = filter.ApplyToPredicate(addonPreds, params.AddonID, addondb.FieldID) + addonPreds = filter.ApplyToPredicate(addonPreds, params.AddonKey, addondb.FieldKey) + addonPreds = filter.ApplyToPredicate(addonPreds, params.AddonName, addondb.FieldName) - if len(params.AddonKeys) > 0 { - addonOrFilters = append(addonOrFilters, addondb.KeyIn(params.AddonKeys...)) - } - - if len(params.PlanKeyVersions) > 0 { - var planKeyVersionFilters []predicate.Addon - - for key, version := range params.PlanKeyVersions { - addonOrFilters = append(addonOrFilters, addondb.And(addondb.Key(key), addondb.VersionIn(version...))) + if len(params.AddonKeyVersions) > 0 { + var addonKeyVersionPreds []predicate.Addon + for key, version := range params.AddonKeyVersions { + addonKeyVersionPreds = append(addonKeyVersionPreds, addondb.And(addondb.Key(key), addondb.VersionIn(version...))) } - - addonOrFilters = append(addonOrFilters, addondb.Or(planKeyVersionFilters...)) - } - - if len(params.Currencies) > 0 { - addonOrFilters = append(addonOrFilters, addondb.CurrencyIn(params.Currencies...)) - } - - if len(addonOrFilters) > 0 { - orFilters = append(orFilters, planaddondb.HasAddonWith( - addondb.Or(addonOrFilters...), - )) + addonPreds = append(addonPreds, addondb.Or(addonKeyVersionPreds...)) } - if len(orFilters) > 0 { - query = query.Where(planaddondb.Or(orFilters...)) + if len(addonPreds) > 0 { + query = query.Where(planaddondb.HasAddonWith(addonPreds...)) } if !params.IncludeDeleted { diff --git a/openmeter/productcatalog/planaddon/httpdriver/planaddon.go b/openmeter/productcatalog/planaddon/httpdriver/planaddon.go index 5ae1cfe90e..bbec1e9fb2 100644 --- a/openmeter/productcatalog/planaddon/httpdriver/planaddon.go +++ b/openmeter/productcatalog/planaddon/httpdriver/planaddon.go @@ -5,11 +5,13 @@ import ( "fmt" "net/http" + "github.com/oklog/ulid/v2" "github.com/samber/lo" "github.com/openmeterio/openmeter/api" productcataloghttp "github.com/openmeterio/openmeter/openmeter/productcatalog/http" "github.com/openmeterio/openmeter/openmeter/productcatalog/planaddon" + "github.com/openmeterio/openmeter/pkg/filter" "github.com/openmeterio/openmeter/pkg/framework/commonhttp" "github.com/openmeterio/openmeter/pkg/framework/transport/httptransport" "github.com/openmeterio/openmeter/pkg/models" @@ -50,14 +52,25 @@ func (h *handler) ListPlanAddons() ListPlanAddonsHandler { OrderBy: planaddon.OrderBy(lo.FromPtrOr(params.OrderBy, api.PlanAddonOrderById)), Order: sortx.Order(lo.FromPtrOr(params.Order, api.SortOrderDESC)), Namespaces: []string{ns}, - PlanIDs: []string{params.PlanIDOrKey}, - PlanKeys: []string{params.PlanIDOrKey}, - AddonIDs: lo.FromPtrOr(params.Id, nil), - AddonKeys: lo.FromPtrOr(params.Key, nil), AddonKeyVersions: lo.FromPtrOr(params.KeyVersion, nil), IncludeDeleted: lo.FromPtr(params.IncludeDeleted), } + // Detect whether PlanIDOrKey is a ULID or a key string. + if _, err := ulid.Parse(params.PlanIDOrKey); err == nil { + req.PlanID = &filter.FilterULID{FilterString: filter.FilterString{Eq: lo.ToPtr(params.PlanIDOrKey)}} + } else { + req.PlanKey = &filter.FilterString{Eq: lo.ToPtr(params.PlanIDOrKey)} + } + + if ids := lo.FromPtrOr(params.Id, nil); len(ids) > 0 { + req.AddonID = &filter.FilterULID{FilterString: filter.FilterString{In: lo.ToPtr(ids)}} + } + + if keys := lo.FromPtrOr(params.Key, nil); len(keys) > 0 { + req.AddonKey = &filter.FilterString{In: lo.ToPtr(keys)} + } + return req, nil }, func(ctx context.Context, request ListPlanAddonsRequest) (ListPlanAddonsResponse, error) { diff --git a/openmeter/productcatalog/planaddon/service.go b/openmeter/productcatalog/planaddon/service.go index d3143081ae..f36cafe573 100644 --- a/openmeter/productcatalog/planaddon/service.go +++ b/openmeter/productcatalog/planaddon/service.go @@ -8,6 +8,7 @@ import ( "github.com/samber/lo" + "github.com/openmeterio/openmeter/pkg/filter" "github.com/openmeterio/openmeter/pkg/models" "github.com/openmeterio/openmeter/pkg/pagination" "github.com/openmeterio/openmeter/pkg/sortx" @@ -51,36 +52,75 @@ type ListPlanAddonsInput struct { // Namespaces is the list of namespaces to filter by. Namespaces []string - // IDs is the list of PlanAddonAssignment ids to filter by. - IDs []string - - // PlanIDs is the list of plan.Plan ids to filter by. - PlanIDs []string - - // PlanKeys is the list of plan.Plan keys to filter by. - PlanKeys []string - // PlanKeyVersions is the map of plan.Plan versioned keys to filter by. PlanKeyVersions map[string][]int - // AddonIDs is the list of addon.Addon ids to filter by. - AddonIDs []string - - // AddonKeys is the list of addon.Addon keys to filter by. - AddonKeys []string - // AddonKeyVersions is the map of addon.Addon versioned keys to filter by. AddonKeyVersions map[string][]int // IncludeDeleted defines whether to include deleted PlanAddonAssignments. IncludeDeleted bool - // Currencies is the list of currencies to filter by. - Currencies []string + // ID filters by plan-addon assignment id. + ID *filter.FilterULID + + // PlanID filters by plan id. + PlanID *filter.FilterULID + + // PlanKey filters by plan key. + PlanKey *filter.FilterString + + // AddonID filters by add-on id. + AddonID *filter.FilterULID + + // AddonKey filters by add-on key. + AddonKey *filter.FilterString + + // AddonName filters by add-on name. + AddonName *filter.FilterString + + // PlanCurrency filters by currency. + PlanCurrency *filter.FilterString } func (i ListPlanAddonsInput) Validate() error { - return nil + var errs []error + if i.ID != nil { + if err := i.ID.Validate(); err != nil { + errs = append(errs, fmt.Errorf("invalid id filter: %w", err)) + } + } + if i.PlanID != nil { + if err := i.PlanID.Validate(); err != nil { + errs = append(errs, fmt.Errorf("invalid plan_id filter: %w", err)) + } + } + if i.PlanKey != nil { + if err := i.PlanKey.Validate(); err != nil { + errs = append(errs, fmt.Errorf("invalid plan_key filter: %w", err)) + } + } + if i.AddonID != nil { + if err := i.AddonID.Validate(); err != nil { + errs = append(errs, fmt.Errorf("invalid addon_id filter: %w", err)) + } + } + if i.AddonKey != nil { + if err := i.AddonKey.Validate(); err != nil { + errs = append(errs, fmt.Errorf("invalid addon_key filter: %w", err)) + } + } + if i.AddonName != nil { + if err := i.AddonName.Validate(); err != nil { + errs = append(errs, fmt.Errorf("invalid addon_name filter: %w", err)) + } + } + if i.PlanCurrency != nil { + if err := i.PlanCurrency.Validate(); err != nil { + errs = append(errs, fmt.Errorf("invalid currency filter: %w", err)) + } + } + return models.NewNillableGenericValidationError(errors.Join(errs...)) } var _ models.Validator = (*CreatePlanAddonInput)(nil) diff --git a/openmeter/productcatalog/planaddon/service/service_test.go b/openmeter/productcatalog/planaddon/service/service_test.go index 2e5c43626f..12078038c0 100644 --- a/openmeter/productcatalog/planaddon/service/service_test.go +++ b/openmeter/productcatalog/planaddon/service/service_test.go @@ -18,6 +18,7 @@ import ( "github.com/openmeterio/openmeter/openmeter/productcatalog/planaddon" pctestutils "github.com/openmeterio/openmeter/openmeter/productcatalog/testutils" "github.com/openmeterio/openmeter/pkg/datetime" + "github.com/openmeterio/openmeter/pkg/filter" "github.com/openmeterio/openmeter/pkg/models" "github.com/openmeterio/openmeter/pkg/pagination" ) @@ -25,7 +26,7 @@ import ( var MonthPeriod = datetime.NewISODuration(0, 1, 0, 0, 0, 0, 0) func TestPlanAddonService(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithCancel(t.Context()) defer cancel() env := pctestutils.NewTestEnv(t) @@ -310,9 +311,9 @@ func TestPlanAddonService(t *testing.T) { t.Run("List", func(t *testing.T) { t.Run("ById", func(t *testing.T) { - listPlanAddons, err := env.PlanAddon.ListPlanAddons(ctx, planaddon.ListPlanAddonsInput{ + listPlanAddons, err := env.PlanAddon.ListPlanAddons(t.Context(), planaddon.ListPlanAddonsInput{ Namespaces: []string{namespace}, - IDs: []string{planAddon.ID}, + ID: &filter.FilterULID{FilterString: filter.FilterString{Eq: lo.ToPtr(planAddon.ID)}}, }) assert.NoErrorf(t, err, "listing plan add-on assignment by id must not fail") @@ -322,10 +323,10 @@ func TestPlanAddonService(t *testing.T) { }) t.Run("ByResourceKey", func(t *testing.T) { - listPlanAddons, err := env.PlanAddon.ListPlanAddons(ctx, planaddon.ListPlanAddonsInput{ + listPlanAddons, err := env.PlanAddon.ListPlanAddons(t.Context(), planaddon.ListPlanAddonsInput{ Namespaces: []string{namespace}, - PlanKeys: []string{planV1.Key}, - AddonKeys: []string{addonV1.Key}, + PlanKey: &filter.FilterString{Eq: lo.ToPtr(planV1.Key)}, + AddonKey: &filter.FilterString{Eq: lo.ToPtr(addonV1.Key)}, }) assert.NoErrorf(t, err, "listing plan add-on assignment by plan and add-on keys must not fail") @@ -333,6 +334,86 @@ func TestPlanAddonService(t *testing.T) { planaddon.AssertPlanAddonEqual(t, *planAddon, listPlanAddons.Items[0]) }) + + t.Run("ByPlanID", func(t *testing.T) { + listPlanAddons, err := env.PlanAddon.ListPlanAddons(t.Context(), planaddon.ListPlanAddonsInput{ + Namespaces: []string{namespace}, + PlanID: &filter.FilterULID{FilterString: filter.FilterString{Eq: lo.ToPtr(planV1.ID)}}, + }) + assert.NoErrorf(t, err, "listing plan add-on assignment by plan id must not fail") + + require.Lenf(t, listPlanAddons.Items, 1, "plan add-on assignments must not be empty") + + planaddon.AssertPlanAddonEqual(t, *planAddon, listPlanAddons.Items[0]) + }) + + t.Run("ByAddonID", func(t *testing.T) { + listPlanAddons, err := env.PlanAddon.ListPlanAddons(t.Context(), planaddon.ListPlanAddonsInput{ + Namespaces: []string{namespace}, + AddonID: &filter.FilterULID{FilterString: filter.FilterString{Eq: lo.ToPtr(addonV1.ID)}}, + }) + assert.NoErrorf(t, err, "listing plan add-on assignment by addon id must not fail") + + require.Lenf(t, listPlanAddons.Items, 1, "plan add-on assignments must not be empty") + + planaddon.AssertPlanAddonEqual(t, *planAddon, listPlanAddons.Items[0]) + }) + + t.Run("ByAddonName", func(t *testing.T) { + listPlanAddons, err := env.PlanAddon.ListPlanAddons(t.Context(), planaddon.ListPlanAddonsInput{ + Namespaces: []string{namespace}, + AddonName: &filter.FilterString{Contains: lo.ToPtr("Addon")}, + }) + assert.NoErrorf(t, err, "listing plan add-on assignment by addon name must not fail") + + require.Lenf(t, listPlanAddons.Items, 1, "plan add-on assignments must not be empty") + + planaddon.AssertPlanAddonEqual(t, *planAddon, listPlanAddons.Items[0]) + }) + + t.Run("ByCurrency", func(t *testing.T) { + listPlanAddons, err := env.PlanAddon.ListPlanAddons(t.Context(), planaddon.ListPlanAddonsInput{ + Namespaces: []string{namespace}, + PlanCurrency: &filter.FilterString{Eq: lo.ToPtr("USD")}, + }) + assert.NoErrorf(t, err, "listing plan add-on assignment by currency must not fail") + + require.Lenf(t, listPlanAddons.Items, 1, "plan add-on assignments must not be empty") + + planaddon.AssertPlanAddonEqual(t, *planAddon, listPlanAddons.Items[0]) + }) + + t.Run("NoMatch", func(t *testing.T) { + listPlanAddons, err := env.PlanAddon.ListPlanAddons(t.Context(), planaddon.ListPlanAddonsInput{ + Namespaces: []string{namespace}, + PlanCurrency: &filter.FilterString{Eq: lo.ToPtr("EUR")}, + }) + assert.NoErrorf(t, err, "listing plan add-on assignments with non-matching filter must not fail") + + require.Lenf(t, listPlanAddons.Items, 0, "plan add-on assignments must be empty for non-matching currency") + }) + + t.Run("SortByCreatedAtDesc", func(t *testing.T) { + listPlanAddons, err := env.PlanAddon.ListPlanAddons(t.Context(), planaddon.ListPlanAddonsInput{ + Namespaces: []string{namespace}, + OrderBy: planaddon.OrderByCreatedAt, + Order: planaddon.OrderDesc, + }) + assert.NoErrorf(t, err, "listing plan add-on assignments sorted by created_at desc must not fail") + + require.NotEmptyf(t, listPlanAddons.Items, "plan add-on assignments must not be empty") + }) + + t.Run("SortByID", func(t *testing.T) { + listPlanAddons, err := env.PlanAddon.ListPlanAddons(t.Context(), planaddon.ListPlanAddonsInput{ + Namespaces: []string{namespace}, + OrderBy: planaddon.OrderByID, + Order: planaddon.OrderAsc, + }) + assert.NoErrorf(t, err, "listing plan add-on assignments sorted by id must not fail") + + require.NotEmptyf(t, listPlanAddons.Items, "plan add-on assignments must not be empty") + }) }) t.Run("Update", func(t *testing.T) { diff --git a/pkg/filter/filter.go b/pkg/filter/filter.go index 4188b33217..07684506d2 100644 --- a/pkg/filter/filter.go +++ b/pkg/filter/filter.go @@ -856,6 +856,18 @@ func ApplyToQuery[F Filter, Q EntQuery[Q, P], P Predicate](q Q, f *F, field stri return q } +func ApplyToPredicate[F Filter, P Predicate](arr []P, f *F, field string) []P { + if f == nil { + return arr + } + + if p := SelectPredicate[P](Filter(*f), field); p != nil { + return append(arr, *p) + } + + return arr +} + // validateSingleOperator checks that at most one operator field is set on a // filter struct. To combine operators, use the And or Or fields. func validateSingleOperator(v Filter) error {