transactions everywhere

This commit is contained in:
Gustavo Maronato 2023-08-22 14:13:31 -03:00
parent a4b2fcdb7c
commit e67f1ff7cc
Signed by: maronato
SSH Key Fingerprint: SHA256:2Gw7kwMz/As+2UkR1qQ/qYYhn+WNh3FGv6ozhoRrLcs
5 changed files with 341 additions and 162 deletions

View File

@ -65,6 +65,8 @@ func (s *MemoryStorage) CreateShort(ctx context.Context, short *models.Short) (*
return &models.Short{}, errs.ErrShortExists
}
short.CreatedAt = time.Now()
s.shortMap[short.Name] = short
return short, nil
@ -120,6 +122,8 @@ func (s *MemoryStorage) CreateUser(ctx context.Context, user *models.User) (*mod
return &models.User{}, errs.ErrUserExists
}
user.CreatedAt = time.Now()
s.userMap[user.Username] = user
return user, nil

View File

@ -1,10 +1,14 @@
package models
import "time"
type Short struct {
// Name is the shortened name of the URL.
Name string `json:"name,omitempty"`
// URL is the URL that is shortened.
URL string `json:"url"`
// CreatedAt is the time the short was created.
CreatedAt time.Time `json:"created_at,omitempty"`
// User is the user that created the short.
User *User `json:"-"`

View File

@ -2,6 +2,7 @@ package models
import (
"fmt"
"time"
"git.maronato.dev/maronato/goshort/internal/util/passwords"
)
@ -9,6 +10,9 @@ import (
type User struct {
Username string `json:"username"`
password string `json:"-"`
// CreatedAt is the time the user was created.
CreatedAt time.Time `json:"created_at,omitempty"`
}
// NewAuthenticatableUser is a elper function for storages that takes
@ -16,8 +20,9 @@ type User struct {
func NewAuthenticatableUser(user *User, hashedPass string) *User {
return &User{
Username: user.Username,
password: hashedPass,
Username: user.Username,
CreatedAt: user.CreatedAt,
password: hashedPass,
}
}

View File

@ -15,8 +15,12 @@ type ShortModel struct {
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:"-"`
// Deleted is whether the short is soft-deleted or not
Deleted bool `bun:",notnull,default:false" json:"-"`
// CreatedAt is when the short was created (initialized by the storage)
CreatedAt time.Time `bun:",notnull,default:current_timestamp" json:"createdAt"`
// DeletedAt is when the short was deleted
DeletedAt time.Time `bun:",null" json:"-"`
// UserID is the ID of the user that created the short
// This can be null if the short was deleted
@ -27,9 +31,10 @@ type ShortModel struct {
func (s *ShortModel) toShort() *models.Short {
return &models.Short{
Name: s.Name,
URL: s.URL,
User: s.User.toUser(),
Name: s.Name,
URL: s.URL,
CreatedAt: s.CreatedAt,
User: s.User.toUser(),
}
}
@ -42,6 +47,8 @@ type UserModel struct {
Username string `bun:",unique,notnull" json:"username"`
// Password is the user's password
Password string `bun:",notnull" json:"-"`
// CreatedAt is when the user was created (initialized by the storage)
CreatedAt time.Time `bun:",notnull,default:current_timestamp" json:"createdAt"`
// Shorts is the list of shorts created by the user
Shorts []*ShortModel `bun:"rel:has-many,join:id=user_id" json:"shorts,omitempty"`
@ -51,7 +58,8 @@ type UserModel struct {
func (u *UserModel) toUser() *models.User {
return models.NewAuthenticatableUser(&models.User{
Username: u.Username,
Username: u.Username,
CreatedAt: u.CreatedAt,
}, u.Password)
}

View File

@ -4,6 +4,7 @@ import (
"context"
"database/sql"
"fmt"
"time"
"git.maronato.dev/maronato/goshort/internal/config"
"git.maronato.dev/maronato/goshort/internal/errs"
@ -39,57 +40,60 @@ func NewSQLiteStorage(cfg *config.Config) storage.Storage {
}
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)
}
return s.db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error {
// 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)
}
_, err := tx.NewCreateTable().
IfNotExists().
Model((*UserModel)(nil)).
WithForeignKeys().
Exec(ctx)
if err != nil {
return fmt.Errorf("failed to create users table: %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)
}
_, err = tx.NewCreateTable().
IfNotExists().
Model((*ShortModel)(nil)).
ForeignKey(`("user_id") REFERENCES "users" ("id") ON DELETE SET NULL`).
Exec(ctx)
if err != nil {
return fmt.Errorf("failed to create shorts table: %w", err)
}
return nil
_, err = tx.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 = tx.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 = tx.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 {
@ -97,85 +101,124 @@ func (s *SQLiteStorage) Stop(ctx context.Context) error {
}
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
shortModel := new(ShortModel)
err := s.db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error {
err := tx.NewSelect().
Model(shortModel).
Where("name = ? and deleted = false", name).
Relation("User").
Scan(ctx)
if err != nil {
if err == sql.ErrNoRows {
return errs.ErrShortDoesNotExist
}
return fmt.Errorf("failed to find short: %w", err)
}
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)
return nil
})
if err != nil {
return nil, err
}
shortModel := ShortModel{
Name: short.Name,
URL: short.URL,
UserID: &user.ID,
User: user,
}
return shortModel.toShort(), err
}
func (s *SQLiteStorage) CreateShort(ctx context.Context, short *models.Short) (*models.Short, error) {
shortModel := new(ShortModel)
err := s.db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error {
// Get user from username
user, err := findUser(ctx, tx, short.User.Username)
if err != nil {
return err
}
shortModel.Name = short.Name
shortModel.URL = short.URL
shortModel.UserID = &user.ID
shortModel.User = user
_, err = tx.NewInsert().
Model(shortModel).
Exec(ctx)
if err != nil {
return errs.ErrShortExists
}
return nil
})
_, err = s.db.NewInsert().Model(&shortModel).Exec(ctx)
if err != nil {
return nil, errs.ErrShortExists
return nil, err
}
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 s.db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error {
_, err := withShortDeleteUpdates(
tx.NewUpdate().
Model((*ShortModel)(nil)).
Where("name = ?", short.Name),
).Exec(ctx)
if err != nil {
return fmt.Errorf("failed to delete short: %w", err)
}
return nil
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)
shortModels := []*ShortModel{}
err := s.db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error {
// Get user ID from username
userID, err := findUserIDFromUsername(ctx, tx, user.Username)
if err != nil {
return err
}
err = tx.NewSelect().
Model(&shortModels).
Where("user_id = ? and deleted = false", userID).
Relation("User").
Scan(ctx)
if err != nil {
return fmt.Errorf("failed to list shorts: %w", err)
}
return nil
})
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)
var user *UserModel
err := s.db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error {
userFound, err := findUser(ctx, tx, username)
if err != nil {
return err
}
user = userFound
return nil
})
if err != nil {
return nil, err
}
@ -189,60 +232,117 @@ func (s *SQLiteStorage) CreateUser(ctx context.Context, user *models.User) (*mod
Password: user.GetPasswordHash(),
}
_, err := s.db.NewInsert().Model(userModel).Exec(ctx)
err := s.db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error {
_, err := tx.NewInsert().
Model(userModel).
Exec(ctx)
if err != nil {
return errs.ErrUserExists
}
return nil
})
if err != nil {
return nil, errs.ErrUserExists
return nil, err
}
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 s.db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error {
// Delete user shorts
err := deleteUserShorts(ctx, tx, user)
if err != nil {
return fmt.Errorf("failed to delete user: %w", err)
}
_, err = tx.NewDelete().
Model(user).
Where("username = ?", user.Username).
Exec(ctx)
if err != nil {
return fmt.Errorf("failed to delete user: %w", err)
}
return nil
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
tokenModel := new(TokenModel)
err := s.db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error {
err := tx.NewSelect().
Model(tokenModel).
Where("value = ?", value).
Relation("User").
Scan(ctx)
if err != nil {
if err == sql.ErrNoRows {
return errs.ErrTokenDoesNotExist
}
return fmt.Errorf("failed to find token: %w", err)
}
return nil, fmt.Errorf("failed to find token: %w", err)
}
return token.toToken(), nil
}
return 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)
return tokenModel.toToken(), nil
}
func (s *SQLiteStorage) FindTokenByID(ctx context.Context, id string) (*models.Token, error) {
tokenModel := new(TokenModel)
err := s.db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error {
err := tx.NewSelect().
Model(tokenModel).
Where("t.id = ?", id).
Relation("User").
Scan(ctx)
if err != nil {
if err == sql.ErrNoRows {
return errs.ErrTokenDoesNotExist
}
return fmt.Errorf("failed to find token: %w", err)
}
return nil
})
if err != nil {
return nil, fmt.Errorf("failed to list tokens: %w", err)
return nil, err
}
return tokenModel.toToken(), nil
}
func (s *SQLiteStorage) ListTokens(ctx context.Context, user *models.User) ([]*models.Token, error) {
tokenModels := []*TokenModel{}
err := s.db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error {
// Get user ID from username
userID, err := findUserIDFromUsername(ctx, tx, user.Username)
if err != nil {
return err
}
err = tx.NewSelect().
Model(&tokenModels).
Where("user_id = ?", userID).
Relation("User").
Scan(ctx)
if err != nil {
return fmt.Errorf("failed to list tokens: %w", err)
}
return nil
})
if err != nil {
return nil, err
}
tokens := []*models.Token{}
@ -254,40 +354,74 @@ func (s *SQLiteStorage) ListTokens(ctx context.Context, user *models.User) ([]*m
}
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)
tokenModel := new(TokenModel)
err := s.db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error {
// Get user ID from username
user, err := findUser(ctx, tx, token.User.Username)
if err != nil {
return err
}
tokenModel.ID = token.ID
tokenModel.Name = token.Name
tokenModel.Value = token.Value
tokenModel.UserID = &user.ID
tokenModel.User = user
_, err = tx.NewInsert().
Model(tokenModel).
Exec(ctx)
if err != nil {
return errs.ErrTokenExists
}
return nil
})
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 s.db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error {
// Delete in transaction
_, err := tx.NewDelete().
Model(token).
Where("id = ?", token.ID).
Exec(ctx)
if err != nil {
return fmt.Errorf("failed to delete token: %w", err)
}
return nil
return nil
})
}
func (s *SQLiteStorage) findUserIDFromUsername(ctx context.Context, username string) (*int64, error) {
func findUser(ctx context.Context, db bun.IDB, username string) (*UserModel, error) {
userModel := new(UserModel)
err := db.NewSelect().
Model(userModel).
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 userModel, nil
}
func findUserIDFromUsername(ctx context.Context, db bun.IDB, username string) (*int64, error) {
var userID int64
err := s.db.NewSelect().
err := db.NewSelect().
Table("users").
Column("id").
Where("username = ?", username).
@ -302,3 +436,27 @@ func (s *SQLiteStorage) findUserIDFromUsername(ctx context.Context, username str
return &userID, nil
}
func withShortDeleteUpdates(q *bun.UpdateQuery) *bun.UpdateQuery {
return q.Set("deleted_at = ?", time.Now()).
Set("deleted = ?", true).
Set("user_id = ?", nil)
}
func deleteUserShorts(ctx context.Context, db bun.IDB, user *models.User) error {
userID, err := findUserIDFromUsername(ctx, db, user.Username)
if err != nil {
return fmt.Errorf("failed to delete user shorts: %w", err)
}
_, err = withShortDeleteUpdates(
db.NewUpdate().
Model((*ShortModel)(nil)).
Where("user_id = ?", userID),
).Exec(ctx)
if err != nil {
return fmt.Errorf("failed to delete user shorts: %w", err)
}
return nil
}