Create and list posts
This commit is contained in:
commit
80dd345237
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
*.db
|
||||||
|
*.db-shm
|
||||||
|
*.db-wal
|
8
go.mod
Normal file
8
go.mod
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
module git.codemonkeysoftware.net/b/sbsqlitessgcms
|
||||||
|
|
||||||
|
go 1.18
|
||||||
|
|
||||||
|
require (
|
||||||
|
crawshaw.io/sqlite v0.3.3-0.20220618202545-d1964889ea3c
|
||||||
|
gitlab.codemonkeysoftware.net/b/hatmill v0.0.6
|
||||||
|
)
|
9
go.sum
Normal file
9
go.sum
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
crawshaw.io/iox v0.0.0-20181124134642-c51c3df30797/go.mod h1:sXBiorCo8c46JlQV3oXPKINnZ8mcqnye1EkVkqsectk=
|
||||||
|
crawshaw.io/sqlite v0.3.2 h1:N6IzTjkiw9FItHAa0jp+ZKC6tuLzXqAYIv+ccIWos1I=
|
||||||
|
crawshaw.io/sqlite v0.3.2/go.mod h1:igAO5JulrQ1DbdZdtVq48mnZUBAPOeFzer7VhDWNtW4=
|
||||||
|
crawshaw.io/sqlite v0.3.3-0.20220618202545-d1964889ea3c h1:wvzox0eLO6CKQAMcOqz7oH3UFqMpMmK7kwmwV+22HIs=
|
||||||
|
crawshaw.io/sqlite v0.3.3-0.20220618202545-d1964889ea3c/go.mod h1:igAO5JulrQ1DbdZdtVq48mnZUBAPOeFzer7VhDWNtW4=
|
||||||
|
github.com/leanovate/gopter v0.2.4 h1:U4YLBggDFhJdqQsG4Na2zX7joVTky9vHaj/AGEwSuXU=
|
||||||
|
github.com/leanovate/gopter v0.2.4/go.mod h1:gNcbPWNEWRe4lm+bycKqxUYoH5uoVje5SkOJ3uoLer8=
|
||||||
|
gitlab.codemonkeysoftware.net/b/hatmill v0.0.6 h1:5Vs30ORHoujCYRvtbIwrusGBQgPzbKv011xKrTFa5ng=
|
||||||
|
gitlab.codemonkeysoftware.net/b/hatmill v0.0.6/go.mod h1:T19ms3BmsEzy5YTS4icMO1oLC53xWhOP9MFeBv4NcFQ=
|
142
handler.go
Normal file
142
handler.go
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
h "gitlab.codemonkeysoftware.net/b/hatmill"
|
||||||
|
ha "gitlab.codemonkeysoftware.net/b/hatmill/attribute"
|
||||||
|
he "gitlab.codemonkeysoftware.net/b/hatmill/element"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
pathRoot = "/"
|
||||||
|
pathCreatePost = "/create-post"
|
||||||
|
pathDoCreatePost = "/create-post/do"
|
||||||
|
)
|
||||||
|
|
||||||
|
type handler struct {
|
||||||
|
mux *http.ServeMux
|
||||||
|
store *Store
|
||||||
|
baseURL string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHandler(store *Store, baseURL string) http.Handler {
|
||||||
|
h := &handler{
|
||||||
|
store: store,
|
||||||
|
mux: http.NewServeMux(),
|
||||||
|
baseURL: baseURL,
|
||||||
|
}
|
||||||
|
|
||||||
|
h.mux.HandleFunc(pathRoot, h.getPosts)
|
||||||
|
h.mux.HandleFunc(pathCreatePost, h.createPost)
|
||||||
|
h.mux.HandleFunc(pathDoCreatePost, h.doCreatePost)
|
||||||
|
|
||||||
|
return h
|
||||||
|
}
|
||||||
|
|
||||||
|
func (hnd *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
hnd.mux.ServeHTTP(w, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (hnd *handler) getPosts(w http.ResponseWriter, r *http.Request) {
|
||||||
|
result, err := hnd.store.GetPosts(r.Context())
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var rows h.Terms
|
||||||
|
for _, post := range result.Posts {
|
||||||
|
rows = append(rows, he.Tr()(
|
||||||
|
he.Td()(h.Text(strconv.FormatInt(post.Id, 10))),
|
||||||
|
he.Td()(h.Text(post.CreatedAt)),
|
||||||
|
he.Td()(h.Text(post.UpdatedAt)),
|
||||||
|
he.Td()(h.Text(post.Author)),
|
||||||
|
he.Td()(h.Text(post.Title)),
|
||||||
|
// he.Td()(h.Text(post.Body)),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
html := he.Html()(
|
||||||
|
he.Head()(),
|
||||||
|
he.Body()(
|
||||||
|
he.H1()(h.Text("Posts")),
|
||||||
|
he.Table()(
|
||||||
|
he.Thead()(
|
||||||
|
he.Tr()(
|
||||||
|
he.Th()(h.Text("ID")),
|
||||||
|
he.Th()(h.Text("Created")),
|
||||||
|
he.Th()(h.Text("Updated")),
|
||||||
|
he.Th()(h.Text("Author")),
|
||||||
|
he.Th()(h.Text("Title")),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
he.Tbody()(
|
||||||
|
rows...,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
he.A(ha.Href(hnd.baseURL+pathCreatePost))(h.Text("Create post")),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
_, err = h.WriteDocument(w, html)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
fieldNamePostTitle = "post-title"
|
||||||
|
fieldNamePostBody = "post-body"
|
||||||
|
fieldNamePostAuthor = "post-author"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (hnd *handler) createPost(w http.ResponseWriter, r *http.Request) {
|
||||||
|
html := he.Html()(
|
||||||
|
he.Head()(),
|
||||||
|
he.Body()(
|
||||||
|
he.H1()(h.Text("Create post")),
|
||||||
|
he.Form(ha.Action(hnd.baseURL+pathDoCreatePost), ha.Method("POST"))(
|
||||||
|
he.Label(ha.For(fieldNamePostTitle))(h.Text("Title")),
|
||||||
|
he.Input(ha.Name(fieldNamePostTitle)),
|
||||||
|
he.Br(),
|
||||||
|
he.Label(ha.For(fieldNamePostAuthor))(h.Text("Author")),
|
||||||
|
he.Input(ha.Name(fieldNamePostAuthor)),
|
||||||
|
he.Br(),
|
||||||
|
he.Label(ha.For(fieldNamePostBody))(h.Text("Body")),
|
||||||
|
he.Textarea(ha.For(fieldNamePostBody))(),
|
||||||
|
he.Br(),
|
||||||
|
he.Input(ha.Type("submit")),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
_, err := h.WriteDocument(w, html)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (hnd *handler) doCreatePost(w http.ResponseWriter, r *http.Request) {
|
||||||
|
err := r.ParseForm()
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := CreatePostCommand{
|
||||||
|
Title: r.PostForm.Get(fieldNamePostTitle),
|
||||||
|
Author: r.PostForm.Get(fieldNamePostAuthor),
|
||||||
|
Body: r.PostForm.Get(fieldNamePostBody),
|
||||||
|
}
|
||||||
|
result, err := hnd.store.CreatePost(r.Context(), cmd)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Println("created post", result.PostId)
|
||||||
|
http.Redirect(w, r, pathRoot, http.StatusFound)
|
||||||
|
}
|
38
main.go
Normal file
38
main.go
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"crawshaw.io/sqlite/sqlitex"
|
||||||
|
)
|
||||||
|
|
||||||
|
const addr = ":31337"
|
||||||
|
const dbPath = "./sbsqlitessgcms.db"
|
||||||
|
const baseURL = "http://localhost:31337"
|
||||||
|
|
||||||
|
var dbPool *sqlitex.Pool
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
err := run()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func run() error {
|
||||||
|
store, err := NewStore(dbPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer store.Close()
|
||||||
|
|
||||||
|
log.Println("Listening on", addr)
|
||||||
|
err = http.ListenAndServe(addr, NewHandler(store, baseURL))
|
||||||
|
if errors.Is(err, http.ErrServerClosed) {
|
||||||
|
return nil
|
||||||
|
} else {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
163
store.go
Normal file
163
store.go
Normal file
@ -0,0 +1,163 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"crawshaw.io/sqlite"
|
||||||
|
"crawshaw.io/sqlite/sqlitex"
|
||||||
|
)
|
||||||
|
|
||||||
|
var ErrNoConn = errors.New("could not get a database connection")
|
||||||
|
|
||||||
|
type Store struct {
|
||||||
|
pool *sqlitex.Pool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewStore(filename string) (*Store, error) {
|
||||||
|
var needCreate bool
|
||||||
|
if _, err := os.Stat(filename); os.IsNotExist(err) {
|
||||||
|
needCreate = true
|
||||||
|
}
|
||||||
|
// If the file exists, then assume it was created properly.
|
||||||
|
|
||||||
|
pool, err := sqlitex.Open(filename, 0, 10)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
store := &Store{
|
||||||
|
pool: pool,
|
||||||
|
}
|
||||||
|
if needCreate {
|
||||||
|
log.Println("creating schema")
|
||||||
|
err = store.createSchema()
|
||||||
|
if err != nil {
|
||||||
|
defer os.Remove(filename)
|
||||||
|
defer pool.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return store, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMemoryStore() (*Store, error) {
|
||||||
|
pool, err := sqlitex.Open("file::memory:?mode=memory&cache=shared", 0, 10)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
store := &Store{
|
||||||
|
pool: pool,
|
||||||
|
}
|
||||||
|
|
||||||
|
err = store.createSchema()
|
||||||
|
if err != nil {
|
||||||
|
defer pool.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return store, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) Close() error {
|
||||||
|
return s.pool.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) createSchema() error {
|
||||||
|
conn := s.pool.Get(context.Background())
|
||||||
|
defer s.pool.Put(conn)
|
||||||
|
return sqlitex.ExecScript(conn, dbSchema)
|
||||||
|
}
|
||||||
|
|
||||||
|
const dbSchema = `
|
||||||
|
CREATE TABLE post (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
author TEXT NOT NULL,
|
||||||
|
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
body TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TRIGGER update_post_updated_at
|
||||||
|
AFTER update ON post
|
||||||
|
BEGIN
|
||||||
|
UPDATE post SET updated_at = CURRENT_TIMESTAMP
|
||||||
|
WHERE id = NEW.id;
|
||||||
|
END;
|
||||||
|
`
|
||||||
|
|
||||||
|
type Post struct {
|
||||||
|
Id int64
|
||||||
|
Author string
|
||||||
|
CreatedAt string
|
||||||
|
UpdatedAt string
|
||||||
|
Title string
|
||||||
|
Body string
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetPostsResult struct {
|
||||||
|
Posts []Post
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) GetPosts(ctx context.Context) (GetPostsResult, error) {
|
||||||
|
conn := s.pool.Get(ctx)
|
||||||
|
if conn == nil {
|
||||||
|
return GetPostsResult{}, ErrNoConn
|
||||||
|
}
|
||||||
|
defer s.pool.Put(conn)
|
||||||
|
|
||||||
|
const dbQuery = `
|
||||||
|
SELECT id, author, created_at, updated_at, title, body
|
||||||
|
FROM post
|
||||||
|
ORDER BY created_at DESC`
|
||||||
|
var result GetPostsResult
|
||||||
|
err := sqlitex.Exec(conn, dbQuery,
|
||||||
|
func(stmt *sqlite.Stmt) error {
|
||||||
|
result.Posts = append(result.Posts, Post{
|
||||||
|
Id: stmt.ColumnInt64(0),
|
||||||
|
Author: stmt.ColumnText(1),
|
||||||
|
CreatedAt: stmt.ColumnText(2),
|
||||||
|
UpdatedAt: stmt.ColumnText(3),
|
||||||
|
Title: stmt.ColumnText(4),
|
||||||
|
Body: stmt.ColumnText(5),
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return GetPostsResult{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreatePostCommand struct {
|
||||||
|
Title string
|
||||||
|
Author string
|
||||||
|
Body string
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreatePostResult struct {
|
||||||
|
PostId int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) CreatePost(ctx context.Context, cmd CreatePostCommand) (CreatePostResult, error) {
|
||||||
|
conn := s.pool.Get(ctx)
|
||||||
|
if conn == nil {
|
||||||
|
return CreatePostResult{}, ErrNoConn
|
||||||
|
}
|
||||||
|
defer s.pool.Put(conn)
|
||||||
|
|
||||||
|
const dbQuery = `
|
||||||
|
INSERT INTO post(title, author, body)
|
||||||
|
VALUES (?, ?, ?)
|
||||||
|
RETURNING id;`
|
||||||
|
var result CreatePostResult
|
||||||
|
err := sqlitex.Exec(conn, dbQuery,
|
||||||
|
func(stmt *sqlite.Stmt) error {
|
||||||
|
result.PostId = stmt.ColumnInt64(0)
|
||||||
|
return nil
|
||||||
|
}, cmd.Title, cmd.Author, cmd.Body)
|
||||||
|
return result, err
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user