package store import ( "database/sql" "errors" "fmt" "github.com/google/uuid" ) // CreateProject inserts a new project and returns it. func (s *Store) CreateProject(p Project) (Project, error) { p.ID = uuid.New().String() p.CreatedAt = Now() p.UpdatedAt = p.CreatedAt _, err := s.db.Exec( `INSERT INTO projects (id, name, registry, image, port, healthcheck, env, volumes, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, p.ID, p.Name, p.Registry, p.Image, p.Port, p.Healthcheck, p.Env, p.Volumes, p.CreatedAt, p.UpdatedAt, ) if err != nil { return Project{}, fmt.Errorf("insert project: %w", err) } return p, nil } // GetProjectByID returns a single project by its ID. func (s *Store) GetProjectByID(id string) (Project, error) { var p Project err := s.db.QueryRow( `SELECT id, name, registry, image, port, healthcheck, env, volumes, created_at, updated_at FROM projects WHERE id = ?`, id, ).Scan(&p.ID, &p.Name, &p.Registry, &p.Image, &p.Port, &p.Healthcheck, &p.Env, &p.Volumes, &p.CreatedAt, &p.UpdatedAt) if errors.Is(err, sql.ErrNoRows) { return Project{}, fmt.Errorf("project %s: %w", id, ErrNotFound) } if err != nil { return Project{}, fmt.Errorf("query project: %w", err) } return p, nil } // GetAllProjects returns every project ordered by name. func (s *Store) GetAllProjects() ([]Project, error) { rows, err := s.db.Query( `SELECT id, name, registry, image, port, healthcheck, env, volumes, created_at, updated_at FROM projects ORDER BY name`, ) if err != nil { return nil, fmt.Errorf("query projects: %w", err) } defer rows.Close() projects := []Project{} for rows.Next() { var p Project if err := rows.Scan(&p.ID, &p.Name, &p.Registry, &p.Image, &p.Port, &p.Healthcheck, &p.Env, &p.Volumes, &p.CreatedAt, &p.UpdatedAt); err != nil { return nil, fmt.Errorf("scan project: %w", err) } projects = append(projects, p) } return projects, rows.Err() } // GetProjectsByImage returns all projects using the given image, newest first. func (s *Store) GetProjectsByImage(image string) ([]Project, error) { rows, err := s.db.Query( `SELECT id, name, registry, image, port, healthcheck, env, volumes, created_at, updated_at FROM projects WHERE image = ? ORDER BY created_at DESC`, image, ) if err != nil { return nil, fmt.Errorf("query projects by image: %w", err) } defer rows.Close() projects := []Project{} for rows.Next() { var p Project if err := rows.Scan(&p.ID, &p.Name, &p.Registry, &p.Image, &p.Port, &p.Healthcheck, &p.Env, &p.Volumes, &p.CreatedAt, &p.UpdatedAt); err != nil { return nil, fmt.Errorf("scan project: %w", err) } projects = append(projects, p) } return projects, rows.Err() } // UpdateProject updates an existing project's mutable fields. func (s *Store) UpdateProject(p Project) error { p.UpdatedAt = Now() result, err := s.db.Exec( `UPDATE projects SET name=?, registry=?, image=?, port=?, healthcheck=?, env=?, volumes=?, updated_at=? WHERE id=?`, p.Name, p.Registry, p.Image, p.Port, p.Healthcheck, p.Env, p.Volumes, p.UpdatedAt, p.ID, ) if err != nil { return fmt.Errorf("update project: %w", err) } n, _ := result.RowsAffected() if n == 0 { return fmt.Errorf("project %s: %w", p.ID, ErrNotFound) } return nil } // DeleteProject removes a project by ID. Cascading deletes handle stages, instances, and deploys. func (s *Store) DeleteProject(id string) error { result, err := s.db.Exec(`DELETE FROM projects WHERE id = ?`, id) if err != nil { return fmt.Errorf("delete project: %w", err) } n, _ := result.RowsAffected() if n == 0 { return fmt.Errorf("project %s: %w", id, ErrNotFound) } return nil }