feat(api): customer list search by partial key#3263
Conversation
📝 WalkthroughWalkthroughUpdated customer “key” filter semantics from case-sensitive exact match to case-insensitive partial match across specs, client SDK docs, and OpenAPI descriptions. Backend implementation changed the predicate to case-insensitive substring matching in ListCustomers. No public API signatures were modified. Changes
Estimated code review effort🎯 2 (Simple) | ⏱️ ~10 minutes Tip 🔌 Remote MCP (Model Context Protocol) integration is now available!Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats. ✨ Finishing Touches
🧪 Generate unit tests
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. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (14)
api/openapi.cloud.yaml (3)
12181-12185: Clarify matching semantics and add examples/constraints to the parameter doc.Great that the doc now reflects the case-insensitive partial match. To reduce ambiguity and improve client UX, consider documenting what “partial” means with a short example and add basic constraints to prevent pathological queries.
Proposed edit:
- description: |- - Filter customers by key. - Case-insensitive partial match. + description: |- + Filter customers by key. + Case-insensitive substring match (e.g., `?key=acme` matches `acme-inc`, `AcMe_2`). + Matching is performed against the entire key value. schema: type: string + minLength: 2 + maxLength: 128 explode: false + example: acme
12182-12182: Call out Unicode and normalization expectations (one sentence).If the backend uses case folding (e.g., LOWER/ILIKE or equivalent), specifying that matching is Unicode-aware (or ASCII-only) removes guesswork for clients using non-ASCII keys.
Suggested wording to append to the description:
- “Case-insensitive comparison is Unicode-aware and locale-independent.” (If true)
- Or: “Case-insensitive comparison is ASCII-only.” (If true)
Please select the accurate statement based on the implementation.
12181-12184: Plan for performance: substring searches need indexing and guardrails.Substring matches (
%term%) can degrade query performance on large datasets without proper indexing. If the backend uses Postgres, consider a trigram index on the key (e.g., GIN with gin_trgm_ops) and/or a functional index on lower(key). The proposedminLengthhelps reduce extremely short terms.api/spec/src/customer/customer.tsp (2)
122-127: Doc change LGTM; consider adding example and clarifying “exact match” path to avoid ambiguityThe “Case-insensitive partial match” update aligns with the backend change. To help users and SDKs, add an example and explicitly point to the exact-match alternative.
Apply this diff:
/** * Filter customers by key. - * Case-insensitive partial match. + * Case-insensitive partial (substring) match. + * For exact key lookup, use GET /api/v1/customers/{customerIdOrKey}. */ @query + @example("acme") key?: string;
43-53: Clarify case-sensitivity for GET by keySince list uses case-insensitive partial matching, explicitly documenting that “Get customer by ID or key” performs an exact key match will prevent confusion.
Suggestion: Add a sentence to the operation summary/description like “When using a key, matching is exact (not partial).”
openmeter/customer/adapter/customer.go (2)
49-56: Consider query performance: case-insensitive substring filters need indexes
ContainsFoldtypically compiles toLOWER(key) LIKE '%' || LOWER($1) || '%', which can’t use standard btree indexes. On larger tables this becomes expensive, especially when filters are broad.Options:
- Add trigram index (Postgres):
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_customer_key_trgm ON customer USING gin (key gin_trgm_ops);- If you standardize on case-insensitive equality elsewhere, consider
citextfor equality predicates; for substring, keep trigram.- If applicable, reject too-short patterns (e.g., < 2 chars) in
ListCustomersInput.Validate()to reduce unselective scans.
49-72: Add tests to lock in new semantics and the empty-string behaviorGiven the change in semantics, add tests that assert:
- key filter is case-insensitive and substring-based,
- empty/whitespace key is ignored (no filter applied).
I can draft table-driven tests for ListCustomers covering positive, negative, and empty-string cases. Want me to open a follow-up PR with tests?
api/openapi.yaml (3)
12178-12183: Clarify semantics and add examples for substring search.“Case-insensitive partial match.” is terse and can be misread. Recommend making it explicit that this is a substring match and that passing the full key may still return multiple rows. Also document empty-string behavior.
Apply this diff to improve the description and add parameter-level examples:
description: |- Filter customers by key. - Case-insensitive partial match. + Case-insensitive substring match. + Matches any customer keys containing the provided value, regardless of case. + Examples: + - ?key=acme → matches "ACME-001", "myAcmeCompany" + - ?key=001 → matches "acme-001", "cust-001" + Note: Providing the full key may still return multiple results if other keys contain it as a substring. schema: type: string + minLength: 1 explode: falseIf the backend treats key="" as “match all”, consider stating that explicitly instead of enforcing minLength: 1.
12181-12183: Consider removing explode: false for a plain string param.For a string with style=form (default), explode is irrelevant; keeping it is harmless but redundant.
Apply this diff if you prefer to reduce noise:
schema: type: string - explode: false
12178-12183: Add a release-note snippet describing the changed matching semantics.Even without signature changes, clients depending on exact match semantics may see broader result sets. Surface this explicitly in release notes with upgrade guidance.
I can draft a concise release-note block and an SDK changelog entry if you confirm the desired behavior for empty strings and trimming rules (e.g., whitespace, Unicode normalization).
api/client/javascript/src/zod/index.ts (2)
6741-6744: Normalize and ignore empty/whitespace-only values to avoid accidental “match-all” queriesWith partial matching, passing an empty string ("") would likely match everything. Consider trimming and converting blank inputs to
undefinedso that an empty value behaves the same as omitting the filter.Apply this diff within the current chain:
key: zod.coerce .string() + .transform((s) => s.trim() || undefined) .optional() - .describe('Filter customers by key.\nCase-insensitive partial match.'), + .describe('Filter customers by key.\nCase-insensitive partial match (substring).\nEmpty/whitespace-only values are ignored.'),
6741-6744: Optional: guard against multi-valued query params being coerced to a comma-joined string
zod.coerce.string()will stringify arrays (e.g.,['ac','me'] -> 'ac,me'), which could unintentionally broaden the search. If your query builder allows repeated params (e.g.,?key=a&key=b), consider taking only the first element.If desired, switch to a preprocess step before coercion (requires adjusting the surrounding code slightly):
// Example helper (outside this diff range) const firstString = <T>(v: T) => Array.isArray(v) ? v[0] : v; // Replace chain with: key: zod.preprocess(firstString, zod.coerce.string()) .transform((s) => s.trim() || undefined) .optional() .describe('Filter customers by key.\nCase-insensitive partial match (substring).\nEmpty/whitespace-only values are ignored.')api/client/javascript/src/client/schemas.ts (2)
10680-10680: Clarify “partial” to “substring” and add an example for unambiguous behavior.“Partial match” can be read as prefix, infix, or fuzzy. If the backend uses plain substring containment, let’s say so and include a short example to set expectations.
Apply this diff:
- * Case-insensitive partial match. */ + * Case-insensitive substring match. For example, key=acme matches "ACME", "acme-co", and "myacme123". */
15080-15080: Mirror the same clarification and example in the operations interface.Keep wording consistent across the components and operations sections to avoid divergent client docs.
Apply this diff:
- * Case-insensitive partial match. */ + * Case-insensitive substring match. For example, key=acme matches "ACME", "acme-co", and "myacme123". */
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled
- Linear integration is disabled
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (6)
api/client/javascript/src/client/schemas.ts(2 hunks)api/client/javascript/src/zod/index.ts(1 hunks)api/openapi.cloud.yaml(1 hunks)api/openapi.yaml(1 hunks)api/spec/src/customer/customer.tsp(1 hunks)openmeter/customer/adapter/customer.go(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
openmeter/customer/adapter/customer.go (1)
openmeter/ent/db/customer/where.go (2)
KeyContainsFold(1122-1124)Key(137-139)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (7)
- GitHub Check: E2E
- GitHub Check: Quickstart
- GitHub Check: Code Generators
- GitHub Check: Test
- GitHub Check: Build
- GitHub Check: Lint
- GitHub Check: Analyze (go)
🔇 Additional comments (3)
openmeter/customer/adapter/customer.go (1)
337-346: Confirm intended case-sensitivity for GET by key vs LIST by key
GetCustomeruses equality:customerdb.Key(...). Unless the column iscitext, equality is case-sensitive, while the list filter is case-insensitive. If this divergence is intentional, document it; otherwise, considerKeyEqualFold(if generated) or aligning storage tocitext.Would you like me to scan the repo to confirm:
- whether the key column uses
citext,- whether an
EqualFoldpredicate exists forcustomerdb.Key?I can run a repo scan script to verify and report findings.
api/openapi.yaml (1)
12178-12183: No stale “exact match” wording remains; new filter docs are consistentAll occurrences of the old “case-sensitive exact match” have been removed, and the new “Case-insensitive partial match” appears consistently in:
- API specs (api/openapi.yaml & api/openapi.cloud.yaml)
- Generated Go client (api/api.gen.go & client/go/client.gen.go)
- JavaScript client (schemas.ts & Zod index)
- Internal spec (api/spec/src/customer/customer.tsp)
Please ensure that any user-facing documentation (e.g., markdown guides) and the release notes explicitly mention this behavioral change.
api/client/javascript/src/zod/index.ts (1)
6741-6744: LGTM: description now reflects case-insensitive partial match semanticsThe updated description aligns with the backend change to a case-insensitive substring filter and clarifies client expectations. No signature or validation changes are introduced here, which keeps it backward compatible for callers.
| /** @description Filter customers by key. | ||
| * Case-sensitive exact match. */ | ||
| * Case-insensitive partial match. */ | ||
| 'queryCustomerList.key': string |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
Confirm cross-repo consistency and note the semantic change in release notes.
Since this changes filter semantics (exact/case-sensitive → substring/case-insensitive), please ensure:
- All SDKs and OpenAPI specs consistently state “case-insensitive substring match.”
- Any remaining references to “exact match” or “case-sensitive” are removed.
- Release notes/changelog call out this behavioral change to set expectations for broader result sets.
Run this script to find lingering, conflicting phrasing:
If you want, I can propose coordinated doc tweaks for OpenAPI, other language SDKs, and a short release-note entry.
Also applies to: 15079-15081
🏁 Script executed:
#!/bin/bash
# Search for phrasing that may contradict the new semantics
rg -n -C2 -i \
-e 'exact match' \
-e 'case[- ]sensitive' \
-e 'partial match' \
-e 'substring match' \
api/ api.client/ client/ sdk/ packages/ | sed 's/^/HIT: /'Length of output: 20088
Update all filter documentation to “case-insensitive substring match” across specs and SDKs
The following locations still refer to “exact match” or imply case-sensitive filtering and must be updated to reflect the new substring/case-insensitive behavior:
• OpenAPI specs
– api/openapi.yaml (e.g. “Simple filter for group bys with exact match.” at lines ~11861, 19149)
– api/openapi.cloud.yaml (same descriptions at lines ~11863, 18568)
• TSP specs
– api/spec/src/customer/customer.tsp (descriptions at lines 124, 131, 139, 147)
– api/spec/src/meters.tsp (lines 391, 465)
• Generated Go clients
– api/api.gen.go (comments at lines 4596, 8010, 8258)
– api/client/go/client.gen.go (comments at lines 4184, 7423, 7671)
• JavaScript Zod schemas
– api/client/javascript/src/zod/index.ts (describe() calls at lines 9332, 9411, 12660)
• JS client schemas
– api/client/javascript/src/client/schemas.ts (comments at lines 6937, 10588, 19619, 22487)
• Portal SDK
– api/client/javascript/src/portal/index.ts (comment at line 64)
Please remove or replace any “exact match” / “case-sensitive” phrasing with “case-insensitive substring match” and ensure all SDKs and OpenAPI specs consistently use the updated language. Also, add a clear entry in the release notes/changelog explaining that filter semantics have changed from exact/case-sensitive to substring/case-insensitive, so users understand broader result sets.
🤖 Prompt for AI Agents
In api/client/javascript/src/client/schemas.ts around lines 10679-10681, the
filter description currently reads "Case-insensitive partial match." — update
this phrasing to "case-insensitive substring match" to align with the new
canonical wording; audit nearby schema comment blocks in this file for other
occurrences (lines noted in the review like ~6937, 10588, 19619, 22487) and
replace any "exact match", "partial match", or "case-sensitive" language with
"case-insensitive substring match" so the SDK documentation consistently
reflects the updated filter semantics.
| key: zod.coerce | ||
| .string() | ||
| .optional() | ||
| .describe('Filter customers by key.\nCase-sensitive exact match.'), | ||
| .describe('Filter customers by key.\nCase-insensitive partial match.'), |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
Repo-wide consistency check for wording and behavior
Ensure there are no lingering “exact match” mentions in SDKs/docs and that server predicates use a case-insensitive contains operator consistently.
Run:
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# 1) Find any stale "exact match" mentions around customer key filters
rg -nI -C2 -S -e 'exact match' -e 'Exact match' -- 'api/**' 'openmeter/**' 'docs/**' || true
# 2) Confirm updated phrasing appears across specs/clients
rg -nI -C2 -S -e 'Case-insensitive partial match' -- 'api/**' 'docs/**' 'clients/**' || true
# 3) Double-check backend predicate uses case-insensitive containment
rg -nI -C2 -S -e 'ContainsFold' -e 'ILIKE' -e 'LOWER\(.*\) LIKE' -- 'openmeter/**' || trueLength of output: 642
I’m seeing that the globs in the original script don’t match any paths—let’s discover the actual top-level directories so we can re-run the searches correctly.
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# List root-level entries to identify correct paths
ls -1Length of output: 422
Let’s re-run the consistency checks against the correct directories:
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# 1) Find any stale "exact match" mentions
rg -nI -C2 -S -e 'exact match' -e 'Exact match' -- api/**/* openmeter/**/* docs/**/* || true
# 2) Confirm updated phrasing appears across specs/clients
rg -nI -C2 -S -e 'Case-insensitive partial match' -- api/**/* docs/**/* api/client/**/* || true
# 3) Double-check backend predicate uses case-insensitive containment
rg -nI -C2 -S -e 'ContainsFold' -e 'ILIKE' -e 'LOWER\(.*\) LIKE' -- openmeter/**/* || trueLength of output: 108592
Clean up remaining “exact match” references and confirm consistent use of case-insensitive contains
I ran a repo-wide search and confirmed:
-
Stale “exact match” mentions still exist in several docs and SDKs:
• openmeter/apiconverter/filter.go – the planFilterFunc and addonFilterFunc comments refer to stopping on an “exact match.”
• api/client/javascript/src/zod/index.ts around lines 389–393 and 9330–9334 – descriptions forfilterGroupBystill say “Simple filter for group bys with exact match.”
• Go client (api/client/go) generated types – theFilterGroupByfield inclient.gen.go/types/doc.gocomments “Simple filter for group bys with exact match.”
• Python client (api/client/python/src) – similarfilterGroupBydocstrings. -
The updated phrasing (“Case-insensitive partial match”) is already applied to all customer filter parameters across SDKs:
• API clients (Go, JavaScript, Node, Python, Web) describekey,name,primaryEmail, andsubjectfilters as “Case-insensitive partial match.”
• OpenAPI specs and docs reference the same wording for those parameters. -
Backend predicates uniformly use case-insensitive containment for customer filters:
•KeyContainsFold,NameContainsFold,PrimaryEmailContainsFold, andSubjectKeyContainsFoldare used in all relevant query builders.
Please update the above “exact match” references to align with the new semantics (e.g. replace with “Case-insensitive partial match”) so the entire codebase and docs consistently reflect case-insensitive contains behavior.
🤖 Prompt for AI Agents
In api/client/javascript/src/zod/index.ts around lines 6741-6744 (and also check
and update the similar descriptions at ~389-393 and ~9330-9334 in the same
file), the filter descriptions still reference “exact match”; replace those
phrases with “Case-insensitive partial match” so the `key`, `filterGroupBy` and
any other customer-filter description text reflects the case-insensitive
contains semantics, ensuring wording is identical to other SDKs and OpenAPI
docs.
| required: false | ||
| description: |- | ||
| Filter customers by key. | ||
| Case-sensitive exact match. | ||
| Case-insensitive partial match. | ||
| schema: | ||
| type: string | ||
| explode: false |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
Confirm cross-spec and SDK consistency; add a release-note caveat for behavior change.
This is a behavior change (from exact to substring matching) that could broaden results for existing queries. Ensure all surfaces reflect the update, and note it in release notes as a potentially impactful change.
Run to verify consistency across specs and SDKs:
If any matches remain, update them and add a release-note entry that existing filters may return more rows due to substring semantics.
🏁 Script executed:
#!/bin/bash
# Verify all public specs and clients reflect the new semantics.
set -euo pipefail
echo "Searching for outdated 'Case-sensitive exact match' wording…"
rg -nP -C1 'Case-?sensitive exact match' -- \
api/ api/spec/ api/client/ || true
echo "Inspecting all customer key filter docs…"
rg -nP -C2 '(Filter customers by key|listCustomers.*key|ListCustomers.*key)' -- \
api/ api/spec/ api/client/
echo "Optional: check for references to exact equality in Go adapters…"
rg -nP -C2 '\bKey(EQ|Equal)\b|\bContains(Fold)?\b' -- \
openmeter/ || trueLength of output: 285388
Docs/SDKs show "case‑insensitive partial match" — backend mapping still needs verification and fixes; add release note
Short: the API spec and generated SDKs were updated to "Case-insensitive partial match" (api/openapi.cloud.yaml, api/openapi.yaml, api/spec/src/customer/customer.tsp and generated client files). However the server code still exposes equality predicates and at least one adapter call uses equality semantics, so behavior may be inconsistent.
Please check/fix these locations (examples from the verification run):
- api/openapi.cloud.yaml:12179-12182 — description updated to "Case-insensitive partial match".
- api/openapi.yaml:12177-12180 and api/spec/src/customer/customer.tsp:123-125 — spec text updated.
- api/api.gen.go (≈7628-7630) and api/client/javascript/src/{zod,index,schemas}.ts — generated clients/docs reflect partial-match text.
- openmeter/ent/db/customer/where.go — KeyEQ (equality) and KeyContains / KeyContainsFold (contains) predicates both present.
- openmeter/billing/adapter/customeroverride.go:175 — confirmed use of dbcustomer.KeyEQ(input.CustomerKey) (needs change if API intends substring semantics).
- openmeter/customer/httpdriver/apimapping.go — verify mapping of the query param -> DB predicate (ensure it uses KeyContainsFold when intended).
Action items:
- Update server-side mappings/adapters that still use KeyEQ to use the case-insensitive contains predicate (KeyContainsFold) where the API promises substring matching.
- Run unit/integration tests and regenerate clients if needed.
- Add a release-note: "Customer key filter changed from exact match to case-insensitive substring match; existing callers may see broader result sets — please review any code relying on exact equality."
🤖 Prompt for AI Agents
api/openapi.cloud.yaml (lines 12179-12185) and related spec files were updated
to state "Case-insensitive partial match" but server code still uses equality
predicates (e.g. openmeter/billing/adapter/customeroverride.go:175 uses
dbcustomer.KeyEQ and openmeter/customer/httpdriver/apimapping.go may map query
-> KeyEQ), causing inconsistent behavior; update those server-side mappings to
use the case-insensitive substring predicate (dbcustomer.KeyContainsFold)
wherever the API promises substring matching, adjust any logic that builds
predicates to prefer ContainsFold over EQ for the customer key, run
unit/integration tests, regenerate clients/docs if needed, and add a release
note: "Customer key filter changed from exact match to case-insensitive
substring match; existing callers may see broader result sets — please review
any code relying on exact equality."
| // Filters | ||
| if input.Key != nil { | ||
| query = query.Where(customerdb.KeyEQ(*input.Key)) | ||
| query = query.Where(customerdb.KeyContainsFold(*input.Key)) | ||
| } |
There was a problem hiding this comment.
Guard against empty string: KeyContainsFold("") will match all rows
If clients pass key="" (or only whitespace), KeyContainsFold degrades to a full table match (LIKE '%%'), causing unexpected results and avoidable load. Trim and ignore empty after trim.
Apply this diff within this block:
- if input.Key != nil {
- query = query.Where(customerdb.KeyContainsFold(*input.Key))
- }
+ if input.Key != nil {
+ if trimmed := strings.TrimSpace(*input.Key); trimmed != "" {
+ query = query.Where(customerdb.KeyContainsFold(trimmed))
+ }
+ }Add the missing import:
import (
"context"
"fmt"
"time"
"entgo.io/ent/dialect/sql"
"github.com/samber/lo"
+ "strings"🤖 Prompt for AI Agents
In openmeter/customer/adapter/customer.go around lines 49-52, the filter
currently calls KeyContainsFold on input.Key without trimming or checking for
empty/whitespace which causes KeyContainsFold("") to match all rows; fix by
importing the strings package and, before applying the filter, trim whitespace
from *input.Key and only call query =
query.Where(customerdb.KeyContainsFold(trimmed)) if the trimmed string is
non-empty (i.e., length > 0), otherwise skip adding the filter.
Summary by CodeRabbit
New Features
Documentation