- make:admin-controller, make:command and make:migration output, finished: the Posts controller serves models.Post behind acme.blog.access_posts, WinterCMS-style form and list YAML embedded through pact.AdminAssets, blog:publish sets published_at by slug with a bound parameter - the posts route lists published posts only, newest first - Docker tests migrate an ICU pl-PL database, roll back published_at, serve the route and run blog:publish with a published and an opened database - the page gains the admin controller, console command and added-column sections
78 lines
2.4 KiB
Go
78 lines
2.4 KiB
Go
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)
|
|
}
|