350 lines
8.7 KiB
Go
350 lines
8.7 KiB
Go
package asset
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/rs/zerolog"
|
|
"go.uber.org/fx"
|
|
"golang.org/x/sync/errgroup"
|
|
|
|
domainAsset "base/internal/domain/asset"
|
|
"base/internal/dto"
|
|
)
|
|
|
|
var (
|
|
ErrAssetNotFound = errors.New("asset not found")
|
|
ErrCategoryNotFound = errors.New("asset category not found")
|
|
)
|
|
|
|
type Service interface {
|
|
Create(ctx context.Context, req dto.CreateAssetRequest) (*dto.AssetResponse, error)
|
|
GetByID(ctx context.Context, id uuid.UUID) (*dto.AssetResponse, error)
|
|
Update(ctx context.Context, req dto.UpdateAssetRequest) (*dto.AssetResponse, error)
|
|
Delete(ctx context.Context, id uuid.UUID) error
|
|
FindByProfileID(ctx context.Context, profileID uuid.UUID) (*dto.ListAssetsResponse, error)
|
|
ListCategories(ctx context.Context) (*dto.ListCategoriesResponse, error)
|
|
ListByCategoryID(ctx context.Context, categoryID uuid.UUID, limit, page int) (*dto.ListAssetsByCategoryIDResponse, error)
|
|
GetCategoriesWithPreview(ctx context.Context, req dto.CategoriesPreviewRequest) (*dto.CategoriesPreviewResponse, error)
|
|
}
|
|
|
|
type service struct {
|
|
logger zerolog.Logger
|
|
assetRepo domainAsset.AssetRepository
|
|
categoryRepo domainAsset.CategoryRepository
|
|
}
|
|
|
|
type Param struct {
|
|
Logger zerolog.Logger
|
|
AssetRepo domainAsset.AssetRepository
|
|
CategoryRepo domainAsset.CategoryRepository
|
|
|
|
fx.In
|
|
}
|
|
|
|
func New(param Param) Service {
|
|
return &service{
|
|
logger: param.Logger,
|
|
assetRepo: param.AssetRepo,
|
|
categoryRepo: param.CategoryRepo,
|
|
}
|
|
}
|
|
|
|
func (s *service) Create(ctx context.Context, req dto.CreateAssetRequest) (*dto.AssetResponse, error) {
|
|
profileID, err := uuid.Parse(req.ProfileID)
|
|
if err != nil {
|
|
return nil, ErrAssetNotFound
|
|
}
|
|
categoryID, err := uuid.Parse(req.AssetCategoryID)
|
|
if err != nil {
|
|
return nil, ErrCategoryNotFound
|
|
}
|
|
|
|
// Verify category exists
|
|
category, err := s.categoryRepo.FindByID(ctx, categoryID)
|
|
if err != nil || category == nil {
|
|
return nil, ErrCategoryNotFound
|
|
}
|
|
|
|
asset := &domainAsset.Asset{
|
|
ID: uuid.New(),
|
|
ProfileID: profileID,
|
|
AssetCategoryID: categoryID,
|
|
AssetCategory: *category,
|
|
Title: req.Title,
|
|
Description: req.Description,
|
|
Link: req.Link,
|
|
Status: domainAsset.StatusPublished,
|
|
}
|
|
|
|
if err := s.assetRepo.Create(ctx, asset); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return s.toAssetResponse(asset), nil
|
|
}
|
|
|
|
func (s *service) GetByID(ctx context.Context, id uuid.UUID) (*dto.AssetResponse, error) {
|
|
asset, err := s.assetRepo.FindByID(ctx, id)
|
|
if err != nil {
|
|
return nil, ErrAssetNotFound
|
|
}
|
|
return s.toAssetResponse(asset), nil
|
|
}
|
|
|
|
func (s *service) Update(ctx context.Context, req dto.UpdateAssetRequest) (*dto.AssetResponse, error) {
|
|
id, err := uuid.Parse(req.ID)
|
|
if err != nil {
|
|
return nil, ErrAssetNotFound
|
|
}
|
|
|
|
asset, err := s.assetRepo.FindByID(ctx, id)
|
|
if err != nil {
|
|
return nil, ErrAssetNotFound
|
|
}
|
|
|
|
asset.Title = req.Title
|
|
asset.Description = req.Description
|
|
asset.Link = req.Link
|
|
|
|
if req.AssetCategoryID != "" {
|
|
categoryID, err := uuid.Parse(req.AssetCategoryID)
|
|
if err == nil {
|
|
category, err := s.categoryRepo.FindByID(ctx, categoryID)
|
|
if err == nil && category != nil {
|
|
asset.AssetCategoryID = categoryID
|
|
asset.AssetCategory = *category
|
|
}
|
|
}
|
|
}
|
|
|
|
if req.Status != nil && *req.Status >= 0 && *req.Status <= 3 {
|
|
asset.Status = domainAsset.Status(*req.Status)
|
|
}
|
|
|
|
if err := s.assetRepo.Update(ctx, asset); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return s.toAssetResponse(asset), nil
|
|
}
|
|
|
|
func (s *service) Delete(ctx context.Context, id uuid.UUID) error {
|
|
asset, err := s.assetRepo.FindByID(ctx, id)
|
|
if err != nil {
|
|
return ErrAssetNotFound
|
|
}
|
|
return s.assetRepo.Delete(ctx, asset)
|
|
}
|
|
|
|
func (s *service) FindByProfileID(ctx context.Context, profileID uuid.UUID) (*dto.ListAssetsResponse, error) {
|
|
assets, err := s.assetRepo.FindByProfileID(ctx, profileID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
resp := &dto.ListAssetsResponse{
|
|
Assets: make([]dto.AssetResponse, len(assets)),
|
|
}
|
|
for i, a := range assets {
|
|
resp.Assets[i] = *s.toAssetResponse(a)
|
|
}
|
|
return resp, nil
|
|
}
|
|
|
|
func (s *service) ListCategories(ctx context.Context) (*dto.ListCategoriesResponse, error) {
|
|
categories, err := s.categoryRepo.FindAll(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
resp := &dto.ListCategoriesResponse{
|
|
Categories: make([]dto.CategoryDTO, len(categories)),
|
|
}
|
|
for i, c := range categories {
|
|
resp.Categories[i] = dto.CategoryDTO{
|
|
ID: c.ID,
|
|
Name: c.Name,
|
|
Icon: c.Icon,
|
|
Color: c.Color,
|
|
CardType: c.CardType,
|
|
Featured: c.Featured,
|
|
Description: c.Description,
|
|
}
|
|
}
|
|
return resp, nil
|
|
}
|
|
|
|
func (s *service) ListByCategoryID(ctx context.Context, categoryID uuid.UUID, limit, page int) (*dto.ListAssetsByCategoryIDResponse, error) {
|
|
if limit < 1 {
|
|
limit = 10
|
|
}
|
|
if page < 1 {
|
|
page = 1
|
|
}
|
|
|
|
category, err := s.categoryRepo.FindByID(ctx, categoryID)
|
|
if err != nil || category == nil {
|
|
return nil, ErrCategoryNotFound
|
|
}
|
|
|
|
total, err := s.assetRepo.CountByCategory(ctx, categoryID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
offset := (page - 1) * limit
|
|
assets, err := s.assetRepo.FindLatestByCategoryPaginated(ctx, categoryID, limit, offset)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
totalPages := (total + limit - 1) / limit
|
|
if totalPages < 1 {
|
|
totalPages = 1
|
|
}
|
|
|
|
resp := &dto.ListAssetsByCategoryIDResponse{
|
|
Category: dto.CategoryDTO{
|
|
ID: category.ID,
|
|
Name: category.Name,
|
|
Icon: category.Icon,
|
|
Color: category.Color,
|
|
CardType: category.CardType,
|
|
Featured: category.Featured,
|
|
Description: category.Description,
|
|
},
|
|
Assets: make([]dto.AssetResponse, len(assets)),
|
|
Total: total,
|
|
Page: page,
|
|
PageSize: limit,
|
|
TotalPages: totalPages,
|
|
}
|
|
for i, a := range assets {
|
|
resp.Assets[i] = *s.toAssetResponse(a)
|
|
}
|
|
return resp, nil
|
|
}
|
|
|
|
func (s *service) GetCategoriesWithPreview(ctx context.Context, req dto.CategoriesPreviewRequest) (*dto.CategoriesPreviewResponse, error) {
|
|
perCategory := req.AssetsPerCategory
|
|
if perCategory < 1 {
|
|
perCategory = 8
|
|
}
|
|
if perCategory > 20 {
|
|
perCategory = 20
|
|
}
|
|
|
|
var categoryIDs []uuid.UUID
|
|
for _, s := range req.CategoryIDs {
|
|
if id, err := uuid.Parse(s); err == nil {
|
|
categoryIDs = append(categoryIDs, id)
|
|
}
|
|
}
|
|
|
|
var categories []*domainAsset.Category
|
|
var err error
|
|
if len(categoryIDs) > 0 {
|
|
categories, err = s.categoryRepo.FindByIDs(ctx, categoryIDs)
|
|
} else {
|
|
categories, err = s.categoryRepo.FindAll(ctx)
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if req.FeaturedOnly {
|
|
filtered := make([]*domainAsset.Category, 0, len(categories))
|
|
for _, c := range categories {
|
|
if c.Featured {
|
|
filtered = append(filtered, c)
|
|
}
|
|
}
|
|
categories = filtered
|
|
}
|
|
|
|
results := make([]dto.CategoryWithPreviewAssetsDTO, len(categories))
|
|
g, gCtx := errgroup.WithContext(ctx)
|
|
|
|
for index, category := range categories {
|
|
i, cat := index, category
|
|
g.Go(func() error {
|
|
assets, assetErr := s.assetRepo.FindLatestByCategory(gCtx, cat.ID, perCategory)
|
|
if assetErr != nil {
|
|
return assetErr
|
|
}
|
|
|
|
total, _ := s.assetRepo.CountByCategory(gCtx, cat.ID)
|
|
|
|
assetResps := make([]dto.AssetResponse, len(assets))
|
|
for j, a := range assets {
|
|
assetResps[j] = *s.toAssetResponse(a)
|
|
}
|
|
|
|
results[i] = dto.CategoryWithPreviewAssetsDTO{
|
|
Category: dto.CategoryDTO{
|
|
ID: cat.ID,
|
|
Name: cat.Name,
|
|
Icon: cat.Icon,
|
|
Color: cat.Color,
|
|
CardType: cat.CardType,
|
|
Featured: cat.Featured,
|
|
Description: cat.Description,
|
|
},
|
|
Assets: assetResps,
|
|
TotalAssets: total,
|
|
HasMore: total > perCategory,
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
if err := g.Wait(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &dto.CategoriesPreviewResponse{Categories: results}, nil
|
|
}
|
|
|
|
func (s *service) toAssetResponse(a *domainAsset.Asset) *dto.AssetResponse {
|
|
coverImage := ""
|
|
for _, art := range a.AssetArtifacts {
|
|
if strings.Contains(strings.ToLower(art.Type), "image") {
|
|
coverImage = art.DownloadURL
|
|
break
|
|
}
|
|
}
|
|
|
|
resp := &dto.AssetResponse{
|
|
ID: a.ID,
|
|
ProfileID: a.ProfileID,
|
|
AssetCategoryID: a.AssetCategoryID,
|
|
Title: a.Title,
|
|
Description: a.Description,
|
|
Link: a.Link,
|
|
CoverImage: coverImage,
|
|
Status: int(a.Status),
|
|
CreatedAt: formatTime(a.CreatedAt),
|
|
UpdatedAt: formatTime(a.UpdatedAt),
|
|
}
|
|
|
|
resp.Category = dto.CategoryDTO{
|
|
ID: a.AssetCategory.ID,
|
|
Name: a.AssetCategory.Name,
|
|
Icon: a.AssetCategory.Icon,
|
|
Color: a.AssetCategory.Color,
|
|
CardType: a.AssetCategory.CardType,
|
|
Featured: a.AssetCategory.Featured,
|
|
Description: a.AssetCategory.Description,
|
|
}
|
|
return resp
|
|
}
|
|
|
|
func formatTime(t time.Time) string {
|
|
if t.IsZero() {
|
|
return ""
|
|
}
|
|
return t.Format(time.RFC3339)
|
|
}
|