package clients import ( "context" "errors" "regexp" "strings" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgxpool" ) var ErrNotFound = errors.New("client not found") type Store struct { db *pgxpool.Pool } func NewStore(db *pgxpool.Pool) Store { return Store{db: db} } func (s Store) List(ctx context.Context) ([]Client, error) { rows, err := s.db.Query(ctx, ` SELECT id::text, name, slug, status, website_url, instagram_url, facebook_url, linkedin_url, color, notes, created_at, updated_at FROM clients ORDER BY name ASC `) if err != nil { return nil, err } defer rows.Close() clients := []Client{} for rows.Next() { client, err := scanClient(rows) if err != nil { return nil, err } clients = append(clients, client) } return clients, rows.Err() } func (s Store) ListForUser(ctx context.Context, userID string) ([]Client, error) { rows, err := s.db.Query(ctx, ` SELECT clients.id::text, clients.name, clients.slug, clients.status, clients.website_url, clients.instagram_url, clients.facebook_url, clients.linkedin_url, clients.color, clients.notes, clients.created_at, clients.updated_at FROM clients INNER JOIN client_users ON client_users.client_id = clients.id WHERE client_users.user_id = $1::uuid ORDER BY clients.name ASC `, userID) if err != nil { return nil, err } defer rows.Close() clients := []Client{} for rows.Next() { client, err := scanClient(rows) if err != nil { return nil, err } clients = append(clients, client) } return clients, rows.Err() } func (s Store) UserCanAccess(ctx context.Context, userID string, clientID string) (bool, error) { var exists bool err := s.db.QueryRow(ctx, ` SELECT EXISTS ( SELECT 1 FROM client_users WHERE user_id = $1::uuid AND client_id = $2::uuid ) `, userID, clientID).Scan(&exists) return exists, err } func (s Store) FindByID(ctx context.Context, id string) (Client, error) { row := s.db.QueryRow(ctx, ` SELECT id::text, name, slug, status, website_url, instagram_url, facebook_url, linkedin_url, color, notes, created_at, updated_at FROM clients WHERE id = $1 `, id) return scanClient(row) } func (s Store) Create(ctx context.Context, name string) (Client, error) { slug := slugify(name) row := s.db.QueryRow(ctx, ` INSERT INTO clients (name, slug, status) VALUES ($1, $2, 'active') RETURNING id::text, name, slug, status, website_url, instagram_url, facebook_url, linkedin_url, color, notes, created_at, updated_at `, strings.TrimSpace(name), slug) return scanClient(row) } func (s Store) Update(ctx context.Context, id string, name string, status string, websiteURL string, instagramURL string, facebookURL string, linkedinURL string, color string, notes string) (Client, error) { row := s.db.QueryRow(ctx, ` UPDATE clients SET name = COALESCE(NULLIF($2, ''), name), slug = CASE WHEN NULLIF($2, '') IS NULL THEN slug ELSE $3 END, status = COALESCE(NULLIF($4, ''), status), website_url = $5, instagram_url = $6, facebook_url = $7, linkedin_url = $8, color = COALESCE(NULLIF($9, ''), color), notes = $10, updated_at = now() WHERE id = $1 RETURNING id::text, name, slug, status, website_url, instagram_url, facebook_url, linkedin_url, color, notes, created_at, updated_at `, id, strings.TrimSpace(name), slugify(name), strings.TrimSpace(status), strings.TrimSpace(websiteURL), strings.TrimSpace(instagramURL), strings.TrimSpace(facebookURL), strings.TrimSpace(linkedinURL), strings.TrimSpace(color), strings.TrimSpace(notes)) return scanClient(row) } func (s Store) Archive(ctx context.Context, id string) (Client, error) { row := s.db.QueryRow(ctx, ` UPDATE clients SET status = 'archived', updated_at = now() WHERE id = $1 RETURNING id::text, name, slug, status, website_url, instagram_url, facebook_url, linkedin_url, color, notes, created_at, updated_at `, id) return scanClient(row) } func (s Store) Delete(ctx context.Context, id string) error { result, err := s.db.Exec(ctx, ` DELETE FROM clients WHERE id = $1 `, id) if err != nil { return err } if result.RowsAffected() == 0 { return ErrNotFound } return nil } type scanner interface { Scan(dest ...any) error } func scanClient(row scanner) (Client, error) { var client Client err := row.Scan( &client.ID, &client.Name, &client.Slug, &client.Status, &client.WebsiteURL, &client.InstagramURL, &client.FacebookURL, &client.LinkedinURL, &client.Color, &client.Notes, &client.CreatedAt, &client.UpdatedAt, ) if errors.Is(err, pgx.ErrNoRows) { return Client{}, ErrNotFound } if err != nil { return Client{}, err } return client, nil } func slugify(value string) string { value = strings.ToLower(strings.TrimSpace(value)) replacements := map[string]string{ "á": "a", "à": "a", "â": "a", "ã": "a", "ä": "a", "é": "e", "è": "e", "ê": "e", "ë": "e", "í": "i", "ì": "i", "î": "i", "ï": "i", "ó": "o", "ò": "o", "ô": "o", "õ": "o", "ö": "o", "ú": "u", "ù": "u", "û": "u", "ü": "u", "ç": "c", } for from, to := range replacements { value = strings.ReplaceAll(value, from, to) } value = regexp.MustCompile(`[^a-z0-9]+`).ReplaceAllString(value, "-") value = strings.Trim(value, "-") if value == "" { return "cliente" } return value }