init
Some checks failed
Build and Release / release (push) Failing after 51s

This commit is contained in:
2026-03-25 22:29:07 +07:00
parent eedd300861
commit 79199f627d
65 changed files with 3215 additions and 689 deletions

View File

@@ -2,11 +2,15 @@ package repositories
import (
"context"
"fmt"
"time"
"github.com/jackc/pgx/v5/pgtype"
"history-api/internal/gen/sqlc"
"history-api/pkg/models"
"history-api/internal/models"
"history-api/pkg/cache"
"history-api/pkg/convert"
)
type RoleRepository interface {
@@ -25,43 +29,63 @@ type RoleRepository interface {
type roleRepository struct {
q *sqlc.Queries
c cache.Cache
}
func NewRoleRepository(db sqlc.DBTX) RoleRepository {
func NewRoleRepository(db sqlc.DBTX, c cache.Cache) RoleRepository {
return &roleRepository{
q: sqlc.New(db),
c: c,
}
}
func (r *roleRepository) GetByID(ctx context.Context, id pgtype.UUID) (*models.RoleEntity, error) {
cacheId := fmt.Sprintf("role:id:%s", convert.UUIDToString(id))
var role models.RoleEntity
err := r.c.Get(ctx, cacheId, &role)
if err == nil {
return &role, nil
}
row, err := r.q.GetRoleByID(ctx, id)
if err != nil {
return nil, err
}
role := &models.RoleEntity{
ID: row.ID,
role = models.RoleEntity{
ID: convert.UUIDToString(row.ID),
Name: row.Name,
IsDeleted: row.IsDeleted,
CreatedAt: row.CreatedAt,
UpdatedAt: row.UpdatedAt,
CreatedAt: convert.TimeToPtr(row.CreatedAt),
UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
}
return role, nil
_ = r.c.Set(ctx, cacheId, role, 5*time.Minute)
return &role, nil
}
func (r *roleRepository) GetByname(ctx context.Context, name string) (*models.RoleEntity, error) {
cacheId := fmt.Sprintf("role:name:%s", name)
var role models.RoleEntity
err := r.c.Get(ctx, cacheId, &role)
if err == nil {
return &role, nil
}
row, err := r.q.GetRoleByName(ctx, name)
if err != nil {
return nil, err
}
role := &models.RoleEntity{
ID: row.ID,
role = models.RoleEntity{
ID: convert.UUIDToString(row.ID),
Name: row.Name,
IsDeleted: row.IsDeleted,
CreatedAt: row.CreatedAt,
UpdatedAt: row.UpdatedAt,
CreatedAt: convert.TimeToPtr(row.CreatedAt),
UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
}
return role, nil
_ = r.c.Set(ctx, cacheId, role, 5*time.Minute)
return &role, nil
}
func (r *roleRepository) Create(ctx context.Context, name string) (*models.RoleEntity, error) {
@@ -69,14 +93,19 @@ func (r *roleRepository) Create(ctx context.Context, name string) (*models.RoleE
if err != nil {
return nil, err
}
role := &models.RoleEntity{
ID: row.ID,
role := models.RoleEntity{
ID: convert.UUIDToString(row.ID),
Name: row.Name,
IsDeleted: row.IsDeleted,
CreatedAt: row.CreatedAt,
UpdatedAt: row.UpdatedAt,
CreatedAt: convert.TimeToPtr(row.CreatedAt),
UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
}
return role, nil
mapCache := map[string]any{
fmt.Sprintf("role:name:%s", name): role,
fmt.Sprintf("role:id:%s", convert.UUIDToString(row.ID)): role,
}
_ = r.c.MSet(ctx, mapCache, 5*time.Minute)
return &role, nil
}
func (r *roleRepository) Update(ctx context.Context, params sqlc.UpdateRoleParams) (*models.RoleEntity, error) {
@@ -84,14 +113,20 @@ func (r *roleRepository) Update(ctx context.Context, params sqlc.UpdateRoleParam
if err != nil {
return nil, err
}
role := &models.RoleEntity{
ID: row.ID,
role := models.RoleEntity{
ID: convert.UUIDToString(row.ID),
Name: row.Name,
IsDeleted: row.IsDeleted,
CreatedAt: row.CreatedAt,
UpdatedAt: row.UpdatedAt,
CreatedAt: convert.TimeToPtr(row.CreatedAt),
UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
}
return role, nil
mapCache := map[string]any{
fmt.Sprintf("role:name:%s", row.Name): role,
fmt.Sprintf("role:id:%s", convert.UUIDToString(row.ID)): role,
}
_ = r.c.MSet(ctx, mapCache, 5*time.Minute)
return &role, nil
}
func (r *roleRepository) All(ctx context.Context) ([]*models.RoleEntity, error) {
@@ -103,11 +138,11 @@ func (r *roleRepository) All(ctx context.Context) ([]*models.RoleEntity, error)
var users []*models.RoleEntity
for _, row := range rows {
user := &models.RoleEntity{
ID: row.ID,
Name: row.Name,
IsDeleted: row.IsDeleted,
CreatedAt: row.CreatedAt,
UpdatedAt: row.UpdatedAt,
ID: convert.UUIDToString(row.ID),
Name: row.Name,
IsDeleted: row.IsDeleted,
CreatedAt: convert.TimeToPtr(row.CreatedAt),
UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
}
users = append(users, user)
}
@@ -116,33 +151,44 @@ func (r *roleRepository) All(ctx context.Context) ([]*models.RoleEntity, error)
}
func (r *roleRepository) Delete(ctx context.Context, id pgtype.UUID) error {
err := r.q.DeleteRole(ctx, id)
return err
role, err := r.GetByID(ctx, id)
if err != nil {
return err
}
err = r.q.DeleteRole(ctx, id)
if err != nil {
return err
}
_ = r.c.Del(ctx, fmt.Sprintf("role:id:%s", role.ID), fmt.Sprintf("role:name:%s", role.Name))
return nil
}
func (r *roleRepository) Restore(ctx context.Context, id pgtype.UUID) error {
err := r.q.RestoreRole(ctx, id)
return err
if err != nil {
return err
}
_ = r.c.Del(ctx, fmt.Sprintf("role:id:%s", convert.UUIDToString(id)))
return nil
}
func (r *roleRepository) AddUserRole(ctx context.Context, params sqlc.AddUserRoleParams) error {
err := r.q.AddUserRole(ctx, params)
return err
return err
}
func (r *roleRepository) RemoveUserRole(ctx context.Context, params sqlc.RemoveUserRoleParams) error {
err := r.q.RemoveUserRole(ctx, params)
return err
return err
}
func (r *roleRepository) RemoveAllUsersFromRole(ctx context.Context, roleId pgtype.UUID) error {
err := r.q.RemoveAllUsersFromRole(ctx, roleId)
return err
return err
}
func (r *roleRepository) RemoveAllRolesFromUser(ctx context.Context, roleId pgtype.UUID) error {
err := r.q.RemoveAllRolesFromUser(ctx, roleId)
return err
return err
}

View File

@@ -0,0 +1,98 @@
package repositories
import (
"context"
"database/sql"
"fmt"
"history-api/pkg/cache"
"time"
)
type TileRepository interface {
GetMetadata(ctx context.Context) (map[string]string, error)
GetTile(ctx context.Context, z, x, y int) ([]byte, string, bool, error)
}
type tileRepository struct {
db *sql.DB
c cache.Cache
}
func NewTileRepository(db *sql.DB, c cache.Cache) TileRepository {
return &tileRepository{
db: db,
c: c,
}
}
func (r *tileRepository) GetMetadata(ctx context.Context) (map[string]string, error) {
cacheId := "mbtiles:metadata"
var cached map[string]string
err := r.c.Get(ctx, cacheId, &cached)
if err == nil {
return cached, nil
}
rows, err := r.db.QueryContext(ctx, "SELECT name, value FROM metadata")
if err != nil {
return nil, err
}
defer rows.Close()
metadata := make(map[string]string)
for rows.Next() {
var name, value string
if err := rows.Scan(&name, &value); err != nil {
return nil, err
}
metadata[name] = value
}
_ = r.c.Set(ctx, cacheId, metadata, 10*time.Minute)
return metadata, nil
}
func (r *tileRepository) GetTile(ctx context.Context, z, x, y int) ([]byte, string, bool, error) {
if z < 0 || x < 0 || y < 0 {
return nil, "", false, fmt.Errorf("invalid tile coordinates")
}
// cache key
cacheId := fmt.Sprintf("tile:%d:%d:%d", z, x, y)
var cached []byte
err := r.c.Get(ctx, cacheId, &cached)
if err == nil {
meta, _ := r.GetMetadata(ctx)
return cached, meta["format"], meta["format"] == "pbf", nil
}
// XYZ -> TMS
tmsY := (1 << z) - 1 - y
var tileData []byte
err = r.db.QueryRowContext(ctx, `
SELECT tile_data
FROM tiles
WHERE zoom_level = ?
AND tile_column = ?
AND tile_row = ?
`, z, x, tmsY).Scan(&tileData)
if err != nil {
return nil, "", false, err
}
meta, err := r.GetMetadata(ctx)
if err != nil {
return nil, "", false, err
}
_ = r.c.Set(ctx, cacheId, tileData, 5*time.Minute)
return tileData, meta["format"], meta["format"] == "pbf", nil
}

View File

@@ -2,136 +2,187 @@ package repositories
import (
"context"
"fmt"
"time"
"github.com/jackc/pgx/v5/pgtype"
"history-api/internal/gen/sqlc"
"history-api/pkg/models"
"history-api/internal/models"
"history-api/pkg/cache"
"history-api/pkg/convert"
)
type UserRepository interface {
GetByID(ctx context.Context, id pgtype.UUID) (*models.UserEntity, error)
GetByEmail(ctx context.Context, email string) (*models.UserEntity, error)
All(ctx context.Context) ([]*models.UserEntity, error)
Create(ctx context.Context, params sqlc.CreateUserParams) (*models.UserEntity, error)
Update(ctx context.Context, params sqlc.UpdateUserParams) (*models.UserEntity, error)
UpdatePassword(ctx context.Context, params sqlc.UpdateUserPasswordParams) error
ExistEmail(ctx context.Context, email string) (bool, error)
Verify(ctx context.Context, id pgtype.UUID) error
Delete(ctx context.Context, id pgtype.UUID) error
Restore(ctx context.Context, id pgtype.UUID) error
UpsertUser(ctx context.Context, params sqlc.UpsertUserParams) (*models.UserEntity, error)
CreateProfile(ctx context.Context, params sqlc.CreateUserProfileParams) (*models.UserProfileSimple, error)
UpdateProfile(ctx context.Context, params sqlc.UpdateUserProfileParams) (*models.UserEntity, error)
UpdatePassword(ctx context.Context, params sqlc.UpdateUserPasswordParams) error
UpdateRefreshToken(ctx context.Context, params sqlc.UpdateUserRefreshTokenParams) error
GetTokenVersion(ctx context.Context, id pgtype.UUID) (int32, error)
UpdateTokenVersion(ctx context.Context, params sqlc.UpdateTokenVersionParams) error
Verify(ctx context.Context, id pgtype.UUID) error
Delete(ctx context.Context, id pgtype.UUID) error
Restore(ctx context.Context, id pgtype.UUID) error
}
type userRepository struct {
q *sqlc.Queries
c cache.Cache
}
func NewUserRepository(db sqlc.DBTX) UserRepository {
func NewUserRepository(db sqlc.DBTX, c cache.Cache) UserRepository {
return &userRepository{
q: sqlc.New(db),
c: c,
}
}
func (r *userRepository) GetByID(ctx context.Context, id pgtype.UUID) (*models.UserEntity, error) {
cacheId := fmt.Sprintf("user:id:%s", convert.UUIDToString(id))
var user models.UserEntity
err := r.c.Get(ctx, cacheId, &user)
if err == nil {
return &user, nil
}
row, err := r.q.GetUserByID(ctx, id)
if err != nil {
return nil, err
}
user := &models.UserEntity{
ID: row.ID,
Name: row.Name,
user = models.UserEntity{
ID: convert.UUIDToString(row.ID),
Email: row.Email,
PasswordHash: row.PasswordHash,
AvatarUrl: row.AvatarUrl,
IsActive: row.IsActive,
PasswordHash: convert.TextToString(row.PasswordHash),
IsVerified: row.IsVerified,
TokenVersion: row.TokenVersion,
IsDeleted: row.IsDeleted,
CreatedAt: row.CreatedAt,
UpdatedAt: row.UpdatedAt,
CreatedAt: convert.TimeToPtr(row.CreatedAt),
UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
}
if err := user.ParseRoles(row.Roles); err != nil {
return nil, err
return nil, err
}
return user, nil
if err := user.ParseProfile(row.Profile); err != nil {
return nil, err
}
_ = r.c.Set(ctx, cacheId, user, 5*time.Minute)
return &user, nil
}
func (r *userRepository) GetByEmail(ctx context.Context, email string) (*models.UserEntity, error) {
cacheId := fmt.Sprintf("user:email:%s", email)
var user models.UserEntity
err := r.c.Get(ctx, cacheId, &user)
if err == nil {
return &user, nil
}
row, err := r.q.GetUserByEmail(ctx, email)
if err != nil {
return nil, err
}
user := &models.UserEntity{
ID: row.ID,
Name: row.Name,
user = models.UserEntity{
ID: convert.UUIDToString(row.ID),
Email: row.Email,
PasswordHash: row.PasswordHash,
AvatarUrl: row.AvatarUrl,
IsActive: row.IsActive,
PasswordHash: convert.TextToString(row.PasswordHash),
IsVerified: row.IsVerified,
TokenVersion: row.TokenVersion,
IsDeleted: row.IsDeleted,
CreatedAt: row.CreatedAt,
UpdatedAt: row.UpdatedAt,
CreatedAt: convert.TimeToPtr(row.CreatedAt),
UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
}
if err := user.ParseRoles(row.Roles); err != nil {
return nil, err
return nil, err
}
return user, nil
if err := user.ParseProfile(row.Profile); err != nil {
return nil, err
}
_ = r.c.Set(ctx, cacheId, user, 5*time.Minute)
return &user, nil
}
func (r *userRepository) Create(ctx context.Context, params sqlc.CreateUserParams) (*models.UserEntity, error) {
row, err := r.q.CreateUser(ctx, params)
func (r *userRepository) UpsertUser(ctx context.Context, params sqlc.UpsertUserParams) (*models.UserEntity, error) {
row, err := r.q.UpsertUser(ctx, params)
if err != nil {
return nil, err
}
return &models.UserEntity{
ID: row.ID,
Name: row.Name,
ID: convert.UUIDToString(row.ID),
Email: row.Email,
PasswordHash: row.PasswordHash,
AvatarUrl: row.AvatarUrl,
IsActive: row.IsActive,
PasswordHash: convert.TextToString(row.PasswordHash),
IsVerified: row.IsVerified,
TokenVersion: row.TokenVersion,
RefreshToken: row.RefreshToken,
IsDeleted: row.IsDeleted,
CreatedAt: row.CreatedAt,
UpdatedAt: row.UpdatedAt,
CreatedAt: convert.TimeToPtr(row.CreatedAt),
UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
Roles: make([]*models.RoleSimple, 0),
}, nil
}
func (r *userRepository) Update(ctx context.Context, params sqlc.UpdateUserParams) (*models.UserEntity, error) {
row, err := r.q.UpdateUser(ctx, params)
func (r *userRepository) UpdateProfile(ctx context.Context, params sqlc.UpdateUserProfileParams) (*models.UserEntity, error) {
user, err := r.GetByID(ctx, params.UserID)
if err != nil {
return nil, err
}
user := &models.UserEntity{
ID: row.ID,
Name: row.Name,
Email: row.Email,
PasswordHash: row.PasswordHash,
AvatarUrl: row.AvatarUrl,
IsActive: row.IsActive,
IsVerified: row.IsVerified,
TokenVersion: row.TokenVersion,
IsDeleted: row.IsDeleted,
CreatedAt: row.CreatedAt,
UpdatedAt: row.UpdatedAt,
row, err := r.q.UpdateUserProfile(ctx, params)
if err != nil {
return nil, err
}
profile := models.UserProfileSimple{
DisplayName: convert.TextToString(row.DisplayName),
FullName: convert.TextToString(row.FullName),
AvatarUrl: convert.TextToString(row.AvatarUrl),
Bio: convert.TextToString(row.Bio),
Location: convert.TextToString(row.Location),
Website: convert.TextToString(row.Website),
CountryCode: convert.TextToString(row.CountryCode),
Phone: convert.TextToString(row.Phone),
}
if err := user.ParseRoles(row.Roles); err != nil {
return nil, err
user.Profile = &profile
mapCache := map[string]any{
fmt.Sprintf("user:email:%s", user.Email): user,
fmt.Sprintf("user:id:%s", user.ID): user,
}
_ = r.c.MSet(ctx, mapCache, 5*time.Minute)
return user, nil
}
func (r *userRepository) CreateProfile(ctx context.Context, params sqlc.CreateUserProfileParams) (*models.UserProfileSimple, error) {
row, err := r.q.CreateUserProfile(ctx, params)
if err != nil {
return nil, err
}
return &models.UserProfileSimple{
DisplayName: convert.TextToString(row.DisplayName),
FullName: convert.TextToString(row.FullName),
AvatarUrl: convert.TextToString(row.AvatarUrl),
Bio: convert.TextToString(row.Bio),
Location: convert.TextToString(row.Location),
Website: convert.TextToString(row.Website),
CountryCode: convert.TextToString(row.CountryCode),
Phone: convert.TextToString(row.Phone),
}, nil
}
func (r *userRepository) All(ctx context.Context) ([]*models.UserEntity, error) {
rows, err := r.q.GetUsers(ctx)
if err != nil {
@@ -141,23 +192,24 @@ func (r *userRepository) All(ctx context.Context) ([]*models.UserEntity, error)
var users []*models.UserEntity
for _, row := range rows {
user := &models.UserEntity{
ID: row.ID,
Name: row.Name,
ID: convert.UUIDToString(row.ID),
Email: row.Email,
PasswordHash: row.PasswordHash,
AvatarUrl: row.AvatarUrl,
IsActive: row.IsActive,
PasswordHash: convert.TextToString(row.PasswordHash),
IsVerified: row.IsVerified,
TokenVersion: row.TokenVersion,
IsDeleted: row.IsDeleted,
CreatedAt: row.CreatedAt,
UpdatedAt: row.UpdatedAt,
CreatedAt: convert.TimeToPtr(row.CreatedAt),
UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
}
if err := user.ParseRoles(row.Roles); err != nil {
return nil, err
}
if err := user.ParseProfile(row.Profile); err != nil {
return nil, err
}
users = append(users, user)
}
@@ -165,29 +217,138 @@ func (r *userRepository) All(ctx context.Context) ([]*models.UserEntity, error)
}
func (r *userRepository) Verify(ctx context.Context, id pgtype.UUID) error {
err := r.q.VerifyUser(ctx, id)
return err
user, err := r.GetByID(ctx, id)
if err != nil {
return err
}
err = r.q.VerifyUser(ctx, id)
if err != nil {
return err
}
err = r.q.UpdateTokenVersion(ctx, sqlc.UpdateTokenVersionParams{
ID: id,
TokenVersion: user.TokenVersion + 1,
})
if err != nil {
return err
}
user.IsVerified = true
user.TokenVersion += 1
mapCache := map[string]any{
fmt.Sprintf("user:email:%s", user.Email): user,
fmt.Sprintf("user:id:%s", user.ID): user,
}
_ = r.c.MSet(ctx, mapCache, 5*time.Minute)
return nil
}
func (r *userRepository) Delete(ctx context.Context, id pgtype.UUID) error {
err := r.q.DeleteUser(ctx, id)
return err
user, err := r.GetByID(ctx, id)
if err != nil {
return err
}
err = r.q.DeleteUser(ctx, id)
if err != nil {
return err
}
_ = r.c.Del(
ctx,
fmt.Sprintf("user:id:%s", user.ID),
fmt.Sprintf("user:email:%s", user.Email),
fmt.Sprintf("user:token:%s", user.ID),
)
return nil
}
func (r *userRepository) Restore(ctx context.Context, id pgtype.UUID) error {
err := r.q.RestoreUser(ctx, id)
return err
if err != nil {
return err
}
_ = r.c.Del(ctx, fmt.Sprintf("user:id:%s", convert.UUIDToString(id)))
return nil
}
func (r *userRepository) GetTokenVersion(ctx context.Context, id pgtype.UUID) (int32, error) {
cacheId := fmt.Sprintf("user:token:%s", convert.UUIDToString(id))
var token int32
err := r.c.Get(ctx, cacheId, &token)
if err == nil {
return token, nil
}
raw, err := r.q.GetTokenVersion(ctx, id)
if err != nil {
return 0, err
}
_ = r.c.Set(ctx, cacheId, raw, 5*time.Minute)
return raw, nil
}
func (r *userRepository) UpdateTokenVersion(ctx context.Context, params sqlc.UpdateTokenVersionParams) error {
err := r.q.UpdateTokenVersion(ctx, params)
if err != nil {
return err
}
cacheId := fmt.Sprintf("user:token:%s", convert.UUIDToString(params.ID))
_ = r.c.Set(ctx, cacheId, params.TokenVersion, 5*time.Minute)
return nil
}
func (r *userRepository) UpdatePassword(ctx context.Context, params sqlc.UpdateUserPasswordParams) error {
err := r.q.UpdateUserPassword(ctx, params)
return err
user, err := r.GetByID(ctx, params.ID)
if err != nil {
return err
}
err = r.q.UpdateUserPassword(ctx, params)
if err != nil {
return err
}
err = r.UpdateTokenVersion(ctx, sqlc.UpdateTokenVersionParams{
ID: params.ID,
TokenVersion: user.TokenVersion + 1,
})
if err != nil {
return err
}
user.PasswordHash = convert.TextToString(params.PasswordHash)
user.TokenVersion += 1
mapCache := map[string]any{
fmt.Sprintf("user:email:%s", user.Email): user,
fmt.Sprintf("user:id:%s", user.ID): user,
fmt.Sprintf("user:token:%s", user.ID): user.TokenVersion,
}
_ = r.c.MSet(ctx, mapCache, 5*time.Minute)
return nil
}
func (r *userRepository) ExistEmail(ctx context.Context, email string) (bool, error) {
row, err := r.q.ExistsUserByEmail(ctx, email)
func (r *userRepository) UpdateRefreshToken(ctx context.Context, params sqlc.UpdateUserRefreshTokenParams) error {
user, err := r.GetByID(ctx, params.ID)
if err != nil {
return false, err
return err
}
return row, nil
}
err = r.q.UpdateUserRefreshToken(ctx, params)
if err != nil {
return err
}
user.RefreshToken = convert.TextToString(params.RefreshToken)
mapCache := map[string]any{
fmt.Sprintf("user:email:%s", user.Email): user,
fmt.Sprintf("user:id:%s", user.ID): user,
fmt.Sprintf("user:token:%s", user.ID): user.TokenVersion,
}
_ = r.c.MSet(ctx, mapCache, 5*time.Minute)
return nil
}