#13 - browse articles
This commit is contained in:
73
internal/database/createPost.sql.go
Normal file
73
internal/database/createPost.sql.go
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// sqlc v1.28.0
|
||||||
|
// source: createPost.sql
|
||||||
|
|
||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
const createPost = `-- name: CreatePost :one
|
||||||
|
INSERT INTO posts (id, created_at, updated_at, url, title, description, feed_id, published_at)
|
||||||
|
VALUES (
|
||||||
|
$1,
|
||||||
|
$2,
|
||||||
|
$3,
|
||||||
|
$4,
|
||||||
|
$5,
|
||||||
|
$6,
|
||||||
|
$7,
|
||||||
|
$8
|
||||||
|
) ON CONFLICT (url) DO UPDATE
|
||||||
|
SET
|
||||||
|
id = $1,
|
||||||
|
created_at = $2,
|
||||||
|
updated_at = $3,
|
||||||
|
title = $5,
|
||||||
|
description = $6,
|
||||||
|
feed_id = $7,
|
||||||
|
published_at = $8
|
||||||
|
WHERE posts.url = $4
|
||||||
|
RETURNING id, created_at, updated_at, title, url, description, published_at, feed_id
|
||||||
|
`
|
||||||
|
|
||||||
|
type CreatePostParams struct {
|
||||||
|
ID uuid.UUID
|
||||||
|
CreatedAt time.Time
|
||||||
|
UpdatedAt time.Time
|
||||||
|
Url string
|
||||||
|
Title string
|
||||||
|
Description string
|
||||||
|
FeedID uuid.UUID
|
||||||
|
PublishedAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) CreatePost(ctx context.Context, arg CreatePostParams) (Post, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, createPost,
|
||||||
|
arg.ID,
|
||||||
|
arg.CreatedAt,
|
||||||
|
arg.UpdatedAt,
|
||||||
|
arg.Url,
|
||||||
|
arg.Title,
|
||||||
|
arg.Description,
|
||||||
|
arg.FeedID,
|
||||||
|
arg.PublishedAt,
|
||||||
|
)
|
||||||
|
var i Post
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.UpdatedAt,
|
||||||
|
&i.Title,
|
||||||
|
&i.Url,
|
||||||
|
&i.Description,
|
||||||
|
&i.PublishedAt,
|
||||||
|
&i.FeedID,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
54
internal/database/getPostsForUser.sql.go
Normal file
54
internal/database/getPostsForUser.sql.go
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// sqlc v1.28.0
|
||||||
|
// source: getPostsForUser.sql
|
||||||
|
|
||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
const getPostsForUser = `-- name: GetPostsForUser :many
|
||||||
|
SELECT id, created_at, updated_at, title, url, description, published_at, feed_id FROM posts WHERE feed_id IN (SELECT feed_follows.feed_id FROM feed_follows WHERE feed_follows.user_id = $1)
|
||||||
|
ORDER BY published_at DESC LIMIT $2
|
||||||
|
`
|
||||||
|
|
||||||
|
type GetPostsForUserParams struct {
|
||||||
|
UserID uuid.UUID
|
||||||
|
Limit int32
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) GetPostsForUser(ctx context.Context, arg GetPostsForUserParams) ([]Post, error) {
|
||||||
|
rows, err := q.db.QueryContext(ctx, getPostsForUser, arg.UserID, arg.Limit)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []Post
|
||||||
|
for rows.Next() {
|
||||||
|
var i Post
|
||||||
|
if err := rows.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.UpdatedAt,
|
||||||
|
&i.Title,
|
||||||
|
&i.Url,
|
||||||
|
&i.Description,
|
||||||
|
&i.PublishedAt,
|
||||||
|
&i.FeedID,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Close(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
@@ -29,6 +29,17 @@ type FeedFollow struct {
|
|||||||
FeedID uuid.UUID
|
FeedID uuid.UUID
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Post struct {
|
||||||
|
ID uuid.UUID
|
||||||
|
CreatedAt time.Time
|
||||||
|
UpdatedAt time.Time
|
||||||
|
Title string
|
||||||
|
Url string
|
||||||
|
Description string
|
||||||
|
PublishedAt time.Time
|
||||||
|
FeedID uuid.UUID
|
||||||
|
}
|
||||||
|
|
||||||
type User struct {
|
type User struct {
|
||||||
ID uuid.UUID
|
ID uuid.UUID
|
||||||
CreatedAt time.Time
|
CreatedAt time.Time
|
||||||
|
|||||||
36
internal/handlers/browseHandler.go
Normal file
36
internal/handlers/browseHandler.go
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"github.com/rdarius/boot-dev-blog-aggregator/internal/config"
|
||||||
|
"github.com/rdarius/boot-dev-blog-aggregator/internal/database"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
func BrowseHandler(s *config.State, cmd config.Command, user database.User) error {
|
||||||
|
var limit int32
|
||||||
|
if len(cmd.Args) < 1 {
|
||||||
|
limit = 2
|
||||||
|
} else {
|
||||||
|
l, err := strconv.Atoi(cmd.Args[0])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
limit = int32(l)
|
||||||
|
}
|
||||||
|
|
||||||
|
posts, err := s.DB.GetPostsForUser(context.Background(), database.GetPostsForUserParams{
|
||||||
|
UserID: user.ID,
|
||||||
|
Limit: limit,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, post := range posts {
|
||||||
|
fmt.Printf("Title: %s\nURL: %s\nArticle: %sPublished: %s\n\n", post.Title, post.Url, post.Description, post.PublishedAt)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -3,8 +3,12 @@ package handlers
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/google/uuid"
|
||||||
"github.com/rdarius/boot-dev-blog-aggregator/internal/config"
|
"github.com/rdarius/boot-dev-blog-aggregator/internal/config"
|
||||||
|
"github.com/rdarius/boot-dev-blog-aggregator/internal/database"
|
||||||
|
"github.com/rdarius/boot-dev-blog-aggregator/internal/parser"
|
||||||
"github.com/rdarius/boot-dev-blog-aggregator/internal/rss"
|
"github.com/rdarius/boot-dev-blog-aggregator/internal/rss"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
func ScrapeFeedsHandler(s *config.State, cmd config.Command) error {
|
func ScrapeFeedsHandler(s *config.State, cmd config.Command) error {
|
||||||
@@ -29,7 +33,24 @@ func ScrapeFeedsHandler(s *config.State, cmd config.Command) error {
|
|||||||
fmt.Println("Feed Title: " + feedData.Channel.Title)
|
fmt.Println("Feed Title: " + feedData.Channel.Title)
|
||||||
|
|
||||||
for _, i := range feedData.Channel.Item {
|
for _, i := range feedData.Channel.Item {
|
||||||
fmt.Println("Item Title: " + i.Title)
|
|
||||||
|
pubTime, err := parser.ParseTimestamp(i.PubDate)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = s.DB.CreatePost(ctx, database.CreatePostParams{
|
||||||
|
ID: uuid.New(),
|
||||||
|
CreatedAt: time.Time{},
|
||||||
|
UpdatedAt: time.Time{},
|
||||||
|
FeedID: nextFeed.ID,
|
||||||
|
Url: i.Link,
|
||||||
|
Title: i.Title,
|
||||||
|
Description: i.Description,
|
||||||
|
PublishedAt: pubTime,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
29
internal/parser/TimestampParser.go
Normal file
29
internal/parser/TimestampParser.go
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
package parser
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ParseTimestamp(str string) (time.Time, error) {
|
||||||
|
layouts := []string{
|
||||||
|
time.RFC1123, // "Mon, 02 Jan 2006 15:04:05 MST"
|
||||||
|
time.RFC1123Z, // "Mon, 02 Jan 2006 15:04:05 -0700"
|
||||||
|
time.RFC3339, // "2006-01-02T15:04:05Z07:00"
|
||||||
|
time.RFC3339Nano, // "2006-01-02T15:04:05.999999999Z07:00"
|
||||||
|
"2006-01-02 15:04:05", // Common SQL format
|
||||||
|
"02 Jan 2006 15:04:05", // "02 Jan 2025 15:04:05"
|
||||||
|
"02-Jan-2006 15:04:05", // "02-Jan-2025 15:04:05"
|
||||||
|
"02/01/2006 15:04:05", // "02/01/2025 15:04:05"
|
||||||
|
}
|
||||||
|
|
||||||
|
var t time.Time
|
||||||
|
var err error
|
||||||
|
for _, layout := range layouts {
|
||||||
|
t, err = time.Parse(layout, str)
|
||||||
|
if err == nil {
|
||||||
|
return t, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return time.Time{}, fmt.Errorf("unknown timestamp format: %s", str)
|
||||||
|
}
|
||||||
1
main.go
1
main.go
@@ -42,6 +42,7 @@ func main() {
|
|||||||
commands.Register("follow", middlewareLoggedIn(handlers.FollowFeedHandler))
|
commands.Register("follow", middlewareLoggedIn(handlers.FollowFeedHandler))
|
||||||
commands.Register("unfollow", middlewareLoggedIn(handlers.UnfollowFeedHandler))
|
commands.Register("unfollow", middlewareLoggedIn(handlers.UnfollowFeedHandler))
|
||||||
commands.Register("following", middlewareLoggedIn(handlers.GetFeedFollowsByUserHandler))
|
commands.Register("following", middlewareLoggedIn(handlers.GetFeedFollowsByUserHandler))
|
||||||
|
commands.Register("browse", middlewareLoggedIn(handlers.BrowseHandler))
|
||||||
|
|
||||||
if len(os.Args) < 2 {
|
if len(os.Args) < 2 {
|
||||||
log.Fatal("usage: boot-dev-blog-aggregator <command> [args...]")
|
log.Fatal("usage: boot-dev-blog-aggregator <command> [args...]")
|
||||||
|
|||||||
22
sql/queries/createPost.sql
Normal file
22
sql/queries/createPost.sql
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
-- name: CreatePost :one
|
||||||
|
INSERT INTO posts (id, created_at, updated_at, url, title, description, feed_id, published_at)
|
||||||
|
VALUES (
|
||||||
|
$1,
|
||||||
|
$2,
|
||||||
|
$3,
|
||||||
|
$4,
|
||||||
|
$5,
|
||||||
|
$6,
|
||||||
|
$7,
|
||||||
|
$8
|
||||||
|
) ON CONFLICT (url) DO UPDATE
|
||||||
|
SET
|
||||||
|
id = $1,
|
||||||
|
created_at = $2,
|
||||||
|
updated_at = $3,
|
||||||
|
title = $5,
|
||||||
|
description = $6,
|
||||||
|
feed_id = $7,
|
||||||
|
published_at = $8
|
||||||
|
WHERE posts.url = $4
|
||||||
|
RETURNING *;
|
||||||
3
sql/queries/getPostsForUser.sql
Normal file
3
sql/queries/getPostsForUser.sql
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
-- name: GetPostsForUser :many
|
||||||
|
SELECT * FROM posts WHERE feed_id IN (SELECT feed_follows.feed_id FROM feed_follows WHERE feed_follows.user_id = $1)
|
||||||
|
ORDER BY published_at DESC LIMIT $2;
|
||||||
14
sql/schema/0005_posts.sql
Normal file
14
sql/schema/0005_posts.sql
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
-- +goose Up
|
||||||
|
CREATE TABLE posts(
|
||||||
|
id UUID PRIMARY KEY,
|
||||||
|
created_at TIMESTAMP not null DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP not null,
|
||||||
|
title VARCHAR(128) not null,
|
||||||
|
url VARCHAR(512) not null UNIQUE,
|
||||||
|
description TEXT not null,
|
||||||
|
published_at TIMESTAMP not null ,
|
||||||
|
feed_id UUID not null REFERENCES feeds(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- +goose Down
|
||||||
|
DROP TABLE posts;
|
||||||
Reference in New Issue
Block a user