feat(15-03): add journal media upload, RSS, and off-by-default Typesense gate
- POST /_journal/api/v1/media/upload requires access_posts and stays under journal/ - Anonymous GET rss is RSS 2.0 honoring rss_* settings; unpublished posts are omitted - beachcomber Gate reads search_use_typesense (default false) so a fresh save makes no Typesense HTTP Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
80
models/post_search.go
Normal file
80
models/post_search.go
Normal file
@@ -0,0 +1,80 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"git.golem15.com/golem15/summercms/modules/beachcomber"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
var _ beachcomber.Searchable = (*Post)(nil)
|
||||
|
||||
// SearchableAs is the Typesense collection name (D-12).
|
||||
func (Post) SearchableAs() string { return "golem15_journal_posts" }
|
||||
|
||||
// ShouldBeSearchable is false when the post is unpublished or the
|
||||
// beachcomber Gate (search_use_typesense) is off.
|
||||
func (p *Post) ShouldBeSearchable() bool {
|
||||
if p == nil || !p.Published || p.PublishedAt == nil || !p.PublishedAt.Before(time.Now()) {
|
||||
return false
|
||||
}
|
||||
return SearchGateEnabled()
|
||||
}
|
||||
|
||||
// ToSearchableArray builds the Typesense document after commit.
|
||||
func (p *Post) ToSearchableArray(ctx context.Context, db *gorm.DB) (map[string]any, error) {
|
||||
if p == nil {
|
||||
return nil, nil
|
||||
}
|
||||
published := 0
|
||||
if p.Published && p.PublishedAt != nil && p.PublishedAt.Before(time.Now()) {
|
||||
published = 1
|
||||
}
|
||||
pinned := 0
|
||||
if p.IsPinned {
|
||||
pinned = 1
|
||||
}
|
||||
var publishedAt int64
|
||||
if p.PublishedAt != nil {
|
||||
publishedAt = p.PublishedAt.Unix()
|
||||
}
|
||||
var createdAt int64
|
||||
if p.CreatedAt != nil {
|
||||
createdAt = p.CreatedAt.Unix()
|
||||
}
|
||||
userID := 0
|
||||
if p.UserID != nil {
|
||||
userID = int(*p.UserID)
|
||||
}
|
||||
catIDs := make([]int, 0, len(p.Categories))
|
||||
tagIDs := make([]int, 0, len(p.Tags))
|
||||
if db != nil {
|
||||
var cats []PostCategory
|
||||
if err := db.WithContext(ctx).Where("post_id = ?", p.ID).Find(&cats).Error; err == nil {
|
||||
for _, c := range cats {
|
||||
catIDs = append(catIDs, int(c.CategoryID))
|
||||
}
|
||||
}
|
||||
var tags []PostTag
|
||||
if err := db.WithContext(ctx).Where("post_id = ?", p.ID).Find(&tags).Error; err == nil {
|
||||
for _, t := range tags {
|
||||
tagIDs = append(tagIDs, int(t.TagID))
|
||||
}
|
||||
}
|
||||
}
|
||||
return map[string]any{
|
||||
"id": strconv.FormatUint(uint64(p.ID), 10),
|
||||
"published": published,
|
||||
"published_at": publishedAt,
|
||||
"is_pinned": pinned,
|
||||
"user_id": userID,
|
||||
"created_at": createdAt,
|
||||
"category_ids": catIDs,
|
||||
"tag_ids": tagIDs,
|
||||
"title": p.Title,
|
||||
"excerpt": p.Excerpt,
|
||||
"content": p.Content,
|
||||
}, nil
|
||||
}
|
||||
12
models/search_gate.go
Normal file
12
models/search_gate.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package models
|
||||
|
||||
import "sync/atomic"
|
||||
|
||||
var searchGate atomic.Bool
|
||||
|
||||
// SetSearchGate records whether beachcomber.Gate last read search_use_typesense
|
||||
// as on. Read errors and a missing row count as off.
|
||||
func SetSearchGate(on bool) { searchGate.Store(on) }
|
||||
|
||||
// SearchGateEnabled reports the last installed search gate value.
|
||||
func SearchGateEnabled() bool { return searchGate.Load() }
|
||||
Reference in New Issue
Block a user