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 c98459f5f9..e9d3ee4a46 100644 --- a/api/spec/packages/aip-client-javascript/src/index.ts +++ b/api/spec/packages/aip-client-javascript/src/index.ts @@ -196,6 +196,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..c6a79914e6 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,20 @@ 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 eb4622005c..d7fb66baaf 100644 --- a/api/spec/packages/aip-client-javascript/src/models/schemas.ts +++ b/api/spec/packages/aip-client-javascript/src/models/schemas.ts @@ -2914,6 +2914,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(), @@ -5463,6 +5474,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 7652cbbb3a..4ad36c8daf 100644 --- a/api/spec/packages/aip-client-javascript/src/models/types.ts +++ b/api/spec/packages/aip-client-javascript/src/models/types.ts @@ -1811,6 +1811,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. Tax configuration should be provided to ensure correct revenue recognition, including for externally funded grants. */ export interface CreateCreditGrantTaxConfig { /** Tax behavior applied to the invoice line item. */ 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 9bd7423bc1..f2dac93761 100644 --- a/api/v3/api.gen.go +++ b/api/v3/api.gen.go @@ -5177,6 +5177,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 @@ -6257,6 +6284,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. @@ -10048,6 +10088,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) })) @@ -11524,260 +11580,261 @@ var swaggerSpec = []string{ "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", - "prBKBAA=", + "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", + "AP//h5j98vhOBAA=", } // 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 4deea97ab5..1236048ae3 100644 --- a/api/v3/openapi.yaml +++ b/api/v3/openapi.yaml @@ -2178,6 +2178,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. @@ -2195,6 +2218,7 @@ paths: $ref: '#/components/responses/NotFound' tags: - OpenMeter Product Catalog + x-internal: true x-unstable: true post: operationId: create-plan-addon @@ -8943,6 +8967,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 {