package datastore import ( "database/sql" "errors" "fmt" "git.sa.vin/any-remark/backend/types" "math/rand" "strings" ) type Mapper interface { GetOpenGraphData(url string) (string, error) SaveOpenGraphData(url, opengraph string) error AddComment(req types.AddCommentRequest) error GetComments(url string, orderBy string) ([]types.Comment, error) AddVote(commentID, userID, voteType string) error GetWebpageIDFromCommentID(commentID string) (string, error) } type mapper struct { db *sql.DB } func NewMapper(db *sql.DB) Mapper { // Empty function for NewMapper return &mapper{ db: db, } } func (m *mapper) GetOpenGraphData(url string) (string, error) { // get the opengraph data from the db table named "webpages" using the url query := `SELECT opengraph FROM webpages WHERE url = ?` rows, err := m.db.Query(query, url) if err != nil { if err == sql.ErrNoRows { return "", fmt.Errorf("no data found in db for url: %s", url) } return "", fmt.Errorf("error getting webpage data from db: %s", err) } defer rows.Close() // get the opengraph data from the rows array var opengraph string for rows.Next() { err = rows.Scan(&opengraph) if err != nil { return "", fmt.Errorf("error scanning opengraph data: %s", err) } return opengraph, nil } return "", fmt.Errorf("no data found in db for url: %s", url) } // SaveOpenGraphData saves the opengraph data to the db func (m *mapper) SaveOpenGraphData(url, opengraph string) error { // generate a unique id for the webpage make it 24 bytes long id := "wp_" + generateID(24) // insert the opengraph data to the db table named "webpages" using the url query := `INSERT INTO webpages (id, url, opengraph) VALUES (?, ?, ?)` _, err := m.db.Exec(query, id, url, opengraph) if err != nil { return fmt.Errorf("error inserting webpage data to db: %s", err) } return nil } func (m *mapper) GetWebpageID(url string) (string, error) { // get the id of the webpage from the db table named "webpages" using the url query := `SELECT id FROM webpages WHERE url = ?` rows, err := m.db.Query(query, url) if err != nil { if err == sql.ErrNoRows { return "", fmt.Errorf("no data found in db for url: %s", url) } return "", fmt.Errorf("error getting webpage data from db: %s", err) } defer rows.Close() // get the id from the rows array var id string for rows.Next() { err = rows.Scan(&id) if err != nil { return "", fmt.Errorf("error scanning webpage id: %s", err) } return id, nil } return "", fmt.Errorf("no data found in db for url: %s", url) } func (m *mapper) AddComment(req types.AddCommentRequest) error { webpageID, err := m.GetWebpageID(req.URL) if err != nil { return fmt.Errorf("error getting webpage id: %s", err) } // generate a unique id for the comment make it 24 bytes long id := "c_" + generateID(24) // insert the comment data to the db table named "comments" using the url query := `INSERT INTO comments (id, content, webpage_id, commenter) VALUES (?, ?, ?, ?)` _, err = m.db.Exec(query, id, req.Comment, webpageID, req.UserID) if err != nil { return fmt.Errorf("error inserting comment data to db: %s", err) } return nil } func (m *mapper) GetComments(url string, orderBy string) ([]types.Comment, error) { webpageID, err := m.GetWebpageID(url) if err != nil { return nil, fmt.Errorf("error getting webpage id: %s", err) } // TODO: provide order by recent or most upvoted // TODO: bubble up the current user's comments // get the comments from the db table named "comments" using the webpage_id query := `SELECT c.id, c.content, c.commenter, u.username, u.avatar, c.created_at, COUNT(CASE WHEN v.vote_type = 'up' THEN 1 END) AS upvotes, COUNT(CASE WHEN v.vote_type = 'down' THEN 1 END) AS downvotes FROM comments c JOIN users u ON c.commenter = u.id LEFT JOIN votes v ON c.id = v.comment_id WHERE c.webpage_id = ? GROUP BY c.id, c.content, c.commenter, u.username, u.avatar` if orderBy == "upvotes" { query += " ORDER BY upvotes DESC" } else { query += " ORDER BY c.created_at DESC" } rows, err := m.db.Query(query, webpageID) if err != nil { if errors.Is(err, sql.ErrNoRows) { return nil, fmt.Errorf("no data found in db for url: %s", url) } return nil, fmt.Errorf("error getting comments from db: %s", err) } defer rows.Close() // get the comments from the rows array var comments []types.Comment for rows.Next() { var comment types.Comment err = rows.Scan(&comment.ID, &comment.Content, &comment.Commenter, &comment.Username, &comment.Avatar, &comment.CreatedAt, &comment.Upvotes, &comment.Downvotes) if err != nil { return nil, fmt.Errorf("error scanning comments: %s", err) } comments = append(comments, comment) } return comments, nil } func (m *mapper) AddVote(commentID, userID, voteType string) error { voteType = strings.ToLower(voteType) if voteType != "up" && voteType != "down" { return fmt.Errorf("invalid vote type: %s", voteType) } id := "v_" + generateID(24) var err error webpageID := "" if strings.HasPrefix(commentID, "wp_") { webpageID = commentID commentID = "" } if webpageID == "" { webpageID, err = m.GetWebpageIDFromCommentID(commentID) if err != nil { return fmt.Errorf("error getting webpage id from comment id: %s", err) } } // insert the vote data to the db table named "votes" using the comment_id query := `INSERT INTO votes (id, webpage_id, comment_id, voter, vote_type) VALUES (?, ?, ?, ?, ?)` _, err = m.db.Exec(query, id, webpageID, commentID, userID, voteType) if err != nil { // if the vote already exists, update the vote if !strings.Contains(err.Error(), "UNIQUE constraint") { return fmt.Errorf("error inserting vote data to db: %s", err) } query = `UPDATE votes SET vote_type = ? WHERE voter = ? AND comment_id = ? AND webpage_id = ?` _, err = m.db.Exec(query, voteType, userID, commentID, webpageID) if err != nil { return fmt.Errorf("error updating vote data in db: %s", err) } } return nil } func (m *mapper) GetWebpageIDFromCommentID(commentID string) (string, error) { // get the id of the webpage from the db table named "comments" using the comment_id query := `SELECT webpage_id FROM comments WHERE id = ?` rows, err := m.db.Query(query, commentID) if err != nil { if err == sql.ErrNoRows { return "", fmt.Errorf("no data found in db for comment_id: %s", commentID) } return "", fmt.Errorf("error getting webpage id from db: %s", err) } defer rows.Close() // get the id from the rows array var webpageID string for rows.Next() { err = rows.Scan(&webpageID) if err != nil { return "", fmt.Errorf("error scanning webpage id: %s", err) } return webpageID, nil } return "", fmt.Errorf("no data found in db for comment_id: %s", commentID) } func generateID(n int) string { // generate a unique id for the webpage make it 24 bytes // long using the following character set charSet := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" charSetLen := len(charSet) id := make([]byte, n) for i := range id { // choose a random character from the character set id[i] = charSet[rand.Intn(charSetLen)] } return string(id) }