open source is social v-it.org
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat(explore): add single-item cap and skill detail endpoints

Add GET /api/cap (by ref or uri, with optional beacon scoping) and
GET /api/skill (by name or uri) with vouch_count subquery and handle
JOIN. Add idx_caps_ref index for efficient ref-based lookups.

+79
+1
explore/schema.sql
··· 16 16 17 17 CREATE INDEX IF NOT EXISTS idx_caps_beacon ON caps(beacon); 18 18 CREATE INDEX IF NOT EXISTS idx_caps_created_at ON caps(created_at DESC); 19 + CREATE INDEX IF NOT EXISTS idx_caps_ref ON caps(ref); 19 20 20 21 CREATE TABLE IF NOT EXISTS vouches ( 21 22 id INTEGER PRIMARY KEY AUTOINCREMENT,
+78
explore/src/api.js
··· 75 75 }); 76 76 } 77 77 78 + if (pathname === '/api/cap') { 79 + const ref = searchParams.get('ref'); 80 + const uri = searchParams.get('uri'); 81 + const beacon = searchParams.get('beacon'); 82 + 83 + if (!ref && !uri) { 84 + return json({ error: 'ref or uri is required' }, 400); 85 + } 86 + 87 + if (ref && uri) { 88 + return json({ error: 'provide ref or uri, not both' }, 400); 89 + } 90 + 91 + const conditions = []; 92 + const bindings = []; 93 + 94 + if (uri) { 95 + conditions.push('c.uri = ?'); 96 + bindings.push(uri); 97 + } 98 + 99 + if (ref) { 100 + conditions.push('c.ref = ?'); 101 + bindings.push(ref); 102 + 103 + if (beacon) { 104 + conditions.push('c.beacon = ?'); 105 + bindings.push(beacon); 106 + } 107 + } 108 + 109 + let sql = `SELECT c.*, h.handle, 110 + (SELECT COUNT(*) FROM vouches v WHERE v.cap_uri = c.uri) as vouch_count 111 + FROM caps c 112 + LEFT JOIN handles h ON c.did = h.did 113 + WHERE ${conditions.join(' AND ')}`; 114 + sql += ' ORDER BY c.created_at DESC LIMIT 1'; 115 + 116 + const result = await env.DB.prepare(sql).bind(...bindings).first(); 117 + return json({ cap: result }); 118 + } 119 + 78 120 if (pathname === '/api/vouches') { 79 121 const capUri = searchParams.get('cap_uri'); 80 122 if (!capUri) { ··· 129 171 skills: results, 130 172 cursor: results.length > 0 ? results[results.length - 1].id : null, 131 173 }); 174 + } 175 + 176 + if (pathname === '/api/skill') { 177 + const name = searchParams.get('name'); 178 + const uri = searchParams.get('uri'); 179 + 180 + if (!name && !uri) { 181 + return json({ error: 'name or uri is required' }, 400); 182 + } 183 + 184 + if (name && uri) { 185 + return json({ error: 'provide name or uri, not both' }, 400); 186 + } 187 + 188 + const conditions = []; 189 + const bindings = []; 190 + 191 + if (uri) { 192 + conditions.push('s.uri = ?'); 193 + bindings.push(uri); 194 + } 195 + 196 + if (name) { 197 + conditions.push('s.name = ?'); 198 + bindings.push(name); 199 + } 200 + 201 + let sql = `SELECT s.*, h.handle, 202 + (SELECT COUNT(*) FROM vouches v WHERE v.cap_uri = s.uri) as vouch_count 203 + FROM skills s 204 + LEFT JOIN handles h ON s.did = h.did 205 + WHERE ${conditions.join(' AND ')}`; 206 + sql += ' ORDER BY s.created_at DESC LIMIT 1'; 207 + 208 + const result = await env.DB.prepare(sql).bind(...bindings).first(); 209 + return json({ skill: result }); 132 210 } 133 211 134 212 if (pathname === '/api/stats') {