// conversion_adapters.go — main.go 的 narrow adapter 實作。 // // internal/conversion 為了避免 import cycle 與保持 interface 純粹(FAANG 慣例: // consumer 定義介面),定義了 ModelStore / Storage 兩個 narrow interface。 // main.go 在 wire 時把 *model.InMemoryRepository / *storage.LocalFSStore 包成 adapter // 注入;conversion 完全不知道具體實作。 // // 對齊 .autoflow/04-architecture/conversion.md §2.7(NewService 註解)。 // // Phase 0.8 conversion (見 .autoflow/04-architecture/conversion.md §2.7) package main import ( "context" "fmt" "io" "time" "github.com/google/uuid" "visiona-backend/internal/conversion" "visiona-backend/internal/model" "visiona-backend/internal/storage" ) // ========================================================================== // ModelStore adapter // ========================================================================== // conversionModelStoreAdapter 把 model.Repository 包成 conversion.ModelStore。 // // 額外責任: // - ModelRecord ↔ model.Model 雙向轉換 // - FindBySourceJobID 用 List(filter) + 自行過濾 SourceJobID(既有 model.ListFilter // 沒有 SourceJobID 欄位;Phase 1 加 DB 後可改 indexed query) // - GenerateID 用 uuid.NewString(與 internal/api/models.go modelsInitUploadHandler 一致) type conversionModelStoreAdapter struct { repo model.Repository } // newConversionModelStoreAdapter 建立 adapter。 func newConversionModelStoreAdapter(repo model.Repository) conversion.ModelStore { return &conversionModelStoreAdapter{repo: repo} } // Save 把 conversion.ModelRecord 轉成 model.Model 後 upsert。 // // 設計選擇:UploadedAt 設為 now(promote 完成 = 等同 finalize 後的 ready 狀態), // 這樣 GET /api/models/{id} 回的 status 會是 "ready"(對齊 toModelResponse 邏輯)。 func (a *conversionModelStoreAdapter) Save(ctx context.Context, rec *conversion.ModelRecord) error { if rec == nil { return fmt.Errorf("conversion adapter: Save requires non-nil record") } now := time.Now().UTC() uploadedAt := now if !rec.UpdatedAt.IsZero() { uploadedAt = rec.UpdatedAt } m := &model.Model{ ID: rec.ID, OwnerUserID: rec.OwnerUserID, Name: rec.Name, Description: rec.Description, StorageKey: rec.StorageKey, FileSize: rec.FileSize, FileChecksum: rec.FileChecksum, TargetChip: rec.TargetChip, Source: rec.Source, // 應為 "converted" SourceJobID: rec.SourceJobID, CreatedAt: rec.CreatedAt, UpdatedAt: rec.UpdatedAt, UploadedAt: &uploadedAt, // promote 完即 ready(對齊 toModelResponse) } return a.repo.Save(ctx, m) } // FindBySourceJobID 找 user 是否已對某 job 建過 model record(冪等檢查用)。 // // Phase 0.8 雛形實作:用 List(filter) + 過濾 SourceJobID(in-memory 慢但對小量 user 足夠)。 // Phase 1 用 SQL `WHERE owner_user_id = ? AND source_job_id = ?` 加索引。 // // 找不到回 (nil, nil);找到第一個 match 回 (*ModelRecord, nil)。 func (a *conversionModelStoreAdapter) FindBySourceJobID(ctx context.Context, ownerUserID, sourceJobID string) (*conversion.ModelRecord, error) { if ownerUserID == "" || sourceJobID == "" { return nil, nil } models, err := a.repo.List(ctx, model.ListFilter{ OwnerUserID: ownerUserID, Source: model.SourceConverted, }) if err != nil { return nil, fmt.Errorf("conversion adapter: list models for FindBySourceJobID: %w", err) } for _, m := range models { if m.SourceJobID == sourceJobID { return modelToRecord(m), nil } } return nil, nil } // GenerateID 產一個新 model_id(沿用既有 visionA model 命名 — uuid.NewString)。 func (a *conversionModelStoreAdapter) GenerateID() string { return uuid.NewString() } // modelToRecord 把 *model.Model 轉成 *conversion.ModelRecord(給 PromoteToModels 冪等回傳用)。 func modelToRecord(m *model.Model) *conversion.ModelRecord { if m == nil { return nil } return &conversion.ModelRecord{ ID: m.ID, OwnerUserID: m.OwnerUserID, Name: m.Name, Description: m.Description, StorageKey: m.StorageKey, FileSize: m.FileSize, FileChecksum: m.FileChecksum, TargetChip: m.TargetChip, Source: m.Source, SourceJobID: m.SourceJobID, CreatedAt: m.CreatedAt, UpdatedAt: m.UpdatedAt, } } // ========================================================================== // Storage adapter // ========================================================================== // conversionStorageAdapter 把 storage.Store 包成 conversion.Storage。 // // 目前只需要 Put(streaming 寫入),meta 透傳到底層 storage(LocalFS 雛形可能忽略, // S3 接上後會寫進 object metadata)。 type conversionStorageAdapter struct { store storage.Store } // newConversionStorageAdapter 建立 adapter。 func newConversionStorageAdapter(store storage.Store) conversion.Storage { return &conversionStorageAdapter{store: store} } // Put streaming 寫入 storage(meta 透傳給底層 storage 實作)。 // // LocalFS 雛形可能忽略 meta;S3 / R2 等 backend 會寫進 object metadata(給 debug / Tagging)。 func (a *conversionStorageAdapter) Put(ctx context.Context, key string, r io.Reader, size int64, meta map[string]string) error { return a.store.Put(ctx, key, r, size, meta) }