content: add Content.ListPosts capability for blog listing
Adds ListPosts(ctx, ListPostsParams) to the content.Content capability so
wasm plugins can list posts with bodies — the per-plugin Postgres role
denies direct public.blog_posts reads (42501) and GetPost only fetches a
single post's metadata. Extends PostInfo with Body, AuthorName, AuthorSlug
and PublishedAt (additive; existing get_post golden unchanged). Wires all
layers: interface, ABI PostInfo/ContentListPosts{Request,Response} messages
(buf breaking clean), guest stub, and a deterministic content_list_posts
golden replayed by the cms host parity test.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
314a25353d
commit
e663b77b47
@ -98,6 +98,23 @@ message PostInfo {
|
||||
string excerpt = 4;
|
||||
string featured_image_url = 5;
|
||||
string author_id = 6; // UUID
|
||||
string body = 7; // rendered HTML; set by get_post / list_posts(include_body)
|
||||
string author_name = 8;
|
||||
string author_slug = 9;
|
||||
google.protobuf.Timestamp published_at = 10;
|
||||
}
|
||||
|
||||
message ContentListPostsRequest {
|
||||
int32 limit = 1;
|
||||
int32 offset = 2;
|
||||
string category = 3; // optional category slug filter ("" = all)
|
||||
bool published_only = 4;
|
||||
bool include_body = 5;
|
||||
bool include_excerpt = 6;
|
||||
}
|
||||
|
||||
message ContentListPostsResponse {
|
||||
repeated PostInfo posts = 1;
|
||||
}
|
||||
|
||||
message ContentSlugifyRequest {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -2,6 +2,7 @@ package content
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
@ -24,14 +25,33 @@ type PageInfo struct {
|
||||
Title string
|
||||
}
|
||||
|
||||
// PostInfo is a simplified post representation for plugins.
|
||||
// PostInfo is a simplified post representation for plugins. It carries enough
|
||||
// for both a blog index (title/slug/excerpt/author/date/featured image) and a
|
||||
// post detail view (Body — the rendered HTML). Body is populated by GetPost and
|
||||
// by ListPosts when ListPostsParams.IncludeBody is set; it is empty otherwise.
|
||||
type PostInfo struct {
|
||||
ID uuid.UUID
|
||||
Slug string
|
||||
Title string
|
||||
Excerpt string
|
||||
Body string // rendered HTML; populated by GetPost / ListPosts(IncludeBody)
|
||||
FeaturedImageURL string
|
||||
AuthorID uuid.UUID
|
||||
AuthorName string
|
||||
AuthorSlug string
|
||||
PublishedAt time.Time
|
||||
}
|
||||
|
||||
// ListPostsParams filters and shapes a ListPosts query. The zero value lists
|
||||
// published posts from the start with no category filter, excerpts included and
|
||||
// bodies omitted (the cheap blog-index shape).
|
||||
type ListPostsParams struct {
|
||||
Limit int // 0 = host default
|
||||
Offset int // pagination offset
|
||||
Category string // optional category slug filter ("" = all)
|
||||
PublishedOnly bool // reserved: plugins only ever see published posts
|
||||
IncludeBody bool // render each post's body to HTML (expensive)
|
||||
IncludeExcerpt bool // include the post excerpt
|
||||
}
|
||||
|
||||
// Content provides content access for plugins.
|
||||
@ -40,6 +60,7 @@ type Content interface {
|
||||
GetAuthorProfile(ctx context.Context, id uuid.UUID) (*AuthorProfile, error)
|
||||
GetPage(ctx context.Context, slug string) (*PageInfo, error)
|
||||
GetPost(ctx context.Context, slug string) (*PostInfo, error)
|
||||
ListPosts(ctx context.Context, params ListPostsParams) ([]PostInfo, error)
|
||||
Slugify(text string) string
|
||||
BlockNoteToHTML(ctx context.Context, doc map[string]any) string
|
||||
GenerateExcerpt(html string, maxLen int) string
|
||||
|
||||
@ -12,6 +12,7 @@ import (
|
||||
|
||||
abiv1 "git.dev.alexdunmow.com/block/core/abi/v1"
|
||||
"git.dev.alexdunmow.com/block/core/ai"
|
||||
"git.dev.alexdunmow.com/block/core/content"
|
||||
"git.dev.alexdunmow.com/block/core/gating"
|
||||
"git.dev.alexdunmow.com/block/core/plugin"
|
||||
"github.com/google/uuid"
|
||||
@ -188,6 +189,33 @@ func TestCapabilityRoundTrip(t *testing.T) {
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "content_list_posts", wantMethod: "content.list_posts",
|
||||
resp: &abiv1.ContentListPostsResponse{Posts: []*abiv1.PostInfo{
|
||||
{
|
||||
Id: idAuthor.String(), Slug: "first", Title: "First", Excerpt: "one",
|
||||
FeaturedImageUrl: "media:1", AuthorId: idAuthor.String(),
|
||||
AuthorName: "Ada", AuthorSlug: "ada", PublishedAt: timestamppb.New(planTime),
|
||||
},
|
||||
{
|
||||
Id: idPlan.String(), Slug: "second", Title: "Second", Excerpt: "two",
|
||||
AuthorId: idAuthor.String(), AuthorName: "Ada", AuthorSlug: "ada",
|
||||
PublishedAt: timestamppb.New(planTime),
|
||||
},
|
||||
}},
|
||||
run: func(t *testing.T, cs plugin.CoreServices) {
|
||||
got, err := cs.Content.ListPosts(ctx, content.ListPostsParams{Limit: 10, IncludeExcerpt: true})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(got) != 2 || got[0].Slug != "first" || got[0].AuthorName != "Ada" {
|
||||
t.Fatalf("posts = %+v", got)
|
||||
}
|
||||
if got[1].Slug != "second" || !got[1].PublishedAt.Equal(planTime) {
|
||||
t.Errorf("post[1] = %+v", got[1])
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "content_slugify", wantMethod: "content.slugify",
|
||||
resp: &abiv1.ContentSlugifyResponse{Slug: "hello-world"},
|
||||
|
||||
@ -3,6 +3,7 @@ package caps
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
abiv1 "git.dev.alexdunmow.com/block/core/abi/v1"
|
||||
"git.dev.alexdunmow.com/block/core/content"
|
||||
@ -55,14 +56,49 @@ func (s *contentStub) GetPost(ctx context.Context, slug string) (*content.PostIn
|
||||
if p == nil {
|
||||
return nil, nil
|
||||
}
|
||||
return &content.PostInfo{
|
||||
info := postInfoFromProto(p)
|
||||
return &info, nil
|
||||
}
|
||||
|
||||
func (s *contentStub) ListPosts(ctx context.Context, params content.ListPostsParams) ([]content.PostInfo, error) {
|
||||
req := &abiv1.ContentListPostsRequest{
|
||||
Limit: int32(params.Limit),
|
||||
Offset: int32(params.Offset),
|
||||
Category: params.Category,
|
||||
PublishedOnly: params.PublishedOnly,
|
||||
IncludeBody: params.IncludeBody,
|
||||
IncludeExcerpt: params.IncludeExcerpt,
|
||||
}
|
||||
resp := &abiv1.ContentListPostsResponse{}
|
||||
if err := s.invoke(ctx, "list_posts", req, resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := make([]content.PostInfo, 0, len(resp.GetPosts()))
|
||||
for _, p := range resp.GetPosts() {
|
||||
out = append(out, postInfoFromProto(p))
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// postInfoFromProto decodes an abiv1.PostInfo into the SDK content.PostInfo,
|
||||
// shared by GetPost and ListPosts.
|
||||
func postInfoFromProto(p *abiv1.PostInfo) content.PostInfo {
|
||||
var publishedAt time.Time
|
||||
if ts := p.GetPublishedAt(); ts != nil {
|
||||
publishedAt = ts.AsTime()
|
||||
}
|
||||
return content.PostInfo{
|
||||
ID: parseUUID(p.GetId()),
|
||||
Slug: p.GetSlug(),
|
||||
Title: p.GetTitle(),
|
||||
Excerpt: p.GetExcerpt(),
|
||||
Body: p.GetBody(),
|
||||
FeaturedImageURL: p.GetFeaturedImageUrl(),
|
||||
AuthorID: parseUUID(p.GetAuthorId()),
|
||||
}, nil
|
||||
AuthorName: p.GetAuthorName(),
|
||||
AuthorSlug: p.GetAuthorSlug(),
|
||||
PublishedAt: publishedAt,
|
||||
}
|
||||
}
|
||||
|
||||
// Slugify has no error channel in the interface; a transport failure degrades
|
||||
|
||||
2
plugin/wasmguest/caps/testdata/golden/content_list_posts_req.pb
vendored
Normal file
2
plugin/wasmguest/caps/testdata/golden/content_list_posts_req.pb
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
|
||||
0
|
||||
5
plugin/wasmguest/caps/testdata/golden/content_list_posts_resp.pb
vendored
Normal file
5
plugin/wasmguest/caps/testdata/golden/content_list_posts_resp.pb
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
|
||||
z
|
||||
$22222222-2222-2222-2222-222222222222firstFirst"one*media:12$22222222-2222-2222-2222-222222222222BAdaJadaRÀÈžÒ
|
||||
s
|
||||
$88888888-8888-8888-8888-888888888888secondSecond"two2$22222222-2222-2222-2222-222222222222BAdaJadaRÀÈžÒ
|
||||
Loading…
x
Reference in New Issue
Block a user