generated from templates/go-generic
feat: add project files
This commit is contained in:
parent
f8441a48b7
commit
a832053349
26 changed files with 1515 additions and 3 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,2 +1,3 @@
|
||||||
/__build/*
|
/__build/*
|
||||||
!__build/.gitkeep
|
!__build/.gitkeep
|
||||||
|
/.environment
|
||||||
|
|
|
@ -13,6 +13,15 @@ output:
|
||||||
sort-results: true
|
sort-results: true
|
||||||
|
|
||||||
linters-settings:
|
linters-settings:
|
||||||
|
depguard:
|
||||||
|
rules:
|
||||||
|
main:
|
||||||
|
files:
|
||||||
|
- $all
|
||||||
|
allow:
|
||||||
|
- $gostd
|
||||||
|
- codeflow.dananglin.me.uk/apollo/gator
|
||||||
|
- github.com/google/uuid
|
||||||
lll:
|
lll:
|
||||||
line-length: 140
|
line-length: 140
|
||||||
|
|
||||||
|
|
454
commands.go
Normal file
454
commands.go
Normal file
|
@ -0,0 +1,454 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"codeflow.dananglin.me.uk/apollo/gator/internal/database"
|
||||||
|
"codeflow.dananglin.me.uk/apollo/gator/internal/rss"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/lib/pq"
|
||||||
|
)
|
||||||
|
|
||||||
|
type commands struct {
|
||||||
|
commandMap map[string]commandFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
type commandFunc func(*state, command) error
|
||||||
|
|
||||||
|
type command struct {
|
||||||
|
name string
|
||||||
|
args []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *commands) register(name string, f commandFunc) {
|
||||||
|
c.commandMap[name] = f
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *commands) run(s *state, cmd command) error {
|
||||||
|
runFunc, ok := c.commandMap[cmd.name]
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("unrecognised command: %s", cmd.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
return runFunc(s, cmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
func handlerLogin(s *state, cmd command) error {
|
||||||
|
if len(cmd.args) != 1 {
|
||||||
|
return fmt.Errorf("unexpected number of arguments: want 1, got %d", len(cmd.args))
|
||||||
|
}
|
||||||
|
|
||||||
|
username := cmd.args[0]
|
||||||
|
|
||||||
|
user, err := s.db.GetUserByName(context.Background(), username)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to get the user from the database: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.config.SetUser(user.Name); err != nil {
|
||||||
|
return fmt.Errorf("login error: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("The current user is set to %q.\n", username)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func handlerRegister(s *state, cmd command) error {
|
||||||
|
if len(cmd.args) != 1 {
|
||||||
|
return fmt.Errorf("unexpected number of arguments: want 1, got %d", len(cmd.args))
|
||||||
|
}
|
||||||
|
|
||||||
|
name := cmd.args[0]
|
||||||
|
|
||||||
|
timestamp := time.Now()
|
||||||
|
|
||||||
|
args := database.CreateUserParams{
|
||||||
|
ID: uuid.New(),
|
||||||
|
CreatedAt: timestamp,
|
||||||
|
UpdatedAt: timestamp,
|
||||||
|
Name: name,
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := s.db.CreateUser(context.Background(), args)
|
||||||
|
if err != nil {
|
||||||
|
if uniqueViolation(err) {
|
||||||
|
return errors.New("this user is already registered")
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("unable to register the user: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.config.SetUser(name); err != nil {
|
||||||
|
return fmt.Errorf("unable to update the configuration: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Successfully registered %s.\n", user.Name)
|
||||||
|
fmt.Println("DEBUG:", user)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func handlerReset(s *state, _ command) error {
|
||||||
|
if err := s.db.DeleteAllUsers(context.Background()); err != nil {
|
||||||
|
fmt.Errorf("unable to delete the users from the database: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("Successfully removed all users from the database.")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func handlerUsers(s *state, _ command) error {
|
||||||
|
users, err := s.db.GetAllUsers(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
fmt.Errorf("unable to get the users from the database: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(users) == 0 {
|
||||||
|
fmt.Println("There are no registered users.")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Registered users:\n\n")
|
||||||
|
|
||||||
|
for _, user := range users {
|
||||||
|
if user.Name == s.config.CurrentUsername {
|
||||||
|
fmt.Printf("- %s (current)\n", user.Name)
|
||||||
|
} else {
|
||||||
|
fmt.Printf("- %s\n", user.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func handlerAgg(s *state, cmd command) error {
|
||||||
|
if len(cmd.args) != 1 {
|
||||||
|
return fmt.Errorf("unexpected number of arguments: want 1, got %d", len(cmd.args))
|
||||||
|
}
|
||||||
|
|
||||||
|
intervalArg := cmd.args[0]
|
||||||
|
|
||||||
|
interval, err := time.ParseDuration(intervalArg)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to parse the interval: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Fetching feeds every %s\n", interval.String())
|
||||||
|
|
||||||
|
tick := time.Tick(interval)
|
||||||
|
|
||||||
|
for range tick {
|
||||||
|
if err := scrapeFeeds(s); err != nil {
|
||||||
|
fmt.Println("ERROR: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func handlerAddFeed(s *state, cmd command, user database.User) error {
|
||||||
|
if len(cmd.args) != 2 {
|
||||||
|
return fmt.Errorf("unexpected number of arguments: want 2, got %d", len(cmd.args))
|
||||||
|
}
|
||||||
|
|
||||||
|
name, url := cmd.args[0], cmd.args[1]
|
||||||
|
|
||||||
|
timestamp := time.Now()
|
||||||
|
|
||||||
|
createdFeedArgs := database.CreateFeedParams{
|
||||||
|
ID: uuid.New(),
|
||||||
|
CreatedAt: timestamp,
|
||||||
|
UpdatedAt: timestamp,
|
||||||
|
Name: name,
|
||||||
|
Url: url,
|
||||||
|
UserID: user.ID,
|
||||||
|
}
|
||||||
|
|
||||||
|
feed, err := s.db.CreateFeed(context.Background(), createdFeedArgs)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to add the feed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("Successfully added the feed.")
|
||||||
|
|
||||||
|
fmt.Println("DEBUG:", feed)
|
||||||
|
|
||||||
|
createFeedFollowArgs := database.CreateFeedFollowParams{
|
||||||
|
ID: uuid.New(),
|
||||||
|
CreatedAt: timestamp,
|
||||||
|
UpdatedAt: timestamp,
|
||||||
|
UserID: user.ID,
|
||||||
|
FeedID: feed.ID,
|
||||||
|
}
|
||||||
|
|
||||||
|
followRecord, err := s.db.CreateFeedFollow(context.Background(), createFeedFollowArgs)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to create the feed follow record in the database: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("You are now following the feed %q.\n", followRecord.FeedName)
|
||||||
|
fmt.Println("DEBUG:", followRecord)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func handlerFeeds(s *state, _ command) error {
|
||||||
|
feeds, err := s.db.GetAllFeeds(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to get the feeds from the database: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Feeds:\n\n")
|
||||||
|
|
||||||
|
for _, feed := range feeds {
|
||||||
|
user, err := s.db.GetUserByID(context.Background(), feed.UserID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf(
|
||||||
|
"unable to get the creator of %s: %w",
|
||||||
|
feed.Name,
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf(
|
||||||
|
"- Name: %s\n URL: %s\n Created by: %s\n",
|
||||||
|
feed.Name,
|
||||||
|
feed.Url,
|
||||||
|
user.Name,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func handlerFollow(s *state, cmd command, user database.User) error {
|
||||||
|
if len(cmd.args) != 1 {
|
||||||
|
return fmt.Errorf("unexpected number of arguments: want 2, got %d", len(cmd.args))
|
||||||
|
}
|
||||||
|
|
||||||
|
url := cmd.args[0]
|
||||||
|
|
||||||
|
feed, err := s.db.GetFeedByUrl(context.Background(), url)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to get the feed data from the database: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
timestamp := time.Now()
|
||||||
|
|
||||||
|
args := database.CreateFeedFollowParams{
|
||||||
|
ID: uuid.New(),
|
||||||
|
CreatedAt: timestamp,
|
||||||
|
UpdatedAt: timestamp,
|
||||||
|
UserID: user.ID,
|
||||||
|
FeedID: feed.ID,
|
||||||
|
}
|
||||||
|
|
||||||
|
followRecord, err := s.db.CreateFeedFollow(context.Background(), args)
|
||||||
|
if err != nil {
|
||||||
|
if uniqueViolation(err) {
|
||||||
|
return errors.New("you are already following this feed")
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("unable to create the feed follow record in the database: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("You are now following the feed %q.\n", followRecord.FeedName)
|
||||||
|
fmt.Println("DEBUG:", followRecord)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func handlerFollowing(s *state, _ command, user database.User) error {
|
||||||
|
following, err := s.db.GetFeedFollowsForUser(context.Background(), user.ID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to get the list of feeds from the database: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(following) == 0 {
|
||||||
|
fmt.Println("You are not following any feeds.")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("\nYou are following:\n\n")
|
||||||
|
|
||||||
|
for _, feed := range following {
|
||||||
|
fmt.Printf("- %s\n", feed)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func handlerUnfollow(s *state, cmd command, user database.User) error {
|
||||||
|
if len(cmd.args) != 1 {
|
||||||
|
return fmt.Errorf("unexpected number of arguments: want 2, got %d", len(cmd.args))
|
||||||
|
}
|
||||||
|
|
||||||
|
url := cmd.args[0]
|
||||||
|
|
||||||
|
feed, err := s.db.GetFeedByUrl(context.Background(), url)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to get the feed data from the database: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
args := database.DeleteFeedFollowParams{
|
||||||
|
UserID: user.ID,
|
||||||
|
FeedID: feed.ID,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.db.DeleteFeedFollow(context.Background(), args); err != nil {
|
||||||
|
return fmt.Errorf("unable to delete the feed follow record from the database: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("You have successfully unfollowed %q.\n", feed.Name)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func handlerBrowse(s *state, cmd command, user database.User) error {
|
||||||
|
if len(cmd.args) > 1 {
|
||||||
|
return fmt.Errorf("unexpected number of arguments: want 0 or 1, got %d", len(cmd.args))
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
|
||||||
|
limit := 2
|
||||||
|
|
||||||
|
if len(cmd.args) == 1 {
|
||||||
|
limit, err = strconv.Atoi(cmd.args[0])
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to convert %s to a number: %w", cmd.args[0], err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
args := database.GetPostsForUserParams{
|
||||||
|
UserID: user.ID,
|
||||||
|
Limit: int32(limit),
|
||||||
|
}
|
||||||
|
|
||||||
|
posts, err := s.db.GetPostsForUser(context.Background(), args)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to get the posts: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("\nPosts:\n\n")
|
||||||
|
|
||||||
|
for _, post := range posts {
|
||||||
|
fmt.Printf(
|
||||||
|
"- Title: %s\n URL: %s\n Published at: %s\n",
|
||||||
|
post.Title,
|
||||||
|
post.Url,
|
||||||
|
post.PublishedAt,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func scrapeFeeds(s *state) error {
|
||||||
|
feed, err := s.db.GetNextFeedToFetch(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to get the next feed from the database: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("\nFetching feed from %s\n", feed.Url)
|
||||||
|
|
||||||
|
feedDetails, err := rss.FetchFeed(context.Background(), feed.Url)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to fetch the feed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
timestamp := time.Now()
|
||||||
|
|
||||||
|
lastFetched := sql.NullTime{
|
||||||
|
Time: timestamp,
|
||||||
|
Valid: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
markFeedFetchedArgs := database.MarkFeedFetchedParams{
|
||||||
|
ID: feed.ID,
|
||||||
|
LastFetchedAt: lastFetched,
|
||||||
|
UpdatedAt: timestamp,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.db.MarkFeedFetched(context.Background(), markFeedFetchedArgs); err != nil {
|
||||||
|
return fmt.Errorf("unable to mark the feed as fetched in the database: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
timeParsingFormats := []string{
|
||||||
|
time.RFC1123Z,
|
||||||
|
time.RFC1123,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, item := range feedDetails.Channel.Items {
|
||||||
|
var (
|
||||||
|
pubDate time.Time
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
pubDateFormatted := false
|
||||||
|
|
||||||
|
for _, format := range timeParsingFormats {
|
||||||
|
pubDate, err = time.Parse(format, item.PubDate)
|
||||||
|
if err == nil {
|
||||||
|
pubDateFormatted = true
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !pubDateFormatted {
|
||||||
|
fmt.Printf(
|
||||||
|
"Error: unable to format the publication date (%s) of %q.\n",
|
||||||
|
item.PubDate,
|
||||||
|
item.Title,
|
||||||
|
)
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
timestamp := time.Now()
|
||||||
|
|
||||||
|
args := database.CreatePostParams{
|
||||||
|
ID: uuid.New(),
|
||||||
|
CreatedAt: timestamp,
|
||||||
|
UpdatedAt: timestamp,
|
||||||
|
Title: item.Title,
|
||||||
|
Url: item.Link,
|
||||||
|
Description: item.Description,
|
||||||
|
FeedID: feed.ID,
|
||||||
|
PublishedAt: pubDate,
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = s.db.CreatePost(context.Background(), args)
|
||||||
|
if err != nil && !uniqueViolation(err) {
|
||||||
|
fmt.Printf(
|
||||||
|
"Error: unable to add the post %q to the database: %v.\n",
|
||||||
|
item.Title,
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func uniqueViolation(err error) bool {
|
||||||
|
var pqError *pq.Error
|
||||||
|
|
||||||
|
if errors.As(err, &pqError) {
|
||||||
|
if pqError.Code.Name() == "unique_violation" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
5
go.mod
5
go.mod
|
@ -1,3 +1,8 @@
|
||||||
module codeflow.dananglin.me.uk/apollo/gator
|
module codeflow.dananglin.me.uk/apollo/gator
|
||||||
|
|
||||||
go 1.23.1
|
go 1.23.1
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/google/uuid v1.6.0
|
||||||
|
github.com/lib/pq v1.10.9
|
||||||
|
)
|
||||||
|
|
4
go.sum
Normal file
4
go.sum
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||||
|
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
76
internal/config/config.go
Normal file
76
internal/config/config.go
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
CurrentUsername string `json:"currentUsername"`
|
||||||
|
DBConfig DBConfig `json:"database"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DBConfig struct {
|
||||||
|
URL string `json:"url"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewConfig() (Config, error) {
|
||||||
|
path, err := configFilePath()
|
||||||
|
if err != nil {
|
||||||
|
return Config{}, fmt.Errorf("unable to get the path to the configuration file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return Config{}, fmt.Errorf("unable to read %s: %w", path, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var cfg Config
|
||||||
|
|
||||||
|
if err := json.Unmarshal(data, &cfg); err != nil {
|
||||||
|
return Config{}, fmt.Errorf("unable to decode the JSON data: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return cfg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) SetUser(user string) error {
|
||||||
|
c.CurrentUsername = user
|
||||||
|
|
||||||
|
return write(*c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func configFilePath() (string, error) {
|
||||||
|
userConfigDir, err := os.UserConfigDir()
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("unable to get the user's home config directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
path := filepath.Join(userConfigDir, "gator", "config.json")
|
||||||
|
|
||||||
|
return path, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func write(cfg Config) error {
|
||||||
|
path, err := configFilePath()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to get the path to the configuration file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
file, err := os.Create(path)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to create %s: %w", path, err)
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
encoder := json.NewEncoder(file)
|
||||||
|
encoder.SetIndent("", " ")
|
||||||
|
|
||||||
|
if err := encoder.Encode(cfg); err != nil {
|
||||||
|
return fmt.Errorf("unable to save the config to file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
31
internal/database/db.go
Normal file
31
internal/database/db.go
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// sqlc v1.27.0
|
||||||
|
|
||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DBTX interface {
|
||||||
|
ExecContext(context.Context, string, ...interface{}) (sql.Result, error)
|
||||||
|
PrepareContext(context.Context, string) (*sql.Stmt, error)
|
||||||
|
QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error)
|
||||||
|
QueryRowContext(context.Context, string, ...interface{}) *sql.Row
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(db DBTX) *Queries {
|
||||||
|
return &Queries{db: db}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Queries struct {
|
||||||
|
db DBTX
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) WithTx(tx *sql.Tx) *Queries {
|
||||||
|
return &Queries{
|
||||||
|
db: tx,
|
||||||
|
}
|
||||||
|
}
|
121
internal/database/feed_follows.sql.go
Normal file
121
internal/database/feed_follows.sql.go
Normal file
|
@ -0,0 +1,121 @@
|
||||||
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// sqlc v1.27.0
|
||||||
|
// source: feed_follows.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
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteFeedFollow = `-- name: DeleteFeedFollow :exec
|
||||||
|
DELETE FROM feed_follows
|
||||||
|
WHERE user_id = $1 AND feed_id = $2
|
||||||
|
`
|
||||||
|
|
||||||
|
type DeleteFeedFollowParams struct {
|
||||||
|
UserID uuid.UUID
|
||||||
|
FeedID uuid.UUID
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) DeleteFeedFollow(ctx context.Context, arg DeleteFeedFollowParams) error {
|
||||||
|
_, err := q.db.ExecContext(ctx, deleteFeedFollow, arg.UserID, arg.FeedID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
const getFeedFollowsForUser = `-- name: GetFeedFollowsForUser :many
|
||||||
|
SELECT feeds.name as feeds_name
|
||||||
|
FROM feed_follows
|
||||||
|
INNER JOIN feeds ON feed_follows.feed_id = feeds.id
|
||||||
|
WHERE feed_follows.user_id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetFeedFollowsForUser(ctx context.Context, userID uuid.UUID) ([]string, error) {
|
||||||
|
rows, err := q.db.QueryContext(ctx, getFeedFollowsForUser, userID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []string
|
||||||
|
for rows.Next() {
|
||||||
|
var feeds_name string
|
||||||
|
if err := rows.Scan(&feeds_name); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, feeds_name)
|
||||||
|
}
|
||||||
|
if err := rows.Close(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
160
internal/database/feeds.sql.go
Normal file
160
internal/database/feeds.sql.go
Normal file
|
@ -0,0 +1,160 @@
|
||||||
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// sqlc v1.27.0
|
||||||
|
// source: feeds.sql
|
||||||
|
|
||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
const createFeed = `-- name: CreateFeed :one
|
||||||
|
INSERT INTO feeds(
|
||||||
|
id,
|
||||||
|
created_at,
|
||||||
|
updated_at,
|
||||||
|
name,
|
||||||
|
url,
|
||||||
|
user_id
|
||||||
|
)
|
||||||
|
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
|
||||||
|
Name string
|
||||||
|
Url string
|
||||||
|
UserID uuid.UUID
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) CreateFeed(ctx context.Context, arg CreateFeedParams) (Feed, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, createFeed,
|
||||||
|
arg.ID,
|
||||||
|
arg.CreatedAt,
|
||||||
|
arg.UpdatedAt,
|
||||||
|
arg.Name,
|
||||||
|
arg.Url,
|
||||||
|
arg.UserID,
|
||||||
|
)
|
||||||
|
var i Feed
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.UpdatedAt,
|
||||||
|
&i.Name,
|
||||||
|
&i.Url,
|
||||||
|
&i.UserID,
|
||||||
|
&i.LastFetchedAt,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const getAllFeeds = `-- name: GetAllFeeds :many
|
||||||
|
SELECT id, created_at, updated_at, name, url, user_id, last_fetched_at
|
||||||
|
FROM feeds
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetAllFeeds(ctx context.Context) ([]Feed, error) {
|
||||||
|
rows, err := q.db.QueryContext(ctx, getAllFeeds)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []Feed
|
||||||
|
for rows.Next() {
|
||||||
|
var i Feed
|
||||||
|
if err := rows.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.UpdatedAt,
|
||||||
|
&i.Name,
|
||||||
|
&i.Url,
|
||||||
|
&i.UserID,
|
||||||
|
&i.LastFetchedAt,
|
||||||
|
); 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
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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 ASC 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
|
||||||
|
}
|
||||||
|
|
||||||
|
const markFeedFetched = `-- name: MarkFeedFetched :exec
|
||||||
|
UPDATE feeds
|
||||||
|
SET last_fetched_at = $2, updated_at = $3
|
||||||
|
WHERE id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
type MarkFeedFetchedParams struct {
|
||||||
|
ID uuid.UUID
|
||||||
|
LastFetchedAt sql.NullTime
|
||||||
|
UpdatedAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) MarkFeedFetched(ctx context.Context, arg MarkFeedFetchedParams) error {
|
||||||
|
_, err := q.db.ExecContext(ctx, markFeedFetched, arg.ID, arg.LastFetchedAt, arg.UpdatedAt)
|
||||||
|
return err
|
||||||
|
}
|
48
internal/database/models.go
Normal file
48
internal/database/models.go
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// sqlc v1.27.0
|
||||||
|
|
||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"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 {
|
||||||
|
ID uuid.UUID
|
||||||
|
CreatedAt time.Time
|
||||||
|
UpdatedAt time.Time
|
||||||
|
Name string
|
||||||
|
}
|
119
internal/database/posts.sql.go
Normal file
119
internal/database/posts.sql.go
Normal file
|
@ -0,0 +1,119 @@
|
||||||
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// sqlc v1.27.0
|
||||||
|
// source: posts.sql
|
||||||
|
|
||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
const createPost = `-- name: CreatePost :one
|
||||||
|
INSERT INTO posts (
|
||||||
|
id,
|
||||||
|
created_at,
|
||||||
|
updated_at,
|
||||||
|
title,
|
||||||
|
url,
|
||||||
|
description,
|
||||||
|
published_at,
|
||||||
|
feed_id
|
||||||
|
)
|
||||||
|
VALUES (
|
||||||
|
$1,
|
||||||
|
$2,
|
||||||
|
$3,
|
||||||
|
$4,
|
||||||
|
$5,
|
||||||
|
$6,
|
||||||
|
$7,
|
||||||
|
$8
|
||||||
|
)
|
||||||
|
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
|
||||||
|
Title string
|
||||||
|
Url string
|
||||||
|
Description string
|
||||||
|
PublishedAt time.Time
|
||||||
|
FeedID uuid.UUID
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) CreatePost(ctx context.Context, arg CreatePostParams) (Post, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, createPost,
|
||||||
|
arg.ID,
|
||||||
|
arg.CreatedAt,
|
||||||
|
arg.UpdatedAt,
|
||||||
|
arg.Title,
|
||||||
|
arg.Url,
|
||||||
|
arg.Description,
|
||||||
|
arg.PublishedAt,
|
||||||
|
arg.FeedID,
|
||||||
|
)
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
const getPostsForUser = `-- name: GetPostsForUser :many
|
||||||
|
SELECT title, url, published_at
|
||||||
|
FROM posts
|
||||||
|
WHERE feed_id IN (
|
||||||
|
SELECT feed_id
|
||||||
|
FROM feed_follows
|
||||||
|
WHERE user_id = $1
|
||||||
|
)
|
||||||
|
ORDER BY published_at DESC
|
||||||
|
LIMIT $2
|
||||||
|
`
|
||||||
|
|
||||||
|
type GetPostsForUserParams struct {
|
||||||
|
UserID uuid.UUID
|
||||||
|
Limit int32
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetPostsForUserRow struct {
|
||||||
|
Title string
|
||||||
|
Url string
|
||||||
|
PublishedAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) GetPostsForUser(ctx context.Context, arg GetPostsForUserParams) ([]GetPostsForUserRow, error) {
|
||||||
|
rows, err := q.db.QueryContext(ctx, getPostsForUser, arg.UserID, arg.Limit)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []GetPostsForUserRow
|
||||||
|
for rows.Next() {
|
||||||
|
var i GetPostsForUserRow
|
||||||
|
if err := rows.Scan(&i.Title, &i.Url, &i.PublishedAt); 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
|
||||||
|
}
|
126
internal/database/users.sql.go
Normal file
126
internal/database/users.sql.go
Normal file
|
@ -0,0 +1,126 @@
|
||||||
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// sqlc v1.27.0
|
||||||
|
// source: users.sql
|
||||||
|
|
||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
const createUser = `-- name: CreateUser :one
|
||||||
|
INSERT INTO users (id, created_at, updated_at, name)
|
||||||
|
VALUES (
|
||||||
|
$1,
|
||||||
|
$2,
|
||||||
|
$3,
|
||||||
|
$4
|
||||||
|
)
|
||||||
|
RETURNING id, created_at, updated_at, name
|
||||||
|
`
|
||||||
|
|
||||||
|
type CreateUserParams struct {
|
||||||
|
ID uuid.UUID
|
||||||
|
CreatedAt time.Time
|
||||||
|
UpdatedAt time.Time
|
||||||
|
Name string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (User, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, createUser,
|
||||||
|
arg.ID,
|
||||||
|
arg.CreatedAt,
|
||||||
|
arg.UpdatedAt,
|
||||||
|
arg.Name,
|
||||||
|
)
|
||||||
|
var i User
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.UpdatedAt,
|
||||||
|
&i.Name,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteAllUsers = `-- name: DeleteAllUsers :exec
|
||||||
|
DELETE FROM users
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) DeleteAllUsers(ctx context.Context) error {
|
||||||
|
_, err := q.db.ExecContext(ctx, deleteAllUsers)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
const getAllUsers = `-- name: GetAllUsers :many
|
||||||
|
SELECT id, created_at, updated_at, name
|
||||||
|
FROM users
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetAllUsers(ctx context.Context) ([]User, error) {
|
||||||
|
rows, err := q.db.QueryContext(ctx, getAllUsers)
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
const getUserByID = `-- name: GetUserByID :one
|
||||||
|
SELECT id, created_at, updated_at, name
|
||||||
|
FROM users
|
||||||
|
WHERE id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetUserByID(ctx context.Context, id uuid.UUID) (User, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, getUserByID, id)
|
||||||
|
var i User
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.UpdatedAt,
|
||||||
|
&i.Name,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const getUserByName = `-- name: GetUserByName :one
|
||||||
|
SELECT id, created_at, updated_at, name
|
||||||
|
FROM users
|
||||||
|
WHERE name = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetUserByName(ctx context.Context, name string) (User, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, getUserByName, name)
|
||||||
|
var i User
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.UpdatedAt,
|
||||||
|
&i.Name,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
81
internal/rss/rss.go
Normal file
81
internal/rss/rss.go
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
package rss
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/xml"
|
||||||
|
"fmt"
|
||||||
|
"html"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Feed struct {
|
||||||
|
Channel Channel `xml:"channel"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Channel struct {
|
||||||
|
Title string `xml:"title"`
|
||||||
|
Link string `xml:"link"`
|
||||||
|
Description string `xml:"description"`
|
||||||
|
Items []Item `xml:"item"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Item struct {
|
||||||
|
Title string `xml:"title"`
|
||||||
|
Link string `xml:"link"`
|
||||||
|
Description string `xml:"description"`
|
||||||
|
PubDate string `xml:"pubDate"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func FetchFeed(ctx context.Context, url string) (*Feed, error) {
|
||||||
|
request, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("received an error creating the HTTP request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
request.Header.Set("User-Agent", "Gator/0.0.0")
|
||||||
|
|
||||||
|
client := http.Client{}
|
||||||
|
|
||||||
|
response, err := client.Do(request)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error getting the response from the server: %w", err)
|
||||||
|
}
|
||||||
|
defer response.Body.Close()
|
||||||
|
|
||||||
|
if response.StatusCode >= 400 {
|
||||||
|
return nil, fmt.Errorf(
|
||||||
|
"received a bad status from %s: (%d) %s",
|
||||||
|
url,
|
||||||
|
response.StatusCode,
|
||||||
|
response.Status,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := io.ReadAll(response.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf(
|
||||||
|
"unable to read the response from the server: %w",
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
var feed Feed
|
||||||
|
|
||||||
|
if err := xml.Unmarshal(data, &feed); err != nil {
|
||||||
|
return nil, fmt.Errorf(
|
||||||
|
"unable to decode the XML data: %w",
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
feed.Channel.Title = html.UnescapeString(feed.Channel.Title)
|
||||||
|
feed.Channel.Description = html.UnescapeString(feed.Channel.Description)
|
||||||
|
|
||||||
|
for _, item := range feed.Channel.Items {
|
||||||
|
item.Title = html.UnescapeString(item.Title)
|
||||||
|
item.Description = html.UnescapeString(item.Description)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &feed, nil
|
||||||
|
}
|
|
@ -56,7 +56,6 @@ func Lint() error {
|
||||||
// To rebuild packages that are already up-to-date set GATOR_BUILD_REBUILD_ALL=1
|
// To rebuild packages that are already up-to-date set GATOR_BUILD_REBUILD_ALL=1
|
||||||
// To enable verbose mode set GATOR_BUILD_VERBOSE=1
|
// To enable verbose mode set GATOR_BUILD_VERBOSE=1
|
||||||
func Build() error {
|
func Build() error {
|
||||||
main := "main.go"
|
|
||||||
flags := ldflags()
|
flags := ldflags()
|
||||||
build := sh.RunCmd("go", "build")
|
build := sh.RunCmd("go", "build")
|
||||||
args := []string{"-ldflags=" + flags, "-o", binary}
|
args := []string{"-ldflags=" + flags, "-o", binary}
|
||||||
|
@ -69,7 +68,7 @@ func Build() error {
|
||||||
args = append(args, "-v")
|
args = append(args, "-v")
|
||||||
}
|
}
|
||||||
|
|
||||||
args = append(args, main)
|
args = append(args, ".")
|
||||||
|
|
||||||
return build(args...)
|
return build(args...)
|
||||||
}
|
}
|
||||||
|
|
67
main.go
67
main.go
|
@ -1,10 +1,21 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
|
"codeflow.dananglin.me.uk/apollo/gator/internal/config"
|
||||||
|
"codeflow.dananglin.me.uk/apollo/gator/internal/database"
|
||||||
|
_ "github.com/lib/pq"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type state struct {
|
||||||
|
db *database.Queries
|
||||||
|
config *config.Config
|
||||||
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
binaryVersion string
|
binaryVersion string
|
||||||
buildTime string
|
buildTime string
|
||||||
|
@ -20,5 +31,59 @@ func main() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func run() error {
|
func run() error {
|
||||||
return nil
|
cfg, err := config.NewConfig()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to load the configuration: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
db, err := sql.Open("postgres", cfg.DBConfig.URL)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to open a connection to the database: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
s := state{
|
||||||
|
db: database.New(db),
|
||||||
|
config: &cfg,
|
||||||
|
}
|
||||||
|
|
||||||
|
cmds := commands{
|
||||||
|
commandMap: make(map[string]commandFunc),
|
||||||
|
}
|
||||||
|
|
||||||
|
cmds.register("login", handlerLogin)
|
||||||
|
cmds.register("register", handlerRegister)
|
||||||
|
cmds.register("reset", handlerReset)
|
||||||
|
cmds.register("users", handlerUsers)
|
||||||
|
cmds.register("agg", handlerAgg)
|
||||||
|
cmds.register("addfeed", middlewareLoggedIn(handlerAddFeed))
|
||||||
|
cmds.register("feeds", handlerFeeds)
|
||||||
|
cmds.register("follow", middlewareLoggedIn(handlerFollow))
|
||||||
|
cmds.register("unfollow", middlewareLoggedIn(handlerUnfollow))
|
||||||
|
cmds.register("following", middlewareLoggedIn(handlerFollowing))
|
||||||
|
cmds.register("browse", middlewareLoggedIn(handlerBrowse))
|
||||||
|
|
||||||
|
cmd, err := parseArgs(os.Args[1:])
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to parse the command: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return cmds.run(&s, cmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseArgs(args []string) (command, error) {
|
||||||
|
if len(args) == 0 {
|
||||||
|
return command{}, errors.New("no arguments given")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(args) == 1 {
|
||||||
|
return command{
|
||||||
|
name: args[0],
|
||||||
|
args: make([]string, 0),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return command{
|
||||||
|
name: args[0],
|
||||||
|
args: args[1:],
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
19
middleware.go
Normal file
19
middleware.go
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"codeflow.dananglin.me.uk/apollo/gator/internal/database"
|
||||||
|
)
|
||||||
|
|
||||||
|
func middlewareLoggedIn(handler func(s *state, cmd command, user database.User) error) commandFunc {
|
||||||
|
return func(s *state, cmd command) error {
|
||||||
|
user, err := s.db.GetUserByName(context.Background(), s.config.CurrentUsername)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to get the user from the database: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return handler(s, cmd, user)
|
||||||
|
}
|
||||||
|
}
|
32
sql/queries/feed_follows.sql
Normal file
32
sql/queries/feed_follows.sql
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
-- 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;
|
||||||
|
|
||||||
|
-- name: GetFeedFollowsForUser :many
|
||||||
|
SELECT feeds.name as feeds_name
|
||||||
|
FROM feed_follows
|
||||||
|
INNER JOIN feeds ON feed_follows.feed_id = feeds.id
|
||||||
|
WHERE feed_follows.user_id = $1;
|
||||||
|
|
||||||
|
-- name: DeleteFeedFollow :exec
|
||||||
|
DELETE FROM feed_follows
|
||||||
|
WHERE user_id = $1 AND feed_id = $2;
|
37
sql/queries/feeds.sql
Normal file
37
sql/queries/feeds.sql
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
-- name: CreateFeed :one
|
||||||
|
INSERT INTO feeds(
|
||||||
|
id,
|
||||||
|
created_at,
|
||||||
|
updated_at,
|
||||||
|
name,
|
||||||
|
url,
|
||||||
|
user_id
|
||||||
|
)
|
||||||
|
VALUES (
|
||||||
|
$1,
|
||||||
|
$2,
|
||||||
|
$3,
|
||||||
|
$4,
|
||||||
|
$5,
|
||||||
|
$6
|
||||||
|
)
|
||||||
|
RETURNING *;
|
||||||
|
|
||||||
|
-- name: GetAllFeeds :many
|
||||||
|
SELECT *
|
||||||
|
FROM feeds;
|
||||||
|
|
||||||
|
-- name: GetFeedByUrl :one
|
||||||
|
SELECT *
|
||||||
|
FROM feeds
|
||||||
|
WHERE url = $1;
|
||||||
|
|
||||||
|
-- name: MarkFeedFetched :exec
|
||||||
|
UPDATE feeds
|
||||||
|
SET last_fetched_at = $2, updated_at = $3
|
||||||
|
WHERE id = $1;
|
||||||
|
|
||||||
|
-- name: GetNextFeedToFetch :one
|
||||||
|
SELECT *
|
||||||
|
FROM feeds
|
||||||
|
ORDER BY last_fetched_at ASC NULLS FIRST LIMIT 1;
|
33
sql/queries/posts.sql
Normal file
33
sql/queries/posts.sql
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
-- name: CreatePost :one
|
||||||
|
INSERT INTO posts (
|
||||||
|
id,
|
||||||
|
created_at,
|
||||||
|
updated_at,
|
||||||
|
title,
|
||||||
|
url,
|
||||||
|
description,
|
||||||
|
published_at,
|
||||||
|
feed_id
|
||||||
|
)
|
||||||
|
VALUES (
|
||||||
|
$1,
|
||||||
|
$2,
|
||||||
|
$3,
|
||||||
|
$4,
|
||||||
|
$5,
|
||||||
|
$6,
|
||||||
|
$7,
|
||||||
|
$8
|
||||||
|
)
|
||||||
|
RETURNING *;
|
||||||
|
|
||||||
|
-- name: GetPostsForUser :many
|
||||||
|
SELECT title, url, published_at
|
||||||
|
FROM posts
|
||||||
|
WHERE feed_id IN (
|
||||||
|
SELECT feed_id
|
||||||
|
FROM feed_follows
|
||||||
|
WHERE user_id = $1
|
||||||
|
)
|
||||||
|
ORDER BY published_at DESC
|
||||||
|
LIMIT $2;
|
26
sql/queries/users.sql
Normal file
26
sql/queries/users.sql
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
-- name: CreateUser :one
|
||||||
|
INSERT INTO users (id, created_at, updated_at, name)
|
||||||
|
VALUES (
|
||||||
|
$1,
|
||||||
|
$2,
|
||||||
|
$3,
|
||||||
|
$4
|
||||||
|
)
|
||||||
|
RETURNING *;
|
||||||
|
|
||||||
|
-- name: GetUserByName :one
|
||||||
|
SELECT *
|
||||||
|
FROM users
|
||||||
|
WHERE name = $1;
|
||||||
|
|
||||||
|
-- name: GetUserByID :one
|
||||||
|
SELECT *
|
||||||
|
FROM users
|
||||||
|
WHERE id = $1;
|
||||||
|
|
||||||
|
-- name: GetAllUsers :many
|
||||||
|
SELECT *
|
||||||
|
FROM users;
|
||||||
|
|
||||||
|
-- name: DeleteAllUsers :exec
|
||||||
|
DELETE FROM users;
|
10
sql/schema/001_users.sql
Normal file
10
sql/schema/001_users.sql
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
-- +goose Up
|
||||||
|
CREATE TABLE users (
|
||||||
|
id UUID PRIMARY KEY,
|
||||||
|
created_at TIMESTAMP NOT NULL,
|
||||||
|
updated_at TIMESTAMP NOT NULL,
|
||||||
|
name varchar(255) NOT NULL UNIQUE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- +goose Down
|
||||||
|
DROP TABLE users;
|
13
sql/schema/002_feeds.sql
Normal file
13
sql/schema/002_feeds.sql
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
-- +goose Up
|
||||||
|
CREATE TABLE feeds (
|
||||||
|
id UUID PRIMARY KEY,
|
||||||
|
created_at TIMESTAMP NOT NULL,
|
||||||
|
updated_at TIMESTAMP NOT NULL,
|
||||||
|
name varchar(255) NOT NULL,
|
||||||
|
url varchar(255) NOT NULL UNIQUE,
|
||||||
|
user_id UUID NOT NULL,
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- +goose Down
|
||||||
|
DROP TABLE feeds;
|
14
sql/schema/003_feed_follows.sql
Normal file
14
sql/schema/003_feed_follows.sql
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
-- +goose Up
|
||||||
|
CREATE TABLE feed_follows (
|
||||||
|
id UUID PRIMARY KEY,
|
||||||
|
created_at TIMESTAMP NOT NULL,
|
||||||
|
updated_at TIMESTAMP NOT NULL,
|
||||||
|
user_id UUID NOT NULL,
|
||||||
|
feed_id UUID NOT NULL,
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (feed_id) REFERENCES feeds(id) ON DELETE CASCADE,
|
||||||
|
UNIQUE(user_id, feed_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- +goose Down
|
||||||
|
DROP TABLE feed_follows;
|
5
sql/schema/004_add_last_fetched_at_to_feeds.sql
Normal file
5
sql/schema/004_add_last_fetched_at_to_feeds.sql
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
-- +goose Up
|
||||||
|
ALTER TABLE feeds ADD COLUMN last_fetched_at TIMESTAMP;
|
||||||
|
|
||||||
|
-- +goose Down
|
||||||
|
ALTER TABLE feeds DROP COLUMN last_fetched_at;
|
15
sql/schema/005_add_posts_table.sql
Normal file
15
sql/schema/005_add_posts_table.sql
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
-- +goose Up
|
||||||
|
CREATE TABLE posts (
|
||||||
|
id UUID PRIMARY KEY,
|
||||||
|
created_at TIMESTAMP NOT NULL,
|
||||||
|
updated_at TIMESTAMP NOT NULL,
|
||||||
|
title TEXT NOT NULL DEFAULT 'Undefined',
|
||||||
|
url varchar(255) NOT NULL UNIQUE,
|
||||||
|
description TEXT NOT NULL DEFAULT 'Undefined',
|
||||||
|
published_at TIMESTAMP NOT NULL,
|
||||||
|
feed_id UUID NOT NULL,
|
||||||
|
FOREIGN KEY (feed_id) REFERENCES feeds(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- +goose Down
|
||||||
|
DROP TABLE posts;
|
9
sqlc.yaml
Normal file
9
sqlc.yaml
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
---
|
||||||
|
version: "2"
|
||||||
|
sql:
|
||||||
|
- schema: "sql/schema"
|
||||||
|
queries: "sql/queries"
|
||||||
|
engine: "postgresql"
|
||||||
|
gen:
|
||||||
|
go:
|
||||||
|
out: "internal/database"
|
Loading…
Reference in a new issue