Compare commits
10 Commits
79b0812a90
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 995f306a6a | |||
| 80602bf04f | |||
| f561eb4c34 | |||
| a76915c7fc | |||
| 5dfe911793 | |||
| b7b7a7d55d | |||
| ed8b8860b7 | |||
| e89bfba392 | |||
| 5a7fac748b | |||
| e74d33ab55 |
44
README.md
Normal file
44
README.md
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
## Requirements
|
||||||
|
|
||||||
|
To run this program you will need to install:
|
||||||
|
- PostresSQL
|
||||||
|
- Go CLI
|
||||||
|
|
||||||
|
For Dev:
|
||||||
|
- Goose (for migrations) `go install github.com/pressly/goose/v3/cmd/goose@latest`
|
||||||
|
- from `sql/schema` directory of the project run `goose postgres "postgres://postgres:postgres@localhost:5432/gator" up` to do the migrations
|
||||||
|
- SQLC (for generating models) `go install github.com/sqlc-dev/sqlc/cmd/sqlc@latest`
|
||||||
|
- from root of the project run `sqlc generate` to build models from SQL files
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
set up a config in your home directory `~/.gatorconfig.json`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"db_url": "protocol://username:password@host:port/database?sslmode=disable",
|
||||||
|
"current_user_name": "username_goes_here"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
Because GO does not allow custom names when installing application (as far as I know) you can either:
|
||||||
|
- install from source with `go install` and use `boot-dev-blog-aggregator` to run the program, or
|
||||||
|
- build the code with custom name `go build -o gator` and use generated executable instead
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
first: make an account
|
||||||
|
> ./gator register YOUR_USERNAME
|
||||||
|
|
||||||
|
then you can follow some topics:
|
||||||
|
> ./gator follow https://blog.boot.dev/index.xml
|
||||||
|
|
||||||
|
let them update:
|
||||||
|
> ./gator agg 1m
|
||||||
|
|
||||||
|
and see the content:
|
||||||
|
> ./gator browse X
|
||||||
|
(where X is number of articles to be shown, they are sorted from latest or oldest by publishing data)
|
||||||
57
internal/database/createFeed.sql.go
Normal file
57
internal/database/createFeed.sql.go
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// sqlc v1.28.0
|
||||||
|
// source: createFeed.sql
|
||||||
|
|
||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
const createFeed = `-- name: CreateFeed :one
|
||||||
|
INSERT INTO feeds (id, created_at, updated_at, user_id, url, name)
|
||||||
|
VALUES (
|
||||||
|
$1,
|
||||||
|
$2,
|
||||||
|
$3,
|
||||||
|
$4,
|
||||||
|
$5,
|
||||||
|
$6
|
||||||
|
)
|
||||||
|
RETURNING id, created_at, updated_at, name, url, user_id, last_fetched_at
|
||||||
|
`
|
||||||
|
|
||||||
|
type CreateFeedParams struct {
|
||||||
|
ID uuid.UUID
|
||||||
|
CreatedAt time.Time
|
||||||
|
UpdatedAt time.Time
|
||||||
|
UserID uuid.UUID
|
||||||
|
Url string
|
||||||
|
Name string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) CreateFeed(ctx context.Context, arg CreateFeedParams) (Feed, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, createFeed,
|
||||||
|
arg.ID,
|
||||||
|
arg.CreatedAt,
|
||||||
|
arg.UpdatedAt,
|
||||||
|
arg.UserID,
|
||||||
|
arg.Url,
|
||||||
|
arg.Name,
|
||||||
|
)
|
||||||
|
var i Feed
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.UpdatedAt,
|
||||||
|
&i.Name,
|
||||||
|
&i.Url,
|
||||||
|
&i.UserID,
|
||||||
|
&i.LastFetchedAt,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
73
internal/database/createFeedFollow.sql.go
Normal file
73
internal/database/createFeedFollow.sql.go
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// sqlc v1.28.0
|
||||||
|
// source: createFeedFollow.sql
|
||||||
|
|
||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
const createFeedFollow = `-- name: CreateFeedFollow :one
|
||||||
|
|
||||||
|
WITH inserted_feed_follow AS (
|
||||||
|
INSERT INTO feed_follows (id, created_at, updated_at, user_id, feed_id)
|
||||||
|
VALUES (
|
||||||
|
$1,
|
||||||
|
$2,
|
||||||
|
$3,
|
||||||
|
$4,
|
||||||
|
$5
|
||||||
|
)
|
||||||
|
RETURNING id, created_at, updated_at, user_id, feed_id
|
||||||
|
) SELECT
|
||||||
|
inserted_feed_follow.id, inserted_feed_follow.created_at, inserted_feed_follow.updated_at, inserted_feed_follow.user_id, inserted_feed_follow.feed_id,
|
||||||
|
feeds.name AS feed_name,
|
||||||
|
users.name AS user_name
|
||||||
|
FROM inserted_feed_follow
|
||||||
|
INNER JOIN users ON users.id = inserted_feed_follow.user_id
|
||||||
|
INNER JOIN feeds ON feeds.id = inserted_feed_follow.feed_id
|
||||||
|
`
|
||||||
|
|
||||||
|
type CreateFeedFollowParams struct {
|
||||||
|
ID uuid.UUID
|
||||||
|
CreatedAt time.Time
|
||||||
|
UpdatedAt time.Time
|
||||||
|
UserID uuid.UUID
|
||||||
|
FeedID uuid.UUID
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateFeedFollowRow struct {
|
||||||
|
ID uuid.UUID
|
||||||
|
CreatedAt time.Time
|
||||||
|
UpdatedAt time.Time
|
||||||
|
UserID uuid.UUID
|
||||||
|
FeedID uuid.UUID
|
||||||
|
FeedName string
|
||||||
|
UserName string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) CreateFeedFollow(ctx context.Context, arg CreateFeedFollowParams) (CreateFeedFollowRow, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, createFeedFollow,
|
||||||
|
arg.ID,
|
||||||
|
arg.CreatedAt,
|
||||||
|
arg.UpdatedAt,
|
||||||
|
arg.UserID,
|
||||||
|
arg.FeedID,
|
||||||
|
)
|
||||||
|
var i CreateFeedFollowRow
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.UpdatedAt,
|
||||||
|
&i.UserID,
|
||||||
|
&i.FeedID,
|
||||||
|
&i.FeedName,
|
||||||
|
&i.UserName,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
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
|
||||||
|
}
|
||||||
26
internal/database/feleteFeedFollow.sql.go
Normal file
26
internal/database/feleteFeedFollow.sql.go
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// sqlc v1.28.0
|
||||||
|
// source: feleteFeedFollow.sql
|
||||||
|
|
||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
const deleteFeedFollow = `-- name: DeleteFeedFollow :exec
|
||||||
|
DELETE FROM feed_follows WHERE feed_follows.user_id = $1 AND feed_follows.feed_id = (SELECT id FROM feeds WHERE url = $2)
|
||||||
|
`
|
||||||
|
|
||||||
|
type DeleteFeedFollowParams struct {
|
||||||
|
UserID uuid.UUID
|
||||||
|
Url string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) DeleteFeedFollow(ctx context.Context, arg DeleteFeedFollowParams) error {
|
||||||
|
_, err := q.db.ExecContext(ctx, deleteFeedFollow, arg.UserID, arg.Url)
|
||||||
|
return err
|
||||||
|
}
|
||||||
29
internal/database/getFeedByUrl.sql.go
Normal file
29
internal/database/getFeedByUrl.sql.go
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// sqlc v1.28.0
|
||||||
|
// source: getFeedByUrl.sql
|
||||||
|
|
||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
)
|
||||||
|
|
||||||
|
const getFeedByUrl = `-- name: GetFeedByUrl :one
|
||||||
|
SELECT id, created_at, updated_at, name, url, user_id, last_fetched_at FROM feeds WHERE url = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetFeedByUrl(ctx context.Context, url string) (Feed, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, getFeedByUrl, url)
|
||||||
|
var i Feed
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.UpdatedAt,
|
||||||
|
&i.Name,
|
||||||
|
&i.Url,
|
||||||
|
&i.UserID,
|
||||||
|
&i.LastFetchedAt,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
65
internal/database/getFeedFollowsByUser.sql.go
Normal file
65
internal/database/getFeedFollowsByUser.sql.go
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// sqlc v1.28.0
|
||||||
|
// source: getFeedFollowsByUser.sql
|
||||||
|
|
||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
const getFeedFollowsByUser = `-- name: GetFeedFollowsByUser :many
|
||||||
|
SELECT
|
||||||
|
feed_follows.id, feed_follows.created_at, feed_follows.updated_at, feed_follows.user_id, feed_follows.feed_id,
|
||||||
|
feeds.name AS feed_name,
|
||||||
|
users.name AS user_name
|
||||||
|
FROM feed_follows
|
||||||
|
INNER JOIN users ON users.id = feed_follows.user_id
|
||||||
|
INNER JOIN feeds ON feeds.id = feed_follows.feed_id
|
||||||
|
WHERE feed_follows.user_id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
type GetFeedFollowsByUserRow struct {
|
||||||
|
ID uuid.UUID
|
||||||
|
CreatedAt time.Time
|
||||||
|
UpdatedAt time.Time
|
||||||
|
UserID uuid.UUID
|
||||||
|
FeedID uuid.UUID
|
||||||
|
FeedName string
|
||||||
|
UserName string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) GetFeedFollowsByUser(ctx context.Context, userID uuid.UUID) ([]GetFeedFollowsByUserRow, error) {
|
||||||
|
rows, err := q.db.QueryContext(ctx, getFeedFollowsByUser, userID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []GetFeedFollowsByUserRow
|
||||||
|
for rows.Next() {
|
||||||
|
var i GetFeedFollowsByUserRow
|
||||||
|
if err := rows.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.UpdatedAt,
|
||||||
|
&i.UserID,
|
||||||
|
&i.FeedID,
|
||||||
|
&i.FeedName,
|
||||||
|
&i.UserName,
|
||||||
|
); 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
internal/database/getNextFeedToFetch.sql.go
Normal file
29
internal/database/getNextFeedToFetch.sql.go
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// sqlc v1.28.0
|
||||||
|
// source: getNextFeedToFetch.sql
|
||||||
|
|
||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
)
|
||||||
|
|
||||||
|
const getNextFeedToFetch = `-- name: GetNextFeedToFetch :one
|
||||||
|
SELECT id, created_at, updated_at, name, url, user_id, last_fetched_at FROM feeds ORDER BY last_fetched_at NULLS FIRST LIMIT 1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetNextFeedToFetch(ctx context.Context) (Feed, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, getNextFeedToFetch)
|
||||||
|
var i Feed
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.UpdatedAt,
|
||||||
|
&i.Name,
|
||||||
|
&i.Url,
|
||||||
|
&i.UserID,
|
||||||
|
&i.LastFetchedAt,
|
||||||
|
)
|
||||||
|
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
|
||||||
|
}
|
||||||
42
internal/database/getUsers.sql.go
Normal file
42
internal/database/getUsers.sql.go
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// sqlc v1.28.0
|
||||||
|
// source: getUsers.sql
|
||||||
|
|
||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
)
|
||||||
|
|
||||||
|
const getUsers = `-- name: GetUsers :many
|
||||||
|
SELECT id, created_at, updated_at, name FROM users WHERE true
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetUsers(ctx context.Context) ([]User, error) {
|
||||||
|
rows, err := q.db.QueryContext(ctx, getUsers)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []User
|
||||||
|
for rows.Next() {
|
||||||
|
var i User
|
||||||
|
if err := rows.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.UpdatedAt,
|
||||||
|
&i.Name,
|
||||||
|
); 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
|
||||||
|
}
|
||||||
43
internal/database/listFeeds.sql.go
Normal file
43
internal/database/listFeeds.sql.go
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// sqlc v1.28.0
|
||||||
|
// source: listFeeds.sql
|
||||||
|
|
||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
)
|
||||||
|
|
||||||
|
const listFeeds = `-- name: ListFeeds :many
|
||||||
|
SELECT name, url, (SELECT name FROM users WHERE users.id = feeds.user_id) as user_name FROM feeds
|
||||||
|
`
|
||||||
|
|
||||||
|
type ListFeedsRow struct {
|
||||||
|
Name string
|
||||||
|
Url string
|
||||||
|
UserName string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) ListFeeds(ctx context.Context) ([]ListFeedsRow, error) {
|
||||||
|
rows, err := q.db.QueryContext(ctx, listFeeds)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []ListFeedsRow
|
||||||
|
for rows.Next() {
|
||||||
|
var i ListFeedsRow
|
||||||
|
if err := rows.Scan(&i.Name, &i.Url, &i.UserName); 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
|
||||||
|
}
|
||||||
32
internal/database/markFeeedFetched.sql.go
Normal file
32
internal/database/markFeeedFetched.sql.go
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// sqlc v1.28.0
|
||||||
|
// source: markFeeedFetched.sql
|
||||||
|
|
||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
const markFeedFetched = `-- name: MarkFeedFetched :one
|
||||||
|
UPDATE feeds SET last_fetched_at = CURRENT_TIMESTAMP, updated_at = CURRENT_TIMESTAMP WHERE id = $1
|
||||||
|
RETURNING id, created_at, updated_at, name, url, user_id, last_fetched_at
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) MarkFeedFetched(ctx context.Context, id uuid.UUID) (Feed, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, markFeedFetched, id)
|
||||||
|
var i Feed
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.UpdatedAt,
|
||||||
|
&i.Name,
|
||||||
|
&i.Url,
|
||||||
|
&i.UserID,
|
||||||
|
&i.LastFetchedAt,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
@@ -5,11 +5,41 @@
|
|||||||
package database
|
package database
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"database/sql"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type Feed struct {
|
||||||
|
ID uuid.UUID
|
||||||
|
CreatedAt time.Time
|
||||||
|
UpdatedAt time.Time
|
||||||
|
Name string
|
||||||
|
Url string
|
||||||
|
UserID uuid.UUID
|
||||||
|
LastFetchedAt sql.NullTime
|
||||||
|
}
|
||||||
|
|
||||||
|
type FeedFollow struct {
|
||||||
|
ID uuid.UUID
|
||||||
|
CreatedAt time.Time
|
||||||
|
UpdatedAt time.Time
|
||||||
|
UserID 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
|
||||||
|
|||||||
58
internal/handlers/addFeedHandler.go
Normal file
58
internal/handlers/addFeedHandler.go
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"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/rss"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func AddFeedHandler(s *config.State, cmd config.Command, user database.User) error {
|
||||||
|
|
||||||
|
if len(cmd.Args) < 2 {
|
||||||
|
log.Fatal("usage: boot-dev-blog-aggregator addfeed NAME URL")
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
name := cmd.Args[0]
|
||||||
|
url := cmd.Args[1]
|
||||||
|
|
||||||
|
_, err := rss.FetchFeed(ctx, url)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Failed to fetch feed: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
f, err := s.DB.CreateFeed(ctx, database.CreateFeedParams{
|
||||||
|
ID: uuid.New(),
|
||||||
|
CreatedAt: time.Time{},
|
||||||
|
UpdatedAt: time.Time{},
|
||||||
|
UserID: user.ID,
|
||||||
|
Url: url,
|
||||||
|
Name: name,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = s.DB.CreateFeedFollow(ctx, database.CreateFeedFollowParams{
|
||||||
|
ID: uuid.New(),
|
||||||
|
CreatedAt: time.Time{},
|
||||||
|
UpdatedAt: time.Time{},
|
||||||
|
UserID: user.ID,
|
||||||
|
FeedID: f.ID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("%v\n", f)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
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
|
||||||
|
}
|
||||||
24
internal/handlers/feedFollowsByUserHandler.go
Normal file
24
internal/handlers/feedFollowsByUserHandler.go
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"github.com/rdarius/boot-dev-blog-aggregator/internal/config"
|
||||||
|
"github.com/rdarius/boot-dev-blog-aggregator/internal/database"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetFeedFollowsByUserHandler(s *config.State, cmd config.Command, user database.User) error {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
ff, err := s.DB.GetFeedFollowsByUser(ctx, user.ID)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, feed := range ff {
|
||||||
|
fmt.Printf("* %s\n", feed.FeedName)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
28
internal/handlers/fetchFeedHandler.go
Normal file
28
internal/handlers/fetchFeedHandler.go
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/rdarius/boot-dev-blog-aggregator/internal/config"
|
||||||
|
"log"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func FetchFeedHandler(s *config.State, cmd config.Command) error {
|
||||||
|
if len(cmd.Args) < 1 {
|
||||||
|
log.Fatal("usage: boot-dev-blog-aggregator agg time_between_reqs")
|
||||||
|
}
|
||||||
|
|
||||||
|
t, err := time.ParseDuration(cmd.Args[0])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
ticker := time.NewTicker(t)
|
||||||
|
for ; ; <-ticker.C {
|
||||||
|
err := ScrapeFeedsHandler(s, cmd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
62
internal/handlers/followFeedHandler.go
Normal file
62
internal/handlers/followFeedHandler.go
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"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/rss"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func FollowFeedHandler(s *config.State, cmd config.Command, user database.User) error {
|
||||||
|
if len(cmd.Args) < 1 {
|
||||||
|
log.Fatal("usage: boot-dev-blog-aggregator follow URL")
|
||||||
|
}
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
url := cmd.Args[0]
|
||||||
|
|
||||||
|
ff, err := s.DB.GetFeedByUrl(ctx, url)
|
||||||
|
if err != nil {
|
||||||
|
// no feed, get it and save it
|
||||||
|
|
||||||
|
fd, err := rss.FetchFeed(ctx, url)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Failed to fetch feed: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := s.DB.CreateFeed(ctx, database.CreateFeedParams{
|
||||||
|
ID: uuid.New(),
|
||||||
|
CreatedAt: time.Time{},
|
||||||
|
UpdatedAt: time.Time{},
|
||||||
|
UserID: user.ID,
|
||||||
|
Url: url,
|
||||||
|
Name: fd.Channel.Title,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Failed to create feed: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
ff = res
|
||||||
|
}
|
||||||
|
|
||||||
|
feed, err := s.DB.CreateFeedFollow(ctx, database.CreateFeedFollowParams{
|
||||||
|
ID: uuid.New(),
|
||||||
|
CreatedAt: time.Time{},
|
||||||
|
UpdatedAt: time.Time{},
|
||||||
|
UserID: user.ID,
|
||||||
|
FeedID: ff.ID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("%v\n", feed)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
30
internal/handlers/getUsersHandler.go
Normal file
30
internal/handlers/getUsersHandler.go
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"github.com/rdarius/boot-dev-blog-aggregator/internal/config"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetUsersHandler(s *config.State, cmd config.Command) error {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
u, err := s.DB.GetUsers(ctx)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Failed to get Users")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, u := range u {
|
||||||
|
fmt.Printf("* %s", u.Name)
|
||||||
|
|
||||||
|
if u.Name == s.Config.CurrentUserName {
|
||||||
|
fmt.Print(" (current)")
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Print("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
21
internal/handlers/listFeedsHandler.go
Normal file
21
internal/handlers/listFeedsHandler.go
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"github.com/rdarius/boot-dev-blog-aggregator/internal/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ListFeedsHandler(s *config.State, cmd config.Command) error {
|
||||||
|
ctx := context.Background()
|
||||||
|
feeds, err := s.DB.ListFeeds(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, feed := range feeds {
|
||||||
|
fmt.Printf("Article: %s | Url: %s | User: %s\n", feed.Name, feed.Url, feed.UserName)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -26,7 +26,7 @@ func LoginHandler(s *config.State, cmd config.Command) error {
|
|||||||
return fmt.Errorf("failed to set current user: %w", err)
|
return fmt.Errorf("failed to set current user: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("User set to %s\n", s.Config.CurrentUserName)
|
fmt.Printf("User set to %s\n", u.Name)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
57
internal/handlers/scrapeFeedsHandler.go
Normal file
57
internal/handlers/scrapeFeedsHandler.go
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"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"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ScrapeFeedsHandler(s *config.State, cmd config.Command) error {
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
nextFeed, err := s.DB.GetNextFeedToFetch(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = s.DB.MarkFeedFetched(ctx, nextFeed.ID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
feedData, err := rss.FetchFeed(ctx, nextFeed.Url)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("Feed Title: " + feedData.Channel.Title)
|
||||||
|
|
||||||
|
for _, i := range feedData.Channel.Item {
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
31
internal/handlers/unfollowFeedHandler.go
Normal file
31
internal/handlers/unfollowFeedHandler.go
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"github.com/rdarius/boot-dev-blog-aggregator/internal/config"
|
||||||
|
"github.com/rdarius/boot-dev-blog-aggregator/internal/database"
|
||||||
|
"log"
|
||||||
|
)
|
||||||
|
|
||||||
|
func UnfollowFeedHandler(s *config.State, cmd config.Command, user database.User) error {
|
||||||
|
if len(cmd.Args) < 1 {
|
||||||
|
log.Fatal("usage: boot-dev-blog-aggregator unfollow URL")
|
||||||
|
}
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
url := cmd.Args[0]
|
||||||
|
|
||||||
|
err := s.DB.DeleteFeedFollow(ctx, database.DeleteFeedFollowParams{
|
||||||
|
UserID: user.ID,
|
||||||
|
Url: url,
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("Feed unfollow successfully")
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
57
internal/rss/rss.go
Normal file
57
internal/rss/rss.go
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
package rss
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/xml"
|
||||||
|
"html"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Feed struct {
|
||||||
|
Channel struct {
|
||||||
|
Title string `xml:"title"`
|
||||||
|
Link string `xml:"link"`
|
||||||
|
Description string `xml:"description"`
|
||||||
|
Item []Item `xml:"item"`
|
||||||
|
} `xml:"channel"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Item struct {
|
||||||
|
Title string `xml:"title"`
|
||||||
|
Link string `xml:"link"`
|
||||||
|
Description string `xml:"description"`
|
||||||
|
PubDate string `xml:"pubDate"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func FetchFeed(ctx context.Context, feedURL string) (*Feed, error) {
|
||||||
|
req, err := http.NewRequestWithContext(ctx, "GET", feedURL, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
req.Header.Set("User-Agent", "gator")
|
||||||
|
resp, err := http.DefaultClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
res, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var feed Feed
|
||||||
|
err = xml.Unmarshal(res, &feed)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
feed.Channel.Title = html.UnescapeString(feed.Channel.Title)
|
||||||
|
feed.Channel.Link = html.UnescapeString(feed.Channel.Link)
|
||||||
|
feed.Channel.Description = html.UnescapeString(feed.Channel.Description)
|
||||||
|
for i, _ := range feed.Channel.Item {
|
||||||
|
feed.Channel.Item[i].Link = html.UnescapeString(feed.Channel.Item[i].Link)
|
||||||
|
feed.Channel.Item[i].Description = html.UnescapeString(feed.Channel.Item[i].Description)
|
||||||
|
feed.Channel.Item[i].PubDate = html.UnescapeString(feed.Channel.Item[i].PubDate)
|
||||||
|
feed.Channel.Item[i].Title = html.UnescapeString(feed.Channel.Item[i].Title)
|
||||||
|
}
|
||||||
|
return &feed, nil
|
||||||
|
}
|
||||||
19
main.go
19
main.go
@@ -1,6 +1,7 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
_ "github.com/lib/pq"
|
_ "github.com/lib/pq"
|
||||||
"github.com/rdarius/boot-dev-blog-aggregator/internal/config"
|
"github.com/rdarius/boot-dev-blog-aggregator/internal/config"
|
||||||
@@ -34,6 +35,14 @@ func main() {
|
|||||||
commands.Register("login", handlers.LoginHandler)
|
commands.Register("login", handlers.LoginHandler)
|
||||||
commands.Register("register", handlers.RegisterHandler)
|
commands.Register("register", handlers.RegisterHandler)
|
||||||
commands.Register("reset", handlers.ResetUsersHandler)
|
commands.Register("reset", handlers.ResetUsersHandler)
|
||||||
|
commands.Register("users", handlers.GetUsersHandler)
|
||||||
|
commands.Register("agg", handlers.FetchFeedHandler)
|
||||||
|
commands.Register("addfeed", middlewareLoggedIn(handlers.AddFeedHandler))
|
||||||
|
commands.Register("feeds", handlers.ListFeedsHandler)
|
||||||
|
commands.Register("follow", middlewareLoggedIn(handlers.FollowFeedHandler))
|
||||||
|
commands.Register("unfollow", middlewareLoggedIn(handlers.UnfollowFeedHandler))
|
||||||
|
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...]")
|
||||||
@@ -48,3 +57,13 @@ func main() {
|
|||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func middlewareLoggedIn(handler func(s *config.State, cmd config.Command, user database.User) error) func(*config.State, config.Command) error {
|
||||||
|
return func(state *config.State, command config.Command) error {
|
||||||
|
user, err := state.DB.GetUser(context.Background(), state.Config.CurrentUserName)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("user not logged in")
|
||||||
|
}
|
||||||
|
return handler(state, command, user)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
11
sql/queries/createFeed.sql
Normal file
11
sql/queries/createFeed.sql
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
-- name: CreateFeed :one
|
||||||
|
INSERT INTO feeds (id, created_at, updated_at, user_id, url, name)
|
||||||
|
VALUES (
|
||||||
|
$1,
|
||||||
|
$2,
|
||||||
|
$3,
|
||||||
|
$4,
|
||||||
|
$5,
|
||||||
|
$6
|
||||||
|
)
|
||||||
|
RETURNING *;
|
||||||
20
sql/queries/createFeedFollow.sql
Normal file
20
sql/queries/createFeedFollow.sql
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
-- name: CreateFeedFollow :one
|
||||||
|
|
||||||
|
WITH inserted_feed_follow AS (
|
||||||
|
INSERT INTO feed_follows (id, created_at, updated_at, user_id, feed_id)
|
||||||
|
VALUES (
|
||||||
|
$1,
|
||||||
|
$2,
|
||||||
|
$3,
|
||||||
|
$4,
|
||||||
|
$5
|
||||||
|
)
|
||||||
|
RETURNING *
|
||||||
|
) SELECT
|
||||||
|
inserted_feed_follow.*,
|
||||||
|
feeds.name AS feed_name,
|
||||||
|
users.name AS user_name
|
||||||
|
FROM inserted_feed_follow
|
||||||
|
INNER JOIN users ON users.id = inserted_feed_follow.user_id
|
||||||
|
INNER JOIN feeds ON feeds.id = inserted_feed_follow.feed_id;
|
||||||
|
|
||||||
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 *;
|
||||||
2
sql/queries/feleteFeedFollow.sql
Normal file
2
sql/queries/feleteFeedFollow.sql
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
-- name: DeleteFeedFollow :exec
|
||||||
|
DELETE FROM feed_follows WHERE feed_follows.user_id = $1 AND feed_follows.feed_id = (SELECT id FROM feeds WHERE url = $2);
|
||||||
2
sql/queries/getFeedByUrl.sql
Normal file
2
sql/queries/getFeedByUrl.sql
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
-- name: GetFeedByUrl :one
|
||||||
|
SELECT * FROM feeds WHERE url = $1;
|
||||||
9
sql/queries/getFeedFollowsByUser.sql
Normal file
9
sql/queries/getFeedFollowsByUser.sql
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
-- name: GetFeedFollowsByUser :many
|
||||||
|
SELECT
|
||||||
|
feed_follows.*,
|
||||||
|
feeds.name AS feed_name,
|
||||||
|
users.name AS user_name
|
||||||
|
FROM feed_follows
|
||||||
|
INNER JOIN users ON users.id = feed_follows.user_id
|
||||||
|
INNER JOIN feeds ON feeds.id = feed_follows.feed_id
|
||||||
|
WHERE feed_follows.user_id = $1;
|
||||||
2
sql/queries/getNextFeedToFetch.sql
Normal file
2
sql/queries/getNextFeedToFetch.sql
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
-- name: GetNextFeedToFetch :one
|
||||||
|
SELECT * FROM feeds ORDER BY last_fetched_at NULLS FIRST LIMIT 1;
|
||||||
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;
|
||||||
2
sql/queries/getUsers.sql
Normal file
2
sql/queries/getUsers.sql
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
-- name: GetUsers :many
|
||||||
|
SELECT * FROM users WHERE true;
|
||||||
2
sql/queries/listFeeds.sql
Normal file
2
sql/queries/listFeeds.sql
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
-- name: ListFeeds :many
|
||||||
|
SELECT name, url, (SELECT name FROM users WHERE users.id = feeds.user_id) as user_name FROM feeds;
|
||||||
3
sql/queries/markFeeedFetched.sql
Normal file
3
sql/queries/markFeeedFetched.sql
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
-- name: MarkFeedFetched :one
|
||||||
|
UPDATE feeds SET last_fetched_at = CURRENT_TIMESTAMP, updated_at = CURRENT_TIMESTAMP WHERE id = $1
|
||||||
|
RETURNING *;
|
||||||
12
sql/schema/0002_feed.sql
Normal file
12
sql/schema/0002_feed.sql
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
-- +goose Up
|
||||||
|
CREATE TABLE feeds(
|
||||||
|
id UUID PRIMARY KEY,
|
||||||
|
created_at TIMESTAMP not null DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP not null,
|
||||||
|
name VARCHAR(128) not null,
|
||||||
|
url VARCHAR(512) not null UNIQUE,
|
||||||
|
user_id UUID not null REFERENCES users(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- +goose Down
|
||||||
|
DROP TABLE feeds;
|
||||||
12
sql/schema/0003_feed_follows.sql
Normal file
12
sql/schema/0003_feed_follows.sql
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
-- +goose Up
|
||||||
|
CREATE TABLE feed_follows(
|
||||||
|
id UUID PRIMARY KEY,
|
||||||
|
created_at TIMESTAMP not null DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP not null,
|
||||||
|
user_id UUID not null REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
feed_id UUID not null REFERENCES feeds(id) ON DELETE CASCADE,
|
||||||
|
CONSTRAINT UC_User_Feed UNIQUE(user_id, feed_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- +goose Down
|
||||||
|
DROP TABLE feeds;
|
||||||
5
sql/schema/0004_last_fetched_field_in_feed.sql
Normal file
5
sql/schema/0004_last_fetched_field_in_feed.sql
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
-- +goose Up
|
||||||
|
ALTER TABLE feeds ADD COLUMN last_fetched_at timestamp DEFAULT null;
|
||||||
|
|
||||||
|
-- +goose Down
|
||||||
|
ALTER TABLE feeds DROP COLUMN last_fetched_at;
|
||||||
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