Skip to content

feat(addons): list filters id, name, key, status, currency#4243

Merged
borosr merged 3 commits intomainfrom
feat/productcatalog-addons-filter
Apr 28, 2026
Merged

feat(addons): list filters id, name, key, status, currency#4243
borosr merged 3 commits intomainfrom
feat/productcatalog-addons-filter

Conversation

@borosr
Copy link
Copy Markdown
Collaborator

@borosr borosr commented Apr 28, 2026

Overview

Extend the GET /api/v3/openmeter/addons endpoint with a filter[id], filter[name], filter[key], filter[status] and filter[currency] query params, also implement sort query param.

Notes for review

Testing guide:

  1. Create addon
curl --request POST \
  --url http://localhost:8888/api/v3/openmeter/addons \
  --header 'Content-Type: application/json' \
  --data '{
  "key": "smokeaddonbasic",
  "name": "Basic Addon",
  "currency": "USD",
  "instance_type": "single",
  "description": "Entry-level USD single-instance addon for smoke testing",
  "rate_cards": [
    {
      "key": "basefee",
      "name": "Base Fee",
      "billing_cadence": "P1M",
      "price": {
        "type": "flat",
        "amount": "10"
      }
    }
  ]
}'
  1. List the addons (sort)
curl --request GET \
  --url 'http://localhost:8888/api/v3/openmeter/addons?sort=key%20asc'
  1. List the plans (name and key)
curl --request GET \
  --url 'http://localhost:8888/api/v3/openmeter/addons?filter%5Bname%5D%5Bcontains%5D=Hello&filter%5Bkey%5D%5Beq%5D=meterid1'

The response should look like this:

{
	"data": [
		{
			"created_at": "2026-04-27T11:10:18.647683+02:00",
			"currency": "USD",
			"description": "Entry-level USD single-instance addon for smoke testing",
			"id": "01KQ738ARQ612TBY4WWZX6X34Y",
			"instance_type": "single",
			"key": "smokeaddonbasic",
			"labels": {},
			"name": "Basic Addon",
			"rate_cards": [
				{
					"billing_cadence": "P1M",
					"key": "basefee",
					"labels": {},
					"name": "Base Fee",
					"payment_term": "in_arrears",
					"price": {
						"amount": "10",
						"type": "flat"
					}
				}
			],
			"status": "draft",
			"updated_at": "2026-04-27T11:10:18.647691+02:00",
			"version": 1
		},
		{
			"created_at": "2026-04-27T11:10:18.68824+02:00",
			"currency": "USD",
			"description": "Professional USD multi-instance addon for smoke testing",
			"id": "01KQ738AT0A1F4GSY2HKCXDXB6",
			"instance_type": "multiple",
			"key": "smokeaddonpro",
			"labels": {},
			"name": "Professional Addon",
			"rate_cards": [
				{
					"billing_cadence": "P1M",
					"key": "profee",
					"labels": {},
					"name": "Professional Fee",
					"payment_term": "in_arrears",
					"price": {
						"amount": "50",
						"type": "flat"
					}
				}
			],
			"status": "draft",
			"updated_at": "2026-04-27T11:10:18.688241+02:00",
			"version": 1
		},
		{
			"created_at": "2026-04-27T11:10:18.696221+02:00",
			"currency": "EUR",
			"description": "Enterprise EUR single-instance addon for smoke testing",
			"id": "01KQ738AT833ANG27QFCH0D2ZV",
			"instance_type": "single",
			"key": "smokeaddonenterprise",
			"labels": {},
			"name": "Enterprise Addon",
			"rate_cards": [
				{
					"billing_cadence": "P1M",
					"key": "entfee",
					"labels": {},
					"name": "Enterprise Fee",
					"payment_term": "in_arrears",
					"price": {
						"amount": "100",
						"type": "flat"
					}
				}
			],
			"status": "draft",
			"updated_at": "2026-04-27T11:10:18.696222+02:00",
			"version": 1
		},
		{
			"created_at": "2026-04-27T11:10:18.703948+02:00",
			"currency": "EUR",
			"description": "Basic EUR multi-instance addon for smoke testing — will be archived",
			"id": "01KQ738ATFKJ8K6RVF9WVZZX67",
			"instance_type": "multiple",
			"key": "smokeaddonbasiceu",
			"labels": {},
			"name": "Basic European Addon",
			"rate_cards": [
				{
					"billing_cadence": "P1M",
					"key": "eufee",
					"labels": {},
					"name": "EU Base Fee",
					"payment_term": "in_arrears",
					"price": {
						"amount": "15",
						"type": "flat"
					}
				}
			],
			"status": "draft",
			"updated_at": "2026-04-27T11:10:18.703948+02:00",
			"version": 1
		}
	],
	"meta": {
		"page": {
			"size": 20,
			"number": 1,
			"total": 4
		}
	}
}

Summary by CodeRabbit

  • New Features

    • Add-on listing supports filtering by ID, key, name, status, and currency via query parameters
    • Add-on listing supports sorting options, including ordering by name
    • Add-on status now has explicit validation
  • Documentation

    • Testing workflow restructured to require a service-level test step and an explicit adapter test layer
  • Tests

    • Added comprehensive table-driven integration and adapter tests for filtering, sorting, and pagination

@borosr borosr self-assigned this Apr 28, 2026
@borosr borosr requested a review from a team as a code owner April 28, 2026 08:33
@borosr borosr added the release-note/ignore Ignore this change when generating release notes label Apr 28, 2026
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 28, 2026

📝 Walkthrough

Walkthrough

Adds deepObject-style filtering and sorting to the add-on listing API: new API filter model and generated types, handler parsing/binding for filter and sort, typed filter propagation through HTTP driver → service → adapter, query construction using filter.ApplyToQuery, validation, and tests.

Changes

Cohort / File(s) Summary
API Spec & Generated Types
api/spec/packages/aip/src/productcatalog/operations.tsp, api/v3/api.gen.go
Adds ListAddonsParamsFilter model and extends listAddons/ListAddonsParams to accept filter (deepObject) and sort query params; regenerates API types and embedded Swagger.
Filter Conversion Utilities
api/v3/filters/convert.go
Adds generic FromAPIStatusFilter[T] to extract Eq/Oeq status values into typed slices and validate each entry; rejects Neq.
Handler & Binding
api/v3/handlers/addons/list.go
Parses/binds filter and sort params into typed request fields, maps parse errors to BadRequest with specific InvalidParameters field paths.
Domain Models & Validation
openmeter/productcatalog/addon.go, openmeter/productcatalog/addon/service.go
Adds AddonStatus.Validate() and changes AddonStatus.Values() to return typed values. Refactors ListAddonsInput to use typed filter pointers (ID, Key, Name, Currency), adds OrderByName, and adds OrderBy.Values()/Validate().
Adapter & Query Construction
openmeter/productcatalog/addon/adapter/addon.go, .../adapter/adapter_test.go
Replaces ad-hoc slices/OR predicates with filter.ApplyToQuery for ID, Key, Name, Currency; rewrites key-version grouping; fixes ordering (key vs version) and adds name ordering; updates adapter tests to use typed filters and new name/currency cases.
HTTP Driver & Service Usage
openmeter/productcatalog/addon/httpdriver/addon.go, openmeter/productcatalog/addon/service/addon.go, openmeter/productcatalog/addon/service/service_test.go
HTTP driver wraps params into filter types; service switches to typed filters in queries; adds table-driven service tests covering ID/key/name/currency/status filters and name ordering.
Documentation & Testing Guidance
.agents/skills/api-filters/SKILL.md
Restructures testing guidance to require a service-level table-driven test, an explicit adapter-test layer validating Ent query generation, and handler/adapter integration tests (four-layer emphasis).

Sequence Diagram

sequenceDiagram
    participant Client
    participant Handler
    participant Service
    participant Adapter
    participant Database

    Client->>Handler: GET /addons?filter[name]=foo&sort=name:desc
    Handler->>Handler: Parse `filter` and `sort` params
    Handler->>Service: ListAddons(ctx, ListAddonsRequest{ Name: FilterString{Contains:"foo"}, OrderBy: name desc })
    Service->>Adapter: List(ctx, input)
    Adapter->>Adapter: Apply filters via filter.ApplyToQuery()
    Adapter->>Adapter: Build predicates (name LIKE, status IN, etc.) and ORDER BY
    Adapter->>Database: Execute SELECT ... WHERE ... ORDER BY ...
    Database-->>Adapter: Rows
    Adapter-->>Service: []Addon
    Service-->>Handler: []Addon
    Handler-->>Client: 200 OK JSON
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested labels

release-note/feature

Suggested reviewers

  • tothandras
  • gergely-kurucz-konghq
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: adding list filters for id, name, key, status, and currency to the addons endpoint.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/productcatalog-addons-filter

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@borosr borosr changed the title feat: add id, key, name, status, currency filters to addons and add s… feat(addons): list filters id, name, key, status, currency Apr 28, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
openmeter/productcatalog/addon/service/service_test.go (1)

31-32: Use t.Context() in both test setups.

The same root-context pattern appears in the main suite and the new list suite; switching to t.Context() keeps cancellation bound to the test harness. As per coding guidelines, in tests prefer t.Context() when a testing.T or testing.TB is available instead of using context.Background().

Also applies to: 449-450

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openmeter/productcatalog/addon/service/service_test.go` around lines 31 - 32,
Replace manual root contexts created with
context.WithCancel(context.Background()) in the tests with the testing harness
context t.Context(); specifically, in the test setup where you do ctx, cancel :=
context.WithCancel(context.Background()) / defer cancel(), change to ctx :=
t.Context() so cancellation is bound to the test harness (apply the same change
to the other occurrence around lines 449-450).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.agents/skills/api-filters/SKILL.md:
- Around line 253-258: The documentation heading incorrectly states "three
additional layers" while the bulleted list contains four items; update the
heading or the list so the count matches—either change the heading to "four
additional layers" or remove/merge one list item; ensure references in the text
to Parser tests (api/v3/filters/parse_test.go), Converter tests
(api/v3/filters/convert_test.go and FromAPIFilter* helpers), Adapter tests
(adapter_test.go and any filter.ApplyToQuery additions), and Handler/adapter
integration tests remain accurate after the edit.

In `@openmeter/productcatalog/addon/adapter/adapter_test.go`:
- Around line 219-230: The subtest "ByCurrencyFilter" isn't verifying the filter
because only one addon exists; create a control addon with a different currency
(e.g., insert a second addon before calling env.AddonRepository.ListAddons) or
explicitly assert the exact result set instead of NotEmpty: call
env.AddonRepository.ListAddons with ListAddonsInput (Namespaces, Currency:
&filter.FilterString{Eq: &currencyStr}) and then require that listAddonV1.Items
has length 1 and that the returned item's ID (or Currency) matches addonV1Input
(or expected currency), ensuring the filter actually limits results; use the
existing names addonV1Input, namespace, and ListAddons to locate where to add
the setup and assertions.

In `@openmeter/productcatalog/addon/httpdriver/addon.go`:
- Around line 61-69: The code in the request builder doesn't forward the new
name filter so filter[name] is dropped; in the same block where params.Id,
params.Key, and params.Currency are mapped to req.ID, req.Key, and req.Currency,
add the equivalent mapping for params.Name to req.Name using
&filter.FilterString{In: params.Name} (mirror the pattern used for Key/Currency)
so the name filter is propagated to the service/adapter layers.

---

Nitpick comments:
In `@openmeter/productcatalog/addon/service/service_test.go`:
- Around line 31-32: Replace manual root contexts created with
context.WithCancel(context.Background()) in the tests with the testing harness
context t.Context(); specifically, in the test setup where you do ctx, cancel :=
context.WithCancel(context.Background()) / defer cancel(), change to ctx :=
t.Context() so cancellation is bound to the test harness (apply the same change
to the other occurrence around lines 449-450).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: c444dd5c-71d2-42a4-aba2-8bc61b203c9a

📥 Commits

Reviewing files that changed from the base of the PR and between 5d6b2c1 and 839c60c.

⛔ Files ignored due to path filters (1)
  • api/v3/openapi.yaml is excluded by !**/openapi.yaml
📒 Files selected for processing (12)
  • .agents/skills/api-filters/SKILL.md
  • api/spec/packages/aip/src/productcatalog/operations.tsp
  • api/v3/api.gen.go
  • api/v3/filters/convert.go
  • api/v3/handlers/addons/list.go
  • openmeter/productcatalog/addon.go
  • openmeter/productcatalog/addon/adapter/adapter_test.go
  • openmeter/productcatalog/addon/adapter/addon.go
  • openmeter/productcatalog/addon/httpdriver/addon.go
  • openmeter/productcatalog/addon/service.go
  • openmeter/productcatalog/addon/service/addon.go
  • openmeter/productcatalog/addon/service/service_test.go

Comment on lines +253 to +258
Write tests at three additional layers:

1. **Parser tests** (`api/v3/filters/parse_test.go`): the parse layer is already covered for the generic operator surface; only add cases when introducing a new filter type or operator.
2. **Converter tests** (`api/v3/filters/convert_test.go`): only when adding a new `FromAPIFilter*` helper.
3. **Handler/adapter integration tests:** the important layer for new endpoints — assert that representative `?filter[...]=` query strings produce the expected results. Cover at minimum:
3. **Adapter tests** (`adapter_test.go`): If you made changes to the adapter (e.g., adding `filter.ApplyToQuery` for new fields) that are not already covered by existing tests, add specific test cases to verify the Ent query generation for these fields.
4. **Handler/adapter integration tests:** the important layer for new endpoints — assert that representative `?filter[...]=` query strings produce the expected results. Cover at minimum:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Keep the layer count consistent.

The heading says three additional layers, but the list now has four items. Tiny doc mismatch, but it will trip up anyone following the workflow.

🧰 Tools
🪛 LanguageTool

[style] ~257-~257: Consider shortening or rephrasing this to strengthen your wording.
Context: ...ter tests** (adapter_test.go): If you made changes to the adapter (e.g., adding `filter.Apply...

(MAKE_CHANGES)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.agents/skills/api-filters/SKILL.md around lines 253 - 258, The
documentation heading incorrectly states "three additional layers" while the
bulleted list contains four items; update the heading or the list so the count
matches—either change the heading to "four additional layers" or remove/merge
one list item; ensure references in the text to Parser tests
(api/v3/filters/parse_test.go), Converter tests (api/v3/filters/convert_test.go
and FromAPIFilter* helpers), Adapter tests (adapter_test.go and any
filter.ApplyToQuery additions), and Handler/adapter integration tests remain
accurate after the edit.

Comment thread openmeter/productcatalog/addon/adapter/adapter_test.go
Comment thread openmeter/productcatalog/addon/httpdriver/addon.go
tothandras
tothandras previously approved these changes Apr 28, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
api/v3/handlers/addons/list.go (1)

56-107: Consider extracting repeated filter/sort parse error mapping.

This block works, but there’s a lot of repeated NewBadRequestError construction. A tiny helper would make this easier to maintain and less error-prone when adding the next filter.

♻️ Small refactor sketch
+ badQueryParam := func(field string, err error) error {
+ 	return apierrors.NewBadRequestError(ctx, err, apierrors.InvalidParameters{
+ 		{Field: field, Reason: err.Error(), Source: apierrors.InvalidParamSourceQuery},
+ 	})
+ }

  if params.Filter != nil {
  	id, err := filters.FromAPIFilterULID(params.Filter.Id)
  	if err != nil {
- 		return ListAddonsRequest{}, apierrors.NewBadRequestError(ctx, err, apierrors.InvalidParameters{
- 			{Field: "filter[id]", Reason: err.Error(), Source: apierrors.InvalidParamSourceQuery},
- 		})
+ 		return ListAddonsRequest{}, badQueryParam("filter[id]", err)
  	}
  	req.ID = id
  	// ...same pattern for key/name/currency/status...
  }

  if params.Sort != nil {
  	sort, err := request.ParseSortBy(*params.Sort)
  	if err != nil {
- 		return ListAddonsRequest{}, apierrors.NewBadRequestError(ctx, err, apierrors.InvalidParameters{
- 			{Field: "sort", Reason: err.Error(), Source: apierrors.InvalidParamSourceQuery},
- 		})
+ 		return ListAddonsRequest{}, badQueryParam("sort", err)
  	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/v3/handlers/addons/list.go` around lines 56 - 107, The parsing block for
params.Filter and params.Sort repeats apierrors.NewBadRequestError construction
for each parse error (see filters.FromAPIFilterULID, FromAPIFilterString,
FromAPIFilterStringExact, FromAPIStatusFilter, request.ParseSortBy used when
building ListAddonsRequest from params.Filter/params.Sort); extract a small
helper (e.g., mapParseErr(ctx, err, field string) error) that wraps the incoming
error into apierrors.NewBadRequestError with the appropriate
apierrors.InvalidParameters entry (using the provided field name like
"filter[id]", "filter[key]", "sort", etc.), then call that helper at each parse
failure to replace the repeated NewBadRequestError blocks so the parsing code
only returns mapParseErr(ctx, err, "<field>") on error.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@openmeter/productcatalog/addon/service.go`:
- Around line 100-131: ListAddonsInput.Validate() currently skips validating the
Status field; update the method to validate Status the same way as
ID/Key/Name/Currency by checking i.Status (or if it's a slice, iterate its
entries) and calling each entry's Validate(), appending any returned errors to
errs; keep the final return using
models.NewNillableGenericValidationError(errors.Join(errs...)) so Status
validation errors are included in the aggregated result.

---

Nitpick comments:
In `@api/v3/handlers/addons/list.go`:
- Around line 56-107: The parsing block for params.Filter and params.Sort
repeats apierrors.NewBadRequestError construction for each parse error (see
filters.FromAPIFilterULID, FromAPIFilterString, FromAPIFilterStringExact,
FromAPIStatusFilter, request.ParseSortBy used when building ListAddonsRequest
from params.Filter/params.Sort); extract a small helper (e.g., mapParseErr(ctx,
err, field string) error) that wraps the incoming error into
apierrors.NewBadRequestError with the appropriate apierrors.InvalidParameters
entry (using the provided field name like "filter[id]", "filter[key]", "sort",
etc.), then call that helper at each parse failure to replace the repeated
NewBadRequestError blocks so the parsing code only returns mapParseErr(ctx, err,
"<field>") on error.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: ce7f33da-3248-4b18-8351-66346b8e7eaa

📥 Commits

Reviewing files that changed from the base of the PR and between 839c60c and 03c0896.

📒 Files selected for processing (4)
  • api/v3/filters/convert.go
  • api/v3/handlers/addons/list.go
  • openmeter/productcatalog/addon.go
  • openmeter/productcatalog/addon/service.go

Comment thread openmeter/productcatalog/addon/service.go
@borosr borosr merged commit b774c1e into main Apr 28, 2026
32 of 33 checks passed
@borosr borosr deleted the feat/productcatalog-addons-filter branch April 28, 2026 09:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

release-note/ignore Ignore this change when generating release notes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants