content page
This commit is contained in:
parent
fd2877da9e
commit
36b2c56987
14 changed files with 590 additions and 20 deletions
|
|
@ -28,10 +28,19 @@ type Content struct {
|
|||
Slug, Title, Summary, Body string
|
||||
Status string
|
||||
Published bool
|
||||
CategoryID uint
|
||||
Category string
|
||||
Badges []string
|
||||
CreatedAt, UpdatedAt time.Time
|
||||
CreatedBy, UpdatedBy uint
|
||||
}
|
||||
|
||||
type Category struct {
|
||||
ID uint
|
||||
Slug string
|
||||
Name string
|
||||
}
|
||||
|
||||
type ContentHistory struct {
|
||||
ID uint
|
||||
ContentID uint
|
||||
|
|
|
|||
|
|
@ -9,7 +9,11 @@ func UserFromDomain(d domain.User) UserModel {
|
|||
return UserModel{ID: d.ID, Email: d.Email, Name: d.Name, Provider: d.Provider, ProviderSubject: d.ProviderSubject, Role: d.Role, Editor: d.Editor, CreatedAt: d.CreatedAt}
|
||||
}
|
||||
func ContentToDomain(m ContentModel) domain.Content {
|
||||
return domain.Content{ID: m.ID, Slug: m.Slug, Title: m.Title, Summary: m.Summary, Body: m.Body, Status: m.Status, Published: m.Published, CreatedAt: m.CreatedAt, UpdatedAt: m.UpdatedAt, CreatedBy: m.CreatedBy, UpdatedBy: m.UpdatedBy}
|
||||
badges := make([]string, len(m.Badges))
|
||||
for i := range m.Badges {
|
||||
badges[i] = m.Badges[i].Name
|
||||
}
|
||||
return domain.Content{ID: m.ID, Slug: m.Slug, Title: m.Title, Summary: m.Summary, Body: m.Body, Status: m.Status, Published: m.Published, CategoryID: m.CategoryID, Category: m.Category.Name, Badges: badges, CreatedAt: m.CreatedAt, UpdatedAt: m.UpdatedAt, CreatedBy: m.CreatedBy, UpdatedBy: m.UpdatedBy}
|
||||
}
|
||||
func ContentFromDomain(d domain.Content) ContentModel {
|
||||
return ContentModel{ID: d.ID, Slug: d.Slug, Title: d.Title, Summary: d.Summary, Body: d.Body, Status: d.Status, Published: d.Published, CreatedAt: d.CreatedAt, UpdatedAt: d.UpdatedAt, CreatedBy: d.CreatedBy, UpdatedBy: d.UpdatedBy}
|
||||
|
|
|
|||
|
|
@ -21,10 +21,28 @@ type ContentModel struct {
|
|||
Body string `gorm:"not null"`
|
||||
Status string `gorm:"not null;default:'draft'"`
|
||||
Published bool `gorm:"not null;default:false"`
|
||||
CategoryID uint
|
||||
Category CategoryModel `gorm:"foreignKey:CategoryID"`
|
||||
Badges []BadgeModel `gorm:"many2many:content_badges;joinForeignKey:ContentID;joinReferences:BadgeID"`
|
||||
CreatedAt, UpdatedAt time.Time
|
||||
CreatedBy, UpdatedBy uint
|
||||
}
|
||||
|
||||
type CategoryModel struct {
|
||||
ID uint `gorm:"primaryKey"`
|
||||
Slug string `gorm:"uniqueIndex;not null"`
|
||||
Name string `gorm:"not null"`
|
||||
}
|
||||
|
||||
func (CategoryModel) TableName() string { return "categories" }
|
||||
|
||||
type BadgeModel struct {
|
||||
ID uint `gorm:"primaryKey"`
|
||||
Name string `gorm:"uniqueIndex;not null"`
|
||||
}
|
||||
|
||||
func (BadgeModel) TableName() string { return "badges" }
|
||||
|
||||
func (ContentModel) TableName() string { return "content" }
|
||||
|
||||
type ContentHistoryModel struct {
|
||||
|
|
|
|||
|
|
@ -63,18 +63,36 @@ func (r *GORMSessionRepository) FindValid(ctx context.Context, t string) (domain
|
|||
type GORMContentRepository struct{ db *gorm.DB }
|
||||
|
||||
func NewContentRepository(db *gorm.DB) IContentRepository { return &GORMContentRepository{db} }
|
||||
func (r *GORMContentRepository) List(ctx context.Context, limit, offset int) ([]domain.Content, error) {
|
||||
func (r *GORMContentRepository) List(ctx context.Context, limit, offset int, category string, badges []string) ([]domain.Content, error) {
|
||||
var rows []persistence.ContentModel
|
||||
err := r.db.WithContext(ctx).Order("updated_at DESC, created_at DESC, id DESC").Limit(limit).Offset(offset).Find(&rows).Error
|
||||
query := r.db.WithContext(ctx).Model(&persistence.ContentModel{}).Preload("Category").Preload("Badges")
|
||||
if category != "" {
|
||||
query = query.Where("category_id IN (SELECT id FROM categories WHERE slug = ?)", category)
|
||||
}
|
||||
for _, badge := range badges {
|
||||
query = query.Where("EXISTS (SELECT 1 FROM content_badges cb JOIN badges b ON b.id = cb.badge_id WHERE cb.content_id = content.id AND b.name = ?)", badge)
|
||||
}
|
||||
err := query.Order("updated_at DESC, created_at DESC, id DESC").Limit(limit).Offset(offset).Find(&rows).Error
|
||||
out := make([]domain.Content, len(rows))
|
||||
for i := range rows {
|
||||
out[i] = persistence.ContentToDomain(rows[i])
|
||||
}
|
||||
return out, err
|
||||
}
|
||||
func (r *GORMContentRepository) ListCategories(ctx context.Context) ([]domain.Category, error) {
|
||||
var rows []persistence.CategoryModel
|
||||
if err := r.db.WithContext(ctx).Order("name ASC").Find(&rows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := make([]domain.Category, len(rows))
|
||||
for i := range rows {
|
||||
out[i] = domain.Category{ID: rows[i].ID, Slug: rows[i].Slug, Name: rows[i].Name}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
func (r *GORMContentRepository) FindBySlug(ctx context.Context, slug string) (domain.Content, error) {
|
||||
var row persistence.ContentModel
|
||||
if err := r.db.WithContext(ctx).Where("slug = ?", slug).First(&row).Error; err != nil {
|
||||
if err := r.db.WithContext(ctx).Preload("Category").Preload("Badges").Where("slug = ?", slug).First(&row).Error; err != nil {
|
||||
return domain.Content{}, err
|
||||
}
|
||||
return persistence.ContentToDomain(row), nil
|
||||
|
|
|
|||
|
|
@ -19,7 +19,8 @@ type ISessionRepository interface {
|
|||
FindValid(context.Context, string) (domain.Session, error)
|
||||
}
|
||||
type IContentRepository interface {
|
||||
List(context.Context, int, int) ([]domain.Content, error)
|
||||
List(context.Context, int, int, string, []string) ([]domain.Content, error)
|
||||
ListCategories(context.Context) ([]domain.Category, error)
|
||||
FindBySlug(context.Context, string) (domain.Content, error)
|
||||
Update(context.Context, domain.Content, domain.ContentHistory) error
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,8 @@ import (
|
|||
)
|
||||
|
||||
type IContentService interface {
|
||||
List(context.Context, int, int) ([]domain.Content, error)
|
||||
List(context.Context, int, int, string, []string) ([]domain.Content, error)
|
||||
ListCategories(context.Context) ([]domain.Category, error)
|
||||
Get(context.Context, string) (domain.Content, error)
|
||||
Update(context.Context, domain.Content, domain.Session) (domain.Content, error)
|
||||
}
|
||||
|
|
@ -24,7 +25,7 @@ type ContentService struct {
|
|||
func NewContentService(r repository.IContentRepository, a *AuthService) *ContentService {
|
||||
return &ContentService{repo: r, auth: a}
|
||||
}
|
||||
func (s *ContentService) List(ctx context.Context, limit, offset int) ([]domain.Content, error) {
|
||||
func (s *ContentService) List(ctx context.Context, limit, offset int, category string, badges []string) ([]domain.Content, error) {
|
||||
if limit < 1 {
|
||||
limit = 20
|
||||
}
|
||||
|
|
@ -34,7 +35,10 @@ func (s *ContentService) List(ctx context.Context, limit, offset int) ([]domain.
|
|||
if offset < 0 {
|
||||
offset = 0
|
||||
}
|
||||
return s.repo.List(ctx, limit, offset)
|
||||
return s.repo.List(ctx, limit, offset, category, badges)
|
||||
}
|
||||
func (s *ContentService) ListCategories(ctx context.Context) ([]domain.Category, error) {
|
||||
return s.repo.ListCategories(ctx)
|
||||
}
|
||||
func (s *ContentService) Get(ctx context.Context, slug string) (domain.Content, error) {
|
||||
if !validSlug(slug) {
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ func (h *Handler) Routes(origins string) http.Handler {
|
|||
mux.HandleFunc("/api/test/protected", h.protected)
|
||||
mux.HandleFunc("/api/protected", h.protected)
|
||||
mux.HandleFunc("/api/content", h.contentList)
|
||||
mux.HandleFunc("/api/content/categories", h.contentCategories)
|
||||
mux.HandleFunc("/api/content/", h.contentDetail)
|
||||
return cors(mux, origins)
|
||||
}
|
||||
|
|
@ -171,13 +172,43 @@ func (h *Handler) contentList(w http.ResponseWriter, r *http.Request) {
|
|||
limit, offset := 20, 0
|
||||
_, _ = fmt.Sscanf(r.URL.Query().Get("limit"), "%d", &limit)
|
||||
_, _ = fmt.Sscanf(r.URL.Query().Get("offset"), "%d", &offset)
|
||||
items, e := h.content.List(r.Context(), limit, offset)
|
||||
badges := r.URL.Query()["badge"]
|
||||
if len(badges) == 0 {
|
||||
badges = strings.Split(strings.TrimSpace(r.URL.Query().Get("badges")), ",")
|
||||
}
|
||||
filtered := badges[:0]
|
||||
for _, badge := range badges {
|
||||
if badge = strings.TrimSpace(strings.ToLower(badge)); badge != "" {
|
||||
filtered = append(filtered, badge)
|
||||
}
|
||||
}
|
||||
items, e := h.content.List(r.Context(), limit, offset, strings.TrimSpace(strings.ToLower(r.URL.Query().Get("category"))), filtered)
|
||||
if e != nil {
|
||||
jsonWrite(w, 500, map[string]string{"error": e.Error()})
|
||||
return
|
||||
}
|
||||
jsonWrite(w, 200, map[string]any{"items": items, "limit": limit, "offset": offset})
|
||||
}
|
||||
func (h *Handler) contentCategories(w http.ResponseWriter, r *http.Request) {
|
||||
if h.content == nil {
|
||||
jsonWrite(w, 503, map[string]string{"error": "content unavailable"})
|
||||
return
|
||||
}
|
||||
if _, ok := h.sessionForRequest(r); !ok {
|
||||
jsonWrite(w, 401, map[string]string{"error": "authentication required"})
|
||||
return
|
||||
}
|
||||
if r.Method != http.MethodGet {
|
||||
jsonWrite(w, http.StatusMethodNotAllowed, nil)
|
||||
return
|
||||
}
|
||||
categories, err := h.content.ListCategories(r.Context())
|
||||
if err != nil {
|
||||
jsonWrite(w, 500, map[string]string{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
jsonWrite(w, 200, map[string]any{"items": categories})
|
||||
}
|
||||
func (h *Handler) contentDetail(w http.ResponseWriter, r *http.Request) {
|
||||
if h.content == nil {
|
||||
jsonWrite(w, 503, map[string]string{"error": "content unavailable"})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue