From 1edcd7686cf9ee9cc5c317c97d05d91912949b43 Mon Sep 17 00:00:00 2001 From: Leon Mika Date: Sun, 22 Mar 2026 18:01:36 +1100 Subject: [PATCH] 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 --- models/errors.go | 2 +- services/pages/service.go | 189 ++++++++++++++++++++++++++++++++++++++ services/services.go | 4 + 3 files changed, 194 insertions(+), 1 deletion(-) create mode 100644 services/pages/service.go diff --git a/models/errors.go b/models/errors.go index eda780c..3efadbc 100644 --- a/models/errors.go +++ b/models/errors.go @@ -7,4 +7,4 @@ var PermissionError = errors.New("permission denied") var NotFoundError = errors.New("not found") var SiteRequiredError = errors.New("site required") 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") diff --git a/services/pages/service.go b/services/pages/service.go new file mode 100644 index 0000000..37c4144 --- /dev/null +++ b/services/pages/service.go @@ -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 +} diff --git a/services/services.go b/services/services.go index beb6727..852dea3 100644 --- a/services/services.go +++ b/services/services.go @@ -8,6 +8,7 @@ import ( "lmika.dev/lmika/weiro/providers/uploadfiles" "lmika.dev/lmika/weiro/services/auth" "lmika.dev/lmika/weiro/services/categories" + "lmika.dev/lmika/weiro/services/pages" "lmika.dev/lmika/weiro/services/posts" "lmika.dev/lmika/weiro/services/publisher" "lmika.dev/lmika/weiro/services/sites" @@ -23,6 +24,7 @@ type Services struct { Sites *sites.Service Uploads *uploads.Service Categories *categories.Service + Pages *pages.Service } func New(cfg config.Config) (*Services, error) { @@ -40,6 +42,7 @@ func New(cfg config.Config) (*Services, error) { siteService := sites.New(dbp) uploadService := uploads.New(dbp, ufp, filepath.Join(cfg.ScratchDir, "uploads", "pending")) categoriesService := categories.New(dbp, publisherQueue) + pagesService := pages.New(dbp, publisherQueue) return &Services{ DB: dbp, @@ -50,6 +53,7 @@ func New(cfg config.Config) (*Services, error) { Sites: siteService, Uploads: uploadService, Categories: categoriesService, + Pages: pagesService, }, nil }