From 3c80f63a55168ddc152c9a695722c6c5f1f7b361 Mon Sep 17 00:00:00 2001 From: Leon Mika Date: Wed, 18 Mar 2026 21:38:41 +1100 Subject: [PATCH] feat: add categories service with CRUD and slug validation --- services/categories/service.go | 162 +++++++++++++++++++++++++++++++++ 1 file changed, 162 insertions(+) create mode 100644 services/categories/service.go diff --git a/services/categories/service.go b/services/categories/service.go new file mode 100644 index 0000000..57b509d --- /dev/null +++ b/services/categories/service.go @@ -0,0 +1,162 @@ +package categories + +import ( + "context" + "time" + + "lmika.dev/lmika/weiro/models" + "lmika.dev/lmika/weiro/providers/db" + "lmika.dev/lmika/weiro/services/publisher" +) + +type CreateCategoryParams struct { + GUID string `form:"guid" json:"guid"` + Name string `form:"name" json:"name"` + Slug string `form:"slug" json:"slug"` + Description string `form:"description" json:"description"` +} + +type Service struct { + db *db.Provider + publisher *publisher.Queue +} + +func New(db *db.Provider, publisher *publisher.Queue) *Service { + return &Service{db: db, publisher: publisher} +} + +func (s *Service) ListCategories(ctx context.Context) ([]*models.Category, error) { + site, ok := models.GetSite(ctx) + if !ok { + return nil, models.SiteRequiredError + } + return s.db.SelectCategoriesOfSite(ctx, site.ID) +} + +// ListCategoriesWithCounts returns all categories for the site with published post counts. +func (s *Service) ListCategoriesWithCounts(ctx context.Context) ([]models.CategoryWithCount, error) { + site, ok := models.GetSite(ctx) + if !ok { + return nil, models.SiteRequiredError + } + + cats, err := s.db.SelectCategoriesOfSite(ctx, site.ID) + if err != nil { + return nil, err + } + + result := make([]models.CategoryWithCount, len(cats)) + for i, cat := range cats { + count, err := s.db.CountPostsOfCategory(ctx, cat.ID) + if err != nil { + return nil, err + } + result[i] = models.CategoryWithCount{ + Category: *cat, + PostCount: int(count), + DescriptionBrief: models.BriefDescription(cat.Description), + } + } + return result, nil +} + +func (s *Service) GetCategory(ctx context.Context, id int64) (*models.Category, error) { + return s.db.SelectCategory(ctx, id) +} + +func (s *Service) CreateCategory(ctx context.Context, params CreateCategoryParams) (*models.Category, error) { + site, ok := models.GetSite(ctx) + if !ok { + return nil, models.SiteRequiredError + } + + now := time.Now() + slug := params.Slug + if slug == "" { + slug = models.GenerateCategorySlug(params.Name) + } + + // Check for slug collision + if _, err := s.db.SelectCategoryBySlugAndSite(ctx, site.ID, slug); err == nil { + return nil, models.SlugConflictError + } + + cat := &models.Category{ + SiteID: site.ID, + GUID: params.GUID, + Name: params.Name, + Slug: slug, + Description: params.Description, + CreatedAt: now, + UpdatedAt: now, + } + if cat.GUID == "" { + cat.GUID = models.NewNanoID() + } + + if err := s.db.SaveCategory(ctx, cat); err != nil { + return nil, err + } + + s.publisher.Queue(site) + return cat, nil +} + +func (s *Service) UpdateCategory(ctx context.Context, id int64, params CreateCategoryParams) (*models.Category, error) { + site, ok := models.GetSite(ctx) + if !ok { + return nil, models.SiteRequiredError + } + + cat, err := s.db.SelectCategory(ctx, id) + if err != nil { + return nil, err + } + if cat.SiteID != site.ID { + return nil, models.NotFoundError + } + + slug := params.Slug + if slug == "" { + slug = models.GenerateCategorySlug(params.Name) + } + + // Check slug collision (exclude self) + if existing, err := s.db.SelectCategoryBySlugAndSite(ctx, site.ID, slug); err == nil && existing.ID != cat.ID { + return nil, models.SlugConflictError + } + + cat.Name = params.Name + cat.Slug = slug + cat.Description = params.Description + cat.UpdatedAt = time.Now() + + if err := s.db.SaveCategory(ctx, cat); err != nil { + return nil, err + } + + s.publisher.Queue(site) + return cat, nil +} + +func (s *Service) DeleteCategory(ctx context.Context, id int64) error { + site, ok := models.GetSite(ctx) + if !ok { + return models.SiteRequiredError + } + + cat, err := s.db.SelectCategory(ctx, id) + if err != nil { + return err + } + if cat.SiteID != site.ID { + return models.NotFoundError + } + + if err := s.db.DeleteCategory(ctx, id); err != nil { + return err + } + + s.publisher.Queue(site) + return nil +}