packages/server/src/routes/feed.ts 2.6 K raw
1
import { Hono } from "hono";
2
import type {
3
	Bindings,
4
	ResolvedDocumentRow,
5
	Document,
6
	Publication,
7
	BskyPostRef,
8
} from "../types";
9
10
const feed = new Hono<{ Bindings: Bindings }>();
11
12
/**
13
 * Transforms a database row into a Document object for the API response.
14
 */
15
function rowToDocument(row: ResolvedDocumentRow): Document {
16
	// Build publication object if we have publication data
17
	let publication: Publication | undefined;
18
	if (row.pub_url && row.pub_name) {
19
		publication = {
20
			url: row.pub_url,
21
			name: row.pub_name,
22
			description: row.pub_description || undefined,
23
			iconCid: row.pub_icon_cid || undefined,
24
			iconUrl: row.pub_icon_url || undefined,
25
		};
26
	}
27
28
	// Parse bskyPostRef if present
29
	let bskyPostRef: BskyPostRef | undefined;
30
	if (row.bsky_post_ref) {
31
		try {
32
			bskyPostRef = JSON.parse(row.bsky_post_ref);
33
		} catch {
34
			// Ignore parse errors
35
		}
36
	}
37
38
	// Parse tags if present
39
	let tags: string[] | undefined;
40
	if (row.tags) {
41
		try {
42
			tags = JSON.parse(row.tags);
43
		} catch {
44
			// Ignore parse errors
45
		}
46
	}
47
48
	return {
49
		uri: row.uri,
50
		did: row.did,
51
		rkey: row.rkey,
52
		title: row.title || "Untitled",
53
		description: row.description || undefined,
54
		path: row.path || undefined,
55
		site: row.site || undefined,
56
		textContent: row.text_content || undefined,
57
		coverImageCid: row.cover_image_cid || undefined,
58
		coverImageUrl: row.cover_image_url || undefined,
59
		bskyPostRef,
60
		tags,
61
		publishedAt: row.published_at || undefined,
62
		updatedAt: row.updated_at || undefined,
63
		publication,
64
		viewUrl: row.view_url || undefined,
65
		pdsEndpoint: row.pds_endpoint || undefined,
66
	};
67
}
68
69
// Get feed of documents with resolved URLs (server-side resolution)
70
feed.get("/", async (c) => {
71
	try {
72
		const db = c.env.DB;
73
		const limit = Number(c.req.query("limit")) || 50;
74
		const offset = Number(c.req.query("offset")) || 0;
75
76
		const { results } = await db
77
			.prepare(
78
				`SELECT uri, did, rkey, title, description, path, site, text_content,
79
                cover_image_cid, cover_image_url, bsky_post_ref, tags,
80
                published_at, updated_at, pub_url, pub_name, pub_description,
81
                pub_icon_cid, pub_icon_url, view_url, pds_endpoint,
82
                resolved_at, stale_at, verified
83
         FROM resolved_documents
84
         WHERE verified = 1
85
         ORDER BY published_at DESC
86
         LIMIT ? OFFSET ?`,
87
			)
88
			.bind(limit, offset)
89
			.all<ResolvedDocumentRow>();
90
91
		const documents = (results || []).map(rowToDocument);
92
93
		return c.json({
94
			count: documents.length,
95
			limit,
96
			offset,
97
			documents,
98
		});
99
	} catch (error) {
100
		return c.json(
101
			{ error: "Failed to fetch feed", details: String(error) },
102
			500,
103
		);
104
	}
105
});
106
107
export default feed;