update architecture and remove heavy dependencies
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/tag Build is passing Details

This commit is contained in:
Adrien Marquès 2020-04-05 11:04:46 +02:00
parent b92c7d0935
commit 16204b1d93
Signed by: xdrm-brackets
GPG Key ID: D75243CA236D825E
19 changed files with 817 additions and 318 deletions

5
.gitignore vendored
View File

@ -1,2 +1,5 @@
*.so
.build
.build
*.db
*.pprof
trace.out

View File

@ -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.

View File

@ -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,23 +112,24 @@
"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": {
"title": { "info": "the article title", "type": "string(5,255)", "name": "Title" },
"body": { "info": "the article body", "type": "string", "name": "Body" }
"{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" }
},
"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": "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
View File

@ -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
View File

@ -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
View File

@ -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
}

78
pkg/handlers/article.go Normal file
View File

@ -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})
}

29
pkg/handlers/handler.go Normal file
View File

@ -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
}
}

69
pkg/handlers/user.go Normal file
View File

@ -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
}

10
pkg/model/article.go Normal file
View File

@ -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"`
}

10
pkg/model/user.go Normal file
View File

@ -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"`
}

8
pkg/model/vote.go Normal file
View File

@ -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"`
}

View File

@ -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
}

View File

@ -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")

View File

@ -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)
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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"`
}

View File

@ -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
}