add mysql-blog-server example

This commit is contained in:
syumai 2024-01-03 23:21:29 +09:00
parent 4789241e05
commit 30569739ef
12 changed files with 292 additions and 1 deletions

3
.gitignore vendored
View File

@ -1,4 +1,5 @@
dist dist
build build
node_modules node_modules
.wrangler .wrangler
.dev.vars

View File

@ -0,0 +1 @@
MYSQL_DSN=user:pass@tcp(hostname)/database-name?interpolateParams=true

View File

@ -0,0 +1,3 @@
build
node_modules
.wrangler

View File

@ -0,0 +1,12 @@
.PHONY: dev
dev:
wrangler dev
.PHONY: build
build:
go run ../../cmd/workers-assets-gen -mode=go
GOOS=js GOARCH=wasm go build -o ./build/app.wasm .
.PHONY: deploy
deploy:
wrangler deploy

View File

@ -0,0 +1,74 @@
# mysql-blog-server
* A simple Blog server implemented in Go.
* This example is using MySQL.
# WIP
### Create blog post
```
$ curl -X POST 'http://localhost:8787/articles' \
-H 'Content-Type: application/json' \
-d '{
"title":"example post",
"body":"body of the example post"
}'
{
"article": {
{
"id": "f9e8119e-881e-4dc5-9307-af4f2dc79891",
"title": "example post",
"body": "body of the example post",
"createdAt": 1677382874
}
}
}
```
### List blog posts
```
$ curl 'http://localhost:8787/articles'
{
"articles": [
{
"id": "bea6cd80-5a83-45f0-b061-0e13a2ad5fba",
"title": "example post 2",
"body": "body of the example post 2",
"createdAt": 1677383758
},
{
"id": "f9e8119e-881e-4dc5-9307-af4f2dc79891",
"title": "example post",
"body": "body of the example post",
"createdAt": 1677382874
}
]
}
```
## Development
### Requirements
This project requires these tools to be installed globally.
* wrangler
* go
### Setup MySQL DB
* This project requires MySQL DB.
- Connection setting: `.dev.vars.example` (please rename to `.dev.vars`.)
- Initial migration SQL: `schema.sql`
* If you want to deploy this app to production, please set `MYSQL_DSN` to your Worker secrets.
- Run: `npx wrangler secret put MYSQL_DSN`.
### Commands
```
make dev # run dev server
make build # build Go Wasm binary
make deploy # deploy worker
```

View File

@ -0,0 +1,135 @@
package app
import (
"context"
"database/sql"
"encoding/json"
"fmt"
"log"
"net"
"net/http"
"os"
"time"
"github.com/go-sql-driver/mysql"
"github.com/syumai/workers/_examples/mysql-blog-server/app/model"
"github.com/syumai/workers/cloudflare"
"github.com/syumai/workers/cloudflare/sockets"
)
type articleHandler struct{}
var _ http.Handler = (*articleHandler)(nil)
func NewArticleHandler() http.Handler {
return &articleHandler{}
}
func (h *articleHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
// initialize DB.
mysql.RegisterDialContext("tcp", func(_ context.Context, addr string) (net.Conn, error) {
return sockets.Connect(req.Context(), addr, &sockets.SocketOptions{
SecureTransport: sockets.SecureTransportOff,
})
})
db, err := sql.Open("mysql",
cloudflare.Getenv(req.Context(), "MYSQL_DSN"))
if err != nil {
log.Fatalf("failed to connect: %v", err)
}
switch req.Method {
case http.MethodGet:
h.listArticles(w, req, db)
return
case http.MethodPost:
h.createArticle(w, req, db)
return
}
w.WriteHeader(http.StatusNotFound)
w.Header().Set("Content-Type", "text/plain")
w.Write([]byte("not found"))
}
func (h *articleHandler) handleErr(w http.ResponseWriter, status int, msg string) {
w.WriteHeader(status)
w.Header().Set("Content-Type", "text/plain")
w.Write([]byte(msg))
}
func (h *articleHandler) createArticle(w http.ResponseWriter, req *http.Request, db *sql.DB) {
var createArticleReq model.CreateArticleRequest
if err := json.NewDecoder(req.Body).Decode(&createArticleReq); err != nil {
h.handleErr(w, http.StatusBadRequest,
"request format is invalid")
return
}
now := time.Now().Unix()
article := model.Article{
Title: createArticleReq.Title,
Body: createArticleReq.Body,
CreatedAt: uint64(now),
}
result, err := db.Exec(`
INSERT INTO articles (title, body, created_at)
VALUES (?, ?, ?)
`, article.Title, article.Body, article.CreatedAt)
if err != nil {
log.Println(err)
h.handleErr(w, http.StatusInternalServerError,
"failed to save article")
return
}
id, err := result.LastInsertId()
if err != nil {
log.Println(err)
h.handleErr(w, http.StatusInternalServerError,
"failed to get ID of inserted article")
return
}
article.ID = uint64(id)
res := model.CreateArticleResponse{
Article: article,
}
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(res); err != nil {
fmt.Fprintf(os.Stderr, "failed to encode response: %w\n", err)
}
}
func (h *articleHandler) listArticles(w http.ResponseWriter, req *http.Request, db *sql.DB) {
rows, err := db.Query(`
SELECT id, title, body, created_at FROM articles
ORDER BY created_at DESC;
`)
if err != nil {
h.handleErr(w, http.StatusInternalServerError,
"failed to load article")
return
}
articles := []model.Article{}
for rows.Next() {
var a model.Article
err = rows.Scan(&a.ID, &a.Title, &a.Body, &a.CreatedAt)
if err != nil {
log.Println(err)
h.handleErr(w, http.StatusInternalServerError,
"failed to scan article")
return
}
articles = append(articles, a)
}
res := model.ListArticlesResponse{
Articles: articles,
}
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(res); err != nil {
fmt.Fprintf(os.Stderr, "failed to encode response: %w\n", err)
}
}

View File

@ -0,0 +1,21 @@
package model
type Article struct {
ID uint64 `json:"id"`
Title string `json:"title"`
Body string `json:"body"`
CreatedAt uint64 `json:"createdAt"`
}
type CreateArticleRequest struct {
Title string `json:"title"`
Body string `json:"body"`
}
type CreateArticleResponse struct {
Article Article `json:"article"`
}
type ListArticlesResponse struct {
Articles []Article `json:"articles"`
}

View File

@ -0,0 +1,10 @@
module github.com/syumai/workers/_examples/mysql-blog-server
go 1.21.3
require (
github.com/go-sql-driver/mysql v1.7.1
github.com/syumai/workers v0.9.0
)
replace github.com/syumai/workers => ../../

View File

@ -0,0 +1,2 @@
github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI=
github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=

View File

@ -0,0 +1,13 @@
package main
import (
"net/http"
"github.com/syumai/workers"
"github.com/syumai/workers/_examples/mysql-blog-server/app"
)
func main() {
http.Handle("/articles", app.NewArticleHandler())
workers.Serve(nil) // use http.DefaultServeMux
}

View File

@ -0,0 +1,13 @@
DROP TABLE IF EXISTS articles;
CREATE TABLE IF NOT EXISTS articles (
id INT PRIMARY KEY AUTO_INCREMENT,
title TEXT NOT NULL,
body TEXT NOT NULL,
created_at INT NOT NULL
);
CREATE INDEX idx_articles_on_created_at ON articles (created_at DESC);
INSERT INTO articles (title, body, created_at) VALUES (
'title of example post',
'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.',
UNIX_TIMESTAMP()
);

View File

@ -0,0 +1,6 @@
name = "mysql-blog-server"
main = "./build/worker.mjs"
compatibility_date = "2024-01-03"
[build]
command = "make build"