Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions api/spec/packages/aip/src/productcatalog/operations.tsp
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,25 @@ interface AddonOperations {
): Shared.UpdateResponse<Addon> | Common.ErrorResponses | 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.
Expand All @@ -294,6 +313,25 @@ interface PlanAddonOperations {
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<PlanAddon> | Common.ErrorResponses | Common.NotFound;

/**
Expand Down
1,346 changes: 702 additions & 644 deletions api/v3/api.gen.go

Large diffs are not rendered by default.

75 changes: 71 additions & 4 deletions api/v3/handlers/plans/planaddons/lists.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
Expand Down
40 changes: 40 additions & 0 deletions api/v3/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2151,6 +2151,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.
Expand Down Expand Up @@ -8368,6 +8391,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:
Expand Down
65 changes: 62 additions & 3 deletions openmeter/productcatalog/planaddon/adapter/adapter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down Expand Up @@ -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")

Expand All @@ -350,15 +351,73 @@ 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")

require.Lenf(t, listPlanAddons.Items, 1, "plan add-on assignments must not be empty")

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) {
Expand Down
Loading
Loading