Files
SkillMate/backend/src/services/skillSeeder.ts
Claude Project Manager 01d0988515 SUE 1-3 ist drin
2025-09-29 22:32:20 +02:00

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)
})
}