mirror of
https://github.com/syumai/workers.git
synced 2025-03-10 09:27:08 +00:00
add mysql-blog-server example
This commit is contained in:
parent
4789241e05
commit
30569739ef
3
.gitignore
vendored
3
.gitignore
vendored
@ -1,4 +1,5 @@
|
|||||||
dist
|
dist
|
||||||
build
|
build
|
||||||
node_modules
|
node_modules
|
||||||
.wrangler
|
.wrangler
|
||||||
|
.dev.vars
|
||||||
|
1
_examples/mysql-blog-server/.dev.vars.example
Normal file
1
_examples/mysql-blog-server/.dev.vars.example
Normal file
@ -0,0 +1 @@
|
|||||||
|
MYSQL_DSN=user:pass@tcp(hostname)/database-name?interpolateParams=true
|
3
_examples/mysql-blog-server/.gitignore
vendored
Normal file
3
_examples/mysql-blog-server/.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
build
|
||||||
|
node_modules
|
||||||
|
.wrangler
|
12
_examples/mysql-blog-server/Makefile
Normal file
12
_examples/mysql-blog-server/Makefile
Normal 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
|
74
_examples/mysql-blog-server/README.md
Normal file
74
_examples/mysql-blog-server/README.md
Normal 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
|
||||||
|
```
|
135
_examples/mysql-blog-server/app/handler.go
Normal file
135
_examples/mysql-blog-server/app/handler.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
21
_examples/mysql-blog-server/app/model/article.go
Normal file
21
_examples/mysql-blog-server/app/model/article.go
Normal 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"`
|
||||||
|
}
|
10
_examples/mysql-blog-server/go.mod
Normal file
10
_examples/mysql-blog-server/go.mod
Normal 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 => ../../
|
2
_examples/mysql-blog-server/go.sum
Normal file
2
_examples/mysql-blog-server/go.sum
Normal 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=
|
13
_examples/mysql-blog-server/main.go
Normal file
13
_examples/mysql-blog-server/main.go
Normal 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
|
||||||
|
}
|
13
_examples/mysql-blog-server/schema.sql
Normal file
13
_examples/mysql-blog-server/schema.sql
Normal 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()
|
||||||
|
);
|
6
_examples/mysql-blog-server/wrangler.toml
Normal file
6
_examples/mysql-blog-server/wrangler.toml
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
name = "mysql-blog-server"
|
||||||
|
main = "./build/worker.mjs"
|
||||||
|
compatibility_date = "2024-01-03"
|
||||||
|
|
||||||
|
[build]
|
||||||
|
command = "make build"
|
Loading…
x
Reference in New Issue
Block a user