Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 11 additions & 6 deletions platform-api/src/internal/constants/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,17 @@ package constants
import "errors"

var (
ErrHandleExists = errors.New("handle already exists")
ErrOrganizationExists = errors.New("organization already exists with the given UUID")
ErrInvalidHandle = errors.New("invalid handle format")
ErrOrganizationNotFound = errors.New("organization not found")
ErrMultipleOrganizations = errors.New("multiple organizations found")
ErrInvalidInput = errors.New("invalid input parameters")
ErrHandleExists = errors.New("handle already exists")
ErrHandleEmpty = errors.New("handle cannot be empty")
ErrHandleTooShort = errors.New("handle must be at least 3 characters")
ErrHandleTooLong = errors.New("handle must be at most 63 characters")
ErrInvalidHandle = errors.New("handle must be lowercase alphanumeric with hyphens only (no consecutive hyphens, cannot start or end with hyphen)")
ErrHandleGenerationFailed = errors.New("failed to generate unique handle after maximum retries")
ErrHandleSourceEmpty = errors.New("source string cannot be empty for handle generation")
ErrOrganizationExists = errors.New("organization already exists with the given UUID")
ErrOrganizationNotFound = errors.New("organization not found")
ErrMultipleOrganizations = errors.New("multiple organizations found")
ErrInvalidInput = errors.New("invalid input parameters")
)

var (
Expand Down
4 changes: 3 additions & 1 deletion platform-api/src/internal/database/schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ CREATE TABLE IF NOT EXISTS projects (
-- APIs table
CREATE TABLE IF NOT EXISTS apis (
uuid VARCHAR(40) PRIMARY KEY,
handle VARCHAR(255) NOT NULL,
name VARCHAR(255) NOT NULL,
display_name VARCHAR(255),
description VARCHAR(1023),
Expand All @@ -62,7 +63,8 @@ CREATE TABLE IF NOT EXISTS apis (
FOREIGN KEY (project_uuid) REFERENCES projects(uuid) ON DELETE CASCADE,
FOREIGN KEY (organization_uuid) REFERENCES organizations(uuid) ON DELETE CASCADE,
UNIQUE(name, organization_uuid),
UNIQUE(context, organization_uuid)
UNIQUE(context, organization_uuid),
UNIQUE(handle, organization_uuid)
);

-- API MTLS Configuration table
Expand Down
26 changes: 14 additions & 12 deletions platform-api/src/internal/handler/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ func (h *APIHandler) CreateAPI(c *gin.Context) {
c.JSON(http.StatusCreated, api)
}

// GetAPI handles GET /api/v1/apis/:apiId and retrieves an API by its ID
// GetAPI handles GET /api/v1/apis/:apiId and retrieves an API by its handle
func (h *APIHandler) GetAPI(c *gin.Context) {
orgId, exists := middleware.GetOrganizationFromContext(c)
if !exists {
Expand All @@ -146,7 +146,7 @@ func (h *APIHandler) GetAPI(c *gin.Context) {
return
}

api, err := h.apiService.GetAPIByUUID(apiId, orgId)
api, err := h.apiService.GetAPIByHandle(apiId, orgId)
if err != nil {
if errors.Is(err, constants.ErrAPINotFound) {
c.JSON(http.StatusNotFound, utils.NewErrorResponse(404, "Not Found",
Expand Down Expand Up @@ -202,7 +202,7 @@ func (h *APIHandler) ListAPIs(c *gin.Context) {
})
}

// UpdateAPI updates an existing API
// UpdateAPI updates an existing API identified by handle
func (h *APIHandler) UpdateAPI(c *gin.Context) {
orgId, exists := middleware.GetOrganizationFromContext(c)
if !exists {
Expand All @@ -225,7 +225,7 @@ func (h *APIHandler) UpdateAPI(c *gin.Context) {
return
}

api, err := h.apiService.UpdateAPI(apiId, &req, orgId)
api, err := h.apiService.UpdateAPIByHandle(apiId, &req, orgId)
if err != nil {
if errors.Is(err, constants.ErrAPINotFound) {
c.JSON(http.StatusNotFound, utils.NewErrorResponse(404, "Not Found",
Expand Down Expand Up @@ -255,7 +255,7 @@ func (h *APIHandler) UpdateAPI(c *gin.Context) {
c.JSON(http.StatusOK, api)
}

// DeleteAPI handles DELETE /api/v1/apis/:apiId and deletes an API by its ID
// DeleteAPI handles DELETE /api/v1/apis/:apiId and deletes an API by its handle
func (h *APIHandler) DeleteAPI(c *gin.Context) {
orgId, exists := middleware.GetOrganizationFromContext(c)
if !exists {
Expand All @@ -271,7 +271,7 @@ func (h *APIHandler) DeleteAPI(c *gin.Context) {
return
}

err := h.apiService.DeleteAPI(apiId, orgId)
err := h.apiService.DeleteAPIByHandle(apiId, orgId)
if err != nil {
if errors.Is(err, constants.ErrAPINotFound) {
c.JSON(http.StatusNotFound, utils.NewErrorResponse(404, "Not Found",
Expand Down Expand Up @@ -320,7 +320,7 @@ func (h *APIHandler) AddGatewaysToAPI(c *gin.Context) {
gatewayIds[i] = gw.GatewayID
}

gateways, err := h.apiService.AddGatewaysToAPI(apiId, gatewayIds, orgId)
gateways, err := h.apiService.AddGatewaysToAPIByHandle(apiId, gatewayIds, orgId)
if err != nil {
if errors.Is(err, constants.ErrAPINotFound) {
c.JSON(http.StatusNotFound, utils.NewErrorResponse(404, "Not Found",
Expand Down Expand Up @@ -356,7 +356,7 @@ func (h *APIHandler) GetAPIGateways(c *gin.Context) {
return
}

gateways, err := h.apiService.GetAPIGateways(apiId, orgId)
gateways, err := h.apiService.GetAPIGatewaysByHandle(apiId, orgId)
if err != nil {
if errors.Is(err, constants.ErrAPINotFound) {
c.JSON(http.StatusNotFound, utils.NewErrorResponse(404, "Not Found",
Expand Down Expand Up @@ -406,7 +406,7 @@ func (h *APIHandler) DeployAPIRevision(c *gin.Context) {
}

// Call service to deploy the API
deployments, err := h.apiService.DeployAPIRevision(apiId, revisionID, deploymentRequests, orgId)
deployments, err := h.apiService.DeployAPIRevisionByHandle(apiId, revisionID, deploymentRequests, orgId)
if err != nil {
if errors.Is(err, constants.ErrAPINotFound) {
c.JSON(http.StatusNotFound, utils.NewErrorResponse(404, "Not Found",
Expand All @@ -416,6 +416,8 @@ func (h *APIHandler) DeployAPIRevision(c *gin.Context) {
if errors.Is(err, constants.ErrInvalidAPIDeployment) {
c.JSON(http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request",
"Invalid API deployment configuration"))
log.Printf("[ERROR] Failed to deploy API revision: apiUUID=%s revisionID=%s error=%v",
apiId, revisionID, err)
return
}
log.Printf("[ERROR] Failed to deploy API revision: apiUUID=%s revisionID=%s error=%v",
Expand Down Expand Up @@ -458,7 +460,7 @@ func (h *APIHandler) PublishToDevPortal(c *gin.Context) {
}

// Publish API to DevPortal through service layer
err := h.apiService.PublishAPIToDevPortal(apiID, &req, orgID)
err := h.apiService.PublishAPIToDevPortalByHandle(apiID, &req, orgID)
if err != nil {
status, errorResp := utils.GetErrorResponse(err)
c.JSON(status, errorResp)
Expand Down Expand Up @@ -506,7 +508,7 @@ func (h *APIHandler) UnpublishFromDevPortal(c *gin.Context) {
}

// Unpublish API from DevPortal through service layer
err := h.apiService.UnpublishAPIFromDevPortal(apiID, req.DevPortalUUID, orgID)
err := h.apiService.UnpublishAPIFromDevPortalByHandle(apiID, req.DevPortalUUID, orgID)
if err != nil {
status, errorResp := utils.GetErrorResponse(err)
c.JSON(status, errorResp)
Expand Down Expand Up @@ -544,7 +546,7 @@ func (h *APIHandler) GetAPIPublications(c *gin.Context) {
return
}
// Get publications through service layer
response, err := h.apiService.GetAPIPublications(apiID, orgID)
response, err := h.apiService.GetAPIPublicationsByHandle(apiID, orgID)
if err != nil {
// Handle specific errors
if errors.Is(err, constants.ErrAPINotFound) {
Expand Down
10 changes: 10 additions & 0 deletions platform-api/src/internal/model/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
// API represents an API entity in the platform
type API struct {
ID string `json:"id" db:"uuid"`
Handle string `json:"handle" db:"handle"`
Name string `json:"name" db:"name"`
DisplayName string `json:"displayName,omitempty" db:"display_name"`
Description string `json:"description,omitempty" db:"description"`
Expand Down Expand Up @@ -55,6 +56,15 @@ func (API) TableName() string {
return "apis"
}

// APIMetadata contains minimal API information for handle-to-UUID resolution
type APIMetadata struct {
ID string `json:"id" db:"uuid"`
Handle string `json:"handle" db:"handle"`
Name string `json:"name" db:"name"`
Context string `json:"context" db:"context"`
OrganizationID string `json:"organizationId" db:"organization_uuid"`
}

// MTLSConfig represents mutual TLS configuration
type MTLSConfig struct {
Enabled bool `json:"enabled,omitempty"`
Expand Down
61 changes: 50 additions & 11 deletions platform-api/src/internal/repository/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ import (

"platform-api/src/internal/database"
"platform-api/src/internal/model"

"github.com/google/uuid"
)

// APIRepo implements APIRepository
Expand All @@ -50,6 +52,8 @@ func (r *APIRepo) CreateAPI(api *model.API) error {
}
defer tx.Rollback()

// Always generate a new UUID for the API
api.ID = uuid.New().String()
api.CreatedAt = time.Now()
api.UpdatedAt = time.Now()

Expand All @@ -58,15 +62,15 @@ func (r *APIRepo) CreateAPI(api *model.API) error {

// Insert main API record
apiQuery := `
INSERT INTO apis (uuid, name, display_name, description, context, version, provider,
project_uuid, organization_uuid, lifecycle_status, has_thumbnail, is_default_version, is_revision,
INSERT INTO apis (uuid, handle, name, display_name, description, context, version, provider,
project_uuid, organization_uuid, lifecycle_status, has_thumbnail, is_default_version, is_revision,
revisioned_api_id, revision_id, type, transport, security_enabled, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`

securityEnabled := api.Security != nil && api.Security.Enabled

_, err = tx.Exec(apiQuery, api.ID, api.Name, api.DisplayName, api.Description,
_, err = tx.Exec(apiQuery, 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, string(transportJSON), securityEnabled, api.CreatedAt, api.UpdatedAt)
Expand Down Expand Up @@ -117,7 +121,7 @@ func (r *APIRepo) GetAPIByUUID(apiId string) (*model.API, error) {
api := &model.API{}

query := `
SELECT uuid, name, display_name, description, context, version, provider,
SELECT uuid, handle, name, display_name, description, context, version, provider,
project_uuid, organization_uuid, lifecycle_status, has_thumbnail, is_default_version, is_revision,
revisioned_api_id, revision_id, type, transport, security_enabled, created_at, updated_at
FROM apis WHERE uuid = ?
Expand All @@ -126,7 +130,7 @@ func (r *APIRepo) GetAPIByUUID(apiId string) (*model.API, error) {
var transportJSON string
var securityEnabled bool
err := r.db.QueryRow(query, apiId).Scan(
&api.ID, &api.Name, &api.DisplayName, &api.Description, &api.Context,
&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,
Expand All @@ -152,10 +156,29 @@ func (r *APIRepo) GetAPIByUUID(apiId string) (*model.API, error) {
return api, nil
}

// GetAPIMetadataByHandle retrieves minimal API information by handle and organization ID
func (r *APIRepo) GetAPIMetadataByHandle(handle, orgId string) (*model.APIMetadata, error) {
metadata := &model.APIMetadata{}

query := `SELECT uuid, handle, name, context, organization_uuid FROM apis WHERE handle = ? AND organization_uuid = ?`

err := r.db.QueryRow(query, handle, orgId).Scan(
&metadata.ID, &metadata.Handle, &metadata.Name, &metadata.Context, &metadata.OrganizationID)

if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, nil
}
return nil, err
}

return metadata, nil
}

// GetAPIsByProjectID retrieves all APIs for a project
func (r *APIRepo) GetAPIsByProjectID(projectID string) ([]*model.API, error) {
query := `
SELECT uuid, name, display_name, description, context, version, provider,
SELECT uuid, handle, name, display_name, description, context, version, provider,
project_uuid, organization_uuid, lifecycle_status, has_thumbnail, is_default_version, is_revision,
revisioned_api_id, revision_id, type, transport, security_enabled, created_at, updated_at
FROM apis WHERE project_uuid = ? ORDER BY created_at DESC
Expand All @@ -173,7 +196,7 @@ func (r *APIRepo) GetAPIsByProjectID(projectID string) ([]*model.API, error) {
var transportJSON string
var securityEnabled bool

err := rows.Scan(&api.ID, &api.Name, &api.DisplayName, &api.Description,
err := rows.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,
Expand Down Expand Up @@ -206,7 +229,7 @@ func (r *APIRepo) GetAPIsByOrganizationID(orgID string, projectID *string) ([]*m
if projectID != nil && *projectID != "" {
// Filter by specific project within the organization
query = `
SELECT uuid, name, display_name, description, context, version, provider,
SELECT uuid, handle, name, display_name, description, context, version, provider,
project_uuid, organization_uuid, lifecycle_status, has_thumbnail, is_default_version, is_revision,
revisioned_api_id, revision_id, type, transport, security_enabled, created_at, updated_at
FROM apis
Expand All @@ -217,7 +240,7 @@ func (r *APIRepo) GetAPIsByOrganizationID(orgID string, projectID *string) ([]*m
} else {
// Get all APIs for the organization
query = `
SELECT uuid, name, display_name, description, context, version, provider,
SELECT uuid, handle, name, display_name, description, context, version, provider,
project_uuid, organization_uuid, lifecycle_status, has_thumbnail, is_default_version, is_revision,
revisioned_api_id, revision_id, type, transport, security_enabled, created_at, updated_at
FROM apis
Expand All @@ -239,7 +262,7 @@ func (r *APIRepo) GetAPIsByOrganizationID(orgID string, projectID *string) ([]*m
var transportJSON string
var securityEnabled bool

err := rows.Scan(&api.ID, &api.Name, &api.DisplayName, &api.Description,
err := rows.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,
Expand Down Expand Up @@ -405,6 +428,22 @@ func (r *APIRepo) DeleteAPI(apiId string) error {
return tx.Commit()
}

// CheckAPIExistsByHandleInOrganization checks if an API with the given handle exists within a specific organization
func (r *APIRepo) CheckAPIExistsByHandleInOrganization(handle, orgId string) (bool, error) {
query := `
SELECT COUNT(*) FROM apis
WHERE handle = ? AND organization_uuid = ?
`

var count int
err := r.db.QueryRow(query, handle, orgId).Scan(&count)
if err != nil {
return false, err
}

return count > 0, nil
}

// Helper methods for loading configurations

func (r *APIRepo) loadAPIConfigurations(api *model.API) error {
Expand Down
2 changes: 2 additions & 0 deletions platform-api/src/internal/repository/interfaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ type ProjectRepository interface {
type APIRepository interface {
CreateAPI(api *model.API) error
GetAPIByUUID(apiId string) (*model.API, error)
GetAPIMetadataByHandle(handle, orgId string) (*model.APIMetadata, error)
GetAPIsByProjectID(projectID string) ([]*model.API, error)
GetAPIsByOrganizationID(orgID string, projectID *string) ([]*model.API, error)
GetAPIsByGatewayID(gatewayID, organizationID string) ([]*model.API, error)
Expand All @@ -66,6 +67,7 @@ type APIRepository interface {
// API name validation methods
CheckAPIExistsByIdentifierInOrganization(identifier, orgId string) (bool, error)
CheckAPIExistsByNameAndVersionInOrganization(name, version, orgId string) (bool, error)
CheckAPIExistsByHandleInOrganization(handle, orgId string) (bool, error)
}

// BackendServiceRepository defines the interface for backend service data operations
Expand Down
Loading