package ports import ( "context" "fmt" "strings" "time" "galaxy/gamemaster/internal/domain/engineversion" ) //go:generate go run go.uber.org/mock/mockgen -destination=../adapters/mocks/mock_engineversionstore.go -package=mocks galaxy/gamemaster/internal/ports EngineVersionStore // EngineVersionStore stores the engine version registry rows used by // Game Lobby's start flow and by GM's admin patch and registry CRUD // surface. Adapters must preserve domain semantics: // // - Get returns engineversion.ErrNotFound when no row exists for // version. // - List with a nil status filter returns every row; with a non-nil // filter, only rows whose status matches are returned. // - Insert installs a fresh row and returns engineversion.ErrConflict // when a row with the same `version` already exists. Adapters // surface PostgreSQL unique violations through that sentinel so // the service layer maps them to a `conflict` REST envelope. // - Update applies a partial update; only fields whose pointer is // non-nil are mutated. The `updated_at` column is always refreshed // from input.Now. // - Deprecate sets `status=deprecated` for an existing version with // `updated_at = now`. It returns engineversion.ErrNotFound when no // row exists. The call is idempotent: deprecating an already // deprecated row succeeds with no further mutation. // - Delete removes the row identified by version. Returns // engineversion.ErrNotFound when no row matches. The service layer // gates Delete behind an explicit IsReferencedByActiveRuntime probe // so referenced rows surface engineversion.ErrInUse before the // adapter is touched; adapters do not enforce that guard themselves. // - IsReferencedByActiveRuntime reports whether any non-finished // `runtime_records` row currently references the version through // `current_engine_version`. type EngineVersionStore interface { // Get returns the row identified by version. Returns // engineversion.ErrNotFound when no row exists. Get(ctx context.Context, version string) (engineversion.EngineVersion, error) // List returns every row whose status matches statusFilter when // non-nil, or every row when nil. The order is adapter-defined. List(ctx context.Context, statusFilter *engineversion.Status) ([]engineversion.EngineVersion, error) // Insert installs record into the registry. Insert(ctx context.Context, record engineversion.EngineVersion) error // Update applies a partial update to the row identified by // input.Version. Only fields whose pointer is non-nil are mutated. // Returns engineversion.ErrNotFound when no row exists. Update(ctx context.Context, input UpdateEngineVersionInput) error // Deprecate sets `status=deprecated` for version and refreshes // `updated_at` from now. Returns engineversion.ErrNotFound when no // row exists. Calling Deprecate on an already-deprecated row // succeeds with no mutation (idempotent). Deprecate(ctx context.Context, version string, now time.Time) error // Delete removes the row identified by version. Returns // engineversion.ErrNotFound when no row matches. Adapters do not // inspect runtime references; the service layer probes // IsReferencedByActiveRuntime first and surfaces // engineversion.ErrInUse independently. Delete(ctx context.Context, version string) error // IsReferencedByActiveRuntime reports whether any non-finished // runtime row currently references version through // `current_engine_version`. Used by the registry hard-delete path // to surface engineversion.ErrInUse. IsReferencedByActiveRuntime(ctx context.Context, version string) (bool, error) } // UpdateEngineVersionInput stores the arguments required to PATCH one // engine version row. Pointer fields communicate «leave alone» (nil) // vs. «write the value» (non-nil). At least one optional field must be // set; otherwise the call is a no-op and Validate rejects it. type UpdateEngineVersionInput struct { // Version identifies the row to mutate. Version string // ImageRef is the new image reference. Nil leaves the column // unchanged; non-nil must be non-empty. ImageRef *string // Options is the new options document (raw JSON). Nil leaves the // column unchanged; non-nil writes the value verbatim. Options *[]byte // Status is the new status. Nil leaves the column unchanged; // non-nil must be a known status. Status *engineversion.Status // Now stores the wall-clock used to refresh the `updated_at` // column on every successful update. Now time.Time } // Validate reports whether input contains a structurally valid PATCH // request. Adapters call Validate before touching the store. func (input UpdateEngineVersionInput) Validate() error { if strings.TrimSpace(input.Version) == "" { return fmt.Errorf("update engine version: version must not be empty") } if input.ImageRef == nil && input.Options == nil && input.Status == nil { return fmt.Errorf("update engine version: at least one field must be set") } if input.ImageRef != nil && strings.TrimSpace(*input.ImageRef) == "" { return fmt.Errorf( "update engine version: image ref must not be empty when set", ) } if input.Status != nil && !input.Status.IsKnown() { return fmt.Errorf( "update engine version: status %q is unsupported", *input.Status, ) } if input.Now.IsZero() { return fmt.Errorf("update engine version: now must not be zero") } return nil }