feat: initial commit
This commit is contained in:
@@ -0,0 +1,205 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/lib/pq"
|
||||
)
|
||||
|
||||
type Post struct {
|
||||
ID int64 `json:"id"`
|
||||
Content string `json:"content"`
|
||||
Title string `json:"title"`
|
||||
UserID int64 `json:"user_id"`
|
||||
Tags []string `json:"tags"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
Version int `json:"version"`
|
||||
Comments []Comment `json:"comments"`
|
||||
User User `json:"user"`
|
||||
}
|
||||
|
||||
type PostWithMetadata struct {
|
||||
Post
|
||||
CommentCount int `json:"comment_count"`
|
||||
}
|
||||
|
||||
type PostStore struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
func (s *PostStore) Create(ctx context.Context, post *Post) error {
|
||||
query := `
|
||||
INSERT INTO posts (content, title, user_id, tags)
|
||||
VALUES ($1, $2, $3, $4) RETURNING id, created_at, updated_at
|
||||
`
|
||||
ctx, cancel := context.WithTimeout(ctx, QueryTimeout)
|
||||
defer cancel()
|
||||
|
||||
err := s.db.QueryRowContext(
|
||||
ctx,
|
||||
query,
|
||||
post.Content,
|
||||
post.Title,
|
||||
post.UserID,
|
||||
pq.Array(post.Tags),
|
||||
).Scan(&post.ID, &post.CreatedAt, &post.UpdatedAt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *PostStore) GetByID(ctx context.Context, id int64) (*Post, error) {
|
||||
query := `
|
||||
SELECT id, content, title, user_id, tags, created_at, updated_at, version
|
||||
FROM posts
|
||||
WHERE id = $1
|
||||
`
|
||||
|
||||
ctx, cancel := context.WithTimeout(ctx, QueryTimeout)
|
||||
defer cancel()
|
||||
|
||||
var post Post
|
||||
err := s.db.QueryRowContext(ctx, query, id).Scan(
|
||||
&post.ID,
|
||||
&post.Content,
|
||||
&post.Title,
|
||||
&post.UserID,
|
||||
pq.Array(&post.Tags),
|
||||
&post.CreatedAt,
|
||||
&post.UpdatedAt,
|
||||
&post.Version,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, sql.ErrNoRows):
|
||||
return nil, ErrNotFound
|
||||
default:
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return &post, nil
|
||||
}
|
||||
|
||||
func (s *PostStore) GetUserFeed(ctx context.Context, userID int64, fq PaginatedFeedQuery) ([]PostWithMetadata, error) {
|
||||
query := `
|
||||
SELECT
|
||||
p.id, p.user_id, p.title, p.content, p.created_at, p.updated_at, p.version, p.tags,
|
||||
u.username,
|
||||
COUNT(c.id) AS comments_count
|
||||
FROM posts p
|
||||
LEFT JOIN comments c on c.post_id = p.id
|
||||
LEFT JOIN users u ON p.user_id = u.id
|
||||
JOIN followers f ON f.follower_id = p.user_id OR p.user_id = $1
|
||||
WHERE
|
||||
f.user_id = $1 AND
|
||||
(p.title ILIKE '%' || $4 || '%' OR p.content ILIKE '%' || $4 || '%') AND
|
||||
(p.tags @> $5 OR $5 = '{}')
|
||||
GROUP BY p.id, u.username
|
||||
ORDER BY p.created_at ` + fq.Sort + `
|
||||
LIMIT $2 OFFSET $3
|
||||
`
|
||||
|
||||
ctx, cancel := context.WithTimeout(ctx, QueryTimeout)
|
||||
defer cancel()
|
||||
|
||||
rows, err := s.db.QueryContext(ctx, query, userID, fq.Limit, fq.Offset, fq.Search, pq.Array(fq.Tags))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var feed []PostWithMetadata
|
||||
for rows.Next() {
|
||||
var p PostWithMetadata
|
||||
err := rows.Scan(
|
||||
&p.ID,
|
||||
&p.UserID,
|
||||
&p.Title,
|
||||
&p.Content,
|
||||
&p.CreatedAt,
|
||||
&p.UpdatedAt,
|
||||
&p.Version,
|
||||
pq.Array(&p.Tags),
|
||||
&p.User.Username,
|
||||
&p.CommentCount,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fmt.Println(p)
|
||||
|
||||
feed = append(feed, p)
|
||||
}
|
||||
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return feed, nil
|
||||
|
||||
}
|
||||
|
||||
func (s *PostStore) Update(ctx context.Context, post *Post) error {
|
||||
query := `
|
||||
UPDATE posts
|
||||
SET content = $1, title = $2, updated_at = NOW(), version = version + 1
|
||||
WHERE id = $3 AND version = $4
|
||||
RETURNING version
|
||||
`
|
||||
ctx, cancel := context.WithTimeout(ctx, QueryTimeout)
|
||||
defer cancel()
|
||||
|
||||
err := s.db.QueryRowContext(
|
||||
ctx,
|
||||
query,
|
||||
post.Content,
|
||||
post.Title,
|
||||
post.ID,
|
||||
post.Version,
|
||||
).Scan(&post.Version)
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, sql.ErrNoRows):
|
||||
return ErrNotFound
|
||||
default:
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *PostStore) Delete(ctx context.Context, id int64) error {
|
||||
query := `
|
||||
DELETE FROM posts
|
||||
WHERE id = $1
|
||||
`
|
||||
ctx, cancel := context.WithTimeout(ctx, QueryTimeout)
|
||||
defer cancel()
|
||||
|
||||
res, err := s.db.ExecContext(ctx, query, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rowsAffected, err := res.RowsAffected()
|
||||
if err != nil {
|
||||
return ErrNotFound
|
||||
}
|
||||
|
||||
if rowsAffected == 0 {
|
||||
return ErrNotFound
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user