package blog import ( "net/http" "strconv" "time" "git.golem15.com/golem15/summercms/docs/examples/blog/models" "git.golem15.com/golem15/summercms/modules/lagoon" "git.golem15.com/golem15/summercms/modules/pact" "git.golem15.com/golem15/summercms/modules/wire" "gorm.io/gorm" ) var _ pact.HasRoutes = (*Plugin)(nil) // Routes replaces routes.php. func (p *Plugin) Routes(r pact.Router) error { r.Get("/api/blog/posts", p.listPosts) return nil } // postJSON is the response shape of one post. It is built field by field, // so a column added to the model never leaks into the API. type postJSON struct { ID uint `json:"id"` Title string `json:"title"` Slug string `json:"slug"` Body string `json:"body"` PublishedAt wire.Time `json:"published_at"` } // listPosts answers GET /api/blog/posts?page=N&per_page=M with one page of // published posts, newest first, in the {data, meta} shape of Laravel's // paginator. Drafts, whose published_at is NULL, are never listed. func (p *Plugin) listPosts(w http.ResponseWriter, r *http.Request) { db, ok := p.app.Lookup[*gorm.DB]() if !ok { wire.WriteJSON(w, http.StatusServiceUnavailable, map[string]string{"message": "database unavailable"}) return } page := queryInt(r, "page", 1, 1, 10000) perPage := queryInt(r, "per_page", p.app.Config.Int("acme.blog.per_page"), 1, 100) q := db.WithContext(r.Context()).Model(&models.Post{}).Where("published_at IS NOT NULL") var total int64 if err := q.Count(&total).Error; err != nil { wire.WriteOpaque500(w) return } var posts []models.Post if err := q.Order("published_at DESC, id DESC").Offset((page - 1) * perPage).Limit(perPage).Find(&posts).Error; err != nil { wire.WriteOpaque500(w) return } rows := make([]postJSON, 0, len(posts)) for _, post := range posts { rows = append(rows, postJSON{ ID: post.ID, Title: post.Title, Slug: post.Slug, Body: post.Body, PublishedAt: wire.Time{Time: post.PublishedAt.UTC().Truncate(time.Second)}, }) } wire.WriteJSON(w, http.StatusOK, lagoon.Paginate(rows, page, perPage, total)) } // queryInt reads an integer query parameter, falling back to def when it is // missing or not a number, and clamps it to [lo, hi]. func queryInt(r *http.Request, name string, def, lo, hi int) int { n, err := strconv.Atoi(r.URL.Query().Get(name)) if err != nil { n = def } return min(max(n, lo), hi) }