package postgres import ( "context" "database/sql" "encoding/json" "fmt" "github.com/orchard9/rdev/internal/domain" "github.com/orchard9/rdev/internal/port" ) // BlueprintRepository implements port.BlueprintRepository using PostgreSQL. type BlueprintRepository struct { db *sql.DB } // NewBlueprintRepository creates a new PostgreSQL blueprint repository. func NewBlueprintRepository(db *sql.DB) *BlueprintRepository { return &BlueprintRepository{db: db} } // Ensure BlueprintRepository implements port.BlueprintRepository at compile time. var _ port.BlueprintRepository = (*BlueprintRepository)(nil) // CreateBlueprint creates a new blueprint. func (r *BlueprintRepository) CreateBlueprint(ctx context.Context, blueprint *domain.Blueprint) error { specJSON, err := json.Marshal(blueprint.Spec) if err != nil { return fmt.Errorf("marshal spec: %w", err) } err = r.db.QueryRowContext(ctx, ` INSERT INTO blueprints (project_id, name, description, spec) VALUES ($1, $2, $3, $4) RETURNING id, created_at, updated_at `, blueprint.ProjectID, blueprint.Name, blueprint.Description, specJSON).Scan( &blueprint.ID, &blueprint.CreatedAt, &blueprint.UpdatedAt, ) if err != nil { return fmt.Errorf("create blueprint: %w", err) } return nil } // GetBlueprint retrieves a blueprint by ID. func (r *BlueprintRepository) GetBlueprint(ctx context.Context, id domain.BlueprintID) (*domain.Blueprint, error) { var blueprint domain.Blueprint var specJSON []byte var description sql.NullString err := r.db.QueryRowContext(ctx, ` SELECT id, project_id, name, description, spec, created_at, updated_at FROM blueprints WHERE id = $1 `, id).Scan( &blueprint.ID, &blueprint.ProjectID, &blueprint.Name, &description, &specJSON, &blueprint.CreatedAt, &blueprint.UpdatedAt, ) if err == sql.ErrNoRows { return nil, domain.ErrBlueprintNotFound } if err != nil { return nil, fmt.Errorf("get blueprint: %w", err) } if description.Valid { blueprint.Description = description.String } if err := json.Unmarshal(specJSON, &blueprint.Spec); err != nil { return nil, fmt.Errorf("unmarshal spec: %w", err) } return &blueprint, nil } // ListBlueprints returns all blueprints for a project. func (r *BlueprintRepository) ListBlueprints(ctx context.Context, projectID string) ([]*domain.Blueprint, error) { rows, err := r.db.QueryContext(ctx, ` SELECT id, project_id, name, description, spec, created_at, updated_at FROM blueprints WHERE project_id = $1 ORDER BY created_at DESC `, projectID) if err != nil { return nil, fmt.Errorf("list blueprints: %w", err) } defer rows.Close() var blueprints []*domain.Blueprint for rows.Next() { var blueprint domain.Blueprint var specJSON []byte var description sql.NullString if err := rows.Scan( &blueprint.ID, &blueprint.ProjectID, &blueprint.Name, &description, &specJSON, &blueprint.CreatedAt, &blueprint.UpdatedAt, ); err != nil { return nil, fmt.Errorf("scan blueprint: %w", err) } if description.Valid { blueprint.Description = description.String } if err := json.Unmarshal(specJSON, &blueprint.Spec); err != nil { return nil, fmt.Errorf("unmarshal spec: %w", err) } blueprints = append(blueprints, &blueprint) } return blueprints, rows.Err() } // UpdateBlueprint updates a blueprint's metadata and spec. func (r *BlueprintRepository) UpdateBlueprint(ctx context.Context, blueprint *domain.Blueprint) error { specJSON, err := json.Marshal(blueprint.Spec) if err != nil { return fmt.Errorf("marshal spec: %w", err) } result, err := r.db.ExecContext(ctx, ` UPDATE blueprints SET name = $1, description = $2, spec = $3 WHERE id = $4 `, blueprint.Name, blueprint.Description, specJSON, blueprint.ID) if err != nil { return fmt.Errorf("update blueprint: %w", err) } rows, err := result.RowsAffected() if err != nil { return fmt.Errorf("rows affected: %w", err) } if rows == 0 { return domain.ErrBlueprintNotFound } return nil } // DeleteBlueprint deletes a blueprint. func (r *BlueprintRepository) DeleteBlueprint(ctx context.Context, id domain.BlueprintID) error { result, err := r.db.ExecContext(ctx, ` DELETE FROM blueprints WHERE id = $1 `, id) if err != nil { return fmt.Errorf("delete blueprint: %w", err) } rows, err := result.RowsAffected() if err != nil { return fmt.Errorf("rows affected: %w", err) } if rows == 0 { return domain.ErrBlueprintNotFound } return nil }