/** Row shapes and corpus loader for the cv-tailor D1 database. */ export interface ProfileRow { full_name: string; email: string | null; phone: string | null; website: string | null; linkedin: string | null; github: string | null; twitter: string | null; } export interface SummaryRow { role: string; summary: string; } export interface ExperienceItem { slug: string; title: string; company: string; start_date: string; end_date: string | null; location: string | null; profile_heading: string | null; bullets: string[]; skills: string[]; } export interface ProjectItem { slug: string; title: string; start_date: string | null; end_date: string | null; url: string | null; associated_with: string | null; bullets: string[]; skills: string[]; } export interface SkillRow { name: string; category: "language" | "framework" | "methodology"; grp: string | null; tier: string | null; position: number; } export interface EducationRow { school: string; degree: string | null; field: string | null; start_date: string | null; end_date: string | null; activities: string | null; } export interface HonorRow { title: string; date: string | null; institution: string | null; } export interface OrganizationRow { name: string; url: string | null; title: string | null; } export interface Corpus { profile: ProfileRow; summaries: SummaryRow[]; experience: ExperienceItem[]; projects: ProjectItem[]; skills: SkillRow[]; education: EducationRow | null; honors: HonorRow[]; organizations: OrganizationRow[]; } interface BulletRow { parent_type: string; parent_slug: string; position: number; text: string; } interface ItemSkillRow { parent_type: string; parent_slug: string; skill: string; } type ExperienceDbRow = Omit; type ProjectDbRow = Omit; const MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; /** * "2021-03-01".."" -> "Mar 2021 – Present" (month style) or "2021 – Present" (year style). * Uses an en dash; the LaTeX escaper converts it to `--`. */ export function formatRange( start: string | null, end: string | null, style: "month" | "year", ): string { if (!start) return ""; let from: string; let to = "Present"; if (style === "month") { from = `${MONTHS[Number(start.slice(5, 7)) - 1]} ${start.slice(0, 4)}`; if (end) to = `${MONTHS[Number(end.slice(5, 7)) - 1]} ${end.slice(0, 4)}`; } else { from = start.slice(0, 4); if (end) to = end.slice(0, 4); } return `${from} – ${to}`; } export async function loadCorpus(db: D1Database): Promise { const [profile, summaries, experience, projects, bullets, itemSkills, skills, education, honors, organizations] = await Promise.all([ db.prepare("SELECT * FROM profile LIMIT 1").all(), db.prepare("SELECT role, summary FROM summaries").all(), db.prepare("SELECT * FROM experience ORDER BY start_date DESC").all(), db.prepare("SELECT * FROM projects ORDER BY start_date DESC").all(), db .prepare("SELECT parent_type, parent_slug, position, text FROM bullets ORDER BY position") .all(), db.prepare("SELECT parent_type, parent_slug, skill FROM item_skills").all(), db .prepare("SELECT name, category, grp, tier, position FROM skills ORDER BY position") .all(), db.prepare("SELECT * FROM education LIMIT 1").all(), db.prepare("SELECT title, date, institution FROM honors").all(), db.prepare("SELECT name, url, title FROM organizations").all(), ]); const profileRow = profile.results[0]; if (!profileRow) throw new Error("D1 is empty — run `bun run sync` first."); const experienceItems: ExperienceItem[] = experience.results.map((r) => ({ ...r, bullets: [], skills: [] })); const projectItems: ProjectItem[] = projects.results.map((r) => ({ ...r, bullets: [], skills: [] })); const bySlug = new Map(); for (const item of experienceItems) bySlug.set(`experience:${item.slug}`, item); for (const item of projectItems) bySlug.set(`project:${item.slug}`, item); for (const b of bullets.results) bySlug.get(`${b.parent_type}:${b.parent_slug}`)?.bullets.push(b.text); for (const s of itemSkills.results) bySlug.get(`${s.parent_type}:${s.parent_slug}`)?.skills.push(s.skill); return { profile: profileRow, summaries: summaries.results, experience: experienceItems, projects: projectItems, skills: skills.results, education: education.results[0] ?? null, honors: honors.results, organizations: organizations.results, }; }