50 lines · 2.3 KB
import { all, chunks, first, MAX_IN_LIST, placeholders } from "./db";
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);
}

/** Aggregated reactions per subject id, with whether the viewer has reacted. */
export async function reactionSummaries(
  subjectType: ReactionSubject,
  ids: string[],
  viewerId: string | null,
): Promise<Map<string, ReactionSummary[]>> {
  const out = new Map<string, ReactionSummary[]>();
  const unique = [...new Set(ids)];
  for (const part of chunks(unique, MAX_IN_LIST - 2)) {
    const rows = await 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<string | null> {
  const queries: Record<ReactionSubject, string> = {
    issue: "SELECT repo_id FROM issues WHERE id = ?",