| # Clean Architecture Migration Plan |
|
|
| ## Current Architecture Issues |
|
|
| 1. **Mixed Concerns**: Handlers contain HTTP, business logic, and persistence code |
| 2. **Tight Coupling**: Direct dependency on Gin, PostgreSQL, external APIs |
| 3. **No Clear Boundaries**: Domain logic scattered across packages |
| 4. **Testing Difficulty**: Hard to unit test without full infrastructure |
|
|
| ## Target Clean Architecture |
|
|
| ``` |
| ┌─────────────────────────────────────────────────────────────┐ |
| │ External World │ |
| │ (Gin, PostgreSQL, Redis, OAuth APIs, AI APIs) │ |
| └──────────────────────┬──────────────────────────────────────┘ |
| │ |
| ┌──────────────────────▼──────────────────────────────────────┐ |
| │ Interface Adapters │ |
| │ ┌──────────────┐ ┌──────────────┐ ┌──────────────────┐ │ |
| │ │ HTTP API │ │ Repository │ │ External Service │ │ |
| │ │ Handlers │ │ Implement │ │ Clients │ │ |
| │ └──────────────┘ └──────────────┘ └──────────────────┘ │ |
| └──────────────────────┬──────────────────────────────────────┘ |
| │ |
| ┌──────────────────────▼──────────────────────────────────────┐ |
| │ Application Layer │ |
| │ ┌────────────────────────────────────────────────────────┐ │ |
| │ │ Use Cases (Services) │ │ |
| │ │ - Orchestrate domain objects │ │ |
| │ │ - Define application workflows │ │ |
| │ │ - Transaction boundaries │ │ |
| │ └────────────────────────────────────────────────────────┘ │ |
| └──────────────────────┬──────────────────────────────────────┘ |
| │ |
| ┌──────────────────────▼──────────────────────────────────────┐ |
| │ Domain Layer │ |
| │ ┌──────────────────┐ ┌──────────────┐ ┌──────────────┐ │ |
| │ │ Entities │ │ Value Objects│ │ Domain Events│ │ |
| │ │ (Auth, Config) │ │ (Token, ID) │ │ (AuthCreated)│ │ |
| │ └──────────────────┘ └──────────────┘ └──────────────┘ │ |
| │ ┌──────────────────┐ ┌──────────────────────────────────┐│ |
| │ │ Domain Services │ │ Repository Interfaces ││ |
| │ │ (TokenValidator) │ │ (ports - no implementation) ││ |
| │ └──────────────────┘ └──────────────────────────────────┘│ |
| └─────────────────────────────────────────────────────────────┘ |
| ``` |
|
|
| ## Phase 1: Domain Layer Extraction (Week 1-2) |
|
|
| ### 1.1 Domain Entities |
| **File**: `internal/domain/entities/auth.go` |
|
|
| ```go |
| package entities |
| |
| import ( |
| "time" |
| ) |
| |
| // Auth represents an authenticated OAuth session |
| type Auth struct { |
| ID AuthID |
| Provider Provider |
| Status AuthStatus |
| AccessToken Token |
| RefreshToken Token |
| ExpiresAt time.Time |
| CreatedAt time.Time |
| UpdatedAt time.Time |
| Metadata map[string]string |
| } |
| |
| // AuthID is a value object |
| type AuthID string |
| |
| func (id AuthID) String() string { return string(id) } |
| |
| // Provider represents supported OAuth providers |
| type Provider string |
| |
| const ( |
| ProviderGemini Provider = "gemini" |
| ProviderClaude Provider = "claude" |
| ProviderCodex Provider = "codex" |
| ProviderAntigravity Provider = "antigravity" |
| ProviderQwen Provider = "qwen" |
| ) |
| |
| // AuthStatus represents the authentication state |
| type AuthStatus int |
| |
| const ( |
| AuthStatusUnknown AuthStatus = iota |
| AuthStatusPending |
| AuthStatusActive |
| AuthStatusExpired |
| AuthStatusRevoked |
| ) |
| |
| func (s AuthStatus) String() string { |
| switch s { |
| case AuthStatusPending: |
| return "pending" |
| case AuthStatusActive: |
| return "active" |
| case AuthStatusExpired: |
| return "expired" |
| case AuthStatusRevoked: |
| return "revoked" |
| default: |
| return "unknown" |
| } |
| } |
| |
| // Domain methods |
| func (a *Auth) IsExpired() bool { |
| return time.Now().After(a.ExpiresAt) |
| } |
| |
| func (a *Auth) CanRefresh() bool { |
| return a.Status == AuthStatusActive && a.RefreshToken != "" |
| } |
| |
| func (a *Auth) Refresh(newToken Token, newExpiry time.Time) error { |
| if !a.CanRefresh() { |
| return domainerrors.New("auth cannot be refreshed") |
| } |
| a.AccessToken = newToken |
| a.ExpiresAt = newExpiry |
| a.UpdatedAt = time.Now() |
| return nil |
| } |
| |
| func (a *Auth) Revoke() { |
| a.Status = AuthStatusRevoked |
| a.AccessToken = "" |
| a.RefreshToken = "" |
| a.UpdatedAt = time.Now() |
| } |
| ``` |
|
|
| ### 1.2 Repository Ports |
| **File**: `internal/domain/ports/repositories.go` |
|
|
| ```go |
| package ports |
| |
| import ( |
| "context" |
| |
| "github.com/echyai/cliproxyapi/internal/domain/entities" |
| ) |
| |
| // AuthRepository defines the interface for auth persistence |
| type AuthRepository interface { |
| FindByID(ctx context.Context, id entities.AuthID) (*entities.Auth, error) |
| FindByProvider(ctx context.Context, provider entities.Provider) ([]*entities.Auth, error) |
| Save(ctx context.Context, auth *entities.Auth) error |
| Delete(ctx context.Context, id entities.AuthID) error |
| List(ctx context.Context, filter AuthFilter) ([]*entities.Auth, error) |
| } |
| |
| // AuthFilter for querying auths |
| type AuthFilter struct { |
| Provider *entities.Provider |
| Status *entities.AuthStatus |
| CreatedAfter *time.Time |
| } |
| |
| // ConfigRepository defines the interface for config persistence |
| type ConfigRepository interface { |
| Get(ctx context.Context) (*entities.Config, error) |
| Save(ctx context.Context, cfg *entities.Config) error |
| Watch(ctx context.Context) (<-chan entities.Config, error) |
| } |
| |
| // TokenStore defines the interface for token storage |
| type TokenStore interface { |
| Get(ctx context.Context, key string) (string, error) |
| Set(ctx context.Context, key string, value string, ttl time.Duration) error |
| Delete(ctx context.Context, key string) error |
| } |
| |
| // UnitOfWork manages transactional operations |
| type UnitOfWork interface { |
| Begin(ctx context.Context) (Context, error) |
| Commit(ctx context.Context) error |
| Rollback(ctx context.Context) error |
| } |
| |
| // Context carries transaction context |
| type Context struct { |
| context.Context |
| Tx interface{} // Driver-specific transaction handle |
| } |
| ``` |
|
|
| ### 1.3 Domain Services |
| **File**: `internal/domain/services/token_validator.go` |
|
|
| ```go |
| package services |
| |
| import ( |
| "context" |
| "time" |
| |
| "github.com/echyai/cliproxyapi/internal/domain/entities" |
| "github.com/echyai/cliproxyapi/internal/domain/ports" |
| ) |
| |
| // TokenValidator handles token validation and refresh logic |
| type TokenValidator struct { |
| authRepo ports.AuthRepository |
| providerRepo ports.ProviderRepository |
| tokenStore ports.TokenStore |
| } |
| |
| func NewTokenValidator( |
| authRepo ports.AuthRepository, |
| providerRepo ports.ProviderRepository, |
| tokenStore ports.TokenStore, |
| ) *TokenValidator { |
| return &TokenValidator{ |
| authRepo: authRepo, |
| providerRepo: providerRepo, |
| tokenStore: tokenStore, |
| } |
| } |
| |
| // ValidateAndRefresh checks token validity and refreshes if needed |
| func (v *TokenValidator) ValidateAndRefresh( |
| ctx context.Context, |
| authID entities.AuthID, |
| ) (*entities.Auth, error) { |
| auth, err := v.authRepo.FindByID(ctx, authID) |
| if err != nil { |
| return nil, err |
| } |
| |
| if auth == nil { |
| return nil, domainerrors.NewNotFound("auth", authID.String()) |
| } |
| |
| // Check if token needs refresh |
| if !auth.IsExpired() { |
| return auth, nil |
| } |
| |
| if !auth.CanRefresh() { |
| return nil, domainerrors.NewUnauthorized("token expired and cannot be refreshed") |
| } |
| |
| // Get provider for refresh |
| provider, err := v.providerRepo.GetOAuthProvider(ctx, auth.Provider) |
| if err != nil { |
| return nil, err |
| } |
| |
| // Perform refresh |
| newToken, newExpiry, err := provider.RefreshToken(ctx, auth.RefreshToken) |
| if err != nil { |
| return nil, domainerrors.NewProviderError(auth.Provider, err) |
| } |
| |
| // Update auth entity |
| if err := auth.Refresh(newToken, newExpiry); err != nil { |
| return nil, err |
| } |
| |
| // Persist changes |
| if err := v.authRepo.Save(ctx, auth); err != nil { |
| return nil, err |
| } |
| |
| return auth, nil |
| } |
| ``` |
|
|
| ## Phase 2: Application Layer (Week 2-3) |
|
|
| ### 2.1 Use Cases |
| **File**: `internal/application/usecase/auth_usecase.go` |
|
|
| ```go |
| package usecase |
| |
| import ( |
| "context" |
| |
| "github.com/echyai/cliproxyapi/internal/application/dto" |
| "github.com/echyai/cliproxyapi/internal/application/mapper" |
| "github.com/echyai/cliproxyapi/internal/domain/entities" |
| "github.com/echyai/cliproxyapi/internal/domain/ports" |
| "github.com/echyai/cliproxyapi/internal/domain/services" |
| ) |
| |
| // AuthUseCase orchestrates auth-related operations |
| type AuthUseCase struct { |
| authRepo ports.AuthRepository |
| providerRepo ports.ProviderRepository |
| tokenStore ports.TokenStore |
| validator *services.TokenValidator |
| mapper *mapper.AuthMapper |
| eventBus ports.EventBus |
| } |
| |
| func NewAuthUseCase( |
| authRepo ports.AuthRepository, |
| providerRepo ports.ProviderRepository, |
| tokenStore ports.TokenStore, |
| eventBus ports.EventBus, |
| ) *AuthUseCase { |
| return &AuthUseCase{ |
| authRepo: authRepo, |
| providerRepo: providerRepo, |
| tokenStore: tokenStore, |
| validator: services.NewTokenValidator(authRepo, providerRepo, tokenStore), |
| mapper: mapper.NewAuthMapper(), |
| eventBus: eventBus, |
| } |
| } |
| |
| // GetAuth retrieves an auth by ID with token validation |
| func (uc *AuthUseCase) GetAuth(ctx context.Context, id string) (*dto.AuthDTO, error) { |
| authID := entities.AuthID(id) |
| |
| auth, err := uc.validator.ValidateAndRefresh(ctx, authID) |
| if err != nil { |
| return nil, err |
| } |
| |
| return uc.mapper.ToDTO(auth), nil |
| } |
| |
| // CreateAuth initiates OAuth flow |
| func (uc *AuthUseCase) CreateAuth( |
| ctx context.Context, |
| input dto.CreateAuthInput, |
| ) (*dto.AuthDTO, error) { |
| // Validate input |
| if err := input.Validate(); err != nil { |
| return nil, err |
| } |
| |
| // Get OAuth provider |
| provider, err := uc.providerRepo.GetOAuthProvider(ctx, entities.Provider(input.Provider)) |
| if err != nil { |
| return nil, err |
| } |
| |
| // Generate OAuth URL |
| authURL, state, err := provider.GenerateAuthURL(ctx, input.RedirectURI) |
| if err != nil { |
| return nil, err |
| } |
| |
| // Store pending state |
| pendingAuth := entities.NewPendingAuth( |
| entities.Provider(input.Provider), |
| state, |
| input.RedirectURI, |
| ) |
| |
| if err := uc.authRepo.Save(ctx, pendingAuth); err != nil { |
| return nil, err |
| } |
| |
| // Publish event |
| uc.eventBus.Publish(ctx, events.AuthInitiated{ |
| AuthID: pendingAuth.ID, |
| Provider: pendingAuth.Provider, |
| AuthURL: authURL, |
| }) |
| |
| return uc.mapper.ToDTO(pendingAuth), nil |
| } |
| |
| // CompleteAuth finishes OAuth flow |
| func (uc *AuthUseCase) CompleteAuth( |
| ctx context.Context, |
| input dto.CompleteAuthInput, |
| ) (*dto.AuthDTO, error) { |
| // Find pending auth by state |
| auth, err := uc.authRepo.FindByState(ctx, input.State) |
| if err != nil { |
| return nil, err |
| } |
| |
| if auth == nil { |
| return nil, domainerrors.NewInvalidState("invalid or expired state") |
| } |
| |
| // Exchange code for tokens |
| provider, err := uc.providerRepo.GetOAuthProvider(ctx, auth.Provider) |
| if err != nil { |
| return nil, err |
| } |
| |
| tokens, err := provider.ExchangeCode(ctx, input.Code) |
| if err != nil { |
| return nil, domainerrors.NewProviderError(auth.Provider, err) |
| } |
| |
| // Activate auth |
| auth.Activate(tokens.AccessToken, tokens.RefreshToken, tokens.ExpiresAt) |
| |
| if err := uc.authRepo.Save(ctx, auth); err != nil { |
| return nil, err |
| } |
| |
| // Publish event |
| uc.eventBus.Publish(ctx, events.AuthCompleted{ |
| AuthID: auth.ID, |
| Provider: auth.Provider, |
| }) |
| |
| return uc.mapper.ToDTO(auth), nil |
| } |
| |
| // ListAuths retrieves auths with filtering |
| func (uc *AuthUseCase) ListAuths( |
| ctx context.Context, |
| filter dto.AuthFilter, |
| ) ([]*dto.AuthDTO, error) { |
| domainFilter := ports.AuthFilter{} |
| if filter.Provider != "" { |
| p := entities.Provider(filter.Provider) |
| domainFilter.Provider = &p |
| } |
| |
| auths, err := uc.authRepo.List(ctx, domainFilter) |
| if err != nil { |
| return nil, err |
| } |
| |
| return uc.mapper.ToDTOList(auths), nil |
| } |
| |
| // DeleteAuth revokes and removes an auth |
| func (uc *AuthUseCase) DeleteAuth(ctx context.Context, id string) error { |
| authID := entities.AuthID(id) |
| |
| auth, err := uc.authRepo.FindByID(ctx, authID) |
| if err != nil { |
| return err |
| } |
| |
| if auth == nil { |
| return domainerrors.NewNotFound("auth", id) |
| } |
| |
| // Revoke with provider if active |
| if auth.Status == entities.AuthStatusActive { |
| provider, err := uc.providerRepo.GetOAuthProvider(ctx, auth.Provider) |
| if err != nil { |
| return err |
| } |
| |
| if err := provider.RevokeToken(ctx, auth.AccessToken); err != nil { |
| // Log but continue |
| } |
| } |
| |
| auth.Revoke() |
| |
| if err := uc.authRepo.Save(ctx, auth); err != nil { |
| return err |
| } |
| |
| uc.eventBus.Publish(ctx, events.AuthRevoked{ |
| AuthID: auth.ID, |
| Provider: auth.Provider, |
| }) |
| |
| return nil |
| } |
| ``` |
|
|
| ### 2.2 DTOs |
| **File**: `internal/application/dto/auth_dto.go` |
|
|
| ```go |
| package dto |
| |
| import ( |
| "time" |
| ) |
| |
| // AuthDTO represents auth data for API responses |
| type AuthDTO struct { |
| ID string `json:"id"` |
| Provider string `json:"provider"` |
| Status string `json:"status"` |
| ExpiresAt *time.Time `json:"expires_at,omitempty"` |
| CreatedAt time.Time `json:"created_at"` |
| Metadata map[string]string `json:"metadata,omitempty"` |
| } |
| |
| // CreateAuthInput for creating new auth |
| type CreateAuthInput struct { |
| Provider string `json:"provider" validate:"required,oneof=gemini claude codex antigravity qwen"` |
| RedirectURI string `json:"redirect_uri" validate:"required,url"` |
| } |
| |
| func (i CreateAuthInput) Validate() error { |
| return validator.New().Struct(i) |
| } |
| |
| // CompleteAuthInput for completing OAuth |
| type CompleteAuthInput struct { |
| Code string `json:"code" validate:"required"` |
| State string `json:"state" validate:"required"` |
| } |
| |
| func (i CompleteAuthInput) Validate() error { |
| return validator.New().Struct(i) |
| } |
| |
| // AuthFilter for listing auths |
| type AuthFilter struct { |
| Provider string `json:"provider,omitempty"` |
| Status string `json:"status,omitempty"` |
| } |
| ``` |
|
|
| ### 2.3 Mappers |
| **File**: `internal/application/mapper/auth_mapper.go` |
|
|
| ```go |
| package mapper |
| |
| type AuthMapper struct{} |
| |
| func NewAuthMapper() *AuthMapper { |
| return &AuthMapper{} |
| } |
| |
| func (m *AuthMapper) ToDTO(auth *entities.Auth) *dto.AuthDTO { |
| if auth == nil { |
| return nil |
| } |
| |
| return &dto.AuthDTO{ |
| ID: auth.ID.String(), |
| Provider: string(auth.Provider), |
| Status: auth.Status.String(), |
| ExpiresAt: nullableTime(auth.ExpiresAt), |
| CreatedAt: auth.CreatedAt, |
| Metadata: auth.Metadata, |
| } |
| } |
| |
| func (m *AuthMapper) ToDTOList(auths []*entities.Auth) []*dto.AuthDTO { |
| result := make([]*dto.AuthDTO, len(auths)) |
| for i, auth := range auths { |
| result[i] = m.ToDTO(auth) |
| } |
| return result |
| } |
| |
| func nullableTime(t time.Time) *time.Time { |
| if t.IsZero() { |
| return nil |
| } |
| return &t |
| } |
| ``` |
|
|
| ## Phase 3: Infrastructure Layer (Week 3-4) |
|
|
| ### 3.1 PostgreSQL Repository |
| **File**: `internal/infrastructure/persistence/postgres/auth_repository.go` |
|
|
| ```go |
| package postgres |
| |
| import ( |
| "context" |
| "database/sql" |
| "errors" |
| |
| "github.com/jackc/pgx/v5" |
| "github.com/jackc/pgx/v5/pgxpool" |
| |
| "github.com/echyai/cliproxyapi/internal/domain/entities" |
| "github.com/echyai/cliproxyapi/internal/domain/ports" |
| ) |
| |
| type AuthRepository struct { |
| db *pgxpool.Pool |
| } |
| |
| func NewAuthRepository(db *pgxpool.Pool) *AuthRepository { |
| return &AuthRepository{db: db} |
| } |
| |
| func (r *AuthRepository) FindByID(ctx context.Context, id entities.AuthID) (*entities.Auth, error) { |
| query := ` |
| SELECT id, provider, status, access_token, refresh_token, |
| expires_at, created_at, updated_at, metadata |
| FROM auths |
| WHERE id = $1 |
| ` |
| |
| var auth entities.Auth |
| var metadata []byte |
| |
| err := r.db.QueryRow(ctx, query, id).Scan( |
| &auth.ID, |
| &auth.Provider, |
| &auth.Status, |
| &auth.AccessToken, |
| &auth.RefreshToken, |
| &auth.ExpiresAt, |
| &auth.CreatedAt, |
| &auth.UpdatedAt, |
| &metadata, |
| ) |
| |
| if errors.Is(err, pgx.ErrNoRows) { |
| return nil, nil |
| } |
| if err != nil { |
| return nil, err |
| } |
| |
| // Unmarshal metadata JSON |
| if len(metadata) > 0 { |
| json.Unmarshal(metadata, &auth.Metadata) |
| } |
| |
| return &auth, nil |
| } |
| |
| func (r *AuthRepository) Save(ctx context.Context, auth *entities.Auth) error { |
| query := ` |
| INSERT INTO auths (id, provider, status, access_token, refresh_token, |
| expires_at, created_at, updated_at, metadata) |
| VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) |
| ON CONFLICT (id) DO UPDATE SET |
| status = EXCLUDED.status, |
| access_token = EXCLUDED.access_token, |
| refresh_token = EXCLUDED.refresh_token, |
| expires_at = EXCLUDED.expires_at, |
| updated_at = EXCLUDED.updated_at, |
| metadata = EXCLUDED.metadata |
| ` |
| |
| metadata, _ := json.Marshal(auth.Metadata) |
| |
| _, err := r.db.Exec(ctx, query, |
| auth.ID, |
| auth.Provider, |
| auth.Status, |
| auth.AccessToken, |
| auth.RefreshToken, |
| auth.ExpiresAt, |
| auth.CreatedAt, |
| auth.UpdatedAt, |
| metadata, |
| ) |
| |
| return err |
| } |
| |
| // ... other methods |
| ``` |
|
|
| ## Phase 4: Interface Adapters (Week 4-5) |
|
|
| ### 4.1 HTTP Handler (Thin) |
| **File**: `internal/api/handlers/auth_handler.go` |
|
|
| ```go |
| package handlers |
| |
| import ( |
| "net/http" |
| |
| "github.com/gin-gonic/gin" |
| |
| "github.com/echyai/cliproxyapi/internal/application/dto" |
| "github.com/echyai/cliproxyapi/internal/application/usecase" |
| ) |
| |
| type AuthHandler struct { |
| usecase *usecase.AuthUseCase |
| } |
| |
| func NewAuthHandler(uc *usecase.AuthUseCase) *AuthHandler { |
| return &AuthHandler{usecase: uc} |
| } |
| |
| func (h *AuthHandler) Register(r *gin.RouterGroup) { |
| g := r.Group("/auths") |
| { |
| g.GET("", h.List) |
| g.GET("/:id", h.Get) |
| g.POST("", h.Create) |
| g.POST("/callback", h.Complete) |
| g.DELETE("/:id", h.Delete) |
| } |
| } |
| |
| func (h *AuthHandler) Get(c *gin.Context) { |
| id := c.Param("id") |
| |
| auth, err := h.usecase.GetAuth(c.Request.Context(), id) |
| if err != nil { |
| _ = c.Error(err) |
| return |
| } |
| |
| c.JSON(http.StatusOK, Response{Data: auth}) |
| } |
| |
| func (h *AuthHandler) List(c *gin.Context) { |
| filter := dto.AuthFilter{ |
| Provider: c.Query("provider"), |
| Status: c.Query("status"), |
| } |
| |
| auths, err := h.usecase.ListAuths(c.Request.Context(), filter) |
| if err != nil { |
| _ = c.Error(err) |
| return |
| } |
| |
| c.JSON(http.StatusOK, Response{Data: auths}) |
| } |
| |
| func (h *AuthHandler) Create(c *gin.Context) { |
| var input dto.CreateAuthInput |
| if err := c.ShouldBindJSON(&input); err != nil { |
| _ = c.Error(domainerrors.NewValidationError(err.Error())) |
| return |
| } |
| |
| auth, err := h.usecase.CreateAuth(c.Request.Context(), input) |
| if err != nil { |
| _ = c.Error(err) |
| return |
| } |
| |
| c.JSON(http.StatusCreated, Response{Data: auth}) |
| } |
| |
| func (h *AuthHandler) Complete(c *gin.Context) { |
| var input dto.CompleteAuthInput |
| if err := c.ShouldBindJSON(&input); err != nil { |
| _ = c.Error(domainerrors.NewValidationError(err.Error())) |
| return |
| } |
| |
| auth, err := h.usecase.CompleteAuth(c.Request.Context(), input) |
| if err != nil { |
| _ = c.Error(err) |
| return |
| } |
| |
| c.JSON(http.StatusOK, Response{Data: auth}) |
| } |
| |
| func (h *AuthHandler) Delete(c *gin.Context) { |
| id := c.Param("id") |
| |
| if err := h.usecase.DeleteAuth(c.Request.Context(), id); err != nil { |
| _ = c.Error(err) |
| return |
| } |
| |
| c.Status(http.StatusNoContent) |
| } |
| ``` |
|
|
| ### 4.2 Dependency Injection Wire |
| **File**: `internal/wire/wire.go` |
|
|
| ```go |
| //go:build wireinject |
| // +build wireinject |
| |
| package wire |
| |
| import ( |
| "github.com/google/wire" |
| |
| "github.com/echyai/cliproxyapi/internal/api/handlers" |
| "github.com/echyai/cliproxyapi/internal/application/usecase" |
| "github.com/echyai/cliproxyapi/internal/infrastructure/persistence/postgres" |
| ) |
| |
| func InitializeAuthHandler(db *pgxpool.Pool) *handlers.AuthHandler { |
| wire.Build( |
| postgres.NewAuthRepository, |
| postgres.NewProviderRepository, |
| postgres.NewTokenStore, |
| usecase.NewAuthUseCase, |
| handlers.NewAuthHandler, |
| ) |
| return nil |
| } |
| ``` |
|
|
| ## Migration Strategy |
|
|
| 1. **Week 1-2**: Create domain layer (entities, ports) |
| 2. **Week 2-3**: Build application layer (use cases, DTOs) |
| 3. **Week 3-4**: Implement infrastructure (repositories) |
| 4. **Week 4-5**: Create thin HTTP handlers |
| 5. **Week 6**: Replace old handlers gradually |
| 6. **Week 7**: Remove old code |
|
|
| ## Success Criteria |
|
|
| - [ ] Domain logic has no external dependencies |
| - [ ] Handlers are < 50 lines each |
| - [ ] 100% unit test coverage for use cases |
| - [ ] Easy to swap PostgreSQL with other storage |
| - [ ] Clear dependency direction: Handler -> UseCase -> Domain |
|
|