From 56d858a7b745df8498d72116be9bc7acbe250c12 Mon Sep 17 00:00:00 2001 From: pei0804 Date: Wed, 14 Aug 2019 00:06:58 +0900 Subject: [PATCH 01/46] review --- backend/controller/article.go | 6 +++--- backend/middleware/auth.go | 8 ++++---- backend/repository/user.go | 2 +- backend/server.go | 3 ++- backend/service/article.go | 2 +- 5 files changed, 11 insertions(+), 10 deletions(-) diff --git a/backend/controller/article.go b/backend/controller/article.go index 524ab3941..6a2a24d54 100644 --- a/backend/controller/article.go +++ b/backend/controller/article.go @@ -60,7 +60,7 @@ func (a *Article) Create(w http.ResponseWriter, r *http.Request) (int, interface return http.StatusBadRequest, nil, err } - articleService := service.NewArticleService(a.dbx) + articleService := service.NewArticle(a.dbx) id, err := articleService.Create(newArticle) if err != nil { return http.StatusInternalServerError, nil, err @@ -87,7 +87,7 @@ func (a *Article) Update(w http.ResponseWriter, r *http.Request) (int, interface return http.StatusBadRequest, nil, err } - articleService := service.NewArticleService(a.dbx) + articleService := service.NewArticle(a.dbx) err = articleService.Update(aid, reqArticle) if err != nil && errors.Cause(err) == sql.ErrNoRows { return http.StatusNotFound, nil, err @@ -110,7 +110,7 @@ func (a *Article) Destroy(w http.ResponseWriter, r *http.Request) (int, interfac return http.StatusBadRequest, nil, err } - articleService := service.NewArticleService(a.dbx) + articleService := service.NewArticle(a.dbx) err = articleService.Destroy(aid) if err != nil && errors.Cause(err) == sql.ErrNoRows { return http.StatusNotFound, nil, err diff --git a/backend/middleware/auth.go b/backend/middleware/auth.go index 709b50094..29797230f 100644 --- a/backend/middleware/auth.go +++ b/backend/middleware/auth.go @@ -16,19 +16,19 @@ const ( bearer = "Bearer" ) -type AuthMiddleware struct { +type Auth struct { client *auth.Client db *sqlx.DB } -func NewAuthMiddleware(client *auth.Client, db *sqlx.DB) *AuthMiddleware { - return &AuthMiddleware{ +func NewAuth(client *auth.Client, db *sqlx.DB) *Auth { + return &Auth{ client: client, db: db, } } -func (auth *AuthMiddleware) Handler(next http.Handler) http.Handler { +func (auth *Auth) Handler(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { idToken, err := getTokenFromHeader(r) if err != nil { diff --git a/backend/repository/user.go b/backend/repository/user.go index 3811a4364..549447173 100644 --- a/backend/repository/user.go +++ b/backend/repository/user.go @@ -10,7 +10,7 @@ import ( func GetUser(db *sqlx.DB, uid string) (*model.User, error) { var u model.User if err := db.Get(&u, ` -select id, firebase_uid, display_name, email, photo_url from user where firebase_uid = ? limit 1 +SELECT id, firebase_uid, display_name, email, photo_url FROM user WHERE firebase_uid = ? LIMIT 1 `, uid); err != nil { return nil, err } diff --git a/backend/server.go b/backend/server.go index 911ba9cde..7be2cb826 100644 --- a/backend/server.go +++ b/backend/server.go @@ -41,6 +41,7 @@ func (s *Server) Init(datasource string) { db := db2.NewDB(datasource) dbx, err := db.Open() + dbx.Close() if err != nil { log.Fatalf("failed db init. %s", err) } @@ -60,7 +61,7 @@ func (s *Server) Run(addr string) { } func (s *Server) Route() *mux.Router { - authMiddleware := middleware.NewAuthMiddleware(s.authClient, s.dbx) + authMiddleware := middleware.NewAuth(s.authClient, s.dbx) corsMiddleware := cors.New(cors.Options{ AllowedOrigins: []string{"*"}, AllowedHeaders: []string{"Authorization"}, diff --git a/backend/service/article.go b/backend/service/article.go index 2a3c58391..92b862d11 100644 --- a/backend/service/article.go +++ b/backend/service/article.go @@ -13,7 +13,7 @@ type Article struct { db *sqlx.DB } -func NewArticleService(db *sqlx.DB) *Article { +func NewArticle(db *sqlx.DB) *Article { return &Article{db} } From 8e3905bbe2e2f7f4713812dafeccc9e20c12a33f Mon Sep 17 00:00:00 2001 From: pei0804 Date: Wed, 14 Aug 2019 00:14:34 +0900 Subject: [PATCH 02/46] fix --- backend/README.md | 40 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/backend/README.md b/backend/README.md index 8fd027a04..345b6f2bd 100644 --- a/backend/README.md +++ b/backend/README.md @@ -4,9 +4,13 @@ `../database` に構成などが定義されているので、読んでみてください。 +以下のターゲットを叩くと、databaseの準備をします。 + ```console ❯ make -f integration.mk database-init -make -C ../database init +``` + +```console which goose || GO111MODULE=off go get -u github.com/pressly/goose/cmd/goose /Users/j-chikamori/go/bin/goose docker-compose up -d @@ -72,9 +76,14 @@ PORT=1991 使っているAPI: [Exchange custom token for an ID and refresh token](https://firebase.google.com/docs/reference/rest/auth/#section-refresh-token) それぞれのキーについては、上記リンクを見れば大体分かる。 +トークンの作成 + ```console ❯ make -f integration.mk create-token UID=demo go run ./cmd/customtoken/main.go demo .idToken +``` + +```console { "kind": "identitytoolkit#VerifyCustomTokenResponse", "idToken": "idtoken", @@ -88,17 +97,27 @@ go run ./cmd/customtoken/main.go demo .idToken ## 5. Hello World +*サーバー立ち上げ* + ```console ❯ make run go run cmd/api/main.go +``` + +```console 2019/08/08 11:32:47 server.go:51: Listening on port 1991 ``` サーバーを立ち上げた状態で、別シェルから以下を叩く。 +*認証ありエンドポイントへのリクエスト* + ```console ❯ make -f integration.mk req-private curl -v -H "Authorization: Bearer Hoge" localhost:1991/private +``` + +```console * Trying ::1... * TCP_NODELAY set * Connected to localhost (::1) port 1991 (#0) @@ -116,10 +135,16 @@ curl -v -H "Authorization: Bearer Hoge" localhost:1991/private < * Connection #0 to host localhost left intact {"message":"Hello from private endpoint! Your firebase uuid is demo"} +``` +*認証なしエンドポイントへのリクエスト* +```console ❯ make -f integration.mk req-public curl -v localhost:1991/public +``` + +```console * Trying ::1... * TCP_NODELAY set * Connected to localhost (::1) port 1991 (#0) @@ -149,16 +174,29 @@ curl -v localhost:1991/public コードを書き換える度に `go run` し直すのは面倒くさいので、書き換えるとrebuildしてくれるコマンドを用意しているので活用してください。 +realizeをgo getする + ```console ❯ make dev-deps GO111MODULE=off go get -u -v \ github.com/oxequa/realize +``` + +``` github.com/oxequa/realize (download) github.com/oxequa/interact (download) github.com/fatih/color (download) github.com/fsnotify/fsnotify (download) +``` + +realizeを使って実行する + +```console ❯ make refresh-run realize start +``` + +``` console [11:36:48][BACKEND] : Watching 21 file/s 14 folder/s [11:36:48][BACKEND] : Install started [11:36:51][BACKEND] : Install completed in 3.653 s From 1460d9531567d4ddb584e7aad6ca5be461df1763 Mon Sep 17 00:00:00 2001 From: pei0804 Date: Wed, 14 Aug 2019 00:20:19 +0900 Subject: [PATCH 03/46] =?UTF-8?q?=E3=81=BF=E3=81=99=E3=81=A3=E3=81=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/controller/article.go | 16 ++++++++-------- backend/dbutil/tx.go | 4 ++-- backend/server.go | 13 ++++++------- 3 files changed, 16 insertions(+), 17 deletions(-) diff --git a/backend/controller/article.go b/backend/controller/article.go index 6a2a24d54..647e6b9e5 100644 --- a/backend/controller/article.go +++ b/backend/controller/article.go @@ -17,15 +17,15 @@ import ( ) type Article struct { - dbx *sqlx.DB + db *sqlx.DB } -func NewArticle(dbx *sqlx.DB) *Article { - return &Article{dbx: dbx} +func NewArticle(db *sqlx.DB) *Article { + return &Article{db: db} } func (a *Article) Index(w http.ResponseWriter, r *http.Request) (int, interface{}, error) { - articles, err := repository.AllArticle(a.dbx) + articles, err := repository.AllArticle(a.db) if err != nil { return http.StatusInternalServerError, nil, err } @@ -44,7 +44,7 @@ func (a *Article) Show(w http.ResponseWriter, r *http.Request) (int, interface{} return http.StatusBadRequest, nil, err } - article, err := repository.FindArticle(a.dbx, aid) + article, err := repository.FindArticle(a.db, aid) if err != nil && err == sql.ErrNoRows { return http.StatusNotFound, nil, err } else if err != nil { @@ -60,7 +60,7 @@ func (a *Article) Create(w http.ResponseWriter, r *http.Request) (int, interface return http.StatusBadRequest, nil, err } - articleService := service.NewArticle(a.dbx) + articleService := service.NewArticle(a.db) id, err := articleService.Create(newArticle) if err != nil { return http.StatusInternalServerError, nil, err @@ -87,7 +87,7 @@ func (a *Article) Update(w http.ResponseWriter, r *http.Request) (int, interface return http.StatusBadRequest, nil, err } - articleService := service.NewArticle(a.dbx) + articleService := service.NewArticle(a.db) err = articleService.Update(aid, reqArticle) if err != nil && errors.Cause(err) == sql.ErrNoRows { return http.StatusNotFound, nil, err @@ -110,7 +110,7 @@ func (a *Article) Destroy(w http.ResponseWriter, r *http.Request) (int, interfac return http.StatusBadRequest, nil, err } - articleService := service.NewArticle(a.dbx) + articleService := service.NewArticle(a.db) err = articleService.Destroy(aid) if err != nil && errors.Cause(err) == sql.ErrNoRows { return http.StatusNotFound, nil, err diff --git a/backend/dbutil/tx.go b/backend/dbutil/tx.go index 99f025778..65947ca3f 100644 --- a/backend/dbutil/tx.go +++ b/backend/dbutil/tx.go @@ -11,8 +11,8 @@ import ( // TXHandler is handler for working with transaction. // This is wrapper function for commit and rollback. -func TXHandler(dbx *sqlx.DB, f func(*sqlx.Tx) error) error { - tx, err := dbx.Beginx() +func TXHandler(db *sqlx.DB, f func(*sqlx.Tx) error) error { + tx, err := db.Beginx() if err != nil { return errors.Wrap(err, "start transaction failed") } diff --git a/backend/server.go b/backend/server.go index 7be2cb826..76677d1bc 100644 --- a/backend/server.go +++ b/backend/server.go @@ -23,7 +23,7 @@ import ( ) type Server struct { - dbx *sqlx.DB + db *sqlx.DB router *mux.Router authClient *auth.Client } @@ -40,12 +40,11 @@ func (s *Server) Init(datasource string) { s.authClient = authClient db := db2.NewDB(datasource) - dbx, err := db.Open() - dbx.Close() + dbcon, err := db.Open() if err != nil { log.Fatalf("failed db init. %s", err) } - s.dbx = dbx + s.db = dbcon s.router = s.Route() } @@ -61,7 +60,7 @@ func (s *Server) Run(addr string) { } func (s *Server) Route() *mux.Router { - authMiddleware := middleware.NewAuth(s.authClient, s.dbx) + authMiddleware := middleware.NewAuth(s.authClient, s.db) corsMiddleware := cors.New(cors.Options{ AllowedOrigins: []string{"*"}, AllowedHeaders: []string{"Authorization"}, @@ -78,9 +77,9 @@ func (s *Server) Route() *mux.Router { r := mux.NewRouter() r.Methods(http.MethodGet).Path("/public").Handler(commonChain.Then(sample.NewPublicHandler())) - r.Methods(http.MethodGet).Path("/private").Handler(authChain.Then(sample.NewPrivateHandler(s.dbx))) + r.Methods(http.MethodGet).Path("/private").Handler(authChain.Then(sample.NewPrivateHandler(s.db))) - articleController := controller.NewArticle(s.dbx) + articleController := controller.NewArticle(s.db) r.Methods(http.MethodPost).Path("/articles").Handler(authChain.Then(AppHandler{articleController.Create})) r.Methods(http.MethodPut).Path("/articles/{id}").Handler(authChain.Then(AppHandler{articleController.Update})) r.Methods(http.MethodDelete).Path("/articles/{id}").Handler(authChain.Then(AppHandler{articleController.Destroy})) From 2bc8d4adbd2d55f18a62938eda902fdd3f10119b Mon Sep 17 00:00:00 2001 From: pei0804 Date: Wed, 14 Aug 2019 00:21:48 +0900 Subject: [PATCH 04/46] fix --- backend/sample/private.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/backend/sample/private.go b/backend/sample/private.go index 0be296c5c..94abdfd21 100644 --- a/backend/sample/private.go +++ b/backend/sample/private.go @@ -12,12 +12,12 @@ import ( ) type PrivateHandler struct { - dbx *sqlx.DB + db *sqlx.DB } -func NewPrivateHandler(dbx *sqlx.DB) *PrivateHandler { +func NewPrivateHandler(db *sqlx.DB) *PrivateHandler { return &PrivateHandler{ - dbx: dbx, + db: db, } } @@ -28,7 +28,7 @@ func (h *PrivateHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { WriteJSON(nil, w, http.StatusInternalServerError) return } - user, err := repository.GetUser(h.dbx, contextUser.FirebaseUID) + user, err := repository.GetUser(h.db, contextUser.FirebaseUID) if err != nil { log.Printf("Show user failed: %s", err) WriteJSON(nil, w, http.StatusInternalServerError) From b6aef0de79dd18f5060d65d150f044e1b1032591 Mon Sep 17 00:00:00 2001 From: pei0804 Date: Wed, 14 Aug 2019 00:22:31 +0900 Subject: [PATCH 05/46] fix --- backend/README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/backend/README.md b/backend/README.md index 345b6f2bd..9bbd12924 100644 --- a/backend/README.md +++ b/backend/README.md @@ -1,3 +1,7 @@ +# Slide + +https://go-talks.appspot.com/github.com/voyagegroup/talks/2019/treasure-go-day2/intro.slide#1 + # Getting Started ## 1. Database 立ち上げ From 6ed4726d2865f0526ece40328672606d196f0fae Mon Sep 17 00:00:00 2001 From: yamachu Date: Wed, 14 Aug 2019 13:17:25 +0900 Subject: [PATCH 06/46] =?UTF-8?q?Client=E3=81=AEsetup=E3=81=8Chard?= =?UTF-8?q?=E3=81=AA=E3=81=AE=E3=81=A7=E6=9C=80=E4=BD=8E=E9=99=90=E3=81=AE?= =?UTF-8?q?=E5=8B=95=E3=81=8B=E3=81=9B=E3=82=8B=E3=82=88=E3=81=86=E3=81=AA?= =?UTF-8?q?=E7=AD=8B=E9=81=93=E3=82=92=E7=A4=BA=E3=81=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/README.md | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 frontend/src/README.md diff --git a/frontend/src/README.md b/frontend/src/README.md new file mode 100644 index 000000000..54d84a326 --- /dev/null +++ b/frontend/src/README.md @@ -0,0 +1,36 @@ +# Getting Started + +## 1. 環境構築 + +**make setup** + +設定ファイルのコピーとパッケージのセットアップを行う + +```console +❯ make init +``` + +**Firebase API Key** + +API講義で作ったFirebaseプロジェクトを使う + +1. [Firebase](https://firebase.google.com/) のプロジェクトページを開く +1. `Project Overview` の右にある `⚙(歯車)` ボタンを押して、 `プロジェクトの設定` へ遷移 +1. `Firebase SDK snippet` をコピーして、`.env` ファイルの `FIREBASE_...` に設定する + +``` +FIREBASE_APIKEY="REPLACE_ME" +FIREBASE_AUTHDOMAIN="REPLACE_ME" +FIREBASE_DATABASEURL="REPLACE_ME" +FIREBASE_PROJECTID="REPLACE_ME" +FIREBASE_MESSAGINGSENDERID="REPLACE_ME" +FIREBASE_APPID="REPLACE_ME" +``` + +## 2. Hello World + +**フロント立ち上げ** + +```console +❯ make dev +``` From 97b881163c707936293ccf2d9f97eb5cbad00fb3 Mon Sep 17 00:00:00 2001 From: onetk Date: Fri, 16 Aug 2019 00:44:25 +0900 Subject: [PATCH 07/46] merge front/back --- frontend/index.html | 20 +++---- frontend/package-lock.json | 41 +++++++++---- frontend/src/App.js | 116 +++++++++++++++++++++++++++++++++++-- frontend/src/README.md | 36 ------------ 4 files changed, 149 insertions(+), 64 deletions(-) delete mode 100644 frontend/src/README.md diff --git a/frontend/index.html b/frontend/index.html index 1e4865fd3..9bb1b903b 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -1,14 +1,12 @@ - + + + + + - - - - - - -
- - - + +
+ + diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 6fb9b5f70..af7010b29 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -3639,7 +3639,8 @@ "version": "2.1.1", "resolved": false, "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true + "dev": true, + "optional": true }, "aproba": { "version": "1.2.0", @@ -3663,13 +3664,15 @@ "version": "1.0.0", "resolved": false, "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", - "dev": true + "dev": true, + "optional": true }, "brace-expansion": { "version": "1.1.11", "resolved": false, "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, + "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -3686,19 +3689,22 @@ "version": "1.1.0", "resolved": false, "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", - "dev": true + "dev": true, + "optional": true }, "concat-map": { "version": "0.0.1", "resolved": false, "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true + "dev": true, + "optional": true }, "console-control-strings": { "version": "1.1.0", "resolved": false, "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", - "dev": true + "dev": true, + "optional": true }, "core-util-is": { "version": "1.0.2", @@ -3829,7 +3835,8 @@ "version": "2.0.3", "resolved": false, "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "dev": true + "dev": true, + "optional": true }, "ini": { "version": "1.3.5", @@ -3843,6 +3850,7 @@ "resolved": false, "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", "dev": true, + "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -3859,6 +3867,7 @@ "resolved": false, "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", "dev": true, + "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -3867,13 +3876,15 @@ "version": "0.0.8", "resolved": false, "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", - "dev": true + "dev": true, + "optional": true }, "minipass": { "version": "2.3.5", "resolved": false, "integrity": "sha512-Gi1W4k059gyRbyVUZQ4mEqLm0YIUiGYfvxhF6SIlk3ui1WVxMTGfGdQ2SInh3PDrRTVvPKgULkpJtT4RH10+VA==", "dev": true, + "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -3894,6 +3905,7 @@ "resolved": false, "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", "dev": true, + "optional": true, "requires": { "minimist": "0.0.8" } @@ -3982,7 +3994,8 @@ "version": "1.0.1", "resolved": false, "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", - "dev": true + "dev": true, + "optional": true }, "object-assign": { "version": "4.1.1", @@ -3996,6 +4009,7 @@ "resolved": false, "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", "dev": true, + "optional": true, "requires": { "wrappy": "1" } @@ -4091,7 +4105,8 @@ "version": "5.1.2", "resolved": false, "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true + "dev": true, + "optional": true }, "safer-buffer": { "version": "2.1.2", @@ -4133,6 +4148,7 @@ "resolved": false, "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", "dev": true, + "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -4154,6 +4170,7 @@ "resolved": false, "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", "dev": true, + "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -4202,13 +4219,15 @@ "version": "1.0.2", "resolved": false, "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true + "dev": true, + "optional": true }, "yallist": { "version": "3.0.3", "resolved": false, "integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==", - "dev": true + "dev": true, + "optional": true } } }, diff --git a/frontend/src/App.js b/frontend/src/App.js index c6302ba8a..f8e27c5fc 100644 --- a/frontend/src/App.js +++ b/frontend/src/App.js @@ -2,12 +2,84 @@ import { h, Component } from "preact"; import firebase from "./firebase"; import { getPrivateMessage, getPublicMessage } from "./api"; +const API_ENDPOINT = process.env.BACKEND_API_BASE; + +class ArticleClient { + constructor() { + this.token = ""; + } + + async postArticle(title, body) { + await this.getToken(); + return fetch(`${API_ENDPOINT}/articles`, { + method: "POST", + headers: { + Authorization: `Bearer ${this.token}` + }, + body: JSON.stringify({ title, body }) + }).then(v => v.json()); + } + + async getToken() { + if (this.token === "") { + this.token = await firebase.auth().currentUser.getIdToken(); + } + } +} + +const successHandler = function(text) { + const lists = JSON.parse(text); + const items = []; + for (let i = 0; i < lists.length; i++) { + // console.log(lists[i]); + items.push( +
+ {lists[i].id} {lists[i].title} {lists[i].body} +
+ ); + } + + return items; +}; +const errorHandler = function(error) { + return error.message; +}; + +function request(method, url) { + return fetch(url).then(function(res) { + if (res.ok) { + // console.log(res.status); + if (res.status == 200 && method == "PUT") { + return "success!!"; + } + + try { + JSON.parse(res); + return res.json(); + } catch (error) { + return res.text(); + } + } + if (res.status < 500) { + throw new Error("4xx error"); + } + throw new Error("5xx error"); + }); +} + class App extends Component { constructor() { super(); this.state.user = null; this.state.message = ""; this.state.errorMessage = ""; + this.state.token = ""; + } + + async getToken() { + if (this.state.token === "") { + this.state.token = await firebase.auth().currentUser.getIdToken(); + } } componentDidMount() { @@ -40,18 +112,50 @@ class App extends Component { }); } + getAllArticles() { + request("GET", "http://localhost:1991/articles") + .then(resp => { + this.setState({ + message: successHandler(resp) + }); + }) + .catch(error => { + this.setState({ + errorMessage: errorHandler(error) + }); + }); + } + + async postArticles() { + await this.getToken(); + + return fetch(`http://localhost:1991/articles`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${this.state.token}` + }, + body: JSON.stringify({ title: "ok", body: "google" }) + }); + } + render(props, state) { if (state.user === null) { return ; } + return (
-
{state.message}
-

{state.errorMessage}

- - +
{state.message}
+
+

{state.errorMessage}

+ + + + +
); } diff --git a/frontend/src/README.md b/frontend/src/README.md deleted file mode 100644 index 54d84a326..000000000 --- a/frontend/src/README.md +++ /dev/null @@ -1,36 +0,0 @@ -# Getting Started - -## 1. 環境構築 - -**make setup** - -設定ファイルのコピーとパッケージのセットアップを行う - -```console -❯ make init -``` - -**Firebase API Key** - -API講義で作ったFirebaseプロジェクトを使う - -1. [Firebase](https://firebase.google.com/) のプロジェクトページを開く -1. `Project Overview` の右にある `⚙(歯車)` ボタンを押して、 `プロジェクトの設定` へ遷移 -1. `Firebase SDK snippet` をコピーして、`.env` ファイルの `FIREBASE_...` に設定する - -``` -FIREBASE_APIKEY="REPLACE_ME" -FIREBASE_AUTHDOMAIN="REPLACE_ME" -FIREBASE_DATABASEURL="REPLACE_ME" -FIREBASE_PROJECTID="REPLACE_ME" -FIREBASE_MESSAGINGSENDERID="REPLACE_ME" -FIREBASE_APPID="REPLACE_ME" -``` - -## 2. Hello World - -**フロント立ち上げ** - -```console -❯ make dev -``` From e9454b077da00cdd2e9c2a4f57262ea8b446c6b9 Mon Sep 17 00:00:00 2001 From: onetk Date: Fri, 16 Aug 2019 00:46:47 +0900 Subject: [PATCH 08/46] add back/database --- backend/controller/article.go | 96 +++++++++++++++++-- backend/integration.mk | 8 +- backend/model/article.go | 22 ++++- backend/repository/article.go | 54 ++++++++++- backend/repository/user.go | 2 +- backend/server.go | 28 ++++-- backend/service/article.go | 62 +++++++++++- .../3_add_user_column_to_article.sql | 3 + database/migrations/4_add_article_comment.sql | 12 +++ database/migrations/5_add_table_tag.sql | 10 ++ .../migrations/6_add_table_article_tag.sql | 9 ++ 11 files changed, 282 insertions(+), 24 deletions(-) create mode 100644 database/migrations/3_add_user_column_to_article.sql create mode 100644 database/migrations/4_add_article_comment.sql create mode 100644 database/migrations/5_add_table_tag.sql create mode 100644 database/migrations/6_add_table_article_tag.sql diff --git a/backend/controller/article.go b/backend/controller/article.go index 647e6b9e5..954c298b2 100644 --- a/backend/controller/article.go +++ b/backend/controller/article.go @@ -3,6 +3,7 @@ package controller import ( "database/sql" "encoding/json" + "fmt" "net/http" "strconv" @@ -17,21 +18,56 @@ import ( ) type Article struct { - db *sqlx.DB + dbx *sqlx.DB } -func NewArticle(db *sqlx.DB) *Article { - return &Article{db: db} +type ArticleComment struct { + dbx *sqlx.DB } +type ArticleTag struct { + dbx *sqlx.DB +} + +func NewArticle(dbx *sqlx.DB) *Article { + return &Article{dbx: dbx} +} + +func NewArticleComment(dbx *sqlx.DB) *ArticleComment { + return &ArticleComment{dbx: dbx} +} + +func NewArticleTag(dbx *sqlx.DB) *ArticleTag { + return &ArticleTag{dbx: dbx} +} + +// 返り値のintは status code func (a *Article) Index(w http.ResponseWriter, r *http.Request) (int, interface{}, error) { - articles, err := repository.AllArticle(a.db) + articles, err := repository.AllArticle(a.dbx) if err != nil { return http.StatusInternalServerError, nil, err } return http.StatusOK, articles, nil } +func (a *Article) SearchIndex(w http.ResponseWriter, r *http.Request) (int, interface{}, error) { + vars := mux.Vars(r) + tag, ok := vars["tag"] + if !ok { + return http.StatusBadRequest, nil, &httputil.HTTPError{Message: "invalid path parameter"} + + } + + articles, err := repository.SearchArticle(a.dbx, tag) + if err != nil && err == sql.ErrNoRows { + return http.StatusNotFound, nil, err + } else if err != nil { + return http.StatusInternalServerError, nil, err + } + + return http.StatusOK, articles, nil +} + func (a *Article) Show(w http.ResponseWriter, r *http.Request) (int, interface{}, error) { vars := mux.Vars(r) id, ok := vars["id"] @@ -44,7 +80,7 @@ func (a *Article) Show(w http.ResponseWriter, r *http.Request) (int, interface{} return http.StatusBadRequest, nil, err } - article, err := repository.FindArticle(a.db, aid) + article, err := repository.FindArticle(a.dbx, aid) if err != nil && err == sql.ErrNoRows { return http.StatusNotFound, nil, err } else if err != nil { @@ -60,7 +96,13 @@ func (a *Article) Create(w http.ResponseWriter, r *http.Request) (int, interface return http.StatusBadRequest, nil, err } - articleService := service.NewArticle(a.db) + user, err := httputil.GetUserFromContext(r.Context()) + if err != nil { + fmt.Println(err) + } + newArticle.UserID = &user.ID + + articleService := service.NewArticleService(a.dbx) id, err := articleService.Create(newArticle) if err != nil { return http.StatusInternalServerError, nil, err @@ -87,7 +129,7 @@ func (a *Article) Update(w http.ResponseWriter, r *http.Request) (int, interface return http.StatusBadRequest, nil, err } - articleService := service.NewArticle(a.db) + articleService := service.NewArticleService(a.dbx) err = articleService.Update(aid, reqArticle) if err != nil && errors.Cause(err) == sql.ErrNoRows { return http.StatusNotFound, nil, err @@ -110,7 +152,7 @@ func (a *Article) Destroy(w http.ResponseWriter, r *http.Request) (int, interfac return http.StatusBadRequest, nil, err } - articleService := service.NewArticle(a.db) + articleService := service.NewArticleService(a.dbx) err = articleService.Destroy(aid) if err != nil && errors.Cause(err) == sql.ErrNoRows { return http.StatusNotFound, nil, err @@ -120,3 +162,41 @@ func (a *Article) Destroy(w http.ResponseWriter, r *http.Request) (int, interfac return http.StatusNoContent, nil, nil } + +func (a *ArticleComment) CreateArticleComment(w http.ResponseWriter, r *http.Request) (int, interface{}, error) { + newArticleComment := &model.ArticleComment{} + if err := json.NewDecoder(r.Body).Decode(&newArticleComment); err != nil { + return http.StatusBadRequest, nil, err + } + + user, err := httputil.GetUserFromContext(r.Context()) + if err != nil { + fmt.Println(err) + } + newArticleComment.UserID = &user.ID + + articleCommentService := service.NewArticleCommentService(a.dbx) + id, err := articleCommentService.CreateArticleComment(newArticleComment) + if err != nil { + return http.StatusInternalServerError, nil, err + } + newArticleComment.ID = id + + return http.StatusCreated, newArticleComment, nil +} + +func (a *ArticleTag) CreateArticleTag(w http.ResponseWriter, r *http.Request) (int, interface{}, error) { + newArticleTag := &model.ArticleTag{} + if err := json.NewDecoder(r.Body).Decode(&newArticleTag); err != nil { + return http.StatusBadRequest, nil, err + } + + articleTagService := service.NewArticleTagService(a.dbx) + id, err := articleTagService.CreateArticleTag(newArticleTag) + if err != nil { + return http.StatusInternalServerError, nil, err + } + newArticleTag.ID = id + + return http.StatusCreated, newArticleTag, nil +} diff --git a/backend/integration.mk b/backend/integration.mk index 628adc375..af1ef72d2 100644 --- a/backend/integration.mk +++ b/backend/integration.mk @@ -9,6 +9,8 @@ ARTICLE_ID:=1 ARTICLE_TITLE:=title ARTICLE_BODY:=body +ARTICLE_COMMENT_BODY:=bodycomment + create-token: go run ./cmd/customtoken/main.go $(UID) $(TOKEN_FILE) @@ -22,7 +24,7 @@ req-articles-get: curl -v $(HOST):$(PORT)/articles/$(ARTICLE_ID) req-articles-post: - curl -v -XPOST -H "Authorization: Bearer $(shell cat ./$(TOKEN_FILE))" $(HOST):$(PORT)/articles -d '{"title": "$(ARTICLE_TITLE)", "body": "$(ARTICLE_BODY)"}' + curl -v -XPOST -H "Authorization: Bearer $(shell cat ./$(TOKEN_FILE))" $(HOST):$(PORT)/articles -d '{"title": "$(ARTICLE_TITLE)", "body": "$(ARTICLE_BODY)", "tag_ids": [1, 2]}' req-articles-update: curl -v -XPUT -H "Authorization: Bearer $(shell cat ./$(TOKEN_FILE))" $(HOST):$(PORT)/articles/$(ARTICLE_ID) -d '{"title": "$(ARTICLE_TITLE)", "body": "$(ARTICLE_BODY)"}' @@ -30,6 +32,10 @@ req-articles-update: req-articles-delete: curl -v -XDELETE -H "Authorization: Bearer $(shell cat ./$(TOKEN_FILE))" $(HOST):$(PORT)/articles/$(ARTICLE_ID) +req-articles-comment-post: + curl -v -XPOST -H "Authorization: Bearer $(shell cat ./$(TOKEN_FILE))" $(HOST):$(PORT)/articles/$(ARTICLE_ID)/comments -d '{"body": "$(ARTICLE_COMMENT_BODY)"}' + + req-public: curl -v $(HOST):$(PORT)/public diff --git a/backend/model/article.go b/backend/model/article.go index 864a9659f..abce34753 100644 --- a/backend/model/article.go +++ b/backend/model/article.go @@ -1,7 +1,23 @@ package model +type ArticleTag struct { + ID int64 `db:"id" json:"id"` + ArticleID int64 `db:"ariticle_id" json:"article_id"` + Tag string `db:"tag" json:"tag"` +} + +type ArticleComment struct { + ID int64 `db:"id" json:"id"` + Body string `db:"body" json:"body"` + UserID *int64 `db:"user_id" json:"user_id"` + ArticleID int64 `db:"ariticle_id" json:"article_id"` +} + type Article struct { - ID int64 `db:"id" json:"id"` - Title string `db:"title" json:"title"` - Body string `db:"body" json:"body"` + ID int64 `db:"id" json:"id"` + Title string `db:"title" json:"title"` + Body string `db:"body" json:"body"` + UserID *int64 `db:"user_id" json:"user_id"` + ArticleID int64 `db:"ariticle_id" json:"article_id"` + Tag *string `db:"tag" json:"tag"` } diff --git a/backend/repository/article.go b/backend/repository/article.go index 6b556db1d..9c3e0d7f3 100644 --- a/backend/repository/article.go +++ b/backend/repository/article.go @@ -9,7 +9,33 @@ import ( func AllArticle(db *sqlx.DB) ([]model.Article, error) { a := make([]model.Article, 0) - if err := db.Select(&a, `SELECT id, title, body FROM article`); err != nil { + if err := db.Select(&a, `SELECT id, title, body, user_id FROM article`); err != nil { + return nil, err + } + return a, nil +} + +// func SearchArticle(db *sqlx.DB, tag string) ([]model.Article, error) { +// a := make([]model.Article, 0) +// if err := db.Select(&a, `SELECT id, title, body, user_id, tag FROM article WHERE tag= ?`, tag); err != nil { +// return nil, err +// } +// return a, nil +// } + +func SearchArticle(db *sqlx.DB, tag string) ([]model.Article, error) { + a := make([]model.Article, 0) + if err := db.Select(&a, + ` SELECT + article.id, title, body, user_id, article.tag + FROM + article + INNER JOIN + article_tag + ON + article.id = article_tag.article_id + WHERE + article_tag.tag= ? `, tag); err != nil { return nil, err } return a, nil @@ -27,13 +53,13 @@ SELECT id, title, body FROM article WHERE id = ? func CreateArticle(db *sqlx.Tx, a *model.Article) (sql.Result, error) { stmt, err := db.Prepare(` -INSERT INTO article (title, body) VALUES (?, ?) +INSERT INTO article (user_id, title, body, tag) VALUES (?, ?, ?, ?) `) if err != nil { return nil, err } defer stmt.Close() - return stmt.Exec(a.Title, a.Body) + return stmt.Exec(a.UserID, a.Title, a.Body, a.Tag) } func UpdateArticle(db *sqlx.Tx, id int64, a *model.Article) (sql.Result, error) { @@ -57,3 +83,25 @@ DELETE FROM article WHERE id = ? defer stmt.Close() return stmt.Exec(id) } + +func CreateArticleComment(db *sqlx.Tx, a *model.ArticleComment) (sql.Result, error) { + stmt, err := db.Prepare(` +INSERT INTO article_comment (user_id, article_id, body) VALUES (?, ?, ?) +`) + if err != nil { + return nil, err + } + defer stmt.Close() + return stmt.Exec(a.UserID, a.ArticleID, a.Body) +} + +func CreateArticleTag(db *sqlx.Tx, a *model.ArticleTag) (sql.Result, error) { + stmt, err := db.Prepare(` +INSERT INTO article_tag (article_id, tag) VALUES (?, ?) +`) + if err != nil { + return nil, err + } + defer stmt.Close() + return stmt.Exec(a.ArticleID, a.Tag) +} diff --git a/backend/repository/user.go b/backend/repository/user.go index 549447173..3811a4364 100644 --- a/backend/repository/user.go +++ b/backend/repository/user.go @@ -10,7 +10,7 @@ import ( func GetUser(db *sqlx.DB, uid string) (*model.User, error) { var u model.User if err := db.Get(&u, ` -SELECT id, firebase_uid, display_name, email, photo_url FROM user WHERE firebase_uid = ? LIMIT 1 +select id, firebase_uid, display_name, email, photo_url from user where firebase_uid = ? limit 1 `, uid); err != nil { return nil, err } diff --git a/backend/server.go b/backend/server.go index 76677d1bc..f4ca06b64 100644 --- a/backend/server.go +++ b/backend/server.go @@ -23,7 +23,7 @@ import ( ) type Server struct { - db *sqlx.DB + dbx *sqlx.DB router *mux.Router authClient *auth.Client } @@ -40,11 +40,11 @@ func (s *Server) Init(datasource string) { s.authClient = authClient db := db2.NewDB(datasource) - dbcon, err := db.Open() + dbx, err := db.Open() if err != nil { log.Fatalf("failed db init. %s", err) } - s.db = dbcon + s.dbx = dbx s.router = s.Route() } @@ -59,11 +59,18 @@ func (s *Server) Run(addr string) { } } +// func (s *Server) Route() *mux.Router { +// authMiddleware := middleware.NewAuthMiddleware(s.authClient, s.dbx) +// corsMiddleware := cors.New(cors.Options{ +// AllowedOrigins: []string{"*"}, +// AllowedHeaders: []string{"Authorization"}, +// }) func (s *Server) Route() *mux.Router { - authMiddleware := middleware.NewAuth(s.authClient, s.db) + authMiddleware := middleware.NewAuth(s.authClient, s.dbx) corsMiddleware := cors.New(cors.Options{ AllowedOrigins: []string{"*"}, - AllowedHeaders: []string{"Authorization"}, + AllowedHeaders: []string{"Authorization", "Content-Type"}, + AllowedMethods: []string{"GET", "POST", "DELETE", "PUT", "Head"}, }) commonChain := alice.New( @@ -77,14 +84,21 @@ func (s *Server) Route() *mux.Router { r := mux.NewRouter() r.Methods(http.MethodGet).Path("/public").Handler(commonChain.Then(sample.NewPublicHandler())) - r.Methods(http.MethodGet).Path("/private").Handler(authChain.Then(sample.NewPrivateHandler(s.db))) + r.Methods(http.MethodGet).Path("/private").Handler(authChain.Then(sample.NewPrivateHandler(s.dbx))) - articleController := controller.NewArticle(s.db) + articleController := controller.NewArticle(s.dbx) r.Methods(http.MethodPost).Path("/articles").Handler(authChain.Then(AppHandler{articleController.Create})) r.Methods(http.MethodPut).Path("/articles/{id}").Handler(authChain.Then(AppHandler{articleController.Update})) r.Methods(http.MethodDelete).Path("/articles/{id}").Handler(authChain.Then(AppHandler{articleController.Destroy})) r.Methods(http.MethodGet).Path("/articles").Handler(commonChain.Then(AppHandler{articleController.Index})) r.Methods(http.MethodGet).Path("/articles/{id}").Handler(commonChain.Then(AppHandler{articleController.Show})) + r.Methods(http.MethodGet).Path("/articles/search/{tag}").Handler(commonChain.Then(AppHandler{articleController.SearchIndex})) + + articleCommentController := controller.NewArticleComment(s.dbx) + r.Methods(http.MethodPost).Path("/articles/{article_id}/comments").Handler(authChain.Then(AppHandler{articleCommentController.CreateArticleComment})) + + articleTagController := controller.NewArticleTag(s.dbx) + r.Methods(http.MethodPost).Path("/articles/tag/{article_id}").Handler(authChain.Then(AppHandler{articleTagController.CreateArticleTag})) r.PathPrefix("").Handler(commonChain.Then(http.StripPrefix("/img", http.FileServer(http.Dir("./img"))))) return r diff --git a/backend/service/article.go b/backend/service/article.go index 92b862d11..95f00943e 100644 --- a/backend/service/article.go +++ b/backend/service/article.go @@ -13,10 +13,26 @@ type Article struct { db *sqlx.DB } -func NewArticle(db *sqlx.DB) *Article { +func NewArticleService(db *sqlx.DB) *Article { return &Article{db} } +type ArticleComment struct { + db *sqlx.DB +} + +type ArticleTag struct { + db *sqlx.DB +} + +func NewArticleCommentService(db *sqlx.DB) *ArticleComment { + return &ArticleComment{db} +} + +func NewArticleTagService(db *sqlx.DB) *ArticleTag { + return &ArticleTag{db} +} + func (a *Article) Update(id int64, newArticle *model.Article) error { _, err := repository.FindArticle(a.db, id) if err != nil { @@ -80,3 +96,47 @@ func (a *Article) Create(newArticle *model.Article) (int64, error) { } return createdId, nil } + +func (a *ArticleComment) CreateArticleComment(newArticleComment *model.ArticleComment) (int64, error) { + var createdId int64 + if err := dbutil.TXHandler(a.db, func(tx *sqlx.Tx) error { + result, err := repository.CreateArticleComment(tx, newArticleComment) + if err != nil { + return err + } + if err := tx.Commit(); err != nil { + return err + } + id, err := result.LastInsertId() + if err != nil { + return err + } + createdId = id + return err + }); err != nil { + return 0, errors.Wrap(err, "failed article insert transaction") + } + return createdId, nil +} + +func (a *ArticleTag) CreateArticleTag(newArticleTag *model.ArticleTag) (int64, error) { + var createdId int64 + if err := dbutil.TXHandler(a.db, func(tx *sqlx.Tx) error { + result, err := repository.CreateArticleTag(tx, newArticleTag) + if err != nil { + return err + } + if err := tx.Commit(); err != nil { + return err + } + id, err := result.LastInsertId() + if err != nil { + return err + } + createdId = id + return err + }); err != nil { + return 0, errors.Wrap(err, "failed article insert transaction") + } + return createdId, nil +} diff --git a/database/migrations/3_add_user_column_to_article.sql b/database/migrations/3_add_user_column_to_article.sql new file mode 100644 index 000000000..a4a96701c --- /dev/null +++ b/database/migrations/3_add_user_column_to_article.sql @@ -0,0 +1,3 @@ +-- +goose Up +ALTER TABLE article ADD user_id int(10) UNSIGNED DEFAULT NULL; +ALTER TABLE article ADD CONSTRAINT article_fk_user FOREIGN KEY (user_id) REFERENCES user(id); \ No newline at end of file diff --git a/database/migrations/4_add_article_comment.sql b/database/migrations/4_add_article_comment.sql new file mode 100644 index 000000000..6388cd0b4 --- /dev/null +++ b/database/migrations/4_add_article_comment.sql @@ -0,0 +1,12 @@ +-- +goose Up +CREATE TABLE article_comment ( + id int(10) UNSIGNED NOT NULL AUTO_INCREMENT, + user_id int(10) UNSIGNED NOT NULL, + article_id int(10) UNSIGNED NOT NULL, + body VARCHAR(255), + ctime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + utime TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (id), + CONSTRAINT comment_fk_user FOREIGN KEY (user_id) REFERENCES user (id), + CONSTRAINT comment_fk_article FOREIGN KEY (article_id) REFERENCES article (id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; \ No newline at end of file diff --git a/database/migrations/5_add_table_tag.sql b/database/migrations/5_add_table_tag.sql new file mode 100644 index 000000000..3599edeb2 --- /dev/null +++ b/database/migrations/5_add_table_tag.sql @@ -0,0 +1,10 @@ +-- +goose Up +CREATE TABLE tag ( + id int(10) UNSIGNED NOT NULL AUTO_INCREMENT, + name VARCHAR(255), + ctime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +INSERT INTO tag (name) VALUES ('tag1'); +INSERT INTO tag (name) VALUES ('tag2'); diff --git a/database/migrations/6_add_table_article_tag.sql b/database/migrations/6_add_table_article_tag.sql new file mode 100644 index 000000000..fa61bb5cb --- /dev/null +++ b/database/migrations/6_add_table_article_tag.sql @@ -0,0 +1,9 @@ +-- +goose Up +CREATE TABLE article_tag ( + article_id int(10) UNSIGNED NOT NULL, + tag_id int(10) UNSIGNED NOT NULL, + ctime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (article_id, tag_id), + CONSTRAINT article_tag_fk_article FOREIGN KEY (article_id) REFERENCES article (id), + CONSTRAINT article_tag_fk_tag FOREIGN KEY (tag_id) REFERENCES tag (id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; \ No newline at end of file From a33cd2712317a5d8d0344e99fa1f44174a124161 Mon Sep 17 00:00:00 2001 From: onetk Date: Fri, 16 Aug 2019 11:37:47 +0900 Subject: [PATCH 09/46] add delete all /fix db cascade type --- backend/controller/article.go | 13 ++++++++ backend/integration.mk | 3 ++ backend/repository/article.go | 32 +++++++++++++++++-- backend/server.go | 1 + backend/service/article.go | 17 ++++++++++ .../migrations/10_add_table_article_tag.sql | 9 ++++++ .../migrations/11_add_article_comment.sql | 12 +++++++ frontend/src/App.js | 14 +++++++- 8 files changed, 98 insertions(+), 3 deletions(-) create mode 100644 database/migrations/10_add_table_article_tag.sql create mode 100644 database/migrations/11_add_article_comment.sql diff --git a/backend/controller/article.go b/backend/controller/article.go index 954c298b2..b9b2223c5 100644 --- a/backend/controller/article.go +++ b/backend/controller/article.go @@ -163,6 +163,19 @@ func (a *Article) Destroy(w http.ResponseWriter, r *http.Request) (int, interfac return http.StatusNoContent, nil, nil } +func (a *Article) DestroyAll(w http.ResponseWriter, r *http.Request) (int, interface{}, error) { + + articleService := service.NewArticleService(a.dbx) + err := articleService.DestroyAll() + if err != nil && errors.Cause(err) == sql.ErrNoRows { + return http.StatusNotFound, nil, err + } else if err != nil { + return http.StatusInternalServerError, nil, err + } + + return http.StatusNoContent, nil, nil +} + func (a *ArticleComment) CreateArticleComment(w http.ResponseWriter, r *http.Request) (int, interface{}, error) { newArticleComment := &model.ArticleComment{} if err := json.NewDecoder(r.Body).Decode(&newArticleComment); err != nil { diff --git a/backend/integration.mk b/backend/integration.mk index af1ef72d2..b77d58280 100644 --- a/backend/integration.mk +++ b/backend/integration.mk @@ -32,6 +32,9 @@ req-articles-update: req-articles-delete: curl -v -XDELETE -H "Authorization: Bearer $(shell cat ./$(TOKEN_FILE))" $(HOST):$(PORT)/articles/$(ARTICLE_ID) +req-articles-delete-all: + curl -v -XDELETE -H "Authorization: Bearer $(shell cat ./$(TOKEN_FILE))" $(HOST):$(PORT)/articles + req-articles-comment-post: curl -v -XPOST -H "Authorization: Bearer $(shell cat ./$(TOKEN_FILE))" $(HOST):$(PORT)/articles/$(ARTICLE_ID)/comments -d '{"body": "$(ARTICLE_COMMENT_BODY)"}' diff --git a/backend/repository/article.go b/backend/repository/article.go index 9c3e0d7f3..3f22e3716 100644 --- a/backend/repository/article.go +++ b/backend/repository/article.go @@ -2,6 +2,7 @@ package repository import ( "database/sql" + "fmt" "github.com/jmoiron/sqlx" "github.com/voyagegroup/treasure-app/model" @@ -53,13 +54,13 @@ SELECT id, title, body FROM article WHERE id = ? func CreateArticle(db *sqlx.Tx, a *model.Article) (sql.Result, error) { stmt, err := db.Prepare(` -INSERT INTO article (user_id, title, body, tag) VALUES (?, ?, ?, ?) +INSERT INTO article (user_id, title, body) VALUES (?, ?, ?) `) if err != nil { return nil, err } defer stmt.Close() - return stmt.Exec(a.UserID, a.Title, a.Body, a.Tag) + return stmt.Exec(a.UserID, a.Title, a.Body) } func UpdateArticle(db *sqlx.Tx, id int64, a *model.Article) (sql.Result, error) { @@ -84,6 +85,33 @@ DELETE FROM article WHERE id = ? return stmt.Exec(id) } +func DestroyAllArticle(db *sqlx.Tx) (sql.Result, error) { + fmt.Println("-- ↓↓↓ -- before -- ↓↓↓ --") + + a := make([]model.Article, 0) + if err := db.Select(&a, `SELECT id, title, body, user_id FROM article`); err != nil { + return nil, err + } + fmt.Println(a) + fmt.Println("-- before -- / -- after --") + + stmt, err := db.Prepare(` +DELETE FROM article +`) + if err != nil { + return nil, err + } + + b := make([]model.Article, 0) + if err := db.Select(&b, `SELECT id, title, body, user_id FROM article`); err != nil { + return nil, err + } + fmt.Println(b) + fmt.Println("-- ↑↑↑ -- after -- ↑↑↑ --") + defer stmt.Close() + return stmt.Exec() +} + func CreateArticleComment(db *sqlx.Tx, a *model.ArticleComment) (sql.Result, error) { stmt, err := db.Prepare(` INSERT INTO article_comment (user_id, article_id, body) VALUES (?, ?, ?) diff --git a/backend/server.go b/backend/server.go index f4ca06b64..abf2fc702 100644 --- a/backend/server.go +++ b/backend/server.go @@ -90,6 +90,7 @@ func (s *Server) Route() *mux.Router { r.Methods(http.MethodPost).Path("/articles").Handler(authChain.Then(AppHandler{articleController.Create})) r.Methods(http.MethodPut).Path("/articles/{id}").Handler(authChain.Then(AppHandler{articleController.Update})) r.Methods(http.MethodDelete).Path("/articles/{id}").Handler(authChain.Then(AppHandler{articleController.Destroy})) + r.Methods(http.MethodDelete).Path("/articles").Handler(authChain.Then(AppHandler{articleController.DestroyAll})) r.Methods(http.MethodGet).Path("/articles").Handler(commonChain.Then(AppHandler{articleController.Index})) r.Methods(http.MethodGet).Path("/articles/{id}").Handler(commonChain.Then(AppHandler{articleController.Show})) r.Methods(http.MethodGet).Path("/articles/search/{tag}").Handler(commonChain.Then(AppHandler{articleController.SearchIndex})) diff --git a/backend/service/article.go b/backend/service/article.go index 95f00943e..ffb017873 100644 --- a/backend/service/article.go +++ b/backend/service/article.go @@ -75,6 +75,23 @@ func (a *Article) Destroy(id int64) error { return nil } +func (a *Article) DestroyAll() error { + + if err := dbutil.TXHandler(a.db, func(tx *sqlx.Tx) error { + _, err := repository.DestroyAllArticle(tx) + if err != nil { + return err + } + if err := tx.Commit(); err != nil { + return err + } + return err + }); err != nil { + return errors.Wrap(err, "failed article delete transaction") + } + return nil +} + func (a *Article) Create(newArticle *model.Article) (int64, error) { var createdId int64 if err := dbutil.TXHandler(a.db, func(tx *sqlx.Tx) error { diff --git a/database/migrations/10_add_table_article_tag.sql b/database/migrations/10_add_table_article_tag.sql new file mode 100644 index 000000000..0993a07ab --- /dev/null +++ b/database/migrations/10_add_table_article_tag.sql @@ -0,0 +1,9 @@ +-- +goose Up +CREATE TABLE article_tag ( + id int(10) UNSIGNED NOT NULL AUTO_INCREMENT, + article_id int(10) UNSIGNED NOT NULL, + tag VARCHAR(255) NOT NULL, + ctime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (id), + CONSTRAINT article_tag_fk_article FOREIGN KEY (article_id) REFERENCES article (id) on delete cascade on update cascade +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; \ No newline at end of file diff --git a/database/migrations/11_add_article_comment.sql b/database/migrations/11_add_article_comment.sql new file mode 100644 index 000000000..bcd82fa04 --- /dev/null +++ b/database/migrations/11_add_article_comment.sql @@ -0,0 +1,12 @@ +-- +goose Up +CREATE TABLE article_comment ( + id int(10) UNSIGNED NOT NULL AUTO_INCREMENT, + user_id int(10) UNSIGNED NOT NULL, + article_id int(10) UNSIGNED NOT NULL, + body VARCHAR(255), + ctime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + utime TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (id), + CONSTRAINT comment_fk_user FOREIGN KEY (user_id) REFERENCES user (id) on delete cascade on update cascade, + CONSTRAINT comment_fk_article FOREIGN KEY (article_id) REFERENCES article (id) on delete cascade on update cascade +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; \ No newline at end of file diff --git a/frontend/src/App.js b/frontend/src/App.js index f8e27c5fc..8e60f2e37 100644 --- a/frontend/src/App.js +++ b/frontend/src/App.js @@ -125,6 +125,17 @@ class App extends Component { }); }); } + async deleteArticles() { + await this.getToken(); + + return fetch(`http://localhost:1991/articles`, { + method: "DELETE", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${this.state.token}` + } + }); + } async postArticles() { await this.getToken(); @@ -147,7 +158,7 @@ class App extends Component { return (
{state.message}
-
+

{state.errorMessage}

+
); From e3f660e33bba0616041dc76692915224802ae4cd Mon Sep 17 00:00:00 2001 From: onetk Date: Fri, 16 Aug 2019 15:45:46 +0900 Subject: [PATCH 10/46] add search box as textarea --- backend/controller/article.go | 47 +++++++++++ backend/go.mod | 1 + backend/server.go | 2 + backend/service/article.go | 1 + frontend/index.html | 1 + frontend/search.png | Bin 0 -> 35681 bytes frontend/src/App.js | 149 ++++++++++++++++++++++++++++------ frontend/src/search.png | Bin 0 -> 35681 bytes frontend/style.css | 71 ++++++++++++++++ 9 files changed, 247 insertions(+), 25 deletions(-) create mode 100644 frontend/search.png create mode 100644 frontend/src/search.png create mode 100644 frontend/style.css diff --git a/backend/controller/article.go b/backend/controller/article.go index b9b2223c5..ff85c2096 100644 --- a/backend/controller/article.go +++ b/backend/controller/article.go @@ -10,6 +10,7 @@ import ( "github.com/gorilla/mux" "github.com/jmoiron/sqlx" "github.com/pkg/errors" + "golang.org/x/net/html" "github.com/voyagegroup/treasure-app/httputil" "github.com/voyagegroup/treasure-app/model" @@ -41,6 +42,15 @@ func NewArticleTag(dbx *sqlx.DB) *ArticleTag { return &ArticleTag{dbx: dbx} } +func isDescription(attrs []html.Attribute) bool { + for _, attr := range attrs { + if attr.Key == "name" && attr.Val == "description" { + return true + } + } + return false +} + // 返り値のintは status code func (a *Article) Index(w http.ResponseWriter, r *http.Request) (int, interface{}, error) { articles, err := repository.AllArticle(a.dbx) @@ -92,6 +102,7 @@ func (a *Article) Show(w http.ResponseWriter, r *http.Request) (int, interface{} func (a *Article) Create(w http.ResponseWriter, r *http.Request) (int, interface{}, error) { newArticle := &model.Article{} + if err := json.NewDecoder(r.Body).Decode(&newArticle); err != nil { return http.StatusBadRequest, nil, err } @@ -103,6 +114,7 @@ func (a *Article) Create(w http.ResponseWriter, r *http.Request) (int, interface newArticle.UserID = &user.ID articleService := service.NewArticleService(a.dbx) + fmt.Println(newArticle) id, err := articleService.Create(newArticle) if err != nil { return http.StatusInternalServerError, nil, err @@ -112,6 +124,41 @@ func (a *Article) Create(w http.ResponseWriter, r *http.Request) (int, interface return http.StatusCreated, newArticle, nil } +func (a *Article) CreatePaper(w http.ResponseWriter, r *http.Request) (int, interface{}, error) { + + // vars := mux.Vars(r) + // keyword, ok := vars["keyword"] + // if !ok { + // return http.StatusBadRequest, nil, &httputil.HTTPError{Message: "invalid path parameter"} + // } + + // fmt.Println(keyword) + + newArticle := &model.Article{} + + if err := json.NewDecoder(r.Body).Decode(&newArticle); err != nil { + return http.StatusBadRequest, nil, err + } + fmt.Println(newArticle) + + // user, err := httputil.GetUserFromContext(r.Context()) + // if err != nil { + // fmt.Println(err) + // } + // newArticle.UserID = &user.ID + + // articleService := service.NewArticleService(a.dbx) + // fmt.Println(newArticle) + // id, err := articleService.Create(newArticle) + // if err != nil { + // return http.StatusInternalServerError, nil, err + // } + // newArticle.ID = id + + // return http.StatusCreated, newArticle, nil + return http.StatusCreated, newArticle, nil +} + func (a *Article) Update(w http.ResponseWriter, r *http.Request) (int, interface{}, error) { vars := mux.Vars(r) id, ok := vars["id"] diff --git a/backend/go.mod b/backend/go.mod index eadca7bd1..735515c8a 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -16,4 +16,5 @@ require ( go.uber.org/atomic v1.4.0 // indirect go.uber.org/multierr v1.1.0 // indirect go.uber.org/zap v1.10.0 + golang.org/x/net v0.0.0-20190620200207-3b0461eec859 ) diff --git a/backend/server.go b/backend/server.go index abf2fc702..ae1c3eabc 100644 --- a/backend/server.go +++ b/backend/server.go @@ -95,6 +95,8 @@ func (s *Server) Route() *mux.Router { r.Methods(http.MethodGet).Path("/articles/{id}").Handler(commonChain.Then(AppHandler{articleController.Show})) r.Methods(http.MethodGet).Path("/articles/search/{tag}").Handler(commonChain.Then(AppHandler{articleController.SearchIndex})) + r.Methods(http.MethodPost).Path("/articles/paper").Handler(authChain.Then(AppHandler{articleController.CreatePaper})) + articleCommentController := controller.NewArticleComment(s.dbx) r.Methods(http.MethodPost).Path("/articles/{article_id}/comments").Handler(authChain.Then(AppHandler{articleCommentController.CreateArticleComment})) diff --git a/backend/service/article.go b/backend/service/article.go index ffb017873..2a7e93a57 100644 --- a/backend/service/article.go +++ b/backend/service/article.go @@ -96,6 +96,7 @@ func (a *Article) Create(newArticle *model.Article) (int64, error) { var createdId int64 if err := dbutil.TXHandler(a.db, func(tx *sqlx.Tx) error { result, err := repository.CreateArticle(tx, newArticle) + // fmt.Println(newArticle.Title) if err != nil { return err } diff --git a/frontend/index.html b/frontend/index.html index 9bb1b903b..301fd59a7 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -3,6 +3,7 @@ + diff --git a/frontend/search.png b/frontend/search.png new file mode 100644 index 0000000000000000000000000000000000000000..a8f0be1f0b0f9893cb07354f89c9ba50e45b7354 GIT binary patch literal 35681 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 v.json()); - } - - async getToken() { - if (this.token === "") { - this.token = await firebase.auth().currentUser.getIdToken(); - } - } -} - const successHandler = function(text) { const lists = JSON.parse(text); const items = []; @@ -45,10 +22,24 @@ const errorHandler = function(error) { return error.message; }; +const successPaperHandler = function(text) { + const lists = text; + // const items = []; + // for (let i = 0; i < lists.length; i++) { + // // console.log(lists[i]); + // items.push( + //
+ // {lists[i].id} {lists[i].title} {lists[i].body} + //
+ // ); + // } + + return lists; +}; + function request(method, url) { return fetch(url).then(function(res) { if (res.ok) { - // console.log(res.status); if (res.status == 200 && method == "PUT") { return "success!!"; } @@ -74,6 +65,7 @@ class App extends Component { this.state.message = ""; this.state.errorMessage = ""; this.state.token = ""; + this.state.text = ""; } async getToken() { @@ -125,6 +117,62 @@ class App extends Component { }); }); } + + // getPapers() { + // request("GET", "http://localhost:1991/articles/paper") + // .then(resp => { + // this.setState({ + // message: successHandler(resp) + // }); + // }) + // .catch(error => { + // this.setState({ + // errorMessage: errorHandler(error) + // }); + // }); + // request( + // "GET", + // "http://export.arxiv.org/api/query?search_query=all:" + + // "deeplearning" + + // "&start=0&max_results=100" + // ) + // .then(resp => { + // this.setState({ + // message: successPaperHandler(resp) + // }); + // }) + // .catch(error => { + // this.setState({ + // errorMessage: errorHandler(error) + // }); + // }); + // } + + async getPapers() { + await this.getToken(); + + return fetch(`http://localhost:1991/articles/paper`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${this.state.token}` + }, + body: JSON.stringify({ title: "ok", body: "google" }) + // body: JSON.stringify({ keyword: "google" }) + }) + .then(resp => { + console.log(resp); + this.setState({ + message: successPaperHandler(resp) + }); + }) + .catch(error => { + this.setState({ + errorMessage: errorHandler(error) + }); + }); + } + async deleteArticles() { await this.getToken(); @@ -150,6 +198,44 @@ class App extends Component { }); } + handleChange(e) { + this.setState({ text: e.target.value }); + } + + handleKeyDown(e) { + if (e.key === "Enter") { + console.log(this.state.text, "ok"); + e.preventDefault(); + + const textareaElement = e.target; + + const currentText = textareaElement.value; + + const start = textareaElement.selectionStart; + const end = textareaElement.selectionEnd; + + const spaceCount = 4; + const substitution = Array(spaceCount + 1).join(" "); + + const newText = + currentText.substring(0, start) + + substitution + + currentText.substring(end, currentText.length); + + this.setState( + { + text: newText + }, + () => { + textareaElement.setSelectionRange( + start + spaceCount, + start + spaceCount + ); + } + ); + } + } + render(props, state) { if (state.user === null) { return ; @@ -157,7 +243,19 @@ class App extends Component { return (
-
{state.message}
+

Arxiv Cloud

+ +
+