update architecture and remove heavy dependencies
This commit is contained in:
parent
b92c7d0935
commit
16204b1d93
|
@ -1,2 +1,5 @@
|
|||
*.so
|
||||
.build
|
||||
*.db
|
||||
*.pprof
|
||||
trace.out
|
31
README.md
31
README.md
|
@ -1,34 +1,31 @@
|
|||
# xdrm go framework
|
||||
# Articles API
|
||||
|
||||
### 1. Download and build the package
|
||||
This repository showcases the use of the [aicra](https://git.xdrm.io/go/aicra) API engine.
|
||||
|
||||
### 1. download and build the package
|
||||
|
||||
```bash
|
||||
go get -u git.xdrm.io/go/tiny-url-ex
|
||||
go get -u git.xdrm.io/go/articles-api
|
||||
```
|
||||
|
||||
### 2. launch in docker
|
||||
|
||||
Build the image.
|
||||
Build the image :
|
||||
```bash
|
||||
$ docker build -t tiny-url .
|
||||
$ docker build -t articles-api .
|
||||
```
|
||||
|
||||
Then you can run the container with :
|
||||
Run the container :
|
||||
```bash
|
||||
$ docker run -p 127.0.0.1:8888:4242 tiny-url
|
||||
$ docker run -p 127.0.0.1:8888:4242 articles-api
|
||||
```
|
||||
|
||||
You can now play with it with any regular REST API client at `127.0.0.1:8888`.
|
||||
|
||||
##### 1. Get an authentication token
|
||||
#### 2. Play with it
|
||||
|
||||
- `POST /token/admin` to get an authentication token.
|
||||
The [api.json](./api.json) configuration file defines what this api provides, refer to it to know what requests are available.
|
||||
|
||||
#### 2. Manage tiny urls
|
||||
|
||||
> The authentication token must be set inside the `Authorization` header of each request except for the `GET` method which does not require you to be authenticated.
|
||||
|
||||
- `POST /<some-url>` to set a new tiny-url for the url `<some-url>` to redirect to the FORM variable named `target`. Also you can send the FORM data as _x-www-form-urlencoded_, _form-data_ or _json_.
|
||||
- `PUT /<some-url>` to update an existing tiny-url for the url `<some-url>` to redirect to the FORM variable named `target`.
|
||||
- `DELETE /<some-url>` to delete the tiny-url for `<some-url>`.
|
||||
- `GET /<some-url>` to redirect to the target of an existing url `<some-url>`.
|
||||
Examples :
|
||||
- `GET /user/12` returns information about the user identified with 12.
|
||||
- `POST /article` given required parameters creates a new article.
|
50
api.json
50
api.json
|
@ -13,7 +13,7 @@
|
|||
{
|
||||
"method": "GET",
|
||||
"path": "/user/{id}",
|
||||
"scope": [],
|
||||
"scope": [["admin"], ["reader"]],
|
||||
"info": "returns info about an existing user",
|
||||
"in": {
|
||||
"{id}": { "info": "the target user id", "name": "ID", "type": "uint" }
|
||||
|
@ -80,7 +80,7 @@
|
|||
{
|
||||
"method": "GET",
|
||||
"path": "/user/{id}/articles",
|
||||
"scope": [[]],
|
||||
"scope": [["reader"]],
|
||||
"info": "returns the list of existing articles a user wrote",
|
||||
"in": {
|
||||
"{id}": { "info": "author user id", "name": "ID", "type": "uint" }
|
||||
|
@ -93,7 +93,7 @@
|
|||
{
|
||||
"method": "GET",
|
||||
"path": "/articles",
|
||||
"scope": [[]],
|
||||
"scope": [["reader"]],
|
||||
"info": "returns the list of existing articles",
|
||||
"in": {},
|
||||
"out": {
|
||||
|
@ -102,7 +102,7 @@
|
|||
}, {
|
||||
"method": "GET",
|
||||
"path": "/article/{id}",
|
||||
"scope": [[]],
|
||||
"scope": [["reader"]],
|
||||
"info": "returns an existing article",
|
||||
"in": {
|
||||
"{id}": { "info": "the target article id", "name": "ID", "type": "uint" }
|
||||
|
@ -112,14 +112,15 @@
|
|||
"title": { "info": "the article title", "type": "string(5,255)", "name": "Title" },
|
||||
"body": { "info": "the article body", "type": "string", "name": "Body" },
|
||||
"author": { "info": "the author user id", "type": "uint", "name": "Author" },
|
||||
"score": { "info": "absolute vote score", "type": "uint", "name": "Score" }
|
||||
"score": { "info": "absolute vote score", "type": "int", "name": "Score" }
|
||||
}
|
||||
}, {
|
||||
"method": "POST",
|
||||
"path": "/article",
|
||||
"path": "/article/{author}",
|
||||
"scope": [["author"]],
|
||||
"info": "post a new article",
|
||||
"in": {
|
||||
"{author}": { "info": "the author id", "type": "uint", "name": "Author" },
|
||||
"title": { "info": "the article title", "type": "string(5,255)", "name": "Title" },
|
||||
"body": { "info": "the article body", "type": "string", "name": "Body" }
|
||||
},
|
||||
|
@ -128,7 +129,7 @@
|
|||
"title": { "info": "the article title", "type": "string(5,255)", "name": "Title" },
|
||||
"body": { "info": "the article body", "type": "string", "name": "Body" },
|
||||
"author": { "info": "the author user id", "type": "uint", "name": "Author" },
|
||||
"score": { "info": "absolute vote score", "type": "uint", "name": "Score" }
|
||||
"score": { "info": "absolute vote score", "type": "int", "name": "Score" }
|
||||
}
|
||||
}, {
|
||||
"method": "DELETE",
|
||||
|
@ -139,6 +140,41 @@
|
|||
"{id}": { "info": "the target article id", "name": "ID", "type": "uint" }
|
||||
},
|
||||
"out": { }
|
||||
},
|
||||
|
||||
{
|
||||
"method": "POST",
|
||||
"path": "/article/{id}/up",
|
||||
"scope": [["reader"]],
|
||||
"info": "upvotes an article",
|
||||
"in": {
|
||||
"{id}": { "info": "the target article id", "name": "Article", "type": "uint" },
|
||||
"user": { "info": "user id", "name": "User", "type": "uint" }
|
||||
},
|
||||
"out": {
|
||||
"id": { "info": "the article id", "type": "uint", "name": "ID" },
|
||||
"title": { "info": "the article title", "type": "string(5,255)", "name": "Title" },
|
||||
"body": { "info": "the article body", "type": "string", "name": "Body" },
|
||||
"author": { "info": "the author user id", "type": "uint", "name": "Author" },
|
||||
"score": { "info": "absolute vote score", "type": "int", "name": "Score" }
|
||||
}
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"path": "/article/{id}/down",
|
||||
"scope": [["reader"]],
|
||||
"info": "downvotes an article",
|
||||
"in": {
|
||||
"{id}": { "info": "the target article id", "name": "Article", "type": "uint" },
|
||||
"user": { "info": "user id", "name": "User", "type": "uint" }
|
||||
},
|
||||
"out": {
|
||||
"id": { "info": "the article id", "type": "uint", "name": "ID" },
|
||||
"title": { "info": "the article title", "type": "string(5,255)", "name": "Title" },
|
||||
"body": { "info": "the article body", "type": "string", "name": "Body" },
|
||||
"author": { "info": "the author user id", "type": "uint", "name": "Author" },
|
||||
"score": { "info": "absolute vote score", "type": "int", "name": "Score" }
|
||||
}
|
||||
}
|
||||
|
||||
]
|
10
go.mod
10
go.mod
|
@ -1,10 +1,10 @@
|
|||
module git.xdrm.io/go/tiny-url-ex
|
||||
module git.xdrm.io/go/articles-api
|
||||
|
||||
go 1.14
|
||||
|
||||
require (
|
||||
git.xdrm.io/go/aicra v0.3.0
|
||||
github.com/jinzhu/gorm v1.9.12
|
||||
github.com/mattn/go-sqlite3 v2.0.3+incompatible // indirect
|
||||
git.xdrm.io/go/aicra v0.3.1
|
||||
github.com/jmoiron/sqlx v1.2.0
|
||||
github.com/mattn/go-sqlite3 v2.0.3+incompatible
|
||||
google.golang.org/appengine v1.6.5 // indirect
|
||||
)
|
||||
|
||||
|
|
30
go.sum
30
go.sum
|
@ -1,22 +1,14 @@
|
|||
github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
|
||||
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0=
|
||||
github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
||||
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/jinzhu/gorm v1.9.12 h1:Drgk1clyWT9t9ERbzHza6Mj/8FY/CqMyVzOiHviMo6Q=
|
||||
github.com/jinzhu/gorm v1.9.12/go.mod h1:vhTjlKSJUTWNtcbQtrMBFCxy7eXTzeCAzfL5fBZT/Qs=
|
||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/mattn/go-sqlite3 v2.0.1+incompatible h1:xQ15muvnzGBHpIpdrNi1DA5x0+TcBZzsIDwmw9uTHzw=
|
||||
github.com/mattn/go-sqlite3 v2.0.1+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
||||
git.xdrm.io/go/aicra v0.3.1/go.mod h1:nkrG/J0/Y/t+pTvGxCAWSNPYePTJmdYCXDdX+7W+y0A=
|
||||
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks=
|
||||
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
||||
github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
|
|
118
main.go
118
main.go
|
@ -1,70 +1,104 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"git.xdrm.io/go/aicra/datatype"
|
||||
"git.xdrm.io/go/aicra/datatype/builtin"
|
||||
"git.xdrm.io/go/tiny-url-ex/service/article"
|
||||
"git.xdrm.io/go/tiny-url-ex/service/user"
|
||||
|
||||
"github.com/jinzhu/gorm"
|
||||
_ "github.com/jinzhu/gorm/dialects/sqlite"
|
||||
"os"
|
||||
|
||||
"git.xdrm.io/go/aicra"
|
||||
"git.xdrm.io/go/aicra/datatype/builtin"
|
||||
"git.xdrm.io/go/articles-api/pkg/handlers"
|
||||
"git.xdrm.io/go/articles-api/pkg/persistence/drivers/sqlite"
|
||||
"git.xdrm.io/go/articles-api/pkg/persistence/storage"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if err := run(); err != nil {
|
||||
log.Fatalf("%s", err)
|
||||
}
|
||||
}
|
||||
|
||||
listenTo := ":4242"
|
||||
func run() error {
|
||||
// 1. prepare builder
|
||||
builder := &aicra.Builder{}
|
||||
builder.AddType(builtin.AnyDataType{})
|
||||
builder.AddType(builtin.BoolDataType{})
|
||||
builder.AddType(builtin.UintDataType{})
|
||||
builder.AddType(builtin.IntDataType{})
|
||||
builder.AddType(builtin.StringDataType{})
|
||||
|
||||
// 1. build server
|
||||
log.Printf("[server] create server")
|
||||
server, err := aicra.New("api.json", buildDataTypes()...)
|
||||
// 2. setup with configuration file
|
||||
config, err := os.OpenFile("api.json", os.O_RDONLY, os.ModePerm)
|
||||
if err != nil {
|
||||
log.Fatalf("/!\\ cannot init server: %v\n", err)
|
||||
return fmt.Errorf("cannot open config file: %w", err)
|
||||
}
|
||||
err = builder.Setup(config)
|
||||
config.Close()
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot setup builder: %w", err)
|
||||
}
|
||||
|
||||
// 2. connect to storage
|
||||
db, err := gorm.Open("sqlite3", "test.db")
|
||||
// 3. connect to storage
|
||||
db := &sqlite.Storage{}
|
||||
err = db.Open()
|
||||
if err != nil {
|
||||
log.Fatalf("/!\\ cannot open database: %v\n", err)
|
||||
return fmt.Errorf("cannot open database: %w", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
// 3. init services
|
||||
userService := &user.Service{DB: db}
|
||||
articleService := &article.Service{DB: db}
|
||||
// 3. create services
|
||||
SetupRoutes(builder, db)
|
||||
|
||||
// 4. wire services
|
||||
if err = userService.Wire(server); err != nil {
|
||||
log.Fatalf("/!\\ cannot wire service 'user': %s", err)
|
||||
}
|
||||
if err = articleService.Wire(server); err != nil {
|
||||
log.Fatalf("/!\\ cannot wire service 'article': %s", err)
|
||||
}
|
||||
|
||||
// 5. create http server
|
||||
httpServer, err := server.ToHTTPServer()
|
||||
// 4. create http server
|
||||
server, err := builder.Build()
|
||||
if err != nil {
|
||||
log.Fatalf("cannot get http server: %s", err)
|
||||
return fmt.Errorf("cannot build server: %w", err)
|
||||
}
|
||||
|
||||
// 5. listen and serve
|
||||
log.Printf("[server] listening to '%s'\n\n", listenTo)
|
||||
log.Fatal(http.ListenAndServe(listenTo, httpServer))
|
||||
|
||||
log.Printf("[server] listening to '%s'", ":4242")
|
||||
return http.ListenAndServe(":4242", server)
|
||||
}
|
||||
|
||||
func buildDataTypes() []datatype.T {
|
||||
dtypes := make([]datatype.T, 0)
|
||||
func SetupRoutes(b *aicra.Builder, db storage.T) error {
|
||||
app := &handlers.App{Db: db}
|
||||
if err := b.Bind(http.MethodGet, "/articles", app.HandleGetAllArticles); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := b.Bind(http.MethodGet, "/user/{id}/articles", app.HandleGetArticlesByAuthor); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := b.Bind(http.MethodGet, "/article/{id}", app.HandleGetArticleByID); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := b.Bind(http.MethodPost, "/article/{author}", app.HandleCreateArticle); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := b.Bind(http.MethodDelete, "/article/{id}", app.HandleDeleteArticle); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := b.Bind(http.MethodPost, "/article/{id}/up", app.HandleUpVote); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := b.Bind(http.MethodPost, "/article/{id}/down", app.HandleDownVote); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dtypes = append(dtypes, builtin.AnyDataType{})
|
||||
dtypes = append(dtypes, builtin.BoolDataType{})
|
||||
dtypes = append(dtypes, builtin.UintDataType{})
|
||||
dtypes = append(dtypes, builtin.IntDataType{})
|
||||
dtypes = append(dtypes, builtin.StringDataType{})
|
||||
|
||||
return dtypes
|
||||
if err := b.Bind(http.MethodGet, "/user/{id}", app.HandleGetUserByID); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := b.Bind(http.MethodGet, "/users", app.HandleGetAllUsers); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := b.Bind(http.MethodPost, "/user", app.HandleCreateUser); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := b.Bind(http.MethodPut, "/user/{id}", app.HandleUpdateUser); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := b.Bind(http.MethodDelete, "/user/{id}", app.HandleDeleteUser); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -0,0 +1,78 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"git.xdrm.io/go/aicra/api"
|
||||
"git.xdrm.io/go/articles-api/pkg/model"
|
||||
)
|
||||
|
||||
type articleList struct {
|
||||
Articles []model.Article
|
||||
}
|
||||
|
||||
type createArticleRequest struct {
|
||||
Author uint
|
||||
Title string
|
||||
Body string
|
||||
}
|
||||
|
||||
type voteRequest struct {
|
||||
User uint
|
||||
Article uint
|
||||
}
|
||||
|
||||
func (a *App) HandleGetArticlesByAuthor(req byID) (*articleList, api.Error) {
|
||||
articles, err := a.Db.GetArticlesByAuthor(req.ID)
|
||||
if err != nil {
|
||||
return nil, translateStorageError(err, api.ErrorFailure)
|
||||
}
|
||||
return &articleList{Articles: articles}, api.ErrorSuccess
|
||||
}
|
||||
|
||||
func (a *App) HandleGetAllArticles() (*articleList, api.Error) {
|
||||
articles, err := a.Db.GetAllArticles()
|
||||
if err != nil {
|
||||
return nil, translateStorageError(err, api.ErrorFailure)
|
||||
}
|
||||
return &articleList{Articles: articles}, api.ErrorSuccess
|
||||
}
|
||||
|
||||
func (a *App) HandleGetArticleByID(req byID) (*model.Article, api.Error) {
|
||||
article, err := a.Db.GetArticleByID(req.ID)
|
||||
if err != nil {
|
||||
return nil, translateStorageError(err, api.ErrorFailure)
|
||||
}
|
||||
return article, api.ErrorSuccess
|
||||
}
|
||||
|
||||
func (a *App) HandleCreateArticle(param createArticleRequest) (*model.Article, api.Error) {
|
||||
article, err := a.Db.CreateArticle(param.Title, param.Body, param.Author)
|
||||
if err != nil {
|
||||
return nil, translateStorageError(err, api.ErrorFailure)
|
||||
}
|
||||
return article, api.ErrorSuccess
|
||||
}
|
||||
|
||||
func (a *App) HandleDeleteArticle(req byID) api.Error {
|
||||
err := a.Db.DeleteArticle(req.ID)
|
||||
if err != nil {
|
||||
return translateStorageError(err, api.ErrorFailure)
|
||||
}
|
||||
return api.ErrorSuccess
|
||||
}
|
||||
|
||||
func (a *App) HandleUpVote(req voteRequest) (*model.Article, api.Error) {
|
||||
_, err := a.Db.UpVote(req.User, req.Article)
|
||||
if err != nil {
|
||||
return nil, translateStorageError(err, api.ErrorFailure)
|
||||
}
|
||||
// get article
|
||||
return a.HandleGetArticleByID(byID{ID: req.Article})
|
||||
}
|
||||
func (a *App) HandleDownVote(req voteRequest) (*model.Article, api.Error) {
|
||||
_, err := a.Db.DownVote(req.User, req.Article)
|
||||
if err != nil {
|
||||
return nil, translateStorageError(err, api.ErrorFailure)
|
||||
}
|
||||
// get article
|
||||
return a.HandleGetArticleByID(byID{ID: req.Article})
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"git.xdrm.io/go/aicra/api"
|
||||
"git.xdrm.io/go/articles-api/pkg/persistence/storage"
|
||||
)
|
||||
|
||||
type App struct {
|
||||
Db storage.T
|
||||
}
|
||||
|
||||
type byID struct{ ID uint }
|
||||
|
||||
func translateStorageError(err error, fallback api.Error) api.Error {
|
||||
switch err {
|
||||
case storage.Transaction:
|
||||
return api.ErrorTransaction
|
||||
case storage.NotCreated:
|
||||
return api.ErrorCreation
|
||||
case storage.NotFound:
|
||||
return api.ErrorNoMatchFound
|
||||
case storage.NotUpdated:
|
||||
return api.ErrorModification
|
||||
case storage.NotDeleted:
|
||||
return api.ErrorDeletion
|
||||
default:
|
||||
return fallback
|
||||
}
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"git.xdrm.io/go/aicra/api"
|
||||
"git.xdrm.io/go/articles-api/pkg/model"
|
||||
)
|
||||
|
||||
type userList struct {
|
||||
Users []model.User
|
||||
}
|
||||
|
||||
type createUserRequest struct {
|
||||
Username string
|
||||
Firstname string
|
||||
Lastname string
|
||||
}
|
||||
|
||||
type updateUserRequest struct {
|
||||
ID uint
|
||||
Username *string
|
||||
Firstname *string
|
||||
Lastname *string
|
||||
}
|
||||
|
||||
func (a *App) HandleGetAllUsers() (*userList, api.Error) {
|
||||
users, err := a.Db.GetAllUsers()
|
||||
if err != nil {
|
||||
return nil, translateStorageError(err, api.ErrorFailure)
|
||||
}
|
||||
return &userList{Users: users}, api.ErrorSuccess
|
||||
}
|
||||
|
||||
func (a *App) HandleGetUserByID(req byID) (*model.User, api.Error) {
|
||||
user, err := a.Db.GetUserByID(req.ID)
|
||||
if err != nil {
|
||||
return nil, translateStorageError(err, api.ErrorFailure)
|
||||
}
|
||||
return user, api.ErrorSuccess
|
||||
}
|
||||
|
||||
func (a *App) HandleCreateUser(req createUserRequest) (*model.User, api.Error) {
|
||||
user, err := a.Db.CreateUser(req.Username, req.Firstname, req.Lastname)
|
||||
if err != nil {
|
||||
return nil, translateStorageError(err, api.ErrorFailure)
|
||||
}
|
||||
return user, api.ErrorSuccess
|
||||
}
|
||||
|
||||
func (a *App) HandleUpdateUser(req updateUserRequest) (*model.User, api.Error) {
|
||||
// nothing to update, ignore
|
||||
if req.Username == nil && req.Firstname == nil && req.Lastname == nil {
|
||||
return a.HandleGetUserByID(byID{req.ID})
|
||||
}
|
||||
|
||||
user, err := a.Db.UpdateUser(req.ID, req.Username, req.Firstname, req.Lastname)
|
||||
if err != nil {
|
||||
return nil, translateStorageError(err, api.ErrorFailure)
|
||||
}
|
||||
|
||||
return user, api.ErrorSuccess
|
||||
}
|
||||
|
||||
func (a *App) HandleDeleteUser(req byID) api.Error {
|
||||
err := a.Db.DeleteUser(req.ID)
|
||||
if err != nil {
|
||||
return translateStorageError(err, api.ErrorFailure)
|
||||
}
|
||||
return api.ErrorSuccess
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
package model
|
||||
|
||||
// Article representation of an article in storage
|
||||
type Article struct {
|
||||
ID uint `json:"article_id" db:"article_id"`
|
||||
Author uint `json:"author" db:"author_ref"`
|
||||
Title string `json:"title" db:"title"`
|
||||
Body string `json:"body" db:"body"`
|
||||
Score int `json:"score" db:"score"`
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
package model
|
||||
|
||||
// User representation of a user in storage
|
||||
type User struct {
|
||||
ID uint `json:"user_id" db:"user_id"`
|
||||
Username string `json:"username" db:"username"`
|
||||
Firstname string `json:"firstname" db:"firstname"`
|
||||
Lastname string `json:"lastname" db:"lastname"`
|
||||
Articles []Article `json:"articles" db:"articles"`
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
package model
|
||||
|
||||
// Vote representation of a user in storage
|
||||
type Vote struct {
|
||||
User uint `json:"user" db:"user_ref"`
|
||||
Article uint `json:"article" db:"article_ref"`
|
||||
Value int `json:"value" db:"value"`
|
||||
}
|
|
@ -0,0 +1,402 @@
|
|||
package sqlite
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"git.xdrm.io/go/articles-api/pkg/model"
|
||||
"git.xdrm.io/go/articles-api/pkg/persistence/storage"
|
||||
"github.com/jmoiron/sqlx"
|
||||
|
||||
// sqlite3 driver
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
)
|
||||
|
||||
// ErrNotInitialized is fired when using a non initialized database
|
||||
var ErrNotInitialized = errors.New("storage not initialized")
|
||||
|
||||
// Storage using sqlite3 driver
|
||||
type Storage struct {
|
||||
db *sqlx.DB
|
||||
}
|
||||
|
||||
const schema = `
|
||||
CREATE TABLE IF NOT EXISTS user(
|
||||
user_id INTEGER,
|
||||
username VARCHAR(100) NOT NULL,
|
||||
firstname VARCHAR(200) NOT NULL,
|
||||
lastname VARCHAR(200) NOT NULL,
|
||||
|
||||
PRIMARY KEY(user_id)
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS article(
|
||||
article_id INTEGER,
|
||||
title VARCHAR(255) NOT NULL,
|
||||
body text NOT NULL,
|
||||
author_ref INTEGER,
|
||||
|
||||
PRIMARY KEY(article_id),
|
||||
FOREIGN KEY(author_ref) REFERENCES user(user_id)
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS vote(
|
||||
user_ref INTEGER,
|
||||
article_ref VARCHAR(100) NOT NULL,
|
||||
value INTEGER NOT NULL,
|
||||
|
||||
PRIMARY KEY(user_ref, article_ref),
|
||||
FOREIGN KEY(user_ref) REFERENCES user(user_id),
|
||||
FOREIGN KEY(article_ref) REFERENCES article(article_id),
|
||||
CHECK( value = 1 OR value = -1 )
|
||||
);
|
||||
`
|
||||
|
||||
// Open the database
|
||||
func (s *Storage) Open() error {
|
||||
db, err := sqlx.Connect("sqlite3", "./local.db")
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot open storage: %w", err)
|
||||
}
|
||||
s.db = db
|
||||
|
||||
// create tables
|
||||
tx, err := s.db.Begin()
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot create schema: %w", storage.Transaction)
|
||||
}
|
||||
_, err = tx.Exec(schema)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot create tables: %s", err)
|
||||
}
|
||||
tx.Commit()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close the databse
|
||||
func (s *Storage) Close() error {
|
||||
return s.db.Close()
|
||||
}
|
||||
|
||||
// CreateUser ...
|
||||
func (s *Storage) CreateUser(username, firstname, lastname string) (*model.User, error) {
|
||||
user := model.User{
|
||||
Username: username,
|
||||
Firstname: firstname,
|
||||
Lastname: lastname,
|
||||
}
|
||||
|
||||
// insert user
|
||||
tx, err := s.db.Beginx()
|
||||
if err != nil {
|
||||
return nil, storage.Transaction
|
||||
}
|
||||
res, err := tx.NamedExec(`INSERT INTO
|
||||
user(username, firstname, lastname)
|
||||
VALUES(:username,:firstname,:lastname);`, &user)
|
||||
if err != nil {
|
||||
return nil, storage.NotCreated
|
||||
}
|
||||
|
||||
// find inserted id
|
||||
insertedID, err := res.LastInsertId()
|
||||
if err != nil {
|
||||
return nil, storage.NotFound
|
||||
}
|
||||
tx.Commit()
|
||||
user.ID = uint(insertedID)
|
||||
|
||||
// return user info
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
// UpdateUser ...
|
||||
func (s *Storage) UpdateUser(id uint, username, firstname, lastname *string) (*model.User, error) {
|
||||
tx, err := s.db.Beginx()
|
||||
if err != nil {
|
||||
return nil, storage.Transaction
|
||||
}
|
||||
|
||||
// update fields
|
||||
if username != nil {
|
||||
_, err := tx.Exec(`UPDATE user SET username = ? WHERE user_id = ?;`, *username, id)
|
||||
if err != nil {
|
||||
return nil, storage.NotUpdated
|
||||
}
|
||||
|
||||
}
|
||||
if firstname != nil {
|
||||
_, err := tx.Exec(`UPDATE user SET firstname = ? WHERE user_id = ?;`, *firstname, id)
|
||||
if err != nil {
|
||||
return nil, storage.NotUpdated
|
||||
}
|
||||
}
|
||||
if lastname != nil {
|
||||
_, err := tx.Exec(`UPDATE user SET lastname = ? WHERE user_id = ?;`, *lastname, id)
|
||||
if err != nil {
|
||||
return nil, storage.NotUpdated
|
||||
}
|
||||
}
|
||||
|
||||
// select updated user
|
||||
user := model.User{}
|
||||
err = tx.Get(&user, `SELECT user_id, username, firstname, lastname FROM user WHERE user_id = $1;`, id)
|
||||
if err != nil {
|
||||
return nil, storage.NotFound
|
||||
}
|
||||
tx.Commit()
|
||||
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
// DeleteUser ...
|
||||
func (s *Storage) DeleteUser(id uint) error {
|
||||
tx, err := s.db.Beginx()
|
||||
if err != nil {
|
||||
return storage.Transaction
|
||||
}
|
||||
|
||||
res, err := tx.Exec("DELETE FROM user WHERE user_id = ?;")
|
||||
if err != nil {
|
||||
return storage.NotDeleted
|
||||
}
|
||||
|
||||
mustBeOne, err := res.RowsAffected()
|
||||
if err != nil || mustBeOne < 1 {
|
||||
return storage.NotDeleted
|
||||
}
|
||||
tx.Commit()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetUserByID ...
|
||||
func (s *Storage) GetUserByID(id uint) (*model.User, error) {
|
||||
user := model.User{}
|
||||
|
||||
err := s.db.Get(&user, `SELECT user_id, username, firstname, lastname FROM user WHERE user_id = $1;`, id)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, storage.NotFound
|
||||
}
|
||||
return nil, storage.Failure
|
||||
}
|
||||
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
// GetAllUsers ...
|
||||
func (s *Storage) GetAllUsers() ([]model.User, error) {
|
||||
users := []model.User{}
|
||||
err := s.db.Select(&users, "SELECT user_id, username, firstname, lastname FROM user;")
|
||||
if err != nil {
|
||||
return nil, storage.NotFound
|
||||
}
|
||||
return users, nil
|
||||
}
|
||||
|
||||
// CreateArticle ...
|
||||
func (s *Storage) CreateArticle(title, body string, author uint) (*model.Article, error) {
|
||||
article := model.Article{
|
||||
Title: title,
|
||||
Body: body,
|
||||
Author: author,
|
||||
}
|
||||
|
||||
// insert article
|
||||
tx, err := s.db.Beginx()
|
||||
if err != nil {
|
||||
return nil, storage.Transaction
|
||||
}
|
||||
|
||||
res, err := tx.NamedExec(`INSERT INTO
|
||||
article(title, body, author_ref)
|
||||
VALUES(:title,:body,:author_ref);`, &article)
|
||||
if err != nil {
|
||||
log.Printf("err: %s", err)
|
||||
return nil, storage.NotCreated
|
||||
}
|
||||
|
||||
// find inserted id
|
||||
insertedID, err := res.LastInsertId()
|
||||
if err != nil {
|
||||
return nil, storage.NotFound
|
||||
}
|
||||
tx.Commit()
|
||||
article.ID = uint(insertedID)
|
||||
|
||||
return &article, nil
|
||||
}
|
||||
|
||||
// UpdateArticle ...
|
||||
func (s *Storage) UpdateArticle(id uint, title *string, body *string) (*model.Article, error) {
|
||||
tx, err := s.db.Beginx()
|
||||
if err != nil {
|
||||
return nil, storage.Transaction
|
||||
}
|
||||
|
||||
if title != nil {
|
||||
_, err := tx.Exec(`UPDATE article SET title = ? WHERE article_id = ?;`, *title, id)
|
||||
if err != nil {
|
||||
return nil, storage.NotUpdated
|
||||
}
|
||||
}
|
||||
if body != nil {
|
||||
_, err := tx.Exec(`UPDATE article SET body = ? WHERE article_id = ?;`, *body, id)
|
||||
if err != nil {
|
||||
return nil, storage.NotUpdated
|
||||
}
|
||||
}
|
||||
|
||||
// select updated article
|
||||
article, err := s.GetArticleByID(id)
|
||||
if err != nil {
|
||||
return nil, storage.NotFound
|
||||
}
|
||||
tx.Commit()
|
||||
|
||||
return article, nil
|
||||
}
|
||||
|
||||
// DeleteArticle ...
|
||||
func (s *Storage) DeleteArticle(id uint) error {
|
||||
tx, err := s.db.Beginx()
|
||||
if err != nil {
|
||||
return storage.Transaction
|
||||
}
|
||||
|
||||
res, err := tx.Exec("DELETE FROM article WHERE article_id = ?;")
|
||||
if err != nil {
|
||||
return storage.NotDeleted
|
||||
}
|
||||
|
||||
mustBeOne, err := res.RowsAffected()
|
||||
if err != nil || mustBeOne < 1 {
|
||||
return storage.NotDeleted
|
||||
}
|
||||
tx.Commit()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetArticleByID ...
|
||||
func (s *Storage) GetArticleByID(id uint) (*model.Article, error) {
|
||||
article := model.Article{}
|
||||
|
||||
err := s.db.Get(&article, `SELECT article_id, title, body, author_ref, COALESCE(SUM(vote.value),0) as score
|
||||
FROM article
|
||||
LEFT OUTER JOIN vote ON article.article_id = vote.article_ref
|
||||
WHERE article_id = $1
|
||||
GROUP BY article_id, title, body, author_ref
|
||||
LIMIT 1;`, id)
|
||||
if err != nil {
|
||||
log.Printf("err: %s", err)
|
||||
return nil, storage.NotFound
|
||||
}
|
||||
|
||||
return &article, nil
|
||||
}
|
||||
|
||||
// GetArticlesByAuthor ...
|
||||
func (s *Storage) GetArticlesByAuthor(id uint) ([]model.Article, error) {
|
||||
articles := []model.Article{}
|
||||
|
||||
err := s.db.Select(&articles, `SELECT article_id, title, body, author_ref, COALESCE(SUM(vote.value),0) as score
|
||||
FROM article
|
||||
LEFT OUTER JOIN vote ON article.article_id = vote.article_ref
|
||||
WHERE author_ref = $1
|
||||
GROUP BY article_id, title, body, author_ref;`, id)
|
||||
if err != nil {
|
||||
log.Printf("err: %s", err)
|
||||
return nil, storage.NotFound
|
||||
}
|
||||
|
||||
return articles, nil
|
||||
}
|
||||
|
||||
// GetAllArticles ...
|
||||
func (s *Storage) GetAllArticles() ([]model.Article, error) {
|
||||
articles := []model.Article{}
|
||||
|
||||
err := s.db.Select(&articles, `SELECT article_id, title, body, author_ref, COALESCE(SUM(vote.value),0) as score
|
||||
FROM article
|
||||
LEFT OUTER JOIN vote ON article.article_id = vote.article_ref
|
||||
GROUP BY article_id, title, body, author_ref;`)
|
||||
if err != nil {
|
||||
return nil, storage.NotFound
|
||||
}
|
||||
|
||||
return articles, nil
|
||||
}
|
||||
|
||||
// UpVote ...
|
||||
func (s *Storage) UpVote(user uint, article uint) (*model.Vote, error) {
|
||||
// update if alreay exists
|
||||
vote := model.Vote{
|
||||
User: user,
|
||||
Article: article,
|
||||
Value: 1,
|
||||
}
|
||||
|
||||
tx, err := s.db.Beginx()
|
||||
if err != nil {
|
||||
return nil, storage.Transaction
|
||||
}
|
||||
|
||||
// alreay exists -> update
|
||||
row := tx.QueryRow("SELECT user_ref, article_ref, value FROM vote WHERE article_ref=? AND user_ref=?;", vote.Article, vote.User)
|
||||
if row != nil && row.Scan() != sql.ErrNoRows {
|
||||
_, err := tx.Exec(`UPDATE vote SET value = ? WHERE article_ref = ? AND user_ref = ?;`, vote.Value, vote.Article, vote.User)
|
||||
if err != nil {
|
||||
return nil, storage.NotUpdated
|
||||
}
|
||||
|
||||
tx.Commit()
|
||||
return &vote, nil
|
||||
}
|
||||
|
||||
// create vote
|
||||
_, err = tx.Exec("INSERT INTO vote(user_ref, article_ref, value) VALUES(?, ?, ?);", vote.User, vote.Article, vote.Value)
|
||||
if err != nil {
|
||||
return nil, storage.NotCreated
|
||||
}
|
||||
tx.Commit()
|
||||
|
||||
return &vote, nil
|
||||
}
|
||||
|
||||
// DownVote ...
|
||||
func (s *Storage) DownVote(user uint, article uint) (*model.Vote, error) {
|
||||
// update if alreay exists
|
||||
vote := model.Vote{
|
||||
User: user,
|
||||
Article: article,
|
||||
Value: -1,
|
||||
}
|
||||
|
||||
tx, err := s.db.Beginx()
|
||||
if err != nil {
|
||||
return nil, storage.Transaction
|
||||
}
|
||||
|
||||
// alreay exists -> update
|
||||
row := tx.QueryRow("SELECT user_ref, article_ref, value FROM vote WHERE article_ref=? AND user_ref=?;", vote.Article, vote.User)
|
||||
if row != nil && row.Scan() != sql.ErrNoRows {
|
||||
_, err := tx.Exec(`UPDATE vote SET value = ? WHERE article_ref = ? AND user_ref = ?;`, vote.Value, vote.Article, vote.User)
|
||||
if err != nil {
|
||||
return nil, storage.NotUpdated
|
||||
}
|
||||
|
||||
tx.Commit()
|
||||
return &vote, nil
|
||||
}
|
||||
|
||||
// create vote
|
||||
_, err = tx.Exec("INSERT INTO vote(user_ref, article_ref, value) VALUES(?, ?, ?);", vote.User, vote.Article, vote.Value)
|
||||
if err != nil {
|
||||
return nil, storage.NotCreated
|
||||
}
|
||||
|
||||
tx.Commit()
|
||||
return &vote, nil
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
package storage
|
||||
|
||||
// cerr allows you to create constant "const" error with type boxing.
|
||||
type cerr string
|
||||
|
||||
// Error implements the error builtin interface.
|
||||
func (err cerr) Error() string {
|
||||
return string(err)
|
||||
}
|
||||
|
||||
// Transaction - on transaction error
|
||||
const Transaction = cerr("transaction error")
|
||||
|
||||
// NotFound - when not found
|
||||
const NotFound = cerr("not found")
|
||||
|
||||
// NotCreated - when not created
|
||||
const NotCreated = cerr("not created")
|
||||
|
||||
// NotUpdated - when not updated
|
||||
const NotUpdated = cerr("not updated")
|
||||
|
||||
// NotDeleted - when not deleted
|
||||
const NotDeleted = cerr("not deleted")
|
||||
|
||||
// Failure - unknown error
|
||||
const Failure = cerr("unknown error")
|
|
@ -0,0 +1,29 @@
|
|||
package storage
|
||||
|
||||
import "git.xdrm.io/go/articles-api/pkg/model"
|
||||
|
||||
// T defines the storage interface
|
||||
type T interface {
|
||||
// general
|
||||
Open() error
|
||||
Close() error
|
||||
|
||||
// user
|
||||
CreateUser(username, firstname, lastname string) (*model.User, error)
|
||||
UpdateUser(id uint, username, firstname, lastname *string) (*model.User, error)
|
||||
DeleteUser(id uint) error
|
||||
GetUserByID(id uint) (*model.User, error)
|
||||
GetAllUsers() ([]model.User, error)
|
||||
|
||||
// article
|
||||
CreateArticle(title, body string, author uint) (*model.Article, error)
|
||||
UpdateArticle(id uint, title *string, body *string) (*model.Article, error)
|
||||
DeleteArticle(id uint) error
|
||||
GetArticleByID(id uint) (*model.Article, error)
|
||||
GetArticlesByAuthor(id uint) ([]model.Article, error)
|
||||
GetAllArticles() ([]model.Article, error)
|
||||
|
||||
// user-article
|
||||
UpVote(user uint, article uint) (*model.Vote, error)
|
||||
DownVote(user uint, article uint) (*model.Vote, error)
|
||||
}
|
|
@ -1,86 +0,0 @@
|
|||
package article
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"git.xdrm.io/go/aicra"
|
||||
"git.xdrm.io/go/aicra/api"
|
||||
"git.xdrm.io/go/tiny-url-ex/service/model"
|
||||
"github.com/jinzhu/gorm"
|
||||
)
|
||||
|
||||
// Service to manage users
|
||||
type Service struct {
|
||||
DB *gorm.DB
|
||||
}
|
||||
|
||||
// Wire services to their paths
|
||||
func (s Service) Wire(server *aicra.Server) error {
|
||||
if !s.DB.HasTable(&model.Article{}) {
|
||||
s.DB.CreateTable(&model.Article{})
|
||||
}
|
||||
|
||||
if err := server.Handle(http.MethodGet, "/articles", s.getAllArticles); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := server.Handle(http.MethodGet, "/user/{id}/articles", s.getArticlesByAuthor); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := server.Handle(http.MethodGet, "/article/{id}", s.getArticleByID); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := server.Handle(http.MethodPost, "/article", s.postArticle); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := server.Handle(http.MethodDelete, "/article/{id}", s.deleteArticle); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type articleList struct {
|
||||
Articles []model.Article
|
||||
}
|
||||
|
||||
func (s Service) getArticlesByAuthor(param struct{ ID uint }) (*articleList, api.Error) {
|
||||
articles := make([]model.Article, 0)
|
||||
s.DB.Where("author = ?", param.ID).Find(&articles)
|
||||
return &articleList{
|
||||
Articles: articles,
|
||||
}, api.ErrorSuccess
|
||||
}
|
||||
|
||||
func (s Service) getAllArticles() (*articleList, api.Error) {
|
||||
articles := make([]model.Article, 0)
|
||||
s.DB.Find(&articles)
|
||||
return &articleList{
|
||||
Articles: articles,
|
||||
}, api.ErrorSuccess
|
||||
}
|
||||
|
||||
func (s Service) getArticleByID(param struct{ ID uint }) (*model.Article, api.Error) {
|
||||
var article model.Article
|
||||
if s.DB.First(&article, param.ID).RecordNotFound() {
|
||||
return nil, api.ErrorNoMatchFound
|
||||
}
|
||||
|
||||
return &article, api.ErrorSuccess
|
||||
}
|
||||
|
||||
type createRequest struct {
|
||||
Title string
|
||||
Body string
|
||||
}
|
||||
|
||||
func (s Service) postArticle(param createRequest) (*model.Article, api.Error) {
|
||||
return nil, api.ErrorNotImplemented
|
||||
}
|
||||
|
||||
func (s Service) deleteArticle(param struct{ ID uint }) api.Error {
|
||||
article := model.Article{
|
||||
ID: param.ID,
|
||||
}
|
||||
s.DB.Delete(&article)
|
||||
|
||||
return api.ErrorSuccess
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
package model
|
||||
|
||||
// Article representation of an article in storage
|
||||
type Article struct {
|
||||
ID uint `json:"article_id" gorm:"primary_key"`
|
||||
Author uint `json:"author"`
|
||||
Title string `json:"title"`
|
||||
Body string `json:"body"`
|
||||
Score int
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
package model
|
||||
|
||||
// User representation of a user in storage
|
||||
type User struct {
|
||||
ID uint `json:"user_id" gorm:"primary_key"`
|
||||
Username string `json:"username"`
|
||||
Firstname string `json:"firstname"`
|
||||
Lastname string `json:"lastname"`
|
||||
Articles []Article `json:"articles" gorm:"foreignKey:Author"`
|
||||
}
|
|
@ -1,119 +0,0 @@
|
|||
package user
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"git.xdrm.io/go/aicra"
|
||||
"git.xdrm.io/go/aicra/api"
|
||||
"git.xdrm.io/go/tiny-url-ex/service/model"
|
||||
"github.com/jinzhu/gorm"
|
||||
)
|
||||
|
||||
// Service to manage users
|
||||
type Service struct {
|
||||
DB *gorm.DB
|
||||
}
|
||||
|
||||
// Wire services to their paths
|
||||
func (s Service) Wire(server *aicra.Server) error {
|
||||
if !s.DB.HasTable(&model.User{}) {
|
||||
s.DB.CreateTable(&model.User{})
|
||||
}
|
||||
|
||||
if err := server.Handle(http.MethodGet, "/user/{id}", s.getUserByID); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := server.Handle(http.MethodGet, "/users", s.getAllUsers); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := server.Handle(http.MethodPost, "/user", s.createUser); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := server.Handle(http.MethodPut, "/user/{id}", s.updateUser); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := server.Handle(http.MethodDelete, "/user/{id}", s.deleteUser); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type userList struct {
|
||||
Users []model.User
|
||||
}
|
||||
|
||||
func (s Service) getAllUsers() (*userList, api.Error) {
|
||||
users := userList{}
|
||||
s.DB.Find(&users.Users)
|
||||
return &users, api.ErrorSuccess
|
||||
}
|
||||
|
||||
func (s Service) getUserByID(param struct{ ID uint }) (*model.User, api.Error) {
|
||||
var user model.User
|
||||
if s.DB.First(&user, param.ID).RecordNotFound() {
|
||||
return nil, api.ErrorNoMatchFound
|
||||
}
|
||||
|
||||
return &user, api.ErrorSuccess
|
||||
}
|
||||
|
||||
type createRequest struct {
|
||||
Username string
|
||||
Firstname string
|
||||
Lastname string
|
||||
}
|
||||
|
||||
func (s Service) createUser(param createRequest) (*model.User, api.Error) {
|
||||
user := model.User{
|
||||
Username: param.Username,
|
||||
Firstname: param.Firstname,
|
||||
Lastname: param.Lastname,
|
||||
}
|
||||
|
||||
s.DB.Create(&user)
|
||||
if s.DB.Last(&user).RecordNotFound() {
|
||||
return nil, api.ErrorNoMatchFound
|
||||
}
|
||||
|
||||
return &user, api.ErrorSuccess
|
||||
}
|
||||
|
||||
type updateRequest struct {
|
||||
ID uint
|
||||
Username *string
|
||||
Firstname *string
|
||||
Lastname *string
|
||||
}
|
||||
|
||||
func (s Service) updateUser(param updateRequest) (*model.User, api.Error) {
|
||||
var user model.User
|
||||
if s.DB.First(&user, param.ID).RecordNotFound() {
|
||||
return nil, api.ErrorNoMatchFound
|
||||
}
|
||||
|
||||
// override with updated values
|
||||
if param.Username != nil {
|
||||
user.Username = *param.Username
|
||||
}
|
||||
if param.Firstname != nil {
|
||||
user.Firstname = *param.Firstname
|
||||
}
|
||||
if param.Lastname != nil {
|
||||
user.Lastname = *param.Lastname
|
||||
}
|
||||
|
||||
// update
|
||||
if s.DB.Save(&user).RowsAffected < 1 {
|
||||
return nil, api.ErrorFailure
|
||||
}
|
||||
|
||||
return &user, api.ErrorSuccess
|
||||
}
|
||||
|
||||
func (s Service) deleteUser(param struct{ ID uint }) api.Error {
|
||||
user := model.User{
|
||||
ID: param.ID,
|
||||
}
|
||||
s.DB.Delete(&user)
|
||||
return api.ErrorSuccess
|
||||
}
|
Loading…
Reference in New Issue