package repositories import ( "context" "crypto/md5" "encoding/json" "fmt" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgtype" "history-api/internal/gen/sqlc" "history-api/internal/models" "history-api/pkg/cache" "history-api/pkg/constants" "history-api/pkg/convert" ) type RoleRepository interface { GetByID(ctx context.Context, id pgtype.UUID) (*models.RoleEntity, error) GetByIDs(ctx context.Context, ids []string) ([]*models.RoleEntity, error) GetByName(ctx context.Context, name string) (*models.RoleEntity, error) All(ctx context.Context) ([]*models.RoleEntity, error) Create(ctx context.Context, name string) (*models.RoleEntity, error) Update(ctx context.Context, params sqlc.UpdateRoleParams) (*models.RoleEntity, error) Delete(ctx context.Context, id pgtype.UUID) error Restore(ctx context.Context, id pgtype.UUID) error CreateUserRole(ctx context.Context, params sqlc.CreateUserRoleParams) error DeleteUserRole(ctx context.Context, params sqlc.DeleteUserRoleParams) error BulkDeleteRolesFromUser(ctx context.Context, userId pgtype.UUID) error BulkDeleteUsersFromRole(ctx context.Context, roleId pgtype.UUID) error WithTx(tx pgx.Tx) RoleRepository } type roleRepository struct { q *sqlc.Queries c cache.Cache } func NewRoleRepository(db sqlc.DBTX, c cache.Cache) RoleRepository { return &roleRepository{ q: sqlc.New(db), c: c, } } func (r *roleRepository) WithTx(tx pgx.Tx) RoleRepository { return &roleRepository{ q: r.q.WithTx(tx), c: r.c, } } func (r *roleRepository) generateQueryKey(prefix string, params any) string { b, _ := json.Marshal(params) hash := fmt.Sprintf("%x", md5.Sum(b)) return fmt.Sprintf("%s:%s", prefix, hash) } func (r *roleRepository) getByIDsWithFallback(ctx context.Context, ids []string) ([]*models.RoleEntity, error) { if len(ids) == 0 { return []*models.RoleEntity{}, nil } keys := make([]string, len(ids)) for i, id := range ids { keys[i] = fmt.Sprintf("role:id:%s", id) } raws := r.c.MGet(ctx, keys...) var roles []*models.RoleEntity missingRolesToCache := make(map[string]any) var missingPgIds []pgtype.UUID for i, b := range raws { if len(b) == 0 { pgId := pgtype.UUID{} err := pgId.Scan(ids[i]) if err == nil { missingPgIds = append(missingPgIds, pgId) } } } dbMap := make(map[string]*models.RoleEntity) if len(missingPgIds) > 0 { dbRows, err := r.q.GetRolesByIDs(ctx, missingPgIds) if err == nil { for _, row := range dbRows { item := models.RoleEntity{ ID: convert.UUIDToString(row.ID), Name: row.Name, IsDeleted: row.IsDeleted, CreatedAt: convert.TimeToPtr(row.CreatedAt), UpdatedAt: convert.TimeToPtr(row.UpdatedAt), } dbMap[item.ID] = &item } } } for i, b := range raws { if len(b) > 0 { var u models.RoleEntity if err := json.Unmarshal(b, &u); err == nil { roles = append(roles, &u) } } else { if item, ok := dbMap[ids[i]]; ok { roles = append(roles, item) missingRolesToCache[keys[i]] = item } } } if len(missingRolesToCache) > 0 { _ = r.c.MSet(ctx, missingRolesToCache, constants.NormalCacheDuration) } return roles, nil } func (r *roleRepository) GetByIDs(ctx context.Context, ids []string) ([]*models.RoleEntity, error) { return r.getByIDsWithFallback(ctx, ids) } 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 { _ = r.c.Set(ctx, cacheId, role, constants.NormalCacheDuration) return &role, nil } row, err := r.q.GetRoleByID(ctx, id) if err != nil { return nil, err } role = models.RoleEntity{ ID: convert.UUIDToString(row.ID), Name: row.Name, IsDeleted: row.IsDeleted, CreatedAt: convert.TimeToPtr(row.CreatedAt), UpdatedAt: convert.TimeToPtr(row.UpdatedAt), } _ = r.c.Set(ctx, cacheId, role, constants.NormalCacheDuration) 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 { _ = r.c.Set(ctx, cacheId, role, constants.NormalCacheDuration) return &role, nil } row, err := r.q.GetRoleByName(ctx, name) if err != nil { return nil, err } role = models.RoleEntity{ ID: convert.UUIDToString(row.ID), Name: row.Name, IsDeleted: row.IsDeleted, CreatedAt: convert.TimeToPtr(row.CreatedAt), UpdatedAt: convert.TimeToPtr(row.UpdatedAt), } _ = r.c.Set(ctx, cacheId, role, constants.NormalCacheDuration) return &role, nil } func (r *roleRepository) Create(ctx context.Context, name string) (*models.RoleEntity, error) { row, err := r.q.CreateRole(ctx, name) if err != nil { return nil, err } go func() { bgCtx := context.Background() _ = r.c.DelByPattern(bgCtx, "role:all*") }() role := models.RoleEntity{ ID: convert.UUIDToString(row.ID), Name: row.Name, IsDeleted: row.IsDeleted, CreatedAt: convert.TimeToPtr(row.CreatedAt), UpdatedAt: convert.TimeToPtr(row.UpdatedAt), } return &role, nil } func (r *roleRepository) Update(ctx context.Context, params sqlc.UpdateRoleParams) (*models.RoleEntity, error) { row, err := r.q.UpdateRole(ctx, params) if err != nil { return nil, err } role := models.RoleEntity{ ID: convert.UUIDToString(row.ID), Name: row.Name, IsDeleted: row.IsDeleted, CreatedAt: convert.TimeToPtr(row.CreatedAt), UpdatedAt: convert.TimeToPtr(row.UpdatedAt), } _ = r.c.Del(ctx, fmt.Sprintf("role:id:%s", convert.UUIDToString(row.ID)), fmt.Sprintf("role:name:%s", row.Name)) return &role, nil } func (r *roleRepository) All(ctx context.Context) ([]*models.RoleEntity, error) { queryKey := "role:all" var cachedIDs []string if err := r.c.Get(ctx, queryKey, &cachedIDs); err == nil && len(cachedIDs) > 0 { listItem, err := r.getByIDsWithFallback(ctx, cachedIDs) if err != nil { return nil, err } newCachedIDs := make([]string, len(listItem)) for i, media := range listItem { newCachedIDs[i] = media.ID } _ = r.c.Set(ctx, queryKey, newCachedIDs, constants.ListCacheDuration) return listItem, err } rows, err := r.q.GetRoles(ctx) if err != nil { return nil, err } var roles []*models.RoleEntity var ids []string roleToCache := make(map[string]any) for _, row := range rows { role := &models.RoleEntity{ ID: convert.UUIDToString(row.ID), Name: row.Name, IsDeleted: row.IsDeleted, CreatedAt: convert.TimeToPtr(row.CreatedAt), UpdatedAt: convert.TimeToPtr(row.UpdatedAt), } ids = append(ids, role.ID) roles = append(roles, role) roleToCache[fmt.Sprintf("role:id:%s", role.ID)] = role } if len(roleToCache) > 0 { _ = r.c.MSet(ctx, roleToCache, constants.NormalCacheDuration) } if len(ids) > 0 { _ = r.c.Set(ctx, queryKey, ids, constants.ListCacheDuration) } return roles, nil } func (r *roleRepository) Delete(ctx context.Context, id pgtype.UUID) error { 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) if err != nil { return err } _ = r.c.Del(ctx, fmt.Sprintf("role:id:%s", convert.UUIDToString(id))) return nil } func (r *roleRepository) CreateUserRole(ctx context.Context, params sqlc.CreateUserRoleParams) error { err := r.q.CreateUserRole(ctx, params) return err } func (r *roleRepository) DeleteUserRole(ctx context.Context, params sqlc.DeleteUserRoleParams) error { err := r.q.DeleteUserRole(ctx, params) return err } func (r *roleRepository) BulkDeleteUsersFromRole(ctx context.Context, roleId pgtype.UUID) error { err := r.q.BulkDeleteUsersFromRole(ctx, roleId) return err } func (r *roleRepository) BulkDeleteRolesFromUser(ctx context.Context, roleId pgtype.UUID) error { err := r.q.BulkDeleteRolesFromUser(ctx, roleId) return err }