Add nested posts UI (#5)
## Summary - Cut hunt, question, submission, voting, hiding, and profile flows over to unified posts - Render nested replies with permission-aware inline Reply/Edit controls and edited markers - Add post profile queries and the author index migration they depend on Co-authored-by: codegirl-007 <s.raide@gmail.com>
This commit was merged in pull request #5.
This commit is contained in:
@@ -438,6 +438,21 @@ func (m *Memory) GetPostThread(_ context.Context, rootID string) (*Post, error)
|
||||
return buildPostTree(posts, rootID)
|
||||
}
|
||||
|
||||
func (m *Memory) GetPostThreadForViewer(ctx context.Context, rootID, viewerID string) (*Post, error) {
|
||||
root, err := m.GetPostThread(ctx, rootID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
for _, value := range m.postVotes[rootID] {
|
||||
root.Score += value
|
||||
}
|
||||
root.UserVote = m.postVotes[rootID][viewerID]
|
||||
root.Answered = m.threadContainsAdminReply(rootID)
|
||||
return root, nil
|
||||
}
|
||||
|
||||
func (m *Memory) UpdatePost(_ context.Context, post *Post) error {
|
||||
if post == nil {
|
||||
return fmt.Errorf("%w: post is nil", ErrInvalidPost)
|
||||
@@ -489,6 +504,73 @@ func (m *Memory) ListRootPosts(_ context.Context, postDate, viewerID string) ([]
|
||||
return posts, nil
|
||||
}
|
||||
|
||||
func (m *Memory) ListRootPostsByAuthor(_ context.Context, authorID string) ([]Post, error) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
posts := make([]Post, 0)
|
||||
for _, post := range m.posts {
|
||||
if post.ParentID != nil ||
|
||||
post.AuthorID != authorID ||
|
||||
post.PostState == PostStateHidden {
|
||||
continue
|
||||
}
|
||||
posts = append(posts, *clonePostWithAuthor(post, m.users))
|
||||
}
|
||||
return sortProfilePosts(posts), nil
|
||||
}
|
||||
|
||||
func (m *Memory) ListRootPostsAnsweredBy(_ context.Context, adminID string) ([]Post, error) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
posts := make([]Post, 0)
|
||||
for _, root := range m.posts {
|
||||
if root.ParentID != nil || root.PostState == PostStateHidden {
|
||||
continue
|
||||
}
|
||||
participated := false
|
||||
for _, post := range m.posts {
|
||||
if post.AuthorID == adminID && m.postIsDescendantOf(post, root.ID) {
|
||||
participated = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if participated {
|
||||
posts = append(posts, *clonePostWithAuthor(root, m.users))
|
||||
}
|
||||
}
|
||||
return sortProfilePosts(posts), nil
|
||||
}
|
||||
|
||||
func (m *Memory) SetRootPostState(_ context.Context, id string, state PostState) error {
|
||||
switch state {
|
||||
case PostStateVisible, PostStateHidden, PostStateLocked:
|
||||
default:
|
||||
return fmt.Errorf("%w: invalid post state", ErrInvalidPost)
|
||||
}
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
post, ok := m.posts[id]
|
||||
if !ok || post.ParentID != nil {
|
||||
return sql.ErrNoRows
|
||||
}
|
||||
post.PostState = state
|
||||
post.UpdatedAt = time.Now().UTC().Format(time.RFC3339Nano)
|
||||
return nil
|
||||
}
|
||||
|
||||
func sortProfilePosts(posts []Post) []Post {
|
||||
sort.Slice(posts, func(i, j int) bool {
|
||||
if posts[i].CreatedAt != posts[j].CreatedAt {
|
||||
return posts[i].CreatedAt > posts[j].CreatedAt
|
||||
}
|
||||
return posts[i].ID > posts[j].ID
|
||||
})
|
||||
if len(posts) > ProfileListLimit {
|
||||
posts = posts[:ProfileListLimit]
|
||||
}
|
||||
return posts
|
||||
}
|
||||
|
||||
func (m *Memory) VotePost(_ context.Context, userID, postID string, value int) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
@@ -64,6 +64,9 @@ CREATE TABLE IF NOT EXISTS posts (
|
||||
{name: "index post replies", sql: `
|
||||
CREATE INDEX IF NOT EXISTS idx_posts_parent_created
|
||||
ON posts(parent_id, created_at, id)`},
|
||||
{name: "index post authors", sql: `
|
||||
CREATE INDEX IF NOT EXISTS idx_posts_author_created
|
||||
ON posts(author_id, created_at DESC, id DESC)`},
|
||||
{name: "index root posts", sql: `
|
||||
CREATE INDEX IF NOT EXISTS idx_posts_root_date
|
||||
ON posts(post_date, post_state)
|
||||
@@ -117,6 +120,15 @@ CREATE INDEX IF NOT EXISTS idx_post_votes_post_id
|
||||
return nil
|
||||
}
|
||||
|
||||
func migratePostAuthorIndex(ctx context.Context, exec execContext) error {
|
||||
if _, err := exec.ExecContext(ctx, `
|
||||
CREATE INDEX IF NOT EXISTS idx_posts_author_created
|
||||
ON posts(author_id, created_at DESC, id DESC)`); err != nil {
|
||||
return fmt.Errorf("idx_posts_author_created: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func migratePostDate(ctx context.Context, exec execContext) error {
|
||||
steps := []struct {
|
||||
name string
|
||||
@@ -306,6 +318,7 @@ CREATE TABLE IF NOT EXISTS schema_migrations (
|
||||
{"005_post_vote_post_id_index", migratePostVoteIndex},
|
||||
{"006_post_date", migratePostDate},
|
||||
{"007_post_state", migratePostState},
|
||||
{"008_post_author_index", migratePostAuthorIndex},
|
||||
}
|
||||
for _, m := range migrations {
|
||||
if applied[m.version] {
|
||||
|
||||
@@ -138,6 +138,18 @@ WHERE schemaname = current_schema()
|
||||
if postVoteIndexCount != 1 {
|
||||
t.Fatalf("post vote index count = %d, want 1", postVoteIndexCount)
|
||||
}
|
||||
var postAuthorIndexCount int
|
||||
if err := conn.QueryRowContext(ctx, `
|
||||
SELECT count(*)
|
||||
FROM pg_indexes
|
||||
WHERE schemaname = current_schema()
|
||||
AND tablename = 'posts'
|
||||
AND indexname = 'idx_posts_author_created'`).Scan(&postAuthorIndexCount); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if postAuthorIndexCount != 1 {
|
||||
t.Fatalf("post author index count = %d, want 1", postAuthorIndexCount)
|
||||
}
|
||||
if _, err := conn.ExecContext(ctx, `
|
||||
INSERT INTO post_votes (user_id, post_id, value)
|
||||
VALUES ('homeowner', 'question-1', -1)`); err == nil {
|
||||
@@ -276,6 +288,39 @@ INSERT INTO posts (
|
||||
roots[0].UserVote != 1 {
|
||||
t.Fatalf("root annotations = %+v", roots)
|
||||
}
|
||||
summary, err := queries.GetRootPostVoteSummary(ctx, sqlc.GetRootPostVoteSummaryParams{
|
||||
ViewerID: "plumber",
|
||||
RootID: "question-1",
|
||||
})
|
||||
if err != nil || summary.Score != 2 || summary.UserVote != 1 {
|
||||
t.Fatalf("root vote summary = %+v, %v", summary, err)
|
||||
}
|
||||
byAuthor, err := queries.ListRootPostsByAuthor(ctx, sqlc.ListRootPostsByAuthorParams{
|
||||
AuthorID: "homeowner",
|
||||
HiddenState: string(PostStateHidden),
|
||||
RowLimit: 50,
|
||||
})
|
||||
if err != nil || len(byAuthor) != 1 || byAuthor[0].ID != "question-1" {
|
||||
t.Fatalf("roots by author = %+v, %v", byAuthor, err)
|
||||
}
|
||||
answeredBy, err := queries.ListRootPostsAnsweredBy(ctx, sqlc.ListRootPostsAnsweredByParams{
|
||||
HiddenState: string(PostStateHidden),
|
||||
AdminID: "plumber",
|
||||
RowLimit: 50,
|
||||
})
|
||||
if err != nil || len(answeredBy) != 1 || answeredBy[0].ID != "question-1" {
|
||||
t.Fatalf("roots answered by admin = %+v, %v", answeredBy, err)
|
||||
}
|
||||
for _, state := range []PostState{PostStateLocked, PostStateVisible} {
|
||||
n, err := queries.UpdateRootPostState(ctx, sqlc.UpdateRootPostStateParams{
|
||||
PostState: string(state),
|
||||
UpdatedAt: "2026-08-26T10:10:00Z",
|
||||
ID: "question-1",
|
||||
})
|
||||
if err != nil || n != 1 {
|
||||
t.Fatalf("set root state %q rows=%d error=%v", state, n, err)
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := conn.ExecContext(ctx, `
|
||||
DROP INDEX idx_posts_root_date;
|
||||
@@ -413,7 +458,7 @@ WHERE schemaname = current_schema()
|
||||
func TestMigratePostsReportsStep(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
exec := &failingMigrationExec{failAt: 5}
|
||||
exec := &failingMigrationExec{failAt: 6}
|
||||
err := migratePosts(context.Background(), exec)
|
||||
if err == nil || !strings.Contains(err.Error(), "copy questions") {
|
||||
t.Fatalf("error = %v, want copy questions context", err)
|
||||
|
||||
@@ -258,6 +258,39 @@ func GetPostThread(ctx context.Context, db *sql.DB, rootID string) (*Post, error
|
||||
return buildPostTree(posts, rootID)
|
||||
}
|
||||
|
||||
// GetPostThreadForViewer includes root voting and answered annotations.
|
||||
func GetPostThreadForViewer(
|
||||
ctx context.Context,
|
||||
db *sql.DB,
|
||||
rootID string,
|
||||
viewerID string,
|
||||
) (*Post, error) {
|
||||
root, err := GetPostThread(ctx, db, rootID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
summary, err := sqlc.New(db).GetRootPostVoteSummary(ctx, sqlc.GetRootPostVoteSummaryParams{
|
||||
ViewerID: viewerID,
|
||||
RootID: rootID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
root.Score = int(summary.Score)
|
||||
root.UserVote = int(summary.UserVote)
|
||||
root.Answered = postTreeContainsRole(root, RoleAdmin)
|
||||
return root, nil
|
||||
}
|
||||
|
||||
func postTreeContainsRole(post *Post, role Role) bool {
|
||||
for _, reply := range post.Replies {
|
||||
if reply.AuthorRole == role || postTreeContainsRole(reply, role) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func buildPostTree(posts []Post, rootID string) (*Post, error) {
|
||||
byID := make(map[string]*Post, len(posts))
|
||||
for i := range posts {
|
||||
@@ -334,6 +367,89 @@ func ListRootPosts(ctx context.Context, db *sql.DB, postDate, viewerID string) (
|
||||
return posts, nil
|
||||
}
|
||||
|
||||
// ListRootPostsByAuthor returns visible roots created by an author, newest first.
|
||||
func ListRootPostsByAuthor(ctx context.Context, db *sql.DB, authorID string) ([]Post, error) {
|
||||
rows, err := sqlc.New(db).ListRootPostsByAuthor(ctx, sqlc.ListRootPostsByAuthorParams{
|
||||
AuthorID: authorID,
|
||||
HiddenState: string(PostStateHidden),
|
||||
RowLimit: ProfileListLimit,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
posts := make([]Post, 0, len(rows))
|
||||
for _, r := range rows {
|
||||
posts = append(posts, postFromValues(
|
||||
db,
|
||||
r.ID,
|
||||
r.ParentID,
|
||||
r.AuthorID,
|
||||
r.AuthorName,
|
||||
r.AuthorRole,
|
||||
r.Title,
|
||||
r.Body,
|
||||
r.City,
|
||||
r.PostDate,
|
||||
r.PostState,
|
||||
r.CreatedAt,
|
||||
r.UpdatedAt,
|
||||
))
|
||||
}
|
||||
return posts, nil
|
||||
}
|
||||
|
||||
// ListRootPostsAnsweredBy returns visible roots containing a reply by adminID.
|
||||
func ListRootPostsAnsweredBy(ctx context.Context, db *sql.DB, adminID string) ([]Post, error) {
|
||||
rows, err := sqlc.New(db).ListRootPostsAnsweredBy(ctx, sqlc.ListRootPostsAnsweredByParams{
|
||||
HiddenState: string(PostStateHidden),
|
||||
AdminID: adminID,
|
||||
RowLimit: ProfileListLimit,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
posts := make([]Post, 0, len(rows))
|
||||
for _, r := range rows {
|
||||
posts = append(posts, postFromValues(
|
||||
db,
|
||||
r.ID,
|
||||
r.ParentID,
|
||||
r.AuthorID,
|
||||
r.AuthorName,
|
||||
r.AuthorRole,
|
||||
r.Title,
|
||||
r.Body,
|
||||
r.City,
|
||||
r.PostDate,
|
||||
r.PostState,
|
||||
r.CreatedAt,
|
||||
r.UpdatedAt,
|
||||
))
|
||||
}
|
||||
return posts, nil
|
||||
}
|
||||
|
||||
// SetRootPostState changes a root post's state.
|
||||
func SetRootPostState(ctx context.Context, db *sql.DB, id string, state PostState) error {
|
||||
switch state {
|
||||
case PostStateVisible, PostStateHidden, PostStateLocked:
|
||||
default:
|
||||
return fmt.Errorf("%w: invalid post state", ErrInvalidPost)
|
||||
}
|
||||
n, err := sqlc.New(db).UpdateRootPostState(ctx, sqlc.UpdateRootPostStateParams{
|
||||
PostState: string(state),
|
||||
UpdatedAt: time.Now().UTC().Format(time.RFC3339Nano),
|
||||
ID: id,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if n == 0 {
|
||||
return sql.ErrNoRows
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetPostVote sets value to 1, -1, or 0 on a visible root post.
|
||||
func SetPostVote(ctx context.Context, db *sql.DB, userID, postID string, value int) error {
|
||||
if value != 1 && value != -1 && value != 0 {
|
||||
|
||||
@@ -141,6 +141,26 @@ func TestMemoryPostLifecycle(t *testing.T) {
|
||||
if err := mem.VotePost(ctx, voter.ID, later.ID, 1); !errors.Is(err, ErrPostNotVotable) {
|
||||
t.Fatalf("reply vote error = %v", err)
|
||||
}
|
||||
byAuthor, err := mem.ListRootPostsByAuthor(ctx, homeowner.ID)
|
||||
if err != nil || len(byAuthor) != 1 || byAuthor[0].ID != root.ID {
|
||||
t.Fatalf("roots by author = %+v, %v", byAuthor, err)
|
||||
}
|
||||
answeredBy, err := mem.ListRootPostsAnsweredBy(ctx, plumber.ID)
|
||||
if err != nil || len(answeredBy) != 1 || answeredBy[0].ID != root.ID {
|
||||
t.Fatalf("roots answered by admin = %+v, %v", answeredBy, err)
|
||||
}
|
||||
if err := mem.SetRootPostState(ctx, later.ID, PostStateHidden); !errors.Is(err, sql.ErrNoRows) {
|
||||
t.Fatalf("reply state error = %v, want sql.ErrNoRows", err)
|
||||
}
|
||||
if err := mem.SetRootPostState(ctx, root.ID, PostStateHidden); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if roots, err := mem.ListRootPostsByAuthor(ctx, homeowner.ID); err != nil || len(roots) != 0 {
|
||||
t.Fatalf("hidden author roots = %+v, %v", roots, err)
|
||||
}
|
||||
if roots, err := mem.ListRootPostsAnsweredBy(ctx, plumber.ID); err != nil || len(roots) != 0 {
|
||||
t.Fatalf("hidden answered roots = %+v, %v", roots, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMemoryPostValidation(t *testing.T) {
|
||||
|
||||
@@ -157,6 +157,10 @@ func (p *Postgres) GetPostThread(ctx context.Context, rootID string) (*Post, err
|
||||
return GetPostThread(ctx, p.db, rootID)
|
||||
}
|
||||
|
||||
func (p *Postgres) GetPostThreadForViewer(ctx context.Context, rootID, viewerID string) (*Post, error) {
|
||||
return GetPostThreadForViewer(ctx, p.db, rootID, viewerID)
|
||||
}
|
||||
|
||||
func (p *Postgres) UpdatePost(ctx context.Context, post *Post) error {
|
||||
post.db = p.db
|
||||
return post.Update(ctx)
|
||||
@@ -166,6 +170,18 @@ func (p *Postgres) ListRootPosts(ctx context.Context, postDate, viewerID string)
|
||||
return ListRootPosts(ctx, p.db, postDate, viewerID)
|
||||
}
|
||||
|
||||
func (p *Postgres) ListRootPostsByAuthor(ctx context.Context, authorID string) ([]Post, error) {
|
||||
return ListRootPostsByAuthor(ctx, p.db, authorID)
|
||||
}
|
||||
|
||||
func (p *Postgres) ListRootPostsAnsweredBy(ctx context.Context, adminID string) ([]Post, error) {
|
||||
return ListRootPostsAnsweredBy(ctx, p.db, adminID)
|
||||
}
|
||||
|
||||
func (p *Postgres) SetRootPostState(ctx context.Context, id string, state PostState) error {
|
||||
return SetRootPostState(ctx, p.db, id, state)
|
||||
}
|
||||
|
||||
func (p *Postgres) VotePost(ctx context.Context, userID, postID string, value int) error {
|
||||
return SetPostVote(ctx, p.db, userID, postID, value)
|
||||
}
|
||||
|
||||
@@ -117,6 +117,34 @@ func (q *Queries) GetPost(ctx context.Context, id string) (GetPostRow, error) {
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getRootPostVoteSummary = `-- name: GetRootPostVoteSummary :one
|
||||
SELECT
|
||||
COALESCE(SUM(value), 0)::bigint AS score,
|
||||
COALESCE(
|
||||
MAX(value) FILTER (WHERE user_id = $1),
|
||||
0
|
||||
)::bigint AS user_vote
|
||||
FROM post_votes
|
||||
WHERE post_id = $2
|
||||
`
|
||||
|
||||
type GetRootPostVoteSummaryParams struct {
|
||||
ViewerID string
|
||||
RootID string
|
||||
}
|
||||
|
||||
type GetRootPostVoteSummaryRow struct {
|
||||
Score int64
|
||||
UserVote int64
|
||||
}
|
||||
|
||||
func (q *Queries) GetRootPostVoteSummary(ctx context.Context, arg GetRootPostVoteSummaryParams) (GetRootPostVoteSummaryRow, error) {
|
||||
row := q.db.QueryRowContext(ctx, getRootPostVoteSummary, arg.ViewerID, arg.RootID)
|
||||
var i GetRootPostVoteSummaryRow
|
||||
err := row.Scan(&i.Score, &i.UserVote)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const listPostThread = `-- name: ListPostThread :many
|
||||
WITH RECURSIVE thread AS (
|
||||
SELECT p.id, p.parent_id, p.author_id, p.title, p.body, p.city, p.post_date, p.post_state, p.created_at, p.updated_at
|
||||
@@ -309,6 +337,162 @@ func (q *Queries) ListRootPosts(ctx context.Context, arg ListRootPostsParams) ([
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const listRootPostsAnsweredBy = `-- name: ListRootPostsAnsweredBy :many
|
||||
WITH RECURSIVE ancestors AS (
|
||||
SELECT p.id, p.parent_id
|
||||
FROM posts p
|
||||
WHERE p.author_id = $3
|
||||
AND p.parent_id IS NOT NULL
|
||||
|
||||
UNION
|
||||
|
||||
SELECT parent.id, parent.parent_id
|
||||
FROM posts parent
|
||||
JOIN ancestors child ON child.parent_id = parent.id
|
||||
)
|
||||
SELECT DISTINCT
|
||||
root.id, root.parent_id, root.author_id,
|
||||
u.name AS author_name, u.role AS author_role,
|
||||
root.title, root.body, root.city, root.post_date,
|
||||
root.post_state, root.created_at, root.updated_at
|
||||
FROM posts root
|
||||
JOIN ancestors ON ancestors.id = root.id
|
||||
JOIN users u ON u.id = root.author_id
|
||||
WHERE root.parent_id IS NULL
|
||||
AND root.post_state <> $1
|
||||
ORDER BY root.created_at DESC, root.id DESC
|
||||
LIMIT $2
|
||||
`
|
||||
|
||||
type ListRootPostsAnsweredByParams struct {
|
||||
HiddenState string
|
||||
RowLimit int32
|
||||
AdminID string
|
||||
}
|
||||
|
||||
type ListRootPostsAnsweredByRow struct {
|
||||
ID string
|
||||
ParentID sql.NullString
|
||||
AuthorID string
|
||||
AuthorName string
|
||||
AuthorRole string
|
||||
Title string
|
||||
Body string
|
||||
City string
|
||||
PostDate string
|
||||
PostState string
|
||||
CreatedAt string
|
||||
UpdatedAt string
|
||||
}
|
||||
|
||||
func (q *Queries) ListRootPostsAnsweredBy(ctx context.Context, arg ListRootPostsAnsweredByParams) ([]ListRootPostsAnsweredByRow, error) {
|
||||
rows, err := q.db.QueryContext(ctx, listRootPostsAnsweredBy, arg.HiddenState, arg.RowLimit, arg.AdminID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
items := []ListRootPostsAnsweredByRow{}
|
||||
for rows.Next() {
|
||||
var i ListRootPostsAnsweredByRow
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.ParentID,
|
||||
&i.AuthorID,
|
||||
&i.AuthorName,
|
||||
&i.AuthorRole,
|
||||
&i.Title,
|
||||
&i.Body,
|
||||
&i.City,
|
||||
&i.PostDate,
|
||||
&i.PostState,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const listRootPostsByAuthor = `-- name: ListRootPostsByAuthor :many
|
||||
SELECT
|
||||
p.id, p.parent_id, p.author_id,
|
||||
u.name AS author_name, u.role AS author_role,
|
||||
p.title, p.body, p.city, p.post_date,
|
||||
p.post_state, p.created_at, p.updated_at
|
||||
FROM posts p
|
||||
JOIN users u ON u.id = p.author_id
|
||||
WHERE p.parent_id IS NULL
|
||||
AND p.author_id = $1
|
||||
AND p.post_state <> $2
|
||||
ORDER BY p.created_at DESC, p.id DESC
|
||||
LIMIT $3
|
||||
`
|
||||
|
||||
type ListRootPostsByAuthorParams struct {
|
||||
AuthorID string
|
||||
HiddenState string
|
||||
RowLimit int32
|
||||
}
|
||||
|
||||
type ListRootPostsByAuthorRow struct {
|
||||
ID string
|
||||
ParentID sql.NullString
|
||||
AuthorID string
|
||||
AuthorName string
|
||||
AuthorRole string
|
||||
Title string
|
||||
Body string
|
||||
City string
|
||||
PostDate string
|
||||
PostState string
|
||||
CreatedAt string
|
||||
UpdatedAt string
|
||||
}
|
||||
|
||||
func (q *Queries) ListRootPostsByAuthor(ctx context.Context, arg ListRootPostsByAuthorParams) ([]ListRootPostsByAuthorRow, error) {
|
||||
rows, err := q.db.QueryContext(ctx, listRootPostsByAuthor, arg.AuthorID, arg.HiddenState, arg.RowLimit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
items := []ListRootPostsByAuthorRow{}
|
||||
for rows.Next() {
|
||||
var i ListRootPostsByAuthorRow
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.ParentID,
|
||||
&i.AuthorID,
|
||||
&i.AuthorName,
|
||||
&i.AuthorRole,
|
||||
&i.Title,
|
||||
&i.Body,
|
||||
&i.City,
|
||||
&i.PostDate,
|
||||
&i.PostState,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const postIsVisibleRoot = `-- name: PostIsVisibleRoot :one
|
||||
SELECT EXISTS(
|
||||
SELECT 1
|
||||
@@ -353,6 +537,29 @@ func (q *Queries) UpdatePost(ctx context.Context, arg UpdatePostParams) (int64,
|
||||
return result.RowsAffected()
|
||||
}
|
||||
|
||||
const updateRootPostState = `-- name: UpdateRootPostState :execrows
|
||||
UPDATE posts
|
||||
SET
|
||||
post_state = $1,
|
||||
updated_at = $2
|
||||
WHERE id = $3
|
||||
AND parent_id IS NULL
|
||||
`
|
||||
|
||||
type UpdateRootPostStateParams struct {
|
||||
PostState string
|
||||
UpdatedAt string
|
||||
ID string
|
||||
}
|
||||
|
||||
func (q *Queries) UpdateRootPostState(ctx context.Context, arg UpdateRootPostStateParams) (int64, error) {
|
||||
result, err := q.db.ExecContext(ctx, updateRootPostState, arg.PostState, arg.UpdatedAt, arg.ID)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return result.RowsAffected()
|
||||
}
|
||||
|
||||
const upsertPostVoteOnVisibleRoot = `-- name: UpsertPostVoteOnVisibleRoot :execrows
|
||||
INSERT INTO post_votes (user_id, post_id, value)
|
||||
SELECT $1, $2, $3
|
||||
|
||||
@@ -40,8 +40,12 @@ type Store interface {
|
||||
CreatePost(ctx context.Context, post *Post) error
|
||||
GetPost(ctx context.Context, id string) (*Post, error)
|
||||
GetPostThread(ctx context.Context, rootID string) (*Post, error)
|
||||
GetPostThreadForViewer(ctx context.Context, rootID, viewerID string) (*Post, error)
|
||||
UpdatePost(ctx context.Context, post *Post) error
|
||||
ListRootPosts(ctx context.Context, postDate, viewerID string) ([]Post, error)
|
||||
ListRootPostsByAuthor(ctx context.Context, authorID string) ([]Post, error)
|
||||
ListRootPostsAnsweredBy(ctx context.Context, adminID string) ([]Post, error)
|
||||
SetRootPostState(ctx context.Context, id string, state PostState) error
|
||||
VotePost(ctx context.Context, userID, postID string, value int) error
|
||||
|
||||
// Vote sets the vote to 1, -1, or 0 (clear) on a visible question.
|
||||
|
||||
Reference in New Issue
Block a user