import { all, batch, chunks, first, placeholders, stmt } from "./db"; import { allocateIssueNumber, indexIssue } from "./issues"; import type { IssueRow } from "./issues"; import { addIssueEvent } from "./pulls"; import { assertBelowCap } from "./limits"; import type { RepoRow } from "~/lib/types"; /** Per-scope count caps for workstream 6 entities; kept here so `limits.ts` stays merge-friendly. */ export const PLANNING_CAPS = { milestonesPerRepo: 100, issueTypesPerRepo: 25, subIssuesPerParent: 100, pinnedPerRepo: 3, projectsPerOwner: 100, fieldsPerProject: 30, optionsPerField: 50, itemsPerProject: 1000, categoriesPerRepo: 25, discussionCommentsPerThread: 2000, } as const; export async function assertCount(sql: string, param: string, cap: number, what: string): Promise { const row = await first<{ count: number }>(sql, param); assertBelowCap(row?.count ?? 0, cap, what); } export type Progress = { total: number; closed: number }; /** Closed/total counts of sub-issues for each parent issue id. */ export async function subIssueProgress(parentIds: string[]): Promise> { const out = new Map(); for (const part of chunks([...new Set(parentIds)])) { const rows = await all<{ parent_id: string; total: number; closed: number }>( `SELECT parent_id, COUNT(*) AS total, SUM(state = 'closed') AS closed FROM issues WHERE parent_id IN (${placeholders(part.length)}) GROUP BY parent_id`, ...part, ); for (const row of rows) out.set(row.parent_id, { total: row.total, closed: row.closed ?? 0 }); } return out; } /** Resolves an issue number, following redirects left behind by transfers. */ export async function resolveIssueNumber(repoId: string, number: number): Promise { const direct = await first("SELECT * FROM issues WHERE repo_id = ? AND number = ?", repoId, number); if (direct) return direct; return first( "SELECT i.* FROM issue_redirects r JOIN issues i ON i.id = r.issue_id WHERE r.repo_id = ? AND r.number = ?", repoId, number, ); } /** Moves an issue (with its comments and events) to another repository and leaves a redirect behind. */ export async function transferIssue( issue: IssueRow, source: RepoRow, target: RepoRow, actorId: string, ): Promise { const number = await allocateIssueNumber(target.id); const open = issue.state === "open" ? 1 : 0; await batch([ stmt( "INSERT OR REPLACE INTO issue_redirects (repo_id, number, issue_id) VALUES (?, ?, ?)", issue.repo_id, issue.number, issue.id, ), stmt( `UPDATE issues SET repo_id = ?, number = ?, milestone_id = NULL, type_id = NULL, parent_id = NULL, updated_at = ? WHERE id = ?`, target.id, number, Date.now(), issue.id, ), stmt("UPDATE issues SET parent_id = NULL WHERE parent_id = ?", issue.id), stmt("DELETE FROM issue_labels WHERE issue_id = ?", issue.id), stmt("DELETE FROM pinned_issues WHERE issue_id = ?", issue.id), stmt("UPDATE repos SET open_issues_count = MAX(open_issues_count - ?, 0) WHERE id = ?", open, issue.repo_id), stmt("UPDATE repos SET open_issues_count = open_issues_count + ? WHERE id = ?", open, target.id), ]); await indexIssue({ id: issue.id, repo_id: target.id, title: issue.title, body: issue.body }); await addIssueEvent(issue.id, actorId, "transferred", { from: `${source.owner_name}/${source.name}#${issue.number}`, }); return number; }