137 lines
2.8 KiB
Go
137 lines
2.8 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"encoding/json"
|
|
"time"
|
|
)
|
|
|
|
const migration = `
|
|
CREATE TABLE IF NOT EXISTS posts (
|
|
id serial PRIMARY KEY,
|
|
author VARCHAR NOT NULL,
|
|
content TEXT NOT NULL,
|
|
views INT NOT NULL DEFAULT 0,
|
|
reactions JSONB NOT NULL DEFAULT '{}',
|
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
|
|
INSERT INTO posts (id, author, content) VALUES
|
|
(1, 'Miłosz', 'Oh, I remember the days when we used to write code in PHP!'),
|
|
(2, 'Robert', 'Back in my days, we used to write code in assembly!')
|
|
ON CONFLICT (id) DO NOTHING;
|
|
`
|
|
|
|
func MigrateDB(db *sql.DB) error {
|
|
_, err := db.Exec(migration)
|
|
return err
|
|
}
|
|
|
|
type Repository struct {
|
|
db *sql.DB
|
|
}
|
|
|
|
func NewRepository(db *sql.DB) *Repository {
|
|
return &Repository{
|
|
db: db,
|
|
}
|
|
}
|
|
|
|
func (s *Repository) PostByID(ctx context.Context, id int) (Post, error) {
|
|
row := s.db.QueryRowContext(ctx, `SELECT id, author, content, views, reactions, created_at FROM posts WHERE id = $1`, id)
|
|
post, err := scanPost(row)
|
|
if err != nil {
|
|
return Post{}, err
|
|
}
|
|
|
|
return post, nil
|
|
}
|
|
|
|
func (s *Repository) AllPosts(ctx context.Context) ([]Post, error) {
|
|
rows, err := s.db.QueryContext(ctx, `SELECT id, author, content, views, reactions, created_at FROM posts ORDER BY id ASC`)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var posts []Post
|
|
for rows.Next() {
|
|
post, err := scanPost(rows)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
posts = append(posts, post)
|
|
}
|
|
|
|
return posts, nil
|
|
}
|
|
|
|
func (s *Repository) UpdatePost(ctx context.Context, id int, updateFn func(post *Post)) (err error) {
|
|
tx, err := s.db.BeginTx(ctx, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
defer func() {
|
|
if err == nil {
|
|
err = tx.Commit()
|
|
} else {
|
|
txErr := tx.Rollback()
|
|
if txErr != nil {
|
|
err = txErr
|
|
}
|
|
}
|
|
}()
|
|
|
|
row := s.db.QueryRowContext(ctx, `SELECT id, author, content, views, reactions, created_at FROM posts WHERE id = $1 FOR UPDATE`, id)
|
|
post, err := scanPost(row)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
updateFn(&post)
|
|
|
|
reactionsJSON, err := json.Marshal(post.Reactions)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
_, err = tx.ExecContext(ctx, `UPDATE posts SET views = $1, reactions = $2 WHERE id = $3`, post.Views, reactionsJSON, post.ID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
type scanner interface {
|
|
Scan(dest ...any) error
|
|
}
|
|
|
|
func scanPost(s scanner) (Post, error) {
|
|
var id, postViews int
|
|
var author, content string
|
|
var reactions []byte
|
|
var createdAt time.Time
|
|
|
|
err := s.Scan(&id, &author, &content, &postViews, &reactions, &createdAt)
|
|
if err != nil {
|
|
return Post{}, err
|
|
}
|
|
|
|
var reactionsMap map[string]int
|
|
err = json.Unmarshal(reactions, &reactionsMap)
|
|
if err != nil {
|
|
return Post{}, err
|
|
}
|
|
|
|
return Post{
|
|
ID: id,
|
|
Author: author,
|
|
Content: content,
|
|
CreatedAt: createdAt,
|
|
Views: postViews,
|
|
Reactions: reactionsMap,
|
|
}, nil
|
|
}
|