diff --git a/README.md b/README.md index 6be1f270a..bfe909662 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,106 @@ -# treasure-app +# Arxiv Cloud ☁📄 -> Remember, Makefile is always your friend +## 概要 + +- 日頃の論文サーベイのをより良く、更なる研究を。 +- バックエンド Go, フロントエンド React, サーバ MySQL, ユーザ認証 Firebase +- URL: none +- github: here + +
+ + +
+ +## コア体験 + +- Arxiv にある論文を検索、自動で和訳した Abstract や抽出されたキーフレーズによるタグを表示し、より高速な論文サーベイ・タグの可視化ができる
+ +- 動機 + - 論文サーベイは時間がかかるが、より高速に様々な論文を読みたい。 + - 日本語で読むほうが早い / 文章のキーワードがあるとより早く判別しやすい + - 単純なテキスト表示だけでなく、論文の動向の可視化を行いたい + +## コア機能 + +論文検索・和訳表示・タグの可視化(word cloud) + +- + +1. 論文検索 + +- [Arxiv API](https://arxiv.org/help/api) +- Get でクエリに検索ワード、検索個数を載せてリクエストを送ると html 形式で返してくる + +2. 和訳表示 + +- [Google Cloud Translation API](https://cloud.google.com/translate/pricing?hl=ja) +- いつもの。V2 だと有料なので、V3beta1 を経由すべき。 + +3. タグの可視化 + +- [react-tag-cloud](https://www.npmjs.com/package/react-tag-cloud])のパッケージ +- word cloud を react 経由で使える。preact はダメなので注意を。 + +## コア機能の設計 + +### ユーザに関して + +| Name | About | Type | +| :----------- | :----------------------------- | :----- | +| id | ユーザー ID を保持 | int | +| firebase_uid | firebase のユーザー ID を保持 | string | +| email | ユーザーのメールアドレスを保持 | string | +| display_name | ユーザー表示名を保持 | string | +| photo_url | ユーザー表示画像 URL を保持 | string | +| ctime | user を作成した時間 | date | +| utime | user を更新した時間 | date | + +user + +### 論文に関して + +#### ツイート内容 + +| Name | About | Type | +| :---- | :------------------------- | :----- | +| id | 論文の id を保持 | int | +| title | 論文の タイトル(EN) を保持 | string | +| body | 論文の Abstract(JP) を保持 | text | +| ctime | article を作成した時間 | date | +| utime | article を更新した時間 | date | + +article - has many tag + +#### タグ内容 + +| Name | About | Type | +| :--------- | :--------------------------- | :----- | +| id | タグの id を保持 | int | +| article_id | 紐づいている論文の id を保持 | int | +| tag | タグの内容(jp) を保持 | string | +| ctime | tag を作成した時間 | date | +| utime | tag を更新した時間 | date | + +tag - belong to article + +## URI 設計 + +| Methods | URI | About | +| :------ | :---------- | :-------------------------------------------------- | +| GET | /paper | ArxivAPI に対し query に keyword を入れて検索 | +| GET | /articles | 全ての論文を出す | +| GET | /tags | 全ての論文に紐づいたタグをだす(word cloud に投げる) | +| POST | /delete/:id | 特定の論文を削除、見た目状の変化なし | + +## View 設計 + +| ページ | 内容 | +| :------------------ | :------------------- | +| index.html / App.js | React 経由で全て表示 | + +## local 環境 -### local環境 ``` ❯ docker --version Docker version 18.09.2, build 6247962 @@ -18,4 +116,5 @@ v10.15.3 ``` ### Links - - https://12factor.net + +- https://12factor.net diff --git a/backend/README.md b/backend/README.md index 8fd027a04..9bbd12924 100644 --- a/backend/README.md +++ b/backend/README.md @@ -1,12 +1,20 @@ +# Slide + +https://go-talks.appspot.com/github.com/voyagegroup/talks/2019/treasure-go-day2/intro.slide#1 + # Getting Started ## 1. Database 立ち上げ `../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 +80,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 +101,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 +139,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 +178,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 diff --git a/backend/controller/article.go b/backend/controller/article.go index 524ab3941..b8d942dd0 100644 --- a/backend/controller/article.go +++ b/backend/controller/article.go @@ -1,14 +1,23 @@ package controller import ( + "context" "database/sql" "encoding/json" + "fmt" + "io/ioutil" "net/http" + "net/url" + "os" "strconv" + "strings" + "cloud.google.com/go/translate" "github.com/gorilla/mux" "github.com/jmoiron/sqlx" "github.com/pkg/errors" + "golang.org/x/net/html" + "golang.org/x/text/language" "github.com/voyagegroup/treasure-app/httputil" "github.com/voyagegroup/treasure-app/model" @@ -16,19 +25,311 @@ import ( "github.com/voyagegroup/treasure-app/service" ) +type Paper struct { + Title string + Abstract string +} + +type Result struct { + Keyphrase string `xml:"Keyphrase"` + Score string `xml:"Score"` +} + type Article struct { dbx *sqlx.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} +} + func (a *Article) Index(w http.ResponseWriter, r *http.Request) (int, interface{}, error) { articles, err := repository.AllArticle(a.dbx) if err != nil { return http.StatusInternalServerError, nil, err } + + tags, err := repository.AllTag(a.dbx) + + if err != nil { + return http.StatusInternalServerError, nil, err + } + + var temp int64 = -1 + var count int = 0 + var concat string + + fmt.Println(len(tags)) + + for i := 0; i < len(tags)-3; i++ { + if tags[i].ArticleID != temp { + concat = tags[i].Tag + "," + tags[i+1].Tag + "," + tags[i+2].Tag + temp = tags[i].ArticleID + count++ + fmt.Println(tags[i].ArticleID, concat) + // articles.Tag = &concat + } + + } + + return http.StatusOK, articles, nil +} + +func (a *Article) TagIndex(w http.ResponseWriter, r *http.Request) (int, interface{}, error) { + tags, err := repository.AllTag(a.dbx) + + if err != nil { + return http.StatusInternalServerError, nil, err + } + + m := map[string]int{} + for i := 0; i < len(tags); i++ { + m[tags[i].Tag] = m[tags[i].Tag] + 1 + } + + // for key, count := range m { + // fmt.Print(key, count) + // fmt.Print(" ") + // } + // return http.StatusOK, tags, nil + return http.StatusOK, m, nil +} + +func searchArxiv(keyword string, limit int) (map[int][]string, error) { + + resp, err := http.Get("http://export.arxiv.org/api/query?search_query=all:" + keyword + "&start=0&max_results=" + strconv.Itoa(limit)) + + if err != nil { + return nil, err + } + defer resp.Body.Close() + + doc, err := html.Parse(resp.Body) + if err != nil { + return nil, err + } + + var count int = 0 + dictionary := make(map[int][]string) + + var paper Paper + var f func(*html.Node) + + f = func(n *html.Node) { + + if n.Type == html.ElementNode && n.Data == "title" { + paper.Title = n.FirstChild.Data + } + if n.Type == html.ElementNode && n.Data == "summary" { + paper.Abstract = n.FirstChild.Data + } + if n.Type == html.ElementNode && n.Data == "entry" { + dictionary[count] = []string{paper.Title, paper.Abstract} + count++ + } + for c := n.FirstChild; c != nil; c = c.NextSibling { + f(c) + } + } + f(doc) + return dictionary, nil +} + +func translateText(targetLanguage, text string) (string, error) { + ctx := context.Background() + + lang, err := language.Parse(targetLanguage) + if err != nil { + return "", err + } + + client, err := translate.NewClient(ctx) + if err != nil { + return "", err + } + defer client.Close() + + resp, err := client.Translate(ctx, []string{text}, lang, nil) + if err != nil { + return "", err + } + return resp[0].Text, nil +} + +func extractKeyword(text string) ([]string, error) { + + baseUrl, err := url.Parse("https://jlp.yahooapis.jp/KeyphraseService/V1/extract?") + if err != nil { + fmt.Println(err) + return nil, err + } + + params := url.Values{} + params.Add("appid", os.Getenv("YAHOO_KEYWORD_API")) + // params.Add("sentence", url.QueryEscape(text)) + params.Add("sentence", text) + params.Add("output", "json") + + baseUrl.RawQuery = params.Encode() + fmt.Println(baseUrl.String()) + resp, err := http.Get(baseUrl.String()) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + doc, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + // やっっっっっばい + docStrs := string(doc)[1 : len(string(doc))-1] + docTrim := strings.Replace(docStrs, "\"", "`", -1) + docUni8, _ := strconv.Unquote("\"" + docTrim + "\"") + docReplace := strings.Replace(docUni8, "`", "", -1) + docArray := strings.Split(docReplace, ",") + + return docArray, nil +} + +func (a *Article) TagCreate(w http.ResponseWriter, r *http.Request) (int, interface{}, error) { + + articles, err := repository.AllArticle(a.dbx) + if err != nil { + return http.StatusInternalServerError, nil, err + } + + keywords, err := extractKeyword(articles[2].Body) + if err != nil { + return http.StatusInternalServerError, nil, err + } + + for j := 1; j < len(keywords); j++ { + splitKeys := strings.Split(keywords[j], ":") + newArticleTag := &model.ArticleTag{Tag: splitKeys[0]} //, Body: splitKeys[1]} + + articleTagService := service.NewArticleTagService(a.dbx) + id, err := articleTagService.CreateArticleTag(newArticleTag) + + if err != nil { + return http.StatusInternalServerError, nil, err + } + newArticleTag.ID = id + } + + return http.StatusOK, articles, nil +} + +func (a *Article) CreatePaper(w http.ResponseWriter, r *http.Request) (int, interface{}, error) { + + vars := r.URL.Query() + keyword := vars["keyword"][0] + + dictionary, err := searchArxiv(keyword, 3) + + if err != nil { + return http.StatusBadRequest, nil, err + } + + for i := 0; i < len(dictionary); i++ { + translated, err := translateText("ja", dictionary[i][1]) + if err != nil { + return http.StatusBadRequest, nil, err + } + dictionary[i] = []string{dictionary[i][0], dictionary[i][1], translated} + } + + if err != nil { + return http.StatusBadRequest, nil, err + } + + // ここでdictionaryに欠損ないか = DBに正しく入るかの処理をしたい + for j := 1; j < len(dictionary); j++ { + newArticle := &model.Article{Title: dictionary[j][0], Body: dictionary[j][2]} + fmt.Println(j) + fmt.Println(dictionary[j][0]) + + // 認証無しはやばいので。。。 現状Getのクエリで対処している部分をPostでUserID付与の状態に + + // if err := json.NewDecoder(r.Body).Decode(&newArticle); err != nil { + // return http.StatusBadRequest, nil, err + // } + // 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 + } + newArticle.ID = id + + keywords, err := extractKeyword(dictionary[j][2]) + if err != nil { + return http.StatusInternalServerError, nil, err + } + + for k := 1; k < len(keywords); k++ { + splitKeys := strings.Split(keywords[k], ":") + dictionary[j] = append(dictionary[j], splitKeys[0]) + // fmt.Println(splitKeys[0]) + // fmt.Println(dictionary[j]) + + newArticleTag := &model.ArticleTag{ArticleID: id, Tag: splitKeys[0]} //, Body: splitKeys[1]} + + articleTagService := service.NewArticleTagService(a.dbx) + id, err := articleTagService.CreateArticleTag(newArticleTag) + + if err != nil { + return http.StatusInternalServerError, nil, err + } + newArticleTag.ID = id + } + fmt.Println("ok") + + } + + fmt.Println("ok2") + + return http.StatusOK, dictionary, 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.FindArticleByTag(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 } @@ -38,13 +339,13 @@ func (a *Article) Show(w http.ResponseWriter, r *http.Request) (int, interface{} if !ok { return http.StatusBadRequest, nil, &httputil.HTTPError{Message: "invalid path parameter"} } - + fmt.Println("show") aid, err := strconv.ParseInt(id, 10, 64) if err != nil { return http.StatusBadRequest, nil, err } - article, err := repository.FindArticle(a.dbx, aid) + article, err := repository.FindArticleByID(a.dbx, aid) if err != nil && err == sql.ErrNoRows { return http.StatusNotFound, nil, err } else if err != nil { @@ -56,11 +357,20 @@ 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{} + fmt.Println("tes") + if err := json.NewDecoder(r.Body).Decode(&newArticle); err != nil { return http.StatusBadRequest, nil, err } + 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 @@ -76,6 +386,7 @@ func (a *Article) Update(w http.ResponseWriter, r *http.Request) (int, interface if !ok { return http.StatusBadRequest, nil, &httputil.HTTPError{Message: "invalid path parameter"} } + fmt.Println("update") aid, err := strconv.ParseInt(id, 10, 64) if err != nil { @@ -104,6 +415,7 @@ func (a *Article) Destroy(w http.ResponseWriter, r *http.Request) (int, interfac if !ok { return http.StatusBadRequest, nil, &httputil.HTTPError{Message: "invalid path parameter"} } + fmt.Println("destroy") aid, err := strconv.ParseInt(id, 10, 64) if err != nil { @@ -120,3 +432,54 @@ 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 { + 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/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/go.mod b/backend/go.mod index eadca7bd1..e6c009752 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -3,8 +3,9 @@ module github.com/voyagegroup/treasure-app go 1.12 require ( - cloud.google.com/go v0.43.0 // indirect + cloud.google.com/go v0.43.0 firebase.google.com/go v3.8.1+incompatible + github.com/gin-gonic/gin v1.4.0 github.com/go-sql-driver/mysql v1.4.0 github.com/gorilla/handlers v1.4.2 github.com/gorilla/mux v1.7.3 @@ -13,7 +14,11 @@ require ( github.com/justinas/alice v0.0.0-20171023064455-03f45bd4b7da github.com/pkg/errors v0.8.1 github.com/rs/cors v1.6.0 + github.com/wickett/word-cloud-generator v0.0.0-20180821165703-e543811ca0c1 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 + golang.org/x/text v0.3.2 + google.golang.org/api v0.8.0 ) diff --git a/backend/go.sum b/backend/go.sum index 849b55e7a..dbb2a7d2c 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -8,6 +8,11 @@ firebase.google.com/go v3.8.1+incompatible/go.mod h1:xlah6XbEyW6tbfSklcfe5FHJIwj github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3 h1:t8FVkw33L+wilf2QiWkw0UV77qRpcH/JHPKGpKa2E8g= +github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s= +github.com/gin-gonic/gin v1.4.0 h1:3tMoCCfM7ppqsR0ptz/wi1impNpT7/9wQtMZ8lr1mCQ= +github.com/gin-gonic/gin v1.4.0/go.mod h1:OW2EZn3DO8Ln9oIKOvM++LBO+5UPHJJDH72/q/3rZdM= github.com/go-sql-driver/mysql v1.4.0 h1:7LxgVwFb2hIQtMm87NdgAVfXjnt4OePseqT1tKx+opk= github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= @@ -42,17 +47,29 @@ github.com/jmoiron/sqlx v1.2.0 h1:41Ip0zITnmWNR/vHV+S4m+VoUivnWY5E4OJfLZjCJMA= github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks= github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/justinas/alice v0.0.0-20171023064455-03f45bd4b7da h1:5y58+OCjoHCYB8182mpf/dEsq0vwTKPOo4zGfH0xW9A= github.com/justinas/alice v0.0.0-20171023064455-03f45bd4b7da/go.mod h1:oLH0CmIaxCGXD67VKGR5AacGXZSMznlmeqM8RzPrcY8= github.com/lib/pq v1.0.0 h1:X5PMW56eZitiTeO7tKzZxFCSpbFZJtkMMooicw2us9A= github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/mattn/go-isatty v0.0.7 h1:UvyT9uN+3r7yLEYSlJsbQGdsaB/a0DlgWP3pql6iwOc= +github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-sqlite3 v1.9.0 h1:pDRiWfl+++eC2FEFRy6jXmQlvp4Yh3z1MJKg4UeYM/4= github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rs/cors v1.6.0 h1:G9tHG9lebljV9mfp9SNPDL36nCDxmo3zTlAf1YgvzmI= github.com/rs/cors v1.6.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/ugorji/go v1.1.4 h1:j4s+tAvLfL3bZyefP2SEWmhBzmuIlH/eqNuPdFPgngw= +github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= +github.com/wickett/word-cloud-generator v0.0.0-20180821165703-e543811ca0c1 h1:Qc8dPlxFtHa0tx/2pXpcOQlHUbepc74YiLuCk6C5lJo= +github.com/wickett/word-cloud-generator v0.0.0-20180821165703-e543811ca0c1/go.mod h1:XtyfoZ9N8a1S6BGUaRylYO4sq+ICtfKeYj5pZFRIvHA= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0 h1:C9hSCOW830chIVkdja34wa6Ky+IzWllkUinR+BtRZd4= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= @@ -95,6 +112,7 @@ golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -121,6 +139,8 @@ golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgw google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0 h1:9sdfJOzWlkqPltHAuzT2Cp+yrBeY1KRVYgms8soxMwM= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0 h1:VGGbLNyPF7dvYHhcUGYBBGCRDDK0RRJAI6KCvo0CL+E= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -137,6 +157,12 @@ google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZi google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1 h1:j6XxA85m/6txkUCHvzlV5f+HBNl/1r5cZ2A/3IEFOO8= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= +gopkg.in/go-playground/validator.v8 v8.18.2 h1:lFB4DoMU6B626w8ny76MV7VX6W2VHct2GVOI3xgiMrQ= +gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y= +gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/backend/integration.mk b/backend/integration.mk index 628adc375..b77d58280 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,13 @@ 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)"}' + + req-public: curl -v $(HOST):$(PORT)/public 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/model/article.go b/backend/model/article.go index 864a9659f..ce0a9677e 100644 --- a/backend/model/article.go +++ b/backend/model/article.go @@ -1,7 +1,22 @@ package model +type ArticleTag struct { + ID int64 `db:"id" json:"id"` + ArticleID int64 `db:"article_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:"article_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:"article_id" json:"article_id"` } diff --git a/backend/repository/article.go b/backend/repository/article.go index 6b556db1d..e393baf32 100644 --- a/backend/repository/article.go +++ b/backend/repository/article.go @@ -2,44 +2,70 @@ package repository import ( "database/sql" + "fmt" "github.com/jmoiron/sqlx" "github.com/voyagegroup/treasure-app/model" ) func AllArticle(db *sqlx.DB) ([]model.Article, error) { + fmt.Println("ok") 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 FindArticle(db *sqlx.DB, id int64) (*model.Article, error) { +func AllTag(db *sqlx.DB) ([]model.ArticleTag, error) { + a := make([]model.ArticleTag, 0) + + if err := db.Select(&a, `SELECT id, article_id, tag FROM article_tag`); err != nil { + return nil, err + } + + return a, nil +} + + + +func FindArticleByTag(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 +} + +func FindArticleByID(db *sqlx.DB, id int64) (*model.Article, error) { a := model.Article{} - if err := db.Get(&a, ` -SELECT id, title, body FROM article WHERE id = ? -`, id); err != nil { + if err := db.Get(&a, `SELECT id, title, body FROM article WHERE id = ?`, id); err != nil { return nil, err } return &a, nil } func CreateArticle(db *sqlx.Tx, a *model.Article) (sql.Result, error) { - stmt, err := db.Prepare(` -INSERT INTO article (title, body) VALUES (?, ?) -`) + stmt, err := db.Prepare(`INSERT INTO article (user_id, title, body) 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) } -func UpdateArticle(db *sqlx.Tx, id int64, a *model.Article) (sql.Result, error) { - stmt, err := db.Prepare(` -UPDATE article SET title = ?, body = ? WHERE id = ? -`) +func UpdateArticleByID(db *sqlx.Tx, id int64, a *model.Article) (sql.Result, error) { + stmt, err := db.Prepare(`UPDATE article SET title = ?, body = ? WHERE id = ?`) if err != nil { return nil, err } @@ -47,13 +73,54 @@ UPDATE article SET title = ?, body = ? WHERE id = ? return stmt.Exec(a.Title, a.Body, id) } -func DestroyArticle(db *sqlx.Tx, id int64) (sql.Result, error) { - stmt, err := db.Prepare(` -DELETE FROM article WHERE id = ? -`) +func DeleteArticleByID(db *sqlx.Tx, id int64) (sql.Result, error) { + stmt, err := db.Prepare(`DELETE FROM article WHERE id = ?`) if err != nil { return nil, err } defer stmt.Close() return stmt.Exec(id) } + +func DeleteAllArticle(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 (?, ?, ?)`) + 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/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) diff --git a/backend/server.go b/backend/server.go index 911ba9cde..9b0e2d853 100644 --- a/backend/server.go +++ b/backend/server.go @@ -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.NewAuthMiddleware(s.authClient, s.dbx) + 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( @@ -81,11 +88,28 @@ func (s *Server) Route() *mux.Router { 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.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})) + + // r.Methods(http.MethodGet).Path("/tag").Handler(commonChain.Then(AppHandler{articleController.TagCreate})) + r.Methods(http.MethodGet).Path("/tags").Handler(commonChain.Then(AppHandler{articleController.TagIndex})) + + r.Methods(http.MethodGet).Path("/paper").Handler(commonChain.Then(AppHandler{articleController.CreatePaper})) + + 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 2a3c58391..196d57dd7 100644 --- a/backend/service/article.go +++ b/backend/service/article.go @@ -17,14 +17,30 @@ 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) + _, err := repository.FindArticleByID(a.db, id) if err != nil { return errors.Wrap(err, "failed find article") } if err := dbutil.TXHandler(a.db, func(tx *sqlx.Tx) error { - _, err := repository.UpdateArticle(tx, id, newArticle) + _, err := repository.UpdateArticleByID(tx, id, newArticle) if err != nil { return err } @@ -39,13 +55,30 @@ func (a *Article) Update(id int64, newArticle *model.Article) error { } func (a *Article) Destroy(id int64) error { - _, err := repository.FindArticle(a.db, id) + _, err := repository.FindArticleByID(a.db, id) if err != nil { return errors.Wrap(err, "failed find article") } if err := dbutil.TXHandler(a.db, func(tx *sqlx.Tx) error { - _, err := repository.DestroyArticle(tx, id) + _, err := repository.DeleteArticleByID(tx, id) + 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) DestroyAll() error { + + if err := dbutil.TXHandler(a.db, func(tx *sqlx.Tx) error { + _, err := repository.DeleteAllArticle(tx) if err != nil { return err } @@ -63,6 +96,51 @@ 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 + } + 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 *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 } diff --git a/database/Makefile b/database/Makefile index 503de7c0a..7df9372d1 100644 --- a/database/Makefile +++ b/database/Makefile @@ -1,6 +1,6 @@ export GO111MODULE := off -DBNAME:=treasure_app +DBNAME:=arxiv_cloud DATASOURCE:=root:password@tcp(127.0.0.1:3306)/$(DBNAME) install: diff --git a/database/migrations/1_init.sql b/database/migrations/201908141445_user.sql similarity index 100% rename from database/migrations/1_init.sql rename to database/migrations/201908141445_user.sql diff --git a/database/migrations/2_article.sql b/database/migrations/201908141450_article.sql similarity index 72% rename from database/migrations/2_article.sql rename to database/migrations/201908141450_article.sql index 2e4631c76..a6a07e550 100644 --- a/database/migrations/2_article.sql +++ b/database/migrations/201908141450_article.sql @@ -1,11 +1,13 @@ -- +goose Up CREATE TABLE article ( id int(10) UNSIGNED NOT NULL AUTO_INCREMENT, + user_id int(10) UNSIGNED DEFAULT NULL, title VARCHAR(255) NOT NULL DEFAULT '', body TEXT, ctime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, utime TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - PRIMARY KEY (id) + PRIMARY KEY (id), + CONSTRAINT article_fk_user FOREIGN KEY (user_id) REFERENCES user(id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- +goose Down diff --git a/database/migrations/201908141500_article_comment.sql b/database/migrations/201908141500_article_comment.sql new file mode 100644 index 000000000..5fb0b2ba1 --- /dev/null +++ b/database/migrations/201908141500_article_comment.sql @@ -0,0 +1,15 @@ +-- +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; + +-- +goose Down +DROP TABLE article_comment; diff --git a/database/migrations/201908141515_article_tag.sql b/database/migrations/201908141515_article_tag.sql new file mode 100644 index 000000000..81c91b754 --- /dev/null +++ b/database/migrations/201908141515_article_tag.sql @@ -0,0 +1,12 @@ +-- +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; + +-- +goose Down +DROP TABLE article_tag; \ No newline at end of file diff --git a/frontend/.env.example b/frontend/.env.example deleted file mode 100644 index 9d88696d6..000000000 --- a/frontend/.env.example +++ /dev/null @@ -1,8 +0,0 @@ -BACKEND_API_BASE=http://localhost:1991 - -FIREBASE_APIKEY= -FIREBASE_AUTHDOMAIN= -FIREBASE_DATABASEURL= -FIREBASE_PROJECTID= -FIREBASE_MESSAGINGSENDERID= -FIREBASE_APPID= diff --git a/frontend/Makefile b/frontend/Makefile index 347238b90..0fa8f0c49 100644 --- a/frontend/Makefile +++ b/frontend/Makefile @@ -13,3 +13,6 @@ setup: __clean install __clean: rm -rf .cache rm -rf node_modules + +run: + npm start \ No newline at end of file diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 000000000..9d9614c4f --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,68 @@ +This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). + +## Available Scripts + +In the project directory, you can run: + +### `npm start` + +Runs the app in the development mode.
+Open [http://localhost:3000](http://localhost:3000) to view it in the browser. + +The page will reload if you make edits.
+You will also see any lint errors in the console. + +### `npm test` + +Launches the test runner in the interactive watch mode.
+See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. + +### `npm run build` + +Builds the app for production to the `build` folder.
+It correctly bundles React in production mode and optimizes the build for the best performance. + +The build is minified and the filenames include the hashes.
+Your app is ready to be deployed! + +See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. + +### `npm run eject` + +**Note: this is a one-way operation. Once you `eject`, you can’t go back!** + +If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. + +Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. + +You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. + +## Learn More + +You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). + +To learn React, check out the [React documentation](https://reactjs.org/). + +### Code Splitting + +This section has moved here: https://facebook.github.io/create-react-app/docs/code-splitting + +### Analyzing the Bundle Size + +This section has moved here: https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size + +### Making a Progressive Web App + +This section has moved here: https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app + +### Advanced Configuration + +This section has moved here: https://facebook.github.io/create-react-app/docs/advanced-configuration + +### Deployment + +This section has moved here: https://facebook.github.io/create-react-app/docs/deployment + +### `npm run build` fails to minify + +This section has moved here: https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify diff --git a/frontend/build/asset-manifest.json b/frontend/build/asset-manifest.json new file mode 100644 index 000000000..ba6948f16 --- /dev/null +++ b/frontend/build/asset-manifest.json @@ -0,0 +1,15 @@ +{ + "files": { + "main.css": "/static/css/main.34de6062.chunk.css", + "main.js": "/static/js/main.b752a1aa.chunk.js", + "main.js.map": "/static/js/main.b752a1aa.chunk.js.map", + "runtime~main.js": "/static/js/runtime~main.821e3928.js", + "runtime~main.js.map": "/static/js/runtime~main.821e3928.js.map", + "static/js/2.ef6f1649.chunk.js": "/static/js/2.ef6f1649.chunk.js", + "static/js/2.ef6f1649.chunk.js.map": "/static/js/2.ef6f1649.chunk.js.map", + "index.html": "/index.html", + "precache-manifest.0d3b8b2eda5914b9b0c9ccb9e2aee7a0.js": "/precache-manifest.0d3b8b2eda5914b9b0c9ccb9e2aee7a0.js", + "service-worker.js": "/service-worker.js", + "static/css/main.34de6062.chunk.css.map": "/static/css/main.34de6062.chunk.css.map" + } +} \ No newline at end of file diff --git a/frontend/build/favicon.ico b/frontend/build/favicon.ico new file mode 100644 index 000000000..a11777cc4 Binary files /dev/null and b/frontend/build/favicon.ico differ diff --git a/frontend/build/index.html b/frontend/build/index.html new file mode 100644 index 000000000..49fc0ff99 --- /dev/null +++ b/frontend/build/index.html @@ -0,0 +1 @@ +React App
\ No newline at end of file diff --git a/frontend/build/logo192.png b/frontend/build/logo192.png new file mode 100644 index 000000000..fc44b0a37 Binary files /dev/null and b/frontend/build/logo192.png differ diff --git a/frontend/build/logo512.png b/frontend/build/logo512.png new file mode 100644 index 000000000..a4e47a654 Binary files /dev/null and b/frontend/build/logo512.png differ diff --git a/frontend/build/manifest.json b/frontend/build/manifest.json new file mode 100644 index 000000000..e4c94e632 --- /dev/null +++ b/frontend/build/manifest.json @@ -0,0 +1,25 @@ +{ + "short_name": "React App", + "name": "Create React App Sample", + "icons": [ + { + "src": "favicon.ico", + "sizes": "64x64 32x32 24x24 16x16", + "type": "image/x-icon" + }, + { + "src": "logo192.png", + "type": "image/png", + "sizes": "192x192" + }, + { + "src": "logo512.png", + "type": "image/png", + "sizes": "512x512" + } + ], + "start_url": ".", + "display": "standalone", + "theme_color": "#000000", + "background_color": "#ffffff" +} diff --git a/frontend/build/precache-manifest.0d3b8b2eda5914b9b0c9ccb9e2aee7a0.js b/frontend/build/precache-manifest.0d3b8b2eda5914b9b0c9ccb9e2aee7a0.js new file mode 100644 index 000000000..6d9422c74 --- /dev/null +++ b/frontend/build/precache-manifest.0d3b8b2eda5914b9b0c9ccb9e2aee7a0.js @@ -0,0 +1,22 @@ +self.__precacheManifest = (self.__precacheManifest || []).concat([ + { + "revision": "8f359a8869b70176e17ba86f8660e1b2", + "url": "/index.html" + }, + { + "revision": "c6cb1671940993100e7f", + "url": "/static/css/main.34de6062.chunk.css" + }, + { + "revision": "116948fd58f55f3e7061", + "url": "/static/js/2.ef6f1649.chunk.js" + }, + { + "revision": "c6cb1671940993100e7f", + "url": "/static/js/main.b752a1aa.chunk.js" + }, + { + "revision": "219707186523ad4b8cde", + "url": "/static/js/runtime~main.821e3928.js" + } +]); \ No newline at end of file diff --git a/frontend/build/robots.txt b/frontend/build/robots.txt new file mode 100644 index 000000000..01b0f9a10 --- /dev/null +++ b/frontend/build/robots.txt @@ -0,0 +1,2 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * diff --git a/frontend/build/service-worker.js b/frontend/build/service-worker.js new file mode 100644 index 000000000..f7ca91127 --- /dev/null +++ b/frontend/build/service-worker.js @@ -0,0 +1,39 @@ +/** + * Welcome to your Workbox-powered service worker! + * + * You'll need to register this file in your web app and you should + * disable HTTP caching for this file too. + * See https://goo.gl/nhQhGp + * + * The rest of the code is auto-generated. Please don't update this file + * directly; instead, make changes to your Workbox build configuration + * and re-run your build process. + * See https://goo.gl/2aRDsh + */ + +importScripts("https://storage.googleapis.com/workbox-cdn/releases/4.3.1/workbox-sw.js"); + +importScripts( + "/precache-manifest.0d3b8b2eda5914b9b0c9ccb9e2aee7a0.js" +); + +self.addEventListener('message', (event) => { + if (event.data && event.data.type === 'SKIP_WAITING') { + self.skipWaiting(); + } +}); + +workbox.core.clientsClaim(); + +/** + * The workboxSW.precacheAndRoute() method efficiently caches and responds to + * requests for URLs in the manifest. + * See https://goo.gl/S9QRab + */ +self.__precacheManifest = [].concat(self.__precacheManifest || []); +workbox.precaching.precacheAndRoute(self.__precacheManifest, {}); + +workbox.routing.registerNavigationRoute(workbox.precaching.getCacheKeyForURL("/index.html"), { + + blacklist: [/^\/_/,/\/[^\/?]+\.[^\/]+$/], +}); diff --git a/frontend/build/static/css/main.34de6062.chunk.css b/frontend/build/static/css/main.34de6062.chunk.css new file mode 100644 index 000000000..4ada487c8 --- /dev/null +++ b/frontend/build/static/css/main.34de6062.chunk.css @@ -0,0 +1,2 @@ +body{margin:0;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Fira Sans,Droid Sans,Helvetica Neue,sans-serif;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}code{font-family:source-code-pro,Menlo,Monaco,Consolas,Courier New,monospace} +/*# sourceMappingURL=main.34de6062.chunk.css.map */ \ No newline at end of file diff --git a/frontend/build/static/css/main.34de6062.chunk.css.map b/frontend/build/static/css/main.34de6062.chunk.css.map new file mode 100644 index 000000000..d7351b075 --- /dev/null +++ b/frontend/build/static/css/main.34de6062.chunk.css.map @@ -0,0 +1 @@ +{"version":3,"sources":["index.css"],"names":[],"mappings":"AAAA,KACE,QAAS,CACT,mIAEY,CACZ,kCAAmC,CACnC,iCACF,CAEA,KACE,uEAEF","file":"main.34de6062.chunk.css","sourcesContent":["body {\n margin: 0;\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", \"Roboto\", \"Oxygen\",\n \"Ubuntu\", \"Cantarell\", \"Fira Sans\", \"Droid Sans\", \"Helvetica Neue\",\n sans-serif;\n -webkit-font-smoothing: antialiased;\n -moz-osx-font-smoothing: grayscale;\n}\n\ncode {\n font-family: source-code-pro, Menlo, Monaco, Consolas, \"Courier New\",\n monospace;\n}\n"]} \ No newline at end of file diff --git a/frontend/build/static/js/2.ef6f1649.chunk.js b/frontend/build/static/js/2.ef6f1649.chunk.js new file mode 100644 index 000000000..c9be5401a --- /dev/null +++ b/frontend/build/static/js/2.ef6f1649.chunk.js @@ -0,0 +1,2 @@ +(window.webpackJsonpfrontend=window.webpackJsonpfrontend||[]).push([[2],[function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var r,i=n(6),o=n(21),a=n(22),u=((r={})["no-app"]="No Firebase App '{$appName}' has been created - call Firebase App.initializeApp()",r["bad-app-name"]="Illegal App name: '{$appName}",r["duplicate-app"]="Firebase App named '{$appName}' already exists",r["app-deleted"]="Firebase App named '{$appName}' already deleted",r["duplicate-service"]="Firebase service named '{$appName}' already registered",r["invalid-app-argument"]="firebase.{$appName}() takes either no argument or a Firebase App instance.",r),l=new o.ErrorFactory("app","Firebase",u),s="[DEFAULT]",c=[],f=function(){function e(e,t,n){this.firebase_=n,this.isDeleted_=!1,this.services_={},this.name_=t.name,this.automaticDataCollectionEnabled_=t.automaticDataCollectionEnabled||!1,this.options_=o.deepCopy(e),this.INTERNAL={getUid:function(){return null},getToken:function(){return Promise.resolve(null)},addAuthTokenListener:function(e){c.push(e),setTimeout(function(){return e(null)},0)},removeAuthTokenListener:function(e){c=c.filter(function(t){return t!==e})}}}return Object.defineProperty(e.prototype,"automaticDataCollectionEnabled",{get:function(){return this.checkDestroyed_(),this.automaticDataCollectionEnabled_},set:function(e){this.checkDestroyed_(),this.automaticDataCollectionEnabled_=e},enumerable:!0,configurable:!0}),Object.defineProperty(e.prototype,"name",{get:function(){return this.checkDestroyed_(),this.name_},enumerable:!0,configurable:!0}),Object.defineProperty(e.prototype,"options",{get:function(){return this.checkDestroyed_(),this.options_},enumerable:!0,configurable:!0}),e.prototype.delete=function(){var e=this;return new Promise(function(t){e.checkDestroyed_(),t()}).then(function(){e.firebase_.INTERNAL.removeApp(e.name_);for(var t=[],n=0,r=Object.keys(e.services_);n=0&&d.warn("\n Warning: You are trying to load Firebase while using Firebase Performance standalone script.\n You should load Firebase Performance with this instance of Firebase to avoid loading duplicate code.\n ")}var v=function e(){var t=function(e){var t={},n={},r={},i={__esModule:!0,initializeApp:function(n,r){void 0===r&&(r={}),"object"===typeof r&&null!==r||(r={name:r});var a=r;void 0===a.name&&(a.name=s);var u=a.name;if("string"!==typeof u||!u)throw l.create("bad-app-name",{appName:String(u)});if(o.contains(t,u))throw l.create("duplicate-app",{appName:u});var f=new e(n,a,i);return t[u]=f,c(f,"create"),f},app:a,apps:null,SDK_VERSION:h,INTERNAL:{registerService:function(t,s,c,f,h){if(void 0===h&&(h=!1),n[t])throw l.create("duplicate-service",{appName:t});function d(e){if(void 0===e&&(e=a()),"function"!==typeof e[t])throw l.create("invalid-app-argument",{appName:t});return e[t]()}return n[t]=s,f&&(r[t]=f,u().forEach(function(e){f("create",e)})),void 0!==c&&o.deepExtend(d,c),i[t]=d,e.prototype[t]=function(){for(var e=[],n=0;n=0;u--)(i=e[u])&&(a=(o<3?i(a):o>3?i(t,n,a):i(t,n))||a);return o>3&&a&&Object.defineProperty(t,n,a),a}function l(e,t){return function(n,r){t(n,r,e)}}function s(e,t){if("object"===typeof Reflect&&"function"===typeof Reflect.metadata)return Reflect.metadata(e,t)}function c(e,t,n,r){return new(n||(n=Promise))(function(i,o){function a(e){try{l(r.next(e))}catch(t){o(t)}}function u(e){try{l(r.throw(e))}catch(t){o(t)}}function l(e){e.done?i(e.value):new n(function(t){t(e.value)}).then(a,u)}l((r=r.apply(e,t||[])).next())})}function f(e,t){var n,r,i,o,a={label:0,sent:function(){if(1&i[0])throw i[1];return i[1]},trys:[],ops:[]};return o={next:u(0),throw:u(1),return:u(2)},"function"===typeof Symbol&&(o[Symbol.iterator]=function(){return this}),o;function u(o){return function(u){return function(o){if(n)throw new TypeError("Generator is already executing.");for(;a;)try{if(n=1,r&&(i=2&o[0]?r.return:o[0]?r.throw||((i=r.return)&&i.call(r),0):r.next)&&!(i=i.call(r,o[1])).done)return i;switch(r=0,i&&(o=[2&o[0],i.value]),o[0]){case 0:case 1:i=o;break;case 4:return a.label++,{value:o[1],done:!1};case 5:a.label++,r=o[1],o=[0];continue;case 7:o=a.ops.pop(),a.trys.pop();continue;default:if(!(i=(i=a.trys).length>0&&i[i.length-1])&&(6===o[0]||2===o[0])){a=0;continue}if(3===o[0]&&(!i||o[1]>i[0]&&o[1]=e.length&&(e=void 0),{value:e&&e[n++],done:!e}}}}function p(e,t){var n="function"===typeof Symbol&&e[Symbol.iterator];if(!n)return e;var r,i,o=n.call(e),a=[];try{for(;(void 0===t||t-- >0)&&!(r=o.next()).done;)a.push(r.value)}catch(u){i={error:u}}finally{try{r&&!r.done&&(n=o.return)&&n.call(o)}finally{if(i)throw i.error}}return a}function v(){for(var e=[],t=0;t1||u(e,t)})})}function u(e,t){try{(n=i[e](t)).value instanceof y?Promise.resolve(n.value.v).then(l,s):c(o[0][2],n)}catch(r){c(o[0][3],r)}var n}function l(e){u("next",e)}function s(e){u("throw",e)}function c(e,t){e(t),o.shift(),o.length&&u(o[0][0],o[0][1])}}function b(e){var t,n;return t={},r("next"),r("throw",function(e){throw e}),r("return"),t[Symbol.iterator]=function(){return this},t;function r(r,i){t[r]=e[r]?function(t){return(n=!n)?{value:y(e[r](t)),done:"return"===r}:i?i(t):t}:i}}function w(e){if(!Symbol.asyncIterator)throw new TypeError("Symbol.asyncIterator is not defined.");var t,n=e[Symbol.asyncIterator];return n?n.call(e):(e=d(e),t={},r("next"),r("throw"),r("return"),t[Symbol.asyncIterator]=function(){return this},t);function r(n){t[n]=e[n]&&function(t){return new Promise(function(r,i){(function(e,t,n,r){Promise.resolve(r).then(function(t){e({value:t,done:n})},t)})(r,i,(t=e[n](t)).done,t.value)})}}}function E(e,t){return Object.defineProperty?Object.defineProperty(e,"raw",{value:t}):e.raw=t,e}function k(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var n in e)Object.hasOwnProperty.call(e,n)&&(t[n]=e[n]);return t.default=e,t}function T(e){return e&&e.__esModule?e:{default:e}}},function(e,t){var n;n=function(){return this}();try{n=n||new Function("return this")()}catch(r){"object"===typeof window&&(n=window)}e.exports=n},function(e,t,n){"use strict";!function e(){if("undefined"!==typeof __REACT_DEVTOOLS_GLOBAL_HOOK__&&"function"===typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE)try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(e)}catch(t){console.error(t)}}(),e.exports=n(16)},function(e,t,n){"use strict";function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}n.d(t,"a",function(){return r})},function(e,t,n){"use strict";function r(e,t){for(var n=0;nR.length&&R.push(e)}function M(e,t,n){return null==e?0:function e(t,n,r,i){var u=typeof t;"undefined"!==u&&"boolean"!==u||(t=null);var l=!1;if(null===t)l=!0;else switch(u){case"string":case"number":l=!0;break;case"object":switch(t.$$typeof){case o:case a:l=!0}}if(l)return r(i,t,""===n?"."+j(t,0):n),1;if(l=0,n=""===n?".":n+":",Array.isArray(t))for(var s=0;sthis.eventPool.length&&this.eventPool.push(e)}function fe(e){e.eventPool=[],e.getPooled=se,e.release=ce}i(le.prototype,{preventDefault:function(){this.defaultPrevented=!0;var e=this.nativeEvent;e&&(e.preventDefault?e.preventDefault():"unknown"!==typeof e.returnValue&&(e.returnValue=!1),this.isDefaultPrevented=ae)},stopPropagation:function(){var e=this.nativeEvent;e&&(e.stopPropagation?e.stopPropagation():"unknown"!==typeof e.cancelBubble&&(e.cancelBubble=!0),this.isPropagationStopped=ae)},persist:function(){this.isPersistent=ae},isPersistent:ue,destructor:function(){var e,t=this.constructor.Interface;for(e in t)this[e]=null;this.nativeEvent=this._targetInst=this.dispatchConfig=null,this.isPropagationStopped=this.isDefaultPrevented=ue,this._dispatchInstances=this._dispatchListeners=null}}),le.Interface={type:null,target:null,currentTarget:function(){return null},eventPhase:null,bubbles:null,cancelable:null,timeStamp:function(e){return e.timeStamp||Date.now()},defaultPrevented:null,isTrusted:null},le.extend=function(e){function t(){}function n(){return r.apply(this,arguments)}var r=this;t.prototype=r.prototype;var o=new t;return i(o,n.prototype),n.prototype=o,n.prototype.constructor=n,n.Interface=i({},r.Interface,e),n.extend=r.extend,fe(n),n},fe(le);var he=le.extend({data:null}),de=le.extend({data:null}),pe=[9,13,27,32],ve=K&&"CompositionEvent"in window,me=null;K&&"documentMode"in document&&(me=document.documentMode);var ye=K&&"TextEvent"in window&&!me,ge=K&&(!ve||me&&8=me),be=String.fromCharCode(32),we={beforeInput:{phasedRegistrationNames:{bubbled:"onBeforeInput",captured:"onBeforeInputCapture"},dependencies:["compositionend","keypress","textInput","paste"]},compositionEnd:{phasedRegistrationNames:{bubbled:"onCompositionEnd",captured:"onCompositionEndCapture"},dependencies:"blur compositionend keydown keypress keyup mousedown".split(" ")},compositionStart:{phasedRegistrationNames:{bubbled:"onCompositionStart",captured:"onCompositionStartCapture"},dependencies:"blur compositionstart keydown keypress keyup mousedown".split(" ")},compositionUpdate:{phasedRegistrationNames:{bubbled:"onCompositionUpdate",captured:"onCompositionUpdateCapture"},dependencies:"blur compositionupdate keydown keypress keyup mousedown".split(" ")}},Ee=!1;function ke(e,t){switch(e){case"keyup":return-1!==pe.indexOf(t.keyCode);case"keydown":return 229!==t.keyCode;case"keypress":case"mousedown":case"blur":return!0;default:return!1}}function Te(e){return"object"===typeof(e=e.detail)&&"data"in e?e.data:null}var _e=!1;var Se={eventTypes:we,extractEvents:function(e,t,n,r){var i=void 0,o=void 0;if(ve)e:{switch(e){case"compositionstart":i=we.compositionStart;break e;case"compositionend":i=we.compositionEnd;break e;case"compositionupdate":i=we.compositionUpdate;break e}i=void 0}else _e?ke(e,n)&&(i=we.compositionEnd):"keydown"===e&&229===n.keyCode&&(i=we.compositionStart);return i?(ge&&"ko"!==n.locale&&(_e||i!==we.compositionStart?i===we.compositionEnd&&_e&&(o=oe()):(re="value"in(ne=r)?ne.value:ne.textContent,_e=!0)),i=he.getPooled(i,t,n,r),o?i.data=o:null!==(o=Te(n))&&(i.data=o),H(i),o=i):o=null,(e=ye?function(e,t){switch(e){case"compositionend":return Te(t);case"keypress":return 32!==t.which?null:(Ee=!0,be);case"textInput":return(e=t.data)===be&&Ee?null:e;default:return null}}(e,n):function(e,t){if(_e)return"compositionend"===e||!ve&&ke(e,t)?(e=oe(),ie=re=ne=null,_e=!1,e):null;switch(e){case"paste":return null;case"keypress":if(!(t.ctrlKey||t.altKey||t.metaKey)||t.ctrlKey&&t.altKey){if(t.char&&1