import { chunks, MAX_IN_LIST, placeholders } from "./db"; import { repoDb } from "./scoped-db"; import { locate } from "./scoped-db/locators"; import { REACTION_CONTENTS } from "~/lib/reactions"; import type { ReactionContent, ReactionSubject, ReactionSummary } from "~/lib/reactions"; function isReaction(value: string): value is ReactionContent { return REACTION_CONTENTS.some((content) => content === value); } /** Groups subject ids by the repository that holds them, via the locator directory. */ async function byRepo(subjectType: ReactionSubject, ids: string[]): Promise> { const grouped = new Map(); for (const id of ids) { const repoId = await locate(subjectType, id); if (!repoId) continue; grouped.set(repoId, [...(grouped.get(repoId) ?? []), id]); } return grouped; } /** * Aggregated reactions per subject id, with whether the viewer has reacted. Pass `repoId` when every subject is in * that repository (the usual case: one thread); otherwise each subject's repository is looked up. */ export async function reactionSummaries( subjectType: ReactionSubject, ids: string[], viewerId: string | null, repoId?: string, ): Promise> { const out = new Map(); const unique = [...new Set(ids)]; if (unique.length === 0) return out; const groups = repoId ? new Map([[repoId, unique]]) : await byRepo(subjectType, unique); for (const [scope, part] of [...groups].flatMap(([scope, list]) => chunks(list, MAX_IN_LIST - 2).map((chunk) => [scope, chunk] as const), )) { const rows = await repoDb(scope).all<{ subject_id: string; content: string; count: number; mine: number; }>( `SELECT subject_id, content, COUNT(*) AS count, MAX(user_id = ?) AS mine FROM reactions WHERE subject_type = ? AND subject_id IN (${placeholders(part.length)}) GROUP BY subject_id, content`, viewerId ?? "", subjectType, ...part, ); for (const row of rows) { if (!isReaction(row.content)) continue; const list = out.get(row.subject_id) ?? []; list.push({ content: row.content, count: row.count, viewerReacted: row.mine === 1, }); out.set(row.subject_id, list); } } for (const list of out.values()) list.sort((left, right) => REACTION_CONTENTS.indexOf(left.content) - REACTION_CONTENTS.indexOf(right.content)); return out; } /** Repository that owns a reactable subject, used to authorize reactions. */ export async function subjectRepoId(subjectType: ReactionSubject, subjectId: string): Promise { const tables: Record = { issue: "issues", comment: "comments", review_comment: "review_comments", discussion: "discussions", discussion_comment: "discussion_comments", }; const repoId = await locate(subjectType, subjectId); if (!repoId) return null; const row = await repoDb(repoId).first<{ ok: number }>( `SELECT 1 AS ok FROM ${tables[subjectType]} WHERE id = ?`, subjectId, ); return row ? repoId : null; }