package network import ( "context" "errors" "fmt" "github.com/flowy-live/llink/internal/utils" "github.com/jackc/pgx/v5/pgxpool" ) type Reader interface { // GetByID returns ErrNotFound if network doesn't exist. GetByID(ctx context.Context, id string) (*Network, error) // ListForHuman returns ErrInvalidHumanId if humanId is empty. ListForHuman(ctx context.Context, humanId string) ([]*Network, error) // IsMember returns ErrInvalidHumanId if humanId is empty. IsMember(ctx context.Context, networkID, humanId string) (bool, error) // ListMembers returns an empty slice if the network doesn't exist. ListMembers(ctx context.Context, networkID string) ([]string, error) ListAll(ctx context.Context) ([]*Network, error) // ListAllMemberships returns humanId -> networkIds for every human with at // least one membership. Humans with zero memberships are absent from the map. ListAllMemberships(ctx context.Context) (map[string][]string, error) CountSeats(ctx context.Context, networkID string) (int, error) // ListInvitationsForEmail returns ErrInvalidEmail if normalization fails. ListInvitationsForEmail(ctx context.Context, email string) ([]*Invitation, error) ListInvitationsForNetwork(ctx context.Context, networkID string) ([]*Invitation, error) } type readerImpl struct { pool *pgxpool.Pool repo repository } // newReader exposes the concrete type so the service can embed it without // hiding pool/repo behind the Reader interface. func newReader(pool *pgxpool.Pool) *readerImpl { return &readerImpl{ pool: pool, repo: newRepository(pool), } } func NewReader(pool *pgxpool.Pool) Reader { return newReader(pool) } func (r *readerImpl) GetByID(ctx context.Context, id string) (*Network, error) { n, err := r.repo.getByID(ctx, id) if errors.Is(err, errNotFound) { return nil, ErrNotFound } return n, err } func (r *readerImpl) ListForHuman(ctx context.Context, humanId string) ([]*Network, error) { if humanId == "" { return nil, ErrInvalidHumanId } return r.repo.getNetworksForHuman(ctx, humanId) } func (r *readerImpl) IsMember(ctx context.Context, networkID, humanId string) (bool, error) { if humanId == "" { return false, ErrInvalidHumanId } return r.repo.isMember(ctx, networkID, humanId) } func (r *readerImpl) ListMembers(ctx context.Context, networkID string) ([]string, error) { // Admin is guaranteed to be in network_members: Create() calls AddMembers // for the admin, and RemoveMemberFromNetwork rejects admin removal. return r.repo.getMemberHumanIds(ctx, networkID) } func (r *readerImpl) ListAll(ctx context.Context) ([]*Network, error) { return r.repo.listAll(ctx) } func (r *readerImpl) ListAllMemberships(ctx context.Context) (map[string][]string, error) { return r.repo.listAllMemberships(ctx) } func (r *readerImpl) CountSeats(ctx context.Context, networkID string) (int, error) { return r.repo.countSeats(ctx, r.pool, networkID) } func (r *readerImpl) ListInvitationsForEmail(ctx context.Context, email string) ([]*Invitation, error) { normalized, err := utils.NormalizeEmail(email) if err != nil { return nil, fmt.Errorf("%w: %w", ErrInvalidEmail, err) } return r.repo.getInvitationsByEmail(ctx, normalized) } func (r *readerImpl) ListInvitationsForNetwork(ctx context.Context, networkID string) ([]*Invitation, error) { return r.repo.getInvitationsByNetwork(ctx, networkID) }