added sqlite support
All checks were successful
Build / build (push) Successful in 29m14s

This commit is contained in:
Gustavo Maronato 2023-08-22 00:48:12 -03:00
parent 08794960bf
commit f5829aafa9
Signed by: maronato
SSH Key Fingerprint: SHA256:2Gw7kwMz/As+2UkR1qQ/qYYhn+WNh3FGv6ozhoRrLcs
12 changed files with 522 additions and 5 deletions

View File

@ -75,6 +75,12 @@ func serveAPI(ctx context.Context, cfg *config.Config) error {
// Create services
storage := shared.InitStorage(cfg)
err := storage.Start(ctx)
if err != nil {
return fmt.Errorf("failed to start storage, %w", err)
}
defer storage.Stop(ctx)
shortService := shortservice.NewShortService(storage)
userService := userservice.NewUserService(cfg, storage)
tokenService := tokenservice.NewTokenService(storage)

View File

@ -49,6 +49,12 @@ func exec(ctx context.Context, cfg *config.Config) error {
// Create services
storage := shared.InitStorage(cfg)
err := storage.Start(ctx)
if err != nil {
return fmt.Errorf("failed to start storage, %w", err)
}
defer storage.Stop(ctx)
shortService := shortservice.NewShortService(storage)
userService := userservice.NewUserService(cfg, storage)
tokenService := tokenservice.NewTokenService(storage)

View File

@ -10,6 +10,7 @@ import (
"git.maronato.dev/maronato/goshort/internal/config"
"git.maronato.dev/maronato/goshort/internal/storage"
memorystorage "git.maronato.dev/maronato/goshort/internal/storage/memory"
sqlitestorage "git.maronato.dev/maronato/goshort/internal/storage/sqlite"
"github.com/peterbourgon/ff/v3"
)
@ -60,6 +61,8 @@ func InitStorage(cfg *config.Config) storage.Storage {
switch cfg.DBType {
case config.DBTypeMemory:
return memorystorage.NewMemoryStorage()
case config.DBTypeSQLite:
return sqlitestorage.NewSQLiteStorage(cfg)
default:
panic("database type not implemented!")
}

28
go.mod
View File

@ -8,11 +8,39 @@ require (
github.com/go-chi/cors v1.2.1
github.com/go-chi/render v1.0.3
github.com/peterbourgon/ff/v3 v3.4.0
github.com/uptrace/bun v1.1.14
github.com/uptrace/bun/dialect/sqlitedialect v1.1.14
github.com/uptrace/bun/driver/sqliteshim v1.1.14
github.com/uptrace/bun/extra/bundebug v1.1.14
golang.org/x/crypto v0.12.0
golang.org/x/sync v0.3.0
)
require (
github.com/ajg/form v1.5.1 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/fatih/color v1.15.0 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/mattn/go-sqlite3 v1.14.16 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc // indirect
github.com/vmihailenco/msgpack/v5 v5.3.5 // indirect
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
golang.org/x/mod v0.10.0 // indirect
golang.org/x/sys v0.11.0 // indirect
golang.org/x/tools v0.9.1 // indirect
lukechampine.com/uint128 v1.3.0 // indirect
modernc.org/cc/v3 v3.40.0 // indirect
modernc.org/ccgo/v3 v3.16.13 // indirect
modernc.org/libc v1.24.1 // indirect
modernc.org/mathutil v1.5.0 // indirect
modernc.org/memory v1.6.0 // indirect
modernc.org/opt v0.1.3 // indirect
modernc.org/sqlite v1.25.0 // indirect
modernc.org/strutil v1.1.3 // indirect
modernc.org/token v1.1.0 // indirect
)

76
go.sum
View File

@ -2,17 +2,93 @@ github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU=
github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
github.com/alexedwards/scs/v2 v2.5.1 h1:EhAz3Kb3OSQzD8T+Ub23fKsiuvE0GzbF5Lgn0uTwM3Y=
github.com/alexedwards/scs/v2 v2.5.1/go.mod h1:ToaROZxyKukJKT/xLcVQAChi5k6+Pn1Gvmdl7h3RRj8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs=
github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw=
github.com/go-chi/chi/v5 v5.0.10 h1:rLz5avzKpjqxrYwXNfmjkrYYXOyLJd37pz53UFHC6vk=
github.com/go-chi/chi/v5 v5.0.10/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4=
github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4=
github.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
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/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/peterbourgon/ff/v3 v3.4.0 h1:QBvM/rizZM1cB0p0lGMdmR7HxZeI/ZrBWB4DqLkMUBc=
github.com/peterbourgon/ff/v3 v3.4.0/go.mod h1:zjJVUhx+twciwfDl0zBcFzl4dW8axCRyXE/eKY9RztQ=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc h1:9lRDQMhESg+zvGYmW5DyG0UqvY96Bu5QYsTLvCHdrgo=
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc/go.mod h1:bciPuU6GHm1iF1pBvUfxfsH0Wmnc2VbpgvbI9ZWuIRs=
github.com/uptrace/bun v1.1.14 h1:S5vvNnjEynJ0CvnrBOD7MIRW7q/WbtvFXrdfy0lddAM=
github.com/uptrace/bun v1.1.14/go.mod h1:RHk6DrIisO62dv10pUOJCz5MphXThuOTpVNYEYv7NI8=
github.com/uptrace/bun/dialect/sqlitedialect v1.1.14 h1:SlwXLxr+N1kEo8Q0cheRlnIZLZlWniEB1OI+jkiLgWE=
github.com/uptrace/bun/dialect/sqlitedialect v1.1.14/go.mod h1:9RTEj1l4bB9a4l1Mnc9y4COTwWlFYe1dh6fyxq1rR7A=
github.com/uptrace/bun/driver/sqliteshim v1.1.14 h1:DFPUJ6KjDP2myjq15gtYYNngmAFMww1Y2UFZv4tbUw8=
github.com/uptrace/bun/driver/sqliteshim v1.1.14/go.mod h1:5BFN7V6Sm37Tn7UE4FWNm/F6V3iJPUzAJ7QyRwA5b1k=
github.com/uptrace/bun/extra/bundebug v1.1.14 h1:9OCGfP9ZDlh41u6OLerWdhBtJAVGXHr0xtxO4xWi6t0=
github.com/uptrace/bun/extra/bundebug v1.1.14/go.mod h1:lto3guzS2v6mnQp1+akyE+ecBLOltevDDe324NXEYdw=
github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9znI5mJU=
github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc=
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk=
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk=
golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/tools v0.9.1 h1:8WMNJAz3zrtPmnYC7ISf5dEn3MT0gY7jBJfw27yrrLo=
golang.org/x/tools v0.9.1/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
lukechampine.com/uint128 v1.3.0 h1:cDdUVfRwDUDovz610ABgFD17nXD4/uDgVHl2sC3+sbo=
lukechampine.com/uint128 v1.3.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
modernc.org/cc/v3 v3.40.0 h1:P3g79IUS/93SYhtoeaHW+kRCIrYaxJ27MFPv+7kaTOw=
modernc.org/cc/v3 v3.40.0/go.mod h1:/bTg4dnWkSXowUO6ssQKnOV0yMVxDYNIsIrzqTFDGH0=
modernc.org/ccgo/v3 v3.16.13 h1:Mkgdzl46i5F/CNR/Kj80Ri59hC8TKAhZrYSaqvkwzUw=
modernc.org/ccgo/v3 v3.16.13/go.mod h1:2Quk+5YgpImhPjv2Qsob1DnZ/4som1lJTodubIcoUkY=
modernc.org/ccorpus v1.11.6 h1:J16RXiiqiCgua6+ZvQot4yUuUy8zxgqbqEEUuGPlISk=
modernc.org/httpfs v1.0.6 h1:AAgIpFZRXuYnkjftxTAZwMIiwEqAfk8aVB2/oA6nAeM=
modernc.org/libc v1.24.1 h1:uvJSeCKL/AgzBo2yYIPPTy82v21KgGnizcGYfBHaNuM=
modernc.org/libc v1.24.1/go.mod h1:FmfO1RLrU3MHJfyi9eYYmZBfi/R+tqZ6+hQ3yQQUkak=
modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=
modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
modernc.org/memory v1.6.0 h1:i6mzavxrE9a30whzMfwf7XWVODx2r5OYXvU46cirX7o=
modernc.org/memory v1.6.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
modernc.org/sqlite v1.25.0 h1:AFweiwPNd/b3BoKnBOfFm+Y260guGMF+0UFk0savqeA=
modernc.org/sqlite v1.25.0/go.mod h1:FL3pVXie73rg3Rii6V/u5BoHlSoyeZeIgKZEgHARyCU=
modernc.org/strutil v1.1.3 h1:fNMm+oJklMGYfU9Ylcywl0CO5O6nTfaowNsh2wpPjzY=
modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw=
modernc.org/tcl v1.15.2 h1:C4ybAYCGJw968e+Me18oW55kD/FexcHbqH2xak1ROSY=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
modernc.org/z v1.7.3 h1:zDJf6iHjrnB+WRD88stbXokugjyc0/pB91ri1gO6LZY=

View File

@ -32,7 +32,6 @@ func NewServer(cfg *config.Config) *Server {
mux.Use(middleware.Recoverer)
mux.Use(servermiddleware.SessionManager(cfg))
mux.Use(middleware.Timeout(config.RequestTimeout))
mux.Mount("/debug", middleware.Profiler())
// Create the server
srv := &http.Server{

View File

@ -90,7 +90,7 @@ func (s *TokenService) CreateToken(ctx context.Context, user *models.User) (*mod
token := &models.Token{
ID: id,
Name: fmt.Sprintf("%s's token #%s", user.Username, id[:5]),
Value: TokenPrefix + tokenutil.GenerateSecureToken(TokenLength),
Value: TokenPrefix + tokenutil.GenerateSecureToken(TokenLength/2),
User: user,
}

View File

@ -24,7 +24,7 @@ type MemoryStorage struct {
}
// NewMemoryStorage creates a new MemoryStorage.
func NewMemoryStorage() *MemoryStorage {
func NewMemoryStorage() storage.Storage {
return &MemoryStorage{
shortMap: make(map[string]*models.Short),
userMap: make(map[string]*models.User),
@ -33,6 +33,16 @@ func NewMemoryStorage() *MemoryStorage {
}
}
// Start starts the storage.
func (s *MemoryStorage) Start(ctx context.Context) error {
return nil
}
// Stop stops the storage.
func (s *MemoryStorage) Stop(ctx context.Context) error {
return nil
}
// FindShort finds a short in the storage.
func (s *MemoryStorage) FindShort(ctx context.Context, name string) (*models.Short, error) {
s.shortMu.RLock()

View File

@ -28,8 +28,6 @@ func (u *User) SetPassword(hasher passwords.PasswordHasher, pass string) error {
return fmt.Errorf("failed to hash password: %w", err)
}
fmt.Println(hash)
// Set the hashed password
u.password = hash

View File

@ -0,0 +1,84 @@
package sqlitestorage
import (
"time"
"git.maronato.dev/maronato/goshort/internal/storage/models"
"github.com/uptrace/bun"
)
type ShortModel struct {
bun.BaseModel `bun:"table:shorts,alias:s" json:"-"`
ID int64 `bun:",pk,autoincrement" json:"id"`
// Name is the short's name, which is also the path to access it
Name string `bun:",unique,notnull" json:"name"`
// URL is the URL that the short will redirect to
URL string `bun:",notnull" json:"url"`
// Active is whether the short is active or not
Active bool `bun:",notnull,default:true" json:"-"`
// UserID is the ID of the user that created the short
// This can be null if the short was deleted
UserID *int64 `json:"-"`
// User is the user that created the short
User *UserModel `bun:"rel:belongs-to,join:user_id=id" json:"user,omitempty"`
}
func (s *ShortModel) toShort() *models.Short {
return &models.Short{
Name: s.Name,
URL: s.URL,
User: s.User.toUser(),
}
}
type UserModel struct {
bun.BaseModel `bun:"table:users,alias:u"`
// ID is the primary key
ID int64 `bun:",pk,autoincrement" json:"id"`
// Username is the user's username
Username string `bun:",unique,notnull" json:"username"`
// Password is the user's password
Password string `bun:",notnull" json:"-"`
// Shorts is the list of shorts created by the user
Shorts []*ShortModel `bun:"rel:has-many,join:id=user_id" json:"shorts,omitempty"`
// Tokens is the list of access tokens created by the user
Tokens []*TokenModel `bun:"rel:has-many,join:id=user_id" json:"tokens,omitempty"`
}
func (u *UserModel) toUser() *models.User {
return models.NewAuthenticatableUser(&models.User{
Username: u.Username,
}, u.Password)
}
type TokenModel struct {
bun.BaseModel `bun:"table:tokens,alias:t"`
// ID is the primary key
ID string `bun:",pk" json:"id"`
// Name is the user-friendly name of the token
Name string `bun:",notnull" json:"name"`
// Value is the actual token
Value string `bun:",unique,notnull" json:"value"`
// UserID is the ID of the user that created the token
UserID *int64 `bun:",notnull" json:"-"`
// User is the user that created the token
User *UserModel `bun:"rel:belongs-to,join:user_id=id" json:"user,omitempty"`
// CreatedAt is when the token was created (initialized by the storage)
CreatedAt time.Time `bun:",notnull,default:current_timestamp" json:"createdAt"`
}
func (t *TokenModel) toToken() *models.Token {
return &models.Token{
ID: t.ID,
Name: t.Name,
Value: t.Value,
CreatedAt: t.CreatedAt,
User: t.User.toUser(),
}
}

View File

@ -0,0 +1,304 @@
package sqlitestorage
import (
"context"
"database/sql"
"fmt"
"git.maronato.dev/maronato/goshort/internal/config"
"git.maronato.dev/maronato/goshort/internal/errs"
"git.maronato.dev/maronato/goshort/internal/storage"
"git.maronato.dev/maronato/goshort/internal/storage/models"
"github.com/uptrace/bun"
"github.com/uptrace/bun/dialect/sqlitedialect"
"github.com/uptrace/bun/driver/sqliteshim"
"github.com/uptrace/bun/extra/bundebug"
)
type SQLiteStorage struct {
storage.Storage
db *bun.DB
}
func NewSQLiteStorage(cfg *config.Config) storage.Storage {
sqldb, err := sql.Open(sqliteshim.ShimName, cfg.DBURL)
if err != nil {
panic(err)
}
db := bun.NewDB(sqldb, sqlitedialect.New())
if cfg.Debug {
db.AddQueryHook(bundebug.NewQueryHook(bundebug.WithVerbose(true)))
}
return &SQLiteStorage{
db: db,
}
}
func (s *SQLiteStorage) Start(ctx context.Context) error {
_, err := s.db.
NewCreateTable().
IfNotExists().
Model((*UserModel)(nil)).
WithForeignKeys().
Exec(ctx)
if err != nil {
return fmt.Errorf("failed to create users table: %w", err)
}
_, err = s.db.
NewCreateTable().
IfNotExists().
Model((*ShortModel)(nil)).
ForeignKey(`("user_id") REFERENCES "users" ("id") ON DELETE CASCADE`).
Exec(ctx)
if err != nil {
return fmt.Errorf("failed to create shorts table: %w", err)
}
_, err = s.db.
NewCreateTable().
IfNotExists().
Model((*TokenModel)(nil)).
ForeignKey(`("user_id") REFERENCES "users" ("id") ON DELETE CASCADE`).
Exec(ctx)
if err != nil {
return fmt.Errorf("failed to create tokens table: %w", err)
}
// shorts user_id index
_, err = s.db.NewCreateIndex().
IfNotExists().
Model((*ShortModel)(nil)).
Index("idx_shorts_user_id").
Column("user_id").
Exec(ctx)
if err != nil {
return fmt.Errorf("failed to create shorts user_id index: %w", err)
}
// tokens user_id index
_, err = s.db.NewCreateIndex().
IfNotExists().
Model((*TokenModel)(nil)).
Index("idx_tokens_user_id").
Column("user_id").
Exec(ctx)
if err != nil {
return fmt.Errorf("failed to create tokens user_id index: %w", err)
}
return nil
}
func (s *SQLiteStorage) Stop(ctx context.Context) error {
return s.db.Close()
}
func (s *SQLiteStorage) FindShort(ctx context.Context, name string) (*models.Short, error) {
short := new(ShortModel)
err := s.db.NewSelect().Model(short).Where("name = ?", name).Relation("User").Scan(ctx)
if err != nil {
if err == sql.ErrNoRows {
return nil, errs.ErrShortDoesNotExist
}
return nil, fmt.Errorf("failed to find short: %w", err)
}
return short.toShort(), nil
}
func (s *SQLiteStorage) CreateShort(ctx context.Context, short *models.Short) (*models.Short, error) {
// Get user from username
user, err := s.findUser(ctx, short.User.Username)
if err != nil {
return nil, err
}
shortModel := ShortModel{
Name: short.Name,
URL: short.URL,
UserID: &user.ID,
User: user,
}
_, err = s.db.NewInsert().Model(&shortModel).Exec(ctx)
if err != nil {
return nil, errs.ErrShortExists
}
return shortModel.toShort(), nil
}
func (s *SQLiteStorage) DeleteShort(ctx context.Context, short *models.Short) error {
_, err := s.db.NewDelete().Model(short).Where("name = ?", short.Name).Exec(ctx)
if err != nil {
return fmt.Errorf("failed to delete short: %w", err)
}
return nil
}
func (s *SQLiteStorage) ListShorts(ctx context.Context, user *models.User) ([]*models.Short, error) {
// Get user ID from username
userID, err := s.findUserIDFromUsername(ctx, user.Username)
if err != nil {
return nil, err
}
shortModels := []*ShortModel{}
err = s.db.NewSelect().Model(&shortModels).Where("user_id = ?", userID).Relation("User").Scan(ctx)
if err != nil {
return nil, fmt.Errorf("failed to list shorts: %w", err)
}
shorts := []*models.Short{}
for _, shortModel := range shortModels {
shorts = append(shorts, shortModel.toShort())
}
return shorts, nil
}
func (s *SQLiteStorage) findUser(ctx context.Context, username string) (*UserModel, error) {
user := new(UserModel)
err := s.db.NewSelect().Model(user).Where("username = ?", username).Scan(ctx)
if err != nil {
if err == sql.ErrNoRows {
return nil, errs.ErrUserDoesNotExist
}
return nil, fmt.Errorf("failed to find user: %w", err)
}
return user, nil
}
func (s *SQLiteStorage) FindUser(ctx context.Context, username string) (*models.User, error) {
user, err := s.findUser(ctx, username)
if err != nil {
return nil, err
}
return user.toUser(), nil
}
func (s *SQLiteStorage) CreateUser(ctx context.Context, user *models.User) (*models.User, error) {
userModel := &UserModel{
Username: user.Username,
Password: user.GetPasswordHash(),
}
_, err := s.db.NewInsert().Model(userModel).Exec(ctx)
if err != nil {
return nil, errs.ErrUserExists
}
return userModel.toUser(), nil
}
func (s *SQLiteStorage) DeleteUser(ctx context.Context, user *models.User) error {
_, err := s.db.NewDelete().Model(user).Where("username = ?", user.Username).Exec(ctx)
if err != nil {
return fmt.Errorf("failed to delete user: %w", err)
}
return nil
}
func (s *SQLiteStorage) FindToken(ctx context.Context, value string) (*models.Token, error) {
token := new(TokenModel)
err := s.db.NewSelect().Model(token).Where("value = ?", value).Relation("User").Scan(ctx)
if err != nil {
if err == sql.ErrNoRows {
return nil, errs.ErrTokenDoesNotExist
}
return nil, fmt.Errorf("failed to find token: %w", err)
}
return token.toToken(), nil
}
func (s *SQLiteStorage) FindTokenByID(ctx context.Context, id string) (*models.Token, error) {
token := new(TokenModel)
err := s.db.NewSelect().Model(token).Where("t.id = ?", id).Relation("User").Scan(ctx)
if err != nil {
if err == sql.ErrNoRows {
return nil, errs.ErrTokenDoesNotExist
}
return nil, fmt.Errorf("failed to find token: %w", err)
}
return token.toToken(), nil
}
func (s *SQLiteStorage) ListTokens(ctx context.Context, user *models.User) ([]*models.Token, error) {
// Get user ID from username
userID, err := s.findUserIDFromUsername(ctx, user.Username)
if err != nil {
return nil, err
}
tokenModels := []*TokenModel{}
err = s.db.NewSelect().Model(&tokenModels).Where("user_id = ?", userID).Relation("User").Scan(ctx)
if err != nil {
return nil, fmt.Errorf("failed to list tokens: %w", err)
}
tokens := []*models.Token{}
for _, tokenModel := range tokenModels {
tokens = append(tokens, tokenModel.toToken())
}
return tokens, nil
}
func (s *SQLiteStorage) CreateToken(ctx context.Context, token *models.Token) (*models.Token, error) {
// Get user ID from username
user, err := s.findUser(ctx, token.User.Username)
if err != nil {
return nil, err
}
tokenModel := &TokenModel{
ID: token.ID,
Name: token.Name,
Value: token.Value,
UserID: &user.ID,
User: user,
}
_, err = s.db.NewInsert().Model(tokenModel).Exec(ctx)
if err != nil {
return nil, errs.ErrTokenExists
}
return tokenModel.toToken(), nil
}
func (s *SQLiteStorage) DeleteToken(ctx context.Context, token *models.Token) error {
_, err := s.db.NewDelete().Model(token).Where("id = ?", token.ID).Exec(ctx)
if err != nil {
return fmt.Errorf("failed to delete token: %w", err)
}
return nil
}
func (s *SQLiteStorage) findUserIDFromUsername(ctx context.Context, username string) (*int64, error) {
var userID int64
err := s.db.NewSelect().
Table("users").
Column("id").
Where("username = ?", username).
Scan(ctx, &userID)
if err != nil {
if err == sql.ErrNoRows {
return nil, errs.ErrUserDoesNotExist
}
return nil, fmt.Errorf("failed to get user ID: %w", err)
}
return &userID, nil
}

View File

@ -7,6 +7,9 @@ import (
)
type Storage interface {
// Lifecycle
Start(ctx context.Context) error
Stop(ctx context.Context) error
// Short Storage
// FindShort finds a short in the storage using its name.