mirror of
https://github.com/prdlk/cv.git
synced 2026-08-02 17:31:41 +00:00
212 lines
7.3 KiB
TypeScript
212 lines
7.3 KiB
TypeScript
/**
|
|||
|
|
* 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.");
|