implement postgres repositories

This commit is contained in:
Adrien Marquès 2019-05-02 20:50:58 +02:00
parent 80f372952c
commit cde7fa4798
5 changed files with 180 additions and 51 deletions

12
main.go
View File

@ -37,15 +37,21 @@ func main() {
defer db.Close() defer db.Close()
// 4. init services // 4. init services
authService := auth.New(db) authService, err := auth.New(db)
shortenerService := shortener.New(db, authService) if err != nil {
log.Fatalf("cannot create auth service: %v", err)
}
shortenerService, err := shortener.New(db, authService)
if err != nil {
log.Fatalf("cannot create auth service: %v", err)
}
// 5. wire services // 5. wire services
shortenerService.Wire(server) shortenerService.Wire(server)
authService.Wire(server) authService.Wire(server)
// 6. listen and serve // 6. listen and serve
log.Printf("[server] listening") log.Printf("[server] listening\n\n")
err = http.ListenAndServe(listenTo, server) err = http.ListenAndServe(listenTo, server)
if err != nil { if err != nil {
log.Fatalf("/!\\ cannot listen: %v\n", err) log.Fatalf("/!\\ cannot listen: %v\n", err)

View File

@ -9,7 +9,6 @@ import (
"strings" "strings"
"time" "time"
"git.xdrm.io/example/aicra/storage"
"git.xdrm.io/go/aicra" "git.xdrm.io/go/aicra"
"git.xdrm.io/go/aicra/api" "git.xdrm.io/go/aicra/api"
) )
@ -17,19 +16,29 @@ import (
// Service manages the url shortener // Service manages the url shortener
type Service struct { type Service struct {
storage *sql.DB storage *sql.DB
repo *repository
} }
// New returns a bare service // New returns a bare service
func New(storage *sql.DB) *Service { func New(storage *sql.DB) (*Service, error) {
log.Printf("[service.auth] created") log.Printf("[auth] creating service")
return &Service{
storage: storage, log.Printf("[auth] creating repo")
repo, err := newRepo(storage)
if err != nil {
return nil, err
} }
log.Printf("[auth] repo created")
log.Printf("[auth] service created")
return &Service{
repo: repo,
}, nil
} }
// Wire to the aicra server // Wire to the aicra server
func (svc *Service) Wire(server *aicra.Server) { func (svc *Service) Wire(server *aicra.Server) {
log.Printf("[service.auth] wired") log.Printf("[auth] service wired")
server.HandleFunc("POST", "/token", svc.generateToken) server.HandleFunc("POST", "/token", svc.generateToken)
} }
@ -50,12 +59,14 @@ func (svc *Service) generateToken(req api.Request, res *api.Response) {
token := hex.EncodeToString(hasher.Sum(nil)) token := hex.EncodeToString(hasher.Sum(nil))
// 3. store token // 3. store token
if !svc.storage.Set(storage.TOKEN, token, role, 5*time.Minute) { model := svc.repo.NewModel(token, role, 5*time.Minute)
if model.Create() != nil {
res.SetError(api.ErrorFailure()) res.SetError(api.ErrorFailure())
return return
} }
// 4. return data // 4. return data
log.Printf("[auth] new token")
res.Data["token"] = token res.Data["token"] = token
res.SetError(api.ErrorSuccess()) res.SetError(api.ErrorSuccess())
@ -75,11 +86,15 @@ func (svc *Service) CheckToken(handler api.HandlerFunc) api.HandlerFunc {
// fail if invalid header // fail if invalid header
if len(headerToken) != 128 || strings.ContainsAny(headerToken, "$-_") { if len(headerToken) != 128 || strings.ContainsAny(headerToken, "$-_") {
res.SetError(api.ErrorPermission()) res.SetError(api.ErrorToken())
return return
} }
tokenRole := svc.storage.Get(storage.TOKEN, headerToken) model := svc.repo.NewModel(headerToken, "", 0)
if model.Search() != nil {
res.SetError(api.ErrorToken())
return
}
// fail if the role of the token does not match any scope // fail if the role of the token does not match any scope
for _, scope := range req.Scope { for _, scope := range req.Scope {
@ -88,7 +103,7 @@ func (svc *Service) CheckToken(handler api.HandlerFunc) api.HandlerFunc {
} }
// success // success
if scope[0] == string(tokenRole) { if scope[0] == model.role {
handler(req, res) handler(req, res)
return return
} }

70
service/auth/model.go Normal file
View File

@ -0,0 +1,70 @@
package auth
import (
"database/sql"
"time"
)
// tokenModel represents an actual tiny url entry in the database.
type tokenModel struct {
db *sql.DB
token string
role string
expires int64
}
type repository struct {
db *sql.DB
}
// newRepo returns an initialized repository.
func newRepo(db *sql.DB) (*repository, error) {
_, err := db.Exec(`CREATE TABLE if not exists token(
token varchar(128) PRIMARY KEY,
role varchar(300) NOT NULL,
expires INT NOT NULL
);`)
return &repository{db}, err
}
func (repo *repository) NewModel(token, role string, duration time.Duration) *tokenModel {
return &tokenModel{
db: repo.db,
token: token,
role: role,
expires: time.Now().Add(duration).Unix(),
}
}
func (mod *tokenModel) Search() error {
row := mod.db.QueryRow(`SELECT token, role, expires FROM token WHERE token = $1 LIMIT 1;`, mod.token)
receiver := &tokenModel{}
err := row.Scan(&receiver.token, &receiver.role, &receiver.expires)
if err != nil {
return err
}
// delete if expired
if receiver.expires < time.Now().Unix() {
receiver.db = mod.db
receiver.Delete()
return sql.ErrNoRows
}
mod.token = receiver.token
mod.role = receiver.role
mod.expires = receiver.expires
return nil
}
func (mod *tokenModel) Create() error {
_, err := mod.db.Exec(`INSERT INTO token (token, role, expires) VALUES ($1, $2, $3);`, mod.token, mod.role, mod.expires)
return err
}
func (mod *tokenModel) Delete() error {
_, err := mod.db.Exec(`DELETE FROM token WHERE token = $1;`, mod.token)
return err
}

View File

@ -2,30 +2,63 @@ package shortener
import ( import (
"database/sql" "database/sql"
"context"
"git.xdrm.io/example/aicra/storage"
) )
// tinyModel represents an actual tiny url entry in the database. // tinyModel represents an actual tiny url entry in the database.
type tinyModel BadExpr type tinyModel struct {
db *sql.DB
BadDecl tiny string
long string
}
type repository struct { type repository struct {
db *sql.DB db *sql.DB
} }
// newRepo returns an initialized repository. // newRepo returns an initialized repository.
func newRepo(db *sql.DB) (*model, error) { func newRepo(db *sql.DB) (*repository, error) {
log.Printf("[service.shortener] creating repository") _, err := db.Exec(`CREATE TABLE if not exists tiny(
tiny varchar(30) PRIMARY KEY,
long varchar(300) NOT NULL
);`)
res, err := db.Exec(ctx, `CREATE TABLE if not exist tiny( return &repository{db}, err
tiny varchar(30) PRIMARY, }
target varchar(300) NOT NULL,
)`) func (repo *repository) NewModel(tiny string, long string) *tinyModel {
return &tinyModel{
db: repo.db,
tiny: tiny,
long: long,
}
}
func (mod *tinyModel) Search() error {
row := mod.db.QueryRow(`SELECT tiny,long FROM tiny WHERE tiny = $1 LIMIT 1;`, mod.tiny)
receiver := &tinyModel{}
err := row.Scan(&receiver.tiny, &receiver.long)
if err != nil { if err != nil {
return nil, err return err
} }
return &repository{ctx, db} mod.tiny = receiver.tiny
mod.long = receiver.long
return nil
}
func (mod *tinyModel) Create() error {
_, err := mod.db.Exec(`INSERT INTO tiny(tiny,long) VALUES($1,$2);`, mod.tiny, mod.long)
return err
}
func (mod *tinyModel) Update() error {
_, err := mod.db.Exec(`UPDATE tiny SET long = $1 WHERE tiny = $2;`, mod.long, mod.tiny)
return err
}
func (mod *tinyModel) Delete() error {
_, err := mod.db.Exec(`DELETE FROM tiny WHERE tiny = $1;`, mod.tiny)
return err
} }

View File

@ -6,7 +6,6 @@ import (
"net/http" "net/http"
"git.xdrm.io/example/aicra/service/auth" "git.xdrm.io/example/aicra/service/auth"
"git.xdrm.io/example/aicra/storage"
"git.xdrm.io/go/aicra" "git.xdrm.io/go/aicra"
"git.xdrm.io/go/aicra/api" "git.xdrm.io/go/aicra/api"
) )
@ -19,28 +18,26 @@ type Service struct {
} }
// New returns a bare service // New returns a bare service
func New(storage *sql.DB, auth *auth.Service) *Service { func New(storage *sql.DB, auth *auth.Service) (*Service, error) {
log.Printf("[service.shortener] creating") log.Printf("[shortener] creating service")
service := &Service{ log.Printf("[shortener] creating repo")
storage: storage, repo, err := newRepo(storage)
authService: auth,
}
// init repo
repo, err := newRepo(db)
if err != nil { if err != nil {
log.Printf("[service.shortener] cannot create repo") return nil, err
return service
} }
log.Printf("[shortener] repo created")
log.Printf("[service.shortener] creating") log.Printf("[shortener] service created")
return service return &Service{
repo: repo,
authService: auth,
}, nil
} }
// Wire to the aicra server // Wire to the aicra server
func (svc *Service) Wire(server *aicra.Server) { func (svc *Service) Wire(server *aicra.Server) {
log.Printf("[service.shortener] wired") log.Printf("[shortener] service wired")
server.HandleFunc("GET", "/", svc.redirect) server.HandleFunc("GET", "/", svc.redirect)
server.HandleFunc("POST", "/", svc.authService.CheckToken(svc.register)) server.HandleFunc("POST", "/", svc.authService.CheckToken(svc.register))
server.HandleFunc("PUT", "/", svc.authService.CheckToken(svc.update)) server.HandleFunc("PUT", "/", svc.authService.CheckToken(svc.update))
@ -58,15 +55,15 @@ func (svc *Service) redirect(req api.Request, res *api.Response) {
} }
// 2. check in db if exists // 2. check in db if exists
longURL := svc.storage.Get(storage.DATA, tinyURL) model := svc.repo.NewModel(tinyURL, "")
if longURL == nil { if model.Search() != nil {
res.SetError(api.ErrorNoMatchFound()) res.SetError(api.ErrorNoMatchFound())
return return
} }
// 3. redirect // 3. redirect
res.Status = http.StatusPermanentRedirect res.Status = http.StatusPermanentRedirect
res.Headers.Set("Location", string(longURL)) res.Headers.Set("Location", model.long)
res.SetError(api.ErrorSuccess()) res.SetError(api.ErrorSuccess())
} }
@ -87,16 +84,19 @@ func (svc *Service) register(req api.Request, res *api.Response) {
} }
// 2. fail if already used // 2. fail if already used
if svc.storage.Get(storage.DATA, tinyURL) != nil { model := svc.repo.NewModel(tinyURL, "")
if model.Search() == nil {
res.SetError(api.ErrorAlreadyExists(), "url") res.SetError(api.ErrorAlreadyExists(), "url")
return return
} }
// 3. store association // 3. store association
if !svc.storage.Set(storage.DATA, tinyURL, longURL) { model.long = longURL
if model.Create() != nil {
res.SetError(api.ErrorFailure()) res.SetError(api.ErrorFailure())
return return
} }
log.Printf("[shortener] new %q -> %q", model.tiny, model.long)
res.SetError(api.ErrorSuccess()) res.SetError(api.ErrorSuccess())
} }
@ -117,16 +117,19 @@ func (svc *Service) update(req api.Request, res *api.Response) {
} }
// 2. fail if not already existing // 2. fail if not already existing
if svc.storage.Get(storage.DATA, tinyURL) == nil { model := svc.repo.NewModel(tinyURL, "")
if model.Search() != nil {
res.SetError(api.ErrorNoMatchFound()) res.SetError(api.ErrorNoMatchFound())
return return
} }
// 3. update association // 3. update association
if !svc.storage.Set(storage.DATA, tinyURL, longURL) { model.long = longURL
if model.Update() != nil {
res.SetError(api.ErrorFailure()) res.SetError(api.ErrorFailure())
return return
} }
log.Printf("[shortener] upd %q -> %q", model.tiny, model.long)
res.SetError(api.ErrorSuccess()) res.SetError(api.ErrorSuccess())
} }
@ -142,16 +145,18 @@ func (svc *Service) delete(req api.Request, res *api.Response) {
} }
// 2. fail if not already existing // 2. fail if not already existing
if svc.storage.Get(storage.DATA, tinyURL) == nil { model := svc.repo.NewModel(tinyURL, "")
if model.Search() != nil {
res.SetError(api.ErrorNoMatchFound()) res.SetError(api.ErrorNoMatchFound())
return return
} }
// 3. update association // 3. delete association
if !svc.storage.Del(storage.DATA, tinyURL) { if model.Delete() != nil {
res.SetError(api.ErrorFailure()) res.SetError(api.ErrorFailure())
return return
} }
log.Printf("[shortener] del %q", model.tiny)
res.SetError(api.ErrorSuccess()) res.SetError(api.ErrorSuccess())
} }