Skip to content

Conversation

@malinthaprasan
Copy link
Contributor

@malinthaprasan malinthaprasan commented Dec 12, 2025

Purpose

#407

Goals

Describe what solutions this feature or fix introduces to address the problems outlined above.

Approach

Describe how you are implementing the solutions. Include an animated GIF or screenshot if the change affects the UI. Include a link to a Markdown file or Google doc if the feature write-up is too long to paste here.

User stories

Summary of user stories addressed by this change>

Documentation

Link(s) to product documentation that addresses the changes of this PR. If no doc impact, enter “N/A” plus brief explanation of why there’s no doc impact

Automation tests

  • Unit tests

    Code coverage information

  • Integration tests

    Details about the test cases and coverage

Security checks

Samples

Provide high-level details about the samples related to this feature

Related PRs

List any other related PRs

Test environment

List all JDK versions, operating systems, databases, and browser/versions on which this feature/fix was tested

Summary by CodeRabbit

  • New Features

    • APIs can be identified by human-readable handles (user-provided or auto-generated); many API operations now accept handles.
  • Improvements

    • API creation accepts a public handle/ID and exposes it in responses and paths.
    • Database enforces non-null handle with a per-organization uniqueness constraint.
    • OpenAPI/docs updated to reference handles instead of UUIDs.
    • Handle validation/generation utilities added with clearer, specific error messages.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 12, 2025

Walkthrough

API identification moved from UUIDs to user-facing handles across model, DB schema, repository, service, handler, utils, and OpenAPI; services continue to use internal UUIDs for relational operations while handlers and DTOs surface handles.

Changes

Cohort / File(s) Summary
Error Constants
platform-api/src/internal/constants/error.go
Added handle-related errors (ErrHandleEmpty, ErrHandleTooShort, ErrHandleTooLong, ErrHandleGenerationFailed, ErrHandleSourceEmpty) and tightened ErrInvalidHandle message.
Database Schema
platform-api/src/internal/database/schema.sql
Added handle VARCHAR(255) NOT NULL to apis and a unique constraint UNIQUE(handle, organization_uuid).
Model & API Contract
platform-api/src/internal/model/api.go, platform-api/src/resources/openapi.yaml
Added Handle field to API model and new APIMetadata type; OpenAPI API.id changed from readOnly UUID to a user-supplied/auto-generated handle and apiID parameter updated to represent a handle.
Repository Layer
platform-api/src/internal/repository/interfaces.go, platform-api/src/internal/repository/api.go
Persist/return handle in queries; added GetAPIMetadataByHandle and CheckAPIExistsByHandleInOrganization; create/select statements include handle; UUID assignment on create added.
Handler Layer
platform-api/src/internal/handler/api.go
Switched handler calls from UUID-based to handle-based service methods for get, update, delete, gateways, deploy, publish/unpublish, and publications; added extra deployment-failure logging.
Service Layer
platform-api/src/internal/service/api.go, platform-api/src/internal/service/gateway_internal.go
Introduced separation between user-facing handle and internal UUIDs: handle validation/generation, collision checks (HandleExistsCheck), handle-based service methods (get, deploy, publish/unpublish, gateways, publications), helpers (applyAPIUpdates, updateAPIBackendServices); gateway internal flow updated to accept apiHandle and resolve internal apiUUID.
Utilities
platform-api/src/internal/utils/api.go, platform-api/src/internal/utils/handle.go
DTO↔Model mapping updated to map DTO.ID ↔ Model.Handle; added ValidateHandle(handle string) error and GenerateHandle(source string, existsCheck func(string) bool) (string, error) with sanitization, retry-on-collision, and length rules.

Sequence Diagram(s)

mermaid
sequenceDiagram
participant Client
participant Handler
participant Service
participant Repo
participant DB
Note over Client,Handler: Client supplies API handle (or omits to auto-generate)
Client->>Handler: CreateAPI(req { id/handle?, payload })
Handler->>Service: CreateAPI(dto with ID/handle)
alt handle provided
Service->>Service: ValidateHandle(handle)
Service->>Repo: CheckAPIExistsByHandleInOrganization(handle, org)
Repo->>DB: SELECT ... WHERE handle = ? AND organization_uuid = ?
DB-->>Repo: exists? / not exists
else handle not provided
Service->>Service: GenerateHandle(source, existsCheck)
Note right of Service: sanitize, append suffix, retry up to MAX_RETRIES
Service->>Repo: CheckAPIExistsByHandleInOrganization(candidate, org)
Repo->>DB: SELECT ...
DB-->>Repo: exists? / not exists
end
Service->>Service: generate internal UUID for DB relations
Service->>Repo: CreateAPI(internal UUID, handle, payload)
Repo->>DB: INSERT into apis (uuid, handle, ...)
DB-->>Repo: inserted
Repo-->>Service: created model (includes handle & uuid)
Service-->>Handler: DTO of created API (ID = handle)
Handler-->>Client: 201 Created (body with id = handle)

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45–60 minutes

Areas requiring extra attention:

  • platform-api/src/internal/service/api.go — handle generation/validation, internal UUID management, many new handle-based methods and helpers.
  • platform-api/src/internal/repository/api.go — SQL changes for handle, create/select changes, UUID assignment on create.
  • platform-api/src/internal/utils/handle.go — sanitization, regex correctness, length enforcement, and retry/collision logic.
  • platform-api/src/internal/service/gateway_internal.go — signature change and correct resolution from handle → internal UUID for downstream records.
  • platform-api/src/resources/openapi.yaml — ensure contract changes match implementation and examples.

Poem

🐰 I nibbled bytes and stitched a name so bright,
Handles hop into place, neat and light.
I sanitize, retry, then add a playful tag,
Internal UUIDs hide beneath the shag.
Hopping forward—small suffix, big delight,
New handles lead the APIs into the light.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description follows the template structure but is largely incomplete, with only the Purpose section containing substantive information (an issue reference), while Goals, Approach, User stories, Documentation, Automation tests, Security checks, Samples, Related PRs, and Test environment sections remain as unfilled placeholders. Complete all template sections: explain the goals/solutions, implementation approach, affected user stories, documentation impact, test coverage details, security validation status, related samples, other related PRs, and tested environments.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main feature being introduced: support for user-provided identifiers (handles) for APIs, which is the primary change across all modified files.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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.

Copy link
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: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (5)
platform-api/src/internal/handler/api.go (1)

374-431: Fix misleading log field name (apiUUID) now that param is a handle
The error log uses apiUUID=%s but apiId is a handle now.

- log.Printf("[ERROR] Failed to deploy API revision: apiUUID=%s revisionID=%s error=%v",
+ log.Printf("[ERROR] Failed to deploy API revision: apiHandle=%s revisionID=%s error=%v",
     apiId, revisionID, err)
platform-api/src/resources/openapi.yaml (2)

1645-1660: OpenAPI: add handle constraints (pattern + length) and quote examples
Right now id is described as a handle, but clients don’t get the validation rules (and examples are unquoted).

     API:
       ...
       properties:
         id:
           type: string
           description: Unique handle/identifier for the API. Can be provided during creation or auto-generated.
-          example: my-api-handle
+          pattern: '^[a-z0-9]+(-[a-z0-9]+)*$'
+          minLength: 3
+          maxLength: 63
+          example: "my-api-handle"

3630-3639: OpenAPI: update remaining “UUID” wording/error examples for apiId
The parameter now correctly says “handle”, but multiple endpoint descriptions and examples still mention UUID (e.g., “API ID must be a valid UUID”). Those should be updated to “handle” to avoid breaking clients and SDKs.

platform-api/src/internal/database/schema.sql (1)

40-68: Align DB handle constraint with validation and reconsider context uniqueness scope

The handle column allows VARCHAR(255) but the validation code enforces a maximum of 63 characters (see platform-api/src/internal/utils/handle.go). Align the DB schema to match: use VARCHAR(63).

Additionally, UNIQUE(context, organization_uuid) prevents having multiple API versions with the same context within an organization. However, the index idx_apis_name_context_version and the documented API identity pattern suggest versioning under a shared context is intended (similar to /petstore/v1, /petstore/v2). This constraint may be too restrictive. Consider whether the uniqueness should be scoped as UNIQUE(context, version, organization_uuid) or UNIQUE(name, context, version, organization_uuid) instead.

Since this schema appears to support fresh installations only (no migration files or ALTER TABLE logic found), ensure any constraint changes are in place before the first production deployment.

platform-api/src/internal/repository/api.go (1)

307-336: GetAPIsByGatewayID does not populate the handle field.

Unlike other retrieval methods (GetAPIByUUID, GetAPIsByProjectID, GetAPIsByOrganizationID), this method's SELECT query omits the handle column (line 310). APIs returned by this method will have an empty Handle field.

 query := `
-	SELECT a.uuid, a.name, a.display_name, a.type, a.created_at, a.updated_at
+	SELECT a.uuid, a.handle, a.name, a.display_name, a.type, a.created_at, a.updated_at
 	FROM apis a
 	INNER JOIN api_deployments ad ON a.uuid = ad.api_uuid
 	WHERE ad.gateway_uuid = ? AND a.organization_uuid = ?
 	ORDER BY a.created_at DESC
 `

And update the Scan accordingly:

 err := rows.Scan(
-	&api.ID, &api.Name, &api.DisplayName, &api.Type, &api.CreatedAt, &api.UpdatedAt,
+	&api.ID, &api.Handle, &api.Name, &api.DisplayName, &api.Type, &api.CreatedAt, &api.UpdatedAt,
 )
🧹 Nitpick comments (8)
platform-api/src/internal/model/api.go (1)

24-52: Avoid accidentally exposing internal UUIDs and ensure Handle is always set
model.API now has both json:"id" (UUID) and json:"handle". If this type is ever marshaled directly, clients may see the UUID even though the API contract treats the identifier as the handle. Also, DB handle is NOT NULL, so empty string will fail inserts unless always populated.

 type API struct {
-    ID               string              `json:"id" db:"uuid"`
+    // Internal UUID (should not be exposed via REST responses)
+    ID               string              `json:"-" db:"uuid"`
     Handle           string              `json:"handle" db:"handle"`
     Name             string              `json:"name" db:"name"`
     ...
 }

If you still need id in some internal JSON flows, consider a dedicated internal DTO instead of reusing model.API.

platform-api/src/internal/repository/interfaces.go (1)

46-73: Make handle-based repository method signatures consistent (naming + arg order)
Purely ergonomics, but having consistent (orgID, handle) ordering and orgID naming reduces call-site mistakes across service/handler layers.

platform-api/src/internal/handler/api.go (1)

133-162: Good switch to handle-based lookup; rename variables/messages to “handle” for clarity
Now that apiId path param is a handle, consider updating user-facing error text and internal variable naming to avoid UUID confusion.

platform-api/src/internal/constants/error.go (1)

22-34: Align API contract with handle validation errors (3–63 + regex)
These new errors are useful, but they should be reflected in the OpenAPI schema (pattern + min/max) so clients can pre-validate.

platform-api/src/internal/utils/handle.go (1)

156-159: Suffix generation: consider using a hyphen-free source (and/or longer suffix)
Current uuid.New().String()[:4] is probably fine (hex), but it’s a bit non-obvious and has a small space (65,536). Consider using a hyphen-free UUID string (or a 6–8 char suffix) to reduce collision risk.

platform-api/src/internal/repository/api.go (1)

409-478: Consider extracting shared update logic to reduce duplication.

UpdateAPIByHandle duplicates ~60 lines of configuration handling logic from UpdateAPI. This increases maintenance burden and risk of divergence.

Extract the configuration update logic into a helper method:

func (r *APIRepo) updateAPIConfigurations(tx *sql.Tx, api *model.API) error {
    if err := r.deleteAPIConfigurations(tx, api.ID); err != nil {
        return err
    }
    
    if api.MTLS != nil {
        if err := r.insertMTLSConfig(tx, api.ID, api.MTLS); err != nil {
            return err
        }
    }
    // ... rest of configuration inserts
    
    for _, operation := range api.Operations {
        if err := r.insertOperation(tx, api.ID, api.OrganizationID, &operation); err != nil {
            return err
        }
    }
    return nil
}
platform-api/src/internal/service/api.go (2)

266-277: Consider logging errors in HandleExistsCheck for observability.

When the database check fails (line 272), the error is silently swallowed. While returning true is a safe fallback, logging the error would help with debugging handle generation failures.

 func (s *APIService) HandleExistsCheck(orgId string) func(string) bool {
 	return func(handle string) bool {
 		exists, err := s.apiRepo.CheckAPIExistsByHandleInOrganization(handle, orgId)
 		if err != nil {
-			// On error, assume it exists to be safe (will trigger retry)
+			// On error, assume it exists to be safe (will trigger retry)
+			log.Printf("[WARN] Failed to check handle existence for '%s': %v", handle, err)
 			return true
 		}
 		return exists
 	}
 }

398-419: Minor: Redundant API lookup in DeleteAPIByHandle.

The service method fetches the API (line 405), then calls repo.DeleteAPIByHandle which fetches it again (per repository implementation at line 522). Consider using repo.DeleteAPI(api.ID) directly since you already have the UUID.

 func (s *APIService) DeleteAPIByHandle(handle, orgId string) error {
 	if handle == "" {
 		return errors.New("API handle is required")
 	}

 	// Check if API exists
 	api, err := s.apiRepo.GetAPIByHandle(handle, orgId)
 	if err != nil {
 		return err
 	}
 	if api == nil {
 		return constants.ErrAPINotFound
 	}

 	// Delete API from repository
-	if err := s.apiRepo.DeleteAPIByHandle(handle, orgId); err != nil {
+	if err := s.apiRepo.DeleteAPI(api.ID); err != nil {
 		return fmt.Errorf("failed to delete api: %w", err)
 	}

 	return nil
 }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 311308a and 1b29638.

📒 Files selected for processing (10)
  • platform-api/src/internal/constants/error.go (1 hunks)
  • platform-api/src/internal/database/schema.sql (2 hunks)
  • platform-api/src/internal/handler/api.go (13 hunks)
  • platform-api/src/internal/model/api.go (1 hunks)
  • platform-api/src/internal/repository/api.go (10 hunks)
  • platform-api/src/internal/repository/interfaces.go (2 hunks)
  • platform-api/src/internal/service/api.go (8 hunks)
  • platform-api/src/internal/utils/api.go (2 hunks)
  • platform-api/src/internal/utils/handle.go (1 hunks)
  • platform-api/src/resources/openapi.yaml (2 hunks)
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-11-08T13:06:22.133Z
Learnt from: thivindu
Repo: wso2/api-platform PR: 110
File: platform-api/src/internal/service/api.go:432-476
Timestamp: 2025-11-08T13:06:22.133Z
Learning: In the platform-api Go codebase (file: platform-api/src/internal/service/api.go), the DeployAPIRevision method is designed to automatically create API-gateway associations during deployment if they don't already exist. There is no requirement that associations must pre-exist before deployment; the system checks for existing associations and creates them on-the-fly as needed.

Applied to files:

  • platform-api/src/internal/handler/api.go
  • platform-api/src/internal/service/api.go
📚 Learning: 2025-11-06T07:04:01.101Z
Learnt from: DakshithaS
Repo: wso2/api-platform PR: 105
File: platform-api/src/internal/model/api_publication.go:63-79
Timestamp: 2025-11-06T07:04:01.101Z
Learning: In the API Platform codebase (platform-api/src/internal/model/api_publication.go), the PublicationStateMachine intentionally disallows Published → Publishing transitions. The design requires APIs to be explicitly unpublished before they can be republished, enforcing the lifecycle: Published → Unpublishing → Unpublished → Publishing → Published. This prevents accidental overwrites and ensures proper cleanup.

Applied to files:

  • platform-api/src/internal/service/api.go
🧬 Code graph analysis (5)
platform-api/src/internal/utils/api.go (2)
platform-api/src/internal/model/api.go (2)
  • API (25-52)
  • API (55-57)
platform-api/src/internal/dto/api.go (1)
  • API (25-51)
platform-api/src/internal/repository/interfaces.go (1)
platform-api/src/internal/model/api.go (2)
  • API (25-52)
  • API (55-57)
platform-api/src/internal/utils/handle.go (1)
platform-api/src/internal/constants/error.go (6)
  • ErrHandleEmpty (24-24)
  • ErrHandleTooShort (25-25)
  • ErrHandleTooLong (26-26)
  • ErrInvalidHandle (27-27)
  • ErrHandleSourceEmpty (29-29)
  • ErrHandleGenerationFailed (28-28)
platform-api/src/internal/service/api.go (7)
platform-api/src/internal/utils/handle.go (2)
  • ValidateHandle (53-67)
  • GenerateHandle (80-117)
platform-api/src/internal/constants/error.go (2)
  • ErrHandleExists (23-23)
  • ErrAPINotFound (45-45)
platform-api/src/internal/model/api.go (3)
  • API (25-52)
  • API (55-57)
  • BackendService (132-144)
platform-api/src/internal/dto/api.go (1)
  • API (25-51)
platform-api/src/internal/client/devportal_client/errors.go (1)
  • ErrAPINotFound (73-73)
platform-api/src/internal/dto/gateway.go (1)
  • APIGatewayListResponse (116-120)
platform-api/src/internal/dto/devportal.go (1)
  • PublishToDevPortalRequest (129-134)
platform-api/src/internal/repository/api.go (1)
platform-api/src/internal/model/api.go (2)
  • API (25-52)
  • API (55-57)
🔇 Additional comments (12)
platform-api/src/internal/handler/api.go (3)

205-256: Prevent identifier drift: Update/Delete by handle should not accept/derive handle from request body
Handlers correctly route by :apiId (handle). Ensure the service layer never uses request payload id to change the handle implicitly during update.

Also applies to: 258-287


289-372: Gateway association endpoints look consistent after migrating to handle-based APIs
The handle-based service calls are aligned across add/list gateway endpoints.


433-567: DevPortal publish/unpublish/publications: consistent handle-based routing
Nice consistency across publish/unpublish/list-publications flows using ...ByHandle.

platform-api/src/internal/utils/api.go (1)

42-75: Handle changes are already protected from updates

The original concern about accidental handle renaming via DTOToModel is already mitigated by existing safeguards:

  • UpdateAPIRequest struct has no ID field, making it structurally impossible to send a handle in update request bodies
  • applyAPIUpdates never modifies the Handle field
  • Repository UPDATE statements do not include handle in the SET clause
  • Service explicitly preserves internal UUID before persistence (UpdateAPIByHandle: updatedAPIModel.ID = existingAPIModel.ID)
  • Create flow properly sets model.API.ID (UUID) before persistence

All suggested guardrails are already in place. No additional changes needed.

platform-api/src/internal/repository/api.go (2)

155-193: LGTM! New GetAPIByHandle method is well-implemented.

The method correctly:

  • Filters by both handle and organization_uuid for proper multi-tenancy
  • Returns nil for non-existent APIs (consistent with GetAPIByUUID)
  • Loads all related configurations

534-548: LGTM! CheckAPIExistsByHandleInOrganization is correctly implemented.

The existence check properly scopes by organization, which is essential for handle uniqueness within an organization.

platform-api/src/internal/service/api.go (6)

107-133: Handle generation and validation logic is well-implemented.

The flow correctly:

  1. Validates user-provided handles using utils.ValidateHandle
  2. Checks for collisions within the organization before accepting
  3. Auto-generates handles from API name with collision detection when not provided

367-396: LGTM! UpdateAPIByHandle correctly preserves the internal UUID.

Line 390 ensures the internal UUID is preserved before calling the repository method, which addresses the data integrity concern at the repository layer.


917-983: Good refactoring with applyAPIUpdates helper.

Extracting the update logic into a shared helper reduces code duplication between UpdateAPI and UpdateAPIByHandle, improving maintainability.


985-1022: LGTM! updateAPIBackendServices correctly handles backend service lifecycle.

The method properly:

  1. Upserts backend services (creates new or updates existing)
  2. Removes old associations
  3. Creates new associations with correct default flags

1082-1105: LGTM! CreateAPIRequest updated to support user-provided handles.

The optional ID field allows users to specify their own handle while maintaining backward compatibility when omitted (auto-generation kicks in).


157-196: [Your rewritten review comment text here]
[Exactly ONE classification tag]

Copy link
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: 0

🧹 Nitpick comments (4)
platform-api/src/internal/repository/api.go (4)

142-145: Handle JSON unmarshal errors.

The error from json.Unmarshal for transport data is silently ignored. If the stored JSON is malformed, this could lead to unexpected behavior with empty transport arrays.

Consider logging or returning the error:

 	// Parse transport JSON
 	if transportJSON != "" {
-		json.Unmarshal([]byte(transportJSON), &api.Transport)
+		if err := json.Unmarshal([]byte(transportJSON), &api.Transport); err != nil {
+			return nil, fmt.Errorf("failed to unmarshal transport JSON: %w", err)
+		}
 	}

155-193: Extract common logic to reduce duplication.

GetAPIByHandle and GetAPIByUUID share nearly identical logic for scanning, parsing transport JSON, and loading configurations (lines 166-193 vs. lines 126-153). This duplication makes maintenance harder.

Consider extracting the common logic into a helper method:

func (r *APIRepo) scanAndLoadAPI(scanner interface{ Scan(...interface{}) error }) (*model.API, error) {
	api := &model.API{}
	var transportJSON string
	var securityEnabled bool
	
	err := scanner.Scan(
		&api.ID, &api.Handle, &api.Name, &api.DisplayName, &api.Description, &api.Context,
		&api.Version, &api.Provider, &api.ProjectID, &api.OrganizationID, &api.LifeCycleStatus,
		&api.HasThumbnail, &api.IsDefaultVersion, &api.IsRevision,
		&api.RevisionedAPIID, &api.RevisionID, &api.Type, &transportJSON,
		&securityEnabled, &api.CreatedAt, &api.UpdatedAt)
	
	if err != nil {
		if errors.Is(err, sql.ErrNoRows) {
			return nil, nil
		}
		return nil, err
	}
	
	// Parse transport JSON
	if transportJSON != "" {
		if err := json.Unmarshal([]byte(transportJSON), &api.Transport); err != nil {
			return nil, fmt.Errorf("failed to unmarshal transport: %w", err)
		}
	}
	
	// Load related configurations
	if err := r.loadAPIConfigurations(api); err != nil {
		return nil, err
	}
	
	return api, nil
}

Then both methods would simply execute their query and call this helper.


480-494: Consider using EXISTS for better performance.

Using COUNT(*) requires scanning all matching rows, while EXISTS can short-circuit after finding the first match. For large tables, this can improve performance.

 func (r *APIRepo) CheckAPIExistsByHandleInOrganization(handle, orgId string) (bool, error) {
 	query := `
-		SELECT COUNT(*) FROM apis
+		SELECT EXISTS(SELECT 1 FROM apis
 		WHERE handle = ? AND organization_uuid = ?
+		)
 	`
 
-	var count int
-	err := r.db.QueryRow(query, handle, orgId).Scan(&count)
+	var exists bool
+	err := r.db.QueryRow(query, handle, orgId).Scan(&exists)
 	if err != nil {
 		return false, err
 	}
 
-	return count > 0, nil
+	return exists, nil
 }

1185-1199: Parameter name doesn't match queried field.

The method parameter is named name, but the query uses the display_name column. This inconsistency could lead to confusion about which field is being checked.

Consider renaming the parameter to match the field:

-func (r *APIRepo) CheckAPIExistsByNameAndVersionInOrganization(name, version, orgId string) (bool, error) {
+func (r *APIRepo) CheckAPIExistsByNameAndVersionInOrganization(displayName, version, orgId string) (bool, error) {
 	query := `
 		SELECT COUNT(*) FROM apis 
 		WHERE display_name = ? AND version = ? AND organization_uuid = ?
 	`
 
 	var count int
-	err := r.db.QueryRow(query, name, version, orgId).Scan(&count)
+	err := r.db.QueryRow(query, displayName, version, orgId).Scan(&count)
 	if err != nil {
 		return false, err
 	}
 
 	return count > 0, nil
 }

The same EXISTS optimization mentioned for other check methods applies here as well.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1b29638 and 2356a08.

📒 Files selected for processing (2)
  • platform-api/src/internal/repository/api.go (10 hunks)
  • platform-api/src/internal/utils/handle.go (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • platform-api/src/internal/utils/handle.go
🧰 Additional context used
🧬 Code graph analysis (1)
platform-api/src/internal/repository/api.go (2)
platform-api/src/internal/model/api.go (2)
  • API (25-52)
  • API (55-57)
platform-api/src/internal/dto/api.go (1)
  • API (25-51)
🔇 Additional comments (4)
platform-api/src/internal/repository/api.go (4)

409-424: LGTM! Past review concern addressed.

The UpdateAPIByHandle method now correctly fetches the API UUID before delegating to UpdateAPI, addressing the critical issue raised in the previous review. The implementation properly handles the case where api.ID is not set by the caller.


465-478: LGTM!

The DeleteAPIByHandle method follows the correct pattern by resolving the handle to a UUID before delegating to the existing DeleteAPI method. Error handling is appropriate.


1169-1183: Clarify "identifier" terminology.

The method name CheckAPIExistsByIdentifierInOrganization suggests it checks a general identifier, but it specifically queries the name field. Given the PR introduces handle as a user-facing identifier, this naming could be confusing.

Please verify the intended behavior:

  • Should "identifier" refer to the handle field (the new user-provided identifier)?
  • Or should it check the name field (as currently implemented)?
  • Or is "identifier" meant to encompass both?

Consider renaming to CheckAPIExistsByNameInOrganization for clarity if the current implementation is correct.

Additionally, the same EXISTS optimization mentioned for CheckAPIExistsByHandleInOrganization applies here.


60-75: The database schema already enforces handle uniqueness within an organization through a UNIQUE constraint defined on line 67 of platform-api/src/internal/database/schema.sql: UNIQUE(handle, organization_uuid). The application-level check suggested in the original comment is unnecessary, and the CreateAPI method's implementation is correct.

Likely an incorrect or invalid review comment.

Copy link
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: 0

🧹 Nitpick comments (3)
platform-api/src/internal/repository/api.go (1)

311-340: Consider adding handle to the SELECT for consistency.

GetAPIsByGatewayID does not select or populate the handle field, while other GetAPIs* methods do. If callers expect a fully populated model.API, this inconsistency could cause issues.

If the handle field should be available to callers:

 query := `
-	SELECT a.uuid, a.name, a.display_name, a.type, a.created_at, a.updated_at
+	SELECT a.uuid, a.handle, a.name, a.display_name, a.type, a.created_at, a.updated_at
 	FROM apis a
 	INNER JOIN api_deployments ad ON a.uuid = ad.api_uuid
 	WHERE ad.gateway_uuid = ? AND a.organization_uuid = ?
 	ORDER BY a.created_at DESC
 `

And update the Scan accordingly:

 err := rows.Scan(
-	&api.ID, &api.Name, &api.DisplayName, &api.Type, &api.CreatedAt, &api.UpdatedAt,
+	&api.ID, &api.Handle, &api.Name, &api.DisplayName, &api.Type, &api.CreatedAt, &api.UpdatedAt,
 )
platform-api/src/internal/service/gateway_internal.go (2)

168-169: Update stale comment.

The comment references apiID but the parameter was renamed to apiHandle.

 newAPI := &model.API{
-	Handle:           apiHandle, // Use provided apiID as handle
+	Handle:           apiHandle, // Use provided apiHandle as handle
 	Name:             notification.Configuration.Spec.Name,

193-199: Remove unnecessary database round trip.

The CreateAPI method already assigns the generated UUID to newAPI.ID (see api.ID = uuid.New().String() in repository/api.go line 56). After CreateAPI returns successfully, newAPI.ID contains the UUID, making this additional fetch redundant.

 err = s.apiRepo.CreateAPI(newAPI)
 if err != nil {
 	return nil, fmt.Errorf("failed to create API: %w", err)
 }

-// Retrieve the existing API or newly created API to get its ID
-existingAPI, err = s.apiRepo.GetAPIByHandle(apiHandle, orgID)
-if err != nil {
-	return nil, fmt.Errorf("failed to check existing API: %w", err)
-}
-
-apiUUID = existingAPI.ID
+apiUUID = newAPI.ID
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2356a08 and 5df3fbe.

📒 Files selected for processing (2)
  • platform-api/src/internal/repository/api.go (12 hunks)
  • platform-api/src/internal/service/gateway_internal.go (9 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-11-08T13:06:22.133Z
Learnt from: thivindu
Repo: wso2/api-platform PR: 110
File: platform-api/src/internal/service/api.go:432-476
Timestamp: 2025-11-08T13:06:22.133Z
Learning: In the platform-api Go codebase (file: platform-api/src/internal/service/api.go), the DeployAPIRevision method is designed to automatically create API-gateway associations during deployment if they don't already exist. There is no requirement that associations must pre-exist before deployment; the system checks for existing associations and creates them on-the-fly as needed.

Applied to files:

  • platform-api/src/internal/service/gateway_internal.go
🧬 Code graph analysis (2)
platform-api/src/internal/service/gateway_internal.go (3)
gateway/gateway-controller/pkg/utils/api_utils.go (1)
  • APIDeploymentNotification (201-210)
platform-api/src/internal/dto/gateway_internal.go (2)
  • APIDeploymentNotification (23-32)
  • GatewayAPIDeploymentResponse (63-68)
platform-api/src/internal/constants/constants.go (1)
  • AssociationTypeGateway (106-106)
platform-api/src/internal/repository/api.go (2)
platform-api/src/internal/model/api.go (2)
  • API (25-52)
  • API (55-57)
platform-api/src/internal/dto/api.go (1)
  • API (25-51)
🔇 Additional comments (14)
platform-api/src/internal/repository/api.go (10)

29-31: LGTM!

Clean import addition for UUID generation.


55-56: LGTM!

Good approach to always generate a new UUID server-side, ensuring consistent ID generation regardless of client input.


64-76: LGTM!

The INSERT statement correctly includes the handle column and value. The placeholder count matches the column count.


123-157: LGTM!

GetAPIByUUID correctly selects and scans the handle field, and properly handles sql.ErrNoRows by returning nil, nil.


159-197: LGTM!

GetAPIByHandle is well-implemented with proper error handling and configuration loading.


200-242: LGTM!

GetAPIsByProjectID correctly includes handle in the SELECT and Scan operations.


252-309: LGTM!

GetAPIsByOrganizationID consistently includes the handle field for both query branches.


413-428: LGTM!

The previous review feedback was addressed. The method now correctly fetches the API by handle to obtain the UUID before delegating to UpdateAPI.


469-482: LGTM!

DeleteAPIByHandle correctly looks up the API first to get the UUID, then delegates to DeleteAPI.


484-498: LGTM!

CheckAPIExistsByHandleInOrganization is a clean, simple existence check.

platform-api/src/internal/service/gateway_internal.go (4)

99-106: LGTM!

Good refactoring to use apiHandle parameter name, clarifying that this is a user-provided handle rather than a UUID.


133-136: LGTM!

Correctly changed from GetAPIByUUID to GetAPIByHandle to support the handle-based lookup flow.


201-216: LGTM!

Correctly uses apiUUID for backend service associations and handles the existing API case properly.


218-280: LGTM!

All downstream operations (deployments, associations, response) correctly use apiUUID instead of the handle.

Copy link
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)
platform-api/src/internal/service/api.go (1)

266-277: Consider logging errors in HandleExistsCheck for observability.

The current approach of returning true on error is safe for handle generation (prevents collisions), but silently swallowing the error makes debugging harder if there are underlying database connectivity issues.

 func (s *APIService) HandleExistsCheck(orgId string) func(string) bool {
 	return func(handle string) bool {
 		exists, err := s.apiRepo.CheckAPIExistsByHandleInOrganization(handle, orgId)
 		if err != nil {
 			// On error, assume it exists to be safe (will trigger retry)
+			log.Printf("[WARN] HandleExistsCheck failed for handle %s: %v", handle, err)
 			return true
 		}
 		return exists
 	}
 }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5df3fbe and 7fa933e.

📒 Files selected for processing (4)
  • platform-api/src/internal/model/api.go (2 hunks)
  • platform-api/src/internal/repository/api.go (11 hunks)
  • platform-api/src/internal/repository/interfaces.go (2 hunks)
  • platform-api/src/internal/service/api.go (9 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-11-08T13:06:22.133Z
Learnt from: thivindu
Repo: wso2/api-platform PR: 110
File: platform-api/src/internal/service/api.go:432-476
Timestamp: 2025-11-08T13:06:22.133Z
Learning: In the platform-api Go codebase (file: platform-api/src/internal/service/api.go), the DeployAPIRevision method is designed to automatically create API-gateway associations during deployment if they don't already exist. There is no requirement that associations must pre-exist before deployment; the system checks for existing associations and creates them on-the-fly as needed.

Applied to files:

  • platform-api/src/internal/service/api.go
🧬 Code graph analysis (2)
platform-api/src/internal/repository/interfaces.go (1)
platform-api/src/internal/model/api.go (3)
  • API (25-52)
  • API (55-57)
  • APIMetadata (60-66)
platform-api/src/internal/repository/api.go (2)
platform-api/src/internal/model/api.go (3)
  • API (25-52)
  • API (55-57)
  • APIMetadata (60-66)
platform-api/src/internal/dto/api.go (1)
  • API (25-51)
🔇 Additional comments (13)
platform-api/src/internal/model/api.go (2)

27-27: LGTM!

The Handle field is correctly added with appropriate JSON and DB tags, aligning with the PR objective to support user-provided identifiers for APIs.


59-66: LGTM!

The APIMetadata struct is a well-designed lightweight projection type for efficient handle-to-UUID resolution, containing only the essential fields needed for lookups. No TableName() method is required since this is used for SELECT queries against the existing apis table.

platform-api/src/internal/repository/interfaces.go (2)

50-51: LGTM!

The handle-based retrieval methods are well-designed:

  • GetAPIByHandle for full API retrieval with configurations
  • GetAPIMetadataByHandle for lightweight handle-to-UUID resolution

Both follow the established naming convention and parameter ordering.


71-71: LGTM!

The CheckAPIExistsByHandleInOrganization method is correctly added to support handle uniqueness validation during API creation, consistent with the existing CheckAPIExistsByIdentifierInOrganization pattern.

platform-api/src/internal/service/api.go (6)

107-133: LGTM!

The handle validation and generation logic correctly handles both scenarios:

  1. User-provided handle: validates format and checks for uniqueness within the organization
  2. Auto-generated handle: derives from API name with collision detection via HandleExistsCheck

The separation between internal UUID (apiUUID) and user-facing handle is well-implemented.


385-414: LGTM!

The UpdateAPIByHandle implementation correctly:

  1. Fetches the full API model by handle to get all fields including the internal UUID
  2. Applies updates using the shared applyAPIUpdates helper
  3. Preserves the internal UUID (line 408) before calling UpdateAPI

This addresses the concern from previous reviews about UUID handling in handle-based operations.


888-954: LGTM!

The applyAPIUpdates helper centralizes update logic well:

  • Validates the request before applying changes
  • Properly handles optional fields with nil checks
  • Correctly passes the internal UUID to updateAPIBackendServices

956-993: LGTM!

The updateAPIBackendServices helper correctly manages backend service associations:

  • Upserts backend services to get/create UUIDs
  • Removes existing associations before adding new ones (full replacement semantics)
  • Preserves the isDefault logic

428-481: LGTM!

The handle-based wrapper methods follow a consistent pattern:

  1. Resolve handle to internal UUID via getAPIUUIDByHandle
  2. Delegate to existing UUID-based implementation

This approach maximizes code reuse and maintains a single source of truth for business logic.


1054-1054: LGTM!

The optional ID field in CreateAPIRequest allows users to provide their own handle while maintaining backward compatibility (auto-generation when omitted).

platform-api/src/internal/repository/api.go (3)

159-197: LGTM!

GetAPIByHandle correctly implements handle-based retrieval:

  • Queries by both handle and organization_uuid (ensuring org-scoped uniqueness)
  • Parses transport JSON and loads full configurations
  • Follows the same pattern as GetAPIByUUID

199-216: LGTM!

GetAPIMetadataByHandle is a well-designed lightweight query that returns only the essential fields needed for handle-to-UUID resolution, avoiding the overhead of loading full configurations.


471-485: LGTM!

CheckAPIExistsByHandleInOrganization correctly implements existence checking with a simple COUNT query scoped by organization.

Copy link
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: 2

🧹 Nitpick comments (1)
platform-api/src/internal/service/api.go (1)

195-196: Note: UUID assignment may be redundant.

Line 196 sets apiModel.ID = apiUUID, but the repository's CreateAPI unconditionally generates a new UUID (platform-api/src/internal/repository/api.go:56). While redundant, this doesn't cause issues—the repository UUID will be authoritative. Consider removing this assignment for clarity, or document that the repository controls UUID generation.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7fa933e and 1d3d25c.

📒 Files selected for processing (4)
  • platform-api/src/internal/repository/api.go (11 hunks)
  • platform-api/src/internal/repository/interfaces.go (2 hunks)
  • platform-api/src/internal/service/api.go (9 hunks)
  • platform-api/src/internal/service/gateway_internal.go (8 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-11-08T13:06:22.133Z
Learnt from: thivindu
Repo: wso2/api-platform PR: 110
File: platform-api/src/internal/service/api.go:432-476
Timestamp: 2025-11-08T13:06:22.133Z
Learning: In the platform-api Go codebase (file: platform-api/src/internal/service/api.go), the DeployAPIRevision method is designed to automatically create API-gateway associations during deployment if they don't already exist. There is no requirement that associations must pre-exist before deployment; the system checks for existing associations and creates them on-the-fly as needed.

Applied to files:

  • platform-api/src/internal/service/gateway_internal.go
  • platform-api/src/internal/service/api.go
🧬 Code graph analysis (4)
platform-api/src/internal/repository/api.go (2)
platform-api/src/internal/model/api.go (3)
  • APIMetadata (60-66)
  • API (25-52)
  • API (55-57)
platform-api/src/internal/dto/api.go (1)
  • API (25-51)
platform-api/src/internal/repository/interfaces.go (1)
platform-api/src/internal/model/api.go (1)
  • APIMetadata (60-66)
platform-api/src/internal/service/gateway_internal.go (4)
gateway/gateway-controller/pkg/utils/api_utils.go (1)
  • APIDeploymentNotification (201-210)
platform-api/src/internal/dto/gateway_internal.go (2)
  • APIDeploymentNotification (23-32)
  • GatewayAPIDeploymentResponse (63-68)
platform-api/src/internal/constants/error.go (1)
  • ErrAPINotFound (45-45)
platform-api/src/internal/constants/constants.go (1)
  • AssociationTypeGateway (106-106)
platform-api/src/internal/service/api.go (7)
platform-api/src/internal/utils/handle.go (2)
  • ValidateHandle (53-67)
  • GenerateHandle (80-120)
platform-api/src/internal/constants/error.go (2)
  • ErrHandleExists (23-23)
  • ErrAPINotFound (45-45)
platform-api/src/internal/model/api.go (3)
  • API (25-52)
  • API (55-57)
  • BackendService (141-153)
platform-api/src/internal/dto/api.go (1)
  • API (25-51)
platform-api/src/internal/client/devportal_client/errors.go (1)
  • ErrAPINotFound (73-73)
platform-api/src/internal/websocket/transport.go (1)
  • Transport (30-83)
platform-api/src/internal/dto/backend_service.go (1)
  • BackendService (23-36)
🔇 Additional comments (19)
platform-api/src/internal/repository/interfaces.go (1)

50-50: LGTM: Handle-based repository methods added.

The new interface methods GetAPIMetadataByHandle and CheckAPIExistsByHandleInOrganization follow consistent naming conventions and parameter ordering with existing methods in the interface.

Also applies to: 70-70

platform-api/src/internal/service/gateway_internal.go (6)

99-119: LGTM: Method signature updated to accept handle.

The signature change from apiID to apiHandle is consistent with the PR's goal of using handles as user-facing identifiers. Early gateway validation is good practice for fail-fast error handling.


132-141: LGTM: Efficient metadata-based API lookup.

Using GetAPIMetadataByHandle for existence checking is more efficient than fetching the full API model, as it retrieves only minimal information needed for validation.


168-194: Verify: API UUID assignment relies on CreateAPI side effects.

The code assumes CreateAPI sets newAPI.ID (line 194), but past review comments flagged that the repository's CreateAPI unconditionally generates a new UUID (platform-api/src/internal/repository/api.go:55-56), which could overwrite any pre-set ID. While this works for this flow where the handle is separate, ensure the pattern is documented so other callers understand the UUID is always repository-generated.


196-202: LGTM: Backend service associations use internal UUID.

Backend service associations correctly use the internal apiUUID for database foreign key relationships, maintaining proper separation between user-facing handles and internal identifiers.


205-211: LGTM: Proper organization validation for existing APIs.

The organization check ensures that even if an API exists with the given handle, it must belong to the correct organization before proceeding with deployment.


213-276: LGTM: Consistent UUID usage throughout deployment flow.

All database operations (deployments, associations) correctly use the internal apiUUID. The response at line 271 returns the UUID, which is appropriate for internal gateway operations where stable identifiers are needed for tracking.

platform-api/src/internal/repository/api.go (6)

55-56: LGTM: UUID generation pattern clarified.

The unconditional UUID generation in CreateAPI establishes clear ownership: the repository layer always controls UUID assignment. This resolves the past review concern by making the behavior explicit and consistent.


64-76: LGTM: Handle persisted during API creation.

The handle field is correctly included in the INSERT statement and bound at the appropriate position in the parameter list.


124-144: LGTM: Handle field retrieved in GetAPIByUUID.

The handle is correctly added to the SELECT clause and scanned into the API model, maintaining consistency with the updated schema.


159-176: LGTM: Efficient metadata retrieval by handle.

This lightweight method retrieves only essential API information (ID, handle, name, context, organizationID) for handle-to-UUID resolution, avoiding the overhead of loading full configurations.


178-288: LGTM: Handle field consistently retrieved across query methods.

The handle field is properly included in SELECT statements and Scan operations for GetAPIsByProjectID and GetAPIsByOrganizationID, ensuring the handle is available wherever the full API model is returned.


431-445: LGTM: Handle existence check implementation.

The CheckAPIExistsByHandleInOrganization method uses an efficient COUNT query with proper organization scoping to verify handle uniqueness within an organization.

platform-api/src/internal/service/api.go (6)

107-133: LGTM: Robust handle generation with validation and collision detection.

The handle logic correctly:

  • Generates an internal UUID separate from the user-facing handle
  • Validates user-provided handles via utils.ValidateHandle
  • Checks for handle collision within the organization
  • Auto-generates collision-free handles using utils.GenerateHandle when not provided

The separation of internal UUID (for DB relationships) and handle (for user identification) is architecturally sound.


205-216: LGTM: Backend service associations use internal UUID.

Backend service associations correctly use apiUUID for database foreign key relationships, maintaining consistency with the internal identifier scheme.


248-268: LGTM: Clean handle-based helpers with safe error handling.

  • GetAPIByHandle follows a simple delegation pattern that resolves the handle to UUID before calling the existing UUID-based method
  • HandleExistsCheck returns a closure for collision detection with safe error handling (assumes handle exists on error to avoid collisions)

These helpers promote code reuse and maintain consistent error handling.


270-286: LGTM: Efficient handle-to-UUID resolution.

The getAPIUUIDByHandle helper provides lightweight UUID resolution using GetAPIMetadataByHandle, avoiding the overhead of loading full API configurations when only the internal ID is needed.


376-450: LGTM: Consistent handle-based method wrappers.

All handle-based wrapper methods (UpdateAPIByHandle, DeleteAPIByHandle, AddGatewaysToAPIByHandle, GetAPIGatewaysByHandle, DeployAPIRevisionByHandle, PublishAPIToDevPortalByHandle, UnpublishAPIFromDevPortalByHandle, GetAPIPublicationsByHandle) follow a consistent pattern:

  1. Resolve handle to internal UUID via getAPIUUIDByHandle
  2. Delegate to existing UUID-based method

This promotes code reuse and maintains a single source of truth for business logic.


1023-1023: LGTM: CreateAPIRequest supports user-provided handles.

Adding the ID field to CreateAPIRequest allows users to provide custom handles during API creation, aligning with the PR's goal of supporting user-defined identifiers.

Comment on lines +857 to +923
// applyAPIUpdates applies update request fields to an existing API model and handles backend services
func (s *APIService) applyAPIUpdates(existingAPIModel *model.API, req *UpdateAPIRequest, orgId string) (*dto.API, error) {
// Validate update request
if err := s.validateUpdateAPIRequest(req); err != nil {
return nil, err
}

existingAPI := s.apiUtil.ModelToDTO(existingAPIModel)

// Update fields (only allow certain fields to be updated)
if req.DisplayName != nil {
existingAPI.DisplayName = *req.DisplayName
}
if req.Description != nil {
existingAPI.Description = *req.Description
}
if req.Provider != nil {
existingAPI.Provider = *req.Provider
}
if req.LifeCycleStatus != nil {
existingAPI.LifeCycleStatus = *req.LifeCycleStatus
}
if req.HasThumbnail != nil {
existingAPI.HasThumbnail = *req.HasThumbnail
}
if req.IsDefaultVersion != nil {
existingAPI.IsDefaultVersion = *req.IsDefaultVersion
}
if req.IsRevision != nil {
existingAPI.IsRevision = *req.IsRevision
}
if req.RevisionedAPIID != nil {
existingAPI.RevisionedAPIID = *req.RevisionedAPIID
}
if req.RevisionID != nil {
existingAPI.RevisionID = *req.RevisionID
}
if req.Type != nil {
existingAPI.Type = *req.Type
}
if req.Transport != nil {
existingAPI.Transport = *req.Transport
}
if req.MTLS != nil {
existingAPI.MTLS = req.MTLS
}
if req.Security != nil {
existingAPI.Security = req.Security
}
if req.CORS != nil {
existingAPI.CORS = req.CORS
}
if req.BackendServices != nil {
if err := s.updateAPIBackendServices(existingAPIModel.ID, req.BackendServices, orgId); err != nil {
return nil, err
}
existingAPI.BackendServices = *req.BackendServices
}
if req.APIRateLimiting != nil {
existingAPI.APIRateLimiting = req.APIRateLimiting
}
if req.Operations != nil {
existingAPI.Operations = *req.Operations
}

return existingAPI, nil
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

LGTM: Update logic refactored into reusable helper.

Extracting the update logic into applyAPIUpdates centralizes field-by-field update handling and promotes code reuse. The method properly delegates backend service updates to updateAPIBackendServices, maintaining clean separation of concerns.

🤖 Prompt for AI Agents
In platform-api/src/internal/service/api.go around lines 857 to 923, the
reviewer approved the refactor of update logic into applyAPIUpdates and noted
delegation to updateAPIBackendServices; no functional issues were raised so no
code changes are required—leave the implementation as-is.

Comment on lines +925 to +962
// updateAPIBackendServices handles backend service updates for an API
func (s *APIService) updateAPIBackendServices(apiUUID string, backendServices *[]dto.BackendService, orgId string) error {
// Process backend services: check if they exist, create or update them
var backendServiceUUIDs []string
for _, backendService := range *backendServices {
backendServiceUUID, err := s.upstreamService.UpsertBackendService(&backendService, orgId)
if err != nil {
return fmt.Errorf("failed to process backend service '%s': %w", backendService.Name, err)
}
backendServiceUUIDs = append(backendServiceUUIDs, backendServiceUUID)
}

// Remove existing associations
existingBackendServices, err := s.upstreamService.GetBackendServicesByAPIID(apiUUID)
if err != nil {
return fmt.Errorf("failed to get existing backend services: %w", err)
}

for _, existingService := range existingBackendServices {
if err := s.upstreamService.DisassociateBackendServiceFromAPI(apiUUID, existingService.ID); err != nil {
return fmt.Errorf("failed to remove existing backend service association: %w", err)
}
}

// Add new associations
for i, backendServiceUUID := range backendServiceUUIDs {
isDefault := i == 0
if len(*backendServices) > 0 && i < len(*backendServices) {
isDefault = (*backendServices)[i].IsDefault
}

if err := s.upstreamService.AssociateBackendServiceWithAPI(apiUUID, backendServiceUUID, isDefault); err != nil {
return fmt.Errorf("failed to associate backend service with API: %w", err)
}
}

return nil
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

LGTM: Backend service update logic properly isolated.

The updateAPIBackendServices helper correctly:

  • Processes and upserts backend services
  • Removes existing associations before adding new ones
  • Uses the internal apiUUID for all database operations
  • Provides detailed error messages for troubleshooting

This refactoring improves maintainability by isolating backend service handling.

🤖 Prompt for AI Agents
platform-api/src/internal/service/api.go around lines 925 to 962: no changes
required — the updateAPIBackendServices function correctly upserts backend
services, removes old associations, and re-associates new ones using the apiUUID
with clear error propagation; leave the implementation as-is.

Copy link
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: 2

🧹 Nitpick comments (1)
platform-api/src/internal/service/api.go (1)

254-265: Don’t silently treat repo errors as “handle exists” during GenerateHandle()

HandleExistsCheck returns true on repo error, which can turn transient DB issues into repeated collisions and ultimately ErrHandleGenerationFailed with no signal of the real cause. At minimum, log the error (with orgId + handle) so operators can diagnose; ideally, consider a GenerateHandle variant that can propagate the error instead of converting it into “exists”.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 68826ce and dbfda5f.

📒 Files selected for processing (1)
  • platform-api/src/internal/service/api.go (8 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-11-08T13:06:22.133Z
Learnt from: thivindu
Repo: wso2/api-platform PR: 110
File: platform-api/src/internal/service/api.go:432-476
Timestamp: 2025-11-08T13:06:22.133Z
Learning: In the platform-api Go codebase (file: platform-api/src/internal/service/api.go), the DeployAPIRevision method is designed to automatically create API-gateway associations during deployment if they don't already exist. There is no requirement that associations must pre-exist before deployment; the system checks for existing associations and creates them on-the-fly as needed.

Applied to files:

  • platform-api/src/internal/service/api.go
🧬 Code graph analysis (1)
platform-api/src/internal/service/api.go (5)
platform-api/src/internal/utils/handle.go (2)
  • ValidateHandle (53-67)
  • GenerateHandle (80-120)
platform-api/src/internal/constants/error.go (2)
  • ErrHandleExists (23-23)
  • ErrAPINotFound (45-45)
platform-api/src/internal/model/api.go (3)
  • API (25-52)
  • API (55-57)
  • BackendService (141-153)
platform-api/src/internal/dto/api.go (1)
  • API (25-51)
platform-api/src/internal/client/devportal_client/errors.go (1)
  • ErrAPINotFound (73-73)
🔇 Additional comments (1)
platform-api/src/internal/service/api.go (1)

245-283: Handle→UUID resolution + wrapper methods look clean; verify org scoping and uniqueness assumptions

The handle-based wrappers correctly resolve UUID then delegate to existing UUID-based methods (good for consistency and authorization checks). Please verify apiRepo.GetAPIMetadataByHandle(handle, orgId) is scoped by orgId and relies on the same uniqueness guarantee you intend (one handle per org).

Also applies to: 373-447

Comment on lines +106 to +129
// Handle the API handle (user-facing identifier)
var handle string
if req.ID != "" {
// Validate user-provided handle
if err := utils.ValidateHandle(req.ID); err != nil {
return nil, err
}
// Check if handle already exists in the organization
handleExists, err := s.apiRepo.CheckAPIExistsByHandleInOrganization(req.ID, orgId)
if err != nil {
return nil, err
}
if handleExists {
return nil, constants.ErrHandleExists
}
handle = req.ID
} else {
// Generate handle from API name with collision detection
var err error
handle, err = utils.GenerateHandle(req.Name, s.HandleExistsCheck(orgId))
if err != nil {
return nil, err
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Enforce handle uniqueness at the DB/repo layer (TOCTOU) + clarify “ID == handle” semantics

The pre-check CheckAPIExistsByHandleInOrganization can race (two requests pass the check, then both try create). Please ensure there’s a unique constraint (at least (organization_uuid, handle)) and that apiRepo.CreateAPI(...) maps duplicate-handle DB errors to constants.ErrHandleExists (otherwise this returns a generic “failed to create api”). Also, since CreateAPIRequest.ID and dto.API.ID now carry the handle (not the internal UUID), consider renaming CreateAPIRequest.IDHandle (or similar) to avoid future misuse.

Also applies to: 153-178, 190-221

Comment on lines +202 to 213
// Associate backend services with the API (use internal UUID)
for i, backendServiceUUID := range backendServiceIdList {
isDefault := i == 0 // First backend service is default
if len(req.BackendServices) > 0 && i < len(req.BackendServices) {
// Check if isDefault was explicitly set in the request
isDefault = req.BackendServices[i].IsDefault
}

if err := s.upstreamService.AssociateBackendServiceWithAPI(apiId, backendServiceUUID, isDefault); err != nil {
if err := s.upstreamService.AssociateBackendServiceWithAPI(apiUUID, backendServiceUUID, isDefault); err != nil {
return nil, fmt.Errorf("failed to associate backend service with API: %w", err)
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Default backend-service selection is not “explicit” (bool cannot express omitted); ensure exactly one default

Both create and update paths overwrite isDefault := i == 0 with BackendService.IsDefault, but IsDefault is a non-pointer bool, so “omitted” is indistinguishable from “explicit false”. Result: you can easily end up with no default backend service even when the user didn’t intend to set one. Prefer: if none of the provided backend services are marked default, force the first to be default (and ideally also prevent multiple defaults).

Suggested patch pattern (apply similarly in both loops):

+   // pick a default deterministically if none provided
+   defaultIdx := 0
+   foundDefault := false
+   for i, bs := range req.BackendServices { // or *backendServices in update
+       if bs.IsDefault {
+           defaultIdx = i
+           foundDefault = true
+           break
+       }
+   }
...
-   isDefault := i == 0 // First backend service is default
-   if len(req.BackendServices) > 0 && i < len(req.BackendServices) {
-       // Check if isDefault was explicitly set in the request
-       isDefault = req.BackendServices[i].IsDefault
-   }
+   isDefault := i == defaultIdx
+   _ = foundDefault // (optional) use to validate “only one default” if desired

Also applies to: 946-956

🤖 Prompt for AI Agents
platform-api/src/internal/service/api.go around lines 202-213 (also apply same
fix at 946-956): the loop currently overwrites the implicit default (isDefault
:= i == 0) with req.BackendServices[i].IsDefault where IsDefault is a
non-pointer bool so omission cannot be distinguished from explicit false, which
can leave zero or multiple defaults; fix by first scanning req.BackendServices
to detect how many entries explicitly set IsDefault==true, if zero then treat
the first backend as default, if more than one return a validation error (or
normalize by picking the first and rejecting others), then in the loop use that
computed decision (don’t rely on the raw non-pointer bool to indicate omission);
apply the same pre-scan-and-enforce logic in both the create and update loops.

@thivindu thivindu merged commit 3982d34 into wso2:main Dec 13, 2025
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants