feat(customer-portal): add deployed products, attachments, activities, products - #1313
Conversation
…, products
Adds 10 more endpoints to backend-v2 (26 total): deployed-product
search/create/update, attachment create/search/download/delete, case
activity feed search, and product/product-version search.
- POST /deployed-products/search, POST /deployed-products,
PATCH /deployed-products/{id} — create/update only succeed against
entity-service's ServiceNow data source (Postgres always 400s).
PATCH enforces entity-service's mutually-exclusive rule: either
cores/tps/description or active=false, never both.
- POST /attachments, POST /attachments/search,
GET /attachments/{id}/content, DELETE /attachments/{id} — the content
endpoint is the first binary (non-JSON) response in this backend; adds
entity.Client.doBinary alongside the existing JSON do()/getJSON()/
postJSON()/patchJSON()/deleteJSON(), and the handler sets
Content-Disposition: attachment itself rather than relying on
entity-service's own response headers, since Content-Type/Disposition
is a fresh response this backend constructs.
- POST /cases/{id}/activities/search — the discriminated-union
CaseActivity type (comment/attachment/field_change) is mirrored
field-for-field from entity-service, including its exact non-pointer
omitempty pattern for the type-specific fields.
- POST /products/search, POST /products/{id}/versions/search — reuses
the accounts pattern for entity-service's Postgres/ServiceNow
data-source-dependent response shapes: a superset Go struct with
ambiguous fields typed as *string, normalized into one contract by the
DTO mapper.
Also updates openapi.yaml (10 new paths, 20 new schemas), README.md, and
CLAUDE.md (new notes on the json.RawMessage three-state pattern and the
binary-response convention).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
Warning Review limit reached
Next review available in: 28 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (4)
📝 WalkthroughWalkthroughThe backend adds deployed-product, attachment, case-activity, product, and product-version APIs. It introduces entity contracts, DTO mappings, clients, handlers, routes, OpenAPI schemas, documentation, and binary attachment download handling. ChangesCustomer portal resource APIs
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant Client
participant AttachmentHandler
participant EntityClient
participant EntityService
Client->>AttachmentHandler: GET /attachments/{id}/content
AttachmentHandler->>EntityClient: GetAttachmentContent(id)
EntityClient->>EntityService: Authenticated binary GET
EntityService-->>EntityClient: Bytes and Content-Type
EntityClient-->>AttachmentHandler: Bytes and Content-Type
AttachmentHandler-->>Client: Binary response with attachment disposition
Possibly related PRs
Suggested labels: Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (2)
apps/customer-portal/backend-v2/openapi.yaml (1)
1230-1267: 🔒 Security & Privacy | 🔵 Trivial | 💤 Low valueUnbounded array in
GET /updates/product-update-levelsresponse.Static analysis flags the
productUpdateLevelsitems array as having nomaxItemsconstraint. Unlike the new paginated search endpoints in this diff, this response is not paginated, so nothing bounds the number of items returned or documented as a contract limit.🤖 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 `@apps/customer-portal/backend-v2/openapi.yaml` around lines 1230 - 1267, Add a maxItems constraint to the array schema in the 200 response of the getUpdatesProductUpdateLevels operation to document and enforce the maximum number of ProductUpdateLevel items that can be returned. Since this endpoint is not paginated, the array schema requires an explicit upper bound to define the API contract and prevent unbounded responses.Source: Linters/SAST tools
apps/customer-portal/backend-v2/internal/entity/client.go (1)
194-239: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winExtract shared logic between
doanddoBinary.
doBinaryduplicates the header-injection block and the bounded-read/error-excerpt block fromdo. A future change to authentication headers or error formatting needs to be applied in both places, which invites drift.♻️ Proposed extraction of shared helpers
func (c *Client) setAuthHeaders(ctx context.Context, req *http.Request) { if token := userIDTokenFromContext(ctx); token != "" { req.Header.Set("x-user-id-token", token) } if id := correlationIDFromContext(ctx); id != "" { req.Header.Set("X-CSM-Correlation-ID", id) } } func readBoundedResponse(resp *http.Response, maxBytes int64) ([]byte, error) { limited := io.LimitReader(resp.Body, maxBytes+1) respBody, err := io.ReadAll(limited) if err != nil { return nil, fmt.Errorf("entity: read response body: %w", err) } if int64(len(respBody)) > maxBytes { return nil, fmt.Errorf("entity: response body exceeds %d bytes", maxBytes) } return respBody, nil } func upstreamError(resp *http.Response, respBody []byte) error { const maxErrBody = 256 excerpt := respBody if len(excerpt) > maxErrBody { excerpt = excerpt[:maxErrBody] } return &apierror.Error{StatusCode: resp.StatusCode, Body: string(excerpt)} }
doanddoBinaryboth callsetAuthHeaders,readBoundedResponse, andupstreamErrorinstead of repeating the logic inline.🤖 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 `@apps/customer-portal/backend-v2/internal/entity/client.go` around lines 194 - 239, Extract three helper methods to eliminate duplication between do and doBinary: create setAuthHeaders to handle the x-user-id-token and X-CSM-Correlation-ID header injection, create readBoundedResponse to encapsulate the io.LimitReader, io.ReadAll, and size validation logic, and create upstreamError to format the error response with the status code and truncated body excerpt. Update both do and doBinary to call these helpers instead of repeating the logic inline, ensuring both methods use the same authentication and error-handling paths.
🤖 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 `@apps/customer-portal/backend-v2/internal/entity/types.go`:
- Around line 693-700: Remove the unused ID field from
UpdateDeployedProductRequest, since PatchDeployedProduct already passes the path
ID separately to UpdateDeployedProduct and JSON decoding cannot populate it.
Keep the request payload fields unchanged and follow the existing pattern used
by SearchCaseActivitiesRequest.CaseID and
SearchProductVersionsRequest.ProductID.
- Around line 833-839: The SearchCaseActivitiesRequest struct contains a CaseID
field with a misleading comment, but the field is never populated—the caseID is
instead handled as a separate parameter in the SearchCaseActivities handler and
entity.Client.SearchCaseActivities method. Remove the unused CaseID field from
the SearchCaseActivitiesRequest struct and its associated doc comment to align
the code with the actual behavior.
- Around line 882-888: The SearchProductVersionsRequest struct contains a
ProductID field with a comment claiming it is populated from the URL path
parameter, but since the SearchProductVersions handler and
entity.Client.SearchProductVersions both accept productID as a separate function
parameter, this field is never assigned and remains unused. Remove the ProductID
field from the SearchProductVersionsRequest struct to eliminate the misleading
documentation and unused field, preserving the Pagination and SearchQuery
fields.
- Around line 616-624: Update SearchProducts-related types in types.go to match
entity-service/openapi.yaml: model the oneOf response as the required
discriminated wrapper containing SearchProductsResponse or
SearchSNProductsResponse, or consistently expose the two oneOf shapes with their
discriminator. Ensure SearchProducts and its response types serialize to the
OpenAPI contract without changing unrelated deployed-product types.
In `@apps/customer-portal/backend-v2/internal/handler/deployed_products.go`:
- Around line 131-143: Update PatchDeployedProduct to use a restricted customer
update DTO rather than decoding directly into
entity.UpdateDeployedProductRequest, excluding deploymentId unless it is
explicitly supported. Reject requests containing deploymentId before the
detailFieldsSet/activeSet validation and entity-service call; if retained, add
its customer-appropriate validation and include it in the update contract.
---
Nitpick comments:
In `@apps/customer-portal/backend-v2/internal/entity/client.go`:
- Around line 194-239: Extract three helper methods to eliminate duplication
between do and doBinary: create setAuthHeaders to handle the x-user-id-token and
X-CSM-Correlation-ID header injection, create readBoundedResponse to encapsulate
the io.LimitReader, io.ReadAll, and size validation logic, and create
upstreamError to format the error response with the status code and truncated
body excerpt. Update both do and doBinary to call these helpers instead of
repeating the logic inline, ensuring both methods use the same authentication
and error-handling paths.
In `@apps/customer-portal/backend-v2/openapi.yaml`:
- Around line 1230-1267: Add a maxItems constraint to the array schema in the
200 response of the getUpdatesProductUpdateLevels operation to document and
enforce the maximum number of ProductUpdateLevel items that can be returned.
Since this endpoint is not paginated, the array schema requires an explicit
upper bound to define the API contract and prevent unbounded responses.
🪄 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 Plus
Run ID: 4edb8dda-6211-4b93-8708-2ffc1f675d23
📒 Files selected for processing (18)
apps/customer-portal/backend-v2/CLAUDE.mdapps/customer-portal/backend-v2/README.mdapps/customer-portal/backend-v2/cmd/server/main.goapps/customer-portal/backend-v2/internal/dto/attachment.goapps/customer-portal/backend-v2/internal/dto/case.goapps/customer-portal/backend-v2/internal/dto/deployed_product.goapps/customer-portal/backend-v2/internal/dto/product.goapps/customer-portal/backend-v2/internal/entity/attachments.goapps/customer-portal/backend-v2/internal/entity/cases.goapps/customer-portal/backend-v2/internal/entity/client.goapps/customer-portal/backend-v2/internal/entity/deployed_products.goapps/customer-portal/backend-v2/internal/entity/products.goapps/customer-portal/backend-v2/internal/entity/types.goapps/customer-portal/backend-v2/internal/handler/attachments.goapps/customer-portal/backend-v2/internal/handler/cases.goapps/customer-portal/backend-v2/internal/handler/deployed_products.goapps/customer-portal/backend-v2/internal/handler/products.goapps/customer-portal/backend-v2/openapi.yaml
- entity.Client.UpdateDeployedProduct/SearchCaseActivities/SearchProductVersions
now actually assign the path-derived ID/CaseID/ProductID fields (json:"-",
so no wire-format change) instead of leaving them permanently zero-valued,
fixing a mismatch between the struct doc comments (which claimed these
fields were path-populated) and the actual code.
- Documented why PATCH /deployed-products/{id} decodes directly into
entity.UpdateDeployedProductRequest without a restricted portal DTO:
DeploymentID is an IDOR-style scope guard per entity-service's own doc
comment, not an internal-only mutation field like the ones excluded from
the case-update endpoint.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
d85bf42
into
wso2-open-operations:dev-app-csm-portal
Summary
Adds 10 more endpoints to
apps/customer-portal/backend-v2(26 total):POST /deployed-products/search,POST /deployed-products,PATCH /deployed-products/{id}— create/update only succeed against entity-service's ServiceNow data source (Postgres always 400s, documented in code/README/openapi). PATCH enforces entity-service's mutually-exclusive rule (eithercores/tps/descriptionoractive=false, never both) — verified in testing below.POST /attachments,POST /attachments/search,GET /attachments/{id}/content,DELETE /attachments/{id}— the content endpoint is this backend's first binary (non-JSON) response. Addsentity.Client.doBinaryalongside the existing JSONdo()/getJSON()/postJSON()/patchJSON()/deleteJSON(); the handler explicitly setsContent-Disposition: attachment(a fresh response header this backend constructs, not just relayed from entity-service) so browsers never render an attachment inline.POST /cases/{id}/activities/search— entity-service'sCaseActivityis a discriminated union ontype(comment/attachment/field_change); the Go struct mirrors its exact non-pointeromitemptypattern for the type-specific fields rather than "improving" it to pointers, to keep the JSON shape identical.POST /products/search,POST /products/{id}/versions/search— reuses the same superset-struct technique from the accounts endpoints (PR feat(customer-portal): add accounts/updates/scim modules and next 5 endpoints #1302) for entity-service's Postgres/ServiceNow data-source-dependent response shapes.Also updates
openapi.yaml(10 new paths, ~24 new schemas),README.md, andCLAUDE.md(new notes on thejson.RawMessagethree-state pattern forUpdateDeployedProductRequest.Description, and the binary-response convention).Test plan
go build ./...,go vet ./...,gofmt -l .all cleangosec -fmt=text ./...reports 0 issuesopenapi.yamlvalidated as well-formed YAML with all 24 paths / new schemas present (verified no content was lost despite a large diff from mid-file insertion)PATCH /deployed-products/{id}correctly rejects neither-nor and both-at-once field combos with 400, passes with exactly one; invalid UUID path params return 400; valid requests against an unreachable upstream map cleanly to 500 (no leakage)🤖 Generated with Claude Code
Summary by CodeRabbit
Release Notes
New Features
Documentation