Files
sm-sitemap-plugin/classes/listing.go
Jakub Zych 6b67ab759b feat: contract.Rebuilder asks for a debounced rebuild
The sitemap publishes classes.Listing as contract.Rebuilder too, so a
plugin whose Filter reads data of its own can queue the debounced rebuild
a record event queues when that data changes.
2026-11-04 22:39:14 +01:00

167 lines
4.7 KiB
Go

package classes
import (
"context"
"strconv"
"strings"
"git.golem15.com/golem15/sm-sitemap-plugin/contract"
"git.golem15.com/golem15/sm-sitemap-plugin/models"
"git.golem15.com/golem15/summercms/modules/backpack"
"git.golem15.com/golem15/summercms/modules/signpost"
"gorm.io/gorm"
)
// Listing implements contract.Listing over the slot table and the
// settings (D-95): other plugins read the site address, the index address,
// the file groups and each group's listed addresses without importing the
// sitemap plugin.
type Listing struct {
App *backpack.App
}
var (
_ contract.Listing = Listing{}
_ contract.Rebuilder = Listing{}
)
// The page sizes of Entries.
const (
defaultEntriesLimit = 100
maxEntriesLimit = 1000
)
func (l Listing) db(ctx context.Context) (*gorm.DB, error) {
if l.App == nil {
return nil, ErrNoDatabase
}
db, ok := l.App.Lookup[*gorm.DB]()
if !ok || db == nil {
return nil, ErrNoDatabase
}
return db.WithContext(ctx), nil
}
// RequestRebuild queues one debounced rebuild through
// RequestDebouncedRebuild, the same one a record event queues
// (contract.Rebuilder).
func (l Listing) RequestRebuild(ctx context.Context) error {
db, err := l.db(ctx)
if err != nil {
return err
}
return RequestDebouncedRebuild(ctx, l.App, db)
}
// Settings answers the site address, the index address, the default
// locale and the groups with listed addresses (by key), with their titles
// (a content type's saved HTML sitemap headings, else the titles of the
// published Filter's group of that key), the HTML sitemap settings and the
// IndexNow key file name (while IndexNow is on with a key), and the
// content types switched off.
func (l Listing) Settings(ctx context.Context) (contract.ListingSettings, error) {
var out contract.ListingSettings
db, err := l.db(ctx)
if err != nil {
return out, err
}
settings, err := LoadSettings(db)
if err != nil {
return out, err
}
out.SiteURL = SiteURL(l.App, settings)
if out.SiteURL != "" {
out.IndexURL = out.SiteURL + "/sitemap.xml"
}
out.DefaultLocale = models.Text(settings.DefaultLocale)
var counts []struct {
Grp string `gorm:"column:grp"`
N int `gorm:"column:n"`
}
if err := db.Raw(`SELECT grp, COUNT(*) AS n FROM golem15_sitemap_urls WHERE excluded_reason = '' GROUP BY grp ORDER BY grp`).Scan(&counts).Error; err != nil {
return out, err
}
titles, _, _, err := groupTitles(ctx, l.App, db)
if err != nil {
return out, err
}
out.Groups = make([]contract.GroupInfo, 0, len(counts))
for _, c := range counts {
out.Groups = append(out.Groups, contract.GroupInfo{Key: c.Grp, Slug: c.Grp, Titles: titles[c.Grp], Count: c.N})
}
out.HTMLEnabled = settings.HTMLEnabled
out.HTMLPaths = settings.EffectiveHTMLPaths()
out.HTMLPageSize = settings.EffectiveHTMLPageSize()
out.KeyFile = KeyFileName(ActiveKey(settings))
out.TypesOff = []string{}
if reg, ok := l.App.Lookup[*signpost.Registry](); ok && reg != nil {
rows, err := LoadTypeSettings(db)
if err != nil {
return out, err
}
for _, typ := range reg.ContentTypes() {
if !EffectiveType(typ, rows).Include {
out.TypesOff = append(out.TypesOff, typ.Key)
}
}
}
return out, nil
}
// Entries pages a group's listed addresses in the order they were stored
// (row id); Next is the last row id of a page that has a successor. A
// Locale keeps the addresses of that locale and those without one.
func (l Listing) Entries(ctx context.Context, q contract.EntriesQuery) (contract.EntriesPage, error) {
page := contract.EntriesPage{Entries: []contract.Entry{}}
db, err := l.db(ctx)
if err != nil {
return page, err
}
settings, err := LoadSettings(db)
if err != nil {
return page, err
}
site := SiteURL(l.App, settings)
limit := q.Limit
if limit <= 0 {
limit = defaultEntriesLimit
}
limit = min(limit, maxEntriesLimit)
scope := func() *gorm.DB {
s := db.Model(&models.URL{}).Where("grp = ? AND excluded_reason = ''", q.Group)
if loc := strings.TrimSpace(q.Locale); loc != "" {
s = s.Where("locale = ? OR locale = ''", loc)
}
return s
}
var total int64
if err := scope().Count(&total).Error; err != nil {
return page, err
}
page.Total = int(total)
q2 := scope()
if after, err := strconv.ParseUint(strings.TrimSpace(q.After), 10, 64); err == nil && after > 0 {
q2 = q2.Where("id > ?", after)
}
var rows []models.URL
if err := q2.Order("id ASC").Limit(limit + 1).Find(&rows).Error; err != nil {
return page, err
}
if len(rows) > limit {
rows = rows[:limit]
page.Next = strconv.FormatUint(rows[len(rows)-1].ID, 10)
}
for _, r := range rows {
page.Entries = append(page.Entries, contract.Entry{
Group: r.Grp,
TypeKey: r.TypeKey,
RecordID: r.RecordID,
Locale: r.Locale,
URL: Absolute(site, r.URL),
Title: r.Title,
LastMod: r.LastMod,
})
}
return page, nil
}