Skip to content

feat: add plan addon filters#4366

Open
borosr wants to merge 3 commits into
mainfrom
feat/planaddon-v3-api-filters
Open

feat: add plan addon filters#4366
borosr wants to merge 3 commits into
mainfrom
feat/planaddon-v3-api-filters

Conversation

@borosr
Copy link
Copy Markdown
Collaborator

@borosr borosr commented May 15, 2026

Add filters and sorting to the List Plan Addons endpoint

What

Adds filter[*] query parameters and a sort query parameter to GET /api/v3/openmeter/plans/{planId}/addons.

Supported filters: id, plan_key, addon_id, addon_key, addon_name, plan_currency (passing filter[plan_id] is rejected since the plan is already scoped via the path parameter).

Supported sort fields: id (default), created_at, updated_at, with optional asc/desc suffix.

Why

Callers need a way to narrow down plan addon results without client-side filtering, e.g. finding all addons for a given currency or sorted by creation time.

How

  • Extended the TypeSpec model with ListPlanAddonsParamsFilter and wired sort/filter query params into listPlanAddons
  • Regenerated OpenAPI spec and Go server bindings
  • Updated the HTTP handler to parse and validate filter/sort params, then pass them into ListPlanAddonsRequest
  • Propagated the new fields through the service and adapter layers

Testing

Filter by addon key:

curl -s "http://localhost:8888/api/v3/openmeter/plans/01HX.../addons?filter[addon_key]=storage-addon"

Sort by creation date descending:

curl -s http://localhost:8888/api/v3/openmeter/plans/01HX.../addons?sort=created_at%3Adesc"

Summary by CodeRabbit

  • New Features

    • Deep-object query filtering for plan add-ons (id, plan id/key, add-on id/key/name, plan currency).
    • Query-time sorting for plan add-ons (id default, created_at, updated_at).
  • Bug Fixes / Behavior

    • Handlers now disambiguate plan ID vs plan key and validate filter inputs, returning errors for invalid parameters.
  • Tests

    • Expanded tests for filtering, sorting, ordering, and no-match scenarios.

Review Change Stack

@borosr borosr requested a review from tothandras May 15, 2026 10:38
@borosr borosr self-assigned this May 15, 2026
@borosr borosr requested a review from a team as a code owner May 15, 2026 10:38
@borosr borosr added the release-note/ignore Ignore this change when generating release notes label May 15, 2026
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 15, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 74254d4e-d285-44c9-8499-2967bc1ac1bb

📥 Commits

Reviewing files that changed from the base of the PR and between 6e77082 and 3715ac1.

⛔ Files ignored due to path filters (1)
  • api/v3/openapi.yaml is excluded by !**/openapi.yaml
📒 Files selected for processing (9)
  • api/spec/packages/aip/src/productcatalog/operations.tsp
  • api/v3/api.gen.go
  • api/v3/handlers/plans/planaddons/lists.go
  • openmeter/productcatalog/planaddon/adapter/adapter_test.go
  • openmeter/productcatalog/planaddon/adapter/planaddon.go
  • openmeter/productcatalog/planaddon/httpdriver/planaddon.go
  • openmeter/productcatalog/planaddon/service.go
  • openmeter/productcatalog/planaddon/service/service_test.go
  • pkg/filter/filter.go
🚧 Files skipped from review as they are similar to previous changes (7)
  • pkg/filter/filter.go
  • api/v3/handlers/plans/planaddons/lists.go
  • api/spec/packages/aip/src/productcatalog/operations.tsp
  • openmeter/productcatalog/planaddon/httpdriver/planaddon.go
  • openmeter/productcatalog/planaddon/service/service_test.go
  • openmeter/productcatalog/planaddon/adapter/adapter_test.go
  • openmeter/productcatalog/planaddon/service.go

📝 Walkthrough

Walkthrough

Adds deepObject-style filtering and sortable queries to ListPlanAddons: API spec and generated types, middleware binding, handler parsing, service input/validation, adapter predicate refactor using shared filter helpers, and updated tests.

Changes

ListPlanAddons Filter & Sort Implementation

Layer / File(s) Summary
API contract and code generation
api/spec/packages/aip/src/productcatalog/operations.tsp, api/v3/api.gen.go
TypeSpec adds ListPlanAddonsParamsFilter with plan/addon id/key/name and plan_currency; listPlanAddons documents supported sort attributes and wires filter as deepObject; codegen adds ListPlanAddonsParamsFilter type, extends ListPlanAddonsParams with Sort/Filter, updates middleware, and refreshes embedded swagger payload.
HTTP handler binding, parsing, and validation
api/v3/handlers/plans/planaddons/lists.go
Handler enforces plan scoping from path, parses params.Params.Filter deepObject fields into typed request filters, returns 400 on parse errors, and maps params.Params.Sort into request OrderBy/Order.
HTTP driver request mapping
openmeter/productcatalog/planaddon/httpdriver/planaddon.go
Branches on ULID vs key for PlanIDOrKey, wires KeyVersion into AddonKeyVersions, and maps params.Id/params.Key into typed addon ID/key filters.
Service input types and validation
openmeter/productcatalog/planaddon/service.go
Replaces slice-based filters with pointer-typed filters (FilterULID/FilterString) on ListPlanAddonsInput and updates Validate() to validate each non-nil filter and aggregate errors.
Adapter query predicate construction
openmeter/productcatalog/planaddon/adapter/planaddon.go
Refactors query building to use filter.ApplyToQuery and relation predicates: apply assignment-level ID predicates, build HasPlanWith/HasAddonWith predicates combining per-field filters, and convert key→version maps into OR predicates inside relation predicates.
Filter helper utilities
pkg/filter/filter.go
Adds generic ApplyToPredicate helper that conditionally appends a typed Ent predicate selected from a filter into a predicate slice.
Test updates for filters and sorting
openmeter/productcatalog/planaddon/adapter/adapter_test.go, openmeter/productcatalog/planaddon/service/service_test.go
Tests updated to use FilterULID/FilterString inputs for ID, PlanID, AddonID, PlanKey, AddonKey, AddonName, and PlanCurrency; include sorting checks and no-match edge cases.

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 40.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 'feat: add plan addon filters' accurately and concisely describes the main change—adding filtering and sorting capabilities to the List Plan 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/planaddon-v3-api-filters

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 force-pushed the feat/planaddon-v3-api-filters branch from 7da7fd2 to b411e6d Compare May 15, 2026 10:42
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)
openmeter/productcatalog/planaddon/service/service_test.go (1)

396-416: ⚡ Quick win

Sort tests don’t currently prove sorting behavior.

Right now these checks only assert non-empty results, so they can pass even if ordering is wrong. Please add at least two plan-addon assignments with distinct created_at/id and assert relative order in the returned list.

As per coding guidelines: “Make sure the tests are comprehensive and cover the changes. Keep a strong focus on unit tests and in-code integration tests.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@openmeter/productcatalog/planaddon/service/service_test.go` around lines 396
- 416, The two sort tests (SortByCreatedAtDesc and SortByID) only assert
non-empty results; create two distinct plan-addon records before each test (use
env.PlanAddon.CreatePlanAddon or the test fixture helper) with different
created_at timestamps and/or IDs, then call env.PlanAddon.ListPlanAddons with
ListPlanAddonsInput (OrderByCreatedAt/OrderByID and OrderDesc/OrderAsc) and
assert the returned Items are ordered by comparing Items[0].ID vs Items[1].ID or
Items[0].CreatedAt vs Items[1].CreatedAt as appropriate; keep this setup and
assertions inside the existing t.Run blocks to ensure the tests validate
ordering.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@api/spec/packages/aip/src/productcatalog/operations.tsp`:
- Line 293: Remove the plan_id filter field from the filter model: delete the
"plan_id?: Common.ULIDFieldFilter;" entry so the TypeSpec model no longer
advertises filter[plan_id]; this ensures the model matches runtime behavior
where planId is provided via the path parameter. Update any related model/type
declarations that reference this field (and associated docs/tests) to avoid
exposing or validating plan_id in the filter. Ensure compilation/typechecks pass
after removing the field.

---

Nitpick comments:
In `@openmeter/productcatalog/planaddon/service/service_test.go`:
- Around line 396-416: The two sort tests (SortByCreatedAtDesc and SortByID)
only assert non-empty results; create two distinct plan-addon records before
each test (use env.PlanAddon.CreatePlanAddon or the test fixture helper) with
different created_at timestamps and/or IDs, then call
env.PlanAddon.ListPlanAddons with ListPlanAddonsInput
(OrderByCreatedAt/OrderByID and OrderDesc/OrderAsc) and assert the returned
Items are ordered by comparing Items[0].ID vs Items[1].ID or Items[0].CreatedAt
vs Items[1].CreatedAt as appropriate; keep this setup and assertions inside the
existing t.Run blocks to ensure the tests validate ordering.
🪄 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: c3685fee-aac8-4a51-8c0c-0393da3f779b

📥 Commits

Reviewing files that changed from the base of the PR and between f67a8a1 and 7da7fd2.

⛔ Files ignored due to path filters (1)
  • api/v3/openapi.yaml is excluded by !**/openapi.yaml
📒 Files selected for processing (9)
  • api/spec/packages/aip/src/productcatalog/operations.tsp
  • api/v3/api.gen.go
  • api/v3/handlers/plans/planaddons/lists.go
  • openmeter/productcatalog/planaddon/adapter/adapter_test.go
  • openmeter/productcatalog/planaddon/adapter/planaddon.go
  • openmeter/productcatalog/planaddon/httpdriver/planaddon.go
  • openmeter/productcatalog/planaddon/service.go
  • openmeter/productcatalog/planaddon/service/service_test.go
  • pkg/filter/filter.go

Comment thread api/spec/packages/aip/src/productcatalog/operations.tsp Outdated
Comment on lines +59 to +72
// 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)}
}
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.

This change breaks backward compatibility. v1 endpoint silently changes from OR to AND semantics. A v1 caller sending ?id=A,B&key=k1,k2 previously got the union; now gets the intersection.

I'd argue that this was the intended behavior for v1 all along and this is acceptable.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I think we discussed this at a previously edited v3 endpoint that intersection would be the expected behavior. Should I revert this part to keep the union logic in the old APIs?

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

t.Run("SortByID", func(t *testing.T) {
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.

Every Order/OrderBy combination passes, including OrderBy("") and OrderBy("bogus"). Should we add validation to the handlers https://github.com/openmeterio/openmeter/pull/4366/changes#diff-116ada52ce8ebb117ef465249f9281dd0d490e07c226fb1c79909f6e87cca5a3R115?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I think, this is a global issue, what do you think about resolving this in a separate PR?

Comment thread openmeter/productcatalog/planaddon/adapter/adapter_test.go
Comment thread pkg/filter/filter.go
return q
}

func ApplyToPredicate[F Filter, P Predicate](arr []P, f *F, field string) []P {
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.

Is this a function we could reuse? Why was this introduced?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I created this function to simplify the code by removing code duplication inside the adapter layer. I guess it would be useful in other cases where we are using nested queries for filtering, such as here:

// ...
var planPreds []predicate.Plan
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(planPreds) > 0 {
	query = query.Where(planaddondb.HasPlanWith(planPreds...))
}

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.

🧹 Nitpick comments (2)
openmeter/productcatalog/planaddon/service/service_test.go (2)

396-416: 🏗️ Heavy lift

Sort tests don’t validate ordering yet.

Both sort cases only check non-empty results. That can pass even if ordering is broken. Please add at least one additional assignment and assert the returned order for created_at:desc and id:asc.

As per coding guidelines **/*_test.go: "Make sure the tests are comprehensive and cover the changes. Keep a strong focus on unit tests and in-code integration tests."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@openmeter/productcatalog/planaddon/service/service_test.go` around lines 396
- 416, The sort test cases ("SortByCreatedAtDesc" and "SortByID") only assert
non-empty results; add deterministic fixtures and explicit order assertions:
create at least two plan-addons before calling env.PlanAddon.ListPlanAddons (use
the same namespace) so you have known insertion order/IDs, then in the
"SortByCreatedAtDesc" case assert that returned Items are ordered by CreatedAt
descending (newest first) and in "SortByID" assert Items are ordered by ID
ascending; update the t.Run blocks that call env.PlanAddon.ListPlanAddons (using
planaddon.ListPlanAddonsInput, OrderByCreatedAt/OrderByID and
OrderDesc/OrderAsc) to include these explicit comparisons of item IDs or
CreatedAt timestamps immediately after require.NotEmptyf.

338-416: ⚡ Quick win

Please add given/when/then intent comments to these non-trivial subtests.

The new filter/sort subtests are dense; quick intent comments at each subtest start would make failures much easier to scan.

As per coding guidelines **/*_test.go: "For service and lifecycle subtests in Go, start each subtest body with concise intent comments using 'given:', 'when:', 'then:' format when the scenario is non-trivial".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@openmeter/productcatalog/planaddon/service/service_test.go` around lines 338
- 416, Add concise "given:/when:/then:" intent comments at the start of each
non-trivial subtest body (the t.Run blocks named "ByPlanID", "ByAddonID",
"ByAddonName", "ByCurrency", "NoMatch", "SortByCreatedAtDesc", and "SortByID")
describing the setup (given), the call under test (when — e.g., calling
env.PlanAddon.ListPlanAddons with the specific planaddon.ListPlanAddonsInput
filter/order), and the expected outcome (then — e.g., expect 1 item, 0 items, or
non-empty and no error); place the comments immediately inside each t.Run before
invoking ListPlanAddons so test failures are easier to scan.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@openmeter/productcatalog/planaddon/service/service_test.go`:
- Around line 396-416: The sort test cases ("SortByCreatedAtDesc" and
"SortByID") only assert non-empty results; add deterministic fixtures and
explicit order assertions: create at least two plan-addons before calling
env.PlanAddon.ListPlanAddons (use the same namespace) so you have known
insertion order/IDs, then in the "SortByCreatedAtDesc" case assert that returned
Items are ordered by CreatedAt descending (newest first) and in "SortByID"
assert Items are ordered by ID ascending; update the t.Run blocks that call
env.PlanAddon.ListPlanAddons (using planaddon.ListPlanAddonsInput,
OrderByCreatedAt/OrderByID and OrderDesc/OrderAsc) to include these explicit
comparisons of item IDs or CreatedAt timestamps immediately after
require.NotEmptyf.
- Around line 338-416: Add concise "given:/when:/then:" intent comments at the
start of each non-trivial subtest body (the t.Run blocks named "ByPlanID",
"ByAddonID", "ByAddonName", "ByCurrency", "NoMatch", "SortByCreatedAtDesc", and
"SortByID") describing the setup (given), the call under test (when — e.g.,
calling env.PlanAddon.ListPlanAddons with the specific
planaddon.ListPlanAddonsInput filter/order), and the expected outcome (then —
e.g., expect 1 item, 0 items, or non-empty and no error); place the comments
immediately inside each t.Run before invoking ListPlanAddons so test failures
are easier to scan.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: c0809fe7-7aa8-43f7-9447-0de28a08a7f2

📥 Commits

Reviewing files that changed from the base of the PR and between 9bef020 and 6e77082.

📒 Files selected for processing (2)
  • openmeter/productcatalog/planaddon/adapter/adapter_test.go
  • openmeter/productcatalog/planaddon/service/service_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • openmeter/productcatalog/planaddon/adapter/adapter_test.go

@borosr borosr requested a review from tothandras May 18, 2026 05:59
@borosr borosr force-pushed the feat/planaddon-v3-api-filters branch from 6e77082 to 3715ac1 Compare May 18, 2026 06:28
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