← back to Professional Directory
scripts/seed_specialties.js
85 lines
#!/usr/bin/env node
/**
* Seed the `specialties` table from the official NUCC Health Care Provider
* Taxonomy code set. Free public download — no key required.
*
* https://www.nucc.org/ → Code Sets → Provider Taxonomy
*
* The CSV ships with these columns (varies slightly across versions, but
* `Code` and `Display Name`/`Classification` are stable):
* Code, Grouping, Classification, Specialization, Definition, Notes,
* Display Name, Section
*/
const fs = require('node:fs');
const path = require('node:path');
const { parse } = require('csv-parse/sync');
const { fetch } = require('undici');
const { pool, query } = require('../agents/shared/db');
const NUCC_URL = process.env.NUCC_URL
|| 'https://www.nucc.org/images/stories/CSV/nucc_taxonomy_250.csv';
const CACHE = path.resolve(__dirname, '..', 'data', 'nucc_taxonomy.csv');
async function downloadIfNeeded() {
if (fs.existsSync(CACHE)) {
const stat = fs.statSync(CACHE);
const ageDays = (Date.now() - stat.mtimeMs) / 86400000;
if (ageDays < 30) {
console.log(`[nucc] using cache ${CACHE} (${ageDays.toFixed(1)}d old)`);
return fs.readFileSync(CACHE, 'utf8');
}
}
console.log(`[nucc] downloading ${NUCC_URL}`);
const res = await fetch(NUCC_URL, { signal: AbortSignal.timeout(30000) });
if (!res.ok) throw new Error(`NUCC download failed: HTTP ${res.status}`);
const body = await res.text();
fs.mkdirSync(path.dirname(CACHE), { recursive: true });
fs.writeFileSync(CACHE, body);
console.log(`[nucc] cached → ${CACHE} (${body.length} bytes)`);
return body;
}
function pickName(row) {
// Prefer "Display Name", then Classification + Specialization, else Code.
const display = (row['Display Name'] || '').trim();
if (display) return display;
const cls = (row['Classification'] || '').trim();
const spec = (row['Specialization'] || '').trim();
if (cls && spec) return `${cls} — ${spec}`;
return cls || (row['Code'] || '').trim();
}
async function main() {
const csv = await downloadIfNeeded();
const rows = parse(csv, { columns: true, skip_empty_lines: true, bom: true, relax_column_count: true });
console.log(`[nucc] parsed ${rows.length} taxonomy rows`);
let inserted = 0, updated = 0;
for (const row of rows) {
const code = (row['Code'] || '').trim();
if (!code) continue;
const name = pickName(row);
const parent = (row['Grouping'] || row['Classification'] || null) || null;
const r = await query(`
INSERT INTO specialties (name, taxonomy_code, parent_specialty)
VALUES ($1, $2, $3)
ON CONFLICT (taxonomy_code) DO UPDATE
SET name = EXCLUDED.name,
parent_specialty = COALESCE(EXCLUDED.parent_specialty, specialties.parent_specialty)
RETURNING (xmax = 0) AS is_insert
`, [name, code, parent]);
if (r.rows[0].is_insert) inserted++; else updated++;
}
console.log(`[nucc] done. inserted=${inserted} updated=${updated}`);
await pool.end();
}
main().catch(async (err) => {
console.error('[nucc] fatal:', err);
try { await pool.end(); } catch (_) {}
process.exit(1);
});