postgres and such
This commit is contained in:
parent
088365c411
commit
6f7bcc9503
1
.env.example
Normal file
1
.env.example
Normal file
@ -0,0 +1 @@
|
|||||||
|
DATABASE_URL=postgres://postgres@localhost:5432/todos
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -24,3 +24,5 @@ go.work
|
|||||||
.direnv/
|
.direnv/
|
||||||
|
|
||||||
tmp/
|
tmp/
|
||||||
|
|
||||||
|
.env
|
||||||
|
@ -562,6 +562,10 @@ video {
|
|||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.table {
|
||||||
|
display: table;
|
||||||
|
}
|
||||||
|
|
||||||
.flex-col {
|
.flex-col {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
14
go.mod
14
go.mod
@ -4,4 +4,16 @@ go 1.22.1
|
|||||||
|
|
||||||
require github.com/a-h/templ v0.2.707
|
require github.com/a-h/templ v0.2.707
|
||||||
|
|
||||||
require github.com/google/uuid v1.6.0 // indirect
|
require (
|
||||||
|
github.com/gofrs/uuid v4.4.0+incompatible // indirect
|
||||||
|
github.com/gofrs/uuid/v5 v5.0.0 // indirect
|
||||||
|
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||||
|
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
|
||||||
|
github.com/jackc/pgx-gofrs-uuid v0.0.0-20230224015001-1d428863c2e2 // indirect
|
||||||
|
github.com/jackc/pgx/v5 v5.6.0 // indirect
|
||||||
|
github.com/jackc/puddle/v2 v2.2.1 // indirect
|
||||||
|
github.com/joho/godotenv v1.5.1 // indirect
|
||||||
|
golang.org/x/crypto v0.17.0 // indirect
|
||||||
|
golang.org/x/sync v0.1.0 // indirect
|
||||||
|
golang.org/x/text v0.14.0 // indirect
|
||||||
|
)
|
||||||
|
29
go.sum
29
go.sum
@ -1,6 +1,35 @@
|
|||||||
github.com/a-h/templ v0.2.707 h1:T1Gkd2ugbRglZ9rYw/VBchWOSZVKmetDbBkm4YubM7U=
|
github.com/a-h/templ v0.2.707 h1:T1Gkd2ugbRglZ9rYw/VBchWOSZVKmetDbBkm4YubM7U=
|
||||||
github.com/a-h/templ v0.2.707/go.mod h1:5cqsugkq9IerRNucNsI4DEamdHPsoGMQy99DzydLhM8=
|
github.com/a-h/templ v0.2.707/go.mod h1:5cqsugkq9IerRNucNsI4DEamdHPsoGMQy99DzydLhM8=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA=
|
||||||
|
github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||||
|
github.com/gofrs/uuid/v5 v5.0.0 h1:p544++a97kEL+svbcFbCQVM9KFu0Yo25UoISXGNNH9M=
|
||||||
|
github.com/gofrs/uuid/v5 v5.0.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8=
|
||||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
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/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||||
|
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||||
|
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
|
||||||
|
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||||
|
github.com/jackc/pgx-gofrs-uuid v0.0.0-20230224015001-1d428863c2e2 h1:QWdhlQz98hUe1xmjADOl2mr8ERLrOqj0KWLdkrnNsRQ=
|
||||||
|
github.com/jackc/pgx-gofrs-uuid v0.0.0-20230224015001-1d428863c2e2/go.mod h1:Ti7pyNDU/UpXKmBTeFgxTvzYDM9xHLiYKMsLdt4b9cg=
|
||||||
|
github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY=
|
||||||
|
github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw=
|
||||||
|
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
|
||||||
|
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||||
|
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||||
|
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
|
||||||
|
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
|
||||||
|
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
|
||||||
|
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||||
|
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
@ -3,7 +3,7 @@ package handlers
|
|||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/gofrs/uuid/v5"
|
||||||
"michaelthomson.dev/mthomson/go-todos-app/services"
|
"michaelthomson.dev/mthomson/go-todos-app/services"
|
||||||
"michaelthomson.dev/mthomson/go-todos-app/templates/partials"
|
"michaelthomson.dev/mthomson/go-todos-app/templates/partials"
|
||||||
)
|
)
|
||||||
@ -42,10 +42,74 @@ func (h *TodoHandler) Create(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *TodoHandler) Done(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
id, err := uuid.FromString(r.PathValue("id"))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
todo, err := h.ts.GetTodoById(id)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
todo, err = h.ts.UpdateTodo(id, todo.Name, true)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = partials.Todo(todo).Render(r.Context(), w)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *TodoHandler) Undone(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
id, err := uuid.FromString(r.PathValue("id"))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
todo, err := h.ts.GetTodoById(id)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
todo, err = h.ts.UpdateTodo(id, todo.Name, false)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = partials.Todo(todo).Render(r.Context(), w)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (h *TodoHandler) Delete(w http.ResponseWriter, r *http.Request) {
|
func (h *TodoHandler) Delete(w http.ResponseWriter, r *http.Request) {
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
id, err := uuid.Parse(r.PathValue("id"))
|
id, err := uuid.FromString(r.PathValue("id"))
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
113
main.go
113
main.go
@ -1,20 +1,121 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
"michaelthomson.dev/mthomson/go-todos-app/db"
|
pgxuuid "github.com/jackc/pgx-gofrs-uuid"
|
||||||
|
"github.com/jackc/pgx/v5"
|
||||||
|
"github.com/jackc/pgx/v5/pgxpool"
|
||||||
|
"github.com/joho/godotenv"
|
||||||
"michaelthomson.dev/mthomson/go-todos-app/handlers"
|
"michaelthomson.dev/mthomson/go-todos-app/handlers"
|
||||||
|
"michaelthomson.dev/mthomson/go-todos-app/repositories"
|
||||||
"michaelthomson.dev/mthomson/go-todos-app/services"
|
"michaelthomson.dev/mthomson/go-todos-app/services"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
todosStore := db.NewTodoStore()
|
// load environment
|
||||||
ts := services.NewTodoService(&todosStore)
|
err := godotenv.Load()
|
||||||
|
|
||||||
homeHandler := handlers.NewHomeHandler(ts)
|
if err != nil {
|
||||||
todoHandler := handlers.NewTodoHandler(ts)
|
log.Fatalf("Error loading .env file: %v", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
databaseUrl := os.Getenv("DATABASE_URL")
|
||||||
|
|
||||||
|
// connect to Db
|
||||||
|
dbconfig, err := pgxpool.ParseConfig(databaseUrl)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Unable to parse db config: %v", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
dbconfig.AfterConnect = func(ctx context.Context, conn *pgx.Conn) error {
|
||||||
|
pgxuuid.Register(conn.TypeMap())
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
dbpool, err := pgxpool.New(context.Background(), databaseUrl)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Unable to create connection pool: %v", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
defer dbpool.Close()
|
||||||
|
|
||||||
|
// apply initial sql
|
||||||
|
log.Print("Applying migrations table")
|
||||||
|
content, err := os.ReadFile(filepath.Join("sql", "init.sql"))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = dbpool.Exec(context.Background(), string(content))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
//apply migrations
|
||||||
|
log.Print("Starting migrations...")
|
||||||
|
files, err := os.ReadDir("./sql/migrations")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, file := range files {
|
||||||
|
path := filepath.Join("sql", "migrations", file.Name())
|
||||||
|
content, err := os.ReadFile(path)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
var count int
|
||||||
|
err = dbpool.QueryRow(context.Background(), "SELECT COUNT(1) FROM migration WHERE name=$1", file.Name()).Scan(&count)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if count == 0 {
|
||||||
|
log.Printf("Running migration: %s", file.Name())
|
||||||
|
_, err = dbpool.Exec(context.Background(), string(content))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = dbpool.Exec(context.Background(), "INSERT INTO migration(name) values($1)", file.Name())
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.Printf("Migration already exists: %s", file.Name())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// set up repositories
|
||||||
|
todoRepository := repositories.NewPostgresTodoRepository(dbpool)
|
||||||
|
|
||||||
|
// set up services
|
||||||
|
todoService := services.NewTodoService(&todoRepository)
|
||||||
|
|
||||||
|
// set up handlers
|
||||||
|
homeHandler := handlers.NewHomeHandler(todoService)
|
||||||
|
todoHandler := handlers.NewTodoHandler(todoService)
|
||||||
|
|
||||||
router := http.NewServeMux()
|
router := http.NewServeMux()
|
||||||
|
|
||||||
@ -25,6 +126,8 @@ func main() {
|
|||||||
router.HandleFunc("GET /{$}", homeHandler.Home)
|
router.HandleFunc("GET /{$}", homeHandler.Home)
|
||||||
router.HandleFunc("POST /todos", todoHandler.Create)
|
router.HandleFunc("POST /todos", todoHandler.Create)
|
||||||
router.HandleFunc("DELETE /todos/{id}", todoHandler.Delete)
|
router.HandleFunc("DELETE /todos/{id}", todoHandler.Delete)
|
||||||
|
router.HandleFunc("PATCH /todos/{id}/done", todoHandler.Done)
|
||||||
|
router.HandleFunc("PATCH /todos/{id}/undone", todoHandler.Undone)
|
||||||
|
|
||||||
server := http.Server{
|
server := http.Server{
|
||||||
Addr: "localhost:3000",
|
Addr: "localhost:3000",
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
package models
|
package models
|
||||||
|
|
||||||
import "github.com/google/uuid"
|
import "github.com/gofrs/uuid/v5"
|
||||||
|
|
||||||
type Todo struct {
|
type Todo struct {
|
||||||
Id uuid.UUID
|
Id uuid.UUID
|
||||||
@ -8,9 +8,9 @@ type Todo struct {
|
|||||||
Done bool
|
Done bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewTodo(name string, done bool) Todo {
|
func NewTodo(id uuid.UUID, name string, done bool) Todo {
|
||||||
return Todo{
|
return Todo{
|
||||||
Id: uuid.New(),
|
Id: id,
|
||||||
Name: name,
|
Name: name,
|
||||||
Done: done,
|
Done: done,
|
||||||
}
|
}
|
||||||
|
@ -1,21 +1,21 @@
|
|||||||
package db
|
package repositories
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/gofrs/uuid/v5"
|
||||||
"michaelthomson.dev/mthomson/go-todos-app/models"
|
"michaelthomson.dev/mthomson/go-todos-app/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
type TodosStore struct {
|
type InMemoryTodoRepository struct {
|
||||||
Todos []models.Todo
|
Todos []models.Todo
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ts *TodosStore) List() (todo []models.Todo, err error) {
|
func (ts *InMemoryTodoRepository) List() (todo []models.Todo, err error) {
|
||||||
return ts.Todos, nil
|
return ts.Todos, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ts *TodosStore) Get(id uuid.UUID) (todo models.Todo, err error) {
|
func (ts *InMemoryTodoRepository) Get(id uuid.UUID) (todo models.Todo, err error) {
|
||||||
index := -1
|
index := -1
|
||||||
for i, todo := range ts.Todos {
|
for i, todo := range ts.Todos {
|
||||||
if id == todo.Id {
|
if id == todo.Id {
|
||||||
@ -32,12 +32,12 @@ func (ts *TodosStore) Get(id uuid.UUID) (todo models.Todo, err error) {
|
|||||||
return ts.Todos[index], nil
|
return ts.Todos[index], nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ts *TodosStore) Add(todo models.Todo) (addedTodo models.Todo, err error) {
|
func (ts *InMemoryTodoRepository) Add(todo models.Todo) (addedTodo models.Todo, err error) {
|
||||||
ts.Todos = append(ts.Todos, todo)
|
ts.Todos = append(ts.Todos, todo)
|
||||||
return todo, nil
|
return todo, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ts *TodosStore) Delete(id uuid.UUID) (err error) {
|
func (ts *InMemoryTodoRepository) Delete(id uuid.UUID) (err error) {
|
||||||
index := -1
|
index := -1
|
||||||
for i, todo := range ts.Todos {
|
for i, todo := range ts.Todos {
|
||||||
if id == todo.Id {
|
if id == todo.Id {
|
||||||
@ -54,8 +54,8 @@ func (ts *TodosStore) Delete(id uuid.UUID) (err error) {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewTodoStore() TodosStore {
|
func NewInMemoryTodoRepository() InMemoryTodoRepository {
|
||||||
return TodosStore{
|
return InMemoryTodoRepository{
|
||||||
Todos: []models.Todo{},
|
Todos: []models.Todo{},
|
||||||
}
|
}
|
||||||
}
|
}
|
86
repositories/postgresTodoRepository.go
Normal file
86
repositories/postgresTodoRepository.go
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
package repositories
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/gofrs/uuid/v5"
|
||||||
|
"github.com/jackc/pgx/v5/pgxpool"
|
||||||
|
"michaelthomson.dev/mthomson/go-todos-app/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PostgresTodoRepository struct {
|
||||||
|
db *pgxpool.Pool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ts *PostgresTodoRepository) List() ([]models.Todo, error) {
|
||||||
|
var todos []models.Todo
|
||||||
|
rows, err := ts.db.Query(context.Background(), "SELECT id, name, done FROM todo")
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
return todos, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for rows.Next() {
|
||||||
|
var r models.Todo
|
||||||
|
err := rows.Scan(&r.Id, &r.Name, &r.Done)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
return todos, err
|
||||||
|
}
|
||||||
|
todos = append(todos, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
return todos, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ts *PostgresTodoRepository) Get(id uuid.UUID) (models.Todo, error) {
|
||||||
|
var todo models.Todo
|
||||||
|
err := ts.db.QueryRow(context.Background(), "SELECT id, name, done FROM todo WHERE id=$1", id).Scan(&todo.Id, &todo.Name, &todo.Done)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
return todo, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return todo, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ts *PostgresTodoRepository) Update(id uuid.UUID, name string, done bool) error {
|
||||||
|
_, err := ts.db.Exec(context.Background(), "UPDATE todo SET name=$1, done=$2 WHERE id=$3", name, done, id)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ts *PostgresTodoRepository) Add(todo models.Todo) error {
|
||||||
|
_, err := ts.db.Exec(context.Background(), "INSERT INTO todo(id, name, done) values($1, $2, $3)", todo.Id, todo.Name, todo.Done)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ts *PostgresTodoRepository) Delete(id uuid.UUID) error {
|
||||||
|
_, err := ts.db.Exec(context.Background(), "DELETE FROM todo where id=$1", id)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPostgresTodoRepository(db *pgxpool.Pool) PostgresTodoRepository {
|
||||||
|
return PostgresTodoRepository{
|
||||||
|
db: db,
|
||||||
|
}
|
||||||
|
}
|
@ -3,15 +3,16 @@ package services
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/gofrs/uuid/v5"
|
||||||
"michaelthomson.dev/mthomson/go-todos-app/models"
|
"michaelthomson.dev/mthomson/go-todos-app/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Db interface {
|
type Db interface {
|
||||||
List() (todo []models.Todo, err error)
|
List() ([]models.Todo, error)
|
||||||
Get(id uuid.UUID) (todo models.Todo, err error)
|
Get(id uuid.UUID) (models.Todo, error)
|
||||||
Add(todo models.Todo) (addedTodo models.Todo, err error)
|
Add(todo models.Todo) error
|
||||||
Delete(id uuid.UUID) (err error)
|
Delete(id uuid.UUID) error
|
||||||
|
Update(id uuid.UUID, name string, done bool) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type TodoService struct {
|
type TodoService struct {
|
||||||
@ -24,11 +25,33 @@ func NewTodoService(db Db) *TodoService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ts *TodoService) AddTodo(name string) (todo models.Todo, err error) {
|
func (ts *TodoService) AddTodo(name string) (models.Todo, error) {
|
||||||
if name == "" {
|
if name == "" {
|
||||||
return models.Todo{}, fmt.Errorf("Must provide a name")
|
return models.Todo{}, fmt.Errorf("Must provide a name")
|
||||||
}
|
}
|
||||||
return ts.db.Add(models.NewTodo(name, false))
|
|
||||||
|
id, err := uuid.NewV4()
|
||||||
|
if err != nil {
|
||||||
|
return models.Todo{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = ts.db.Add(models.NewTodo(id, name, false))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return models.Todo{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return ts.db.Get(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ts *TodoService) UpdateTodo(id uuid.UUID, name string, done bool) (models.Todo, error) {
|
||||||
|
err := ts.db.Update(id, name, done)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return models.Todo{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return ts.db.Get(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ts *TodoService) GetTodos() (todos []models.Todo, err error) {
|
func (ts *TodoService) GetTodos() (todos []models.Todo, err error) {
|
||||||
|
5
sql/init.sql
Normal file
5
sql/init.sql
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS migration (
|
||||||
|
id serial primary key not null,
|
||||||
|
name text not null,
|
||||||
|
applied_at timestamp not null default now()
|
||||||
|
);
|
5
sql/migrations/20240617T1630_createtable-todo.sql
Normal file
5
sql/migrations/20240617T1630_createtable-todo.sql
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
CREATE TABLE todo (
|
||||||
|
id UUID PRIMARY KEY,
|
||||||
|
name VARCHAR(255) NOT NULL,
|
||||||
|
done BOOLEAN NOT NULL DEFAULT false
|
||||||
|
);
|
@ -6,7 +6,7 @@ import "michaelthomson.dev/mthomson/go-todos-app/models"
|
|||||||
|
|
||||||
templ Home(todos []models.Todo) {
|
templ Home(todos []models.Todo) {
|
||||||
@shared.Page("Todos") {
|
@shared.Page("Todos") {
|
||||||
<div class="flex content-center items-center flex-col">
|
<div class="flex flex-col">
|
||||||
<h1 class="text-3xl">Todos</h1>
|
<h1 class="text-3xl">Todos</h1>
|
||||||
<form hx-post="/todos" hx-target="#todos" hx-swap="beforeend">
|
<form hx-post="/todos" hx-target="#todos" hx-swap="beforeend">
|
||||||
<input name="name" type="text" class="border border-black" />
|
<input name="name" type="text" class="border border-black" />
|
||||||
|
@ -33,7 +33,7 @@ func Home(todos []models.Todo) templ.Component {
|
|||||||
templ_7745c5c3_Buffer = templ.GetBuffer()
|
templ_7745c5c3_Buffer = templ.GetBuffer()
|
||||||
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
|
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"flex content-center items-center flex-col\"><h1 class=\"text-3xl\">Todos</h1><form hx-post=\"/todos\" hx-target=\"#todos\" hx-swap=\"beforeend\"><input name=\"name\" type=\"text\" class=\"border border-black\"> <button type=\"submit\">Submit</button></form><div id=\"todos\">")
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"flex flex-col\"><h1 class=\"text-3xl\">Todos</h1><form hx-post=\"/todos\" hx-target=\"#todos\" hx-swap=\"beforeend\"><input name=\"name\" type=\"text\" class=\"border border-black\"> <button type=\"submit\">Submit</button></form><div id=\"todos\">")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
|
@ -1,14 +1,23 @@
|
|||||||
package partials
|
package partials
|
||||||
|
|
||||||
import "michaelthomson.dev/mthomson/go-todos-app/models"
|
import "michaelthomson.dev/mthomson/go-todos-app/models"
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
func todoId(todo models.Todo) string {
|
func todoId(todo models.Todo) string {
|
||||||
return "todo-" + todo.Id.String()
|
return "todo-" + todo.Id.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func patchUrl(todo models.Todo) string {
|
||||||
|
if todo.Done {
|
||||||
|
return string(templ.URL(fmt.Sprintf("/todos/%s/undone", todo.Id)))
|
||||||
|
}
|
||||||
|
return string(templ.URL(fmt.Sprintf("/todos/%s/done", todo.Id)))
|
||||||
|
}
|
||||||
|
|
||||||
templ Todo(todo models.Todo) {
|
templ Todo(todo models.Todo) {
|
||||||
<div id={ todoId(todo) }>
|
<div id={ todoId(todo) }>
|
||||||
{ todo.Name }
|
{ todo.Name }
|
||||||
<button hx-delete={ "/todos/" + todo.Id.String() } hx-target={ "#" + todoId(todo) } hx-swap="delete">Delete</button>
|
<button hx-delete={ string(templ.URL(fmt.Sprintf("/todos/%s", todo.Id))) } hx-target={ "#" + todoId(todo) } hx-swap="delete">Delete</button>
|
||||||
|
<input type="checkbox" checked?={ todo.Done } hx-target={ "#" + todoId(todo) } hx-patch={ patchUrl(todo) } hx-swap="outerHTML" />
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
@ -11,11 +11,19 @@ import "io"
|
|||||||
import "bytes"
|
import "bytes"
|
||||||
|
|
||||||
import "michaelthomson.dev/mthomson/go-todos-app/models"
|
import "michaelthomson.dev/mthomson/go-todos-app/models"
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
func todoId(todo models.Todo) string {
|
func todoId(todo models.Todo) string {
|
||||||
return "todo-" + todo.Id.String()
|
return "todo-" + todo.Id.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func patchUrl(todo models.Todo) string {
|
||||||
|
if todo.Done {
|
||||||
|
return string(templ.URL(fmt.Sprintf("/todos/%s/undone", todo.Id)))
|
||||||
|
}
|
||||||
|
return string(templ.URL(fmt.Sprintf("/todos/%s/done", todo.Id)))
|
||||||
|
}
|
||||||
|
|
||||||
func Todo(todo models.Todo) templ.Component {
|
func Todo(todo models.Todo) templ.Component {
|
||||||
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
|
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
|
||||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
|
||||||
@ -36,7 +44,7 @@ func Todo(todo models.Todo) templ.Component {
|
|||||||
var templ_7745c5c3_Var2 string
|
var templ_7745c5c3_Var2 string
|
||||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(todoId(todo))
|
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(todoId(todo))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/partials/todo.templ`, Line: 10, Col: 24}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/partials/todo.templ`, Line: 18, Col: 24}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
@ -49,7 +57,7 @@ func Todo(todo models.Todo) templ.Component {
|
|||||||
var templ_7745c5c3_Var3 string
|
var templ_7745c5c3_Var3 string
|
||||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(todo.Name)
|
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(todo.Name)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/partials/todo.templ`, Line: 11, Col: 15}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/partials/todo.templ`, Line: 19, Col: 15}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
@ -60,9 +68,9 @@ func Todo(todo models.Todo) templ.Component {
|
|||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
var templ_7745c5c3_Var4 string
|
var templ_7745c5c3_Var4 string
|
||||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs("/todos/" + todo.Id.String())
|
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(string(templ.URL(fmt.Sprintf("/todos/%s", todo.Id))))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/partials/todo.templ`, Line: 12, Col: 52}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/partials/todo.templ`, Line: 20, Col: 76}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
@ -75,13 +83,49 @@ func Todo(todo models.Todo) templ.Component {
|
|||||||
var templ_7745c5c3_Var5 string
|
var templ_7745c5c3_Var5 string
|
||||||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs("#" + todoId(todo))
|
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs("#" + todoId(todo))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/partials/todo.templ`, Line: 12, Col: 85}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/partials/todo.templ`, Line: 20, Col: 109}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\" hx-swap=\"delete\">Delete</button></div>")
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\" hx-swap=\"delete\">Delete</button> <input type=\"checkbox\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if todo.Done {
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" checked")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" hx-target=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var6 string
|
||||||
|
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs("#" + todoId(todo))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/partials/todo.templ`, Line: 21, Col: 80}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\" hx-patch=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var7 string
|
||||||
|
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(patchUrl(todo))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/partials/todo.templ`, Line: 21, Col: 108}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\" hx-swap=\"outerHTML\"></div>")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user