import path from 'path' import { db } from '../config/secureDatabase' import { logger } from '../utils/logger' type SkillHierarchy = Array<{ id: string name: string subcategories?: Array<{ id: string name: string skills?: Array<{ id: string; name: string }> }> }> function loadSharedHierarchy(): SkillHierarchy | null { try { const sharedPath = path.join(__dirname, '../../../shared/skills.js') // eslint-disable-next-line @typescript-eslint/no-var-requires const mod = require(sharedPath) if (mod && Array.isArray(mod.SKILL_HIERARCHY)) { return mod.SKILL_HIERARCHY as SkillHierarchy } } catch (err) { logger.warn('SkillSeeder: Could not load shared/skills.js. Skipping seeding.') } return null } export function ensureSkillsSeeded() { try { // Always ensure vocabulary tables exist (idempotent) db.exec(` CREATE TABLE IF NOT EXISTS controlled_vocabulary ( id TEXT PRIMARY KEY, category TEXT NOT NULL, value TEXT NOT NULL, description TEXT, is_active INTEGER DEFAULT 1, created_at TEXT NOT NULL, UNIQUE(category, value) ); CREATE INDEX IF NOT EXISTS idx_vocab_category ON controlled_vocabulary(category); CREATE INDEX IF NOT EXISTS idx_vocab_value ON controlled_vocabulary(value); `) db.exec(` CREATE TABLE IF NOT EXISTS skills ( id TEXT PRIMARY KEY, name TEXT NOT NULL, category TEXT NOT NULL, description TEXT, expires_after INTEGER ) `) const hierarchy = loadSharedHierarchy() if (!hierarchy) return const now = new Date().toISOString() const insertVocab = db.prepare(` INSERT OR IGNORE INTO controlled_vocabulary (id, category, value, description, is_active, created_at) VALUES (?, ?, ?, ?, ?, ?) `) const insertSkill = db.prepare(` INSERT OR IGNORE INTO skills (id, name, category, description, expires_after) VALUES (?, ?, ?, ?, ?) `) let cats = 0, subs = 0, skills = 0 for (const cat of hierarchy) { const catId = String(cat.id) const catName = String(cat.name || cat.id) // category insertVocab.run(cryptoRandomUUID(), 'skill_category', catId, catName, 1, now) cats++ for (const sub of (cat.subcategories || [])) { const subId = String(sub.id) const subName = String(sub.name || sub.id) const key = `${catId}.${subId}` insertVocab.run(cryptoRandomUUID(), 'skill_subcategory', key, subName, 1, now) subs++ for (const sk of (sub.skills || [])) { const sId = `${key}.${sk.id}` const sName = String(sk.name || sk.id) const expires = (catId === 'certifications') ? 36 : null insertSkill.run(sId, sName, key, null, expires) skills++ } } } // Explicitly remove deprecated skills that should no longer appear try { const deprecatedIds = [ 'certifications.security_clearance.nato', 'certifications.security_clearance.vs' ] const delEmp = db.prepare('DELETE FROM employee_skills WHERE skill_id = ?') const delSkill = db.prepare('DELETE FROM skills WHERE id = ?') for (const depId of deprecatedIds) { delEmp.run(depId) delSkill.run(depId) } } catch {} logger.info(`SkillSeeder: ensured ${cats} categories, ${subs} subcategories, ${skills} skills (idempotent).`) } catch (err) { logger.warn('SkillSeeder failed (non-fatal): ' + (err as Error).message) } } function cryptoRandomUUID() { // Node 18 has crypto.randomUUID, but keep a tiny fallback try { return (require('crypto').randomUUID() as string) } catch {} return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => { const r = Math.random() * 16 | 0, v = c === 'x' ? r : (r & 0x3 | 0x8) return v.toString(16) }) }