Files
cv/app/scripts/sync.ts
T

212 lines
7.3 KiB
TypeScript
Raw Normal View History

/**
* Synchronize the YAML corpus under ../docs into the local D1 database.
*
* Usage: bun scripts/sync.ts [--remote]
*
* Applies migrations, regenerates seed.generated.sql from the YAML files,
* and executes it against D1 (local by default).
*/
import { execSync } from "node:child_process";
import { readFileSync, readdirSync, writeFileSync } from "node:fs";
import { basename, join } from "node:path";
import yaml from "js-yaml";
const APP = join(import.meta.dirname, "..");
const DOCS = join(APP, "..", "docs");
const TARGET = process.argv.includes("--remote") ? "--remote" : "--local";
// ---------- helpers ----------
const sql = (v: string | number | null | undefined): string =>
v == null ? "NULL" : typeof v === "number" ? String(v) : `'${v.replace(/'/g, "''")}'`;
/** js-yaml parses ISO dates into Date objects; normalize everything to strings. */
function iso(v: unknown): string | null {
if (v == null) return null;
if (v instanceof Date) return v.toISOString().slice(0, 10);
return String(v);
}
/** Split a `description` block scalar into its "- " bullets (with wrapped-line continuation). */
function bulletsOf(desc: unknown): string[] {
if (typeof desc !== "string") return [];
const out: string[] = [];
for (const line of desc.split("\n")) {
const t = line.trim();
if (t.startsWith("- ")) out.push(t.slice(2).trim());
else if (t && out.length > 0) out[out.length - 1] += ` ${t}`;
}
return out;
}
function loadYaml(path: string): Record<string, unknown> {
return yaml.load(readFileSync(path, "utf8")) as Record<string, unknown>;
}
// ---------- build statements ----------
const stmts: string[] = [
"DELETE FROM profile;",
"DELETE FROM summaries;",
"DELETE FROM experience;",
"DELETE FROM projects;",
"DELETE FROM bullets;",
"DELETE FROM item_skills;",
"DELETE FROM skills;",
"DELETE FROM education;",
"DELETE FROM honors;",
"DELETE FROM organizations;",
];
// about/profile.yml
{
const p = loadYaml(join(DOCS, "about/profile.yml"));
stmts.push(
`INSERT INTO profile (id, full_name, email, phone, website, linkedin, github, twitter) VALUES (1, ${sql(
String(p["full-name"]),
)}, ${sql(iso(p.email))}, ${sql(iso(p.phone))}, ${sql(iso(p.website))}, ${sql(
iso(p.linkedin),
)}, ${sql(iso(p.github))}, ${sql(iso(p.twitter))});`,
);
for (const s of (p.summary as { role: string; summary: string }[] | undefined) ?? []) {
stmts.push(
`INSERT INTO summaries (role, summary) VALUES (${sql(s.role)}, ${sql(s.summary.trim())});`,
);
}
}
// about/education.yml
{
const e = loadYaml(join(DOCS, "about/education.yml"));
stmts.push(
`INSERT INTO education (id, school, degree, field, start_date, end_date, activities) VALUES (1, ${sql(
iso(e.school),
)}, ${sql(iso(e.degree))}, ${sql(iso(e.field))}, ${sql(iso(e.start))}, ${sql(
iso(e.end),
)}, ${sql(typeof e.activities === "string" ? e.activities.trim() : null)});`,
);
for (const skill of (e.skills as string[] | undefined) ?? []) {
stmts.push(
`INSERT INTO item_skills (parent_type, parent_slug, skill) VALUES ('education', 'education', ${sql(skill)});`,
);
}
}
// about/honors.yml + organizations.yml (top-level lists)
for (const h of yaml.load(readFileSync(join(DOCS, "about/honors.yml"), "utf8")) as Record<
string,
unknown
>[]) {
stmts.push(
`INSERT INTO honors (title, date, institution) VALUES (${sql(iso(h.title))}, ${sql(
iso(h.date),
)}, ${sql(iso(h.institution))});`,
);
}
for (const o of yaml.load(readFileSync(join(DOCS, "about/organizations.yml"), "utf8")) as Record<
string,
unknown
>[]) {
stmts.push(
`INSERT INTO organizations (name, url, title) VALUES (${sql(iso(o.name))}, ${sql(
iso(o.url),
)}, ${sql(iso(o.title))});`,
);
}
// skills/languages.yml (tiered map)
{
const tiers = loadYaml(join(DOCS, "skills/languages.yml")) as Record<string, string[]>;
let pos = 0;
for (const tier of ["expert", "strong", "familiar"]) {
for (const name of tiers[tier] ?? []) {
stmts.push(
`INSERT INTO skills (name, category, tier, position) VALUES (${sql(name)}, 'language', ${sql(tier)}, ${pos++});`,
);
}
}
}
// skills/frameworks.yml (flat list; group headers live in YAML comments)
{
let grp: string | null = null;
let pos = 0;
for (const line of readFileSync(join(DOCS, "skills/frameworks.yml"), "utf8").split("\n")) {
const t = line.trim();
if (t.startsWith("#")) grp = t.replace(/^#\s*/, "");
else if (t.startsWith("- "))
stmts.push(
`INSERT INTO skills (name, category, grp, position) VALUES (${sql(t.slice(2).trim())}, 'framework', ${sql(grp)}, ${pos++});`,
);
}
}
// skills/methodology.yml (flat list)
{
const items = yaml.load(readFileSync(join(DOCS, "skills/methodology.yml"), "utf8")) as string[];
items.forEach((name, pos) =>
stmts.push(
`INSERT INTO skills (name, category, position) VALUES (${sql(name)}, 'methodology', ${pos});`,
),
);
}
// experience/*.yml
for (const file of readdirSync(join(DOCS, "experience")).filter((f) => /\.ya?ml$/.test(f)).sort()) {
const slug = file.replace(/\.ya?ml$/, "");
const e = loadYaml(join(DOCS, "experience", file));
stmts.push(
`INSERT INTO experience (slug, title, company, start_date, end_date, location, profile_heading) VALUES (${sql(
slug,
)}, ${sql(iso(e.title))}, ${sql(iso(e.company))}, ${sql(iso(e.start))}, ${sql(
iso(e.end),
)}, ${sql(iso(e.location))}, ${sql(iso(e["profile-heading"]))});`,
);
bulletsOf(e.description).forEach((text, position) =>
stmts.push(
`INSERT INTO bullets (parent_type, parent_slug, position, text) VALUES ('experience', ${sql(slug)}, ${position}, ${sql(text)});`,
),
);
for (const skill of (e.skills as string[] | undefined) ?? []) {
stmts.push(
`INSERT INTO item_skills (parent_type, parent_slug, skill) VALUES ('experience', ${sql(slug)}, ${sql(skill)});`,
);
}
}
// projects/*.yml
for (const file of readdirSync(join(DOCS, "projects")).filter((f) => /\.ya?ml$/.test(f)).sort()) {
const slug = file.replace(/\.ya?ml$/, "");
const p = loadYaml(join(DOCS, "projects", file));
stmts.push(
`INSERT INTO projects (slug, title, start_date, end_date, url, associated_with) VALUES (${sql(
slug,
)}, ${sql(iso(p.title))}, ${sql(iso(p.start))}, ${sql(iso(p.end))}, ${sql(
iso(p["project-url"]),
)}, ${sql(typeof p["associated-with"] === "string" ? basename(p["associated-with"]).replace(/\.ya?ml$/, "") : null)});`,
);
bulletsOf(p.description).forEach((text, position) =>
stmts.push(
`INSERT INTO bullets (parent_type, parent_slug, position, text) VALUES ('project', ${sql(slug)}, ${position}, ${sql(text)});`,
),
);
for (const skill of (p.skills as string[] | undefined) ?? []) {
stmts.push(
`INSERT INTO item_skills (parent_type, parent_slug, skill) VALUES ('project', ${sql(slug)}, ${sql(skill)});`,
);
}
}
// ---------- execute ----------
const seedPath = join(APP, "seed.generated.sql");
writeFileSync(seedPath, `${stmts.join("\n")}\n`);
console.log(`Wrote ${stmts.length} statements to seed.generated.sql`);
execSync(`bunx wrangler d1 migrations apply cv-tailor ${TARGET}`, { cwd: APP, stdio: "inherit" });
execSync(`bunx wrangler d1 execute cv-tailor ${TARGET} --file seed.generated.sql`, {
cwd: APP,
stdio: "inherit",
});
console.log("Sync complete.");