package link import ( "context" "errors" "github.com/jackc/pgtype" "github.com/jackc/pgx/v4" "github.com/jackc/pgx/v4/pgxpool" "net/url" "time" ) type Repository interface { Save(link *Link) error FindById(id string) (*Link, error) FindAll(limit int, offset int) (Links, error) DeleteById(id string) error } type PgRepository struct { pool *pgxpool.Pool } func (r *PgRepository) Save(link *Link) error { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() tx, err := r.pool.Begin(ctx) if err != nil { return err } sql := ` INSERT INTO links (id, name, redirect_url, creation_time) VALUES ($1, $2, $3, $4::timestamp) ` _, err = tx.Exec(ctx, sql, link.Id, link.Name, link.RedirectURL.String(), link.CreationTime.Format("2006-01-02 15:04:05")) if err != nil { _ = tx.Rollback(ctx) return err } if err = tx.Commit(ctx); err != nil { return err } return nil } func (r *PgRepository) FindById(id string) (*Link, error) { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() tx, err := r.pool.Begin(ctx) if err != nil { return nil, err } sql := ` SELECT id, name, redirect_url, creation_time FROM links WHERE id = $1 ` entity, err := mapRowToEntity(tx.QueryRow(ctx, sql, id)) if err != nil { if errors.Is(err, pgx.ErrNoRows) { return nil, nil } else { _ = tx.Rollback(ctx) return nil, err } } if err = tx.Commit(ctx); err != nil { return nil, err } return entity, nil } func (r *PgRepository) FindAll(limit int, offset int) (Links, error) { if limit < 0 { return nil, errors.New("limit can't be negative") } if offset < 0 { return nil, errors.New("offset can't be negative") } ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() tx, err := r.pool.Begin(ctx) if err != nil { return nil, err } sql := ` SELECT id, name, redirect_url, creation_time FROM links LIMIT $1 OFFSET $2 ` rows, err := tx.Query(ctx, sql, limit, offset) if err != nil { _ = tx.Rollback(ctx) return nil, err } defer rows.Close() links := make(Links, 0) for rows.Next() { link, err := mapRowToEntity(rows) if err != nil { _ = tx.Rollback(ctx) return nil, err } links = append(links, link) } if err = rows.Err(); err != nil { _ = tx.Rollback(ctx) return nil, err } if err = tx.Commit(ctx); err != nil { return nil, err } return links, nil } func (r *PgRepository) DeleteById(id string) error { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() tx, err := r.pool.Begin(ctx) if err != nil { return err } sql := ` DELETE FROM links WHERE id = $1 ` _, err = tx.Exec(ctx, sql, id) if err != nil { _ = tx.Rollback(ctx) return err } if err = tx.Commit(ctx); err != nil { return err } return nil } func mapRowToEntity(r interface{}) (*Link, error) { var entity Link var urlStr string var t pgtype.Timestamp switch v := r.(type) { case pgx.Row: if err := v.Scan(&entity.Id, &entity.Name, &urlStr, &t); err != nil { return nil, err } case pgx.Rows: if err := v.Scan(&entity.Id, &entity.Name, &urlStr, &t); err != nil { return nil, err } default: return nil, errors.New("unsupported type") } u, err := url.Parse(urlStr) if err != nil { return nil, err } entity.RedirectURL = *u entity.CreationTime = t.Time return &entity, nil }