Antonio Mika
·
2026-10-25
1package prose
2
3import (
4 "fmt"
5 "sync"
6 "time"
7
8 "github.com/hashicorp/golang-lru/v2/expirable"
9 "github.com/picosh/pico/pkg/db"
10 "github.com/picosh/pico/pkg/db/postgres"
11)
12
13// DB is the storage prose uses.
14type DB interface {
15 db.AuthDB
16 db.PostDB
17 db.TagDB
18 FindUserPostsByTag(pager *db.Pager, tag, userID, space string) (*db.Paginate[*db.Post], error)
19 FindPopularPosts() ([]*db.Post, error)
20}
21
22// PsqlDB is prose's postgres storage: the queries apps share, and the ones
23// only prose makes.
24type PsqlDB struct {
25 *postgres.PsqlDB
26
27 popularPostsCache *expirable.LRU[string, []*db.Post]
28 popularPostsMu sync.Mutex
29}
30
31var _ DB = (*PsqlDB)(nil)
32
33func NewDB(base *postgres.PsqlDB) *PsqlDB {
34 return &PsqlDB{
35 PsqlDB: base,
36 popularPostsCache: expirable.NewLRU[string, []*db.Post](16, nil, 15*time.Minute),
37 }
38}
39
40func (me *PsqlDB) ClearPopularPostsCache() {
41 if me.popularPostsCache != nil {
42 me.popularPostsCache.Purge()
43 }
44}
45
46func (me *PsqlDB) FindPopularPosts() ([]*db.Post, error) {
47 if me.popularPostsCache != nil {
48 if posts, ok := me.popularPostsCache.Get("popular_posts"); ok {
49 return posts, nil
50 }
51 }
52
53 me.popularPostsMu.Lock()
54 defer me.popularPostsMu.Unlock()
55
56 if me.popularPostsCache != nil {
57 if posts, ok := me.popularPostsCache.Get("popular_posts"); ok {
58 return posts, nil
59 }
60 }
61
62 query := `
63 WITH candidate_posts AS (
64 SELECT
65 posts.id,
66 posts.user_id,
67 posts.filename,
68 posts.slug,
69 posts.title,
70 posts.text,
71 posts.description,
72 posts.publish_at,
73 app_users.name AS username,
74 posts.updated_at,
75 posts.mime_type
76 FROM posts
77 LEFT JOIN app_users ON app_users.id = posts.user_id
78 WHERE
79 hidden = FALSE
80 AND publish_at::date <= CURRENT_DATE
81 AND publish_at >= NOW() - INTERVAL '30 days'
82 AND cur_space = 'prose'
83 AND EXISTS (
84 SELECT 1 FROM feature_flags
85 WHERE feature_flags.user_id = posts.user_id
86 AND (name = 'plus' OR name = 'prose')
87 AND expires_at > NOW()
88 )
89 ),
90 post_visitors AS (
91 SELECT post_id, COUNT(DISTINCT ip_address) AS unique_visitors
92 FROM analytics_visits
93 WHERE post_id IN (SELECT id FROM candidate_posts)
94 AND status = 200
95 AND created_at >= NOW() - INTERVAL '30 days'
96 GROUP BY post_id
97 ),
98 scored_posts AS (
99 SELECT
100 cp.*,
101 COALESCE(pv.unique_visitors, 0) AS score,
102 (
103 LOG(GREATEST(COALESCE(pv.unique_visitors, 0), 1)) +
104 (EXTRACT(EPOCH FROM cp.publish_at) - 1700000000) / 345600.0
105 ) AS rank_score,
106 ROW_NUMBER() OVER (
107 PARTITION BY cp.user_id
108 ORDER BY (
109 LOG(GREATEST(COALESCE(pv.unique_visitors, 0), 1)) +
110 (EXTRACT(EPOCH FROM cp.publish_at) - 1700000000) / 345600.0
111 ) DESC, cp.publish_at DESC
112 ) AS user_rank
113 FROM candidate_posts cp
114 LEFT JOIN post_visitors pv ON pv.post_id = cp.id
115 )
116 SELECT
117 id,
118 user_id,
119 filename,
120 slug,
121 title,
122 text,
123 description,
124 publish_at,
125 username,
126 updated_at,
127 mime_type,
128 score
129 FROM scored_posts
130 WHERE user_rank <= 3
131 ORDER BY rank_score DESC, publish_at DESC
132 LIMIT 30`
133 rs, err := me.Db.Queryx(query)
134 if err != nil {
135 return nil, err
136 }
137 defer func() { _ = rs.Close() }()
138
139 var posts []*db.Post
140 for rs.Next() {
141 post := &db.Post{}
142 err := rs.Scan(
143 &post.ID,
144 &post.UserID,
145 &post.Filename,
146 &post.Slug,
147 &post.Title,
148 &post.Text,
149 &post.Description,
150 &post.PublishAt,
151 &post.Username,
152 &post.UpdatedAt,
153 &post.MimeType,
154 &post.Score,
155 )
156 if err != nil {
157 return nil, err
158 }
159
160 posts = append(posts, post)
161 }
162 if rs.Err() != nil {
163 return nil, rs.Err()
164 }
165
166 if me.popularPostsCache != nil {
167 me.popularPostsCache.Add("popular_posts", posts)
168 }
169
170 return posts, nil
171}
172
173func (me *PsqlDB) FindUserPostsByTag(page *db.Pager, tag, userID, space string) (*db.Paginate[*db.Post], error) {
174 var posts []*db.Post
175 query := fmt.Sprintf(`
176 SELECT %s
177 FROM posts
178 LEFT JOIN app_users ON app_users.id = posts.user_id
179 LEFT JOIN post_tags ON post_tags.post_id = posts.id
180 WHERE
181 hidden = FALSE AND
182 user_id = $1 AND
183 (post_tags.name = $2 OR hidden = true) AND
184 publish_at::date <= CURRENT_DATE AND
185 cur_space = $3
186 ORDER BY publish_at DESC
187 LIMIT $4 OFFSET $5`, postgres.SelectPost)
188 err := me.Db.Select(
189 &posts,
190 query,
191 userID,
192 tag,
193 space,
194 page.Num,
195 page.Num*page.Page,
196 )
197 if err != nil {
198 return nil, err
199 }
200
201 return &db.Paginate[*db.Post]{Data: posts}, nil
202}