From 30569739eff56b246b37b82990beee292366bd57 Mon Sep 17 00:00:00 2001 From: syumai Date: Wed, 3 Jan 2024 23:21:29 +0900 Subject: [PATCH] add mysql-blog-server example --- .gitignore | 3 +- _examples/mysql-blog-server/.dev.vars.example | 1 + _examples/mysql-blog-server/.gitignore | 3 + _examples/mysql-blog-server/Makefile | 12 ++ _examples/mysql-blog-server/README.md | 74 ++++++++++ _examples/mysql-blog-server/app/handler.go | 135 ++++++++++++++++++ .../mysql-blog-server/app/model/article.go | 21 +++ _examples/mysql-blog-server/go.mod | 10 ++ _examples/mysql-blog-server/go.sum | 2 + _examples/mysql-blog-server/main.go | 13 ++ _examples/mysql-blog-server/schema.sql | 13 ++ _examples/mysql-blog-server/wrangler.toml | 6 + 12 files changed, 292 insertions(+), 1 deletion(-) create mode 100644 _examples/mysql-blog-server/.dev.vars.example create mode 100644 _examples/mysql-blog-server/.gitignore create mode 100644 _examples/mysql-blog-server/Makefile create mode 100644 _examples/mysql-blog-server/README.md create mode 100644 _examples/mysql-blog-server/app/handler.go create mode 100644 _examples/mysql-blog-server/app/model/article.go create mode 100644 _examples/mysql-blog-server/go.mod create mode 100644 _examples/mysql-blog-server/go.sum create mode 100644 _examples/mysql-blog-server/main.go create mode 100644 _examples/mysql-blog-server/schema.sql create mode 100644 _examples/mysql-blog-server/wrangler.toml diff --git a/.gitignore b/.gitignore index 68bc29b..96dc998 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ dist build node_modules -.wrangler \ No newline at end of file +.wrangler +.dev.vars diff --git a/_examples/mysql-blog-server/.dev.vars.example b/_examples/mysql-blog-server/.dev.vars.example new file mode 100644 index 0000000..c7a7ae7 --- /dev/null +++ b/_examples/mysql-blog-server/.dev.vars.example @@ -0,0 +1 @@ +MYSQL_DSN=user:pass@tcp(hostname)/database-name?interpolateParams=true diff --git a/_examples/mysql-blog-server/.gitignore b/_examples/mysql-blog-server/.gitignore new file mode 100644 index 0000000..aee7b7e --- /dev/null +++ b/_examples/mysql-blog-server/.gitignore @@ -0,0 +1,3 @@ +build +node_modules +.wrangler diff --git a/_examples/mysql-blog-server/Makefile b/_examples/mysql-blog-server/Makefile new file mode 100644 index 0000000..3140ac9 --- /dev/null +++ b/_examples/mysql-blog-server/Makefile @@ -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 diff --git a/_examples/mysql-blog-server/README.md b/_examples/mysql-blog-server/README.md new file mode 100644 index 0000000..9217545 --- /dev/null +++ b/_examples/mysql-blog-server/README.md @@ -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 +``` diff --git a/_examples/mysql-blog-server/app/handler.go b/_examples/mysql-blog-server/app/handler.go new file mode 100644 index 0000000..990e7bf --- /dev/null +++ b/_examples/mysql-blog-server/app/handler.go @@ -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) + } +} diff --git a/_examples/mysql-blog-server/app/model/article.go b/_examples/mysql-blog-server/app/model/article.go new file mode 100644 index 0000000..08b616a --- /dev/null +++ b/_examples/mysql-blog-server/app/model/article.go @@ -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"` +} diff --git a/_examples/mysql-blog-server/go.mod b/_examples/mysql-blog-server/go.mod new file mode 100644 index 0000000..e852089 --- /dev/null +++ b/_examples/mysql-blog-server/go.mod @@ -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 => ../../ diff --git a/_examples/mysql-blog-server/go.sum b/_examples/mysql-blog-server/go.sum new file mode 100644 index 0000000..fd7ae07 --- /dev/null +++ b/_examples/mysql-blog-server/go.sum @@ -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= diff --git a/_examples/mysql-blog-server/main.go b/_examples/mysql-blog-server/main.go new file mode 100644 index 0000000..3a20bc0 --- /dev/null +++ b/_examples/mysql-blog-server/main.go @@ -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 +} diff --git a/_examples/mysql-blog-server/schema.sql b/_examples/mysql-blog-server/schema.sql new file mode 100644 index 0000000..4fa0118 --- /dev/null +++ b/_examples/mysql-blog-server/schema.sql @@ -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() +); \ No newline at end of file diff --git a/_examples/mysql-blog-server/wrangler.toml b/_examples/mysql-blog-server/wrangler.toml new file mode 100644 index 0000000..50d09de --- /dev/null +++ b/_examples/mysql-blog-server/wrangler.toml @@ -0,0 +1,6 @@ +name = "mysql-blog-server" +main = "./build/worker.mjs" +compatibility_date = "2024-01-03" + +[build] +command = "make build"