feat(pages): add pages service layer
Implements the pages service with ListPages, GetPage, CreatePage, UpdatePage, DeletePage, and ReorderPages methods. Wires the service into the service registry and generalises SlugConflictError message. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
2cd9ff8721
commit
1edcd7686c
|
|
@ -7,4 +7,4 @@ var PermissionError = errors.New("permission denied")
|
||||||
var NotFoundError = errors.New("not found")
|
var NotFoundError = errors.New("not found")
|
||||||
var SiteRequiredError = errors.New("site required")
|
var SiteRequiredError = errors.New("site required")
|
||||||
var DeleteDebounceError = errors.New("permanent delete too soon, try again in a few seconds")
|
var DeleteDebounceError = errors.New("permanent delete too soon, try again in a few seconds")
|
||||||
var SlugConflictError = errors.New("a category with this slug already exists")
|
var SlugConflictError = errors.New("a record with this slug already exists")
|
||||||
|
|
|
||||||
189
services/pages/service.go
Normal file
189
services/pages/service.go
Normal file
|
|
@ -0,0 +1,189 @@
|
||||||
|
package pages
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"lmika.dev/lmika/weiro/models"
|
||||||
|
"lmika.dev/lmika/weiro/providers/db"
|
||||||
|
"lmika.dev/lmika/weiro/services/publisher"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CreatePageParams struct {
|
||||||
|
GUID string `form:"guid" json:"guid"`
|
||||||
|
Title string `form:"title" json:"title"`
|
||||||
|
Slug string `form:"slug" json:"slug"`
|
||||||
|
Body string `form:"body" json:"body"`
|
||||||
|
PageType int `form:"page_type" json:"page_type"`
|
||||||
|
ShowInNav bool `form:"show_in_nav" json:"show_in_nav"`
|
||||||
|
}
|
||||||
|
|
||||||
|
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) ListPages(ctx context.Context) ([]*models.Page, error) {
|
||||||
|
site, ok := models.GetSite(ctx)
|
||||||
|
if !ok {
|
||||||
|
return nil, models.SiteRequiredError
|
||||||
|
}
|
||||||
|
return s.db.SelectPagesOfSite(ctx, site.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) GetPage(ctx context.Context, id int64) (*models.Page, error) {
|
||||||
|
site, ok := models.GetSite(ctx)
|
||||||
|
if !ok {
|
||||||
|
return nil, models.SiteRequiredError
|
||||||
|
}
|
||||||
|
|
||||||
|
page, err := s.db.SelectPage(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if page.SiteID != site.ID {
|
||||||
|
return nil, models.NotFoundError
|
||||||
|
}
|
||||||
|
return page, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) CreatePage(ctx context.Context, params CreatePageParams) (*models.Page, error) {
|
||||||
|
site, ok := models.GetSite(ctx)
|
||||||
|
if !ok {
|
||||||
|
return nil, models.SiteRequiredError
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
slug := params.Slug
|
||||||
|
if slug == "" {
|
||||||
|
slug = models.GeneratePageSlug(params.Title)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check slug collision
|
||||||
|
if _, err := s.db.SelectPageBySlugAndSite(ctx, site.ID, slug); err == nil {
|
||||||
|
return nil, models.SlugConflictError
|
||||||
|
} else if !db.ErrorIsNoRows(err) {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine sort order: place at end
|
||||||
|
existingPages, err := s.db.SelectPagesOfSite(ctx, site.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
sortOrder := len(existingPages)
|
||||||
|
|
||||||
|
page := &models.Page{
|
||||||
|
SiteID: site.ID,
|
||||||
|
GUID: params.GUID,
|
||||||
|
Title: params.Title,
|
||||||
|
Slug: slug,
|
||||||
|
Body: params.Body,
|
||||||
|
PageType: params.PageType,
|
||||||
|
ShowInNav: params.ShowInNav,
|
||||||
|
SortOrder: sortOrder,
|
||||||
|
CreatedAt: now,
|
||||||
|
UpdatedAt: now,
|
||||||
|
}
|
||||||
|
if page.GUID == "" {
|
||||||
|
page.GUID = models.NewNanoID()
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.db.SavePage(ctx, page); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
s.publisher.Queue(site)
|
||||||
|
return page, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) UpdatePage(ctx context.Context, id int64, params CreatePageParams) (*models.Page, error) {
|
||||||
|
site, ok := models.GetSite(ctx)
|
||||||
|
if !ok {
|
||||||
|
return nil, models.SiteRequiredError
|
||||||
|
}
|
||||||
|
|
||||||
|
page, err := s.db.SelectPage(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if page.SiteID != site.ID {
|
||||||
|
return nil, models.NotFoundError
|
||||||
|
}
|
||||||
|
|
||||||
|
slug := params.Slug
|
||||||
|
if slug == "" {
|
||||||
|
slug = models.GeneratePageSlug(params.Title)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check slug collision (exclude self)
|
||||||
|
if existing, err := s.db.SelectPageBySlugAndSite(ctx, site.ID, slug); err == nil && existing.ID != page.ID {
|
||||||
|
return nil, models.SlugConflictError
|
||||||
|
} else if err != nil && !db.ErrorIsNoRows(err) {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
page.Title = params.Title
|
||||||
|
page.Slug = slug
|
||||||
|
page.Body = params.Body
|
||||||
|
page.PageType = params.PageType
|
||||||
|
page.ShowInNav = params.ShowInNav
|
||||||
|
page.UpdatedAt = time.Now()
|
||||||
|
|
||||||
|
if err := s.db.SavePage(ctx, page); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
s.publisher.Queue(site)
|
||||||
|
return page, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) DeletePage(ctx context.Context, id int64) error {
|
||||||
|
site, ok := models.GetSite(ctx)
|
||||||
|
if !ok {
|
||||||
|
return models.SiteRequiredError
|
||||||
|
}
|
||||||
|
|
||||||
|
page, err := s.db.SelectPage(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if page.SiteID != site.ID {
|
||||||
|
return models.NotFoundError
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.db.DeletePage(ctx, id); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
s.publisher.Queue(site)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) ReorderPages(ctx context.Context, pageIDs []int64) error {
|
||||||
|
site, ok := models.GetSite(ctx)
|
||||||
|
if !ok {
|
||||||
|
return models.SiteRequiredError
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify all pages belong to this site
|
||||||
|
for i, id := range pageIDs {
|
||||||
|
page, err := s.db.SelectPage(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if page.SiteID != site.ID {
|
||||||
|
return models.NotFoundError
|
||||||
|
}
|
||||||
|
if err := s.db.UpdatePageSortOrder(ctx, id, i); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
s.publisher.Queue(site)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
@ -8,6 +8,7 @@ import (
|
||||||
"lmika.dev/lmika/weiro/providers/uploadfiles"
|
"lmika.dev/lmika/weiro/providers/uploadfiles"
|
||||||
"lmika.dev/lmika/weiro/services/auth"
|
"lmika.dev/lmika/weiro/services/auth"
|
||||||
"lmika.dev/lmika/weiro/services/categories"
|
"lmika.dev/lmika/weiro/services/categories"
|
||||||
|
"lmika.dev/lmika/weiro/services/pages"
|
||||||
"lmika.dev/lmika/weiro/services/posts"
|
"lmika.dev/lmika/weiro/services/posts"
|
||||||
"lmika.dev/lmika/weiro/services/publisher"
|
"lmika.dev/lmika/weiro/services/publisher"
|
||||||
"lmika.dev/lmika/weiro/services/sites"
|
"lmika.dev/lmika/weiro/services/sites"
|
||||||
|
|
@ -23,6 +24,7 @@ type Services struct {
|
||||||
Sites *sites.Service
|
Sites *sites.Service
|
||||||
Uploads *uploads.Service
|
Uploads *uploads.Service
|
||||||
Categories *categories.Service
|
Categories *categories.Service
|
||||||
|
Pages *pages.Service
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(cfg config.Config) (*Services, error) {
|
func New(cfg config.Config) (*Services, error) {
|
||||||
|
|
@ -40,6 +42,7 @@ func New(cfg config.Config) (*Services, error) {
|
||||||
siteService := sites.New(dbp)
|
siteService := sites.New(dbp)
|
||||||
uploadService := uploads.New(dbp, ufp, filepath.Join(cfg.ScratchDir, "uploads", "pending"))
|
uploadService := uploads.New(dbp, ufp, filepath.Join(cfg.ScratchDir, "uploads", "pending"))
|
||||||
categoriesService := categories.New(dbp, publisherQueue)
|
categoriesService := categories.New(dbp, publisherQueue)
|
||||||
|
pagesService := pages.New(dbp, publisherQueue)
|
||||||
|
|
||||||
return &Services{
|
return &Services{
|
||||||
DB: dbp,
|
DB: dbp,
|
||||||
|
|
@ -50,6 +53,7 @@ func New(cfg config.Config) (*Services, error) {
|
||||||
Sites: siteService,
|
Sites: siteService,
|
||||||
Uploads: uploadService,
|
Uploads: uploadService,
|
||||||
Categories: categoriesService,
|
Categories: categoriesService,
|
||||||
|
Pages: pagesService,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue