124 Zeilen
3.8 KiB
TypeScript
124 Zeilen
3.8 KiB
TypeScript
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)
|
|
})
|
|
}
|