feat: 0.0.1

This commit is contained in:
FernandoJVideira committed 2026-01-08 01:15:07 +00:00
commit 961bdd81d4
146 files changed
+10444

No files matched your search

+32
View File
@@ -0,0 +1,32 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
package db
import (
"context"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgconn"
)
type DBTX interface {
Exec(context.Context, string, ...interface{}) (pgconn.CommandTag, error)
Query(context.Context, string, ...interface{}) (pgx.Rows, error)
QueryRow(context.Context, string, ...interface{}) pgx.Row
}
func New(db DBTX) *Queries {
return &Queries{db: db}
}
type Queries struct {
db DBTX
}
func (q *Queries) WithTx(tx pgx.Tx) *Queries {
return &Queries{
db: tx,
}
}
+34
View File
@@ -0,0 +1,34 @@
package db
import (
"errors"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgconn"
)
const (
ForeignKeyViolation = "23503"
UniqueViolation = "23505"
CheckViolation = "23514"
)
var ErrRecordNotFound = pgx.ErrNoRows
var ErrUniqueViolation = &pgconn.PgError{
Code: UniqueViolation,
}
var ErrForeignKeyViolation = &pgconn.PgError{
Code: ForeignKeyViolation,
}
var ErrCheckViolation = &pgconn.PgError{
Code: CheckViolation,
}
func ErrorCode(err error) string {
var pgErr *pgconn.PgError
if errors.As(err, &pgErr) {
return pgErr.Code
}
return ""
}
+24
View File
@@ -0,0 +1,24 @@
package db
import (
"context"
"fmt"
)
// execTx executes a function within a database transaction
func (store *PostgresStore) execTx(ctx context.Context, fn func(*Queries) error) error {
tx, err := store.connPool.Begin(ctx)
if err != nil {
return err
}
q := New(tx)
err = fn(q)
if err != nil {
if rbErr := tx.Rollback(ctx); rbErr != nil {
return fmt.Errorf("tx err: %v, rb err: %v", err, rbErr)
}
return err
}
return tx.Commit(ctx)
}
+30
View File
@@ -0,0 +1,30 @@
package db
import (
"context"
"log"
"os"
"testing"
"github.com/FernandoJVideira/simple_bank/internal/config"
"github.com/jackc/pgx/v5/pgxpool"
_ "github.com/jackc/pgx/v5/stdlib"
)
var testStore Store
func TestMain(m *testing.M) {
config, err := config.LoadTestConfig("../../..")
if err != nil {
log.Fatal("cannot load config:", err)
}
connPool, err := pgxpool.New(context.Background(), config.DBAddr)
if err != nil {
log.Fatal("cannot connect to db:", err)
}
testStore = NewPostgresStore(connPool)
os.Exit(m.Run())
}
+43
View File
@@ -0,0 +1,43 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
package db
import (
"time"
"github.com/google/uuid"
)
type Session struct {
ID uuid.UUID `json:"id"`
Username string `json:"username"`
RefreshToken string `json:"refresh_token"`
UserAgent string `json:"user_agent"`
ClientIp string `json:"client_ip"`
IsBlocked bool `json:"is_blocked"`
ExpiresAt time.Time `json:"expires_at"`
CreatedAt time.Time `json:"created_at"`
}
type User struct {
Username string `json:"username"`
Role string `json:"role"`
HashedPassword string `json:"hashed_password"`
FullName string `json:"full_name"`
Email string `json:"email"`
IsEmailVerified bool `json:"is_email_verified"`
PasswordChangedAt time.Time `json:"password_changed_at"`
CreatedAt time.Time `json:"created_at"`
}
type VerifyEmail struct {
ID int64 `json:"id"`
Username string `json:"username"`
Email string `json:"email"`
SecretCode string `json:"secret_code"`
IsUsed bool `json:"is_used"`
CreatedAt time.Time `json:"created_at"`
ExpiredAt time.Time `json:"expired_at"`
}
+23
View File
@@ -0,0 +1,23 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
package db
import (
"context"
"github.com/google/uuid"
)
type Querier interface {
CreateSession(ctx context.Context, arg CreateSessionParams) (Session, error)
CreateUser(ctx context.Context, arg CreateUserParams) (User, error)
CreateVerifyEmail(ctx context.Context, arg CreateVerifyEmailParams) (VerifyEmail, error)
GetSession(ctx context.Context, id uuid.UUID) (Session, error)
GetUser(ctx context.Context, username string) (User, error)
UpdateUser(ctx context.Context, arg UpdateUserParams) (User, error)
UpdateVerifyEmail(ctx context.Context, arg UpdateVerifyEmailParams) (VerifyEmail, error)
}
var _ Querier = (*Queries)(nil)
+83
View File
@@ -0,0 +1,83 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
// source: sessions.sql
package db
import (
"context"
"time"
"github.com/google/uuid"
)
const createSession = `-- name: CreateSession :one
INSERT INTO sessions (
id,
username,
refresh_token,
user_agent,
client_ip,
is_blocked,
expires_at
) VALUES (
$1, $2, $3, $4, $5, $6, $7
)
RETURNING id, username, refresh_token, user_agent, client_ip, is_blocked, expires_at, created_at
`
type CreateSessionParams struct {
ID uuid.UUID `json:"id"`
Username string `json:"username"`
RefreshToken string `json:"refresh_token"`
UserAgent string `json:"user_agent"`
ClientIp string `json:"client_ip"`
IsBlocked bool `json:"is_blocked"`
ExpiresAt time.Time `json:"expires_at"`
}
func (q *Queries) CreateSession(ctx context.Context, arg CreateSessionParams) (Session, error) {
row := q.db.QueryRow(ctx, createSession,
arg.ID,
arg.Username,
arg.RefreshToken,
arg.UserAgent,
arg.ClientIp,
arg.IsBlocked,
arg.ExpiresAt,
)
var i Session
err := row.Scan(
&i.ID,
&i.Username,
&i.RefreshToken,
&i.UserAgent,
&i.ClientIp,
&i.IsBlocked,
&i.ExpiresAt,
&i.CreatedAt,
)
return i, err
}
const getSession = `-- name: GetSession :one
SELECT id, username, refresh_token, user_agent, client_ip, is_blocked, expires_at, created_at FROM sessions
WHERE id = $1 LIMIT 1
`
func (q *Queries) GetSession(ctx context.Context, id uuid.UUID) (Session, error) {
row := q.db.QueryRow(ctx, getSession, id)
var i Session
err := row.Scan(
&i.ID,
&i.Username,
&i.RefreshToken,
&i.UserAgent,
&i.ClientIp,
&i.IsBlocked,
&i.ExpiresAt,
&i.CreatedAt,
)
return i, err
}
+27
View File
@@ -0,0 +1,27 @@
package db
import (
"context"
"github.com/jackc/pgx/v5/pgxpool"
)
type Store interface {
Querier
CreateUserTx(ctx context.Context, arg CreateUserTxParams) (CreateUserTxResult, error)
VerifyEmailTx(ctx context.Context, arg VerifyEmailTxParams) (VerifyEmailTxResult, error)
}
// PostgresStore provides all functions to execute db queries and transactions
type PostgresStore struct {
*Queries
connPool *pgxpool.Pool
}
// NewPostgresStore creates a new Store
func NewPostgresStore(connPool *pgxpool.Pool) Store {
return &PostgresStore{
connPool: connPool,
Queries: New(connPool),
}
}
+34
View File
@@ -0,0 +1,34 @@
package db
import (
"context"
"github.com/rs/zerolog/log"
)
type CreateUserTxParams struct {
CreateUserParams
AfterCreate func(user User) error
}
type CreateUserTxResult struct {
User User `json:"user"`
}
func (store *PostgresStore) CreateUserTx(ctx context.Context, arg CreateUserTxParams) (CreateUserTxResult, error) {
var result CreateUserTxResult
err := store.execTx(ctx, func(q *Queries) error {
var err error
result.User, err = q.CreateUser(ctx, arg.CreateUserParams)
if err != nil {
return err
}
log.Info().Msgf("user %s created successfully", result.User.Username)
return arg.AfterCreate(result.User)
})
return result, err
}
+44
View File
@@ -0,0 +1,44 @@
package db
import (
"context"
"github.com/jackc/pgx/v5/pgtype"
)
type VerifyEmailTxParams struct {
EmailID int64
SecretCode string
}
type VerifyEmailTxResult struct {
User User
VerifyEmail VerifyEmail
}
func (store *PostgresStore) VerifyEmailTx(ctx context.Context, arg VerifyEmailTxParams) (VerifyEmailTxResult, error) {
var result VerifyEmailTxResult
err := store.execTx(ctx, func(q *Queries) error {
var err error
result.VerifyEmail, err = q.UpdateVerifyEmail(ctx, UpdateVerifyEmailParams{
ID: arg.EmailID,
SecretCode: arg.SecretCode,
})
if err != nil {
return err
}
result.User, err = q.UpdateUser(ctx, UpdateUserParams{
Username: result.VerifyEmail.Username,
IsEmailVerified: pgtype.Bool{
Bool: true,
Valid: true,
},
})
return err
})
return result, err
}
+117
View File
@@ -0,0 +1,117 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
// source: user.sql
package db
import (
"context"
"github.com/jackc/pgx/v5/pgtype"
)
const createUser = `-- name: CreateUser :one
INSERT INTO users (
username,
hashed_password,
full_name,
email
) VALUES (
$1, $2, $3, $4
)
RETURNING username, role, hashed_password, full_name, email, is_email_verified, password_changed_at, created_at
`
type CreateUserParams struct {
Username string `json:"username"`
HashedPassword string `json:"hashed_password"`
FullName string `json:"full_name"`
Email string `json:"email"`
}
func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (User, error) {
row := q.db.QueryRow(ctx, createUser,
arg.Username,
arg.HashedPassword,
arg.FullName,
arg.Email,
)
var i User
err := row.Scan(
&i.Username,
&i.Role,
&i.HashedPassword,
&i.FullName,
&i.Email,
&i.IsEmailVerified,
&i.PasswordChangedAt,
&i.CreatedAt,
)
return i, err
}
const getUser = `-- name: GetUser :one
SELECT username, role, hashed_password, full_name, email, is_email_verified, password_changed_at, created_at FROM users
WHERE username = $1 LIMIT 1
`
func (q *Queries) GetUser(ctx context.Context, username string) (User, error) {
row := q.db.QueryRow(ctx, getUser, username)
var i User
err := row.Scan(
&i.Username,
&i.Role,
&i.HashedPassword,
&i.FullName,
&i.Email,
&i.IsEmailVerified,
&i.PasswordChangedAt,
&i.CreatedAt,
)
return i, err
}
const updateUser = `-- name: UpdateUser :one
UPDATE users
SET
hashed_password = COALESCE($1, hashed_password),
password_changed_at = COALESCE($2, password_changed_at),
full_name = COALESCE($3, full_name),
email = COALESCE($4, email),
is_email_verified = COALESCE($5, is_email_verified)
WHERE username = $6
RETURNING username, role, hashed_password, full_name, email, is_email_verified, password_changed_at, created_at
`
type UpdateUserParams struct {
HashedPassword pgtype.Text `json:"hashed_password"`
PasswordChangedAt pgtype.Timestamptz `json:"password_changed_at"`
FullName pgtype.Text `json:"full_name"`
Email pgtype.Text `json:"email"`
IsEmailVerified pgtype.Bool `json:"is_email_verified"`
Username string `json:"username"`
}
func (q *Queries) UpdateUser(ctx context.Context, arg UpdateUserParams) (User, error) {
row := q.db.QueryRow(ctx, updateUser,
arg.HashedPassword,
arg.PasswordChangedAt,
arg.FullName,
arg.Email,
arg.IsEmailVerified,
arg.Username,
)
var i User
err := row.Scan(
&i.Username,
&i.Role,
&i.HashedPassword,
&i.FullName,
&i.Email,
&i.IsEmailVerified,
&i.PasswordChangedAt,
&i.CreatedAt,
)
return i, err
}
+146
View File
@@ -0,0 +1,146 @@
package db
import (
"context"
"testing"
"time"
"github.com/FernandoJVideira/simple_bank/internal/security"
"github.com/FernandoJVideira/simple_bank/pkg/testutil"
"github.com/jackc/pgx/v5/pgtype"
"github.com/stretchr/testify/require"
)
func createRandomUser(t *testing.T) User {
hashedPassword, err := security.HashPassword(testutil.RandomString(6))
require.NoError(t, err)
// Create the user
arg := CreateUserParams{
Username: testutil.RandomOwner(),
HashedPassword: hashedPassword,
FullName: testutil.RandomOwner(),
Email: testutil.RandomEmail(),
}
user, err := testStore.CreateUser(context.Background(), arg)
require.NoError(t, err) // -> Err should be nil
require.NotEmpty(t, user)
require.Equal(t, arg.Username, user.Username)
require.Equal(t, arg.HashedPassword, user.HashedPassword)
require.Equal(t, arg.FullName, user.FullName)
require.Equal(t, arg.Email, user.Email)
require.True(t, user.PasswordChangedAt.IsZero())
require.NotZero(t, user.CreatedAt)
return user
}
func TestCreateUser(t *testing.T) {
createRandomUser(t)
}
func TestGetUser(t *testing.T) {
user1 := createRandomUser(t)
user2, err := testStore.GetUser(context.Background(), user1.Username)
require.NoError(t, err)
require.NotEmpty(t, user2)
require.Equal(t, user1.Username, user2.Username)
require.Equal(t, user1.HashedPassword, user2.HashedPassword)
require.Equal(t, user1.FullName, user2.FullName)
require.Equal(t, user1.Email, user2.Email)
require.WithinDuration(t, user1.CreatedAt, user2.CreatedAt, time.Second)
}
func TestUpdateUserOnlyFullName(t *testing.T) {
oldUser := createRandomUser(t)
newFullName := testutil.RandomOwner()
updatedUser, err := testStore.UpdateUser(context.Background(), UpdateUserParams{
Username: oldUser.Username,
FullName: pgtype.Text{
String: newFullName,
Valid: true,
},
})
require.NoError(t, err)
require.NotEqual(t, oldUser.FullName, updatedUser.FullName)
require.Equal(t, newFullName, updatedUser.FullName)
require.Equal(t, oldUser.HashedPassword, updatedUser.HashedPassword)
require.Equal(t, oldUser.Email, updatedUser.Email)
}
func TestUpdateUserOnlyEmail(t *testing.T) {
oldUser := createRandomUser(t)
newEmail := testutil.RandomEmail()
updatedUser, err := testStore.UpdateUser(context.Background(), UpdateUserParams{
Username: oldUser.Username,
Email: pgtype.Text{
String: newEmail,
Valid: true,
},
})
require.NoError(t, err)
require.NotEqual(t, oldUser.Email, updatedUser.Email)
require.Equal(t, newEmail, updatedUser.Email)
require.Equal(t, oldUser.HashedPassword, updatedUser.HashedPassword)
require.Equal(t, oldUser.FullName, updatedUser.FullName)
}
func TestUpdateUserOnlyPassword(t *testing.T) {
oldUser := createRandomUser(t)
newPassword, err := security.HashPassword(testutil.RandomString(6))
require.NoError(t, err)
updatedUser, err := testStore.UpdateUser(context.Background(), UpdateUserParams{
Username: oldUser.Username,
HashedPassword: pgtype.Text{
String: newPassword,
Valid: true,
},
})
require.NoError(t, err)
require.NotEqual(t, oldUser.HashedPassword, updatedUser.HashedPassword)
require.Equal(t, newPassword, updatedUser.HashedPassword)
require.Equal(t, oldUser.FullName, updatedUser.FullName)
require.Equal(t, oldUser.Email, updatedUser.Email)
}
func TestUpdateUserAllFields(t *testing.T) {
oldUser := createRandomUser(t)
newFullName := testutil.RandomOwner()
newEmail := testutil.RandomEmail()
newPassword, err := security.HashPassword(testutil.RandomString(6))
require.NoError(t, err)
updatedUser, err := testStore.UpdateUser(context.Background(), UpdateUserParams{
Username: oldUser.Username,
FullName: pgtype.Text{
String: newFullName,
Valid: true,
},
Email: pgtype.Text{
String: newEmail,
Valid: true,
},
HashedPassword: pgtype.Text{
String: newPassword,
Valid: true,
},
})
require.NoError(t, err)
require.NotEqual(t, oldUser.FullName, updatedUser.FullName)
require.Equal(t, newFullName, updatedUser.FullName)
require.NotEqual(t, oldUser.HashedPassword, updatedUser.HashedPassword)
require.Equal(t, newPassword, updatedUser.HashedPassword)
require.NotEqual(t, oldUser.Email, updatedUser.Email)
require.Equal(t, newEmail, updatedUser.Email)
}
+74
View File
@@ -0,0 +1,74 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
// source: verify_email.sql
package db
import (
"context"
)
const createVerifyEmail = `-- name: CreateVerifyEmail :one
INSERT INTO verify_emails (
username,
email,
secret_code
) VALUES (
$1, $2, $3
)
RETURNING id, username, email, secret_code, is_used, created_at, expired_at
`
type CreateVerifyEmailParams struct {
Username string `json:"username"`
Email string `json:"email"`
SecretCode string `json:"secret_code"`
}
func (q *Queries) CreateVerifyEmail(ctx context.Context, arg CreateVerifyEmailParams) (VerifyEmail, error) {
row := q.db.QueryRow(ctx, createVerifyEmail, arg.Username, arg.Email, arg.SecretCode)
var i VerifyEmail
err := row.Scan(
&i.ID,
&i.Username,
&i.Email,
&i.SecretCode,
&i.IsUsed,
&i.CreatedAt,
&i.ExpiredAt,
)
return i, err
}
const updateVerifyEmail = `-- name: UpdateVerifyEmail :one
UPDATE verify_emails
SET
is_used = TRUE
WHERE
id = $1
AND secret_code = $2
AND is_used = FALSE
AND expired_at > NOW()
RETURNING id, username, email, secret_code, is_used, created_at, expired_at
`
type UpdateVerifyEmailParams struct {
ID int64 `json:"id"`
SecretCode string `json:"secret_code"`
}
func (q *Queries) UpdateVerifyEmail(ctx context.Context, arg UpdateVerifyEmailParams) (VerifyEmail, error) {
row := q.db.QueryRow(ctx, updateVerifyEmail, arg.ID, arg.SecretCode)
var i VerifyEmail
err := row.Scan(
&i.ID,
&i.Username,
&i.Email,
&i.SecretCode,
&i.IsUsed,
&i.CreatedAt,
&i.ExpiredAt,
)
return i, err
}