this repo has no description

endpoint

+111 -34
+10
.env.eample
··· 1 + # These need to match can use openssl rand -hex 32 2 + NITRO_TAP_ADMIN_PASSWORD="" 3 + TAP_ADMIN_PASSWORD="" 4 + 5 + TAP_SIGNAL_COLLECTION=dev.npmx.feed.like 6 + TAP_COLLECTION_FILTERS=dev.npmx.feed.like 7 + TAP_WEBHOOK_URL=http://localhost:3000/api/tap/webhook 8 + 9 + 10 + DATABASE_URL="leaderboards.db"
+1 -1
drizzle.config.ts
··· 1 1 import { defineConfig } from "drizzle-kit"; 2 2 3 3 export default defineConfig({ 4 - schema: "./schema.ts", 4 + schema: "server/utils/db/schema.ts", 5 5 out: "./migrations", 6 6 dialect: "sqlite", 7 7 dbCredentials: {
+31 -2
server/api/leaderboard/likes.get.ts
··· 1 - export default defineEventHandler(() => { 2 - return { hello: "API" }; 1 + import { sql, count, countDistinct } from "drizzle-orm"; 2 + import { db } from "../../utils/db/database"; 3 + import { likes } from "../../utils/db/schema"; 4 + import type { LikeLeaderBoard } from "../../utils/types/likeLeaderBoard"; 5 + 6 + export default defineEventHandler(async (event): Promise<LikeLeaderBoard> => { 7 + const query = getQuery(event); 8 + const limit = Number(query.limit) || 10; 9 + 10 + const [totals] = await db 11 + .select({ 12 + totalLikes: count(), 13 + totalUniqueLikers: countDistinct(likes.did), 14 + }) 15 + .from(likes); 16 + 17 + const leaderBoard = await db 18 + .select({ 19 + subjectRef: likes.subjectRef, 20 + totalLikes: count().as("totalLikes"), 21 + }) 22 + .from(likes) 23 + .groupBy(likes.subjectRef) 24 + .orderBy(sql`totalLikes desc`) 25 + .limit(limit); 26 + 27 + return { 28 + totalLikes: totals.totalLikes, 29 + totalUniqueLikers: totals.totalUniqueLikers, 30 + leaderBoard, 31 + }; 3 32 });
+60 -30
server/api/tap/webhook.post.ts
··· 2 2 import { db } from "../../utils/db/database"; 3 3 import { likes, NewLike } from "../../utils/db/schema"; 4 4 import * as dev from "../../utils/types/lexicons/dev"; 5 - import { drizzle } from "drizzle-orm/better-sqlite3"; 6 - 7 - // const db = drizzle(process.env.DATABASE_URL ?? "leaderboards.db"); 5 + import { and, eq } from "drizzle-orm"; 8 6 9 7 export default defineEventHandler(async (event) => { 10 8 const adminPassword = useRuntimeConfig(event).tapAdminPassword; ··· 39 37 case "record": 40 38 const recordEvent: RecordEvent = tapEvent; 41 39 const atUri = `at://${recordEvent.did}/${recordEvent.collection}/${recordEvent.rkey}`; 42 - const likeRecord = dev.npmx.feed.like.$validate(recordEvent.record); 43 - if (!likeRecord) { 44 - console.log("Invalid like record:", recordEvent); 45 - break; 46 - } 47 40 if (recordEvent.collection !== "dev.npmx.feed.like") { 48 41 console.log("Skipping event:", recordEvent); 49 42 break; 50 43 } 44 + await handleLikes(recordEvent, atUri); 51 45 52 - switch (recordEvent.action) { 53 - case "create": 54 - const newLike: NewLike = { 55 - atUri, 56 - subjectRef: likeRecord.subjectRef, 57 - repo: recordEvent.did, 58 - createdAt: new Date(likeRecord.createdAt), 59 - }; 60 - await db.insert(likes).values(newLike); 61 - console.log("new like added"); 62 - break; 63 - case "update": 64 - // const updatedLike: NewLike = {}; 65 - // db.update(likes).where(eq(likes.id, tapEvent.record.id)).set({ 66 - // score: tapEvent.record.score, 67 - // updatedAt: tapEvent.record.updatedAt, 68 - // }); 69 - break; 70 - case "delete": 71 - // db.delete(likes).where(eq(likes.id, tapEvent.record.id)); 72 - break; 73 - } 74 46 break; 75 47 } 76 48 77 49 return {}; 78 50 }); 51 + 52 + const handleLikes = async (recordEvent: RecordEvent, atUri: string) => { 53 + const likeRecord = dev.npmx.feed.like.$validate(recordEvent.record); 54 + if (!likeRecord) { 55 + console.log("Invalid like record:", recordEvent); 56 + return; 57 + } 58 + switch (recordEvent.action) { 59 + case "create": 60 + const newLike: NewLike = { 61 + atUri, 62 + subjectRef: likeRecord.subjectRef, 63 + did: recordEvent.did, 64 + createdAt: new Date(likeRecord.createdAt), 65 + }; 66 + await db 67 + .insert(likes) 68 + .values(newLike) 69 + .onConflictDoUpdate({ 70 + target: likes.atUri, 71 + set: { 72 + atUri, 73 + subjectRef: likeRecord.subjectRef, 74 + did: recordEvent.did, 75 + createdAt: new Date(likeRecord.createdAt), 76 + }, 77 + }); 78 + console.log("new like added"); 79 + break; 80 + case "update": 81 + db.update(likes) 82 + .set({ 83 + atUri, 84 + subjectRef: likeRecord.subjectRef, 85 + did: recordEvent.did, 86 + createdAt: new Date(likeRecord.createdAt), 87 + }) 88 + .where( 89 + and( 90 + eq(likes.did, recordEvent.did), 91 + eq(likes.subjectRef, likeRecord.subjectRef), 92 + ), 93 + ); 94 + console.log("like updated"); 95 + break; 96 + case "delete": 97 + await db 98 + .delete(likes) 99 + .where( 100 + and( 101 + eq(likes.did, recordEvent.did), 102 + eq(likes.subjectRef, likeRecord.subjectRef), 103 + ), 104 + ); 105 + console.log("like deleted"); 106 + break; 107 + } 108 + };
+1 -1
server/utils/db/schema.ts
··· 6 6 id: integer("id").primaryKey({ autoIncrement: true }), 7 7 atUri: text("at_uri").notNull().unique(), 8 8 subjectRef: text("subject_ref").notNull(), 9 - repo: text("did").notNull(), 9 + did: text("did").notNull(), 10 10 createdAt: integer("createdAt", { mode: "timestamp" }), 11 11 indexedAt: integer("indexedAt", { mode: "timestamp" }).$defaultFn( 12 12 () => new Date(),
+8
server/utils/types/likeLeaderBoard.ts
··· 1 + export type LikeLeaderBoard = { 2 + totalLikes: number; 3 + totalUniqueLikers: number; 4 + leaderBoard: { 5 + subjectRef: string; 6 + totalLikes: number; 7 + }[]; 8 + };