Compare commits
3
Commits
55a4a1285d
...
c012f6008c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c012f6008c | ||
|
|
fb53b052b0 | ||
|
|
c1325925ed |
@@ -0,0 +1 @@
|
||||
.sqls.yml
|
||||
@@ -1,3 +1,8 @@
|
||||
module github.com/StevanFreeborn/gator
|
||||
|
||||
go 1.26.3
|
||||
|
||||
require (
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/lib/pq v1.12.3 // indirect
|
||||
)
|
||||
|
||||
@@ -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.12.3 h1:tTWxr2YLKwIvK90ZXEw8GP7UFHtcbTtty8zsI+YjrfQ=
|
||||
github.com/lib/pq v1.12.3/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA=
|
||||
+193
-1
@@ -1,9 +1,18 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"context"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/StevanFreeborn/gator/internal/database"
|
||||
"github.com/StevanFreeborn/gator/internal/rss"
|
||||
"github.com/StevanFreeborn/gator/internal/state"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type Command struct {
|
||||
@@ -36,6 +45,12 @@ func NewRegistry() *CommandRegistry {
|
||||
|
||||
commands := []*Command{
|
||||
loginCommand(),
|
||||
registerCommand(),
|
||||
resetCommand(),
|
||||
usersCommand(),
|
||||
aggCommand(),
|
||||
addFeedCommand(),
|
||||
feedsCommand(),
|
||||
}
|
||||
|
||||
for _, cmd := range commands {
|
||||
@@ -63,7 +78,13 @@ func loginCommand() *Command {
|
||||
|
||||
username := s.Arguments[0]
|
||||
|
||||
err := s.Config.SetUser(username)
|
||||
_, err := s.Database.GetUserByName(context.Background(), username)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to login")
|
||||
}
|
||||
|
||||
err = s.Config.SetUser(username)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -74,3 +95,174 @@ func loginCommand() *Command {
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func registerCommand() *Command {
|
||||
return newCommand("register", func(s *state.State) error {
|
||||
if len(s.Arguments) == 0 {
|
||||
return fmt.Errorf("Did not receive expected username argument")
|
||||
}
|
||||
|
||||
createUserParams := database.CreateUserParams{
|
||||
ID: uuid.New(),
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
Name: s.Arguments[0],
|
||||
}
|
||||
|
||||
createdUser, err := s.Database.CreateUser(context.Background(), createUserParams)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s.Config.SetUser(createdUser.Name)
|
||||
|
||||
fmt.Printf("Successfully registered user '%s':\n", createdUser.Name)
|
||||
fmt.Printf(" Id => %s\n", createdUser.ID)
|
||||
fmt.Printf(" CreatedAt => %s\n", createdUser.CreatedAt)
|
||||
fmt.Printf(" UpdatedAt => %s\n", createdUser.UpdatedAt)
|
||||
fmt.Printf("Current user set to user '%s':\n", createdUser.Name)
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func resetCommand() *Command {
|
||||
return newCommand("reset", func(s *state.State) error {
|
||||
err := s.Database.DeleteAllUsers(context.Background())
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println("Successfully delete all users")
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func usersCommand() *Command {
|
||||
return newCommand("users", func(s *state.State) error {
|
||||
users, err := s.Database.GetAllUsers(context.Background())
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
slices.SortFunc(users, func(a, b database.User) int {
|
||||
return cmp.Compare(a.Name, b.Name)
|
||||
})
|
||||
|
||||
for _, user := range users {
|
||||
msg := "* %s"
|
||||
|
||||
if user.Name == s.Config.CurrentUserName {
|
||||
msg += " (current)"
|
||||
}
|
||||
|
||||
fmt.Printf(msg+"\n", user.Name)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func aggCommand() *Command {
|
||||
return newCommand("agg", func(s *state.State) error {
|
||||
// if len(s.Arguments) == 0 {
|
||||
// return fmt.Errorf("Did not receive expected feed argument")
|
||||
// }
|
||||
|
||||
// feed := s.Arguments[0]
|
||||
//
|
||||
// validUrl, err := url.Parse(feed)
|
||||
//
|
||||
// if err != nil {
|
||||
// return fmt.Errorf("Feed '%s' is not a valid url", feed)
|
||||
// }
|
||||
|
||||
rssFeed, err := rss.FetchFeed(context.Background(), "https://www.wagslane.dev/index.xml")
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("%v\n", rssFeed)
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func addFeedCommand() *Command {
|
||||
return newCommand("addfeed", func(s *state.State) error {
|
||||
if len(s.Arguments) < 2 {
|
||||
return fmt.Errorf("Did not receive expected feed name and url")
|
||||
}
|
||||
|
||||
feedName := s.Arguments[0]
|
||||
feedUrl := s.Arguments[1]
|
||||
|
||||
validatedFeedUrl, err := url.Parse(feedUrl)
|
||||
|
||||
if strings.TrimSpace(feedName) == "" {
|
||||
return fmt.Errorf("Feed name cannot be empty")
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("Feed url '%s' is not a valid url", feedUrl)
|
||||
}
|
||||
|
||||
currentUser, err := s.GetCurrentUser(context.Background())
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("Currently logged in user does not exist. Cannot add feed for non-existent user.")
|
||||
}
|
||||
|
||||
createFeedParams := database.CreateFeedParams{
|
||||
ID: uuid.New(),
|
||||
UserID: currentUser.ID,
|
||||
Name: feedName,
|
||||
Url: validatedFeedUrl.String(),
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
createdFeed, err := s.Database.CreateFeed(context.Background(), createFeedParams)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("Successfully added feed '%s' with url '%s'\n", createdFeed.Name, createdFeed.Url)
|
||||
fmt.Printf(" Id => %s\n", createdFeed.ID)
|
||||
fmt.Printf(" UserId => %s\n", createdFeed.UserID)
|
||||
fmt.Printf(" CreatedAt => %s\n", createdFeed.CreatedAt)
|
||||
fmt.Printf(" UpdatedAt => %s\n", createdFeed.UpdatedAt)
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func feedsCommand() *Command {
|
||||
return newCommand("feeds", func(s *state.State) error {
|
||||
feeds, err := s.Database.GetAllFeeds(context.Background())
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, feed := range feeds {
|
||||
feedsUserName := "Unknown"
|
||||
|
||||
user, err := s.Database.GetUserById(context.Background(), feed.UserID)
|
||||
|
||||
if err == nil {
|
||||
feedsUserName = user.Name
|
||||
}
|
||||
|
||||
fmt.Printf("* %s [%s] (%s)\n", feed.Name, feed.Url, feedsUserName)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.31.1
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.31.1
|
||||
// source: feeds.sql
|
||||
|
||||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"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
|
||||
`
|
||||
|
||||
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,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getAllFeeds = `-- name: GetAllFeeds :many
|
||||
SELECT id, created_at, updated_at, name, url, user_id 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,
|
||||
); 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
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.31.1
|
||||
|
||||
package database
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type Feed struct {
|
||||
ID uuid.UUID
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
Name string
|
||||
Url string
|
||||
UserID uuid.UUID
|
||||
}
|
||||
|
||||
type User struct {
|
||||
ID uuid.UUID
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
Name string
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.31.1
|
||||
// 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
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package rss
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/xml"
|
||||
"html"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type RSSItem struct {
|
||||
Title string `xml:"title"`
|
||||
Link string `xml:"link"`
|
||||
Description string `xml:"description"`
|
||||
PubDate string `xml:"pubDate"`
|
||||
}
|
||||
|
||||
type RSSChannel struct {
|
||||
Title string `xml:"title"`
|
||||
Link string `xml:"link"`
|
||||
Description string `xml:"description"`
|
||||
Item []RSSItem `xml:"item"`
|
||||
}
|
||||
|
||||
type RSSFeed struct {
|
||||
Channel RSSChannel `xml:"channel"`
|
||||
}
|
||||
|
||||
func FetchFeed(ctx context.Context, feedURL string) (*RSSFeed, error) {
|
||||
var rssFeed RSSFeed
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, feedURL, nil)
|
||||
req.Header.Set("User-Agent", "gator")
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res, err := http.DefaultClient.Do(req)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer res.Body.Close()
|
||||
|
||||
err = xml.NewDecoder(res.Body).Decode(&rssFeed)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rssFeed.Channel.Title = html.UnescapeString(rssFeed.Channel.Title)
|
||||
rssFeed.Channel.Description = html.UnescapeString(rssFeed.Channel.Description)
|
||||
|
||||
for _, item := range rssFeed.Channel.Item {
|
||||
item.Title = html.UnescapeString(item.Title)
|
||||
item.Description = html.UnescapeString(item.Description)
|
||||
}
|
||||
|
||||
return &rssFeed, nil
|
||||
}
|
||||
+13
-3
@@ -1,12 +1,22 @@
|
||||
package state
|
||||
|
||||
import "github.com/StevanFreeborn/gator/internal/config"
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/StevanFreeborn/gator/internal/config"
|
||||
"github.com/StevanFreeborn/gator/internal/database"
|
||||
)
|
||||
|
||||
type State struct {
|
||||
Database *database.Queries
|
||||
Config *config.Config
|
||||
Arguments []string
|
||||
}
|
||||
|
||||
func NewState(c *config.Config, args []string) *State {
|
||||
return &State{Config: c, Arguments: args}
|
||||
func NewState(d *database.Queries, c *config.Config, args []string) *State {
|
||||
return &State{Database: d, Config: c, Arguments: args}
|
||||
}
|
||||
|
||||
func (s *State) GetCurrentUser(ctx context.Context) (database.User, error) {
|
||||
return s.Database.GetUserByName(ctx, s.Config.CurrentUserName)
|
||||
}
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/StevanFreeborn/gator/internal/command"
|
||||
"github.com/StevanFreeborn/gator/internal/config"
|
||||
"github.com/StevanFreeborn/gator/internal/database"
|
||||
"github.com/StevanFreeborn/gator/internal/state"
|
||||
_ "github.com/lib/pq"
|
||||
)
|
||||
|
||||
func main() {
|
||||
@@ -19,19 +22,28 @@ func main() {
|
||||
|
||||
if err != nil {
|
||||
fmt.Printf("Error reading config file: %s", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
db, err := sql.Open("postgres", c.DbUrl)
|
||||
|
||||
if err != nil {
|
||||
fmt.Printf("Error connecting to database: %s", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
cmd := os.Args[1]
|
||||
args := os.Args[2:]
|
||||
|
||||
s := state.NewState(c, args)
|
||||
dbQueries := database.New(db)
|
||||
s := state.NewState(dbQueries, c, args)
|
||||
|
||||
registry := command.NewRegistry()
|
||||
err = registry.RunCommand(cmd, s)
|
||||
|
||||
if err != nil {
|
||||
fmt.Printf("Error running '%s' command: %s\n", cmd, err)
|
||||
os.Exit(2)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
os.Exit(0)
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
-- 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 id, created_at, updated_at, name, url, user_id FROM feeds;
|
||||
@@ -0,0 +1,23 @@
|
||||
-- name: CreateUser :one
|
||||
INSERT INTO users (id, created_at, updated_at, name)
|
||||
VALUES (
|
||||
$1,
|
||||
$2,
|
||||
$3,
|
||||
$4
|
||||
)
|
||||
RETURNING *;
|
||||
|
||||
-- name: GetUserByName :one
|
||||
SELECT id, created_at, updated_at, name FROM users
|
||||
WHERE name = $1;
|
||||
|
||||
-- name: GetUserById :one
|
||||
SELECT id, created_at, updated_at, name FROM users
|
||||
WHERE id = $1;
|
||||
|
||||
-- name: GetAllUsers :many
|
||||
SELECT id, created_at, updated_at, name FROM users;
|
||||
|
||||
-- name: DeleteAllUsers :exec
|
||||
DELETE FROM users;
|
||||
@@ -1,8 +1,9 @@
|
||||
-- TODO: Finish writing SQL migration
|
||||
-- for users table
|
||||
-- +goose Up
|
||||
CREATE TABLE users (
|
||||
id UUID
|
||||
id UUID PRIMARY KEY,
|
||||
created_at TIMESTAMP NOT NULL,
|
||||
updated_at TIMESTAMP NOT NULL,
|
||||
name TEXT UNIQUE NOT NULL
|
||||
);
|
||||
|
||||
-- +goose Down
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
-- +goose Up
|
||||
CREATE TABLE feeds (
|
||||
id UUID PRIMARY KEY,
|
||||
created_at TIMESTAMP NOT NULL,
|
||||
updated_at TIMESTAMP NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
url TEXT UNIQUE NOT NULL,
|
||||
user_id UUID NOT NULL,
|
||||
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- +goose Down
|
||||
DROP TABLE feeds;
|
||||
Reference in New Issue
Block a user