package servicecatalog import ( "context" "errors" "github.com/google/uuid" "github.com/jackc/pgx/v5/pgxpool" ) type Repository interface { List(ctx context.Context) ([]Service, error) Create(ctx context.Context, input CreateRequest) (Service, error) Update(ctx context.Context, serviceID uuid.UUID, input UpdateRequest) (Service, error) Delete(ctx context.Context, serviceID uuid.UUID) error ByIDs(ctx context.Context, ids []uuid.UUID) ([]Service, error) } type PGRepository struct { db *pgxpool.Pool } func NewPGRepository(db *pgxpool.Pool) *PGRepository { return &PGRepository{db: db} } func (r *PGRepository) List(ctx context.Context) ([]Service, error) { rows, err := r.db.Query(ctx, ` select id, name, description, domain, host(upstream_ip), host(proxy_ip), ports, is_active from services where deleted_at is null order by name asc `) if err != nil { return nil, err } defer rows.Close() var items []Service for rows.Next() { var item Service if err := rows.Scan(&item.ID, &item.Name, &item.Description, &item.Domain, &item.UpstreamIP, &item.ProxyIP, &item.Ports, &item.IsActive); err != nil { return nil, err } items = append(items, item) } return items, rows.Err() } func (r *PGRepository) Create(ctx context.Context, input CreateRequest) (Service, error) { row := r.db.QueryRow(ctx, ` insert into services (id, name, description, domain, upstream_ip, proxy_ip, ports, is_active) values ($1, $2, $3, $4, $5::inet, $6::inet, $7::integer[], coalesce($8, true)) returning id, name, description, domain, host(upstream_ip), host(proxy_ip), ports, is_active `, uuid.New(), input.Name, input.Description, input.Domain, input.UpstreamIP, input.ProxyIP, normalizePorts(input.Ports), input.IsActive) var item Service err := row.Scan(&item.ID, &item.Name, &item.Description, &item.Domain, &item.UpstreamIP, &item.ProxyIP, &item.Ports, &item.IsActive) return item, err } func (r *PGRepository) Update(ctx context.Context, serviceID uuid.UUID, input UpdateRequest) (Service, error) { var ports *[]int if input.Ports != nil { normalized := normalizePorts(input.Ports) ports = &normalized } row := r.db.QueryRow(ctx, ` update services set name = coalesce($2, name), description = coalesce($3, description), domain = coalesce($4, domain), upstream_ip = coalesce($5::inet, upstream_ip), proxy_ip = coalesce($6::inet, proxy_ip), ports = coalesce($7::integer[], ports), is_active = coalesce($8, is_active), updated_at = now() where id = $1 and deleted_at is null returning id, name, description, domain, host(upstream_ip), host(proxy_ip), ports, is_active `, serviceID, input.Name, input.Description, input.Domain, input.UpstreamIP, input.ProxyIP, ports, input.IsActive) var item Service err := row.Scan(&item.ID, &item.Name, &item.Description, &item.Domain, &item.UpstreamIP, &item.ProxyIP, &item.Ports, &item.IsActive) return item, err } func (r *PGRepository) Delete(ctx context.Context, serviceID uuid.UUID) error { _, err := r.db.Exec(ctx, ` update services set deleted_at = now(), updated_at = now() where id = $1 and deleted_at is null `, serviceID) return err } func (r *PGRepository) ByIDs(ctx context.Context, ids []uuid.UUID) ([]Service, error) { if len(ids) == 0 { return nil, nil } rows, err := r.db.Query(ctx, ` select id, name, description, domain, host(upstream_ip), host(proxy_ip), ports, is_active from services where deleted_at is null and id = any($1::uuid[]) order by name asc `, ids) if err != nil { return nil, err } defer rows.Close() var items []Service for rows.Next() { var item Service if err := rows.Scan(&item.ID, &item.Name, &item.Description, &item.Domain, &item.UpstreamIP, &item.ProxyIP, &item.Ports, &item.IsActive); err != nil { return nil, err } items = append(items, item) } if err := rows.Err(); err != nil { return nil, err } if len(items) == 0 { return nil, errors.New("services not found") } return items, nil } func normalizePorts(ports []int) []int { if len(ports) == 0 { return []int{80, 443} } seen := make(map[int]struct{}, len(ports)) result := make([]int, 0, len(ports)) for _, port := range ports { if port <= 0 { continue } if _, ok := seen[port]; ok { continue } seen[port] = struct{}{} result = append(result, port) } if len(result) == 0 { return []int{80, 443} } return result }