mirror of
https://github.com/prdlk/leetcode.git
synced 2026-09-16 23:16:26 +00:00
144 lines
4.9 KiB
TypeScript
144 lines
4.9 KiB
TypeScript
/**
|
|
* Best-effort mirror into the "Interview Prep" user Project — ported from
|
|
* scripts/srs-project.ts. D1 is the truth; a GraphQL failure here becomes a
|
|
* console warning and never blocks a state write. Only problem issues are
|
|
* ever passed in, so topic rows' Target Date is never written.
|
|
*
|
|
* Target Date is the ONLY mirrored column. SRS stage and first-attempt result
|
|
* live exclusively in D1 — they are read from there (charts, digest, /api/stats)
|
|
* and duplicating them into Project single-selects bought nothing but drift and
|
|
* two extra writes per log. Do not re-add them here.
|
|
*
|
|
* Field IDs are resolved by NAME per invocation (a Worker isolate is
|
|
* short-lived; the extra query per mirror burst is irrelevant at this
|
|
* volume and survives field re-creation).
|
|
*/
|
|
import type { GitHub } from "./github.ts";
|
|
|
|
const PROJECT_TITLE = "Interview Prep";
|
|
|
|
interface ProjectInfo {
|
|
id: string;
|
|
targetDate: string;
|
|
}
|
|
|
|
export interface Mirror {
|
|
setTargetDate(issue: number, date: string | null): Promise<void>;
|
|
warnings: string[];
|
|
}
|
|
|
|
export function projectMirror(gh: GitHub, owner: string): Mirror {
|
|
const warnings: string[] = [];
|
|
let info: ProjectInfo | undefined;
|
|
const itemIds = new Map<number, string | undefined>();
|
|
|
|
async function resolve(): Promise<ProjectInfo> {
|
|
if (info) return info;
|
|
const data = (await gh.graphql(
|
|
`query($owner: String!, $title: String!) {
|
|
user(login: $owner) {
|
|
projectsV2(first: 10, query: $title) {
|
|
nodes {
|
|
id title
|
|
fields(first: 30) {
|
|
nodes {
|
|
... on ProjectV2FieldCommon { id name dataType }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}`,
|
|
{ owner, title: PROJECT_TITLE },
|
|
)) as {
|
|
user: {
|
|
projectsV2: {
|
|
nodes: {
|
|
id: string;
|
|
title: string;
|
|
fields: {
|
|
nodes: { id: string; name: string }[];
|
|
};
|
|
}[];
|
|
};
|
|
};
|
|
};
|
|
const node = data.user.projectsV2.nodes.find((n) => n.title === PROJECT_TITLE);
|
|
if (!node) throw new Error(`project "${PROJECT_TITLE}" not found for @${owner}`);
|
|
const date = node.fields.nodes.find((n) => n.name === "Target Date");
|
|
if (!date) throw new Error(`date field "Target Date" missing`);
|
|
info = { id: node.id, targetDate: date.id };
|
|
return info;
|
|
}
|
|
|
|
async function itemId(issue: number): Promise<string> {
|
|
if (itemIds.has(issue)) {
|
|
const cached = itemIds.get(issue);
|
|
if (!cached) throw new Error(`issue #${issue} is not in the project`);
|
|
return cached;
|
|
}
|
|
const project = await resolve();
|
|
const [repoOwner, repoName] = gh.repo.split("/");
|
|
const data = (await gh.graphql(
|
|
`query($owner: String!, $name: String!, $issue: Int!) {
|
|
repository(owner: $owner, name: $name) {
|
|
issue(number: $issue) {
|
|
projectItems(first: 10, includeArchived: true) {
|
|
nodes { id project { id } }
|
|
}
|
|
}
|
|
}
|
|
}`,
|
|
{ owner: repoOwner, name: repoName, issue },
|
|
)) as {
|
|
repository: { issue: { projectItems: { nodes: { id: string; project: { id: string } }[] } } };
|
|
};
|
|
const item = data.repository.issue.projectItems.nodes.find((n) => n.project.id === project.id);
|
|
itemIds.set(issue, item?.id);
|
|
if (!item) throw new Error(`issue #${issue} is not in the project`);
|
|
return item.id;
|
|
}
|
|
|
|
async function setField(issue: number, fieldId: string, value: object): Promise<void> {
|
|
const project = await resolve();
|
|
await gh.graphql(
|
|
`mutation($project: ID!, $item: ID!, $field: ID!, $value: ProjectV2FieldValue!) {
|
|
updateProjectV2ItemFieldValue(
|
|
input: { projectId: $project, itemId: $item, fieldId: $field, value: $value }
|
|
) { projectV2Item { id } }
|
|
}`,
|
|
{ project: project.id, item: await itemId(issue), field: fieldId, value },
|
|
);
|
|
}
|
|
|
|
async function attempt(what: string, op: () => Promise<void>): Promise<void> {
|
|
try {
|
|
await op();
|
|
} catch (err) {
|
|
const message = `mirror ${what}: ${err instanceof Error ? err.message : String(err)}`;
|
|
warnings.push(message);
|
|
console.warn(message); // observability picks this up; never rethrow
|
|
}
|
|
}
|
|
|
|
return {
|
|
warnings,
|
|
setTargetDate: (issue, date) =>
|
|
attempt(`Target Date #${issue}`, async () => {
|
|
const project = await resolve();
|
|
if (date === null) {
|
|
await gh.graphql(
|
|
`mutation($project: ID!, $item: ID!, $field: ID!) {
|
|
clearProjectV2ItemFieldValue(
|
|
input: { projectId: $project, itemId: $item, fieldId: $field }
|
|
) { projectV2Item { id } }
|
|
}`,
|
|
{ project: project.id, item: await itemId(issue), field: project.targetDate },
|
|
);
|
|
} else {
|
|
await setField(issue, project.targetDate, { date });
|
|
}
|
|
}),
|
|
};
|
|
}
|